Refactor registration and login and improve routing
Changes:
- Improve registration and login pages
- Extract Home component from App.tsx file into its own
- Make Home component display registration or login
- Extract routes from App component and refactor routing
GitLab: #12
Change-Id: I68b01890781308282072b6dcf5e6df0d54837b4a
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 3e5de94..3f95af2 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -15,88 +15,21 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
-import './dayjsInitializer';
+import { useState } from 'react';
+import { Outlet } from 'react-router-dom';
-import { ThemeProvider } from '@mui/material/styles';
-import { useEffect, useState } from 'react';
-import { Navigate, Route, Routes } from 'react-router-dom';
-
-import authManager from './AuthManager';
import WelcomeAnimation from './components/welcome';
-import NotFoundPage from './pages/404';
-import AccountCreationDialog from './pages/AccountCreation';
-import AccountSelection from './pages/AccountSelection';
-import AccountSettings from './pages/AccountSettings';
-import JamiAccountDialog from './pages/JamiAccountCreation';
-import JamiMessenger from './pages/JamiMessenger';
-import SignInPage from './pages/LoginDialog';
-import ServerSetup from './pages/ServerSetup';
-import defaultTheme from './themes/Default';
-import { ThemeDemonstrator } from './themes/ThemeDemonstrator';
-
-// import { useSelector, useDispatch } from 'react-redux'
-// import { useAppSelector, useAppDispatch } from '../redux/hooks'
-
-const Home = () => {
- return <Navigate to="/account" />;
-};
const App = () => {
- // const count = useSelector(state => state.counter.value)
- // const dispatch = useDispatch();
- // const count = useAppSelector((state) => state.counter.value);
- // const dispatch = useAppDispatch();
-
- const [state, setState] = useState({
- loaded: false,
- auth: authManager.getState(),
- });
- const [displayWelcome, setDisplayWelcome] = useState(true);
-
- useEffect(() => {
- authManager.init((auth) => {
- setState({ loaded: false, auth });
- });
- return () => authManager.deinit();
- }, []);
+ const [displayWelcome, setDisplayWelcome] = useState<boolean>(true);
console.log('App render');
if (displayWelcome) {
- return <WelcomeAnimation showSetup={!state.auth.setupComplete} onComplete={() => setDisplayWelcome(false)} />;
- } else if (!state.auth.setupComplete) {
- return (
- <Routes>
- <Route path="/setup" element={<ServerSetup />} />
- <Route path="/" element={<Navigate to="/setup" replace />} />
- <Route path="*" element={<Navigate to="/setup" replace />} />
- </Routes>
- );
+ return <WelcomeAnimation onComplete={() => setDisplayWelcome(false)} />;
}
- return (
- <ThemeProvider theme={defaultTheme}>
- <Routes>
- <Route path="/account">
- <Route index element={<AccountSelection />} />
- <Route path=":accountId">
- <Route index element={<JamiMessenger />} />
- <Route path="*" element={<JamiMessenger />} />
- <Route path="settings" element={<AccountSettings />} />
- </Route>
- </Route>
- <Route path="/newAccount" element={<AccountCreationDialog />}>
- <Route path="jami" element={<JamiAccountDialog />} />
- </Route>
- {/* <Route path="/Contacts" element={<ContactList />} /> */}
- <Route path="/Theme" element={<ThemeDemonstrator />} />
- <Route path="/setup" element={<ServerSetup />} />
- <Route path="/" element={<Home />} />
- <Route path="*" element={<NotFoundPage />} />
- </Routes>
- {!state.auth.authenticated && <SignInPage key="signin" open={!state.auth.authenticated} />}
- </ThemeProvider>
- );
+ return <Outlet />;
};
export default App;
diff --git a/client/src/components/Input.tsx b/client/src/components/Input.tsx
index e867e26..ae640cf 100644
--- a/client/src/components/Input.tsx
+++ b/client/src/components/Input.tsx
@@ -88,6 +88,7 @@
</RulesDialog>
<TextField
{...props}
+ color={inputColor(props.error, props.success)}
label={'Choose an identifier'}
variant="standard"
InputLabelProps={{ shrink: !!(isSelected || input) }}
@@ -145,6 +146,7 @@
</RulesDialog>
<TextField
{...props}
+ color={inputColor(props.error, props.success)}
label="Password"
type={showPassword ? 'text' : 'password'}
variant="standard"
@@ -235,7 +237,7 @@
);
};
-export function inputColor(
+function inputColor(
error?: boolean,
success?: boolean
): 'success' | 'error' | 'primary' | 'secondary' | 'info' | 'warning' | undefined {
@@ -268,13 +270,13 @@
<ListItemIcon>
<GppMaybe />
</ListItemIcon>
- The password must contain at least one special character.
+ The password must contain at least 1 special character.
</ListItem>
<ListItem>
<ListItemIcon>
<GppMaybe />
</ListItemIcon>
- The password must be eight characters or longer for Strong strength.
+ The password must be 10 characters or longer to be considered strong.
</ListItem>
</List>
</Typography>
diff --git a/client/src/components/JamiWelcomeLogo.tsx b/client/src/components/JamiWelcomeLogo.tsx
index 8002aab..31311b6 100644
--- a/client/src/components/JamiWelcomeLogo.tsx
+++ b/client/src/components/JamiWelcomeLogo.tsx
@@ -15,31 +15,29 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
-import { Box, SxProps, Typography } from '@mui/material';
+import { Stack, StackProps, Typography } from '@mui/material';
import { ReactComponent as JamiLogo } from '../icons/jami-logo-icon.svg';
import { jamiLogoDefaultSize } from '../utils/constants';
-interface WelcomeLogoProps {
+interface WelcomeLogoProps extends StackProps {
logoWidth?: string;
logoHeight?: string;
- boxSxProps?: SxProps;
}
-export default function JamiWelcomeLogo(props: WelcomeLogoProps) {
+export default function JamiWelcomeLogo({ logoWidth, logoHeight, ...stackProps }: WelcomeLogoProps) {
return (
- <Box
+ <Stack
+ {...stackProps}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
- textAlign: 'center',
- flexDirection: 'column',
- ...props.boxSxProps,
+ ...stackProps.sx,
}}
>
- <JamiLogo width={props.logoWidth ?? jamiLogoDefaultSize} height={props.logoHeight ?? jamiLogoDefaultSize} />
+ <JamiLogo width={logoWidth ?? jamiLogoDefaultSize} height={logoHeight ?? jamiLogoDefaultSize} />
<Typography variant="h1">Welcome to Jami!</Typography>
- </Box>
+ </Stack>
);
}
diff --git a/client/src/index.scss b/client/src/index.scss
index a9b9681..25ce911 100644
--- a/client/src/index.scss
+++ b/client/src/index.scss
@@ -1,5 +1,6 @@
html,
-body {
+body,
+#root {
height: 100%;
margin: 0;
padding: 0;
diff --git a/client/src/index.tsx b/client/src/index.tsx
index 83c2ada..90aa841 100644
--- a/client/src/index.tsx
+++ b/client/src/index.tsx
@@ -19,17 +19,24 @@
import './index.scss';
import './i18n';
-// import config from "../sentry-client.config.json"
+import { ThemeProvider } from '@mui/material/styles';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
-import { BrowserRouter as Router } from 'react-router-dom';
+import { createBrowserRouter, createRoutesFromElements, Route, RouterProvider } from 'react-router-dom';
import socketio from 'socket.io-client';
import App from './App';
+import ContactList from './components/ContactList';
import { SocketProvider } from './contexts/Socket';
+import AccountSettings from './pages/AccountSettings';
+import Home from './pages/Home';
+import JamiMessenger from './pages/JamiMessenger';
+import ServerSetup from './pages/ServerSetup';
import { store } from './redux/store';
+import defaultTheme from './themes/Default';
+import { ThemeDemonstrator } from './themes/ThemeDemonstrator';
const queryClient = new QueryClient({
defaultOptions: {
@@ -41,6 +48,19 @@
const socket = socketio();
+const router = createBrowserRouter(
+ createRoutesFromElements(
+ <Route path="/" element={<App />}>
+ <Route index element={<Home />} />
+ <Route path="theme" element={<ThemeDemonstrator />} />
+ <Route path="account" element={<JamiMessenger />} />
+ <Route path="settings" element={<AccountSettings />} />
+ <Route path="contacts" element={<ContactList />} />
+ <Route path="setup" element={<ServerSetup />} />
+ </Route>
+ )
+);
+
const container = document.getElementById('root');
if (!container) {
throw new Error('Failed to get the root element');
@@ -51,9 +71,9 @@
<StrictMode>
<QueryClientProvider client={queryClient}>
<SocketProvider socket={socket}>
- <Router>
- <App />
- </Router>
+ <ThemeProvider theme={defaultTheme}>
+ <RouterProvider router={router} />
+ </ThemeProvider>
</SocketProvider>
</QueryClientProvider>
</StrictMode>
diff --git a/client/src/locale/en/translation.json b/client/src/locale/en/translation.json
index 9a3982f..b2a2935 100644
--- a/client/src/locale/en/translation.json
+++ b/client/src/locale/en/translation.json
@@ -21,5 +21,16 @@
"message_input_placeholder_two": "Write to {{member0}} and {{member1}}",
"message_input_placeholder_three": "Write to {{member0}}, {{member1}} and {{member2}}",
"message_input_placeholder_four": "Write to {{member0}}, {{member1}}, {{member2}}, +1 other member",
- "message_input_placeholder_more": "Write to {{member0}}, {{member1}}, {{member2}}, +{{excess}} other members"
+ "message_input_placeholder_more": "Write to {{member0}}, {{member1}}, {{member2}}, +{{excess}} other members",
+ "username_input_default_helper_text": "",
+ "username_input_success_helper_text": "Username available",
+ "username_input_taken_helper_text": "Username already taken",
+ "username_input_invalid_helper_text": "Username doesn't follow required pattern",
+ "username_input_registration_failed_helper_text": "Username not correct!",
+ "password_input_default_helper_text": "",
+ "password_input_too_weak_helper_text": "Too weak",
+ "password_input_weak_helper_text": "Weak",
+ "password_input_medium_helper_text": "Medium",
+ "password_input_strong_helper_text": "Strong",
+ "password_input_registration_failed_helper_text": "Choose another password!"
}
diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx
new file mode 100644
index 0000000..8a5e3d6
--- /dev/null
+++ b/client/src/pages/Home.tsx
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program. If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import { Box, Grid, Paper, useMediaQuery } from '@mui/material';
+import { Theme, useTheme } from '@mui/material/styles';
+import { useState } from 'react';
+
+import JamiWelcomeLogo from '../components/JamiWelcomeLogo';
+import JamiLogin from './JamiLogin';
+import JamiRegistration from './JamiRegistration';
+
+const borderRadius = 30;
+
+export default function Home() {
+ const theme: Theme = useTheme();
+ const [isRegistrationDisplayed, setIsRegistrationDisplayed] = useState<boolean>(false);
+
+ const child = !isRegistrationDisplayed ? (
+ <JamiLogin register={() => setIsRegistrationDisplayed(true)} />
+ ) : (
+ <JamiRegistration login={() => setIsRegistrationDisplayed(false)} />
+ );
+
+ const isDesktopOrLaptop: boolean = useMediaQuery(theme.breakpoints.up('md'));
+ const isMobile: boolean = useMediaQuery(theme.breakpoints.only('xs'));
+
+ return (
+ <Box
+ sx={{
+ width: '100%',
+ height: '100%',
+ display: 'flex',
+ backgroundColor: `${isDesktopOrLaptop ? theme.palette.primary.dark : 'white'}`,
+ }}
+ >
+ <Paper
+ elevation={10}
+ sx={{
+ width: '100%',
+ backgroundColor: 'white',
+ margin: `${isDesktopOrLaptop ? theme.typography.pxToRem(100) : 0}`,
+ borderRadius: `${isDesktopOrLaptop ? theme.typography.pxToRem(borderRadius) : 0}`,
+ }}
+ >
+ <Grid container spacing={0} sx={{ height: '100%' }}>
+ {!isMobile && (
+ <Grid
+ item
+ xs={6}
+ id="logo"
+ sx={{
+ height: '100%',
+ backgroundColor: theme.palette.secondary.main,
+ borderRadius: `${
+ isDesktopOrLaptop
+ ? `${theme.typography.pxToRem(borderRadius)} 0 0 ${theme.typography.pxToRem(borderRadius)}`
+ : 0
+ }`, // To follow paper border-radius
+ }}
+ >
+ <JamiWelcomeLogo logoWidth="90%" sx={{ height: '100%' }} />
+ </Grid>
+ )}
+ <Grid item xs={isMobile ? 12 : 6} sx={{ height: '100%' }}>
+ {isMobile && (
+ <JamiWelcomeLogo
+ logoWidth={theme.typography.pxToRem(100)}
+ logoHeight={theme.typography.pxToRem(100)}
+ sx={{ mt: theme.typography.pxToRem(30), mb: theme.typography.pxToRem(20) }}
+ />
+ )}
+ <Box className="home-child" sx={{ height: `${isMobile ? 'auto' : '100%'}` }}>
+ {child}
+ </Box>
+ </Grid>
+ </Grid>
+ </Paper>
+ </Box>
+ );
+}
diff --git a/client/src/pages/JamiAccountCreation.tsx b/client/src/pages/JamiAccountCreation.tsx
deleted file mode 100644
index da80533..0000000
--- a/client/src/pages/JamiAccountCreation.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2022 Savoir-faire Linux Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this program. If not, see
- * <https://www.gnu.org/licenses/>.
- */
-import { AddRounded } from '@mui/icons-material';
-import { Box, Card, CardActions, CardContent, Container, Fab, Typography } from '@mui/material';
-import { FormEvent, useState } from 'react';
-import { useNavigate } from 'react-router';
-
-import authManager from '../AuthManager';
-import UsernameChooser from '../components/UsernameChooser';
-
-export default function JamiAccountDialog() {
- const [name, setName] = useState('');
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(false);
- const navigate = useNavigate();
-
- const onSubmit = async (event: FormEvent) => {
- event.preventDefault();
- setLoading(true);
- const result = await authManager
- .fetch('/api/accounts', {
- method: 'POST',
- headers: {
- Accept: 'application/json',
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ registerName: name }),
- })
- .then((res) => res.json())
- .catch((error) => {
- setLoading(false);
- setError(error);
- });
- console.log(result);
- if (result && result.accountId) navigate(`/account/${result.accountId}/settings`);
- };
-
- return (
- <Container>
- <Card component="form" onSubmit={onSubmit}>
- <CardContent>
- <Typography gutterBottom variant="h5" component="h2">
- Create Jami account
- </Typography>
- <Typography variant="body2" color="textSecondary" component="p">
- Welcome to the Jami web node setup.
- <br />
- Let's start by creating a new administrator account to control access to the server configuration.
- </Typography>
-
- <Box>
- <UsernameChooser disabled={loading} setName={setName} />
- </Box>
- </CardContent>
- <CardActions>
- {error && <Typography color="error">Error: {JSON.stringify(error)}</Typography>}
- <Fab color="primary" type="submit" variant="extended" disabled={!name || loading}>
- <AddRounded />
- Register name
- </Fab>
- </CardActions>
- </Card>
- </Container>
- );
-}
diff --git a/client/src/pages/JamiLogin.tsx b/client/src/pages/JamiLogin.tsx
new file mode 100644
index 0000000..66fcf3e
--- /dev/null
+++ b/client/src/pages/JamiLogin.tsx
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program. If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import { Box, Button, Stack, Typography, useMediaQuery } from '@mui/material';
+import { Theme, useTheme } from '@mui/material/styles';
+import { ChangeEvent, FormEvent, MouseEvent, useState } from 'react';
+import { Form } from 'react-router-dom';
+
+import { PasswordInput, UsernameInput } from '../components/Input';
+import ProcessingRequest from '../components/ProcessingRequest';
+import { inputWidth } from '../utils/constants';
+
+type JamiLoginProps = {
+ register: () => void;
+};
+
+export default function JamiLogin(props: JamiLoginProps) {
+ const theme: Theme = useTheme();
+ const [username, setUsername] = useState<string>('');
+ const [password, setPassword] = useState<string>('');
+ const [isLoggingInUser, setIsLoggingInUser] = useState<boolean>(false);
+
+ const handleUsername = (event: ChangeEvent<HTMLInputElement>) => {
+ setUsername(event.target.value);
+ };
+
+ const handlePassword = (event: ChangeEvent<HTMLInputElement>) => {
+ setPassword(event.target.value);
+ };
+
+ const register = (event: MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ props.register();
+ };
+
+ const authenticateUser = async (event: FormEvent) => {
+ event.preventDefault();
+ if (username.length > 0 && password.length > 0) {
+ setIsLoggingInUser(true);
+
+ // TODO: Replace with login logic (https://git.jami.net/savoirfairelinux/jami-web/-/issues/75).
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+ console.log('Login');
+ setIsLoggingInUser(false);
+ }
+ };
+
+ const isMobile: boolean = useMediaQuery(theme.breakpoints.only('xs'));
+
+ return (
+ <>
+ <ProcessingRequest open={isLoggingInUser} />
+
+ <Stack
+ sx={{
+ minHeight: `${isMobile ? 'auto' : '100%'}`,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ }}
+ >
+ <Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(20) }}>
+ <Typography component={'span'} variant="h2">
+ LOGIN
+ </Typography>
+ </Box>
+
+ <Form method="post" id="login-form">
+ <div>
+ <UsernameInput
+ onChange={handleUsername}
+ tooltipTitle={'The username you registered with'}
+ sx={{ width: theme.typography.pxToRem(inputWidth) }}
+ />
+ </div>
+ <div>
+ <PasswordInput
+ onChange={handlePassword}
+ tooltipTitle={'The password you registered with'}
+ sx={{ width: theme.typography.pxToRem(inputWidth) }}
+ />
+ </div>
+
+ <Button
+ variant="contained"
+ type="submit"
+ onClick={authenticateUser}
+ sx={{ width: theme.typography.pxToRem(inputWidth), mt: theme.typography.pxToRem(20) }}
+ >
+ LOG IN
+ </Button>
+ </Form>
+
+ <Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(50) }}>
+ <Typography variant="body1">
+ Need an account ?
+ <a href="" onClick={register}>
+ REGISTER
+ </a>
+ </Typography>
+ </Box>
+ </Stack>
+ </>
+ );
+}
diff --git a/client/src/pages/JamiRegistration.tsx b/client/src/pages/JamiRegistration.tsx
new file mode 100644
index 0000000..ebfc5fd
--- /dev/null
+++ b/client/src/pages/JamiRegistration.tsx
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program. If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import { Box, Button, Stack, Typography, useMediaQuery } from '@mui/material';
+import { Theme, useTheme } from '@mui/material/styles';
+import { ChangeEvent, FormEvent, MouseEvent, useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Form } from 'react-router-dom';
+
+import { PasswordInput, UsernameInput } from '../components/Input';
+import ProcessingRequest from '../components/ProcessingRequest';
+import { checkPasswordStrength, isNameRegistered, StrengthValueCode } from '../utils/auth';
+import { inputWidth, jamiUsernamePattern } from '../utils/constants';
+
+const usernameTooltipTitle =
+ 'Choose a password hard to guess for others but easy to remember for you, ' +
+ 'it must be at least 8 characters. ' +
+ "Your account won't be recovered if you forget it!\n\n" +
+ 'Click for more details';
+
+const passwordTooltipTitle =
+ 'Username may be from 3 to 32 chraracters long and contain a-z, A-Z, -, _\n\n' + 'Click for more details';
+
+type NameStatus = 'default' | 'success' | 'taken' | 'invalid' | 'registration_failed';
+type PasswordStatus = StrengthValueCode | 'registration_failed';
+
+type JamiRegistrationProps = {
+ login: () => void;
+};
+
+export default function JamiRegistration(props: JamiRegistrationProps) {
+ const theme: Theme = useTheme();
+ const { t } = useTranslation();
+
+ const [isCreatingUser, setIsCreatingUser] = useState(false);
+ const [usernameValue, setUsernameValue] = useState('');
+ const [passwordValue, setPasswordValue] = useState('');
+ const [usernameStatus, setUsernameStatus] = useState<NameStatus>('default');
+ const [passwordStatus, setPasswordStatus] = useState<PasswordStatus>('default');
+
+ const usernameError = usernameStatus !== 'success' && usernameStatus !== 'default';
+ const usernameSuccess = usernameStatus === 'success';
+ const passwordError = passwordStatus !== 'strong' && passwordStatus !== 'default';
+ const passwordSuccess = passwordStatus === 'strong';
+
+ useEffect(() => {
+ // To prevent lookup if field is empty, in error state or lookup already done
+ if (usernameValue.length > 0 && usernameStatus === 'default') {
+ const validateUsername = async () => {
+ if (await isNameRegistered(usernameValue)) {
+ setUsernameStatus('taken');
+ } else {
+ setUsernameStatus('success');
+ }
+ };
+ const timeout = setTimeout(validateUsername, 1000);
+
+ return () => clearTimeout(timeout);
+ }
+ }, [usernameValue, usernameStatus]);
+
+ const handleUsername = async (event: ChangeEvent<HTMLInputElement>) => {
+ const username: string = event.target.value;
+ setUsernameValue(username);
+
+ if (username.length > 0 && !jamiUsernamePattern.test(username)) {
+ setUsernameStatus('invalid');
+ } else {
+ setUsernameStatus('default');
+ }
+ };
+
+ const handlePassword = (event: ChangeEvent<HTMLInputElement>) => {
+ const password: string = event.target.value;
+ setPasswordValue(password);
+
+ if (password.length > 0) {
+ const checkResult = checkPasswordStrength(password);
+ setPasswordStatus(checkResult.valueCode);
+ } else {
+ setPasswordStatus('default');
+ }
+ };
+
+ const login = (event: MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ props.login();
+ };
+
+ const handleSubmit = async (event: FormEvent) => {
+ event.preventDefault();
+ const canCreate = usernameSuccess && passwordSuccess;
+
+ if (canCreate) {
+ setIsCreatingUser(true);
+ // TODO: Replace with registration logic (https://git.jami.net/savoirfairelinux/jami-web/-/issues/75).
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+ console.log('Account created');
+ setIsCreatingUser(false);
+ } else {
+ if (usernameError || usernameValue.length === 0) {
+ setUsernameStatus('registration_failed');
+ }
+ if (!passwordSuccess) {
+ setPasswordStatus('registration_failed');
+ }
+ }
+ };
+
+ const isMobile: boolean = useMediaQuery(theme.breakpoints.only('xs'));
+
+ return (
+ <>
+ <ProcessingRequest open={isCreatingUser} />
+
+ <Stack
+ sx={{
+ minHeight: `${isMobile ? 'auto' : '100%'}`,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ }}
+ >
+ <Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(20) }}>
+ <Typography component={'span'} variant="h2">
+ REGISTRATION
+ </Typography>
+ </Box>
+
+ <Form method="post" id="register-form">
+ <div>
+ <UsernameInput
+ value={usernameValue}
+ onChange={handleUsername}
+ error={usernameError}
+ success={usernameSuccess}
+ helperText={t(`username_input_${usernameStatus}_helper_text`)}
+ sx={{ width: theme.typography.pxToRem(inputWidth) }}
+ tooltipTitle={usernameTooltipTitle}
+ />
+ </div>
+ <div>
+ <PasswordInput
+ value={passwordValue}
+ onChange={handlePassword}
+ error={passwordError}
+ success={passwordSuccess}
+ helperText={t(`password_input_${passwordStatus}_helper_text`)}
+ sx={{ width: theme.typography.pxToRem(inputWidth) }}
+ tooltipTitle={passwordTooltipTitle}
+ />
+ </div>
+
+ <Button
+ variant="contained"
+ type="submit"
+ onClick={handleSubmit}
+ sx={{ width: theme.typography.pxToRem(inputWidth), mt: theme.typography.pxToRem(20) }}
+ >
+ REGISTER
+ </Button>
+ </Form>
+
+ <Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(50) }}>
+ <Typography variant="body1">
+ Already have an account ?
+ <a href="" onClick={login}>
+ LOG IN
+ </a>
+ </Typography>
+ </Box>
+ </Stack>
+ </>
+ );
+}
diff --git a/client/src/pages/LoginDialog.tsx b/client/src/pages/LoginDialog.tsx
deleted file mode 100644
index ee990bc..0000000
--- a/client/src/pages/LoginDialog.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * Copyright (C) 2022 Savoir-faire Linux Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this program. If not, see
- * <https://www.gnu.org/licenses/>.
- */
-import Button from '@mui/material/Button';
-import Checkbox from '@mui/material/Checkbox';
-import Dialog from '@mui/material/Dialog';
-import DialogActions from '@mui/material/DialogActions';
-import DialogContent from '@mui/material/DialogContent';
-import DialogTitle from '@mui/material/DialogTitle';
-import FormControlLabel from '@mui/material/FormControlLabel';
-import TextField from '@mui/material/TextField';
-import { ChangeEvent, Component, MouseEvent } from 'react';
-
-import authManager from '../AuthManager';
-
-type SignInPageProps = {
- open: boolean;
-};
-type SignInPageState = {
- username?: string;
- password?: string;
- submitted?: boolean;
- loading?: boolean;
- redirect?: boolean;
- error?: boolean;
- open?: boolean;
- errorMessage?: string;
-};
-
-class SignInPage extends Component<SignInPageProps, SignInPageState> {
- constructor(props: SignInPageProps) {
- console.log('SignInPage ' + props.open);
- super(props);
- this.state = {
- submitted: false,
- loading: false,
- };
- this.handleSubmit = this.handleSubmit.bind(this);
- this.localLogin = this.localLogin.bind(this);
- }
-
- handleusername(text: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
- this.setState({ username: text.target.value });
- }
-
- handlePassword(text: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
- this.setState({ password: text.target.value });
- }
-
- localLogin() {
- this.setState({
- submitted: true,
- loading: true,
- });
- authManager.authenticate('admin', 'admin');
- /*fetch('/api/localLogin?username=none&password=none', {
- header: { "Content-Type": "application/json" },
- method: "POST",
- credentials: 'same-origin'
- //body: JSON.stringify({ obj })
- })
- .then((res) => {
- if (res.status === '200') {
- this.setState({
- redirect: true
- });
- } else if (res.status === '401') {
- this.setState({
- loading: false,
- error: true,
- open: true,
- errorMessage: "Wrong credentials! Your are not allowed to connect"
- })
- }
- //this.setState({ session: res });
- }).catch((e) => {
- this.setState({
- loading: false,
- error: true,
- open: true,
- errorMessage: e.toString()
- })
- })*/
- }
-
- handleSubmit(event: MouseEvent<HTMLButtonElement>) {
- event.preventDefault();
- const obj = {
- username: this.state.username,
- password: this.state.password,
- };
-
- this.setState({
- submitted: true,
- loading: true,
- });
-
- fetch('/api/login?username=' + obj.username + '&password=' + obj.password, {
- headers: {
- 'Content-Type': 'application/json',
- },
- method: 'POST',
- credentials: 'same-origin',
- //body: JSON.stringify({ obj })
- })
- .then((res) => {
- if (res.status === 200) {
- this.setState({
- redirect: true,
- });
- } else if (res.status === 401) {
- this.setState({
- loading: false,
- error: true,
- open: true,
- errorMessage: 'Wrong credentials! Your are not allowed to connect',
- });
- }
- //this.setState({ session: res });
- })
- .catch((e) => {
- this.setState({
- loading: false,
- error: true,
- open: true,
- errorMessage: e.toString(),
- });
- });
- }
-
- render() {
- console.log('SignInPage render ' + this.props.open);
- return (
- <Dialog open={this.props.open}>
- <DialogTitle>Se connecter</DialogTitle>
- <DialogContent>
- <Button
- type="submit"
- fullWidth
- variant="contained"
- color="primary"
- className="" /*{classes.submit}*/
- onClick={() => {
- this.localLogin();
- }}
- >
- Compte local
- </Button>
- <TextField
- variant="outlined"
- margin="normal"
- required
- fullWidth
- id="username"
- label="LDAP Savoir-faire Linux"
- name="username"
- autoComplete="email"
- autoFocus
- onChange={(text) => {
- this.handleusername(text);
- }}
- />
- <TextField
- variant="outlined"
- margin="normal"
- required
- fullWidth
- name="password"
- label="Mot de passe"
- type="password"
- id="password"
- autoComplete="current-password"
- onChange={(text) => {
- this.handlePassword(text);
- }}
- />
- <FormControlLabel control={<Checkbox value="remember" color="primary" />} label="Se rapeller de moi" />
- </DialogContent>
-
- <DialogActions>
- <Button type="submit" size="medium" onClick={this.handleSubmit}>
- Se connecter
- </Button>
- </DialogActions>
- </Dialog>
- );
- }
-}
-
-export default SignInPage;
diff --git a/client/src/themes/Default.ts b/client/src/themes/Default.ts
index c0f5884..60fb1ec 100644
--- a/client/src/themes/Default.ts
+++ b/client/src/themes/Default.ts
@@ -46,6 +46,9 @@
success: {
main: '#009980',
},
+ secondary: {
+ main: '#A3C2DA',
+ },
},
InfoTooltip: {
backgroundColor: {
diff --git a/client/src/utils/auth.ts b/client/src/utils/auth.ts
index 8d4d863..98ecdba 100644
--- a/client/src/utils/auth.ts
+++ b/client/src/utils/auth.ts
@@ -29,9 +29,13 @@
export interface PasswordCheckResult {
strong: boolean;
- value: string;
+ valueCode: StrengthValueCode;
}
+export type StrengthValueCode = 'default' | 'too_weak' | 'weak' | 'medium' | 'strong';
+
+const idToStrengthValueCode: StrengthValueCode[] = ['too_weak', 'weak', 'medium', 'strong'];
+
// TODO: Find a way to do it differently or remove this check from account creation.
// It doesn't work if the server has secured this path, so I tweaked the server for test.
// The tweak is to remove secured of apiRouter middleware in the server (app.ts).
@@ -41,8 +45,10 @@
if (response.status === HttpStatusCode.Ok) {
const data: LookupResolveValue = await response.json();
return data.name === name;
+ } else if (response.status === HttpStatusCode.NotFound) {
+ return false;
}
- return false;
+ return true;
} catch (err) {
return true;
}
@@ -53,7 +59,7 @@
const checkResult: PasswordCheckResult = {
strong: strengthResult.id === PasswordStrength.Strong.valueOf(),
- value: strengthResult.value,
+ valueCode: idToStrengthValueCode[strengthResult.id] ?? 'default',
};
return checkResult;
diff --git a/client/src/utils/constants.ts b/client/src/utils/constants.ts
index 06d1b92..341622f 100644
--- a/client/src/utils/constants.ts
+++ b/client/src/utils/constants.ts
@@ -15,7 +15,7 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
-export const jamiUsernamePattern = '^[a-zA-Z0-9-_]{3,32}$';
+export const jamiUsernamePattern = /^[a-zA-Z0-9-_]{3,32}$/;
export const inputWidth = 260;