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

@@ -2,9 +2,14 @@ import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Plus, Trash2, Pencil, ChevronRight } from 'lucide-react';
import { toast } from 'sonner';
import { fetchWorkspaces, deleteWorkspace, type Workspace } from '../api/workspaces';
import { useAppContext } from '../context/AppContext';
import { useTranslation } from '../hooks/useTranslation';
import { fetchWorkspaces, deleteWorkspace, type Workspace } from '../api/workspaces';
import { useTranslation } from '../hooks/useTranslation';
import {
WORKSPACE_DELETE,
WORKSPACE_EDIT,
canWorkspace,
type WorkspaceRole,
} from '../lib/permissions';
import FilterBar from '../components/FilterBar';
import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
@@ -12,9 +17,7 @@ import { Card, CardContent, CardTitle } from '../components/ui/card';
import { Pagination } from '../components/Pagination';
import { Modal } from '../components/Modal';
type WorkspaceRole = "owner" | "admin" | "member" | "guest";
const RoleBadge = ({ role }: { role?: WorkspaceRole }) => {
const RoleBadge = ({ role }: { role?: WorkspaceRole }) => {
const { t } = useTranslation();
if (!role) return null;
@@ -45,9 +48,8 @@ export default function Workspaces() {
const [deleteModal, setDeleteModal] = useState<{isOpen: boolean; workspace: Workspace | null}>({isOpen: false, workspace: null});
const [deleteInput, setDeleteInput] = useState('');
const navigate = useNavigate();
const { user } = useAppContext();
const { t } = useTranslation();
const navigate = useNavigate();
const { t } = useTranslation();
const orderingOptions = [
{ value: '-created_at', label: t.ordering?.createdAtDesc || 'Newest First' },
@@ -120,13 +122,13 @@ export default function Workspaces() {
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">{t.workspace?.title || 'Workspaces'}</h1>
<p className="text-slate-500 dark:text-slate-400 mt-1">{t.workspace?.subtitle || 'Manage your workspaces'}</p>
</div>
<Button
onClick={() => navigate('/workspaces/create')}
className="gap-2 shadow-sm"
>
<Plus className="h-5 w-5" />
{t.workspace?.createNew || 'Create New'}
</Button>
<Button
onClick={() => navigate('/workspaces/create')}
className="gap-2 shadow-sm"
>
<Plus className="h-5 w-5" />
{t.workspace?.createNew || 'Create New'}
</Button>
</div>
<FilterBar
@@ -145,11 +147,11 @@ export default function Workspaces() {
) : (
<div className="flex flex-col flex-1">
<div className="flex flex-col gap-4 mb-6">
{workspaces.map((workspace) => {
const isOwner = workspace.owner === user?.id || workspace.my_role === 'owner';
const isAdmin = workspace.my_role === 'admin' || isOwner;
return (
{workspaces.map((workspace) => {
const canDeleteWorkspace = canWorkspace(workspace.my_role, WORKSPACE_DELETE);
const canEditWorkspace = canWorkspace(workspace.my_role, WORKSPACE_EDIT);
return (
<Card key={workspace.id} className="flex flex-col text-slate-800 dark:text-slate-100 dark:bg-slate-800 dark:border-slate-700 shadow-sm">
<CardContent className="flex flex-col sm:flex-row items-start sm:items-center justify-between py-4 px-6 gap-4">
<div className="flex-1">
@@ -165,8 +167,8 @@ export default function Workspaces() {
</div>
<div className="flex items-center gap-2 shrink-0">
{isOwner && (
<Button
{canDeleteWorkspace && (
<Button
variant="ghost"
size="icon"
onClick={() => setDeleteModal({ isOpen: true, workspace })}
@@ -177,8 +179,8 @@ export default function Workspaces() {
</Button>
)}
{isAdmin && (
<Button
{canEditWorkspace && (
<Button
variant="ghost"
size="icon"
onClick={() => navigate(`/workspaces/${workspace.id}/edit`)}