refactor(api): add granular permission enforcement on admin routes
- add optional `permission` field to route definitions with type validation in `define.js` - check `hasPermission()` in router after `requireAdmin()` and return 403 if denied - document `permission` and `skipRateLimit` optional fields in api README - load user permissions in `AdminPage.server.js` and pass them to client via `user` prop - use `user.permissions` in `RolesPage` and `UsersPage` to conditionally render actions - expose permission-gated API routes in `auth/api.js`
This commit is contained in:
@@ -3,18 +3,23 @@ import { protectAdmin } from './protect.js';
|
||||
import { collectWidgetData } from './registry.js';
|
||||
import { getAppConfig, getPublicBaseUrl } from '@zen/core';
|
||||
import { isDevkitEnabled } from '../../shared/lib/appConfig.js';
|
||||
import { getUserPermissions } from '@zen/core/users';
|
||||
|
||||
export default async function AdminPage({ params }) {
|
||||
const resolvedParams = await params;
|
||||
const session = await protectAdmin();
|
||||
const widgetData = await collectWidgetData();
|
||||
const [widgetData, permissions] = await Promise.all([
|
||||
collectWidgetData(),
|
||||
getUserPermissions(session.user.id),
|
||||
]);
|
||||
const appConfig = { ...getAppConfig(), siteUrl: getPublicBaseUrl() };
|
||||
const devkitEnabled = isDevkitEnabled();
|
||||
const user = { ...session.user, permissions };
|
||||
|
||||
return (
|
||||
<AdminPageClient
|
||||
params={resolvedParams}
|
||||
user={session.user}
|
||||
user={user}
|
||||
widgetData={widgetData}
|
||||
appConfig={appConfig}
|
||||
devkitEnabled={devkitEnabled}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useToast } from '@zen/core/toast';
|
||||
import AdminHeader from '../components/AdminHeader.js';
|
||||
import RoleEditModal from '../components/RoleEditModal.client.js';
|
||||
|
||||
const RolesPageClient = () => {
|
||||
const RolesPageClient = ({ canManage }) => {
|
||||
const toast = useToast();
|
||||
const [roles, setRoles] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -73,7 +73,7 @@ const RolesPageClient = () => {
|
||||
render: (role) => role.is_system ? <Badge variant="default" size="sm">système</Badge> : null,
|
||||
skeleton: { height: 'h-4', width: '60px' },
|
||||
},
|
||||
{
|
||||
...(canManage ? [{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
sortable: false,
|
||||
@@ -99,7 +99,7 @@ const RolesPageClient = () => {
|
||||
</div>
|
||||
),
|
||||
skeleton: { height: 'h-8', width: '80px', className: 'rounded-lg' },
|
||||
},
|
||||
}] : []),
|
||||
];
|
||||
|
||||
const fetchRoles = async () => {
|
||||
@@ -161,14 +161,17 @@ const RolesPageClient = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const RolesPage = () => (
|
||||
<div className="flex flex-col gap-4 sm:gap-6 lg:gap-8">
|
||||
<RolesPageHeader />
|
||||
<RolesPageClient />
|
||||
</div>
|
||||
);
|
||||
const RolesPage = ({ user }) => {
|
||||
const canManage = user?.permissions?.includes('roles.manage');
|
||||
return (
|
||||
<div className="flex flex-col gap-4 sm:gap-6 lg:gap-8">
|
||||
<RolesPageHeader canManage={canManage} />
|
||||
<RolesPageClient canManage={canManage} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RolesPageHeader = () => {
|
||||
const RolesPageHeader = ({ canManage }) => {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -176,17 +179,19 @@ const RolesPageHeader = () => {
|
||||
<AdminHeader
|
||||
title="Rôles"
|
||||
description="Gérez les rôles et leurs permissions"
|
||||
action={
|
||||
action={canManage && (
|
||||
<Button variant="primary" onClick={() => setModalOpen(true)}>
|
||||
Nouveau rôle
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<RoleEditModal
|
||||
roleId="new"
|
||||
isOpen={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
)}
|
||||
/>
|
||||
{canManage && (
|
||||
<RoleEditModal
|
||||
roleId="new"
|
||||
isOpen={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import AdminHeader from '../components/AdminHeader.js';
|
||||
import UserEditModal from '../components/UserEditModal.client.js';
|
||||
import UserCreateModal from '../components/UserCreateModal.client.js';
|
||||
|
||||
const UsersPageClient = ({ currentUserId, refreshKey }) => {
|
||||
const UsersPageClient = ({ currentUserId, refreshKey, canEdit }) => {
|
||||
const toast = useToast();
|
||||
const [users, setUsers] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -78,7 +78,7 @@ const UsersPageClient = ({ currentUserId, refreshKey }) => {
|
||||
render: (user) => <RelativeDate date={user.created_at} />,
|
||||
skeleton: { height: 'h-4', width: '70%' },
|
||||
},
|
||||
{
|
||||
...(canEdit ? [{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
sortable: false,
|
||||
@@ -94,7 +94,7 @@ const UsersPageClient = ({ currentUserId, refreshKey }) => {
|
||||
</Button>
|
||||
),
|
||||
skeleton: { height: 'h-8', width: '80px', className: 'rounded-lg' },
|
||||
},
|
||||
}] : []),
|
||||
];
|
||||
|
||||
const fetchUsers = async () => {
|
||||
@@ -158,13 +158,15 @@ const UsersPageClient = ({ currentUserId, refreshKey }) => {
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<UserEditModal
|
||||
userId={editingUserId}
|
||||
currentUserId={currentUserId}
|
||||
isOpen={!!editingUserId}
|
||||
onClose={() => setEditingUserId(null)}
|
||||
onSaved={fetchUsers}
|
||||
/>
|
||||
{canEdit && (
|
||||
<UserEditModal
|
||||
userId={editingUserId}
|
||||
currentUserId={currentUserId}
|
||||
isOpen={!!editingUserId}
|
||||
onClose={() => setEditingUserId(null)}
|
||||
onSaved={fetchUsers}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -172,24 +174,27 @@ const UsersPageClient = ({ currentUserId, refreshKey }) => {
|
||||
const UsersPage = ({ user }) => {
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const canEdit = user?.permissions?.includes('users.edit');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 sm:gap-6 lg:gap-8">
|
||||
<AdminHeader
|
||||
title="Utilisateurs"
|
||||
description="Gérez les comptes utilisateurs"
|
||||
action={
|
||||
action={canEdit && (
|
||||
<Button variant="primary" onClick={() => setCreateModalOpen(true)}>
|
||||
Nouvel utilisateur
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<UsersPageClient currentUserId={user?.id} refreshKey={refreshKey} />
|
||||
<UserCreateModal
|
||||
isOpen={createModalOpen}
|
||||
onClose={() => setCreateModalOpen(false)}
|
||||
onSaved={() => setRefreshKey(k => k + 1)}
|
||||
)}
|
||||
/>
|
||||
<UsersPageClient currentUserId={user?.id} refreshKey={refreshKey} canEdit={canEdit} />
|
||||
{canEdit && (
|
||||
<UserCreateModal
|
||||
isOpen={createModalOpen}
|
||||
onClose={() => setCreateModalOpen(false)}
|
||||
onSaved={() => setRefreshKey(k => k + 1)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user