Migrate server setup functionality
Changes:
- Check if admin is set up
- Redirect to admin setup if not already done
- Create admin password
- Login as admin and get access token for future requests
- Add a router for setup
- Middleware to prevent any route from being accessible on the server till the admin setup is done
GitLab: #80
GitLab: #73
Change-Id: I8b7ecab68f6b4d5c6313ce2e72a4ae4fdef9eda0
diff --git a/client/src/pages/ServerSetup.tsx b/client/src/pages/ServerSetup.tsx
deleted file mode 100644
index e6ff0aa..0000000
--- a/client/src/pages/ServerSetup.tsx
+++ /dev/null
@@ -1,88 +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 GroupAddRounded from '@mui/icons-material/GroupAddRounded';
-import { Box, Card, CardContent, Container, Fab, Input, Typography } from '@mui/material';
-import { FormEvent, useState } from 'react';
-
-export default function ServerSetup() {
- const [password, setPassword] = useState('');
- const [passwordRepeat, setPasswordRepeat] = useState('');
- const [loading, setLoading] = useState(false);
-
- const isValid = () => password && password === passwordRepeat;
-
- const handleSubmit = (e: FormEvent) => {
- e.preventDefault();
- setLoading(true);
- if (!isValid()) return;
- // TODO: Migrate to new server
- // authManager.setup(password);
- };
-
- return (
- <Container className="message-list">
- <Card>
- <CardContent component="form" onSubmit={handleSubmit}>
- <Typography gutterBottom variant="h5" component="h2">
- Jami Web Node setup
- </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 style={{ textAlign: 'center', marginTop: 8, marginBottom: 16 }}>
- <div>
- <Input value="admin" name="username" autoComplete="username" disabled />
- </div>
- <div>
- <Input
- value={password}
- onChange={(e) => setPassword(e.target.value)}
- name="password"
- type="password"
- placeholder="New password"
- autoComplete="new-password"
- disabled={loading}
- />
- </div>
- <div>
- <Input
- value={passwordRepeat}
- onChange={(e) => setPasswordRepeat(e.target.value)}
- name="password"
- error={!!passwordRepeat && !isValid()}
- type="password"
- placeholder="Repeat password"
- autoComplete="new-password"
- disabled={loading}
- />
- </div>
- </Box>
- <Box style={{ textAlign: 'center', marginTop: 24 }}>
- <Fab variant="extended" color="primary" type="submit" disabled={!isValid() || loading}>
- <GroupAddRounded />
- Create admin account
- </Fab>
- </Box>
- </CardContent>
- </Card>
- </Container>
- );
-}
diff --git a/client/src/pages/Setup.tsx b/client/src/pages/Setup.tsx
new file mode 100644
index 0000000..28aee3b
--- /dev/null
+++ b/client/src/pages/Setup.tsx
@@ -0,0 +1,46 @@
+/*
+ * 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';
+import { useTranslation } from 'react-i18next';
+import { Navigate, useNavigate } from 'react-router-dom';
+
+import LoadingPage from '../components/Loading';
+
+export default function Setup() {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+
+ const accessToken = localStorage.getItem('adminAccessToken');
+
+ const adminLogout = () => {
+ localStorage.removeItem('adminAccessToken');
+ navigate('/login');
+ };
+
+ if (!accessToken) {
+ return <Navigate to="/login" replace />;
+ }
+ return (
+ <>
+ <Button variant="contained" type="submit" onClick={adminLogout}>
+ {t('logout')}
+ </Button>
+ <LoadingPage />
+ </>
+ );
+}
diff --git a/client/src/pages/SetupLogin.tsx b/client/src/pages/SetupLogin.tsx
new file mode 100644
index 0000000..79079be
--- /dev/null
+++ b/client/src/pages/SetupLogin.tsx
@@ -0,0 +1,164 @@
+/*
+ * 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 GroupAddRounded from '@mui/icons-material/GroupAddRounded';
+import { Box, Card, CardContent, Container, Fab, Input, Typography } from '@mui/material';
+import { HttpStatusCode } from 'jami-web-common';
+import { FormEvent, useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useNavigate } from 'react-router-dom';
+
+import { checkSetupStatus } from '../App';
+import { apiUrl } from '../utils/constants';
+
+export default function SetupLogin() {
+ const [isSetupComplete, setIsSetupComplete] = useState(false);
+ const [password, setPassword] = useState('');
+ const [passwordRepeat, setPasswordRepeat] = useState('');
+ const [loading, setLoading] = useState(false);
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ checkSetupStatus().then(setIsSetupComplete);
+ }, []);
+
+ const adminCreation = async (password: string) => {
+ const url = new URL('/setup/admin/create', apiUrl);
+
+ let response: Response;
+ try {
+ response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ password }),
+ });
+ } catch (e) {
+ throw new Error(`Admin creation failed`);
+ }
+
+ if (response.status !== HttpStatusCode.Created) {
+ throw new Error('Admin creation failed');
+ }
+ };
+
+ const adminLogin = async (password: string) => {
+ const url = new URL('/setup/admin/login', apiUrl);
+
+ let response: Response;
+ try {
+ response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ password }),
+ });
+ } catch (err) {
+ throw new Error(`Admin login failed`);
+ }
+
+ if (response.status === HttpStatusCode.Forbidden) {
+ throw new Error('Invalid password');
+ }
+
+ if (response.status !== HttpStatusCode.Ok) {
+ throw new Error('Admin login failed');
+ }
+
+ const data: { accessToken: string } = await response.json();
+ localStorage.setItem('adminAccessToken', data.accessToken);
+ };
+
+ const isValid = isSetupComplete || (password && password === passwordRepeat);
+
+ const handleSubmit = async (e: FormEvent) => {
+ e.preventDefault();
+ setLoading(true);
+ if (!isValid) return;
+
+ try {
+ if (!isSetupComplete) {
+ await adminCreation(password);
+ }
+ await adminLogin(password);
+ } catch (e) {
+ console.error(e);
+ navigate('/login');
+ return;
+ }
+ navigate('/setup');
+ };
+
+ return (
+ <Container className="message-list">
+ <Card>
+ <CardContent component="form" onSubmit={handleSubmit}>
+ <Typography gutterBottom variant="h5" component="h2">
+ {t('setup_login_title')}
+ </Typography>
+ <Typography variant="body2" color="textSecondary" component="p">
+ {t('setup_login_welcome')}
+ <br />
+ {isSetupComplete ? '' : t('setup_login_admin_creation')}
+ </Typography>
+
+ <Box style={{ textAlign: 'center', marginTop: 8, marginBottom: 16 }}>
+ <div>
+ <Input value="admin" name="username" autoComplete="username" disabled />
+ </div>
+ <div>
+ <Input
+ value={password}
+ onChange={(e) => setPassword(e.target.value)}
+ name="password"
+ type="password"
+ placeholder={
+ isSetupComplete ? t('password_placeholder') : t('setup_login_password_placeholder_creation')
+ }
+ autoComplete="new-password"
+ disabled={loading}
+ />
+ </div>
+ {!isSetupComplete && (
+ <div>
+ <Input
+ value={passwordRepeat}
+ onChange={(e) => setPasswordRepeat(e.target.value)}
+ name="password"
+ error={!!passwordRepeat && !isValid}
+ type="password"
+ placeholder={t('setup_login_password_placeholder_repeat')}
+ autoComplete="new-password"
+ disabled={loading}
+ />
+ </div>
+ )}
+ </Box>
+ <Box style={{ textAlign: 'center', marginTop: 24 }}>
+ <Fab variant="extended" color="primary" type="submit" disabled={!isValid || loading}>
+ <GroupAddRounded />
+ {isSetupComplete ? t('login_form_submit_button') : t('admin_creation_submit_button')}
+ </Fab>
+ </Box>
+ </CardContent>
+ </Card>
+ </Container>
+ );
+}