Skip to content

feat(auth): migrate user identities from integer to UUIDv4#43

Merged
aniebiet-afia merged 3 commits intomainfrom
feat/auth-login
Mar 27, 2026
Merged

feat(auth): migrate user identities from integer to UUIDv4#43
aniebiet-afia merged 3 commits intomainfrom
feat/auth-login

Conversation

@aniebietafia
Copy link
Copy Markdown
Contributor

@aniebietafia aniebietafia commented Mar 27, 2026

  • Altered User.id and VerificationToken.user_id from auto-incrementing integers to uuid.UUID to prevent user enumeration and improve security.
  • Generated and applied an Alembic migration to safely truncate development tables and apply USING uuid_generate_v4() to cast existing column schema types.
  • Updated Pydantic schemas (UserResponse, LoginResponse, UserRegisteredEvent, MediaUploadPayload) to enforce strict uuid.UUID typing.
  • Fixed TypeError: Object of type UUID is not JSON serializable in the POST /login FastAPI JSONResponse endpoint by enabling mode="json" on model_dump().
  • Updated CRUD services and email verification signatures to accept uuid.UUID globally.
  • Resolved MissingGreenlet SQLAlchemy DBAPI crash by establishing psycopg2-binary as the sync fallback for asyncpg within session.py.

Fixes issue #24

Summary by CodeRabbit

  • New Features

    • Added user login endpoint with JWT-based authentication using access tokens and refresh tokens
    • Implemented account lockout protection that locks accounts after repeated failed login attempts
  • Security Improvements

    • Access tokens now valid for 60 minutes (previously 15)
    • Refresh tokens stored in secure, HTTP-only cookies
  • Updates

    • User identifiers migrated from integers to UUIDs in the database

- Altered `User.id` and `VerificationToken.user_id` from auto-incrementing integers to `uuid.UUID` to prevent user enumeration and improve security.
- Generated and applied an Alembic migration to safely truncate development tables and apply `USING uuid_generate_v4()` to cast existing column schema types.
- Updated Pydantic schemas (`UserResponse`, `LoginResponse`, `UserRegisteredEvent`, `MediaUploadPayload`) to enforce strict `uuid.UUID` typing.
- Fixed `TypeError: Object of type UUID is not JSON serializable` in the `POST /login` FastAPI `JSONResponse` endpoint by enabling `mode="json"` on `model_dump()`.
- Updated CRUD services and email verification signatures to accept `uuid.UUID` globally.
- Resolved `MissingGreenlet` SQLAlchemy DBAPI crash by establishing `psycopg2-binary` as the sync fallback for `asyncpg` within `session.py`.

Signed-off-by: aniebietafia <aniebietafia87@gmail.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 27, 2026

Warning

Rate limit exceeded

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

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 10 minutes and 10 seconds.

⌛ 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: b3761553-6bcd-4e34-9c31-23f1a79c1fad

📥 Commits

Reviewing files that changed from the base of the PR and between 6748e9b and b186204.

📒 Files selected for processing (2)
  • alembic/versions/7f066a8213a8_change_user_id_to_uuid.py
  • alembic/versions/e1a664780dc6_add_meeting_model.py
📝 Walkthrough

Walkthrough

The PR introduces UUID-based user identification by migrating the users.id column from integer to UUID via Alembic, switches the PostgreSQL driver from asyncpg to psycopg2, implements a new login endpoint with JWT access tokens and HTTP-only refresh token cookies, adds account lockout enforcement via Redis, centralizes password hashing and token generation in a SecurityService, and updates type annotations across models, schemas, and services accordingly.

Changes

Cohort / File(s) Summary
Database & Driver Configuration
.env.example, requirements.txt, app/core/config.py, app/db/session.py
Switched PostgreSQL driver from asyncpg to psycopg2 in .env.example and requirements.txt; added ALGORITHM, MAX_FAILED_LOGIN_ATTEMPTS, and ACCOUNT_LOCKOUT_DAYS config settings; implemented asyncpg-to-psycopg2 URL coercion in session module.
Alembic Migrations
alembic/versions/e1a664780dc6_add_meeting_model.py, alembic/versions/7f066a8213a8_change_user_id_to_uuid.py, alembic_output.txt
Added two migration revisions: an empty placeholder for meeting model and a full migration converting users.id from INTEGER to UUID with foreign key updates for verification_tokens.user_id.
Core Security Infrastructure
app/core/security.py, app/crud/user/user.py
Introduced SecurityService for centralized password hashing (via bcrypt/passlib with fallbacks) and JWT token creation (access and refresh tokens); refactored user creation to delegate password hashing to the new service.
User & Token Model Type Updates
app/models/user.py, app/models/verification_token.py, app/schemas/user.py, app/crud/verification_token.py, app/services/auth_verification.py
Updated all user ID references from int to uuid.UUID across models, schemas, and service signatures.
Authentication Endpoints & Schemas
app/api/v1/endpoints/auth.py, app/schemas/auth.py
Added POST /auth/login endpoint with rate limiting, account lockout checks, password validation, and JWT token issuance; created LoginRequest and LoginResponse Pydantic schemas.
Account Lockout & Token Persistence
app/services/account_lockout.py, app/services/token_store.py
Introduced Redis-backed AccountLockoutService for tracking failed login attempts and enforcing lockout; introduced TokenStoreService for persisting and validating refresh token JTIs.
Kafka Event Schema
app/kafka/schemas.py
Updated MediaUploadPayload.user_id from int to uuid.UUID; added new UserRegisteredEvent schema with user_id and email fields.
Authentication Tests
tests/test_auth/test_auth_login.py, tests/test_core/test_security.py, tests/test_auth/test_schemas_user.py
Added comprehensive integration tests for login endpoint (successful login, invalid credentials, unverified/deleted accounts, account lockout scenarios); added unit tests for SecurityService password and token operations; updated user schema test to use UUID.
Documentation & Diagnostics
README.md, fail_log.txt
Added "Migrate Head" subsection to migration instructions; captured Windows test run log showing UUID validation failures and bcrypt initialization warnings.

Sequence Diagram

sequenceDiagram
    actor Client
    participant API as Login Endpoint
    participant DB as Database
    participant Redis as Redis Cache
    participant Lockout as AccountLockoutService
    participant Security as SecurityService

    Client->>API: POST /auth/login (email, password)
    API->>Lockout: is_locked(email)
    Lockout->>Redis: GET account_locked:{email}
    alt Account Locked
        Redis-->>Lockout: Key exists
        Lockout-->>API: true
        API-->>Client: 403 ACCOUNT_LOCKED
    else Not Locked
        Redis-->>Lockout: No key
        Lockout-->>API: false
        API->>DB: Find user by email
        alt User Found
            DB-->>API: User record
            API->>Security: verify_password(plain, hashed)
            alt Valid Password
                Security-->>API: true
                alt User Verified & Not Deleted
                    API->>Lockout: reset_attempts(email)
                    Lockout->>Redis: DEL login_attempts:{email}
                    API->>Security: create_access_token(email)
                    Security-->>API: (access_token, expires_in)
                    API->>Security: create_refresh_token(email)
                    Security-->>API: (refresh_token, jti, ttl)
                    API->>Redis: SET refresh_token:{jti} (with TTL)
                    API-->>Client: 200 OK + response + refresh_token cookie
                else User Not Verified or Deleted
                    API-->>Client: 403 EMAIL_NOT_VERIFIED / ACCOUNT_DELETED
                end
            else Invalid Password
                Security-->>API: false
                API->>Lockout: record_failed_attempt(email)
                Lockout->>Redis: INCR login_attempts:{email}
                alt Attempts >= MAX_FAILED_LOGIN_ATTEMPTS
                    Redis-->>Lockout: count
                    Lockout->>Redis: SET account_locked:{email} (with TTL)
                    Lockout->>Redis: DEL login_attempts:{email}
                    Lockout-->>API: locked
                    API-->>Client: 403 ACCOUNT_LOCKED
                else Attempts < Threshold
                    Lockout-->>API: updated
                    API-->>Client: 401 INVALID_CREDENTIALS
                end
            end
        else User Not Found
            DB-->>API: None
            API->>Lockout: record_failed_attempt(email)
            Lockout->>Redis: INCR login_attempts:{email}
            alt Attempts >= MAX_FAILED_LOGIN_ATTEMPTS
                Lockout->>Redis: SET account_locked:{email} (with TTL)
            end
            API-->>Client: 401 INVALID_CREDENTIALS
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

authentication, database, backend, devops

Poem

🐰 A bunny hops through SQLite's past,
Now dancing with UUIDs at last!
With Redis locks and tokens bright,
Login flows sparkle in the night. ✨🔐

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: migrating user identities from integer to UUIDv4, which aligns with the core objective of converting User.id and VerificationToken.user_id types throughout the codebase.

✏️ 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-login

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.

Comment thread app/services/account_lockout.py Fixed
Comment thread alembic/versions/7f066a8213a8_change_user_id_to_uuid.py Fixed
Comment thread alembic/versions/7f066a8213a8_change_user_id_to_uuid.py Fixed
# revision identifiers, used by Alembic.
revision: str = '7f066a8213a8'
down_revision: Union[str, Sequence[str], None] = 'e1a664780dc6'
branch_labels: Union[str, Sequence[str], None] = None

Check notice

Code scanning / CodeQL

Unused global variable Note

The global variable 'branch_labels' is not used.

Copilot Autofix

AI 28 days ago

In general, to fix an unused global variable you either remove the assignment if it serves no purpose, or, if it is intentionally unused, rename it to follow an “unused” naming convention (e.g. prefix with unused_ or use _-style names). Here, branch_labels is set to None and is not used anywhere; Alembic does not require it to be present for a simple linear migration. The safest and cleanest fix without changing existing behavior is to delete the branch_labels line entirely and leave revision, down_revision, and depends_on untouched. This keeps the migration semantics the same while eliminating the unused variable.

Concretely, in alembic/versions/7f066a8213a8_change_user_id_to_uuid.py, remove line 17:

branch_labels: Union[str, Sequence[str], None] = None

No new imports, functions, or other definitions are needed.

Suggested changeset 1
alembic/versions/7f066a8213a8_change_user_id_to_uuid.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/alembic/versions/7f066a8213a8_change_user_id_to_uuid.py b/alembic/versions/7f066a8213a8_change_user_id_to_uuid.py
--- a/alembic/versions/7f066a8213a8_change_user_id_to_uuid.py
+++ b/alembic/versions/7f066a8213a8_change_user_id_to_uuid.py
@@ -14,7 +14,6 @@
 # revision identifiers, used by Alembic.
 revision: str = "7f066a8213a8"
 down_revision: Union[str, Sequence[str], None] = "e1a664780dc6"
-branch_labels: Union[str, Sequence[str], None] = None
 depends_on: Union[str, Sequence[str], None] = None
 
 
EOF
@@ -14,7 +14,6 @@
# revision identifiers, used by Alembic.
revision: str = "7f066a8213a8"
down_revision: Union[str, Sequence[str], None] = "e1a664780dc6"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


