blob: 0cb2f2da7344be17ac322fa934ee97872b81673d [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';
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040030import { ChangeEvent, FormEvent, MouseEvent, ReactNode, useEffect, useState } from 'react';
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040031import { useTranslation } from 'react-i18next';
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040032import { Form, 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';
simonab4eec82022-11-08 20:32:43 -050037import { checkPasswordStrength, isNameRegistered, loginUser, registerUser, setAccessToken } from '../utils/auth';
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040038import { inputWidth, jamiUsernamePattern } from '../utils/constants';
Gabriel Rochon7057b4f2022-11-21 13:28:01 -050039import { InvalidCredentials, InvalidPassword, UsernameNotFound } from '../utils/errors';
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040040
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040041type JamiRegistrationProps = {
42 login: () => void;
43};
44
45export default function JamiRegistration(props: JamiRegistrationProps) {
46 const theme: Theme = useTheme();
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040047 const navigate = useNavigate();
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040048 const { t } = useTranslation();
49
50 const [isCreatingUser, setIsCreatingUser] = useState(false);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040051
52 const [username, setUsername] = useState('');
53 const [password, setPassword] = useState('');
Gabriel Rochon7057b4f2022-11-21 13:28:01 -050054 const [isJams, setIsJams] = useState(false);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040055
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040056 const [usernameStatus, setUsernameStatus] = useState<NameStatus>('default');
57 const [passwordStatus, setPasswordStatus] = useState<PasswordStatus>('default');
58
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040059 const [errorAlertContent, setErrorAlertContent] = useState<ReactNode>(undefined);
60 const [successAlertContent, setSuccessAlertContent] = useState<ReactNode>(undefined);
61
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040062 const usernameError = usernameStatus !== 'success' && usernameStatus !== 'default';
63 const usernameSuccess = usernameStatus === 'success';
64 const passwordError = passwordStatus !== 'strong' && passwordStatus !== 'default';
65 const passwordSuccess = passwordStatus === 'strong';
66
67 useEffect(() => {
68 // To prevent lookup if field is empty, in error state or lookup already done
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040069 if (username.length > 0 && usernameStatus === 'default') {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040070 const validateUsername = async () => {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040071 if (await isNameRegistered(username)) {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040072 setUsernameStatus('taken');
73 } else {
74 setUsernameStatus('success');
75 }
76 };
77 const timeout = setTimeout(validateUsername, 1000);
78
79 return () => clearTimeout(timeout);
80 }
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040081 }, [username, usernameStatus]);
82
83 const firstUserLogin = async () => {
84 try {
Gabriel Rochon7057b4f2022-11-21 13:28:01 -050085 const accessToken = await loginUser(username, password, isJams);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040086 setAccessToken(accessToken);
idillon3470d072022-11-22 15:22:34 -050087 navigate('/conversation', { replace: true });
simon94fe53e2022-11-10 12:51:58 -050088 } catch (e) {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040089 setIsCreatingUser(false);
simon94fe53e2022-11-10 12:51:58 -050090 if (e instanceof UsernameNotFound) {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040091 setErrorAlertContent(t('login_username_not_found'));
simon94fe53e2022-11-10 12:51:58 -050092 } else if (e instanceof InvalidPassword) {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040093 setErrorAlertContent(t('login_invalid_password'));
94 } else {
simon94fe53e2022-11-10 12:51:58 -050095 throw e;
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040096 }
97 }
98 };
99
100 const createAccount = async () => {
Gabriel Rochon7057b4f2022-11-21 13:28:01 -0500101 try {
102 await registerUser(username, password, isJams);
103 setSuccessAlertContent(t('registration_success'));
104 await firstUserLogin();
105 } catch (e) {
106 setIsCreatingUser(false);
107 if (e instanceof UsernameNotFound) {
108 setErrorAlertContent(t('login_username_not_found'));
109 } else if (e instanceof InvalidPassword) {
110 setErrorAlertContent(t('login_invalid_password'));
111 } else if (e instanceof InvalidCredentials) {
112 setErrorAlertContent(t('login_invalid_credentials'));
113 } else {
114 throw e;
115 }
116 }
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400117 };
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400118
119 const handleUsername = async (event: ChangeEvent<HTMLInputElement>) => {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400120 const usernameValue: string = event.target.value;
121 setUsername(usernameValue);
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400122
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400123 if (usernameValue.length > 0 && !jamiUsernamePattern.test(usernameValue)) {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400124 setUsernameStatus('invalid');
125 } else {
126 setUsernameStatus('default');
127 }
128 };
129
130 const handlePassword = (event: ChangeEvent<HTMLInputElement>) => {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400131 const passwordValue: string = event.target.value;
132 setPassword(passwordValue);
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400133
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400134 if (passwordValue.length > 0) {
135 const checkResult = checkPasswordStrength(passwordValue);
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400136 setPasswordStatus(checkResult.valueCode);
137 } else {
138 setPasswordStatus('default');
139 }
140 };
141
Gabriel Rochon7057b4f2022-11-21 13:28:01 -0500142 const handleIsJams = (event: ChangeEvent<HTMLInputElement>) => {
143 setIsJams(event.target.value === 'true');
144 };
145
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400146 const login = (event: MouseEvent<HTMLAnchorElement>) => {
147 event.preventDefault();
148 props.login();
149 };
150
151 const handleSubmit = async (event: FormEvent) => {
152 event.preventDefault();
153 const canCreate = usernameSuccess && passwordSuccess;
154
155 if (canCreate) {
156 setIsCreatingUser(true);
simonff1cb352022-11-24 15:15:26 -0500157 await createAccount();
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400158 } else {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400159 if (usernameError || username.length === 0) {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400160 setUsernameStatus('registration_failed');
161 }
162 if (!passwordSuccess) {
163 setPasswordStatus('registration_failed');
164 }
165 }
166 };
167
168 const isMobile: boolean = useMediaQuery(theme.breakpoints.only('xs'));
169
170 return (
171 <>
172 <ProcessingRequest open={isCreatingUser} />
173
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400174 <AlertSnackbar
175 severity={'success'}
176 open={!!successAlertContent}
177 onClose={() => setSuccessAlertContent(undefined)}
178 >
179 {successAlertContent}
180 </AlertSnackbar>
181
182 <AlertSnackbar severity={'error'} open={!!errorAlertContent} onClose={() => setErrorAlertContent(undefined)}>
183 {errorAlertContent}
184 </AlertSnackbar>
185
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400186 <Stack
187 sx={{
188 minHeight: `${isMobile ? 'auto' : '100%'}`,
189 display: 'flex',
190 alignItems: 'center',
191 justifyContent: 'center',
192 }}
193 >
194 <Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(20) }}>
195 <Typography component={'span'} variant="h2">
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400196 {t('registration_form_title')}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400197 </Typography>
198 </Box>
199
200 <Form method="post" id="register-form">
201 <div>
202 <UsernameInput
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400203 value={username}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400204 onChange={handleUsername}
205 error={usernameError}
206 success={usernameSuccess}
simonab4eec82022-11-08 20:32:43 -0500207 status={usernameStatus}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400208 sx={{ width: theme.typography.pxToRem(inputWidth) }}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400209 tooltipTitle={t('registration_form_username_tooltip')}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400210 />
211 </div>
212 <div>
213 <PasswordInput
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400214 value={password}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400215 onChange={handlePassword}
216 error={passwordError}
217 success={passwordSuccess}
simonab4eec82022-11-08 20:32:43 -0500218 status={passwordStatus}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400219 sx={{ width: theme.typography.pxToRem(inputWidth) }}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400220 tooltipTitle={t('registration_form_password_tooltip')}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400221 />
222 </div>
Gabriel Rochon7057b4f2022-11-21 13:28:01 -0500223 <div>
224 <FormControl
225 sx={{
226 width: theme.typography.pxToRem(inputWidth),
227 alignItems: 'center',
228 justifyContent: 'space-between',
229 }}
230 >
231 <RadioGroup row onChange={handleIsJams} value={isJams}>
232 <FormControlLabel value="false" control={<Radio />} label={t('jami')} />
233 <FormControlLabel value="true" control={<Radio />} label={t('jams')} />
234 </RadioGroup>
235 </FormControl>
236 </div>
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400237
238 <Button
239 variant="contained"
240 type="submit"
241 onClick={handleSubmit}
242 sx={{ width: theme.typography.pxToRem(inputWidth), mt: theme.typography.pxToRem(20) }}
243 >
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400244 {t('registration_form_submit_button')}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400245 </Button>
246 </Form>
247
248 <Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(50) }}>
249 <Typography variant="body1">
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400250 {t('registration_form_to_login_text')} &nbsp;
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400251 <a href="" onClick={login}>
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400252 {t('registration_form_to_login_link')}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400253 </a>
254 </Typography>
255 </Box>
256 </Stack>
257 </>
258 );
259}