feat(auth): make upload review a separate grant instead of an admin privilege
Reviewing the upload queue is now a per-user flag (reviews_uploads), not part of the admin role: several people can hold it, a moderator can have it, and an admin may not. Only admins grant or revoke it; existing admins keep it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
066ba24834
commit
74da457362
27 files changed
+503
-319
No files matched your search
@@ -132,6 +132,11 @@ export function UsersPage() {
|
||||
label: t('users.make_role', { role: t(`common:roles.${r}`) }),
|
||||
onSelect: () => patch.mutate({ id: u.id, body: { role: r } }),
|
||||
});
|
||||
if (me.role === 'admin')
|
||||
items.push({
|
||||
label: t(u.reviews_uploads ? 'users.revoke_review' : 'users.grant_review'),
|
||||
onSelect: () => patch.mutate({ id: u.id, body: { reviews_uploads: !u.reviews_uploads } }),
|
||||
});
|
||||
if (can('delete'))
|
||||
items.push({ label: t('users.delete'), danger: true, onSelect: () => setConfirm({ kind: 'delete', user: u }) });
|
||||
return items;
|
||||
@@ -206,6 +211,7 @@ export function UsersPage() {
|
||||
</td>
|
||||
<td>
|
||||
<span className={cx(s.badge, s[`role_${u.role}`])}>{t(`common:roles.${u.role}`)}</span>
|
||||
{u.reviews_uploads && <span className={cx(s.badge, s.role_moderator)}>{t('users.review_uploads')}</span>}
|
||||
</td>
|
||||
<td>
|
||||
<span className={cx(s.status, s[`status_${st}`])}>{t(`users.status_${st}`)}</span>
|
||||
@@ -285,12 +291,18 @@ function CreateUserDialog({
|
||||
const [username, setUsername] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [role, setRole] = useState<Role>('user');
|
||||
const [reviews, setReviews] = useState(false);
|
||||
|
||||
const create = useMutation({
|
||||
mutationFn: async () =>
|
||||
unwrap(
|
||||
await session.client.POST('/api/v1/admin/users', {
|
||||
body: { username: username.trim(), display_name: name.trim() || undefined, role },
|
||||
body: {
|
||||
username: username.trim(),
|
||||
display_name: name.trim() || undefined,
|
||||
role,
|
||||
reviews_uploads: actorRole === 'admin' && reviews ? true : undefined,
|
||||
},
|
||||
}),
|
||||
),
|
||||
onSuccess: (r) => onCreated(r.user.username, r.temporary_password),
|
||||
@@ -342,6 +354,13 @@ function CreateUserDialog({
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
{actorRole === 'admin' && (
|
||||
<label className={s.sessions} style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||
<input type="checkbox" checked={reviews} onChange={(e) => setReviews(e.target.checked)} />
|
||||
<span>{t('create.review_uploads')}</span>
|
||||
</label>
|
||||
)}
|
||||
{actorRole === 'admin' && <p className={s.note}>{t('create.review_uploads_hint')}</p>}
|
||||
<p className={s.note}>{t('create.password_note')}</p>
|
||||
<FormError>{create.error && !usernameError && !nameError ? errorText(create.error) : null}</FormError>
|
||||
</form>
|
||||
|
||||
Reference in new issue
Block a user