"""Authentication route registration.""" import logging from urllib.parse import quote_plus, urlencode from flask import current_app, flash, redirect, render_template, request, session, url_for from flask_login import current_user, login_required, login_user, logout_user from config import MAX_USERNAME_LENGTH, MIN_PASSWORD_LENGTH from models import User logger = logging.getLogger(__name__) def register_auth_routes(app, user_service, auth0_client): """Register auth routes while preserving legacy endpoint names.""" @app.route("/login", methods=["GET", "POST"]) def login(): """Login page.""" if current_user.is_authenticated: return redirect(url_for("index")) auth0_configured = bool( current_app.config.get("AUTH0_DOMAIN") and current_app.config.get("AUTH0_CLIENT_ID") ) if request.method == "POST": username = request.form.get("username") password = request.form.get("password") remember = request.form.get("remember", False) == "on" if not user_service: flash("User service not available", "error") return render_template("login.html", auth0_configured=auth0_configured) user = user_service.authenticate(username, password) if user: login_user(user, remember=remember) flash(f"Welcome back, {user.username}!", "success") next_page = request.args.get("next") return redirect(next_page) if next_page else redirect(url_for("index")) flash("Invalid username or password", "error") return render_template("login.html", auth0_configured=auth0_configured) @app.route("/password-reset-request", methods=["GET", "POST"]) def password_reset_request(): """Request a password reset.""" if current_user.is_authenticated: return redirect(url_for("index")) if request.method == "POST": email = request.form.get("email", "").strip().lower() if not email: flash("Please enter your email address", "error") return render_template("password_reset_request.html") user = User.query.filter_by(email=email).first() flash( "If an account exists with that email, a password reset link has been sent.", "success", ) if user and user.password_hash: token = user.generate_reset_token() reset_url = url_for("password_reset", token=token, _external=True) logger.info(f"Password reset requested for {email}. Reset URL: {reset_url}") flash(f"Reset link (development only): {reset_url}", "info") return redirect(url_for("login")) return render_template("password_reset_request.html") @app.route("/password-reset/", methods=["GET", "POST"]) def password_reset(token): """Reset password with token.""" if current_user.is_authenticated: return redirect(url_for("index")) user = User.query.filter_by(reset_token=token).first() if not user or not user.verify_reset_token(token): flash("Invalid or expired reset token", "error") return redirect(url_for("login")) if request.method == "POST": password = request.form.get("password", "") confirm_password = request.form.get("confirm_password", "") if not password or len(password) < MIN_PASSWORD_LENGTH: flash(f"Password must be at least {MIN_PASSWORD_LENGTH} characters", "error") return render_template("password_reset.html") if password != confirm_password: flash("Passwords do not match", "error") return render_template("password_reset.html") user.set_password(password) user.clear_reset_token() flash("Your password has been reset successfully. You can now log in.", "success") return redirect(url_for("login")) return render_template("password_reset.html") @app.route("/auth0/login") def auth0_login(): """Redirect to Auth0 for authentication.""" if not current_app.config.get("AUTH0_DOMAIN") or not current_app.config.get("AUTH0_CLIENT_ID"): flash( "Auth0 authentication is not configured. Please use email/password login or contact the administrator.", "error", ) return redirect(url_for("login")) try: redirect_uri = url_for("auth0_callback", _external=True) return auth0_client.authorize_redirect(redirect_uri) except Exception as e: logger.error(f"Auth0 login error: {e}") flash("Auth0 authentication failed. Please use email/password login.", "error") return redirect(url_for("login")) @app.route("/auth0/callback") def auth0_callback(): """Handle Auth0 callback and create/login user.""" try: token = auth0_client.authorize_access_token() user_info = token.get("userinfo") if not user_info: user_info = auth0_client.parse_id_token(token) auth0_id = user_info.get("sub") email = user_info.get("email") username = ( user_info.get("nickname") or user_info.get("preferred_username") or email.split("@")[0] ) if not auth0_id or not email: flash("Unable to get user information from Auth0", "error") return redirect(url_for("login")) user = user_service.get_user_by_auth0_id(auth0_id) if not user: existing_user = user_service.get_user_by_email(email) if existing_user: user_service.link_auth0_account(existing_user.id, auth0_id) user = existing_user flash(f"Account linked successfully! Welcome back, {user.username}!", "success") else: base_username = username[:MAX_USERNAME_LENGTH - 3] unique_username = base_username counter = 1 while user_service.username_exists(unique_username): unique_username = f"{base_username}_{counter}" counter += 1 user_id = user_service.create_user( username=unique_username, email=email, password=None, is_admin=False, auth0_id=auth0_id, ) if user_id: user = user_service.get_user_by_id(user_id) flash(f"Account created successfully! Welcome, {user.username}!", "success") else: flash("Failed to create user account", "error") return redirect(url_for("login")) else: flash(f"Welcome back, {user.username}!", "success") if user: login_user(user, remember=True) session["auth0_user_info"] = user_info next_page = request.args.get("next") return redirect(next_page) if next_page else redirect(url_for("index")) except Exception as e: logger.error(f"Auth0 callback error: {e}") flash("Authentication failed. Please try again.", "error") return redirect(url_for("login")) @app.route("/auth0/logout") @login_required def auth0_logout(): """Logout from Auth0 and local session.""" session.clear() logout_user() domain = current_app.config["AUTH0_DOMAIN"] client_id = current_app.config["AUTH0_CLIENT_ID"] return_to = url_for("index", _external=True) logout_url = f"https://{domain}/v2/logout?" + urlencode( {"returnTo": return_to, "client_id": client_id}, quote_via=quote_plus ) return redirect(logout_url) @app.route("/admin-setup", methods=["GET", "POST"]) def admin_setup(): """Create first admin user.""" try: user_count = User.query.count() if user_count > 0: flash("Admin user already exists.", "info") return redirect(url_for("login")) except Exception as e: logger.warning(f"Database error checking existing users: {e}") if request.method == "POST": username = request.form.get("username") email = request.form.get("email") password = request.form.get("password") password_confirm = request.form.get("password_confirm") if not username or not email or not password: flash("All fields are required", "error") return render_template("admin_setup.html") if password != password_confirm: flash("Passwords do not match", "error") return render_template("admin_setup.html") if len(password) < MIN_PASSWORD_LENGTH: flash(f"Password must be at least {MIN_PASSWORD_LENGTH} characters", "error") return render_template("admin_setup.html") user_id = user_service.create_user(username, email, password, is_admin=True) if user_id: flash("Admin account created successfully! Please log in.", "success") return redirect(url_for("login")) flash("Error creating admin account. Please try again.", "error") return render_template("admin_setup.html") @app.route("/signup", methods=["GET", "POST"]) def signup(): """Signup page.""" if current_user.is_authenticated: return redirect(url_for("index")) if request.method == "POST": username = request.form.get("username") email = request.form.get("email") password = request.form.get("password") password_confirm = request.form.get("password_confirm") if not user_service: flash("User service not available", "error") return render_template("signup.html") if not username or not email or not password: flash("All fields are required", "error") return render_template("signup.html") if password != password_confirm: flash("Passwords do not match", "error") return render_template("signup.html") if len(password) < MIN_PASSWORD_LENGTH: flash(f"Password must be at least {MIN_PASSWORD_LENGTH} characters", "error") return render_template("signup.html") if user_service.username_exists(username): flash("Username already taken", "error") return render_template("signup.html") if user_service.email_exists(email): flash("Email already registered", "error") return render_template("signup.html") user_id = user_service.create_user(username, email, password) if user_id: flash("Account created successfully! Please log in.", "success") return redirect(url_for("login")) flash("Error creating account. Please try again.", "error") return render_template("signup.html") @app.route("/logout") @login_required def logout(): """Logout current user.""" logout_user() flash("You have been logged out.", "info") return redirect(url_for("index"))