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'])) { 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)) { 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'); api_json([ 'token' => $issued['token'], 'expiresAt' => $issued['expires_at'], 'staff' => api_staff_payload($staff), ]);