Skip to content
This repository was archived by the owner on May 12, 2026. It is now read-only.

Feat(auth): Implement Signup Authentication Flow - #4

Merged
CyberwizD merged 22 commits into
devfrom
feat/auth-signup
May 6, 2026
Merged

CyberwizD merged 22 commits into
devfrom
feat/auth-signup

Conversation

@Afeh

@Afeh Afeh commented May 6, 2026

Copy link
Copy Markdown
Collaborator

Description

Implemented a secure, production-ready user registration (/signup) flow. This PR also establishes modern security best practices for token management using httpOnly cookies and transitions the codebase toward explicit domain-driven exception handling.

Type of Change

  • feat — New feature
  • fix — Bug fix
  • refactor — Code refactoring (no functional change)
  • docs — Documentation update
  • test — Adding or updating tests
  • chore — Maintenance (dependencies, CI, tooling)

Related Issue

Closes AUTH-SU-01-BE

Changes Made

  • Secure Authentication Flow: Added a signup endpoint that hashes user passwords securely via bcrypt and provisions two distinct tokens upon successful account creation.
  • Hybrid Token Architecture:
  • Access Token: An short-lived, stateless JSON Web Token (JWT) containing basic user claims.
  • Refresh Token: A long-lived, highly secure, stateful Opaque Token generated via secrets.token_urlsafe. A SHA-256 hash of this token is stored in the database for secure session revocation and tracking. Created a RefreshToken model and created alembic migrations to this effect
  • Enhanced Client Security (httpOnly Cookies): Configured both tokens to be delivered to the client inside httpOnly, Secure, and SameSite=Lax cookies, effectively shielding the system from Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF) vulnerabilities.
  • Domain Exception Handling: Refactored the error-handling layer to catch domain-specific exceptions (e.g., UserAlreadyExistsException) and translate them cleanly into appropriate FastAPI HTTPException responses, preventing internal server tracebacks from leaking to clients.

How to Verify

  • Ensure dependencies are in sync: uv sync
  • Run database migrations: uv run alembic upgrade head
  • Execute the auth test suite: uv run pytest tests/test_auth.py

Proof of Work

API Response / Screenshots
// POST /api/v1/auth/signup
// Status: 201 Created
{
  "status_code": 201,
  "message": "Account created successfully",
  "data": {
    "id": "019dfef6-94ce-7603-aec0-2d6d259ecab5",
    "email": "newuser@gmail.com",
    "name": "New User",
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwMTlkZmVmNi05NGNlLTc2MDMtYWVjMC0yZDZkMjU5ZWNhYjUiLCJuYW1lIjoiTmV3IFVzZXIiLCJlbWFpbCI6Im5ld3VzZXJAZ21haWwuY29tIiwiZXhwIjoxNzc4MDk5MjQ2LCJpYXQiOjE3NzgwOTkwNjYsInR5cGUiOiJhY2Nlc3MifQ.YvZeXDnuoQ1y5a_wzoFPo2kd42lBud0xDRkuVAKbpZM",
    "refresh_token": "fkpQ6enW_yG8HGzwjVwL0XCD0_FlZfipEu6LidJCBgs2ZYJWLN6tQWynAOnmmX6X"
  }
}

// POST /api/v1/auth/signup
// Status: 422 Unprocessable Content
{
  "status_code": 422,
  "message": "Invalid input for body -> password: Value error, Password must contain at least one uppercase letter"
}

// POST /api/v1/auth/signup
// Status: 400 Bad Request
{
  "status_code": 400,
  "message": "Email 'newuserx@gmail.com' is already registered."
}

Screenshot from 2026-05-06 21-25-04 Screenshot from 2026-05-06 22-39-51 Screenshot from 2026-05-06 22-40-23

Test Cases

This PR introduces a comprehensive suite of unit tests for the /api/v1/auth/signup endpoint. These tests use mocking to isolate the endpoint layer, ensuring that request validation, response formatting, and error handling are correct.

Success Scenarios

  • test_signup_returns_201_on_valid_payload and correct response

Duplicate Email Scenarios

  • test_signup_returns_400_when_email_is_duplicate

Input Validation: name

  • test_signup_returns_422_when_name_is_missing

Input Validation: email

  • test_signup_returns_422_when_email_is_missing
  • test_signup_returns_422_when_email_format_is_invalid

Input Validation: password

  • test_signup_returns_422_when_password_is_missing
  • test_signup_returns_422_when_password_is_too_short
  • test_signup_returns_422_when_password_lacks_uppercase
  • test_signup_returns_422_when_password_lacks_lowercase
  • test_signup_returns_422_when_password_lacks_digit
  • test_signup_succeeds_when_password_is_at_min_length
Test output
================================================================ test session starts =================================================================
platform linux -- Python 3.13.0, pytest-9.0.3, pluggy-1.6.0 -- /home/afebu/Documents/ProgrammingStuff/work/meetmind-be/.venv/bin/python3
cachedir: .pytest_cache
rootdir: /home/afebu/Documents/ProgrammingStuff/work/meetmind-be
configfile: pyproject.toml
plugins: anyio-4.13.0, asyncio-1.3.0
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 12 items                                                                                                                                   

tests/test_auth.py::TestSignupSuccess::test_response_body_shape[asyncio] PASSED                                                                [  8%]
tests/test_auth.py::TestSignupDuplicateEmail::test_returns_400_when_email_already_registered[asyncio] PASSED                                   [ 16%]
tests/test_auth.py::TestSignupServerError::test_returns_500_on_unexpected_exception[asyncio] PASSED                                            [ 25%]
tests/test_auth.py::TestSignupNameValidation::test_missing_name_returns_422[asyncio] PASSED                                                    [ 33%]
tests/test_auth.py::TestSignupEmailValidation::test_missing_email_returns_422[asyncio] PASSED                                                  [ 41%]
tests/test_auth.py::TestSignupEmailValidation::test_invalid_email_format_returns_422[asyncio] PASSED                                           [ 50%]
tests/test_auth.py::TestSignupPasswordValidation::test_missing_password_returns_422[asyncio] PASSED                                            [ 58%]
tests/test_auth.py::TestSignupPasswordValidation::test_password_too_short_returns_422[asyncio] PASSED                                          [ 66%]
tests/test_auth.py::TestSignupPasswordValidation::test_password_without_uppercase_returns_422[asyncio] PASSED                                  [ 75%]
tests/test_auth.py::TestSignupPasswordValidation::test_password_without_lowercase_returns_422[asyncio] PASSED                                  [ 83%]
tests/test_auth.py::TestSignupPasswordValidation::test_password_without_digit_returns_422[asyncio] PASSED                                      [ 91%]
tests/test_auth.py::TestSignupPasswordValidation::test_password_at_min_length_is_accepted[asyncio] PASSED                                      [100%]

