ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: self-service workspace invite approval
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import os
|
||||
|
||||
import requests
|
||||
|
||||
from plane.authentication.views.nodedc_logout import get_nodedc_internal_token
|
||||
|
||||
|
||||
ROLE_SLUGS = {
|
||||
5: "guest",
|
||||
15: "member",
|
||||
20: "admin",
|
||||
}
|
||||
|
||||
|
||||
def get_nodedc_workspace_invite_request_url():
|
||||
launcher_base_url = os.environ.get("PLANE_NODEDC_LAUNCHER_URL", "http://launcher.local.nodedc").rstrip("/")
|
||||
return (
|
||||
os.environ.get("PLANE_NODEDC_WORKSPACE_INVITE_REQUEST_URL", "").strip()
|
||||
or f"{launcher_base_url}/api/internal/tasker/invite-requests"
|
||||
)
|
||||
|
||||
|
||||
def get_nodedc_workspace_invite_cancel_url():
|
||||
launcher_base_url = os.environ.get("PLANE_NODEDC_LAUNCHER_URL", "http://launcher.local.nodedc").rstrip("/")
|
||||
return (
|
||||
os.environ.get("PLANE_NODEDC_WORKSPACE_INVITE_CANCEL_URL", "").strip()
|
||||
or f"{launcher_base_url}/api/internal/tasker/invite-requests/cancel"
|
||||
)
|
||||
|
||||
|
||||
def request_nodedc_workspace_invite_approval(request, workspace, invitation):
|
||||
request_url = get_nodedc_workspace_invite_request_url()
|
||||
token = get_nodedc_internal_token()
|
||||
|
||||
if not request_url or not token:
|
||||
raise RuntimeError("NODE.DC invite approval is not configured")
|
||||
|
||||
response = requests.post(
|
||||
request_url,
|
||||
json={
|
||||
"taskerInviteId": str(invitation.id),
|
||||
"workspace": {
|
||||
"id": str(workspace.id),
|
||||
"slug": workspace.slug,
|
||||
"name": workspace.name,
|
||||
},
|
||||
"invitee": {
|
||||
"email": invitation.email,
|
||||
"role": ROLE_SLUGS.get(invitation.role, "member"),
|
||||
},
|
||||
"inviter": {
|
||||
"planeUserId": str(request.user.id),
|
||||
"subject": get_nodedc_subject(request.user),
|
||||
"email": request.user.email,
|
||||
"name": get_user_display_name(request.user),
|
||||
},
|
||||
},
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
timeout=float(os.environ.get("PLANE_NODEDC_WORKSPACE_INVITE_REQUEST_TIMEOUT_SECONDS", "3") or "3"),
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
def cancel_nodedc_workspace_invite_approval(request, invitation):
|
||||
request_url = get_nodedc_workspace_invite_cancel_url()
|
||||
token = get_nodedc_internal_token()
|
||||
|
||||
if not request_url or not token:
|
||||
raise RuntimeError("NODE.DC invite approval cancellation is not configured")
|
||||
|
||||
response = requests.post(
|
||||
request_url,
|
||||
json={
|
||||
"taskerInviteId": str(invitation.id),
|
||||
"requestId": invitation.nodedc_approval_request_id,
|
||||
"workspaceSlug": invitation.workspace.slug,
|
||||
"inviteeEmail": invitation.email,
|
||||
"cancelledBy": {
|
||||
"planeUserId": str(request.user.id),
|
||||
"subject": get_nodedc_subject(request.user),
|
||||
"email": request.user.email,
|
||||
"name": get_user_display_name(request.user),
|
||||
},
|
||||
},
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
timeout=float(os.environ.get("PLANE_NODEDC_WORKSPACE_INVITE_REQUEST_TIMEOUT_SECONDS", "3") or "3"),
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
def cancel_nodedc_workspace_member_access(request, workspace_member):
|
||||
request_url = get_nodedc_workspace_invite_cancel_url()
|
||||
token = get_nodedc_internal_token()
|
||||
|
||||
if not request_url or not token:
|
||||
raise RuntimeError("NODE.DC workspace member cancellation is not configured")
|
||||
|
||||
response = requests.post(
|
||||
request_url,
|
||||
json={
|
||||
"workspaceSlug": workspace_member.workspace.slug,
|
||||
"inviteeEmail": workspace_member.member.email,
|
||||
"comment": "Пользователь удалён из workspace Operational Core.",
|
||||
"cancelledBy": {
|
||||
"planeUserId": str(request.user.id),
|
||||
"subject": get_nodedc_subject(request.user),
|
||||
"email": request.user.email,
|
||||
"name": get_user_display_name(request.user),
|
||||
},
|
||||
},
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
timeout=float(os.environ.get("PLANE_NODEDC_WORKSPACE_INVITE_REQUEST_TIMEOUT_SECONDS", "3") or "3"),
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
def get_user_display_name(user):
|
||||
display_name = getattr(user, "display_name", None)
|
||||
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 = user.external_identity_links.filter(provider="authentik", status="active").first()
|
||||
return link.subject if link else None
|
||||
@@ -0,0 +1,126 @@
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from plane.bgtasks.event_tracking_task import track_event
|
||||
from plane.db.models import Profile, WorkspaceMember, WorkspaceMemberInvite
|
||||
from plane.utils.analytics_events import USER_JOINED_WORKSPACE
|
||||
from plane.utils.cache import invalidate_cache_directly
|
||||
from plane.utils.workspace_bans import is_workspace_member_currently_banned, release_workspace_member_ban
|
||||
|
||||
|
||||
class NodeDCWorkspaceInviteJoinError(Exception):
|
||||
def __init__(self, code, status_code=400):
|
||||
super().__init__(code)
|
||||
self.code = code
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
def accept_nodedc_workspace_invite_request_for_user(request_id, user):
|
||||
if not request_id:
|
||||
raise NodeDCWorkspaceInviteJoinError("workspace_invite_request_missing", 400)
|
||||
|
||||
if not user or user.is_anonymous:
|
||||
raise NodeDCWorkspaceInviteJoinError("workspace_invite_user_not_authenticated", 401)
|
||||
|
||||
with transaction.atomic():
|
||||
invitation = (
|
||||
WorkspaceMemberInvite.objects.select_for_update(of=("self",))
|
||||
.select_related("workspace", "created_by")
|
||||
.filter(nodedc_approval_request_id=request_id, deleted_at__isnull=True)
|
||||
.first()
|
||||
)
|
||||
|
||||
if invitation is None:
|
||||
raise NodeDCWorkspaceInviteJoinError("workspace_invite_not_found", 404)
|
||||
|
||||
if invitation.nodedc_approval_status != WorkspaceMemberInvite.NODEDC_APPROVAL_APPROVED:
|
||||
raise NodeDCWorkspaceInviteJoinError("workspace_invite_not_approved", 403)
|
||||
|
||||
if (invitation.email or "").strip().lower() != (user.email or "").strip().lower():
|
||||
raise NodeDCWorkspaceInviteJoinError("workspace_invite_email_mismatch", 403)
|
||||
|
||||
workspace_member = (
|
||||
WorkspaceMember.all_objects.select_for_update()
|
||||
.filter(workspace=invitation.workspace, member=user)
|
||||
.order_by("-created_at")
|
||||
.first()
|
||||
)
|
||||
|
||||
if is_workspace_member_currently_banned(workspace_member):
|
||||
raise NodeDCWorkspaceInviteJoinError("workspace_invite_user_banned", 403)
|
||||
|
||||
if workspace_member is not None and workspace_member.is_banned:
|
||||
release_workspace_member_ban(workspace_member)
|
||||
|
||||
created = workspace_member is None
|
||||
if workspace_member is None:
|
||||
workspace_member = WorkspaceMember.objects.create(
|
||||
workspace=invitation.workspace,
|
||||
member=user,
|
||||
role=invitation.role,
|
||||
created_by=invitation.created_by,
|
||||
)
|
||||
else:
|
||||
workspace_member.role = invitation.role
|
||||
workspace_member.is_active = True
|
||||
workspace_member.is_banned = False
|
||||
workspace_member.banned_at = None
|
||||
workspace_member.banned_until = None
|
||||
workspace_member.ban_project_member_ids = []
|
||||
workspace_member.deleted_at = None
|
||||
workspace_member.save(
|
||||
update_fields=[
|
||||
"role",
|
||||
"is_active",
|
||||
"is_banned",
|
||||
"banned_at",
|
||||
"banned_until",
|
||||
"ban_project_member_ids",
|
||||
"deleted_at",
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
|
||||
profile, _ = Profile.objects.get_or_create(user=user)
|
||||
profile.last_workspace_id = invitation.workspace.id
|
||||
profile.save(update_fields=["last_workspace_id", "updated_at"])
|
||||
|
||||
invitation.accepted = True
|
||||
invitation.responded_at = timezone.now()
|
||||
invitation.save(update_fields=["accepted", "responded_at", "updated_at"])
|
||||
|
||||
workspace_slug = invitation.workspace.slug
|
||||
workspace_id = invitation.workspace.id
|
||||
role = invitation.role
|
||||
|
||||
invitation.delete()
|
||||
|
||||
invalidate_cache_directly(
|
||||
path=f"/api/workspaces/{workspace_slug}/members/",
|
||||
url_params=False,
|
||||
user=False,
|
||||
multiple=True,
|
||||
)
|
||||
invalidate_cache_directly(path="/api/workspaces/", user=False, multiple=True)
|
||||
invalidate_cache_directly(path="/api/users/me/workspaces/", user=False, multiple=True)
|
||||
|
||||
track_event.delay(
|
||||
user_id=user.id,
|
||||
event_name=USER_JOINED_WORKSPACE,
|
||||
slug=workspace_slug,
|
||||
event_properties={
|
||||
"user_id": user.id,
|
||||
"workspace_id": workspace_id,
|
||||
"workspace_slug": workspace_slug,
|
||||
"role": role,
|
||||
"joined_at": str(timezone.now()),
|
||||
"source": "nodedc_platform_invite",
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
"created": created,
|
||||
"workspaceSlug": workspace_slug,
|
||||
"workspaceId": str(workspace_id),
|
||||
"role": role,
|
||||
}
|
||||
@@ -23,6 +23,8 @@ def get_nodedc_workspace_creation_policy(user, workspace_slug=None):
|
||||
"mode": "standalone",
|
||||
"managed_by": "tasker",
|
||||
"default_managed_by": "tasker",
|
||||
"invite_approval": "tasker",
|
||||
"default_invite_approval": "tasker",
|
||||
"workspaces": [],
|
||||
"reason": "NODE.DC workspace policy is not configured.",
|
||||
}
|
||||
@@ -41,6 +43,8 @@ def get_nodedc_workspace_creation_policy(user, workspace_slug=None):
|
||||
"mode": "unlinked",
|
||||
"managed_by": "tasker",
|
||||
"default_managed_by": "tasker",
|
||||
"invite_approval": "tasker",
|
||||
"default_invite_approval": "tasker",
|
||||
"workspaces": [],
|
||||
"reason": "NODE.DC identity is not linked." if enforce_unlinked else "Standalone user without NODE.DC identity.",
|
||||
}
|
||||
@@ -69,6 +73,8 @@ def get_nodedc_workspace_creation_policy(user, workspace_slug=None):
|
||||
"mode": "unavailable",
|
||||
"managed_by": "tasker",
|
||||
"default_managed_by": "tasker",
|
||||
"invite_approval": "disabled",
|
||||
"default_invite_approval": "tasker",
|
||||
"workspaces": [],
|
||||
"reason": "NODE.DC workspace policy is unavailable.",
|
||||
}
|
||||
@@ -82,6 +88,8 @@ def get_nodedc_workspace_creation_policy(user, workspace_slug=None):
|
||||
"mode": "legacy_access_check",
|
||||
"managed_by": "tasker",
|
||||
"default_managed_by": "tasker",
|
||||
"invite_approval": "tasker",
|
||||
"default_invite_approval": "tasker",
|
||||
"workspaces": [],
|
||||
"reason": payload.get("reason") or "NODE.DC access check does not expose workspace policy.",
|
||||
}
|
||||
@@ -93,6 +101,13 @@ def get_nodedc_workspace_creation_policy(user, workspace_slug=None):
|
||||
workspaces=workspaces,
|
||||
fallback=workspace_policy.get("managedBy") or workspace_policy.get("defaultManagedBy"),
|
||||
)
|
||||
invite_approval = normalize_invite_approval(workspace_policy.get("inviteApproval") or workspace_policy.get("invite_approval"))
|
||||
default_invite_approval = normalize_invite_approval(
|
||||
workspace_policy.get("defaultInviteApproval")
|
||||
or workspace_policy.get("default_invite_approval")
|
||||
or workspace_policy.get("inviteApproval")
|
||||
or workspace_policy.get("invite_approval")
|
||||
)
|
||||
|
||||
return {
|
||||
"enabled": True,
|
||||
@@ -100,6 +115,8 @@ def get_nodedc_workspace_creation_policy(user, workspace_slug=None):
|
||||
"mode": workspace_policy.get("mode") or "unknown",
|
||||
"managed_by": managed_by,
|
||||
"default_managed_by": normalize_managed_by(workspace_policy.get("defaultManagedBy") or workspace_policy.get("managedBy")),
|
||||
"invite_approval": invite_approval,
|
||||
"default_invite_approval": default_invite_approval,
|
||||
"workspaces": workspaces,
|
||||
"reason": workspace_policy.get("reason") or payload.get("reason") or "NODE.DC workspace policy decision.",
|
||||
}
|
||||
@@ -113,6 +130,10 @@ def normalize_managed_by(value):
|
||||
return "launcher" if value == "launcher" else "tasker"
|
||||
|
||||
|
||||
def normalize_invite_approval(value):
|
||||
return value if value in {"tasker", "nodedc", "launcher", "disabled"} else "tasker"
|
||||
|
||||
|
||||
def normalize_workspace_management_list(value):
|
||||
if not isinstance(value, list):
|
||||
return []
|
||||
@@ -161,3 +182,7 @@ def nodedc_launcher_managed_workspace_response():
|
||||
"error": "nodedc_launcher_managed_workspace",
|
||||
"reason": "Участниками и ролями этого workspace управляет Launcher.",
|
||||
}
|
||||
|
||||
|
||||
def is_nodedc_workspace_invite_approval_required(policy):
|
||||
return bool(policy.get("enabled")) and policy.get("managed_by") == "tasker" and policy.get("invite_approval") == "nodedc"
|
||||
|
||||
@@ -17,6 +17,10 @@ from django.views import View
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.authentication.utils.login import user_login
|
||||
from plane.authentication.utils.redirection_path import get_redirection_path
|
||||
from plane.authentication.nodedc_workspace_join import (
|
||||
NodeDCWorkspaceInviteJoinError,
|
||||
accept_nodedc_workspace_invite_request_for_user,
|
||||
)
|
||||
from plane.authentication.views.nodedc_logout import get_nodedc_internal_token
|
||||
from plane.db.models import ExternalIdentityLink, Profile, User
|
||||
from plane.utils.path_validator import get_safe_redirect_url, validate_next_path
|
||||
@@ -24,6 +28,7 @@ from plane.utils.path_validator import get_safe_redirect_url, validate_next_path
|
||||
|
||||
OIDC_SESSION_KEY = "nodedc_oidc"
|
||||
OIDC_PROVIDER = "authentik"
|
||||
NODEDC_WORKSPACE_INVITE_ACCEPT_PREFIX = "/auth/nodedc/workspace-invite/accept/"
|
||||
DEFAULT_REQUIRED_GROUPS = "nodedc:superadmin,nodedc:taskmanager:admin,nodedc:taskmanager:user"
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -161,10 +166,45 @@ class NodeDCHandoffEndpoint(View):
|
||||
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
|
||||
workspace_invite_redirect = resolve_nodedc_workspace_invite_accept_redirect(base_url, next_path, user)
|
||||
if workspace_invite_redirect is not None:
|
||||
return workspace_invite_redirect
|
||||
|
||||
path = next_path or get_redirection_path(user=user)
|
||||
return HttpResponseRedirect(get_safe_redirect_url(base_url=base_url, next_path=path, params={}))
|
||||
|
||||
|
||||
def resolve_nodedc_workspace_invite_accept_redirect(base_url, next_path, user):
|
||||
if not next_path.startswith(NODEDC_WORKSPACE_INVITE_ACCEPT_PREFIX):
|
||||
return None
|
||||
|
||||
request_id = next_path.removeprefix(NODEDC_WORKSPACE_INVITE_ACCEPT_PREFIX).strip("/")
|
||||
|
||||
try:
|
||||
result = accept_nodedc_workspace_invite_request_for_user(request_id, user)
|
||||
except NodeDCWorkspaceInviteJoinError as error:
|
||||
logger.warning(
|
||||
"NODEDC managed workspace invite accept failed: code=%s user_id=%s request_id=%s",
|
||||
error.code,
|
||||
user.id,
|
||||
request_id,
|
||||
)
|
||||
return oidc_error_redirect(base_url, "", error.code)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"NODEDC managed workspace invite accept crashed: user_id=%s request_id=%s",
|
||||
user.id,
|
||||
request_id,
|
||||
)
|
||||
return oidc_error_redirect(base_url, "", "workspace_invite_accept_failed")
|
||||
|
||||
workspace_slug = result.get("workspaceSlug")
|
||||
if not workspace_slug:
|
||||
return oidc_error_redirect(base_url, "", "workspace_invite_redirect_missing")
|
||||
|
||||
return HttpResponseRedirect(f"{base_url.rstrip('/')}/{workspace_slug}/")
|
||||
|
||||
|
||||
def get_oidc_config():
|
||||
issuer = os.environ.get("PLANE_OIDC_ISSUER", "").strip()
|
||||
client_id = os.environ.get("PLANE_OIDC_CLIENT_ID", "").strip()
|
||||
|
||||
@@ -4,11 +4,13 @@ from urllib.parse import urlparse
|
||||
|
||||
from django.db import transaction
|
||||
from django.http import JsonResponse
|
||||
from django.utils import timezone
|
||||
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.utils.host import base_host
|
||||
from plane.db.models import (
|
||||
ExternalIdentityLink,
|
||||
IssueAssignee,
|
||||
@@ -18,6 +20,7 @@ from plane.db.models import (
|
||||
User,
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
WorkspaceMemberInvite,
|
||||
)
|
||||
|
||||
|
||||
@@ -226,6 +229,44 @@ def serialize_membership(membership, created):
|
||||
}
|
||||
|
||||
|
||||
def resolve_workspace_invite(payload):
|
||||
invite_id = payload.get("taskerInviteId") or payload.get("tasker_invite_id") or payload.get("inviteId")
|
||||
request_id = payload.get("requestId") or payload.get("request_id")
|
||||
|
||||
queryset = WorkspaceMemberInvite.objects.filter(deleted_at__isnull=True).select_related("workspace")
|
||||
if request_id:
|
||||
queryset = queryset.filter(nodedc_approval_request_id=request_id)
|
||||
if invite_id:
|
||||
return queryset.filter(id=invite_id).first()
|
||||
return None
|
||||
|
||||
|
||||
def build_workspace_invite_link(request, invitation):
|
||||
invite_path = f"/workspace-invitations/?invitation_id={invitation.id}&slug={invitation.workspace.slug}&token={invitation.token}"
|
||||
return f"{base_host(request=request, is_app=True).rstrip('/')}{invite_path}"
|
||||
|
||||
|
||||
def serialize_workspace_invite(request, invitation):
|
||||
tasker_invite_link = build_workspace_invite_link(request, invitation)
|
||||
return {
|
||||
"id": str(invitation.id),
|
||||
"email": invitation.email,
|
||||
"status": invitation.nodedc_approval_status,
|
||||
"workspace": serialize_workspace(invitation.workspace),
|
||||
"platformInviteLink": invitation.nodedc_platform_invite_link,
|
||||
"taskerInviteLink": (
|
||||
tasker_invite_link
|
||||
if invitation.nodedc_approval_status == WorkspaceMemberInvite.NODEDC_APPROVAL_APPROVED
|
||||
else None
|
||||
),
|
||||
"inviteLink": (
|
||||
invitation.nodedc_platform_invite_link or tasker_invite_link
|
||||
if invitation.nodedc_approval_status == WorkspaceMemberInvite.NODEDC_APPROVAL_APPROVED
|
||||
else None
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def serialize_project_membership(project_member, created):
|
||||
return {
|
||||
"created": created,
|
||||
@@ -414,6 +455,68 @@ class NodeDCInternalWorkspaceMembershipRemoveEndpoint(View):
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class NodeDCInternalWorkspaceInviteApproveEndpoint(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)
|
||||
|
||||
invitation = resolve_workspace_invite(payload)
|
||||
if invitation is None:
|
||||
return JsonResponse({"ok": False, "error": "workspace_invite_not_found"}, status=404)
|
||||
|
||||
invitation.nodedc_approval_status = WorkspaceMemberInvite.NODEDC_APPROVAL_APPROVED
|
||||
invitation.nodedc_approval_request_id = payload.get("requestId") or invitation.nodedc_approval_request_id
|
||||
invitation.nodedc_approval_decided_at = timezone.now()
|
||||
invitation.nodedc_platform_invite_link = payload.get("platformInviteLink") or invitation.nodedc_platform_invite_link
|
||||
invitation.save(
|
||||
update_fields=[
|
||||
"nodedc_approval_status",
|
||||
"nodedc_approval_request_id",
|
||||
"nodedc_approval_decided_at",
|
||||
"nodedc_platform_invite_link",
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
|
||||
return JsonResponse({"ok": True, "invite": serialize_workspace_invite(request, invitation)})
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class NodeDCInternalWorkspaceInviteRejectEndpoint(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)
|
||||
|
||||
invitation = resolve_workspace_invite(payload)
|
||||
if invitation is None:
|
||||
return JsonResponse({"ok": False, "error": "workspace_invite_not_found"}, status=404)
|
||||
|
||||
invitation.nodedc_approval_status = WorkspaceMemberInvite.NODEDC_APPROVAL_REJECTED
|
||||
invitation.nodedc_approval_request_id = payload.get("requestId") or invitation.nodedc_approval_request_id
|
||||
invitation.nodedc_approval_decided_at = timezone.now()
|
||||
invitation.message = payload.get("comment") or invitation.message
|
||||
invitation.save(
|
||||
update_fields=[
|
||||
"nodedc_approval_status",
|
||||
"nodedc_approval_request_id",
|
||||
"nodedc_approval_decided_at",
|
||||
"message",
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
|
||||
return JsonResponse({"ok": True, "invite": serialize_workspace_invite(request, invitation)})
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class NodeDCInternalProjectMembershipEnsureEndpoint(View):
|
||||
def post(self, request):
|
||||
|
||||
Reference in New Issue
Block a user