Scaffold Homelab Manager foundation
Monorepo (Express+TS+Drizzle/libSQL server, React+Vite+Tabler web) matching the stack used by ScheduleTaskManager and Sloth Manager. Includes Authentik OIDC login with local admin/operator/viewer roles (first user becomes admin), a generalized audit log, encrypted-at-rest storage for future integration API tokens, the DB schema for all planned modules, and the Tabler-styled app shell/nav. Also ports the Secrets (expiry tracker) and IP Addresses (IPAM) modules from Sloth Manager onto the new stack. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
commit
6bd2ed52c1
52 files changed
+8103
No files matched your search
@@ -0,0 +1,134 @@
|
||||
export type UserRole = "admin" | "operator" | "viewer";
|
||||
|
||||
export interface CurrentUser {
|
||||
id: number;
|
||||
sub: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
role: UserRole;
|
||||
}
|
||||
|
||||
export interface UserRecord {
|
||||
id: number;
|
||||
oidcSub: string;
|
||||
email: string | null;
|
||||
name: string | null;
|
||||
role: UserRole;
|
||||
createdAt: string;
|
||||
lastLoginAt: string | null;
|
||||
}
|
||||
|
||||
export interface AuditLogEntry {
|
||||
id: number;
|
||||
actorUserId: number | null;
|
||||
actorLabel: string | null;
|
||||
category: string;
|
||||
action: string;
|
||||
targetType: string | null;
|
||||
targetId: string | null;
|
||||
detail: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export type SecretType = "api_token" | "ssl_certificate" | "password" | "generic";
|
||||
export type SecretStatus = "ok" | "expiring" | "expired";
|
||||
|
||||
export interface SecretRecord {
|
||||
id: number;
|
||||
name: string;
|
||||
type: SecretType;
|
||||
description: string | null;
|
||||
expiryDate: string;
|
||||
warnDays: number;
|
||||
notes: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
status: SecretStatus;
|
||||
daysLeft: number;
|
||||
}
|
||||
|
||||
export interface SecretInput {
|
||||
name: string;
|
||||
type: SecretType;
|
||||
description?: string;
|
||||
expiryDate: string;
|
||||
warnDays: number;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface IpamEntry {
|
||||
id: number;
|
||||
ipAddress: string;
|
||||
label: string | null;
|
||||
vendor: string | null;
|
||||
location: string | null;
|
||||
notes: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
matchingDnsRecords: string[];
|
||||
}
|
||||
|
||||
export interface IpamInput {
|
||||
ipAddress: string;
|
||||
label?: string;
|
||||
vendor?: string;
|
||||
location?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class UnauthorizedError extends Error {}
|
||||
export class ForbiddenError extends Error {}
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(path, {
|
||||
...init,
|
||||
headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) },
|
||||
credentials: "same-origin",
|
||||
});
|
||||
if (res.status === 401) {
|
||||
throw new UnauthorizedError("unauthorized");
|
||||
}
|
||||
if (res.status === 403) {
|
||||
throw new ForbiddenError("forbidden");
|
||||
}
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => "");
|
||||
throw new Error(`Request failed (${res.status}): ${body}`);
|
||||
}
|
||||
if (res.status === 204) return undefined as T;
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
me: () => request<{ user: CurrentUser }>("/api/me"),
|
||||
users: {
|
||||
list: () => request<{ users: UserRecord[] }>("/api/users"),
|
||||
updateRole: (id: number, role: UserRole) =>
|
||||
request<{ user: UserRecord }>(`/api/users/${id}/role`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ role }),
|
||||
}),
|
||||
},
|
||||
auditLog: {
|
||||
list: (limit = 200) => request<{ entries: AuditLogEntry[] }>(`/api/audit-log?limit=${limit}`),
|
||||
},
|
||||
secrets: {
|
||||
list: () => request<{ secrets: SecretRecord[] }>("/api/secrets"),
|
||||
create: (data: SecretInput) =>
|
||||
request<{ secret: SecretRecord }>("/api/secrets", { method: "POST", body: JSON.stringify(data) }),
|
||||
update: (id: number, data: Partial<SecretInput>) =>
|
||||
request<{ secret: SecretRecord }>(`/api/secrets/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
remove: (id: number) => request<void>(`/api/secrets/${id}`, { method: "DELETE" }),
|
||||
},
|
||||
ipam: {
|
||||
list: () => request<{ entries: IpamEntry[] }>("/api/ipam"),
|
||||
create: (data: IpamInput) =>
|
||||
request<{ entry: IpamEntry }>("/api/ipam", { method: "POST", body: JSON.stringify(data) }),
|
||||
update: (id: number, data: Partial<Omit<IpamInput, "ipAddress">>) =>
|
||||
request<{ entry: IpamEntry }>(`/api/ipam/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
|
||||
remove: (id: number) => request<void>(`/api/ipam/${id}`, { method: "DELETE" }),
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user