blob: 2e8d23d185291bedb020f21c2a7c370a1bd497aa [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
38const usernameTooltipTitle =
39 'Choose a password hard to guess for others but easy to remember for you, ' +
40 'it must be at least 8 characters. ' +
41 "Your account won't be recovered if you forget it!\n\n" +
42 'Click for more details';
43
44const passwordTooltipTitle =
45 'Username may be from 3 to 32 chraracters long and contain a-z, A-Z, -, _\n\n' + 'Click for more details';
46
47type NameStatus = 'default' | 'success' | 'taken' | 'invalid' | 'registration_failed';
48type PasswordStatus = StrengthValueCode | 'registration_failed';
49
50type JamiRegistrationProps = {
51 login: () => void;
52};
53
54export default function JamiRegistration(props: JamiRegistrationProps) {
55 const theme: Theme = useTheme();
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040056 const navigate = useNavigate();
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040057 const { t } = useTranslation();
58
59 const [isCreatingUser, setIsCreatingUser] = useState(false);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040060
61 const [username, setUsername] = useState('');
62 const [password, setPassword] = useState('');
63
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040064 const [usernameStatus, setUsernameStatus] = useState<NameStatus>('default');
65 const [passwordStatus, setPasswordStatus] = useState<PasswordStatus>('default');
66
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040067 const [errorAlertContent, setErrorAlertContent] = useState<ReactNode>(undefined);
68 const [successAlertContent, setSuccessAlertContent] = useState<ReactNode>(undefined);
69
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040070 const usernameError = usernameStatus !== 'success' && usernameStatus !== 'default';
71 const usernameSuccess = usernameStatus === 'success';
72 const passwordError = passwordStatus !== 'strong' && passwordStatus !== 'default';
73 const passwordSuccess = passwordStatus === 'strong';
74
75 useEffect(() => {
76 // To prevent lookup if field is empty, in error state or lookup already done
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040077 if (username.length > 0 && usernameStatus === 'default') {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040078 const validateUsername = async () => {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040079 if (await isNameRegistered(username)) {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -040080 setUsernameStatus('taken');
81 } else {
82 setUsernameStatus('success');
83 }
84 };
85 const timeout = setTimeout(validateUsername, 1000);
86
87 return () => clearTimeout(timeout);
88 }
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040089 }, [username, usernameStatus]);
90
91 const firstUserLogin = async () => {
92 try {
93 const accessToken = await loginUser(username, password);
94 setAccessToken(accessToken);
95 navigate('/settings', { replace: true });
96 } catch (err) {
97 setIsCreatingUser(false);
98 if (err instanceof UsernameNotFound) {
99 setErrorAlertContent(t('login_username_not_found'));
100 } else if (err instanceof InvalidPassword) {
101 setErrorAlertContent(t('login_invalid_password'));
102 } else {
103 throw err;
104 }
105 }
106 };
107
108 const createAccount = async () => {
109 await registerUser(username, password);
110 setSuccessAlertContent(t('registration_success'));
111 await firstUserLogin();
112 };
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400113
114 const handleUsername = async (event: ChangeEvent<HTMLInputElement>) => {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400115 const usernameValue: string = event.target.value;
116 setUsername(usernameValue);
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400117
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400118 if (usernameValue.length > 0 && !jamiUsernamePattern.test(usernameValue)) {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400119 setUsernameStatus('invalid');
120 } else {
121 setUsernameStatus('default');
122 }
123 };
124
125 const handlePassword = (event: ChangeEvent<HTMLInputElement>) => {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400126 const passwordValue: string = event.target.value;
127 setPassword(passwordValue);
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400128
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400129 if (passwordValue.length > 0) {
130 const checkResult = checkPasswordStrength(passwordValue);
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400131 setPasswordStatus(checkResult.valueCode);
132 } else {
133 setPasswordStatus('default');
134 }
135 };
136
137 const login = (event: MouseEvent<HTMLAnchorElement>) => {
138 event.preventDefault();
139 props.login();
140 };
141
142 const handleSubmit = async (event: FormEvent) => {
143 event.preventDefault();
144 const canCreate = usernameSuccess && passwordSuccess;
145
146 if (canCreate) {
147 setIsCreatingUser(true);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400148 createAccount();
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400149 } else {
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400150 if (usernameError || username.length === 0) {
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400151 setUsernameStatus('registration_failed');
152 }
153 if (!passwordSuccess) {
154 setPasswordStatus('registration_failed');
155 }
156 }
157 };
158
159 const isMobile: boolean = useMediaQuery(theme.breakpoints.only('xs'));
160
161 return (
162 <>
163 <ProcessingRequest open={isCreatingUser} />
164
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400165 <AlertSnackbar
166 severity={'success'}
167 open={!!successAlertContent}
168 onClose={() => setSuccessAlertContent(undefined)}
169 >
170 {successAlertContent}
171 </AlertSnackbar>
172
173 <AlertSnackbar severity={'error'} open={!!errorAlertContent} onClose={() => setErrorAlertContent(undefined)}>
174 {errorAlertContent}
175 </AlertSnackbar>
176
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400177 <Stack
178 sx={{
179 minHeight: `${isMobile ? 'auto' : '100%'}`,
180 display: 'flex',
181 alignItems: 'center',
182 justifyContent: 'center',
183 }}
184 >
185 <Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(20) }}>
186 <Typography component={'span'} variant="h2">
187 REGISTRATION
188 </Typography>
189 </Box>
190
191 <Form method="post" id="register-form">
192 <div>
193 <UsernameInput
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400194 value={username}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400195 onChange={handleUsername}
196 error={usernameError}
197 success={usernameSuccess}
198 helperText={t(`username_input_${usernameStatus}_helper_text`)}
199 sx={{ width: theme.typography.pxToRem(inputWidth) }}
200 tooltipTitle={usernameTooltipTitle}
201 />
202 </div>
203 <div>
204 <PasswordInput
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400205 value={password}
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400206 onChange={handlePassword}
207 error={passwordError}
208 success={passwordSuccess}
209 helperText={t(`password_input_${passwordStatus}_helper_text`)}
210 sx={{ width: theme.typography.pxToRem(inputWidth) }}
211 tooltipTitle={passwordTooltipTitle}
212 />
213 </div>
214
215 <Button
216 variant="contained"
217 type="submit"
218 onClick={handleSubmit}
219 sx={{ width: theme.typography.pxToRem(inputWidth), mt: theme.typography.pxToRem(20) }}
220 >
221 REGISTER
222 </Button>
223 </Form>
224
225 <Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(50) }}>
226 <Typography variant="body1">
227 Already have an account ? &nbsp;
228 <a href="" onClick={login}>
229 LOG IN
230 </a>
231 </Typography>
232 </Box>
233 </Stack>
234 </>
235 );
236}