-
Notifications
You must be signed in to change notification settings - Fork 65
Feat: Added User Authentication and Authorization #120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Feat: Added User Authentication and Authorization #120
Conversation
WalkthroughAdds JWT-based authentication with signup/login endpoints, MongoDB-backed user storage and lifecycle hooks, password hashing, protected API route dependencies and Next.js middleware, frontend login UI and profile menu, embedding robustness (lazy load + fallback), toast/toaster UI variants, and project dependency and gitignore updates. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant FE as Frontend (AuthForm)
participant BE as FastAPI
participant DB as MongoDB
participant Cookie as Browser Cookie
rect rgb(230,245,255)
Note over User,BE: Authentication (signup/login)
User->>FE: submit credentials
FE->>BE: POST /api/auth/signup or /api/auth/login
alt signup
BE->>DB: get_user_by_email(email)
DB-->>BE: None
BE->>BE: hash_password
BE->>DB: create_user(payload)
else login
BE->>DB: get_user_by_email(email)
DB-->>BE: user doc / None
BE->>BE: verify_password (or dummy check)
end
BE->>BE: create_access_token(subject=email)
BE-->>FE: { access_token, token_type, user }
FE->>Cookie: store token cookie
FE->>User: redirect /analyze
end
sequenceDiagram
participant Browser
participant MW as Next Middleware
participant Page as Next Page (/analyze)
participant BE as FastAPI
rect rgb(240,255,235)
Note over Browser,BE: Protected route access
Browser->>MW: Request /analyze
alt token present
MW->>Page: allow
Page->>BE: call protected API (Authorization header)
BE->>BE: decode_token & get_current_user
alt token valid
BE-->>Page: 200 + data
else invalid/expired
BE-->>Page: 401
end
else no token
MW-->>Browser: redirect /login?next=/analyze
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 11
🧹 Nitpick comments (7)
frontend/components/ui/toaster.tsx (1)
12-12: Remove unused import.The
AlertTriangleicon is imported but never used in the component.Apply this diff to remove the unused import:
-import { CheckCircle, XCircle, Info, AlertTriangle } from "lucide-react" +import { CheckCircle, XCircle, Info } from "lucide-react"backend/app/models/user.py (1)
10-10: Add password validation constraints.Consider adding minimum length requirements to the password field to enforce stronger passwords.
Apply this diff to add basic password validation:
class UserCreate(BaseModel): name: str email: EmailStr - password: str + password: str = Field(min_length=8, description="Password must be at least 8 characters")backend/app/utils/auth.py (1)
39-45: Add exception chaining for better debugging.Raising exceptions without
from errorfrom Noneloses the original traceback, making debugging more difficult.Apply this diff:
def decode_token(token: str) -> dict: try: return jwt.decode(token, JWT_SECRET, algorithms=[ALGORITHM]) except jwt.ExpiredSignatureError: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired") + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired") from None except jwt.PyJWTError: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") from Nonebackend/app/routes/auth.py (1)
22-24: Consider removing redundant email check.Lines 22-24 check for an existing user, but
create_useralready performs this check (see backend/app/db/user_store.py lines 18-21). The duplicate check is redundant.You can simplify by relying on
create_userto raiseValueError:@router.post("/signup") async def signup(body: SignupRequest): - existing = await get_user_by_email(body.email) - if existing: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered") user = User(name=body.name, email=body.email, hashed_password=hash_password(body.password)) - user = await create_user(user) + try: + user = await create_user(user) + except ValueError: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered") token = create_access_token(user.email)However, keeping the explicit check may improve readability and avoids relying on exception handling for flow control.
backend/app/db/mongo.py (2)
16-24: Remove unusedappparameter.The
appparameter ininit_mongois unused. If it's not needed for future FastAPI integration, remove it for clarity.Apply this diff:
-def init_mongo(app=None) -> None: +def init_mongo() -> None: global _client, _db
16-24: Add connection error handling and configuration.
init_mongodoesn't handle connection failures. If MongoDB is unavailable, the app will crash with an unhandled exception. Additionally, connection pool settings are not configured.Consider adding error handling and connection options:
def init_mongo() -> None: global _client, _db if _client is None: uri = get_mongo_uri() try: _client = AsyncIOMotorClient( uri, serverSelectionTimeoutMS=5000, # 5 second timeout maxPoolSize=50, minPoolSize=10 ) db_name = os.getenv("MONGODB_DB", "perspective") _db = _client[db_name] except Exception as e: raise RuntimeError(f"Failed to connect to MongoDB: {e}") from eThis ensures graceful failure with clear error messages and configures connection pooling for production use.
frontend/components/auth-form.tsx (1)
117-137: Add client-side password strength validation.The password field has no client-side validation for strength requirements (length, complexity). While server-side validation is essential, client-side checks improve UX by providing immediate feedback.
Add validation before the password input:
const [passwordError, setPasswordError] = useState(""); const validatePassword = (pwd: string) => { if (pwd.length < 8) { setPasswordError("Password must be at least 8 characters"); return false; } setPasswordError(""); return true; }; // In the password Input component: <Input id="password" type={showPassword ? "text" : "password"} placeholder="••••••••" value={password} onChange={(e) => { setPassword(e.target.value); if (isSignUp) validatePassword(e.target.value); }} required minLength={8} /> {passwordError && <p className="text-sm text-red-500">{passwordError}</p>}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
backend/package-lock.jsonis excluded by!**/package-lock.jsonbackend/uv.lockis excluded by!**/*.lock
📒 Files selected for processing (20)
backend/app/.gitignore(1 hunks)backend/app/db/mongo.py(1 hunks)backend/app/db/user_store.py(1 hunks)backend/app/models/__init__.py(1 hunks)backend/app/models/user.py(1 hunks)backend/app/modules/vector_store/embed.py(2 hunks)backend/app/routes/auth.py(1 hunks)backend/app/routes/routes.py(2 hunks)backend/app/utils/auth.py(1 hunks)backend/main.py(2 hunks)backend/pyproject.toml(1 hunks)frontend/app/analyze/page.tsx(2 hunks)frontend/app/layout.tsx(2 hunks)frontend/app/login/page.tsx(1 hunks)frontend/app/page.tsx(2 hunks)frontend/components/auth-form.tsx(1 hunks)frontend/components/profile-menu.tsx(1 hunks)frontend/components/ui/toast.tsx(3 hunks)frontend/components/ui/toaster.tsx(2 hunks)frontend/middleware.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (9)
frontend/app/layout.tsx (1)
frontend/components/ui/toaster.tsx (1)
Toaster(19-70)
frontend/app/page.tsx (1)
frontend/components/profile-menu.tsx (1)
ProfileMenu(16-114)
backend/main.py (1)
backend/app/db/mongo.py (2)
init_mongo(16-23)close_mongo(26-29)
backend/app/db/user_store.py (2)
backend/app/models/user.py (1)
User(13-18)backend/app/db/mongo.py (1)
get_db(32-35)
frontend/app/login/page.tsx (2)
frontend/app/layout.tsx (1)
metadata(10-13)frontend/components/auth-form.tsx (1)
AuthForm(15-160)
backend/app/routes/routes.py (5)
backend/app/modules/pipeline.py (2)
run_scraper_pipeline(48-64)run_langgraph_workflow(67-71)backend/app/modules/bias_detection/check_bias.py (1)
check_bias(38-82)backend/app/modules/chat/get_rag_data.py (1)
search_pinecone(38-50)backend/app/modules/chat/llm_processing.py (1)
ask_llm(45-65)backend/app/utils/auth.py (1)
get_current_user(48-51)
backend/app/routes/auth.py (3)
backend/app/models/user.py (3)
User(13-18)UserCreate(7-10)UserPublic(21-25)backend/app/db/user_store.py (2)
get_user_by_email(8-16)create_user(19-33)backend/app/utils/auth.py (3)
hash_password(20-22)verify_password(25-26)create_access_token(29-36)
frontend/app/analyze/page.tsx (2)
frontend/components/theme-toggle.tsx (1)
ThemeToggle(14-42)frontend/components/profile-menu.tsx (1)
ProfileMenu(16-114)
frontend/components/ui/toaster.tsx (1)
frontend/components/ui/toast.tsx (3)
Toast(127-127)ToastTitle(128-128)ToastDescription(129-129)
🪛 Ruff (0.14.2)
backend/app/modules/vector_store/embed.py
46-46: Consider moving this statement to an else block
(TRY300)
47-47: Do not catch blind exception: Exception
(BLE001)
97-99: try-except-pass detected, consider logging the exception
(S110)
97-97: Do not catch blind exception: Exception
(BLE001)
backend/app/db/mongo.py
16-16: Unused function argument: app
(ARG001)
backend/app/db/user_store.py
23-23: Avoid specifying long messages outside the exception class
(TRY003)
29-29: Do not catch blind exception: Exception
(BLE001)
backend/app/routes/routes.py
64-64: Unused function argument: user
(ARG001)
64-64: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
72-72: Unused function argument: user
(ARG001)
72-72: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
80-80: Unused function argument: user
(ARG001)
80-80: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
backend/app/utils/auth.py
14-14: Redefinition of unused CryptContext from line 8
Remove definition: CryptContext
(F811)
43-43: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
45-45: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
48-48: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
🔇 Additional comments (11)
backend/app/.gitignore (1)
1-28: Verify project structure: why doesbackend/app/.gitignorecontain Next.js patterns?The file location (
backend/app/.gitignore) suggests backend-specific ignore rules, but the content is primarily Next.js/Node.js-focused. This raises questions about the project layout:
- If
backend/app/is a Python FastAPI application directory, it should ignore Python artifacts (__pycache__/,*.pyc,venv/,.pytest_cache/,*.egg-info/, etc.), not Next.js patterns.- If
backend/app/contains the Next.js frontend, the location seems misnamed.- If
backend/app/is a mixed monorepo directory, the rules here should cover both ecosystems.Could you clarify the project structure and confirm whether:
- There's a separate
.gitignoreat the repository root or in a frontend-specific directory?- Python backend artifacts are being ignored elsewhere?
- This file is the sole
.gitignorefor the entire project?
Assuming
backend/app/includes Python code: If Python backend files exist in this directory, add common Python artifacts:+ # python + __pycache__/ + *.py[cod] + *$py.class + *.so + .Python + env/ + venv/ + .venv + *.egg-info/ + .pytest_cache/ + .coveragebackend/app/models/__init__.py (1)
1-1: LGTM!The package initializer is properly documented.
frontend/app/layout.tsx (1)
6-6: LGTM!The Toaster is correctly imported and placed within the ThemeProvider for global toast notifications across the application.
Also applies to: 32-32
frontend/app/page.tsx (1)
23-23: LGTM!The ProfileMenu is correctly integrated into the header alongside the ThemeToggle, providing authentication-aware UI.
Also applies to: 115-118
frontend/app/analyze/page.tsx (1)
27-27: LGTM!The ProfileMenu is correctly integrated into the analyze page header, maintaining consistency with the landing page.
Also applies to: 106-109
frontend/components/ui/toaster.tsx (1)
24-61: LGTM!The variant-based icon and styling logic is well-implemented, providing clear visual feedback for different toast types. The use of
aria-hiddenon the decorative badge is correct.frontend/middleware.ts (1)
4-13: <!-- [scratchpad_start]
[task verify_backend_auth_2/2]
[observations]
- Previous search found backend/app/utils/auth.py with proper JWT validation functions
- Previous search found backend/app/routes/routes.py with three protected routes
- Current attempts to search backend/app/routes/ directory are failing - path may not exist
- Routes were found earlier in backend/app/routes/routes.py (single file, not directory)
[analysis]
- Routes are in a single file (routes.py) not a directory
- Need to search within the correct file path
[pending]
- Find complete directory structure
- Verify all routes in the actual routes file
- Check if there are any unprotected routes
[actions]
- Use fd to find all Python files in backend
- Use ast-grep or rg to search routes.py directly without directory assumption
backend/app/utils/auth.py (1)
20-26: LGTM!The use of
bcrypt_sha256is appropriate for secure password hashing and handles long passwords safely.frontend/components/ui/toast.tsx (1)
6-6: LGTM!The enhanced toast variants with icons and gradients improve user feedback clarity. The consistent top-right positioning provides a better UX.
Also applies to: 19-20, 29-38
frontend/components/auth-form.tsx (2)
35-65: LGTM!The error handling provides clear, user-friendly messages tailored to different failure scenarios (invalid credentials, existing email, etc.), improving the user experience.
24-87: LGTM!The form submission logic correctly handles both signup and login flows, provides appropriate loading states, and gives clear success feedback before navigation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
frontend/components/profile-menu.tsx (1)
24-56: Handle invalid JWTs by resetting authentication state (and optionally clearing the cookie).If
tokenis present but decoding fails, you only callsetUserEmail("")and leaveisAuthenticatedastrue. That means the UI can still render the authenticated menu even though the token is invalid or corrupted.Consider only setting
isAuthenticated(true)after successful decode, and on any decode/parse failure reset bothisAuthenticatedanduserEmail(and optionally clear thetokencookie) so the UI and auth state stay consistent.- if (token) { - setIsAuthenticated(true); - // Robust base64url decoding for JWT payload - try { + if (token) { + // Robust base64url decoding for JWT payload + try { const part = token.split(".")[1]; if (part) { // base64url -> base64 (replace - and _ then pad) let b64 = part.replace(/-/g, "+").replace(/_/g, "/"); while (b64.length % 4 !== 0) b64 += "="; const json = decodeURIComponent( atob(b64) .split("") .map(c => "%" + c.charCodeAt(0).toString(16).padStart(2, "0")) .join("") ); const payload = JSON.parse(json); setUserEmail(payload.sub || ""); + setIsAuthenticated(true); } else { - setUserEmail(""); + setUserEmail(""); + setIsAuthenticated(false); } } catch (e) { - setUserEmail(""); + setUserEmail(""); + setIsAuthenticated(false); + // Optionally clear an invalid token cookie here. } } else { setIsAuthenticated(false); setUserEmail(""); }backend/app/utils/auth.py (1)
10-43: Critical: remove insecure default JWT secret and fail fast when unset.Using
"change-me"as the defaultJWT_SECRETmeans that if the environment variable is missing in any deployed environment, tokens can be forged by anyone who can read this source. This must not reach production.Read
JWT_SECRETfrom the environment without a default and fail fast if it’s missing, so the app cannot start with an insecure secret.-JWT_SECRET = os.getenv("JWT_SECRET", "change-me") +JWT_SECRET = os.getenv("JWT_SECRET") +if not JWT_SECRET: + # Fail fast rather than running with an insecure default + raise RuntimeError("JWT_SECRET environment variable must be set")Optionally, to aid debugging while keeping tracebacks clear, you can also chain the original PyJWT errors:
-def decode_token(token: str) -> dict: - try: - return jwt.decode(token, JWT_SECRET, algorithms=[ALGORITHM]) - except jwt.ExpiredSignatureError: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired") - except jwt.PyJWTError: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") +def decode_token(token: str) -> dict: + try: + return jwt.decode(token, JWT_SECRET, algorithms=[ALGORITHM]) + except jwt.ExpiredSignatureError as err: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token expired", + ) from err + except jwt.PyJWTError as err: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid token", + ) from err
🧹 Nitpick comments (6)
.vscode/settings.json (1)
1-4: Consider whether VSCode workspace settings belong in the repo.Committing
.vscode/settings.jsonmakes these Python environment settings apply to all contributors. That’s fine if this is a team convention, but otherwise you might want to keep them in local settings or document them instead.backend/app/db/mongo.py (1)
16-38: Mongo lifecycle looks good; tighten API and fix unused-arg warning.
close_mongocorrectly closes the client and resets_client/_db, so subsequentget_db()calls reinitialize a fresh connection — nice.Minor nits:
appininit_mongo(app=None)is unused and triggers Ruff ARG001; rename to_appor drop it if not required by any startup hook.- For type-checkers, it can help to assert
_db is not Nonebefore returning.-def init_mongo(app=None) -> None: +def init_mongo(_app=None) -> None: @@ def get_db() -> AsyncIOMotorDatabase: if _db is None: init_mongo() - return _db + assert _db is not None + return _dbfrontend/components/auth-form.tsx (1)
70-88: Good token handling, but plan to migrate to HttpOnly cookies server-side.The conditional
Secureflag and short Max-Age are good improvements. However, because thetokencookie is set from client-side JS, it remains readable by any script on the page, so an XSS could still exfiltrate JWTs.In a future iteration, consider moving token issuance to the backend and setting an HttpOnly, Secure cookie from the server (with a separate UI-safe identifier if needed) so the main auth token is never accessible to client-side JavaScript.
backend/app/utils/auth.py (1)
46-56: Consider returning more user info fromget_current_user.
get_current_useralready verifies that the user exists in the database, which is great. Right now it returns only{"email": email}; for features like per-user rate limiting or saved data, having the user’s ID (and possibly other non-sensitive fields) available would avoid repeated lookups in route handlers.Not mandatory for this PR, but consider expanding the returned payload (e.g.,
{"id": user.id, "email": user.email}) and updating dependent routes accordingly.backend/app/routes/auth.py (2)
20-27: Password strength check is minimal; consider tightening policy.
_validate_password_strengthcurrently only enforces a minimum length of 8 characters. That’s better than nothing, but still allows very weak passwords.If you want stronger defaults, consider adding checks for mixed character classes (e.g., at least one uppercase, one lowercase, one digit, one non-alphanumeric) or at least documenting the chosen policy so clients know what’s required.
41-52: Simplify timing-attack mitigation and avoid per-request dummy hashing.The login flow correctly avoids short-circuiting based on user existence, but it computes a dummy hash on every request and re-imports
hash_password/verify_passwordlocally, which is unnecessary work and makes the comment about “pre-generated” dummy hash inaccurate.You can precompute a dummy hash once at module import and reuse the already-imported helpers:
-from app.utils.auth import hash_password, verify_password, create_access_token +from app.utils.auth import hash_password, verify_password, create_access_token @@ -@router.post("/login") -async def login(body: LoginRequest): - # Timing attack mitigation: - # Always perform a password verification step even if user does not exist. - user = await get_user_by_email(body.email) - # Pre-generated dummy hash (bcrypt_sha256 of a constant) ensures constant-time path. - # We generate it lazily to avoid import-time work. - from app.utils.auth import hash_password as _hp, verify_password as _vp # local import to avoid circularity - dummy_hash = _hp("__dummy_constant_password__") - hashed = user.hashed_password if user else dummy_hash - password_ok = _vp(body.password, hashed) +_DUMMY_HASH = hash_password("__dummy_constant_password__") + + +@router.post("/login") +async def login(body: LoginRequest): + # Timing attack mitigation: + # Always perform a password verification step even if user does not exist. + user = await get_user_by_email(body.email) + hashed = user.hashed_password if user else _DUMMY_HASH + password_ok = verify_password(body.password, hashed)This keeps the constant-time behavior while reducing per-request hashing and removing redundant imports.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (7)
.vscode/settings.json(1 hunks)backend/app/db/mongo.py(1 hunks)backend/app/models/user.py(1 hunks)backend/app/routes/auth.py(1 hunks)backend/app/utils/auth.py(1 hunks)frontend/components/auth-form.tsx(1 hunks)frontend/components/profile-menu.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- backend/app/models/user.py
🧰 Additional context used
🧬 Code graph analysis (2)
backend/app/routes/auth.py (3)
backend/app/models/user.py (3)
User(13-18)UserCreate(7-10)UserPublic(21-25)backend/app/db/user_store.py (2)
get_user_by_email(8-16)create_user(19-33)backend/app/utils/auth.py (3)
hash_password(18-20)verify_password(23-24)create_access_token(27-34)
backend/app/utils/auth.py (1)
backend/app/db/user_store.py (1)
get_user_by_email(8-16)
🪛 Ruff (0.14.5)
backend/app/db/mongo.py
16-16: Unused function argument: app
(ARG001)
backend/app/utils/auth.py
41-41: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
43-43: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
46-46: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
frontend/components/profile-menu.tsx (1)
30-55: Ensure auth state resets when JWT decoding fails.On Line 31,
isAuthenticatedis set totrueas soon as atokenstring exists, and in thecatchblock (Lines 50–52) onlyuserEmailis cleared. If the JWT is malformed, tampered with, or can’t be decoded, the component will still treat the user as authenticated and render the account menu (just without an email).Move
setIsAuthenticated(true)into the successful decode path and set it tofalseboth whenpartis missing and when decoding/parsing throws:- if (token) { - setIsAuthenticated(true); - // Robust base64url decoding for JWT payload - try { + if (token) { + // Robust base64url decoding for JWT payload + try { const part = token.split(".")[1]; if (part) { // base64url -> base64 (replace - and _ then pad) let b64 = part.replace(/-/g, "+").replace(/_/g, "/"); while (b64.length % 4 !== 0) b64 += "="; const json = decodeURIComponent( atob(b64) .split("") - .map(c => "%" + c.charCodeAt(0).toString(16).padStart(2, "0")) + .map((c) => + "%" + c.charCodeAt(0).toString(16).padStart(2, "0"), + ) .join("") ); const payload = JSON.parse(json); - setUserEmail(payload.sub || ""); + setUserEmail(payload.sub || ""); + setIsAuthenticated(true); } else { setUserEmail(""); + setIsAuthenticated(false); } } catch (e) { setUserEmail(""); + setIsAuthenticated(false); } } else { setIsAuthenticated(false); setUserEmail(""); }This way, any decode/parse failure (or structurally invalid token) correctly results in an unauthenticated UI state.
🧹 Nitpick comments (1)
frontend/components/profile-menu.tsx (1)
24-47: Revisit client-side JWT decoding vs secure token storage.Because this component reads and decodes the JWT from a JS-accessible cookie (
document.cookie) to drive auth state and UI, thetokencookie cannot beHttpOnly. That weakens protection against XSS compared to a backend-validated, HttpOnly cookie/session model.Consider, in a follow-up, whether you want to:
- Move JWT verification and user resolution fully server-side (HttpOnly session cookie, frontend only sees a lightweight “session present” flag), or
- Use a less-privileged client-visible token while keeping the real auth token HttpOnly.
This is broader than just this file, but worth aligning with your “secure token storage” objective before production hardening.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
frontend/components/profile-menu.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/components/profile-menu.tsx (2)
backend/app/models/user.py (1)
User(13-18)frontend/components/ui/dropdown-menu.tsx (6)
DropdownMenu(185-185)DropdownMenuTrigger(186-186)DropdownMenuContent(187-187)DropdownMenuLabel(191-191)DropdownMenuSeparator(192-192)DropdownMenuItem(188-188)
🔇 Additional comments (2)
frontend/components/profile-menu.tsx (2)
62-77: Logout flow and cookie clearing look sound.The logout handler now mirrors the
Secureflag logic from login, clears thetokencookie with matching attributes, resets local auth state, shows a toast, and routes to/. This is consistent and should reliably log users out across HTTP/HTTPS.
79-129: Conditional rendering and menu UI are reasonable.Hiding the menu on
/login, showing a simple login button when unauthenticated, and switching to an accessible dropdown with the user icon and email when authenticated is clean and intuitive. ARIA labels and focus styles are in place, and usingDropdownMenuTrigger asChildis aligned with your UI primitives.
Description
Fixes: #119
This PR implements a comprehensive user authentication and authorization system for the Perspective project, enabling user registration, login, session management, and protected routes. This addresses a key item from the project's future work roadmap and makes the application production-ready.
Demo
Screen.Recording.2025-11-03.134359.mp4
Checklist
Additional Notes
Environment Variables Added:
Future Enhancements:
Summary by CodeRabbit
New Features
Style
✏️ Tip: You can customize this high-level summary in your review settings.