Replace hardcoded Google OAuth + password auth registration with auto-discovered auth providers. Each provider in auth/<name>/provider.py implements AuthProvider ABC and is automatically registered at startup. - auth/__init__.py: AuthProvider ABC + discover_providers() scanner - auth/google/: Google OAuth provider (extracted from webapp/auth.py) - auth/password/: Email/password provider (delegates to webapp/password_auth) - auth/desktop/: Desktop JWT auth (API-only, not visible on login page) - webapp/auth.py: stripped to core infra (login_required, /login, /logout) - webapp/app.py: auto-discovery loop replaces manual blueprint registration - login.html: dynamic provider buttons via Jinja loop
81 lines
2.2 KiB
Python
81 lines
2.2 KiB
Python
"""
|
|
Core authentication module - shared infrastructure.
|
|
|
|
Provides:
|
|
- login_required decorator (used by all auth methods)
|
|
- validate_email_domain() (used by all auth providers)
|
|
- /login route (dynamically renders available auth providers)
|
|
- /logout route
|
|
|
|
Auth provider-specific logic lives in auth/<provider>/provider.py.
|
|
"""
|
|
|
|
import functools
|
|
import logging
|
|
|
|
from flask import Blueprint, flash, redirect, render_template, session, url_for
|
|
|
|
from .config import Config
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
auth_bp = Blueprint("auth", __name__)
|
|
|
|
|
|
def login_required(f):
|
|
"""Decorator to require authentication for a route."""
|
|
|
|
@functools.wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if "user" not in session:
|
|
return redirect(url_for("auth.login"))
|
|
return f(*args, **kwargs)
|
|
|
|
return decorated_function
|
|
|
|
|
|
def validate_email_domain(email: str) -> bool:
|
|
"""Check if email belongs to allowed domain or whitelist.
|
|
|
|
Allows access for:
|
|
1. Configured allowed domain (for Google OAuth users)
|
|
2. Whitelisted emails (for password auth external users)
|
|
"""
|
|
if not email:
|
|
return False
|
|
email_lower = email.lower()
|
|
|
|
# Check whitelist first (for password auth users)
|
|
if email_lower in Config.ALLOWED_EMAILS:
|
|
return True
|
|
|
|
# Check domain (for Google OAuth users)
|
|
domain = email_lower.split("@")[-1]
|
|
return domain == Config.ALLOWED_DOMAIN.lower()
|
|
|
|
|
|
@auth_bp.route("/login")
|
|
def login():
|
|
"""Show login page with dynamically discovered auth providers."""
|
|
if "user" in session:
|
|
return redirect(url_for("dashboard"))
|
|
|
|
from auth import discover_providers
|
|
|
|
providers = discover_providers()
|
|
login_buttons = [
|
|
p.get_login_button()
|
|
for p in providers
|
|
if p.get_login_button().get("visible", True)
|
|
]
|
|
return render_template("login.html", login_buttons=login_buttons)
|
|
|
|
|
|
@auth_bp.route("/logout")
|
|
def logout():
|
|
"""Clear session and redirect to login."""
|
|
email = session.get("user", {}).get("email", "unknown")
|
|
session.clear()
|
|
logger.info(f"User logged out: {email}")
|
|
flash("You have been logged out.", "info")
|
|
return redirect(url_for("auth.login"))
|