""" Authentication routes for StarPunk Handles IndieLogin authentication flow including login form, OAuth callback, and logout functionality. """ from flask import ( Blueprint, current_app, flash, redirect, render_template, request, url_for, ) from starpunk.auth import ( IndieLoginError, InvalidStateError, UnauthorizedError, destroy_session, handle_callback, initiate_login, require_auth, verify_session, ) # Create blueprint bp = Blueprint("auth", __name__, url_prefix="/admin") @bp.route("/login", methods=["GET"]) def login_form(): """ Display login form If user is already authenticated, redirects to admin dashboard. Otherwise shows login form for IndieLogin authentication. Returns: Redirect to dashboard if authenticated, otherwise login template Template: templates/admin/login.html """ # Check if already logged in session_token = request.cookies.get("starpunk_session") if session_token and verify_session(session_token): return redirect(url_for("admin.dashboard")) return render_template("admin/login.html") @bp.route("/login", methods=["POST"]) def login_initiate(): """ Initiate IndieLogin authentication flow Validates the submitted 'me' URL and redirects to IndieLogin.com for authentication. Form data: me: User's personal website URL Returns: Redirect to IndieLogin.com or back to login form on error Raises: Flashes error message and redirects on validation failure """ me_url = request.form.get("me", "").strip() if not me_url: flash("Please enter your website URL", "error") return redirect(url_for("auth.login_form")) try: # Initiate IndieLogin flow auth_url = initiate_login(me_url) return redirect(auth_url) except ValueError as e: flash(str(e), "error") return redirect(url_for("auth.login_form")) @bp.route("/callback") def callback(): """ Handle IndieLogin callback Processes the OAuth callback from IndieLogin.com, validates the authorization code and state token, and creates an authenticated session. Query parameters: code: Authorization code from IndieLogin state: CSRF state token Returns: Redirect to admin dashboard on success, login form on failure Sets: session cookie (HttpOnly, Secure, SameSite=Lax, 30 day expiry) """ code = request.args.get("code") state = request.args.get("state") if not code or not state: flash("Missing authentication parameters", "error") return redirect(url_for("auth.login_form")) try: # Handle callback and create session session_token = handle_callback(code, state) # Create response with redirect response = redirect(url_for("admin.dashboard")) # Set secure session cookie secure = current_app.config.get("SITE_URL", "").startswith("https://") response.set_cookie( "starpunk_session", session_token, httponly=True, secure=secure, samesite="Lax", max_age=30 * 24 * 60 * 60, # 30 days ) flash("Login successful!", "success") return response except InvalidStateError as e: current_app.logger.error(f"Invalid state error: {e}") flash("Authentication failed: Invalid state token (possible CSRF)", "error") return redirect(url_for("auth.login_form")) except UnauthorizedError as e: current_app.logger.error(f"Unauthorized: {e}") flash("Authentication failed: Not authorized as admin", "error") return redirect(url_for("auth.login_form")) except IndieLoginError as e: current_app.logger.error(f"IndieLogin error: {e}") flash(f"Authentication failed: {e}", "error") return redirect(url_for("auth.login_form")) except Exception as e: current_app.logger.error(f"Unexpected auth error: {e}") flash("Authentication failed: An unexpected error occurred", "error") return redirect(url_for("auth.login_form")) @bp.route("/logout", methods=["POST"]) @require_auth def logout(): """ Logout and destroy session Destroys the user's session and clears the session cookie. Requires authentication (user must be logged in to logout). Returns: Redirect to homepage with session cookie cleared Decorator: @require_auth """ session_token = request.cookies.get("starpunk_session") # Destroy session in database if session_token: try: destroy_session(session_token) except Exception as e: current_app.logger.error(f"Error destroying session: {e}") # Clear cookie and redirect response = redirect(url_for("public.index")) response.delete_cookie("starpunk_session") flash("Logged out successfully", "success") return response