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, }, )