[PUI] Registration (#6309)

* optimize login layout

* move auth/reg up

* [PUI] Registration
Fixes #6282

* [PUI] Registration
Fixes #6282

* fix type

* style: cleaned imports
This commit is contained in:
Matthias Mair 2024-01-22 10:39:06 +00:00 committed by GitHub
parent 2fbb8c757f
commit d64fbfc254
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 207 additions and 70 deletions

View File

@ -4,7 +4,6 @@ import {
Button, Button,
Group, Group,
Loader, Loader,
Paper,
PasswordInput, PasswordInput,
Stack, Stack,
Text, Text,
@ -17,7 +16,10 @@ import { IconCheck } from '@tabler/icons-react';
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { api } from '../../App';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { doClassicLogin, doSimpleLogin } from '../../functions/auth'; import { doClassicLogin, doSimpleLogin } from '../../functions/auth';
import { apiUrl } from '../../states/ApiState';
export function AuthenticationForm() { export function AuthenticationForm() {
const classicForm = useForm({ const classicForm = useForm({
@ -79,50 +81,178 @@ export function AuthenticationForm() {
} }
return ( return (
<Paper radius="md" p="xl" withBorder> <form onSubmit={classicForm.onSubmit(() => {})}>
<Text size="lg" weight={500}> {classicLoginMode ? (
<Trans>Welcome, log in below</Trans> <Stack spacing={0}>
</Text> <TextInput
<form onSubmit={classicForm.onSubmit(() => {})}> required
{classicLoginMode ? ( label={t`Username`}
<Stack> placeholder={t`Your username`}
<TextInput {...classicForm.getInputProps('username')}
required />
label={t`Username`} <PasswordInput
placeholder={t`Your username`} required
{...classicForm.getInputProps('username')} label={t`Password`}
/> placeholder={t`Your password`}
<PasswordInput {...classicForm.getInputProps('password')}
required />
label={t`Password`} <Group position="apart" mt="0">
placeholder={t`Your password`} <Anchor
{...classicForm.getInputProps('password')} component="button"
/> type="button"
<Group position="apart" mt="0"> color="dimmed"
<Anchor size="xs"
component="button" onClick={() => navigate('/reset-password')}
type="button" >
color="dimmed" <Trans>Reset password</Trans>
size="xs" </Anchor>
onClick={() => navigate('/reset-password')} </Group>
> </Stack>
<Trans>Reset password</Trans> ) : (
</Anchor> <Stack>
</Group> <TextInput
</Stack> required
) : ( label={t`Email`}
<Stack> description={t`We will send you a link to login - if you are registered`}
<TextInput placeholder="email@example.org"
required {...simpleForm.getInputProps('email')}
label={t`Email`} />
description={t`We will send you a link to login - if you are registered`} </Stack>
placeholder="email@example.org" )}
{...simpleForm.getInputProps('email')}
/>
</Stack>
)}
<Group position="apart" mt="xl"> <Group position="apart" mt="xl">
<Anchor
component="button"
type="button"
color="dimmed"
size="xs"
onClick={() => setMode.toggle()}
>
{classicLoginMode ? (
<Trans>Send me an email</Trans>
) : (
<Trans>Use username and password</Trans>
)}
</Anchor>
<Button type="submit" disabled={isLoggingIn} onClick={handleLogin}>
{isLoggingIn ? (
<Loader size="sm" />
) : (
<>
{classicLoginMode ? (
<Trans>Log In</Trans>
) : (
<Trans>Send Email</Trans>
)}
</>
)}
</Button>
</Group>
</form>
);
}
export function RegistrationForm() {
const registrationForm = useForm({
initialValues: { username: '', email: '', password1: '', password2: '' }
});
const navigate = useNavigate();
const [isRegistering, setIsRegistering] = useState<boolean>(false);
function handleRegistration() {
setIsRegistering(true);
api
.post(apiUrl(ApiPaths.user_register), registrationForm.values, {
headers: { Authorization: '' }
})
.then((ret) => {
if (ret?.status === 204) {
setIsRegistering(false);
notifications.show({
title: t`Registration successful`,
message: t`Please confirm your email address to complete the registration`,
color: 'green',
icon: <IconCheck size="1rem" />
});
navigate('/home');
}
})
.catch((err) => {
if (err.response.status === 400) {
setIsRegistering(false);
for (const [key, value] of Object.entries(err.response.data)) {
registrationForm.setFieldError(key, value as string);
}
let err_msg = '';
if (err.response?.data?.non_field_errors) {
err_msg = err.response.data.non_field_errors;
}
notifications.show({
title: t`Input error`,
message: t`Check your input and try again. ` + err_msg,
color: 'red',
autoClose: 30000
});
}
});
}
return (
<form onSubmit={registrationForm.onSubmit(() => {})}>
<Stack spacing={0}>
<TextInput
required
label={t`Username`}
placeholder={t`Your username`}
{...registrationForm.getInputProps('username')}
/>
<TextInput
required
label={t`Email`}
description={t`This will be used for a confirmation`}
placeholder="email@example.org"
{...registrationForm.getInputProps('email')}
/>
<PasswordInput
required
label={t`Password`}
placeholder={t`Your password`}
{...registrationForm.getInputProps('password1')}
/>
<PasswordInput
required
label={t`Password repeat`}
placeholder={t`Repeat password`}
{...registrationForm.getInputProps('password2')}
/>
</Stack>
<Group position="apart" mt="xl">
<Button
type="submit"
disabled={isRegistering}
onClick={handleRegistration}
fullWidth
>
<Trans>Register</Trans>
</Button>
</Group>
</form>
);
}
export function ModeSelector({
loginMode,
setMode
}: {
loginMode: boolean;
setMode: any;
}) {
return (
<Text ta="center" size={'xs'} mt={'md'}>
{loginMode ? (
<>
<Trans>Don&apos;t have an account?</Trans>{' '}
<Anchor <Anchor
component="button" component="button"
type="button" type="button"
@ -130,27 +260,20 @@ export function AuthenticationForm() {
size="xs" size="xs"
onClick={() => setMode.toggle()} onClick={() => setMode.toggle()}
> >
{classicLoginMode ? ( <Trans>Register</Trans>
<Trans>Send me an email</Trans>
) : (
<Trans>I will use username and password</Trans>
)}
</Anchor> </Anchor>
<Button type="submit" disabled={isLoggingIn} onClick={handleLogin}> </>
{isLoggingIn ? ( ) : (
<Loader size="sm" /> <Anchor
) : ( component="button"
<> type="button"
{classicLoginMode ? ( color="dimmed"
<Trans>Log In</Trans> size="xs"
) : ( onClick={() => setMode.toggle()}
<Trans>Send Email</Trans> >
)} <Trans>Go back to login</Trans>
</> </Anchor>
)} )}
</Button> </Text>
</Group>
</form>
</Paper>
); );
} }

View File

@ -20,6 +20,7 @@ export enum ApiPaths {
user_email_primary = 'api-user-email-primary', user_email_primary = 'api-user-email-primary',
user_email_remove = 'api-user-email-remove', user_email_remove = 'api-user-email-remove',
user_logout = 'api-user-logout', user_logout = 'api-user-logout',
user_register = 'api-user-register',
user_list = 'api-user-list', user_list = 'api-user-list',
group_list = 'api-group-list', group_list = 'api-group-list',

View File

@ -1,12 +1,16 @@
import { t } from '@lingui/macro'; import { Trans, t } from '@lingui/macro';
import { Center, Container } from '@mantine/core'; import { Center, Container, Paper, Text } from '@mantine/core';
import { useToggle } from '@mantine/hooks'; import { useDisclosure, useToggle } from '@mantine/hooks';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { setApiDefaults } from '../../App'; import { setApiDefaults } from '../../App';
import { AuthFormOptions } from '../../components/forms/AuthFormOptions'; import { AuthFormOptions } from '../../components/forms/AuthFormOptions';
import { AuthenticationForm } from '../../components/forms/AuthenticationForm'; import {
AuthenticationForm,
ModeSelector,
RegistrationForm
} from '../../components/forms/AuthenticationForm';
import { InstanceOptions } from '../../components/forms/InstanceOptions'; import { InstanceOptions } from '../../components/forms/InstanceOptions';
import { defaultHostKey } from '../../defaults/defaultHostList'; import { defaultHostKey } from '../../defaults/defaultHostList';
import { checkLoginState } from '../../functions/auth'; import { checkLoginState } from '../../functions/auth';
@ -26,6 +30,7 @@ export default function Login() {
const hostname = const hostname =
hostList[hostKey] === undefined ? t`No selection` : hostList[hostKey]?.name; hostList[hostKey] === undefined ? t`No selection` : hostList[hostKey]?.name;
const [hostEdit, setHostEdit] = useToggle([false, true] as const); const [hostEdit, setHostEdit] = useToggle([false, true] as const);
const [loginMode, setMode] = useDisclosure(true);
const navigate = useNavigate(); const navigate = useNavigate();
// Data manipulation functions // Data manipulation functions
@ -63,7 +68,13 @@ export default function Login() {
/> />
) : ( ) : (
<> <>
<AuthenticationForm /> <Paper radius="md" p="xl" withBorder>
<Text size="lg" weight={500}>
<Trans>Welcome, log in below</Trans>
</Text>
{loginMode ? <AuthenticationForm /> : <RegistrationForm />}
<ModeSelector loginMode={loginMode} setMode={setMode} />
</Paper>
<AuthFormOptions hostname={hostname} toggleHostEdit={setHostEdit} /> <AuthFormOptions hostname={hostname} toggleHostEdit={setHostEdit} />
</> </>
)} )}

View File

@ -97,6 +97,8 @@ export function apiEndpoint(path: ApiPaths): string {
return 'auth/emails/:id/primary/'; return 'auth/emails/:id/primary/';
case ApiPaths.user_logout: case ApiPaths.user_logout:
return 'auth/logout/'; return 'auth/logout/';
case ApiPaths.user_register:
return 'auth/registration/';
case ApiPaths.currency_list: case ApiPaths.currency_list:
return 'currency/exchange/'; return 'currency/exchange/';
case ApiPaths.currency_refresh: case ApiPaths.currency_refresh: