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

feat(auth): Implement Login Flow(BE-007) - #15

Merged
TheCodeGhinux merged 31 commits into
devfrom
feat/BE-007-login-flow
May 10, 2026
Merged

TheCodeGhinux merged 31 commits into
devfrom
feat/BE-007-login-flow

Conversation

@sage-ali

@sage-ali sage-ali commented May 10, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Description

This PR implements the secure Login Flow & Rate Limiting system (BE-007). It introduces a robust authentication mechanism designed to protect users against brute-force attacks and credential stuffing through a multi-layered security shield involving PostgreSQL for persistent metadata and Redis for session orchestration.

Key Changes:

  • Secure Authentication: Implemented POST /auth/login with Bcrypt credential validation.
  • Timing Safety: Integrated a "Dummy Hash" comparison to ensure response times are identical for existing vs. non-existent users, preventing user enumeration.
  • Smart Lockout: Configured an automated lockout mechanism (5 failed attempts = 15-minute lockout) using database-level atomic updates to prevent race conditions.
  • Session Management: Implemented dual-token issuance (JWT + Refresh Token) with session tracking in the user_sessions table.
  • Audit Logging: Added logging for failed attempts including IP tracking and timestamps for security reviews.

Related Issue

Fixes [BE-007 Login Flow & Rate Limiting](BE-007 Login Flow & Rate Limiting)

Type of Change

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation updates
  • style: Code style/formatting changes
  • refactor: Code refactoring
  • perf: Performance improvements
  • test: Test additions/updates
  • chore: Build process or tooling changes
  • ci: CI configuration changes
  • other: ## How Has This Been Tested?

How Has This Been Tested?

  • Unit tests
  • Integration tests (Verified lockout trigger after 5th attempt in Postman)
  • Manual tests (Verified timing safety and auto-unlock after 15 minutes)

Test Evidence

image image image

Screenshots (if applicable)

Documentation Screenshots (if applicable)

Checklist

  • My code follows the project's coding style
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published
  • I have included a screenshot showing all tests passing
  • I have included documentation screenshots (if applicable)

Additional Notes

To ensure consistency across distributed nodes, the lockout logic relies on CURRENT_TIMESTAMP from the database. The system also includes a persistence fallback; if Redis is unavailable, security metadata remains enforced via the PostgreSQL auth_metadata table. Standardized error codes (401, 403, 429) have been implemented as per the acceptance criteria.

Summary by CodeRabbit

  • New Features

    • Account lockout: repeated failed logins increment a counter and temporarily lock accounts; locked-account responses are surfaced.
  • Security Improvements

    • Timing-safe password checks, stricter password max-length validation (72 chars), improved session and token handling with safer refresh sessions.
  • Documentation

    • API docs updated to reflect locked-account responses and consolidated auth endpoint documentation.

Review Change Stack

@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai

coderabbitai Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@sage-ali has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 32 seconds before requesting another review.

You’ve run out of usage credits. Purchase more 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: 82e5566c-a524-4206-84ef-6b22c2a5b585

📥 Commits

Reviewing files that changed from the base of the PR and between 086c723 and 4b7cf21.

📒 Files selected for processing (6)
  • src/modules/auth/auth.controller.ts
  • src/modules/auth/auth.module.ts
  • src/modules/auth/auth.service.ts
  • src/modules/auth/lockout.service.ts
  • src/modules/auth/session.service.ts
  • src/modules/auth/tests/auth.service.spec.ts
📝 Walkthrough

Walkthrough

Adds DB and Redis-backed failed-attempt tracking and lockout to login, updates AuthenticationService to enforce lockout and create sessions with refresh tokens, adds DTO password max-length validation, migrates schema, updates Swagger docs, and expands tests.

Changes

Login Lockout & Rate-Limiting Feature

Layer / File(s) Summary
Data Schema & Migration
src/modules/auth/entities/auth-metadata.entity.ts, src/database/migrations/1778323146003-migration.ts, src/database/migrations/1778406115466-add-unique-user-id-to-auth-metadata.ts
auth_metadata gains a non-null failed_attempts integer column defaulting to 0; a unique constraint on auth_metadata.user_id is added; user_roles_role_enum is recreated and migration down preserves reversal steps.
Core Auth & Session Logic
src/modules/auth/auth.service.ts
AuthenticationService injects AuthMetadata repo, DataSource, and RedisService; implements OnModuleInit to precompute a dummy bcrypt hash; loginUser(loginDto, ip) adds Redis fast-path lockout check, timing-safe handling for missing users, DB lockout checks, atomic failed_attempts/locked_until updates, and delegates to session creation that persists hashed refresh tokens and signs access tokens with sid.
Auth Helpers & Persistence
src/modules/auth/auth.service.ts
New helpers load-or-create auth metadata, record failed attempts (atomic DB update + optional lock), increment Redis counters while swallowing Redis errors, audit login events, and create/persist UserSession with refresh token hashing inside a DB transaction.
Controller, DTO & Swagger
src/modules/auth/auth.controller.ts, src/modules/auth/dto/login.dto.ts, src/modules/auth/docs/auth-swagger.doc.ts
login controller now accepts Request and forwards client IP to authService.loginUser; LoginDto.password adds @MaxLength(72); Swagger decorators extracted into LoginDocs() and ChangePasswordDocs() including 403 account-locked response.
Tests & Infrastructure
src/modules/auth/tests/auth.service.spec.ts
Tests mock and inject DataSource, RedisService, and AuthMetadata repo; call service.onModuleInit(); update login tests to pass IP and assert result.data.access_token; failure cases stub metadata and DB lock-status queries.
System Messages
src/shared/constants/SystemMessages.ts
Adds ACCOUNT_LOCKED and ACCOUNT_LOCKED_SECONDS(seconds: number) exports for lockout error messages used in responses/docs.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Controller
  participant AuthService
  participant Redis
  participant Database
  participant JwtService

  Client->>Controller: POST /login (email, password, IP)
  Controller->>AuthService: loginUser(dto, ip)
  
  AuthService->>Redis: Check lockout counter (fast-path)
  alt Account Locked
    AuthService-->>Client: 403 Account Locked
  else Not Locked
    AuthService->>Database: Query user by email
    alt User Not Found
      AuthService->>AuthService: Timing-safe dummy bcrypt compare
      AuthService->>Database: Increment failed_attempts + lock
      AuthService->>Redis: Increment fail counter
      AuthService-->>Client: 401 Invalid credentials
    else User Found
      AuthService->>Database: Check locked_until timestamp
      alt Database Lockout Active
        AuthService-->>Client: 403 Account Locked
      else Lockout Expired
        AuthService->>AuthService: Bcrypt verify password
        alt Password Invalid
          AuthService->>Database: Atomic: failed_attempts++, locked_until update
          AuthService->>Redis: Increment fail counter
          AuthService-->>Client: 401 Invalid credentials
        else Password Valid
          AuthService->>Database: Create UserSession + clear old lockouts (txn)
          AuthService->>Redis: Write session keys
          AuthService->>JwtService: Sign access token with sid
          AuthService-->>Client: 200 access_token + refresh_token
        end
      end
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • TheCodeGhinux
  • Homoakin619
  • ibraheembello
  • JohnUghiovhe
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 clearly and specifically describes the main feature being implemented: a login flow with rate limiting, and references the ticket identifier BE-007.
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.

✏️ 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/BE-007-login-flow

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.

@sage-ali sage-ali changed the title Feat(login): Implement Login Flow & Rate Limiting(BE-007) Feat(auth): Implement Login Flow & Rate Limiting(BE-007) May 10, 2026

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

Actionable comments posted: 3

🤖 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 `@src/modules/auth/auth.service.ts`:
- Around line 6-7: Persisting raw refresh tokens is insecure; change the flow so
create/generate refresh token functions (e.g.,
createRefreshToken/generateRefreshToken) continue to return the raw token to the
client but before persisting call a one-way hash (e.g., SHA-256) and store that
hash in the user_sessions record instead of the raw token; then in
validate/refresh/revoke paths (e.g.,
validateRefreshToken/refreshToken/revokeRefreshToken or any DB lookup using
user_sessions) hash the incoming raw token the same way and compare/delete based
on the hash. Update the code that writes/queries the user_sessions column to use
the hashed value and ensure the raw token is never persisted or logged.
- Around line 151-156: Add a DB-level uniqueness constraint on
auth_metadata.user_id and change loadOrCreateMeta to use an atomic upsert/select
instead of findOneBy-then-create: attempt to insert (via
authMetadataRepository.save/create or a repository.query with INSERT ... ON
CONFLICT DO NOTHING / ON CONFLICT (...) DO UPDATE SET ... RETURNING *) for {
user_id, failed_attempts: 0 } and if the insert did not return a row, select the
row afterwards; ensure loadOrCreateMeta always returns the single row and handle
unique-violation races by selecting the existing record on conflict.
- Around line 159-176: recordFailedAttempt currently always increments
failed_attempts so an expired lockout remains at threshold and re-locks on the
first new failure; update the UPDATE in recordFailedAttempt to reset the counter
when the prior lockout has expired by using locked_until < CURRENT_TIMESTAMP to
branch: if lock expired then set failed_attempts = 1 (this new failure) else
failed_attempts = failed_attempts + 1, and keep the existing logic that sets
locked_until when failed_attempts reaches MAX_FAILED_ATTEMPTS (use
MAX_FAILED_ATTEMPTS and LOCKOUT_MINUTES as before); keep the call to
incrementRedisFailCounter(email).
🪄 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: b9cc3a1c-661e-44c7-940d-0425d65cc0d4

📥 Commits

Reviewing files that changed from the base of the PR and between 0e25509 and c3ecc89.

📒 Files selected for processing (6)
  • src/database/migrations/1778323146003-migration.ts
  • src/modules/auth/auth.controller.ts
  • src/modules/auth/auth.service.ts
  • src/modules/auth/dto/login.dto.ts
  • src/modules/auth/entities/auth-metadata.entity.ts
  • src/modules/auth/tests/auth.service.spec.ts

Comment thread src/modules/auth/auth.service.ts Outdated
Comment thread src/modules/auth/auth.service.ts Outdated
Comment thread src/modules/auth/auth.service.ts Outdated
@sage-ali sage-ali changed the title Feat(auth): Implement Login Flow & Rate Limiting(BE-007) feat(auth): Implement Login Flow & Rate Limiting(BE-007) May 10, 2026

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

Approved

@Nuel-09
Nuel-09 self-requested a review May 10, 2026 08:00
Comment thread src/modules/auth/auth.controller.ts Outdated
@ApiOperation({ summary: 'Log a user in' })
@ApiBody({ type: LoginDto })
@ApiResponse({ status: HttpStatus.OK, description: SYS_MSG.LOGIN_SUCCESSFUL })
@ApiResponse({

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.

the controller should be clean. All swagger documentation should go into the docs folder then decorate the controller with the docs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

resolved in new commit

Comment thread src/modules/auth/auth.service.ts Outdated
try {
const redisFailCount = await this.redisService.get(`fail:${loginDto.email}`);
if (redisFailCount !== null && +redisFailCount >= MAX_FAILED_ATTEMPTS) {
throw new CustomHttpException(`Account locked. Please try again later.`, HttpStatus.FORBIDDEN);

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.

use system messages for this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

resolved in new commit

@Nuel-09
Nuel-09 requested a review from elijaharhinful May 10, 2026 08:19

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

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
`@src/database/migrations/1778406115466-add-unique-user-id-to-auth-metadata.ts`:
- Around line 4-8: The migration's up() adds a UNIQUE constraint
"UQ_auth_metadata_user_id" on "auth_metadata" but will fail if duplicate user_id
rows already exist; update up(queryRunner: QueryRunner) to first dedupe
"auth_metadata" (e.g., use a SQL CTE or queryRunner.query to delete duplicate
rows keeping a single row per user_id based on a deterministic criterion like
MIN(id) or MAX(created_at)), then add the ALTER TABLE ... ADD CONSTRAINT
"UQ_auth_metadata_user_id" UNIQUE ("user_id") statement; reference the up
function, queryRunner, table "auth_metadata" and constraint name
"UQ_auth_metadata_user_id" when making the change.

In `@src/modules/auth/auth.service.ts`:
- Around line 192-194: The auditLog function currently writes raw email (PII) to
logs; change it to avoid logging raw emails by: update auditLog (and its
callers) to prefer logging a userId when available (e.g., userId?: string) and
otherwise log a non-reversible salted hash or truncated/redacted form of the
email; compute the salted hash using a secure HMAC/sha256 with an application
secret/salt (do not store the raw email) and include only the userId OR
hashedEmail in the logger payload (keep reason, event, ip, timestamp); update
callers that pass email to instead pass userId when possible, and when only
email exists, pass the salted hash/redacted value to auditLog.
- Around line 215-236: The transaction catch currently swallows the original
error in the dataSource.manager.transaction call (saving savedSession) by
throwing a generic CustomHttpException; update the catch to capture the original
error (e.g., err), log it via the appropriate logger and include or attach the
original error details when re-throwing (either by passing err.message or err as
a cause) instead of losing it; ensure references are to the existing
savedSession/ dataSource.manager.transaction block and the thrown
CustomHttpException(SYS_MSG.ERROR_OCCURED, HttpStatus.INTERNAL_SERVER_ERROR) so
the log includes the original error and the re-thrown exception preserves useful
diagnostics.
- Around line 167-187: The UPDATE query in auth.service.ts (executed via
this.dataSource.query with parameters [MAX_FAILED_ATTEMPTS,
String(LOCKOUT_MINUTES), metaId]) must clear expired locks so they don't
persist; change the query logic to set locked_until = NULL when locked_until IS
NOT NULL AND locked_until < CURRENT_TIMESTAMP, then compute failed_attempts and
the new locked_until based on the adjusted failed_attempts (so the inner CASE
sees the reset count), ensuring re-locks can occur; update the SQL referenced
(the UPDATE on auth_metadata that uses CASE blocks) accordingly and add an
integration test that simulates 5 failures → expire lock → 1 failure → 4 more
failures → expect 403 to verify the account is re-locked.
🪄 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: 0d647648-c510-43d0-aabd-edad4a49c7a1

📥 Commits

Reviewing files that changed from the base of the PR and between c3ecc89 and 086c723.

📒 Files selected for processing (6)
  • src/database/migrations/1778406115466-add-unique-user-id-to-auth-metadata.ts
  • src/modules/auth/auth.controller.ts
  • src/modules/auth/auth.service.ts
  • src/modules/auth/docs/auth-swagger.doc.ts
  • src/modules/auth/entities/auth-metadata.entity.ts
  • src/shared/constants/SystemMessages.ts

Comment on lines +4 to +8
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "auth_metadata" ADD CONSTRAINT "UQ_auth_metadata_user_id" UNIQUE ("user_id")`
);
}

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.

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

Migration may fail on environments with pre-existing duplicate user_id rows.

The previous loadOrCreateMeta implementation (read-then-insert) is racy and could have produced multiple auth_metadata rows per user_id in any environment that already ran it. Adding a UNIQUE constraint will fail with a unique-violation if such duplicates exist, blocking deployment. Dedupe inside up() before adding the constraint.

🛡️ Suggested dedupe step
   public async up(queryRunner: QueryRunner): Promise<void> {
+    // Collapse any duplicate rows produced by the prior racy load-or-create path,
+    // keeping the most-recently-updated record per user_id.
+    await queryRunner.query(`
+      DELETE FROM "auth_metadata" a
+      USING "auth_metadata" b
+      WHERE a.user_id = b.user_id
+        AND a.created_at < b.created_at
+    `);
     await queryRunner.query(
       `ALTER TABLE "auth_metadata" ADD CONSTRAINT "UQ_auth_metadata_user_id" UNIQUE ("user_id")`
     );
   }
📝 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
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "auth_metadata" ADD CONSTRAINT "UQ_auth_metadata_user_id" UNIQUE ("user_id")`
);
}
public async up(queryRunner: QueryRunner): Promise<void> {
// Collapse any duplicate rows produced by the prior racy load-or-create path,
// keeping the most-recently-updated record per user_id.
await queryRunner.query(`
DELETE FROM "auth_metadata" a
USING "auth_metadata" b
WHERE a.user_id = b.user_id
AND a.created_at < b.created_at
`);
await queryRunner.query(
`ALTER TABLE "auth_metadata" ADD CONSTRAINT "UQ_auth_metadata_user_id" UNIQUE ("user_id")`
);
}
🤖 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 `@src/database/migrations/1778406115466-add-unique-user-id-to-auth-metadata.ts`
around lines 4 - 8, The migration's up() adds a UNIQUE constraint
"UQ_auth_metadata_user_id" on "auth_metadata" but will fail if duplicate user_id
rows already exist; update up(queryRunner: QueryRunner) to first dedupe
"auth_metadata" (e.g., use a SQL CTE or queryRunner.query to delete duplicate
rows keeping a single row per user_id based on a deterministic criterion like
MIN(id) or MAX(created_at)), then add the ALTER TABLE ... ADD CONSTRAINT
"UQ_auth_metadata_user_id" UNIQUE ("user_id") statement; reference the up
function, queryRunner, table "auth_metadata" and constraint name
"UQ_auth_metadata_user_id" when making the change.

Comment thread src/modules/auth/auth.service.ts Outdated
Comment on lines +167 to +187
await this.dataSource.query(
`UPDATE auth_metadata
SET failed_attempts = CASE
WHEN locked_until IS NOT NULL AND locked_until < CURRENT_TIMESTAMP
THEN 1
ELSE failed_attempts + 1
END,
locked_until = CASE
WHEN (
CASE
WHEN locked_until IS NOT NULL AND locked_until < CURRENT_TIMESTAMP
THEN 1
ELSE failed_attempts + 1
END
) >= $1
THEN CURRENT_TIMESTAMP + ($2 || ' minutes')::interval
ELSE locked_until
END
WHERE id = $3`,
[MAX_FAILED_ATTEMPTS, String(LOCKOUT_MINUTES), metaId]
);

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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Re-lock bypass: locked_until is never reset to NULL after expiry, so accounts can never be re-locked.

Trace with failed_attempts=5, locked_until=T-15min (expired):

  1. Bad password: outer CASE resets failed_attempts to 1. Inner CASE for locked_until evaluates the new attempt count (1); 1 >= 5 is false, so locked_until is kept at the old (expired) value.
  2. Bad password #2: condition locked_until IS NOT NULL AND locked_until < CURRENT_TIMESTAMP is still true → failed_attempts is reset to 1 again.
  3. The cycle repeats indefinitely — failed_attempts is pinned at 1 and the account is never re-locked, defeating the brute-force protection. Only a successful login (which sets locked_until = NULL in createSession) restores normal counting.

Reset locked_until to NULL in the same UPDATE whenever the previous lockout has expired.

🔒 Proposed fix
     await this.dataSource.query(
       `UPDATE auth_metadata
        SET failed_attempts = CASE
              WHEN locked_until IS NOT NULL AND locked_until < CURRENT_TIMESTAMP
              THEN 1
              ELSE failed_attempts + 1
            END,
            locked_until = CASE
+             WHEN locked_until IS NOT NULL AND locked_until < CURRENT_TIMESTAMP
+               AND (failed_attempts + 1) < $1
+             THEN NULL
              WHEN (
                CASE
                  WHEN locked_until IS NOT NULL AND locked_until < CURRENT_TIMESTAMP
                  THEN 1
                  ELSE failed_attempts + 1
                END
              ) >= $1
              THEN CURRENT_TIMESTAMP + ($2 || ' minutes')::interval
              ELSE locked_until
            END
        WHERE id = $3`,
       [MAX_FAILED_ATTEMPTS, String(LOCKOUT_MINUTES), metaId]
     );

Please add an integration test that walks 5 failures → wait/expire → 1 failure → 4 more failures → expect 403 again.

🤖 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 `@src/modules/auth/auth.service.ts` around lines 167 - 187, The UPDATE query in
auth.service.ts (executed via this.dataSource.query with parameters
[MAX_FAILED_ATTEMPTS, String(LOCKOUT_MINUTES), metaId]) must clear expired locks
so they don't persist; change the query logic to set locked_until = NULL when
locked_until IS NOT NULL AND locked_until < CURRENT_TIMESTAMP, then compute
failed_attempts and the new locked_until based on the adjusted failed_attempts
(so the inner CASE sees the reset count), ensuring re-locks can occur; update
the SQL referenced (the UPDATE on auth_metadata that uses CASE blocks)
accordingly and add an integration test that simulates 5 failures → expire lock
→ 1 failure → 4 more failures → expect 403 to verify the account is re-locked.

Comment thread src/modules/auth/auth.service.ts Outdated
Comment thread src/modules/auth/auth.service.ts Outdated
@TheCodeGhinux
TheCodeGhinux merged commit 7d2a1a2 into dev May 10, 2026
9 checks passed
@sage-ali sage-ali changed the title feat(auth): Implement Login Flow & Rate Limiting(BE-007) feat(auth): Implement Login Flow(BE-007) May 10, 2026
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.

6 participants