================================================================= 12 passed in 0.09s =================================================================
Screenshot from 2026-05-06 22-39-03

Checklist

  • My branch follows the naming convention (<type>/<short-description>)
  • My commits follow Conventional Commits
  • I have added meaningful tests that cover success and failure paths
  • All new and existing tests pass locally (uv run pytest)
  • I have included proof of work (JSON responses or screenshots)
  • I have updated documentation if needed
  • My code follows the project's style guidelines

Summary by CodeRabbit

  • New Features

    • Added user signup and registration endpoint with JWT-based authentication
    • Implemented access and refresh token generation with secure HttpOnly cookie storage
    • Added password validation requirements (minimum 8 characters, uppercase, lowercase, and digit)
  • Documentation

    • Updated development server startup instructions
  • Tests

    • Added comprehensive test suite for signup and authentication flows

@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@Afeh has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 20 minutes and 48 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b720f29b-9a1d-4407-babb-12c47fcd8f8a

📥 Commits

Reviewing files that changed from the base of the PR and between 1a252f8 and 8f0d58d.

📒 Files selected for processing (6)
  • app/api/v1/endpoints/auth.py
  • app/core/config.py
  • app/main.py
  • app/schemas/auth.py
  • app/services/auth.py
  • tests/conftest.py
📝 Walkthrough

Walkthrough

Adds a signup flow and JWT-based auth: new refresh token DB table and model, Alembic migrations, AuthService (password hashing, JWT create/verify, refresh token storage), signup API endpoint, Pydantic schemas and exceptions, config/env entries, tests, and CI/dev docs tweaks.

Changes

User Signup and Authentication

Layer / File(s) Summary
Configuration & Dependencies
pyproject.toml, .env.example, app/core/config.py
Added bcrypt, psycopg2-binary, python-jose; added TEST_DATABASE_URL, JWT_SECRET, JWT_ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES, REFRESH_TOKEN_EXPIRE_MINUTES.
Database Migrations
alembic/versions/d44e91e81013_add_refresh_token_table.py, alembic/versions/8d114ef61fcc_made_datetime_fields_in_refresh_token_.py
Created refresh_tokens table with FK to users, token_hash, expires_at, revoked, created_at; added created_at to existing tables; converted refresh_tokens timestamp columns to timezone-aware DateTime.
Models
app/models/user.py
Added RefreshToken SQLAlchemy model (UUID PK, user_id FK, token_hash, expires_at, revoked, created_at).
Domain Exceptions & Schemas
app/core/exceptions.py, app/schemas/auth.py
Added AppBaseException and UserAlreadyExistsException; introduced SignupRequest, SignupResponseData, SignupResponse, and ErrorResponse Pydantic models with input validation.
Service Implementation
app/services/auth.py
Added AuthService with password hashing/verification (bcrypt), email-existence check, create_user, JWT access token creation/decoding, refresh token generation, hashing and persistence.
API & Wiring
app/api/v1/endpoints/auth.py, app/api/v1/router.py
Added /signup endpoint that creates user, generates tokens, sets HttpOnly cookies, returns structured response; mounted auth router under /auth.
App-level Error Handling
app/main.py
Added handlers for HTTPException and RequestValidationError returning ErrorResponse JSON.
Tests & Test Setup
tests/conftest.py, tests/test_auth.py, tests/test_models.py
Replaced test DB URL wiring to use TEST_DATABASE_URL; added comprehensive signup tests (success, validation, duplicate, server error); updated expected table set/count for new refresh_tokens.
Docs / Misc
CONTRIBUTING.md, docs/architecture/system-overview.md
Changed dev server command and mermaid layout/classDefs.

Sequence Diagram

sequenceDiagram
    actor User
    participant Client
    participant API as "API (/auth/signup)"
    participant AuthSvc as "AuthService"
    participant DB as Database
    participant Resp as "Response Handler"

    User->>Client: Submit signup (name, email, password)
    Client->>API: POST /auth/signup
    API->>AuthSvc: check_email_exists(email)
    AuthSvc->>DB: SELECT user WHERE email=...
    DB-->>AuthSvc: user or none

    alt email exists
        AuthSvc-->>API: raise UserAlreadyExistsException
        API-->>Resp: HTTP 400 ErrorResponse
        Resp-->>Client: 400
    else email unique
        API->>AuthSvc: create_user(name,email,password)
        AuthSvc->>AuthSvc: hash_password(password)
        AuthSvc->>DB: INSERT user
        DB-->>AuthSvc: saved user
        API->>AuthSvc: create_access_token(user)
        AuthSvc-->>API: access_token (JWT)
        API->>AuthSvc: create_refresh_token(user)
        AuthSvc->>AuthSvc: generate raw token, hash
        AuthSvc->>DB: INSERT refresh_token (hash, expires_at)
        DB-->>AuthSvc: saved refresh token
        API->>Resp: set HttpOnly cookies (access, refresh)
        API-->>Client: 201 SignupResponse (user + tokens)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Feat(auth): Implement Signup Authentication Flow' clearly summarizes the main feature addition of a signup endpoint and authentication mechanism.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description is comprehensive and well-structured, following the template with all major sections completed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/auth-signup

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@Afeh
Afeh requested a review from CyberwizD May 6, 2026 20:40

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a user signup flow, introducing a new authentication service, database migrations for refresh tokens, and a dedicated signup endpoint. The implementation includes password hashing with bcrypt, JWT-based token generation, and custom exception handling for API robustness. Feedback focuses on improving the reliability and security of the new code. Key recommendations include logging exceptions for better observability, hardening exception handlers against potential indexing or type errors, and replacing brittle regex-based XSS prevention with standard sanitization libraries. Additionally, the reviewer suggests using db.flush() instead of db.commit() within service methods to ensure transactional atomicity and improving type hints for better static analysis.

except UserAlreadyExistsException as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))

except Exception as e:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Catching all exceptions and returning a generic 500 error is appropriate for production security to avoid leaking internal details. However, the actual exception should be logged here for observability. Without logging, it will be nearly impossible to diagnose unexpected failures in the signup flow in a production environment.

Comment thread app/main.py Outdated
status_code=exc.status_code,
content=ErrorResponse(
status_code=exc.status_code,
message=exc.detail

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The exc.detail attribute in FastAPI's HTTPException can be a string, a list, or a dictionary. Since the ErrorResponse schema explicitly expects a str for the message field, passing a non-string detail will cause a validation error within this exception handler, resulting in an unhandled 500 error. Explicitly casting to str ensures robustness.

Suggested change
message=exc.detail
message=str(exc.detail)

Comment thread app/main.py Outdated
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
errors = exc.errors()
first_error = errors[0]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Accessing errors[0] without verifying that the errors list is non-empty could lead to an IndexError. While Pydantic validation errors usually contain details, defensive programming here prevents the exception handler from crashing if an unexpected validation state occurs.

Suggested change
first_error = errors[0]
first_error = errors[0] if errors else {"loc": ["body"], "msg": "Unknown validation error"}

Comment thread app/schemas/auth.py Outdated
Comment on lines +16 to +17
if re.search(r'<[^>]+>', v):
raise ValueError('Name contains unsafe characters')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The regex r'<[^>]+>' is a brittle and incomplete method for preventing XSS. It can be easily bypassed (e.g., using javascript: URIs or event handlers like onmouseover) and may also block legitimate user input. Per security best practices, XSS should be mitigated by proper output escaping or by using a dedicated sanitization library like bleach if HTML input is required.

Comment thread app/services/auth.py
@@ -0,0 +1,89 @@
import hashlib

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The uuid module needs to be imported to support the suggested type hint improvement for user_id in the create_refresh_token method.

Suggested change
import hashlib
import uuid
import hashlib

Comment thread app/services/auth.py Outdated
Comment on lines +53 to +54
await db.commit()
await db.refresh(user)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Performing db.commit() inside service methods breaks transactional atomicity. If the signup flow fails after create_user succeeds (e.g., during token generation), the user will remain in the database in an inconsistent state. It is better to use db.flush() to persist changes to the session and let the endpoint handler perform a single db.commit() for the entire unit of work.

Suggested change
await db.commit()
await db.refresh(user)
await db.flush()

Comment thread app/services/auth.py Outdated
return jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM])

@staticmethod
async def create_refresh_token(db: AsyncSession, user_id: str) -> str:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The user_id parameter should be typed as uuid.UUID to match the database model and the actual type being passed from the endpoint. This improves static analysis and code clarity.

Suggested change
async def create_refresh_token(db: AsyncSession, user_id: str) -> str:
async def create_refresh_token(db: AsyncSession, user_id: uuid.UUID) -> str:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 21

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.env.example:
- Around line 1-2: Update the .env.example so TEST_DATABASE_URL does not point
to the same production DSN as DATABASE_URL: change TEST_DATABASE_URL to
reference a separate test database (e.g., change the database name suffix to
postgres_test) and add a note in README/CONTRIBUTING calling out that
TEST_DATABASE_URL must point to a dedicated test DB to avoid destructive
operations on dev/staging; ensure the variables DATABASE_URL and
TEST_DATABASE_URL are clearly named and documented so developers won’t
accidentally reuse the production DSN.
- Around line 5-6: The .env.example uses non-integer placeholder strings for
integer settings which breaks Pydantic validation in app/core/config.py; update
ACCESS_TOKEN_EXPIRE_MINUTES and REFRESH_TOKEN_EXPIRE_MINUTES to sensible integer
placeholders (e.g., 30, 1440) or add commented examples showing integers,
ensuring the variables match the int type expected by the Config parsing so cp
.env.example .env won't fail on startup.

In `@alembic/versions/d44e91e81013_add_refresh_token_table.py`:
- Around line 26-27: The `revoked` column is currently created as nullable with
no default, leading to ambiguous NULL states; update the column definition for
`revoked` in the migration (the sa.Column call that defines 'revoked') to be
non-nullable and supply a server-side default (e.g., replace nullable=True with
nullable=False and add server_default=sa.text('false') or equivalent) so new
rows default to false and revocation filters never see NULL.

In `@app/api/v1/endpoints/auth.py`:
- Around line 21-23: Wrap the signup flow in a single DB transaction so user
creation and refresh-token insertion are atomic: call AuthService.create_user
and AuthService.create_refresh_token inside one transaction boundary (e.g.,
using the db session/transaction context) and commit only once after both
succeed, rolling back on any exception; refactor/remove internal commits from
AuthService.create_user and AuthService.create_refresh_token so they do not
commit independently and ensure AuthService.create_access_token remains pure (no
DB commit) while the endpoint controls the transaction lifecycle.
- Around line 44-53: The SignupResponse currently includes access_token and
refresh_token in SignupResponseData which exposes tokens in JSON; remove
access_token and refresh_token from the SignupResponse/SignupResponseData
payload returned by the signup endpoint (leave id, email, name only) and ensure
the tokens are set as HttpOnly secure cookies in the request handler (e.g.,
where you call create access/refresh tokens) instead of being included in the
response body so cookies remain cookie-only and XSS-resistant.

In `@app/core/config.py`:
- Around line 19-24: Update the Pydantic settings: change TEST_DATABASE_URL from
str to PostgresDsn to get DSN validation like DATABASE_URL; constrain
JWT_ALGORITHM to a fixed set of supported algorithms (use Literal[...] or an
Enum and apply it to the JWT_ALGORITHM field) so typos fail validation early;
add Field(gt=0) to ACCESS_TOKEN_EXPIRE_MINUTES and REFRESH_TOKEN_EXPIRE_MINUTES
to prevent zero/negative values; and enforce a minimum length on JWT_SECRET via
Field(min_length=32) (or constr/min_length) to ensure sufficient secret
strength; adjust imports to include PostgresDsn, Field and Literal/Enum as
needed.

