Merge pull request 'feat: Add optional web interface authentication with session management' (#58) from web-interface-password into dev
Local Docker Build (Dev) / build-dev (push) Successful in 4s

Reviewed-on: #58
This commit was merged in pull request #58.
This commit is contained in:
2025-10-20 16:24:17 -04:00
11 changed files with 570 additions and 17 deletions
+9
View File
@@ -135,6 +135,15 @@ PATH_DEBUG=false
# Suppress TVDB API warnings (true/false) - TVDB failures are common and non-critical # Suppress TVDB API warnings (true/false) - TVDB failures are common and non-critical
SUPPRESS_TVDB_WARNINGS=true SUPPRESS_TVDB_WARNINGS=true
# ===========================================
# WEB INTERFACE AUTHENTICATION
# ===========================================
# Enable web interface authentication (default: false)
WEB_AUTH_ENABLED=false
# Session timeout in seconds (default: 3600 = 1 hour)
WEB_AUTH_SESSION_TIMEOUT=3600
# =========================================== # ===========================================
# SERVER CONFIGURATION # SERVER CONFIGURATION
# =========================================== # ===========================================
+9 -1
View File
@@ -38,4 +38,12 @@ JELLYSEERR_API_KEY=your_jellyseerr_api_key
# TVDB API key (RECOMMENDED for v0.6.1+ Emby TV NFO Compatibility) # TVDB API key (RECOMMENDED for v0.6.1+ Emby TV NFO Compatibility)
# Required to convert IMDB IDs to TVDB IDs for proper Emby metadata loading # Required to convert IMDB IDs to TVDB IDs for proper Emby metadata loading
# Get free API key at: https://thetvdb.com/api-information # Get free API key at: https://thetvdb.com/api-information
TVDB_API_KEY=your_tvdb_api_key TVDB_API_KEY=your_tvdb_api_key
# ===========================================
# WEB INTERFACE AUTHENTICATION (OPTIONAL)
# ===========================================
# Web interface login credentials (only used if WEB_AUTH_ENABLED=true in .env)
# Set WEB_AUTH_ENABLED=true in .env file to enable authentication
WEB_AUTH_USERNAME=admin
WEB_AUTH_PASSWORD=your_secure_web_password
+9 -15
View File
@@ -8,17 +8,11 @@
--- ---
> **⚠️ ALPHA SOFTWARE NOTICE ⚠️**
>
> NFOGuard is currently in **Alpha** stage. While functional, it may have bugs or missing features.
>
> **🔌 Emby Plugin Included**: The Emby companion plugin is now bundled directly into the Docker image — no extra steps required. > **🔌 Emby Plugin Included**: The Emby companion plugin is now bundled directly into the Docker image — no extra steps required.
> >
> **💬 Community Feedback**: Join our Discord if youd like to share feedback, test new features early, or discuss improvements with other users: > **💬 Community Feedback**: Join our Discord if you'd like to share feedback, test new features, or discuss improvements with other users:
> >
> **[Join Discord: https://discord.gg/bbD9Pmtr](https://discord.gg/bbD9Pmtr)** > **[Join Discord: https://discord.gg/ZykJRGt72b](https://discord.gg/ZykJRGt72b)**
>
> *If the Discord link has expired, please [open an issue](https://github.com/sbcrumb/NFOguard/issues) and we'll provide an updated link.*
--- ---
@@ -38,7 +32,7 @@ NFOGuard automatically updates movie and TV show NFO files with proper release d
- **Batch Processing** - Efficient handling of multiple files simultaneously - **Batch Processing** - Efficient handling of multiple files simultaneously
- **PostgreSQL Database** - Production-ready database with optimized queries - **PostgreSQL Database** - Production-ready database with optimized queries
- **Smart Skip Logic** - Database-first checking eliminates expensive filesystem scans - **Smart Skip Logic** - Database-first checking eliminates expensive filesystem scans
- **88% Scan Optimization** - TV library scans reduced from hours to minutes - **88% Scan Optimization** - Subsequent scans reduced from hours to minutes via smart skip logic
### **Web Interface & Management** ### **Web Interface & Management**
- **Complete Web UI** - Episode and movie management with filtering and search - **Complete Web UI** - Episode and movie management with filtering and search
@@ -64,9 +58,9 @@ NFOGuard automatically updates movie and TV show NFO files with proper release d
### 1. Download Configuration Files ### 1. Download Configuration Files
```bash ```bash
wget https://raw.githubusercontent.com/sbcrumb/NFOguard/main/.env.template wget https://raw.githubusercontent.com/sbcrumb/nfoguard/main/.env.template
wget https://raw.githubusercontent.com/sbcrumb/NFOguard/main/.env.secrets.template wget https://raw.githubusercontent.com/sbcrumb/nfoguard/main/.env.secrets.template
wget https://raw.githubusercontent.com/sbcrumb/NFOguard/main/docker-compose.example.yml wget https://raw.githubusercontent.com/sbcrumb/nfoguard/main/docker-compose.example.yml
``` ```
### 2. Configure Environment ### 2. Configure Environment
@@ -149,10 +143,10 @@ image: sbcrumb/nfoguard:dev
### Specific Version ### Specific Version
```yaml ```yaml
image: sbcrumb/nfoguard:v2.0.0 # Latest with monitoring & validation image: sbcrumb/nfoguard:v2.6.7 # Latest with PostgreSQL & optimization
``` ```
> **🚀 Version 2.0.0** includes major architecture improvements, async I/O performance enhancements, comprehensive monitoring, and configuration validation. > **🚀 Version 2.6.7** includes major architecture improvements, PostgreSQL database migration, 88% scan optimization, async I/O performance enhancements, comprehensive monitoring, and configuration validation.
## 🔗 Webhook Setup ## 🔗 Webhook Setup
@@ -499,7 +493,7 @@ NFOGuard will ignore:
## 🆘 Support ## 🆘 Support
- **Issues**: [GitHub Issues](https://github.com/sbcrumb/NFOguard/issues) - **Issues**: [GitHub Issues](https://github.com/sbcrumb/nfoguard/issues)
- **Documentation**: See `SETUP.md` for detailed instructions - **Documentation**: See `SETUP.md` for detailed instructions
- **Docker Hub**: [`sbcrumb/nfoguard`](https://hub.docker.com/r/sbcrumb/nfoguard) - **Docker Hub**: [`sbcrumb/nfoguard`](https://hub.docker.com/r/sbcrumb/nfoguard)
+178
View File
@@ -0,0 +1,178 @@
"""
Simple authentication middleware for NFOGuard web interface
Provides basic HTTP auth and session management for web interface protection
"""
import secrets
import hashlib
from datetime import datetime, timedelta
from typing import Optional, Dict, Any
from fastapi import HTTPException, status, Request, Response
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from starlette.middleware.base import BaseHTTPMiddleware
class AuthSession:
"""Simple session management for web interface"""
def __init__(self, timeout_seconds: int = 3600):
self.sessions: Dict[str, Dict[str, Any]] = {}
self.timeout_seconds = timeout_seconds
def create_session(self, username: str) -> str:
"""Create a new session and return session token"""
session_token = secrets.token_urlsafe(32)
self.sessions[session_token] = {
"username": username,
"created_at": datetime.utcnow(),
"last_activity": datetime.utcnow()
}
return session_token
def validate_session(self, session_token: str) -> bool:
"""Validate session token and update last activity"""
if not session_token or session_token not in self.sessions:
return False
session = self.sessions[session_token]
now = datetime.utcnow()
# Check if session expired
if (now - session["last_activity"]).seconds > self.timeout_seconds:
del self.sessions[session_token]
return False
# Update last activity
session["last_activity"] = now
return True
def get_session_user(self, session_token: str) -> Optional[str]:
"""Get username from valid session"""
if self.validate_session(session_token):
return self.sessions[session_token]["username"]
return None
def delete_session(self, session_token: str) -> None:
"""Delete a session (logout)"""
if session_token in self.sessions:
del self.sessions[session_token]
def cleanup_expired_sessions(self) -> None:
"""Remove expired sessions"""
now = datetime.utcnow()
expired_tokens = []
for token, session in self.sessions.items():
if (now - session["last_activity"]).seconds > self.timeout_seconds:
expired_tokens.append(token)
for token in expired_tokens:
del self.sessions[token]
class SimpleAuthMiddleware(BaseHTTPMiddleware):
"""Simple authentication middleware for web interface routes"""
def __init__(self, app, config):
super().__init__(app)
self.config = config
self.session_manager = AuthSession(config.web_auth_session_timeout)
self.security = HTTPBasic()
# Routes that require authentication (web interface)
self.protected_routes = [
"/", # Main web interface
"/static/", # Static files (CSS, JS)
"/api/movies", # Web API endpoints
"/api/series",
"/api/episodes",
"/api/dashboard",
"/database/" # Database management endpoints
]
# Routes that are always public (webhooks, health checks)
self.public_routes = [
"/webhook/",
"/health",
"/ping",
"/api/v1/health",
"/api/v1/metrics"
]
async def dispatch(self, request: Request, call_next):
"""Process request through authentication middleware"""
# Skip authentication if disabled
if not self.config.web_auth_enabled:
return await call_next(request)
# Check if route requires authentication
path = request.url.path
needs_auth = any(path.startswith(route) for route in self.protected_routes)
is_public = any(path.startswith(route) for route in self.public_routes)
if is_public or not needs_auth:
return await call_next(request)
# Check for existing session
session_token = request.cookies.get("nfoguard_session")
if session_token and self.session_manager.validate_session(session_token):
# Valid session, proceed
return await call_next(request)
# Check for HTTP Basic Auth
auth_header = request.headers.get("authorization")
if auth_header and auth_header.startswith("Basic "):
credentials = self._parse_basic_auth(auth_header)
if credentials and self._validate_credentials(credentials.username, credentials.password):
# Create session for successful login
session_token = self.session_manager.create_session(credentials.username)
response = await call_next(request)
response.set_cookie(
key="nfoguard_session",
value=session_token,
max_age=self.config.web_auth_session_timeout,
httponly=True,
secure=False # Set to True if using HTTPS
)
return response
# Authentication required
return self._auth_required_response()
def _parse_basic_auth(self, auth_header: str) -> Optional[HTTPBasicCredentials]:
"""Parse HTTP Basic Auth header"""
try:
import base64
encoded_credentials = auth_header.split(" ")[1]
decoded_credentials = base64.b64decode(encoded_credentials).decode('utf-8')
username, password = decoded_credentials.split(":", 1)
return HTTPBasicCredentials(username=username, password=password)
except Exception:
return None
def _validate_credentials(self, username: str, password: str) -> bool:
"""Validate username and password"""
return (username == self.config.web_auth_username and
password == self.config.web_auth_password)
def _auth_required_response(self) -> Response:
"""Return 401 response with WWW-Authenticate header"""
return Response(
content="Authentication required",
status_code=status.HTTP_401_UNAUTHORIZED,
headers={"WWW-Authenticate": "Basic realm=\"NFOGuard Web Interface\""}
)
def create_auth_dependencies(config) -> Dict[str, Any]:
"""Create authentication-related dependencies for dependency injection"""
session_manager = AuthSession(config.web_auth_session_timeout)
return {
"session_manager": session_manager,
"auth_enabled": config.web_auth_enabled,
"auth_config": {
"username": config.web_auth_username,
"timeout": config.web_auth_session_timeout
}
}
+33 -1
View File
@@ -7,7 +7,7 @@ import requests
import asyncio import asyncio
from pathlib import Path from pathlib import Path
from datetime import datetime, timezone from datetime import datetime, timezone
from fastapi import HTTPException, BackgroundTasks, Request from fastapi import HTTPException, BackgroundTasks, Request, Response
from typing import Optional from typing import Optional
# Import models # Import models
@@ -1396,6 +1396,38 @@ def register_routes(app, dependencies: dict):
"""Update episode dateadded""" """Update episode dateadded"""
return await update_episode_date(dependencies, imdb_id, season, episode, request.dateadded, request.source) return await update_episode_date(dependencies, imdb_id, season, episode, request.dateadded, request.source)
@app.post("/api/auth/logout")
async def _logout(request: Request, response: Response):
"""Logout endpoint - clears session"""
session_manager = dependencies.get("session_manager")
if session_manager:
session_token = request.cookies.get("nfoguard_session")
if session_token:
session_manager.delete_session(session_token)
response.delete_cookie("nfoguard_session")
return {"status": "logged_out", "message": "Session cleared"}
@app.get("/api/auth/status")
async def _auth_status(request: Request):
"""Check authentication status"""
auth_enabled = dependencies.get("auth_enabled", False)
if not auth_enabled:
return {"authenticated": True, "auth_enabled": False, "message": "Authentication disabled"}
session_manager = dependencies.get("session_manager")
if not session_manager:
return {"authenticated": False, "auth_enabled": True, "message": "Session manager not available"}
session_token = request.cookies.get("nfoguard_session")
if session_token:
username = session_manager.get_session_user(session_token)
if username:
return {"authenticated": True, "auth_enabled": True, "username": username}
return {"authenticated": False, "auth_enabled": True, "message": "Not authenticated"}
@app.post("/api/bulk/update-source") @app.post("/api/bulk/update-source")
async def _bulk_update_source(request: BulkUpdateRequest): async def _bulk_update_source(request: BulkUpdateRequest):
"""Bulk update source for movies or episodes""" """Bulk update source for movies or episodes"""
+10
View File
@@ -79,6 +79,9 @@ class NFOGuardConfig:
# TV processing # TV processing
self._load_tv_settings() self._load_tv_settings()
# Web interface authentication
self._load_auth_settings()
def _load_paths(self) -> None: def _load_paths(self) -> None:
"""Load and validate path configuration""" """Load and validate path configuration"""
@@ -156,6 +159,13 @@ class NFOGuardConfig:
self.tv_season_dir_pattern = os.environ.get("TV_SEASON_DIR_PATTERN", "season ").lower() self.tv_season_dir_pattern = os.environ.get("TV_SEASON_DIR_PATTERN", "season ").lower()
self.tv_webhook_processing_mode = os.environ.get("TV_WEBHOOK_PROCESSING_MODE", "targeted").lower() self.tv_webhook_processing_mode = os.environ.get("TV_WEBHOOK_PROCESSING_MODE", "targeted").lower()
def _load_auth_settings(self) -> None:
"""Load web interface authentication settings"""
self.web_auth_enabled = _bool_env("WEB_AUTH_ENABLED", False)
self.web_auth_username = os.environ.get("WEB_AUTH_USERNAME", "admin")
self.web_auth_password = os.environ.get("WEB_AUTH_PASSWORD", "")
self.web_auth_session_timeout = self._get_int_env("WEB_AUTH_SESSION_TIMEOUT", 3600, 300, 86400) # 1 hour default, 5min-24h range
def _get_int_env(self, name: str, default: int, min_val: int, max_val: int) -> int: def _get_int_env(self, name: str, default: int, min_val: int, max_val: int) -> int:
"""Get integer environment variable with validation""" """Get integer environment variable with validation"""
value_str = os.environ.get(name) value_str = os.environ.get(name)
+201
View File
@@ -0,0 +1,201 @@
#!/usr/bin/env python3
"""
Debug script to check specific TV series/episode data in NFOGuard database
"""
import os
import sys
from pathlib import Path
# Add the project root to the path
sys.path.insert(0, str(Path(__file__).parent))
from core.database import NFOGuardDatabase
def debug_series(imdb_id: str):
"""Debug a specific TV series' data"""
print(f"📺 DEBUG TV SERIES: {imdb_id}")
print("=" * 60)
# Initialize database
db = NFOGuardDatabase()
# Get series episodes
episodes = db.get_series_episodes(imdb_id)
if not episodes:
print(f"❌ TV series {imdb_id} not found in database")
return
print(f"📊 SERIES OVERVIEW:")
print(f" IMDb ID: {imdb_id}")
print(f" Total Episodes: {len(episodes)}")
# Count episodes by status
with_dates = sum(1 for ep in episodes if ep.get('dateadded'))
without_dates = len(episodes) - with_dates
with_video = sum(1 for ep in episodes if ep.get('has_video_file'))
print(f" Episodes with dates: {with_dates}")
print(f" Episodes without dates: {without_dates}")
print(f" Episodes with video files: {with_video}")
# Group by season
seasons = {}
for ep in episodes:
season = ep.get('season', 'Unknown')
if season not in seasons:
seasons[season] = []
seasons[season].append(ep)
print(f" Seasons: {len(seasons)} ({', '.join(f'S{s}' if isinstance(s, int) else str(s) for s in sorted(seasons.keys()))})")
# Show sources breakdown
sources = {}
for ep in episodes:
source = ep.get('source', 'None')
sources[source] = sources.get(source, 0) + 1
print(f"\n📈 SOURCES BREAKDOWN:")
for source, count in sorted(sources.items(), key=lambda x: x[1], reverse=True):
print(f" {source}: {count} episodes")
# Show recent episodes (last 10 by date added)
episodes_with_dates = [ep for ep in episodes if ep.get('dateadded')]
recent_episodes = sorted(episodes_with_dates, key=lambda x: x.get('last_updated', ''), reverse=True)[:10]
if recent_episodes:
print(f"\n🕒 RECENT EPISODES (by last_updated):")
for ep in recent_episodes:
season = ep.get('season', '?')
episode = ep.get('episode', '?')
dateadded = ep.get('dateadded', 'None')
source = ep.get('source', 'None')
video = "" if ep.get('has_video_file') else ""
print(f" S{season:02d}E{episode:02d}: {dateadded} | {source} | Video: {video}")
def debug_episode(imdb_id: str, season: int, episode: int):
"""Debug a specific episode's data"""
print(f"📺 DEBUG TV EPISODE: {imdb_id} S{season:02d}E{episode:02d}")
print("=" * 60)
# Initialize database
db = NFOGuardDatabase()
# Get specific episode
episode_data = db.get_episode_date(imdb_id, season, episode)
if not episode_data:
print(f"❌ Episode S{season:02d}E{episode:02d} for series {imdb_id} not found in database")
return
print("📊 RAW EPISODE DATA:")
for key, value in episode_data.items():
print(f" {key}: {repr(value)}")
print("\n📺 FORMATTED EPISODE DATA:")
print(f" Series IMDb: {episode_data.get('imdb_id', 'Unknown')}")
print(f" Season/Episode: S{episode_data.get('season', '?'):02d}E{episode_data.get('episode', '?'):02d}")
print(f" Title: {episode_data.get('title', 'Unknown')}")
print(f" Air Date: {episode_data.get('air_date', 'None')}")
print(f" Date Added: {episode_data.get('dateadded', 'None')}")
print(f" Source: {episode_data.get('source', 'None')}")
print(f" Has Video: {episode_data.get('has_video_file', False)}")
print(f" Video Path: {episode_data.get('video_path', 'None')}")
print(f" Last Updated: {episode_data.get('last_updated', 'None')}")
# Check if air date is valid
air_date = episode_data.get('air_date')
if air_date and air_date.strip():
try:
from datetime import datetime
test_date = f"{air_date}T00:00:00"
parsed = datetime.fromisoformat(test_date.replace('Z', '+00:00'))
print(f"\n✅ Air date is valid: {parsed}")
except Exception as e:
print(f"\n❌ Air date is INVALID: {e}")
else:
print(f"\n⚠️ Air date is empty or None")
# Check if dateadded is valid
dateadded = episode_data.get('dateadded')
if dateadded and dateadded.strip():
try:
from datetime import datetime
if isinstance(dateadded, str):
test_date = f"{dateadded}T00:00:00" if 'T' not in dateadded else dateadded
parsed = datetime.fromisoformat(test_date.replace('Z', '+00:00'))
else:
parsed = dateadded
print(f"✅ Date added is valid: {parsed}")
except Exception as e:
print(f"❌ Date added is INVALID: {e}")
else:
print(f"⚠️ Date added is empty or None")
def debug_season(imdb_id: str, season: int):
"""Debug all episodes in a specific season"""
print(f"📺 DEBUG TV SEASON: {imdb_id} Season {season}")
print("=" * 60)
# Initialize database
db = NFOGuardDatabase()
# Get series episodes
all_episodes = db.get_series_episodes(imdb_id)
season_episodes = [ep for ep in all_episodes if ep.get('season') == season]
if not season_episodes:
print(f"❌ No episodes found for season {season} of series {imdb_id}")
return
print(f"📊 SEASON {season} OVERVIEW:")
print(f" Total Episodes: {len(season_episodes)}")
# Sort by episode number
season_episodes.sort(key=lambda x: x.get('episode', 0))
with_dates = sum(1 for ep in season_episodes if ep.get('dateadded'))
without_dates = len(season_episodes) - with_dates
with_video = sum(1 for ep in season_episodes if ep.get('has_video_file'))
print(f" Episodes with dates: {with_dates}")
print(f" Episodes without dates: {without_dates}")
print(f" Episodes with video files: {with_video}")
print(f"\n📋 EPISODE LIST:")
for ep in season_episodes:
episode_num = ep.get('episode', '?')
title = ep.get('title', 'Unknown')[:30] + ('...' if len(ep.get('title', '')) > 30 else '')
dateadded = ep.get('dateadded', 'None')
source = ep.get('source', 'None')
video = "" if ep.get('has_video_file') else ""
air_date = ep.get('air_date', 'None')
print(f" E{episode_num:02d}: {title:<33} | Added: {dateadded} | Air: {air_date} | Video: {video}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage:")
print(" python debug_tv.py <imdb_id> # Debug entire series")
print(" python debug_tv.py <imdb_id> <season> # Debug specific season")
print(" python debug_tv.py <imdb_id> <season> <episode> # Debug specific episode")
print("\nExamples:")
print(" python debug_tv.py tt0121955 # Debug South Park")
print(" python debug_tv.py tt0121955 27 # Debug South Park Season 27")
print(" python debug_tv.py tt0121955 27 6 # Debug South Park S27E06")
sys.exit(1)
imdb_id = sys.argv[1]
if not imdb_id.startswith('tt'):
imdb_id = f'tt{imdb_id}'
if len(sys.argv) == 4:
# Debug specific episode
season = int(sys.argv[2])
episode = int(sys.argv[3])
debug_episode(imdb_id, season, episode)
elif len(sys.argv) == 3:
# Debug specific season
season = int(sys.argv[2])
debug_season(imdb_id, season)
else:
# Debug entire series
debug_series(imdb_id)
+14
View File
@@ -15,6 +15,9 @@ from fastapi import FastAPI
# Import configuration first # Import configuration first
from config.settings import config from config.settings import config
# Import authentication
from api.auth import SimpleAuthMiddleware, create_auth_dependencies
from utils.logging import _log from utils.logging import _log
# Import core components # Import core components
@@ -172,6 +175,17 @@ def main():
# Initialize components # Initialize components
dependencies = initialize_components() dependencies = initialize_components()
# Add authentication dependencies
auth_deps = create_auth_dependencies(config)
dependencies.update(auth_deps)
# Add authentication middleware if enabled
if config.web_auth_enabled:
app.add_middleware(SimpleAuthMiddleware, config=config)
_log("INFO", f"Web authentication enabled for user: {config.web_auth_username}")
else:
_log("INFO", "Web authentication disabled - web interface is public")
# Store dependencies globally for signal handler access # Store dependencies globally for signal handler access
signal_handler.dependencies = dependencies signal_handler.dependencies = dependencies
+40
View File
@@ -39,6 +39,7 @@ body {
color: white; color: white;
padding: 1rem 0; padding: 1rem 0;
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
position: relative;
} }
.header-content { .header-content {
@@ -63,6 +64,45 @@ body {
font-size: 1rem; font-size: 1rem;
} }
/* Authentication Status */
.auth-status {
position: absolute;
top: 1rem;
right: 1rem;
display: flex;
align-items: center;
gap: 1rem;
color: white;
font-size: 0.9rem;
}
.auth-user {
display: flex;
align-items: center;
gap: 0.5rem;
opacity: 0.9;
}
.auth-logout {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
padding: 0.5rem 1rem;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s ease;
}
.auth-logout:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.5);
transform: translateY(-1px);
}
.nav-tabs { .nav-tabs {
max-width: 1200px; max-width: 1200px;
margin: 1rem auto 0; margin: 1rem auto 0;
+8
View File
@@ -15,6 +15,14 @@
<h1><i class="fas fa-shield-alt"></i> NFOGuard</h1> <h1><i class="fas fa-shield-alt"></i> NFOGuard</h1>
<p>Database Management & Reporting</p> <p>Database Management & Reporting</p>
</div> </div>
<div class="auth-status" id="auth-status" style="display: none;">
<span class="auth-user">
<i class="fas fa-user"></i> <span id="auth-username">Loading...</span>
</span>
<button class="auth-logout" id="logout-btn" onclick="logout()">
<i class="fas fa-sign-out-alt"></i> Logout
</button>
</div>
<nav class="nav-tabs"> <nav class="nav-tabs">
<button class="nav-tab active" data-tab="dashboard"> <button class="nav-tab active" data-tab="dashboard">
<i class="fas fa-tachometer-alt"></i> Dashboard <i class="fas fa-tachometer-alt"></i> Dashboard
+59
View File
@@ -10,6 +10,7 @@ let dashboardData = null;
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
initializeTabs(); initializeTabs();
initializeEventListeners(); initializeEventListeners();
checkAuthStatus(); // Check authentication status on page load
loadDashboard(); loadDashboard();
loadSeriesSources(); loadSeriesSources();
}); });
@@ -1375,4 +1376,62 @@ function updateEpisodeModalCounts() {
${videoCountText} ${videoCountText}
`; `;
} }
}
// ===========================
// Authentication Functions
// ===========================
async function checkAuthStatus() {
try {
const response = await fetch('/api/auth/status');
const authStatus = await response.json();
const authStatusDiv = document.getElementById('auth-status');
const authUsernameSpan = document.getElementById('auth-username');
if (authStatus.auth_enabled && authStatus.authenticated) {
// Show authentication status with username
authUsernameSpan.textContent = authStatus.username;
authStatusDiv.style.display = 'flex';
} else if (authStatus.auth_enabled && !authStatus.authenticated) {
// This shouldn't happen if middleware is working, but handle it
console.warn('Auth enabled but not authenticated - middleware may be misconfigured');
} else {
// Authentication disabled - hide auth status
authStatusDiv.style.display = 'none';
}
} catch (error) {
console.error('Failed to check authentication status:', error);
// Hide auth status on error
document.getElementById('auth-status').style.display = 'none';
}
}
async function logout() {
if (!confirm('Are you sure you want to logout?')) {
return;
}
try {
const response = await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'same-origin'
});
if (response.ok) {
showToast('✅ Logged out successfully', 'success');
// Reload page to trigger authentication prompt
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
showToast('❌ Logout failed', 'error');
}
} catch (error) {
console.error('Logout failed:', error);
showToast('❌ Logout error', 'error');
}
} }