Public Access
feat(registration): add email confirmation UI
New ConfirmEmailPage for users to verify their email and set password. Resend form on registration and error pages. Registration form no longer asks for a password. Confirmation requires a button click. The admin list marks unconfirmed requests and offers only delete for them.
This commit is contained in:
1 parent
249973dd69
commit
6dda186bdf
15 files changed
+416
-50
No files matched your search
@@ -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, deleteUser, fetchUsers, setUserBlocked} from '../utils/admin.js';
|
||||
import {decideUser, deleteUnconfirmedUser, deleteUser, fetchUsers, setUserBlocked} from '../utils/admin.js';
|
||||
import {formatDate} from '../utils/format.js';
|
||||
import useAbortPrevious from '../hooks/useAbortPrevious.js';
|
||||
|
||||
@@ -29,12 +29,14 @@ const REFUSAL_KEY = {
|
||||
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',
|
||||
};
|
||||
|
||||
const formatDateOrNull = (value, locale) => (value ? formatDate(value, locale) : null);
|
||||
|
||||
/**
|
||||
* PLATFORM_ADMIN only: list, block and delete users, accept or deny registrations that wait. Auth checks the role; a
|
||||
* PLATFORM_ADMIN only: list, block and delete users, accept or deny registrations that wait, delete the ones whose address is not confirmed. Auth checks the role; a
|
||||
* 403 shows "no access".
|
||||
*/
|
||||
export default function AdminUsersPage() {
|
||||
@@ -45,7 +47,7 @@ export default function AdminUsersPage() {
|
||||
const [pendingOnly, setPendingOnly] = useState(false);
|
||||
const [result, setResult] = useState(null);
|
||||
const [status, setStatus] = useState('loading');
|
||||
// {kind: 'block' | 'delete' | 'accept' | 'deny', user}
|
||||
// {kind: 'block' | 'delete' | 'deleteUnconfirmed' | 'accept' | 'deny', user}
|
||||
const [pending, setPending] = useState(null);
|
||||
const [typedEmail, setTypedEmail] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -92,6 +94,11 @@ export default function AdminUsersPage() {
|
||||
title: t('ADMIN.DELETE_TITLE', {email}), text: t('ADMIN.DELETE_TEXT'), action: 'ADMIN.DELETE',
|
||||
done: 'ADMIN.DONE_DELETE', destructive: true, run: () => deleteUser(pending.user.id),
|
||||
},
|
||||
deleteUnconfirmed: {
|
||||
title: t('ADMIN.DELETE_UNCONFIRMED_TITLE', {email}), text: t('ADMIN.DELETE_UNCONFIRMED_TEXT'),
|
||||
action: 'ADMIN.DELETE', done: 'ADMIN.DONE_DELETE', destructive: true,
|
||||
run: () => deleteUnconfirmedUser(pending.user.id),
|
||||
},
|
||||
block: {
|
||||
...(pending.user.blocked
|
||||
? {title: t('ADMIN.UNBLOCK'), text: email, action: 'ADMIN.UNBLOCK', done: 'ADMIN.DONE_UNBLOCK'}
|
||||
@@ -181,6 +188,7 @@ export default function AdminUsersPage() {
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{user.role !== 'USER' && <Badge variant="outline">{t(`ROLES.${user.role}`)}</Badge>}
|
||||
{user.steamLinked && <Badge variant="secondary">{t('ADMIN.STEAM')}</Badge>}
|
||||
{user.status === 'UNCONFIRMED' && <Badge variant="outline">{t('ADMIN.UNCONFIRMED')}</Badge>}
|
||||
{user.status === 'PENDING' && <Badge>{t('ADMIN.PENDING')}</Badge>}
|
||||
{user.blocked && <Badge variant="destructive">{t('ADMIN.BLOCKED')}</Badge>}
|
||||
{user.deletedAt && (
|
||||
@@ -189,6 +197,12 @@ export default function AdminUsersPage() {
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{user.status === 'UNCONFIRMED' && (
|
||||
<Button type="button" variant="outline" size="sm" className="border-destructive text-destructive"
|
||||
onClick={() => setPending({kind: 'deleteUnconfirmed', user})}>
|
||||
<Trash2/>{t('ADMIN.DELETE')}
|
||||
</Button>
|
||||
)}
|
||||
{user.status === 'PENDING' && (
|
||||
<div className="flex gap-2">
|
||||
<Button type="button" variant="outline" size="sm"
|
||||
@@ -201,7 +215,7 @@ export default function AdminUsersPage() {
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{user.role !== 'PLATFORM_ADMIN' && !user.deletedAt && user.status !== 'PENDING' && (
|
||||
{user.role !== 'PLATFORM_ADMIN' && !user.deletedAt && user.status !== 'PENDING' && user.status !== 'UNCONFIRMED' && (
|
||||
<div className="flex gap-2">
|
||||
<Button type="button" variant="outline" size="sm"
|
||||
onClick={() => setPending({kind: 'block', user})}>
|
||||
|
||||
Reference in new issue
Block a user