diff --git a/server/auth.py b/server/auth.py new file mode 100644 index 0000000..abed911 --- /dev/null +++ b/server/auth.py @@ -0,0 +1,229 @@ +""" +JWT Authentication System for AI Server Admin +Phase 3: Security Enhancement +""" + +import jwt +import bcrypt +import secrets +from datetime import datetime, timedelta +from typing import Optional, Dict, Any +from fastapi import HTTPException, Depends, Request +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +import os + +# JWT Configuration +JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", secrets.token_urlsafe(32)) +JWT_ALGORITHM = "HS256" +JWT_EXPIRATION_HOURS = 24 +JWT_REMEMBER_DAYS = 30 + +# Security +security = HTTPBearer() + +# In-memory user store (in production, use a proper database) +USERS_DB = { + "admin": { + "username": "admin", + "password_hash": bcrypt.hashpw("admin123".encode('utf-8'), bcrypt.gensalt()).decode('utf-8'), + "role": "admin", + "created_at": datetime.now().isoformat(), + "last_login": None, + "login_attempts": 0, + "locked_until": None + }, + "hyungi": { + "username": "hyungi", + "password_hash": bcrypt.hashpw("hyungi123".encode('utf-8'), bcrypt.gensalt()).decode('utf-8'), + "role": "system", + "created_at": datetime.now().isoformat(), + "last_login": None, + "login_attempts": 0, + "locked_until": None + } +} + +# Login attempt tracking +LOGIN_ATTEMPTS = {} +MAX_LOGIN_ATTEMPTS = 5 +LOCKOUT_DURATION_MINUTES = 15 + +class AuthManager: + @staticmethod + def hash_password(password: str) -> str: + """Hash password using bcrypt""" + return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') + + @staticmethod + def verify_password(password: str, password_hash: str) -> bool: + """Verify password against hash""" + return bcrypt.checkpw(password.encode('utf-8'), password_hash.encode('utf-8')) + + @staticmethod + def create_jwt_token(user_data: Dict[str, Any], remember_me: bool = False) -> str: + """Create JWT token""" + expiration = datetime.utcnow() + timedelta( + days=JWT_REMEMBER_DAYS if remember_me else 0, + hours=JWT_EXPIRATION_HOURS if not remember_me else 0 + ) + + payload = { + "username": user_data["username"], + "role": user_data["role"], + "exp": expiration, + "iat": datetime.utcnow(), + "type": "remember" if remember_me else "session" + } + + return jwt.encode(payload, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM) + + @staticmethod + def verify_jwt_token(token: str) -> Dict[str, Any]: + """Verify and decode JWT token""" + try: + payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM]) + return payload + except jwt.ExpiredSignatureError: + raise HTTPException(status_code=401, detail="Token has expired") + except jwt.InvalidTokenError: + raise HTTPException(status_code=401, detail="Invalid token") + + @staticmethod + def is_account_locked(username: str) -> bool: + """Check if account is locked due to failed attempts""" + user = USERS_DB.get(username) + if not user: + return False + + if user["locked_until"]: + locked_until = datetime.fromisoformat(user["locked_until"]) + if datetime.now() < locked_until: + return True + else: + # Unlock account + user["locked_until"] = None + user["login_attempts"] = 0 + + return False + + @staticmethod + def record_login_attempt(username: str, success: bool, ip_address: str = None): + """Record login attempt""" + user = USERS_DB.get(username) + if not user: + return + + if success: + user["login_attempts"] = 0 + user["locked_until"] = None + user["last_login"] = datetime.now().isoformat() + else: + user["login_attempts"] += 1 + + # Lock account after max attempts + if user["login_attempts"] >= MAX_LOGIN_ATTEMPTS: + user["locked_until"] = ( + datetime.now() + timedelta(minutes=LOCKOUT_DURATION_MINUTES) + ).isoformat() + + @staticmethod + def authenticate_user(username: str, password: str) -> Optional[Dict[str, Any]]: + """Authenticate user credentials""" + user = USERS_DB.get(username) + if not user: + return None + + if AuthManager.is_account_locked(username): + raise HTTPException( + status_code=423, + detail=f"Account locked due to too many failed attempts. Try again later." + ) + + if AuthManager.verify_password(password, user["password_hash"]): + return user + + return None + +# Dependency functions +async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)): + """Get current authenticated user from JWT token""" + try: + payload = AuthManager.verify_jwt_token(credentials.credentials) + username = payload.get("username") + + user = USERS_DB.get(username) + if not user: + raise HTTPException(status_code=401, detail="User not found") + + return { + "username": user["username"], + "role": user["role"], + "token_type": payload.get("type", "session") + } + except Exception as e: + raise HTTPException(status_code=401, detail="Invalid authentication credentials") + +async def require_admin_role(current_user: dict = Depends(get_current_user)): + """Require admin or system role""" + if current_user["role"] not in ["admin", "system"]: + raise HTTPException(status_code=403, detail="Admin privileges required") + return current_user + +async def require_system_role(current_user: dict = Depends(get_current_user)): + """Require system role""" + if current_user["role"] != "system": + raise HTTPException(status_code=403, detail="System privileges required") + return current_user + +# Legacy API key support (for backward compatibility) +async def get_current_user_or_api_key( + request: Request, + credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), + x_api_key: Optional[str] = None +): + """Support both JWT and API key authentication""" + # Try JWT first + if credentials: + try: + return await get_current_user(credentials) + except HTTPException: + pass + + # Fall back to API key + api_key = x_api_key or request.headers.get("X-API-Key") + if api_key and api_key == os.getenv("API_KEY", "test-admin-key-123"): + return { + "username": "api_user", + "role": "system", + "token_type": "api_key" + } + + raise HTTPException(status_code=401, detail="Authentication required") + +# Audit logging +class AuditLogger: + @staticmethod + def log_login(username: str, success: bool, ip_address: str = None, user_agent: str = None): + """Log login attempt""" + log_entry = { + "timestamp": datetime.now().isoformat(), + "event": "login_attempt", + "username": username, + "success": success, + "ip_address": ip_address, + "user_agent": user_agent + } + print(f"AUDIT: {log_entry}") # In production, use proper logging + + @staticmethod + def log_admin_action(username: str, action: str, details: str = None, ip_address: str = None): + """Log admin action""" + log_entry = { + "timestamp": datetime.now().isoformat(), + "event": "admin_action", + "username": username, + "action": action, + "details": details, + "ip_address": ip_address + } + print(f"AUDIT: {log_entry}") # In production, use proper logging diff --git a/server/main.py b/server/main.py index f0a2a03..ff7cd76 100644 --- a/server/main.py +++ b/server/main.py @@ -746,3 +746,96 @@ async def admin_get_system_stats(api_key: str = Depends(require_api_key)): "timestamp": datetime.now().isoformat() } + +# Phase 3: JWT Authentication System +from .auth import AuthManager, AuditLogger, get_current_user, require_admin_role, require_system_role + +@app.get("/login", response_class=HTMLResponse) +async def login_page(request: Request): + """로그인 페이지""" + return templates.TemplateResponse("login.html", {"request": request}) + +@app.post("/admin/login") +async def admin_login(request: Request): + """JWT 기반 로그인""" + try: + data = await request.json() + username = data.get("username", "").strip() + password = data.get("password", "") + remember_me = data.get("remember_me", False) + + if not username or not password: + return {"success": False, "message": "Username and password are required"} + + # Get client IP + client_ip = request.client.host + user_agent = request.headers.get("user-agent", "") + + try: + # Authenticate user + user = AuthManager.authenticate_user(username, password) + + if user: + # Create JWT token + token = AuthManager.create_jwt_token(user, remember_me) + + # Record successful login + AuthManager.record_login_attempt(username, True, client_ip) + AuditLogger.log_login(username, True, client_ip, user_agent) + + return { + "success": True, + "message": "Login successful", + "token": token, + "user": { + "username": user["username"], + "role": user["role"] + } + } + else: + # Record failed login + AuthManager.record_login_attempt(username, False, client_ip) + AuditLogger.log_login(username, False, client_ip, user_agent) + + return {"success": False, "message": "Invalid username or password"} + + except HTTPException as e: + # Account locked + AuditLogger.log_login(username, False, client_ip, user_agent) + return {"success": False, "message": e.detail} + + except Exception as e: + return {"success": False, "message": "Login error occurred"} + +@app.get("/admin/verify-token") +async def verify_token(current_user: dict = Depends(get_current_user)): + """JWT 토큰 검증""" + return { + "valid": True, + "user": current_user + } + +@app.post("/admin/logout") +async def admin_logout(request: Request, current_user: dict = Depends(get_current_user)): + """로그아웃 (클라이언트에서 토큰 삭제)""" + client_ip = request.client.host + AuditLogger.log_admin_action( + current_user["username"], + "logout", + "User logged out", + client_ip + ) + + return {"success": True, "message": "Logged out successfully"} + +# Update existing admin routes to use JWT authentication +@app.get("/admin", response_class=HTMLResponse) +async def admin_dashboard(request: Request, current_user: dict = Depends(require_admin_role)): + """관리자 대시보드 페이지 (JWT 인증 필요)""" + return templates.TemplateResponse("admin.html", { + "request": request, + "server_port": settings.ai_server_port, + "ollama_host": settings.ollama_host, + "current_user": current_user + }) + diff --git a/static/admin.css b/static/admin.css index 9e71ee3..0930c65 100644 --- a/static/admin.css +++ b/static/admin.css @@ -74,6 +74,39 @@ body { opacity: 0.9; } +.user-menu { + display: flex; + align-items: center; + gap: 1rem; +} + +.user-info { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9rem; + opacity: 0.9; +} + +.logout-btn { + background: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + color: white; + padding: 0.4rem 0.8rem; + border-radius: 6px; + font-size: 0.8rem; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 0.3rem; +} + +.logout-btn:hover { + background: rgba(255, 255, 255, 0.3); + border-color: rgba(255, 255, 255, 0.5); +} + /* Main Content */ .admin-main { flex: 1; diff --git a/static/admin.js b/static/admin.js index e345b1f..ef8eb85 100644 --- a/static/admin.js +++ b/static/admin.js @@ -8,25 +8,40 @@ class AdminDashboard { } getApiKey() { - // 테스트 모드에서는 기본 API 키 사용 - let apiKey = localStorage.getItem('ai_admin_api_key'); - if (!apiKey) { - // 테스트 모드 기본 키 - apiKey = 'test-admin-key-123'; - localStorage.setItem('ai_admin_api_key', apiKey); - - // 사용자에게 알림 - setTimeout(() => { - alert('테스트 모드입니다.\nAPI Key: test-admin-key-123'); - }, 1000); + // JWT 토큰 사용 + const token = localStorage.getItem('ai_admin_token'); + console.log('Getting token:', token ? token.substring(0, 20) + '...' : 'No token found'); + if (!token) { + // 토큰이 없으면 로그인 페이지로 리다이렉트 + console.log('No token, redirecting to login...'); + window.location.href = '/login'; + return null; } - return apiKey; + return token; } async init() { + // 먼저 토큰 검증 + if (!this.apiKey) { + return; // getApiKey()에서 이미 리다이렉트됨 + } + + // 토큰 유효성 검증 + try { + await this.apiRequest('/admin/verify-token'); + console.log('Token verification successful'); + } catch (error) { + console.log('Token verification failed, redirecting to login'); + localStorage.removeItem('ai_admin_token'); + localStorage.removeItem('ai_admin_user'); + window.location.href = '/login'; + return; + } + this.updateCurrentTime(); setInterval(() => this.updateCurrentTime(), 1000); + await this.loadUserInfo(); // Phase 3: Load user info await this.loadSystemStatus(); await this.loadModels(); await this.loadApiKeys(); @@ -40,6 +55,28 @@ class AdminDashboard { }, 30000); } + // Phase 3: User Management + async loadUserInfo() { + try { + const userInfo = localStorage.getItem('ai_admin_user'); + if (userInfo) { + const user = JSON.parse(userInfo); + document.getElementById('username').textContent = user.username; + } else { + // Verify token and get user info + const response = await this.apiRequest('/admin/verify-token'); + if (response.valid) { + document.getElementById('username').textContent = response.user.username; + localStorage.setItem('ai_admin_user', JSON.stringify(response.user)); + } + } + } catch (error) { + console.error('Failed to load user info:', error); + // Token might be invalid, redirect to login + window.location.href = '/login'; + } + } + updateCurrentTime() { const now = new Date(); document.getElementById('current-time').textContent = @@ -58,18 +95,27 @@ class AdminDashboard { const defaultOptions = { headers: { 'Content-Type': 'application/json', - 'X-API-Key': this.apiKey + 'Authorization': `Bearer ${this.apiKey}` } }; + console.log('API Request:', endpoint, 'with token:', this.apiKey ? this.apiKey.substring(0, 20) + '...' : 'No token'); + try { const response = await fetch(url, { ...defaultOptions, ...options }); + console.log('API Response:', response.status, response.statusText); + if (!response.ok) { if (response.status === 401) { - localStorage.removeItem('ai_admin_api_key'); - location.reload(); + console.log('401 Unauthorized - clearing tokens and redirecting'); + // JWT 토큰이 만료되었거나 유효하지 않음 + localStorage.removeItem('ai_admin_token'); + localStorage.removeItem('ai_admin_user'); + window.location.href = '/login'; return; } + const errorText = await response.text(); + console.log('Error response:', errorText); throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); @@ -435,6 +481,23 @@ function closeModal(modalId) { admin.closeModal(modalId); } +// Phase 3: Logout function +async function logout() { + if (!confirm('Are you sure you want to logout?')) return; + + try { + // Call logout API + await admin.apiRequest('/admin/logout', { method: 'POST' }); + } catch (error) { + console.error('Logout API call failed:', error); + } finally { + // Clear local storage and redirect + localStorage.removeItem('ai_admin_token'); + localStorage.removeItem('ai_admin_user'); + window.location.href = '/login'; + } +} + // Initialize dashboard when page loads document.addEventListener('DOMContentLoaded', () => { admin = new AdminDashboard(); diff --git a/static/login.css b/static/login.css new file mode 100644 index 0000000..32ebe0c --- /dev/null +++ b/static/login.css @@ -0,0 +1,390 @@ +/* AI Server Admin Login Page CSS */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + position: relative; +} + +/* Background Animation */ +.bg-animation { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 0; +} + +.floating-icon { + position: absolute; + font-size: 2rem; + color: rgba(255, 255, 255, 0.1); + animation: float 8s ease-in-out infinite; + animation-delay: var(--delay, 0s); +} + +.floating-icon:nth-child(1) { + top: 20%; + left: 10%; +} + +.floating-icon:nth-child(2) { + top: 60%; + right: 15%; +} + +.floating-icon:nth-child(3) { + bottom: 30%; + left: 20%; +} + +.floating-icon:nth-child(4) { + top: 40%; + right: 30%; +} + +@keyframes float { + 0%, 100% { + transform: translateY(0px) rotate(0deg); + opacity: 0.1; + } + 50% { + transform: translateY(-20px) rotate(180deg); + opacity: 0.3; + } +} + +/* Login Container */ +.login-container { + position: relative; + z-index: 1; + width: 100%; + max-width: 400px; + padding: 2rem; +} + +.login-card { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border-radius: 20px; + padding: 2.5rem; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + animation: slideUp 0.6s ease-out; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(50px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Header */ +.login-header { + text-align: center; + margin-bottom: 2rem; +} + +.logo { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.logo i { + font-size: 2rem; + color: #667eea; +} + +.logo h1 { + font-size: 1.8rem; + font-weight: 600; + color: #2c3e50; +} + +.subtitle { + color: #7f8c8d; + font-size: 0.9rem; + font-weight: 500; +} + +/* Form Styles */ +.login-form { + margin-bottom: 1.5rem; +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; + font-weight: 500; + color: #2c3e50; + font-size: 0.9rem; +} + +.form-group label i { + color: #667eea; + width: 16px; +} + +.form-group input[type="text"], +.form-group input[type="password"] { + width: 100%; + padding: 0.75rem 1rem; + border: 2px solid #e1e8ed; + border-radius: 10px; + font-size: 1rem; + transition: all 0.3s ease; + background: white; +} + +.form-group input:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.password-input { + position: relative; +} + +.password-toggle { + position: absolute; + right: 1rem; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: #7f8c8d; + cursor: pointer; + padding: 0; + font-size: 1rem; + transition: color 0.2s ease; +} + +.password-toggle:hover { + color: #667eea; +} + +/* Checkbox */ +.checkbox-label { + display: flex !important; + align-items: center; + gap: 0.75rem; + cursor: pointer; + font-size: 0.9rem; + color: #7f8c8d; +} + +.checkbox-label input[type="checkbox"] { + display: none; +} + +.checkmark { + width: 18px; + height: 18px; + border: 2px solid #e1e8ed; + border-radius: 4px; + position: relative; + transition: all 0.3s ease; +} + +.checkbox-label input:checked + .checkmark { + background: #667eea; + border-color: #667eea; +} + +.checkbox-label input:checked + .checkmark::after { + content: '\f00c'; + font-family: 'Font Awesome 6 Free'; + font-weight: 900; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: white; + font-size: 0.7rem; +} + +/* Login Button */ +.login-btn { + width: 100%; + padding: 0.875rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 10px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +.login-btn:hover { + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3); +} + +.login-btn:active { + transform: translateY(0); +} + +.login-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +/* Loading State */ +.login-btn.loading { + pointer-events: none; +} + +.login-btn.loading i { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* Error Message */ +.error-message { + background: #fadbd8; + border: 1px solid #f1948a; + border-radius: 8px; + padding: 0.75rem; + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; + color: #e74c3c; + font-size: 0.9rem; + animation: shake 0.5s ease-in-out; +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 75% { transform: translateX(5px); } +} + +/* Security Info */ +.security-info { + display: flex; + justify-content: space-between; + margin-bottom: 1.5rem; + padding: 1rem; + background: rgba(102, 126, 234, 0.05); + border-radius: 8px; + border: 1px solid rgba(102, 126, 234, 0.1); +} + +.security-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; + font-size: 0.75rem; + color: #7f8c8d; + text-align: center; +} + +.security-item i { + color: #667eea; + font-size: 1rem; +} + +/* Footer */ +.login-footer { + text-align: center; + color: #95a5a6; + font-size: 0.8rem; +} + +.version-info { + margin-top: 0.5rem; + font-size: 0.75rem; + color: #bdc3c7; +} + +/* Responsive */ +@media (max-width: 480px) { + .login-container { + padding: 1rem; + } + + .login-card { + padding: 2rem; + } + + .security-info { + flex-direction: column; + gap: 1rem; + } + + .security-item { + flex-direction: row; + justify-content: center; + } + + .floating-icon { + display: none; + } +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .login-card { + background: rgba(44, 62, 80, 0.95); + color: #ecf0f1; + } + + .logo h1 { + color: #ecf0f1; + } + + .form-group label { + color: #ecf0f1; + } + + .form-group input { + background: rgba(52, 73, 94, 0.8); + border-color: #34495e; + color: #ecf0f1; + } + + .form-group input::placeholder { + color: #95a5a6; + } +} diff --git a/static/login.js b/static/login.js new file mode 100644 index 0000000..02cdca4 --- /dev/null +++ b/static/login.js @@ -0,0 +1,235 @@ +// AI Server Admin Login JavaScript + +class LoginManager { + constructor() { + this.baseUrl = window.location.origin; + this.init(); + } + + init() { + // Check if already logged in + this.checkExistingAuth(); + + // Setup form submission + document.getElementById('login-form').addEventListener('submit', (e) => { + e.preventDefault(); + this.handleLogin(); + }); + + // Setup enter key handling + document.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + this.handleLogin(); + } + }); + + // Auto-focus username field + document.getElementById('username').focus(); + } + + async checkExistingAuth() { + const token = localStorage.getItem('ai_admin_token'); + if (token) { + try { + console.log('Checking existing token...'); + // Verify token is still valid + const response = await fetch(`${this.baseUrl}/admin/verify-token`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.ok) { + console.log('Token is valid, redirecting to admin...'); + // Token is valid, redirect to admin + window.location.href = '/admin'; + return; + } else { + console.log('Token verification failed with status:', response.status); + } + } catch (error) { + console.log('Token verification failed:', error); + } + + // Token is invalid, remove it + console.log('Removing invalid token...'); + localStorage.removeItem('ai_admin_token'); + localStorage.removeItem('ai_admin_user'); + } + } + + async handleLogin() { + const username = document.getElementById('username').value.trim(); + const password = document.getElementById('password').value; + const rememberMe = document.getElementById('remember-me').checked; + + // Validation + if (!username || !password) { + this.showError('Please enter both username and password'); + return; + } + + // Show loading state + this.setLoading(true); + this.hideError(); + + try { + const response = await fetch(`${this.baseUrl}/admin/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + username, + password, + remember_me: rememberMe + }) + }); + + const data = await response.json(); + + if (response.ok && data.success) { + // Store JWT token + localStorage.setItem('ai_admin_token', data.token); + + // Store user info + localStorage.setItem('ai_admin_user', JSON.stringify(data.user)); + + console.log('Token stored:', data.token.substring(0, 20) + '...'); + console.log('User stored:', data.user); + + // Show success message + this.showSuccess('Login successful! Redirecting...'); + + // Redirect after short delay + setTimeout(() => { + window.location.href = '/admin'; + }, 1000); + + } else { + this.showError(data.message || 'Login failed. Please check your credentials.'); + } + + } catch (error) { + console.error('Login error:', error); + this.showError('Connection error. Please try again.'); + } finally { + this.setLoading(false); + } + } + + setLoading(loading) { + const btn = document.getElementById('login-btn'); + const icon = btn.querySelector('i'); + + if (loading) { + btn.disabled = true; + btn.classList.add('loading'); + icon.className = 'fas fa-spinner'; + btn.querySelector('span') ? + btn.querySelector('span').textContent = 'Signing In...' : + btn.innerHTML = ' Signing In...'; + } else { + btn.disabled = false; + btn.classList.remove('loading'); + icon.className = 'fas fa-sign-in-alt'; + btn.innerHTML = ' Sign In'; + } + } + + showError(message) { + const errorDiv = document.getElementById('error-message'); + const errorText = document.getElementById('error-text'); + + errorText.textContent = message; + errorDiv.style.display = 'flex'; + + // Auto-hide after 5 seconds + setTimeout(() => { + this.hideError(); + }, 5000); + } + + hideError() { + document.getElementById('error-message').style.display = 'none'; + } + + showSuccess(message) { + // Create success message element if it doesn't exist + let successDiv = document.getElementById('success-message'); + if (!successDiv) { + successDiv = document.createElement('div'); + successDiv.id = 'success-message'; + successDiv.className = 'success-message'; + successDiv.innerHTML = ` + + ${message} + `; + + // Add CSS for success message + const style = document.createElement('style'); + style.textContent = ` + .success-message { + background: #d5f4e6; + border: 1px solid #27ae60; + border-radius: 8px; + padding: 0.75rem; + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; + color: #27ae60; + font-size: 0.9rem; + animation: slideDown 0.3s ease-out; + } + + @keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + `; + document.head.appendChild(style); + + // Insert before error message + const errorDiv = document.getElementById('error-message'); + errorDiv.parentNode.insertBefore(successDiv, errorDiv); + } else { + document.getElementById('success-text').textContent = message; + successDiv.style.display = 'flex'; + } + } +} + +// Password toggle functionality +function togglePassword() { + const passwordInput = document.getElementById('password'); + const passwordEye = document.getElementById('password-eye'); + + if (passwordInput.type === 'password') { + passwordInput.type = 'text'; + passwordEye.className = 'fas fa-eye-slash'; + } else { + passwordInput.type = 'password'; + passwordEye.className = 'fas fa-eye'; + } +} + +// Initialize login manager when page loads +document.addEventListener('DOMContentLoaded', () => { + new LoginManager(); +}); + +// Security: Clear sensitive data on page unload +window.addEventListener('beforeunload', () => { + // Clear password field + const passwordField = document.getElementById('password'); + if (passwordField) { + passwordField.value = ''; + } +}); diff --git a/templates/admin.html b/templates/admin.html index 066d855..58e13cc 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -18,6 +18,16 @@ Online +
diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..5b413a1 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,124 @@ + + + + + +Secure Access Portal
+