Files
orion/app/modules/tenancy/routes/api/merchant_auth.py
Samir Boulahtit f20266167d
Some checks failed
CI / ruff (push) Failing after 7s
CI / pytest (push) Failing after 1s
CI / architecture (push) Failing after 9s
CI / dependency-scanning (push) Successful in 27s
CI / audit (push) Successful in 8s
CI / docs (push) Has been skipped
fix(lint): auto-fix ruff violations and tune lint rules
- Auto-fixed 4,496 lint issues (import sorting, modern syntax, etc.)
- Added ignore rules for patterns intentional in this codebase:
  E402 (late imports), E712 (SQLAlchemy filters), B904 (raise from),
  SIM108/SIM105/SIM117 (readability preferences)
- Added per-file ignores for tests and scripts
- Excluded broken scripts/rename_terminology.py (has curly quotes)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:10:42 +01:00

118 lines
3.9 KiB
Python

# app/modules/tenancy/routes/api/merchant_auth.py
"""
Merchant authentication endpoints.
Implements dual token storage with path restriction:
- Sets HTTP-only cookie with path=/merchants (restricted to merchant routes only)
- Returns token in response for localStorage (API calls)
This prevents merchant cookies from being sent to admin or store routes.
"""
import logging
from fastapi import APIRouter, Depends, Response
from sqlalchemy.orm import Session
from app.api.deps import get_current_merchant_from_cookie_or_header
from app.core.database import get_db
from app.core.environment import should_use_secure_cookies
from app.modules.core.services.auth_service import auth_service
from models.schema.auth import (
LoginResponse,
LogoutResponse,
UserContext,
UserLogin,
UserResponse,
)
merchant_auth_router = APIRouter(prefix="/auth")
logger = logging.getLogger(__name__)
@merchant_auth_router.post("/login", response_model=LoginResponse)
def merchant_login(
user_credentials: UserLogin, response: Response, db: Session = Depends(get_db)
):
"""
Merchant login endpoint.
Only allows users who own at least one active merchant to login.
Returns JWT token for authenticated merchant users.
Sets token in two places:
1. HTTP-only cookie with path=/merchants (for browser page navigation)
2. Response body (for localStorage and API calls)
The cookie is restricted to /merchants/* routes only to prevent
it from being sent to admin or store routes.
"""
# Authenticate user and verify merchant ownership
login_result = auth_service.login_merchant(db=db, user_credentials=user_credentials)
logger.info(f"Merchant login successful: {login_result['user'].username}")
# Set HTTP-only cookie for browser navigation
# CRITICAL: path=/merchants restricts cookie to merchant routes only
response.set_cookie(
key="merchant_token",
value=login_result["token_data"]["access_token"],
httponly=True, # JavaScript cannot access (XSS protection)
secure=should_use_secure_cookies(), # HTTPS only in production/staging
samesite="lax", # CSRF protection
max_age=login_result["token_data"]["expires_in"], # Match JWT expiry
path="/merchants", # RESTRICTED TO MERCHANT ROUTES ONLY
)
logger.debug(
f"Set merchant_token cookie with {login_result['token_data']['expires_in']}s expiry "
f"(path=/merchants, httponly=True, secure={should_use_secure_cookies()})"
)
# Also return token in response for localStorage (API calls)
return LoginResponse(
access_token=login_result["token_data"]["access_token"],
token_type=login_result["token_data"]["token_type"],
expires_in=login_result["token_data"]["expires_in"],
user=login_result["user"],
)
@merchant_auth_router.get("/me", response_model=UserResponse)
def get_current_merchant(
current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header),
):
"""
Get current authenticated merchant user.
This endpoint validates the token and ensures the user owns merchants.
Returns the current user's information.
Token can come from:
- Authorization header (API calls)
- merchant_token cookie (browser navigation, path=/merchants only)
"""
logger.info(f"Merchant user info requested: {current_user.username}")
return current_user
@merchant_auth_router.post("/logout", response_model=LogoutResponse)
def merchant_logout(response: Response):
"""
Merchant logout endpoint.
Clears the merchant_token cookie.
Client should also remove token from localStorage.
"""
logger.info("Merchant logout")
# Clear the cookie (must match path used when setting)
response.delete_cookie(
key="merchant_token",
path="/merchants",
)
logger.debug("Deleted merchant_token cookie (path=/merchants)")
return LogoutResponse(message="Logged out successfully")