ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: 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 django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
from plane.authentication.views.nodedc_logout import is_internal_logout_request_authorized
|
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"
|
OIDC_PROVIDER = "authentik"
|
||||||
|
|
@ -24,6 +24,11 @@ ROLE_VALUES = {
|
||||||
15: 15,
|
15: 15,
|
||||||
ADMIN_ROLE: ADMIN_ROLE,
|
ADMIN_ROLE: ADMIN_ROLE,
|
||||||
}
|
}
|
||||||
|
PROJECT_ROLE_LABELS = {
|
||||||
|
5: "guest",
|
||||||
|
15: "member",
|
||||||
|
ADMIN_ROLE: "admin",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def internal_unauthorized_response():
|
def internal_unauthorized_response():
|
||||||
|
|
@ -56,6 +61,21 @@ def resolve_workspace(payload):
|
||||||
return None
|
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):
|
def resolve_user(payload):
|
||||||
plane_user_id = payload.get("planeUserId") or payload.get("plane_user_id")
|
plane_user_id = payload.get("planeUserId") or payload.get("plane_user_id")
|
||||||
subject = payload.get("subject")
|
subject = payload.get("subject")
|
||||||
|
|
@ -92,6 +112,10 @@ def normalize_role(value):
|
||||||
return ROLE_VALUES.get(value, 15)
|
return ROLE_VALUES.get(value, 15)
|
||||||
|
|
||||||
|
|
||||||
|
def project_role_slug(value):
|
||||||
|
return PROJECT_ROLE_LABELS.get(value, "member")
|
||||||
|
|
||||||
|
|
||||||
def first_payload_string(payload, *keys):
|
def first_payload_string(payload, *keys):
|
||||||
for key in keys:
|
for key in keys:
|
||||||
value = payload.get(key)
|
value = payload.get(key)
|
||||||
|
|
@ -147,7 +171,22 @@ def sync_user_avatar_from_payload(user, payload):
|
||||||
user.save(update_fields=["avatar", "updated_at"])
|
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 {
|
return {
|
||||||
"id": str(workspace.id),
|
"id": str(workspace.id),
|
||||||
"slug": workspace.slug,
|
"slug": workspace.slug,
|
||||||
|
|
@ -159,6 +198,7 @@ def serialize_workspace(workspace):
|
||||||
is_active=True,
|
is_active=True,
|
||||||
member__is_bot=False,
|
member__is_bot=False,
|
||||||
).count(),
|
).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):
|
def restore_admin_project_memberships(workspace, user):
|
||||||
restored = 0
|
restored = 0
|
||||||
for project_member in ProjectMember.objects.filter(
|
for project_member in ProjectMember.objects.filter(
|
||||||
|
|
@ -204,8 +260,22 @@ class NodeDCInternalWorkspaceListEndpoint(View):
|
||||||
if not is_internal_logout_request_authorized(request):
|
if not is_internal_logout_request_authorized(request):
|
||||||
return internal_unauthorized_response()
|
return internal_unauthorized_response()
|
||||||
|
|
||||||
workspaces = Workspace.objects.filter(deleted_at__isnull=True).select_related("owner").order_by("name")
|
workspaces = list(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]})
|
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")
|
@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,
|
NodeDCInternalSessionLogoutEndpoint,
|
||||||
)
|
)
|
||||||
from plane.authentication.views.nodedc_workspace_adapter import (
|
from plane.authentication.views.nodedc_workspace_adapter import (
|
||||||
|
NodeDCInternalProjectMembershipEnsureEndpoint,
|
||||||
|
NodeDCInternalProjectMembershipRemoveEndpoint,
|
||||||
NodeDCInternalWorkspaceListEndpoint,
|
NodeDCInternalWorkspaceListEndpoint,
|
||||||
NodeDCInternalWorkspaceMembershipEnsureEndpoint,
|
NodeDCInternalWorkspaceMembershipEnsureEndpoint,
|
||||||
NodeDCInternalWorkspaceMembershipRemoveEndpoint,
|
NodeDCInternalWorkspaceMembershipRemoveEndpoint,
|
||||||
|
|
@ -44,6 +46,16 @@ urlpatterns = [
|
||||||
NodeDCInternalWorkspaceMembershipRemoveEndpoint.as_view(),
|
NodeDCInternalWorkspaceMembershipRemoveEndpoint.as_view(),
|
||||||
name="nodedc-internal-workspace-membership-remove",
|
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/", 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")),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue