import html import json import secrets from urllib.error import HTTPError, URLError from urllib.parse import urlencode from urllib.request import Request as UrlRequest from urllib.request import urlopen from fastapi import APIRouter, Form, Request from fastapi.responses import HTMLResponse, RedirectResponse from app.auth import authenticate_user, current_user, find_or_create_google_user, mark_last_login from app.config import ( get_auth_mode, get_gitea_public_url, get_gitea_redirect_uri, get_gitea_server_url, get_google_redirect_uri, is_gitea_oauth_button_enabled, is_google_oauth_button_enabled, read_env_value, ) from app.db.audit import log_audit_event from app.gitea_provisioning import sync_gitea_user from app.templates.layout import page router = APIRouter() GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth" GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token" GOOGLE_USERINFO_URL = "https://openidconnect.googleapis.com/v1/userinfo" @router.get("/login", response_class=HTMLResponse) def login_form(request: Request): if current_user(request): return RedirectResponse(url="/portal/apps", status_code=303) return _render_login() @router.post("/login", response_class=HTMLResponse) def login(request: Request, username: str = Form(...), password: str = Form(...)): if get_auth_mode() != "local": return _render_login("Local login is disabled. Use Google sign in.") user = authenticate_user(username, password) if not user: return _render_login("Neplatné přihlašovací údaje.") request.session.clear() request.session["user_id"] = user["id"] log_audit_event( user, action="login_success", target_type="user", target_id=user.get("id"), metadata={"username": user.get("username")}, ) return RedirectResponse(url="/portal/apps", status_code=303) @router.get("/auth/gitea/login") def gitea_login(request: Request): if current_user(request): return RedirectResponse(url="/portal/apps", status_code=303) return _render_login("Gitea login is disabled. Use Google sign in.") gitea_url = get_gitea_public_url() client_id = read_env_value("GITEA_OAUTH_CLIENT_ID", "") client_secret = read_env_value("GITEA_OAUTH_CLIENT_SECRET", "") redirect_uri = get_gitea_redirect_uri() if not gitea_url or not client_id or not client_secret or not redirect_uri: _log_gitea_failure("missing_oauth_config") return _render_login("Gitea login is not fully configured.") state = secrets.token_urlsafe(32) request.session["gitea_oauth_state"] = state log_audit_event( None, action="auth.gitea.login.started", target_type="auth", metadata={"provider": "gitea"}, ) params = urlencode( { "client_id": client_id, "redirect_uri": redirect_uri, "response_type": "code", "scope": "read:user", "state": state, } ) return RedirectResponse(url=f"{gitea_url}/login/oauth/authorize?{params}", status_code=303) @router.get("/auth/gitea/callback", response_class=HTMLResponse) def gitea_callback(request: Request, code: str = "", state: str = "", error: str = ""): return _render_login("Gitea login is disabled. Use Google sign in.") expected_state = request.session.pop("gitea_oauth_state", None) if error: _log_gitea_failure("provider_error", error=error) return _render_login("Přihlášení přes Gitea se nepodařilo.") if not expected_state or not state or not secrets.compare_digest(expected_state, state): _log_gitea_failure("invalid_state") return _render_login("Přihlášení přes Gitea se nepodařilo.") if not code: _log_gitea_failure("missing_code") return _render_login("Přihlášení přes Gitea se nepodařilo.") try: token = _exchange_gitea_code(code) gitea_user = _fetch_gitea_user(token) username = (gitea_user.get("login") or gitea_user.get("username") or "").strip() if not username: _log_gitea_failure("missing_username") return _render_login("Přihlášení přes Gitea se nepodařilo.") display_name = (gitea_user.get("full_name") or username).strip() email = (gitea_user.get("email") or "").strip() user = find_or_create_oauth_user(username, display_name, email) if not user.get("is_enabled", 1): _log_gitea_failure("disabled_user", username=username) return _render_login("User is disabled in the portal.") if not user.get("is_active"): _log_gitea_failure("disabled_user", username=username) return _render_login("Uživatel je v portálu vypnutý.") request.session.clear() request.session["user_id"] = user["id"] mark_last_login(int(user["id"])) log_audit_event( user, action="auth.gitea.login.success", target_type="user", target_id=user.get("id"), metadata={"username": user.get("username"), "provider": "gitea"}, ) return RedirectResponse(url="/portal", status_code=303) except Exception: _log_gitea_failure("callback_failed") return _render_login("Přihlášení přes Gitea se nepodařilo.") @router.get("/auth/google/login") def google_login(request: Request): if current_user(request): return RedirectResponse(url="/portal/apps", status_code=303) if get_auth_mode() == "local": return _render_login("Google login is not enabled in local auth mode.") if not is_google_oauth_button_enabled(): _log_google_failure("missing_oauth_config") return _render_login("Google login is not configured.") redirect_uri = get_google_redirect_uri() if not redirect_uri: _log_google_failure("missing_redirect_uri") return _render_login("Google login is missing redirect URI.") state = secrets.token_urlsafe(32) request.session["google_oauth_state"] = state log_audit_event( None, action="auth.google.login.started", target_type="auth", metadata={"provider": "google"}, ) params = urlencode( { "client_id": read_env_value("GOOGLE_CLIENT_ID", ""), "redirect_uri": redirect_uri, "response_type": "code", "scope": "openid email profile", "state": state, "access_type": "online", "prompt": "select_account", } ) return RedirectResponse(url=f"{GOOGLE_AUTH_URL}?{params}", status_code=303) @router.get("/auth/google/callback", response_class=HTMLResponse) def google_callback(request: Request, code: str = "", state: str = "", error: str = ""): if get_auth_mode() == "local": return _render_login("Google login is not enabled in local auth mode.") expected_state = request.session.pop("google_oauth_state", None) if error: _log_google_failure("provider_error", error=error) return _render_login("Google login failed.") if not expected_state or not state or not secrets.compare_digest(expected_state, state): _log_google_failure("invalid_state") return _render_login("Google login failed.") if not code: _log_google_failure("missing_code") return _render_login("Google login failed.") try: token = _exchange_google_code(code) google_user = _fetch_google_user(token) email = (google_user.get("email") or "").strip().lower() if not email: _log_google_failure("missing_email") return _render_login("Google account did not return an email address.") allowed_domain = read_env_value("GOOGLE_ALLOWED_DOMAIN", "").strip().lower() email_domain = email.rsplit("@", 1)[1] if "@" in email else "" hosted_domain = (google_user.get("hd") or "").strip().lower() if allowed_domain and allowed_domain not in {email_domain, hosted_domain}: _log_google_failure("domain_not_allowed", email_domain=email_domain) return _render_login("Google account is not allowed for this portal.") username = email display_name = (google_user.get("name") or email).strip() provider_subject = (google_user.get("sub") or "").strip() avatar_url = (google_user.get("picture") or "").strip() user, created = find_or_create_google_user(email, display_name, provider_subject, avatar_url) if not user.get("is_enabled", 1): _log_google_failure("disabled_user", username=username) return _render_login("User is disabled in the portal.") if not user.get("is_active"): _log_google_failure("disabled_user", username=username) return _render_login("UĹľivatel je v portálu vypnutĂ˝.") sync_result = sync_gitea_user(user) request.session.clear() request.session["user_id"] = user["id"] mark_last_login(int(user["id"])) if created: log_audit_event( user, action="auth.google.user.created", target_type="user", target_id=user.get("id"), metadata={ "username": user.get("username"), "email": user.get("email"), "role": user.get("role"), "provider": "google", "gitea_sync_status": sync_result.get("status"), }, ) log_audit_event( user, action="auth.google.login.success", target_type="user", target_id=user.get("id"), metadata={"username": user.get("username"), "provider": "google", "gitea_sync_status": sync_result.get("status")}, ) return RedirectResponse(url="/portal", status_code=303) except Exception: _log_google_failure("callback_failed") return _render_login("Google login failed.") @router.get("/guest", response_class=HTMLResponse) def guest_page(request: Request): user = current_user(request) if not user: return RedirectResponse(url="/portal/login", status_code=303) # Účet s přidělenými právy sem nepatří – pošli ho na běžnou úvodní stránku. if (user.get("role") or "").lower() != "guest": return RedirectResponse(url="/portal/apps", status_code=303) return page( "Čekání na schválení", """
Požádejte o přidělení práv administrátora.
Jakmile vám administrátor přiřadí roli, obnovte stránku a portál se vám zpřístupní.
{html.escape(error)}
' auth_mode = get_auth_mode() gitea_login_html = "" google_login_html = "" if auth_mode in {"google", "mixed"} and is_google_oauth_button_enabled(): google_login_html = """ """ elif auth_mode in {"google", "mixed"}: google_login_html = 'Google login is not fully configured.
' local_login_html = "" if auth_mode == "local": local_login_html = """ """ return page( "Sign in", f"""Doporučené přihlášení je přes Gitea. Lokální účet slouží pouze jako nouzový administrátorský přístup.
{error_html} {gitea_login_html} {google_login_html}