From 8999cdbab31f85a264227b5eb33d0eb2f6d09b4c Mon Sep 17 00:00:00 2001 From: Timo Knuth Date: Fri, 27 Feb 2026 21:33:40 +0100 Subject: [PATCH] feat: Add initial project setup including superadmin seeding, a Docker entrypoint for the admin app, and a comprehensive README. --- innungsapp/README.md | 307 ++++++++---------- innungsapp/apps/admin/docker-entrypoint.sh | 18 +- .../packages/shared/prisma/seed-superadmin.js | 64 ++++ 3 files changed, 210 insertions(+), 179 deletions(-) create mode 100644 innungsapp/packages/shared/prisma/seed-superadmin.js diff --git a/innungsapp/README.md b/innungsapp/README.md index 469e44c..930d634 100644 --- a/innungsapp/README.md +++ b/innungsapp/README.md @@ -1,259 +1,219 @@ # InnungsApp -Die digitale Plattform für Innungen — News, Mitgliederverzeichnis, Termine und Lehrlingsbörse. +Digitale Plattform fuer Innungen mit Admin-Dashboard (Next.js) und Mobile App (Expo). ## Stack -| Schicht | Technologie | +| Layer | Technology | |---|---| -| **Monorepo** | pnpm Workspaces + Turborepo | -| **Mobile App** | Expo (React Native) + Expo Router | -| **Admin Dashboard** | Next.js 15 (App Router) | -| **API** | tRPC v11 | -| **Auth** | better-auth (Magic Links) | -| **Datenbank** | PostgreSQL + Prisma ORM | -| **Styling Mobile** | NativeWind v4 (Tailwind CSS) | -| **Styling Admin** | Tailwind CSS | -| **State Management** | Zustand (Mobile) + React Query (beide Apps) | +| Monorepo | pnpm Workspaces + Turborepo | +| Admin Dashboard | Next.js 15 (App Router) | +| Mobile App | Expo + React Native | +| API | tRPC v11 | +| Auth | better-auth (magic links + credential login) | +| Database | SQLite + Prisma ORM | +| Styling | Tailwind CSS (admin), NativeWind (mobile) | -## Projekt-Struktur +## Projektstruktur -``` +```text innungsapp/ -├── apps/ -│ ├── mobile/ # Expo React Native App (iOS + Android) -│ └── admin/ # Next.js Admin Dashboard -├── packages/ -│ └── shared/ # TypeScript-Typen + Prisma Client -└── ... +|-- apps/ +| |-- admin/ +| `-- mobile/ +|-- packages/ +| `-- shared/ +| `-- prisma/ +|-- docker-compose.yml +`-- README.md ``` -## Setup +## Local Setup ### Voraussetzungen - Node.js >= 20 - pnpm >= 9 -- PostgreSQL-Datenbank -- SMTP-Server (für Magic Links) +- SMTP-Zugang (fuer Einladungen und Magic Links) -### 1. Abhängigkeiten installieren +### 1. Abhaengigkeiten installieren ```bash pnpm install ``` -### 2. Umgebungsvariablen +### 2. Umgebungsvariablen setzen (Admin lokal) ```bash cp .env.example apps/admin/.env.local -# .env.local befüllen (DATABASE_URL, BETTER_AUTH_SECRET, SMTP_*) ``` -### 3. Datenbank einrichten +Danach `apps/admin/.env.local` anpassen (mindestens `BETTER_AUTH_SECRET`, SMTP-Werte). + +### 3. DB vorbereiten (lokal) ```bash -# Prisma Client generieren pnpm db:generate +pnpm db:push +``` -# Migrationen anwenden -pnpm db:migrate +Optional Demo-Daten: -# Demo-Daten einspielen (optional) +```bash pnpm db:seed ``` ### 4. Entwicklung starten ```bash -# Admin Dashboard (http://localhost:3000) pnpm --filter @innungsapp/admin dev - -# Mobile App (Expo DevTools) pnpm --filter @innungsapp/mobile dev ``` -Oder alles parallel: +Oder parallel: + ```bash pnpm dev ``` -## Datenbank-Schema +## Production Deployment (Docker, Admin) -Das Schema befindet sich in `packages/shared/prisma/schema.prisma`. +Dieser Abschnitt ist der verbindliche Weg fuer den Productive-Server. -Wichtige Tabellen: -- `organizations` — Innungen (Multi-Tenancy) -- `members` — Mitglieder (verknüpft mit Auth-User nach Einladung) -- `user_roles` — Berechtigungen (admin | member) -- `news`, `news_reads`, `news_attachments` — News-System -- `termine`, `termin_anmeldungen` — Terminverwaltung -- `stellen` — Lehrlingsbörse (öffentlich lesbar) +### Voraussetzungen -## Auth-Flow +- Linux Server mit Docker + Docker Compose +- DNS-Eintrag auf den Server +- SMTP-Zugangsdaten +- Reverse Proxy (z. B. Nginx) fuer HTTPS -1. **Admin einrichten:** Seed-Daten oder manuell in der DB -2. **Mitglied einladen:** Admin erstellt Mitglied → "Einladung senden" → Magic Link per E-Mail -3. **Mitglied loggt ein:** Magic Link → Session → App-Zugang - -## API (tRPC) - -Alle API-Endpunkte sind typsicher über tRPC definiert: - -- `organizations.*` — Org-Einstellungen, Stats, AVV -- `members.*` — CRUD, Einladungen -- `news.*` — CRUD, Lesestatus, Push-Benachrichtigungen -- `termine.*` — CRUD, Anmeldungen -- `stellen.*` — Public + Auth-geschützte Endpunkte - -## Deployment - -### Admin — Docker (empfohlen für Self-Hosting) - -**Voraussetzungen:** Docker + Docker Compose auf dem Server installiert. - -#### Schritt 1: Repository klonen +### 1. Repository klonen ```bash git clone cd innungsapp ``` -#### Schritt 2: Umgebungsvariablen anlegen +### 2. Production-Env anlegen ```bash cp .env.production.example .env ``` -Dann `.env` öffnen und **alle Werte** befüllen: +Pflichtwerte in `.env`: -| Variable | Beschreibung | -|---|---| -| `BETTER_AUTH_SECRET` | Zufälliger String (min. 32 Zeichen) — z.B. `openssl rand -hex 32` | -| `BETTER_AUTH_URL` | Öffentliche URL der App, z.B. `https://app.deine-innung.de` | -| `NEXT_PUBLIC_APP_URL` | Gleicher Wert wie `BETTER_AUTH_URL` | -| `EMAIL_FROM` | Absender-Adresse für Magic Links | -| `SMTP_HOST` | SMTP-Server-Adresse | -| `SMTP_PORT` | Meistens `587` (STARTTLS) oder `465` (SSL) | -| `SMTP_USER` | SMTP-Benutzername | -| `SMTP_PASS` | SMTP-Passwort | +- `BETTER_AUTH_SECRET` (mindestens 32 Zeichen) +- `BETTER_AUTH_URL` (z. B. `https://app.deine-innung.de`) +- `NEXT_PUBLIC_APP_URL` (gleich wie `BETTER_AUTH_URL`) +- `EMAIL_FROM` +- `SMTP_HOST` +- `SMTP_PORT` +- `SMTP_SECURE` +- `SMTP_USER` +- `SMTP_PASS` -#### Schritt 3: Container bauen und starten +### 3. Container bauen und starten ```bash docker compose up -d --build ``` -Der Build dauert beim ersten Mal ~2–3 Minuten. Danach läuft die App auf **Port 3000**. +Hinweis zum DB-Start: + +- Wenn Prisma-Migrationen vorhanden sind, wird `prisma migrate deploy` ausgefuehrt. +- Wenn keine Migrationen vorhanden sind, wird einmalig `prisma db push` ausgefuehrt. + +### 4. Healthcheck und Logs pruefen -Logs prüfen: ```bash docker compose logs -f admin +curl -fsS http://localhost:3000/api/health ``` -#### Schritt 4: Superadmin anlegen (nur beim ersten Start) +Erwartet: JSON mit `status: "ok"`. + +### 5. Superadmin anlegen (nur beim ersten Start) ```bash -docker compose exec admin node -e " -const { PrismaClient } = require('@prisma/client'); -const { scryptSync, randomBytes } = require('crypto'); -const prisma = new PrismaClient(); -// Superadmin wird via seed-superadmin.ts angelegt -" +docker compose exec -w /app admin node packages/shared/prisma/seed-superadmin.js ``` -Einfacher: Den Seed direkt ausführen: +Default Login: -```bash -docker compose exec -w /app admin \ - node packages/shared/prisma/seed-superadmin.js -``` +- E-Mail: `superadmin@innungsapp.de` +- Passwort: `demo1234` -> Standard-Login nach Seed: `superadmin@innungsapp.de` / `demo1234` -> **Passwort sofort in den Einstellungen ändern!** +Passwort direkt nach dem ersten Login aendern. -#### Schritt 5: Reverse Proxy (HTTPS) +### 6. HTTPS (Reverse Proxy) -Nginx-Beispielkonfiguration für `app.deine-innung.de`: +Nginx sollte auf `localhost:3000` weiterleiten und TLS terminieren. +Beispiel: ```nginx server { - listen 80; - server_name app.deine-innung.de; - return 301 https://$host$request_uri; + listen 80; + server_name app.deine-innung.de; + return 301 https://$host$request_uri; } server { - listen 443 ssl; - server_name app.deine-innung.de; + listen 443 ssl; + server_name app.deine-innung.de; - ssl_certificate /etc/letsencrypt/live/app.deine-innung.de/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/app.deine-innung.de/privkey.pem; + ssl_certificate /etc/letsencrypt/live/app.deine-innung.de/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/app.deine-innung.de/privkey.pem; - client_max_body_size 20M; - - location / { - proxy_pass http://localhost:3000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache_bypass $http_upgrade; - } + location / { + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } } ``` -SSL-Zertifikat mit Certbot: -```bash -certbot --nginx -d app.deine-innung.de -``` - -#### Updates einspielen +### 7. Updates einspielen ```bash git pull docker compose up -d --build -``` - -Datenbank und Uploads bleiben dabei erhalten (Docker Volumes). - -#### Häufige Befehle - -```bash -# Status prüfen -docker compose ps - -# Logs ansehen docker compose logs -f admin - -# Container neustarten -docker compose restart admin - -# In Container einloggen -docker compose exec admin sh - -# App stoppen -docker compose down - -# App stoppen + Daten löschen (Vorsicht!) -docker compose down -v ``` ---- +### 8. Backup und Restore (Docker Volumes) -### Admin — Vercel (Alternative) +Vorher die exakten Volumenamen pruefen: ```bash -# Umgebungsvariablen in Vercel setzen: -# DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL, SMTP_* - -vercel --cwd apps/admin +docker volume ls | grep db_data +docker volume ls | grep uploads_data ``` -### Mobile (EAS Build) +Backup: + +```bash +mkdir -p backups +docker run --rm \ + -v innungsapp_db_data:/volume \ + -v "$(pwd)/backups:/backup" \ + alpine sh -c "tar czf /backup/db_data_$(date +%F_%H%M).tar.gz -C /volume ." +``` + +Restore (nur bei gestoppter App): + +```bash +docker compose down +docker run --rm \ + -v innungsapp_db_data:/volume \ + -v "$(pwd)/backups:/backup" \ + alpine sh -c "rm -rf /volume/* && tar xzf /backup/.tar.gz -C /volume" +docker compose up -d +``` + +## Mobile Release (EAS) ```bash cd apps/mobile @@ -261,33 +221,32 @@ eas build --platform all --profile production eas submit --platform all ``` -## DSGVO / AVV +Wichtig: -- AVV-Akzeptanz in Admin → Einstellungen (Pflichtfeld vor Go-Live) -- Alle personenbezogenen Daten in EU-Region (Datenbankserver in Deutschland empfohlen) -- Keine Daten an Dritte außer Expo Push API (anonymisierte Token) +- In `apps/mobile/eas.json` sind Submit-Placeholders vorhanden und muessen ersetzt werden. +- Fuer Production darf keine API-URL auf `localhost` zeigen. -## Roadmap +## Troubleshooting -Siehe `innung-app-mvp.md` für die vollständige Roadmap. +### `migrate deploy` oder `db push` fehlschlaegt -## Apps starten (Schnellstart) +- `DATABASE_URL` pruefen +- Rechte auf `/app/data` pruefen +- Logs: `docker compose logs -f admin` -Um die Anwendungen lokal zu starten, öffne ein Terminal im Hauptverzeichnis (`innungsapp/`) und nutze folgende Befehle: +### Healthcheck liefert Fehler -**Admin Dashboard starten:** -```bash -pnpm --filter @innungsapp/admin dev -``` -Das Dashboard ist im Browser unter [http://localhost:3000](http://localhost:3000) erreichbar. +- Containerstatus: `docker compose ps` +- App-Logs lesen +- Reverse Proxy testweise umgehen und direkt `localhost:3000` pruefen -**Mobile App starten:** -```bash -pnpm --filter @innungsapp/mobile dev -``` -Dies startet den Expo-Server. Scanne den QR-Code mit der **Expo Go App** auf deinem Smartphone oder drücke `a` (für den Android Emulator) bzw. `i` (für den iOS Simulator) im Terminal. +### Login funktioniert nicht nach Seed -**Beides gleichzeitig starten:** -```bash -pnpm dev -``` +- Seed-Command erneut ausfuehren +- In DB pruefen, ob `user` und `account` Eintraege fuer `superadmin@innungsapp.de` existieren + +## Weiterfuehrende Doku + +- Produkt-Roadmap: `../ROADMAP.md` +- Architektur: `../ARCHITECTURE.md` +- API Design: `../API_DESIGN.md` diff --git a/innungsapp/apps/admin/docker-entrypoint.sh b/innungsapp/apps/admin/docker-entrypoint.sh index 1e46798..2cbed26 100644 --- a/innungsapp/apps/admin/docker-entrypoint.sh +++ b/innungsapp/apps/admin/docker-entrypoint.sh @@ -1,11 +1,19 @@ #!/bin/sh set -e -# Run Prisma migrations on startup -echo "Running database migrations..." -DATABASE_URL="${DATABASE_URL:-file:/app/data/prod.db}" \ - node_modules/.bin/prisma migrate deploy \ - --schema=./packages/shared/prisma/schema.prisma +# Keep DATABASE_URL consistent for every Prisma command +export DATABASE_URL="${DATABASE_URL:-file:/app/data/prod.db}" +MIGRATIONS_DIR="./packages/shared/prisma/migrations" + +# Prefer migration-based deploys. Fall back to db push when no migrations exist yet. +set -- "$MIGRATIONS_DIR"/*/migration.sql +if [ -f "$1" ]; then + echo "Applying Prisma migrations..." + node_modules/.bin/prisma migrate deploy --schema=./packages/shared/prisma/schema.prisma +else + echo "No Prisma migrations found. Syncing schema with db push..." + node_modules/.bin/prisma db push --skip-generate --schema=./packages/shared/prisma/schema.prisma +fi echo "Starting Next.js server..." exec node apps/admin/server.js diff --git a/innungsapp/packages/shared/prisma/seed-superadmin.js b/innungsapp/packages/shared/prisma/seed-superadmin.js new file mode 100644 index 0000000..a355bff --- /dev/null +++ b/innungsapp/packages/shared/prisma/seed-superadmin.js @@ -0,0 +1,64 @@ +const { PrismaClient } = require('@prisma/client') +const { randomBytes, scrypt } = require('crypto') +const { promisify } = require('util') + +const prisma = new PrismaClient() +const scryptAsync = promisify(scrypt) + +async function hashPassword(password) { + const salt = randomBytes(16).toString('hex') + const key = await scryptAsync(password.normalize('NFKC'), salt, 64, { + N: 16384, + r: 16, + p: 1, + maxmem: 128 * 16384 * 16 * 2, + }) + return `${salt}:${key.toString('hex')}` +} + +async function main() { + const email = 'superadmin@innungsapp.de' + const password = 'demo1234' + const userId = 'superadmin-user-id' + const accountId = 'superadmin-account-id' + + console.log('Seeding superadmin...') + const hash = await hashPassword(password) + + const user = await prisma.user.upsert({ + where: { email }, + update: { + name: 'Super Admin', + emailVerified: true, + }, + create: { + id: userId, + name: 'Super Admin', + email, + emailVerified: true, + }, + }) + + await prisma.account.upsert({ + where: { id: accountId }, + update: { password: hash }, + create: { + id: accountId, + accountId: user.id, + providerId: 'credential', + userId: user.id, + password: hash, + }, + }) + + console.log(`Done. Login: ${email} / ${password}`) +} + +main() + .catch((error) => { + console.error(error) + process.exit(1) + }) + .finally(async () => { + await prisma.$disconnect() + })