feat: plantilla base workspace SaaS
This commit is contained in:
114
backend/core/workspace_profile_sync.py
Normal file
114
backend/core/workspace_profile_sync.py
Normal file
@@ -0,0 +1,114 @@
|
||||
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,
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user