Skip to content

Implement JWT authentication with signup, login, and refresh endpoints#42

Merged
saudzahirr merged 7 commits intomainfrom
feature/jwt-authentication
Mar 5, 2026
Merged

Implement JWT authentication with signup, login, and refresh endpoints#42
saudzahirr merged 7 commits intomainfrom
feature/jwt-authentication

Conversation

@saudzahirr
Copy link
Copy Markdown
Member

@saudzahirr saudzahirr commented Mar 5, 2026

Resolves #6

Summary by CodeRabbit

  • New Features

    • Vendor authentication: signup, login, and token refresh endpoints with JWT access/refresh tokens.
    • Protected endpoints now accept Bearer tokens.
  • Configuration

    • Access tokens default to 1 hour; refresh tokens configurable (default 7 days).
  • Observability

    • Requests now include a stable request ID returned in responses for tracing.
  • Tests

    • Added unit and integration tests covering authentication flows and token handling.

@saudzahirr saudzahirr self-assigned this Mar 5, 2026
@saudzahirr saudzahirr added enhancement New feature or request sprint-1 feature labels Mar 5, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 5, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds JWT-based vendor authentication (signup, login, refresh), token creation/validation, RLS-aware DB cursor dependency, vendor CRUD and auth service, auth routes, config updates, and extensive unit and integration tests; also updates middleware, exception handling, and project deps.

Changes

Cohort / File(s) Summary
Authentication core & config
backend/app/core/security.py, backend/app/core/config.py
Create/validate JWTs (access/refresh) carrying vendor_id and token_type; add decode/create refresh token; adjust access/refresh lifetimes; update password hashing chain.
Auth service & CRUD
backend/app/services/auth.py, backend/app/crud/vendor.py
Add signup/login/refresh business logic using vendor CRUD helpers; password hashing/upgrade, token issuance, and error handling.
API routes & routing
backend/app/api/routes/auth.py, backend/app/api/main.py, backend/app/api/routes/login.py
New /auth router with signup, login, refresh; replace previous login router inclusion; ensure router registration.
Dependencies / RLS
backend/app/api/deps.py
Add HTTPBearer auth, get_current_vendor_id (validates JWT, enforces token_type/access, UUID vendor_id) and get_rls_cursor (sets app RLS context via pool connection); expose CurrentVendorId and RLSCursorDep.
Schemas & responses
backend/app/schemas/auth.py, backend/app/schemas/response.py
Add auth request/response models (SignupRequest, LoginRequest, RefreshRequest, TokenPair, VendorOut, SignupResponse); add request_id to error response.
Middleware & exception handling
backend/app/api/middlewares.py, backend/app/core/exception_handlers.py, backend/app/core/exceptions.py
Request-id parsing/validation and response header setting; convert async exception handlers to sync; add return-type annotations on exception ctors and minor reflows.
Tests
backend/tests/unit/test_auth.py, backend/tests/integration/test_auth.py, backend/tests/*
Add comprehensive unit and integration tests for auth flows, token lifecycle, dependency behavior; adjust test formatting and fixtures; override deps for integration with Testcontainers Postgres.
Project & tooling
backend/pyproject.toml, .gitignore, backend/app/main.py, backend/app/pre_start.py
Add pyjwt dependency and Ruff config; add *.log to .gitignore; small import/type-hint adjustments.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API as FastAPI
    participant Auth as AuthService
    participant CRUD as VendorCRUD
    participant DB as Database
    participant Sec as Security

    rect rgba(200,150,255,0.5)
    Note over Client,API: Signup
    Client->>API: POST /auth/signup {email,password,client_id}
    API->>Auth: signup(cursor, email, password, client_id, settings)
    Auth->>CRUD: get_vendor_by_email(cursor, email)
    CRUD->>DB: SELECT vendor WHERE LOWER(email)=...
    DB-->>CRUD: None
    Auth->>Sec: hash_password(password)
    Sec-->>Auth: password_hash
    Auth->>CRUD: create_vendor(cursor, email, password_hash)
    CRUD->>DB: INSERT ... RETURNING id,email
    DB-->>CRUD: {id,email}
    CRUD-->>Auth: {id,email}
    Auth-->>API: SignupResponse{vendor}
    API-->>Client: 201 {vendor}
    end

    rect rgba(200,200,150,0.5)
    Note over Client,API: Login
    Client->>API: POST /auth/login {email,password,client_id}
    API->>Auth: login(cursor, email, password, client_id, settings)
    Auth->>CRUD: get_vendor_by_email(cursor, email)
    CRUD->>DB: SELECT ... password_hash
    DB-->>CRUD: {id,email,password_hash}
    Auth->>Sec: verify_password(password, password_hash)
    Sec-->>Auth: ok
    Auth->>Sec: create_access_token(vendor_id, settings)
    Sec-->>Auth: access_token
    Auth->>Sec: create_refresh_token(vendor_id, settings)
    Sec-->>Auth: refresh_token
    Auth-->>API: TokenPair{access,refresh}
    API-->>Client: 200 {access,refresh,token_type}
    end

    rect rgba(150,200,255,0.5)
    Note over Client,API: Protected request (RLS)
    Client->>API: GET /protected Authorization: Bearer {access_token}
    API->>Sec: decode_token(access_token, settings)
    Sec-->>API: {vendor_id, token_type}
    API->>API: validate token_type == "access"
    API->>API: get_rls_cursor(vendor_id)
    API->>DB: connection.run(app.set_app_context(vendor_id))
    DB-->>API: Cursor (RLS context)
    API->>DB: query with cursor (RLS enforced)
    DB-->>API: filtered rows
    API-->>Client: 200 {response}
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • laraibg786
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.86% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Implement JWT authentication with signup, login, and refresh endpoints' accurately summarizes the main changes: adding JWT-based auth with three key endpoints matching the PR's core functionality.
Linked Issues check ✅ Passed The PR implements all functional requirements from Issue #6: signup (201 create), login (token pair), refresh (new token), middleware validation, bcrypt hashing, HS256 tokens with vendor_id/exp/token_type, 1-hour access/7-day refresh lifecycles, and Bearer header validation.
Out of Scope Changes check ✅ Passed All changes are directly aligned with Issue #6 requirements: auth endpoints, security infrastructure, CRUD helpers, schemas, exception handling, and comprehensive tests. Minor updates to middleware, config, and response formatting support the auth implementation without deviating from scope.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/jwt-authentication

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
Copy Markdown

@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: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/api/deps.py`:
- Around line 61-65: The code only checks presence of vendor_id but not its UUID
format, so validate vendor_id is a valid UUID string before returning: attempt
to parse it (e.g., with uuid.UUID or equivalent) and if parsing fails raise
AuthenticationException("Invalid token payload") so callers like
app.set_app_context(vendor_id) never receive an invalid UUID; update the return
path in the function that reads payload->vendor_id and ensure
AuthenticationException is raised on invalid format.

In `@backend/app/core/security.py`:
- Around line 10-11: The PasswordHash configuration currently only includes
BcryptHasher, which removes verification for existing Argon2-hashed passwords;
update the password_hash declaration (the PasswordHash(...) tuple) to include
Argon2Hasher alongside BcryptHasher so existing Argon2 hashes remain verifiable
while new passwords continue to use bcrypt (i.e., add Argon2Hasher to the tuple
used to construct password_hash).

In `@backend/app/crud/vendor.py`:
- Around line 39-49: The create_vendor function can raise a UniqueViolation
during concurrent signups; modify create_vendor(cursor, email, password_hash) to
use an INSERT ... ON CONFLICT DO NOTHING with the same functional index
(conflict target on LOWER("email") or the approriate index expression) and
RETURNING "id", "email", then if cursor.fetchone() is None return None instead
of asserting; update the caller in backend/app/services/auth.py where vendor =
create_vendor(...) to check for None and raise a ConflictException("A vendor
with this email already exists") to surface a 409 instead of letting a 500
bubble up.

In `@backend/app/schemas/auth.py`:
- Around line 15-21: LoginRequest.password and RefreshRequest.refresh_token are
unbounded; add the same validation bounds used by SignupRequest to harden input
(e.g. use Field(..., min_length=8, max_length=128)) so both password and
refresh_token enforce min/max lengths; update the type declarations for
LoginRequest.password and RefreshRequest.refresh_token to use Field with
min_length=8 and max_length=128 to match SignupRequest.

In `@backend/app/services/auth.py`:
- Around line 36-42: Wrap the call to create_vendor(cursor, email, hashed) in a
try/except that catches the database UniqueViolation (the DB-level
UniqueViolation exception) and converts it into a ConflictException("A vendor
with this email already exists"); keep the existing pre-check with
get_vendor_by_email but handle UniqueViolation around create_vendor in auth.py
so concurrent inserts map to 409 instead of bubbling as a 500.

In `@backend/tests/integration/test_auth.py`:
- Around line 245-268: The test is directly calling get_current_vendor_id
instead of exercising the HTTP layer and there is no protected route to validate
dependency wiring; add a temporary protected test endpoint that uses the
dependency (e.g., def _test_protected(vendor_id=Depends(get_current_vendor_id))
or use the CurrentVendorId dependency) mounted on the test app/router, then use
the TestClient to send a GET to that endpoint with no Authorization header and
assert a 401 response; update the test to call the endpoint via client (not
get_current_vendor_id directly) so Bearer parsing, dependency injection, and
exception-to-HTTP translation are all exercised.

In `@backend/tests/unit/test_auth.py`:
- Around line 141-152: Add a new unit test that exercises the race-path where
pre-read shows no existing vendor but the insert collides: call signup (function
signup in app.services.auth) with get_vendor_by_email patched to return None (so
the pre-check passes) and patch create_vendor (or the internal call that
performs the insert, e.g., create_vendor in app.services.auth) to simulate a
concurrent-insert conflict by raising or returning a
ConflictException-equivalent; then assert signup raises ConflictException.
Ensure you still pass a MagicMock cursor and the same settings fixture so the
test isolates the insert-time conflict path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8e1c4da7-8b9a-40ee-9d5d-762e862d79f8

📥 Commits

Reviewing files that changed from the base of the PR and between 265d2b0 and 37a2a9e.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • backend/app/api/deps.py
  • backend/app/api/main.py
  • backend/app/api/routes/auth.py
  • backend/app/core/config.py
  • backend/app/core/security.py
  • backend/app/crud/__init__.py
  • backend/app/crud/vendor.py
  • backend/app/schemas/auth.py
  • backend/app/services/__init__.py
  • backend/app/services/auth.py
  • backend/pyproject.toml
  • backend/tests/integration/test_auth.py
  • backend/tests/unit/test_auth.py

Copy link
Copy Markdown

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/services/auth.py`:
- Around line 105-113: In refresh(), validate the format/type of
payload["vendor_id"] before calling get_vendor_by_id to avoid DB cast/runtime
errors: check the vendor_id value (e.g., ensure it's an int or valid UUID
depending on your schema) and attempt a safe parse/cast with a try/except (or
isinstance check), and if parsing fails raise AuthenticationException("Invalid
token payload") so get_vendor_by_id(cursor, vendor_id) only receives a
properly-typed id; update references to vendor_id, refresh(),
get_vendor_by_id(), and AuthenticationException accordingly.

In `@backend/tests/unit/test_auth.py`:
- Around line 236-275: Add a new unit test in TestGetCurrentVendorId that
constructs an access token whose "vendor_id" claim is present but not a valid
UUID (e.g., "invalid-uuid"), using create_access_token(...) with settings, then
call get_current_vendor_id(creds, settings) and assert it raises
AuthenticationException (match a message indicating invalid vendor id or UUID);
reuse the existing pattern of creating creds = MagicMock(); creds.credentials =
token and follow other tests' structure to ensure the malformed vendor_id path
is covered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3d0a2e7c-2591-4f5f-a0f9-44f353b3f289

📥 Commits

Reviewing files that changed from the base of the PR and between 37a2a9e and 3716ba0.

📒 Files selected for processing (7)
  • backend/app/api/deps.py
  • backend/app/core/security.py
  • backend/app/crud/vendor.py
  • backend/app/schemas/auth.py
  • backend/app/services/auth.py
  • backend/tests/integration/test_auth.py
  • backend/tests/unit/test_auth.py

Copy link
Copy Markdown

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/tests/conftest.py (1)

48-55: ⚠️ Potential issue | 🔴 Critical

Critical: dependency override lifecycle is indented into _get_db, breaking fixture behavior.

Lines 53–55 are inside _get_db instead of at the override_get_db level. This causes two problems:

  1. The dependency override is never installed at fixture setup time—it only executes inside the generator body, which may never run if dependencies are mocked elsewhere.
  2. _get_db becomes a multi-yield generator (yields on line 51 and line 54), which violates FastAPI's dependency cleanup contract and can cause runtime errors.

Move lines 53–55 to the override_get_db function level (matching the indentation of line 48) and wrap the yield in a try/finally block to ensure cleanup:

Fix
 `@pytest.fixture`(scope="module")
 def override_get_db(test_container: PostgresContainer):
     """Start a dedicated Testcontainers Postgres for this module and
     override the get_db dependency so endpoints receive a cursor from it.

     Each test module gets its own isolated container, avoiding
     cross-module side effects when tests run in parallel.
     """

     def _get_db() -> typing.Generator[Cursor, None, None]:
         with connect(test_container.get_connection_url()) as conn:
             with conn.cursor() as cursor:
                 yield cursor

+    app.dependency_overrides[get_db] = _get_db
+    try:
+        yield
+    finally:
+        app.dependency_overrides.pop(get_db, None)
-        app.dependency_overrides[get_db] = _get_db
-        yield
-        app.dependency_overrides.pop(get_db, None)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/conftest.py` around lines 48 - 55, The dependency override
setup/teardown are incorrectly indented inside the `_get_db` generator, making
it a multi-yield generator and preventing the override from being installed at
fixture setup; fix by moving the lines that set and pop
`app.dependency_overrides[get_db]` out to the `override_get_db` fixture scope
(call `app.dependency_overrides[get_db] = _get_db` before yielding from the
fixture and `app.dependency_overrides.pop(get_db, None)` in a finally after the
fixture yield), and change `_get_db` so it yields the `cursor` only once (wrap
the single yield in a try/finally if needed to ensure DB cursor/connection
cleanup) so `_get_db` remains a single-yield FastAPI dependency generator.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/api/middlewares.py`:
- Around line 6-11: The call_next parameter in add_request_id is loosely typed
as object; change it to the Starlette/FastAPI request handler type by importing
and using RequestResponseEndpoint from starlette.types and annotate call_next:
RequestResponseEndpoint so the signature reads async def add_request_id(request:
Request, call_next: RequestResponseEndpoint) -> Response; ensure the import of
RequestResponseEndpoint is added alongside Request/Response imports and update
any related type hints accordingly.

In `@backend/pyproject.toml`:
- Line 16: Update the PyJWT dependency constraint currently specified as the
string "pyjwt>=2.8.0,<3.0.0" so it excludes the vulnerable 2.10.0 release;
change it to either "pyjwt>=2.8.0,!=2.10.0,<3.0.0" or bump to a safe minimum
like "pyjwt>=2.10.1,<3.0.0" (preferably "pyjwt>=2.11.0,<3.0.0" to pick a current
secure release) in the dependency entry so CVE-2024-53861 is avoided.

In `@backend/tests/integration/test_auth.py`:
- Around line 46-101: The pg_container fixture is module-scoped so DB state is
shared across tests; change test isolation by making pg_container (and client)
function-scoped or ensure each test gets a fresh DB/transaction: update the
pg_container fixture (and corresponding client) to scope="function" or wrap each
test in a DB transaction/rollback created in _override_get_db or a per-test
setup that reinitializes the database, referencing the pg_container and client
fixtures and the _override_get_db function to locate the code to modify.

---

Outside diff comments:
In `@backend/tests/conftest.py`:
- Around line 48-55: The dependency override setup/teardown are incorrectly
indented inside the `_get_db` generator, making it a multi-yield generator and
preventing the override from being installed at fixture setup; fix by moving the
lines that set and pop `app.dependency_overrides[get_db]` out to the
`override_get_db` fixture scope (call `app.dependency_overrides[get_db] =
_get_db` before yielding from the fixture and
`app.dependency_overrides.pop(get_db, None)` in a finally after the fixture
yield), and change `_get_db` so it yields the `cursor` only once (wrap the
single yield in a try/finally if needed to ensure DB cursor/connection cleanup)
so `_get_db` remains a single-yield FastAPI dependency generator.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b904356b-42bf-4eca-8894-4ef08613f04a

📥 Commits

Reviewing files that changed from the base of the PR and between 3716ba0 and 8fdef40.

📒 Files selected for processing (23)
  • .gitignore
  • backend/app/api/deps.py
  • backend/app/api/main.py
  • backend/app/api/middlewares.py
  • backend/app/api/routes/auth.py
  • backend/app/api/routes/health.py
  • backend/app/api/routes/login.py
  • backend/app/core/config.py
  • backend/app/core/exception_handlers.py
  • backend/app/core/exceptions.py
  • backend/app/core/security.py
  • backend/app/crud/vendor.py
  • backend/app/main.py
  • backend/app/pre_start.py
  • backend/app/schemas/auth.py
  • backend/app/schemas/response.py
  • backend/app/services/auth.py
  • backend/pyproject.toml
  • backend/tests/__init__.py
  • backend/tests/conftest.py
  • backend/tests/integration/test_auth.py
  • backend/tests/integration/test_smoke.py
  • backend/tests/unit/test_auth.py

Copy link
Copy Markdown

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/app/api/middlewares.py (1)

15-18: ⚠️ Potential issue | 🟠 Major

Validate incoming X-Request-ID before accepting it.

Line 15-Line 17 trusts arbitrary client input and stores it in request.state, then echoes it in Line 21. This allows request-id spoofing and low-quality/oversized correlation values to pollute tracing/logs. Accept only a strict format/length (e.g., UUID-like), otherwise generate a new UUID.

♻️ Proposed hardening
+import re
 import uuid
@@
+REQUEST_ID_RE = re.compile(r"^[A-Za-z0-9\-_.]{1,64}$")
@@
-    raw_header = request.headers.get("X-Request-ID")
-    stripped_header = raw_header.strip() if raw_header else ""
-    request_id = stripped_header if stripped_header else str(uuid.uuid4())
+    raw_header = request.headers.get("X-Request-ID")
+    stripped_header = raw_header.strip() if raw_header else ""
+    request_id = (
+        stripped_header
+        if stripped_header and REQUEST_ID_RE.fullmatch(stripped_header)
+        else str(uuid.uuid4())
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/middlewares.py` around lines 15 - 18, Validate the incoming
X-Request-ID before accepting it: in the code that reads
raw_header/stripped_header and sets request.state.request_id, check that
stripped_header matches a strict UUID-like pattern and length (e.g.,
/^[0-9a-fA-F-]{36}$/ or use a UUID parsing/validation helper) and only use it as
request_id if it passes; otherwise generate a new UUID via uuid.uuid4() and
assign that to request.state.request_id. Ensure trimming still happens, reject
empty/oversized values, and centralize the check in the same block that
currently sets request_id so the echoed ID is always validated or freshly
generated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/pyproject.toml`:
- Line 59: Update the typo in the lint-rule comment text: change the comment
that reads "flake8-buitins" to "flake8-builtins" (the string/comment on the same
line as "A", which documents flake8-builtins checking variable/function names to
avoid shadowing builtins).

In `@backend/tests/integration/test_auth.py`:
- Around line 41-43: The test route _protected_test currently only returns the
JWT claim (vendor_id) and doesn't assert that the DB session RLS context was
set; modify the handler used in tests (function _protected_test that accepts
CurrentVendorId) to query the database for current_setting('app.vendor_id',
true) within the same request and return that value (or add an additional route
used by the test that does the same), then update the assertions in the test to
compare both the echoed JWT vendor_id and the DB session value; apply the same
change to the other test route referenced at the 275-278 area so tests verify
the DB session context is actually set.
- Around line 298-307: Replace the direct call to create_access_token in
test_valid_token_returns_vendor_id with a real login flow: create or use a
vendor identity (vendor_id), POST to the /auth/login endpoint via the TestClient
(client.post(f"{API_V1}/auth/login", ...)) using the expected login payload for
your auth endpoint, assert the login response is successful, extract the
returned access_token from the login JSON response, and use that token in the
Authorization header when calling f"{API_V1}/protected-test"; update assertions
to ensure both the login returned a usable access_token and the protected
endpoint returns 200. Ensure you remove the direct create_access_token usage in
test_valid_token_returns_vendor_id.

---

Outside diff comments:
In `@backend/app/api/middlewares.py`:
- Around line 15-18: Validate the incoming X-Request-ID before accepting it: in
the code that reads raw_header/stripped_header and sets
request.state.request_id, check that stripped_header matches a strict UUID-like
pattern and length (e.g., /^[0-9a-fA-F-]{36}$/ or use a UUID parsing/validation
helper) and only use it as request_id if it passes; otherwise generate a new
UUID via uuid.uuid4() and assign that to request.state.request_id. Ensure
trimming still happens, reject empty/oversized values, and centralize the check
in the same block that currently sets request_id so the echoed ID is always
validated or freshly generated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a6e5e9be-a8f2-4e9e-96b5-16c02c5f80d2

📥 Commits

Reviewing files that changed from the base of the PR and between 8fdef40 and 912e098.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • backend/app/api/middlewares.py
  • backend/pyproject.toml
  • backend/tests/conftest.py
  • backend/tests/integration/test_auth.py

- Add UUID validation for vendor_id in get_current_vendor_id.
- Update create_vendor to handle email conflicts gracefully.
- Refactor auth service to raise ConflictException on duplicate vendor email.
- Improve request schemas for better validation.
- Add integration tests for protected endpoints and concurrent signup conflicts.
- Updated .gitignore to exclude log files.
- Improved docstrings in deps.py, main.py, middlewares.py, auth.py, health.py, and other modules for better clarity on function behavior and return types.
- Refactored exception handling in exception_handlers.py and exceptions.py to include return type hints in constructors.
- Added type hints and improved formatting in various service and CRUD functions for consistency and readability.
- Integrated Ruff configuration in pyproject.toml for enhanced linting and code style enforcement.
- Updated integration and unit tests for better readability and maintainability.
… and consistency; enhance test coverage for error handling
@saudzahirr saudzahirr force-pushed the feature/jwt-authentication branch from ee4d8cf to 10a7615 Compare March 5, 2026 08:45
Copy link
Copy Markdown

@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: 3

♻️ Duplicate comments (1)
backend/app/crud/vendor.py (1)

59-63: ⚠️ Potential issue | 🔴 Critical

Fix ON CONFLICT expression-target syntax to avoid runtime SQL failure.

Line 62 uses ON CONFLICT (LOWER("email")), but PostgreSQL requires expression targets in conflict inference to be wrapped in their own parentheses: ON CONFLICT ((LOWER("email"))). The outer parentheses denote the conflict target list, and the inner parentheses wrap the index expression itself. Without double parentheses, PostgreSQL will not recognize this as an expression form and will fail to match against your expression index, breaking insert handling on signup.

Suggested fix
     cursor.execute(
         'INSERT INTO app."vendors" ("email", "password_hash") '
         "VALUES (%s, %s) "
-        'ON CONFLICT (LOWER("email")) DO NOTHING '
+        'ON CONFLICT ((LOWER("email"))) DO NOTHING '
         'RETURNING "id", "email"',
         (email, password_hash),
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/crud/vendor.py` around lines 59 - 63, The INSERT SQL in the
cursor.execute call (in vendor.py) uses ON CONFLICT (LOWER("email")) which is
invalid for an expression index; change that conflict target to use double
parentheses so it reads ON CONFLICT ((LOWER("email"))) in the same SQL string
passed to cursor.execute to match the expression index and avoid runtime SQL
errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/api/routes/login.py`:
- Line 4: The file defines an empty APIRouter instance named router in login.py
but no routes are attached while backend/app/api/main.py still mounts it at
'/login'; either remove this empty router export and the corresponding
include_router(...) call in main.py, or implement the intended login endpoints
(e.g., add handler functions and router.post/get registrations to the router in
login.py) and ensure the router variable is exported; update main.py to only
include_router(router, prefix="/login") when router contains handlers.

In `@backend/tests/integration/test_auth.py`:
- Around line 302-323: The test test_valid_token_returns_vendor_id only asserts
non-empty vendor IDs; modify it to assert the protected endpoint returns the
exact vendor created by _signup. Capture the created vendor id from the _signup
call (update _signup to return the new vendor id if it doesn't already), store
it as created_vendor_id, then after the GET to f"{API_V1}/protected-test" assert
body["vendor_id"] == created_vendor_id and body["db_vendor_id"] ==
created_vendor_id (keeping the existing token flow using login_resp and token).
- Around line 42-47: The helper _protected_test currently overrides DB context
by calling app.set_app_context, which hides auth-dependency regressions; remove
the call to cursor.execute("SELECT app.set_app_context(%s)", (vendor_id,))
inside _protected_test and only execute the query that reads
current_setting('app.vendor_id', true) so the test asserts the upstream
dependency (CurrentVendorId injection) actually set app.vendor_id; keep
returning {"vendor_id": vendor_id, "db_vendor_id": db_vendor_id} so the test can
compare the provided vendor_id to the DB value.

---

Duplicate comments:
In `@backend/app/crud/vendor.py`:
- Around line 59-63: The INSERT SQL in the cursor.execute call (in vendor.py)
uses ON CONFLICT (LOWER("email")) which is invalid for an expression index;
change that conflict target to use double parentheses so it reads ON CONFLICT
((LOWER("email"))) in the same SQL string passed to cursor.execute to match the
expression index and avoid runtime SQL errors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0e319c01-dc2e-4b5a-b098-9662effa83f9

📥 Commits

Reviewing files that changed from the base of the PR and between 912e098 and 10a7615.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (28)
  • .gitignore
  • backend/app/api/deps.py
  • backend/app/api/main.py
  • backend/app/api/middlewares.py
  • backend/app/api/routes/auth.py
  • backend/app/api/routes/health.py
  • backend/app/api/routes/login.py
  • backend/app/core/config.py
  • backend/app/core/exception_handlers.py
  • backend/app/core/exceptions.py
  • backend/app/core/security.py
  • backend/app/crud/__init__.py
  • backend/app/crud/vendor.py
  • backend/app/main.py
  • backend/app/pre_start.py
  • backend/app/schemas/auth.py
  • backend/app/schemas/response.py
  • backend/app/services/__init__.py
  • backend/app/services/auth.py
  • backend/pyproject.toml
  • backend/tests/__init__.py
  • backend/tests/api/routes/test_health.py
  • backend/tests/conftest.py
  • backend/tests/core/test_exception_handlers.py
  • backend/tests/core/test_exceptions.py
  • backend/tests/integration/test_auth.py
  • backend/tests/schemas/test_response.py
  • backend/tests/unit/test_auth.py

…dor creation SQL to handle email conflicts correctly; enhance integration tests with RLS cursor dependency
@saudzahirr
Copy link
Copy Markdown
Member Author

saudzahirr commented Mar 5, 2026

@coderabbitai review!

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 5, 2026

@saudzahirr Sure! I'll kick off a full review of PR #42 right away.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

1 similar comment
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 5, 2026

@saudzahirr Sure! I'll kick off a full review of PR #42 right away.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/tests/integration/test_auth.py`:
- Around line 123-150: In _signup and _login helper functions, stop calling
.json() directly on the client.post result; instead capture the response, assert
its HTTP status code (e.g., for _signup assert expected creation code like 201
or at least response.ok, and for _login assert expected success like 200 or
response.ok) to fail tests with clear HTTP assertions, then call and return
response.json(); update the bodies of _signup and _login to use a response
variable, assert response.status_code (or response.ok) before returning
response.json().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e9b76b4e-9c5f-45e4-bd06-e5f5e833a2c5

📥 Commits

Reviewing files that changed from the base of the PR and between 10a7615 and e20c328.

📒 Files selected for processing (3)
  • backend/app/api/main.py
  • backend/app/crud/vendor.py
  • backend/tests/integration/test_auth.py

Copy link
Copy Markdown

@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.

♻️ Duplicate comments (1)
backend/tests/integration/test_auth.py (1)

123-150: ⚠️ Potential issue | 🟡 Minor

Assert HTTP status before parsing JSON in helper methods.

Line 128 and Line 143 parse JSON unconditionally; when setup calls fail, failures become less actionable than explicit status assertions.

Suggested patch
 def _signup(
     client: TestClient,
     email: str = "vendor@test.com",
     password: str = "SecurePass123!",
 ) -> dict:
-    return client.post(
+    resp = client.post(
         f"{API_V1}/auth/signup",
         json={
             "email": email,
             "password": password,
             "client_id": "integration-test",
         },
-    ).json()
+    )
+    assert resp.status_code == 201, resp.text
+    return resp.json()
@@
 def _login(
     client: TestClient,
     email: str = "vendor@test.com",
     password: str = "SecurePass123!",
 ) -> dict:
-    return client.post(
+    resp = client.post(
         f"{API_V1}/auth/login",
         json={
             "email": email,
             "password": password,
             "client_id": "integration-test",
         },
-    ).json()
+    )
+    assert resp.status_code == 200, resp.text
+    return resp.json()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/integration/test_auth.py` around lines 123 - 150, The helpers
_signup and _login currently call .json() without checking response status;
change both to first assert the response.status_code is 200 (or the expected
success code) before calling .json() so test failures surface clearly—locate the
_signup and _login functions and add an assertion on the returned
response.status_code (with an explanatory message if desired) prior to invoking
.json().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@backend/tests/integration/test_auth.py`:
- Around line 123-150: The helpers _signup and _login currently call .json()
without checking response status; change both to first assert the
response.status_code is 200 (or the expected success code) before calling
.json() so test failures surface clearly—locate the _signup and _login functions
and add an assertion on the returned response.status_code (with an explanatory
message if desired) prior to invoking .json().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1987fbd7-678a-45b8-9c88-6efd382a2f68

📥 Commits

Reviewing files that changed from the base of the PR and between 10a7615 and e20c328.

📒 Files selected for processing (3)
  • backend/app/api/main.py
  • backend/app/crud/vendor.py
  • backend/tests/integration/test_auth.py

@saudzahirr saudzahirr merged commit 67f20a9 into main Mar 5, 2026
9 checks passed
@saudzahirr saudzahirr deleted the feature/jwt-authentication branch March 5, 2026 10:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Issue 06: JWT Authentication

1 participant