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