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,202 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
||||
|
||||
// ─── Users & roles ──────────────────────────────────────────────────────────
|
||||
|
||||
export const userRoles = ["admin", "operator", "viewer"] as const;
|
||||
export type UserRole = (typeof userRoles)[number];
|
||||
|
||||
export const users = sqliteTable("users", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
oidcSub: text("oidc_sub").notNull().unique(),
|
||||
email: text("email"),
|
||||
name: text("name"),
|
||||
role: text("role").$type<UserRole>().notNull().default("viewer"),
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
lastLoginAt: text("last_login_at"),
|
||||
});
|
||||
|
||||
// ─── Audit log ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const auditLog = sqliteTable("audit_log", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
actorUserId: integer("actor_user_id").references(() => users.id, { onDelete: "set null" }),
|
||||
actorLabel: text("actor_label"), // denormalized name/email snapshot, survives user deletion
|
||||
category: text("category").notNull(), // 'secret' | 'ipam' | 'dns' | 'user' | 'integration' | 'task' | ...
|
||||
action: text("action").notNull(), // 'create' | 'update' | 'delete' | 'start' | 'stop' | ...
|
||||
targetType: text("target_type"),
|
||||
targetId: text("target_id"),
|
||||
detail: text("detail"), // JSON-encoded free-form context
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
});
|
||||
|
||||
// ─── Settings (key/value) ───────────────────────────────────────────────────
|
||||
|
||||
export const settings = sqliteTable("settings", {
|
||||
key: text("key").primaryKey(),
|
||||
value: text("value").notNull(),
|
||||
updatedAt: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
});
|
||||
|
||||
// ─── Secrets expiry tracker (ported from Sloth Manager) ────────────────────
|
||||
|
||||
export const secretTypes = ["api_token", "ssl_certificate", "password", "generic"] as const;
|
||||
export type SecretType = (typeof secretTypes)[number];
|
||||
|
||||
export const secrets = sqliteTable("secrets", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
name: text("name").notNull(),
|
||||
type: text("type").$type<SecretType>().notNull().default("generic"),
|
||||
description: text("description"),
|
||||
expiryDate: text("expiry_date").notNull(), // ISO date, e.g. 2026-03-01
|
||||
warnDays: integer("warn_days").notNull().default(30),
|
||||
notes: text("notes"),
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
updatedAt: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
});
|
||||
|
||||
// ─── IPAM (ported from Sloth Manager) ───────────────────────────────────────
|
||||
|
||||
export const ipamEntries = sqliteTable("ipam_entries", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
ipAddress: text("ip_address").notNull().unique(),
|
||||
label: text("label"),
|
||||
vendor: text("vendor"),
|
||||
location: text("location"),
|
||||
notes: text("notes"),
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
updatedAt: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
});
|
||||
|
||||
// ─── Integration credentials (encrypted API tokens) ─────────────────────────
|
||||
|
||||
export const integrationCredentials = sqliteTable("integration_credentials", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
name: text("name").notNull(),
|
||||
encryptedSecret: text("encrypted_secret").notNull(), // AES-256-GCM, see src/crypto.ts
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
});
|
||||
|
||||
// ─── DNS providers + record cache (ported from Sloth Manager) ──────────────
|
||||
|
||||
export const dnsProviderTypes = [
|
||||
"cloudflare",
|
||||
"loopia",
|
||||
"pihole",
|
||||
"azure",
|
||||
"cpanel",
|
||||
"technitium",
|
||||
] as const;
|
||||
export type DnsProviderType = (typeof dnsProviderTypes)[number];
|
||||
|
||||
export const dnsProviders = sqliteTable("dns_providers", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
providerType: text("provider_type").$type<DnsProviderType>().notNull(),
|
||||
name: text("name").notNull(),
|
||||
credentialId: integer("credential_id").references(() => integrationCredentials.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
config: text("config"), // JSON: non-secret provider config (base URL, zone list, etc.)
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
});
|
||||
|
||||
export const dnsRecordsCache = sqliteTable("dns_records_cache", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
providerId: integer("provider_id")
|
||||
.notNull()
|
||||
.references(() => dnsProviders.id, { onDelete: "cascade" }),
|
||||
zone: text("zone").notNull(),
|
||||
recordType: text("record_type").notNull(), // A, AAAA, CNAME, TXT, MX, ...
|
||||
name: text("name").notNull(),
|
||||
value: text("value").notNull(),
|
||||
ttl: integer("ttl"),
|
||||
raw: text("raw"), // JSON: original provider payload for this record
|
||||
syncedAt: text("synced_at")
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
});
|
||||
|
||||
// ─── Servers & scheduled tasks (ported from Schedule Task Manager) ─────────
|
||||
|
||||
export const servers = sqliteTable("servers", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
name: text("name").notNull(),
|
||||
hostname: text("hostname"),
|
||||
osType: text("os_type").notNull().default("linux"),
|
||||
description: text("description"),
|
||||
apiTokenHash: text("api_token_hash").notNull(),
|
||||
apiTokenPrefix: text("api_token_prefix").notNull(),
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
lastSeenAt: text("last_seen_at"),
|
||||
});
|
||||
|
||||
export const scheduledTasks = sqliteTable("scheduled_tasks", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
serverId: integer("server_id")
|
||||
.notNull()
|
||||
.references(() => servers.id, { onDelete: "cascade" }),
|
||||
scheduleType: text("schedule_type").notNull(), // 'cron' | 'systemd_timer' | 'docker' | 'backup' | 'update' | 'n8n_workflow' | 'manual'
|
||||
origin: text("origin").notNull().default("agent"), // 'agent' | 'manual' — manual rows are never touched by agent sync
|
||||
name: text("name").notNull(),
|
||||
command: text("command"),
|
||||
scheduleExpression: text("schedule_expression"),
|
||||
source: text("source"),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
nextRunAt: text("next_run_at"),
|
||||
rawMetadata: text("raw_metadata"),
|
||||
isStale: integer("is_stale", { mode: "boolean" }).notNull().default(false),
|
||||
firstSeenAt: text("first_seen_at")
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
lastSeenAt: text("last_seen_at")
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
});
|
||||
|
||||
// ─── Live integrations (Proxmox, Synology, Semaphore, Tailscale, Gitea, Dockhand) ─
|
||||
|
||||
export const integrationTypes = [
|
||||
"proxmox",
|
||||
"synology",
|
||||
"semaphore",
|
||||
"tailscale",
|
||||
"gitea",
|
||||
"dockhand",
|
||||
] as const;
|
||||
export type IntegrationType = (typeof integrationTypes)[number];
|
||||
|
||||
export const integrations = sqliteTable("integrations", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
type: text("type").$type<IntegrationType>().notNull(),
|
||||
name: text("name").notNull(),
|
||||
baseUrl: text("base_url").notNull(),
|
||||
credentialId: integer("credential_id").references(() => integrationCredentials.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
config: text("config"), // JSON: per-type non-secret config (tailnet name, node name, etc.)
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
});
|
||||
Reference in new issue
Block a user