| """ |
| Authentication module for Universal Model Trainer |
| Provides password-based authentication with session management |
| SIMPLIFIED VERSION - No middleware, direct session checks |
| """ |
|
|
| from fastapi import HTTPException, Request, status, Depends |
| from starlette.middleware.sessions import SessionMiddleware |
| from starlette.responses import JSONResponse, HTMLResponse |
| import secrets |
| from typing import Optional |
| from app.config import settings |
|
|
|
|
| def setup_auth(app) -> None: |
| """Add session middleware to the FastAPI app""" |
| app.add_middleware( |
| SessionMiddleware, |
| secret_key=settings.SESSION_SECRET_KEY, |
| session_cookie="trainer_session", |
| max_age=settings.SESSION_EXPIRE_HOURS * 3600, |
| same_site="lax", |
| https_only=False |
| ) |
|
|
|
|
| def verify_password(password: str) -> bool: |
| """Verify password against env variable using constant-time comparison""" |
| if not settings.APP_PASSWORD: |
| return True |
| return secrets.compare_digest(password, settings.APP_PASSWORD) |
|
|
|
|
| def is_auth_enabled() -> bool: |
| """Check if authentication is enabled""" |
| return bool(settings.APP_PASSWORD) |
|
|
|
|
| def get_current_user(request: Request) -> Optional[dict]: |
| """Get current user from session if authenticated""" |
| if not settings.APP_PASSWORD: |
| return {"authenticated": True, "username": "anonymous"} |
| |
| user = request.session.get("user") |
| if user and user.get("authenticated"): |
| return user |
| return None |
|
|
|
|
| async def require_auth(request: Request) -> dict: |
| """Dependency that requires authentication - returns user or raises 401""" |
| if not settings.APP_PASSWORD: |
| return {"authenticated": True, "username": "anonymous"} |
| |
| user = get_current_user(request) |
| if not user: |
| raise HTTPException( |
| status_code=status.HTTP_401_UNAUTHORIZED, |
| detail="Authentication required", |
| headers={"WWW-Authenticate": "Session"} |
| ) |
| return user |
|
|
|
|
| def is_authenticated(request: Request) -> bool: |
| """Check if request is authenticated (helper for templates)""" |
| if not settings.APP_PASSWORD: |
| return True |
| user = request.session.get("user") |
| return bool(user and user.get("authenticated")) |
|
|
|
|
| def get_login_html(error: str = None) -> str: |
| """Generate the login page HTML""" |
| error_display = f'<div class="error-msg text-center" style="display:block;">{error}</div>' if error else '<div class="error-msg text-center" id="errorMsg">Invalid password</div>' |
| |
| return f'''<!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Universal Model Trainer - Login</title> |
| <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> |
| <style> |
| body {{ |
| background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); |
| min-height: 100vh; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| }} |
| .login-container {{ |
| max-width: 500px; |
| width: 100%; |
| padding: 20px; |
| }} |
| .login-card {{ |
| background: rgba(255,255,255,0.05); |
| border: 1px solid rgba(255,255,255,0.1); |
| border-radius: 20px; |
| padding: 50px 40px; |
| backdrop-filter: blur(10px); |
| box-shadow: 0 20px 40px rgba(0,0,0,0.3); |
| }} |
| .login-card h1 {{ |
| color: #fff; |
| margin-bottom: 10px; |
| font-weight: 600; |
| font-size: 1.8rem; |
| }} |
| .login-card .subtitle {{ |
| color: rgba(255,255,255,0.6); |
| margin-bottom: 35px; |
| font-size: 0.95rem; |
| }} |
| .login-icon {{ |
| font-size: 3rem; |
| margin-bottom: 20px; |
| display: block; |
| text-align: center; |
| }} |
| .form-control {{ |
| background: rgba(255,255,255,0.1); |
| border: 1px solid rgba(255,255,255,0.2); |
| color: #fff; |
| padding: 15px 20px; |
| font-size: 1rem; |
| border-radius: 12px; |
| }} |
| .form-control::placeholder {{ |
| color: rgba(255,255,255,0.5); |
| }} |
| .form-control:focus {{ |
| background: rgba(255,255,255,0.15); |
| border-color: #4f46e5; |
| color: #fff; |
| box-shadow: 0 0 0 3px rgba(79,70,229,0.25); |
| }} |
| .btn-login {{ |
| background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); |
| border: none; |
| padding: 15px; |
| font-weight: 600; |
| font-size: 1rem; |
| border-radius: 12px; |
| width: 100%; |
| margin-top: 10px; |
| transition: all 0.3s ease; |
| }} |
| .btn-login:hover {{ |
| background: linear-gradient(135deg, #4338ca 0%, #6d28d9 100%); |
| transform: translateY(-2px); |
| box-shadow: 0 5px 20px rgba(79,70,229,0.4); |
| }} |
| .error-msg {{ |
| color: #ef4444; |
| margin-top: 15px; |
| font-size: 0.9rem; |
| }} |
| .footer-text {{ |
| color: rgba(255,255,255,0.4); |
| font-size: 0.8rem; |
| margin-top: 30px; |
| text-align: center; |
| }} |
| </style> |
| </head> |
| <body> |
| <div class="login-container"> |
| <div class="login-card"> |
| <div class="login-icon">🔐</div> |
| <h1 class="text-center">Universal Model Trainer</h1> |
| <p class="subtitle text-center">Enter your password to access the training dashboard</p> |
| |
| <form id="loginForm"> |
| <div class="mb-3"> |
| <input |
| type="password" |
| class="form-control" |
| id="password" |
| placeholder="Enter password" |
| required |
| autofocus |
| autocomplete="current-password" |
| > |
| </div> |
| <button type="submit" class="btn btn-primary btn-login"> |
| <span id="btnText">Access Dashboard</span> |
| <span id="btnLoader" class="spinner-border spinner-border-sm" role="status" style="display:none;"></span> |
| </button> |
| {error_display} |
| </form> |
| |
| <p class="footer-text"> |
| Powered by HuggingFace 🤗 | Secure Session Authentication |
| </p> |
| </div> |
| </div> |
| |
| <script> |
| document.getElementById('loginForm').addEventListener('submit', async (e) => {{ |
| e.preventDefault(); |
| |
| const password = document.getElementById('password').value; |
| const errorMsg = document.getElementById('errorMsg'); |
| const btnText = document.getElementById('btnText'); |
| const btnLoader = document.getElementById('btnLoader'); |
| const submitBtn = e.target.querySelector('button[type="submit"]'); |
| |
| // Reset error state |
| errorMsg.style.display = 'none'; |
| |
| // Show loading state |
| btnText.textContent = 'Authenticating...'; |
| btnLoader.style.display = 'inline-block'; |
| submitBtn.disabled = true; |
| |
| try {{ |
| const formData = new FormData(); |
| formData.append('password', password); |
| |
| const response = await fetch('/api/auth/login', {{ |
| method: 'POST', |
| body: formData, |
| credentials: 'same-origin' |
| }}); |
| |
| const data = await response.json(); |
| |
| if (response.ok && data.success) {{ |
| btnText.textContent = 'Success!'; |
| // Wait for cookie to be fully set, then redirect |
| setTimeout(() => {{ |
| window.location.replace('/'); |
| }}, 200); |
| }} else {{ |
| throw new Error(data.detail || 'Authentication failed'); |
| }} |
| }} catch (error) {{ |
| errorMsg.textContent = 'Invalid password. Please try again.'; |
| errorMsg.style.display = 'block'; |
| btnText.textContent = 'Access Dashboard'; |
| btnLoader.style.display = 'none'; |
| submitBtn.disabled = false; |
| document.getElementById('password').select(); |
| }} |
| }}); |
| |
| // Focus password field |
| document.getElementById('password').focus(); |
| </script> |
| </body> |
| </html>''' |
|
|