Extract pluggable auth provider system into auth/ package
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
This commit is contained in:
parent
f2d3d156e3
commit
c6a711aa27
10 changed files with 374 additions and 99 deletions
102
auth/__init__.py
Normal file
102
auth/__init__.py
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
"""
|
||||||
|
Pluggable authentication provider system.
|
||||||
|
|
||||||
|
Each auth provider lives in auth/<name>/provider.py and implements AuthProvider.
|
||||||
|
Providers are auto-discovered and registered with the Flask app.
|
||||||
|
|
||||||
|
To add a new provider (e.g., Okta):
|
||||||
|
1. Create auth/okta/provider.py
|
||||||
|
2. Implement AuthProvider subclass
|
||||||
|
3. Export `provider` instance at module level
|
||||||
|
4. That's it - no changes to core code needed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import logging
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthProvider(ABC):
|
||||||
|
"""Base class for authentication providers."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_name(self) -> str:
|
||||||
|
"""Internal name (e.g., 'google', 'password')."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_blueprint(self) -> Blueprint:
|
||||||
|
"""Flask blueprint with auth routes."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_login_button(self) -> dict:
|
||||||
|
"""Login button definition for the login page.
|
||||||
|
|
||||||
|
Returns dict with:
|
||||||
|
text: str - Button label (e.g., "Sign in with Google")
|
||||||
|
url: str - Route URL (e.g., "/login/google")
|
||||||
|
icon_html: str - SVG or HTML for the icon
|
||||||
|
subtitle: str - Optional help text below button
|
||||||
|
order: int - Sort order (lower = higher on page)
|
||||||
|
css_class: str - Optional CSS class for the button (e.g., "btn-google")
|
||||||
|
visible: bool - Whether to show on login page (default True)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def is_available(self) -> bool:
|
||||||
|
"""Check if provider is configured and ready.
|
||||||
|
Override to check env vars, API keys, etc.
|
||||||
|
Returns False to skip this provider."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_display_name(self) -> str:
|
||||||
|
"""Human-readable name for UI."""
|
||||||
|
return self.get_name().title()
|
||||||
|
|
||||||
|
def init_app(self, app) -> None:
|
||||||
|
"""Optional: initialize provider with Flask app (e.g., for OAuth setup)."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def discover_providers() -> list[AuthProvider]:
|
||||||
|
"""Auto-discover auth providers from auth/*/provider.py.
|
||||||
|
|
||||||
|
Each provider module must export a `provider` instance of AuthProvider.
|
||||||
|
Providers are sorted by login button order.
|
||||||
|
Only available providers (is_available() == True) are returned.
|
||||||
|
"""
|
||||||
|
providers = []
|
||||||
|
auth_dir = Path(__file__).parent
|
||||||
|
|
||||||
|
for subdir in sorted(auth_dir.iterdir()):
|
||||||
|
if not subdir.is_dir() or subdir.name.startswith("_"):
|
||||||
|
continue
|
||||||
|
provider_file = subdir / "provider.py"
|
||||||
|
if not provider_file.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
mod = importlib.import_module(f"auth.{subdir.name}.provider")
|
||||||
|
provider_instance = getattr(mod, "provider", None)
|
||||||
|
if provider_instance and isinstance(provider_instance, AuthProvider):
|
||||||
|
if provider_instance.is_available():
|
||||||
|
providers.append(provider_instance)
|
||||||
|
logger.info(f"Auth provider loaded: {provider_instance.get_name()}")
|
||||||
|
else:
|
||||||
|
logger.debug(
|
||||||
|
f"Auth provider skipped (not available): {subdir.name}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"Auth provider {subdir.name} has no 'provider' instance"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to load auth provider {subdir.name}: {e}")
|
||||||
|
|
||||||
|
# Sort by login button order
|
||||||
|
providers.sort(key=lambda p: p.get_login_button().get("order", 50))
|
||||||
|
return providers
|
||||||
0
auth/desktop/__init__.py
Normal file
0
auth/desktop/__init__.py
Normal file
53
auth/desktop/provider.py
Normal file
53
auth/desktop/provider.py
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
"""
|
||||||
|
Desktop JWT authentication provider.
|
||||||
|
|
||||||
|
Wraps the existing webapp/desktop_auth.py blueprint.
|
||||||
|
This is NOT a login provider (no login button) - it provides
|
||||||
|
JWT-based API authentication for the native desktop application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
from auth import AuthProvider
|
||||||
|
from webapp.config import Config
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DesktopAuthProvider(AuthProvider):
|
||||||
|
"""Desktop app JWT authentication provider."""
|
||||||
|
|
||||||
|
def get_name(self) -> str:
|
||||||
|
return "desktop"
|
||||||
|
|
||||||
|
def get_display_name(self) -> str:
|
||||||
|
return "Desktop App"
|
||||||
|
|
||||||
|
def get_blueprint(self) -> Blueprint:
|
||||||
|
from webapp.desktop_auth import desktop_bp
|
||||||
|
|
||||||
|
return desktop_bp
|
||||||
|
|
||||||
|
def get_login_button(self) -> dict:
|
||||||
|
return {
|
||||||
|
"text": "",
|
||||||
|
"url": "",
|
||||||
|
"icon_html": "",
|
||||||
|
"subtitle": "",
|
||||||
|
"order": 100,
|
||||||
|
"css_class": "",
|
||||||
|
"visible": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def is_available(self) -> bool:
|
||||||
|
return bool(Config.DESKTOP_JWT_SECRET)
|
||||||
|
|
||||||
|
def init_app(self, app) -> None:
|
||||||
|
"""No additional initialization needed."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Module-level provider instance for auto-discovery
|
||||||
|
provider = DesktopAuthProvider()
|
||||||
0
auth/google/__init__.py
Normal file
0
auth/google/__init__.py
Normal file
124
auth/google/provider.py
Normal file
124
auth/google/provider.py
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
"""
|
||||||
|
Google OAuth authentication provider.
|
||||||
|
|
||||||
|
Handles Google Sign-In flow with domain validation.
|
||||||
|
Extracted from webapp/auth.py - the OAuth-specific routes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from authlib.integrations.flask_client import OAuth
|
||||||
|
from flask import Blueprint, flash, redirect, session, url_for
|
||||||
|
|
||||||
|
from auth import AuthProvider
|
||||||
|
from webapp.auth import validate_email_domain
|
||||||
|
from webapp.config import Config
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
google_bp = Blueprint("google_auth", __name__)
|
||||||
|
oauth = OAuth()
|
||||||
|
|
||||||
|
# Google SVG icon for the login button
|
||||||
|
_GOOGLE_ICON_HTML = (
|
||||||
|
'<svg class="google-icon" viewBox="0 0 24 24" width="24" height="24">'
|
||||||
|
'<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 '
|
||||||
|
"1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z\"/>"
|
||||||
|
'<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 '
|
||||||
|
"1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"/>"
|
||||||
|
'<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07'
|
||||||
|
'H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>'
|
||||||
|
'<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 '
|
||||||
|
'14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>'
|
||||||
|
"</svg>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@google_bp.route("/login/google")
|
||||||
|
def login_google():
|
||||||
|
"""Initiate Google OAuth flow."""
|
||||||
|
redirect_uri = url_for("google_auth.authorize", _external=True)
|
||||||
|
return oauth.google.authorize_redirect(redirect_uri)
|
||||||
|
|
||||||
|
|
||||||
|
@google_bp.route("/authorize")
|
||||||
|
def authorize():
|
||||||
|
"""Handle OAuth callback from Google."""
|
||||||
|
try:
|
||||||
|
token = oauth.google.authorize_access_token()
|
||||||
|
userinfo = token.get("userinfo")
|
||||||
|
|
||||||
|
if not userinfo:
|
||||||
|
logger.warning("No userinfo in OAuth response")
|
||||||
|
flash("Failed to get user information from Google.", "error")
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
|
email = userinfo.get("email", "")
|
||||||
|
name = userinfo.get("name", "")
|
||||||
|
|
||||||
|
# Validate domain
|
||||||
|
if not validate_email_domain(email):
|
||||||
|
logger.warning(f"Login attempt from non-allowed domain: {email}")
|
||||||
|
flash(
|
||||||
|
f"Only @{Config.ALLOWED_DOMAIN} email addresses are allowed.", "error"
|
||||||
|
)
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
|
# Store user info in session (shared contract across all providers)
|
||||||
|
session["user"] = {
|
||||||
|
"email": email,
|
||||||
|
"name": name,
|
||||||
|
"picture": userinfo.get("picture", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"User logged in via Google: {email}")
|
||||||
|
return redirect(url_for("dashboard"))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"OAuth error: {e}")
|
||||||
|
flash("Authentication failed. Please try again.", "error")
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleAuthProvider(AuthProvider):
|
||||||
|
"""Google OAuth authentication provider."""
|
||||||
|
|
||||||
|
def get_name(self) -> str:
|
||||||
|
return "google"
|
||||||
|
|
||||||
|
def get_display_name(self) -> str:
|
||||||
|
return "Google"
|
||||||
|
|
||||||
|
def get_blueprint(self) -> Blueprint:
|
||||||
|
return google_bp
|
||||||
|
|
||||||
|
def get_login_button(self) -> dict:
|
||||||
|
return {
|
||||||
|
"text": "Sign in with Google",
|
||||||
|
"url": "/login/google",
|
||||||
|
"icon_html": _GOOGLE_ICON_HTML,
|
||||||
|
"subtitle": f'For <strong>@{Config.ALLOWED_DOMAIN}</strong> email addresses.',
|
||||||
|
"order": 10,
|
||||||
|
"css_class": "btn-google",
|
||||||
|
"visible": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def is_available(self) -> bool:
|
||||||
|
return bool(Config.GOOGLE_CLIENT_ID)
|
||||||
|
|
||||||
|
def init_app(self, app) -> None:
|
||||||
|
"""Initialize OAuth with the Flask app."""
|
||||||
|
oauth.init_app(app)
|
||||||
|
oauth.register(
|
||||||
|
name="google",
|
||||||
|
client_id=Config.GOOGLE_CLIENT_ID,
|
||||||
|
client_secret=Config.GOOGLE_CLIENT_SECRET,
|
||||||
|
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
|
||||||
|
client_kwargs={
|
||||||
|
"scope": "openid email profile",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Module-level provider instance for auto-discovery
|
||||||
|
provider = GoogleAuthProvider()
|
||||||
0
auth/password/__init__.py
Normal file
0
auth/password/__init__.py
Normal file
52
auth/password/provider.py
Normal file
52
auth/password/provider.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
"""
|
||||||
|
Email/password authentication provider.
|
||||||
|
|
||||||
|
Wraps the existing webapp/password_auth.py blueprint.
|
||||||
|
Available only when SENDGRID_API_KEY is configured.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
from auth import AuthProvider
|
||||||
|
from webapp.config import Config
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordAuthProvider(AuthProvider):
|
||||||
|
"""Email/password authentication provider for external users."""
|
||||||
|
|
||||||
|
def get_name(self) -> str:
|
||||||
|
return "password"
|
||||||
|
|
||||||
|
def get_display_name(self) -> str:
|
||||||
|
return "Email"
|
||||||
|
|
||||||
|
def get_blueprint(self) -> Blueprint:
|
||||||
|
from webapp.password_auth import password_auth_bp
|
||||||
|
|
||||||
|
return password_auth_bp
|
||||||
|
|
||||||
|
def get_login_button(self) -> dict:
|
||||||
|
return {
|
||||||
|
"text": "Sign in with Email",
|
||||||
|
"url": "/login/email",
|
||||||
|
"icon_html": "",
|
||||||
|
"subtitle": "For external users (investors, partners).",
|
||||||
|
"order": 20,
|
||||||
|
"css_class": "btn-secondary",
|
||||||
|
"visible": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def is_available(self) -> bool:
|
||||||
|
return bool(Config.SENDGRID_API_KEY)
|
||||||
|
|
||||||
|
def init_app(self, app) -> None:
|
||||||
|
"""No additional initialization needed."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Module-level provider instance for auto-discovery
|
||||||
|
provider = PasswordAuthProvider()
|
||||||
|
|
@ -15,9 +15,9 @@ from pathlib import Path
|
||||||
|
|
||||||
from flask import Flask, flash, jsonify, redirect, render_template, request, session, url_for
|
from flask import Flask, flash, jsonify, redirect, render_template, request, session, url_for
|
||||||
|
|
||||||
from .auth import auth_bp, init_oauth, login_required
|
from .auth import auth_bp, login_required
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .desktop_auth import desktop_bp, require_desktop_auth
|
from .desktop_auth import require_desktop_auth
|
||||||
from .notification_images import images_bp
|
from .notification_images import images_bp
|
||||||
from .account_service import get_account_details
|
from .account_service import get_account_details
|
||||||
from .sync_settings_service import get_sync_settings, update_sync_settings
|
from .sync_settings_service import get_sync_settings, update_sync_settings
|
||||||
|
|
@ -29,14 +29,6 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
JIRA_AVAILABLE = False
|
JIRA_AVAILABLE = False
|
||||||
jira_bp = None
|
jira_bp = None
|
||||||
|
|
||||||
# Password auth is optional - requires SENDGRID_API_KEY
|
|
||||||
try:
|
|
||||||
from .password_auth import password_auth_bp
|
|
||||||
PASSWORD_AUTH_AVAILABLE = True
|
|
||||||
except ImportError:
|
|
||||||
PASSWORD_AUTH_AVAILABLE = False
|
|
||||||
password_auth_bp = None
|
|
||||||
from .telegram_service import get_telegram_status, link_telegram, unlink_telegram
|
from .telegram_service import get_telegram_status, link_telegram, unlink_telegram
|
||||||
from .corporate_memory_service import (
|
from .corporate_memory_service import (
|
||||||
get_knowledge,
|
get_knowledge,
|
||||||
|
|
@ -73,17 +65,20 @@ def create_app() -> Flask:
|
||||||
for error in errors:
|
for error in errors:
|
||||||
logger.warning(f"Configuration warning: {error}")
|
logger.warning(f"Configuration warning: {error}")
|
||||||
|
|
||||||
# Initialize OAuth
|
# Register core auth blueprint (login_required, login page, logout)
|
||||||
init_oauth(app)
|
|
||||||
|
|
||||||
# Register blueprints
|
|
||||||
app.register_blueprint(auth_bp)
|
app.register_blueprint(auth_bp)
|
||||||
app.register_blueprint(desktop_bp)
|
|
||||||
|
# Auto-discover and register auth providers
|
||||||
|
from auth import discover_providers
|
||||||
|
|
||||||
|
for provider_instance in discover_providers():
|
||||||
|
provider_instance.init_app(app)
|
||||||
|
app.register_blueprint(provider_instance.get_blueprint())
|
||||||
|
|
||||||
|
# Register other blueprints
|
||||||
app.register_blueprint(images_bp)
|
app.register_blueprint(images_bp)
|
||||||
if JIRA_AVAILABLE and jira_bp:
|
if JIRA_AVAILABLE and jira_bp:
|
||||||
app.register_blueprint(jira_bp)
|
app.register_blueprint(jira_bp)
|
||||||
if PASSWORD_AUTH_AVAILABLE and password_auth_bp:
|
|
||||||
app.register_blueprint(password_auth_bp)
|
|
||||||
|
|
||||||
# Register main routes
|
# Register main routes
|
||||||
register_routes(app)
|
register_routes(app)
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,25 @@
|
||||||
"""
|
"""
|
||||||
Google OAuth authentication module.
|
Core authentication module - shared infrastructure.
|
||||||
|
|
||||||
Handles Google Sign-In flow and domain validation.
|
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 functools
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from authlib.integrations.flask_client import OAuth
|
from flask import Blueprint, flash, redirect, render_template, session, url_for
|
||||||
from flask import Blueprint, current_app, flash, redirect, session, url_for
|
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
auth_bp = Blueprint("auth", __name__)
|
auth_bp = Blueprint("auth", __name__)
|
||||||
oauth = OAuth()
|
|
||||||
|
|
||||||
|
|
||||||
def init_oauth(app):
|
|
||||||
"""Initialize OAuth with the Flask app."""
|
|
||||||
oauth.init_app(app)
|
|
||||||
|
|
||||||
oauth.register(
|
|
||||||
name="google",
|
|
||||||
client_id=Config.GOOGLE_CLIENT_ID,
|
|
||||||
client_secret=Config.GOOGLE_CLIENT_SECRET,
|
|
||||||
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
|
|
||||||
client_kwargs={
|
|
||||||
"scope": "openid email profile",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def login_required(f):
|
def login_required(f):
|
||||||
|
|
@ -67,58 +56,19 @@ def validate_email_domain(email: str) -> bool:
|
||||||
|
|
||||||
@auth_bp.route("/login")
|
@auth_bp.route("/login")
|
||||||
def login():
|
def login():
|
||||||
"""Show login page or redirect to dashboard if already logged in."""
|
"""Show login page with dynamically discovered auth providers."""
|
||||||
if "user" in session:
|
if "user" in session:
|
||||||
return redirect(url_for("dashboard"))
|
return redirect(url_for("dashboard"))
|
||||||
from flask import render_template
|
|
||||||
|
|
||||||
return render_template("login.html")
|
from auth import discover_providers
|
||||||
|
|
||||||
|
providers = discover_providers()
|
||||||
@auth_bp.route("/login/google")
|
login_buttons = [
|
||||||
def login_google():
|
p.get_login_button()
|
||||||
"""Initiate Google OAuth flow."""
|
for p in providers
|
||||||
redirect_uri = url_for("auth.authorize", _external=True)
|
if p.get_login_button().get("visible", True)
|
||||||
return oauth.google.authorize_redirect(redirect_uri)
|
]
|
||||||
|
return render_template("login.html", login_buttons=login_buttons)
|
||||||
|
|
||||||
@auth_bp.route("/authorize")
|
|
||||||
def authorize():
|
|
||||||
"""Handle OAuth callback from Google."""
|
|
||||||
try:
|
|
||||||
token = oauth.google.authorize_access_token()
|
|
||||||
userinfo = token.get("userinfo")
|
|
||||||
|
|
||||||
if not userinfo:
|
|
||||||
logger.warning("No userinfo in OAuth response")
|
|
||||||
flash("Failed to get user information from Google.", "error")
|
|
||||||
return redirect(url_for("auth.login"))
|
|
||||||
|
|
||||||
email = userinfo.get("email", "")
|
|
||||||
name = userinfo.get("name", "")
|
|
||||||
|
|
||||||
# Validate domain
|
|
||||||
if not validate_email_domain(email):
|
|
||||||
logger.warning(f"Login attempt from non-allowed domain: {email}")
|
|
||||||
flash(
|
|
||||||
f"Only @{Config.ALLOWED_DOMAIN} email addresses are allowed.", "error"
|
|
||||||
)
|
|
||||||
return redirect(url_for("auth.login"))
|
|
||||||
|
|
||||||
# Store user info in session
|
|
||||||
session["user"] = {
|
|
||||||
"email": email,
|
|
||||||
"name": name,
|
|
||||||
"picture": userinfo.get("picture", ""),
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(f"User logged in: {email}")
|
|
||||||
return redirect(url_for("dashboard"))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception(f"OAuth error: {e}")
|
|
||||||
flash("Authentication failed. Please try again.", "error")
|
|
||||||
return redirect(url_for("auth.login"))
|
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/logout")
|
@auth_bp.route("/logout")
|
||||||
|
|
|
||||||
|
|
@ -95,31 +95,30 @@
|
||||||
Access your AI data analysis workspace
|
Access your AI data analysis workspace
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a href="{{ url_for('auth.login_google') }}" class="btn btn-google">
|
{% for btn in login_buttons %}
|
||||||
<svg class="google-icon" viewBox="0 0 24 24" width="24" height="24">
|
<a href="{{ btn.url }}" class="btn {{ btn.css_class|default('btn-secondary') }}" style="width: 100%; max-width: 280px;">
|
||||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
{% if btn.icon_html %}{{ btn.icon_html|safe }}{% endif %}
|
||||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
{{ btn.text }}
|
||||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
|
||||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
|
||||||
</svg>
|
|
||||||
Sign in with Google
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
{% if btn.subtitle %}
|
||||||
<p class="login-note">
|
<p class="login-note">
|
||||||
For <strong>@{{ config.ALLOWED_DOMAIN }}</strong> email addresses.
|
{{ btn.subtitle|safe }}
|
||||||
</p>
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not loop.last %}
|
||||||
<div class="divider">
|
<div class="divider">
|
||||||
<span>or</span>
|
<span>or</span>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
<a href="{{ url_for('password_auth.login_email') }}" class="btn btn-secondary" style="width: 100%; max-width: 280px;">
|
{% if not login_buttons %}
|
||||||
Sign in with Email
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<p class="login-note">
|
<p class="login-note">
|
||||||
For external users (investors, partners).
|
No authentication providers are configured. Please set up at least one provider.
|
||||||
</p>
|
</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue