Files
homelab-monitor/apps/api/src/config/index.ts
T
jhodgkin a431b87f1f
CI / web (push) Successful in 19s
CI / api (push) Successful in 28s
Add Authentik OIDC login as an additional sign-in option
Local auth stays the primary/always-available login (don't want to
lock out the saved admin password) — OIDC is additive, shown as a
second button when OIDC_ENABLED=true. Uses openid-client v6 with PKCE.

Authentik-side provider was set up via an authentik blueprint (its own
declarative automation, see docs/oidc-setup.md) rather than touching
any existing admin credentials.

Closes #12.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 20:59:25 -06:00

119 lines
3.5 KiB
TypeScript

import { readFileSync } from "node:fs";
import { parse } from "yaml";
import type { SshHostConfig } from "../collectors/sshHost.js";
interface RawSshHost {
id: string;
displayName: string;
group: string;
host: string;
port?: number;
username: string;
diskPaths?: { path: string; label: string }[];
}
interface RawKnownDevice {
ip: string;
name: string;
}
export interface HostsConfig {
proxmox: {
host: string;
node: string;
};
sshHosts?: RawSshHost[];
knownDevices?: RawKnownDevice[];
}
export interface AppConfig {
port: number;
dbPath: string;
pollIntervalSeconds: number;
snapshotRetentionHours: number;
sessionSecret: string;
// Only set once a TLS-terminating reverse proxy sits in front (see issue #13) —
// browsers silently drop `secure` cookies over plain HTTP.
cookieSecure: boolean;
adminUsername: string;
adminPassword: string | undefined;
proxmox: {
host: string;
node: string;
tokenId: string;
tokenSecret: string;
};
sshHosts: SshHostConfig[];
knownDevices: Map<string, string>;
discoveryFilePath: string;
oidc: OidcConfig | undefined;
hosts: HostsConfig;
}
export interface OidcConfig {
issuerUrl: string;
clientId: string;
clientSecret: string;
redirectUri: string;
// Authentik's cert is self-signed on the LAN, same as Proxmox's.
allowInsecureTls: boolean;
}
function required(name: string): string {
const v = process.env[name];
if (!v) throw new Error(`Missing required env var: ${name}`);
return v;
}
export function loadConfig(hostsConfigPath: string): AppConfig {
const hosts = parse(readFileSync(hostsConfigPath, "utf8")) as HostsConfig;
const sshPrivateKeyPath = process.env.SSH_PRIVATE_KEY_PATH ?? "./ssh/monitor_ed25519";
// Local auth (bcrypt + session) is always available. OIDC is an *additional*
// sign-in option when configured, not a replacement — see issue #12 comments
// for why (don't want to lock out the already-saved local admin password).
const oidc: OidcConfig | undefined =
process.env.OIDC_ENABLED === "true"
? {
issuerUrl: required("OIDC_ISSUER_URL"),
clientId: required("OIDC_CLIENT_ID"),
clientSecret: required("OIDC_CLIENT_SECRET"),
redirectUri: required("OIDC_REDIRECT_URI"),
allowInsecureTls: process.env.OIDC_ALLOW_INSECURE_TLS === "true",
}
: undefined;
const sshHosts: SshHostConfig[] = (hosts.sshHosts ?? []).map((h) => ({
id: h.id,
displayName: h.displayName,
group: h.group,
host: h.host,
port: h.port ?? 22,
username: h.username,
privateKeyPath: sshPrivateKeyPath,
diskPaths: h.diskPaths ?? [{ path: "/", label: "root" }],
}));
return {
port: Number(process.env.PORT ?? 3000),
dbPath: process.env.DB_PATH ?? "./data/monitor.db",
pollIntervalSeconds: Number(process.env.POLL_INTERVAL_SECONDS ?? 30),
snapshotRetentionHours: Number(process.env.SNAPSHOT_RETENTION_HOURS ?? 24),
sessionSecret: required("SESSION_SECRET"),
cookieSecure: process.env.COOKIE_SECURE === "true",
adminUsername: process.env.ADMIN_USERNAME ?? "admin",
adminPassword: process.env.ADMIN_PASSWORD,
proxmox: {
host: hosts.proxmox.host,
node: hosts.proxmox.node,
tokenId: required("PROXMOX_TOKEN_ID"),
tokenSecret: required("PROXMOX_TOKEN_SECRET"),
},
sshHosts,
knownDevices: new Map((hosts.knownDevices ?? []).map((d) => [d.ip, d.name])),
discoveryFilePath: process.env.DISCOVERY_FILE_PATH ?? "./data/devices-raw.json",
oidc,
hosts,
};
}