fix(auth): allow admins to change any username including their own
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m6s
ci / ts / ts (packages + web) (push) Successful in 2m59s
ci / api / api (push) Successful in 49s
ci / db / db (push) Successful in 1m17s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m25s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m6s
ci / ts / ts (packages + web) (push) Successful in 2m59s
ci / api / api (push) Successful in 49s
ci / db / db (push) Successful in 1m17s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m25s
This commit is contained in:
1 parent
b0cbd0f149
commit
c34272d508
16 files changed
+431
-266
No files matched your search
@@ -46,6 +46,7 @@ export function UsersPage() {
|
||||
const [role, setRole] = useState<Role | null>(null);
|
||||
const query = useDebounced(q.trim(), 200);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [renaming, setRenaming] = useState<AdminUser | null>(null);
|
||||
const [confirm, setConfirm] = useState<Confirm | null>(null);
|
||||
const [secret, setSecret] = useState<{ username: string; password: string } | null>(null);
|
||||
const [actionError, setActionError] = useState<unknown>(null);
|
||||
@@ -112,6 +113,8 @@ export function UsersPage() {
|
||||
const actionsFor = (u: AdminUser): MenuItem[] => {
|
||||
const items: MenuItem[] = [];
|
||||
const can = (a: Parameters<typeof canManageUser>[2]) => canManageUser(me, u, a);
|
||||
if (me.role === 'admin')
|
||||
items.push({ label: t('users.change_username'), onSelect: () => setRenaming(u) });
|
||||
if (can('reset_password'))
|
||||
items.push({ label: t('users.reset_password'), icon: lockResetIcon, onSelect: () => setConfirm({ kind: 'reset', user: u }) });
|
||||
if (can('reset_recommendations'))
|
||||
@@ -250,6 +253,18 @@ export function UsersPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{renaming && (
|
||||
<ChangeUsernameDialog
|
||||
user={renaming}
|
||||
onClose={() => setRenaming(null)}
|
||||
onSaved={async (updated) => {
|
||||
if (updated.id === me.id) session.setUser({ ...me, username: updated.username });
|
||||
setRenaming(null);
|
||||
await refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{confirm && (
|
||||
<Dialog
|
||||
title={t(`users.${TITLES[confirm.kind]}`)}
|
||||
@@ -368,6 +383,40 @@ function CreateUserDialog({
|
||||
);
|
||||
}
|
||||
|
||||
function ChangeUsernameDialog({ user, onClose, onSaved }: {
|
||||
user: AdminUser;
|
||||
onClose: () => void;
|
||||
onSaved: (user: AdminUser) => Promise<void>;
|
||||
}) {
|
||||
const { t } = useTranslation(['admin', 'common']);
|
||||
const session = useSession();
|
||||
const errorText = useErrorText();
|
||||
const fieldError = useFieldError();
|
||||
const [username, setUsername] = useState(user.username);
|
||||
const save = useMutation({
|
||||
mutationFn: async () => unwrap(await session.client.PATCH('/api/v1/admin/users/{id}', {
|
||||
params: { path: { id: user.id } }, body: { username: username.trim() },
|
||||
})),
|
||||
onSuccess: onSaved,
|
||||
});
|
||||
const error = fieldError(save.error, 'username');
|
||||
return (
|
||||
<Dialog title={t('users.change_username')} onClose={onClose} footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={onClose}>{t('common:form.cancel')}</Button>
|
||||
<Button type="submit" form="change-username" variant="primary" busy={save.isPending}
|
||||
disabled={!username.trim() || username.trim() === user.username}>{t('common:settings.save')}</Button>
|
||||
</>
|
||||
}>
|
||||
<form id="change-username" className={s.sessions} noValidate onSubmit={(e) => { e.preventDefault(); save.mutate(); }}>
|
||||
<Field label={t('create.username')} value={username} onChange={setUsername} autoCapitalize="none"
|
||||
spellCheck={false} hint={t('create.username_hint')} error={error} mono />
|
||||
<FormError>{save.error && !error ? errorText(save.error) : null}</FormError>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function TemporaryPasswordDialog({ username, password, onClose }: { username: string; password: string; onClose: () => void }) {
|
||||
const { t } = useTranslation(['admin', 'common']);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
Reference in new issue
Block a user