69 lines
2.4 KiB
JavaScript
69 lines
2.4 KiB
JavaScript
const argon2 = require('argon2');
|
|
const { db } = require('../../db');
|
|
|
|
const insertStmt = db.prepare(
|
|
`INSERT INTO users (email, display_name, password_hash, role) VALUES (?, ?, ?, ?)`
|
|
);
|
|
const findByEmailStmt = db.prepare('SELECT * FROM users WHERE email = ?');
|
|
const findByIdStmt = db.prepare('SELECT * FROM users WHERE id = ?');
|
|
const updateNameStmt = db.prepare('UPDATE users SET display_name = ? WHERE id = ?');
|
|
const updatePasswordStmt = db.prepare('UPDATE users SET password_hash = ? WHERE id = ?');
|
|
const countAdminsStmt = db.prepare("SELECT COUNT(*) AS c FROM users WHERE role = 'admin'");
|
|
|
|
function publicView(row) {
|
|
if (!row) return null;
|
|
return { id: row.id, email: row.email, displayName: row.display_name, role: row.role };
|
|
}
|
|
|
|
async function createUser({ email, displayName, password }) {
|
|
email = email.trim().toLowerCase();
|
|
const hash = await argon2.hash(password, { type: argon2.argon2id });
|
|
// Auto-promote to admin if no admins exist yet and env bootstrap email matches.
|
|
let role = 'user';
|
|
const bootstrap = (process.env.ADMIN_BOOTSTRAP_EMAIL || '').trim().toLowerCase();
|
|
if (bootstrap && email === bootstrap && countAdminsStmt.get().c === 0) {
|
|
role = 'admin';
|
|
}
|
|
const info = insertStmt.run(email, displayName.trim(), hash, role);
|
|
return publicView(findByIdStmt.get(info.lastInsertRowid));
|
|
}
|
|
|
|
async function verifyCredentials(email, password) {
|
|
const row = findByEmailStmt.get(email.trim().toLowerCase());
|
|
if (!row) return null;
|
|
const ok = await argon2.verify(row.password_hash, password);
|
|
return ok ? publicView(row) : null;
|
|
}
|
|
|
|
function findByEmail(email) {
|
|
return publicView(findByEmailStmt.get(email.trim().toLowerCase()));
|
|
}
|
|
|
|
function findById(id) {
|
|
return publicView(findByIdStmt.get(id));
|
|
}
|
|
|
|
function updateDisplayName(id, displayName) {
|
|
updateNameStmt.run(displayName.trim(), id);
|
|
return findById(id);
|
|
}
|
|
|
|
async function changePassword(id, currentPassword, newPassword) {
|
|
const row = findByIdStmt.get(id);
|
|
if (!row) return { ok: false, reason: 'not_found' };
|
|
const ok = await argon2.verify(row.password_hash, currentPassword);
|
|
if (!ok) return { ok: false, reason: 'bad_current' };
|
|
const hash = await argon2.hash(newPassword, { type: argon2.argon2id });
|
|
updatePasswordStmt.run(hash, id);
|
|
return { ok: true };
|
|
}
|
|
|
|
module.exports = {
|
|
createUser,
|
|
verifyCredentials,
|
|
findByEmail,
|
|
findById,
|
|
updateDisplayName,
|
|
changePassword,
|
|
};
|