Superuser

This commit is contained in:
root
2025-10-24 18:39:32 +00:00
parent 9133928401
commit f0b1ca4b68
19 changed files with 865 additions and 188 deletions
+212
View File
@@ -0,0 +1,212 @@
import crypto from 'crypto';
import { db } from '../db/index.js';
export const SUPERUSER_GROUP_NAME = 'superuser';
const selectGroupByName = db.prepare('SELECT * FROM user_groups WHERE name = ?');
const insertGroup = db.prepare('INSERT INTO user_groups (name, description) VALUES (?, ?)');
const selectSuperuser = db.prepare(`
SELECT u.*
FROM users u
INNER JOIN user_group_memberships m ON m.user_id = u.id
INNER JOIN user_groups g ON g.id = m.group_id
WHERE g.name = ?
LIMIT 1
`);
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 insertUser = db.prepare(`
INSERT INTO users (username, email, password_hash, password_salt, is_active, created_at, updated_at)
VALUES (?, ?, ?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`);
const updateUser = db.prepare(`
UPDATE users
SET username = ?, email = ?, password_hash = ?, password_salt = ?, is_active = 1, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`);
const insertMembership = db.prepare(`
INSERT OR IGNORE INTO user_group_memberships (user_id, group_id, role)
VALUES (?, ?, ?)
`);
const removeMembershipsForGroup = db.prepare(`
DELETE FROM user_group_memberships
WHERE group_id = ?
`);
const selectUserIdsForGroup = db.prepare(`
SELECT u.id
FROM users u
INNER JOIN user_group_memberships m ON m.user_id = u.id
WHERE m.group_id = ?
`);
const deleteUserById = db.prepare('DELETE FROM users WHERE id = ?');
const deleteGroupById = db.prepare('DELETE FROM user_groups WHERE id = ?');
const countSuperusers = db.prepare(`
SELECT COUNT(*) as count
FROM user_group_memberships m
INNER JOIN user_groups g ON g.id = m.group_id
WHERE g.name = ?
`);
const creationTransaction = db.transaction(({ username, email, passwordHash, passwordSalt, groupId }) => {
const existingUser = selectUserByUsername.get(username) || selectUserByEmail.get(email);
let userId;
if (existingUser) {
updateUser.run(username, email, passwordHash, passwordSalt, existingUser.id);
userId = existingUser.id;
} else {
const result = insertUser.run(username, email, passwordHash, passwordSalt);
userId = result.lastInsertRowid;
}
removeMembershipsForGroup.run(groupId);
insertMembership.run(userId, groupId, SUPERUSER_GROUP_NAME);
return selectUserById.get(userId);
});
function hashPassword(password) {
if (!password || typeof password !== 'string') {
throw new Error('INVALID_PASSWORD');
}
const trimmed = password.trim();
if (trimmed.length < 8) {
throw new Error('PASSWORD_TOO_SHORT');
}
const salt = crypto.randomBytes(16).toString('hex');
const hash = crypto.pbkdf2Sync(trimmed, salt, 120000, 64, 'sha512').toString('hex');
return { salt, hash };
}
function ensureGroup(name, description = null) {
let group = selectGroupByName.get(name);
if (group) return group;
insertGroup.run(name, description);
group = selectGroupByName.get(name);
return group;
}
export function hasSuperuser() {
const { count } = countSuperusers.get(SUPERUSER_GROUP_NAME);
return count > 0;
}
function createOrUpdateSuperuser({ username, email, password }) {
const normalizedUsername = String(username || '').trim();
const normalizedEmail = String(email || '').trim().toLowerCase();
if (!normalizedUsername) {
throw new Error('USERNAME_REQUIRED');
}
if (!normalizedEmail || !normalizedEmail.includes('@')) {
throw new Error('EMAIL_INVALID');
}
const { hash, salt } = hashPassword(password);
const group = ensureGroup(SUPERUSER_GROUP_NAME, 'System Superuser');
const user = creationTransaction({
username: normalizedUsername,
email: normalizedEmail,
passwordHash: hash,
passwordSalt: salt,
groupId: group.id
});
return {
id: user.id,
username: user.username,
email: user.email
};
}
const deletionTransaction = db.transaction((groupId) => {
const members = selectUserIdsForGroup.all(groupId);
members.forEach(({ id }) => {
deleteUserById.run(id);
});
const { changes: groupChanges } = deleteGroupById.run(groupId);
return {
usersRemoved: members.length,
groupRemoved: groupChanges > 0
};
});
export function ensureSuperuserFromEnv() {
const username = process.env.SUPERUSER_USERNAME;
const email = process.env.SUPERUSER_EMAIL;
const password = process.env.SUPERUSER_PASSWORD;
if (hasSuperuser()) {
if (username || email || password) {
console.log('️ Superuser existiert bereits - Umgebungsvariablen werden ignoriert.');
}
return false;
}
if (!username || !email || !password) {
return false;
}
try {
createOrUpdateSuperuser({ username, email, password });
console.log('✅ Superuser aus Umgebungsvariablen angelegt.');
return true;
} catch (error) {
console.error('⚠️ Superuser konnte nicht aus Umgebungsvariablen erzeugt werden:', error.message);
return false;
}
}
export function registerSuperuser({ username, email, password }) {
if (hasSuperuser()) {
const err = new Error('SUPERUSER_ALREADY_EXISTS');
err.code = 'SUPERUSER_ALREADY_EXISTS';
throw err;
}
try {
return createOrUpdateSuperuser({ username, email, password });
} catch (error) {
if (error.message === 'USERNAME_REQUIRED' || error.message === 'EMAIL_INVALID' || error.message === 'PASSWORD_TOO_SHORT') {
error.code = error.message;
}
throw error;
}
}
export function removeSuperuser() {
const group = selectGroupByName.get(SUPERUSER_GROUP_NAME);
if (!group) {
const error = new Error('SUPERUSER_NOT_FOUND');
error.code = 'SUPERUSER_NOT_FOUND';
throw error;
}
return deletionTransaction(group.id);
}
export function getSuperuserSummary() {
const user = selectSuperuser.get(SUPERUSER_GROUP_NAME);
if (!user) return null;
return {
id: user.id,
username: user.username,
email: user.email
};
}