revamped authentication system
This commit is contained in:
484
app/api/deps.py
484
app/api/deps.py
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
81
app/api/v1/public/vendors/auth.py
vendored
81
app/api/v1/public/vendors/auth.py
vendored
@@ -2,14 +2,17 @@
|
|||||||
"""
|
"""
|
||||||
Customer authentication endpoints (public-facing).
|
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"
|
||||||
|
}
|
||||||
|
|||||||
86
app/api/v1/vendor/auth.py
vendored
86
app/api/v1/vendor/auth.py
vendored
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
6
main.py
6
main.py
@@ -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
430
temp.md
@@ -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
|
|
||||||
Reference in New Issue
Block a user