feat: Datenmodell für Katalog, Tracking und Universen
Prisma-Schema in vier Schichten: Konten und Zugang, gemeinsamer Katalog (über die TMDB-ID dedupliziert), persönliches Tracking je Nutzer sowie Universen mit mehreren Reihenfolgen. Dazu Benachrichtigungskanäle und Protokoll der Hintergrundläufe. Prisma 7 verlangt einen Treiber-Adapter statt der URL im Schema; die Konfiguration liegt deshalb in prisma.config.mjs — bewusst als .mjs, damit die CLI auch im schlanken Produktionsimage ohne TypeScript-Loader läuft.
This commit is contained in:
@@ -0,0 +1,78 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Flimmer — Beispielkonfiguration
|
||||||
|
#
|
||||||
|
# cp .env.example .env
|
||||||
|
#
|
||||||
|
# Alles, was Flimmer über die Außenwelt wissen muss, steht hier. Die Datei .env
|
||||||
|
# gehört NICHT ins Repository (siehe .gitignore).
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# --- Datenbank ---------------------------------------------------------------
|
||||||
|
# Im Docker-Compose-Betrieb zeigt der Host auf den Dienst "db".
|
||||||
|
POSTGRES_USER=flimmer
|
||||||
|
POSTGRES_PASSWORD=bitte-aendern
|
||||||
|
POSTGRES_DB=flimmer
|
||||||
|
DATABASE_URL=postgresql://flimmer:bitte-aendern@db:5432/flimmer?schema=public
|
||||||
|
|
||||||
|
# --- Grundeinstellungen ------------------------------------------------------
|
||||||
|
# Öffentlich erreichbare Adresse der Instanz (ohne abschließenden Schrägstrich).
|
||||||
|
# Wird für Links in Benachrichtigungen und für OIDC-Rücksprünge gebraucht.
|
||||||
|
APP_URL=http://localhost:3000
|
||||||
|
APP_NAME=Flimmer
|
||||||
|
|
||||||
|
# Zufälliger Wert, mindestens 32 Zeichen. Erzeugen z. B. mit:
|
||||||
|
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||||
|
AUTH_SECRET=bitte-durch-zufallswert-ersetzen
|
||||||
|
|
||||||
|
# Ablage für Bild-Cache und sonstige Laufzeitdaten.
|
||||||
|
DATA_DIR=/app/data
|
||||||
|
|
||||||
|
# Zeitzone für Kalender, Statistiken und geplante Läufe
|
||||||
|
TZ=Europe/Berlin
|
||||||
|
|
||||||
|
# --- Registrierung -----------------------------------------------------------
|
||||||
|
# Der erste registrierte Nutzer wird automatisch Admin. Danach lässt sich die
|
||||||
|
# Registrierung im Adminbereich abschalten; dieser Wert ist nur die Vorgabe
|
||||||
|
# für die allererste Inbetriebnahme.
|
||||||
|
ALLOW_REGISTRATION=true
|
||||||
|
|
||||||
|
# --- TMDB --------------------------------------------------------------------
|
||||||
|
# Kostenloser Schlüssel: https://www.themoviedb.org/settings/api
|
||||||
|
# Entweder der klassische API-Key (v3) …
|
||||||
|
TMDB_API_KEY=
|
||||||
|
# … oder das Bearer-Token (v4). Eines von beidem genügt.
|
||||||
|
TMDB_ACCESS_TOKEN=
|
||||||
|
# Sprache der Metadaten und Region für die Streaming-Verfügbarkeit
|
||||||
|
TMDB_LANGUAGE=de-DE
|
||||||
|
TMDB_REGION=DE
|
||||||
|
|
||||||
|
# --- Hintergrundläufe --------------------------------------------------------
|
||||||
|
# Eingebauter Scheduler (prüft laufende Serien auf neue Episoden, räumt den
|
||||||
|
# Papierkorb, verschickt Benachrichtigungen). Bei mehreren App-Instanzen nur
|
||||||
|
# in einer aktivieren.
|
||||||
|
ENABLE_SCHEDULER=true
|
||||||
|
# Uhrzeit des täglichen Laufs (24-Stunden-Format)
|
||||||
|
SYNC_HOUR=4
|
||||||
|
# Alternativ von außen anstoßen: curl -H "Authorization: Bearer $CRON_SECRET" \
|
||||||
|
# https://flimmer.example/api/cron/sync
|
||||||
|
CRON_SECRET=
|
||||||
|
|
||||||
|
# --- E-Mail (optional) -------------------------------------------------------
|
||||||
|
SMTP_HOST=
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USER=
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
SMTP_SECURE=false
|
||||||
|
SMTP_FROM=Flimmer <flimmer@example.com>
|
||||||
|
|
||||||
|
# --- OIDC / SSO (optional) ---------------------------------------------------
|
||||||
|
# Anbindung an Authelia, Keycloak, Authentik … Leer lassen = deaktiviert.
|
||||||
|
# Redirect-URI beim Anbieter eintragen: <APP_URL>/api/auth/oidc/callback
|
||||||
|
OIDC_ISSUER=
|
||||||
|
OIDC_CLIENT_ID=
|
||||||
|
OIDC_CLIENT_SECRET=
|
||||||
|
OIDC_SCOPES=openid profile email
|
||||||
|
# Beschriftung des Login-Knopfes
|
||||||
|
OIDC_BUTTON_LABEL=Mit SSO anmelden
|
||||||
|
# Neue Nutzer per OIDC automatisch anlegen
|
||||||
|
OIDC_AUTO_REGISTER=true
|
||||||
@@ -41,3 +41,4 @@ Thumbs.db
|
|||||||
/playwright-report/
|
/playwright-report/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
.playwright/
|
.playwright/
|
||||||
|
src/generated/
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Nur die Datenbank — die App läuft bei der Entwicklung direkt über `npm run dev`.
|
||||||
|
# docker compose -f docker-compose.dev.yml up -d
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
container_name: flimmer-dev-db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: flimmer
|
||||||
|
POSTGRES_PASSWORD: flimmer-dev
|
||||||
|
POSTGRES_DB: flimmer
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:5439:5432"
|
||||||
|
volumes:
|
||||||
|
- flimmer-dev-db:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U flimmer -d flimmer"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
flimmer-dev-db:
|
||||||
Generated
+197
-3
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@prisma/adapter-pg": "^7.10.0",
|
||||||
"@prisma/client": "7.10.0",
|
"@prisma/client": "7.10.0",
|
||||||
"bcryptjs": "3.0.3",
|
"bcryptjs": "3.0.3",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
@@ -16,8 +17,10 @@
|
|||||||
"jose": "6.2.10",
|
"jose": "6.2.10",
|
||||||
"next": "16.3.4",
|
"next": "16.3.4",
|
||||||
"nodemailer": "9.1.0",
|
"nodemailer": "9.1.0",
|
||||||
|
"pg": "^8.23.0",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8",
|
"react-dom": "19.2.8",
|
||||||
|
"server-only": "^0.0.1",
|
||||||
"zod": "4.5.4"
|
"zod": "4.5.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -25,6 +28,7 @@
|
|||||||
"@types/bcryptjs": "3.0.0",
|
"@types/bcryptjs": "3.0.0",
|
||||||
"@types/node": "24.10.1",
|
"@types/node": "24.10.1",
|
||||||
"@types/nodemailer": "8.0.1",
|
"@types/nodemailer": "8.0.1",
|
||||||
|
"@types/pg": "^8.23.1",
|
||||||
"@types/react": "19.2.18",
|
"@types/react": "19.2.18",
|
||||||
"@types/react-dom": "19.2.5",
|
"@types/react-dom": "19.2.5",
|
||||||
"playwright": "1.62.1",
|
"playwright": "1.62.1",
|
||||||
@@ -1278,6 +1282,18 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@prisma/adapter-pg": {
|
||||||
|
"version": "7.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.10.0.tgz",
|
||||||
|
"integrity": "sha512-N7nwSor0HO1Kz6xBv0TPAjAPysKK0fac6p4fVN3ensLOuzc/83Fgmln5k92eK/cvzqdkSR/2kkAqlbcdwVrwpw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/driver-adapter-utils": "7.10.0",
|
||||||
|
"@types/pg": "^8.16.0",
|
||||||
|
"pg": "^8.16.3",
|
||||||
|
"postgres-array": "3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@prisma/client": {
|
"node_modules/@prisma/client": {
|
||||||
"version": "7.10.0",
|
"version": "7.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.10.0.tgz",
|
||||||
@@ -1325,7 +1341,6 @@
|
|||||||
"version": "7.10.0",
|
"version": "7.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.10.0.tgz",
|
||||||
"integrity": "sha512-caygJKtltmRIgdJ3jRpkOr7yM4DW6zxo5uOmojKWFb3asnxWoRkQOwZmXBgD8FZp4htrX+nMpcWqDwzlQ1+Y4g==",
|
"integrity": "sha512-caygJKtltmRIgdJ3jRpkOr7yM4DW6zxo5uOmojKWFb3asnxWoRkQOwZmXBgD8FZp4htrX+nMpcWqDwzlQ1+Y4g==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/dev": {
|
"node_modules/@prisma/dev": {
|
||||||
@@ -1352,6 +1367,15 @@
|
|||||||
"zeptomatch": "2.1.0"
|
"zeptomatch": "2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@prisma/driver-adapter-utils": {
|
||||||
|
"version": "7.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.10.0.tgz",
|
||||||
|
"integrity": "sha512-u8zkcRLlaryO652T4qavBg0HmzNW5tSKdsCn6hc1PhWAp/J6k0vrxLuUs+b9o+HcjsK7Dfa01o4OFSn0frauJA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/debug": "7.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@prisma/engines": {
|
"node_modules/@prisma/engines": {
|
||||||
"version": "7.10.0",
|
"version": "7.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.10.0.tgz",
|
||||||
@@ -2039,7 +2063,6 @@
|
|||||||
"version": "24.10.1",
|
"version": "24.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
||||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
@@ -2055,6 +2078,17 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pg": {
|
||||||
|
"version": "8.23.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz",
|
||||||
|
"integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"pg-protocol": "*",
|
||||||
|
"pg-types": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.2.18",
|
"version": "19.2.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
|
||||||
@@ -3748,6 +3782,104 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pg": {
|
||||||
|
"version": "8.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz",
|
||||||
|
"integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pg-connection-string": "^2.14.0",
|
||||||
|
"pg-pool": "^3.14.0",
|
||||||
|
"pg-protocol": "^1.16.0",
|
||||||
|
"pg-types": "2.2.0",
|
||||||
|
"pgpass": "1.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"pg-cloudflare": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"pg-native": ">=3.0.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"pg-native": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg-cloudflare": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/pg-connection-string": {
|
||||||
|
"version": "2.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz",
|
||||||
|
"integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/pg-int8": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg-pool": {
|
||||||
|
"version": "3.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
|
||||||
|
"integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"pg": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg-protocol": {
|
||||||
|
"version": "1.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz",
|
||||||
|
"integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/pg-types": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pg-int8": "1.0.1",
|
||||||
|
"postgres-array": "~2.0.0",
|
||||||
|
"postgres-bytea": "~1.0.0",
|
||||||
|
"postgres-date": "~1.0.4",
|
||||||
|
"postgres-interval": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg-types/node_modules/postgres-array": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pgpass": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"split2": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -3841,6 +3973,45 @@
|
|||||||
"url": "https://github.com/sponsors/porsager"
|
"url": "https://github.com/sponsors/porsager"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postgres-array": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postgres-bytea": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postgres-date": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postgres-interval": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"xtend": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prisma": {
|
"node_modules/prisma": {
|
||||||
"version": "7.10.0",
|
"version": "7.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-7.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-7.10.0.tgz",
|
||||||
@@ -4059,6 +4230,12 @@
|
|||||||
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==",
|
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/server-only": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/sharp": {
|
"node_modules/sharp": {
|
||||||
"version": "0.35.4",
|
"version": "0.35.4",
|
||||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz",
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz",
|
||||||
@@ -4154,6 +4331,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/split2": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sqlstring": {
|
"node_modules/sqlstring": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
|
||||||
@@ -4294,7 +4480,6 @@
|
|||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/valibot": {
|
"node_modules/valibot": {
|
||||||
@@ -4328,6 +4513,15 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xtend": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zeptomatch": {
|
"node_modules/zeptomatch": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"screenshots": "tsx scripts/screenshots.ts"
|
"screenshots": "tsx scripts/screenshots.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@prisma/adapter-pg": "^7.10.0",
|
||||||
"@prisma/client": "7.10.0",
|
"@prisma/client": "7.10.0",
|
||||||
"bcryptjs": "3.0.3",
|
"bcryptjs": "3.0.3",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
@@ -25,8 +26,10 @@
|
|||||||
"jose": "6.2.10",
|
"jose": "6.2.10",
|
||||||
"next": "16.3.4",
|
"next": "16.3.4",
|
||||||
"nodemailer": "9.1.0",
|
"nodemailer": "9.1.0",
|
||||||
|
"pg": "^8.23.0",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8",
|
"react-dom": "19.2.8",
|
||||||
|
"server-only": "^0.0.1",
|
||||||
"zod": "4.5.4"
|
"zod": "4.5.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -34,6 +37,7 @@
|
|||||||
"@types/bcryptjs": "3.0.0",
|
"@types/bcryptjs": "3.0.0",
|
||||||
"@types/node": "24.10.1",
|
"@types/node": "24.10.1",
|
||||||
"@types/nodemailer": "8.0.1",
|
"@types/nodemailer": "8.0.1",
|
||||||
|
"@types/pg": "^8.23.1",
|
||||||
"@types/react": "19.2.18",
|
"@types/react": "19.2.18",
|
||||||
"@types/react-dom": "19.2.5",
|
"@types/react-dom": "19.2.5",
|
||||||
"playwright": "1.62.1",
|
"playwright": "1.62.1",
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { defineConfig } from "prisma/config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prisma-Konfiguration.
|
||||||
|
*
|
||||||
|
* Bewusst als .mjs und nicht als .ts: die Prisma-CLI läuft auch im schlanken
|
||||||
|
* Produktionsimage (Migrationen beim Containerstart), und dort ist kein
|
||||||
|
* TypeScript-Loader vorhanden.
|
||||||
|
*
|
||||||
|
* Prisma 7 lädt .env nicht mehr von selbst — Node bringt das aber mit.
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
process.loadEnvFile(".env");
|
||||||
|
} catch {
|
||||||
|
// Keine .env vorhanden (z. B. im Container): Werte kommen von außen.
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: "prisma/schema.prisma",
|
||||||
|
datasource: {
|
||||||
|
url: process.env.DATABASE_URL,
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
seed: "tsx prisma/seed.ts",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,553 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "Role" AS ENUM ('ADMIN', 'USER');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "TitleType" AS ENUM ('SERIES', 'MOVIE');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "ProviderKind" AS ENUM ('FLATRATE', 'RENT', 'BUY', 'ADS', 'FREE');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "EntryStatus" AS ENUM ('WATCHING', 'PAUSED', 'COMPLETED', 'DROPPED', 'WATCHLIST');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "SharePermission" AS ENUM ('READ', 'WRITE');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "OrderKind" AS ENUM ('RELEASE', 'CUSTOM');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "ChannelKind" AS ENUM ('EMAIL', 'NTFY', 'GOTIFY', 'WEBHOOK');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"passwordHash" TEXT,
|
||||||
|
"role" "Role" NOT NULL DEFAULT 'USER',
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"region" TEXT NOT NULL DEFAULT 'DE',
|
||||||
|
"locale" TEXT NOT NULL DEFAULT 'de',
|
||||||
|
"publicProfile" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"profileSlug" TEXT,
|
||||||
|
"notifyNewEpisode" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"notifyRelease" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"notifyAvailability" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Session" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"tokenHash" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"userAgent" TEXT,
|
||||||
|
"ip" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"lastSeen" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ApiKey" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"prefix" TEXT NOT NULL,
|
||||||
|
"keyHash" TEXT NOT NULL,
|
||||||
|
"lastUsedAt" TIMESTAMP(3),
|
||||||
|
"revokedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "ApiKey_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "OidcAccount" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"issuer" TEXT NOT NULL,
|
||||||
|
"subject" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "OidcAccount_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "AppSetting" (
|
||||||
|
"key" TEXT NOT NULL,
|
||||||
|
"value" JSONB NOT NULL,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "AppSetting_pkey" PRIMARY KEY ("key")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Title" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"type" "TitleType" NOT NULL,
|
||||||
|
"tmdbId" INTEGER,
|
||||||
|
"imdbId" TEXT,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"originalTitle" TEXT,
|
||||||
|
"overview" TEXT,
|
||||||
|
"tagline" TEXT,
|
||||||
|
"posterPath" TEXT,
|
||||||
|
"backdropPath" TEXT,
|
||||||
|
"releaseDate" TIMESTAMP(3),
|
||||||
|
"lastAirDate" TIMESTAMP(3),
|
||||||
|
"runtime" INTEGER,
|
||||||
|
"status" TEXT,
|
||||||
|
"inProduction" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"genres" TEXT[],
|
||||||
|
"originalLang" TEXT,
|
||||||
|
"homepage" TEXT,
|
||||||
|
"voteAverage" DOUBLE PRECISION,
|
||||||
|
"seasonCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"episodeCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"tmdbCollectionId" INTEGER,
|
||||||
|
"tmdbCollectionName" TEXT,
|
||||||
|
"syncedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Title_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Season" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"titleId" TEXT NOT NULL,
|
||||||
|
"tmdbId" INTEGER,
|
||||||
|
"seasonNumber" INTEGER NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"overview" TEXT,
|
||||||
|
"posterPath" TEXT,
|
||||||
|
"airDate" TIMESTAMP(3),
|
||||||
|
"episodeCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
CONSTRAINT "Season_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Episode" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"titleId" TEXT NOT NULL,
|
||||||
|
"seasonId" TEXT NOT NULL,
|
||||||
|
"tmdbId" INTEGER,
|
||||||
|
"seasonNumber" INTEGER NOT NULL,
|
||||||
|
"episodeNumber" INTEGER NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"overview" TEXT,
|
||||||
|
"stillPath" TEXT,
|
||||||
|
"airDate" TIMESTAMP(3),
|
||||||
|
"runtime" INTEGER,
|
||||||
|
|
||||||
|
CONSTRAINT "Episode_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "WatchProvider" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"tmdbId" INTEGER NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"logoPath" TEXT,
|
||||||
|
"color" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "WatchProvider_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "TitleProvider" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"titleId" TEXT NOT NULL,
|
||||||
|
"providerId" TEXT NOT NULL,
|
||||||
|
"region" TEXT NOT NULL,
|
||||||
|
"kind" "ProviderKind" NOT NULL,
|
||||||
|
"firstSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "TitleProvider_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "LibraryEntry" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"titleId" TEXT NOT NULL,
|
||||||
|
"status" "EntryStatus" NOT NULL DEFAULT 'WATCHLIST',
|
||||||
|
"rating" INTEGER,
|
||||||
|
"notes" TEXT,
|
||||||
|
"whereToWatch" TEXT,
|
||||||
|
"favorite" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"currentRun" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"startedAt" TIMESTAMP(3),
|
||||||
|
"finishedAt" TIMESTAMP(3),
|
||||||
|
"deletedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "LibraryEntry_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Tag" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"color" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "Tag_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "TagOnEntry" (
|
||||||
|
"tagId" TEXT NOT NULL,
|
||||||
|
"entryId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "TagOnEntry_pkey" PRIMARY KEY ("tagId","entryId")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Watch" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"entryId" TEXT,
|
||||||
|
"titleId" TEXT NOT NULL,
|
||||||
|
"episodeId" TEXT,
|
||||||
|
"run" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"watchedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"minutes" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "Watch_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Universe" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"ownerId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"slug" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"coverPath" TEXT,
|
||||||
|
"deletedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Universe_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "UniverseMember" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"universeId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"permission" "SharePermission" NOT NULL DEFAULT 'READ',
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "UniverseMember_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "UniverseItem" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"universeId" TEXT NOT NULL,
|
||||||
|
"titleId" TEXT NOT NULL,
|
||||||
|
"note" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "UniverseItem_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "UniverseOrder" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"universeId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"kind" "OrderKind" NOT NULL DEFAULT 'CUSTOM',
|
||||||
|
"isDefault" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "UniverseOrder_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "UniverseOrderItem" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"orderId" TEXT NOT NULL,
|
||||||
|
"itemId" TEXT NOT NULL,
|
||||||
|
"position" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "UniverseOrderItem_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "NotificationChannel" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"kind" "ChannelKind" NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"config" JSONB NOT NULL,
|
||||||
|
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "NotificationChannel_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "NotificationLog" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"dedupeKey" TEXT NOT NULL,
|
||||||
|
"subject" TEXT NOT NULL,
|
||||||
|
"body" TEXT NOT NULL,
|
||||||
|
"sentAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"ok" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"error" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "NotificationLog_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "SyncRun" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"kind" TEXT NOT NULL,
|
||||||
|
"startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"finishedAt" TIMESTAMP(3),
|
||||||
|
"ok" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"message" TEXT,
|
||||||
|
"stats" JSONB,
|
||||||
|
|
||||||
|
CONSTRAINT "SyncRun_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_profileSlug_key" ON "User"("profileSlug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "User_createdAt_idx" ON "User"("createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Session_tokenHash_key" ON "Session"("tokenHash");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Session_expiresAt_idx" ON "Session"("expiresAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ApiKey_prefix_key" ON "ApiKey"("prefix");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ApiKey_keyHash_key" ON "ApiKey"("keyHash");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ApiKey_userId_idx" ON "ApiKey"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "OidcAccount_userId_idx" ON "OidcAccount"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "OidcAccount_issuer_subject_key" ON "OidcAccount"("issuer", "subject");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Title_type_idx" ON "Title"("type");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Title_releaseDate_idx" ON "Title"("releaseDate");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Title_title_idx" ON "Title"("title");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Title_tmdbCollectionId_idx" ON "Title"("tmdbCollectionId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Title_type_tmdbId_key" ON "Title"("type", "tmdbId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Season_titleId_idx" ON "Season"("titleId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Season_titleId_seasonNumber_key" ON "Season"("titleId", "seasonNumber");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Episode_titleId_idx" ON "Episode"("titleId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Episode_seasonId_idx" ON "Episode"("seasonId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Episode_airDate_idx" ON "Episode"("airDate");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Episode_titleId_seasonNumber_episodeNumber_key" ON "Episode"("titleId", "seasonNumber", "episodeNumber");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "WatchProvider_tmdbId_key" ON "WatchProvider"("tmdbId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "TitleProvider_titleId_region_idx" ON "TitleProvider"("titleId", "region");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "TitleProvider_titleId_providerId_region_kind_key" ON "TitleProvider"("titleId", "providerId", "region", "kind");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "LibraryEntry_userId_status_idx" ON "LibraryEntry"("userId", "status");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "LibraryEntry_userId_updatedAt_idx" ON "LibraryEntry"("userId", "updatedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "LibraryEntry_deletedAt_idx" ON "LibraryEntry"("deletedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "LibraryEntry_userId_titleId_key" ON "LibraryEntry"("userId", "titleId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Tag_userId_name_key" ON "Tag"("userId", "name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "TagOnEntry_entryId_idx" ON "TagOnEntry"("entryId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Watch_userId_watchedAt_idx" ON "Watch"("userId", "watchedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Watch_userId_titleId_idx" ON "Watch"("userId", "titleId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Watch_entryId_idx" ON "Watch"("entryId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Watch_userId_episodeId_run_key" ON "Watch"("userId", "episodeId", "run");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Universe_ownerId_idx" ON "Universe"("ownerId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Universe_ownerId_slug_key" ON "Universe"("ownerId", "slug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "UniverseMember_userId_idx" ON "UniverseMember"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "UniverseMember_universeId_userId_key" ON "UniverseMember"("universeId", "userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "UniverseItem_universeId_idx" ON "UniverseItem"("universeId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "UniverseItem_universeId_titleId_key" ON "UniverseItem"("universeId", "titleId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "UniverseOrder_universeId_idx" ON "UniverseOrder"("universeId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "UniverseOrderItem_orderId_position_idx" ON "UniverseOrderItem"("orderId", "position");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "UniverseOrderItem_orderId_itemId_key" ON "UniverseOrderItem"("orderId", "itemId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "NotificationChannel_userId_idx" ON "NotificationChannel"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "NotificationLog_userId_sentAt_idx" ON "NotificationLog"("userId", "sentAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "NotificationLog_userId_dedupeKey_key" ON "NotificationLog"("userId", "dedupeKey");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "SyncRun_kind_startedAt_idx" ON "SyncRun"("kind", "startedAt");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "OidcAccount" ADD CONSTRAINT "OidcAccount_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Season" ADD CONSTRAINT "Season_titleId_fkey" FOREIGN KEY ("titleId") REFERENCES "Title"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Episode" ADD CONSTRAINT "Episode_titleId_fkey" FOREIGN KEY ("titleId") REFERENCES "Title"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Episode" ADD CONSTRAINT "Episode_seasonId_fkey" FOREIGN KEY ("seasonId") REFERENCES "Season"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "TitleProvider" ADD CONSTRAINT "TitleProvider_titleId_fkey" FOREIGN KEY ("titleId") REFERENCES "Title"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "TitleProvider" ADD CONSTRAINT "TitleProvider_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "WatchProvider"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "LibraryEntry" ADD CONSTRAINT "LibraryEntry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "LibraryEntry" ADD CONSTRAINT "LibraryEntry_titleId_fkey" FOREIGN KEY ("titleId") REFERENCES "Title"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Tag" ADD CONSTRAINT "Tag_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "TagOnEntry" ADD CONSTRAINT "TagOnEntry_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "TagOnEntry" ADD CONSTRAINT "TagOnEntry_entryId_fkey" FOREIGN KEY ("entryId") REFERENCES "LibraryEntry"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Watch" ADD CONSTRAINT "Watch_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Watch" ADD CONSTRAINT "Watch_entryId_fkey" FOREIGN KEY ("entryId") REFERENCES "LibraryEntry"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Watch" ADD CONSTRAINT "Watch_titleId_fkey" FOREIGN KEY ("titleId") REFERENCES "Title"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Watch" ADD CONSTRAINT "Watch_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "Episode"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Universe" ADD CONSTRAINT "Universe_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UniverseMember" ADD CONSTRAINT "UniverseMember_universeId_fkey" FOREIGN KEY ("universeId") REFERENCES "Universe"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UniverseMember" ADD CONSTRAINT "UniverseMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UniverseItem" ADD CONSTRAINT "UniverseItem_universeId_fkey" FOREIGN KEY ("universeId") REFERENCES "Universe"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UniverseItem" ADD CONSTRAINT "UniverseItem_titleId_fkey" FOREIGN KEY ("titleId") REFERENCES "Title"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UniverseOrder" ADD CONSTRAINT "UniverseOrder_universeId_fkey" FOREIGN KEY ("universeId") REFERENCES "Universe"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UniverseOrderItem" ADD CONSTRAINT "UniverseOrderItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "UniverseOrder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UniverseOrderItem" ADD CONSTRAINT "UniverseOrderItem_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "UniverseItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "NotificationChannel" ADD CONSTRAINT "NotificationChannel_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "NotificationLog" ADD CONSTRAINT "NotificationLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (e.g., Git)
|
||||||
|
provider = "postgresql"
|
||||||
@@ -0,0 +1,510 @@
|
|||||||
|
// Flimmer — Datenmodell
|
||||||
|
//
|
||||||
|
// Aufbau in vier Schichten:
|
||||||
|
// 1. Konten & Zugang (User, Session, ApiKey, OidcAccount, AppSetting)
|
||||||
|
// 2. Katalog (Title, Season, Episode, WatchProvider …)
|
||||||
|
// — geteilt zwischen allen Nutzern, dedupliziert über die TMDB-ID
|
||||||
|
// 3. Persönliches Tracking (LibraryEntry, Watch, Tag …)
|
||||||
|
// 4. Universen (Universe, UniverseItem, UniverseOrder …)
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client"
|
||||||
|
output = "../src/generated/prisma"
|
||||||
|
runtime = "nodejs"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 1. Konten & Zugang
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
enum Role {
|
||||||
|
ADMIN
|
||||||
|
USER
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
email String @unique
|
||||||
|
name String
|
||||||
|
passwordHash String?
|
||||||
|
role Role @default(USER)
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
|
||||||
|
/// Region für die Streaming-Verfügbarkeit (ISO-3166-1, z. B. "DE")
|
||||||
|
region String @default("DE")
|
||||||
|
locale String @default("de")
|
||||||
|
|
||||||
|
/// Öffentliche Profilseite unter /u/<profileSlug>
|
||||||
|
publicProfile Boolean @default(false)
|
||||||
|
profileSlug String? @unique
|
||||||
|
|
||||||
|
// Benachrichtigungswünsche
|
||||||
|
notifyNewEpisode Boolean @default(true)
|
||||||
|
notifyRelease Boolean @default(true)
|
||||||
|
notifyAvailability Boolean @default(false)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
sessions Session[]
|
||||||
|
apiKeys ApiKey[]
|
||||||
|
oidcAccounts OidcAccount[]
|
||||||
|
entries LibraryEntry[]
|
||||||
|
watches Watch[]
|
||||||
|
tags Tag[]
|
||||||
|
universes Universe[] @relation("UniverseOwner")
|
||||||
|
memberships UniverseMember[]
|
||||||
|
channels NotificationChannel[]
|
||||||
|
notifications NotificationLog[]
|
||||||
|
|
||||||
|
@@index([createdAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Session {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
|
||||||
|
/// SHA-256 des Cookie-Tokens — das Klartext-Token verlässt den Server nie erneut
|
||||||
|
tokenHash String @unique
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
expiresAt DateTime
|
||||||
|
userAgent String?
|
||||||
|
ip String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
lastSeen DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@index([expiresAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
model ApiKey {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
name String
|
||||||
|
/// Sichtbares Präfix zur Wiedererkennung, z. B. "flm_a1b2c3d4"
|
||||||
|
prefix String @unique
|
||||||
|
keyHash String @unique
|
||||||
|
|
||||||
|
lastUsedAt DateTime?
|
||||||
|
revokedAt DateTime?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model OidcAccount {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
issuer String
|
||||||
|
subject String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([issuer, subject])
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Instanzweite Einstellungen (Registrierung offen/zu, Instanzname …)
|
||||||
|
model AppSetting {
|
||||||
|
key String @id
|
||||||
|
value Json
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 2. Katalog
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
enum TitleType {
|
||||||
|
SERIES
|
||||||
|
MOVIE
|
||||||
|
}
|
||||||
|
|
||||||
|
model Title {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
type TitleType
|
||||||
|
|
||||||
|
/// TMDB-ID; null bei manuell angelegten Titeln ohne TMDB-Treffer
|
||||||
|
tmdbId Int?
|
||||||
|
imdbId String?
|
||||||
|
|
||||||
|
title String
|
||||||
|
originalTitle String?
|
||||||
|
overview String?
|
||||||
|
tagline String?
|
||||||
|
posterPath String?
|
||||||
|
backdropPath String?
|
||||||
|
|
||||||
|
/// Erstausstrahlung (Serie) bzw. Kinostart (Film)
|
||||||
|
releaseDate DateTime?
|
||||||
|
lastAirDate DateTime?
|
||||||
|
|
||||||
|
/// Laufzeit in Minuten: bei Filmen die Filmlänge, bei Serien die
|
||||||
|
/// durchschnittliche Episodenlänge (Rückfallwert für Episoden ohne Angabe)
|
||||||
|
runtime Int?
|
||||||
|
|
||||||
|
/// TMDB-Status: "Returning Series", "Ended", "Released", "Planned" …
|
||||||
|
status String?
|
||||||
|
inProduction Boolean @default(false)
|
||||||
|
|
||||||
|
genres String[]
|
||||||
|
originalLang String?
|
||||||
|
homepage String?
|
||||||
|
voteAverage Float?
|
||||||
|
|
||||||
|
seasonCount Int @default(0)
|
||||||
|
episodeCount Int @default(0)
|
||||||
|
|
||||||
|
/// Offizielle TMDB-Collection (z. B. „Spider-Man Collection")
|
||||||
|
tmdbCollectionId Int?
|
||||||
|
tmdbCollectionName String?
|
||||||
|
|
||||||
|
/// Zeitpunkt des letzten TMDB-Abgleichs
|
||||||
|
syncedAt DateTime?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
seasons Season[]
|
||||||
|
episodes Episode[]
|
||||||
|
entries LibraryEntry[]
|
||||||
|
watches Watch[]
|
||||||
|
providers TitleProvider[]
|
||||||
|
universeItems UniverseItem[]
|
||||||
|
|
||||||
|
@@unique([type, tmdbId])
|
||||||
|
@@index([type])
|
||||||
|
@@index([releaseDate])
|
||||||
|
@@index([title])
|
||||||
|
@@index([tmdbCollectionId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Season {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
titleId String
|
||||||
|
title Title @relation(fields: [titleId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
tmdbId Int?
|
||||||
|
seasonNumber Int
|
||||||
|
name String
|
||||||
|
overview String?
|
||||||
|
posterPath String?
|
||||||
|
airDate DateTime?
|
||||||
|
episodeCount Int @default(0)
|
||||||
|
|
||||||
|
episodes Episode[]
|
||||||
|
|
||||||
|
@@unique([titleId, seasonNumber])
|
||||||
|
@@index([titleId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Episode {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
titleId String
|
||||||
|
title Title @relation(fields: [titleId], references: [id], onDelete: Cascade)
|
||||||
|
seasonId String
|
||||||
|
season Season @relation(fields: [seasonId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
tmdbId Int?
|
||||||
|
seasonNumber Int
|
||||||
|
episodeNumber Int
|
||||||
|
name String
|
||||||
|
overview String?
|
||||||
|
stillPath String?
|
||||||
|
airDate DateTime?
|
||||||
|
runtime Int?
|
||||||
|
|
||||||
|
watches Watch[]
|
||||||
|
|
||||||
|
@@unique([titleId, seasonNumber, episodeNumber])
|
||||||
|
@@index([titleId])
|
||||||
|
@@index([seasonId])
|
||||||
|
@@index([airDate])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Streamingdienst laut TMDB/JustWatch
|
||||||
|
model WatchProvider {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
tmdbId Int @unique
|
||||||
|
name String
|
||||||
|
logoPath String?
|
||||||
|
/// Akzentfarbe für Badges im UI (Hex); fällt sonst auf einen Standardton zurück
|
||||||
|
color String?
|
||||||
|
|
||||||
|
titles TitleProvider[]
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ProviderKind {
|
||||||
|
FLATRATE
|
||||||
|
RENT
|
||||||
|
BUY
|
||||||
|
ADS
|
||||||
|
FREE
|
||||||
|
}
|
||||||
|
|
||||||
|
model TitleProvider {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
titleId String
|
||||||
|
title Title @relation(fields: [titleId], references: [id], onDelete: Cascade)
|
||||||
|
providerId String
|
||||||
|
provider WatchProvider @relation(fields: [providerId], references: [id], onDelete: Cascade)
|
||||||
|
region String
|
||||||
|
kind ProviderKind
|
||||||
|
/// Für die Benachrichtigung „ist jetzt verfügbar"
|
||||||
|
firstSeenAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([titleId, providerId, region, kind])
|
||||||
|
@@index([titleId, region])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 3. Persönliches Tracking
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
enum EntryStatus {
|
||||||
|
WATCHING
|
||||||
|
PAUSED
|
||||||
|
COMPLETED
|
||||||
|
DROPPED
|
||||||
|
WATCHLIST
|
||||||
|
}
|
||||||
|
|
||||||
|
model LibraryEntry {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
titleId String
|
||||||
|
title Title @relation(fields: [titleId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
status EntryStatus @default(WATCHLIST)
|
||||||
|
|
||||||
|
/// Eigene Bewertung 1–10
|
||||||
|
rating Int?
|
||||||
|
notes String?
|
||||||
|
|
||||||
|
/// „Wo schaue ich das" — Vorschlag aus der Verfügbarkeit, überschreibbar
|
||||||
|
whereToWatch String?
|
||||||
|
|
||||||
|
favorite Boolean @default(false)
|
||||||
|
|
||||||
|
/// Aktueller Durchlauf: 0 = Erstsichtung, 1 = erster Rewatch …
|
||||||
|
currentRun Int @default(0)
|
||||||
|
|
||||||
|
startedAt DateTime?
|
||||||
|
finishedAt DateTime?
|
||||||
|
|
||||||
|
/// Papierkorb: 30 Tage aufbewahren statt hart löschen
|
||||||
|
deletedAt DateTime?
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
tags TagOnEntry[]
|
||||||
|
watches Watch[]
|
||||||
|
|
||||||
|
@@unique([userId, titleId])
|
||||||
|
@@index([userId, status])
|
||||||
|
@@index([userId, updatedAt])
|
||||||
|
@@index([deletedAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Tag {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
name String
|
||||||
|
color String?
|
||||||
|
|
||||||
|
entries TagOnEntry[]
|
||||||
|
|
||||||
|
@@unique([userId, name])
|
||||||
|
}
|
||||||
|
|
||||||
|
model TagOnEntry {
|
||||||
|
tagId String
|
||||||
|
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
||||||
|
entryId String
|
||||||
|
entry LibraryEntry @relation(fields: [entryId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@id([tagId, entryId])
|
||||||
|
@@index([entryId])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ein Sichtungsvorgang: entweder eine Episode oder ein Film.
|
||||||
|
/// Rewatches werden über `run` gezählt (0 = Erstsichtung).
|
||||||
|
model Watch {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
entryId String?
|
||||||
|
entry LibraryEntry? @relation(fields: [entryId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
|
titleId String
|
||||||
|
title Title @relation(fields: [titleId], references: [id], onDelete: Cascade)
|
||||||
|
episodeId String?
|
||||||
|
episode Episode? @relation(fields: [episodeId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
run Int @default(0)
|
||||||
|
watchedAt DateTime @default(now())
|
||||||
|
|
||||||
|
/// Tatsächlich angerechnete Minuten — friert die Laufzeit zum Zeitpunkt der
|
||||||
|
/// Sichtung ein, damit spätere TMDB-Korrekturen die Statistik nicht verbiegen.
|
||||||
|
minutes Int @default(0)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([userId, episodeId, run])
|
||||||
|
@@index([userId, watchedAt])
|
||||||
|
@@index([userId, titleId])
|
||||||
|
@@index([entryId])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 4. Universen
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
model Universe {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
ownerId String
|
||||||
|
owner User @relation("UniverseOwner", fields: [ownerId], references: [id], onDelete: Cascade)
|
||||||
|
name String
|
||||||
|
slug String
|
||||||
|
description String?
|
||||||
|
coverPath String?
|
||||||
|
|
||||||
|
deletedAt DateTime?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
items UniverseItem[]
|
||||||
|
orders UniverseOrder[]
|
||||||
|
members UniverseMember[]
|
||||||
|
|
||||||
|
@@unique([ownerId, slug])
|
||||||
|
@@index([ownerId])
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SharePermission {
|
||||||
|
READ
|
||||||
|
WRITE
|
||||||
|
}
|
||||||
|
|
||||||
|
model UniverseMember {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
universeId String
|
||||||
|
universe Universe @relation(fields: [universeId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
permission SharePermission @default(READ)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([universeId, userId])
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model UniverseItem {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
universeId String
|
||||||
|
universe Universe @relation(fields: [universeId], references: [id], onDelete: Cascade)
|
||||||
|
titleId String
|
||||||
|
title Title @relation(fields: [titleId], references: [id], onDelete: Cascade)
|
||||||
|
note String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
positions UniverseOrderItem[]
|
||||||
|
|
||||||
|
@@unique([universeId, titleId])
|
||||||
|
@@index([universeId])
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderKind {
|
||||||
|
/// Automatisch aus den Erscheinungsdaten berechnet
|
||||||
|
RELEASE
|
||||||
|
/// Frei sortierbar (Story-Chronologie, Einsteiger-Reihenfolge …)
|
||||||
|
CUSTOM
|
||||||
|
}
|
||||||
|
|
||||||
|
model UniverseOrder {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
universeId String
|
||||||
|
universe Universe @relation(fields: [universeId], references: [id], onDelete: Cascade)
|
||||||
|
name String
|
||||||
|
kind OrderKind @default(CUSTOM)
|
||||||
|
isDefault Boolean @default(false)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
items UniverseOrderItem[]
|
||||||
|
|
||||||
|
@@index([universeId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model UniverseOrderItem {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
orderId String
|
||||||
|
order UniverseOrder @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||||
|
itemId String
|
||||||
|
item UniverseItem @relation(fields: [itemId], references: [id], onDelete: Cascade)
|
||||||
|
position Int
|
||||||
|
|
||||||
|
@@unique([orderId, itemId])
|
||||||
|
@@index([orderId, position])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Benachrichtigungen & Hintergrundläufe
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
enum ChannelKind {
|
||||||
|
EMAIL
|
||||||
|
NTFY
|
||||||
|
GOTIFY
|
||||||
|
WEBHOOK
|
||||||
|
}
|
||||||
|
|
||||||
|
model NotificationChannel {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
kind ChannelKind
|
||||||
|
name String
|
||||||
|
/// Kanalspezifische Einstellungen (URL, Topic, Token …)
|
||||||
|
config Json
|
||||||
|
enabled Boolean @default(true)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model NotificationLog {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
/// Verhindert Doppelversand, z. B. "episode:<episodeId>"
|
||||||
|
dedupeKey String
|
||||||
|
subject String
|
||||||
|
body String
|
||||||
|
sentAt DateTime @default(now())
|
||||||
|
ok Boolean @default(true)
|
||||||
|
error String?
|
||||||
|
|
||||||
|
@@unique([userId, dedupeKey])
|
||||||
|
@@index([userId, sentAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
model SyncRun {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
kind String
|
||||||
|
startedAt DateTime @default(now())
|
||||||
|
finishedAt DateTime?
|
||||||
|
ok Boolean @default(false)
|
||||||
|
message String?
|
||||||
|
stats Json?
|
||||||
|
|
||||||
|
@@index([kind, startedAt])
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import "server-only";
|
||||||
|
import { PrismaPg } from "@prisma/adapter-pg";
|
||||||
|
import { PrismaClient } from "@/generated/prisma/client";
|
||||||
|
import { env } from "./env";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prisma-Client als Singleton.
|
||||||
|
*
|
||||||
|
* Im Entwicklungsmodus lädt Next Module bei jedem Hot-Reload neu; ohne das
|
||||||
|
* globale Zwischenspeichern entstünde pro Reload ein neuer Verbindungspool.
|
||||||
|
*/
|
||||||
|
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
|
||||||
|
|
||||||
|
function createClient() {
|
||||||
|
const adapter = new PrismaPg({ connectionString: env.databaseUrl });
|
||||||
|
return new PrismaClient({
|
||||||
|
adapter,
|
||||||
|
log: process.env.NODE_ENV === "development" ? ["warn", "error"] : ["error"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const prisma = globalForPrisma.prisma ?? createClient();
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
globalForPrisma.prisma = prisma;
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import "server-only";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zentraler, geprüfter Zugriff auf die Umgebungsvariablen.
|
||||||
|
*
|
||||||
|
* Absicht: Tippfehler und fehlende Pflichtwerte fallen beim Start auf, nicht
|
||||||
|
* erst irgendwo tief in einem Request. Optionale Bausteine (TMDB, SMTP, OIDC)
|
||||||
|
* melden sich stattdessen über `isConfigured`-Flags ab.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function required(name: string, fallback?: string): string {
|
||||||
|
const value = process.env[name] ?? fallback;
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(
|
||||||
|
`Umgebungsvariable ${name} fehlt. Bitte in der .env setzen — siehe .env.example.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function optional(name: string, fallback = ""): string {
|
||||||
|
return process.env[name]?.trim() ?? fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bool(name: string, fallback: boolean): boolean {
|
||||||
|
const raw = process.env[name]?.trim().toLowerCase();
|
||||||
|
if (raw === undefined || raw === "") return fallback;
|
||||||
|
return raw === "1" || raw === "true" || raw === "yes" || raw === "ja";
|
||||||
|
}
|
||||||
|
|
||||||
|
function int(name: string, fallback: number): number {
|
||||||
|
const raw = Number.parseInt(process.env[name] ?? "", 10);
|
||||||
|
return Number.isFinite(raw) ? raw : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBuildPhase = process.env.NEXT_PHASE === "phase-production-build";
|
||||||
|
|
||||||
|
export const env = {
|
||||||
|
appName: optional("APP_NAME", "Flimmer"),
|
||||||
|
appUrl: optional("APP_URL", "http://localhost:3000").replace(/\/+$/, ""),
|
||||||
|
databaseUrl: isBuildPhase
|
||||||
|
? optional("DATABASE_URL", "postgresql://build:build@localhost:5432/build")
|
||||||
|
: required("DATABASE_URL"),
|
||||||
|
authSecret: isBuildPhase ? optional("AUTH_SECRET", "build-time-placeholder") : required("AUTH_SECRET"),
|
||||||
|
dataDir: optional("DATA_DIR", "./data"),
|
||||||
|
timezone: optional("TZ", "Europe/Berlin"),
|
||||||
|
|
||||||
|
allowRegistrationDefault: bool("ALLOW_REGISTRATION", true),
|
||||||
|
|
||||||
|
tmdb: {
|
||||||
|
apiKey: optional("TMDB_API_KEY"),
|
||||||
|
accessToken: optional("TMDB_ACCESS_TOKEN"),
|
||||||
|
language: optional("TMDB_LANGUAGE", "de-DE"),
|
||||||
|
region: optional("TMDB_REGION", "DE"),
|
||||||
|
get isConfigured() {
|
||||||
|
return Boolean(this.apiKey || this.accessToken);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
scheduler: {
|
||||||
|
enabled: bool("ENABLE_SCHEDULER", false),
|
||||||
|
hour: Math.min(23, Math.max(0, int("SYNC_HOUR", 4))),
|
||||||
|
cronSecret: optional("CRON_SECRET"),
|
||||||
|
},
|
||||||
|
|
||||||
|
smtp: {
|
||||||
|
host: optional("SMTP_HOST"),
|
||||||
|
port: int("SMTP_PORT", 587),
|
||||||
|
user: optional("SMTP_USER"),
|
||||||
|
password: optional("SMTP_PASSWORD"),
|
||||||
|
secure: bool("SMTP_SECURE", false),
|
||||||
|
from: optional("SMTP_FROM", "Flimmer <flimmer@localhost>"),
|
||||||
|
get isConfigured() {
|
||||||
|
return Boolean(this.host);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
oidc: {
|
||||||
|
issuer: optional("OIDC_ISSUER"),
|
||||||
|
clientId: optional("OIDC_CLIENT_ID"),
|
||||||
|
clientSecret: optional("OIDC_CLIENT_SECRET"),
|
||||||
|
scopes: optional("OIDC_SCOPES", "openid profile email"),
|
||||||
|
buttonLabel: optional("OIDC_BUTTON_LABEL", "Mit SSO anmelden"),
|
||||||
|
autoRegister: bool("OIDC_AUTO_REGISTER", true),
|
||||||
|
get isConfigured() {
|
||||||
|
return Boolean(this.issuer && this.clientId && this.clientSecret);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const isProduction = process.env.NODE_ENV === "production";
|
||||||
Reference in New Issue
Block a user