added user manager page
This commit is contained in:
+87
-13
@@ -11,6 +11,7 @@ type AdminRecord = {
|
||||
username: string;
|
||||
salt: string;
|
||||
hash: string;
|
||||
createdAt?: number;
|
||||
};
|
||||
|
||||
type SessionRecord = {
|
||||
@@ -23,40 +24,110 @@ function hashPassword(password: string, salt: string): string {
|
||||
return scryptSync(password, salt, 64).toString('hex');
|
||||
}
|
||||
|
||||
function loadAdmins(): AdminRecord[] {
|
||||
const raw = readJson<AdminRecord | AdminRecord[] | null>(ADMIN_FILE, null);
|
||||
if (!raw) return [];
|
||||
if (Array.isArray(raw)) return raw;
|
||||
// Legacy single-record format — migrate in-memory (persisted on next write).
|
||||
return [raw];
|
||||
}
|
||||
|
||||
function saveAdmins(admins: AdminRecord[]): void {
|
||||
writeJson(ADMIN_FILE, admins);
|
||||
}
|
||||
|
||||
export function seedDefaultAdminIfMissing(): { username: string; tempPassword?: string } {
|
||||
const existing = readJson<AdminRecord | null>(ADMIN_FILE, null);
|
||||
if (existing) return { username: existing.username };
|
||||
const admins = loadAdmins();
|
||||
if (admins.length > 0) {
|
||||
// Ensure migration is persisted if legacy format was detected.
|
||||
const raw = readJson<AdminRecord | AdminRecord[] | null>(ADMIN_FILE, null);
|
||||
if (raw && !Array.isArray(raw)) saveAdmins(admins);
|
||||
return { username: admins[0].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);
|
||||
const record: AdminRecord = {
|
||||
username,
|
||||
salt,
|
||||
hash: hashPassword(password, salt),
|
||||
createdAt: Date.now()
|
||||
};
|
||||
saveAdmins([record]);
|
||||
return { username, tempPassword: password };
|
||||
}
|
||||
|
||||
export function verifyCredentials(username: string, password: string): boolean {
|
||||
const record = readJson<AdminRecord | null>(ADMIN_FILE, null);
|
||||
const record = loadAdmins().find((a) => a.username === username);
|
||||
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;
|
||||
export function changePassword(
|
||||
username: string,
|
||||
currentPassword: string,
|
||||
newPassword: string
|
||||
): boolean {
|
||||
if (!verifyCredentials(username, currentPassword)) return false;
|
||||
const admins = loadAdmins();
|
||||
const idx = admins.findIndex((a) => a.username === username);
|
||||
if (idx === -1) return false;
|
||||
const salt = randomBytes(16).toString('hex');
|
||||
writeJson(ADMIN_FILE, {
|
||||
username: record.username,
|
||||
admins[idx] = {
|
||||
...admins[idx],
|
||||
salt,
|
||||
hash: hashPassword(newPassword, salt)
|
||||
});
|
||||
};
|
||||
saveAdmins(admins);
|
||||
return true;
|
||||
}
|
||||
|
||||
export type AdminSummary = { username: string; createdAt?: number };
|
||||
|
||||
export function listAdmins(): AdminSummary[] {
|
||||
return loadAdmins().map(({ username, createdAt }) => ({ username, createdAt }));
|
||||
}
|
||||
|
||||
export function createAdmin(username: string, password: string): { ok: true } | { ok: false; error: string } {
|
||||
const clean = username.trim();
|
||||
if (!clean) return { ok: false, error: 'Username obbligatorio.' };
|
||||
if (clean.length < 3) return { ok: false, error: 'Username troppo corto (min 3 caratteri).' };
|
||||
if (!/^[a-zA-Z0-9_.-]+$/.test(clean)) {
|
||||
return { ok: false, error: 'Username: usa solo lettere, numeri, . _ -' };
|
||||
}
|
||||
if (password.length < 8) return { ok: false, error: 'Password troppo corta (min 8 caratteri).' };
|
||||
const admins = loadAdmins();
|
||||
if (admins.some((a) => a.username === clean)) {
|
||||
return { ok: false, error: 'Username già in uso.' };
|
||||
}
|
||||
const salt = randomBytes(16).toString('hex');
|
||||
admins.push({
|
||||
username: clean,
|
||||
salt,
|
||||
hash: hashPassword(password, salt),
|
||||
createdAt: Date.now()
|
||||
});
|
||||
saveAdmins(admins);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export function deleteAdmin(username: string): { ok: true } | { ok: false; error: string } {
|
||||
const admins = loadAdmins();
|
||||
if (admins.length <= 1) {
|
||||
return { ok: false, error: 'Impossibile eliminare l\'ultimo amministratore.' };
|
||||
}
|
||||
const next = admins.filter((a) => a.username !== username);
|
||||
if (next.length === admins.length) return { ok: false, error: 'Amministratore non trovato.' };
|
||||
saveAdmins(next);
|
||||
// Revoke any active sessions for the removed user.
|
||||
const sessions = readJson<SessionRecord[]>(SESSION_FILE, []).filter((s) => s.username !== username);
|
||||
writeJson(SESSION_FILE, sessions);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
function loadSessions(): SessionRecord[] {
|
||||
const now = Date.now();
|
||||
const all = readJson<SessionRecord[]>(SESSION_FILE, []);
|
||||
@@ -95,5 +166,8 @@ export function getSessionUser(cookies: Cookies): { username: string } | null {
|
||||
if (!token) return null;
|
||||
const session = loadSessions().find((s) => s.token === token);
|
||||
if (!session) return null;
|
||||
// If the user has been removed since, invalidate.
|
||||
const exists = loadAdmins().some((a) => a.username === session.username);
|
||||
if (!exists) return null;
|
||||
return { username: session.username };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user