Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ POSTGRES_SERVER=localhost
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=fluentmeet
DATABASE_URL="postgresql+asyncpg://postgres:postgres@localhost:5432/fluentmeet"
DATABASE_URL=postgresql+psycopg2://postgres:postgres@localhost:5432/fluentmeet

# Redis
REDIS_HOST=localhost
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ Ensure your models are imported in `app/models/init.py` for Alembic to detect th
```bash
python -m alembic revision --autogenerate -m "Add Meeting model"
```
### **Migrate Head**
```bash
alembic upgrade head
```

### **Applying Migrations**
```bash
Expand Down
73 changes: 73 additions & 0 deletions alembic/versions/7f066a8213a8_change_user_id_to_uuid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Change User.id to UUID

Revision ID: 7f066a8213a8
Revises: e1a664780dc6
Create Date: 2026-03-27 22:35:06.005762

"""

from typing import Sequence, Union

from alembic import op
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.

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.

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.
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
coderabbitai[bot] marked this conversation as resolved.


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
# Setup extension for UUID generation if needed
op.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"')

# We truncate because casting int -> uuid with a random function would break existing foreign key relations.
op.execute("TRUNCATE TABLE users CASCADE")

# Drop foreign key constraint on verification_tokens
op.drop_constraint(
"verification_tokens_user_id_fkey", "verification_tokens", type_="foreignkey"
)

# Alter the columns
# Alter the columns. First drop the integer default from the id column to allow cast.
op.execute("ALTER TABLE users ALTER COLUMN id DROP DEFAULT")
op.execute(
"ALTER TABLE users ALTER COLUMN id SET DATA TYPE UUID USING (uuid_generate_v4())"
)
op.execute(
"ALTER TABLE verification_tokens ALTER COLUMN user_id SET DATA TYPE UUID USING (uuid_generate_v4())"
)

# Re-add foreign key constraint
op.create_foreign_key(
"verification_tokens_user_id_fkey",
"verification_tokens",
"users",
["user_id"],
["id"],
)
# ### end Alembic commands ###


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 ###
Comment on lines +56 to +73
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.

32 changes: 32 additions & 0 deletions alembic/versions/e1a664780dc6_add_meeting_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Add Meeting model

Revision ID: e1a664780dc6
Revises: 19dc9714d9ea
Create Date: 2026-03-27 22:18:35.070453

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
Comment thread Fixed

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.

# revision identifiers, used by Alembic.
revision: str = "e1a664780dc6"
down_revision: Union[str, Sequence[str], None] = "19dc9714d9ea"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
Comment on lines +21 to +31
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

Empty migration with misleading title.

The migration is titled "Add Meeting model" but contains no operations. If this is a placeholder for future work, consider:

  1. Renaming the revision message to reflect its actual purpose (e.g., "Checkpoint before UUID migration")
  2. Removing the unused import sqlalchemy as sa flagged by CodeQL
  3. Adding a comment explaining why the migration is intentionally empty

If the Meeting model operations were intended but omitted, please add them.

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

In `@alembic/versions/e1a664780dc6_add_meeting_model.py` around lines 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.

# ### end Alembic commands ###
3 changes: 3 additions & 0 deletions alembic_output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,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
Comment on lines +1 to +3
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.

170 changes: 170 additions & 0 deletions app/api/v1/endpoints/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,46 @@
from uuid import uuid4

from fastapi import APIRouter, Depends, Query, Request, status
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session

from app.core.config import settings
from app.core.exceptions import ForbiddenException, UnauthorizedException
from app.core.rate_limiter import limiter
from app.core.sanitize import sanitize_log_args
from app.core.security import SecurityService, get_security_service
from app.crud.user.user import create_user, get_user_by_email
from app.db.session import get_db
from app.schemas.auth import (
ActionAcknowledgement,
ForgotPasswordRequest,
LoginRequest,
LoginResponse,
ResendVerificationRequest,
SignupResponse,
VerifyEmailResponse,
)
from app.schemas.user import UserCreate
from app.services.account_lockout import (
AccountLockoutService,
get_account_lockout_service,
)
from app.services.auth_verification import (
AuthVerificationService,
get_auth_verification_service,
)
from app.services.email_producer import EmailProducerService, get_email_producer_service
from app.services.token_store import TokenStoreService, get_token_store_service

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/auth", tags=["auth"])
DB_SESSION_DEPENDENCY = Depends(get_db)
EMAIL_PRODUCER_DEPENDENCY = Depends(get_email_producer_service)
AUTH_VERIFICATION_SERVICE_DEPENDENCY = Depends(get_auth_verification_service)
SECURITY_SERVICE_DEPENDENCY = Depends(get_security_service)
TOKEN_STORE_SERVICE_DEPENDENCY = Depends(get_token_store_service)
ACCOUNT_LOCKOUT_SERVICE_DEPENDENCY = Depends(get_account_lockout_service)


@router.post(
Expand Down Expand Up @@ -216,3 +229,160 @@ async def resend_verification(
"If an account with that email exists, we have sent a verification email."
)
)


@router.post(
"/login",
response_model=LoginResponse,
status_code=status.HTTP_200_OK,
summary="Authenticate a registered user",
description=(
"Validates email and password, issues a JWT access token (returned "
"in the body) and a JWT refresh token (set as an HttpOnly cookie). "
"Rate-limited to 10 requests/minute per IP. The account is locked "
"after 5 consecutive failed attempts for 5 days."
),
responses={
401: {
"description": "Invalid credentials",
"content": {
"application/json": {
"example": {
"status": "error",
"code": "INVALID_CREDENTIALS",
"message": "Invalid email or password.",
"details": [],
}
}
},
},
403: {
"description": "Account not verified, deleted, or locked",
"content": {
"application/json": {
"examples": {
"not_verified": {
"value": {
"status": "error",
"code": "EMAIL_NOT_VERIFIED",
"message": (
"Please verify your email before logging in."
),
"details": [],
}
},
"deleted": {
"value": {
"status": "error",
"code": "ACCOUNT_DELETED",
"message": "This account has been deleted.",
"details": [],
}
},
"locked": {
"value": {
"status": "error",
"code": "ACCOUNT_LOCKED",
"message": (
"Account is temporarily locked due to "
"too many failed login attempts. "
"Please try again later."
),
"details": [],
}
},
}
}
},
},
},
)
@limiter.limit("10/minute")
async def login(
request: Request,
payload: LoginRequest,
db: Session = DB_SESSION_DEPENDENCY,
security_svc: SecurityService = SECURITY_SERVICE_DEPENDENCY,
token_store: TokenStoreService = TOKEN_STORE_SERVICE_DEPENDENCY,
lockout_svc: AccountLockoutService = ACCOUNT_LOCKOUT_SERVICE_DEPENDENCY,
) -> JSONResponse:
del request # consumed by slowapi
email = payload.email.lower()

# Check lockout
if await lockout_svc.is_locked(email):
raise ForbiddenException(
code="ACCOUNT_LOCKED",
message=(
"Account is temporarily locked due to too many failed "
"login attempts. Please try again later."
),
)

# Lookup user
user = get_user_by_email(db, email)
if user is None:
# Record a failed attempt even for non-existent emails so that
# timing is indistinguishable from a wrong-password attempt.
await lockout_svc.record_failed_attempt(email)
raise UnauthorizedException(
code="INVALID_CREDENTIALS",
message="Invalid email or password.",
)

# Verify password
if not security_svc.verify_password(payload.password, user.hashed_password):
await lockout_svc.record_failed_attempt(email)
raise UnauthorizedException(
code="INVALID_CREDENTIALS",
message="Invalid email or password.",
)

# Guard: email verified?
if not user.is_verified:
raise ForbiddenException(
code="EMAIL_NOT_VERIFIED",
message="Please verify your email before logging in.",
)

# Guard: soft-deleted?
if user.deleted_at is not None:
raise ForbiddenException(
code="ACCOUNT_DELETED",
message="This account has been deleted.",
)

# Reset failed-login counter on success
await lockout_svc.reset_attempts(email)

# Issue tokens
access_token, expires_in = security_svc.create_access_token(email=email)
refresh_token, refresh_jti, refresh_ttl = security_svc.create_refresh_token(
email=email,
)

# Persist refresh JTI in Redis
await token_store.save_refresh_token(jti=refresh_jti, ttl_seconds=refresh_ttl)

# Build JSON body
body = LoginResponse(
access_token=access_token,
user_id=user.id,
token_type="bearer",
expires_in=expires_in,
)

response = JSONResponse(content=body.model_dump(mode="json"), status_code=200)

# Set HttpOnly refresh-token cookie
response.set_cookie(
key="refresh_token",
value=refresh_token,
httponly=True,
secure=True,
samesite="strict",
path=f"{settings.API_V1_STR}/auth",
max_age=refresh_ttl,
)

return response
7 changes: 6 additions & 1 deletion app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@ class Settings(BaseSettings):

# Security
SECRET_KEY: str = "placeholder_secret_key"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 15
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
VERIFICATION_TOKEN_EXPIRE_HOURS: int = 24

# Account Lockout
MAX_FAILED_LOGIN_ATTEMPTS: int = 5
ACCOUNT_LOCKOUT_DAYS: int = 5
Comment on lines +24 to +30
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILES="$(fd -i 'config.py$' app) $(fd -i 'security.py$' app) $(fd -i 'account_lockout.py$' app)"
for f in $FILES; do
  echo "=== $f ==="
  rg -n -C2 'ACCESS_TOKEN_EXPIRE_MINUTES|MAX_FAILED_LOGIN_ATTEMPTS|ACCOUNT_LOCKOUT_DAYS|REFRESH_TOKEN_EXPIRE_DAYS' "$f"
done

Repository: Brints/FluentMeet

Length of output: 1567


🏁 Script executed:

# Read the full config.py file to understand BaseSettings setup and inheritance
wc -l app/core/config.py && head -50 app/core/config.py

Repository: Brints/FluentMeet

Length of output: 1500


🏁 Script executed:

# Read the account_lockout.py file to see how these config values are used
wc -l app/services/account_lockout.py && cat app/services/account_lockout.py

Repository: Brints/FluentMeet

Length of output: 3574


🏁 Script executed:

# Read the relevant parts of security.py to understand token creation logic
wc -l app/core/security.py && sed -n '60,120p' app/core/security.py

Repository: Brints/FluentMeet

Length of output: 1765


🏁 Script executed:

# Check if there's any validation or clamping of these values anywhere in the codebase
rg -n 'max\(.*MAX_FAILED_LOGIN_ATTEMPTS\)|min\(.*ACCOUNT_LOCKOUT_DAYS\)|clamp|assert.*EXPIRE' app/

Repository: Brints/FluentMeet

Length of output: 43


Add bounds validation for security-critical config values.

Lines 24, 25, 29, and 30 accept any integer; 0 or negative env overrides can invalidate token/lockout behavior. ACCESS_TOKEN_EXPIRE_MINUTES ≤ 0 would create immediately-expired tokens. MAX_FAILED_LOGIN_ATTEMPTS ≤ 0 would lock accounts on every login attempt. ACCOUNT_LOCKOUT_DAYS ≤ 0 would fail Redis TTL validation. No downstream clamping or validation exists.

Suggested fix
+from pydantic import Field
 from pydantic_settings import BaseSettings, SettingsConfigDict
@@
-    ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
+    ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=60, gt=0)
-    REFRESH_TOKEN_EXPIRE_DAYS: int = 7
+    REFRESH_TOKEN_EXPIRE_DAYS: int = Field(default=7, gt=0)
-    VERIFICATION_TOKEN_EXPIRE_HOURS: int = 24
+    VERIFICATION_TOKEN_EXPIRE_HOURS: int = Field(default=24, gt=0)
@@
-    MAX_FAILED_LOGIN_ATTEMPTS: int = 5
+    MAX_FAILED_LOGIN_ATTEMPTS: int = Field(default=5, gt=0)
-    ACCOUNT_LOCKOUT_DAYS: int = 5
+    ACCOUNT_LOCKOUT_DAYS: int = Field(default=5, gt=0)
📝 Committable suggestion

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

Suggested change
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
VERIFICATION_TOKEN_EXPIRE_HOURS: int = 24
# Account Lockout
MAX_FAILED_LOGIN_ATTEMPTS: int = 5
ACCOUNT_LOCKOUT_DAYS: int = 5
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=60, gt=0)
REFRESH_TOKEN_EXPIRE_DAYS: int = Field(default=7, gt=0)
VERIFICATION_TOKEN_EXPIRE_HOURS: int = Field(default=24, gt=0)
# Account Lockout
MAX_FAILED_LOGIN_ATTEMPTS: int = Field(default=5, gt=0)
ACCOUNT_LOCKOUT_DAYS: int = Field(default=5, gt=0)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/core/config.py` around lines 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.


# Database
POSTGRES_SERVER: str = "localhost"
POSTGRES_USER: str = "postgres"
Expand Down
Loading
Loading