Copilot is powered by AI and may make mistakes. Always verify output.
revision: str = '7f066a8213a8'
down_revision: Union[str, Sequence[str], None] = 'e1a664780dc6'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None

Check notice

Code scanning / CodeQL

Unused global variable Note

The global variable 'depends_on' is not used.

Copilot Autofix

AI 28 days ago

To fix an unused global variable, either remove its assignment if it has no side effects and is not needed, or rename it to a clearly “unused” style if it must remain. Here, depends_on is assigned None with no side effects, and Alembic does not require depends_on to be present when it is None. Removing the line will not change behavior.

Concretely, in alembic/versions/7f066a8213a8_change_user_id_to_uuid.py, delete the line that assigns depends_on (line 18). No new imports, functions, or variables are required; we only remove this redundant declaration and leave the rest of the Alembic revision identifiers unchanged.

Suggested changeset 1
alembic/versions/7f066a8213a8_change_user_id_to_uuid.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/alembic/versions/7f066a8213a8_change_user_id_to_uuid.py b/alembic/versions/7f066a8213a8_change_user_id_to_uuid.py
--- a/alembic/versions/7f066a8213a8_change_user_id_to_uuid.py
+++ b/alembic/versions/7f066a8213a8_change_user_id_to_uuid.py
@@ -15,7 +15,6 @@
 revision: str = "7f066a8213a8"
 down_revision: Union[str, Sequence[str], None] = "e1a664780dc6"
 branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
 
 
 def upgrade() -> None:
EOF
@@ -15,7 +15,6 @@
revision: str = "7f066a8213a8"
down_revision: Union[str, Sequence[str], None] = "e1a664780dc6"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread alembic/versions/e1a664780dc6_add_meeting_model.py Fixed
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: 14

♻️ Duplicate comments (1)
test_output.txt (1)

1-42: ⚠️ Potential issue | 🟡 Minor

Remove test output files from the repository.

As noted for fail_log.txt, test output logs are development artifacts that should not be committed. Add these patterns to .gitignore:

fail_log.txt
test_output.txt
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test_output.txt` around lines 1 - 42, The repo contains committed test output
files (e.g., test_output.txt and fail_log.txt) which should be removed and
ignored; update .gitignore to include the patterns "fail_log.txt" and
"test_output.txt", remove the tracked files from Git (e.g., git rm --cached
<file> or delete and commit the deletion) so they are no longer in the index,
and commit the updated .gitignore and removal so future test artifacts are not
re-committed.
🧹 Nitpick comments (4)
app/kafka/schemas.py (1)

47-50: UserRegisteredEvent doesn't follow the BaseEvent[T] pattern.

Unlike EmailEvent(BaseEvent[EmailPayload]) and MediaUploadEvent(BaseEvent[MediaUploadPayload]), this event extends BaseEvent without a type parameter, leaving payload typed as Any. Additionally, user_id and email are defined at the event level rather than within a payload class.

If this event needs to carry user registration data, consider creating a UserRegisteredPayload and using BaseEvent[UserRegisteredPayload]:

♻️ Suggested refactor
+class UserRegisteredPayload(BaseModel):
+    user_id: uuid.UUID
+    email: EmailStr
+

-class UserRegisteredEvent(BaseEvent):
-    user_id: uuid.UUID
-    email: EmailStr
+class UserRegisteredEvent(BaseEvent[UserRegisteredPayload]):
+    event_type: str = "user.registered"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/kafka/schemas.py` around lines 47 - 50, UserRegisteredEvent currently
subclasses BaseEvent without a generic and declares user_id and email at the
event level, so payload is typed as Any; create a new dataclass/Pydantic model
named UserRegisteredPayload containing user_id: uuid.UUID and email: EmailStr,
then change UserRegisteredEvent to extend BaseEvent[UserRegisteredPayload] and
remove user_id/email from the event class so the event uses the typed payload;
also update any imports/annotations referencing UserRegisteredEvent.payload to
the new payload type.
README.md (1)

133-141: Consolidate duplicate migration instructions.

Lines 133–141 duplicate the same action already described in the following section. Keeping one canonical command block will reduce documentation drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 133 - 141, Remove the duplicate migration command by
consolidating the two headings "Migrate Head" and "Applying Migrations" into a
single canonical block; keep one command (prefer the explicit python -m alembic
upgrade head or the simpler alembic upgrade head per project convention), delete
the other heading and its code fence, and ensure the remaining section title and
command accurately describe how to run migrations.
app/services/account_lockout.py (1)

71-74: Consider TTL on the attempts counter to prevent unbounded persistence.

Per the docstring (line 9), login_attempts:{email} has no TTL and is only cleared on successful login. If a user never logs in successfully after a few failed attempts (but below threshold), the counter persists indefinitely.

Consider adding a TTL (e.g., 24 hours) to the counter on incr to auto-expire stale attempt counters for abandoned accounts.

Optional: Add TTL to attempts counter
     async def record_failed_attempt(self, email: str) -> None:
         """Increment the failure counter and lock the account if threshold reached."""
         attempts_key = self._attempts_key(email)
         count = await self._redis.incr(attempts_key)
+        # Auto-expire attempts after 24 hours if no further activity
+        await self._redis.expire(attempts_key, 86400)
 
         if count >= self._max_attempts:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/services/account_lockout.py` around lines 71 - 74, The attempts counter
in record_failed_attempt currently increments via self._redis.incr(attempts_key)
with no TTL; change record_failed_attempt to set an expiration (e.g., 24 hours)
after calling self._redis.incr by calling the Redis expire/pexpire method on
attempts_key (or use a single call that sets/increments with TTL if your client
supports it) so that login_attempts:{email} auto-expires for stale accounts;
reference the record_failed_attempt method, attempts_key variable, and
self._redis.incr call when making the change.
app/core/security.py (1)

54-55: Avoid silently swallowing unexpected verification failures.

Line 54 catches all exceptions and returns False, which hides backend/runtime faults and makes auth incidents hard to triage. Log unexpected failures at least once.

🛠️ Proposed observability improvement
+import logging
@@
+logger = logging.getLogger(__name__)
@@
-            except Exception:
-                return False
+            except Exception:
+                logger.exception("Unexpected error during password verification fallback")
+                return False
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/core/security.py` around lines 54 - 55, The except block that currently
reads "except Exception: return False" hides runtime errors; update the
exception handler in the function containing that block (the verification
routine in app/core/security.py) to log the exception before returning False —
e.g., catch as "except Exception as e:" and call the module logger (or Python
logging) with logging.exception(...) or logger.error(..., exc_info=True)
including context like the token value or user id where available; keep the
behavior of returning False for verification failures but ensure unexpected
errors are logged once for triage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.env.example:
- Line 17: The DATABASE_URL entry in .env.example currently has surrounding
quotes which trips dotenv-linter; update the DATABASE_URL variable
(DATABASE_URL) by removing the surrounding double quotes so it appears as an
unquoted assignment (e.g., DATABASE_URL=postgresql+psycopg2://...) to satisfy
the linter and preserve the same connection string.

In `@alembic_output.txt`:
- Around line 1-3: Remove the committed Alembic run log file alembic_output.txt
from the repository (delete the file and stage the removal) and update
.gitignore to prevent future commits by adding an entry for alembic_output.txt
or a suitable pattern (e.g., alembic_*.txt or *.txt logs) so Alembic execution
logs are ignored going forward; ensure the removal is included in the same
commit that updates .gitignore.

In `@alembic/versions/7f066a8213a8_change_user_id_to_uuid.py`:
- Around line 44-55: The downgrade() implementation is incomplete and will fail
converting UUIDs back to INTEGER; either implement a proper reversal that
mirrors upgrade() (remove/recreate foreign key constraints around changing
users.id and verification_tokens.user_id, perform safe data conversion/casting
or a migration script that maps UUIDs back to integer ids, and recreate the FK)
or explicitly disable downgrade by replacing downgrade() with a single raise
NotImplementedError("Downgrade from UUID to INTEGER is not supported");
reference the downgrade() function and the alter_column calls on 'users' id and
'verification_tokens' user_id when making this change.
- Around line 27-28: The migration currently executes op.execute('TRUNCATE TABLE
users CASCADE') which is data-destructive; either replace this with a safe
conversion (add a new UUID column on users, backfill UUIDs from existing ids,
update foreign keys, then drop/rename columns) or if truncation is intentional
for fresh deployments add a prominent WARNING comment above the op.execute line
and a runtime guard that prevents running unless an explicit DEV_ONLY flag is
set; reference the truncate call and the migration file's upgrade function to
locate where to add the safe backfill logic or the warning/guard.
- Around line 14-18: The Alembic revision module (look for the module defining
the symbols revision and down_revision in the new migration) fails CI due to
formatting; run the project's formatter (black) on that migration file so the
top-level declarations (revision, down_revision, branch_labels, depends_on) and
surrounding imports/whitespace conform to Black’s style, then re-run tests/CI.

In `@alembic/versions/e1a664780dc6_add_meeting_model.py`:
- Around line 14-18: The file fails the project's Black formatting check; run
the formatter or apply Black-style changes to this Alembic revision file (the
block containing the module-level variables revision and down_revision) so it
matches the project's formatting rules—e.g., run `black
alembic/versions/e1a664780dc6_add_meeting_model.py` or reformat the lines around
the revision: str = 'e1a664780dc6' and down_revision: Union[str, Sequence[str],
None] = '19dc9714d9ea' to satisfy `black --check .`.
- Around line 21-31: The migration titled "Add Meeting model" is empty: update
the upgrade() and downgrade() functions or clarify intent—either implement the
missing Meeting model DDL in upgrade()/downgrade() (referencing the Alembic
revision functions upgrade and downgrade) or leave them empty but (1) change the
migration message/revision title to accurately describe its purpose (e.g.,
"checkpoint before UUID migration") and (2) remove the unused import sqlalchemy
as sa and add a short comment inside upgrade()/downgrade() explaining that the
migration is intentionally empty for now. Ensure the revision title and
docstring reflect the chosen action.

In `@app/core/config.py`:
- Around line 24-30: Add explicit bounds validation for the security-critical
constants in config.py: validate ACCESS_TOKEN_EXPIRE_MINUTES,
REFRESH_TOKEN_EXPIRE_DAYS, VERIFICATION_TOKEN_EXPIRE_HOURS,
MAX_FAILED_LOGIN_ATTEMPTS, and ACCOUNT_LOCKOUT_DAYS after they are loaded from
env (or set to defaults) and either clamp them to sane minimums or raise a clear
exception when values are ≤ 0; for example enforce ACCESS_TOKEN_EXPIRE_MINUTES
>= 1, REFRESH_TOKEN_EXPIRE_DAYS >= 1, VERIFICATION_TOKEN_EXPIRE_HOURS >= 1,
MAX_FAILED_LOGIN_ATTEMPTS >= 1, and ACCOUNT_LOCKOUT_DAYS >= 1 using a simple
check that raises ValueError with a descriptive message mentioning the offending
symbol (e.g., ACCESS_TOKEN_EXPIRE_MINUTES) so misconfiguration fails fast
instead of allowing invalid token/lockout behavior.

In `@app/core/security.py`:
- Around line 21-23: SecurityService currently signs JWTs without validating
runtime config; add a fail-fast check in SecurityService.__init__ to validate
the SECRET_KEY (not equal to the known placeholder, non-empty, and meets minimum
entropy/length) and validate JWT_ALGORITHM is a secure algorithm (e.g., starts
with "HS" only if SECRET_KEY strong or prefer "RS"/"ES"); if checks fail, raise
a RuntimeError with a clear message. Also add the same guard at entrypoints that
sign tokens (e.g., create_access_token, create_refresh_token or any methods
around lines referenced 81-85 and 108-112) to avoid issuing tokens if config is
insecure, and ensure any logging includes the offending config key name but
never prints secret values.

In `@app/db/session.py`:
- Around line 16-32: There is a mismatch between the app's _coerce_sync_url
(which forces psycopg2) and Alembic's async config; either make migrations
synchronous or commit to async migrations—pick one: (A) To align migrations with
the app, change alembic.ini to use postgresql+psycopg2 URI and swap
async_engine_from_config/any async-specific code in alembic/env.py to the
synchronous engine_from_config and standard run_migrations_online/offline flow
so Alembic uses psycopg2, or (B) to keep async migrations, document this and
ensure asyncpg is listed in dependencies and keep alembic.ini and env.py using
async_engine_from_config; update tests/README accordingly. Reference:
_coerce_sync_url, alembic.ini, and alembic/env.py (async_engine_from_config).

In `@app/services/account_lockout.py`:
- Around line 84-88: The logger.warning call is logging user-controlled email
directly; wrap the email with the existing sanitize_log_args helper (as used in
app/api/v1/endpoints/auth.py) before passing it to logger.warning to prevent log
injection, and add an import for sanitize_log_args if missing; specifically
update the arguments passed to logger.warning (the call to logger.warning in
account_lockout.py) to use sanitize_log_args(email) so sanitized output is
logged.

In `@fail_log.txt`:
- Around line 1-42: Remove the committed test output file fail_log.txt from the
repository and add an entry for test output logs (e.g., fail_log.txt and
test_output.txt) to .gitignore so these transient artifacts are not tracked;
then update the failing assertion in the test method
test_returns_access_token_and_user_id (TestLoginSuccess) to account for JSON
serialization by comparing body["user_id"] to str(user.id) or by parsing
body["user_id"] into a UUID before comparing to user.id (i.e., change the
comparison involving body["user_id"] and user.id to use matching types).

In `@test_output_full.txt`:
- Around line 1-30: The committed test output file test_output_full.txt is a
local CI/test artifact and should be removed from version control: remove
test_output_full.txt from the repository (use git rm --cached or equivalent to
stop tracking but keep local copy if desired), add "test_output_full.txt" (or a
pattern like "*.log" or "test_output*.txt") to your .gitignore so it won’t be
re-added, commit the removal and .gitignore change, and instead include test
summaries or attach CI artifacts to the PR/CI system rather than committing the
raw log file.
- Around line 25-27: Pytest is warning because the helper class TestConsumer
(subclassing BaseConsumer) is being collected as a test class; to fix, either
rename the class to a non-test name (e.g., MockConsumer) or add the attribute
__test__ = False to the TestConsumer class definition so pytest skips it; update
the class declaration for TestConsumer (the subclass of BaseConsumer)
accordingly and run the test collection to confirm the warning is gone.

---

Duplicate comments:
In `@test_output.txt`:
- Around line 1-42: The repo contains committed test output files (e.g.,
test_output.txt and fail_log.txt) which should be removed and ignored; update
.gitignore to include the patterns "fail_log.txt" and "test_output.txt", remove
the tracked files from Git (e.g., git rm --cached <file> or delete and commit
the deletion) so they are no longer in the index, and commit the updated
.gitignore and removal so future test artifacts are not re-committed.

---

Nitpick comments:
In `@app/core/security.py`:
- Around line 54-55: The except block that currently reads "except Exception:
return False" hides runtime errors; update the exception handler in the function
containing that block (the verification routine in app/core/security.py) to log
the exception before returning False — e.g., catch as "except Exception as e:"
and call the module logger (or Python logging) with logging.exception(...) or
logger.error(..., exc_info=True) including context like the token value or user
id where available; keep the behavior of returning False for verification
failures but ensure unexpected errors are logged once for triage.

In `@app/kafka/schemas.py`:
- Around line 47-50: UserRegisteredEvent currently subclasses BaseEvent without
a generic and declares user_id and email at the event level, so payload is typed
as Any; create a new dataclass/Pydantic model named UserRegisteredPayload
containing user_id: uuid.UUID and email: EmailStr, then change
UserRegisteredEvent to extend BaseEvent[UserRegisteredPayload] and remove
user_id/email from the event class so the event uses the typed payload; also
update any imports/annotations referencing UserRegisteredEvent.payload to the
new payload type.

In `@app/services/account_lockout.py`:
- Around line 71-74: The attempts counter in record_failed_attempt currently
increments via self._redis.incr(attempts_key) with no TTL; change
record_failed_attempt to set an expiration (e.g., 24 hours) after calling
self._redis.incr by calling the Redis expire/pexpire method on attempts_key (or
use a single call that sets/increments with TTL if your client supports it) so
that login_attempts:{email} auto-expires for stale accounts; reference the
record_failed_attempt method, attempts_key variable, and self._redis.incr call
when making the change.

In `@README.md`:
- Around line 133-141: Remove the duplicate migration command by consolidating
the two headings "Migrate Head" and "Applying Migrations" into a single
canonical block; keep one command (prefer the explicit python -m alembic upgrade
head or the simpler alembic upgrade head per project convention), delete the
other heading and its code fence, and ensure the remaining section title and
command accurately describe how to run migrations.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 26868774-e608-4459-839d-a74da946d1fc

📥 Commits

Reviewing files that changed from the base of the PR and between ec80e0d and df887e3.

📒 Files selected for processing (27)
  • .env.example
  • README.md
  • alembic/versions/7f066a8213a8_change_user_id_to_uuid.py
  • alembic/versions/e1a664780dc6_add_meeting_model.py
  • alembic_output.txt
  • app/api/v1/endpoints/auth.py
  • app/core/config.py
  • app/core/security.py
  • app/crud/user/user.py
  • app/crud/verification_token.py
  • app/db/session.py
  • app/kafka/schemas.py
  • app/models/user.py
  • app/models/verification_token.py
  • app/schemas/auth.py
  • app/schemas/user.py
  • app/services/account_lockout.py
  • app/services/auth_verification.py
  • app/services/token_store.py
  • fail_log.txt
  • requirements.txt
  • ruff_output.txt
  • test_output.txt
  • test_output_full.txt
  • tests/test_auth/test_auth_login.py
  • tests/test_auth/test_schemas_user.py
  • tests/test_core/test_security.py

Comment thread .env.example Outdated
Comment thread alembic_output.txt
Comment on lines +1 to +3
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
INFO [alembic.runtime.migration] Running upgrade 19dc9714d9ea -> e1a664780dc6, Add Meeting model
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

Remove migration log from repository.

Similar to the test output files, Alembic execution logs are development artifacts. Consider adding alembic_output.txt or *.txt logs to .gitignore.

🧰 Tools
🪛 LanguageTool

[style] ~2-~2: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...gration] Will assume transactional DDL. INFO [alembic.runtime.migration] Running up...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@alembic_output.txt` around lines 1 - 3, Remove the committed Alembic run log
file alembic_output.txt from the repository (delete the file and stage the
removal) and update .gitignore to prevent future commits by adding an entry for
alembic_output.txt or a suitable pattern (e.g., alembic_*.txt or *.txt logs) so
Alembic execution logs are ignored going forward; ensure the removal is included
in the same commit that updates .gitignore.

Comment thread alembic/versions/7f066a8213a8_change_user_id_to_uuid.py
Comment on lines +27 to +28
# We truncate because casting int -> uuid with a random function would break existing foreign key relations.
op.execute('TRUNCATE TABLE users CASCADE')
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

⚠️ Data-destructive migration: TRUNCATE TABLE users CASCADE.

This migration will permanently delete all user data and any records in tables with foreign keys to users. Ensure this is acceptable for your deployment strategy:

  • Development/staging: Acceptable if data loss is expected
  • Production with existing users: This migration cannot be applied without data loss. Consider an alternative approach that preserves data (e.g., adding a new UUID column, backfilling, then swapping)

If this is intentional for a fresh deployment, add a prominent comment or guard:

# WARNING: This migration truncates all user data. 
# Only run on fresh/development databases.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@alembic/versions/7f066a8213a8_change_user_id_to_uuid.py` around lines 27 -
28, The migration currently executes op.execute('TRUNCATE TABLE users CASCADE')
which is data-destructive; either replace this with a safe conversion (add a new
UUID column on users, backfill UUIDs from existing ids, update foreign keys,
then drop/rename columns) or if truncation is intentional for fresh deployments
add a prominent WARNING comment above the op.execute line and a runtime guard
that prevents running unless an explicit DEV_ONLY flag is set; reference the
truncate call and the migration file's upgrade function to locate where to add
the safe backfill logic or the warning/guard.

Comment on lines +44 to +55
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('verification_tokens', 'user_id',
existing_type=sa.Uuid(),
type_=sa.INTEGER(),
existing_nullable=False)
op.alter_column('users', 'id',
existing_type=sa.Uuid(),
type_=sa.INTEGER(),
existing_nullable=False)
# ### end Alembic commands ###
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

Incomplete downgrade() will fail.

The downgrade() function attempts to convert UUID columns back to INTEGER but:

  1. Doesn't drop/recreate the foreign key constraint (upgrade does)
  2. Doesn't handle data conversion (UUIDs can't cast to integers)
  3. Will fail with a constraint violation or type conversion error

If downgrade support is required, it should mirror upgrade:

♻️ Suggested fix for downgrade
 def downgrade() -> None:
     """Downgrade schema."""
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.alter_column('verification_tokens', 'user_id',
-               existing_type=sa.Uuid(),
-               type_=sa.INTEGER(),
-               existing_nullable=False)
-    op.alter_column('users', 'id',
-               existing_type=sa.Uuid(),
-               type_=sa.INTEGER(),
-               existing_nullable=False)
-    # ### end Alembic commands ###
+    # WARNING: Downgrade will lose all user data
+    op.drop_constraint('verification_tokens_user_id_fkey', 'verification_tokens', type_='foreignkey')
+    op.execute('TRUNCATE TABLE users CASCADE')
+    op.execute('ALTER TABLE verification_tokens ALTER COLUMN user_id SET DATA TYPE INTEGER USING 1')
+    op.execute('ALTER TABLE users ALTER COLUMN id SET DATA TYPE INTEGER USING 1')
+    op.execute('ALTER TABLE users ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY')
+    op.create_foreign_key('verification_tokens_user_id_fkey', 'verification_tokens', 'users', ['user_id'], ['id'])

Alternatively, if downgrade isn't supported, raise NotImplementedError:

def downgrade() -> None:
    raise NotImplementedError("Downgrade from UUID to INTEGER is not supported")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@alembic/versions/7f066a8213a8_change_user_id_to_uuid.py` around lines 44 -
55, The downgrade() implementation is incomplete and will fail converting UUIDs
back to INTEGER; either implement a proper reversal that mirrors upgrade()
(remove/recreate foreign key constraints around changing users.id and
verification_tokens.user_id, perform safe data conversion/casting or a migration
script that maps UUIDs back to integer ids, and recreate the FK) or explicitly
disable downgrade by replacing downgrade() with a single raise
NotImplementedError("Downgrade from UUID to INTEGER is not supported");
reference the downgrade() function and the alter_column calls on 'users' id and
'verification_tokens' user_id when making this change.

Comment thread app/db/session.py
Comment on lines +16 to +32
def _coerce_sync_url(url: str) -> str:
"""Replace the async ``asyncpg`` driver with sync ``psycopg2``.

The application uses synchronous SQLAlchemy (``create_engine`` +
``Session``), so the ``asyncpg`` DBAPI - which requires
``create_async_engine`` - will fail at runtime with a
``MissingGreenlet`` error. This helper silently swaps the driver
so that the connection string from ``.env`` works out of the box.
"""
if "+asyncpg" in url:
fixed = url.replace("+asyncpg", "+psycopg2")
logger.info(
"Replaced async driver 'asyncpg' with sync driver 'psycopg2' "
"in DATABASE_URL."
)
return fixed
return 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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify alembic configuration and env.py migration approach
echo "=== alembic.ini sqlalchemy.url ==="
rg -n 'sqlalchemy\.url' alembic.ini

echo ""
echo "=== alembic/env.py engine creation ==="
rg -n 'engine_from_config|create_engine|create_async_engine' alembic/env.py

Repository: Brints/FluentMeet

Length of output: 329


Alembic configuration mismatch: migrations use asyncpg while application uses psycopg2.

The _coerce_sync_url function converts DATABASE_URL from asyncpg to psycopg2, allowing the application to run with synchronous SQLAlchemy. However, alembic.ini (line 89) still hardcodes postgresql+asyncpg://... and alembic/env.py uses async_engine_from_config(), creating a divergence:

  • App runtime: psycopg2 (sync)
  • Migrations: asyncpg (async)

Either update alembic.ini and env.py to use synchronous migrations with psycopg2, or clarify that async migrations are intentional and ensure asyncpg is installed alongside psycopg2.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/db/session.py` around lines 16 - 32, There is a mismatch between the
app's _coerce_sync_url (which forces psycopg2) and Alembic's async config;
either make migrations synchronous or commit to async migrations—pick one: (A)
To align migrations with the app, change alembic.ini to use postgresql+psycopg2
URI and swap async_engine_from_config/any async-specific code in alembic/env.py
to the synchronous engine_from_config and standard run_migrations_online/offline
flow so Alembic uses psycopg2, or (B) to keep async migrations, document this
and ensure asyncpg is listed in dependencies and keep alembic.ini and env.py
using async_engine_from_config; update tests/README accordingly. Reference:
_coerce_sync_url, alembic.ini, and alembic/env.py (async_engine_from_config).

Comment thread app/services/account_lockout.py
Comment thread fail_log.txt
Comment on lines +1 to +42
============================= test session starts =============================
platform win32 -- Python 3.13.12, pytest-9.0.2, pluggy-1.6.0
rootdir: C:\Users\afiaa\Desktop\projects\Brints\FluentMeet
configfile: pyproject.toml
plugins: anyio-4.12.1, asyncio-1.3.0, cov-7.0.0
asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 13 items

tests\test_auth\test_schemas_user.py F [ 7%]
tests\test_auth\test_auth_login.py F........... [100%]

================================== FAILURES ===================================
_______________ test_user_response_can_validate_from_attributes _______________
tests\test_auth\test_schemas_user.py:19: in test_user_response_can_validate_from_attributes
result = UserResponse.model_validate(source)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E pydantic_core._pydantic_core.ValidationError: 1 validation error for UserResponse
E id
E UUID input should be a string, bytes or UUID object [type=uuid_type, input_value=123, input_type=int]
E For further information visit https://errors.pydantic.dev/2.12/v/uuid_type
___________ TestLoginSuccess.test_returns_access_token_and_user_id ____________
tests\test_auth\test_auth_login.py:188: in test_returns_access_token_and_user_id
assert body["user_id"] == user.id
E AssertionError: assert 'fe03281c-c879-493c-a135-f0fb8603c156' == UUID('fe03281c-c879-493c-a135-f0fb8603c156')
E + where UUID('fe03281c-c879-493c-a135-f0fb8603c156') = <app.models.user.User object at 0x000001DDD85DEF90>.id
------------------------------ Captured log call ------------------------------
WARNING passlib.handlers.bcrypt:bcrypt.py:642 (trapped) error reading bcrypt version
Traceback (most recent call last):
File "C:\Users\afiaa\Desktop\projects\Brints\FluentMeet\.venv\Lib\site-packages\passlib\handlers\bcrypt.py", line 640, in _load_backend_mixin
version = _bcrypt.__about__.__version__
^^^^^^^^^^^^^^^^^
AttributeError: module 'bcrypt' has no attribute '__about__'
WARNING passlib.handlers.bcrypt:bcrypt.py:642 (trapped) error reading bcrypt version
Traceback (most recent call last):
File "C:\Users\afiaa\Desktop\projects\Brints\FluentMeet\.venv\Lib\site-packages\passlib\handlers\bcrypt.py", line 640, in _load_backend_mixin
version = _bcrypt.__about__.__version__
^^^^^^^^^^^^^^^^^
AttributeError: module 'bcrypt' has no attribute '__about__'
=========================== short test summary info ===========================
FAILED tests/test_auth/test_schemas_user.py::test_user_response_can_validate_from_attributes
FAILED tests/test_auth/test_auth_login.py::TestLoginSuccess::test_returns_access_token_and_user_id
======================== 2 failed, 11 passed in 45.84s ========================
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

Remove this development artifact from the repository.

Test output logs should not be committed to the codebase. These are transient development artifacts that add noise to the repository history. Consider adding fail_log.txt and test_output.txt to .gitignore.

Additionally, this log indicates the test at test_auth_login.py:188 compares a JSON string (body["user_id"]) against a UUID object (user.id). The test assertion should account for the JSON serialization:

# Instead of:
assert body["user_id"] == user.id
# Use:
assert body["user_id"] == str(user.id)
# Or:
assert UUID(body["user_id"]) == user.id
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fail_log.txt` around lines 1 - 42, Remove the committed test output file
fail_log.txt from the repository and add an entry for test output logs (e.g.,
fail_log.txt and test_output.txt) to .gitignore so these transient artifacts are
not tracked; then update the failing assertion in the test method
test_returns_access_token_and_user_id (TestLoginSuccess) to account for JSON
serialization by comparing body["user_id"] to str(user.id) or by parsing
body["user_id"] into a UUID before comparing to user.id (i.e., change the
comparison involving body["user_id"] and user.id to use matching types).

Comment thread test_output_full.txt Outdated
Comment thread test_output_full.txt Outdated
Comment on lines +25 to +27
tests\test_kafka\test_consumer.py:13
C:\Users\afiaa\Desktop\projects\Brints\FluentMeet\tests\test_kafka\test_consumer.py:13: PytestCollectionWarning: cannot collect test class 'TestConsumer' because it has a __init__ constructor (from: tests/test_kafka/test_consumer.py)
class TestConsumer(BaseConsumer):
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

Clean up the pytest collection warning for TestConsumer.

PytestCollectionWarning indicates pytest is trying to collect TestConsumer as a test class. In tests/test_kafka/test_consumer.py:13-19, this class is a fixture helper, not a test case. Rename it (e.g., MockConsumer) or set __test__ = False on the class to suppress this warning and keep test output clean.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test_output_full.txt` around lines 25 - 27, Pytest is warning because the
helper class TestConsumer (subclassing BaseConsumer) is being collected as a
test class; to fix, either rename the class to a non-test name (e.g.,
MockConsumer) or add the attribute __test__ = False to the TestConsumer class
definition so pytest skips it; update the class declaration for TestConsumer
(the subclass of BaseConsumer) accordingly and run the test collection to
confirm the warning is gone.

Signed-off-by: aniebietafia <aniebietafia87@gmail.com>
Signed-off-by: aniebietafia <aniebietafia87@gmail.com>
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision: str = "7f066a8213a8"

Check notice

Code scanning / CodeQL

Unused global variable Note

The global variable 'revision' is not used.

Copilot Autofix

AI 28 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.


# revision identifiers, used by Alembic.
revision: str = "7f066a8213a8"
down_revision: Union[str, Sequence[str], None] = "e1a664780dc6"

Check notice

Code scanning / CodeQL

Unused global variable Note

The global variable 'down_revision' is not used.

Copilot Autofix

AI 28 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa

Check notice

Code scanning / CodeQL

Unused import Note

Import of 'sa' is not used.

Copilot Autofix

AI 28 days ago

To fix the problem, remove the unused import so that every imported name is actually referenced in the file. This keeps the code cleaner and avoids unnecessary dependencies.

Concretely, in alembic/versions/e1a664780dc6_add_meeting_model.py, delete the line import sqlalchemy as sa (line 12 in the snippet). No additional methods, imports, or definitions are required, because nothing in the file currently uses sa. The rest of the file, including the typing and alembic.op imports and the upgrade/downgrade stubs, can remain unchanged.

Suggested changeset 1
alembic/versions/e1a664780dc6_add_meeting_model.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/alembic/versions/e1a664780dc6_add_meeting_model.py b/alembic/versions/e1a664780dc6_add_meeting_model.py
--- a/alembic/versions/e1a664780dc6_add_meeting_model.py
+++ b/alembic/versions/e1a664780dc6_add_meeting_model.py
@@ -9,7 +9,6 @@
 from typing import Sequence, Union
 
 from alembic import op
-import sqlalchemy as sa
 
 # revision identifiers, used by Alembic.
 revision: str = "e1a664780dc6"
EOF
@@ -9,7 +9,6 @@
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision: str = "e1a664780dc6"
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link
Copy Markdown
Contributor

@aniebiet-afia aniebiet-afia left a comment

Choose a reason for hiding this comment

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

lgtm

@aniebiet-afia aniebiet-afia merged commit e68112b into main Mar 27, 2026
9 checks passed
@aniebietafia aniebietafia linked an issue Mar 27, 2026 that may be closed by this pull request
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

User Authentication Endpoint

3 participants