blob: acbf8f97cf7df9dffe372f25c6f663738eb01b31 [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 */
Gabriel Rochon7057b4f2022-11-21 13:28:01 -050018import {
19 Box,
20 Button,
21 FormControl,
22 FormControlLabel,
23 Radio,
24 RadioGroup,
25 Stack,
26 Typography,
27 useMediaQuery,
28} from '@mui/material';
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040029import { Theme, useTheme } from '@mui/material/styles';
Ziwei Wang3ce1ac02023-02-03 11:59:03 -050030import { ChangeEvent, FormEvent, ReactNode, useEffect, useState } from 'react';
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040031import { useTranslation } from 'react-i18next';
Ziwei Wang3ce1ac02023-02-03 11:59:03 -050032import { Form, Link, useNavigate } from 'react-router-dom';
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040033
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040034import { AlertSnackbar } from '../components/AlertSnackbar';
simonab4eec82022-11-08 20:32:43 -050035import { NameStatus, PasswordInput, PasswordStatus, UsernameInput } from '../components/Input';
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040036import ProcessingRequest from '../components/ProcessingRequest';
Ziwei Wang3ce1ac02023-02-03 11:59:03 -050037import withAuthUI from '../components/WithAuthUI';
simonab4eec82022-11-08 20:32:43 -050038import { checkPasswordStrength, isNameRegistered, loginUser, registerUser, setAccessToken } from '../utils/auth';
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040039import { inputWidth, jamiUsernamePattern } from '../utils/constants';
Gabriel Rochon7057b4f2022-11-21 13:28:01 -050040import { InvalidCredentials, InvalidPassword, UsernameNotFound } from '../utils/errors';
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040041
Ziwei Wang3ce1ac02023-02-03 11:59:03 -050042function RegistrationForm() {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040043 const theme: Theme = useTheme();
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040044 const navigate = useNavigate();
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040045 const { t } = useTranslation();
46
47 const [isCreatingUser, setIsCreatingUser] = useState(false);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040048
49 const [username, setUsername] = useState('');
50 const [password, setPassword] = useState('');
Gabriel Rochon7057b4f2022-11-21 13:28:01 -050051 const [isJams, setIsJams] = useState(false);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040052
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040053 const [usernameStatus, setUsernameStatus] = useState<NameStatus>('default');
54 const [passwordStatus, setPasswordStatus] = useState<PasswordStatus>('default');
55
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040056 const [errorAlertContent, setErrorAlertContent] = useState<ReactNode>(undefined);
57 const [successAlertContent, setSuccessAlertContent] = useState<ReactNode>(undefined);
58
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040059 const usernameError = usernameStatus !== 'success' && usernameStatus !== 'default';
60 const usernameSuccess = usernameStatus === 'success';
61 const passwordError = passwordStatus !== 'strong' && passwordStatus !== 'default';
62 const passwordSuccess = passwordStatus === 'strong';
63
64 useEffect(() => {
65 // To prevent lookup if field is empty, in error state or lookup already done
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040066 if (username.length > 0 && usernameStatus === 'default') {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040067 const validateUsername = async () => {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040068 if (await isNameRegistered(username)) {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040069 setUsernameStatus('taken');
70 } else {
71 setUsernameStatus('success');
72 }
73 };
74 const timeout = setTimeout(validateUsername, 1000);
75
76 return () => clearTimeout(timeout);
77 }
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040078 }, [username, usernameStatus]);
79
80 const firstUserLogin = async () => {
81 try {
Gabriel Rochon7057b4f2022-11-21 13:28:01 -050082 const accessToken = await loginUser(username, password, isJams);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040083 setAccessToken(accessToken);
idillon3470d072022-11-22 15:22:34 -050084 navigate('/conversation', { replace: true });
simon94fe53e2022-11-10 12:51:58 -050085 } catch (e) {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040086 setIsCreatingUser(false);
simon94fe53e2022-11-10 12:51:58 -050087 if (e instanceof UsernameNotFound) {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040088 setErrorAlertContent(t('login_username_not_found'));
simon94fe53e2022-11-10 12:51:58 -050089 } else if (e instanceof InvalidPassword) {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040090 setErrorAlertContent(t('login_invalid_password'));
91 } else {
simon94fe53e2022-11-10 12:51:58 -050092 throw e;
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040093 }
94 }
95 };
96
97 const createAccount = async () => {
Gabriel Rochon7057b4f2022-11-21 13:28:01 -050098 try {
99 await registerUser(username, password, isJams);
100 setSuccessAlertContent(t('registration_success'));
101 await firstUserLogin();
102 } catch (e) {
103 setIsCreatingUser(false);
104 if (e instanceof UsernameNotFound) {
105 setErrorAlertContent(t('login_username_not_found'));
106 } else if (e instanceof InvalidPassword) {
107 setErrorAlertContent(t('login_invalid_password'));
108 } else if (e instanceof InvalidCredentials) {
109 setErrorAlertContent(t('login_invalid_credentials'));
110 } else {
111 throw e;
112 }
113 }
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400114 };
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400115
116 const handleUsername = async (event: ChangeEvent<HTMLInputElement>) => {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400117 const usernameValue: string = event.target.value;
118 setUsername(usernameValue);
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400119
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400120 if (usernameValue.length > 0 && !jamiUsernamePattern.test(usernameValue)) {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400121 setUsernameStatus('invalid');
122 } else {
123 setUsernameStatus('default');
124 }
125 };
126
127 const handlePassword = (event: ChangeEvent<HTMLInputElement>) => {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400128 const passwordValue: string = event.target.value;
129 setPassword(passwordValue);
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400130
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400131 if (passwordValue.length > 0) {
132 const checkResult = checkPasswordStrength(passwordValue);
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400133 setPasswordStatus(checkResult.valueCode);
134 } else {
135 setPasswordStatus('default');
136 }
137 };
138
Gabriel Rochon7057b4f2022-11-21 13:28:01 -0500139 const handleIsJams = (event: ChangeEvent<HTMLInputElement>) => {
140 setIsJams(event.target.value === 'true');
141 };
142
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400143 const handleSubmit = async (event: FormEvent) => {
144 event.preventDefault();
145 const canCreate = usernameSuccess && passwordSuccess;
146
147 if (canCreate) {
148 setIsCreatingUser(true);
simonff1cb352022-11-24 15:15:26 -0500149 await createAccount();
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400150 } else {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400151 if (usernameError || username.length === 0) {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400152 setUsernameStatus('registration_failed');
153 }
154 if (!passwordSuccess) {
155 setPasswordStatus('registration_failed');
156 }
157 }
158 };
159
160 const isMobile: boolean = useMediaQuery(theme.breakpoints.only('xs'));
161
162 return (
163 <>
164 <ProcessingRequest open={isCreatingUser} />
165
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400166 <AlertSnackbar
167 severity={'success'}
168 open={!!successAlertContent}
169 onClose={() => setSuccessAlertContent(undefined)}
170 >
171 {successAlertContent}
172 </AlertSnackbar>
173
174 <AlertSnackbar severity={'error'} open={!!errorAlertContent} onClose={() => setErrorAlertContent(undefined)}>
175 {errorAlertContent}
176 </AlertSnackbar>
177
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400178 <Stack
179 sx={{
180 minHeight: `${isMobile ? 'auto' : '100%'}`,
181 display: 'flex',
182 alignItems: 'center',
183 justifyContent: 'center',
184 }}
185 >
186 <Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(20) }}>
187 <Typography component={'span'} variant="h2">
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400188 {t('registration_form_title')}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400189 </Typography>
190 </Box>
191
192 <Form method="post" id="register-form">
193 <div>
194 <UsernameInput
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400195 value={username}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400196 onChange={handleUsername}
197 error={usernameError}
198 success={usernameSuccess}
simonab4eec82022-11-08 20:32:43 -0500199 status={usernameStatus}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400200 sx={{ width: theme.typography.pxToRem(inputWidth) }}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400201 tooltipTitle={t('registration_form_username_tooltip')}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400202 />
203 </div>
204 <div>
205 <PasswordInput
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400206 value={password}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400207 onChange={handlePassword}
208 error={passwordError}
209 success={passwordSuccess}
simonab4eec82022-11-08 20:32:43 -0500210 status={passwordStatus}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400211 sx={{ width: theme.typography.pxToRem(inputWidth) }}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400212 tooltipTitle={t('registration_form_password_tooltip')}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400213 />
214 </div>
Gabriel Rochon7057b4f2022-11-21 13:28:01 -0500215 <div>
216 <FormControl
217 sx={{
218 width: theme.typography.pxToRem(inputWidth),
219 alignItems: 'center',
220 justifyContent: 'space-between',
221 }}
222 >
223 <RadioGroup row onChange={handleIsJams} value={isJams}>
224 <FormControlLabel value="false" control={<Radio />} label={t('jami')} />
225 <FormControlLabel value="true" control={<Radio />} label={t('jams')} />
226 </RadioGroup>
227 </FormControl>
228 </div>
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400229
230 <Button
231 variant="contained"
232 type="submit"
233 onClick={handleSubmit}
234 sx={{ width: theme.typography.pxToRem(inputWidth), mt: theme.typography.pxToRem(20) }}
235 >
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400236 {t('registration_form_submit_button')}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400237 </Button>
238 </Form>
239
240 <Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(50) }}>
241 <Typography variant="body1">
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400242 {t('registration_form_to_login_text')} &nbsp;
Ziwei Wang3ce1ac02023-02-03 11:59:03 -0500243 <Link to={'/login'}>{t('registration_form_to_login_link')}</Link>
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400244 </Typography>
245 </Box>
246 </Stack>
247 </>
248 );
249}
Ziwei Wang3ce1ac02023-02-03 11:59:03 -0500250
251export default withAuthUI(RegistrationForm);