Spaces:
Running
Running
| """Simple session-based authentication.""" | |
| import os | |
| import secrets | |
| import time | |
| from fastapi import Request | |
| # Session storage: token -> expiry timestamp | |
| _sessions = {} | |
| SESSION_COOKIE = "session" | |
| SESSION_MAX_AGE = 86400 * 30 # 7 days | |
| def get_credentials(): | |
| username = os.environ.get("USERNAME", "") | |
| password = os.environ.get("PASSWORD", "") | |
| return username, password | |
| def verify_credentials(username: str, password: str) -> bool: | |
| correct_user, correct_pass = get_credentials() | |
| if not correct_user or not correct_pass: | |
| # No credentials configured: allow all | |
| return True | |
| return ( | |
| secrets.compare_digest(username, correct_user) | |
| and secrets.compare_digest(password, correct_pass) | |
| ) | |
| def auth_enabled() -> bool: | |
| u, p = get_credentials() | |
| return bool(u and p) | |
| def create_session() -> str: | |
| token = secrets.token_urlsafe(32) | |
| _sessions[token] = time.time() + SESSION_MAX_AGE | |
| # Cleanup old sessions | |
| now = time.time() | |
| expired = [k for k, v in _sessions.items() if v < now] | |
| for k in expired: | |
| del _sessions[k] | |
| return token | |
| def validate_session(token: str) -> bool: | |
| if not token: | |
| return False | |
| expiry = _sessions.get(token) | |
| if expiry is None: | |
| return False | |
| if time.time() > expiry: | |
| del _sessions[token] | |
| return False | |
| return True | |
| def is_authenticated(request: Request) -> bool: | |
| if not auth_enabled(): | |
| return True | |
| token = request.cookies.get(SESSION_COOKIE, "") | |
| return validate_session(token) | |
| def login_page_html(error: str = "") -> str: | |
| error_html = f'<div class="login-error">{error}</div>' if error else '' | |
| return f'''<!DOCTYPE html> | |
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>NAI Studio - 登录</title> | |
| <style> | |
| * {{ box-sizing: border-box; margin: 0; padding: 0; }} | |
| body {{ | |
| font-family: 'Segoe UI', 'Noto Sans SC', sans-serif; | |
| background: #1a1a2e; | |
| color: #e0e0e0; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| min-height: 100vh; | |
| }} | |
| .login-card {{ | |
| background: #16213e; | |
| border: 1px solid #2a3456; | |
| border-radius: 12px; | |
| padding: 40px 36px; | |
| width: 100%; | |
| max-width: 380px; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.4); | |
| }} | |
| .login-card h1 {{ | |
| text-align: center; | |
| font-size: 1.5rem; | |
| color: #6c63ff; | |
| margin-bottom: 8px; | |
| }} | |
| .login-card .subtitle {{ | |
| text-align: center; | |
| font-size: 0.8rem; | |
| color: #6b7394; | |
| margin-bottom: 28px; | |
| }} | |
| .login-field {{ | |
| margin-bottom: 16px; | |
| }} | |
| .login-field label {{ | |
| display: block; | |
| font-size: 0.8rem; | |
| color: #a0a8c0; | |
| margin-bottom: 6px; | |
| font-weight: 600; | |
| }} | |
| .login-field input {{ | |
| width: 100%; | |
| padding: 10px 14px; | |
| background: #1a2340; | |
| border: 1px solid #2a3456; | |
| border-radius: 8px; | |
| color: #e0e0e0; | |
| font-size: 0.95rem; | |
| outline: none; | |
| transition: border-color 0.2s; | |
| }} | |
| .login-field input:focus {{ | |
| border-color: #6c63ff; | |
| box-shadow: 0 0 0 2px rgba(108,99,255,0.3); | |
| }} | |
| .login-btn {{ | |
| width: 100%; | |
| padding: 12px; | |
| background: #6c63ff; | |
| color: #fff; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: background 0.2s; | |
| margin-top: 8px; | |
| }} | |
| .login-btn:hover {{ background: #5a52d5; }} | |
| .login-error {{ | |
| background: rgba(244,67,54,0.1); | |
| border: 1px solid rgba(244,67,54,0.3); | |
| color: #f44336; | |
| padding: 8px 12px; | |
| border-radius: 6px; | |
| font-size: 0.85rem; | |
| margin-bottom: 16px; | |
| text-align: center; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="login-card"> | |
| <h1>NAI Studio</h1> | |
| <div class="subtitle">请登录以继续</div> | |
| {error_html} | |
| <form method="POST" action="/login"> | |
| <div class="login-field"> | |
| <label for="username">用户名</label> | |
| <input type="text" id="username" name="username" required autofocus> | |
| </div> | |
| <div class="login-field"> | |
| <label for="password">密码</label> | |
| <input type="password" id="password" name="password" required> | |
| </div> | |
| <button type="submit" class="login-btn">登录</button> | |
| </form> | |
| </div> | |
| </body> | |
| </html>''' |