Add nasctl: Go NAS control plane with React frontend

This commit is contained in:
2026-07-05 17:37:19 -04:00
parent 359fd5a160
commit 4f0754ecc5
56 changed files with 6725 additions and 1 deletions
+121
View File
@@ -0,0 +1,121 @@
export interface SambaShare {
id: number;
name: string;
path: string;
comment: string;
read_only: boolean;
guest_ok: boolean;
valid_users: string[];
valid_groups: string[];
}
export interface NFSExport {
id: number;
path: string;
clients: string[];
options: string;
}
export interface User {
id: number;
username: string;
groups: string[];
smb_enabled: boolean;
disabled: boolean;
}
export interface DirtyModule {
module: string;
marked_at: string;
}
export interface ApplyLogEntry {
id: number;
module: string;
message: string;
success: boolean;
created_at: string;
}
export interface DiskUsage {
path: string;
total_bytes: number;
free_bytes: number;
used_bytes: number;
used_percent: number;
}
export interface ServiceStatus {
name: string;
active: boolean;
state: string;
}
export interface SystemStatus {
disks: DiskUsage[];
services: ServiceStatus[];
}
export class ApiError extends Error {
status: number;
constructor(status: number, message: string) {
super(message);
this.status = status;
}
}
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
const res = await fetch(`/api${path}`, {
method,
headers: body ? { "Content-Type": "application/json" } : undefined,
body: body ? JSON.stringify(body) : undefined,
});
if (res.status === 204) {
return undefined as T;
}
const text = await res.text();
const data = text ? JSON.parse(text) : {};
if (!res.ok) {
throw new ApiError(res.status, data.error || res.statusText);
}
return data as T;
}
export const api = {
// auth
status: () => request<{ authenticated: boolean; username: string }>("GET", "/auth/status"),
login: (username: string, password: string) =>
request<{ username: string }>("POST", "/auth/login", { username, password }),
logout: () => request<{ ok: boolean }>("POST", "/auth/logout"),
// dirty / apply
dirty: () => request<{ modules: DirtyModule[] | null }>("GET", "/dirty"),
apply: () => request<{ results: { module: string; applied: boolean; error?: string }[] }>("POST", "/apply"),
applyLog: () => request<{ entries: ApplyLogEntry[] }>("GET", "/apply/log"),
systemStatus: () => request<SystemStatus>("GET", "/system/status"),
// samba
listShares: () => request<{ shares: SambaShare[] | null }>("GET", "/samba/shares/"),
createShare: (s: Partial<SambaShare>) => request<SambaShare>("POST", "/samba/shares/", s),
updateShare: (id: number, s: Partial<SambaShare>) => request<SambaShare>("PUT", `/samba/shares/${id}/`, s),
deleteShare: (id: number) => request<void>("DELETE", `/samba/shares/${id}/`),
// nfs
listExports: () => request<{ exports: NFSExport[] | null }>("GET", "/nfs/exports/"),
createExport: (e: Partial<NFSExport>) => request<NFSExport>("POST", "/nfs/exports/", e),
updateExport: (id: number, e: Partial<NFSExport>) => request<NFSExport>("PUT", `/nfs/exports/${id}/`, e),
deleteExport: (id: number) => request<void>("DELETE", `/nfs/exports/${id}/`),
// users
listUsers: () => request<{ users: User[] | null }>("GET", "/users/"),
createUser: (u: Partial<User> & { password?: string }) => request<User>("POST", "/users/", u),
updateUser: (id: number, u: Partial<User> & { password?: string }) => request<User>("PUT", `/users/${id}/`, u),
deleteUser: (id: number) => request<void>("DELETE", `/users/${id}/`),
};
export function formatBytes(bytes: number): string {
if (bytes === 0) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB", "PB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
}