Baustein 1: Grundgeruest, Datenbank, Anmeldung, zwei Rollen
Legt das Fundament der Lern-Bar an: - Next.js 15 (App Router), React 19, TypeScript, ESLint, Vitest - SQLite ueber Drizzle hinter einer Repository-Schicht - Eigener Migrationslauf mit Pruefsummen, beim Start angewendet - Argon2id, serverseitige Sitzungen, HttpOnly-Cookie, SameSite=Lax - Rollen Ausbilder und Azubi, Rechteregeln als reine Funktionen - Verwaltung: Azubikonto und Ausbildung anlegen, Passwort, Sperren - Gestaltung Petrol/Bernstein, helle und dunkle Darstellung - Dockerfile, docker-compose, Lebenszeichen, update.sh mit Rueckfall - Sicherung und Wiederherstellung als Shell-Skripte - 29 Tests fuer Rechte, Zeitrechnung und Migrationslauf Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/bin/sh
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sicherung: Datenbank und Anhänge in ein Archiv schreiben.
|
||||
#
|
||||
# Läuft sowohl im Container als auch auf dem Entwicklungsrechner (Git Bash).
|
||||
#
|
||||
# sicherung.sh [zielverzeichnis]
|
||||
#
|
||||
# Ergebnis: <zielverzeichnis>/lernbar-JJJJMMTT-HHMMSS.tar.gz
|
||||
# ---------------------------------------------------------------------------
|
||||
set -eu
|
||||
|
||||
DATEN="${LERNBAR_DATEN:-/daten}"
|
||||
DB="${LERNBAR_DB:-$DATEN/lernbar.sqlite}"
|
||||
ZIEL="${1:-${LERNBAR_SICHERUNGEN:-$DATEN/sicherungen}}"
|
||||
|
||||
if [ ! -f "$DB" ]; then
|
||||
echo "Keine Datenbank unter $DB gefunden." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$ZIEL"
|
||||
|
||||
STEMPEL="$(date +%Y%m%d-%H%M%S)"
|
||||
ARBEIT="$(mktemp -d)"
|
||||
trap 'rm -rf "$ARBEIT"' EXIT
|
||||
|
||||
# Konsistente Kopie über die Sicherungsschnittstelle von SQLite. Ein blosses
|
||||
# `cp` wäre bei aktivem WAL-Modus nicht verlässlich.
|
||||
node -e "
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database(process.argv[1], { readonly: true });
|
||||
db.backup(process.argv[2]).then(() => { db.close(); }).catch((f) => { console.error(f); process.exit(1); });
|
||||
" "$DB" "$ARBEIT/lernbar.sqlite"
|
||||
|
||||
mkdir -p "$ARBEIT/anhaenge"
|
||||
if [ -d "$DATEN/anhaenge" ]; then
|
||||
cp -R "$DATEN/anhaenge/." "$ARBEIT/anhaenge/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
cat > "$ARBEIT/sicherung.txt" <<ENDE
|
||||
Lern-Bar Sicherung
|
||||
Erstellt am: $(date -Iseconds)
|
||||
Quelle: $DATEN
|
||||
Inhalt: lernbar.sqlite (Datenbank), anhaenge/ (hochgeladene Dateien)
|
||||
Zurückspielen: wiederherstellen.sh <diese-datei.tar.gz>
|
||||
ENDE
|
||||
|
||||
ARCHIV="$ZIEL/lernbar-$STEMPEL.tar.gz"
|
||||
tar -czf "$ARCHIV" -C "$ARBEIT" lernbar.sqlite anhaenge sicherung.txt
|
||||
|
||||
echo "$ARCHIV"
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/bin/sh
|
||||
# ---------------------------------------------------------------------------
|
||||
# Wiederherstellung aus einem Sicherungsarchiv.
|
||||
#
|
||||
# wiederherstellen.sh <archiv.tar.gz> [--ohne-rueckfrage]
|
||||
#
|
||||
# Der vorhandene Datenbestand wird vorher zur Seite gelegt
|
||||
# (daten/verdraengt-<zeitstempel>/), nichts wird endgültig gelöscht.
|
||||
# Die Anwendung sollte dabei gestoppt sein.
|
||||
# ---------------------------------------------------------------------------
|
||||
set -eu
|
||||
|
||||
ARCHIV="${1:-}"
|
||||
BESTAETIGT="${2:-}"
|
||||
|
||||
if [ -z "$ARCHIV" ] || [ ! -f "$ARCHIV" ]; then
|
||||
echo "Aufruf: wiederherstellen.sh <archiv.tar.gz> [--ohne-rueckfrage]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DATEN="${LERNBAR_DATEN:-/daten}"
|
||||
DB="${LERNBAR_DB:-$DATEN/lernbar.sqlite}"
|
||||
|
||||
if [ "$BESTAETIGT" != "--ohne-rueckfrage" ]; then
|
||||
printf 'Der aktuelle Datenbestand unter %s wird ersetzt. Fortfahren? [j/N] ' "$DATEN"
|
||||
read -r antwort
|
||||
case "$antwort" in
|
||||
j | J | ja | Ja) ;;
|
||||
*)
|
||||
echo "Abgebrochen."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
ARBEIT="$(mktemp -d)"
|
||||
trap 'rm -rf "$ARBEIT"' EXIT
|
||||
|
||||
tar -xzf "$ARCHIV" -C "$ARBEIT"
|
||||
|
||||
if [ ! -f "$ARBEIT/lernbar.sqlite" ]; then
|
||||
echo "Das Archiv enthält keine lernbar.sqlite – falsche Datei?" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STEMPEL="$(date +%Y%m%d-%H%M%S)"
|
||||
VERDRAENGT="$DATEN/verdraengt-$STEMPEL"
|
||||
mkdir -p "$VERDRAENGT"
|
||||
|
||||
for datei in "$DB" "$DB-wal" "$DB-shm"; do
|
||||
[ -e "$datei" ] && mv "$datei" "$VERDRAENGT/" || true
|
||||
done
|
||||
if [ -d "$DATEN/anhaenge" ]; then
|
||||
mv "$DATEN/anhaenge" "$VERDRAENGT/anhaenge"
|
||||
fi
|
||||
|
||||
mkdir -p "$DATEN"
|
||||
cp "$ARBEIT/lernbar.sqlite" "$DB"
|
||||
mkdir -p "$DATEN/anhaenge"
|
||||
if [ -d "$ARBEIT/anhaenge" ]; then
|
||||
cp -R "$ARBEIT/anhaenge/." "$DATEN/anhaenge/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "Wiederhergestellt aus $ARCHIV."
|
||||
echo "Der vorherige Stand liegt unter $VERDRAENGT und kann gelöscht werden, sobald alles passt."
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Legt ein Konto an – gedacht für das allererste Ausbilderkonto.
|
||||
*
|
||||
* npm run konto:anlegen -- --name "Max Mustermann" --email max@betrieb.de --rolle ausbilder
|
||||
*
|
||||
* Ohne --passwort wird eines erzeugt und ausgegeben.
|
||||
*/
|
||||
import crypto from 'node:crypto';
|
||||
import { benutzerAnlegen, benutzerNachEmail, passwortSetzen } from '../src/lib/repos/benutzer';
|
||||
import { rohVerbindung } from '../src/lib/db';
|
||||
|
||||
function argument(name: string): string | undefined {
|
||||
const index = process.argv.indexOf(`--${name}`);
|
||||
if (index === -1) return undefined;
|
||||
return process.argv[index + 1];
|
||||
}
|
||||
|
||||
function erzeugePasswort(): string {
|
||||
// Gut vorlesbar: keine leicht verwechselbaren Zeichen.
|
||||
const zeichen = 'abcdefghijkmnpqrstuvwxyz23456789';
|
||||
const roh = crypto.randomBytes(16);
|
||||
return Array.from(roh, (b) => zeichen[b % zeichen.length]).join('');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
rohVerbindung(); // Migrationen anwenden
|
||||
|
||||
const name = argument('name');
|
||||
const email = argument('email');
|
||||
const rolle = (argument('rolle') ?? 'ausbilder') as 'ausbilder' | 'azubi';
|
||||
const passwort = argument('passwort') ?? erzeugePasswort();
|
||||
|
||||
if (!name || !email) {
|
||||
console.error('Aufruf: npm run konto:anlegen -- --name "Vorname Nachname" --email adresse@betrieb.de [--rolle ausbilder|azubi] [--passwort ...]');
|
||||
process.exit(1);
|
||||
}
|
||||
if (rolle !== 'ausbilder' && rolle !== 'azubi') {
|
||||
console.error('--rolle muss "ausbilder" oder "azubi" sein.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const vorhanden = benutzerNachEmail(email);
|
||||
if (vorhanden) {
|
||||
await passwortSetzen(vorhanden.id, passwort);
|
||||
console.log(`Konto ${email} gab es schon – Passwort neu gesetzt.`);
|
||||
} else {
|
||||
await benutzerAnlegen({ name, email, passwort, rolle });
|
||||
console.log(`Konto angelegt: ${name} <${email}> als ${rolle}.`);
|
||||
}
|
||||
|
||||
console.log(`Passwort: ${passwort}`);
|
||||
console.log('Bitte notieren und nach der ersten Anmeldung ändern.');
|
||||
}
|
||||
|
||||
main().catch((fehler) => {
|
||||
console.error(fehler);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user