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/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>
+ );
+}