100 lines
3.3 KiB
TypeScript
100 lines
3.3 KiB
TypeScript
import { readJson, writeJson } from './storage';
|
|
import { randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
|
|
import type { Cookies } from '@sveltejs/kit';
|
|
|
|
const ADMIN_FILE = 'admin.json';
|
|
const SESSION_FILE = 'sessions.json';
|
|
const SESSION_COOKIE = 'gds_admin';
|
|
const SESSION_TTL_MS = 1000 * 60 * 60 * 24 * 7; // 7 days
|
|
|
|
type AdminRecord = {
|
|
username: string;
|
|
salt: string;
|
|
hash: string;
|
|
};
|
|
|
|
type SessionRecord = {
|
|
token: string;
|
|
username: string;
|
|
expiresAt: number;
|
|
};
|
|
|
|
function hashPassword(password: string, salt: string): string {
|
|
return scryptSync(password, salt, 64).toString('hex');
|
|
}
|
|
|
|
export function seedDefaultAdminIfMissing(): { username: string; tempPassword?: string } {
|
|
const existing = readJson<AdminRecord | null>(ADMIN_FILE, null);
|
|
if (existing) return { username: existing.username };
|
|
const username = process.env.ADMIN_USERNAME || 'admin';
|
|
const password = process.env.ADMIN_PASSWORD || 'changeme';
|
|
const salt = randomBytes(16).toString('hex');
|
|
const record: AdminRecord = { username, salt, hash: hashPassword(password, salt) };
|
|
writeJson(ADMIN_FILE, record);
|
|
return { username, tempPassword: password };
|
|
}
|
|
|
|
export function verifyCredentials(username: string, password: string): boolean {
|
|
const record = readJson<AdminRecord | null>(ADMIN_FILE, null);
|
|
if (!record) return false;
|
|
if (record.username !== username) return false;
|
|
const candidate = Buffer.from(hashPassword(password, record.salt), 'hex');
|
|
const stored = Buffer.from(record.hash, 'hex');
|
|
if (candidate.length !== stored.length) return false;
|
|
return timingSafeEqual(candidate, stored);
|
|
}
|
|
|
|
export function changePassword(currentPassword: string, newPassword: string): boolean {
|
|
const record = readJson<AdminRecord | null>(ADMIN_FILE, null);
|
|
if (!record) return false;
|
|
if (!verifyCredentials(record.username, currentPassword)) return false;
|
|
const salt = randomBytes(16).toString('hex');
|
|
writeJson(ADMIN_FILE, {
|
|
username: record.username,
|
|
salt,
|
|
hash: hashPassword(newPassword, salt)
|
|
});
|
|
return true;
|
|
}
|
|
|
|
function loadSessions(): SessionRecord[] {
|
|
const now = Date.now();
|
|
const all = readJson<SessionRecord[]>(SESSION_FILE, []);
|
|
const live = all.filter((s) => s.expiresAt > now);
|
|
if (live.length !== all.length) writeJson(SESSION_FILE, live);
|
|
return live;
|
|
}
|
|
|
|
export function createSession(username: string, cookies: Cookies): string {
|
|
const sessions = loadSessions();
|
|
const token = randomBytes(32).toString('hex');
|
|
const record: SessionRecord = { token, username, expiresAt: Date.now() + SESSION_TTL_MS };
|
|
sessions.push(record);
|
|
writeJson(SESSION_FILE, sessions);
|
|
cookies.set(SESSION_COOKIE, token, {
|
|
path: '/',
|
|
httpOnly: true,
|
|
sameSite: 'lax',
|
|
secure: process.env.NODE_ENV === 'production',
|
|
maxAge: SESSION_TTL_MS / 1000
|
|
});
|
|
return token;
|
|
}
|
|
|
|
export function destroySession(cookies: Cookies): void {
|
|
const token = cookies.get(SESSION_COOKIE);
|
|
if (token) {
|
|
const sessions = loadSessions().filter((s) => s.token !== token);
|
|
writeJson(SESSION_FILE, sessions);
|
|
}
|
|
cookies.delete(SESSION_COOKIE, { path: '/' });
|
|
}
|
|
|
|
export function getSessionUser(cookies: Cookies): { username: string } | null {
|
|
const token = cookies.get(SESSION_COOKIE);
|
|
if (!token) return null;
|
|
const session = loadSessions().find((s) => s.token === token);
|
|
if (!session) return null;
|
|
return { username: session.username };
|
|
}
|