revamped authentication system

This commit is contained in:
2025-11-02 18:40:03 +01:00
parent 9cc92e5fc4
commit e4bc438069
18 changed files with 674 additions and 636 deletions

View File

@@ -2,16 +2,38 @@
""" """
Authentication dependencies for FastAPI routes. Authentication dependencies for FastAPI routes.
Implements dual token storage pattern: This module provides authentication dependencies for all three contexts in the
- Checks Authorization header first (for API calls from JavaScript) multi-tenant application, implementing dual token storage with proper isolation:
- Falls back to cookie (for browser page navigation)
This allows: ADMIN ROUTES (/admin/*):
- JavaScript API calls: Use localStorage + Authorization header - Cookie: admin_token (path=/admin) OR Authorization header
- Browser page loads: Use HTTP-only cookies - Role: admin only
- Blocks: vendors, customers
VENDOR ROUTES (/vendor/*):
- Cookie: vendor_token (path=/vendor) OR Authorization header
- Role: vendor only
- Blocks: admins, customers
CUSTOMER/SHOP ROUTES (/shop/account/*):
- Cookie: customer_token (path=/shop) OR Authorization header
- Role: customer only
- Blocks: admins, vendors
- Note: Public shop pages (/shop/products, etc.) don't require auth
This dual authentication approach supports:
- HTML pages: Use cookies (automatic browser behavior)
- API calls: Use Authorization headers (explicit JavaScript control)
The cookie path restrictions prevent cross-context cookie leakage:
- admin_token is NEVER sent to /vendor/* or /shop/*
- vendor_token is NEVER sent to /admin/* or /shop/*
- customer_token is NEVER sent to /admin/* or /vendor/*
""" """
import logging
from typing import Optional from typing import Optional
from fastapi import Depends, Request, Cookie from fastapi import Depends, Request, Cookie
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -23,148 +45,434 @@ from models.database.vendor import Vendor
from models.database.user import User from models.database.user import User
from app.exceptions import ( from app.exceptions import (
AdminRequiredException, AdminRequiredException,
InvalidTokenException,
InsufficientPermissionsException,
VendorNotFoundException, VendorNotFoundException,
UnauthorizedVendorAccessException, UnauthorizedVendorAccessException
InvalidTokenException
) )
# Set auto_error=False to prevent automatic 403 responses # Initialize dependencies
security = HTTPBearer(auto_error=False) security = HTTPBearer(auto_error=False) # auto_error=False prevents automatic 403
auth_manager = AuthManager() auth_manager = AuthManager()
rate_limiter = RateLimiter() rate_limiter = RateLimiter()
logger = logging.getLogger(__name__)
def get_current_user( # ============================================================================
request: Request, # HELPER FUNCTIONS
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), # ============================================================================
admin_token: Optional[str] = Cookie(None), # Check admin_token cookie
db: Session = Depends(get_db), def _get_token_from_request(
): credentials: Optional[HTTPAuthorizationCredentials],
cookie_value: Optional[str],
cookie_name: str,
request_path: str
) -> tuple[Optional[str], Optional[str]]:
""" """
Get current authenticated user. Extract token from Authorization header or cookie.
Checks for token in this priority order: Priority:
1. Authorization header (for API calls from JavaScript) 1. Authorization header (for API calls from JavaScript)
2. admin_token cookie (for browser page navigation) 2. Cookie (for browser page navigation)
This dual approach supports:
- API calls: JavaScript adds token from localStorage to Authorization header
- Page navigation: Browser automatically sends cookie
Args: Args:
request: FastAPI request object
credentials: Optional Bearer token from Authorization header credentials: Optional Bearer token from Authorization header
admin_token: Optional token from cookie cookie_value: Optional token from cookie
cookie_name: Name of the cookie (for logging)
request_path: Request URL path (for logging)
Returns:
Tuple of (token, source) where source is "header" or "cookie"
"""
if credentials:
logger.debug(f"Token found in Authorization header for {request_path}")
return credentials.credentials, "header"
elif cookie_value:
logger.debug(f"Token found in {cookie_name} cookie for {request_path}")
return cookie_value, "cookie"
return None, None
def _validate_user_token(token: str, db: Session) -> User:
"""
Validate JWT token and return user.
Args:
token: JWT token string
db: Database session db: Database session
Returns: Returns:
User: Authenticated user object User: Authenticated user object
Raises: Raises:
InvalidTokenException: If no token found or token invalid InvalidTokenException: If token is invalid
""" """
token = None
token_source = None
# Priority 1: Authorization header (API calls from JavaScript)
if credentials:
token = credentials.credentials
token_source = "header"
# Priority 2: Cookie (browser page navigation)
elif admin_token:
token = admin_token
token_source = "cookie"
# No token found in either location
if not token:
raise InvalidTokenException("Authorization header or cookie required")
# Log token source for debugging
import logging
logger = logging.getLogger(__name__)
logger.debug(f"Token found in {token_source} for {request.url.path}")
# Create a mock credentials object for auth_manager
mock_credentials = HTTPAuthorizationCredentials( mock_credentials = HTTPAuthorizationCredentials(
scheme="Bearer", scheme="Bearer",
credentials=token credentials=token
) )
return auth_manager.get_current_user(db, mock_credentials) return auth_manager.get_current_user(db, mock_credentials)
def get_current_admin_user(current_user: User = Depends(get_current_user)): # ============================================================================
""" # ADMIN AUTHENTICATION
Require admin user. # ============================================================================
This dependency ensures the current user has admin role. def get_current_admin_from_cookie_or_header(
Used for protecting admin-only routes. request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
admin_token: Optional[str] = Cookie(None),
db: Session = Depends(get_db),
) -> User:
"""
Get current admin user from admin_token cookie or Authorization header.
Used for admin HTML pages (/admin/*) that need cookie-based auth.
Priority:
1. Authorization header (API calls)
2. admin_token cookie (page navigation)
Args: Args:
current_user: User object from get_current_user dependency request: FastAPI request
credentials: Optional Bearer token from header
admin_token: Optional token from admin_token cookie
db: Database session
Returns: Returns:
User: Admin user object User: Authenticated admin user
Raises: Raises:
AdminRequiredException: If user is not an admin InvalidTokenException: If no token or invalid token
AdminRequiredException: If user is not admin
""" """
return auth_manager.require_admin(current_user) token, source = _get_token_from_request(
credentials,
admin_token,
"admin_token",
str(request.url.path)
)
if not token:
logger.warning(f"Admin auth failed: No token for {request.url.path}")
raise InvalidTokenException("Admin authentication required")
# Validate token and get user
user = _validate_user_token(token, db)
# Verify user is admin
if user.role != "admin":
logger.warning(
f"Non-admin user {user.username} attempted admin route: {request.url.path}"
)
raise AdminRequiredException("Admin privileges required")
return user
def get_current_vendor_user(current_user: User = Depends(get_current_user)): def get_current_admin_api(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: Session = Depends(get_db),
) -> User:
""" """
Require vendor user (vendor owner or vendor staff). Get current admin user from Authorization header ONLY.
This dependency ensures the current user has vendor role. Used for admin API endpoints that should not accept cookies.
Used for protecting vendor-only routes. This prevents CSRF attacks on API endpoints.
Args: Args:
current_user: User object from get_current_user dependency credentials: Bearer token from Authorization header
db: Database session
Returns: Returns:
User: Vendor user object User: Authenticated admin user
Raises: Raises:
InsufficientPermissionsException: If user is not a vendor user InvalidTokenException: If no token or invalid token
AdminRequiredException: If user is not admin
""" """
return auth_manager.require_vendor(current_user) if not credentials:
raise InvalidTokenException("Authorization header required for API calls")
user = _validate_user_token(credentials.credentials, db)
if user.role != "admin":
logger.warning(f"Non-admin user {user.username} attempted admin API")
raise AdminRequiredException("Admin privileges required")
return user
def get_current_customer_user(current_user: User = Depends(get_current_user)): # ============================================================================
# VENDOR AUTHENTICATION
# ============================================================================
def get_current_vendor_from_cookie_or_header(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
vendor_token: Optional[str] = Cookie(None),
db: Session = Depends(get_db),
) -> User:
""" """
Require customer user. Get current vendor user from vendor_token cookie or Authorization header.
This dependency ensures the current user has customer role. Used for vendor HTML pages (/vendor/*) that need cookie-based auth.
Used for protecting customer account routes.
Priority:
1. Authorization header (API calls)
2. vendor_token cookie (page navigation)
Args: Args:
current_user: User object from get_current_user dependency request: FastAPI request
credentials: Optional Bearer token from header
vendor_token: Optional token from vendor_token cookie
db: Database session
Returns: Returns:
User: Customer user object User: Authenticated vendor user
Raises: Raises:
InsufficientPermissionsException: If user is not a customer InvalidTokenException: If no token or invalid token
InsufficientPermissionsException: If user is not vendor or is admin
""" """
return auth_manager.require_customer(current_user) token, source = _get_token_from_request(
credentials,
vendor_token,
"vendor_token",
str(request.url.path)
)
if not token:
logger.warning(f"Vendor auth failed: No token for {request.url.path}")
raise InvalidTokenException("Vendor authentication required")
# Validate token and get user
user = _validate_user_token(token, db)
# CRITICAL: Block admins from vendor routes
if user.role == "admin":
logger.warning(
f"Admin user {user.username} attempted vendor route: {request.url.path}"
)
raise InsufficientPermissionsException(
"Vendor access only - admins cannot use vendor portal"
)
# Verify user is vendor
if user.role != "vendor":
logger.warning(
f"Non-vendor user {user.username} attempted vendor route: {request.url.path}"
)
raise InsufficientPermissionsException("Vendor privileges required")
return user
def get_current_vendor_api(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: Session = Depends(get_db),
) -> User:
"""
Get current vendor user from Authorization header ONLY.
Used for vendor API endpoints that should not accept cookies.
Args:
credentials: Bearer token from Authorization header
db: Database session
Returns:
User: Authenticated vendor user
Raises:
InvalidTokenException: If no token or invalid token
InsufficientPermissionsException: If user is not vendor or is admin
"""
if not credentials:
raise InvalidTokenException("Authorization header required for API calls")
user = _validate_user_token(credentials.credentials, db)
# Block admins from vendor API
if user.role == "admin":
logger.warning(f"Admin user {user.username} attempted vendor API")
raise InsufficientPermissionsException("Vendor access only")
if user.role != "vendor":
logger.warning(f"Non-vendor user {user.username} attempted vendor API")
raise InsufficientPermissionsException("Vendor privileges required")
return user
# ============================================================================
# CUSTOMER AUTHENTICATION (SHOP)
# ============================================================================
def get_current_customer_from_cookie_or_header(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
customer_token: Optional[str] = Cookie(None),
db: Session = Depends(get_db),
) -> User:
"""
Get current customer user from customer_token cookie or Authorization header.
Used for shop account HTML pages (/shop/account/*) that need cookie-based auth.
Note: Public shop pages (/shop/products, etc.) don't use this dependency.
Priority:
1. Authorization header (API calls)
2. customer_token cookie (page navigation)
Args:
request: FastAPI request
credentials: Optional Bearer token from header
customer_token: Optional token from customer_token cookie
db: Database session
Returns:
User: Authenticated customer user
Raises:
InvalidTokenException: If no token or invalid token
InsufficientPermissionsException: If user is not customer (admin/vendor blocked)
"""
token, source = _get_token_from_request(
credentials,
customer_token,
"customer_token",
str(request.url.path)
)
if not token:
logger.warning(f"Customer auth failed: No token for {request.url.path}")
raise InvalidTokenException("Customer authentication required")
# Validate token and get user
user = _validate_user_token(token, db)
# CRITICAL: Block admins from customer routes
if user.role == "admin":
logger.warning(
f"Admin user {user.username} attempted shop account: {request.url.path}"
)
raise InsufficientPermissionsException(
"Customer access only - admins cannot use shop"
)
# CRITICAL: Block vendors from customer routes
if user.role == "vendor":
logger.warning(
f"Vendor user {user.username} attempted shop account: {request.url.path}"
)
raise InsufficientPermissionsException(
"Customer access only - vendors cannot use shop"
)
# Verify user is customer
if user.role != "customer":
logger.warning(
f"Non-customer user {user.username} attempted shop account: {request.url.path}"
)
raise InsufficientPermissionsException("Customer privileges required")
return user
def get_current_customer_api(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: Session = Depends(get_db),
) -> User:
"""
Get current customer user from Authorization header ONLY.
Used for shop API endpoints that should not accept cookies.
Args:
credentials: Bearer token from Authorization header
db: Database session
Returns:
User: Authenticated customer user
Raises:
InvalidTokenException: If no token or invalid token
InsufficientPermissionsException: If user is not customer (admin/vendor blocked)
"""
if not credentials:
raise InvalidTokenException("Authorization header required for API calls")
user = _validate_user_token(credentials.credentials, db)
# Block admins from customer API
if user.role == "admin":
logger.warning(f"Admin user {user.username} attempted customer API")
raise InsufficientPermissionsException("Customer access only")
# Block vendors from customer API
if user.role == "vendor":
logger.warning(f"Vendor user {user.username} attempted customer API")
raise InsufficientPermissionsException("Customer access only")
if user.role != "customer":
logger.warning(f"Non-customer user {user.username} attempted customer API")
raise InsufficientPermissionsException("Customer privileges required")
return user
# ============================================================================
# GENERIC AUTHENTICATION (for mixed-use endpoints)
# ============================================================================
def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: Session = Depends(get_db)
) -> User:
"""
Get current authenticated user from Authorization header only.
Generic authentication without role checking.
Used for endpoints accessible to any authenticated user.
Args:
credentials: Bearer token from Authorization header
db: Database session
Returns:
User: Authenticated user (any role)
Raises:
InvalidTokenException: If no token or invalid token
"""
if not credentials:
raise InvalidTokenException("Authorization header required")
return _validate_user_token(credentials.credentials, db)
# ============================================================================
# VENDOR OWNERSHIP VERIFICATION
# ============================================================================
def get_user_vendor( def get_user_vendor(
vendor_code: str, vendor_code: str,
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_vendor_from_cookie_or_header),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ) -> Vendor:
""" """
Get vendor and verify user ownership. Get vendor and verify user ownership/membership.
Ensures the current user has access to the specified vendor. Ensures the current user has access to the specified vendor.
Admin users can access any vendor, regular users only their own. - Vendor owners can access their own vendor
- Team members can access their vendor
- Admins are BLOCKED (use admin routes instead)
Args: Args:
vendor_code: Vendor code to look up vendor_code: Vendor code to look up
current_user: Current authenticated user current_user: Current authenticated vendor user
db: Database session db: Database session
Returns: Returns:
@@ -174,11 +482,19 @@ def get_user_vendor(
VendorNotFoundException: If vendor doesn't exist VendorNotFoundException: If vendor doesn't exist
UnauthorizedVendorAccessException: If user doesn't have access UnauthorizedVendorAccessException: If user doesn't have access
""" """
vendor = db.query(Vendor).filter(Vendor.vendor_code == vendor_code.upper()).first() vendor = db.query(Vendor).filter(
Vendor.vendor_code == vendor_code.upper()
).first()
if not vendor: if not vendor:
raise VendorNotFoundException(vendor_code) raise VendorNotFoundException(vendor_code)
if current_user.role != "admin" and vendor.owner_user_id != current_user.id: # Check if user owns this vendor
raise UnauthorizedVendorAccessException(vendor_code, current_user.id) if vendor.owner_user_id == current_user.id:
return vendor
return vendor # Check if user is team member
# TODO: Add team member check when VendorUser relationship is set up
# User doesn't have access to this vendor
raise UnauthorizedVendorAccessException(vendor_code, current_user.id)

View File

@@ -15,7 +15,7 @@ from datetime import datetime
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_user from app.api.deps import get_current_admin_api
from app.core.database import get_db from app.core.database import get_db
from app.services.admin_audit_service import admin_audit_service from app.services.admin_audit_service import admin_audit_service
from models.schema.admin import ( from models.schema.admin import (
@@ -39,7 +39,7 @@ def get_audit_logs(
skip: int = Query(0, ge=0, description="Number of records to skip"), skip: int = Query(0, ge=0, description="Number of records to skip"),
limit: int = Query(100, ge=1, le=1000, description="Maximum records to return"), limit: int = Query(100, ge=1, le=1000, description="Maximum records to return"),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
Get filtered admin audit logs. Get filtered admin audit logs.
@@ -74,7 +74,7 @@ def get_audit_logs(
def get_recent_audit_logs( def get_recent_audit_logs(
limit: int = Query(20, ge=1, le=100), limit: int = Query(20, ge=1, le=100),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Get recent audit logs (last 20 by default).""" """Get recent audit logs (last 20 by default)."""
filters = AdminAuditLogFilters(limit=limit) filters = AdminAuditLogFilters(limit=limit)
@@ -85,7 +85,7 @@ def get_recent_audit_logs(
def get_my_actions( def get_my_actions(
limit: int = Query(50, ge=1, le=100), limit: int = Query(50, ge=1, le=100),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Get audit logs for current admin's actions.""" """Get audit logs for current admin's actions."""
return admin_audit_service.get_recent_actions_by_admin( return admin_audit_service.get_recent_actions_by_admin(
@@ -101,7 +101,7 @@ def get_actions_by_target(
target_id: str, target_id: str,
limit: int = Query(50, ge=1, le=100), limit: int = Query(50, ge=1, le=100),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
Get all actions performed on a specific target. Get all actions performed on a specific target.

View File

@@ -2,9 +2,11 @@
""" """
Admin authentication endpoints. Admin authentication endpoints.
Implements dual token storage: Implements dual token storage with path restriction:
- Sets HTTP-only cookie for browser page navigation - Sets HTTP-only cookie with path=/admin (restricted to admin routes only)
- Returns token in response for localStorage (API calls) - Returns token in response for localStorage (API calls)
This prevents admin cookies from being sent to vendor routes.
""" """
import logging import logging
@@ -16,7 +18,7 @@ from app.services.auth_service import auth_service
from app.exceptions import InvalidCredentialsException from app.exceptions import InvalidCredentialsException
from models.schema.auth import LoginResponse, UserLogin, UserResponse from models.schema.auth import LoginResponse, UserLogin, UserResponse
from models.database.user import User from models.database.user import User
from app.api.deps import get_current_admin_user from app.api.deps import get_current_admin_api
from app.core.config import settings from app.core.config import settings
router = APIRouter(prefix="/auth") router = APIRouter(prefix="/auth")
@@ -36,8 +38,11 @@ def admin_login(
Returns JWT token for authenticated admin users. Returns JWT token for authenticated admin users.
Sets token in two places: Sets token in two places:
1. HTTP-only cookie (for browser page navigation) 1. HTTP-only cookie with path=/admin (for browser page navigation)
2. Response body (for localStorage and API calls) 2. Response body (for localStorage and API calls)
The cookie is restricted to /admin/* routes only to prevent
it from being sent to vendor or other routes.
""" """
# Authenticate user # Authenticate user
login_result = auth_service.login_user(db=db, user_credentials=user_credentials) login_result = auth_service.login_user(db=db, user_credentials=user_credentials)
@@ -50,17 +55,21 @@ def admin_login(
logger.info(f"Admin login successful: {login_result['user'].username}") logger.info(f"Admin login successful: {login_result['user'].username}")
# Set HTTP-only cookie for browser navigation # Set HTTP-only cookie for browser navigation
# CRITICAL: path=/admin restricts cookie to admin routes only
response.set_cookie( response.set_cookie(
key="admin_token", key="admin_token",
value=login_result["token_data"]["access_token"], value=login_result["token_data"]["access_token"],
httponly=True, # JavaScript cannot access (XSS protection) httponly=True, # JavaScript cannot access (XSS protection)
secure=False, # Set to True in production (requires HTTPS) secure=settings.environment == "production", # HTTPS only in production
samesite="lax", # CSRF protection samesite="lax", # CSRF protection
max_age=login_result["token_data"]["expires_in"], # Match JWT expiry max_age=login_result["token_data"]["expires_in"], # Match JWT expiry
path="/", # Available for all routes path="/admin", # RESTRICTED TO ADMIN ROUTES ONLY
) )
logger.debug(f"Set admin_token cookie with {login_result['token_data']['expires_in']}s expiry") logger.debug(
f"Set admin_token cookie with {login_result['token_data']['expires_in']}s expiry "
f"(path=/admin, httponly=True, secure={settings.environment == 'production'})"
)
# Also return token in response for localStorage (API calls) # Also return token in response for localStorage (API calls)
return LoginResponse( return LoginResponse(
@@ -72,7 +81,7 @@ def admin_login(
@router.get("/me", response_model=UserResponse) @router.get("/me", response_model=UserResponse)
def get_current_admin(current_user: User = Depends(get_current_admin_user)): def get_current_admin(current_user: User = Depends(get_current_admin_api)):
""" """
Get current authenticated admin user. Get current authenticated admin user.
@@ -81,11 +90,9 @@ def get_current_admin(current_user: User = Depends(get_current_admin_user)):
Token can come from: Token can come from:
- Authorization header (API calls) - Authorization header (API calls)
- admin_token cookie (browser navigation) - admin_token cookie (browser navigation, path=/admin only)
""" """
logger.info(f"Admin user info requested: {current_user.username}") logger.info(f"Admin user info requested: {current_user.username}")
# Pydantic will automatically serialize the User model to UserResponse
return current_user return current_user
@@ -99,10 +106,10 @@ def admin_logout(response: Response):
""" """
logger.info("Admin logout") logger.info("Admin logout")
# Clear the cookie # Clear the cookie (must match path used when setting)
response.delete_cookie( response.delete_cookie(
key="admin_token", key="admin_token",
path="/", path="/admin",
) )
logger.debug("Deleted admin_token cookie") logger.debug("Deleted admin_token cookie")

View File

@@ -8,7 +8,7 @@ from typing import List
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_user from app.api.deps import get_current_admin_api
from app.core.database import get_db from app.core.database import get_db
from app.services.admin_service import admin_service from app.services.admin_service import admin_service
from app.services.stats_service import stats_service from app.services.stats_service import stats_service
@@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
@router.get("") @router.get("")
def get_admin_dashboard( def get_admin_dashboard(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Get admin dashboard with platform statistics (Admin only).""" """Get admin dashboard with platform statistics (Admin only)."""
return { return {
@@ -40,7 +40,7 @@ def get_admin_dashboard(
@router.get("/stats", response_model=StatsResponse) @router.get("/stats", response_model=StatsResponse)
def get_comprehensive_stats( def get_comprehensive_stats(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Get comprehensive platform statistics (Admin only).""" """Get comprehensive platform statistics (Admin only)."""
stats_data = stats_service.get_comprehensive_stats(db=db) stats_data = stats_service.get_comprehensive_stats(db=db)
@@ -59,7 +59,7 @@ def get_comprehensive_stats(
@router.get("/stats/marketplace", response_model=List[MarketplaceStatsResponse]) @router.get("/stats/marketplace", response_model=List[MarketplaceStatsResponse])
def get_marketplace_stats( def get_marketplace_stats(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Get statistics broken down by marketplace (Admin only).""" """Get statistics broken down by marketplace (Admin only)."""
marketplace_stats = stats_service.get_marketplace_breakdown_stats(db=db) marketplace_stats = stats_service.get_marketplace_breakdown_stats(db=db)
@@ -78,7 +78,7 @@ def get_marketplace_stats(
@router.get("/stats/platform") @router.get("/stats/platform")
def get_platform_statistics( def get_platform_statistics(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Get comprehensive platform statistics (Admin only).""" """Get comprehensive platform statistics (Admin only)."""
return { return {

View File

@@ -9,7 +9,7 @@ from typing import List, Optional
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_user from app.api.deps import get_current_admin_api
from app.core.database import get_db from app.core.database import get_db
from app.services.admin_service import admin_service from app.services.admin_service import admin_service
from app.services.stats_service import stats_service from app.services.stats_service import stats_service
@@ -28,7 +28,7 @@ def get_all_marketplace_import_jobs(
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=100), limit: int = Query(100, ge=1, le=100),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Get all marketplace import jobs (Admin only).""" """Get all marketplace import jobs (Admin only)."""
return admin_service.get_marketplace_import_jobs( return admin_service.get_marketplace_import_jobs(
@@ -44,7 +44,7 @@ def get_all_marketplace_import_jobs(
@router.get("/stats") @router.get("/stats")
def get_import_statistics( def get_import_statistics(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Get marketplace import statistics (Admin only).""" """Get marketplace import statistics (Admin only)."""
return stats_service.get_import_statistics(db) return stats_service.get_import_statistics(db)

View File

@@ -14,7 +14,7 @@ from typing import Optional
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_user from app.api.deps import get_current_admin_api
from app.core.database import get_db from app.core.database import get_db
from models.schema.admin import ( from models.schema.admin import (
AdminNotificationCreate, AdminNotificationCreate,
@@ -42,7 +42,7 @@ def get_notifications(
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100), limit: int = Query(50, ge=1, le=100),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Get admin notifications with filtering.""" """Get admin notifications with filtering."""
# TODO: Implement notification service # TODO: Implement notification service
@@ -58,7 +58,7 @@ def get_notifications(
@router.get("/unread-count") @router.get("/unread-count")
def get_unread_count( def get_unread_count(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Get count of unread notifications.""" """Get count of unread notifications."""
# TODO: Implement # TODO: Implement
@@ -69,7 +69,7 @@ def get_unread_count(
def mark_as_read( def mark_as_read(
notification_id: int, notification_id: int,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Mark notification as read.""" """Mark notification as read."""
# TODO: Implement # TODO: Implement
@@ -79,7 +79,7 @@ def mark_as_read(
@router.put("/mark-all-read") @router.put("/mark-all-read")
def mark_all_as_read( def mark_all_as_read(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Mark all notifications as read.""" """Mark all notifications as read."""
# TODO: Implement # TODO: Implement
@@ -97,7 +97,7 @@ def get_platform_alerts(
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100), limit: int = Query(50, ge=1, le=100),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Get platform alerts with filtering.""" """Get platform alerts with filtering."""
# TODO: Implement alert service # TODO: Implement alert service
@@ -115,7 +115,7 @@ def get_platform_alerts(
def create_platform_alert( def create_platform_alert(
alert_data: PlatformAlertCreate, alert_data: PlatformAlertCreate,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Create new platform alert (manual).""" """Create new platform alert (manual)."""
# TODO: Implement # TODO: Implement
@@ -128,7 +128,7 @@ def resolve_platform_alert(
alert_id: int, alert_id: int,
resolve_data: PlatformAlertResolve, resolve_data: PlatformAlertResolve,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Resolve platform alert.""" """Resolve platform alert."""
# TODO: Implement # TODO: Implement
@@ -139,7 +139,7 @@ def resolve_platform_alert(
@router.get("/alerts/stats") @router.get("/alerts/stats")
def get_alert_statistics( def get_alert_statistics(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Get alert statistics for dashboard.""" """Get alert statistics for dashboard."""
# TODO: Implement # TODO: Implement

View File

@@ -14,7 +14,7 @@ from typing import Optional
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_user from app.api.deps import get_current_admin_api
from app.core.database import get_db from app.core.database import get_db
from app.services.admin_settings_service import admin_settings_service from app.services.admin_settings_service import admin_settings_service
from app.services.admin_audit_service import admin_audit_service from app.services.admin_audit_service import admin_audit_service
@@ -35,7 +35,7 @@ def get_all_settings(
category: Optional[str] = Query(None, description="Filter by category"), category: Optional[str] = Query(None, description="Filter by category"),
is_public: Optional[bool] = Query(None, description="Filter by public flag"), is_public: Optional[bool] = Query(None, description="Filter by public flag"),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
Get all platform settings. Get all platform settings.
@@ -55,7 +55,7 @@ def get_all_settings(
@router.get("/categories") @router.get("/categories")
def get_setting_categories( def get_setting_categories(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Get list of all setting categories.""" """Get list of all setting categories."""
# This could be enhanced to return counts per category # This could be enhanced to return counts per category
@@ -75,7 +75,7 @@ def get_setting_categories(
def get_setting( def get_setting(
key: str, key: str,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Get specific setting by key.""" """Get specific setting by key."""
setting = admin_settings_service.get_setting_by_key(db, key) setting = admin_settings_service.get_setting_by_key(db, key)
@@ -91,7 +91,7 @@ def get_setting(
def create_setting( def create_setting(
setting_data: AdminSettingCreate, setting_data: AdminSettingCreate,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
Create new platform setting. Create new platform setting.
@@ -122,7 +122,7 @@ def update_setting(
key: str, key: str,
update_data: AdminSettingUpdate, update_data: AdminSettingUpdate,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Update existing setting value.""" """Update existing setting value."""
old_value = admin_settings_service.get_setting_value(db, key) old_value = admin_settings_service.get_setting_value(db, key)
@@ -151,7 +151,7 @@ def update_setting(
def upsert_setting( def upsert_setting(
setting_data: AdminSettingCreate, setting_data: AdminSettingCreate,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
Create or update setting (upsert). Create or update setting (upsert).
@@ -182,7 +182,7 @@ def delete_setting(
key: str, key: str,
confirm: bool = Query(False, description="Must be true to confirm deletion"), confirm: bool = Query(False, description="Must be true to confirm deletion"),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
Delete platform setting. Delete platform setting.

View File

@@ -9,7 +9,7 @@ from typing import List
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_user from app.api.deps import get_current_admin_api
from app.core.database import get_db from app.core.database import get_db
from app.services.admin_service import admin_service from app.services.admin_service import admin_service
from app.services.stats_service import stats_service from app.services.stats_service import stats_service
@@ -25,7 +25,7 @@ def get_all_users(
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000), limit: int = Query(100, ge=1, le=1000),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Get all users (Admin only).""" """Get all users (Admin only)."""
users = admin_service.get_all_users(db=db, skip=skip, limit=limit) users = admin_service.get_all_users(db=db, skip=skip, limit=limit)
@@ -36,7 +36,7 @@ def get_all_users(
def toggle_user_status( def toggle_user_status(
user_id: int, user_id: int,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Toggle user active status (Admin only).""" """Toggle user active status (Admin only)."""
user, message = admin_service.toggle_user_status(db, user_id, current_admin.id) user, message = admin_service.toggle_user_status(db, user_id, current_admin.id)
@@ -46,7 +46,7 @@ def toggle_user_status(
@router.get("/stats") @router.get("/stats")
def get_user_statistics( def get_user_statistics(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Get user statistics for admin dashboard (Admin only).""" """Get user statistics for admin dashboard (Admin only)."""
return stats_service.get_user_statistics(db) return stats_service.get_user_statistics(db)

View File

@@ -15,7 +15,7 @@ from typing import List
from fastapi import APIRouter, Depends, Path, Body, Query from fastapi import APIRouter, Depends, Path, Body, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_user from app.api.deps import get_current_admin_api
from app.core.database import get_db from app.core.database import get_db
from app.services.vendor_domain_service import vendor_domain_service from app.services.vendor_domain_service import vendor_domain_service
from app.exceptions import VendorNotFoundException from app.exceptions import VendorNotFoundException
@@ -60,7 +60,7 @@ def add_vendor_domain(
vendor_id: int = Path(..., description="Vendor ID", gt=0), vendor_id: int = Path(..., description="Vendor ID", gt=0),
domain_data: VendorDomainCreate = Body(...), domain_data: VendorDomainCreate = Body(...),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
Add a custom domain to vendor (Admin only). Add a custom domain to vendor (Admin only).
@@ -113,7 +113,7 @@ def add_vendor_domain(
def list_vendor_domains( def list_vendor_domains(
vendor_id: int = Path(..., description="Vendor ID", gt=0), vendor_id: int = Path(..., description="Vendor ID", gt=0),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
List all domains for a vendor (Admin only). List all domains for a vendor (Admin only).
@@ -156,7 +156,7 @@ def list_vendor_domains(
def get_domain_details( def get_domain_details(
domain_id: int = Path(..., description="Domain ID", gt=0), domain_id: int = Path(..., description="Domain ID", gt=0),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
Get detailed information about a specific domain (Admin only). Get detailed information about a specific domain (Admin only).
@@ -187,7 +187,7 @@ def update_vendor_domain(
domain_id: int = Path(..., description="Domain ID", gt=0), domain_id: int = Path(..., description="Domain ID", gt=0),
domain_update: VendorDomainUpdate = Body(...), domain_update: VendorDomainUpdate = Body(...),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
Update domain settings (Admin only). Update domain settings (Admin only).
@@ -231,7 +231,7 @@ def update_vendor_domain(
def delete_vendor_domain( def delete_vendor_domain(
domain_id: int = Path(..., description="Domain ID", gt=0), domain_id: int = Path(..., description="Domain ID", gt=0),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
Delete a custom domain (Admin only). Delete a custom domain (Admin only).
@@ -260,7 +260,7 @@ def delete_vendor_domain(
def verify_domain_ownership( def verify_domain_ownership(
domain_id: int = Path(..., description="Domain ID", gt=0), domain_id: int = Path(..., description="Domain ID", gt=0),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
Verify domain ownership via DNS TXT record (Admin only). Verify domain ownership via DNS TXT record (Admin only).
@@ -298,7 +298,7 @@ def verify_domain_ownership(
def get_domain_verification_instructions( def get_domain_verification_instructions(
domain_id: int = Path(..., description="Domain ID", gt=0), domain_id: int = Path(..., description="Domain ID", gt=0),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
Get DNS verification instructions for domain (Admin only). Get DNS verification instructions for domain (Admin only).

View File

@@ -17,7 +17,7 @@ import logging
from fastapi import APIRouter, Depends, Path from fastapi import APIRouter, Depends, Path
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_user, get_db from app.api.deps import get_current_admin_api, get_db
from app.services.vendor_theme_service import vendor_theme_service from app.services.vendor_theme_service import vendor_theme_service
from models.database.user import User from models.database.user import User
from models.schema.vendor_theme import ( from models.schema.vendor_theme import (
@@ -36,7 +36,7 @@ logger = logging.getLogger(__name__)
@router.get("/presets", response_model=ThemePresetListResponse) @router.get("/presets", response_model=ThemePresetListResponse)
async def get_theme_presets( async def get_theme_presets(
current_admin: User = Depends(get_current_admin_user) current_admin: User = Depends(get_current_admin_api)
): ):
""" """
Get all available theme presets with preview information. Get all available theme presets with preview information.
@@ -63,7 +63,7 @@ async def get_theme_presets(
async def get_vendor_theme( async def get_vendor_theme(
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user) current_admin: User = Depends(get_current_admin_api)
): ):
""" """
Get theme configuration for a vendor. Get theme configuration for a vendor.
@@ -98,7 +98,7 @@ async def update_vendor_theme(
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
theme_data: VendorThemeUpdate = None, theme_data: VendorThemeUpdate = None,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user) current_admin: User = Depends(get_current_admin_api)
): ):
""" """
Update or create theme for a vendor. Update or create theme for a vendor.
@@ -145,7 +145,7 @@ async def apply_theme_preset(
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
preset_name: str = Path(..., description="Preset name"), preset_name: str = Path(..., description="Preset name"),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user) current_admin: User = Depends(get_current_admin_api)
): ):
""" """
Apply a theme preset to a vendor. Apply a theme preset to a vendor.
@@ -196,7 +196,7 @@ async def apply_theme_preset(
async def delete_vendor_theme( async def delete_vendor_theme(
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user) current_admin: User = Depends(get_current_admin_api)
): ):
""" """
Delete custom theme for a vendor. Delete custom theme for a vendor.

View File

@@ -9,7 +9,7 @@ from typing import Optional
from fastapi import APIRouter, Depends, Query, Path, Body from fastapi import APIRouter, Depends, Query, Path, Body
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_user from app.api.deps import get_current_admin_api
from app.core.database import get_db from app.core.database import get_db
from app.services.admin_service import admin_service from app.services.admin_service import admin_service
from app.services.stats_service import stats_service from app.services.stats_service import stats_service
@@ -74,7 +74,7 @@ def _get_vendor_by_identifier(db: Session, identifier: str) -> Vendor:
def create_vendor_with_owner( def create_vendor_with_owner(
vendor_data: VendorCreate, vendor_data: VendorCreate,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
Create a new vendor with owner user account (Admin only). Create a new vendor with owner user account (Admin only).
@@ -133,7 +133,7 @@ def get_all_vendors_admin(
is_active: Optional[bool] = Query(None), is_active: Optional[bool] = Query(None),
is_verified: Optional[bool] = Query(None), is_verified: Optional[bool] = Query(None),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Get all vendors with filtering (Admin only).""" """Get all vendors with filtering (Admin only)."""
vendors, total = admin_service.get_all_vendors( vendors, total = admin_service.get_all_vendors(
@@ -150,7 +150,7 @@ def get_all_vendors_admin(
@router.get("/stats", response_model=VendorStatsResponse) @router.get("/stats", response_model=VendorStatsResponse)
def get_vendor_statistics_endpoint( def get_vendor_statistics_endpoint(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
"""Get vendor statistics for admin dashboard (Admin only).""" """Get vendor statistics for admin dashboard (Admin only)."""
stats = stats_service.get_vendor_statistics(db) stats = stats_service.get_vendor_statistics(db)
@@ -167,7 +167,7 @@ def get_vendor_statistics_endpoint(
def get_vendor_details( def get_vendor_details(
vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"), vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
Get detailed vendor information including owner details (Admin only). Get detailed vendor information including owner details (Admin only).
@@ -211,7 +211,7 @@ def update_vendor(
vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"), vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"),
vendor_update: VendorUpdate = Body(...), vendor_update: VendorUpdate = Body(...),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
Update vendor information (Admin only). Update vendor information (Admin only).
@@ -262,7 +262,7 @@ def transfer_vendor_ownership(
vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"), vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"),
transfer_data: VendorTransferOwnership = Body(...), transfer_data: VendorTransferOwnership = Body(...),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
Transfer vendor ownership to another user (Admin only). Transfer vendor ownership to another user (Admin only).
@@ -314,7 +314,7 @@ def toggle_vendor_verification(
vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"), vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"),
verification_data: dict = Body(..., example={"is_verified": True}), verification_data: dict = Body(..., example={"is_verified": True}),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
Toggle vendor verification status (Admin only). Toggle vendor verification status (Admin only).
@@ -365,7 +365,7 @@ def toggle_vendor_status(
vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"), vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"),
status_data: dict = Body(..., example={"is_active": True}), status_data: dict = Body(..., example={"is_active": True}),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
Toggle vendor active status (Admin only). Toggle vendor active status (Admin only).
@@ -416,7 +416,7 @@ def delete_vendor(
vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"), vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"),
confirm: bool = Query(False, description="Must be true to confirm deletion"), confirm: bool = Query(False, description="Must be true to confirm deletion"),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user), current_admin: User = Depends(get_current_admin_api),
): ):
""" """
Delete vendor and all associated data (Admin only). Delete vendor and all associated data (Admin only).

View File

@@ -2,14 +2,17 @@
""" """
Customer authentication endpoints (public-facing). Customer authentication endpoints (public-facing).
This module provides: Implements dual token storage with path restriction:
- Customer registration (vendor-scoped) - Sets HTTP-only cookie with path=/shop (restricted to shop routes only)
- Customer login (vendor-scoped) - Returns token in response for localStorage (API calls)
- Customer password reset
This prevents:
- Customer cookies from being sent to admin or vendor routes
- Cross-context authentication confusion
""" """
import logging import logging
from fastapi import APIRouter, Depends, Path from fastapi import APIRouter, Depends, Response
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.core.database import get_db 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.auth import LoginResponse, UserLogin
from models.schema.customer import CustomerRegister, CustomerResponse from models.schema.customer import CustomerRegister, CustomerResponse
from models.database.vendor import Vendor from models.database.vendor import Vendor
from app.core.config import settings
router = APIRouter(prefix="/auth") router = APIRouter(prefix="/auth")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -63,6 +67,7 @@ def register_customer(
def customer_login( def customer_login(
vendor_id: int, vendor_id: int,
user_credentials: UserLogin, user_credentials: UserLogin,
response: Response,
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -70,6 +75,13 @@ def customer_login(
Authenticates customer and returns JWT token. Authenticates customer and returns JWT token.
Customer must belong to the specified vendor. 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 # Verify vendor exists and is active
vendor = db.query(Vendor).filter( vendor = db.query(Vendor).filter(
@@ -92,6 +104,24 @@ def customer_login(
f"for vendor {vendor.vendor_code}" 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( return LoginResponse(
access_token=login_result["token_data"]["access_token"], access_token=login_result["token_data"]["access_token"],
token_type=login_result["token_data"]["token_type"], token_type=login_result["token_data"]["token_type"],
@@ -101,12 +131,26 @@ def customer_login(
@router.post("/{vendor_id}/customers/logout") @router.post("/{vendor_id}/customers/logout")
def customer_logout(vendor_id: int): def customer_logout(
vendor_id: int,
response: Response
):
""" """
Customer logout. 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"} return {"message": "Logged out successfully"}
@@ -173,3 +217,26 @@ def reset_password(
logger.info(f"Password reset completed for vendor {vendor.vendor_code}") logger.info(f"Password reset completed for vendor {vendor.vendor_code}")
return {"message": "Password reset successful"} 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"
}

View File

@@ -2,14 +2,18 @@
""" """
Vendor team authentication endpoints. Vendor team authentication endpoints.
This module provides: Implements dual token storage with path restriction:
- Vendor team member login - Sets HTTP-only cookie with path=/vendor (restricted to vendor routes only)
- Vendor owner login - Returns token in response for localStorage (API calls)
- Vendor-scoped authentication
This prevents:
- Vendor cookies from being sent to admin routes
- Admin cookies from being sent to vendor routes
- Cross-context authentication confusion
""" """
import logging import logging
from fastapi import APIRouter, Depends, Request, HTTPException from fastapi import APIRouter, Depends, Request, Response
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.core.database import get_db from app.core.database import get_db
@@ -18,7 +22,9 @@ from app.exceptions import InvalidCredentialsException
from middleware.vendor_context import get_current_vendor from middleware.vendor_context import get_current_vendor
from models.schema.auth import UserLogin from models.schema.auth import UserLogin
from models.database.vendor import Vendor, VendorUser, Role from models.database.vendor import Vendor, VendorUser, Role
from models.database.user import User
from pydantic import BaseModel from pydantic import BaseModel
from app.core.config import settings
router = APIRouter(prefix="/auth") router = APIRouter(prefix="/auth")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -38,6 +44,7 @@ class VendorLoginResponse(BaseModel):
def vendor_login( def vendor_login(
user_credentials: UserLogin, user_credentials: UserLogin,
request: Request, request: Request,
response: Response,
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -45,6 +52,12 @@ def vendor_login(
Authenticates users who are part of a vendor team. Authenticates users who are part of a vendor team.
Validates against vendor context if available. Validates against vendor context if available.
Sets token in two places:
1. HTTP-only cookie with path=/vendor (for browser page navigation)
2. Response body (for localStorage and API calls)
Prevents admin users from logging into vendor portal.
""" """
# Try to get vendor from middleware first # Try to get vendor from middleware first
vendor = get_current_vendor(request) vendor = get_current_vendor(request)
@@ -62,10 +75,10 @@ def vendor_login(
login_result = auth_service.login_user(db=db, user_credentials=user_credentials) login_result = auth_service.login_user(db=db, user_credentials=user_credentials)
user = login_result["user"] user = login_result["user"]
# Prevent admin users from using vendor login # CRITICAL: Prevent admin users from using vendor login
if user.role == "admin": if user.role == "admin":
logger.warning(f"Admin user attempted vendor login: {user.username}") logger.warning(f"Admin user attempted vendor login: {user.username}")
raise InvalidCredentialsException("Please use admin portal to login") raise InvalidCredentialsException("Admins cannot access vendor portal. Please use admin portal.")
# Determine vendor and role # Determine vendor and role
vendor_role = "Member" vendor_role = "Member"
@@ -120,6 +133,24 @@ def vendor_login(
f"for vendor {vendor.vendor_code} as {vendor_role}" f"for vendor {vendor.vendor_code} as {vendor_role}"
) )
# Set HTTP-only cookie for browser navigation
# CRITICAL: path=/vendor restricts cookie to vendor routes only
response.set_cookie(
key="vendor_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="/vendor", # RESTRICTED TO VENDOR ROUTES ONLY
)
logger.debug(
f"Set vendor_token cookie with {login_result['token_data']['expires_in']}s expiry "
f"(path=/vendor, httponly=True, secure={settings.environment == 'production'})"
)
# Return full login response
return VendorLoginResponse( return VendorLoginResponse(
access_token=login_result["token_data"]["access_token"], access_token=login_result["token_data"]["access_token"],
token_type=login_result["token_data"]["token_type"], token_type=login_result["token_data"]["token_type"],
@@ -144,10 +175,45 @@ def vendor_login(
@router.post("/logout") @router.post("/logout")
def vendor_logout(): def vendor_logout(response: Response):
""" """
Vendor team member logout. Vendor team member logout.
Client should remove token from storage. Clears the vendor_token cookie.
Client should also remove token from localStorage.
""" """
return {"message": "Logged out successfully"} logger.info("Vendor logout")
# Clear the cookie (must match path used when setting)
response.delete_cookie(
key="vendor_token",
path="/vendor",
)
logger.debug("Deleted vendor_token cookie")
return {"message": "Logged out successfully"}
@router.get("/me")
def get_current_vendor_user(
request: Request,
db: Session = Depends(get_db)
):
"""
Get current authenticated vendor user.
This endpoint can be called to verify authentication and get user info.
"""
from app.api.deps import get_current_vendor_api
# This will check both cookie and header
user = get_current_vendor_api(request, db=db)
return {
"id": user.id,
"username": user.username,
"email": user.email,
"role": user.role,
"is_active": user.is_active
}

View File

@@ -28,7 +28,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_user, get_db from app.api.deps import get_current_admin_from_cookie_or_header, get_db
from models.database.user import User from models.database.user import User
router = APIRouter() router = APIRouter()
@@ -70,7 +70,7 @@ async def admin_login_page(request: Request):
@router.get("/dashboard", response_class=HTMLResponse, include_in_schema=False) @router.get("/dashboard", response_class=HTMLResponse, include_in_schema=False)
async def admin_dashboard_page( async def admin_dashboard_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_user), current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -93,7 +93,7 @@ async def admin_dashboard_page(
@router.get("/vendors", response_class=HTMLResponse, include_in_schema=False) @router.get("/vendors", response_class=HTMLResponse, include_in_schema=False)
async def admin_vendors_list_page( async def admin_vendors_list_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_user), current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -112,7 +112,7 @@ async def admin_vendors_list_page(
@router.get("/vendors/create", response_class=HTMLResponse, include_in_schema=False) @router.get("/vendors/create", response_class=HTMLResponse, include_in_schema=False)
async def admin_vendor_create_page( async def admin_vendor_create_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_user), current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -131,7 +131,7 @@ async def admin_vendor_create_page(
async def admin_vendor_detail_page( async def admin_vendor_detail_page(
request: Request, request: Request,
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_admin_user), current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -152,7 +152,7 @@ async def admin_vendor_detail_page(
async def admin_vendor_edit_page( async def admin_vendor_edit_page(
request: Request, request: Request,
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_admin_user), current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -176,7 +176,7 @@ async def admin_vendor_edit_page(
async def admin_vendor_domains_page( async def admin_vendor_domains_page(
request: Request, request: Request,
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_admin_user), current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -201,7 +201,7 @@ async def admin_vendor_domains_page(
async def admin_vendor_theme_page( async def admin_vendor_theme_page(
request: Request, request: Request,
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_admin_user), current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -225,7 +225,7 @@ async def admin_vendor_theme_page(
@router.get("/users", response_class=HTMLResponse, include_in_schema=False) @router.get("/users", response_class=HTMLResponse, include_in_schema=False)
async def admin_users_page( async def admin_users_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_user), current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -248,7 +248,7 @@ async def admin_users_page(
@router.get("/imports", response_class=HTMLResponse, include_in_schema=False) @router.get("/imports", response_class=HTMLResponse, include_in_schema=False)
async def admin_imports_page( async def admin_imports_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_user), current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -271,7 +271,7 @@ async def admin_imports_page(
@router.get("/settings", response_class=HTMLResponse, include_in_schema=False) @router.get("/settings", response_class=HTMLResponse, include_in_schema=False)
async def admin_settings_page( async def admin_settings_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_user), current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -294,7 +294,7 @@ async def admin_settings_page(
@router.get("/components", response_class=HTMLResponse, include_in_schema=False) @router.get("/components", response_class=HTMLResponse, include_in_schema=False)
async def admin_components_page( async def admin_components_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_user), current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -313,7 +313,7 @@ async def admin_components_page(
@router.get("/icons", response_class=HTMLResponse, include_in_schema=False) @router.get("/icons", response_class=HTMLResponse, include_in_schema=False)
async def admin_icons_page( async def admin_icons_page(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_user), current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -332,7 +332,7 @@ async def admin_icons_page(
@router.get("/testing", response_class=HTMLResponse, include_in_schema=False) @router.get("/testing", response_class=HTMLResponse, include_in_schema=False)
async def admin_testing_hub( async def admin_testing_hub(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_user), current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -351,7 +351,7 @@ async def admin_testing_hub(
@router.get("/test/auth-flow", response_class=HTMLResponse, include_in_schema=False) @router.get("/test/auth-flow", response_class=HTMLResponse, include_in_schema=False)
async def admin_test_auth_flow( async def admin_test_auth_flow(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_user), current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -370,7 +370,7 @@ async def admin_test_auth_flow(
@router.get("/test/vendors-users-migration", response_class=HTMLResponse, include_in_schema=False) @router.get("/test/vendors-users-migration", response_class=HTMLResponse, include_in_schema=False)
async def admin_test_vendors_users_migration( async def admin_test_vendors_users_migration(
request: Request, request: Request,
current_user: User = Depends(get_current_admin_user), current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """

View File

@@ -5,6 +5,14 @@ Shop/Customer HTML page routes using Jinja2 templates.
These routes serve the public-facing shop interface for customers. These routes serve the public-facing shop interface for customers.
Authentication required only for account pages. Authentication required only for account pages.
AUTHENTICATION:
- Public pages (catalog, products): No auth required
- Account pages (dashboard, orders): Requires customer authentication
- Customer authentication accepts:
* customer_token cookie (path=/shop) - for page navigation
* Authorization header - for API calls
- Customers CANNOT access admin or vendor routes
Routes: Routes:
- GET /shop/ → Shop homepage / product catalog - GET /shop/ → Shop homepage / product catalog
- GET /shop/products → Product catalog - GET /shop/products → Product catalog
@@ -26,7 +34,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_current_customer_user, get_db from app.api.deps import get_current_customer_from_cookie_or_header, get_db
from models.database.user import User from models.database.user import User
router = APIRouter() router = APIRouter()
@@ -191,7 +199,7 @@ async def shop_account_root():
@router.get("/shop/account/dashboard", response_class=HTMLResponse, include_in_schema=False) @router.get("/shop/account/dashboard", response_class=HTMLResponse, include_in_schema=False)
async def shop_account_dashboard_page( async def shop_account_dashboard_page(
request: Request, request: Request,
current_user: User = Depends(get_current_customer_user), current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -211,7 +219,7 @@ async def shop_account_dashboard_page(
@router.get("/shop/account/orders", response_class=HTMLResponse, include_in_schema=False) @router.get("/shop/account/orders", response_class=HTMLResponse, include_in_schema=False)
async def shop_orders_page( async def shop_orders_page(
request: Request, request: Request,
current_user: User = Depends(get_current_customer_user), current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -232,7 +240,7 @@ async def shop_orders_page(
async def shop_order_detail_page( async def shop_order_detail_page(
request: Request, request: Request,
order_id: int = Path(..., description="Order ID"), order_id: int = Path(..., description="Order ID"),
current_user: User = Depends(get_current_customer_user), current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -253,7 +261,7 @@ async def shop_order_detail_page(
@router.get("/shop/account/profile", response_class=HTMLResponse, include_in_schema=False) @router.get("/shop/account/profile", response_class=HTMLResponse, include_in_schema=False)
async def shop_profile_page( async def shop_profile_page(
request: Request, request: Request,
current_user: User = Depends(get_current_customer_user), current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -273,7 +281,7 @@ async def shop_profile_page(
@router.get("/shop/account/addresses", response_class=HTMLResponse, include_in_schema=False) @router.get("/shop/account/addresses", response_class=HTMLResponse, include_in_schema=False)
async def shop_addresses_page( async def shop_addresses_page(
request: Request, request: Request,
current_user: User = Depends(get_current_customer_user), current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -293,7 +301,7 @@ async def shop_addresses_page(
@router.get("/shop/account/wishlist", response_class=HTMLResponse, include_in_schema=False) @router.get("/shop/account/wishlist", response_class=HTMLResponse, include_in_schema=False)
async def shop_wishlist_page( async def shop_wishlist_page(
request: Request, request: Request,
current_user: User = Depends(get_current_customer_user), current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
@@ -313,7 +321,7 @@ async def shop_wishlist_page(
@router.get("/shop/account/settings", response_class=HTMLResponse, include_in_schema=False) @router.get("/shop/account/settings", response_class=HTMLResponse, include_in_schema=False)
async def shop_settings_page( async def shop_settings_page(
request: Request, request: Request,
current_user: User = Depends(get_current_customer_user), current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """

View File

@@ -25,7 +25,7 @@ from fastapi import APIRouter, Request, Depends, Path
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from app.api.deps import get_current_vendor_user from app.api.deps import get_current_vendor_from_cookie_or_header
from models.database.user import User from models.database.user import User
router = APIRouter() router = APIRouter()
@@ -85,7 +85,7 @@ async def vendor_login_page(
async def vendor_dashboard_page( async def vendor_dashboard_page(
request: Request, request: Request,
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_user) current_user: User = Depends(get_current_vendor_from_cookie_or_header)
): ):
""" """
Render vendor dashboard. Render vendor dashboard.
@@ -114,7 +114,7 @@ async def vendor_dashboard_page(
async def vendor_products_page( async def vendor_products_page(
request: Request, request: Request,
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_user) current_user: User = Depends(get_current_vendor_from_cookie_or_header)
): ):
""" """
Render products management page. Render products management page.
@@ -138,7 +138,7 @@ async def vendor_products_page(
async def vendor_orders_page( async def vendor_orders_page(
request: Request, request: Request,
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_user) current_user: User = Depends(get_current_vendor_from_cookie_or_header)
): ):
""" """
Render orders management page. Render orders management page.
@@ -162,7 +162,7 @@ async def vendor_orders_page(
async def vendor_customers_page( async def vendor_customers_page(
request: Request, request: Request,
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_user) current_user: User = Depends(get_current_vendor_from_cookie_or_header)
): ):
""" """
Render customers management page. Render customers management page.
@@ -186,7 +186,7 @@ async def vendor_customers_page(
async def vendor_inventory_page( async def vendor_inventory_page(
request: Request, request: Request,
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_user) current_user: User = Depends(get_current_vendor_from_cookie_or_header)
): ):
""" """
Render inventory management page. Render inventory management page.
@@ -210,7 +210,7 @@ async def vendor_inventory_page(
async def vendor_marketplace_page( async def vendor_marketplace_page(
request: Request, request: Request,
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_user) current_user: User = Depends(get_current_vendor_from_cookie_or_header)
): ):
""" """
Render marketplace import page. Render marketplace import page.
@@ -234,7 +234,7 @@ async def vendor_marketplace_page(
async def vendor_team_page( async def vendor_team_page(
request: Request, request: Request,
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_user) current_user: User = Depends(get_current_vendor_from_cookie_or_header)
): ):
""" """
Render team management page. Render team management page.
@@ -258,7 +258,7 @@ async def vendor_team_page(
async def vendor_settings_page( async def vendor_settings_page(
request: Request, request: Request,
vendor_code: str = Path(..., description="Vendor code"), vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_user) current_user: User = Depends(get_current_vendor_from_cookie_or_header)
): ):
""" """
Render vendor settings page. Render vendor settings page.

View File

@@ -30,6 +30,7 @@ from app.exceptions.handler import setup_exception_handlers
from app.exceptions import ServiceUnavailableException from app.exceptions import ServiceUnavailableException
from middleware.theme_context import theme_context_middleware from middleware.theme_context import theme_context_middleware
from middleware.vendor_context import vendor_context_middleware from middleware.vendor_context import vendor_context_middleware
from middleware.logging_middleware import LoggingMiddleware
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -67,6 +68,9 @@ app.middleware("http")(vendor_context_middleware)
# Add theme context middleware (must be after vendor context) # Add theme context middleware (must be after vendor context)
app.middleware("http")(theme_context_middleware) app.middleware("http")(theme_context_middleware)
# Add logging middleware (logs all requests/responses)
app.add_middleware(LoggingMiddleware)
# ======================================== # ========================================
# MOUNT STATIC FILES - Use absolute path # MOUNT STATIC FILES - Use absolute path
# ======================================== # ========================================
@@ -213,4 +217,4 @@ async def documentation():
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

430
temp.md
View File

@@ -1,430 +0,0 @@
Project Continuation Guide: Multi-Tenant E-commerce Component System
🎯 Project Overview
We're building a universal component-based architecture for a multi-tenant e-commerce platform with three distinct sections:
Admin Portal - Internal management dashboard
Vendor Dashboard - Business owner portal
Shop Frontend - Customer-facing storefront
Main Goals:
✅ Eliminate code duplication (header, sidebar, modals repeated across pages)
✅ Create consistent UX across all sections
✅ Use Alpine.js components for reusability
✅ Support all three sections from day one (no rework needed later)
✅ Maintain consistent modal behavior everywhere
🏗️ Architecture Overview
Technology Stack:
Frontend: Plain HTML, CSS, JavaScript (no frameworks)
State Management: Alpine.js 3.x
API Client: Custom apiClient class
Backend: FastAPI (Python) with multi-tenant architecture
Component Architecture:
Universal Modal System (shared by all sections)
├── Admin Layout Component
├── Vendor Layout Component
└── Shop Layout Component
└── Shop Account Layout Component
```
### **Key Design Decisions:**
1. **Universal Modals** - Same confirmation/success/error modals work in all sections
2. **Section-Specific Layouts** - Each section has its own header/sidebar/navigation
3. **Shared Utilities** - Common functions (Auth, Utils, apiClient) used everywhere
4. **Session-Based Cart** - No authentication required for shopping
5. **Vendor-Scoped Customers** - Each vendor has independent customer base
---
## 📊 **Current Project Structure**
```
static/
├── css/
│ ├── shared/
│ │ ├── base.css # ✅ Exists
│ │ ├── auth.css # ✅ Exists
│ │ ├── responsive-utilities.css # ✅ Exists
│ │ ├── components.css # 🔄 Needs creation
│ │ └── modals.css # 🔄 Needs creation (optional)
│ ├── admin/
│ │ └── admin.css # ✅ Exists
│ ├── vendor/
│ │ └── vendor.css # ✅ Exists
│ └── shop/
│ └── shop.css # 🔄 Needs creation
├── js/
│ ├── shared/
│ │ ├── api-client.js # ✅ Exists (working)
│ │ ├── alpine-components.js # 🔄 IN PROGRESS
│ │ └── modal-system.js # 🔄 Needs creation
│ ├── admin/
│ │ ├── dashboard.js # ✅ Exists (needs conversion)
│ │ ├── vendor-edit.js # ✅ Exists (partially converted)
│ │ ├── vendors.js # ✅ Exists (needs conversion)
│ │ └── login.js # ✅ Exists (working)
│ ├── vendor/
│ │ ├── dashboard.js # ✅ Exists
│ │ ├── products.js # ✅ Exists
│ │ └── orders.js # ✅ Exists
│ └── shop/
│ ├── catalog.js # 🔄 Needs creation
│ ├── product-detail.js # 🔄 Needs creation
│ └── cart.js # 🔄 Needs creation
├── admin/
│ ├── dashboard.html # ✅ Exists (needs conversion)
│ ├── vendor-edit.html # ✅ Exists (partially converted)
│ ├── vendors.html # ✅ Exists (needs conversion)
│ ├── users.html # ✅ Exists (needs conversion)
│ ├── marketplace.html # ✅ Exists
│ ├── monitoring.html # ✅ Exists
│ └── login.html # ✅ Exists (working)
├── vendor/
│ ├── dashboard.html # ✅ Exists (needs conversion)
│ ├── (admin pages) # ✅ Exist (need conversion)
│ └── login.html # ✅ Exists
└── shop/
├── home.html # ✅ Exists (needs conversion)
├── products.html # ✅ Exists (needs conversion)
├── product.html # ✅ Exists (needs conversion)
├── cart.html # ✅ Exists (needs conversion)
└── account/
├── orders.html # ✅ Exists (needs conversion)
├── profile.html # ✅ Exists
├── addresses.html # ✅ Exists
└── login.html # ✅ Exists
✅ What's Been Completed
1. Problem Identification
✅ Identified code duplication issue (header/sidebar/modals repeated)
✅ Analyzed current structure (7 admin pages, ~1,600 lines of duplicated code)
✅ Calculated 85% code reduction potential with component system
2. Architecture Design
✅ Designed universal modal system (works in admin, vendor, shop)
✅ Planned section-specific layouts (admin, vendor, shop, shop-account)
✅ Created component inheritance structure (baseModalSystem)
✅ Planned API integration strategy
3. Initial Implementation
✅ Started alpine-components.js with:
baseModalSystem() - Universal modal functions
adminLayout() - Admin header, sidebar, logout
vendorLayout() - Vendor header, sidebar, logout
shopLayout() - Shop header, cart, search, logout
shopAccountLayout() - Shop account area layout
4. Admin Section Progress
✅ vendor-edit.html - Partially converted
Has custom modals (confirm, success) working
Logout modal working
Needs migration to universal component
✅ vendor-edit.js - Working with modals
✅ Identified all admin pages needing conversion
🔄 Current Status: IN PROGRESS
Last Working On:
Creating the complete alpine-components.js file with all layout components.
What Was Just Completed:
javascript// alpine-components.js structure:
✅ baseModalSystem() - Universal modals (confirm, success, error)
✅ adminLayout() - Complete admin layout with logout modal
✅ vendorLayout() - Complete vendor layout with logout modal
✅ shopLayout() - Complete shop layout with cart integration
✅ shopAccountLayout() - Shop account area layout
// Features implemented:
✅ Session-based cart (no auth required)
✅ Vendor detection and context
✅ Logout confirmation modals for all sections
✅ Cart count tracking
✅ Search functionality
✅ Mobile menu support
```
### **File Status:**
- **`alpine-components.js`** - ✅ **95% COMPLETE** (ready to save)
- **`modal-system.js`** - 🔄 Ready to create next
- **`components.css`** - 🔄 Ready to create next
---
## 🚀 **Next Steps**
### **Immediate Next Actions:**
#### **Step 1: Complete Core Files (30 mins)**
1. ✅ Save `alpine-components.js` (already created)
2. 🔄 Create `modal-system.js` - Helper functions for modals
3. 🔄 Create `components.css` - Universal component styles
4. 🔄 Create `modals.css` - Modal-specific styles (optional)
#### **Step 2: Create Modal HTML Templates (15 mins)**
Create reusable modal HTML snippets that can be copy-pasted into pages:
- Confirmation Modal template
- Success Modal template
- Error Modal template
- Loading Overlay template
#### **Step 3: Convert Admin Pages (2 hours)**
Convert pages to use new component system:
1. **`vendor-edit.html`** (30 mins) - Already partially done
- Replace duplicated modals with component
- Use `adminLayout()` component
- Test all functionality
2. **`dashboard.html`** (30 mins)
- Add `adminLayout()` component
- Add modal templates
- Update logout button
3. **`vendors.html`** (30 mins)
- Same conversion pattern
- Test vendor management features
4. **`users.html`** (30 mins)
- Same conversion pattern
#### **Step 4: Create Shop Pages (3 hours)**
1. **`home.html`** (45 mins)
- Use `shopLayout()` component
- Integrate vendor detection
- Add featured products section
2. **`products.html`** (45 mins)
- Product catalog with filters
- Integrate with `/public/vendors/{vendor_id}/products` API
- Add to cart functionality
3. **`product.html`** (45 mins)
- Product detail page
- Image gallery
- Add to cart
- Quantity selector
4. **`cart.html`** (45 mins)
- Shopping cart display
- Update quantities
- Remove items
- Checkout button
5. **`account/orders.html`** (45 mins)
- Use `shopAccountLayout()` component
- Order history display
- Order detail links
#### **Step 5: Create Shop JavaScript (2 hours)**
1. **`catalog.js`** - Product listing logic
2. **`product-detail.js`** - Single product logic
3. **`cart.js`** - Cart management
#### **Step 6: Convert Vendor Pages (1.5 hours)**
1. **`dashboard.html`** - Use `vendorLayout()`
2. **`products.html`** - Product management
3. **`orders.html`** - Order management
---
## 📡 **API Integration Details**
### **Backend API Endpoints Available:**
#### **Vendor APIs:**
```
GET /api/v1/public/vendors/by-code/{vendor_code}
GET /api/v1/public/vendors/by-subdomain/{subdomain}
GET /api/v1/public/vendors/{vendor_id}/info
```
#### **Product APIs:**
```
GET /api/v1/public/vendors/{vendor_id}/products
?skip=0&limit=100&search=query&is_featured=true
GET /api/v1/public/vendors/{vendor_id}/products/{product_id}
GET /api/v1/public/vendors/{vendor_id}/products/search?q=query
```
#### **Cart APIs (Session-based, no auth required):**
```
GET /api/v1/public/vendors/{vendor_id}/cart/{session_id}
POST /api/v1/public/vendors/{vendor_id}/cart/{session_id}/items
Body: { product_id: 1, quantity: 2 }
PUT /api/v1/public/vendors/{vendor_id}/cart/{session_id}/items/{product_id}
Body: { quantity: 3 }
DELETE /api/v1/public/vendors/{vendor_id}/cart/{session_id}/items/{product_id}
DELETE /api/v1/public/vendors/{vendor_id}/cart/{session_id}
```
#### **Customer Auth APIs:**
```
POST /api/v1/public/vendors/{vendor_id}/customers/register
POST /api/v1/public/vendors/{vendor_id}/customers/login
POST /api/v1/public/vendors/{vendor_id}/customers/logout
```
#### **Order APIs:**
```
POST /api/v1/public/vendors/{vendor_id}/orders
GET /api/v1/public/vendors/{vendor_id}/customers/{customer_id}/orders
GET /api/v1/public/vendors/{vendor_id}/customers/{customer_id}/orders/{order_id}
Key API Features:
✅ Multi-tenant (vendor-scoped)
✅ Session-based cart (no login required for shopping)
✅ Vendor-scoped customers (same email can register with different vendors)
✅ Public product catalog (no auth needed to browse)
✅ Active/inactive vendor filtering
💡 Important Context
User's Preferences:
Uses Python for backend
Uses plain HTML/CSS/JavaScript for frontend (no frameworks like React/Vue)
Uses Alpine.js for reactivity
Prefers AJAX over full-page reloads
Wants clean, maintainable code
Current Working Features:
✅ Admin login/logout working
✅ Admin dashboard displaying stats
✅ Vendor edit page working with custom modals
✅ API client properly configured
✅ Authentication system working
Known Issues Fixed:
✅ Fixed duplicate /api/v1/ in URLs (was /api/v1/api/v1/)
✅ Fixed admin auth endpoint (uses /admin/auth/me not /auth/me)
✅ Fixed emoji encoding in logs (removed emojis from Python logging)
✅ Fixed Alpine timing issues (initialize vendor as {} not null)
✅ Fixed modal stacking (transfer ownership modal)
📝 Code Patterns Established
Component Usage Pattern:
html<!-- Admin Page -->
<body x-data="adminLayout()" x-init="init()">
<!-- Layout renders header + sidebar + modals -->
<main class="admin-content">
<!-- Page-specific content here -->
</main>
</body>
Modal Usage Pattern:
javascript// In any component/page using adminLayout()
this.showConfirmModal({
title: 'Confirm Action',
message: 'Are you sure?',
warning: 'This cannot be undone',
buttonText: 'Yes, Do It',
buttonClass: 'btn-danger',
onConfirm: () => this.doAction()
});
API Call Pattern:
javascript// Using the existing apiClient
const products = await apiClient.get(
`/public/vendors/${vendorId}/products`,
{ skip: 0, limit: 20 }
);
🎯 Success Criteria
The project will be complete when:
✅ All admin pages use adminLayout() component
✅ All vendor pages use vendorLayout() component
✅ All shop pages use shopLayout() or shopAccountLayout()
✅ Modals work consistently across all sections
✅ No code duplication for headers/sidebars/modals
✅ Shop can browse products, add to cart, checkout
✅ Cart persists across page refreshes (session-based)
✅ Customers can register/login per vendor
✅ Customers can view order history
📦 Files Ready to Deliver
When you continue, ask for these files in this order:
Phase 1: Core System
static/js/shared/alpine-components.js - ✅ Ready (95% complete)
static/js/shared/modal-system.js - Ready to create
static/css/shared/components.css - Ready to create
Phase 2: Modal Templates
Modal HTML templates (copy-paste snippets)
Phase 3: Admin Pages
static/admin/vendor-edit.html - Updated version
static/admin/dashboard.html - Converted version
static/admin/vendors.html - Converted version
Phase 4: Shop Pages
static/shop/home.html - New with shopLayout
static/shop/products.html - Converted
static/shop/product.html - Converted
static/shop/cart.html - Converted
static/shop/account/orders.html - Converted
Phase 5: Shop JavaScript
static/js/shop/catalog.js
static/js/shop/product-detail.js
static/js/shop/cart.js
Phase 6: Documentation
Migration guide for remaining pages
Component usage documentation
🚀 How to Continue
In your next chat, say:
"Let's continue the component system implementation. I have the continuation guide. Please start with Phase 1: create alpine-components.js, modal-system.js, and components.css."
Or ask for specific phases:
"Give me Phase 1 files (core system)"
"Give me Phase 3 files (admin pages)"
"Give me Phase 4 files (shop pages)"
📊 Progress Tracking
Overall Progress: 25% complete
Core System: 60% complete
Admin Section: 40% complete
Vendor Section: 10% complete
Shop Section: 5% complete
Estimated Time to Complete: 8-10 hours of work remaining
Last Updated: Current session
Ready to Continue: Yes - All context preserved
Next Action: Create Phase 1 core files