ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: self-service workspace invite approval
This commit is contained in:
@@ -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