Skip to content

fix(api): resolve registration 404 + bcrypt/passlib incompatibility#65

Open
RegardV wants to merge 2 commits intomainfrom
production-inkypyrus
Open

fix(api): resolve registration 404 + bcrypt/passlib incompatibility#65
RegardV wants to merge 2 commits intomainfrom
production-inkypyrus

Conversation

@RegardV
Copy link
Owner

@RegardV RegardV commented Feb 22, 2026

User description

Summary

  • Fixed 404 on all API endpoints — 5 route files (auth, users, projects, themes, exports) had internal prefixes that doubled the prefix set in app/main.py, resulting in paths like /api/auth/auth/register
  • Fixed missing DB tables — DB was only stamped at Alembic head, not migrated; ran alembic upgrade head to actually create all tables
  • Fixed bcrypt/passlib incompatibility — passlib 1.7.4 is broken with bcrypt 4.x/5.x (chromadb requires bcrypt>=4.0.1); replaced passlib CryptContext with direct bcrypt calls in security.py and auth_service.py
  • Fixed secrets.urlsafe_b64encodebase64.urlsafe_b64encode in email_service.py
  • Fixed SecurityEvent(metadata=)event_metadata= (column was renamed to avoid SQLAlchemy reserved name)
  • Added FRONTEND_URL to Settings (was missing, used in email verification link)

Test plan

  • POST /api/auth/register returns 201 with user_id and verification_required: true
  • All 3 containers healthy after rebuild
  • Alembic at head, all tables exist in DB

🤖 Generated with Claude Code


PR Type

Bug fix, Enhancement


Description

  • Removed duplicate router prefixes from 5 route files (auth, users, projects, themes, exports) that caused all API endpoints to return 404

  • Replaced passlib CryptContext with direct bcrypt calls to resolve incompatibility with bcrypt 4.x/5.x required by chromadb

  • Fixed base64 encoding in email_service.py (secrets.urlsafe_b64encode → base64.urlsafe_b64encode)

  • Renamed reserved SQLAlchemy column metadata → event_metadata in SecurityEvent model

  • Added missing FRONTEND_URL configuration field to Settings

  • Updated session state documentation with deployment status and outstanding issues


Diagram Walkthrough

flowchart LR
  A["5 Route Files<br/>auth, users, projects,<br/>themes, exports"] -->|"Remove duplicate<br/>prefix=/path"| B["Correct API Paths<br/>/api/auth/register"]
  C["passlib CryptContext<br/>+ bcrypt 4.x/5.x"] -->|"Replace with direct<br/>bcrypt calls"| D["Compatible Password<br/>Hashing"]
  E["secrets.urlsafe_b64encode<br/>+ base64 import"] -->|"Fix to base64<br/>module"| F["Correct Email Token<br/>Generation"]
  G["SecurityEvent<br/>metadata column"] -->|"Rename to<br/>event_metadata"| H["Valid SQLAlchemy<br/>Model"]
  I["Missing FRONTEND_URL<br/>in Settings"] -->|"Add config field"| J["Email Verification<br/>Links Work"]
Loading

File Walkthrough

Relevant files
Bug fix
8 files
auth.py
Remove duplicate /auth prefix from router                               
+1/-1     
users.py
Remove duplicate /users prefix from router                             
+1/-1     
projects.py
Remove duplicate /projects prefix from router                       
+1/-1     
themes.py
Remove duplicate /themes prefix from router                           
+1/-1     
exports.py
Remove duplicate /exports prefix from router                         
+1/-1     
security.py
Replace passlib with direct bcrypt implementation               
+11/-9   
auth_service.py
Replace passlib with direct bcrypt in registration and authentication
+7/-19   
email_service.py
Fix base64 encoding and SecurityEvent metadata column name
+5/-4     
Enhancement
1 files
config.py
Add missing FRONTEND_URL configuration setting                     
+1/-0     
Documentation
2 files
session-state.json
Update session state with deployment and system status     
+94/-14 
SESSION_LOG_2026_02_22.md
Add comprehensive session log and outstanding issues         
+112/-0 

RegardV and others added 2 commits February 22, 2026 14:17
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove duplicate router prefixes from 5 route files (auth, users,
  projects, themes, exports) — each had an internal prefix that doubled
  the prefix already set in app/main.py, causing all endpoints to 404
- Run alembic upgrade head to actually create DB tables (DB was only
  stamped, not migrated)
- Replace passlib CryptContext with direct bcrypt calls in security.py
  and auth_service.py — passlib 1.7.4 is incompatible with bcrypt 4.x/5.x
  (chromadb requires bcrypt>=4.0.1, so passlib must be bypassed)
- Fix secrets.urlsafe_b64encode → base64.urlsafe_b64encode in email_service.py
- Fix SecurityEvent(metadata=) → event_metadata= (column was renamed)
- Add FRONTEND_URL field to Settings (missing, used by email verification URL)

Registration now returns 201 with user_id and verification_required=true.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@qodo-code-review
Copy link

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: review

Failed stage: Set up job [❌]

Failed test name: ""

Failure summary:

The workflow failed during the "Prepare all required actions" step because GitHub Actions could not
download the referenced action qodo-ai/pr-agent@v0.29.0.
- Error: Unable to resolve action
'qodo-ai/pr-agent@v0.29.0', unable to find version 'v0.29.0'
- This indicates the tag/branch/commit
v0.29.0 does not exist (or is not accessible) in the qodo-ai/pr-agent repository, so the runner
cannot resolve the action version.

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

13:  ##[group]Runner Image
14:  Image: ubuntu-24.04
15:  Version: 20260201.15.1
16:  Included Software: https://github.com/actions/runner-images/blob/ubuntu24/20260201.15/images/ubuntu/Ubuntu2404-Readme.md
17:  Image Release: https://github.com/actions/runner-images/releases/tag/ubuntu24%2F20260201.15
18:  ##[endgroup]
19:  ##[group]GITHUB_TOKEN Permissions
20:  Contents: read
21:  Metadata: read
22:  Packages: read
23:  ##[endgroup]
24:  Secret source: Actions
25:  Prepare workflow directory
26:  Prepare all required actions
27:  Getting action download info
28:  ##[error]Unable to resolve action `qodo-ai/pr-agent@v0.29.0`, unable to find version `v0.29.0`

@qodo-code-review
Copy link

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Password truncation

Description: The new bcrypt handling truncates passwords to 72 bytes (password_bytes =
password_bytes[:72]), which can cause different long passwords sharing the first 72 bytes
to authenticate as the same password and may weaken effective password strength if users
choose long passphrases.
security.py [123-140]

Referred Code
        password_bytes = password.encode('utf-8')
        if len(password_bytes) > 72:
            password_bytes = password_bytes[:72]
        salt = _bcrypt.gensalt(rounds=12)
        return _bcrypt.hashpw(password_bytes, salt).decode('utf-8')
    except Exception as e:
        logger.error(f"Password hashing failed: {e}")
        raise ValueError("Password hashing failed")

@staticmethod
def verify_password(plain_password: str, hashed_password: str) -> bool:
    """Verify password against hash"""
    try:
        password_bytes = plain_password.encode('utf-8')
        if len(password_bytes) > 72:
            password_bytes = password_bytes[:72]
        stored = hashed_password.encode('utf-8') if isinstance(hashed_password, str) else hashed_password
        return _bcrypt.checkpw(password_bytes, stored)
Token data in logs

Description: A prefix of the email verification token is stored in the SecurityEvent via event_metadata
(e.g., "token": "{token[:8]}..."), which increases sensitive-token exposure in logs/DB and
could aid an attacker if those records are accessed.
email_service.py [58-65]

Referred Code
security_event = SecurityEvent(
    user_id=user.id,
    event_type="verification_email_sent",
    description=f"Email verification sent to {user.email}",
    ip_address=request_ip,
    user_agent=request_client,
    event_metadata=f'{{"token": "{token[:8]}..."}}' # Only log partial token
)
Sensitive info exposure

Description: The committed session state contains operational environment details (internal IPs/URLs,
deployment target, container/service status, and commands) that can assist attackers with
reconnaissance if the repository is exposed beyond trusted staff.
session-state.json [1-95]

Referred Code
{
  "version": "2026-02-22",
  "last_session": "2026-02-22",
  "branch": "production-inkypyrus",
  "deployment_target": "journal.inkypyrus.com (local Docker, LAN access)",

  "system_status": {
    "postgres": "healthy — port 5432 internal",
    "backend": "healthy — port 8000 internal, uvicorn app.main:app",
    "frontend": "healthy — port 8080 exposed (0.0.0.0:8080)",
    "cloudflared": "not running — CLOUDFLARE_TUNNEL_TOKEN set to skip in .env.inkypyrus"
  },

  "access": {
    "local": "http://192.168.1.99:8080",
    "api_docs": "http://192.168.1.99:8080/api/docs",
    "health": "http://192.168.1.99:8080/health"
  },

  "start_command": "docker compose -f docker-compose.inkypyrus.yml --env-file .env.inkypyrus up -d",
  "rebuild_command": "docker compose -f docker-compose.inkypyrus.yml --env-file .env.inkypyrus up --build -d",


 ... (clipped 74 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Silent password truncation: The new bcrypt implementation silently truncates passwords longer than 72 bytes rather
than explicitly rejecting/handling this edge case, which can cause unexpected
authentication behavior.

Referred Code
        password_bytes = password.encode('utf-8')
        if len(password_bytes) > 72:
            password_bytes = password_bytes[:72]
        salt = _bcrypt.gensalt(rounds=12)
        return _bcrypt.hashpw(password_bytes, salt).decode('utf-8')
    except Exception as e:
        logger.error(f"Password hashing failed: {e}")
        raise ValueError("Password hashing failed")

@staticmethod
def verify_password(plain_password: str, hashed_password: str) -> bool:
    """Verify password against hash"""
    try:
        password_bytes = plain_password.encode('utf-8')
        if len(password_bytes) > 72:
            password_bytes = password_bytes[:72]
        stored = hashed_password.encode('utf-8') if isinstance(hashed_password, str) else hashed_password
        return _bcrypt.checkpw(password_bytes, stored)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Weak password validation: Passwords longer than 72 bytes are truncated before hashing/verification without rejecting
the input, allowing distinct user passwords to collapse to the same effective secret.

Referred Code
password_bytes = password.encode('utf-8')
if len(password_bytes) > 72:
    password_bytes = password_bytes[:72]
hashed_password = _bcrypt.hashpw(password_bytes, _bcrypt.gensalt(rounds=12)).decode('utf-8')

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Security
Pre-hash passwords to prevent truncation

To prevent a security risk from password truncation, pre-hash passwords using
SHA-256 before passing them to bcrypt for both hashing and verification.

journal-platform-backend/app/core/security.py [119-143]

 @staticmethod
 def hash_password(password: str) -> str:
-    """Hash password using bcrypt"""
+    """Hash password using bcrypt after pre-hashing with SHA-256"""
     try:
-        password_bytes = password.encode('utf-8')
-        if len(password_bytes) > 72:
-            password_bytes = password_bytes[:72]
+        # Pre-hash the password with SHA-256 to handle bcrypt's 72-byte limit
+        password_hash = hashlib.sha256(password.encode('utf-8')).hexdigest()
+        password_bytes = password_hash.encode('utf-8')
+        
         salt = _bcrypt.gensalt(rounds=12)
         return _bcrypt.hashpw(password_bytes, salt).decode('utf-8')
     except Exception as e:
         logger.error(f"Password hashing failed: {e}")
         raise ValueError("Password hashing failed")
 
 @staticmethod
 def verify_password(plain_password: str, hashed_password: str) -> bool:
     """Verify password against hash"""
     try:
-        password_bytes = plain_password.encode('utf-8')
-        if len(password_bytes) > 72:
-            password_bytes = password_bytes[:72]
+        # Pre-hash the plain password with SHA-256 before verification
+        password_hash = hashlib.sha256(plain_password.encode('utf-8')).hexdigest()
+        password_bytes = password_hash.encode('utf-8')
+
         stored = hashed_password.encode('utf-8') if isinstance(hashed_password, str) else hashed_password
         return _bcrypt.checkpw(password_bytes, stored)
     except Exception as e:
         logger.error(f"Password verification failed: {e}")
         return False

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies and fixes a significant security vulnerability (password truncation) introduced in the PR by proposing a standard and robust solution (pre-hashing).

High
Possible issue
Use centralized password hashing utility

Replace the duplicated password hashing logic in auth_service.py with a call to
the centralized password_manager.hash_password method to improve
maintainability.

journal-platform-backend/app/services/auth_service.py [80-85]

-# Hash password using bcrypt directly
-import bcrypt as _bcrypt
-password_bytes = password.encode('utf-8')
-if len(password_bytes) > 72:
-    password_bytes = password_bytes[:72]
-hashed_password = _bcrypt.hashpw(password_bytes, _bcrypt.gensalt(rounds=12)).decode('utf-8')
+# Hash password using the centralized password manager
+hashed_password = password_manager.hash_password(password)
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies duplicated code and proposes using a centralized utility, which improves code maintainability and consistency.

Low
Use centralized password verification utility

Replace the duplicated password verification logic in auth_service.py with a
call to the centralized password_manager.verify_password method to improve
maintainability.

journal-platform-backend/app/services/auth_service.py [166-173]

-# Verify password using bcrypt directly
-import bcrypt as _bcrypt
-password_bytes = password.encode('utf-8')
-if len(password_bytes) > 72:
-    password_bytes = password_bytes[:72]
-stored_hash = user.password_hash.encode('utf-8') if isinstance(user.password_hash, str) else user.password_hash
+# Verify password using the centralized password manager
+if not password_manager.verify_password(password, user.password_hash):
 
-if not _bcrypt.checkpw(password_bytes, stored_hash):
-
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies duplicated code and proposes using a centralized utility, which improves code maintainability and consistency.

Low
  • More

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.

1 participant