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
+4
View File
@@ -1,3 +1,7 @@
PORTAINER_URL=Your_Portainer_Server_Adress
PORTAINER_API_KEY=Your_Portainer_API_Key
PORTAINER_ENDPOINT_ID=Your_Portainer_Endpoint_ID
SUPERUSER_USERNAME=Your_Superuser_Username (optional)
SUPERUSER_EMAIL=Your_Superuser_Email (optional)
SUPERUSER_PASSWORD=Your_Superuser_Password (optional)
SELF_STACK_ID=Your_StackPulse_Stack_ID
+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
};
}
+67 -9
View File
@@ -1,5 +1,7 @@
import { db } from './index.js';
db.exec('PRAGMA foreign_keys = ON;');
const createRedeployLogsTable = `
CREATE TABLE IF NOT EXISTS redeploy_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -13,7 +15,60 @@ CREATE TABLE IF NOT EXISTS redeploy_logs (
);
`;
const createSettingsTable = `
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
`;
const createUsersTable = `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
password_salt TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
last_login DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`;
const createUserGroupsTable = `
CREATE TABLE IF NOT EXISTS user_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`;
const createUserGroupMembershipsTable = `
CREATE TABLE IF NOT EXISTS user_group_memberships (
user_id INTEGER NOT NULL,
group_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, group_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE
);
`;
const createUserSettingsTable = `
CREATE TABLE IF NOT EXISTS user_settings (
user_id INTEGER NOT NULL,
setting_key TEXT NOT NULL,
setting_value TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, setting_key),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
`;
db.exec(createRedeployLogsTable);
@@ -30,17 +85,20 @@ try {
console.log('✅ redeploy_logs table ready');
const createSettingsTable = `
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`;
db.exec(createSettingsTable);
console.log('✅ settings table ready');
db.exec(createUsersTable);
console.log('✅ users table ready');
db.exec(createUserGroupsTable);
console.log('✅ user_groups table ready');
db.exec(createUserGroupMembershipsTable);
console.log('✅ user_group_memberships table ready');
db.exec(createUserSettingsTable);
console.log('✅ user_settings table ready');
db.close();
+58
View File
@@ -11,6 +11,7 @@ import os from 'os';
import crypto from 'crypto';
import { fileURLToPath } from 'url';
import { db } from './db/index.js';
import { ensureSuperuserFromEnv, getSuperuserSummary, hasSuperuser, registerSuperuser, removeSuperuser } from './auth/superuser.js';
import {
logRedeployEvent,
buildLogFilter,
@@ -23,6 +24,8 @@ import { activateMaintenanceMode, deactivateMaintenanceMode, getMaintenanceState
dotenv.config();
ensureSuperuserFromEnv();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -1115,6 +1118,61 @@ const redeployStackById = async (stackId, redeployType) => {
// --- API Endpoints ---
// Superuser Setup
app.get('/api/auth/superuser/status', (req, res) => {
const exists = hasSuperuser();
const user = exists ? getSuperuserSummary() : null;
res.json({ exists, user });
});
app.post('/api/auth/superuser/register', (req, res) => {
if (hasSuperuser()) {
return res.status(409).json({ error: 'SUPERUSER_EXISTS' });
}
const { username, email, password } = req.body || {};
if (!username || !email || !password) {
return res.status(400).json({ error: 'MISSING_FIELDS' });
}
try {
const user = registerSuperuser({ username, email, password });
res.status(201).json({ user });
} catch (error) {
switch (error.code) {
case 'USERNAME_REQUIRED':
return res.status(400).json({ error: 'USERNAME_REQUIRED' });
case 'EMAIL_INVALID':
return res.status(400).json({ error: 'EMAIL_INVALID' });
case 'PASSWORD_TOO_SHORT':
return res.status(400).json({ error: 'PASSWORD_TOO_SHORT' });
case 'SUPERUSER_ALREADY_EXISTS':
return res.status(409).json({ error: 'SUPERUSER_EXISTS' });
default:
console.error('⚠️ Fehler beim Registrieren des Superusers:', error);
return res.status(500).json({ error: 'INTERNAL_ERROR' });
}
}
});
app.delete('/api/auth/superuser', (req, res) => {
if (!hasSuperuser()) {
return res.status(404).json({ error: 'SUPERUSER_NOT_FOUND' });
}
try {
const result = removeSuperuser();
res.json({ success: true, usersRemoved: result.usersRemoved, groupRemoved: result.groupRemoved });
} catch (error) {
if (error.code === 'SUPERUSER_NOT_FOUND') {
return res.status(404).json({ error: 'SUPERUSER_NOT_FOUND' });
}
console.error('⚠️ Fehler beim Löschen des Superusers:', error);
res.status(500).json({ error: 'INTERNAL_ERROR' });
}
});
// Stacks abrufen
app.get('/api/stacks', maintenanceGuard, async (req, res) => {
console.log("️ [API] GET /api/stacks: Abruf gestartet");
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -32,8 +32,8 @@
<!-- Nepcha Analytics (nepcha.com) -->
<!-- Nepcha is a easy-to-use web analytics. No cookies and fully compliant with GDPR, CCPA and PECR. -->
<script defer data-site="YOUR_DOMAIN_HERE" src="https://api.nepcha.com/js/nepcha-analytics.js"></script>
<script type="module" crossorigin src="/assets/index-4797da57.js"></script>
<link rel="stylesheet" href="/assets/index-53559ba1.css">
<script type="module" crossorigin src="/assets/index-61a55789.js"></script>
<link rel="stylesheet" href="/assets/index-8ee0592b.css">
</head>
<body>
<div id="root"></div>