mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-07 23:13:57 -05:00
Enhance OIDC provider support and frontend chunking
Improves OIDC provider handling in the backend by adding detailed error handling, HTTP client reuse, and new templates for Microsoft and Google providers. Updates frontend to use environment-based URLs for identity provider login, refines i18n text, and configures Vite to optimize chunk splitting and API proxying for better performance and development experience.
This commit is contained in:
@@ -901,7 +901,7 @@ def location_based_on_coordinates(latitude, longitude) -> dict | None:
|
||||
|
||||
# Make the request and get the response
|
||||
try:
|
||||
headers = {"User-Agent": "Endurain"}
|
||||
headers = {"User-Agent": "Endurain/0.16.0 (ReverseGeocoding)"}
|
||||
# Make the request and get the response
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
@@ -25,6 +25,29 @@ class IdentityProviderService:
|
||||
self._discovery_cache: Dict[int, Dict[str, Any]] = {}
|
||||
self._cache_expiry: Dict[int, datetime] = {}
|
||||
self._cache_ttl = timedelta(hours=1)
|
||||
self._http_client: httpx.AsyncClient | None = None
|
||||
|
||||
async def _get_http_client(self) -> httpx.AsyncClient:
|
||||
"""
|
||||
Asynchronously retrieves or creates an instance of httpx.AsyncClient for making HTTP requests.
|
||||
|
||||
If the HTTP client does not already exist, it initializes a new AsyncClient with a timeout of 10 seconds
|
||||
and connection limits (maximum 5 keep-alive connections and 10 total connections). Returns the client instance.
|
||||
|
||||
Returns:
|
||||
httpx.AsyncClient: The HTTP client instance for asynchronous requests.
|
||||
"""
|
||||
if self._http_client is None:
|
||||
self._http_client = httpx.AsyncClient(
|
||||
timeout=10.0,
|
||||
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10),
|
||||
follow_redirects=True,
|
||||
headers={
|
||||
"User-Agent": "Endurain/0.16.0 (OIDC Client)",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
)
|
||||
return self._http_client
|
||||
|
||||
async def get_oidc_configuration(
|
||||
self, idp: idp_models.IdentityProvider
|
||||
@@ -54,22 +77,48 @@ class IdentityProviderService:
|
||||
discovery_url = (
|
||||
f"{idp.issuer_url.rstrip('/')}/.well-known/openid-configuration"
|
||||
)
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(discovery_url, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
config = response.json()
|
||||
core_logger.print_to_log(
|
||||
f"Fetching OIDC configuration from: {discovery_url}", "info"
|
||||
)
|
||||
|
||||
client = await self._get_http_client()
|
||||
response = await client.get(discovery_url)
|
||||
|
||||
core_logger.print_to_log(
|
||||
f"OIDC discovery response status: {response.status_code}", "debug"
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
config = response.json()
|
||||
|
||||
# Cache the configuration
|
||||
self._discovery_cache[idp.id] = config
|
||||
self._cache_expiry[idp.id] = (
|
||||
datetime.now(timezone.utc) + self._cache_ttl
|
||||
)
|
||||
# Cache the configuration
|
||||
self._discovery_cache[idp.id] = config
|
||||
self._cache_expiry[idp.id] = (
|
||||
datetime.now(timezone.utc) + self._cache_ttl
|
||||
)
|
||||
|
||||
core_logger.print_to_log(
|
||||
f"Successfully fetched OIDC configuration for {idp.name}", "info"
|
||||
)
|
||||
|
||||
return config
|
||||
return config
|
||||
except httpx.HTTPStatusError as err:
|
||||
core_logger.print_to_log(
|
||||
f"HTTP error fetching OIDC discovery for {idp.name}: {err.response.status_code} - {err.response.text}",
|
||||
"warning"
|
||||
)
|
||||
return None
|
||||
except httpx.ConnectError as err:
|
||||
core_logger.print_to_log(
|
||||
f"Connection error fetching OIDC discovery for {idp.name}. "
|
||||
f"URL: {discovery_url}. Error: {err}. "
|
||||
f"Check if the service is reachable and not using 'localhost' in Docker.",
|
||||
"error"
|
||||
)
|
||||
return None
|
||||
except httpx.RequestError as err:
|
||||
core_logger.print_to_log(
|
||||
f"Request error fetching OIDC discovery for {idp.name}. "
|
||||
f"URL: {discovery_url}. Error: {err}",
|
||||
"warning"
|
||||
)
|
||||
return None
|
||||
except Exception as err:
|
||||
core_logger.print_to_log(
|
||||
f"Failed to fetch OIDC discovery for {idp.name}: {err}", "warning"
|
||||
@@ -87,7 +136,7 @@ class IdentityProviderService:
|
||||
str: The complete redirect URI for the specified identity provider.
|
||||
"""
|
||||
base_url = core_config.ENDURAIN_HOST
|
||||
return f"{base_url}/api/v1/idp/callback/{idp_slug}"
|
||||
return f"{base_url}/api/v1/public/idp/callback/{idp_slug}"
|
||||
|
||||
async def initiate_login(
|
||||
self, idp: idp_models.IdentityProvider, request: Request, db: Session
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Identity Provider utility functions and templates"""
|
||||
|
||||
from typing import List, Dict, Any
|
||||
import identity_providers.schema as idp_schema
|
||||
|
||||
@@ -14,10 +15,10 @@ IDP_TEMPLATES = {
|
||||
"user_mapping": {
|
||||
"username": ["preferred_username", "username", "email"],
|
||||
"email": ["email", "mail"],
|
||||
"name": ["name", "display_name", "full_name"]
|
||||
"name": ["name", "display_name", "full_name"],
|
||||
},
|
||||
"description": "Keycloak - Open Source Identity and Access Management",
|
||||
"configuration_notes": "Replace {your-keycloak-domain} with your Keycloak server domain (e.g., keycloak.example.com) and {realm} with your realm name. Create an OIDC client in Keycloak admin console."
|
||||
"configuration_notes": "Replace {your-keycloak-domain} with your Keycloak server domain (e.g., keycloak.example.com) and {realm} with your realm name. Create an OIDC client in Keycloak admin console.",
|
||||
},
|
||||
"authentik": {
|
||||
"name": "Authentik",
|
||||
@@ -28,10 +29,10 @@ IDP_TEMPLATES = {
|
||||
"user_mapping": {
|
||||
"username": ["preferred_username", "username", "email"],
|
||||
"email": ["email", "mail"],
|
||||
"name": ["name", "display_name"]
|
||||
"name": ["name", "display_name"],
|
||||
},
|
||||
"description": "Authentik - Open-source Identity Provider",
|
||||
"configuration_notes": "Replace {your-authentik-domain} with your Authentik server domain (e.g., authentik.example.com) and {slug} with your application slug. Create an OAuth2/OIDC provider in Authentik."
|
||||
"configuration_notes": "Replace {your-authentik-domain} with your Authentik server domain (e.g., authentik.example.com) and {slug} with your application slug. Create an OAuth2/OIDC provider in Authentik.",
|
||||
},
|
||||
"authelia": {
|
||||
"name": "Authelia",
|
||||
@@ -42,27 +43,23 @@ IDP_TEMPLATES = {
|
||||
"user_mapping": {
|
||||
"username": ["preferred_username", "username", "email"],
|
||||
"email": ["email"],
|
||||
"name": ["name"]
|
||||
"name": ["name"],
|
||||
},
|
||||
"description": "Authelia - Open-source authentication and authorization server",
|
||||
"configuration_notes": "Replace {your-authelia-domain} with your Authelia server domain (e.g., auth.example.com). Configure an OIDC client in your Authelia configuration file."
|
||||
"configuration_notes": "Replace {your-authelia-domain} with your Authelia server domain (e.g., auth.example.com). Configure an OIDC client in your Authelia configuration file.",
|
||||
},
|
||||
"google": {
|
||||
"google_consumer": {
|
||||
"name": "Google",
|
||||
"provider_type": "oidc",
|
||||
"issuer_url": "https://accounts.google.com",
|
||||
"scopes": "openid profile email",
|
||||
"icon": "fa-google",
|
||||
"user_mapping": {
|
||||
"username": ["email"],
|
||||
"email": ["email"],
|
||||
"name": ["name"]
|
||||
},
|
||||
"user_mapping": {"username": ["email"], "email": ["email"], "name": ["name"]},
|
||||
"description": "Google OAuth 2.0",
|
||||
"configuration_notes": "Create OAuth 2.0 credentials in Google Cloud Console."
|
||||
"configuration_notes": "Create OAuth 2.0 credentials in Google Cloud Console.",
|
||||
},
|
||||
"microsoft": {
|
||||
"name": "Microsoft",
|
||||
"microsoft_entra": {
|
||||
"name": "Microsoft Entra ID",
|
||||
"provider_type": "oidc",
|
||||
"issuer_url": "https://login.microsoftonline.com/{tenant}/v2.0",
|
||||
"scopes": "openid profile email",
|
||||
@@ -70,28 +67,48 @@ IDP_TEMPLATES = {
|
||||
"user_mapping": {
|
||||
"username": ["preferred_username", "email"],
|
||||
"email": ["email"],
|
||||
"name": ["name"]
|
||||
"name": ["name"],
|
||||
},
|
||||
"description": "Microsoft Azure AD / Entra ID",
|
||||
"configuration_notes": "Replace {tenant} with your tenant ID or 'common'. Register an app in Azure Portal."
|
||||
}
|
||||
"description": "Microsoft Entra ID (Azure AD) - Enterprise Accounts",
|
||||
"configuration_notes": "Replace {tenant} with your tenant ID (for single tenant) or 'organizations' (for multi-tenant). Register an app in Azure Portal under 'App registrations'.",
|
||||
},
|
||||
"microsoft_consumer": {
|
||||
"name": "Microsoft Account",
|
||||
"provider_type": "oidc",
|
||||
"issuer_url": "https://login.microsoftonline.com/consumers/v2.0",
|
||||
"scopes": "openid profile email",
|
||||
"icon": "fa-microsoft",
|
||||
"user_mapping": {
|
||||
"username": ["preferred_username", "email"],
|
||||
"email": ["email"],
|
||||
"name": ["name"],
|
||||
},
|
||||
"description": "Microsoft Account - Consumer Accounts (Outlook, Hotmail, Xbox)",
|
||||
"configuration_notes": "For personal Microsoft accounts only (@outlook.com, @hotmail.com, @live.com, Xbox accounts). Register an app in Azure Portal with 'Accounts in any organizational directory and personal Microsoft accounts' option.",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_idp_templates() -> List[idp_schema.IdentityProviderTemplate]:
|
||||
"""
|
||||
Retrieve a list of identity provider templates.
|
||||
Retrieve a list of identity provider templates, excluding specific providers.
|
||||
|
||||
Returns:
|
||||
List[idp_schema.IdentityProviderTemplate]: A list of IdentityProviderTemplate instances,
|
||||
each representing a predefined identity provider template.
|
||||
List[idp_schema.IdentityProviderTemplate]:
|
||||
A list of IdentityProviderTemplate objects for all identity providers
|
||||
except 'microsoft_consumer', 'microsoft_entra', and 'google_consumer'.
|
||||
"""
|
||||
templates = []
|
||||
for template_id, template_data in IDP_TEMPLATES.items():
|
||||
if (
|
||||
template_id == "microsoft_consumer"
|
||||
or template_id == "microsoft_entra"
|
||||
or template_id == "google_consumer"
|
||||
):
|
||||
continue
|
||||
templates.append(
|
||||
idp_schema.IdentityProviderTemplate(
|
||||
template_id=template_id,
|
||||
**template_data
|
||||
template_id=template_id, **template_data
|
||||
)
|
||||
)
|
||||
return templates
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
{
|
||||
"addProviderButton": "Add Provider",
|
||||
"addProviderButton": "Add identity provider",
|
||||
"enabledBadge": "Enabled",
|
||||
"disabledBadge": "Disabled",
|
||||
"oidcType": "OpenID Connect",
|
||||
"oauth2Type": "OAuth 2.0",
|
||||
"enableButton": "Enable",
|
||||
"disableButton": "Disable",
|
||||
"editButton": "Edit",
|
||||
"deleteButton": "Delete",
|
||||
"infoAlert": "Identity providers allow users to authenticate using external services like Google, Microsoft, Keycloak, or Authentik.",
|
||||
|
||||
@@ -92,6 +92,6 @@ export const identityProviders = {
|
||||
* @param {string} slug - The provider slug
|
||||
*/
|
||||
initiateLogin(slug) {
|
||||
window.location.href = `/api/v1/idp/login/${slug}`
|
||||
window.location.href = `${window.env.ENDURAIN_HOST}/api/v1/public/idp/login/${slug}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,47 @@ import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
// Core Vue framework and state management
|
||||
'vue-vendor': ['vue', 'vue-router', 'pinia'],
|
||||
|
||||
// Large charting library - only needed on stats/activity pages
|
||||
'chart': ['chart.js', 'chartjs-plugin-datalabels'],
|
||||
|
||||
// Map library - only needed on activity detail pages with GPS data
|
||||
'leaflet': ['leaflet'],
|
||||
|
||||
// UI framework - used across the app
|
||||
'bootstrap': ['bootstrap'],
|
||||
|
||||
// FontAwesome icons - used throughout the app
|
||||
'fontawesome': [
|
||||
'@fortawesome/fontawesome-svg-core',
|
||||
'@fortawesome/free-solid-svg-icons',
|
||||
'@fortawesome/free-regular-svg-icons',
|
||||
'@fortawesome/free-brands-svg-icons',
|
||||
'@fortawesome/vue-fontawesome'
|
||||
],
|
||||
|
||||
// Date/time utilities and notifications
|
||||
'utils': ['luxon', 'notivue', 'vue-i18n']
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
// Proxy all /api requests to the backend server during development
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
VitePWA({
|
||||
@@ -62,6 +103,7 @@ export default defineConfig({
|
||||
skipWaiting: true,
|
||||
sourcemap: false,
|
||||
navigateFallback: '/',
|
||||
navigateFallbackDenylist: [/^\/api\//],
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: ({ url }) => /^\/api\/v1(?:\/|$)/.test(url.pathname),
|
||||
|
||||
Reference in New Issue
Block a user