Vier Luecken geschlossen, die im Alltag sofort aufgefallen waeren: - Passwort aendern war im eingeloggten Zustand gar nicht moeglich; es gab nur den Reset per Mail-Link. saas_change_password() prueft das aktuelle Passwort, verlangt ein tatsaechlich anderes und erneuert danach die Session-ID. Formular in konto.php. - mandant-auswahl.php war nur direkt nach dem Login erreichbar. Wer bei mehreren Mandanten Mitglied ist, musste sich zum Wechseln abmelden. Die Seite bedient jetzt beide Wege, die Mandantenpruefung bleibt unveraendert ueber saas_identity_for_user_tenant(). Menuepunkt ab zwei Mitgliedschaften. - email_verified_at wurde nirgends geprueft, nur angezeigt - bei offener Selbstregistrierung konnte sich jemand mit fremder Adresse anmelden und alles nutzen. Erzwungen wird jetzt gezielt dort, wo eine Aktion nach aussen wirkt: Einladung, Info-Mail, Jahresabschluss. Der Login selbst bleibt bewusst frei, sonst waeren alle migrierten Bestandsnutzer mit NULL-Verifikation ausgesperrt. Zusaetzlich setzt der Passwort-Reset die Verifikation mit, weil der Mail-Link den Postfachzugriff nachweist - sonst blieben eingeladene Mitglieder dauerhaft unbestaetigt. - Login landete auf konto.php statt auf dem Dashboard. Landingpage: die gruene Vertrauenszeile auf "DSGVO-konform" gekuerzt, die Eintraege zu Paragraf 19 UStG und "Bestehende Ablaeufe bleiben" entfernt. Geprueft: neues scripts/check-konto-und-mandantenwechsel.php mit 18 Assertions gruen, Passwortformular zusaetzlich manuell inkl. CSRF (419). Bestehende Suiten unveraendert gruen: HTTP-Smoke 34 Seiten, Rollenmatrix 55, Mandanten-Isolation 12, M3-Auth 9, M3-Settings 15. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
128 lines
5.2 KiB
PHP
128 lines
5.2 KiB
PHP
<?php
|
|
|
|
require_once __DIR__ . '/functions.php';
|
|
require_once __DIR__ . '/app/rate-limit.php';
|
|
|
|
$pdo = app_db_pdo();
|
|
$errors = [];
|
|
$email = trim((string)($_POST['email'] ?? ''));
|
|
$tenantSlug = trim((string)($_POST['tenant_slug'] ?? ''));
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
app_require_csrf();
|
|
|
|
$emailBucketOk = app_rate_limit_check($pdo, 'login_email:' . saas_email_norm($email), 10, 900);
|
|
$ipBucketOk = app_rate_limit_check($pdo, 'login_ip:' . app_client_ip(), 20, 900);
|
|
|
|
if (!$emailBucketOk || !$ipBucketOk) {
|
|
$errors = ['Zu viele Anmeldeversuche. Bitte versuche es in einigen Minuten erneut.'];
|
|
} else {
|
|
$result = saas_authenticate(
|
|
$pdo,
|
|
$email,
|
|
(string)($_POST['password'] ?? ''),
|
|
$tenantSlug
|
|
);
|
|
|
|
if ($result['ok'] && !empty($result['needs_tenant_selection'])) {
|
|
saas_start_pending_tenant_selection((int)$result['user_id']);
|
|
header('Location: mandant-auswahl.php');
|
|
exit;
|
|
}
|
|
|
|
if ($result['ok']) {
|
|
saas_session_login($result['identity']);
|
|
// Nach dem Login gehoert das Dashboard nach vorne, nicht die
|
|
// Stammdatenseite: fuer die meisten Nutzer ist die eigene
|
|
// Kaffeeliste der einzige Grund, sich anzumelden.
|
|
header('Location: index.php');
|
|
exit;
|
|
}
|
|
|
|
$errors = $result['errors'];
|
|
}
|
|
}
|
|
|
|
?>
|
|
|
|
<!DOCTYPE HTML>
|
|
<html lang="de">
|
|
<head>
|
|
<title>Kaffeeliste Login</title>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<link rel="stylesheet" href="assets/css/main.css" />
|
|
<link rel="stylesheet" href="assets/css/public.css" />
|
|
</head>
|
|
<body class="is-preload public-page">
|
|
<div class="public-shell">
|
|
<section class="public-auth">
|
|
<nav class="public-nav" aria-label="Hauptnavigation">
|
|
<strong><a href="landing.php">Kaffeeliste</a></strong>
|
|
<ul class="actions">
|
|
<li><a href="register.php" class="button">Registrieren</a></li>
|
|
</ul>
|
|
</nav>
|
|
|
|
<div class="public-auth-grid">
|
|
<div class="public-auth-copy">
|
|
<h1>Login</h1>
|
|
<p>Melde dich mit deinem Kundenkonto an. Wenn du mehreren Mandanten zugeordnet bist, wählst du danach den passenden Bereich aus.</p>
|
|
</div>
|
|
|
|
<div class="public-panel">
|
|
<h2>Zur App</h2>
|
|
|
|
<?php if (isset($_GET['logged_out'])): ?>
|
|
<div class="hint-box success"><p>Du wurdest abgemeldet.</p></div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($errors !== []): ?>
|
|
<div class="hint-box error">
|
|
<?php foreach ($errors as $error): ?>
|
|
<p><?php echo saas_html($error); ?></p>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<form method="post" action="login.php">
|
|
<?php echo app_csrf_field(); ?>
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<label for="email">E-Mail</label>
|
|
<input type="email" name="email" id="email" value="<?php echo saas_html($email); ?>" required>
|
|
</div>
|
|
<div class="col-12">
|
|
<label for="password">Passwort</label>
|
|
<input type="password" name="password" id="password" required>
|
|
</div>
|
|
<div class="col-12">
|
|
<label for="tenant_slug">Kundenkürzel</label>
|
|
<input type="text" name="tenant_slug" id="tenant_slug" value="<?php echo saas_html($tenantSlug); ?>" placeholder="optional">
|
|
</div>
|
|
</div>
|
|
<ul class="actions">
|
|
<li><button type="submit" class="primary">Einloggen</button></li>
|
|
<li><a href="passwort-vergessen.php" class="button">Passwort vergessen</a></li>
|
|
</ul>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<footer class="public-footer-links">
|
|
<a href="impressum.php">Impressum</a>
|
|
<a href="agb.php">AGB</a>
|
|
<a href="datenschutz.php">Datenschutz</a>
|
|
<a href="preise.php">Preise</a>
|
|
</footer>
|
|
|
|
<script src="assets/js/jquery.min.js"></script>
|
|
<script src="assets/js/browser.min.js"></script>
|
|
<script src="assets/js/breakpoints.min.js"></script>
|
|
<script src="assets/js/util.js"></script>
|
|
<script src="assets/js/main.js"></script>
|
|
</body>
|
|
</html>
|