Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/api/plane/api/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@
from .invite import WorkspaceInviteSerializer
from .member import ProjectMemberSerializer
from .sticky import StickySerializer
from .page import PageSerializer
73 changes: 73 additions & 0 deletions apps/api/plane/api/serializers/page.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.

# Module imports
from .base import BaseSerializer
from plane.db.models import Page, ProjectPage, Project


class PageSerializer(BaseSerializer):
"""
Serializer for project pages.

Handles creation of a Page along with its ProjectPage join row so the
public API can create, list, retrieve and update pages scoped to a
project. Labels and revisions are not exposed in the MVP serializer.
"""

class Meta:
model = Page
fields = [
"id",
"name",
"description_html",
"description_json",
"owned_by",
"access",
"color",
"parent",
"is_locked",
"archived_at",
"view_props",
"logo_props",
"sort_order",
"external_id",
"external_source",
"workspace",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = [
"id",
"owned_by",
"workspace",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
Comment on lines +26 to +58
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

is_locked and archived_at should be read-only.

Both fields are managed exclusively by the dedicated /lock/ and /archive/ endpoints (which enforce ownership checks, recursive cascade, etc.). Exposing them as writable in the serializer lets a client silently set is_locked=true or archived_at=<timestamp> on a POST /pages/ or PATCH /pages/<id>/ call, completely bypassing that business logic.

🛡️ Proposed fix
         read_only_fields = [
             "id",
             "owned_by",
             "workspace",
             "created_at",
             "updated_at",
             "created_by",
             "updated_by",
+            "is_locked",
+            "archived_at",
         ]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/api/serializers/page.py` around lines 26 - 58, The
serializer's Meta currently exposes is_locked and archived_at as writable;
update the Meta.read_only_fields to include "is_locked" and "archived_at" so
these attributes cannot be set via Page creation or update (leave them listed in
Meta.fields but add both "is_locked" and "archived_at" to the read_only_fields
list in the Page serializer's Meta class).


def create(self, validated_data):
project_id = self.context["project_id"]
owned_by_id = self.context["owned_by_id"]

project = Project.objects.get(pk=project_id)

page = Page.objects.create(
**validated_data,
owned_by_id=owned_by_id,
workspace_id=project.workspace_id,
)

ProjectPage.objects.create(
workspace_id=page.workspace_id,
project_id=project_id,
page_id=page.id,
created_by_id=page.created_by_id,
updated_by_id=page.updated_by_id,
)

return page
Comment thread
coderabbitai[bot] marked this conversation as resolved.
2 changes: 2 additions & 0 deletions apps/api/plane/api/urls/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .work_item import urlpatterns as work_item_patterns
from .invite import urlpatterns as invite_patterns
from .sticky import urlpatterns as sticky_patterns
from .page import urlpatterns as page_patterns

urlpatterns = [
*asset_patterns,
Expand All @@ -28,4 +29,5 @@
*work_item_patterns,
*invite_patterns,
*sticky_patterns,
*page_patterns,
]
64 changes: 64 additions & 0 deletions apps/api/plane/api/urls/page.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.

from django.urls import path

from plane.api.views import (
ProjectPageListCreateAPIEndpoint,
ProjectPageDetailAPIEndpoint,
ProjectPageArchiveAPIEndpoint,
ProjectPageLockAPIEndpoint,
ProjectPageAccessAPIEndpoint,
ProjectPageDuplicateAPIEndpoint,
ProjectPageSummaryAPIEndpoint,
)

urlpatterns = [
# CRUD
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/",
ProjectPageListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/",
ProjectPageDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
name="project-pages",
),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# Summary
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages-summary/",
ProjectPageSummaryAPIEndpoint.as_view(http_method_names=["get"]),
name="project-pages-summary",
),
# Archive / unarchive
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/archive/",
ProjectPageArchiveAPIEndpoint.as_view(http_method_names=["post", "delete"]),
name="project-page-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-pages/",
ProjectPageArchiveAPIEndpoint.as_view(http_method_names=["get"]),
name="project-archived-pages",
),
# Lock / unlock
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/lock/",
ProjectPageLockAPIEndpoint.as_view(http_method_names=["post", "delete"]),
name="project-pages-lock-unlock",
),
# Access toggle
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/access/",
ProjectPageAccessAPIEndpoint.as_view(http_method_names=["post"]),
name="project-pages-access",
),
# Duplicate
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/duplicate/",
ProjectPageDuplicateAPIEndpoint.as_view(http_method_names=["post"]),
name="project-pages-duplicate",
),
]
10 changes: 10 additions & 0 deletions apps/api/plane/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,13 @@
from .invite import WorkspaceInvitationsViewset

from .sticky import StickyViewSet

from .page import (
ProjectPageListCreateAPIEndpoint,
ProjectPageDetailAPIEndpoint,
ProjectPageArchiveAPIEndpoint,
ProjectPageLockAPIEndpoint,
ProjectPageAccessAPIEndpoint,
ProjectPageDuplicateAPIEndpoint,
ProjectPageSummaryAPIEndpoint,
)
Loading