diff --git a/.env.example b/.env.example index 279a421..cf43fca 100644 --- a/.env.example +++ b/.env.example @@ -36,6 +36,12 @@ AUTH_TRUST_HOST=true # false, damit nur ueber OIDC angemeldet wird. ANMELDUNG_PASSWORT=true +# Erstes Verwaltungskonto, wird vom Seed angelegt. Ohne ADMIN_PASSWORT +# entsteht ein Konto ohne Passwort, das sich nur per OIDC oder Passkey +# anmelden kann. Nach der Einrichtung dürfen beide Werte wieder raus. +ADMIN_EMAIL= +ADMIN_PASSWORT= + # --- OIDC ------------------------------------------------------------------ # firma = Entra ID, daheim = authentik. Leer lassen schaltet OIDC ab. # Der Rueckleit-URI beim Anbieter lautet: diff --git a/devlog/zeiten.csv b/devlog/zeiten.csv new file mode 100644 index 0000000..0de7710 --- /dev/null +++ b/devlog/zeiten.csv @@ -0,0 +1 @@ +datum;start;ende;minuten;meilenstein;art;notiz diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..4cfa0a9 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,156 @@ +/** + * Grundausstattung für eine frische Datenbank. + * + * Läuft mehrfach ohne Schaden: alles wird per upsert angelegt, vorhandene + * Einträge bleiben unangetastet. Es entstehen keine erfundenen Aufgaben oder + * Termine – dafür gibt es prisma/demo-seed.ts. + */ +import 'dotenv/config' +import { PrismaPg } from '@prisma/adapter-pg' +import bcrypt from 'bcryptjs' +import { PrismaClient } from '../src/generated/prisma/client.js' + +const verbindung = process.env.DATABASE_URL +if (!verbindung) { + throw new Error('DATABASE_URL ist nicht gesetzt.') +} + +const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: verbindung }) }) + +const BEREICHE = [ + { + name: 'Firma', + farbe: '#2563eb', + reihenfolge: 1, + arbeitsfensterVon: 7 * 60 + 30, + arbeitsfensterBis: 16 * 60 + 30, + kapazitaetMo: 360, + kapazitaetDi: 360, + kapazitaetMi: 360, + kapazitaetDo: 360, + kapazitaetFr: 240, + kapazitaetSa: 0, + kapazitaetSo: 0, + }, + { + name: 'Gewerbe', + farbe: '#7c3aed', + reihenfolge: 2, + arbeitsfensterVon: 17 * 60, + arbeitsfensterBis: 21 * 60, + kapazitaetMo: 90, + kapazitaetDi: 90, + kapazitaetMi: 90, + kapazitaetDo: 90, + kapazitaetFr: 60, + kapazitaetSa: 180, + kapazitaetSo: 0, + }, + { + name: 'Privat', + farbe: '#16a34a', + reihenfolge: 3, + arbeitsfensterVon: 17 * 60, + arbeitsfensterBis: 22 * 60, + kapazitaetMo: 60, + kapazitaetDi: 60, + kapazitaetMi: 60, + kapazitaetDo: 60, + kapazitaetFr: 120, + kapazitaetSa: 240, + kapazitaetSo: 240, + }, + { + name: 'Homelab', + farbe: '#ea580c', + reihenfolge: 4, + arbeitsfensterVon: 19 * 60, + arbeitsfensterBis: 23 * 60, + kapazitaetMo: 0, + kapazitaetDi: 60, + kapazitaetMi: 0, + kapazitaetDo: 60, + kapazitaetFr: 0, + kapazitaetSa: 120, + kapazitaetSo: 60, + }, +] + +async function bereicheAnlegen(): Promise { + for (const bereich of BEREICHE) { + await db.bereich.upsert({ + where: { name: bereich.name }, + update: {}, + create: bereich, + }) + } + console.log(`Bereiche geprüft: ${BEREICHE.map((b) => b.name).join(', ')}`) +} + +async function verwaltungAnlegen(): Promise { + const email = process.env.ADMIN_EMAIL?.trim().toLowerCase() + const passwort = process.env.ADMIN_PASSWORT + + if (!email) { + console.log( + 'ADMIN_EMAIL ist nicht gesetzt – kein Verwaltungskonto angelegt. ' + + 'Bei reiner OIDC-Anmeldung ist das richtig: die erste Person, die sich ' + + 'anmeldet, muss danach von Hand hochgestuft werden.', + ) + return + } + + const vorhanden = await db.benutzer.findUnique({ + where: { email }, + select: { id: true, rolle: true }, + }) + + if (vorhanden) { + if (vorhanden.rolle !== 'ADMIN') { + await db.benutzer.update({ where: { id: vorhanden.id }, data: { rolle: 'ADMIN' } }) + console.log(`${email} zur Verwaltung hochgestuft.`) + } else { + console.log(`${email} ist bereits Verwaltung.`) + } + return + } + + if (!passwort) { + await db.benutzer.create({ + data: { email, rolle: 'ADMIN', name: null }, + }) + console.log( + `${email} als Verwaltung angelegt, ohne Passwort. ` + + 'Anmeldung ist damit nur über OIDC oder einen Passkey möglich.', + ) + return + } + + if (passwort.length < 12) { + throw new Error('ADMIN_PASSWORT ist zu kurz, mindestens 12 Zeichen.') + } + + await db.benutzer.create({ + data: { + email, + rolle: 'ADMIN', + passwortHash: await bcrypt.hash(passwort, 12), + }, + }) + console.log(`${email} als Verwaltung mit Passwort angelegt.`) +} + +async function main(): Promise { + await bereicheAnlegen() + await verwaltungAnlegen() +} + +main() + .then(async () => { + await db.$disconnect() + }) + .catch(async (fehler: unknown) => { + console.error(fehler) + await db.$disconnect() + process.exit(1) + }) diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..89e1881 --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,36 @@ +import { NextResponse } from 'next/server' +import { VERSION } from '@/lib/version' +import { db } from '@/server/db' +import { aktuelleInstanz } from '@/server/instanz' + +export const dynamic = 'force-dynamic' + +/** + * Auskunft fuer Betrieb und update.sh. Bewusst ohne Anmeldung, deshalb steht + * hier nichts, was nicht ohnehin oeffentlich sein darf – insbesondere keine + * Zaehlstaende, Namen oder Adressen. + */ +export async function GET() { + let datenbank: 'ok' | 'fehler' = 'ok' + try { + await db.$queryRaw`SELECT 1` + } catch { + datenbank = 'fehler' + } + + const status = datenbank === 'ok' ? 'ok' : 'fehler' + + return NextResponse.json( + { + status, + version: VERSION, + instanz: aktuelleInstanz(), + datenbank, + zeit: new Date().toISOString(), + }, + { + status: status === 'ok' ? 200 : 503, + headers: { 'Cache-Control': 'no-store' }, + }, + ) +}