post('username'); $password = (string)$req->post('password'); $u = $this->users->byUsername($username); if ($u === null) return Response::json(['error' => 'invalid_credentials'], 401); if ((int)($u['locked_until'] ?? 0) > $now) { $this->audit->append($username, 'login_locked', 'auth', [], []); return Response::json(['error' => 'locked'], 423); } if (!Password::verify($password, (string)$u['password_hash'])) { $attempts = (int)($u['failed_attempts'] ?? 0) + 1; $lock = $attempts >= $this->maxAttempts ? $now + $this->lockSeconds : 0; $this->users->recordFailure((int)$u['id'], $lock); $this->audit->append($username, 'login_failed', 'auth', [], ['attempts' => $attempts]); return Response::json(['error' => 'invalid_credentials'], 401); } $this->users->clearFailures((int)$u['id']); $sess->regenerate(); $sess->set('user_id', (int)$u['id']); $sess->set('username', $u['username']); $sess->set('role', $u['role']); $sess->set('site_scope', $u['site_scope'] ?? null); $this->audit->append($username, 'login_ok', 'auth', [], []); return Response::json(['ok' => true], 200); } public function logout(SessionStore $sess): Response { $sess->clear(); $sess->regenerate(); return Response::json(['ok' => true], 200); } }