ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: Tasker workspace adapter API

This commit is contained in:
DCCONSTRUCTIONS 2026-05-06 10:10:37 +03:00
parent 8ec762f790
commit a5a347e839
2 changed files with 200 additions and 0 deletions

View File

@ -0,0 +1,186 @@
import json
from django.db import transaction
from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from plane.authentication.views.nodedc_logout import is_internal_logout_request_authorized
from plane.db.models import ExternalIdentityLink, Profile, User, Workspace, WorkspaceMember
OIDC_PROVIDER = "authentik"
ROLE_VALUES = {
"guest": 5,
"viewer": 5,
"member": 15,
"admin": 20,
"owner": 20,
5: 5,
15: 15,
20: 20,
}
def internal_unauthorized_response():
return JsonResponse({"ok": False, "error": "internal_access_unauthorized"}, status=401)
def parse_json_body(request):
if not request.body:
return {}
try:
return json.loads(request.body.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
return None
def normalize_email(value):
return value.strip().lower() if isinstance(value, str) else ""
def resolve_workspace(payload):
workspace_id = payload.get("workspaceId") or payload.get("workspace_id")
workspace_slug = payload.get("workspaceSlug") or payload.get("workspace_slug") or payload.get("slug")
queryset = Workspace.objects.filter(deleted_at__isnull=True)
if workspace_id:
return queryset.filter(id=workspace_id).first()
if workspace_slug:
return queryset.filter(slug=workspace_slug).first()
return None
def resolve_user(payload):
plane_user_id = payload.get("planeUserId") or payload.get("plane_user_id")
subject = payload.get("subject")
email = normalize_email(payload.get("email"))
if plane_user_id:
user = User.objects.filter(id=plane_user_id, is_bot=False).first()
if user:
return user
if subject:
link = ExternalIdentityLink.objects.filter(
provider=OIDC_PROVIDER,
subject=subject,
status=ExternalIdentityLink.Status.ACTIVE,
).select_related("user").first()
if link:
return link.user
if email:
link = ExternalIdentityLink.objects.filter(
provider=OIDC_PROVIDER,
email__iexact=email,
status=ExternalIdentityLink.Status.ACTIVE,
).select_related("user").first()
if link:
return link.user
return User.objects.filter(email__iexact=email, is_bot=False).first()
return None
def normalize_role(value):
return ROLE_VALUES.get(value, 15)
def serialize_workspace(workspace):
return {
"id": str(workspace.id),
"slug": workspace.slug,
"name": workspace.name,
"ownerEmail": workspace.owner.email if workspace.owner_id else None,
"memberCount": WorkspaceMember.objects.filter(
workspace=workspace,
deleted_at__isnull=True,
is_active=True,
member__is_bot=False,
).count(),
}
def serialize_membership(membership, created):
return {
"created": created,
"workspace": serialize_workspace(membership.workspace),
"member": {
"id": str(membership.member.id),
"email": membership.member.email,
"displayName": membership.member.display_name,
},
"role": membership.role,
"isActive": membership.is_active,
"isBanned": membership.is_banned,
}
@method_decorator(csrf_exempt, name="dispatch")
class NodeDCInternalWorkspaceListEndpoint(View):
def get(self, request):
if not is_internal_logout_request_authorized(request):
return internal_unauthorized_response()
workspaces = Workspace.objects.filter(deleted_at__isnull=True).select_related("owner").order_by("name")
return JsonResponse({"ok": True, "workspaces": [serialize_workspace(workspace) for workspace in workspaces]})
@method_decorator(csrf_exempt, name="dispatch")
class NodeDCInternalWorkspaceMembershipEnsureEndpoint(View):
def post(self, request):
if not is_internal_logout_request_authorized(request):
return internal_unauthorized_response()
payload = parse_json_body(request)
if payload is None:
return JsonResponse({"ok": False, "error": "invalid_json"}, status=400)
workspace = resolve_workspace(payload)
if workspace is None:
return JsonResponse({"ok": False, "error": "workspace_not_found"}, status=404)
user = resolve_user(payload)
if user is None:
return JsonResponse({"ok": False, "error": "user_not_found"}, status=404)
role = normalize_role(payload.get("role"))
company_role = payload.get("companyRole") or payload.get("company_role")
set_last_workspace = payload.get("setLastWorkspace", True) is not False
with transaction.atomic():
membership = WorkspaceMember.objects.filter(
workspace=workspace,
member=user,
deleted_at__isnull=True,
).first()
created = membership is None
if membership is None:
membership = WorkspaceMember.objects.create(
workspace=workspace,
member=user,
role=role,
company_role=company_role if isinstance(company_role, str) else None,
is_active=True,
is_banned=False,
)
else:
membership.role = role
if isinstance(company_role, str):
membership.company_role = company_role
membership.is_active = True
membership.is_banned = False
membership.banned_at = None
membership.banned_until = None
membership.save(update_fields=["role", "company_role", "is_active", "is_banned", "banned_at", "banned_until", "updated_at"])
if set_last_workspace:
profile, _ = Profile.objects.get_or_create(user=user)
profile.last_workspace_id = workspace.id
profile.save(update_fields=["last_workspace_id", "updated_at"])
return JsonResponse({"ok": True, "membership": serialize_membership(membership, created)})

View File

@ -15,6 +15,10 @@ from plane.authentication.views.nodedc_logout import (
NodeDCFrontChannelLogoutEndpoint, NodeDCFrontChannelLogoutEndpoint,
NodeDCInternalSessionLogoutEndpoint, NodeDCInternalSessionLogoutEndpoint,
) )
from plane.authentication.views.nodedc_workspace_adapter import (
NodeDCInternalWorkspaceListEndpoint,
NodeDCInternalWorkspaceMembershipEnsureEndpoint,
)
handler404 = "plane.app.views.error_404.custom_404_view" handler404 = "plane.app.views.error_404.custom_404_view"
@ -24,6 +28,16 @@ urlpatterns = [
NodeDCInternalSessionLogoutEndpoint.as_view(), NodeDCInternalSessionLogoutEndpoint.as_view(),
name="nodedc-internal-session-logout", name="nodedc-internal-session-logout",
), ),
path(
"api/internal/nodedc/workspaces/",
NodeDCInternalWorkspaceListEndpoint.as_view(),
name="nodedc-internal-workspaces",
),
path(
"api/internal/nodedc/workspace-memberships/ensure/",
NodeDCInternalWorkspaceMembershipEnsureEndpoint.as_view(),
name="nodedc-internal-workspace-membership-ensure",
),
path("api/", include("plane.app.urls")), path("api/", include("plane.app.urls")),
path("api/public/", include("plane.space.urls")), path("api/public/", include("plane.space.urls")),
path("api/instances/", include("plane.license.urls")), path("api/instances/", include("plane.license.urls")),