feat: Add initial project setup including superadmin seeding, a Docker entrypoint for the admin app, and a comprehensive README.

This commit is contained in:
Timo Knuth 2026-02-27 21:33:40 +01:00
parent 244da5e69a
commit 8999cdbab3
3 changed files with 210 additions and 179 deletions

View File

@ -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 <repo-url>
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 ~23 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/<backup-file>.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`

View File

@ -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

View File

@ -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()
})