Container-Setup, Backups und Dokumentation
Mehrstufiges Image: gebaut wird mit allen Abhaengigkeiten, ausgeliefert wird nur die Laufzeit. Migrationen laufen beim Containerstart. Der App-Port wird bewusst nicht nach aussen gemappt - davor gehoert ein Reverse Proxy, der HTTPS terminiert. Der Backup-Service schreibt taeglich einen pg_dump und raeumt Dumps aelter als 14 Tage weg.
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
node_modules
|
||||
**/node_modules
|
||||
**/dist
|
||||
server/public
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
**/*.log
|
||||
coverage
|
||||
.idea
|
||||
.vscode
|
||||
@@ -0,0 +1,24 @@
|
||||
# Kopie als .env neben die compose.yaml legen und Werte ersetzen.
|
||||
|
||||
# Passwort des Datenbank-Nutzers. Lang und zufaellig waehlen.
|
||||
DB_PASSWORD=hier-ein-langes-zufaelliges-passwort
|
||||
|
||||
# Schluessel fuer Sitzungen und CSRF-Token, mindestens 32 Zeichen.
|
||||
# Erzeugen z. B. mit: openssl rand -base64 48
|
||||
SESSION_SECRET=hier-einen-langen-zufallsstring-einsetzen
|
||||
|
||||
# Erster Administrator, wird nur angelegt solange noch kein Konto existiert.
|
||||
# Bleibt das Passwort leer, erzeugt die App eines und schreibt es einmalig
|
||||
# ins Container-Log. In beiden Faellen ist ein Wechsel beim ersten Login Pflicht.
|
||||
BOOTSTRAP_ADMIN_USER=admin
|
||||
BOOTSTRAP_ADMIN_PASSWORD=
|
||||
|
||||
# --- Optional ---
|
||||
|
||||
# Gueltigkeit einer Sitzung in Stunden (Standard: 12).
|
||||
# SESSION_TTL_HOURS=12
|
||||
|
||||
# Nur abschalten, wenn ohne HTTPS getestet wird.
|
||||
# COOKIE_SECURE=true
|
||||
|
||||
# LOG_LEVEL=info
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
# --- Build ---
|
||||
FROM node:22-slim AS build
|
||||
|
||||
# Prisma benoetigt OpenSSL fuer seine Query-Engine.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends openssl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
COPY server/package.json ./server/
|
||||
COPY web/package.json ./web/
|
||||
RUN npm ci
|
||||
|
||||
COPY server/prisma ./server/prisma
|
||||
RUN npx prisma generate --schema server/prisma/schema.prisma
|
||||
|
||||
COPY server ./server
|
||||
COPY web ./web
|
||||
|
||||
# Das Frontend landet laut vite.config.ts direkt in server/public.
|
||||
RUN npm run build --workspace=web \
|
||||
&& npx tsc -p server/tsconfig.json
|
||||
|
||||
# --- Laufzeit ---
|
||||
FROM node:22-slim AS runtime
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends openssl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
COPY server/package.json ./server/
|
||||
COPY web/package.json ./web/
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
COPY server/prisma ./server/prisma
|
||||
RUN npx prisma generate --schema server/prisma/schema.prisma
|
||||
|
||||
COPY --from=build /app/server/dist ./server/dist
|
||||
COPY --from=build /app/server/public ./server/public
|
||||
COPY docker-entrypoint.sh ./
|
||||
RUN chmod +x docker-entrypoint.sh && chown -R node:node /app
|
||||
|
||||
USER node
|
||||
EXPOSE 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||
CMD node -e "fetch('http://127.0.0.1:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||||
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
CMD ["node", "server/dist/index.js"]
|
||||
@@ -1,3 +1,178 @@
|
||||
# CC-Manager
|
||||
|
||||
For managing Custom Characters on a Server
|
||||
Selbst gehostete Web-App zur Verwaltung von Custom Characters (CCs) auf einem SCP-RP-Server.
|
||||
Sie führt pro Spieler ein Konto über gekaufte Ansprüche und deren Verbrauch, damit die Frage
|
||||
„darf der gerade einen neuen CC?" nicht mehr von Hand abgeglichen werden muss.
|
||||
|
||||
## Das Prinzip
|
||||
|
||||
Jeder Spieler hat drei Guthaben-Töpfe:
|
||||
|
||||
| Topf | Wird gefüllt durch | Wird verbraucht durch |
|
||||
|------|--------------------|-----------------------|
|
||||
| CC-Guthaben | jedes CC-Paket (+1) | ein neuer CC |
|
||||
| Redesign-Guthaben (bezahlt) | jedes CC-Paket (+1, immer inklusive) und jedes Redesign-Paket (+1) | ein Redesign |
|
||||
| Kulanz-Redesign | manuell eingetragene Kulanz (+1) | ein Redesign |
|
||||
|
||||
Zwei Regeln sind fest verdrahtet:
|
||||
|
||||
1. **Ein CC-Paket erzeugt beide Guthaben.** Der inklusive Redesign wird automatisch mitgebucht,
|
||||
in derselben Transaktion und mit gemeinsamer Gruppen-ID — ein Storno nimmt beide zurück.
|
||||
2. **Inaktive CCs kosten unterschiedlich.** Wurde ein CC nur wegen Spieler-Inaktivität vom Server
|
||||
entfernt, kommt er gratis zurück. Wurde er redesignt und existiert im Original nicht mehr,
|
||||
kostet die Reaktivierung ein Redesign- oder CC-Guthaben.
|
||||
|
||||
Guthaben werden als **Kontobuch** geführt: jede Bewegung ist eine eigene Zeile, der Stand eines
|
||||
Topfes ist deren Summe. Das macht jeden Stand nachvollziehbar und Korrekturen sauber — storniert
|
||||
wird per Gegenbuchung, nie durch Löschen. Ein überzogenes Konto ist erlaubt (kommt vor, wenn ein
|
||||
Redesign vor dem zugehörigen Paket erfasst wird), wird aber rot markiert.
|
||||
|
||||
Ein Redesign legt einen **neuen** CC-Datensatz an und verkettet ihn mit dem alten. So bleibt die
|
||||
Kette „A → B → C" sichtbar und die Reaktivierungsregel greift automatisch richtig.
|
||||
|
||||
## Schnellstart
|
||||
|
||||
Vorausgesetzt wird Docker mit Compose-Plugin (oder DockGe).
|
||||
|
||||
```bash
|
||||
git clone https://git.harlelux.de/Harlekin/CC-Manager.git
|
||||
cd CC-Manager
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
In der `.env` mindestens `DB_PASSWORD` und `SESSION_SECRET` setzen:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 48 # für SESSION_SECRET
|
||||
```
|
||||
|
||||
Dann starten:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
docker compose logs -f app
|
||||
```
|
||||
|
||||
Beim ersten Start legt die App einen Administrator an. Ist `BOOTSTRAP_ADMIN_PASSWORD` leer,
|
||||
erzeugt sie ein Einmal-Passwort und schreibt es **einmalig** ins Log — dort steht es in der
|
||||
Ausgabe oben. Beim ersten Login ist ein Passwortwechsel Pflicht; bis dahin sind alle anderen
|
||||
Seiten gesperrt.
|
||||
|
||||
Für DockGe: Repo-Inhalt als Stack anlegen, `.env` daneben, fertig. Datenbank-Migrationen laufen
|
||||
bei jedem Containerstart automatisch mit.
|
||||
|
||||
## HTTPS
|
||||
|
||||
Der App-Port wird bewusst **nicht** nach außen gemappt. Davor gehört ein Reverse Proxy
|
||||
(Nginx Proxy Manager, Traefik oder Caddy), der HTTPS terminiert und intern auf `app:3000`
|
||||
weiterleitet. So ist nur Port 443 offen und die Zertifikate laufen über Let's Encrypt.
|
||||
|
||||
Läuft die App hinter einem Proxy, muss `TRUST_PROXY=true` gesetzt bleiben — sonst landen im
|
||||
Audit-Log die Proxy-IPs statt der echten. In Produktion werden Cookies automatisch auf `Secure`
|
||||
gesetzt; nur zum Testen ohne TLS lässt sich das über `COOKIE_SECURE=false` abschalten.
|
||||
|
||||
## Backups
|
||||
|
||||
Der `backup`-Service im Stack schreibt beim Start und danach alle 24 Stunden einen
|
||||
`pg_dump` nach `./backups/` und räumt Dumps älter als 14 Tage weg. Die Daten bilden bezahlte
|
||||
Ansprüche ab — das Verzeichnis gehört auf eine Sicherung außerhalb des Hosts.
|
||||
|
||||
Wiederherstellen:
|
||||
|
||||
```bash
|
||||
gunzip -c backups/ccmanager-2025-01-15-0300.sql.gz | \
|
||||
docker compose exec -T db psql -U ccuser -d ccmanager
|
||||
```
|
||||
|
||||
## Import
|
||||
|
||||
Kaufdaten lassen sich als CSV oder JSON hochladen; das Format ist nicht festgelegt. Der Ablauf:
|
||||
|
||||
1. Datei hochladen — Spalten und Beispielzeilen werden angezeigt.
|
||||
2. Spalten zuordnen (Spieler-Kennung, Produktname, optional Bestell-ID und Datum). Die üblichen
|
||||
Spaltennamen werden vorausgewählt.
|
||||
3. **Vorschau** prüfen: für jede Zeile steht da, was passieren würde.
|
||||
4. Buchen.
|
||||
|
||||
Welcher Produktname welches Guthaben ergibt, steht in der Mapping-Tabelle unter *Verwaltung →
|
||||
Produkt-Mappings* (Teiltreffer, Groß-/Kleinschreibung egal). Neue Produkte lassen sich dort
|
||||
jederzeit ergänzen, ohne den Code anzufassen.
|
||||
|
||||
Wird eine Bestell-ID zugeordnet, bucht ein zweiter Import dieselben Käufe **nicht** erneut —
|
||||
Dubletten werden sowohl gegen bereits gebuchte Bestellungen als auch innerhalb der Datei erkannt.
|
||||
Ohne Bestell-ID entfällt dieser Schutz; darauf weist das Formular hin.
|
||||
|
||||
Kulanz-Redesigns tauchen in keinem Shop-Export auf und werden immer von Hand eingetragen.
|
||||
|
||||
## Rollen
|
||||
|
||||
Es gibt **keine öffentliche Registrierung**. Konten legt ausschließlich ein Administrator an.
|
||||
|
||||
- **Viewer** — darf nur lesen.
|
||||
- **Editor** — Spieler anlegen, Guthaben eintragen, CCs erstellen, redesignen, reaktivieren.
|
||||
- **Admin** — zusätzlich Import, Nutzerverwaltung, Produkt-Mappings, Audit-Log und Korrekturbuchungen.
|
||||
|
||||
Jede ändernde Aktion landet im Audit-Log. Für Administratoren lässt sich unter *Mein Konto*
|
||||
eine Zwei-Faktor-Anmeldung per TOTP einrichten.
|
||||
|
||||
## Entwicklung
|
||||
|
||||
Gebraucht werden Node 20+ und eine erreichbare PostgreSQL-Instanz.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
cp server/.env.example server/.env # DATABASE_URL auf die lokale DB zeigen lassen
|
||||
npm run migrate:dev
|
||||
npm run dev # API auf :3000, Frontend auf :5173
|
||||
```
|
||||
|
||||
Das Frontend leitet `/api` im Entwicklungsbetrieb an den API-Port weiter, Anmeldung und Cookies
|
||||
funktionieren also wie in Produktion.
|
||||
|
||||
Die Tests brauchen **keine** laufende Datenbank — sie starten eine Postgres-Instanz im Speicher:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Abgedeckt sind die Guthaben-Regeln, der CC-Lebenszyklus samt Reaktivierungslogik, Korrekturen
|
||||
und Stornos, Anmeldung mit CSRF- und Rollenprüfung sowie der Import mit Dedup.
|
||||
|
||||
### Aufbau
|
||||
|
||||
```
|
||||
server/
|
||||
prisma/schema.prisma Datenmodell und Migrationen
|
||||
src/services/ Fachlogik: Ledger, CC-Lebenszyklus, Import
|
||||
src/routes/ HTTP-Endpunkte
|
||||
src/auth/ Passwörter, Sitzungen, CSRF, Rollen, TOTP
|
||||
test/ Integrationstests gegen eine echte Postgres-Instanz
|
||||
web/
|
||||
src/pages/ Login, Übersicht, Spieler-Detail, Import, Verwaltung
|
||||
src/components/ Wiederverwendete Bausteine
|
||||
```
|
||||
|
||||
Backend ist Fastify mit Prisma, Frontend eine React-SPA. Gebaut wird beides in ein einziges
|
||||
Image: Fastify liefert die API und die fertigen Frontend-Dateien aus.
|
||||
|
||||
Nach Änderungen am Datenmodell:
|
||||
|
||||
```bash
|
||||
npm run migrate:dev -- --name beschreibung-der-aenderung
|
||||
```
|
||||
|
||||
## Sicherheit
|
||||
|
||||
- Passwörter mit Argon2id gehasht, Mindestlänge 12 Zeichen.
|
||||
- Sitzungen serverseitig; im Cookie steht nur ein Token, in der Datenbank dessen Hash.
|
||||
Cookie ist `httpOnly`, `SameSite=Lax` und in Produktion `Secure`.
|
||||
- CSRF-Schutz für alle ändernden Anfragen über ein aus der Sitzung abgeleitetes Token.
|
||||
- Alle Eingaben werden validiert, alle Datenbankzugriffe laufen parametrisiert über das ORM.
|
||||
- Rate-Limit auf dem Login, generische Fehlermeldung ohne Hinweis auf existierende Konten.
|
||||
- Sicherheits-Header inklusive Content-Security-Policy.
|
||||
- Ein gesperrtes Konto verliert seine offenen Sitzungen sofort; der letzte aktive Administrator
|
||||
kann sich nicht selbst entfernen.
|
||||
|
||||
## Lizenz
|
||||
|
||||
GPL-3.0-or-later, siehe [LICENSE](LICENSE).
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
# Alternativ ein vorgebautes Image verwenden:
|
||||
# image: ghcr.io/harlekin/cc-manager:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DATABASE_URL: postgres://ccuser:${DB_PASSWORD}@db:5432/ccmanager
|
||||
SESSION_SECRET: ${SESSION_SECRET}
|
||||
NODE_ENV: production
|
||||
# Erstzugang; nach der ersten Anmeldung wird ein Wechsel erzwungen.
|
||||
BOOTSTRAP_ADMIN_USER: ${BOOTSTRAP_ADMIN_USER:-admin}
|
||||
BOOTSTRAP_ADMIN_PASSWORD: ${BOOTSTRAP_ADMIN_PASSWORD:-}
|
||||
TRUST_PROXY: "true"
|
||||
expose:
|
||||
# Bewusst nicht nach aussen gemappt: der Reverse Proxy terminiert HTTPS.
|
||||
- "3000"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ccuser
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_DB: ccmanager
|
||||
volumes:
|
||||
- cc_db:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ccuser -d ccmanager"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
backup:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PGPASSWORD: ${DB_PASSWORD}
|
||||
volumes:
|
||||
- ./backups:/backups
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
# Dump beim Start und danach alle 24 Stunden, Aufbewahrung 14 Tage.
|
||||
entrypoint: >
|
||||
sh -c 'while true; do
|
||||
pg_dump -h db -U ccuser -d ccmanager | gzip > /backups/ccmanager-$$(date +%F-%H%M).sql.gz &&
|
||||
echo "Backup geschrieben: $$(date +%F-%H%M)";
|
||||
find /backups -name "ccmanager-*.sql.gz" -mtime +14 -delete;
|
||||
sleep 86400;
|
||||
done'
|
||||
|
||||
volumes:
|
||||
cc_db:
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Schema aktuell halten, bevor die App startet. Bereits angewandte
|
||||
# Migrationen werden dabei uebersprungen.
|
||||
echo "Wende Datenbank-Migrationen an ..."
|
||||
npx prisma migrate deploy --schema server/prisma/schema.prisma
|
||||
|
||||
exec "$@"
|
||||
@@ -0,0 +1,15 @@
|
||||
# Nur fuer die lokale Entwicklung. Als server/.env kopieren.
|
||||
# Im Container kommen diese Werte aus der compose.yaml.
|
||||
|
||||
DATABASE_URL=postgresql://ccuser:passwort@localhost:5432/ccmanager
|
||||
SESSION_SECRET=lokaler-entwicklungsschluessel-mindestens-32-zeichen
|
||||
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
|
||||
# Ohne HTTPS muessen die Cookies unsicher ausgeliefert werden duerfen.
|
||||
COOKIE_SECURE=false
|
||||
|
||||
# Erstadministrator beim ersten Start.
|
||||
BOOTSTRAP_ADMIN_USER=admin
|
||||
BOOTSTRAP_ADMIN_PASSWORD=EntwicklungPasswort1
|
||||
Reference in New Issue
Block a user