feat(betrieb): Health-Endpunkt und Seed für eine frische Datenbank
/api/health meldet Status, Version, Instanz und Erreichbarkeit der Datenbank – ohne Anmeldung, deshalb ohne Zählstände, Namen oder Adressen. update.sh wird sich später darauf stützen. Der Seed legt die vier Bereiche an und ein erstes Verwaltungskonto aus ADMIN_EMAIL und ADMIN_PASSWORT. Er läuft mehrfach ohne Schaden; ohne ADMIN_PASSWORT entsteht ein Konto, das sich nur über OIDC oder Passkey anmelden kann. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e9a2b4b463
commit
1876ae4c6b
@@ -36,6 +36,12 @@ AUTH_TRUST_HOST=true
|
|||||||
# false, damit nur ueber OIDC angemeldet wird.
|
# false, damit nur ueber OIDC angemeldet wird.
|
||||||
ANMELDUNG_PASSWORT=true
|
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 ------------------------------------------------------------------
|
# --- OIDC ------------------------------------------------------------------
|
||||||
# firma = Entra ID, daheim = authentik. Leer lassen schaltet OIDC ab.
|
# firma = Entra ID, daheim = authentik. Leer lassen schaltet OIDC ab.
|
||||||
# Der Rueckleit-URI beim Anbieter lautet:
|
# Der Rueckleit-URI beim Anbieter lautet:
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
datum;start;ende;minuten;meilenstein;art;notiz
|
||||||
|
+156
@@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
await bereicheAnlegen()
|
||||||
|
await verwaltungAnlegen()
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.then(async () => {
|
||||||
|
await db.$disconnect()
|
||||||
|
})
|
||||||
|
.catch(async (fehler: unknown) => {
|
||||||
|
console.error(fehler)
|
||||||
|
await db.$disconnect()
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -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' },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user