- Updated sidebar component to merge user data from Keycloak and Workspace, including avatar URLs. - Refactored nav-user component to utilize new avatar resolution logic and display user information more effectively. - Introduced a new utility function to resolve user avatar URLs, prioritizing Workspace avatars. - Implemented backend changes to support synchronization of user profile data, including avatar URLs from Workspace. - Added database migration to include new fields for Workspace profile synchronization in user_tenants. - Created a new client for fetching user profiles from Workspace. - Updated dashboard components to reflect changes in user data structure and avatar handling. - Removed avatar upload functionality from the profile update process, relying on Workspace for avatar management. - Added tests for avatar resolution logic to ensure correct prioritization of avatar sources.
115 lines
3.7 KiB
Python
115 lines
3.7 KiB
Python
import logging
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Any, Optional
|
|
from urllib.parse import urlparse
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from api.v1.modules.core.user_tenant.models import UserTenant
|
|
from core.workspace_profile_client import WorkspaceProfileClient
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SYNC_TTL_SECONDS = 300
|
|
|
|
|
|
def _is_valid_http_url(url: Optional[str]) -> bool:
|
|
if not url or not isinstance(url, str):
|
|
return False
|
|
parsed = urlparse(url.strip())
|
|
return parsed.scheme in ("http", "https") and bool(parsed.netloc)
|
|
|
|
|
|
def _is_fresh(ts: Optional[datetime], ttl_seconds: int = SYNC_TTL_SECONDS) -> bool:
|
|
if not ts:
|
|
return False
|
|
now = datetime.now(timezone.utc)
|
|
if ts.tzinfo is None:
|
|
ts = ts.replace(tzinfo=timezone.utc)
|
|
return ts >= (now - timedelta(seconds=ttl_seconds))
|
|
|
|
|
|
async def sync_workspace_profile_for_user(
|
|
db: Session,
|
|
*,
|
|
access_token: Optional[str],
|
|
keycloak_user_id: Optional[str],
|
|
tenant_id: Optional[int] = None,
|
|
company_id: Optional[int] = None,
|
|
workspace_profile: Optional[dict[str, Any]] = None,
|
|
force: bool = False,
|
|
) -> None:
|
|
"""
|
|
Sincroniza sub/avatar_url desde Workspace hacia core.user_tenants.
|
|
Nunca lanza excepción para no bloquear login ni requests autenticados.
|
|
"""
|
|
if not access_token or not keycloak_user_id:
|
|
return
|
|
|
|
try:
|
|
query = db.query(UserTenant).filter(
|
|
UserTenant.keycloak_user_id == keycloak_user_id,
|
|
UserTenant.is_active == True,
|
|
)
|
|
if tenant_id is not None:
|
|
query = query.filter(UserTenant.tenant_id == int(tenant_id))
|
|
if company_id is not None:
|
|
query = query.filter(UserTenant.company_id == int(company_id))
|
|
|
|
target = query.first()
|
|
if not target:
|
|
return
|
|
|
|
if not force and _is_fresh(target.workspace_profile_synced_at):
|
|
return
|
|
|
|
payload = workspace_profile
|
|
if payload is None:
|
|
client = WorkspaceProfileClient()
|
|
payload = await client.get_me(access_token)
|
|
|
|
workspace_sub = payload.get("sub")
|
|
if not workspace_sub:
|
|
logger.warning(
|
|
"workspace_profile_sync_warning",
|
|
extra={
|
|
"event": "workspace_profile_sync_warning",
|
|
"reason": "missing_sub",
|
|
"keycloak_user_id": keycloak_user_id,
|
|
"tenant_id": target.tenant_id,
|
|
},
|
|
)
|
|
return
|
|
|
|
avatar_url = payload.get("avatar_url")
|
|
sanitized_avatar = avatar_url.strip() if isinstance(avatar_url, str) else None
|
|
if sanitized_avatar and not _is_valid_http_url(sanitized_avatar):
|
|
logger.warning(
|
|
"workspace_profile_sync_warning",
|
|
extra={
|
|
"event": "workspace_profile_sync_warning",
|
|
"reason": "invalid_avatar_url",
|
|
"keycloak_user_id": keycloak_user_id,
|
|
"tenant_id": target.tenant_id,
|
|
},
|
|
)
|
|
sanitized_avatar = None
|
|
|
|
target.workspace_user_id = str(workspace_sub)
|
|
target.workspace_avatar_url = sanitized_avatar
|
|
target.workspace_profile_synced_at = datetime.now(timezone.utc)
|
|
db.add(target)
|
|
db.commit()
|
|
except Exception as exc:
|
|
db.rollback()
|
|
logger.warning(
|
|
"workspace_profile_sync_failed",
|
|
extra={
|
|
"event": "workspace_profile_sync_failed",
|
|
"error": str(exc),
|
|
"keycloak_user_id": keycloak_user_id,
|
|
"tenant_id": tenant_id,
|
|
"company_id": company_id,
|
|
},
|
|
)
|