Added custom error pages, user password change, initial setup workflow with default admin user.
This commit is contained in:
@@ -3,11 +3,11 @@
|
|||||||
let
|
let
|
||||||
version = "1.8.2";
|
version = "1.8.2";
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
devenv_root = "/home/th3r00t/Projects/dosfrontend";
|
devenv_root = "/home/th3r00t/Projects/DosVault";
|
||||||
devenv_dotfile = "/home/th3r00t/Projects/dosfrontend/.devenv";
|
devenv_dotfile = "/home/th3r00t/Projects/DosVault/.devenv";
|
||||||
devenv_dotfile_path = ./.devenv;
|
devenv_dotfile_path = ./.devenv;
|
||||||
devenv_tmpdir = "/run/user/1000";
|
devenv_tmpdir = "/run/user/1000";
|
||||||
devenv_runtime = "/run/user/1000/devenv-9a894c0";
|
devenv_runtime = "/run/user/1000/devenv-3442663";
|
||||||
devenv_istesting = false;
|
devenv_istesting = false;
|
||||||
devenv_direnvrc_latest_version = 1;
|
devenv_direnvrc_latest_version = 1;
|
||||||
container_name = null;
|
container_name = null;
|
||||||
@@ -26,11 +26,11 @@ container_name = null;
|
|||||||
let
|
let
|
||||||
version = "1.8.2";
|
version = "1.8.2";
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
devenv_root = "/home/th3r00t/Projects/dosfrontend";
|
devenv_root = "/home/th3r00t/Projects/DosVault";
|
||||||
devenv_dotfile = "/home/th3r00t/Projects/dosfrontend/.devenv";
|
devenv_dotfile = "/home/th3r00t/Projects/DosVault/.devenv";
|
||||||
devenv_dotfile_path = ./.devenv;
|
devenv_dotfile_path = ./.devenv;
|
||||||
devenv_tmpdir = "/run/user/1000";
|
devenv_tmpdir = "/run/user/1000";
|
||||||
devenv_runtime = "/run/user/1000/devenv-9a894c0";
|
devenv_runtime = "/run/user/1000/devenv-3442663";
|
||||||
devenv_istesting = false;
|
devenv_istesting = false;
|
||||||
devenv_direnvrc_latest_version = 1;
|
devenv_direnvrc_latest_version = 1;
|
||||||
container_name = null;
|
container_name = null;
|
||||||
|
|||||||
419
src/webapp.py
419
src/webapp.py
@@ -13,8 +13,15 @@ from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse, JSON
|
|||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.exception_handlers import http_exception_handler
|
||||||
|
from fastapi.exceptions import HTTPException as StarletteHTTPException
|
||||||
|
from starlette.exceptions import HTTPException as StarletteBaseHTTPException
|
||||||
from sqlalchemy import create_engine, select, func
|
from sqlalchemy import create_engine, select, func
|
||||||
from sqlalchemy.orm import Session, sessionmaker
|
from sqlalchemy.orm import Session, sessionmaker
|
||||||
|
from sqlalchemy.exc import OperationalError
|
||||||
|
from alembic.config import Config as AlembicConfig
|
||||||
|
from alembic import command
|
||||||
|
import subprocess
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Try relative imports first (when run as module)
|
# Try relative imports first (when run as module)
|
||||||
@@ -54,13 +61,219 @@ app.add_middleware(
|
|||||||
|
|
||||||
security = HTTPBearer(auto_error=False)
|
security = HTTPBearer(auto_error=False)
|
||||||
|
|
||||||
# Database tables are now managed by migrations
|
# Database initialization functions using Alembic
|
||||||
# Base.metadata.create_all(bind=engine)
|
def check_database_exists():
|
||||||
|
"""Check if the database has been initialized with basic tables"""
|
||||||
|
try:
|
||||||
|
with SessionLocal() as db:
|
||||||
|
# Try to query a basic table to see if database is initialized
|
||||||
|
db.scalar(select(func.count(User_table.id)))
|
||||||
|
return True
|
||||||
|
except OperationalError:
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run_alembic_migrations():
|
||||||
|
"""Run Alembic migrations to initialize/update database schema"""
|
||||||
|
try:
|
||||||
|
# Use the migrate command via subprocess to ensure proper environment
|
||||||
|
result = subprocess.run(
|
||||||
|
["python", "-m", "devenv", "db-upgrade"],
|
||||||
|
cwd=".",
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
logging.info("Database migrations completed successfully")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logging.error(f"Migration failed: {result.stderr}")
|
||||||
|
# Fall back to direct Alembic command
|
||||||
|
try:
|
||||||
|
# Create alembic config
|
||||||
|
alembic_cfg = AlembicConfig("alembic.ini")
|
||||||
|
command.upgrade(alembic_cfg, "head")
|
||||||
|
logging.info("Database migrations completed via direct Alembic")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Direct Alembic migration also failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to run migrations: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def ensure_super_user():
|
||||||
|
"""Ensure at least one super user exists, create default if none"""
|
||||||
|
try:
|
||||||
|
with SessionLocal() as db:
|
||||||
|
# Check if any super user exists
|
||||||
|
super_user = db.scalar(select(User_table).where(User_table.role == UserRole.SUPER.value))
|
||||||
|
|
||||||
|
if super_user:
|
||||||
|
logging.info(f"Super user already exists: {super_user.username}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Create default super user
|
||||||
|
logging.info("No super user found, creating default admin user...")
|
||||||
|
try:
|
||||||
|
default_admin = AuthManager.create_user(
|
||||||
|
session=db,
|
||||||
|
username="admin",
|
||||||
|
email="admin@dosvault.local",
|
||||||
|
password="admin123",
|
||||||
|
role=UserRole.SUPER.value
|
||||||
|
)
|
||||||
|
logging.warning("Default super user created: admin / admin123")
|
||||||
|
logging.warning("IMPORTANT: Please change the default admin password immediately!")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to create default super user: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to ensure super user: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def initialize_database_and_users():
|
||||||
|
"""Initialize database and ensure super user exists"""
|
||||||
|
success = True
|
||||||
|
|
||||||
|
# Check if database exists
|
||||||
|
if not check_database_exists():
|
||||||
|
logging.info("Database not initialized, running migrations...")
|
||||||
|
if not run_alembic_migrations():
|
||||||
|
logging.error("Failed to initialize database")
|
||||||
|
success = False
|
||||||
|
|
||||||
|
# Ensure super user exists
|
||||||
|
if success and not ensure_super_user():
|
||||||
|
logging.error("Failed to ensure super user exists")
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
# Global flags to track initialization status
|
||||||
|
db_initialization_attempted = False
|
||||||
|
db_initialization_successful = False
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_event():
|
||||||
|
"""Handle application startup - initialize database in background"""
|
||||||
|
global db_initialization_attempted, db_initialization_successful
|
||||||
|
|
||||||
|
logging.info("Starting database initialization...")
|
||||||
|
db_initialization_attempted = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Run initialization in background
|
||||||
|
success = await asyncio.get_event_loop().run_in_executor(
|
||||||
|
None, initialize_database_and_users
|
||||||
|
)
|
||||||
|
db_initialization_successful = success
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logging.info("Database initialization completed successfully")
|
||||||
|
else:
|
||||||
|
logging.error("Database initialization failed")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error during database initialization: {e}")
|
||||||
|
db_initialization_successful = False
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
templates.env.globals['max'] = max
|
templates.env.globals['max'] = max
|
||||||
templates.env.globals['min'] = min
|
templates.env.globals['min'] = min
|
||||||
|
|
||||||
|
# Error handlers
|
||||||
|
@app.exception_handler(StarletteHTTPException)
|
||||||
|
async def custom_http_exception_handler(request: Request, exc: StarletteHTTPException):
|
||||||
|
"""Handle HTTP exceptions with custom error pages"""
|
||||||
|
try:
|
||||||
|
# Get current user for error page context
|
||||||
|
current_user = None
|
||||||
|
try:
|
||||||
|
if "auth_token" in request.cookies:
|
||||||
|
token = request.cookies["auth_token"]
|
||||||
|
with SessionLocal() as db:
|
||||||
|
username = AuthManager.verify_token(token)
|
||||||
|
if username:
|
||||||
|
current_user = AuthManager.get_user_by_username(db, username)
|
||||||
|
if current_user and not current_user.is_active:
|
||||||
|
current_user = None
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Handle authentication errors - redirect to login page
|
||||||
|
if exc.status_code == 401:
|
||||||
|
# For AJAX/API requests, return JSON error
|
||||||
|
if request.headers.get("accept", "").startswith("application/json") or "/api/" in str(request.url):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=401,
|
||||||
|
content={"detail": "Authentication required"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# For page requests, redirect to login with error message
|
||||||
|
redirect_url = f"/login?error={exc.detail}&redirect={request.url.path}"
|
||||||
|
return RedirectResponse(url=redirect_url, status_code=303)
|
||||||
|
|
||||||
|
# Handle 404 and other errors with custom error page
|
||||||
|
error_details = {
|
||||||
|
"request": request,
|
||||||
|
"current_user": current_user,
|
||||||
|
"error_code": exc.status_code,
|
||||||
|
"error_title": {
|
||||||
|
404: "Page Not Found",
|
||||||
|
403: "Access Forbidden",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
}.get(exc.status_code, "Error"),
|
||||||
|
"error_description": {
|
||||||
|
404: "The page you're looking for doesn't exist or has been moved.",
|
||||||
|
403: "You don't have permission to access this resource.",
|
||||||
|
500: "An internal server error occurred. Please try again later.",
|
||||||
|
}.get(exc.status_code, str(exc.detail)),
|
||||||
|
"error_details": str(exc.detail) if exc.status_code >= 500 else None
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"error.html",
|
||||||
|
error_details,
|
||||||
|
status_code=exc.status_code
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Fallback to default error handling if our custom handler fails
|
||||||
|
logging.error(f"Error in custom exception handler: {e}")
|
||||||
|
return await http_exception_handler(request, exc)
|
||||||
|
|
||||||
|
@app.get("/login", response_class=HTMLResponse)
|
||||||
|
async def login_page(
|
||||||
|
request: Request,
|
||||||
|
error: Optional[str] = Query(None),
|
||||||
|
redirect: Optional[str] = Query(None)
|
||||||
|
):
|
||||||
|
"""Display login page"""
|
||||||
|
# If user is already logged in, redirect to home or requested page
|
||||||
|
try:
|
||||||
|
if "auth_token" in request.cookies:
|
||||||
|
token = request.cookies["auth_token"]
|
||||||
|
with SessionLocal() as db:
|
||||||
|
username = AuthManager.verify_token(token)
|
||||||
|
if username:
|
||||||
|
current_user = AuthManager.get_user_by_username(db, username)
|
||||||
|
if current_user and current_user.is_active:
|
||||||
|
return RedirectResponse(url=redirect or "/", status_code=303)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return templates.TemplateResponse("login.html", {
|
||||||
|
"request": request,
|
||||||
|
"error": error,
|
||||||
|
"redirect": redirect or "/"
|
||||||
|
})
|
||||||
|
|
||||||
# Removed proxy image URL function
|
# Removed proxy image URL function
|
||||||
|
|
||||||
|
|
||||||
@@ -125,7 +338,53 @@ def require_super_user(current_user: User_table = Depends(require_auth)):
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request, db: Session = Depends(get_db), current_user: Optional[User_table] = Depends(get_current_user)):
|
async def index(request: Request):
|
||||||
|
global db_initialization_attempted, db_initialization_successful
|
||||||
|
|
||||||
|
# Check if database is ready
|
||||||
|
if not db_initialization_attempted:
|
||||||
|
# Database initialization hasn't started yet
|
||||||
|
return templates.TemplateResponse("setup.html", {
|
||||||
|
"request": request,
|
||||||
|
"status": "initializing",
|
||||||
|
"message": "Database initialization starting..."
|
||||||
|
})
|
||||||
|
|
||||||
|
if not db_initialization_successful:
|
||||||
|
# Database initialization failed
|
||||||
|
return templates.TemplateResponse("setup.html", {
|
||||||
|
"request": request,
|
||||||
|
"status": "error",
|
||||||
|
"message": "Database initialization failed. Please check the logs and try running the setup manually.",
|
||||||
|
"setup_commands": [
|
||||||
|
"db-init",
|
||||||
|
"create-admin"
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
# Database is ready, proceed with normal operation
|
||||||
|
try:
|
||||||
|
# Create a database session for this request
|
||||||
|
with SessionLocal() as db:
|
||||||
|
# Try to get current user (this will handle auth gracefully)
|
||||||
|
current_user = None
|
||||||
|
try:
|
||||||
|
token = None
|
||||||
|
|
||||||
|
# Check for auth token in cookie
|
||||||
|
if "auth_token" in request.cookies:
|
||||||
|
token = request.cookies["auth_token"]
|
||||||
|
|
||||||
|
if token:
|
||||||
|
username = AuthManager.verify_token(token)
|
||||||
|
if username:
|
||||||
|
current_user = AuthManager.get_user_by_username(db, username)
|
||||||
|
if current_user and not current_user.is_active:
|
||||||
|
current_user = None
|
||||||
|
except Exception as e:
|
||||||
|
logging.debug(f"Error getting current user: {e}")
|
||||||
|
current_user = None
|
||||||
|
|
||||||
page = int(request.query_params.get("page", 1))
|
page = int(request.query_params.get("page", 1))
|
||||||
per_page = int(request.query_params.get("per_page", 20))
|
per_page = int(request.query_params.get("per_page", 20))
|
||||||
search = request.query_params.get("search", "").strip()
|
search = request.query_params.get("search", "").strip()
|
||||||
@@ -190,6 +449,27 @@ async def index(request: Request, db: Session = Depends(get_db), current_user: O
|
|||||||
"user_favorites": user_favorites
|
"user_favorites": user_favorites
|
||||||
})
|
})
|
||||||
|
|
||||||
|
except OperationalError as e:
|
||||||
|
# Database tables don't exist or other database issue
|
||||||
|
logging.error(f"Database error in index route: {e}")
|
||||||
|
return templates.TemplateResponse("setup.html", {
|
||||||
|
"request": request,
|
||||||
|
"status": "error",
|
||||||
|
"message": "Database not properly initialized. Please run setup commands.",
|
||||||
|
"setup_commands": [
|
||||||
|
"db-init",
|
||||||
|
"create-admin"
|
||||||
|
]
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
# Other unexpected errors
|
||||||
|
logging.error(f"Unexpected error in index route: {e}")
|
||||||
|
return templates.TemplateResponse("setup.html", {
|
||||||
|
"request": request,
|
||||||
|
"status": "error",
|
||||||
|
"message": f"An unexpected error occurred: {str(e)}"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.post("/login")
|
@app.post("/login")
|
||||||
async def login(
|
async def login(
|
||||||
@@ -1169,6 +1449,106 @@ async def health_check():
|
|||||||
"""Health check endpoint for Docker/monitoring"""
|
"""Health check endpoint for Docker/monitoring"""
|
||||||
return {"status": "healthy", "service": "DosVault"}
|
return {"status": "healthy", "service": "DosVault"}
|
||||||
|
|
||||||
|
@app.get("/profile")
|
||||||
|
async def user_profile(
|
||||||
|
request: Request,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User_table = Depends(require_auth)
|
||||||
|
):
|
||||||
|
"""Display user profile page"""
|
||||||
|
return templates.TemplateResponse("user_profile.html", {
|
||||||
|
"request": request,
|
||||||
|
"current_user": current_user
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.post("/profile/change-password")
|
||||||
|
async def change_password(
|
||||||
|
request: Request,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User_table = Depends(require_auth)
|
||||||
|
):
|
||||||
|
"""Change user password"""
|
||||||
|
try:
|
||||||
|
# Get JSON data from request
|
||||||
|
data = await request.json()
|
||||||
|
current_password = data.get("current_password")
|
||||||
|
new_password = data.get("new_password")
|
||||||
|
|
||||||
|
if not current_password or not new_password:
|
||||||
|
raise HTTPException(status_code=400, detail="Current password and new password are required")
|
||||||
|
|
||||||
|
if len(new_password) < 6:
|
||||||
|
raise HTTPException(status_code=400, detail="New password must be at least 6 characters long")
|
||||||
|
|
||||||
|
# Verify current password
|
||||||
|
if not AuthManager.verify_password(current_password, current_user.password_hash):
|
||||||
|
raise HTTPException(status_code=400, detail="Current password is incorrect")
|
||||||
|
|
||||||
|
# Update password
|
||||||
|
current_user.password_hash = AuthManager.get_password_hash(new_password)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
logging.info(f"Password changed for user: {current_user.username}")
|
||||||
|
return {"message": "Password updated successfully"}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error changing password for user {current_user.username}: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Internal server error")
|
||||||
|
|
||||||
|
@app.post("/profile/update-info")
|
||||||
|
async def update_profile_info(
|
||||||
|
request: Request,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User_table = Depends(require_super_user)
|
||||||
|
):
|
||||||
|
"""Update user profile information (super users only)"""
|
||||||
|
try:
|
||||||
|
# Get JSON data from request
|
||||||
|
data = await request.json()
|
||||||
|
username = data.get("username", "").strip()
|
||||||
|
email = data.get("email", "").strip()
|
||||||
|
|
||||||
|
if not username or not email:
|
||||||
|
raise HTTPException(status_code=400, detail="Username and email are required")
|
||||||
|
|
||||||
|
if len(username) < 3:
|
||||||
|
raise HTTPException(status_code=400, detail="Username must be at least 3 characters long")
|
||||||
|
|
||||||
|
# Check if username already exists (excluding current user)
|
||||||
|
existing_user = db.query(User_table).filter(
|
||||||
|
User_table.username == username,
|
||||||
|
User_table.id != current_user.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing_user:
|
||||||
|
raise HTTPException(status_code=400, detail="Username already exists")
|
||||||
|
|
||||||
|
# Check if email already exists (excluding current user)
|
||||||
|
existing_email = db.query(User_table).filter(
|
||||||
|
User_table.email == email,
|
||||||
|
User_table.id != current_user.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing_email:
|
||||||
|
raise HTTPException(status_code=400, detail="Email already exists")
|
||||||
|
|
||||||
|
# Update user information
|
||||||
|
old_username = current_user.username
|
||||||
|
current_user.username = username
|
||||||
|
current_user.email = email
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
logging.info(f"Profile information updated for user: {old_username} -> {username} ({email})")
|
||||||
|
return {"message": "Profile updated successfully"}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error updating profile for user {current_user.username}: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Internal server error")
|
||||||
|
|
||||||
@app.get("/api/auth/token")
|
@app.get("/api/auth/token")
|
||||||
async def get_auth_token(
|
async def get_auth_token(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -1179,6 +1559,39 @@ async def get_auth_token(
|
|||||||
token = request.cookies.get("auth_token", "")
|
token = request.cookies.get("auth_token", "")
|
||||||
return {"token": token, "username": current_user.username}
|
return {"token": token, "username": current_user.username}
|
||||||
|
|
||||||
|
# Catch-all route for 404 errors - this must be the last route
|
||||||
|
@app.get("/{path:path}", response_class=HTMLResponse)
|
||||||
|
async def catch_all_404(request: Request, path: str):
|
||||||
|
"""Catch-all route for 404 errors"""
|
||||||
|
# Get current user for error page context
|
||||||
|
current_user = None
|
||||||
|
try:
|
||||||
|
if "auth_token" in request.cookies:
|
||||||
|
token = request.cookies["auth_token"]
|
||||||
|
with SessionLocal() as db:
|
||||||
|
username = AuthManager.verify_token(token)
|
||||||
|
if username:
|
||||||
|
current_user = AuthManager.get_user_by_username(db, username)
|
||||||
|
if current_user and not current_user.is_active:
|
||||||
|
current_user = None
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
error_details = {
|
||||||
|
"request": request,
|
||||||
|
"current_user": current_user,
|
||||||
|
"error_code": 404,
|
||||||
|
"error_title": "Page Not Found",
|
||||||
|
"error_description": f"The page '/{path}' doesn't exist or has been moved.",
|
||||||
|
"error_details": None
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"error.html",
|
||||||
|
error_details,
|
||||||
|
status_code=404
|
||||||
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host=config.host, port=config.port)
|
uvicorn.run(app, host=config.host, port=config.port)
|
||||||
@@ -319,9 +319,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if current_user %}
|
{% if current_user %}
|
||||||
<span class="text-sm text-primary">
|
<a href="/profile" class="text-sm text-accent hover:text-accent-hover transition-colors">
|
||||||
Welcome, {{ current_user.username }}
|
{{ current_user.username }}
|
||||||
</span>
|
</a>
|
||||||
<button onclick="logout()" class="bg-red-600 hover:bg-red-700 px-4 py-2 rounded text-sm transition-colors">
|
<button onclick="logout()" class="bg-red-600 hover:bg-red-700 px-4 py-2 rounded text-sm transition-colors">
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
@@ -403,8 +403,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if current_user %}
|
{% if current_user %}
|
||||||
<div class="text-base font-medium text-primary">{{ current_user.username }}</div>
|
<div class="mb-3">
|
||||||
<button onclick="logout()" class="bg-red-600 hover:bg-red-700 px-4 py-2 rounded text-sm w-full mt-3 transition-colors">
|
<a href="/profile" class="text-base font-medium text-accent hover:text-accent-hover transition-colors">
|
||||||
|
{{ current_user.username }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<button onclick="logout()" class="bg-red-600 hover:bg-red-700 px-4 py-2 rounded text-sm w-full transition-colors">
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
211
templates/error.html
Normal file
211
templates/error.html
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% if error_code %}{{ error_code }} - {% endif %}{{ error_title or "Error" }} - DosVault</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
/* Default Dark Theme */
|
||||||
|
--primary-bg: #111827;
|
||||||
|
--secondary-bg: #1f2937;
|
||||||
|
--tertiary-bg: #374151;
|
||||||
|
--accent-bg: #2563eb;
|
||||||
|
--accent-hover: #1d4ed8;
|
||||||
|
--text-primary: #ffffff;
|
||||||
|
--text-secondary: #9ca3af;
|
||||||
|
--text-accent: #60a5fa;
|
||||||
|
--border-color: #4b5563;
|
||||||
|
--success-color: #059669;
|
||||||
|
--warning-color: #d97706;
|
||||||
|
--danger-color: #dc2626;
|
||||||
|
--gradient-from: #1f2937;
|
||||||
|
--gradient-to: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apply CSS custom properties */
|
||||||
|
body {
|
||||||
|
background-color: var(--primary-bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.bg-primary { background-color: var(--primary-bg); }
|
||||||
|
.bg-secondary { background-color: var(--secondary-bg); }
|
||||||
|
.bg-tertiary { background-color: var(--tertiary-bg); }
|
||||||
|
.bg-accent { background-color: var(--accent-bg); }
|
||||||
|
.bg-accent:hover { background-color: var(--accent-hover); }
|
||||||
|
.bg-accent-hover { background-color: var(--accent-hover); }
|
||||||
|
.text-primary { color: var(--text-primary); }
|
||||||
|
.text-secondary { color: var(--text-secondary); }
|
||||||
|
.text-accent { color: var(--text-accent); }
|
||||||
|
.border-theme { border-color: var(--border-color); }
|
||||||
|
.bg-gradient-theme { background: linear-gradient(to bottom right, var(--gradient-from), var(--gradient-to)); }
|
||||||
|
|
||||||
|
/* Fade-in animation */
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glitch effect for error code */
|
||||||
|
.glitch {
|
||||||
|
font-size: 4rem;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
position: relative;
|
||||||
|
text-shadow: 0.05em 0 0 #00ffff, -0.03em -0.04em 0 #ff00ff,
|
||||||
|
0.025em 0.04em 0 #ffff00;
|
||||||
|
animation: glitch 500ms infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glitch {
|
||||||
|
0% {
|
||||||
|
text-shadow: 0.05em 0 0 #00ffff, -0.03em -0.04em 0 #ff00ff,
|
||||||
|
0.025em 0.04em 0 #ffff00;
|
||||||
|
}
|
||||||
|
15% {
|
||||||
|
text-shadow: 0.05em 0 0 #00ffff, -0.03em -0.04em 0 #ff00ff,
|
||||||
|
0.025em 0.04em 0 #ffff00;
|
||||||
|
}
|
||||||
|
16% {
|
||||||
|
text-shadow: -0.05em -0.025em 0 #00ffff, 0.025em 0.035em 0 #ff00ff,
|
||||||
|
-0.05em -0.05em 0 #ffff00;
|
||||||
|
}
|
||||||
|
49% {
|
||||||
|
text-shadow: -0.05em -0.025em 0 #00ffff, 0.025em 0.035em 0 #ff00ff,
|
||||||
|
-0.05em -0.05em 0 #ffff00;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
text-shadow: 0.05em 0.035em 0 #00ffff, 0.03em 0 0 #ff00ff,
|
||||||
|
0 -0.04em 0 #ffff00;
|
||||||
|
}
|
||||||
|
99% {
|
||||||
|
text-shadow: 0.05em 0.035em 0 #00ffff, 0.03em 0 0 #ff00ff,
|
||||||
|
0 -0.04em 0 #ffff00;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
text-shadow: 0.05em 0 0 #00ffff, -0.03em -0.04em 0 #ff00ff,
|
||||||
|
0.025em 0.04em 0 #ffff00;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-primary text-primary min-h-screen">
|
||||||
|
<div class="min-h-screen flex items-center justify-center bg-gradient-theme py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="max-w-lg w-full text-center animate-fade-in">
|
||||||
|
<!-- DosVault Logo -->
|
||||||
|
<div class="mx-auto h-20 w-20 flex items-center justify-center mb-8">
|
||||||
|
<svg class="w-16 h-16 text-accent opacity-50" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Vault door -->
|
||||||
|
<circle cx="16" cy="16" r="14" fill="currentColor" opacity="0.1"/>
|
||||||
|
<circle cx="16" cy="16" r="12" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||||
|
<circle cx="16" cy="16" r="8" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||||
|
<circle cx="16" cy="16" r="4" fill="currentColor" opacity="0.3"/>
|
||||||
|
|
||||||
|
<!-- DOS-style pixels -->
|
||||||
|
<rect x="6" y="6" width="2" height="2" fill="currentColor" opacity="0.6"/>
|
||||||
|
<rect x="10" y="6" width="2" height="2" fill="currentColor" opacity="0.8"/>
|
||||||
|
<rect x="20" y="6" width="2" height="2" fill="currentColor" opacity="0.8"/>
|
||||||
|
<rect x="24" y="6" width="2" height="2" fill="currentColor" opacity="0.6"/>
|
||||||
|
<rect x="6" y="24" width="2" height="2" fill="currentColor" opacity="0.6"/>
|
||||||
|
<rect x="24" y="24" width="2" height="2" fill="currentColor" opacity="0.6"/>
|
||||||
|
|
||||||
|
<!-- Handle -->
|
||||||
|
<rect x="20" y="14" width="6" height="4" rx="2" fill="currentColor" opacity="0.7"/>
|
||||||
|
<circle cx="24" cy="16" r="1" fill="currentColor"/>
|
||||||
|
|
||||||
|
<!-- Error X overlay -->
|
||||||
|
<line x1="8" y1="8" x2="24" y2="24" stroke="currentColor" stroke-width="2" opacity="0.8"/>
|
||||||
|
<line x1="24" y1="8" x2="8" y2="24" stroke="currentColor" stroke-width="2" opacity="0.8"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Code with Glitch Effect -->
|
||||||
|
{% if error_code %}
|
||||||
|
<div class="glitch text-accent mb-4">{{ error_code }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Error Title -->
|
||||||
|
<h1 class="text-3xl font-bold text-primary mb-4">
|
||||||
|
{{ error_title or "Oops! Something went wrong" }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- Error Description -->
|
||||||
|
<p class="text-secondary text-lg mb-8 max-w-md mx-auto">
|
||||||
|
{{ error_description or "The page you're looking for doesn't exist or has been moved." }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Error Details (if available) -->
|
||||||
|
{% if error_details %}
|
||||||
|
<div class="bg-tertiary border border-theme rounded-lg p-4 mb-8 text-left">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<svg class="w-5 h-5 text-warning-color mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-sm font-medium text-primary">Error Details</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-secondary font-mono">{{ error_details }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="space-y-4 sm:space-y-0 sm:space-x-4 sm:flex sm:justify-center">
|
||||||
|
<a href="/"
|
||||||
|
class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-accent hover:bg-accent-hover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent transition-colors">
|
||||||
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5a2 2 0 012-2h4a2 2 0 012 2v0M8 5a2 2 0 012-2h4a2 2 0 012 2v0"></path>
|
||||||
|
</svg>
|
||||||
|
Browse Games
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button onclick="goBack()"
|
||||||
|
class="inline-flex items-center px-6 py-3 border border-theme text-base font-medium rounded-md text-primary bg-secondary hover:bg-tertiary focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent transition-colors">
|
||||||
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||||
|
</svg>
|
||||||
|
Go Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional Help -->
|
||||||
|
<div class="mt-12 text-center">
|
||||||
|
<p class="text-sm text-secondary mb-4">Still having trouble?</p>
|
||||||
|
<div class="space-x-6">
|
||||||
|
{% if current_user and current_user.role == "super" %}
|
||||||
|
<a href="/admin" class="text-accent hover:text-accent-hover text-sm transition-colors">
|
||||||
|
Admin Panel
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="/login" class="text-accent hover:text-accent-hover text-sm transition-colors">
|
||||||
|
Login
|
||||||
|
</a>
|
||||||
|
<span class="text-secondary text-sm">•</span>
|
||||||
|
<span class="text-secondary text-sm">DosVault v1.0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function goBack() {
|
||||||
|
// Try to go back in history, fallback to home page
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
window.history.back();
|
||||||
|
} else {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -442,10 +442,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Game Detail Overlay -->
|
<!-- Game Detail Overlay -->
|
||||||
<div id="gameDetailOverlay" class="hidden fixed inset-0 bg-black bg-opacity-75 z-50 flex items-center justify-center p-4 overflow-y-auto backdrop-blur-sm">
|
<div id="gameDetailOverlay" class="hidden fixed inset-0 bg-black bg-opacity-75 z-50 overflow-y-auto backdrop-blur-sm">
|
||||||
<div class="bg-secondary rounded-lg max-w-6xl max-h-full w-full overflow-hidden relative shadow-2xl border border-theme animate-fade-in">
|
<div class="min-h-screen flex items-start sm:items-center justify-center p-4 py-8 sm:py-4">
|
||||||
|
<div class="bg-secondary rounded-lg max-w-6xl w-full overflow-hidden relative shadow-2xl border border-theme animate-fade-in">
|
||||||
<!-- Close Button -->
|
<!-- Close Button -->
|
||||||
<button onclick="closeGameDetail()" class="absolute top-4 right-4 text-primary hover:text-secondary text-2xl z-30 bg-primary bg-opacity-20 hover:bg-opacity-30 rounded-full w-10 h-10 flex items-center justify-center transition-all">
|
<button onclick="closeGameDetail()" class="absolute top-2 left-2 text-primary hover:text-secondary text-2xl z-30 bg-primary bg-opacity-20 hover:bg-opacity-30 rounded-full w-10 h-10 flex items-center justify-center transition-all">
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -455,9 +456,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Game Detail Content -->
|
<!-- Game Detail Content -->
|
||||||
<div id="gameDetailContent" class="hidden max-h-screen overflow-y-auto">
|
<div id="gameDetailContent" class="hidden">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="border-b border-theme p-6">
|
<div class="border-b border-theme p-6 pl-16">
|
||||||
<div class="flex justify-between items-start">
|
<div class="flex justify-between items-start">
|
||||||
<div>
|
<div>
|
||||||
<h1 id="gameTitle" class="text-3xl font-bold mb-2 text-primary"></h1>
|
<h1 id="gameTitle" class="text-3xl font-bold mb-2 text-primary"></h1>
|
||||||
@@ -540,6 +541,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Screenshot Modal -->
|
<!-- Screenshot Modal -->
|
||||||
|
|||||||
222
templates/login.html
Normal file
222
templates/login.html
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login - DosVault</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
/* Default Dark Theme */
|
||||||
|
--primary-bg: #111827;
|
||||||
|
--secondary-bg: #1f2937;
|
||||||
|
--tertiary-bg: #374151;
|
||||||
|
--accent-bg: #2563eb;
|
||||||
|
--accent-hover: #1d4ed8;
|
||||||
|
--text-primary: #ffffff;
|
||||||
|
--text-secondary: #9ca3af;
|
||||||
|
--text-accent: #60a5fa;
|
||||||
|
--border-color: #4b5563;
|
||||||
|
--success-color: #059669;
|
||||||
|
--warning-color: #d97706;
|
||||||
|
--danger-color: #dc2626;
|
||||||
|
--gradient-from: #1f2937;
|
||||||
|
--gradient-to: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apply CSS custom properties */
|
||||||
|
body {
|
||||||
|
background-color: var(--primary-bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.bg-primary { background-color: var(--primary-bg); }
|
||||||
|
.bg-secondary { background-color: var(--secondary-bg); }
|
||||||
|
.bg-tertiary { background-color: var(--tertiary-bg); }
|
||||||
|
.bg-accent { background-color: var(--accent-bg); }
|
||||||
|
.bg-accent:hover { background-color: var(--accent-hover); }
|
||||||
|
.bg-accent-hover { background-color: var(--accent-hover); }
|
||||||
|
.text-primary { color: var(--text-primary); }
|
||||||
|
.text-secondary { color: var(--text-secondary); }
|
||||||
|
.text-accent { color: var(--text-accent); }
|
||||||
|
.border-theme { border-color: var(--border-color); }
|
||||||
|
.bg-gradient-theme { background: linear-gradient(to bottom right, var(--gradient-from), var(--gradient-to)); }
|
||||||
|
|
||||||
|
/* Fade-in animation */
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-primary text-primary min-h-screen">
|
||||||
|
<div class="min-h-screen flex items-center justify-center bg-gradient-theme py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="max-w-md w-full space-y-8 animate-fade-in">
|
||||||
|
<div>
|
||||||
|
<!-- DosVault Logo -->
|
||||||
|
<div class="mx-auto h-24 w-24 flex items-center justify-center">
|
||||||
|
<svg class="w-20 h-20 text-accent" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Vault door -->
|
||||||
|
<circle cx="16" cy="16" r="14" fill="currentColor" opacity="0.1"/>
|
||||||
|
<circle cx="16" cy="16" r="12" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||||
|
<circle cx="16" cy="16" r="8" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||||
|
<circle cx="16" cy="16" r="4" fill="currentColor" opacity="0.3"/>
|
||||||
|
|
||||||
|
<!-- DOS-style pixels -->
|
||||||
|
<rect x="6" y="6" width="2" height="2" fill="currentColor" opacity="0.6"/>
|
||||||
|
<rect x="10" y="6" width="2" height="2" fill="currentColor" opacity="0.8"/>
|
||||||
|
<rect x="20" y="6" width="2" height="2" fill="currentColor" opacity="0.8"/>
|
||||||
|
<rect x="24" y="6" width="2" height="2" fill="currentColor" opacity="0.6"/>
|
||||||
|
<rect x="6" y="24" width="2" height="2" fill="currentColor" opacity="0.6"/>
|
||||||
|
<rect x="24" y="24" width="2" height="2" fill="currentColor" opacity="0.6"/>
|
||||||
|
|
||||||
|
<!-- Handle -->
|
||||||
|
<rect x="20" y="14" width="6" height="4" rx="2" fill="currentColor" opacity="0.7"/>
|
||||||
|
<circle cx="24" cy="16" r="1" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1 class="mt-6 text-center text-3xl font-extrabold text-primary">
|
||||||
|
Welcome to DosVault
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-center text-sm text-secondary">
|
||||||
|
Sign in to access your DOS game collection
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Login Form -->
|
||||||
|
<form class="mt-8 space-y-6" onsubmit="handleLogin(event)">
|
||||||
|
<div class="rounded-md shadow-sm -space-y-px">
|
||||||
|
<div>
|
||||||
|
<label for="username" class="sr-only">Username</label>
|
||||||
|
<input id="username" name="username" type="text" required
|
||||||
|
class="appearance-none rounded-none relative block w-full px-3 py-2 bg-secondary border border-theme border-b-0 rounded-t-md placeholder-secondary text-primary focus:outline-none focus:ring-accent focus:border-accent focus:z-10 sm:text-sm"
|
||||||
|
placeholder="Username">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password" class="sr-only">Password</label>
|
||||||
|
<input id="password" name="password" type="password" required
|
||||||
|
class="appearance-none rounded-none relative block w-full px-3 py-2 bg-secondary border border-theme rounded-b-md placeholder-secondary text-primary focus:outline-none focus:ring-accent focus:border-accent focus:z-10 sm:text-sm"
|
||||||
|
placeholder="Password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="login-status" class="text-sm text-center hidden"></div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit" id="login-btn"
|
||||||
|
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-accent hover:bg-accent-hover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent transition-colors">
|
||||||
|
<span class="absolute left-0 inset-y-0 flex items-center pl-3">
|
||||||
|
<svg class="h-5 w-5 text-accent-hover group-hover:text-white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="login-text">Sign in</span>
|
||||||
|
<span class="login-loading hidden">Signing in...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="/" class="text-accent hover:text-accent-hover text-sm transition-colors">
|
||||||
|
← Back to game library
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Demo Mode Notice -->
|
||||||
|
<div class="mt-8 p-4 bg-tertiary border border-theme rounded-md">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg class="w-5 h-5 text-warning-color mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-primary">Demo Mode Available</h3>
|
||||||
|
<p class="text-xs text-secondary mt-1">You can browse the game library without logging in, but downloads require authentication.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Check if there's an error message in URL params
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const errorMessage = urlParams.get('error');
|
||||||
|
const redirectUrl = urlParams.get('redirect') || '/';
|
||||||
|
|
||||||
|
if (errorMessage) {
|
||||||
|
const statusDiv = document.getElementById('login-status');
|
||||||
|
statusDiv.textContent = decodeURIComponent(errorMessage);
|
||||||
|
statusDiv.className = 'text-sm text-center text-red-400';
|
||||||
|
statusDiv.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogin(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const username = document.getElementById('username').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
const btn = document.getElementById('login-btn');
|
||||||
|
const status = document.getElementById('login-status');
|
||||||
|
const loginText = btn.querySelector('.login-text');
|
||||||
|
const loginLoading = btn.querySelector('.login-loading');
|
||||||
|
|
||||||
|
// Disable form and show loading
|
||||||
|
btn.disabled = true;
|
||||||
|
loginText.classList.add('hidden');
|
||||||
|
loginLoading.classList.remove('hidden');
|
||||||
|
status.textContent = '';
|
||||||
|
status.classList.add('hidden');
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('username', username);
|
||||||
|
formData.append('password', password);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
localStorage.setItem('authToken', data.access_token);
|
||||||
|
|
||||||
|
// Show success message briefly
|
||||||
|
status.textContent = 'Login successful! Redirecting...';
|
||||||
|
status.className = 'text-sm text-center text-green-400';
|
||||||
|
status.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Redirect after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
status.textContent = errorData.detail || 'Login failed. Please check your credentials.';
|
||||||
|
status.className = 'text-sm text-center text-red-400';
|
||||||
|
status.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
status.textContent = 'Network error. Please try again.';
|
||||||
|
status.className = 'text-sm text-center text-red-400';
|
||||||
|
status.classList.remove('hidden');
|
||||||
|
} finally {
|
||||||
|
// Re-enable form
|
||||||
|
btn.disabled = false;
|
||||||
|
loginText.classList.remove('hidden');
|
||||||
|
loginLoading.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
110
templates/setup.html
Normal file
110
templates/setup.html
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}DosVault - Setup{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="min-h-screen flex items-center justify-center px-4">
|
||||||
|
<div class="max-w-md w-full space-y-8 text-center">
|
||||||
|
<div>
|
||||||
|
<div class="mx-auto w-24 h-24 mb-6">
|
||||||
|
<svg class="w-full h-full text-accent" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-3xl font-bold text-primary mb-2">DosVault Setup</h2>
|
||||||
|
|
||||||
|
{% if status == "initializing" %}
|
||||||
|
<div class="animate-spin inline-block w-8 h-8 border-4 border-accent border-t-transparent rounded-full mb-4"></div>
|
||||||
|
<p class="text-secondary text-lg">{{ message }}</p>
|
||||||
|
<p class="text-secondary text-sm mt-2">This may take a few moments...</p>
|
||||||
|
|
||||||
|
<!-- Auto refresh every 3 seconds while initializing -->
|
||||||
|
<script>
|
||||||
|
setTimeout(function() {
|
||||||
|
window.location.reload();
|
||||||
|
}, 3000);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% elif status == "error" %}
|
||||||
|
<div class="w-16 h-16 mx-auto mb-4 text-red-500">
|
||||||
|
<svg fill="currentColor" viewBox="0 0 24 24" class="w-full h-full">
|
||||||
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p class="text-red-400 text-lg mb-4">{{ message }}</p>
|
||||||
|
|
||||||
|
{% if setup_commands %}
|
||||||
|
<div class="bg-secondary p-6 rounded-lg border border-tertiary text-left">
|
||||||
|
<h3 class="text-primary text-lg font-semibold mb-4">Manual Setup Instructions</h3>
|
||||||
|
<p class="text-secondary text-sm mb-4">
|
||||||
|
Please run the following commands in your terminal to set up DosVault:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
{% for command in setup_commands %}
|
||||||
|
<div class="bg-primary p-3 rounded border border-border-color">
|
||||||
|
<code class="text-accent text-sm font-mono">{{ command }}</code>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 p-4 bg-yellow-900 border border-yellow-700 rounded">
|
||||||
|
<p class="text-yellow-200 text-sm">
|
||||||
|
<strong>Note:</strong> After running these commands, refresh this page to continue.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<button onclick="window.location.reload()"
|
||||||
|
class="px-6 py-3 bg-accent hover:bg-accent-hover text-white rounded-lg font-medium transition-colors">
|
||||||
|
Retry Setup
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="w-16 h-16 mx-auto mb-4 text-green-500">
|
||||||
|
<svg fill="currentColor" viewBox="0 0 24 24" class="w-full h-full">
|
||||||
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p class="text-green-400 text-lg mb-4">Setup Complete!</p>
|
||||||
|
<p class="text-secondary text-sm">DosVault has been initialized successfully.</p>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<a href="/" class="px-6 py-3 bg-accent hover:bg-accent-hover text-white rounded-lg font-medium transition-colors inline-block">
|
||||||
|
Continue to DosVault
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Default credentials notice for successful setup -->
|
||||||
|
{% if status == "initializing" or status == "error" %}
|
||||||
|
<div class="mt-8 p-4 bg-blue-900 border border-blue-700 rounded-lg">
|
||||||
|
<h4 class="text-blue-200 font-semibold mb-2">Default Admin Credentials</h4>
|
||||||
|
<p class="text-blue-300 text-sm">
|
||||||
|
Username: <code class="bg-blue-800 px-1 py-0.5 rounded">admin</code><br>
|
||||||
|
Password: <code class="bg-blue-800 px-1 py-0.5 rounded">admin123</code>
|
||||||
|
</p>
|
||||||
|
<p class="text-blue-400 text-xs mt-2">
|
||||||
|
<strong>Important:</strong> Please change these credentials after your first login!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.animate-spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
392
templates/user_profile.html
Normal file
392
templates/user_profile.html
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}User Profile - DosVault{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="mb-8">
|
||||||
|
<h1 class="text-3xl font-bold mb-2 text-primary">User Profile</h1>
|
||||||
|
<p class="text-secondary">Manage your account settings and preferences</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
|
<!-- Profile Information Card -->
|
||||||
|
<div class="lg:col-span-1">
|
||||||
|
<div class="bg-secondary rounded-lg p-6 border border-theme">
|
||||||
|
<div class="flex items-center mb-6">
|
||||||
|
<div class="w-16 h-16 bg-accent rounded-full flex items-center justify-center mr-4">
|
||||||
|
<span class="text-2xl font-bold text-white">{{ current_user.username[0].upper() }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold text-primary">{{ current_user.username }}</h2>
|
||||||
|
<p class="text-secondary">{{ current_user.email }}</p>
|
||||||
|
<span class="px-2 py-1 rounded text-xs text-white mt-1 inline-block
|
||||||
|
{% if current_user.role == 'super' %}bg-danger-color
|
||||||
|
{% elif current_user.role == 'normal' %}bg-accent
|
||||||
|
{% else %}bg-warning-color{% endif %}">
|
||||||
|
{{ current_user.role.upper() }} USER
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span class="text-secondary">Joined:</span>
|
||||||
|
<span class="text-primary">{{ current_user.created_at.strftime('%B %d, %Y') if current_user.created_at else 'Unknown' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if current_user.last_login %}
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span class="text-secondary">Last Login:</span>
|
||||||
|
<span class="text-primary">{{ current_user.last_login.strftime('%B %d, %Y at %I:%M %p') }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span class="text-secondary">Account Status:</span>
|
||||||
|
<span class="{% if current_user.is_active %}text-green-400{% else %}text-red-400{% endif %}">
|
||||||
|
{% if current_user.is_active %}Active{% else %}Inactive{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Account Management -->
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- Profile Information Edit Section (Super users only) -->
|
||||||
|
{% if current_user.role == "super" %}
|
||||||
|
<div class="bg-secondary rounded-lg p-6 border border-theme">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<svg class="w-6 h-6 text-accent mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Edit Profile Information</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-secondary text-sm mb-4">Update your username and email address.</p>
|
||||||
|
|
||||||
|
<form onsubmit="updateProfile(event)" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-secondary mb-1">Username</label>
|
||||||
|
<input type="text" id="edit-username" value="{{ current_user.username }}" required
|
||||||
|
class="w-full px-3 py-2 bg-tertiary border border-theme rounded text-primary placeholder-secondary focus:outline-none focus:ring-2 focus:ring-accent">
|
||||||
|
<p class="text-xs text-secondary mt-1">Choose a unique username for your account.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-secondary mb-1">Email Address</label>
|
||||||
|
<input type="email" id="edit-email" value="{{ current_user.email }}" required
|
||||||
|
class="w-full px-3 py-2 bg-tertiary border border-theme rounded text-primary placeholder-secondary focus:outline-none focus:ring-2 focus:ring-accent">
|
||||||
|
<p class="text-xs text-secondary mt-1">A valid email address for account recovery.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<button type="submit" id="profile-btn"
|
||||||
|
class="px-6 py-2 bg-green-600 hover:bg-green-700 rounded font-medium text-white transition-colors">
|
||||||
|
<span class="profile-text">Update Profile</span>
|
||||||
|
<span class="profile-loading hidden">Updating...</span>
|
||||||
|
</button>
|
||||||
|
<div id="profile-status" class="text-sm"></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Password Change Section -->
|
||||||
|
<div class="bg-secondary rounded-lg p-6 border border-theme">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<svg class="w-6 h-6 text-accent mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m0 0a2 2 0 012 2m-2-2h-6m6 0v6a2 2 0 01-2 2H9a2 2 0 01-2-2V9a2 2 0 012-2h2m0 0V7a2 2 0 012-2"/>
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Change Password</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-secondary text-sm mb-4">Update your password to keep your account secure.</p>
|
||||||
|
|
||||||
|
<form onsubmit="changePassword(event)" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-secondary mb-1">Current Password</label>
|
||||||
|
<input type="password" id="current-password" required
|
||||||
|
class="w-full px-3 py-2 bg-tertiary border border-theme rounded text-primary placeholder-secondary focus:outline-none focus:ring-2 focus:ring-accent">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-secondary mb-1">New Password</label>
|
||||||
|
<input type="password" id="new-password" required minlength="6"
|
||||||
|
class="w-full px-3 py-2 bg-tertiary border border-theme rounded text-primary placeholder-secondary focus:outline-none focus:ring-2 focus:ring-accent">
|
||||||
|
<p class="text-xs text-secondary mt-1">Password must be at least 6 characters long.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-secondary mb-1">Confirm New Password</label>
|
||||||
|
<input type="password" id="confirm-password" required
|
||||||
|
class="w-full px-3 py-2 bg-tertiary border border-theme rounded text-primary placeholder-secondary focus:outline-none focus:ring-2 focus:ring-accent">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<button type="submit" id="password-btn"
|
||||||
|
class="px-6 py-2 bg-accent hover:bg-accent-hover rounded font-medium text-white transition-colors">
|
||||||
|
<span class="password-text">Update Password</span>
|
||||||
|
<span class="password-loading hidden">Updating...</span>
|
||||||
|
</button>
|
||||||
|
<div id="password-status" class="text-sm"></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Account Statistics (for non-demo users) -->
|
||||||
|
{% if current_user.role != "demo" %}
|
||||||
|
<div class="bg-secondary rounded-lg p-6 border border-theme">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<svg class="w-6 h-6 text-accent mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Your Statistics</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4" id="user-stats">
|
||||||
|
<div class="bg-tertiary p-4 rounded-lg border border-theme">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="text-2xl text-accent mr-3">❤️</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-lg font-bold text-primary" id="favorites-count">{{ current_user.favorites|length }}</p>
|
||||||
|
<p class="text-secondary text-sm">Favorite Games</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-tertiary p-4 rounded-lg border border-theme">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="text-2xl text-accent mr-3">⬇️</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-lg font-bold text-primary">N/A</p>
|
||||||
|
<p class="text-secondary text-sm">Downloads</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="/favorites" class="inline-flex items-center px-4 py-2 bg-accent hover:bg-accent-hover rounded font-medium text-white transition-colors">
|
||||||
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/>
|
||||||
|
</svg>
|
||||||
|
View My Favorites
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="bg-secondary rounded-lg p-6 border border-theme">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<svg class="w-6 h-6 text-accent mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Quick Actions</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<a href="/" class="flex items-center p-3 bg-tertiary hover:bg-accent rounded-lg transition-colors group">
|
||||||
|
<div class="text-2xl mr-3 group-hover:text-white">🎮</div>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium text-primary group-hover:text-white">Browse Games</p>
|
||||||
|
<p class="text-sm text-secondary group-hover:text-gray-200">Explore the library</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% if current_user.role != "demo" %}
|
||||||
|
<a href="/favorites" class="flex items-center p-3 bg-tertiary hover:bg-accent rounded-lg transition-colors group">
|
||||||
|
<div class="text-2xl mr-3 group-hover:text-white">❤️</div>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium text-primary group-hover:text-white">My Favorites</p>
|
||||||
|
<p class="text-sm text-secondary group-hover:text-gray-200">View saved games</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if current_user.role == "super" %}
|
||||||
|
<a href="/admin" class="flex items-center p-3 bg-tertiary hover:bg-accent rounded-lg transition-colors group">
|
||||||
|
<div class="text-2xl mr-3 group-hover:text-white">⚙️</div>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium text-primary group-hover:text-white">Admin Panel</p>
|
||||||
|
<p class="text-sm text-secondary group-hover:text-gray-200">System management</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<button onclick="logout()" class="flex items-center p-3 bg-red-600 hover:bg-red-700 rounded-lg transition-colors group">
|
||||||
|
<div class="text-2xl mr-3 text-white">🚪</div>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium text-white">Sign Out</p>
|
||||||
|
<p class="text-sm text-red-200">End your session</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function getCookie(name) {
|
||||||
|
const value = `; ${document.cookie}`;
|
||||||
|
const parts = value.split(`; ${name}=`);
|
||||||
|
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changePassword(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const currentPassword = document.getElementById('current-password').value;
|
||||||
|
const newPassword = document.getElementById('new-password').value;
|
||||||
|
const confirmPassword = document.getElementById('confirm-password').value;
|
||||||
|
|
||||||
|
const btn = document.getElementById('password-btn');
|
||||||
|
const status = document.getElementById('password-status');
|
||||||
|
const passwordText = btn.querySelector('.password-text');
|
||||||
|
const passwordLoading = btn.querySelector('.password-loading');
|
||||||
|
|
||||||
|
// Client-side validation
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
status.textContent = 'New passwords do not match';
|
||||||
|
status.className = 'text-sm text-red-400';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword.length < 6) {
|
||||||
|
status.textContent = 'Password must be at least 6 characters long';
|
||||||
|
status.className = 'text-sm text-red-400';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable form and show loading
|
||||||
|
btn.disabled = true;
|
||||||
|
passwordText.classList.add('hidden');
|
||||||
|
passwordLoading.classList.remove('hidden');
|
||||||
|
status.textContent = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/profile/change-password', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${getCookie('auth_token')}`
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({
|
||||||
|
current_password: currentPassword,
|
||||||
|
new_password: newPassword
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
status.textContent = 'Password updated successfully!';
|
||||||
|
status.className = 'text-sm text-green-400';
|
||||||
|
|
||||||
|
// Clear form
|
||||||
|
document.getElementById('current-password').value = '';
|
||||||
|
document.getElementById('new-password').value = '';
|
||||||
|
document.getElementById('confirm-password').value = '';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
status.textContent = result.detail || 'Failed to update password';
|
||||||
|
status.className = 'text-sm text-red-400';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Password change error:', error);
|
||||||
|
status.textContent = 'Network error. Please try again.';
|
||||||
|
status.className = 'text-sm text-red-400';
|
||||||
|
} finally {
|
||||||
|
// Re-enable form
|
||||||
|
btn.disabled = false;
|
||||||
|
passwordText.classList.remove('hidden');
|
||||||
|
passwordLoading.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateProfile(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const username = document.getElementById('edit-username').value.trim();
|
||||||
|
const email = document.getElementById('edit-email').value.trim();
|
||||||
|
|
||||||
|
const btn = document.getElementById('profile-btn');
|
||||||
|
const status = document.getElementById('profile-status');
|
||||||
|
const profileText = btn.querySelector('.profile-text');
|
||||||
|
const profileLoading = btn.querySelector('.profile-loading');
|
||||||
|
|
||||||
|
// Basic client-side validation
|
||||||
|
if (!username || !email) {
|
||||||
|
status.textContent = 'Username and email are required';
|
||||||
|
status.className = 'text-sm text-red-400';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username.length < 3) {
|
||||||
|
status.textContent = 'Username must be at least 3 characters long';
|
||||||
|
status.className = 'text-sm text-red-400';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable form and show loading
|
||||||
|
btn.disabled = true;
|
||||||
|
profileText.classList.add('hidden');
|
||||||
|
profileLoading.classList.remove('hidden');
|
||||||
|
status.textContent = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/profile/update-info', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${getCookie('auth_token')}`
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: username,
|
||||||
|
email: email
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
status.textContent = 'Profile updated successfully! Refreshing page...';
|
||||||
|
status.className = 'text-sm text-green-400';
|
||||||
|
|
||||||
|
// Refresh page after a short delay to show the updated info
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
status.textContent = result.detail || 'Failed to update profile';
|
||||||
|
status.className = 'text-sm text-red-400';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Profile update error:', error);
|
||||||
|
status.textContent = 'Network error. Please try again.';
|
||||||
|
status.className = 'text-sm text-red-400';
|
||||||
|
} finally {
|
||||||
|
// Re-enable form
|
||||||
|
btn.disabled = false;
|
||||||
|
profileText.classList.remove('hidden');
|
||||||
|
profileLoading.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout function (if not already defined in base template)
|
||||||
|
async function logout() {
|
||||||
|
try {
|
||||||
|
await fetch('/logout', { method: 'POST' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error);
|
||||||
|
}
|
||||||
|
localStorage.removeItem('authToken');
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user