feat(password): add reset and change password pages

/reset-password page for the email request and the new password,
ChangePasswordCard in settings, password reset button on the admin
users page, 'Forgot password?' link on the login page, de/en texts.
This commit is contained in:
lobo committed 2026-10-02 10:35:08 +02:00
1 parent f71e8f7aff
commit 6627a8db66
19 files changed
+565 -25

No files matched your search

+15 -3
View File
@@ -1,6 +1,6 @@
import {useCallback, useEffect, useState} from 'react';
import {useTranslation} from 'react-i18next';
import {Ban, Check, ChevronLeft, ChevronRight, Loader2, Search, ShieldCheck, Trash2, X} from 'lucide-react';
import {Ban, Check, ChevronLeft, ChevronRight, KeyRound, Loader2, Search, ShieldCheck, Trash2, X} from 'lucide-react';
import {toast} from 'sonner';
import {Badge} from '@/components/ui/badge';
@@ -18,7 +18,7 @@ import {
import {Input} from '@/components/ui/input';
import {Label} from '@/components/ui/label';
import PageContainer from '../components/PageContainer.jsx';
import {decideUser, deleteUnconfirmedUser, deleteUser, fetchUsers, setUserBlocked} from '../utils/admin.js';
import {decideUser, deleteUnconfirmedUser, deleteUser, fetchUsers, sendPasswordReset, setUserBlocked} from '../utils/admin.js';
import {formatDate} from '../utils/format.js';
import useAbortPrevious from '../hooks/useAbortPrevious.js';
@@ -31,6 +31,7 @@ const REFUSAL_KEY = {
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);
@@ -47,7 +48,7 @@ export default function AdminUsersPage() {
const [pendingOnly, setPendingOnly] = useState(false);
const [result, setResult] = useState(null);
const [status, setStatus] = useState('loading');
// {kind: 'block' | 'delete' | 'deleteUnconfirmed' | 'accept' | 'deny', user}
// {kind: 'block' | 'delete' | 'deleteUnconfirmed' | 'accept' | 'deny' | 'passwordReset', user}
const [pending, setPending] = useState(null);
const [typedEmail, setTypedEmail] = useState('');
const [busy, setBusy] = useState(false);
@@ -105,6 +106,11 @@ export default function AdminUsersPage() {
: {title: t('ADMIN.BLOCK_TITLE', {email}), text: t('ADMIN.BLOCK_TEXT'), action: 'ADMIN.BLOCK', done: 'ADMIN.DONE_BLOCK'}),
run: () => setUserBlocked(pending.user.id, !pending.user.blocked),
},
passwordReset: {
title: t('ADMIN.PASSWORD_RESET_TITLE', {email}), text: t('ADMIN.PASSWORD_RESET_TEXT'),
action: 'ADMIN.SEND_PASSWORD_RESET', done: 'ADMIN.DONE_PASSWORD_RESET',
run: () => sendPasswordReset(pending.user.id),
},
accept: {
title: t('ADMIN.ACCEPT_TITLE', {email}), text: t('ADMIN.ACCEPT_TEXT'), action: 'ADMIN.ACCEPT',
done: 'ADMIN.DONE_ACCEPT', run: () => decideUser(pending.user.id, true),
@@ -217,6 +223,12 @@ export default function AdminUsersPage() {
)}
{user.role !== 'PLATFORM_ADMIN' && !user.deletedAt && user.status !== 'PENDING' && user.status !== 'UNCONFIRMED' && (
<div className="flex gap-2">
{user.status === 'ACTIVE' && !user.blocked && (
<Button type="button" variant="outline" size="sm"
onClick={() => setPending({kind: 'passwordReset', user})}>
<KeyRound/>{t('ADMIN.SEND_PASSWORD_RESET')}
</Button>
)}
<Button type="button" variant="outline" size="sm"
onClick={() => setPending({kind: 'block', user})}>
{user.blocked ? <ShieldCheck/> : <Ban/>}