import { EntraSignInButton } from "@/components/auth/EntraSignInButton";
import { PasswortFormular } from "@/components/auth/PasswortFormular";
import { AppWortmarke } from "@/components/brand/Logo";
import { logout, signInWithEntra } from "@/actions/auth";
// The query string is attacker-controlled, so the login page renders a message
// looked up by code rather than whatever text ?error= carries. Reflecting the
// raw parameter let anyone put arbitrary wording ("Ihr Konto wurde gesperrt,
// rufen Sie …") on the real, correctly-branded sign-in screen. Dasselbe gilt
// für die Fehlertexte, die Entra im Rückweg mitschickt.
const ERROR_MESSAGES = {
no_hr_access: {
title: "Kein HR-Zugriff",
body: "Ihr Firmenkonto ist bekannt, aber nicht für die Personalverwaltung freigeschaltet. Bitte wenden Sie sich an eine:n bestehende:n HR-Benutzer:in.",
},
sso_failed: {
title: "Anmeldung fehlgeschlagen",
body: "Die Anmeldung über das Firmenkonto konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut.",
},
// Auth.js' eigener Code für eine gescheiterte Passwortanmeldung. Im Regelfall
// kommt er hier nicht an — die Server Action fängt den Fehlschlag ab und
// zeigt ihn am Formular. Ohne diesen Eintrag fiele er aber auf `sso_failed`
// zurück, und dort stünde „über das Firmenkonto", was mit dem tatsächlichen
// Weg nichts zu tun hätte.
CredentialsSignin: {
title: "Anmeldung fehlgeschlagen",
body: "E-Mail-Adresse oder Passwort stimmt nicht.",
},
} as const;
type ErrorCode = keyof typeof ERROR_MESSAGES;
type LoginPageProps = {
searchParams: Promise<{ error?: string }>;
};
export default async function LoginPage({ searchParams }: LoginPageProps) {
const params = await searchParams;
// Ein unbekannter Code wird nicht verschluckt, sondern auf die allgemeine
// Meldung abgebildet: Auth.js schickt bei einem Fehlschlag seine eigenen
// Codes („Configuration", „AccessDenied", „OAuthCallbackError" …), und ohne
// diese Abbildung stünde man vor einer Anmeldeseite, die so tut, als wäre
// nichts gewesen. Angezeigt wird trotzdem nur eigener Text — der Parameter
// selbst kommt nie auf die Seite.
const code = params.error
? Object.hasOwn(ERROR_MESSAGES, params.error)
? (params.error as ErrorCode)
: "sso_failed"
: null;
const error = code ? ERROR_MESSAGES[code] : null;
return (
// dvh statt vh: auf iOS zählt vh die Adressleiste mit, wodurch die Karte
// im ersten Moment unter dem Faltenrand sitzt.
Anmelden
Der Zugang läuft über Ihr Firmenkonto. Ein eigenes Passwort gibt es nicht.
{error && (
{error.title}
{error.body}
{code === "no_hr_access" && (
)}
)}
{/* Der Passwortweg steht bewusst *unter* dem Firmenkonto und ohne
eigene Überschrift: er ist die Ausnahme für die Testphase, nicht
die zweite gleichwertige Möglichkeit. Wer ein Firmenkonto hat,
soll oben klicken. */}
oder
Die Anmeldung allein erteilt keinen Zugriff. HR-Rechte vergibt die Personalabteilung — bis dahin bleiben alle
Personaldaten verschlossen.
);
}
function BrandPanel() {
return (
// Rosa und nicht Blau: Rosa ist die Farbe, in der die Marke auftritt, und
// die Flaeche ist das Groesste, was diese Seite zu vergeben hat. Blau war
// hier doppelt falsch — es drehte das Verhaeltnis der beiden Markenfarben
// um, und das Logo bringt sein eigenes rosa Feld mit (§1.2), das auf Blau
// wie ein aufgeklebtes Rechteck stand. Auf Rosa geht es in die Flaeche
// ueber, und genau das meint §3.1 mit „in einem grossflaechigen rosa
// Umfeld eingebettet".
);
}
// Die vier Quadrate, die hier als Platzhalter-Bildmarke standen, sind
// entfallen: an ihrer Stelle steht jetzt das Manner-Logo
// (components/brand/Logo.tsx).