feat(permissions): gate workspace resources by role

This commit is contained in:
2026-04-25 18:48:49 +03:30
parent c8c689e693
commit 7f0e00f09d
11 changed files with 511 additions and 200 deletions

View File

@@ -13,7 +13,15 @@ import {
getWorkspace
} from '../api/workspaces';
import { searchUserByExactMobile, type SearchedUser } from '../api/users';
import { useAppContext } from '../context/AppContext';
import { useAppContext } from '../context/AppContext';
import {
WORKSPACE_EDIT,
WORKSPACE_MEMBERS_ADD,
WORKSPACE_MEMBERS_CHANGE_ROLE,
canChangeWorkspaceMember,
canWorkspace,
type WorkspaceRole,
} from '../lib/permissions';
import { Button } from '../components/ui/button';
import { InfiniteScroll } from '../components/InfiniteScroll';
import { Select } from '../components/ui/Select';
@@ -45,7 +53,7 @@ export default function EditWorkspace() {
// Workspace Info States
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [myRole, setMyRole] = useState<'owner' | 'admin' | 'member' | 'guest'>('member');
const [myRole, setMyRole] = useState<WorkspaceRole>('member');
const [workspaceOwnerId, setWorkspaceOwnerId] = useState<string>('');
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
@@ -101,9 +109,16 @@ export default function EditWorkspace() {
return false;
});
useEffect(() => {
if (id) loadData();
}, [id]);
useEffect(() => {
if (id) loadData();
}, [id]);
useEffect(() => {
if (!isLoading && id && !canWorkspace(myRole, WORKSPACE_EDIT)) {
toast.error("You do not have permission to edit this workspace.");
navigate(`/workspaces/${id}`);
}
}, [id, isLoading, myRole, navigate]);
const loadData = async () => {
try {
@@ -258,10 +273,17 @@ export default function EditWorkspace() {
}
};
const canManageMembers = myRole === 'owner' || myRole === 'admin';
const isFirstOwner = currentUserId === workspaceOwnerId;
const canManageMembers = canWorkspace(myRole, WORKSPACE_MEMBERS_CHANGE_ROLE);
const isFirstOwner = currentUserId === workspaceOwnerId;
if (isLoading) return <div className="p-8 text-center">{t.workspace?.loading || "Loading..."}</div>;
const roleOptions = (allowOwner: boolean) => [
...(allowOwner ? [{ value: "owner", label: t.workspace?.roles?.owner || "Owner" }] : []),
{ value: "admin", label: t.workspace?.roles?.admin || "Admin" },
{ value: "member", label: t.workspace?.roles?.member || "Member" },
{ value: "guest", label: t.workspace?.roles?.guest || "Guest" },
];
if (isLoading) return <div className="p-8 text-center">{t.workspace?.loading || "Loading..."}</div>;
return (
<div className="absolute inset-0 flex flex-col p-4 sm:p-6 bg-slate-50 dark:bg-slate-900 overflow-hidden">
@@ -312,8 +334,8 @@ export default function EditWorkspace() {
{ t.workspace?.members || "Members" }
</h2>
{canManageMembers && (
<div className="space-y-3">
{canWorkspace(myRole, WORKSPACE_MEMBERS_ADD) && (
<div className="space-y-3">
<Input
type="text"
placeholder={t.workspace?.searchMemberPlaceholder || "Search user by exact mobile number..."}
@@ -357,11 +379,8 @@ export default function EditWorkspace() {
value={newMemberRole}
onChange={(val) => setNewMemberRole(val as any)}
options={[
...(isFirstOwner ? [{ value: "owner", label: t.workspace?.roles?.owner || "Owner" }] : []),
{ value: "admin", label: t.workspace?.roles?.admin || "Admin" },
{ value: "member", label: t.workspace?.roles?.member || "Member" },
{ value: "guest", label: t.workspace?.roles?.guest || "Guest" },
]}
...roleOptions(isFirstOwner),
]}
className="flex-1 sm:flex-none"
buttonClassName="w-full sm:w-[110px] px-3 py-1.5 text-sm"
/>
@@ -395,7 +414,13 @@ export default function EditWorkspace() {
>
{members.map((m) => {
const isThisMemberTheFirstOwner = m.user?.id === workspaceOwnerId;
const canChangeThisUserRole = canManageMembers && !isThisMemberTheFirstOwner && (isFirstOwner || m.role !== 'owner');
const canChangeThisUserRole = canChangeWorkspaceMember({
actorRole: myRole,
actorUserId: currentUserId,
targetRole: m.role,
targetUserId: m.user?.id,
ownerUserId: workspaceOwnerId,
});
return (
<div key={m.id} className="flex flex-col sm:flex-row sm:items-center justify-between p-3 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-lg gap-3 shadow-sm hover:border-blue-200 dark:hover:border-blue-800 transition-colors">
@@ -420,12 +445,7 @@ export default function EditWorkspace() {
<Select
value={m.role}
onChange={(val) => handleChangeRole(m.id, val)}
options={[
...(isFirstOwner ? [{ value: "owner", label: t.workspace?.roles?.owner || "Owner" }] : []),
{ value: "admin", label: t.workspace?.roles?.admin || "Admin" },
{ value: "member", label: t.workspace?.roles?.member || "Member" },
{ value: "guest", label: t.workspace?.roles?.guest || "Guest" },
]}
options={roleOptions(isFirstOwner)}
buttonClassName="w-[110px] px-3 py-1.5 text-sm"
/>
) : (
@@ -437,7 +457,7 @@ export default function EditWorkspace() {
</span>
)}
{canManageMembers && !isThisMemberTheFirstOwner && (isFirstOwner || m.role !== 'owner') && (
{canChangeThisUserRole && (
<Button
type="button"
variant="ghost"