{"record":{"id":"9d4fc8f4a3ea208c","repo":"odysseus-dev/odysseus","slug":"admin-only-9d4fc8","errorCode":null,"errorMessage":"Admin only","messagePattern":"Admin only","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"routes/auth_routes.py","lineNumber":278,"sourceCode":"            raise HTTPException(401, \"Not authenticated\")\n        if not auth_manager.totp_disable(user, body.password):\n            raise HTTPException(400, \"Invalid password\")\n        return {\"ok\": True}\n\n    @router.get(\"/2fa/status\")\n    async def totp_status(request: Request):\n        \"\"\"Check if 2FA is enabled for the current user.\"\"\"\n        user = _get_current_user(request)\n        if not user:\n            raise HTTPException(401, \"Not authenticated\")\n        return {\"enabled\": auth_manager.totp_enabled(user)}\n\n    # Admin-only routes\n    @router.get(\"/users\")\n    async def list_users(request: Request):\n        user = _get_current_user(request)\n        if not user or not auth_manager.is_admin(user):\n            raise HTTPException(403, \"Admin only\")\n        return {\"users\": auth_manager.list_users()}\n\n    @router.post(\"/users\")\n    async def admin_create_user(body: CreateUserRequest, request: Request):\n        user = _get_current_user(request)\n        if not user or not auth_manager.is_admin(user):\n            raise HTTPException(403, \"Admin only\")\n        if len(body.password) < PASSWORD_MIN_LENGTH:\n            raise HTTPException(400, f\"Password must be at least {PASSWORD_MIN_LENGTH} characters\")\n        if len(body.username.strip()) < 1:\n            raise HTTPException(400, \"Username is required\")\n        if body.username.lower() in RESERVED_USERNAMES:\n            raise HTTPException(403, \"Username is reserved\")\n        ok = auth_manager.create_user(body.username, body.password, body.is_admin)\n        if not ok:\n            raise HTTPException(409, \"Username already taken\")\n        return {\"ok\": True}\n","sourceCodeStart":260,"sourceCodeEnd":296,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/auth_routes.py#L260-L296","documentation":"Raised as HTTP 403 by GET /users when _get_current_user(request) returns falsy OR auth_manager.is_admin(user) is false. Note the route conflates 'no session' and 'not admin' into one 403 'Admin only' (other routes use 401 for unauthenticated). User listing exposes account data, so it's restricted to administrators.","triggerScenarios":"Calling GET /users while logged in as a non-admin user, with no/expired session, or with a session whose user lost admin privileges via PUT /users/{username}/privileges.","commonSituations":"First-run deployments where the only account isn't admin yet (admin flag never set on the initial user), privilege revoked while a tab stayed open, or scripts reusing a non-admin session cookie.","solutions":["Log in as an account with is_admin=true, then retry.","If no admin exists yet, create/promote one: use setup to bootstrap the initial admin, or have the operator set is_admin directly in the user store.","Grant admin to a legitimate account via PUT /users/{username}/privileges from an existing admin session.","Fix clients to distinguish this 403 from other failures and surface 'admin required' to the operator."],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":"// Verify admin privileges before calling admin endpoints\nconst me = await (await fetch('/me', {credentials:'include'})).json(); // or session info endpoint\nif (!me?.is_admin) { showError('This view requires an admin account'); return; }","typeGuard":"function isAdminSession(session) {\n  return session != null && session.user != null && session.user.is_admin === true;\n}","tryCatchPattern":"catch (e) { if (e.status === 403 && /admin only/i.test(e.message)) hideAdminUiAndWarn(); }","preventionTips":["Gate the admin UI on a verified is_admin flag, not on username guesses.","Ensure first-run setup creates at least one admin.","Re-check privileges after PUT /users/{username}/privileges changes."],"tags":["auth","http-403","authorization","admin","fastapi"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}