Add shared auth login flow

This commit is contained in:
2026-06-15 11:20:22 +02:00
parent b648d789e9
commit da29732cba
9 changed files with 883 additions and 1 deletions
+385
View File
@@ -0,0 +1,385 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../db.php';
const AUTH_SESSION_NAME = 'erp_naurua_session';
const AUTH_LOGIN_RETURN_TO_KEY = 'auth_return_to';
const AUTH_CSRF_TOKEN_KEY = 'auth_csrf_token';
const AUTH_SESSION_USER_ID_KEY = 'auth_user_id';
const AUTH_FAILED_LOGIN_LIMIT = 5;
const AUTH_LOCK_DURATION_SECONDS = 900;
function auth_is_https_request(): bool
{
if (!empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off') {
return true;
}
$forwardedProto = strtolower((string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''));
if ($forwardedProto === 'https') {
return true;
}
return false;
}
function auth_bootstrap_session(): void
{
if (session_status() === PHP_SESSION_ACTIVE) {
return;
}
session_name(AUTH_SESSION_NAME);
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => auth_is_https_request(),
'httponly' => true,
'samesite' => 'Lax',
]);
ini_set('session.use_only_cookies', '1');
ini_set('session.use_strict_mode', '1');
ini_set('session.use_trans_sid', '0');
session_start();
if (!isset($_SESSION[AUTH_CSRF_TOKEN_KEY]) || !is_string($_SESSION[AUTH_CSRF_TOKEN_KEY]) || $_SESSION[AUTH_CSRF_TOKEN_KEY] === '') {
$_SESSION[AUTH_CSRF_TOKEN_KEY] = bin2hex(random_bytes(32));
}
}
function auth_csrf_token(): string
{
auth_bootstrap_session();
return (string) $_SESSION[AUTH_CSRF_TOKEN_KEY];
}
function auth_validate_csrf_token(?string $token): bool
{
auth_bootstrap_session();
if (!is_string($token) || $token === '') {
return false;
}
return hash_equals((string) $_SESSION[AUTH_CSRF_TOKEN_KEY], $token);
}
function auth_escape_html(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
function auth_normalize_identifier(string $identifier): string
{
return trim($identifier);
}
function auth_normalize_return_to(?string $returnTo): string
{
$candidate = trim((string) $returnTo);
if ($candidate === '' || str_starts_with($candidate, '//') || str_contains($candidate, '://')) {
return '/';
}
if (!str_starts_with($candidate, '/')) {
return '/';
}
return $candidate;
}
function auth_set_return_to(string $returnTo): void
{
auth_bootstrap_session();
$_SESSION[AUTH_LOGIN_RETURN_TO_KEY] = auth_normalize_return_to($returnTo);
}
function auth_take_return_to(): string
{
auth_bootstrap_session();
$returnTo = auth_normalize_return_to($_SESSION[AUTH_LOGIN_RETURN_TO_KEY] ?? '/');
unset($_SESSION[AUTH_LOGIN_RETURN_TO_KEY]);
return $returnTo;
}
function auth_ensure_schema(PDO $pdo): void
{
$requiredColumns = [
'id',
'username',
'email',
'password_hash',
'role',
'is_active',
'failed_login_count',
'locked_until',
'last_login_at',
'created_by_user_id',
'updated_by_user_id',
'created_at',
'updated_at',
];
$stmt = $pdo->query(
"SELECT column_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'system_user'"
);
$columns = $stmt->fetchAll(PDO::FETCH_COLUMN);
$existing = array_map('strval', $columns ?: []);
$missing = array_values(array_diff($requiredColumns, $existing));
if ($missing !== []) {
throw new RuntimeException('DB schema not initialized. Missing auth columns: ' . implode(', ', $missing));
}
}
function auth_fetch_user_row_by_id(PDO $pdo, int $userId): ?array
{
$stmt = $pdo->prepare(
'SELECT id, username, email, role, is_active, locked_until, last_login_at, created_at, updated_at
FROM public.system_user
WHERE id = :id
LIMIT 1'
);
$stmt->execute([':id' => $userId]);
$row = $stmt->fetch();
return is_array($row) ? $row : null;
}
function auth_current_user(PDO $pdo): ?array
{
auth_bootstrap_session();
$userId = $_SESSION[AUTH_SESSION_USER_ID_KEY] ?? null;
if (!is_int($userId) && !ctype_digit((string) $userId)) {
return null;
}
$user = auth_fetch_user_row_by_id($pdo, (int) $userId);
if ($user === null) {
unset($_SESSION[AUTH_SESSION_USER_ID_KEY]);
return null;
}
if (!((bool) $user['is_active'])) {
unset($_SESSION[AUTH_SESSION_USER_ID_KEY]);
return null;
}
$lockedUntil = $user['locked_until'] ?? null;
if ($lockedUntil !== null && $lockedUntil !== '') {
try {
$lockedUntilAt = new DateTimeImmutable((string) $lockedUntil);
if ($lockedUntilAt > new DateTimeImmutable('now')) {
unset($_SESSION[AUTH_SESSION_USER_ID_KEY]);
return null;
}
} catch (Throwable) {
unset($_SESSION[AUTH_SESSION_USER_ID_KEY]);
return null;
}
}
return $user;
}
function auth_require_user(PDO $pdo): array
{
$user = auth_current_user($pdo);
if ($user !== null) {
return $user;
}
$requestUri = (string) ($_SERVER['REQUEST_URI'] ?? '/');
auth_set_return_to($requestUri);
header('Location: /');
exit;
}
function auth_find_user_by_identifier(PDO $pdo, string $identifier): ?array
{
$stmt = $pdo->prepare(
'SELECT id, username, email, password_hash, role, is_active, failed_login_count, locked_until
FROM public.system_user
WHERE LOWER(username) = LOWER(:identifier_username)
OR LOWER(email) = LOWER(:identifier_email)
LIMIT 1
FOR UPDATE'
);
$stmt->execute([
':identifier_username' => $identifier,
':identifier_email' => $identifier,
]);
$row = $stmt->fetch();
return is_array($row) ? $row : null;
}
function auth_login(PDO $pdo, string $identifier, string $password): array
{
auth_ensure_schema($pdo);
auth_bootstrap_session();
$normalizedIdentifier = auth_normalize_identifier($identifier);
$password = (string) $password;
$errors = [
'identifier' => null,
'password' => null,
];
if ($normalizedIdentifier === '') {
$errors['identifier'] = 'Bitte Benutzername oder E-Mail eingeben.';
}
if ($password === '') {
$errors['password'] = 'Bitte Passwort eingeben.';
}
if ($errors['identifier'] !== null || $errors['password'] !== null) {
return [
'ok' => false,
'errors' => $errors,
];
}
$pdo->beginTransaction();
try {
$user = auth_find_user_by_identifier($pdo, $normalizedIdentifier);
if ($user === null) {
$pdo->commit();
return [
'ok' => false,
'errors' => [
'identifier' => 'Benutzername oder E-Mail ist unbekannt.',
'password' => null,
],
];
}
$userId = (int) $user['id'];
if (!((bool) $user['is_active'])) {
$pdo->commit();
return [
'ok' => false,
'errors' => [
'identifier' => 'Benutzerkonto ist deaktiviert.',
'password' => null,
],
];
}
$lockedUntil = $user['locked_until'] ?? null;
if ($lockedUntil !== null && $lockedUntil !== '') {
try {
$lockedUntilAt = new DateTimeImmutable((string) $lockedUntil);
if ($lockedUntilAt > new DateTimeImmutable('now')) {
$pdo->commit();
return [
'ok' => false,
'errors' => [
'identifier' => 'Benutzerkonto ist vorübergehend gesperrt. Bitte später erneut versuchen.',
'password' => null,
],
];
}
} catch (Throwable) {
$pdo->commit();
return [
'ok' => false,
'errors' => [
'identifier' => 'Benutzerkonto ist vorübergehend gesperrt. Bitte später erneut versuchen.',
'password' => null,
],
];
}
}
if (!password_verify($password, (string) $user['password_hash'])) {
$failedCount = (int) ($user['failed_login_count'] ?? 0) + 1;
$lockedUntilSql = null;
if ($failedCount >= AUTH_FAILED_LOGIN_LIMIT) {
$lockedUntilSql = 'NOW() + INTERVAL \'' . AUTH_LOCK_DURATION_SECONDS . ' seconds\'';
}
$updateSql = 'UPDATE public.system_user
SET failed_login_count = :failed_login_count,
locked_until = ' . ($lockedUntilSql !== null ? $lockedUntilSql : 'NULL') . ',
updated_at = NOW(),
updated_by_user_id = NULL
WHERE id = :id';
$stmt = $pdo->prepare($updateSql);
$stmt->execute([
':failed_login_count' => $failedCount,
':id' => $userId,
]);
$pdo->commit();
return [
'ok' => false,
'errors' => [
'identifier' => null,
'password' => 'Passwort ist falsch.',
],
];
}
$stmt = $pdo->prepare(
'UPDATE public.system_user
SET failed_login_count = 0,
locked_until = NULL,
last_login_at = NOW(),
updated_at = NOW(),
updated_by_user_id = NULL
WHERE id = :id'
);
$stmt->execute([':id' => $userId]);
session_regenerate_id(true);
$_SESSION[AUTH_SESSION_USER_ID_KEY] = $userId;
$pdo->commit();
return [
'ok' => true,
'errors' => [
'identifier' => null,
'password' => null,
],
];
} catch (Throwable $exception) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $exception;
}
}
function auth_logout(): void
{
auth_bootstrap_session();
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, [
'path' => $params['path'] ?? '/',
'domain' => $params['domain'] ?? '',
'secure' => (bool) ($params['secure'] ?? false),
'httponly' => (bool) ($params['httponly'] ?? true),
'samesite' => $params['samesite'] ?? 'Lax',
]);
}
session_destroy();
}
+188
View File
@@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../service.php';
function render_auth_home_page(array $user): void
{
$userName = auth_escape_html((string) ($user['username'] ?? ''));
echo '<!doctype html>';
echo '<html lang="de">';
echo '<head>';
echo '<meta charset="UTF-8">';
echo '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
echo '<title>Naurua ERP</title>';
echo '<link rel="stylesheet" href="/assets/styles.css">';
echo '</head>';
echo '<body>';
echo '<section id="pattern-portal-header" class="sg-vsf-list-overview-page-v2">';
echo '<article class="sg-portal-header-pattern-variant" aria-label="Portal Header mit Options Row">';
echo '<p class="sg-table-label sg-portal-header-pattern-variant__label sg-text-on-dark">Variante: Portal Header mit Options Row</p>';
echo '<header class="sg-portal-header" aria-label="Portal Header" data-pattern="portal-header">';
echo '<div class="sg-portal-header__main" data-pattern-part="portal-header-main">';
echo '<p class="sg-portal-header__brand sg-brand-title" data-pattern-part="portal-header-brand">Naurua ERP</p>';
echo '<div class="sg-portal-header__menu-wrap sg-sandwich-menu-wrap" data-open="false" data-component="sandwich-menu" data-component-size="default" data-component-context="portal-header" data-pattern-part="portal-header-action">';
echo '<button class="sg-interaction-element sg-sandwich-button" type="button" aria-expanded="false" aria-label="Menü öffnen" data-component-part="sandwich-trigger">';
echo '<span class="sg-sandwich-button__icon" aria-hidden="true">';
echo '<span class="sg-sandwich-button__line"></span>';
echo '<span class="sg-sandwich-button__line"></span>';
echo '<span class="sg-sandwich-button__line"></span>';
echo '</span>';
echo '</button>';
echo '<div class="sg-sandwich-menu-panel" aria-label="Ausgeklapptes Menü" data-component-part="sandwich-panel">';
echo '<a class="sg-sandwich-menu-link" href="/" data-component-part="sandwich-menu-link">Startseite</a>';
echo '<a class="sg-sandwich-menu-link" href="/logout.php" data-component-part="sandwich-menu-link">Abmelden</a>';
echo '</div>';
echo '</div>';
echo '<nav class="sg-portal-header__tabs sg-tab-button-group" aria-label="Hauptnavigation" data-component="tab-navigation" data-component-size="large" data-component-context="portal-header" data-pattern-part="portal-header-navigation">';
echo '<button class="sg-interaction-element sg-button sg-tab-button" type="button" aria-selected="false" data-component-part="tab-button" data-component-state="inactive">Übersicht</button>';
echo '<button class="sg-interaction-element sg-button sg-tab-button" type="button" aria-selected="true" data-component-part="tab-button" data-component-state="active">Stammdaten</button>';
echo '<button class="sg-interaction-element sg-button sg-tab-button" type="button" aria-selected="false" data-component-part="tab-button" data-component-state="inactive">Bestellungen</button>';
echo '<button class="sg-interaction-element sg-button sg-tab-button" type="button" aria-selected="false" data-component-part="tab-button" data-component-state="inactive">Lager</button>';
echo '</nav>';
echo '</div>';
echo '</header>';
echo '<div class="sg-options-row" aria-label="Optionszeile" data-pattern="options-row">';
echo '<div class="sg-options-row__left" data-pattern-part="options-row-primary-actions">';
echo '<div class="sg-pulldown-demo" data-open="false" data-align="left" data-selection-mode="single" data-component="pulldown" data-component-context="options-row" data-component-state="inactive-selectable">';
echo '<button class="sg-interaction-element sg-pulldown sg-pulldown-demo__trigger" type="button" aria-expanded="false" aria-label="Pulldown Sortierung" data-component-part="pulldown-trigger" data-label-base="Sortierung">Sortierung</button>';
echo '<div class="sg-pulldown-panel" aria-label="Geöffnetes Pulldown Sortierung" data-component-part="pulldown-panel">';
echo '<ul class="sg-pulldown-option-list" aria-label="Sortierungsoptionen">';
echo '<li class="sg-pulldown-option" role="checkbox" aria-checked="false" data-pulldown-option><span>Menüpunkt 1</span></li>';
echo '<li class="sg-pulldown-option" role="checkbox" aria-checked="false" data-pulldown-option><span>Menüpunkt 2</span></li>';
echo '<li class="sg-pulldown-option" role="checkbox" aria-checked="false" data-pulldown-option><span>Menüpunkt 3</span></li>';
echo '<li class="sg-pulldown-option" role="checkbox" aria-checked="false" data-pulldown-option><span>Menüpunkt 4</span></li>';
echo '<li class="sg-pulldown-option sg-pulldown-option--disabled"><span>Menüpunkt 5</span></li>';
echo '</ul>';
echo '</div>';
echo '</div>';
echo '<div class="sg-pulldown-demo" data-open="false" data-align="left" data-selection-mode="single" data-component="pulldown" data-component-context="options-row" data-component-state="inactive-selectable">';
echo '<button class="sg-interaction-element sg-pulldown sg-pulldown-demo__trigger" type="button" aria-expanded="false" aria-label="Pulldown Bereich" data-component-part="pulldown-trigger" data-label-base="Bereich">Bereich</button>';
echo '<div class="sg-pulldown-panel" aria-label="Geöffnetes Pulldown Bereich" data-component-part="pulldown-panel">';
echo '<ul class="sg-pulldown-option-list" aria-label="Bereichsoptionen">';
echo '<li class="sg-pulldown-option" role="checkbox" aria-checked="false" data-pulldown-option><span>Menüpunkt 1</span></li>';
echo '<li class="sg-pulldown-option" role="checkbox" aria-checked="false" data-pulldown-option><span>Menüpunkt 2</span></li>';
echo '<li class="sg-pulldown-option" role="checkbox" aria-checked="false" data-pulldown-option><span>Menüpunkt 3</span></li>';
echo '<li class="sg-pulldown-option" role="checkbox" aria-checked="false" data-pulldown-option><span>Menüpunkt 4</span></li>';
echo '<li class="sg-pulldown-option sg-pulldown-option--disabled"><span>Menüpunkt 5</span></li>';
echo '</ul>';
echo '</div>';
echo '</div>';
echo '<div class="sg-search-field-row">';
echo '<span class="sg-input-single-line-wrap sg-search-field-input" data-has-value="false" data-component="single-line-input" data-component-context="options-row" data-component-state="inactive-selectable">';
echo '<input class="sg-interaction-element sg-input-single-line" type="text" placeholder="Suche" aria-label="Suche">';
echo '<button class="sg-input-clear-button" type="button" aria-label="Eingabe löschen" data-component-part="input-clear-button">×</button>';
echo '</span>';
echo '<span class="sg-search-result-count sg-table-label" aria-live="polite" data-pattern-part="options-row-search-result-count">0 Treffer</span>';
echo '</div>';
echo '</div>';
echo '<div class="sg-options-row__right" data-pattern-part="options-row-secondary-actions">';
echo '<button class="sg-mode-toggle" type="button" data-active="relative" aria-label="Modus Schieber global: relativ aktiv" data-component="mode-toggle" data-component-context="options-row">';
echo '<span class="sg-mode-toggle__label" data-component-part="toggle-label">absolut</span>';
echo '<span class="sg-mode-toggle__switch" aria-hidden="true" data-component-part="toggle-track"><span class="sg-mode-toggle__handle" data-component-part="toggle-handle"></span></span>';
echo '<span class="sg-mode-toggle__label" data-component-part="toggle-label">relativ</span>';
echo '</button>';
echo '<span class="sg-help-icon-wrap" data-open="false" data-align="left" data-component="help-icon" data-component-context="options-row">';
echo '<button class="sg-help-icon" type="button" aria-expanded="false" aria-label="Hilfetext anzeigen" data-component-part="help-trigger">?</button>';
echo '<span class="sg-help-icon-panel sg-table-label" role="tooltip" data-component-part="help-panel">Angemeldet als ' . $userName . '</span>';
echo '</span>';
echo '</div>';
echo '</div>';
echo '</article>';
echo '<h1 class="sg-main-heading">Willkommen im Naurua ERP</h1>';
echo '<section class="sg-left-navigation-pattern" aria-label="Linke Navigation">';
echo '<div class="sg-left-navigation-pattern__layout" aria-label="Left Navigation Demo">';
echo '<aside class="sg-group-card sg-left-navigation-pattern__group-card sg-left-navigation-pattern__group-card--navigation" data-component="group-card" aria-label="Navigation">';
echo '<div class="sg-group-card__header-row sg-left-navigation-pattern__header-row">';
echo '<h2 class="sg-heading-h2 sg-text-on-dark sg-group-card__heading">Navigation</h2>';
echo '<button class="sg-interaction-element sg-button sg-button--active sg-left-navigation-pattern__toggle" type="button" data-left-navigation-toggle aria-expanded="true" aria-controls="left-navigation-menu">Ausblenden</button>';
echo '</div>';
echo '<nav class="sg-tab-button-group" id="left-navigation-menu" role="tablist" aria-label="Linksmenue Items" data-component="tab-navigation" data-component-size="large" data-component-variant="linksmenu-items">';
echo '<button class="sg-interaction-element sg-button sg-tab-button" type="button" role="tab" aria-selected="true" data-component-part="tab-button">Übersicht</button>';
echo '<button class="sg-interaction-element sg-button sg-tab-button" type="button" role="tab" aria-selected="false" data-component-part="tab-button">Kontakte</button>';
echo '<button class="sg-interaction-element sg-button sg-tab-button" type="button" role="tab" aria-selected="false" data-component-part="tab-button">Bestellungen</button>';
echo '<button class="sg-interaction-element sg-button sg-tab-button" type="button" role="tab" aria-selected="false" data-component-part="tab-button">Lager</button>';
echo '<button class="sg-interaction-element sg-button sg-tab-button" type="button" role="tab" aria-selected="false" data-component-part="tab-button">Einstellungen</button>';
echo '</nav>';
echo '</aside>';
echo '<section class="sg-group-card sg-left-navigation-pattern__group-card sg-left-navigation-pattern__group-card--content" data-component="group-card" aria-hidden="true"></section>';
echo '</div>';
echo '</section>';
echo '</section>';
echo '<script src="/assets/help-icon-overlays.js"></script>';
echo '<script>';
echo "document.querySelectorAll('.sg-portal-header__tabs').forEach((group) => {";
echo " group.querySelectorAll('.sg-tab-button').forEach((button) => {";
echo " button.addEventListener('click', () => {";
echo " group.querySelectorAll('.sg-tab-button').forEach((otherButton) => {";
echo " const isActive = otherButton === button;";
echo " otherButton.setAttribute('aria-selected', String(isActive));";
echo " otherButton.dataset.componentState = isActive ? 'active' : 'inactive';";
echo " });";
echo " });";
echo " });";
echo "});";
echo "document.querySelectorAll('.sg-sandwich-menu-wrap').forEach((wrap) => {";
echo " const button = wrap.querySelector('.sg-sandwich-button');";
echo " button.addEventListener('click', (event) => {";
echo " event.stopPropagation();";
echo " const nextState = wrap.dataset.open !== 'true';";
echo " document.querySelectorAll('.sg-sandwich-menu-wrap').forEach((otherWrap) => {";
echo " const otherButton = otherWrap.querySelector('.sg-sandwich-button');";
echo " otherWrap.dataset.open = 'false';";
echo " if (otherButton) { otherButton.setAttribute('aria-expanded', 'false'); }";
echo " });";
echo " wrap.dataset.open = String(nextState);";
echo " button.setAttribute('aria-expanded', String(nextState));";
echo " });";
echo "});";
echo "document.querySelectorAll('.sg-mode-toggle').forEach((toggle) => {";
echo " toggle.addEventListener('click', () => {";
echo " const nextState = toggle.dataset.active === 'relative' ? 'absolute' : 'relative';";
echo " toggle.dataset.active = nextState;";
echo " toggle.dataset.componentState = nextState;";
echo " toggle.setAttribute('aria-label', 'Modus Schieber global: ' + (nextState === 'relative' ? 'relativ' : 'absolut') + ' aktiv');";
echo " });";
echo "});";
echo "(() => {";
echo " const mediaQuery = window.matchMedia('(max-width: 767px)');";
echo " const toggle = document.querySelector('[data-left-navigation-toggle]');";
echo " const menu = document.getElementById('left-navigation-menu');";
echo " if (!toggle || !menu) { return; }";
echo " const setMenuState = (expanded) => {";
echo " menu.hidden = !expanded;";
echo " menu.classList.toggle('sg-left-navigation-pattern__menu--collapsed', !expanded);";
echo " toggle.setAttribute('aria-expanded', String(expanded));";
echo " toggle.textContent = expanded ? 'Ausblenden' : 'Einblenden';";
echo " toggle.classList.add('sg-button--active');";
echo " toggle.classList.remove('sg-button--inactive');";
echo " };";
echo " const syncMode = () => {";
echo " if (mediaQuery.matches) {";
echo " if (toggle.getAttribute('aria-expanded') !== 'true' && toggle.getAttribute('aria-expanded') !== 'false') { setMenuState(true); return; }";
echo " setMenuState(toggle.getAttribute('aria-expanded') !== 'false');";
echo " return;";
echo " }";
echo " menu.hidden = false;";
echo " menu.classList.remove('sg-left-navigation-pattern__menu--collapsed');";
echo " toggle.setAttribute('aria-expanded', 'true');";
echo " toggle.textContent = 'Ausblenden';";
echo " toggle.classList.add('sg-button--active');";
echo " toggle.classList.remove('sg-button--inactive');";
echo " };";
echo " toggle.addEventListener('click', () => { setMenuState(menu.hidden); });";
echo " menu.querySelectorAll('.sg-tab-button').forEach((button) => {";
echo " button.addEventListener('click', () => {";
echo " menu.querySelectorAll('.sg-tab-button').forEach((otherButton) => {";
echo " otherButton.setAttribute('aria-selected', String(otherButton === button));";
echo " });";
echo " });";
echo " });";
echo " syncMode();";
echo " mediaQuery.addEventListener('change', syncMode);";
echo "})();";
echo '</script>';
echo '</body>';
echo '</html>';
}
+86
View File
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../service.php';
function render_auth_login_page(array $state): void
{
$identifierValue = auth_escape_html((string) ($state['identifier_value'] ?? ''));
$identifierError = (string) ($state['errors']['identifier'] ?? '');
$passwordError = (string) ($state['errors']['password'] ?? '');
$pageTitle = 'Anmeldung zum Naurua ERP';
$headerTitle = 'Willkommen im Naurua ERP';
$subtitle = 'Mit Benutzername oder E-Mail anmelden';
$csrf = auth_csrf_token();
$identifierInvalid = $identifierError !== '';
$passwordInvalid = $passwordError !== '';
echo '<!doctype html>';
echo '<html lang="de">';
echo '<head>';
echo '<meta charset="UTF-8">';
echo '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
echo '<title>' . auth_escape_html($pageTitle) . '</title>';
echo '<link rel="stylesheet" href="/assets/styles.css">';
echo '</head>';
echo '<body class="sg-vsf-register-step-1-page">';
echo '<h1 class="sg-main-heading">Anmeldung Naurua ERP</h1>';
echo '<main class="sg-vsf-register-step-1">';
echo '<article class="sg-card sg-object-card sg-object-card--variable-height sg-vsf-register-step-1__card" data-pattern="object-card" aria-label="Anmeldung">';
echo '<header class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-object-card__header" data-pattern-part="object-card-header">';
echo '<div class="sg-strong">' . auth_escape_html($headerTitle) . '</div>';
echo '</header>';
echo '<footer class="sg-card-segment sg-card-segment--gray" aria-label="Anmeldeformular">';
echo '<div class="sg-form-sections-card-wrapper" data-pattern="form-sections" aria-label="Formular mit Abschnitten">';
echo '<form class="sg-form-sections-card" action="/" method="post" novalidate>';
echo '<input type="hidden" name="csrf_token" value="' . auth_escape_html($csrf) . '">';
echo '<div class="sg-form-sections-card__body" data-pattern-part="form-body">';
echo '<h2 class="sg-strong sg-form-sections-card__title">' . auth_escape_html($subtitle) . '</h2>';
echo '<div class="sg-form-sections-card__field-group">';
echo '<label class="sg-labeled-input-row">';
echo '<span class="sg-label">Benutzername oder E-Mail</span>';
echo '<span class="sg-input-validation-stack">';
echo '<input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="text" name="identifier" value="' . $identifierValue . '" aria-label="Benutzername oder E-Mail" autocomplete="username" autocapitalize="none" spellcheck="false"';
if ($identifierInvalid) {
echo ' aria-invalid="true" aria-describedby="login-identifier-error"';
}
echo '>';
if ($identifierInvalid) {
echo '<span class="sg-form-validation-text" id="login-identifier-error">' . auth_escape_html($identifierError) . '</span>';
}
echo '</span>';
echo '</label>';
echo '<label class="sg-labeled-input-row">';
echo '<span class="sg-label">Passwort</span>';
echo '<span class="sg-input-validation-stack">';
echo '<input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="password" name="password" aria-label="Passwort" autocomplete="current-password"';
if ($passwordInvalid) {
echo ' aria-invalid="true" aria-describedby="login-password-error"';
}
echo '>';
if ($passwordInvalid) {
echo '<span class="sg-form-validation-text" id="login-password-error">' . auth_escape_html($passwordError) . '</span>';
}
echo '</span>';
echo '</label>';
echo '</div>';
echo '</div>';
echo '<footer class="sg-form-sections-card__actions-segment" data-pattern-part="form-actions-segment">';
echo '<div class="sg-form-sections-card__actions" data-pattern-part="form-actions">';
echo '<button class="sg-interaction-element sg-button sg-button--active sg-form-sections-card__action" type="reset">Abbrechen</button>';
echo '<button class="sg-interaction-element sg-button sg-button--process sg-form-sections-card__action" type="submit">Anmelden</button>';
echo '</div>';
echo '</footer>';
echo '</form>';
echo '</div>';
echo '</footer>';
echo '</article>';
echo '</main>';
echo '</body>';
echo '</html>';
}