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

This commit is contained in:
mrleo1nid committed 2026-10-21 17:33:19 +03:00
1 parent b0cbd0f149
commit c34272d508
16 files changed
+431 -266

No files matched your search

+49
View File
@@ -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);