blob: a1024d565c34c6fb00be8c296ba2b56e45194959 [file] [log] [blame]
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -04001/*
2 * Copyright (C) 2022 Savoir-faire Linux Inc.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation; either version 3 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Affero General Public License for more details.
13 *
14 * You should have received a copy of the GNU Affero General Public
15 * License along with this program. If not, see
16 * <https://www.gnu.org/licenses/>.
17 */
18import { Box, Button, Stack, Typography, useMediaQuery } from '@mui/material';
19import { Theme, useTheme } from '@mui/material/styles';
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040020import { ChangeEvent, FormEvent, MouseEvent, ReactNode, useEffect, useState } from 'react';
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040021import { useTranslation } from 'react-i18next';
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040022import { Form, useNavigate } from 'react-router-dom';
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040023
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040024import { AlertSnackbar } from '../components/AlertSnackbar';
simonab4eec82022-11-08 20:32:43 -050025import { NameStatus, PasswordInput, PasswordStatus, UsernameInput } from '../components/Input';
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040026import ProcessingRequest from '../components/ProcessingRequest';
simonab4eec82022-11-08 20:32:43 -050027import { checkPasswordStrength, isNameRegistered, loginUser, registerUser, setAccessToken } from '../utils/auth';
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040028import { inputWidth, jamiUsernamePattern } from '../utils/constants';
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040029import { InvalidPassword, UsernameNotFound } from '../utils/errors';
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040030
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040031type JamiRegistrationProps = {
32 login: () => void;
33};
34
35export default function JamiRegistration(props: JamiRegistrationProps) {
36 const theme: Theme = useTheme();
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040037 const navigate = useNavigate();
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040038 const { t } = useTranslation();
39
40 const [isCreatingUser, setIsCreatingUser] = useState(false);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040041
42 const [username, setUsername] = useState('');
43 const [password, setPassword] = useState('');
44
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040045 const [usernameStatus, setUsernameStatus] = useState<NameStatus>('default');
46 const [passwordStatus, setPasswordStatus] = useState<PasswordStatus>('default');
47
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040048 const [errorAlertContent, setErrorAlertContent] = useState<ReactNode>(undefined);
49 const [successAlertContent, setSuccessAlertContent] = useState<ReactNode>(undefined);
50
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040051 const usernameError = usernameStatus !== 'success' && usernameStatus !== 'default';
52 const usernameSuccess = usernameStatus === 'success';
53 const passwordError = passwordStatus !== 'strong' && passwordStatus !== 'default';
54 const passwordSuccess = passwordStatus === 'strong';
55
56 useEffect(() => {
57 // To prevent lookup if field is empty, in error state or lookup already done
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040058 if (username.length > 0 && usernameStatus === 'default') {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040059 const validateUsername = async () => {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040060 if (await isNameRegistered(username)) {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040061 setUsernameStatus('taken');
62 } else {
63 setUsernameStatus('success');
64 }
65 };
66 const timeout = setTimeout(validateUsername, 1000);
67
68 return () => clearTimeout(timeout);
69 }
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040070 }, [username, usernameStatus]);
71
72 const firstUserLogin = async () => {
73 try {
74 const accessToken = await loginUser(username, password);
75 setAccessToken(accessToken);
76 navigate('/settings', { replace: true });
77 } catch (err) {
78 setIsCreatingUser(false);
79 if (err instanceof UsernameNotFound) {
80 setErrorAlertContent(t('login_username_not_found'));
81 } else if (err instanceof InvalidPassword) {
82 setErrorAlertContent(t('login_invalid_password'));
83 } else {
84 throw err;
85 }
86 }
87 };
88
89 const createAccount = async () => {
90 await registerUser(username, password);
91 setSuccessAlertContent(t('registration_success'));
92 await firstUserLogin();
93 };
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040094
95 const handleUsername = async (event: ChangeEvent<HTMLInputElement>) => {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040096 const usernameValue: string = event.target.value;
97 setUsername(usernameValue);
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040098
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040099 if (usernameValue.length > 0 && !jamiUsernamePattern.test(usernameValue)) {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400100 setUsernameStatus('invalid');
101 } else {
102 setUsernameStatus('default');
103 }
104 };
105
106 const handlePassword = (event: ChangeEvent<HTMLInputElement>) => {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400107 const passwordValue: string = event.target.value;
108 setPassword(passwordValue);
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400109
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400110 if (passwordValue.length > 0) {
111 const checkResult = checkPasswordStrength(passwordValue);
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400112 setPasswordStatus(checkResult.valueCode);
113 } else {
114 setPasswordStatus('default');
115 }
116 };
117
118 const login = (event: MouseEvent<HTMLAnchorElement>) => {
119 event.preventDefault();
120 props.login();
121 };
122
123 const handleSubmit = async (event: FormEvent) => {
124 event.preventDefault();
125 const canCreate = usernameSuccess && passwordSuccess;
126
127 if (canCreate) {
128 setIsCreatingUser(true);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400129 createAccount();
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400130 } else {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400131 if (usernameError || username.length === 0) {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400132 setUsernameStatus('registration_failed');
133 }
134 if (!passwordSuccess) {
135 setPasswordStatus('registration_failed');
136 }
137 }
138 };
139
140 const isMobile: boolean = useMediaQuery(theme.breakpoints.only('xs'));
141
142 return (
143 <>
144 <ProcessingRequest open={isCreatingUser} />
145
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400146 <AlertSnackbar
147 severity={'success'}
148 open={!!successAlertContent}
149 onClose={() => setSuccessAlertContent(undefined)}
150 >
151 {successAlertContent}
152 </AlertSnackbar>
153
154 <AlertSnackbar severity={'error'} open={!!errorAlertContent} onClose={() => setErrorAlertContent(undefined)}>
155 {errorAlertContent}
156 </AlertSnackbar>
157
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400158 <Stack
159 sx={{
160 minHeight: `${isMobile ? 'auto' : '100%'}`,
161 display: 'flex',
162 alignItems: 'center',
163 justifyContent: 'center',
164 }}
165 >
166 <Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(20) }}>
167 <Typography component={'span'} variant="h2">
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400168 {t('registration_form_title')}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400169 </Typography>
170 </Box>
171
172 <Form method="post" id="register-form">
173 <div>
174 <UsernameInput
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400175 value={username}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400176 onChange={handleUsername}
177 error={usernameError}
178 success={usernameSuccess}
simonab4eec82022-11-08 20:32:43 -0500179 status={usernameStatus}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400180 sx={{ width: theme.typography.pxToRem(inputWidth) }}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400181 tooltipTitle={t('registration_form_username_tooltip')}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400182 />
183 </div>
184 <div>
185 <PasswordInput
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400186 value={password}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400187 onChange={handlePassword}
188 error={passwordError}
189 success={passwordSuccess}
simonab4eec82022-11-08 20:32:43 -0500190 status={passwordStatus}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400191 sx={{ width: theme.typography.pxToRem(inputWidth) }}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400192 tooltipTitle={t('registration_form_password_tooltip')}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400193 />
194 </div>
195
196 <Button
197 variant="contained"
198 type="submit"
199 onClick={handleSubmit}
200 sx={{ width: theme.typography.pxToRem(inputWidth), mt: theme.typography.pxToRem(20) }}
201 >
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400202 {t('registration_form_submit_button')}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400203 </Button>
204 </Form>
205
206 <Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(50) }}>
207 <Typography variant="body1">
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400208 {t('registration_form_to_login_text')} &nbsp;
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400209 <a href="" onClick={login}>
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400210 {t('registration_form_to_login_link')}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400211 </a>
212 </Typography>
213 </Box>
214 </Stack>
215 </>
216 );
217}