feat(permissions): gate workspace resources by role
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user