ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: синхронизация профиля Tasker
This commit is contained in:
parent
480f85cce8
commit
1f7ecc39a0
|
|
@ -23,6 +23,8 @@ from plane.settings.storage import S3Storage
|
||||||
from plane.app.permissions import allow_permission, ROLE
|
from plane.app.permissions import allow_permission, ROLE
|
||||||
from plane.utils.cache import invalidate_cache_directly
|
from plane.utils.cache import invalidate_cache_directly
|
||||||
from plane.throttles.asset import AssetRateThrottle
|
from plane.throttles.asset import AssetRateThrottle
|
||||||
|
from plane.app.realtime.nodedc_events import publish_nodedc_user_profile_event_on_commit
|
||||||
|
from plane.authentication.nodedc_profile_sync import push_nodedc_user_profile_update_on_commit
|
||||||
from plane.utils.upload_limits import get_project_storage_quota_response, resolve_workspace_upload_size_limit
|
from plane.utils.upload_limits import get_project_storage_quota_response, resolve_workspace_upload_size_limit
|
||||||
from plane.utils.file_dedup import (
|
from plane.utils.file_dedup import (
|
||||||
UploadedObjectMissing,
|
UploadedObjectMissing,
|
||||||
|
|
@ -57,6 +59,8 @@ class UserAssetsV2Endpoint(BaseAPIView):
|
||||||
# Save the new avatar
|
# Save the new avatar
|
||||||
user.avatar_asset_id = asset_id
|
user.avatar_asset_id = asset_id
|
||||||
user.save()
|
user.save()
|
||||||
|
publish_nodedc_user_profile_event_on_commit(user, changed_fields=["avatar"])
|
||||||
|
push_nodedc_user_profile_update_on_commit(user, changed_fields=["avatar"])
|
||||||
invalidate_cache_directly(path="/api/users/me/", url_params=False, user=True, request=request)
|
invalidate_cache_directly(path="/api/users/me/", url_params=False, user=True, request=request)
|
||||||
invalidate_cache_directly(
|
invalidate_cache_directly(
|
||||||
path="/api/users/me/settings/",
|
path="/api/users/me/settings/",
|
||||||
|
|
@ -89,8 +93,11 @@ class UserAssetsV2Endpoint(BaseAPIView):
|
||||||
# User Avatar
|
# User Avatar
|
||||||
if entity_type == FileAsset.EntityTypeContext.USER_AVATAR:
|
if entity_type == FileAsset.EntityTypeContext.USER_AVATAR:
|
||||||
user = User.objects.get(id=asset.user_id)
|
user = User.objects.get(id=asset.user_id)
|
||||||
|
user.avatar = ""
|
||||||
user.avatar_asset_id = None
|
user.avatar_asset_id = None
|
||||||
user.save()
|
user.save()
|
||||||
|
publish_nodedc_user_profile_event_on_commit(user, changed_fields=["avatar"])
|
||||||
|
push_nodedc_user_profile_update_on_commit(user, changed_fields=["avatar"])
|
||||||
invalidate_cache_directly(path="/api/users/me/", url_params=False, user=True, request=request)
|
invalidate_cache_directly(path="/api/users/me/", url_params=False, user=True, request=request)
|
||||||
invalidate_cache_directly(
|
invalidate_cache_directly(
|
||||||
path="/api/users/me/settings/",
|
path="/api/users/me/settings/",
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,12 @@ from plane.utils.host import base_host
|
||||||
from plane.bgtasks.user_email_update_task import send_email_update_magic_code, send_email_update_confirmation
|
from plane.bgtasks.user_email_update_task import send_email_update_magic_code, send_email_update_confirmation
|
||||||
from plane.authentication.rate_limit import EmailVerificationThrottle
|
from plane.authentication.rate_limit import EmailVerificationThrottle
|
||||||
from plane.license.utils.instance_value import get_configuration_value
|
from plane.license.utils.instance_value import get_configuration_value
|
||||||
|
from plane.app.realtime.nodedc_events import publish_nodedc_user_profile_event_on_commit
|
||||||
|
from plane.authentication.nodedc_profile_sync import push_nodedc_user_profile_update_on_commit
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("plane")
|
logger = logging.getLogger("plane")
|
||||||
|
NODEDC_PROFILE_SYNC_FIELDS = ("display_name", "first_name", "last_name", "avatar")
|
||||||
|
|
||||||
|
|
||||||
class UserEndpoint(BaseViewSet):
|
class UserEndpoint(BaseViewSet):
|
||||||
|
|
@ -91,7 +94,21 @@ class UserEndpoint(BaseViewSet):
|
||||||
return Response({"is_instance_admin": is_admin}, status=status.HTTP_200_OK)
|
return Response({"is_instance_admin": is_admin}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def partial_update(self, request, *args, **kwargs):
|
def partial_update(self, request, *args, **kwargs):
|
||||||
return super().partial_update(request, *args, **kwargs)
|
user = self.get_object()
|
||||||
|
previous_profile = {field: getattr(user, field, None) for field in NODEDC_PROFILE_SYNC_FIELDS}
|
||||||
|
response = super().partial_update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
if response.status_code < 400:
|
||||||
|
user.refresh_from_db()
|
||||||
|
changed_fields = [
|
||||||
|
field for field in NODEDC_PROFILE_SYNC_FIELDS if previous_profile.get(field) != getattr(user, field, None)
|
||||||
|
]
|
||||||
|
|
||||||
|
if changed_fields:
|
||||||
|
publish_nodedc_user_profile_event_on_commit(user, changed_fields=changed_fields)
|
||||||
|
push_nodedc_user_profile_update_on_commit(user, changed_fields=changed_fields)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
def _validate_new_email(self, user, new_email):
|
def _validate_new_email(self, user, new_email):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from plane.authentication.views.nodedc_logout import get_nodedc_internal_token
|
||||||
|
from plane.db.models import ExternalIdentityLink, User
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger("plane")
|
||||||
|
OIDC_PROVIDER = "authentik"
|
||||||
|
|
||||||
|
|
||||||
|
def get_nodedc_profile_sync_url():
|
||||||
|
launcher_base_url = (
|
||||||
|
os.environ.get("PLANE_NODEDC_LAUNCHER_URL", "").strip()
|
||||||
|
or os.environ.get("PLANE_NODEDC_LAUNCHER_PUBLIC_URL", "").strip()
|
||||||
|
or "http://launcher.local.nodedc"
|
||||||
|
).rstrip("/")
|
||||||
|
return (
|
||||||
|
os.environ.get("PLANE_NODEDC_PROFILE_SYNC_URL", "").strip()
|
||||||
|
or f"{launcher_base_url}/api/internal/tasker/profile-sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_tasker_public_origin():
|
||||||
|
explicit_origin = os.environ.get("PLANE_NODEDC_TASK_PUBLIC_URL", "").strip()
|
||||||
|
if explicit_origin:
|
||||||
|
return explicit_origin.rstrip("/")
|
||||||
|
|
||||||
|
configured_url = os.environ.get("WEB_URL", "").strip()
|
||||||
|
if configured_url:
|
||||||
|
parsed_url = urlparse(configured_url)
|
||||||
|
if parsed_url.scheme and parsed_url.netloc:
|
||||||
|
return f"{parsed_url.scheme}://{parsed_url.netloc}"
|
||||||
|
|
||||||
|
task_domain = os.environ.get("TASK_DOMAIN", "").strip()
|
||||||
|
if task_domain:
|
||||||
|
return f"http://{task_domain}"
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_tasker_avatar_url(value):
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return None
|
||||||
|
|
||||||
|
avatar_url = value.strip()
|
||||||
|
if not avatar_url:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if avatar_url.startswith(("http://", "https://", "data:")):
|
||||||
|
return avatar_url
|
||||||
|
|
||||||
|
if avatar_url.startswith("/"):
|
||||||
|
tasker_origin = get_tasker_public_origin()
|
||||||
|
return f"{tasker_origin}{avatar_url}" if tasker_origin else avatar_url
|
||||||
|
|
||||||
|
return avatar_url
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_display_name(user):
|
||||||
|
display_name = getattr(user, "display_name", "")
|
||||||
|
if display_name:
|
||||||
|
return display_name
|
||||||
|
|
||||||
|
name = " ".join(
|
||||||
|
value for value in [getattr(user, "first_name", ""), getattr(user, "last_name", "")] if value
|
||||||
|
).strip()
|
||||||
|
return name or user.email
|
||||||
|
|
||||||
|
|
||||||
|
def get_nodedc_subject(user):
|
||||||
|
link = ExternalIdentityLink.objects.filter(provider=OIDC_PROVIDER, user=user, status="active").first()
|
||||||
|
return link.subject if link else None
|
||||||
|
|
||||||
|
|
||||||
|
def build_nodedc_profile_payload(user, changed_fields=None):
|
||||||
|
return {
|
||||||
|
"source": "tasker",
|
||||||
|
"planeUserId": str(user.id),
|
||||||
|
"subject": get_nodedc_subject(user),
|
||||||
|
"email": user.email,
|
||||||
|
"name": get_user_display_name(user),
|
||||||
|
"displayName": user.display_name or get_user_display_name(user),
|
||||||
|
"firstName": user.first_name,
|
||||||
|
"lastName": user.last_name,
|
||||||
|
"avatarUrl": normalize_tasker_avatar_url(user.avatar_url),
|
||||||
|
"changedFields": sorted(set(changed_fields or [])),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def push_nodedc_user_profile_update(user, changed_fields=None):
|
||||||
|
request_url = get_nodedc_profile_sync_url()
|
||||||
|
token = get_nodedc_internal_token()
|
||||||
|
|
||||||
|
if not request_url or not token:
|
||||||
|
logger.warning("NODE.DC profile sync is not configured")
|
||||||
|
return None
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
request_url,
|
||||||
|
json=build_nodedc_profile_payload(user, changed_fields=changed_fields),
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Accept": "application/json",
|
||||||
|
},
|
||||||
|
timeout=float(os.environ.get("PLANE_NODEDC_PROFILE_SYNC_TIMEOUT_SECONDS", "3") or "3"),
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def push_nodedc_user_profile_update_on_commit(user, changed_fields=None):
|
||||||
|
user_id = user.id
|
||||||
|
changed_fields = sorted(set(changed_fields or []))
|
||||||
|
|
||||||
|
def _push():
|
||||||
|
fresh_user = User.objects.filter(id=user_id, is_bot=False).first()
|
||||||
|
if fresh_user is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
push_nodedc_user_profile_update(fresh_user, changed_fields=changed_fields)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to push NODE.DC profile update to Launcher")
|
||||||
|
|
||||||
|
transaction.on_commit(_push)
|
||||||
|
|
@ -183,15 +183,17 @@ def resolve_nodedc_launcher_origin():
|
||||||
def sync_user_profile_from_payload(user, payload):
|
def sync_user_profile_from_payload(user, payload):
|
||||||
updated_fields = []
|
updated_fields = []
|
||||||
display_name = first_payload_string(payload, "displayName", "display_name", "name")
|
display_name = first_payload_string(payload, "displayName", "display_name", "name")
|
||||||
|
has_avatar = any(key in payload for key in ["avatarUrl", "avatar_url", "avatar"])
|
||||||
avatar_url = normalize_nodedc_avatar_url(first_payload_string(payload, "avatarUrl", "avatar_url", "avatar"))
|
avatar_url = normalize_nodedc_avatar_url(first_payload_string(payload, "avatarUrl", "avatar_url", "avatar"))
|
||||||
|
|
||||||
if display_name and user.display_name != display_name:
|
if display_name and user.display_name != display_name:
|
||||||
user.display_name = display_name
|
user.display_name = display_name
|
||||||
updated_fields.append("display_name")
|
updated_fields.append("display_name")
|
||||||
|
|
||||||
if avatar_url and user.avatar != avatar_url:
|
if has_avatar and (user.avatar != avatar_url or user.avatar_asset_id is not None):
|
||||||
user.avatar = avatar_url
|
user.avatar = avatar_url
|
||||||
updated_fields.append("avatar")
|
user.avatar_asset_id = None
|
||||||
|
updated_fields.extend(["avatar", "avatar_asset"])
|
||||||
|
|
||||||
if updated_fields:
|
if updated_fields:
|
||||||
updated_fields.append("updated_at")
|
updated_fields.append("updated_at")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue