# backend/main.py
# import standard modules
+from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, RedirectResponse, JSONResponse, Response
logger = get_logger("backend.main")
# ------------------------------------------------------------------------------
-# App init
-# ------------------------------------------------------------------------------
-app = FastAPI(
- title=settings.APP_NAME,
- version=settings.APP_VERSION,
-)
-
-# ------------------------------------------------------------------------------
-# Routers
-# ------------------------------------------------------------------------------
-app.include_router(about_router)
-app.include_router(health_router)
-app.include_router(login_router)
-app.include_router(hosts_router)
-
+# Welcome log
# ------------------------------------------------------------------------------
-# Startup log
-# ------------------------------------------------------------------------------
-@app.on_event("startup")
-async def startup_log():
- logger = get_logger(__name__)
-
+def print_welcome():
safe_secret = "****" if settings.SECRET_KEY else "undefined"
safe_admin_pwd = "****" if settings.ADMIN_PASSWORD else "undefined"
safe_admin_hash = "****" if settings.ADMIN_PASSWORD_HASH else "undefined"
settings.ADMIN_USER, safe_admin_pwd, safe_admin_hash, settings.ADMIN_PASSWORD_HASH_FILE
)
+# ------------------------------------------------------------------------------
+# Shutdown log
+# ------------------------------------------------------------------------------
+def print_goodbye():
+ logger = get_logger("backend.main")
+
+ logger.info(
+ "Application %s shutting down | app_version=%s",
+ settings.APP_NAME, settings.APP_VERSION
+ )
+
+# ------------------------------------------------------------------------------
+# Lifespan for startup and shutdown events
+# ------------------------------------------------------------------------------
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ # STARTUP
+ print_welcome()
+
+ try:
+ yield
+ finally:
+ # SHUTDOWN
+ print_goodbye()
+
+# ------------------------------------------------------------------------------
+# App init
+# ------------------------------------------------------------------------------
+app = FastAPI(
+ title=settings.APP_NAME,
+ version=settings.APP_VERSION,
+ lifespan=lifespan,
+)
+
+# ------------------------------------------------------------------------------
+# Routers
+# ------------------------------------------------------------------------------
+app.include_router(about_router)
+app.include_router(health_router)
+app.include_router(login_router)
+app.include_router(hosts_router)
+
# ------------------------------------------------------------------------------
# CORS
# ------------------------------------------------------------------------------
path = request.url.path
token = request.cookies.get("session")
- # Excludes the login methods
- if path.startswith("/login") or path.startswith("/api/login"):
+ # Excludes the login/logout methods
+ if path.startswith("/login") or path.startswith("/api/login") or \
+ path.startswith("/logout") or path.startswith("/api/logout"):
+ return await call_next(request)
+
+ # Excludes the about & health methods
+ if path.startswith("/about") or path.startswith("/api/health"):
return await call_next(request)
# Excludes static files
# Protected APIs
if path.startswith("/api"):
if not is_logged_in(request):
- return JSONResponse({"error": "Not authenticated"}, status_code=401)
+ logger.error("API access denied - not logged in")
+ return JSONResponse({"error": "Unauthorized"}, status_code=401)
response = await call_next(request)
# Sliding expiration
apply_session(response, username=None, token=token)
def css_layout(request: Request):
return FileResponse(os.path.join(settings.FRONTEND_DIR, "css/layout.css"))
+# ------------------------------------------------------------------------------
+# Entry-point
+# ------------------------------------------------------------------------------
+if __name__ == "__main__":
+ import uvicorn
+
+ # Uvicorn config da settings con fallback
+ host = getattr(settings, "HTTP_HOST", "0.0.0.0")
+ port = int(getattr(settings, "HTTP_PORT", 8000))
+ reload_flag = bool(getattr(settings, "DEV_RELOAD", False))
+
+ logger.info(f"Server running on http://{host}:{port} (reload={reload_flag})")
+
+ uvicorn.run(
+ app,
+ host=host,
+ port=port,
+ reload=reload_flag,
+ proxy_headers=True,
+ forwarded_allow_ips="*",
+ log_level=(settings.LOG_LEVEL or "info").lower(),
+ #access_log=True,
+ log_config=None,
+ # workers=1, # GRGR in prod valuta gunicorn+uvicorn workers
+ )
logger = get_logger(__name__)
user = get_user_by_username(username)
if not user:
- logger.error("LOGIN failed - user %s not found", username)
+ logger.error("Login failed - user %s not found", username)
return False
if user["status"] != "active":
- logger.error("LOGIN Failed - user %s disabled", username)
+ logger.error("Login Failed - user %s disabled", username)
return False
if not bcrypt.checkpw(password.encode(), user["password_hash"].encode()):
- logger.error("LOGIN Failed - password wrong for user %s", username)
+ logger.error("Login Failed - password wrong for user %s", username)
return False
- logger.info("LOGIN user %s", username)
+ logger.info("Login successful - user %s", username)
return True
# ----------------------------
# First Login
if username is not None and token is None:
token = signer.sign(username).decode()
- logger.info("SESSION_CREATE - %s", username)
+ logger.info("Session created - %s", username)
if username is None:
username = signer.unsign(token, max_age=86400).decode()
- logger.info("SESSION_UPDATE - %s", username)
+ logger.info("Session updated - %s", username)
if username is None or token is None:
- logger.error("SESSION_ERROR")
+ logger.error("Session Error - missing username or token")
return
response.set_cookie(
httponly=True,
max_age=86400,
path="/",
- #secure=True, # solo via HTTPS
+ #secure=True, # GRGR solo via HTTPS
samesite="Strict"
)
return False
# -----------------------------
-# check login
+# Close Session
# -----------------------------
-def require_login(request: Request):
- if not is_logged_in(request):
- raise HTTPException(status_code=401, detail="Not authenticated")
+def close_session(response):
+ logger = get_logger(__name__)
+
+ response.delete_cookie(
+ key="session",
+ path="/"
+ )
+
+ logger.info("Session closed")