import { randomUUID } from 'crypto'; import { db } from './db/index.js'; import { setLocalCredentialPassword } from './local-credentials.js'; export interface AppUser { id: string; username: string; full_name: string; email: string | null; auth_source: string; created_at: string; updated_at: string; } const DEFAULT_AUTH_SOURCE = 'OIDC - Synology'; const LOCAL_AUTH_SOURCE = 'Local'; const normalizeOptional = (value?: string | null): string | undefined => { const trimmed = value?.trim(); return trimmed ? trimmed : undefined; }; const normalizeEmail = (value?: string | null): string | null | undefined => { if (value === undefined) return undefined; if (value === null) return null; const trimmed = value.trim(); return trimmed ? trimmed : null; }; export function listUsers(): (AppUser & { has_local_credentials: boolean })[] { return db .all( `SELECT u.id, u.username, u.full_name, u.email, u.auth_source, u.created_at, u.updated_at, CASE WHEN lc.user_id IS NOT NULL THEN 1 ELSE 0 END AS has_local_credentials FROM users u LEFT JOIN local_credentials lc ON lc.user_id = u.id ORDER BY u.username COLLATE NOCASE` ) .map((row) => ({ ...row, has_local_credentials: row.has_local_credentials === 1 })); } export function upsertUserFromAuth(input: { id: string; username?: string | null; fullName?: string | null; email?: string | null; authSource?: string | null; }): AppUser { const existing = db.get('SELECT * FROM users WHERE id = ?', [input.id]); const username = normalizeOptional(input.username) ?? existing?.username ?? input.id; const fullName = normalizeOptional(input.fullName) ?? existing?.full_name ?? ''; const email = normalizeOptional(input.email) ?? existing?.email ?? null; const authSource = normalizeOptional(input.authSource) ?? existing?.auth_source ?? DEFAULT_AUTH_SOURCE; if (existing) { const setClauses: string[] = []; const values: Array = []; if (username !== existing.username) { setClauses.push('username = ?'); values.push(username); } if (fullName !== existing.full_name) { setClauses.push('full_name = ?'); values.push(fullName); } if (email !== existing.email) { setClauses.push('email = ?'); values.push(email); } if (authSource !== existing.auth_source) { setClauses.push('auth_source = ?'); values.push(authSource); } if (setClauses.length > 0) { setClauses.push("updated_at = datetime('now')"); values.push(input.id); db.run(`UPDATE users SET ${setClauses.join(', ')} WHERE id = ?`, values); } } else { db.run( `INSERT INTO users (id, username, full_name, email, auth_source) VALUES (?, ?, ?, ?, ?)`, [input.id, username, fullName, email, authSource] ); } return db.get('SELECT * FROM users WHERE id = ?', [input.id])!; } export function upsertUserProfile( userId: string, fullName: string, email?: string | null ): AppUser { return upsertUserFromAuth({ id: userId, fullName, email, authSource: DEFAULT_AUTH_SOURCE }); } export function updateUser(input: { id: string; username: string; fullName: string; email?: string | null; }): void { const existing = db.get<{ email: string | null }>('SELECT email FROM users WHERE id = ?', [ input.id ]); const email = normalizeEmail(input.email); const nextEmail = email === undefined ? existing?.email ?? null : email; db.run( `UPDATE users SET username = ?, full_name = ?, email = ?, updated_at = datetime('now') WHERE id = ?`, [input.username.trim(), input.fullName.trim(), nextEmail, input.id] ); syncSelfProfile(input.id, input.fullName, nextEmail); } export function deleteUser(id: string): void { db.run('DELETE FROM users WHERE id = ?', [id]); } export async function createLocalUser(input: { username: string; fullName: string; email?: string | null; password: string; }): Promise { const username = input.username?.trim() ?? ''; const fullName = input.fullName?.trim() ?? ''; if (!username || !fullName) { throw new Error('Username and full name are required'); } const email = normalizeEmail(input.email); const existingUsername = db.get<{ id: string }>( 'SELECT id FROM users WHERE lower(username) = lower(?)', [username] ); if (existingUsername) { throw new Error('Username already exists'); } if (email) { const existingEmail = db.get<{ id: string }>( 'SELECT id FROM users WHERE lower(email) = lower(?)', [email] ); if (existingEmail) { throw new Error('Email already exists'); } } const id = randomUUID(); db.run( `INSERT INTO users (id, username, full_name, email, auth_source) VALUES (?, ?, ?, ?, ?)`, [id, username, fullName, email ?? null, LOCAL_AUTH_SOURCE] ); await setLocalCredentialPassword(id, input.password); return db.get('SELECT * FROM users WHERE id = ?', [id])!; } export async function setLocalPasswordForUser(userId: string, password: string): Promise { const id = userId?.trim() ?? ''; if (!id) { throw new Error('User id required'); } const existing = db.get<{ id: string }>('SELECT id FROM users WHERE id = ?', [id]); if (!existing) { throw new Error('User not found'); } await setLocalCredentialPassword(id, password); } function syncSelfProfile(userId: string, fullName: string, email?: string | null): void { const profile = db.get<{ id: string; first_name: string; last_name: string; email: string | null }>( `SELECT id, first_name, last_name, email FROM people WHERE user_id = ? AND is_self = 1 LIMIT 1`, [userId] ); if (!profile) return; const trimmed = fullName.trim(); let firstName = profile.first_name; let lastName = profile.last_name; if (trimmed) { const parts = trimmed.split(/\s+/); firstName = parts[0] ?? profile.first_name; if (parts.length > 1) { lastName = parts.slice(1).join(' '); } } const nextEmail = email === undefined ? profile.email : email; db.run( `UPDATE people SET first_name = ?, last_name = ?, email = ?, updated_at = datetime('now') WHERE id = ?`, [firstName, lastName, nextEmail, profile.id] ); }