prepare('SELECT * FROM staff WHERE LOWER(email) = LOWER(?) AND is_active = 1'); $stmt->execute([$email]); $staff = $stmt->fetch(); if (!$staff || !password_verify($password, $staff['password_hash'])) { login_guard_failed($staff ?: null, $email, 'app', $staff ? 'Falsches Passwort' : 'Unbekannte E-Mail-Adresse'); api_error(401, 'INVALID_CREDENTIALS', 'E-Mail-Adresse oder Passwort ist falsch.'); } if (!empty($staff['totp_enabled'])) { if ($totpCode === '') { api_json(['requiresTotp' => true], 202); } if (!preg_match('/^\d{6}$/', $totpCode) || !totp_verify($staff['totp_secret'], $totpCode)) { login_guard_failed($staff, $email, 'app', 'Falscher 2FA-Code'); api_error(401, 'INVALID_TOTP', 'Der Authenticator-Code ist ungültig.'); } } rate_limit_reset('mobile_staff_login_email', strtolower($email)); rate_limit_reset('mobile_staff_login_ip', client_ip()); $issued = api_issue_token($pdo, (int) $staff['id'], isset($body['deviceName']) ? (string) $body['deviceName'] : null); audit_log((int) $staff['id'], 'Mobile App: Anmeldung'); login_guard_success($staff, 'app', isset($body['deviceName']) ? (string) $body['deviceName'] : null); api_json([ 'token' => $issued['token'], 'expiresAt' => $issued['expires_at'], 'staff' => api_staff_payload($staff), ]);