diff --git a/app/auth.py b/app/auth.py
index 5b92b156..0090cf08 100644
--- a/app/auth.py
+++ b/app/auth.py
@@ -102,6 +102,24 @@ if AUTH_ENABLED and settings.social_auth_apple_enabled:
else:
logger.warning("SOCIAL_AUTH_APPLE_ENABLED=true but client ID/team ID not configured")
+
+def _dropbox_userinfo_compliance_fix(client, user_cls, token, data):
+ """Normalize Dropbox userinfo response for authlib compatibility.
+
+ Dropbox's /2/users/get_current_account returns a non-standard response
+ format. This compliance fix normalizes the response data — the HTTP
+ method (POST) is handled by authlib's compliance infrastructure.
+ """
+ # Dropbox returns account_id instead of sub
+ if "account_id" in data and "sub" not in data:
+ data["sub"] = data["account_id"]
+ # Normalize name field
+ name_info = data.get("name", {})
+ if isinstance(name_info, dict) and "display_name" in name_info:
+ data["name"] = name_info["display_name"]
+ return data
+
+
if AUTH_ENABLED and settings.social_auth_dropbox_enabled:
# Determine which credentials to use for Dropbox social login
_dropbox_client_id = settings.social_auth_dropbox_client_id
@@ -118,13 +136,77 @@ if AUTH_ENABLED and settings.social_auth_dropbox_enabled:
authorize_url="https://www.dropbox.com/oauth2/authorize",
access_token_url="https://api.dropboxapi.com/oauth2/token",
userinfo_endpoint="https://api.dropboxapi.com/2/users/get_current_account",
- client_kwargs={"token_endpoint_auth_method": "client_secret_post"},
+ userinfo_compliance_fix=_dropbox_userinfo_compliance_fix,
+ client_kwargs={
+ "token_endpoint_auth_method": "client_secret_post",
+ "token_access_type": "offline",
+ },
)
SOCIAL_PROVIDERS["dropbox"] = {"name": "Dropbox", "icon": "fab fa-dropbox", "color": "blue"}
logger.info("Social login provider registered: Dropbox")
else:
logger.warning("SOCIAL_AUTH_DROPBOX_ENABLED=true but client ID/secret not configured")
+if AUTH_ENABLED and settings.social_auth_github_enabled:
+ if settings.social_auth_github_client_id and settings.social_auth_github_client_secret:
+ oauth.register(
+ name="github",
+ client_id=settings.social_auth_github_client_id,
+ client_secret=settings.social_auth_github_client_secret,
+ authorize_url="https://github.com/login/oauth/authorize",
+ access_token_url="https://github.com/login/oauth/access_token",
+ userinfo_endpoint="https://api.github.com/user",
+ client_kwargs={"scope": "read:user user:email"},
+ )
+ SOCIAL_PROVIDERS["github"] = {"name": "GitHub", "icon": "fab fa-github", "color": "gray"}
+ logger.info("Social login provider registered: GitHub")
+ else:
+ logger.warning("SOCIAL_AUTH_GITHUB_ENABLED=true but client ID/secret not configured")
+
+if AUTH_ENABLED and settings.social_auth_keycloak_enabled:
+ _kc_server = settings.social_auth_keycloak_server_url
+ _kc_realm = settings.social_auth_keycloak_realm
+ if (
+ settings.social_auth_keycloak_client_id
+ and settings.social_auth_keycloak_client_secret
+ and _kc_server
+ and _kc_realm
+ ):
+ _kc_base = f"{_kc_server.rstrip('/')}/realms/{_kc_realm}"
+ oauth.register(
+ name="keycloak",
+ client_id=settings.social_auth_keycloak_client_id,
+ client_secret=settings.social_auth_keycloak_client_secret,
+ server_metadata_url=f"{_kc_base}/.well-known/openid-configuration",
+ client_kwargs={"scope": "openid profile email"},
+ )
+ SOCIAL_PROVIDERS["keycloak"] = {"name": "Keycloak", "icon": "fas fa-key", "color": "gray"}
+ logger.info("Social login provider registered: Keycloak (realm=%s)", _kc_realm)
+ else:
+ logger.warning("SOCIAL_AUTH_KEYCLOAK_ENABLED=true but required settings not configured")
+
+if AUTH_ENABLED and settings.social_auth_generic_oauth2_enabled:
+ if (
+ settings.social_auth_generic_oauth2_client_id
+ and settings.social_auth_generic_oauth2_client_secret
+ and settings.social_auth_generic_oauth2_authorize_url
+ and settings.social_auth_generic_oauth2_token_url
+ ):
+ oauth.register(
+ name="generic_oauth2",
+ client_id=settings.social_auth_generic_oauth2_client_id,
+ client_secret=settings.social_auth_generic_oauth2_client_secret,
+ authorize_url=settings.social_auth_generic_oauth2_authorize_url,
+ access_token_url=settings.social_auth_generic_oauth2_token_url,
+ userinfo_endpoint=settings.social_auth_generic_oauth2_userinfo_url,
+ client_kwargs={"scope": settings.social_auth_generic_oauth2_scope},
+ )
+ _generic_name = settings.social_auth_generic_oauth2_name or "OAuth2"
+ SOCIAL_PROVIDERS["generic_oauth2"] = {"name": _generic_name, "icon": "fas fa-sign-in-alt", "color": "indigo"}
+ logger.info("Social login provider registered: Generic OAuth2 (%s)", _generic_name)
+ else:
+ logger.warning("SOCIAL_AUTH_GENERIC_OAUTH2_ENABLED=true but required settings not configured")
+
router = APIRouter()
@@ -348,13 +430,21 @@ async def login(request: Request):
get_client_ip(request),
)
+ error = request.query_params.get("error")
+ message = request.query_params.get("message")
+ show_oauth = OAUTH_CONFIGURED
+
+ # SSO Auto Login: redirect directly to SSO provider if configured
+ if show_oauth and settings.sso_auto_login and not error and not message:
+ return RedirectResponse(url="/oauth-login", status_code=status.HTTP_302_FOUND)
+
return templates.TemplateResponse(
"login.html",
{
"request": request,
- "error": request.query_params.get("error"),
- "message": request.query_params.get("message"),
- "show_oauth": OAUTH_CONFIGURED,
+ "error": error,
+ "message": message,
+ "show_oauth": show_oauth,
"oauth_provider_name": OAUTH_PROVIDER_NAME,
"social_providers": SOCIAL_PROVIDERS,
"app_version": settings.version,
@@ -440,6 +530,17 @@ def _normalize_social_userinfo(provider: str, token: dict, raw_userinfo: dict |
"picture": userinfo.get("profile_photo_url", ""),
}
+ if provider == "github":
+ # GitHub returns login, id, name, email, avatar_url
+ email = userinfo.get("email", "")
+ return {
+ "sub": str(userinfo.get("id", "")),
+ "email": email,
+ "name": userinfo.get("name", "") or userinfo.get("login", ""),
+ "preferred_username": userinfo.get("login", email),
+ "picture": userinfo.get("avatar_url", ""),
+ }
+
# Standard OIDC providers (Google, Microsoft, Apple)
return {
"sub": userinfo.get("sub", ""),
diff --git a/app/config.py b/app/config.py
index ab0bfd4c..99c12757 100644
--- a/app/config.py
+++ b/app/config.py
@@ -305,6 +305,38 @@ class Settings(BaseSettings):
authentik_client_secret: Optional[str] = None
authentik_config_url: Optional[str] = None
oauth_provider_name: Optional[str] = None # Name to display for the OAuth provider
+ sso_auto_login: bool = Field(
+ default=False,
+ description=(
+ "Automatically redirect to SSO login when authentication is required. "
+ "When enabled, users are sent directly to the SSO provider instead of "
+ "seeing the login page. Only effective when OIDC is configured."
+ ),
+ )
+
+ # Keycloak SSO
+ social_auth_keycloak_enabled: bool = False
+ social_auth_keycloak_client_id: Optional[str] = None
+ social_auth_keycloak_client_secret: Optional[str] = None
+ social_auth_keycloak_server_url: Optional[str] = None
+ social_auth_keycloak_realm: Optional[str] = None
+
+ # Generic OAuth2 SSO
+ social_auth_generic_oauth2_enabled: bool = False
+ social_auth_generic_oauth2_client_id: Optional[str] = None
+ social_auth_generic_oauth2_client_secret: Optional[str] = None
+ social_auth_generic_oauth2_authorize_url: Optional[str] = None
+ social_auth_generic_oauth2_token_url: Optional[str] = None
+ social_auth_generic_oauth2_userinfo_url: Optional[str] = None
+ social_auth_generic_oauth2_scope: str = "openid profile email"
+ social_auth_generic_oauth2_name: str = "OAuth2"
+
+ # SAML2 SSO
+ social_auth_saml2_enabled: bool = False
+ social_auth_saml2_entity_id: Optional[str] = None
+ social_auth_saml2_sso_url: Optional[str] = None
+ social_auth_saml2_certificate: Optional[str] = None
+ social_auth_saml2_name: str = "SAML2"
# Social Login Providers
# Google OAuth2
@@ -348,6 +380,11 @@ class Settings(BaseSettings):
),
)
+ # GitHub OAuth2
+ social_auth_github_enabled: bool = False
+ social_auth_github_client_id: Optional[str] = None
+ social_auth_github_client_secret: Optional[str] = None
+
# Local user signup
allow_local_signup: bool = Field(
default=False,
@@ -844,6 +881,11 @@ class Settings(BaseSettings):
),
)
+ # Telegram Bot
+ telegram_bot_token: Optional[str] = None
+ telegram_chat_id: Optional[str] = None
+ telegram_enabled: bool = False
+
# Notification settings
notification_urls: Union[List[str], str] = Field(
default_factory=list,
diff --git a/app/utils/settings_service.py b/app/utils/settings_service.py
index 0c990143..9388bf97 100644
--- a/app/utils/settings_service.py
+++ b/app/utils/settings_service.py
@@ -262,6 +262,17 @@ SETTING_METADATA = {
"required": False,
"restart_required": True,
},
+ "sso_auto_login": {
+ "category": "Authentication",
+ "description": (
+ "Automatically redirect to SSO login when authentication is required. "
+ "Skips the login page and sends users directly to the configured SSO provider."
+ ),
+ "type": "boolean",
+ "sensitive": False,
+ "required": False,
+ "restart_required": False,
+ },
# Social Login Providers
"social_auth_google_enabled": {
"category": "Social Login",
@@ -422,6 +433,182 @@ SETTING_METADATA = {
"required": False,
"restart_required": True,
},
+ "social_auth_github_enabled": {
+ "category": "Social Login",
+ "description": (
+ "Enable GitHub Sign-In. Requires SOCIAL_AUTH_GITHUB_CLIENT_ID and "
+ "SOCIAL_AUTH_GITHUB_CLIENT_SECRET from GitHub Developer Settings."
+ ),
+ "type": "boolean",
+ "sensitive": False,
+ "required": False,
+ "restart_required": True,
+ "help_link": "https://github.com/settings/developers",
+ "help_link_label": "GitHub Developer Settings",
+ },
+ "social_auth_github_client_id": {
+ "category": "Social Login",
+ "description": "GitHub OAuth2 client ID from GitHub Developer Settings.",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": True,
+ },
+ "social_auth_github_client_secret": {
+ "category": "Social Login",
+ "description": "GitHub OAuth2 client secret from GitHub Developer Settings.",
+ "type": "string",
+ "sensitive": True,
+ "required": False,
+ "restart_required": True,
+ },
+ # Keycloak SSO
+ "social_auth_keycloak_enabled": {
+ "category": "Social Login",
+ "description": "Enable Keycloak SSO. Requires server URL, realm, client ID, and client secret.",
+ "type": "boolean",
+ "sensitive": False,
+ "required": False,
+ "restart_required": True,
+ },
+ "social_auth_keycloak_client_id": {
+ "category": "Social Login",
+ "description": "Keycloak OAuth2 client ID.",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": True,
+ },
+ "social_auth_keycloak_client_secret": {
+ "category": "Social Login",
+ "description": "Keycloak OAuth2 client secret.",
+ "type": "string",
+ "sensitive": True,
+ "required": False,
+ "restart_required": True,
+ },
+ "social_auth_keycloak_server_url": {
+ "category": "Social Login",
+ "description": "Keycloak server base URL (e.g. https://keycloak.example.com).",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": True,
+ },
+ "social_auth_keycloak_realm": {
+ "category": "Social Login",
+ "description": "Keycloak realm name.",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": True,
+ },
+ # Generic OAuth2 SSO
+ "social_auth_generic_oauth2_enabled": {
+ "category": "Social Login",
+ "description": "Enable a generic OAuth2 SSO provider.",
+ "type": "boolean",
+ "sensitive": False,
+ "required": False,
+ "restart_required": True,
+ },
+ "social_auth_generic_oauth2_client_id": {
+ "category": "Social Login",
+ "description": "Generic OAuth2 client ID.",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": True,
+ },
+ "social_auth_generic_oauth2_client_secret": {
+ "category": "Social Login",
+ "description": "Generic OAuth2 client secret.",
+ "type": "string",
+ "sensitive": True,
+ "required": False,
+ "restart_required": True,
+ },
+ "social_auth_generic_oauth2_authorize_url": {
+ "category": "Social Login",
+ "description": "Generic OAuth2 authorization URL.",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": True,
+ },
+ "social_auth_generic_oauth2_token_url": {
+ "category": "Social Login",
+ "description": "Generic OAuth2 token endpoint URL.",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": True,
+ },
+ "social_auth_generic_oauth2_userinfo_url": {
+ "category": "Social Login",
+ "description": "Generic OAuth2 userinfo endpoint URL.",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": True,
+ },
+ "social_auth_generic_oauth2_scope": {
+ "category": "Social Login",
+ "description": "Space-separated list of OAuth2 scopes to request (default: openid profile email).",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": True,
+ },
+ "social_auth_generic_oauth2_name": {
+ "category": "Social Login",
+ "description": "Display name for the generic OAuth2 provider button on the login page.",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": True,
+ },
+ # SAML2 SSO
+ "social_auth_saml2_enabled": {
+ "category": "Social Login",
+ "description": "Enable SAML2 SSO authentication.",
+ "type": "boolean",
+ "sensitive": False,
+ "required": False,
+ "restart_required": True,
+ },
+ "social_auth_saml2_entity_id": {
+ "category": "Social Login",
+ "description": "SAML2 Identity Provider Entity ID.",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": True,
+ },
+ "social_auth_saml2_sso_url": {
+ "category": "Social Login",
+ "description": "SAML2 Identity Provider SSO URL.",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": True,
+ },
+ "social_auth_saml2_certificate": {
+ "category": "Social Login",
+ "description": "SAML2 Identity Provider X.509 certificate (PEM format).",
+ "type": "string",
+ "sensitive": True,
+ "required": False,
+ "restart_required": True,
+ },
+ "social_auth_saml2_name": {
+ "category": "Social Login",
+ "description": "Display name for the SAML2 provider.",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": True,
+ },
# AI Services
"openai_api_key": {
"category": "AI Services",
@@ -1952,6 +2139,30 @@ SETTING_METADATA = {
"required": False,
"restart_required": False,
},
+ "telegram_enabled": {
+ "category": "Notifications",
+ "description": "Enable Telegram bot notifications.",
+ "type": "boolean",
+ "sensitive": False,
+ "required": False,
+ "restart_required": False,
+ },
+ "telegram_bot_token": {
+ "category": "Notifications",
+ "description": "Telegram Bot API token from @BotFather.",
+ "type": "string",
+ "sensitive": True,
+ "required": False,
+ "restart_required": False,
+ },
+ "telegram_chat_id": {
+ "category": "Notifications",
+ "description": "Telegram chat ID to send notifications to.",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": False,
+ },
# Notifications Settings
"notification_urls": {
"category": "Notifications",
diff --git a/app/views/settings.py b/app/views/settings.py
index 1e520e33..25ccccea 100644
--- a/app/views/settings.py
+++ b/app/views/settings.py
@@ -195,6 +195,299 @@ async def credentials_page(request: Request, db: Session = Depends(get_db)):
)
+@router.get("/admin/connections")
+@require_login
+@require_admin_access
+async def connections_page(request: Request, db: Session = Depends(get_db)):
+ """
+ Connections management page - admin only.
+
+ Allows administrators to configure external authentication providers,
+ SSO settings, and service integrations through a wizard-like interface.
+ """
+ try:
+ from app.auth import OAUTH_CONFIGURED, SOCIAL_PROVIDERS
+
+ db_settings = get_all_settings_from_db(db)
+
+ def _get_effective(key: str):
+ """Return DB value if present, else fall back to settings attr."""
+ if key in db_settings and db_settings[key] is not None:
+ return db_settings[key]
+ return getattr(settings, key, None)
+
+ def _is_truthy(val) -> bool:
+ if isinstance(val, bool):
+ return val
+ if isinstance(val, str):
+ return val.lower() in ("true", "1", "yes")
+ return bool(val)
+
+ # Build service status list
+ services = []
+
+ # --- SSO (Authentik / OIDC) ---
+ services.append(
+ {
+ "key": "oidc",
+ "name": settings.oauth_provider_name or "Single Sign-On",
+ "icon": "fas fa-lock",
+ "type": "SSO",
+ "linked": OAUTH_CONFIGURED,
+ "description": "OpenID Connect SSO provider",
+ "settings_keys": [
+ "authentik_client_id",
+ "authentik_client_secret",
+ "authentik_config_url",
+ "oauth_provider_name",
+ ],
+ }
+ )
+
+ # --- Google ---
+ services.append(
+ {
+ "key": "google",
+ "name": "Google",
+ "icon": "fab fa-google",
+ "type": "Sign-in authentication",
+ "linked": "google" in SOCIAL_PROVIDERS,
+ "description": "Sign-in authentication",
+ "settings_keys": [
+ "social_auth_google_enabled",
+ "social_auth_google_client_id",
+ "social_auth_google_client_secret",
+ ],
+ }
+ )
+
+ # --- GitHub ---
+ services.append(
+ {
+ "key": "github",
+ "name": "GitHub",
+ "icon": "fab fa-github",
+ "type": "Sign-in authentication",
+ "linked": "github" in SOCIAL_PROVIDERS,
+ "description": "Sign-in authentication",
+ "settings_keys": [
+ "social_auth_github_enabled",
+ "social_auth_github_client_id",
+ "social_auth_github_client_secret",
+ ],
+ }
+ )
+
+ # --- Microsoft ---
+ services.append(
+ {
+ "key": "microsoft",
+ "name": "Microsoft",
+ "icon": "fab fa-microsoft",
+ "type": "Sign-in authentication",
+ "linked": "microsoft" in SOCIAL_PROVIDERS,
+ "description": "Sign-in authentication",
+ "settings_keys": [
+ "social_auth_microsoft_enabled",
+ "social_auth_microsoft_client_id",
+ "social_auth_microsoft_client_secret",
+ "social_auth_microsoft_tenant",
+ ],
+ }
+ )
+
+ # --- Apple ---
+ services.append(
+ {
+ "key": "apple",
+ "name": "Apple",
+ "icon": "fab fa-apple",
+ "type": "Sign-in authentication",
+ "linked": "apple" in SOCIAL_PROVIDERS,
+ "description": "Sign-in authentication",
+ "settings_keys": [
+ "social_auth_apple_enabled",
+ "social_auth_apple_client_id",
+ "social_auth_apple_team_id",
+ "social_auth_apple_key_id",
+ "social_auth_apple_private_key",
+ ],
+ }
+ )
+
+ # --- Dropbox ---
+ services.append(
+ {
+ "key": "dropbox",
+ "name": "Dropbox",
+ "icon": "fab fa-dropbox",
+ "type": "Sign-in authentication",
+ "linked": "dropbox" in SOCIAL_PROVIDERS,
+ "description": "Sign-in authentication",
+ "settings_keys": [
+ "social_auth_dropbox_enabled",
+ "social_auth_dropbox_client_id",
+ "social_auth_dropbox_client_secret",
+ "social_auth_dropbox_use_global_credentials",
+ ],
+ }
+ )
+
+ # --- Keycloak ---
+ services.append(
+ {
+ "key": "keycloak",
+ "name": "Keycloak",
+ "icon": "fas fa-key",
+ "type": "SSO",
+ "linked": "keycloak" in SOCIAL_PROVIDERS,
+ "description": "SSO",
+ "settings_keys": [
+ "social_auth_keycloak_enabled",
+ "social_auth_keycloak_client_id",
+ "social_auth_keycloak_client_secret",
+ "social_auth_keycloak_server_url",
+ "social_auth_keycloak_realm",
+ ],
+ }
+ )
+
+ # --- Generic OAuth2 ---
+ services.append(
+ {
+ "key": "generic_oauth2",
+ "name": "Generic OAuth2",
+ "icon": "fas fa-sign-in-alt",
+ "type": "SSO",
+ "linked": "generic_oauth2" in SOCIAL_PROVIDERS,
+ "description": "SSO",
+ "settings_keys": [
+ "social_auth_generic_oauth2_enabled",
+ "social_auth_generic_oauth2_client_id",
+ "social_auth_generic_oauth2_client_secret",
+ "social_auth_generic_oauth2_authorize_url",
+ "social_auth_generic_oauth2_token_url",
+ "social_auth_generic_oauth2_userinfo_url",
+ "social_auth_generic_oauth2_scope",
+ "social_auth_generic_oauth2_name",
+ ],
+ }
+ )
+
+ # --- SAML2 ---
+ _saml2_configured = bool(
+ _is_truthy(_get_effective("social_auth_saml2_enabled"))
+ and _get_effective("social_auth_saml2_sso_url")
+ and _get_effective("social_auth_saml2_entity_id")
+ )
+ services.append(
+ {
+ "key": "saml2",
+ "name": settings.social_auth_saml2_name or "SAML2",
+ "icon": "fas fa-id-badge",
+ "type": "SSO (SAML)",
+ "linked": _saml2_configured,
+ "description": "SSO (SAML)",
+ "settings_keys": [
+ "social_auth_saml2_enabled",
+ "social_auth_saml2_entity_id",
+ "social_auth_saml2_sso_url",
+ "social_auth_saml2_certificate",
+ "social_auth_saml2_name",
+ ],
+ }
+ )
+
+ # --- SMTP Mail ---
+ _smtp_configured = bool(_get_effective("email_host") and _get_effective("email_username"))
+ services.append(
+ {
+ "key": "smtp",
+ "name": "SMTP Mail",
+ "icon": "fas fa-envelope",
+ "type": "Email Notifications",
+ "linked": _smtp_configured,
+ "description": "Email Notifications",
+ "settings_keys": [
+ "email_host",
+ "email_port",
+ "email_username",
+ "email_password",
+ "email_use_tls",
+ "email_sender",
+ ],
+ }
+ )
+
+ # --- Telegram Bot ---
+ _telegram_configured = bool(
+ _is_truthy(_get_effective("telegram_enabled")) and _get_effective("telegram_bot_token")
+ )
+ services.append(
+ {
+ "key": "telegram",
+ "name": "Telegram Bot",
+ "icon": "fab fa-telegram",
+ "type": "Notifications",
+ "linked": _telegram_configured,
+ "description": "Configure Telegram bot connectivity, access controls, and feedback behavior.",
+ "settings_keys": [
+ "telegram_enabled",
+ "telegram_bot_token",
+ "telegram_chat_id",
+ ],
+ }
+ )
+
+ # Get setting details for the modal forms
+ service_settings = {}
+ for svc in services:
+ svc_settings = []
+ for skey in svc["settings_keys"]:
+ meta = get_setting_metadata(skey)
+ # Get current effective value
+ val = _get_effective(skey)
+ display_val = val
+ if meta.get("sensitive") and val:
+ display_val = mask_sensitive_value(val)
+ svc_settings.append(
+ {
+ "key": skey,
+ "value": val,
+ "display_value": display_val if display_val is not None else "",
+ "metadata": meta,
+ }
+ )
+ service_settings[svc["key"]] = svc_settings
+
+ # Feature toggles
+ sso_auto_login = _is_truthy(_get_effective("sso_auto_login"))
+ qr_login_enabled = _is_truthy(_get_effective("qr_login_challenge_ttl_seconds"))
+ frontend_url_configured = bool(_get_effective("public_base_url"))
+
+ return templates.TemplateResponse(
+ "admin_connections.html",
+ {
+ "request": request,
+ "services": services,
+ "service_settings": service_settings,
+ "sso_auto_login": sso_auto_login,
+ "oauth_configured": OAUTH_CONFIGURED,
+ "qr_login_enabled": qr_login_enabled,
+ "frontend_url_configured": frontend_url_configured,
+ "app_version": settings.version,
+ },
+ )
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error loading connections page: {e}")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail="Failed to load connections page",
+ )
+
+
@router.get("/admin/settings/audit-log")
@require_login
@require_admin_access
diff --git a/frontend/templates/admin_connections.html b/frontend/templates/admin_connections.html
new file mode 100644
index 00000000..aca4f105
--- /dev/null
+++ b/frontend/templates/admin_connections.html
@@ -0,0 +1,305 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("connections.title") }} - {{ _("app.name") }}{% endblock %}
+
+{% block content %}
+
+
+
{{ _("connections.title") }}
+
{{ _("connections.description") }}
+
+
+
+ {% if oauth_configured %}
+
+
+
+
{{ _("connections.sso_auto_login_title") }}
+
{{ _("connections.sso_auto_login_description") }}
+
+
+
+
+
+
+ {% endif %}
+
+
+
+
+
+
{{ _("connections.mobile_upload_title") }}
+
{{ _("connections.mobile_upload_description") }}
+ {% if not frontend_url_configured %}
+
+
+ {{ _("connections.frontend_url_note") }}
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+ {{ _("connections.unlinked_services") }}
+
+ {% for service in services %}
+
+
+
+
+
+
+
+
{{ service.name }}
+
{{ service.description }}
+
+
+
+ {% if service.linked %}
+
+ {{ _("connections.linked") }}
+
+ {% else %}
+
+ {{ _("connections.unlinked") }}
+
+ {% endif %}
+
+
+
+
+ {% endfor %}
+
+
+
+
+
+ {{ _("connections.save_note") }}
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/frontend/templates/base.html b/frontend/templates/base.html
index b7e47e44..afd24408 100644
--- a/frontend/templates/base.html
+++ b/frontend/templates/base.html
@@ -155,6 +155,9 @@
{{ _("nav.credentials") }}
+
+ {{ _("nav.connections") }}
+
{{ _("nav.file_manager") }}
@@ -429,6 +432,9 @@
{{ _("nav.credentials") }}
+
+ {{ _("nav.connections") }}
+
{{ _("nav.file_manager") }}
diff --git a/frontend/translations/en.json b/frontend/translations/en.json
index 1af7a7a0..12050e5a 100644
--- a/frontend/translations/en.json
+++ b/frontend/translations/en.json
@@ -617,6 +617,22 @@
"cookie_policy.s5_p3_pre": "This Cookie Policy is part of and incorporated into our",
"cookie_policy.s5_privacy_link": "Privacy Notice",
"cookie_policy.s5_terms_link": "Terms of Service",
+ "connections.configure": "Configure",
+ "connections.configured": "Connected",
+ "connections.description": "Configure external authentication providers like OAuth2 and SAML.",
+ "connections.frontend_url_note": "Note: Requires Frontend URL to be configured. Configure in System Settings.",
+ "connections.linked": "Linked",
+ "connections.mobile_upload_description": "Allow users to upload files from mobile devices by scanning a QR code.",
+ "connections.mobile_upload_title": "Mobile Phone Upload",
+ "connections.qr_code_enabled": "Enable QR Code Upload",
+ "connections.save_note": "Changes to authentication providers require a restart to take effect.",
+ "connections.sso_auto_login": "Enable SSO Auto Login",
+ "connections.sso_auto_login_description": "Automatically redirect to SSO login when authentication is required.",
+ "connections.sso_auto_login_title": "SSO Auto Login",
+ "connections.title": "Connections",
+ "connections.unconfigure": "Disconnect",
+ "connections.unlinked": "Unlinked",
+ "connections.unlinked_services": "Services",
"credentials.col_action": "Action",
"credentials.col_credential": "Credential",
"credentials.col_source": "Source",
@@ -1230,6 +1246,7 @@
"nav.api_docs": "API Docs",
"nav.api_tokens": "API Tokens",
"nav.backup_restore": "Backup & Restore",
+ "nav.connections": "Connections",
"nav.credentials": "Credentials",
"nav.dark_mode": "Dark Mode",
"nav.dashboard": "Dashboard",