This commit is contained in:
root
2025-10-27 14:14:57 +00:00
parent f0b1ca4b68
commit a76567969a
27 changed files with 2612 additions and 378 deletions
+55 -6
View File
@@ -18,6 +18,12 @@ const selectSuperuser = db.prepare(`
const selectUserByUsername = db.prepare('SELECT * FROM users WHERE username = ?');
const selectUserByEmail = db.prepare('SELECT * FROM users WHERE email = ?');
const selectUserById = db.prepare('SELECT * FROM users WHERE id = ?');
const updateLastLogin = db.prepare(`
UPDATE users
SET last_login = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`);
const insertUser = db.prepare(`
INSERT INTO users (username, email, password_hash, password_salt, is_active, created_at, updated_at)
@@ -31,8 +37,8 @@ const updateUser = db.prepare(`
`);
const insertMembership = db.prepare(`
INSERT OR IGNORE INTO user_group_memberships (user_id, group_id, role)
VALUES (?, ?, ?)
INSERT OR IGNORE INTO user_group_memberships (user_id, group_id)
VALUES (?, ?)
`);
const removeMembershipsForGroup = db.prepare(`
@@ -70,24 +76,48 @@ const creationTransaction = db.transaction(({ username, email, passwordHash, pas
}
removeMembershipsForGroup.run(groupId);
insertMembership.run(userId, groupId, SUPERUSER_GROUP_NAME);
insertMembership.run(userId, groupId);
return selectUserById.get(userId);
});
function hashPassword(password) {
export function hashPassword(password) {
if (!password || typeof password !== 'string') {
throw new Error('INVALID_PASSWORD');
const err = new Error('INVALID_PASSWORD');
err.code = 'INVALID_PASSWORD';
throw err;
}
const trimmed = password.trim();
if (!trimmed) {
const err = new Error('INVALID_PASSWORD');
err.code = 'INVALID_PASSWORD';
throw err;
}
if (trimmed.length < 8) {
throw new Error('PASSWORD_TOO_SHORT');
const err = new Error('PASSWORD_TOO_SHORT');
err.code = 'PASSWORD_TOO_SHORT';
throw err;
}
const salt = crypto.randomBytes(16).toString('hex');
const hash = crypto.pbkdf2Sync(trimmed, salt, 120000, 64, 'sha512').toString('hex');
return { salt, hash };
}
export function verifyPassword(password, hash, salt) {
if (!hash || !salt) return false;
if (!password || typeof password !== 'string') return false;
const trimmed = password.trim();
if (!trimmed) return false;
const derived = crypto.pbkdf2Sync(trimmed, salt, 120000, 64, 'sha512').toString('hex');
const storedBuffer = Buffer.from(hash, 'hex');
const derivedBuffer = Buffer.from(derived, 'hex');
if (storedBuffer.length !== derivedBuffer.length) {
return false;
}
return crypto.timingSafeEqual(storedBuffer, derivedBuffer);
}
function ensureGroup(name, description = null) {
let group = selectGroupByName.get(name);
if (group) return group;
@@ -101,6 +131,25 @@ export function hasSuperuser() {
return count > 0;
}
export function findUserByIdentifier(identifier) {
const value = String(identifier || '').trim();
if (!value) return null;
if (value.includes('@')) {
return selectUserByEmail.get(value.toLowerCase());
}
return selectUserByUsername.get(value);
}
export function findUserById(id) {
if (!id) return null;
return selectUserById.get(id);
}
export function markUserLogin(userId) {
if (!userId) return;
updateLastLogin.run(userId);
}
function createOrUpdateSuperuser({ username, email, password }) {
const normalizedUsername = String(username || '').trim();
const normalizedEmail = String(email || '').trim().toLowerCase();