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
+7
-1
@@ -7,7 +7,7 @@ export const ROLES = ['USER', 'AUDITOR', 'PLATFORM_ADMIN'];
|
||||
/**
|
||||
* GET /api/admin/users: {content, page, size, totalElements, totalPages}. PLATFORM_ADMIN only (403 otherwise).
|
||||
*
|
||||
* @param status PENDING, ACTIVE or DENIED; empty for all
|
||||
* @param status UNCONFIRMED, PENDING, ACTIVE or DENIED; empty for all
|
||||
*/
|
||||
export const fetchUsers = (query, status, page, size, init) =>
|
||||
apiFetch(`${AUTH()}/api/admin/users?${new URLSearchParams({query, ...(status && {status}), page, size})}`, init);
|
||||
@@ -34,6 +34,12 @@ export const setUserBlocked = (userId, blocked) =>
|
||||
body: JSON.stringify({blocked}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Hard delete of a request whose address is not confirmed: 204, or 409 NOT_UNCONFIRMED for any other status.
|
||||
*/
|
||||
export const deleteUnconfirmedUser = (userId) =>
|
||||
apiFetch(`${AUTH()}/api/admin/users/${userId}/unconfirmed`, {method: 'DELETE'});
|
||||
|
||||
export const deleteUser = (userId) => apiFetch(`${AUTH()}/api/admin/users/${userId}`, {method: 'DELETE'});
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,12 +24,16 @@ const displayNameField = z
|
||||
message: 'VALIDATION.DISPLAY_NAME_INVALID',
|
||||
});
|
||||
|
||||
export const registrationSchema = z
|
||||
// No password: the address owner chooses it on the confirm page.
|
||||
export const registrationSchema = z.object({
|
||||
email: emailField,
|
||||
displayName: displayNameField,
|
||||
});
|
||||
|
||||
export const confirmEmailSchema = z
|
||||
.object({
|
||||
email: emailField,
|
||||
password: passwordField,
|
||||
passwordConfirmation: z.string().min(1, 'VALIDATION.PASSWORD_CONFIRMATION_REQUIRED'),
|
||||
displayName: displayNameField,
|
||||
})
|
||||
.refine((d) => d.password === d.passwordConfirmation, {
|
||||
path: ['passwordConfirmation'],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {registrationSchema, settingsSchema} from './validation.js';
|
||||
import {confirmEmailSchema, registrationSchema, settingsSchema} from './validation.js';
|
||||
|
||||
const settings = () => ({useDarkTheme: true, language: 'de'});
|
||||
|
||||
@@ -7,7 +7,7 @@ describe('Steam ID', () => {
|
||||
it('is no form field any more: it comes only from "Sign in through Steam"', () => {
|
||||
expect(settingsSchema.parse({...settings(), steamId: '76561198005679788'})).not.toHaveProperty('steamId');
|
||||
expect(registrationSchema.parse({
|
||||
email: 'a@b.de', password: 'password1', passwordConfirmation: 'password1',
|
||||
email: 'a@b.de',
|
||||
steamId: '76561198005679788',
|
||||
})).not.toHaveProperty('steamId');
|
||||
});
|
||||
@@ -30,11 +30,24 @@ describe('displayName validation', () => {
|
||||
});
|
||||
|
||||
|
||||
describe('registration password', () => {
|
||||
describe('confirm page password', () => {
|
||||
it('rejects a password over 72 UTF-8 bytes, as Auth does', () => {
|
||||
const password = 'é'.repeat(37);
|
||||
expect(registrationSchema.safeParse({
|
||||
email: 'a@b.de', password, passwordConfirmation: password,
|
||||
}).success).toBe(false);
|
||||
expect(confirmEmailSchema.safeParse({password, passwordConfirmation: password}).success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a short password and one that differs from its repeat', () => {
|
||||
expect(confirmEmailSchema.safeParse({password: 'short', passwordConfirmation: 'short'}).success).toBe(false);
|
||||
expect(confirmEmailSchema.safeParse({password: 'password1', passwordConfirmation: 'password2'}).success).toBe(false);
|
||||
});
|
||||
|
||||
it('takes a good password', () => {
|
||||
expect(confirmEmailSchema.safeParse({password: 'password1', passwordConfirmation: 'password1'}).success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registration', () => {
|
||||
it('has no password field', () => {
|
||||
expect(registrationSchema.parse({email: 'a@b.de', password: 'password1'})).not.toHaveProperty('password');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user