feat: Phase 1 — Projektstruktur, DB-Schema, Auth-System

- Vollständige Verzeichnisstruktur gemäß CLAUDE.md
- Express-Server mit Helmet, Sessions, Rate Limiting, SPA-Fallback
- SQLite-Schema (Migration v1): 10 Tabellen, updated_at-Triggers, Indizes
- Versioniertes Migrations-System (schema_migrations)
- Auth-Routen: Login, Logout, /me, Admin-User-CRUD
- Frontend App-Shell: SPA-Router, API-Client, Design-System (CSS Tokens)
- PWA: Service Worker, Web App Manifest
- Setup-Script für ersten Admin-User (node setup.js)
- DB-Tests mit node:sqlite built-in: 29/29 bestanden
- Docker Compose + Dockerfile + Nginx-Beispielkonfiguration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ulsklyc
2026-03-24 14:32:36 +01:00
parent b3a6a6da2a
commit d49cbe33b3
44 changed files with 6635 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
/**
* Modul: Setup-Script
* Zweck: Erstmalige Einrichtung — ersten Admin-User anlegen.
* Wird einmalig nach dem ersten Start ausgeführt: `node setup.js`
* Abhängigkeiten: server/db.js, bcrypt, dotenv
*/
'use strict';
require('dotenv').config();
const readline = require('node:readline');
const bcrypt = require('bcrypt');
const db = require('./server/db');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function prompt(question) {
return new Promise((resolve) => rl.question(question, resolve));
}
function promptPassword(question) {
return new Promise((resolve) => {
process.stdout.write(question);
process.stdin.setRawMode(true);
process.stdin.resume();
let password = '';
process.stdin.on('data', function handler(char) {
char = char.toString();
if (char === '\r' || char === '\n') {
process.stdin.setRawMode(false);
process.stdin.removeListener('data', handler);
process.stdout.write('\n');
resolve(password);
} else if (char === '\u0003') {
process.exit();
} else if (char === '\u007f') {
if (password.length > 0) {
password = password.slice(0, -1);
process.stdout.write('\b \b');
}
} else {
password += char;
process.stdout.write('*');
}
});
});
}
async function main() {
console.log('\n=== Oikos Setup ===\n');
// Datenbank initialisieren
db.init();
// Prüfen ob bereits Admin vorhanden
const existingAdmin = db.get()
.prepare("SELECT id FROM users WHERE role = 'admin' LIMIT 1")
.get();
if (existingAdmin) {
console.log(' Es existiert bereits ein Admin-Account.\n');
const proceed = await prompt('Trotzdem einen weiteren Admin anlegen? (j/N): ');
if (proceed.toLowerCase() !== 'j') {
console.log('Setup abgebrochen.');
rl.close();
process.exit(0);
}
}
console.log('Admin-Account anlegen:\n');
const username = (await prompt('Benutzername: ')).trim();
if (!username || username.length < 3) {
console.error('Fehler: Benutzername muss mindestens 3 Zeichen lang sein.');
process.exit(1);
}
const displayName = (await prompt('Anzeigename (z.B. "Max Mustermann"): ')).trim();
if (!displayName) {
console.error('Fehler: Anzeigename darf nicht leer sein.');
process.exit(1);
}
const password = await promptPassword('Passwort: ');
if (password.length < 8) {
console.error('Fehler: Passwort muss mindestens 8 Zeichen lang sein.');
process.exit(1);
}
const passwordConfirm = await promptPassword('Passwort bestätigen: ');
if (password !== passwordConfirm) {
console.error('Fehler: Passwörter stimmen nicht überein.');
process.exit(1);
}
const avatarColors = ['#007AFF', '#34C759', '#FF9500', '#FF3B30', '#AF52DE', '#FF2D55'];
const avatarColor = avatarColors[Math.floor(Math.random() * avatarColors.length)];
console.log('\nAccount wird erstellt …');
const hash = await bcrypt.hash(password, 12);
try {
const result = db.get()
.prepare(`
INSERT INTO users (username, display_name, password_hash, avatar_color, role)
VALUES (?, ?, ?, ?, 'admin')
`)
.run(username, displayName, hash, avatarColor);
console.log(`\n✓ Admin-Account erstellt (ID: ${result.lastInsertRowid})`);
console.log(` Benutzername: ${username}`);
console.log(` Anzeigename: ${displayName}`);
console.log(` Rolle: admin`);
console.log('\nDu kannst dich jetzt unter /login anmelden.\n');
} catch (err) {
if (err.message?.includes('UNIQUE constraint')) {
console.error(`\nFehler: Benutzername "${username}" ist bereits vergeben.`);
} else {
console.error('\nFehler beim Erstellen:', err.message);
}
process.exit(1);
}
rl.close();
process.exit(0);
}
main().catch((err) => {
console.error('Unerwarteter Fehler:', err.message);
process.exit(1);
});