Working state before icon/utils fixes - Oct 22

This commit is contained in:
2025-10-21 21:56:54 +02:00
parent a7d9d44a13
commit 5be47b91a2
39 changed files with 6017 additions and 508 deletions

View File

@@ -1,13 +1,18 @@
# app/api/deps.py
"""Summary description ....
"""
Authentication dependencies for FastAPI routes.
This module provides classes and functions for:
- ....
- ....
- ....
Implements dual token storage pattern:
- Checks Authorization header first (for API calls from JavaScript)
- Falls back to cookie (for browser page navigation)
This allows:
- JavaScript API calls: Use localStorage + Authorization header
- Browser page loads: Use HTTP-only cookies
"""
from fastapi import Depends
from typing import Optional
from fastapi import Depends, Request, Cookie
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy.orm import Session
@@ -16,7 +21,12 @@ from middleware.auth import AuthManager
from middleware.rate_limiter import RateLimiter
from models.database.vendor import Vendor
from models.database.user import User
from app.exceptions import (AdminRequiredException, VendorNotFoundException, UnauthorizedVendorAccessException)
from app.exceptions import (
AdminRequiredException,
VendorNotFoundException,
UnauthorizedVendorAccessException,
InvalidTokenException
)
# Set auto_error=False to prevent automatic 403 responses
security = HTTPBearer(auto_error=False)
@@ -25,30 +35,107 @@ rate_limiter = RateLimiter()
def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: Session = Depends(get_db),
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
admin_token: Optional[str] = Cookie(None), # Check admin_token cookie
db: Session = Depends(get_db),
):
"""Get current authenticated user."""
# Check if credentials are provided
if not credentials:
from app.exceptions.auth import InvalidTokenException
raise InvalidTokenException("Authorization header required")
"""
Get current authenticated user.
return auth_manager.get_current_user(db, credentials)
Checks for token in this priority order:
1. Authorization header (for API calls from JavaScript)
2. admin_token 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:
request: FastAPI request object
credentials: Optional Bearer token from Authorization header
admin_token: Optional token from cookie
db: Database session
Returns:
User: Authenticated user object
Raises:
InvalidTokenException: If no token found or token 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(
scheme="Bearer",
credentials=token
)
return auth_manager.get_current_user(db, mock_credentials)
def get_current_admin_user(current_user: User = Depends(get_current_user)):
"""Require admin user."""
"""
Require admin user.
This dependency ensures the current user has admin role.
Used for protecting admin-only routes.
Args:
current_user: User object from get_current_user dependency
Returns:
User: Admin user object
Raises:
AdminRequiredException: If user is not an admin
"""
return auth_manager.require_admin(current_user)
def get_user_vendor(
vendor_code: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
vendor_code: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Get vendor and verify user ownership."""
"""
Get vendor and verify user ownership.
Ensures the current user has access to the specified vendor.
Admin users can access any vendor, regular users only their own.
Args:
vendor_code: Vendor code to look up
current_user: Current authenticated user
db: Database session
Returns:
Vendor: Vendor object if user has access
Raises:
VendorNotFoundException: If vendor doesn't exist
UnauthorizedVendorAccessException: If user doesn't have access
"""
vendor = db.query(Vendor).filter(Vendor.vendor_code == vendor_code.upper()).first()
if not vendor:
raise VendorNotFoundException(vendor_code)
@@ -57,4 +144,3 @@ def get_user_vendor(
raise UnauthorizedVendorAccessException(vendor_code, current_user.id)
return vendor

View File

@@ -8,9 +8,10 @@ This module combines all admin-related API endpoints:
- User management (status, roles)
- Dashboard and statistics
- Marketplace monitoring
- Audit logging (NEW)
- Platform settings (NEW)
- Notifications and alerts (NEW)
- Audit logging
- Platform settings
- Notifications and alerts
- HTML Pages - Server-rendered pages using Jinja2
"""
from fastapi import APIRouter
@@ -25,7 +26,8 @@ from . import (
monitoring,
audit,
settings,
notifications
notifications,
pages
)
# Create admin router
@@ -51,7 +53,7 @@ router.include_router(marketplace.router, tags=["admin-marketplace"])
# router.include_router(monitoring.router, tags=["admin-monitoring"])
# ============================================================================
# NEW: Admin Models Integration
# Admin Models Integration
# ============================================================================
# Include audit logging endpoints
@@ -63,6 +65,12 @@ router.include_router(settings.router, tags=["admin-settings"])
# Include notifications and alerts endpoints
router.include_router(notifications.router, tags=["admin-notifications"])
# ============================================================================
# HTML Page Routes (Jinja2 Templates)
# ============================================================================
# Include HTML page routes (these return rendered templates, not JSON)
router.include_router(pages.router, tags=["admin-pages"])
# Export the router
__all__ = ["router"]

View File

@@ -2,32 +2,42 @@
"""
Admin authentication endpoints.
This module provides:
- Admin user login
- Admin token validation
- Admin-specific authentication logic
Implements dual token storage:
- Sets HTTP-only cookie for browser page navigation
- Returns token in response for localStorage (API calls)
"""
import logging
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Response
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.services.auth_service import auth_service
from app.exceptions import InvalidCredentialsException
from models.schema.auth import LoginResponse, UserLogin
from models.schema.auth import LoginResponse, UserLogin, UserResponse
from models.database.user import User
from app.api.deps import get_current_admin_user
from app.core.config import settings
router = APIRouter(prefix="/auth")
logger = logging.getLogger(__name__)
@router.post("/login", response_model=LoginResponse)
def admin_login(user_credentials: UserLogin, db: Session = Depends(get_db)):
def admin_login(
user_credentials: UserLogin,
response: Response,
db: Session = Depends(get_db)
):
"""
Admin login endpoint.
Only allows users with 'admin' role to login.
Returns JWT token for authenticated admin users.
Sets token in two places:
1. HTTP-only cookie (for browser page navigation)
2. Response body (for localStorage and API calls)
"""
# Authenticate user
login_result = auth_service.login_user(db=db, user_credentials=user_credentials)
@@ -39,6 +49,20 @@ def admin_login(user_credentials: UserLogin, db: Session = Depends(get_db)):
logger.info(f"Admin login successful: {login_result['user'].username}")
# Set HTTP-only cookie for browser navigation
response.set_cookie(
key="admin_token",
value=login_result["token_data"]["access_token"],
httponly=True, # JavaScript cannot access (XSS protection)
secure=False, # Set to True in production (requires HTTPS)
samesite="lax", # CSRF protection
max_age=login_result["token_data"]["expires_in"], # Match JWT expiry
path="/", # Available for all routes
)
logger.debug(f"Set admin_token cookie with {login_result['token_data']['expires_in']}s expiry")
# Also return token in response for localStorage (API calls)
return LoginResponse(
access_token=login_result["token_data"]["access_token"],
token_type=login_result["token_data"]["token_type"],
@@ -47,12 +71,40 @@ def admin_login(user_credentials: UserLogin, db: Session = Depends(get_db)):
)
@router.get("/me", response_model=UserResponse)
def get_current_admin(current_user: User = Depends(get_current_admin_user)):
"""
Get current authenticated admin user.
This endpoint validates the token and ensures the user has admin privileges.
Returns the current user's information.
Token can come from:
- Authorization header (API calls)
- admin_token cookie (browser navigation)
"""
logger.info(f"Admin user info requested: {current_user.username}")
# Pydantic will automatically serialize the User model to UserResponse
return current_user
@router.post("/logout")
def admin_logout():
def admin_logout(response: Response):
"""
Admin logout endpoint.
Client should remove token from storage.
Server-side token invalidation can be implemented here if needed.
Clears the admin_token cookie.
Client should also remove token from localStorage.
"""
logger.info("Admin logout")
# Clear the cookie
response.delete_cookie(
key="admin_token",
path="/",
)
logger.debug("Deleted admin_token cookie")
return {"message": "Logged out successfully"}

110
app/api/v1/admin/pages.py Normal file
View File

@@ -0,0 +1,110 @@
# app/api/v1/admin/pages.py
"""
Admin HTML page routes using Jinja2 templates.
These routes return rendered HTML pages (response_class=HTMLResponse).
Separate from other admin routes which return JSON data.
Routes:
- GET / - Admin root (redirects to login)
- GET /login - Admin login page (no auth required)
- GET /dashboard - Admin dashboard (requires auth)
- GET /vendors - Vendor management page (requires auth)
- GET /users - User management page (requires auth)
"""
from fastapi import APIRouter, Request, Depends
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_user, get_db
from models.database.user import User
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
@router.get("/", response_class=RedirectResponse, include_in_schema=False)
async def admin_root():
"""
Redirect /admin/ to /admin/login.
This is the simplest approach:
- Unauthenticated users: see login form
- Authenticated users: login page clears token and shows form
(they can manually navigate to dashboard if needed)
Alternative: Could redirect to /admin/dashboard and let auth
dependency handle the redirect, but that's an extra hop.
"""
return RedirectResponse(url="/admin/login", status_code=302)
@router.get("/login", response_class=HTMLResponse, include_in_schema=False)
async def admin_login_page(request: Request):
"""
Render admin login page.
No authentication required.
"""
return templates.TemplateResponse(
"admin/login.html",
{"request": request}
)
@router.get("/dashboard", response_class=HTMLResponse, include_in_schema=False)
async def admin_dashboard_page(
request: Request,
current_user: User = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
"""
Render admin dashboard page.
Requires admin authentication - will redirect to login if not authenticated.
"""
return templates.TemplateResponse(
"admin/dashboard.html",
{
"request": request,
"user": current_user,
}
)
@router.get("/vendors", response_class=HTMLResponse, include_in_schema=False)
async def admin_vendors_page(
request: Request,
current_user: User = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
"""
Render vendors management page.
Requires admin authentication.
"""
return templates.TemplateResponse(
"admin/vendors.html",
{
"request": request,
"user": current_user,
}
)
@router.get("/users", response_class=HTMLResponse, include_in_schema=False)
async def admin_users_page(
request: Request,
current_user: User = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
"""
Render users management page.
Requires admin authentication.
"""
return templates.TemplateResponse(
"admin/users.html",
{
"request": request,
"user": current_user,
}
)