Skip to content

Conversation

@harshitaphadtare
Copy link

@harshitaphadtare harshitaphadtare commented Nov 3, 2025

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

  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • My changes generate no new warnings.
  • I have verified that no API keys or other secrets are committed.

Additional Notes

Environment Variables Added:

GROQ_API_KEY=<your-groq-key>
PINECONE_API_KEY=<your-vector-database-key>
SEARCH_KEY=<your-search-key>
JWT_SECRET==<jwt-secret>
MONGODB_URI=mongodb+srv://<user>:<password>@cluster0.t5zy6pj.mongodb.net/?appName=Cluster0
MONGODB_DB=perspective_mongodb

Future Enhancements:

  • OAuth integration (Google, GitHub)
  • Email verification workflow
  • Password reset functionality
  • Two-factor authentication (2FA)

Summary by CodeRabbit

  • New Features

    • User signup/login with JWT sessions, protected API routes, and DB-backed user persistence.
    • Login page and client auth form with validation, token storage, and redirect after auth.
    • Profile menu in header showing account info and logout.
    • App-level startup/shutdown hooks to manage DB connection.
  • Style

    • Redesigned toast notifications with per-variant icons, colors, and improved layout.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 3, 2025

Walkthrough

Adds 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

Cohort / File(s) Summary
Gitignore & Project deps
backend/app/.gitignore, backend/pyproject.toml
Added ignores for env and build artifacts; added dependencies: pyjwt, passlib[bcrypt], bcrypt, pydantic[email], motor.
MongoDB client
backend/app/db/mongo.py
New motor AsyncIOMotorClient helpers: get_mongo_uri, init_mongo, close_mongo, get_db with lazy init and env-driven config.
User store
backend/app/db/user_store.py
New Mongo-backed user ops: get_user_by_email(email) and create_user(user) with uniqueness check and created_at normalization.
User models
backend/app/models/__init__.py, backend/app/models/user.py
New Pydantic models: UserCreate, User, UserPublic (UUID id, created_at defaults).
Auth utilities
backend/app/utils/auth.py
New password hashing/verification (passlib bcrypt_sha256), JWT create/decode, get_current_user FastAPI dependency using HTTPBearer.
Auth routes
backend/app/routes/auth.py
New /signup and /login endpoints issuing JWTs and returning public user payloads; exports router.
Route integration & lifecycle
backend/app/routes/routes.py, backend/main.py
Injected get_current_user dependency into API handlers, included auth router at /api/auth, and added MongoDB startup/shutdown handlers.
Embedding robustness
backend/app/modules/vector_store/embed.py
Lazy-load embedder, env model override, SHA256 deterministic fallback embedder, input validation, and numpy->list handling.
Frontend: auth UI & pages
frontend/components/auth-form.tsx, frontend/app/login/page.tsx, frontend/app/analyze/page.tsx, frontend/app/page.tsx
Added AuthForm component and login page; integrated ProfileMenu into headers; minor header spacing tweaks.
Frontend: profile menu
frontend/components/profile-menu.tsx
New ProfileMenu component reading/decoding JWT from cookie, shows user email, logout handling, hides on /login.
Frontend: toast & toaster UI
frontend/components/ui/toast.tsx, frontend/components/ui/toaster.tsx
Added per-variant styles, icons, gradient backgrounds, top-right viewport, and variant-specific badge/icon rendering.
Frontend: root layout & toaster
frontend/app/layout.tsx
Injected <Toaster /> into RootLayout's ThemeProvider.
Frontend: middleware
frontend/middleware.ts
New Next.js middleware that redirects unauthenticated requests for /analyze/:path* to /login?next=....
Editor settings
.vscode/settings.json
Added VSCode workspace settings for Python env manager defaults.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Areas needing extra attention:
    • backend/app/utils/auth.py — JWT claim handling, secret/expiry defaults, error mapping to HTTP responses.
    • backend/app/db/mongo.py and backend/main.py — startup/shutdown lifecycle correctness and idempotence.
    • backend/app/modules/vector_store/embed.py — fallback embedder determinism, numpy output handling, and input validation.
    • backend/app/routes/auth.py & backend/app/db/user_store.py — race conditions on email uniqueness and error handling.
    • frontend/components/auth-form.tsx & frontend/middleware.ts — cookie attributes, secure flag behavior, redirect next preservation.

