blob: 71c38bd01b9fbebee50aeb5a7a8154810739eed2 [file] [log] [blame]
/*
* Copyright (C) 2022 Savoir-faire Linux Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
import { Box, Button, Stack, Typography, useMediaQuery } from '@mui/material';
import { Theme, useTheme } from '@mui/material/styles';
import { ChangeEvent, FormEvent, MouseEvent, ReactNode, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Form, useNavigate } from 'react-router-dom';
import { AlertSnackbar } from '../components/AlertSnackbar';
import { PasswordInput, UsernameInput } from '../components/Input';
import ProcessingRequest from '../components/ProcessingRequest';
import {
checkPasswordStrength,
isNameRegistered,
loginUser,
registerUser,
setAccessToken,
StrengthValueCode,
} from '../utils/auth';
import { inputWidth, jamiUsernamePattern } from '../utils/constants';
import { InvalidPassword, UsernameNotFound } from '../utils/errors';
type NameStatus = 'default' | 'success' | 'taken' | 'invalid' | 'registration_failed';
type PasswordStatus = StrengthValueCode | 'registration_failed';
type JamiRegistrationProps = {
login: () => void;
};
export default function JamiRegistration(props: JamiRegistrationProps) {
const theme: Theme = useTheme();
const navigate = useNavigate();
const { t } = useTranslation();
const [isCreatingUser, setIsCreatingUser] = useState(false);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [usernameStatus, setUsernameStatus] = useState<NameStatus>('default');
const [passwordStatus, setPasswordStatus] = useState<PasswordStatus>('default');
const [errorAlertContent, setErrorAlertContent] = useState<ReactNode>(undefined);
const [successAlertContent, setSuccessAlertContent] = useState<ReactNode>(undefined);
const usernameError = usernameStatus !== 'success' && usernameStatus !== 'default';
const usernameSuccess = usernameStatus === 'success';
const passwordError = passwordStatus !== 'strong' && passwordStatus !== 'default';
const passwordSuccess = passwordStatus === 'strong';
useEffect(() => {
// To prevent lookup if field is empty, in error state or lookup already done
if (username.length > 0 && usernameStatus === 'default') {
const validateUsername = async () => {
if (await isNameRegistered(username)) {
setUsernameStatus('taken');
} else {
setUsernameStatus('success');
}
};
const timeout = setTimeout(validateUsername, 1000);
return () => clearTimeout(timeout);
}
}, [username, usernameStatus]);
const firstUserLogin = async () => {
try {
const accessToken = await loginUser(username, password);
setAccessToken(accessToken);
navigate('/settings', { replace: true });
} catch (err) {
setIsCreatingUser(false);
if (err instanceof UsernameNotFound) {
setErrorAlertContent(t('login_username_not_found'));
} else if (err instanceof InvalidPassword) {
setErrorAlertContent(t('login_invalid_password'));
} else {
throw err;
}
}
};
const createAccount = async () => {
await registerUser(username, password);
setSuccessAlertContent(t('registration_success'));
await firstUserLogin();
};
const handleUsername = async (event: ChangeEvent<HTMLInputElement>) => {
const usernameValue: string = event.target.value;
setUsername(usernameValue);
if (usernameValue.length > 0 && !jamiUsernamePattern.test(usernameValue)) {
setUsernameStatus('invalid');
} else {
setUsernameStatus('default');
}
};
const handlePassword = (event: ChangeEvent<HTMLInputElement>) => {
const passwordValue: string = event.target.value;
setPassword(passwordValue);
if (passwordValue.length > 0) {
const checkResult = checkPasswordStrength(passwordValue);
setPasswordStatus(checkResult.valueCode);
} else {
setPasswordStatus('default');
}
};
const login = (event: MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
props.login();
};
const handleSubmit = async (event: FormEvent) => {
event.preventDefault();
const canCreate = usernameSuccess && passwordSuccess;
if (canCreate) {
setIsCreatingUser(true);
createAccount();
} else {
if (usernameError || username.length === 0) {
setUsernameStatus('registration_failed');
}
if (!passwordSuccess) {
setPasswordStatus('registration_failed');
}
}
};
const isMobile: boolean = useMediaQuery(theme.breakpoints.only('xs'));
return (
<>
<ProcessingRequest open={isCreatingUser} />
<AlertSnackbar
severity={'success'}
open={!!successAlertContent}
onClose={() => setSuccessAlertContent(undefined)}
>
{successAlertContent}
</AlertSnackbar>
<AlertSnackbar severity={'error'} open={!!errorAlertContent} onClose={() => setErrorAlertContent(undefined)}>
{errorAlertContent}
</AlertSnackbar>
<Stack
sx={{
minHeight: `${isMobile ? 'auto' : '100%'}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(20) }}>
<Typography component={'span'} variant="h2">
{t('registration_form_title')}
</Typography>
</Box>
<Form method="post" id="register-form">
<div>
{/* For i18n-parser.
t('username_input_helper_text_default')
t('username_input_helper_text_success')
t('username_input_helper_text_taken')
t('username_input_helper_text_invalid')
t('username_input_helper_text_registration_failed')
*/}
<UsernameInput
value={username}
onChange={handleUsername}
error={usernameError}
success={usernameSuccess}
helperText={usernameStatus === 'default' ? '' : t(`username_input_helper_text_${usernameStatus}`)}
sx={{ width: theme.typography.pxToRem(inputWidth) }}
tooltipTitle={t('registration_form_username_tooltip')}
/>
</div>
<div>
{/* For i18n-parser.
t('password_input_helper_text_default')
t('password_input_helper_text_too_weak')
t('password_input_helper_text_weak')
t('password_input_helper_text_medium')
t('password_input_helper_text_strong')
t('password_input_helper_text_registration_failed')
*/}
<PasswordInput
value={password}
onChange={handlePassword}
error={passwordError}
success={passwordSuccess}
helperText={passwordStatus === 'default' ? '' : t(`password_input_helper_text_${passwordStatus}`)}
sx={{ width: theme.typography.pxToRem(inputWidth) }}
tooltipTitle={t('registration_form_password_tooltip')}
/>
</div>
<Button
variant="contained"
type="submit"
onClick={handleSubmit}
sx={{ width: theme.typography.pxToRem(inputWidth), mt: theme.typography.pxToRem(20) }}
>
{t('registration_form_submit_button')}
</Button>
</Form>
<Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(50) }}>
<Typography variant="body1">
{t('registration_form_to_login_text')} &nbsp;
<a href="" onClick={login}>
{t('registration_form_to_login_link')}
</a>
</Typography>
</Box>
</Stack>
</>
);
}