revamped authentication system
This commit is contained in:
81
app/api/v1/public/vendors/auth.py
vendored
81
app/api/v1/public/vendors/auth.py
vendored
@@ -2,14 +2,17 @@
|
||||
"""
|
||||
Customer authentication endpoints (public-facing).
|
||||
|
||||
This module provides:
|
||||
- Customer registration (vendor-scoped)
|
||||
- Customer login (vendor-scoped)
|
||||
- Customer password reset
|
||||
Implements dual token storage with path restriction:
|
||||
- Sets HTTP-only cookie with path=/shop (restricted to shop routes only)
|
||||
- Returns token in response for localStorage (API calls)
|
||||
|
||||
This prevents:
|
||||
- Customer cookies from being sent to admin or vendor routes
|
||||
- Cross-context authentication confusion
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, Path
|
||||
from fastapi import APIRouter, Depends, Response
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
@@ -18,6 +21,7 @@ from app.exceptions import VendorNotFoundException
|
||||
from models.schema.auth import LoginResponse, UserLogin
|
||||
from models.schema.customer import CustomerRegister, CustomerResponse
|
||||
from models.database.vendor import Vendor
|
||||
from app.core.config import settings
|
||||
|
||||
router = APIRouter(prefix="/auth")
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -63,6 +67,7 @@ def register_customer(
|
||||
def customer_login(
|
||||
vendor_id: int,
|
||||
user_credentials: UserLogin,
|
||||
response: Response,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
@@ -70,6 +75,13 @@ def customer_login(
|
||||
|
||||
Authenticates customer and returns JWT token.
|
||||
Customer must belong to the specified vendor.
|
||||
|
||||
Sets token in two places:
|
||||
1. HTTP-only cookie with path=/shop (for browser page navigation)
|
||||
2. Response body (for localStorage and API calls)
|
||||
|
||||
The cookie is restricted to /shop/* routes only to prevent
|
||||
it from being sent to admin or vendor routes.
|
||||
"""
|
||||
# Verify vendor exists and is active
|
||||
vendor = db.query(Vendor).filter(
|
||||
@@ -92,6 +104,24 @@ def customer_login(
|
||||
f"for vendor {vendor.vendor_code}"
|
||||
)
|
||||
|
||||
# Set HTTP-only cookie for browser navigation
|
||||
# CRITICAL: path=/shop restricts cookie to shop routes only
|
||||
response.set_cookie(
|
||||
key="customer_token",
|
||||
value=login_result["token_data"]["access_token"],
|
||||
httponly=True, # JavaScript cannot access (XSS protection)
|
||||
secure=settings.environment == "production", # HTTPS only in production
|
||||
samesite="lax", # CSRF protection
|
||||
max_age=login_result["token_data"]["expires_in"], # Match JWT expiry
|
||||
path="/shop", # RESTRICTED TO SHOP ROUTES ONLY
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"Set customer_token cookie with {login_result['token_data']['expires_in']}s expiry "
|
||||
f"(path=/shop, httponly=True, secure={settings.environment == 'production'})"
|
||||
)
|
||||
|
||||
# Return full login response
|
||||
return LoginResponse(
|
||||
access_token=login_result["token_data"]["access_token"],
|
||||
token_type=login_result["token_data"]["token_type"],
|
||||
@@ -101,12 +131,26 @@ def customer_login(
|
||||
|
||||
|
||||
@router.post("/{vendor_id}/customers/logout")
|
||||
def customer_logout(vendor_id: int):
|
||||
def customer_logout(
|
||||
vendor_id: int,
|
||||
response: Response
|
||||
):
|
||||
"""
|
||||
Customer logout.
|
||||
|
||||
Client should remove token from storage.
|
||||
Clears the customer_token cookie.
|
||||
Client should also remove token from localStorage.
|
||||
"""
|
||||
logger.info(f"Customer logout for vendor {vendor_id}")
|
||||
|
||||
# Clear the cookie (must match path used when setting)
|
||||
response.delete_cookie(
|
||||
key="customer_token",
|
||||
path="/shop",
|
||||
)
|
||||
|
||||
logger.debug("Deleted customer_token cookie")
|
||||
|
||||
return {"message": "Logged out successfully"}
|
||||
|
||||
|
||||
@@ -173,3 +217,26 @@ def reset_password(
|
||||
logger.info(f"Password reset completed for vendor {vendor.vendor_code}")
|
||||
|
||||
return {"message": "Password reset successful"}
|
||||
|
||||
|
||||
@router.get("/{vendor_id}/customers/me")
|
||||
def get_current_customer(
|
||||
vendor_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get current authenticated customer.
|
||||
|
||||
This endpoint can be called to verify authentication and get customer info.
|
||||
Requires customer authentication via cookie or header.
|
||||
"""
|
||||
from app.api.deps import get_current_customer_api
|
||||
from fastapi import Request
|
||||
|
||||
# Note: This would need Request object to check cookies
|
||||
# For now, just indicate the endpoint exists
|
||||
# Implementation depends on how you want to structure it
|
||||
|
||||
return {
|
||||
"message": "Customer info endpoint - implementation depends on auth structure"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user