feat: Add initial project setup including superadmin seeding, a Docker entrypoint for the admin app, and a comprehensive README.
This commit is contained in:
parent
244da5e69a
commit
8999cdbab3
|
|
@ -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 ~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/<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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
Loading…
Reference in New Issue