From b1ad372474cc713d7d33e16cad8e585a086b3f22 Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Tue, 28 Apr 2026 10:02:37 +0330 Subject: [PATCH] fix(permissions): align workspace resource actions with role rules --- src/api/projects.ts | 20 ++++++++---- src/api/tags.ts | 8 +++++ src/lib/permissions.ts | 15 +++++++++ src/pages/Clients.tsx | 62 +++++++++++++++++++++---------------- src/pages/Projects.tsx | 48 ++++++++++++++++------------ src/pages/Tags.tsx | 16 +++++++--- src/pages/WorkspaceEdit.tsx | 23 +++++++------- src/types/client.ts | 26 ++++++++++------ 8 files changed, 141 insertions(+), 77 deletions(-) diff --git a/src/api/projects.ts b/src/api/projects.ts index 2514613..be4d7e0 100644 --- a/src/api/projects.ts +++ b/src/api/projects.ts @@ -1,9 +1,16 @@ -import { authFetch } from "./client"; - -export interface ProjectClient { - id: string; - name: string; -} +import { authFetch } from "./client"; + +interface AuditUser { + id: string; + first_name?: string; + last_name?: string; + mobile?: string; +} + +export interface ProjectClient { + id: string; + name: string; +} export interface ProjectMemberPayload { user_id: string; @@ -33,6 +40,7 @@ export interface Project { is_archived: boolean; is_deleted?: boolean; workspace: string; + created_by?: AuditUser | null; client: ProjectClient | null; my_role?: string; members?: ProjectMembership[]; diff --git a/src/api/tags.ts b/src/api/tags.ts index f928383..c05124d 100644 --- a/src/api/tags.ts +++ b/src/api/tags.ts @@ -1,11 +1,19 @@ import { authFetch } from "./client"; +interface AuditUser { + id: string; + first_name?: string; + last_name?: string; + mobile?: string; +} + export interface Tag { id: string; workspace: string; name: string; color: string; is_deleted?: boolean; + created_by?: AuditUser | null; created_at: string; updated_at: string; } diff --git a/src/lib/permissions.ts b/src/lib/permissions.ts index 151ac80..633f394 100644 --- a/src/lib/permissions.ts +++ b/src/lib/permissions.ts @@ -163,6 +163,21 @@ export const canProject = ({ return projectRole === "manager" && PROJECT_MANAGER_CAPABILITIES.has(capability); }; +export const canDeleteWorkspaceResource = ({ + workspaceRole, + currentUserId, + createdById, +}: { + workspaceRole: WorkspaceRole | null | undefined; + currentUserId?: string | null; + createdById?: string | null; +}) => { + if (!workspaceRole) return false; + if (workspaceRole === "owner") return true; + if (!currentUserId || !createdById) return false; + return currentUserId === createdById; +}; + export const canChangeWorkspaceMember = ({ actorRole, actorUserId, diff --git a/src/pages/Clients.tsx b/src/pages/Clients.tsx index 7b76ab3..33d2ac2 100644 --- a/src/pages/Clients.tsx +++ b/src/pages/Clients.tsx @@ -1,13 +1,14 @@ import { useEffect, useState } from "react" import { Plus, Building2, Loader2, Pencil, Trash2 } from "lucide-react" -import { useWorkspace } from "../context/WorkspaceContext" -import { useTranslation } from "../hooks/useTranslation" -import { - CLIENTS_CREATE, - CLIENTS_DELETE, - CLIENTS_EDIT, - canWorkspace, -} from "../lib/permissions" +import { useWorkspace } from "../context/WorkspaceContext" +import { useAppContext } from "../context/AppContext" +import { useTranslation } from "../hooks/useTranslation" +import { + CLIENTS_CREATE, + CLIENTS_EDIT, + canDeleteWorkspaceResource, + canWorkspace, +} from "../lib/permissions" import { type Client } from "../types/client" import { getClients } from "../api/clients" import CreateClientModal from "../components/CreateClientModal" @@ -18,9 +19,10 @@ import { Button } from "../components/ui/button" import { Card } from "../components/ui/card" import { Pagination } from "../components/Pagination" -export default function Clients() { - const { activeWorkspace } = useWorkspace() - const [clients, setClients] = useState([]) +export default function Clients() { + const { activeWorkspace } = useWorkspace() + const { user } = useAppContext() + const [clients, setClients] = useState([]) const [isLoading, setIsLoading] = useState(true) // Pagination States @@ -40,10 +42,9 @@ export default function Clients() { const { t, lang } = useTranslation() const isFa = lang === "fa" - const workspaceRole = activeWorkspace?.my_role - const canCreateClient = canWorkspace(workspaceRole, CLIENTS_CREATE) - const canEditClient = canWorkspace(workspaceRole, CLIENTS_EDIT) - const canDeleteClient = canWorkspace(workspaceRole, CLIENTS_DELETE) + const workspaceRole = activeWorkspace?.my_role + const canCreateClient = canWorkspace(workspaceRole, CLIENTS_CREATE) + const canEditClient = canWorkspace(workspaceRole, CLIENTS_EDIT) const orderingOptions = [ { value: "-created_at", label: t.ordering?.createdAtDesc || "Newest First" }, @@ -161,8 +162,14 @@ export default function Clients() { ) : (
    - {clients.map((client) => ( -
  • + {clients.map((client) => { + const canDeleteClient = canDeleteWorkspaceResource({ + workspaceRole, + currentUserId: user?.id, + createdById: client.created_by?.id, + }) + return ( +
  • {client.name}

    {client.notes && ( @@ -196,10 +203,11 @@ export default function Clients() { )}
    )} -
  • - ))} -
- )} + + ) + })} + + )} @@ -231,12 +239,12 @@ export default function Clients() { /> )} - {canDeleteClient && ( - setDeleteClient(null)} - onSuccess={fetchClientsList} - client={deleteClient} + {!!deleteClient && ( + setDeleteClient(null)} + onSuccess={fetchClientsList} + client={deleteClient} /> )} diff --git a/src/pages/Projects.tsx b/src/pages/Projects.tsx index e03f2b9..e2311c0 100644 --- a/src/pages/Projects.tsx +++ b/src/pages/Projects.tsx @@ -1,7 +1,8 @@ import React, { useState, useEffect } from "react"; import { useTranslation } from "../hooks/useTranslation"; -import { getProjects, deleteProject, type Project } from "../api/projects"; -import { useWorkspace } from "../context/WorkspaceContext"; +import { getProjects, deleteProject, type Project } from "../api/projects"; +import { useAppContext } from "../context/AppContext"; +import { useWorkspace } from "../context/WorkspaceContext"; import { ProjectCreateModal } from "../components/projects/ProjectCreateModal"; import { ProjectEditModal } from "../components/projects/ProjectEditModal"; import { Pagination } from "../components/Pagination"; @@ -14,21 +15,21 @@ import { Modal } from "../components/Modal"; import { toast } from "sonner"; import { Input } from "../components/ui/input"; import { - PROJECTS_ARCHIVE, - PROJECTS_CREATE, - PROJECTS_DELETE, - PROJECTS_EDIT, - canWorkspace, -} from "../lib/permissions"; + PROJECTS_ARCHIVE, + PROJECTS_CREATE, + PROJECTS_EDIT, + canDeleteWorkspaceResource, + canWorkspace, +} from "../lib/permissions"; export const Projects: React.FC = () => { - const { t, lang } = useTranslation(); - const { activeWorkspace } = useWorkspace(); - const workspaceRole = activeWorkspace?.my_role; - const canCreateProject = canWorkspace(workspaceRole, PROJECTS_CREATE); - const canEditProject = canWorkspace(workspaceRole, PROJECTS_EDIT); - const canDeleteProject = canWorkspace(workspaceRole, PROJECTS_DELETE); - const canArchiveProject = canWorkspace(workspaceRole, PROJECTS_ARCHIVE); + const { t, lang } = useTranslation(); + const { user } = useAppContext(); + const { activeWorkspace } = useWorkspace(); + const workspaceRole = activeWorkspace?.my_role; + const canCreateProject = canWorkspace(workspaceRole, PROJECTS_CREATE); + const canEditProject = canWorkspace(workspaceRole, PROJECTS_EDIT); + const canArchiveProject = canWorkspace(workspaceRole, PROJECTS_ARCHIVE); const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(false); @@ -188,9 +189,15 @@ export const Projects: React.FC = () => { ) : (
    - {projects.map((project) => ( -
  • { + const canDeleteProject = canDeleteWorkspaceResource({ + workspaceRole, + currentUserId: user?.id, + createdById: project.created_by?.id, + }); + return ( +
  • @@ -232,8 +239,9 @@ export const Projects: React.FC = () => { )}
    )} -
  • - ))} + + ); + })}
)} diff --git a/src/pages/Tags.tsx b/src/pages/Tags.tsx index cd8dabc..353fc6b 100644 --- a/src/pages/Tags.tsx +++ b/src/pages/Tags.tsx @@ -3,9 +3,10 @@ import { Edit2, Plus, Tag as TagIcon, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { createTag, deleteTag, getTags, type Tag, updateTag } from "../api/tags"; +import { useAppContext } from "../context/AppContext"; import { useWorkspace } from "../context/WorkspaceContext"; import { useTranslation } from "../hooks/useTranslation"; -import { TAGS_CREATE, TAGS_DELETE, TAGS_EDIT, canWorkspace } from "../lib/permissions"; +import { TAGS_CREATE, TAGS_EDIT, canDeleteWorkspaceResource, canWorkspace } from "../lib/permissions"; import FilterBar from "../components/FilterBar"; import { Modal } from "../components/Modal"; import { Pagination } from "../components/Pagination"; @@ -17,11 +18,11 @@ const DEFAULT_COLOR = "#3B82F6"; export default function Tags() { const { t } = useTranslation(); + const { user } = useAppContext(); const { activeWorkspace } = useWorkspace(); const workspaceRole = activeWorkspace?.my_role; const canCreateTag = canWorkspace(workspaceRole, TAGS_CREATE); const canEditTag = canWorkspace(workspaceRole, TAGS_EDIT); - const canDeleteTag = canWorkspace(workspaceRole, TAGS_DELETE); const [tags, setTags] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -172,7 +173,13 @@ export default function Tags() { ) : (
- {tags.map((tag) => ( + {tags.map((tag) => { + const canDeleteTag = canDeleteWorkspaceResource({ + workspaceRole, + currentUserId: user?.id, + createdById: tag.created_by?.id, + }); + return (
@@ -208,7 +215,8 @@ export default function Tags() {
- ))} + ); + })} {tags.length === 0 && (
diff --git a/src/pages/WorkspaceEdit.tsx b/src/pages/WorkspaceEdit.tsx index 89f0704..fa85136 100644 --- a/src/pages/WorkspaceEdit.tsx +++ b/src/pages/WorkspaceEdit.tsx @@ -285,10 +285,11 @@ export default function EditWorkspace() { const canManageMembers = canWorkspace(myRole, WORKSPACE_MEMBERS_CHANGE_ROLE); const isFirstOwner = currentUserId === workspaceOwnerId; - - const roleOptions = (allowOwner: boolean) => [ - ...(allowOwner ? [{ value: "owner", label: t.workspace?.roles?.owner || "Owner" }] : []), - { value: "admin", label: t.workspace?.roles?.admin || "Admin" }, + const isOwner = myRole === "owner"; + + const roleOptions = (allowOwnerRole: boolean, allowAdminRole: boolean) => [ + ...(allowOwnerRole ? [{ value: "owner", label: t.workspace?.roles?.owner || "Owner" }] : []), + ...(allowAdminRole ? [{ value: "admin", label: t.workspace?.roles?.admin || "Admin" }] : []), { value: "member", label: t.workspace?.roles?.member || "Member" }, { value: "guest", label: t.workspace?.roles?.guest || "Guest" }, ]; @@ -386,13 +387,13 @@ export default function EditWorkspace() {