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:
2026-04-25 09:21:07 -04:00
parent 188e1d82f8
commit c959b16db5
7 changed files with 91 additions and 53 deletions
+23 -18
View File
@@ -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>
);
};