{"record":{"id":"bde97a18a2a890a9","repo":"odysseus-dev/odysseus","slug":"not-authenticated-bde97a","errorCode":null,"errorMessage":"Not authenticated","messagePattern":"Not authenticated","errorType":"http","errorClass":"HTTPException","httpStatus":401,"severity":"error","filePath":"routes/auth_routes.py","lineNumber":204,"sourceCode":"        # set merged with DEFAULT_PRIVILEGES.\n        try:\n            u = result.get(\"username\")\n            if u:\n                result[\"privileges\"] = auth_manager.get_privileges(u)\n        except Exception:\n            pass\n        return result\n\n    @router.get(\"/policy\")\n    async def auth_policy():\n        \"\"\"Return public auth policy constants for the frontend.\"\"\"\n        return auth_manager.policy()\n\n    @router.post(\"/change-password\")\n    async def change_password(body: ChangePasswordRequest, request: Request):\n        user = _get_current_user(request)\n        if not user:\n            raise HTTPException(401, \"Not authenticated\")\n        if len(body.new_password) < PASSWORD_MIN_LENGTH:\n            raise HTTPException(400, f\"Password must be at least {PASSWORD_MIN_LENGTH} characters\")\n        current_token = request.cookies.get(SESSION_COOKIE)\n        ok = await asyncio.to_thread(auth_manager.change_password, user, body.current_password, body.new_password)\n        if not ok:\n            raise HTTPException(400, \"Current password is incorrect\")\n        await asyncio.to_thread(auth_manager.revoke_user_sessions, user, current_token)\n        return {\"ok\": True}\n\n    # ------------------------------------------------------------------\n    # Two-factor authentication\n    # ------------------------------------------------------------------\n\n    @router.post(\"/2fa/setup\")\n    async def totp_setup(request: Request):\n        \"\"\"Generate a TOTP secret and return the QR code URI.\"\"\"\n        user = _get_current_user(request)\n        if not user:","sourceCodeStart":186,"sourceCodeEnd":222,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/auth_routes.py#L186-L222","documentation":"Raised as HTTP 401 by POST /change-password when _get_current_user(request) returns falsy, i.e. the request carries no valid session cookie. _get_current_user resolves the SESSION_COOKIE against the auth manager's session store; missing, expired, revoked, or unknown tokens all yield None. Password-change is a privileged operation, so an unauthenticated request is rejected before any validation.","triggerScenarios":"Calling /change-password with no session cookie, with a session that expired (past TOKEN_TTL or browser session end), with a token revoked by another password change or logout-all, or with a cookie not sent because the request is cross-site and SameSite=lax blocks it.","commonSituations":"Session expired while the settings page sat open, cookie lost after password change elsewhere (sessions revoked), testing API endpoints with curl while forgetting the cookie jar, or a frontend fetch missing credentials: 'include'.","solutions":["Log in again to obtain a fresh session cookie, then retry the password change.","Ensure the client sends cookies: fetch(url, {credentials: 'include'}) or keeps the cookie jar in curl/tests.","If sessions were revoked (e.g. password changed on another device), re-authenticate on each device.","Have the frontend detect 401 on this route and redirect to the login page with a return URL."],"exampleFix":"// before\nawait fetch('/change-password', {method:'POST', body: JSON.stringify(payload)});\n// after — include the session cookie and handle expiry\nconst res = await fetch('/change-password', {method:'POST', credentials:'include', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload)});\nif (res.status === 401) { location.href = '/login?next=/settings'; }","handlingStrategy":"validation","validationCode":"// Confirm the session is alive before showing the change-password form\nconst s = await fetch('/2fa/status', {credentials:'include'});\nif (s.status === 401) { location.href = '/login?next=/settings'; return; }","typeGuard":null,"tryCatchPattern":"catch (e) { if (e.status === 401) { saveDraft(); location.href = '/login?next=/settings'; } }","preventionTips":["Always send credentials: 'include' on authenticated endpoints.","Probe session validity on page load instead of waiting for the form submit to fail.","Preserve form state across the re-login redirect."],"tags":["auth","http-401","session","cookie","fastapi"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}