docker-compose.yml reicht ENC_KEY_V2 immer an den Behaelter durch - mit leerem Wert, wenn in der .env keiner steht. src/lib/env.ts erkannte Schluessel aber am Variablennamen statt am Inhalt, fand also ein leeres ENC_KEY_V2 und brach ab mit "Umgebungsvariable ENC_KEY_V2 fehlt". Die Meldung war doppelt irrefuehrend: Sie nannte einen Schluessel, den niemand wollte, und sie erschien an einer Stelle, die damit nichts zu tun hat - beim Einrichten des zweiten Faktors, als Serverfehler ohne Text. Betroffen war jede Stelle, die verschluesselte Felder schreibt oder liest, also praktisch alles ausser der Anmeldung selbst. Die kam durch, weil sie ohne Verschluesselung auskommt - der Fehler trat deshalb erst auf der naechsten Seite auf, und der Zusammenhang war nicht zu sehen. Leere Schluesselvariablen werden jetzt uebersprungen. Sieben Tests decken die Auswertung ab, darunter genau diese Konstellation. Die Meldung beim Schluesselwechsel nennt jetzt ausserdem, welche Versionen vorhanden sind, und die richtige Reihenfolge: erst den neuen Schluessel eintragen, dann ENC_KEY_AKTIV umstellen. Dazu scripts/anmeldung-pruefen.mjs. Das Anmeldeformular sagt bewusst nicht, woran eine Anmeldung scheiterte - sonst liesse sich daran ablesen, welche Adressen es gibt. Auf dem Server darf man es wissen: Jeder Versuch wird mit Grund protokolliert, und das Skript liest ihn vor, zusammen mit dem Zustand des Zugangs, dem Stand der Sperre und der AUTH_URL. Etappe: 8 Phase: Haertung
166 lines
6.5 KiB
JavaScript
166 lines
6.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Warum klappt die Anmeldung nicht?
|
|
*
|
|
* node --env-file=.env scripts/anmeldung-pruefen.mjs <e-mail>
|
|
* docker compose exec app node scripts/anmeldung-pruefen.mjs <e-mail>
|
|
*
|
|
* Die Anwendung sagt am Anmeldeformular bewusst nicht, woran es lag - sonst
|
|
* liesse sich daran ablesen, welche Adressen es gibt. Auf dem Server darf
|
|
* man es wissen: Jeder Versuch wird mit Grund protokolliert, und dieses
|
|
* Skript liest ihn vor.
|
|
*
|
|
* Es aendert nichts. Es liest nur.
|
|
*/
|
|
import { PrismaClient } from '@prisma/client'
|
|
|
|
const email = (process.argv[2] ?? '').trim().toLowerCase()
|
|
|
|
if (!email) {
|
|
console.error('Aufruf: node scripts/anmeldung-pruefen.mjs <e-mail>')
|
|
process.exit(1)
|
|
}
|
|
|
|
const db = new PrismaClient()
|
|
|
|
function zeit(d) {
|
|
return d ? new Date(d).toLocaleString('de-DE') : '—'
|
|
}
|
|
|
|
/** Klartext zu den Gruenden, die src/auth.ts protokolliert. */
|
|
const GRUND = {
|
|
unbekannt: 'Diese E-Mail-Adresse gibt es nicht (Tippfehler? andere Adresse?)',
|
|
'passwort-falsch': 'Das Passwort stimmt nicht',
|
|
'code-falsch': 'Der Code aus der Authenticator-App stimmt nicht',
|
|
gesperrt: 'Abgewiesen, weil zu diesem Zeitpunkt bereits gesperrt war',
|
|
'konto-inaktiv': 'Der Zugang ist deaktiviert oder im Papierkorb',
|
|
}
|
|
|
|
try {
|
|
const benutzer = await db.user.findUnique({ where: { email } })
|
|
|
|
console.log('\n══ Zugang ' + '═'.repeat(50))
|
|
|
|
if (!benutzer) {
|
|
console.log(` Es gibt KEINEN Zugang mit der Adresse ${email}.`)
|
|
const alle = await db.user.findMany({
|
|
select: { email: true, aktiv: true, geloeschtAm: true },
|
|
orderBy: { erstelltAm: 'asc' },
|
|
})
|
|
console.log(
|
|
alle.length
|
|
? '\n Vorhandene Adressen:\n' +
|
|
alle.map((u) => ` ${u.email}${u.geloeschtAm ? ' (im Papierkorb)' : u.aktiv ? '' : ' (deaktiviert)'}`).join('\n')
|
|
: '\n Es gibt ueberhaupt keinen Zugang. Leg ihn im Browser unter /ersteinrichtung an.',
|
|
)
|
|
} else {
|
|
const zweiterFaktor = benutzer.totpAktiviertAm
|
|
? `eingerichtet am ${zeit(benutzer.totpAktiviertAm)}`
|
|
: benutzer.totpSecretVerschluesselt
|
|
? 'vorbereitet, aber NICHT bestaetigt - beim Anmelden wird noch kein Code verlangt'
|
|
: 'noch nicht eingerichtet - beim Anmelden wird noch kein Code verlangt'
|
|
|
|
console.log(` Name: ${benutzer.name}`)
|
|
console.log(` Rolle: ${benutzer.rolle}`)
|
|
console.log(` Aktiv: ${benutzer.aktiv ? 'ja' : 'NEIN - Anmeldung nicht moeglich'}`)
|
|
console.log(` Papierkorb: ${benutzer.geloeschtAm ? 'JA seit ' + zeit(benutzer.geloeschtAm) : 'nein'}`)
|
|
console.log(` Zweiter Faktor: ${zweiterFaktor}`)
|
|
console.log(` Wiederherstellung: ${benutzer.wiederherstellungscodes.length} Codes uebrig`)
|
|
console.log(` Passwort gesetzt: ${zeit(benutzer.passwortGeaendertAm)}`)
|
|
console.log(` Letzte Anmeldung: ${zeit(benutzer.letzteAnmeldung)}`)
|
|
|
|
const zugaenge = await db.betreuungsZugang.count({ where: { userId: benutzer.id } })
|
|
const betreute = await db.betreute.count({ where: { geloeschtAm: null } })
|
|
console.log(` Betreute Personen: ${zugaenge} zugeordnet, ${betreute} insgesamt angelegt`)
|
|
|
|
if (zugaenge === 0 && betreute === 0) {
|
|
console.log(
|
|
'\n Hinweis: Nach dem Anmelden landest du zuerst bei der\n' +
|
|
' Ersteinrichtung - erst zweiter Faktor, dann betreute Person.',
|
|
)
|
|
}
|
|
}
|
|
|
|
// --- Versuche der letzten Tage -------------------------------------------
|
|
const versuche = await db.loginVersuch.findMany({
|
|
where: { email },
|
|
orderBy: { zeitpunkt: 'desc' },
|
|
take: 15,
|
|
})
|
|
|
|
console.log('\n══ Letzte Anmeldeversuche ' + '═'.repeat(35))
|
|
|
|
if (versuche.length === 0) {
|
|
console.log(
|
|
' Keine. Es ist also NIE ein Versuch beim Server angekommen.\n\n' +
|
|
' Das heisst: Das Formular hat gar nicht abgeschickt, oder es lief\n' +
|
|
' auf eine andere Adresse. Pruef AUTH_URL - siehe unten.',
|
|
)
|
|
} else {
|
|
for (const v of versuche) {
|
|
const kennzeichen = v.erfolg ? 'OK ' : 'FEHLER'
|
|
const grund = v.erfolg ? 'angemeldet' : (GRUND[v.grund] ?? v.grund ?? 'unbekannter Grund')
|
|
console.log(` ${zeit(v.zeitpunkt).padEnd(20)} ${kennzeichen} ${grund}`)
|
|
}
|
|
}
|
|
|
|
// --- Sperre ---------------------------------------------------------------
|
|
const fenster = Number.parseInt(process.env.LOGIN_FENSTER_SEKUNDEN ?? '900', 10)
|
|
const grenze = Number.parseInt(process.env.LOGIN_MAX_VERSUCHE ?? '5', 10)
|
|
const seit = new Date(Date.now() - fenster * 1000)
|
|
|
|
const offen = await db.loginVersuch.count({
|
|
where: { email, erfolg: false, zeitpunkt: { gte: seit } },
|
|
})
|
|
|
|
console.log('\n══ Sperre ' + '═'.repeat(51))
|
|
console.log(` ${offen} von ${grenze} Fehlversuchen im laufenden Fenster (${fenster / 60} Minuten)`)
|
|
|
|
if (offen >= grenze) {
|
|
console.log(
|
|
'\n GESPERRT. Warte das Fenster ab, oder setz die Zaehler zurueck:\n' +
|
|
' docker compose exec db psql -U cockpit betreuungs_cockpit \\\n' +
|
|
' -c "DELETE FROM login_versuch WHERE erfolg = false;"',
|
|
)
|
|
}
|
|
|
|
// --- Umgebung -------------------------------------------------------------
|
|
console.log('\n══ Umgebung ' + '═'.repeat(49))
|
|
console.log(` AUTH_URL: ${process.env.AUTH_URL ?? 'NICHT GESETZT'}`)
|
|
console.log(
|
|
'\n Diese Adresse muss EXAKT die sein, die du im Browser aufrufst -\n' +
|
|
' gleiches Schema (https), gleicher Hostname, kein Schraegstrich am\n' +
|
|
' Ende. Weicht sie ab, wird das Sitzungs-Cookie fuer die falsche\n' +
|
|
' Herkunft gesetzt: Die Anmeldung landet ohne Fehlermeldung wieder\n' +
|
|
' auf dem Formular, und oben steht kein einziger Fehlversuch.\n',
|
|
)
|
|
} catch (fehler) {
|
|
// Ein Prisma-Stacktrace hilft hier niemandem weiter.
|
|
const meldung = fehler instanceof Error ? fehler.message : String(fehler)
|
|
|
|
if (/Can't reach database server|ECONNREFUSED/i.test(meldung)) {
|
|
// Das Passwort in der Verbindungszeichenkette gehoert nicht auf den Schirm.
|
|
const url = (process.env.DATABASE_URL ?? 'NICHT GESETZT').replace(/:[^:@/]+@/, ':***@')
|
|
console.error(
|
|
[
|
|
'',
|
|
'Die Datenbank ist nicht erreichbar.',
|
|
'',
|
|
` DATABASE_URL: ${url}`,
|
|
'',
|
|
' Im Docker-Betrieb gehoert dieses Skript IN den Behaelter:',
|
|
' docker compose exec app node scripts/anmeldung-pruefen.mjs <e-mail>',
|
|
'',
|
|
' Laeuft die Datenbank ueberhaupt?',
|
|
' docker compose ps',
|
|
'',
|
|
].join('\n'),
|
|
)
|
|
} else {
|
|
console.error(`\nFehler: ${meldung}\n`)
|
|
}
|
|
process.exitCode = 1
|
|
} finally {
|
|
await db.$disconnect()
|
|
}
|