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,180 +1,154 @@
|
||||||
# InnungsApp
|
# 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
|
## Stack
|
||||||
|
|
||||||
| Schicht | Technologie |
|
| Layer | Technology |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Monorepo** | pnpm Workspaces + Turborepo |
|
| Monorepo | pnpm Workspaces + Turborepo |
|
||||||
| **Mobile App** | Expo (React Native) + Expo Router |
|
| Admin Dashboard | Next.js 15 (App Router) |
|
||||||
| **Admin Dashboard** | Next.js 15 (App Router) |
|
| Mobile App | Expo + React Native |
|
||||||
| **API** | tRPC v11 |
|
| API | tRPC v11 |
|
||||||
| **Auth** | better-auth (Magic Links) |
|
| Auth | better-auth (magic links + credential login) |
|
||||||
| **Datenbank** | PostgreSQL + Prisma ORM |
|
| Database | SQLite + Prisma ORM |
|
||||||
| **Styling Mobile** | NativeWind v4 (Tailwind CSS) |
|
| Styling | Tailwind CSS (admin), NativeWind (mobile) |
|
||||||
| **Styling Admin** | Tailwind CSS |
|
|
||||||
| **State Management** | Zustand (Mobile) + React Query (beide Apps) |
|
|
||||||
|
|
||||||
## Projekt-Struktur
|
## Projektstruktur
|
||||||
|
|
||||||
```
|
```text
|
||||||
innungsapp/
|
innungsapp/
|
||||||
├── apps/
|
|-- apps/
|
||||||
│ ├── mobile/ # Expo React Native App (iOS + Android)
|
| |-- admin/
|
||||||
│ └── admin/ # Next.js Admin Dashboard
|
| `-- mobile/
|
||||||
├── packages/
|
|-- packages/
|
||||||
│ └── shared/ # TypeScript-Typen + Prisma Client
|
| `-- shared/
|
||||||
└── ...
|
| `-- prisma/
|
||||||
|
|-- docker-compose.yml
|
||||||
|
`-- README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## Setup
|
## Local Setup
|
||||||
|
|
||||||
### Voraussetzungen
|
### Voraussetzungen
|
||||||
|
|
||||||
- Node.js >= 20
|
- Node.js >= 20
|
||||||
- pnpm >= 9
|
- pnpm >= 9
|
||||||
- PostgreSQL-Datenbank
|
- SMTP-Zugang (fuer Einladungen und Magic Links)
|
||||||
- SMTP-Server (für Magic Links)
|
|
||||||
|
|
||||||
### 1. Abhängigkeiten installieren
|
### 1. Abhaengigkeiten installieren
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm install
|
pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Umgebungsvariablen
|
### 2. Umgebungsvariablen setzen (Admin lokal)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example apps/admin/.env.local
|
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
|
```bash
|
||||||
# Prisma Client generieren
|
|
||||||
pnpm db:generate
|
pnpm db:generate
|
||||||
|
pnpm db:push
|
||||||
|
```
|
||||||
|
|
||||||
# Migrationen anwenden
|
Optional Demo-Daten:
|
||||||
pnpm db:migrate
|
|
||||||
|
|
||||||
# Demo-Daten einspielen (optional)
|
```bash
|
||||||
pnpm db:seed
|
pnpm db:seed
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Entwicklung starten
|
### 4. Entwicklung starten
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Admin Dashboard (http://localhost:3000)
|
|
||||||
pnpm --filter @innungsapp/admin dev
|
pnpm --filter @innungsapp/admin dev
|
||||||
|
|
||||||
# Mobile App (Expo DevTools)
|
|
||||||
pnpm --filter @innungsapp/mobile dev
|
pnpm --filter @innungsapp/mobile dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Oder alles parallel:
|
Oder parallel:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dev
|
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:
|
### Voraussetzungen
|
||||||
- `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)
|
|
||||||
|
|
||||||
## 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
|
### 1. Repository klonen
|
||||||
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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone <repo-url>
|
git clone <repo-url>
|
||||||
cd innungsapp
|
cd innungsapp
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Schritt 2: Umgebungsvariablen anlegen
|
### 2. Production-Env anlegen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.production.example .env
|
cp .env.production.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
Dann `.env` öffnen und **alle Werte** befüllen:
|
Pflichtwerte in `.env`:
|
||||||
|
|
||||||
| Variable | Beschreibung |
|
- `BETTER_AUTH_SECRET` (mindestens 32 Zeichen)
|
||||||
|---|---|
|
- `BETTER_AUTH_URL` (z. B. `https://app.deine-innung.de`)
|
||||||
| `BETTER_AUTH_SECRET` | Zufälliger String (min. 32 Zeichen) — z.B. `openssl rand -hex 32` |
|
- `NEXT_PUBLIC_APP_URL` (gleich wie `BETTER_AUTH_URL`)
|
||||||
| `BETTER_AUTH_URL` | Öffentliche URL der App, z.B. `https://app.deine-innung.de` |
|
- `EMAIL_FROM`
|
||||||
| `NEXT_PUBLIC_APP_URL` | Gleicher Wert wie `BETTER_AUTH_URL` |
|
- `SMTP_HOST`
|
||||||
| `EMAIL_FROM` | Absender-Adresse für Magic Links |
|
- `SMTP_PORT`
|
||||||
| `SMTP_HOST` | SMTP-Server-Adresse |
|
- `SMTP_SECURE`
|
||||||
| `SMTP_PORT` | Meistens `587` (STARTTLS) oder `465` (SSL) |
|
- `SMTP_USER`
|
||||||
| `SMTP_USER` | SMTP-Benutzername |
|
- `SMTP_PASS`
|
||||||
| `SMTP_PASS` | SMTP-Passwort |
|
|
||||||
|
|
||||||
#### Schritt 3: Container bauen und starten
|
### 3. Container bauen und starten
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d --build
|
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
|
```bash
|
||||||
docker compose logs -f admin
|
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
|
```bash
|
||||||
docker compose exec admin node -e "
|
docker compose exec -w /app admin node packages/shared/prisma/seed-superadmin.js
|
||||||
const { PrismaClient } = require('@prisma/client');
|
|
||||||
const { scryptSync, randomBytes } = require('crypto');
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
// Superadmin wird via seed-superadmin.ts angelegt
|
|
||||||
"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Einfacher: Den Seed direkt ausführen:
|
Default Login:
|
||||||
|
|
||||||
```bash
|
- E-Mail: `superadmin@innungsapp.de`
|
||||||
docker compose exec -w /app admin \
|
- Passwort: `demo1234`
|
||||||
node packages/shared/prisma/seed-superadmin.js
|
|
||||||
```
|
|
||||||
|
|
||||||
> Standard-Login nach Seed: `superadmin@innungsapp.de` / `demo1234`
|
Passwort direkt nach dem ersten Login aendern.
|
||||||
> **Passwort sofort in den Einstellungen ändern!**
|
|
||||||
|
|
||||||
#### 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
|
```nginx
|
||||||
server {
|
server {
|
||||||
|
|
@ -190,70 +164,56 @@ server {
|
||||||
ssl_certificate /etc/letsencrypt/live/app.deine-innung.de/fullchain.pem;
|
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_key /etc/letsencrypt/live/app.deine-innung.de/privkey.pem;
|
||||||
|
|
||||||
client_max_body_size 20M;
|
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://localhost:3000;
|
proxy_pass http://localhost:3000;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
SSL-Zertifikat mit Certbot:
|
### 7. Updates einspielen
|
||||||
```bash
|
|
||||||
certbot --nginx -d app.deine-innung.de
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Updates einspielen
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git pull
|
git pull
|
||||||
docker compose up -d --build
|
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
|
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
|
```bash
|
||||||
# Umgebungsvariablen in Vercel setzen:
|
docker volume ls | grep db_data
|
||||||
# DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL, SMTP_*
|
docker volume ls | grep uploads_data
|
||||||
|
|
||||||
vercel --cwd apps/admin
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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
|
```bash
|
||||||
cd apps/mobile
|
cd apps/mobile
|
||||||
|
|
@ -261,33 +221,32 @@ eas build --platform all --profile production
|
||||||
eas submit --platform all
|
eas submit --platform all
|
||||||
```
|
```
|
||||||
|
|
||||||
## DSGVO / AVV
|
Wichtig:
|
||||||
|
|
||||||
- AVV-Akzeptanz in Admin → Einstellungen (Pflichtfeld vor Go-Live)
|
- In `apps/mobile/eas.json` sind Submit-Placeholders vorhanden und muessen ersetzt werden.
|
||||||
- Alle personenbezogenen Daten in EU-Region (Datenbankserver in Deutschland empfohlen)
|
- Fuer Production darf keine API-URL auf `localhost` zeigen.
|
||||||
- Keine Daten an Dritte außer Expo Push API (anonymisierte Token)
|
|
||||||
|
|
||||||
## 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:**
|
- Containerstatus: `docker compose ps`
|
||||||
```bash
|
- App-Logs lesen
|
||||||
pnpm --filter @innungsapp/admin dev
|
- Reverse Proxy testweise umgehen und direkt `localhost:3000` pruefen
|
||||||
```
|
|
||||||
Das Dashboard ist im Browser unter [http://localhost:3000](http://localhost:3000) erreichbar.
|
|
||||||
|
|
||||||
**Mobile App starten:**
|
### Login funktioniert nicht nach Seed
|
||||||
```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.
|
|
||||||
|
|
||||||
**Beides gleichzeitig starten:**
|
- Seed-Command erneut ausfuehren
|
||||||
```bash
|
- In DB pruefen, ob `user` und `account` Eintraege fuer `superadmin@innungsapp.de` existieren
|
||||||
pnpm dev
|
|
||||||
```
|
## Weiterfuehrende Doku
|
||||||
|
|
||||||
|
- Produkt-Roadmap: `../ROADMAP.md`
|
||||||
|
- Architektur: `../ARCHITECTURE.md`
|
||||||
|
- API Design: `../API_DESIGN.md`
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,19 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Run Prisma migrations on startup
|
# Keep DATABASE_URL consistent for every Prisma command
|
||||||
echo "Running database migrations..."
|
export DATABASE_URL="${DATABASE_URL:-file:/app/data/prod.db}"
|
||||||
DATABASE_URL="${DATABASE_URL:-file:/app/data/prod.db}" \
|
MIGRATIONS_DIR="./packages/shared/prisma/migrations"
|
||||||
node_modules/.bin/prisma migrate deploy \
|
|
||||||
--schema=./packages/shared/prisma/schema.prisma
|
# 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..."
|
echo "Starting Next.js server..."
|
||||||
exec node apps/admin/server.js
|
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