Farben, Schrift, Logo und Symbole folgen jetzt dem CD Manual 2022. An der Logik aendert sich nichts: keine Migration, keine Abfrage, keine Berechtigung. Die zwei Markenfarben stehen in §1.1 als #F69686 (Rosa) und #164194 (Blau). Welche davon was traegt, ist nicht gewaehlt, sondern nachgerechnet: Weiss auf Rosa kommt auf 2.18:1 und faellt damit auch fuer Grossschrift durch, Blau auf Rosa auf 4.34:1 und reicht fuer das Logo, nicht fuer Text. Schwarz auf Rosa sind 7.47:1, Weiss auf Blau 9.47:1. Rosa ist deshalb Flaeche, Blau ist Interaktion — zwei Skalen, brand-* und accent-*, statt einer geteilten. Von den 26 Stellen mit brand-50/100/200 sind nur sieben auf accent gewandert. Der Rest meint einen Zustand und keinen Markenton: die Zeile unter dem Zeiger, der gewaehlte Listeneintrag, der aktive Menuepunkt. Gegen die warme Seitenflaeche ist ein kuehler Ton dort deutlicher, und Blau heisst in dieser Oberflaeche ab jetzt "reagiert auf dich". Nach accent gingen die Faelle ohne eigene Bedeutung — "Geplant", "Offen", der neutrale Protokoll-Chip — und die offene Planstelle im Organigramm, die vorher ein blasses Blau war und damit wie eine schwaechere Person aussah. Die Funktionsfarben bleiben, was sie sind. Das Manual regelt die Identitaet, nicht die Rueckmeldung: Rot heisst Fehler, weil die Benutzerin das mitbringt. `info` bleibt bewusst tuerkis — Blau saehe ab jetzt bedienbar aus, und gemessen kaeme ein blaues info dem violetten Chip auf dE 6.8 nahe, also nicht unterscheidbar. Violett ist dabei nachgezogen: gegen den Fehler-Chip stand es bei dE 8.0, "Austritt" und "Befoerderung" waren im Vorbeigehen dieselbe blasse Flaeche. Jetzt dE 15.0. Die Schrift ist Barlow. Nachgezaehlt ist das Manual zu 95 % in DIN gesetzt (Regular 79 %, Bold 16 %); Helvetica Neue steht nur in den Visitenkarten und im Claim. DIN laesst sich nicht ausliefern — eine Drucklizenz deckt keinen Webfont —, und die freien Nachbauten der DIN 1451 sind Schilderschriften, bei 14 px in einer langen Tabelle schlechter lesbar als das, was sie ersetzen. Die Variable heisst --font-din und nicht --font-barlow: liegt eines Tages eine Web-Lizenz vor, ist der Wechsel diese eine Deklaration. Nebenbei zwei Dinge repariert, die vorher schon falsch waren. Die Umrandung von Eingabefeldern, Knoepfen und Suchfeldern lag bei 1.30:1 und damit unter den 3:1, die WCAG 1.4.11 fuer Bedienelemente verlangt; border-strong bringt 3.56:1. Und das mitgelieferte favicon.ico liess sich gar nicht bauen: eingebettet waren 24-Bit-RGB-PNG, waehrend der Kopf 32 bpp behauptete. Alle Rastersymbole liegen jetzt als RGBA vor, und ihr Blau ist auf den CD-Wert gezogen — samt der kantengeglaetteten Raender, indem je Pixel der Blauanteil bestimmt und neu gemischt wurde. Das Logo ist das Markenlogo (§4.1), nicht das Unternehmenslogo, das §3.2 fuer eine Anwendung mit der AG als Absender vorsaehe — es liegt nicht vor. Der Freiraum X/3 steckt im Bauteil selbst und nicht in den Aufrufstellen, sonst haengt seine Einhaltung daran, dass jede einzelne daran denkt. docs/farbschema.html zeigt Token, Kontraste und Bauteile nebeneinander und laesst sich ohne Server oeffnen. Nicht im Browser gesehen: Anmeldung laeuft ueber das Firmenkonto und die Datenbank ist von hier nicht erreichbar. Lint, Typen, Schemaabgleich, 524 Tests und der Build sind sauber. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
158 lines
7.0 KiB
TypeScript
158 lines
7.0 KiB
TypeScript
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.
|
|
<div className="min-h-dvh lg:grid lg:grid-cols-[1.05fr_1fr]">
|
|
<BrandPanel />
|
|
|
|
<main className="flex items-center justify-center px-6 py-12 lg:py-6">
|
|
<div className="w-full max-w-sm">
|
|
<div className="-ml-2 lg:hidden">
|
|
<AppWortmarke hoehe={26} textKlasse="text-ink" />
|
|
</div>
|
|
|
|
<h2 className="mt-8 text-2xl font-extrabold tracking-tight text-ink lg:mt-0">Anmelden</h2>
|
|
<p className="mt-1.5 text-sm text-ink-muted">
|
|
Der Zugang läuft über Ihr Firmenkonto. Ein eigenes Passwort gibt es nicht.
|
|
</p>
|
|
|
|
{error && (
|
|
<div role="alert" className="mt-6 rounded-md border border-danger-text/20 bg-danger-bg px-4 py-3">
|
|
<p className="text-sm font-bold text-danger-text">{error.title}</p>
|
|
<p className="mt-1 text-sm text-danger-text/90">{error.body}</p>
|
|
{code === "no_hr_access" && (
|
|
<form action={logout} className="mt-3">
|
|
<button
|
|
type="submit"
|
|
className="rounded text-xs font-semibold text-danger-text underline underline-offset-2 hover:no-underline
|
|
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-danger-text"
|
|
>
|
|
Abmelden und mit einem anderen Konto versuchen
|
|
</button>
|
|
</form>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<form action={signInWithEntra} className="mt-7">
|
|
<EntraSignInButton />
|
|
</form>
|
|
|
|
{/* 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. */}
|
|
<div className="mt-6 flex items-center gap-3" aria-hidden>
|
|
<span className="h-px flex-1 bg-border-subtle" />
|
|
<span className="text-xs font-semibold uppercase tracking-wider text-ink-muted">oder</span>
|
|
<span className="h-px flex-1 bg-border-subtle" />
|
|
</div>
|
|
|
|
<div className="mt-6">
|
|
<PasswortFormular />
|
|
</div>
|
|
|
|
<p className="mt-6 border-t border-border-subtle pt-5 text-xs leading-relaxed text-ink-muted">
|
|
Die Anmeldung allein erteilt keinen Zugriff. HR-Rechte vergibt die Personalabteilung — bis dahin bleiben alle
|
|
Personaldaten verschlossen.
|
|
</p>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function BrandPanel() {
|
|
return (
|
|
<aside className="relative hidden overflow-hidden bg-brand-700 lg:flex lg:flex-col lg:justify-between lg:p-12">
|
|
{/* Zwei weiche Lichtpunkte und ein feines Raster — genug Struktur, damit
|
|
die Fläche nicht wie ein Farbfehler wirkt, aber ohne Bilddatei und
|
|
ohne von der einen Schaltfläche gegenüber abzulenken. */}
|
|
<div
|
|
aria-hidden="true"
|
|
className="pointer-events-none absolute inset-0"
|
|
style={{
|
|
backgroundImage:
|
|
"radial-gradient(60rem 40rem at 15% 0%, rgb(255 255 255 / 0.16), transparent 60%)," +
|
|
"radial-gradient(40rem 40rem at 100% 100%, rgb(255 255 255 / 0.10), transparent 55%)," +
|
|
"linear-gradient(rgb(255 255 255 / 0.05) 1px, transparent 1px)," +
|
|
"linear-gradient(90deg, rgb(255 255 255 / 0.05) 1px, transparent 1px)",
|
|
backgroundSize: "auto, auto, 3rem 3rem, 3rem 3rem",
|
|
}}
|
|
/>
|
|
|
|
{/* -ml-4 gleicht den Freiraum des Logos aus, damit die Wortmarke
|
|
buendig mit dem Text darunter beginnt und nicht eingerueckt wirkt. */}
|
|
<div className="relative -ml-4">
|
|
<AppWortmarke hoehe={36} textKlasse="text-white" />
|
|
</div>
|
|
|
|
<div className="relative max-w-md">
|
|
<p className="text-3xl font-extrabold leading-tight tracking-tight text-white">
|
|
Die Organisation, so wie sie heute wirklich aussieht.
|
|
</p>
|
|
<p className="mt-4 text-sm leading-relaxed text-white/70">
|
|
Stammdaten, Planstellen und Berichtslinien der Alpenwerk Industrie GmbH — jederzeit auch zu einem beliebigen
|
|
Stichtag.
|
|
</p>
|
|
</div>
|
|
|
|
<p className="relative text-xs text-white/50">Interne Anwendung · Zugriff nur für die Personalabteilung</p>
|
|
</aside>
|
|
);
|
|
}
|
|
|
|
// Die vier Quadrate, die hier als Platzhalter-Bildmarke standen, sind
|
|
// entfallen: an ihrer Stelle steht jetzt das Manner-Logo
|
|
// (components/brand/Logo.tsx).
|