feat(admin): add user role dialog

Admins change a user's role and see the change log. The last admin
gets a clear error when deleting their own account.
This commit is contained in:
lobo committed 2026-10-04 20:09:22 +02:00
1 parent e5091de50e
commit 13b0d5e7e9
11 files changed
+391 -18

No files matched your search

+12 -12
View File
@@ -1,6 +1,6 @@
import {useCallback, useEffect, useState} from 'react';
import {useTranslation} from 'react-i18next';
import {Ban, Check, ChevronLeft, ChevronRight, KeyRound, Loader2, Search, ShieldCheck, Trash2, X} from 'lucide-react';
import {Ban, Check, ChevronLeft, ChevronRight, KeyRound, Loader2, Search, ShieldCheck, Trash2, UserCog, X} from 'lucide-react';
import {toast} from 'sonner';
import {Badge} from '@/components/ui/badge';
@@ -18,21 +18,13 @@ import {
import {Input} from '@/components/ui/input';
import {Label} from '@/components/ui/label';
import PageContainer from '../components/PageContainer.jsx';
import {decideUser, deleteUnconfirmedUser, deleteUser, fetchUsers, sendPasswordReset, setUserBlocked} from '../utils/admin.js';
import UserRoleDialog from '../components/UserRoleDialog.jsx';
import {useCurrentUserContext} from '../context/CurrentUserContext.jsx';
import {decideUser, deleteUnconfirmedUser, deleteUser, fetchUsers, REFUSAL_KEY, sendPasswordReset, setUserBlocked} from '../utils/admin.js';
import {formatDate} from '../utils/format.js';
import useAbortPrevious from '../hooks/useAbortPrevious.js';
const PAGE_SIZE = 20;
const REFUSAL_KEY = {
SELF_ACTION: 'ADMIN.ERROR_SELF_ACTION',
TARGET_IS_ADMIN: 'ADMIN.ERROR_TARGET_IS_ADMIN',
ALREADY_DELETED: 'ADMIN.ERROR_ALREADY_DELETED',
NOT_PENDING: 'ADMIN.ERROR_NOT_PENDING',
TARGET_IS_PENDING: 'ADMIN.ERROR_TARGET_IS_PENDING',
TARGET_IS_UNCONFIRMED: 'ADMIN.ERROR_TARGET_IS_UNCONFIRMED',
NOT_UNCONFIRMED: 'ADMIN.ERROR_NOT_UNCONFIRMED',
NOT_ACTIVE: 'ADMIN.ERROR_NOT_ACTIVE',
};
const formatDateOrNull = (value, locale) => (value ? formatDate(value, locale) : null);
@@ -54,6 +46,8 @@ export default function AdminUsersPage() {
const [busy, setBusy] = useState(false);
// Only the newest load may show its answer: a slow answer to an older search must not replace the list.
const nextSignal = useAbortPrevious();
const ownId = useCurrentUserContext().user?.id;
const [roleUser, setRoleUser] = useState(null);
const load = useCallback(async () => {
const signal = nextSignal();
@@ -223,6 +217,11 @@ export default function AdminUsersPage() {
</Button>
</div>
)}
{user.status === 'ACTIVE' && !user.blocked && !user.deletedAt && user.id !== ownId && (
<Button type="button" variant="outline" size="sm" onClick={() => setRoleUser(user)}>
<UserCog/>{t('ADMIN.ROLES')}
</Button>
)}
{user.role !== 'PLATFORM_ADMIN' && !user.deletedAt && user.status !== 'PENDING' && user.status !== 'UNCONFIRMED' && (
<div className="flex gap-2">
{user.status === 'ACTIVE' && !user.blocked && (
@@ -289,6 +288,7 @@ export default function AdminUsersPage() {
</DialogContent>
)}
</Dialog>
<UserRoleDialog user={roleUser} onClose={() => setRoleUser(null)} onChanged={load}/>
</PageContainer>
);
}