In `@app/models/user.py`:
- Around line 26-28: Add a non-unique index on refresh_tokens.user_id so queries
that filter by user_id are fast: update the model where user_id is declared (the
mapped_column UUID(as_uuid=True) ForeignKey on refresh_tokens.user_id /
Mapped[uuid.UUID]) to include an index=True (or add an Index for that column),
and create a new Alembic revision that calls
op.create_index("ix_refresh_tokens_user_id", "refresh_tokens", ["user_id"]) (and
the matching op.drop_index on downgrade) rather than editing existing
migrations; leave the existing unique index on token_hash untouched.
- Around line 31-34: The schema allows NULLs for revoked and mismatches the
created_at typing: update the model so revoked is non-optional and has a
DB-level default and NOT NULL (change revoked: Mapped[bool] =
mapped_column(Boolean, nullable=False, server_default=text('false')) or
equivalent) and change created_at typing to non-optional (created_at:
Mapped[datetime] not Mapped[datetime | None]) while keeping
mapped_column(DateTime(timezone=True), server_default=func.now(),
nullable=False); also create an Alembic migration to backfill any existing NULL
revoked values, set a server_default for revoked, and alter the column to NOT
NULL in the database before deploying the model change.

In `@app/schemas/auth.py`:
- Line 8: The schema's password Field currently allows up to 255 chars which
conflicts with bcrypt's 72-byte limit; either reduce the Field max_length to 72
and update the description to reflect the bcrypt limit, or keep the longer
schema but modify AuthService.hash_password to pre-hash the plaintext with
SHA-256 (or similar fixed-length digest) and feed that digest to bcrypt so
bcrypt only ever sees a safe fixed-size input; update validation and comments
accordingly and reference the Field named password in the schema and the
AuthService.hash_password method when making the change.

In `@app/services/auth.py`:
- Around line 16-17: The _now function in app/services/auth.py is indented with
a tab which is inconsistent with the file's 4-space indentation; update the
indentation for the _now definition and its return line to use 4 spaces (fix the
leading tab before "return datetime.now(timezone.utc)") so the function
signature and body use spaces consistently.
- Around line 76-82: The create_refresh_token signature incorrectly types
user_id as str even though RefreshToken.user_id and User.id are uuid.UUID;
change the parameter annotation in create_refresh_token to user_id: uuid.UUID
(and add import uuid at the top) so callers see the correct type and static
checks align with the RefreshToken model.
- Around line 70-73: The decode_access_token function currently returns the
decoded payload without asserting the token purpose; modify
auth.decode_access_token to decode the token as it does now and then verify
payload.get("type") == "access", raising a JWTError (or appropriate
authentication exception) if the check fails so only tokens stamped with "type":
"access" are accepted by access-protected endpoints.
- Around line 38-55: In create_user, keep the fast-path check_email_exists but
wrap the db.add/commit/refresh sequence in a try/except that catches
sqlalchemy.exc.IntegrityError (or the Async DB's IntegrityError), calls await
db.rollback() and raises UserAlreadyExistsException(email=request.email); ensure
other exceptions still rollback and re-raise appropriately so the AsyncSession
is not left poisoned; reference the functions/classes: create_user,
AuthService.check_email_exists, User, and UserAlreadyExistsException.
- Around line 24-30: The bcrypt calls in hash_password and verify_password are
synchronous/CPU-bound and currently defined as async functions which will block
the event loop; change them to run the blocking work in a thread (use
asyncio.to_thread) or convert them to regular synchronous functions.
Specifically, update hash_password(password: str) and the `@staticmethod`
verify_password(password: str, hashed: str) so that bcrypt.hashpw and
bcrypt.checkpw are executed via asyncio.to_thread(...) (and import asyncio) OR
make both functions plain def and call them from sync context; ensure return
types remain str and bool respectively.
- Around line 24-26: Update hash_password and verify_password to pre-hash the
incoming password with SHA-256 before calling bcrypt so passwords of any length
are safe; specifically, import base64, compute
hashlib.sha256(password.encode('utf-8')).digest(), base64-encode that digest (or
use the raw digest) and pass the resulting bytes into
bcrypt.hashpw/bcrypt.checkpw (instead of the raw password), and ensure
verify_password mirrors the same pre-hash process before calling bcrypt.checkpw;
no schema change required.

In `@docs/architecture/system-overview.md`:
- Line 115: The Mermaid diagram currently has two merged classDef declarations
(classDef db and classDef decision) joined with a literal "\n" which prevents
proper parsing; update the diagram so each classDef is on its own line by
removing the embedded "\n" and placing "classDef db
fill:`#f0f9ff`,stroke:`#38bdf8`,color:`#0c4a6e`,stroke-dasharray: 3 2" and "classDef
decision fill:`#fff7ed`,stroke:`#fb923c`,color:`#431407`,stroke-width:1px" as separate
lines so Mermaid can apply both styles correctly.

In `@pyproject.toml`:
- Line 15: The dependency "python-jose>=3.5.0" in pyproject.toml should be
migrated to a maintained JWT library; replace that entry with an appropriate
PyJWT pin (e.g., "PyJWT>=2.8") and update any imports/usages that reference
python-jose (search for imports like "from jose import jwt" or "jose.jwt") to
the PyJWT API (standard "import jwt" and jwt.encode/decode call sites), run
tests and adjust call signatures/algorithms where needed; if full JOSE feature
parity is required, consider instead switching to "joserfc" and follow its
migration path.
- Line 14: Remove the unused synchronous Postgres driver entry
"psycopg2-binary>=2.9.12" from pyproject.toml; locate the dependency string in
the project's dependency list (the exact token "psycopg2-binary>=2.9.12") and
delete it, then run a quick search for any references to
psycopg2/psycopg2-binary in the codebase (imports or config) and remove or
replace them if present so only async drivers (asyncpg and sqlalchemy[asyncio])
remain.

In `@tests/conftest.py`:
- Around line 25-29: The fixture currently clears all dependency overrides with
app.dependency_overrides.clear() which can break other tests; instead remove
only the override you set by deleting or popping the get_session entry (the one
assigned to mock_get_session) from app.dependency_overrides in the teardown of
the fixture so other overrides remain intact—locate the assignment
app.dependency_overrides[get_session] = mock_get_session and replace the final
clear() call with a targeted removal of that key.
- Line 4: Replace the use of os.environ.setdefault for DATABASE_URL to ensure
tests always use the test DB: locate the os.environ.setdefault("DATABASE_URL",
settings.TEST_DATABASE_URL) call in tests/conftest.py and change it to
explicitly override DATABASE_URL so it always equals settings.TEST_DATABASE_URL
(i.e., assign/replace os.environ["DATABASE_URL"] with
settings.TEST_DATABASE_URL) to avoid accidentally using a non-test database
during test runs.

In `@tests/test_auth.py`:
- Around line 94-101: Update the test_cookies_set_on_success test to assert the
security attributes of the cookies, not just their presence: after the POST to
SIGNUP_URL with VALID_PAYLOAD (within the same patched
CREATE_USER/CREATE_ACCESS/CREATE_REFRESH context), inspect
response.cookies["access_token"] and response.cookies["refresh_token"] and
assert their HttpOnly flag is set, Secure is set, and SameSite equals the
expected value (e.g. "Lax" or "Strict" per app policy); keep the existing
presence assertions and add these attribute checks so regressions in cookie
flags are caught.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bfecfed5-5526-4bf5-9823-b029030e3b9c

📥 Commits

Reviewing files that changed from the base of the PR and between 70caecb and 5e884ed.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • .env.example
  • CONTRIBUTING.md
  • alembic/versions/8d114ef61fcc_made_datetime_fields_in_refresh_token_.py
  • alembic/versions/d44e91e81013_add_refresh_token_table.py
  • app/api/v1/endpoints/auth.py
  • app/api/v1/router.py
  • app/core/config.py
  • app/core/exceptions.py
  • app/main.py
  • app/models/user.py
  • app/schemas/auth.py
  • app/services/auth.py
  • docs/architecture/system-overview.md
  • pyproject.toml
  • tests/conftest.py
  • tests/test_auth.py
  • tests/test_models.py

Comment thread .env.example Outdated
Comment thread .env.example Outdated
Comment on lines +26 to +27
sa.Column('revoked', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make revocation state non-null with a default.

Line [26] defines revoked as nullable with no default, which creates ambiguous token state (NULL) and can break revocation filters.

Suggested change
-    sa.Column('revoked', sa.Boolean(), nullable=True),
+    sa.Column('revoked', sa.Boolean(), server_default=sa.text('false'), nullable=False),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
sa.Column('revoked', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.Column('revoked', sa.Boolean(), server_default=sa.text('false'), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic/versions/d44e91e81013_add_refresh_token_table.py` around lines 26 -
27, The `revoked` column is currently created as nullable with no default,
leading to ambiguous NULL states; update the column definition for `revoked` in
the migration (the sa.Column call that defines 'revoked') to be non-nullable and
supply a server-side default (e.g., replace nullable=True with nullable=False
and add server_default=sa.text('false') or equivalent) so new rows default to
false and revocation filters never see NULL.

Comment on lines +21 to +23
user = await AuthService.create_user(request, db)
access_token = await AuthService.create_access_token(user)
refresh_token = await AuthService.create_refresh_token(db, user.id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Signup flow is non-atomic across user and refresh-token persistence.

Line [21] and Line [23] rely on service methods that commit separately, so a refresh-token failure can leave a created user while returning 500. This creates inconsistent state and confusing retries.

Suggested direction
# Wrap user creation + refresh token insert in one DB transaction boundary,
# commit once after both succeed, rollback on any exception.
# Move commit control out of individual service methods.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/v1/endpoints/auth.py` around lines 21 - 23, Wrap the signup flow in a
single DB transaction so user creation and refresh-token insertion are atomic:
call AuthService.create_user and AuthService.create_refresh_token inside one
transaction boundary (e.g., using the db session/transaction context) and commit
only once after both succeed, rolling back on any exception; refactor/remove
internal commits from AuthService.create_user and
AuthService.create_refresh_token so they do not commit independently and ensure
AuthService.create_access_token remains pure (no DB commit) while the endpoint
controls the transaction lifecycle.

Comment on lines +44 to +53
return SignupResponse(
status_code=201,
message="Account created successfully",
data=SignupResponseData(
id=str(user.id),
email=user.email,
name=user.name,
access_token=access_token,
refresh_token=refresh_token
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not return auth tokens in JSON when using HttpOnly cookies.

At Line [51] and Line [52], exposing access_token/refresh_token in the response body undermines the XSS-resistance benefit of HttpOnly cookies. Keep tokens cookie-only and return user data metadata only.

Suggested change
         return SignupResponse(
             status_code=201,
             message="Account created successfully",
             data=SignupResponseData(
                 id=str(user.id),
                 email=user.email,
                 name=user.name,
-                access_token=access_token,
-                refresh_token=refresh_token
             )
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return SignupResponse(
status_code=201,
message="Account created successfully",
data=SignupResponseData(
id=str(user.id),
email=user.email,
name=user.name,
access_token=access_token,
refresh_token=refresh_token
)
return SignupResponse(
status_code=201,
message="Account created successfully",
data=SignupResponseData(
id=str(user.id),
email=user.email,
name=user.name
)
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/v1/endpoints/auth.py` around lines 44 - 53, The SignupResponse
currently includes access_token and refresh_token in SignupResponseData which
exposes tokens in JSON; remove access_token and refresh_token from the
SignupResponse/SignupResponseData payload returned by the signup endpoint (leave
id, email, name only) and ensure the tokens are set as HttpOnly secure cookies
in the request handler (e.g., where you call create access/refresh tokens)
instead of being included in the response body so cookies remain cookie-only and
XSS-resistant.

Comment thread pyproject.toml
"alembic>=1.14.0",
"pydantic-settings>=2.7.0",
"bcrypt>=4.0.0",
"psycopg2-binary>=2.9.12",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether psycopg2 is actually imported anywhere or only used by alembic env.py
rg -nP '\b(psycopg2|psycopg)\b' --type=py -C2
fd -i 'env.py' alembic

Repository: hngprojects/meetmind-be

Length of output: 82


🏁 Script executed:

cat -n alembic/env.py

Repository: hngprojects/meetmind-be

Length of output: 2088


🏁 Script executed:

head -50 pyproject.toml

Repository: hngprojects/meetmind-be

Length of output: 655


🏁 Script executed:

rg 'DATABASE_URL' --type=py -A2 -B2

Repository: hngprojects/meetmind-be

Length of output: 961


🏁 Script executed:

rg '(create_engine|Engine|connect\(\))' --type=py | head -20

Repository: hngprojects/meetmind-be

Length of output: 134


Remove psycopg2-binary from dependencies.

The entire runtime stack is async-only (sqlalchemy[asyncio] + asyncpg), and Alembic also runs migrations asynchronously. psycopg2-binary is unused and adds unnecessary bloat to the production image. No synchronous PostgreSQL driver is required.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` at line 14, Remove the unused synchronous Postgres driver
entry "psycopg2-binary>=2.9.12" from pyproject.toml; locate the dependency
string in the project's dependency list (the exact token
"psycopg2-binary>=2.9.12") and delete it, then run a quick search for any
references to psycopg2/psycopg2-binary in the codebase (imports or config) and
remove or replace them if present so only async drivers (asyncpg and
sqlalchemy[asyncio]) remain.

Comment thread pyproject.toml
"pydantic-settings>=2.7.0",
"bcrypt>=4.0.0",
"psycopg2-binary>=2.9.12",
"python-jose>=3.5.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

python-jose unmaintained CVE 2024 PyJWT replacement

💡 Result:

python-jose has faced maintenance concerns and multiple CVEs in 2024, including CVE-2024-33663 (algorithm confusion, CVSS 6.5)[1][2], CVE-2024-33664 (DoS via JWE compression, CVSS 5.3)[3][4], and indirect exposure via ecdsa dependency CVE-2024-23342[5]. These affected versions up to 3.3.0[4]. A 3.4.0 release in Feb 2025[6] and latest 3.5.0 in May 2025[7][8][9] address earlier issues, with security scanners showing no known vulnerabilities in 3.5.0[9]. However, ongoing open issues question maintenance[10][5], and projects like Okta recommend switching to PyJWT due to unmaintained status and CVEs[11][12][13][14]. Recommended replacement: PyJWT, actively maintained with migration guides available[13][15]. It's a drop-in replacement for basic JWT needs[15], used in production migrations[12][14]. For fuller JOSE support (JWE, etc.), consider joserfc (latest 1.6.3 Feb 2026[16][17]), which has migration guides from python-jose[18][19] and comprehensive RFC coverage[19]. To mitigate immediately: - Upgrade to python-jose>=3.5.0 with cryptography backend: pip install "python-jose[cryptography]" to avoid ecdsa issues[5][8]. - But prefer migrating to PyJWT: pip install pyjwt[12][13]. Example PyJWT migration (from guides[13]): # python-jose from jose import jwt payload = jwt.decode(token, key, algorithms=['RS256']) # PyJWT import jwt payload = jwt.decode(token, key, algorithms=['RS256']) PyJWT is suitable for most JWT use cases and avoids python-jose risks[11][20][15].

Citations:


Consider migrating to PyJWT for long-term maintainability.

While python-jose 3.5.0 addresses the 2024 CVEs (CVE-2024-33663, CVE-2024-33664), the project shows ongoing maintenance concerns with open issues questioning active development. PyJWT is actively maintained, provides a drop-in replacement for basic JWT operations (jwt.encode/decode signatures are nearly identical), and eliminates the supply-chain risk. If fuller JOSE support is needed, joserfc is also maintained and offers migration guides.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` at line 15, The dependency "python-jose>=3.5.0" in
pyproject.toml should be migrated to a maintained JWT library; replace that
entry with an appropriate PyJWT pin (e.g., "PyJWT>=2.8") and update any
imports/usages that reference python-jose (search for imports like "from jose
import jwt" or "jose.jwt") to the PyJWT API (standard "import jwt" and
jwt.encode/decode call sites), run tests and adjust call signatures/algorithms
where needed; if full JOSE feature parity is required, consider instead
switching to "joserfc" and follow its migration path.

Comment thread tests/conftest.py
"DATABASE_URL",
"postgresql+asyncpg://postgres:postgres@localhost:5432/test",
)
os.environ.setdefault("DATABASE_URL", settings.TEST_DATABASE_URL)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Force test DB URL instead of using setdefault.

Line [4] can retain an existing DATABASE_URL, which risks tests running against a non-test database. In tests, this should be explicitly overridden.

Suggested change
-os.environ.setdefault("DATABASE_URL", settings.TEST_DATABASE_URL)
+os.environ["DATABASE_URL"] = settings.TEST_DATABASE_URL
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
os.environ.setdefault("DATABASE_URL", settings.TEST_DATABASE_URL)
os.environ["DATABASE_URL"] = settings.TEST_DATABASE_URL
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/conftest.py` at line 4, Replace the use of os.environ.setdefault for
DATABASE_URL to ensure tests always use the test DB: locate the
os.environ.setdefault("DATABASE_URL", settings.TEST_DATABASE_URL) call in
tests/conftest.py and change it to explicitly override DATABASE_URL so it always
equals settings.TEST_DATABASE_URL (i.e., assign/replace
os.environ["DATABASE_URL"] with settings.TEST_DATABASE_URL) to avoid
accidentally using a non-test database during test runs.

Comment thread tests/conftest.py
Comment on lines +25 to +29
app.dependency_overrides[get_session] = mock_get_session
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
app.dependency_overrides.clear() No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Avoid clearing all dependency overrides in fixture teardown.

Line [29] removes every override, which can interfere with other fixtures/tests. Remove only the override set by this fixture.

Suggested change
 `@pytest.fixture`
 async def client():
     app.dependency_overrides[get_session] = mock_get_session
     transport = ASGITransport(app=app)
     async with AsyncClient(transport=transport, base_url="http://test") as ac:
         yield ac
-    app.dependency_overrides.clear()
+    app.dependency_overrides.pop(get_session, None)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.dependency_overrides[get_session] = mock_get_session
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
app.dependency_overrides.clear()
app.dependency_overrides[get_session] = mock_get_session
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
app.dependency_overrides.pop(get_session, None)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/conftest.py` around lines 25 - 29, The fixture currently clears all
dependency overrides with app.dependency_overrides.clear() which can break other
tests; instead remove only the override you set by deleting or popping the
get_session entry (the one assigned to mock_get_session) from
app.dependency_overrides in the teardown of the fixture so other overrides
remain intact—locate the assignment app.dependency_overrides[get_session] =
mock_get_session and replace the final clear() call with a targeted removal of
that key.

Comment thread tests/test_auth.py Outdated
Comment on lines +94 to +101
async def test_cookies_set_on_success(self, client):
user = make_user()
with patch(CREATE_USER, new_callable=AsyncMock, return_value=user), \
patch(CREATE_ACCESS, new_callable=AsyncMock, return_value=FAKE_ACCESS), \
patch(CREATE_REFRESH, new_callable=AsyncMock, return_value=FAKE_REFRESH):
response = await client.post(SIGNUP_URL, json=VALID_PAYLOAD)
assert "access_token" in response.cookies
assert "refresh_token" in response.cookies

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Assert cookie security attributes, not just cookie presence.

Line [100]-Line [101] validates existence only; this misses regressions in HttpOnly, Secure, and SameSite flags that are core to this auth design.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_auth.py` around lines 94 - 101, Update the
test_cookies_set_on_success test to assert the security attributes of the
cookies, not just their presence: after the POST to SIGNUP_URL with
VALID_PAYLOAD (within the same patched CREATE_USER/CREATE_ACCESS/CREATE_REFRESH
context), inspect response.cookies["access_token"] and
response.cookies["refresh_token"] and assert their HttpOnly flag is set, Secure
is set, and SameSite equals the expected value (e.g. "Lax" or "Strict" per app
policy); keep the existing presence assertions and add these attribute checks so
regressions in cookie flags are caught.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.env.example:
- Around line 5-6: The .env.example uses identical lifetimes for both tokens;
change ACCESS_TOKEN_EXPIRE_MINUTES to a short-lived value (e.g., 15) and
REFRESH_TOKEN_EXPIRE_MINUTES to a long-lived value (e.g., 10080 for 7 days) so
the access/refresh separation and rotation model works; update the .env.example
values for ACCESS_TOKEN_EXPIRE_MINUTES and REFRESH_TOKEN_EXPIRE_MINUTES
accordingly and add a brief inline comment explaining which is short-lived vs
long-lived to prevent future copy-paste mistakes.

In `@app/api/v1/endpoints/auth.py`:
- Around line 58-63: Change the two HTTPException raises inside the signup
exception handlers to use explicit exception chaining: in the
UserAlreadyExistsException except block (catching UserAlreadyExistsException as
e) re-raise the HTTPException with "from e" so the domain exception is preserved
as the cause; in the generic except Exception as e block (which logs via
logger.exception) re-raise the HTTPException with "from None" to suppress
chaining of the internal traceback. Target the raise statements in the except
blocks handling UserAlreadyExistsException and Exception in auth.py (around the
signup handler).

In `@app/main.py`:
- Line 36: Replace the named Starlette constant usage with the integer literal
422 where the validation response status is set (the code referencing
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT); change it to status_code=422
to avoid relying on Starlette 0.48+ symbols and preserve compatibility with
older Starlette/FastAPI versions while keeping the intent of signaling an
Unprocessable Entity response.

In `@tests/test_auth.py`:
- Around line 41-56: The test_response_body_shape currently asserts
body["status_code"] == 201 (a JSON field) but not the actual HTTP status; add an
assertion checking response.status_code == 201 immediately after the POST to
SIGNUP_URL (using the existing response variable) to verify the real HTTP
response code, keeping the existing JSON field assertions intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a8244586-6612-4be1-a6ef-6f3f73111118

📥 Commits

Reviewing files that changed from the base of the PR and between 5e884ed and 1a252f8.

📒 Files selected for processing (6)
  • .env.example
  • app/api/v1/endpoints/auth.py
  • app/main.py
  • app/schemas/auth.py
  • app/services/auth.py
  • tests/test_auth.py

Comment thread .env.example
Comment on lines +5 to +6
ACCESS_TOKEN_EXPIRE_MINUTES=1000
REFRESH_TOKEN_EXPIRE_MINUTES=1000 No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Both token expiry values are identical, contradicting the short-lived access / long-lived refresh model.

ACCESS_TOKEN_EXPIRE_MINUTES=1000 and REFRESH_TOKEN_EXPIRE_MINUTES=1000 set the same ~16.7-hour lifetime for both tokens. The PR explicitly describes access tokens as "short-lived" and refresh tokens as "long-lived"; developers who copy this example verbatim will deploy with no expiry separation, nullifying the refresh-token rotation security model.

♻️ Proposed fix
-ACCESS_TOKEN_EXPIRE_MINUTES=1000
-REFRESH_TOKEN_EXPIRE_MINUTES=1000
+ACCESS_TOKEN_EXPIRE_MINUTES=30
+REFRESH_TOKEN_EXPIRE_MINUTES=10080
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ACCESS_TOKEN_EXPIRE_MINUTES=1000
REFRESH_TOKEN_EXPIRE_MINUTES=1000
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_MINUTES=10080
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 5-5: [UnorderedKey] The ACCESS_TOKEN_EXPIRE_MINUTES key should go before the DATABASE_URL key

(UnorderedKey)


[warning] 6-6: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)


[warning] 6-6: [UnorderedKey] The REFRESH_TOKEN_EXPIRE_MINUTES key should go before the TEST_DATABASE_URL key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.example around lines 5 - 6, The .env.example uses identical lifetimes
for both tokens; change ACCESS_TOKEN_EXPIRE_MINUTES to a short-lived value
(e.g., 15) and REFRESH_TOKEN_EXPIRE_MINUTES to a long-lived value (e.g., 10080
for 7 days) so the access/refresh separation and rotation model works; update
the .env.example values for ACCESS_TOKEN_EXPIRE_MINUTES and
REFRESH_TOKEN_EXPIRE_MINUTES accordingly and add a brief inline comment
explaining which is short-lived vs long-lived to prevent future copy-paste
mistakes.

Comment on lines +58 to +63
except UserAlreadyExistsException as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))

except Exception as e:
logger.exception(f"An unexpected error occurred during signup: {e}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Internal server error") No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Use explicit exception chaining in both raise statements.

Both raise HTTPException(...) calls inside except clauses are missing exception chaining (Ruff B904). Without it, the implicit __context__ chain is ambiguous and debuggers may misattribute the cause.

  • Line 59: use raise ... from e to preserve the domain exception as cause.
  • Line 63: use raise ... from None to explicitly suppress the internal traceback from leaking into the exception chain, since the detail is already logged.
♻️ Proposed fix
     except UserAlreadyExistsException as e:
-        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
+        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e

     except Exception as e:
         logger.exception("An unexpected error occurred during signup")
-        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Internal server error")
+        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Internal server error") from None
🧰 Tools
🪛 Ruff (0.15.12)

[warning] 59-59: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


[warning] 62-62: Logging statement uses f-string

(G004)


[warning] 62-62: Redundant exception object included in logging.exception call

(TRY401)


[warning] 63-63: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/v1/endpoints/auth.py` around lines 58 - 63, Change the two
HTTPException raises inside the signup exception handlers to use explicit
exception chaining: in the UserAlreadyExistsException except block (catching
UserAlreadyExistsException as e) re-raise the HTTPException with "from e" so the
domain exception is preserved as the cause; in the generic except Exception as e
block (which logs via logger.exception) re-raise the HTTPException with "from
None" to suppress chaining of the internal traceback. Target the raise
statements in the except blocks handling UserAlreadyExistsException and
Exception in auth.py (around the signup handler).

Comment thread app/main.py
custom_message = f"Invalid input for {field_name}: {first_error['msg']}"

return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What version of Starlette is required by FastAPI 0.136.1?

💡 Result:

FastAPI 0.136.1 requires Starlette >=0.46.0.[1][2]

Citations:


🌐 Web query:

When was HTTP_422_UNPROCESSABLE_CONTENT introduced in Starlette changelog

💡 Result:

HTTP_422_UNPROCESSABLE_CONTENT was introduced in Starlette version 0.48.0, released on September 13, 2025. It was added as part of implementing RFC 9110 HTTP status names via Pull Request #2939, which renamed HTTP_422_UNPROCESSABLE_ENTITY to HTTP_422_UNPROCESSABLE_CONTENT while keeping the old name for backwards compatibility with a deprecation warning.[1][2][3][4]

Citations:


🏁 Script executed:

# Check FastAPI 0.136.1 source code to see if it uses literal 422 or named constants
# First, let's search for how status codes are used in FastAPI's validation error handling
rg "HTTP_422" --type py -A 2 -B 2

Repository: hngprojects/meetmind-be

Length of output: 272


🏁 Script executed:

# Let's also check the Starlette version history and when this constant was introduced
# by looking at recent Starlette releases
rg "422_UNPROCESSABLE" --type py

Repository: hngprojects/meetmind-be

Length of output: 138


🌐 Web query:

FastAPI 0.136.1 validation error handler status code implementation

💡 Result:

FastAPI uses HTTP 422 (Unprocessable Entity) for request validation errors produced from invalid client input via RequestValidationError, unless you override that exception handler. 1) What handler to override FastAPI raises an internal RequestValidationError when the request body/query/path contains invalid data, and FastAPI ships a default exception handler for it. You can override it by registering an exception handler for RequestValidationError using @app.exception_handler(RequestValidationError) [1]. 2) Default status code behavior The official FastAPI documentation examples show the custom RequestValidationError handler returning a response with status_code=422 (Unprocessable Entity) [1]. This corresponds to the default behavior FastAPI uses for request validation errors. 3) How the override changes the status code If you override the handler, the status code is whatever you return. For example, the docs show a handler that returns PlainTextResponse(..., status_code=400) [1]. So “implementation” is simply: FastAPI’s default handler for RequestValidationError returns 422, but your registered handler fully determines the status code. 4) Where to change it (application-wide) You can also supply exception_handlers when creating the FastAPI app to override how RequestValidationError is handled globally [2]. Regarding FastAPI 0.136.1 specifically: The 0.136.1 release notes primarily mention upgrades like Pydantic v2 deprecation fixes and documentation/tooling changes, not a change to the RequestValidationError status-code behavior [3][4].

Citations:


🏁 Script executed:

# Search for how FastAPI handles validation errors and 422 status codes
# in the FastAPI codebase or documentation
rg "422" app/main.py -B 5 -A 5

Repository: hngprojects/meetmind-be

Length of output: 518


🏁 Script executed:

# Check the broader context of this status code usage in app/main.py
cat -n app/main.py | sed -n '30,45p'

Repository: hngprojects/meetmind-be

Length of output: 627


Replace named constant with literal 422 for Starlette version compatibility.

HTTP_422_UNPROCESSABLE_CONTENT was introduced in Starlette 0.48.0 (September 2025), but FastAPI 0.136.1 requires only Starlette ≥0.46.0. Using this named constant creates a silent version dependency: downgrading Starlette to 0.46.x or 0.47.x will produce an AttributeError at runtime when the validation handler fires. FastAPI's own documentation uses the literal integer 422 for validation error responses, which remains compatible across all supported Starlette versions.

♻️ Proposed fix
-    return JSONResponse(
-        status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
+    return JSONResponse(
+        status_code=422,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/main.py` at line 36, Replace the named Starlette constant usage with the
integer literal 422 where the validation response status is set (the code
referencing status_code=status.HTTP_422_UNPROCESSABLE_CONTENT); change it to
status_code=422 to avoid relying on Starlette 0.48+ symbols and preserve
compatibility with older Starlette/FastAPI versions while keeping the intent of
signaling an Unprocessable Entity response.

Comment thread tests/test_auth.py
Comment on lines +41 to +56
async def test_response_body_shape(self, client):
user = make_user()
with patch(CREATE_USER, new_callable=AsyncMock, return_value=user), \
patch(CREATE_ACCESS, new_callable=AsyncMock, return_value=FAKE_ACCESS), \
patch(CREATE_REFRESH, new_callable=AsyncMock, return_value=FAKE_REFRESH):
response = await client.post(SIGNUP_URL, json=VALID_PAYLOAD)
body = response.json()
data = response.json()["data"]
assert body["status_code"] == 201
assert body["message"] == "Account created successfully"
assert "data" in body
assert data["access_token"] == FAKE_ACCESS
assert data["refresh_token"] == FAKE_REFRESH
assert data["email"] == "john@example.com"
assert data["name"] == "John Doe"
assert "id" in data

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

test_response_body_shape asserts the JSON status_code field but not the HTTP response status.

body["status_code"] == 201 checks a field inside the JSON payload, not the actual HTTP status code. A regression that sends HTTP 200 with the right body would pass silently. Add a direct assertion on response.status_code.

💚 Proposed fix
         body = response.json()
         data = response.json()["data"]
+        assert response.status_code == 201
         assert body["status_code"] == 201
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_auth.py` around lines 41 - 56, The test_response_body_shape
currently asserts body["status_code"] == 201 (a JSON field) but not the actual
HTTP status; add an assertion checking response.status_code == 201 immediately
after the POST to SIGNUP_URL (using the existing response variable) to verify
the real HTTP response code, keeping the existing JSON field assertions intact.

@CyberwizD
CyberwizD merged commit 5168503 into dev May 6, 2026
1 check passed
@coderabbitai coderabbitai Bot mentioned this pull request May 7, 2026
17 tasks
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants