blob: 71c38bd01b9fbebee50aeb5a7a8154810739eed2 [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';
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040025import { PasswordInput, UsernameInput } from '../components/Input';
26import ProcessingRequest from '../components/ProcessingRequest';
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040027import {
28 checkPasswordStrength,
29 isNameRegistered,
30 loginUser,
31 registerUser,
32 setAccessToken,
33 StrengthValueCode,
34} from '../utils/auth';
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040035import { inputWidth, jamiUsernamePattern } from '../utils/constants';
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040036import { InvalidPassword, UsernameNotFound } from '../utils/errors';
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040037
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040038type NameStatus = 'default' | 'success' | 'taken' | 'invalid' | 'registration_failed';
39type PasswordStatus = StrengthValueCode | 'registration_failed';
40
41type 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('');
54
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040055 const [usernameStatus, setUsernameStatus] = useState<NameStatus>('default');
56 const [passwordStatus, setPasswordStatus] = useState<PasswordStatus>('default');
57
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040058 const [errorAlertContent, setErrorAlertContent] = useState<ReactNode>(undefined);
59 const [successAlertContent, setSuccessAlertContent] = useState<ReactNode>(undefined);
60
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040061 const usernameError = usernameStatus !== 'success' && usernameStatus !== 'default';
62 const usernameSuccess = usernameStatus === 'success';
63 const passwordError = passwordStatus !== 'strong' && passwordStatus !== 'default';
64 const passwordSuccess = passwordStatus === 'strong';
65
66 useEffect(() => {
67 // To prevent lookup if field is empty, in error state or lookup already done
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040068 if (username.length > 0 && usernameStatus === 'default') {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040069 const validateUsername = async () => {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040070 if (await isNameRegistered(username)) {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040071 setUsernameStatus('taken');
72 } else {
73 setUsernameStatus('success');
74 }
75 };
76 const timeout = setTimeout(validateUsername, 1000);
77
78 return () => clearTimeout(timeout);
79 }
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040080 }, [username, usernameStatus]);
81
82 const firstUserLogin = async () => {
83 try {
84 const accessToken = await loginUser(username, password);
85 setAccessToken(accessToken);
86 navigate('/settings', { replace: true });
87 } catch (err) {
88 setIsCreatingUser(false);
89 if (err instanceof UsernameNotFound) {
90 setErrorAlertContent(t('login_username_not_found'));
91 } else if (err instanceof InvalidPassword) {
92 setErrorAlertContent(t('login_invalid_password'));
93 } else {
94 throw err;
95 }
96 }
97 };
98
99 const createAccount = async () => {
100 await registerUser(username, password);
101 setSuccessAlertContent(t('registration_success'));
102 await firstUserLogin();
103 };
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400104
105 const handleUsername = async (event: ChangeEvent<HTMLInputElement>) => {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400106 const usernameValue: string = event.target.value;
107 setUsername(usernameValue);
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400108
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400109 if (usernameValue.length > 0 && !jamiUsernamePattern.test(usernameValue)) {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400110 setUsernameStatus('invalid');
111 } else {
112 setUsernameStatus('default');
113 }
114 };
115
116 const handlePassword = (event: ChangeEvent<HTMLInputElement>) => {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400117 const passwordValue: string = event.target.value;
118 setPassword(passwordValue);
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400119
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400120 if (passwordValue.length > 0) {
121 const checkResult = checkPasswordStrength(passwordValue);
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400122 setPasswordStatus(checkResult.valueCode);
123 } else {
124 setPasswordStatus('default');
125 }
126 };
127
128 const login = (event: MouseEvent<HTMLAnchorElement>) => {
129 event.preventDefault();
130 props.login();
131 };
132
133 const handleSubmit = async (event: FormEvent) => {
134 event.preventDefault();
135 const canCreate = usernameSuccess && passwordSuccess;
136
137 if (canCreate) {
138 setIsCreatingUser(true);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400139 createAccount();
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400140 } else {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400141 if (usernameError || username.length === 0) {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400142 setUsernameStatus('registration_failed');
143 }
144 if (!passwordSuccess) {
145 setPasswordStatus('registration_failed');
146 }
147 }
148 };
149
150 const isMobile: boolean = useMediaQuery(theme.breakpoints.only('xs'));
151
152 return (
153 <>
154 <ProcessingRequest open={isCreatingUser} />
155
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400156 <AlertSnackbar
157 severity={'success'}
158 open={!!successAlertContent}
159 onClose={() => setSuccessAlertContent(undefined)}
160 >
161 {successAlertContent}
162 </AlertSnackbar>
163
164 <AlertSnackbar severity={'error'} open={!!errorAlertContent} onClose={() => setErrorAlertContent(undefined)}>
165 {errorAlertContent}
166 </AlertSnackbar>
167
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400168 <Stack
169 sx={{
170 minHeight: `${isMobile ? 'auto' : '100%'}`,
171 display: 'flex',
172 alignItems: 'center',
173 justifyContent: 'center',
174 }}
175 >
176 <Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(20) }}>
177 <Typography component={'span'} variant="h2">
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400178 {t('registration_form_title')}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400179 </Typography>
180 </Box>
181
182 <Form method="post" id="register-form">
183 <div>
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400184 {/* For i18n-parser.
185 t('username_input_helper_text_default')
186 t('username_input_helper_text_success')
187 t('username_input_helper_text_taken')
188 t('username_input_helper_text_invalid')
189 t('username_input_helper_text_registration_failed')
190 */}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400191 <UsernameInput
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400192 value={username}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400193 onChange={handleUsername}
194 error={usernameError}
195 success={usernameSuccess}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400196 helperText={usernameStatus === 'default' ? '' : t(`username_input_helper_text_${usernameStatus}`)}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400197 sx={{ width: theme.typography.pxToRem(inputWidth) }}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400198 tooltipTitle={t('registration_form_username_tooltip')}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400199 />
200 </div>
201 <div>
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400202 {/* For i18n-parser.
203 t('password_input_helper_text_default')
204 t('password_input_helper_text_too_weak')
205 t('password_input_helper_text_weak')
206 t('password_input_helper_text_medium')
207 t('password_input_helper_text_strong')
208 t('password_input_helper_text_registration_failed')
209 */}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400210 <PasswordInput
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400211 value={password}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400212 onChange={handlePassword}
213 error={passwordError}
214 success={passwordSuccess}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400215 helperText={passwordStatus === 'default' ? '' : t(`password_input_helper_text_${passwordStatus}`)}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400216 sx={{ width: theme.typography.pxToRem(inputWidth) }}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400217 tooltipTitle={t('registration_form_password_tooltip')}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400218 />
219 </div>
220
221 <Button
222 variant="contained"
223 type="submit"
224 onClick={handleSubmit}
225 sx={{ width: theme.typography.pxToRem(inputWidth), mt: theme.typography.pxToRem(20) }}
226 >
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400227 {t('registration_form_submit_button')}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400228 </Button>
229 </Form>
230
231 <Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(50) }}>
232 <Typography variant="body1">
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400233 {t('registration_form_to_login_text')} &nbsp;
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400234 <a href="" onClick={login}>
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400235 {t('registration_form_to_login_link')}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400236 </a>
237 </Typography>
238 </Box>
239 </Stack>
240 </>
241 );
242}