Possibly related PRs

Poem

🐰 I hopped through code at break of dawn,

I salted hashes on the lawn,
I tucked tokens safe in cookie nooks,
I whispered logs and closed my books,
Now users log in — carrot-themed yawn. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Linked Issues check ❓ Inconclusive The PR implements most core authentication/authorization requirements from #119: user registration/login endpoints, JWT token creation, password hashing (bcrypt), MongoDB integration, protected routes via middleware, and frontend authentication UI with token storage and protected routes. However, email verification, token refresh mechanism, per-user rate limiting, user profile management, and session auto-refresh features are not yet implemented. Consider documenting which requested features (email verification, token refresh, per-user rate limiting, profile management, auto-refresh) are deferred to future PRs, and ensure subsequent work addresses these gaps systematically.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding user authentication and authorization to the application, which directly matches the comprehensive system implementation across backend, frontend, and database layers in the changeset.
Out of Scope Changes check ✅ Passed All code changes are directly aligned with the authentication/authorization system requirements from #119. Changes include backend auth routes, user models, MongoDB integration, password utilities, frontend auth UI, protected routes middleware, and supporting dependencies—all essential for implementing the requested feature.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 AlertTriangle icon 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 err or from None loses 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 None
backend/app/routes/auth.py (1)

22-24: Consider removing redundant email check.

Lines 22-24 check for an existing user, but create_user already 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_user to raise ValueError:

 @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 unused app parameter.

The app parameter in init_mongo is 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_mongo doesn'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 e

This 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

📥 Commits

Reviewing files that changed from the base of the PR and between 19c83ed and 9dcdaf4.

⛔ Files ignored due to path filters (2)
  • backend/package-lock.json is excluded by !**/package-lock.json
  • backend/uv.lock is 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 does backend/app/.gitignore contain 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:

  1. There's a separate .gitignore at the repository root or in a frontend-specific directory?
  2. Python backend artifacts are being ignored elsewhere?
  3. This file is the sole .gitignore for 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/
+ .coverage
backend/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-hidden on 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_sha256 is 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 token is present but decoding fails, you only call setUserEmail("") and leave isAuthenticated as true. 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 both isAuthenticated and userEmail (and optionally clear the token cookie) 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 default JWT_SECRET means 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_SECRET from 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.json makes 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_mongo correctly closes the client and resets _client/_db, so subsequent get_db() calls reinitialize a fresh connection — nice.

Minor nits:

  • app in init_mongo(app=None) is unused and triggers Ruff ARG001; rename to _app or drop it if not required by any startup hook.
  • For type-checkers, it can help to assert _db is not None before 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 _db
frontend/components/auth-form.tsx (1)

70-88: Good token handling, but plan to migrate to HttpOnly cookies server-side.

The conditional Secure flag and short Max-Age are good improvements. However, because the token cookie 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 from get_current_user.

get_current_user already 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_strength currently 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_password locally, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9dcdaf4 and 1388a9c.

⛔ Files ignored due to path filters (1)
  • package-lock.json is 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)

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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, isAuthenticated is set to true as soon as a token string exists, and in the catch block (Lines 50–52) only userEmail is 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 to false both when part is 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, the token cookie cannot be HttpOnly. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1388a9c and 1b26f50.

📒 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 Secure flag logic from login, clears the token cookie 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 using DropdownMenuTrigger asChild is aligned with your UI primitives.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feat: Add User Authentication and Authorization System (Signup/Login)

1 participant