ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: PROJECT-LEVEL ДОСТУПЫ OPERATIONAL CORE
This commit is contained in:
parent
3afa15d326
commit
882b409d1c
|
|
@ -9,7 +9,7 @@ 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, ProjectMember, User, Workspace, WorkspaceMember
|
||||
from plane.db.models import ExternalIdentityLink, Profile, Project, ProjectMember, User, Workspace, WorkspaceMember
|
||||
|
||||
|
||||
OIDC_PROVIDER = "authentik"
|
||||
|
|
@ -24,6 +24,11 @@ ROLE_VALUES = {
|
|||
15: 15,
|
||||
ADMIN_ROLE: ADMIN_ROLE,
|
||||
}
|
||||
PROJECT_ROLE_LABELS = {
|
||||
5: "guest",
|
||||
15: "member",
|
||||
ADMIN_ROLE: "admin",
|
||||
}
|
||||
|
||||
|
||||
def internal_unauthorized_response():
|
||||
|
|
@ -56,6 +61,21 @@ def resolve_workspace(payload):
|
|||
return None
|
||||
|
||||
|
||||
def resolve_project(payload, workspace=None):
|
||||
project_id = payload.get("projectId") or payload.get("project_id")
|
||||
project_identifier = payload.get("projectIdentifier") or payload.get("project_identifier") or payload.get("identifier")
|
||||
|
||||
queryset = Project.objects.filter(deleted_at__isnull=True).select_related("workspace")
|
||||
if workspace is not None:
|
||||
queryset = queryset.filter(workspace=workspace)
|
||||
|
||||
if project_id:
|
||||
return queryset.filter(id=project_id).first()
|
||||
if project_identifier and workspace is not None:
|
||||
return queryset.filter(identifier__iexact=project_identifier).first()
|
||||
return None
|
||||
|
||||
|
||||
def resolve_user(payload):
|
||||
plane_user_id = payload.get("planeUserId") or payload.get("plane_user_id")
|
||||
subject = payload.get("subject")
|
||||
|
|
@ -92,6 +112,10 @@ def normalize_role(value):
|
|||
return ROLE_VALUES.get(value, 15)
|
||||
|
||||
|
||||
def project_role_slug(value):
|
||||
return PROJECT_ROLE_LABELS.get(value, "member")
|
||||
|
||||
|
||||
def first_payload_string(payload, *keys):
|
||||
for key in keys:
|
||||
value = payload.get(key)
|
||||
|
|
@ -147,7 +171,22 @@ def sync_user_avatar_from_payload(user, payload):
|
|||
user.save(update_fields=["avatar", "updated_at"])
|
||||
|
||||
|
||||
def serialize_workspace(workspace):
|
||||
def serialize_project(project):
|
||||
return {
|
||||
"id": str(project.id),
|
||||
"workspaceSlug": project.workspace.slug,
|
||||
"name": project.name,
|
||||
"identifier": project.identifier,
|
||||
"memberCount": ProjectMember.objects.filter(
|
||||
project=project,
|
||||
deleted_at__isnull=True,
|
||||
is_active=True,
|
||||
member__is_bot=False,
|
||||
).count(),
|
||||
}
|
||||
|
||||
|
||||
def serialize_workspace(workspace, projects=None):
|
||||
return {
|
||||
"id": str(workspace.id),
|
||||
"slug": workspace.slug,
|
||||
|
|
@ -159,6 +198,7 @@ def serialize_workspace(workspace):
|
|||
is_active=True,
|
||||
member__is_bot=False,
|
||||
).count(),
|
||||
"projects": [serialize_project(project) for project in projects] if projects is not None else [],
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -177,6 +217,22 @@ def serialize_membership(membership, created):
|
|||
}
|
||||
|
||||
|
||||
def serialize_project_membership(project_member, created):
|
||||
return {
|
||||
"created": created,
|
||||
"workspace": serialize_workspace(project_member.workspace),
|
||||
"project": serialize_project(project_member.project),
|
||||
"member": {
|
||||
"id": str(project_member.member.id),
|
||||
"email": project_member.member.email,
|
||||
"displayName": project_member.member.display_name,
|
||||
},
|
||||
"role": project_member.role,
|
||||
"roleSlug": project_role_slug(project_member.role),
|
||||
"isActive": project_member.is_active,
|
||||
}
|
||||
|
||||
|
||||
def restore_admin_project_memberships(workspace, user):
|
||||
restored = 0
|
||||
for project_member in ProjectMember.objects.filter(
|
||||
|
|
@ -204,8 +260,22 @@ class NodeDCInternalWorkspaceListEndpoint(View):
|
|||
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]})
|
||||
workspaces = list(Workspace.objects.filter(deleted_at__isnull=True).select_related("owner").order_by("name"))
|
||||
projects_by_workspace = {workspace.id: [] for workspace in workspaces}
|
||||
for project in Project.objects.filter(
|
||||
workspace__in=workspaces,
|
||||
deleted_at__isnull=True,
|
||||
).select_related("workspace").order_by("workspace_id", "name"):
|
||||
projects_by_workspace.setdefault(project.workspace_id, []).append(project)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"ok": True,
|
||||
"workspaces": [
|
||||
serialize_workspace(workspace, projects_by_workspace.get(workspace.id, [])) for workspace in workspaces
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
|
|
@ -329,3 +399,152 @@ class NodeDCInternalWorkspaceMembershipRemoveEndpoint(View):
|
|||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class NodeDCInternalProjectMembershipEnsureEndpoint(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)
|
||||
project = resolve_project(payload, workspace)
|
||||
if project is None:
|
||||
return JsonResponse({"ok": False, "error": "project_not_found"}, status=404)
|
||||
if workspace is None:
|
||||
workspace = project.workspace
|
||||
if project.workspace_id != workspace.id:
|
||||
return JsonResponse({"ok": False, "error": "project_workspace_mismatch"}, status=400)
|
||||
|
||||
user = resolve_user(payload)
|
||||
if user is None:
|
||||
return JsonResponse({"ok": False, "error": "user_not_found"}, status=404)
|
||||
|
||||
role = normalize_role(payload.get("role"))
|
||||
fallback_workspace_role = 5 if role == 5 else 15
|
||||
|
||||
with transaction.atomic():
|
||||
sync_user_avatar_from_payload(user, payload)
|
||||
|
||||
workspace_membership = WorkspaceMember.objects.filter(
|
||||
workspace=workspace,
|
||||
member=user,
|
||||
deleted_at__isnull=True,
|
||||
).first()
|
||||
if workspace_membership is None:
|
||||
WorkspaceMember.objects.create(
|
||||
workspace=workspace,
|
||||
member=user,
|
||||
role=fallback_workspace_role,
|
||||
company_role=None,
|
||||
is_active=True,
|
||||
is_banned=False,
|
||||
)
|
||||
else:
|
||||
update_fields = []
|
||||
if not workspace_membership.is_active:
|
||||
workspace_membership.is_active = True
|
||||
update_fields.append("is_active")
|
||||
if workspace_membership.is_banned:
|
||||
workspace_membership.is_banned = False
|
||||
workspace_membership.banned_at = None
|
||||
workspace_membership.banned_until = None
|
||||
update_fields.extend(["is_banned", "banned_at", "banned_until"])
|
||||
if workspace_membership.role < fallback_workspace_role:
|
||||
workspace_membership.role = fallback_workspace_role
|
||||
update_fields.append("role")
|
||||
if update_fields:
|
||||
update_fields.append("updated_at")
|
||||
workspace_membership.save(update_fields=update_fields)
|
||||
|
||||
project_member = ProjectMember.objects.filter(
|
||||
project=project,
|
||||
member=user,
|
||||
deleted_at__isnull=True,
|
||||
).first()
|
||||
created = project_member is None
|
||||
if project_member is None:
|
||||
project_member = ProjectMember.objects.create(
|
||||
project=project,
|
||||
workspace=workspace,
|
||||
member=user,
|
||||
role=role,
|
||||
is_active=True,
|
||||
)
|
||||
else:
|
||||
project_member.role = role
|
||||
project_member.is_active = True
|
||||
project_member.save(update_fields=["role", "is_active", "updated_at"])
|
||||
|
||||
if payload.get("setLastWorkspace", False) is True:
|
||||
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_project_membership(project_member, created)})
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class NodeDCInternalProjectMembershipRemoveEndpoint(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)
|
||||
project = resolve_project(payload, workspace)
|
||||
if project is None:
|
||||
return JsonResponse({"ok": False, "error": "project_not_found"}, status=404)
|
||||
if workspace is None:
|
||||
workspace = project.workspace
|
||||
if project.workspace_id != workspace.id:
|
||||
return JsonResponse({"ok": False, "error": "project_workspace_mismatch"}, status=400)
|
||||
|
||||
user = resolve_user(payload)
|
||||
if user is None:
|
||||
return JsonResponse({"ok": False, "error": "user_not_found"}, status=404)
|
||||
|
||||
project_member = ProjectMember.objects.filter(
|
||||
project=project,
|
||||
member=user,
|
||||
deleted_at__isnull=True,
|
||||
).first()
|
||||
|
||||
if project_member is None or not project_member.is_active:
|
||||
return JsonResponse(
|
||||
{
|
||||
"ok": True,
|
||||
"removed": False,
|
||||
"workspace": serialize_workspace(workspace),
|
||||
"project": serialize_project(project),
|
||||
"member": {
|
||||
"id": str(user.id),
|
||||
"email": user.email,
|
||||
"displayName": user.display_name,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
project_member.is_active = False
|
||||
project_member.save(update_fields=["is_active", "updated_at"])
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"ok": True,
|
||||
"removed": True,
|
||||
"workspace": serialize_workspace(workspace),
|
||||
"project": serialize_project(project),
|
||||
"member": {
|
||||
"id": str(user.id),
|
||||
"email": user.email,
|
||||
"displayName": user.display_name,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ from plane.authentication.views.nodedc_logout import (
|
|||
NodeDCInternalSessionLogoutEndpoint,
|
||||
)
|
||||
from plane.authentication.views.nodedc_workspace_adapter import (
|
||||
NodeDCInternalProjectMembershipEnsureEndpoint,
|
||||
NodeDCInternalProjectMembershipRemoveEndpoint,
|
||||
NodeDCInternalWorkspaceListEndpoint,
|
||||
NodeDCInternalWorkspaceMembershipEnsureEndpoint,
|
||||
NodeDCInternalWorkspaceMembershipRemoveEndpoint,
|
||||
|
|
@ -44,6 +46,16 @@ urlpatterns = [
|
|||
NodeDCInternalWorkspaceMembershipRemoveEndpoint.as_view(),
|
||||
name="nodedc-internal-workspace-membership-remove",
|
||||
),
|
||||
path(
|
||||
"api/internal/nodedc/project-memberships/ensure/",
|
||||
NodeDCInternalProjectMembershipEnsureEndpoint.as_view(),
|
||||
name="nodedc-internal-project-membership-ensure",
|
||||
),
|
||||
path(
|
||||
"api/internal/nodedc/project-memberships/remove/",
|
||||
NodeDCInternalProjectMembershipRemoveEndpoint.as_view(),
|
||||
name="nodedc-internal-project-membership-remove",
|
||||
),
|
||||
path("api/", include("plane.app.urls")),
|
||||
path("api/public/", include("plane.space.urls")),
|
||||
path("api/instances/", include("plane.license.urls")),
|
||||
|
|
|
|||
Loading…
Reference in New Issue