From 354c57a040f456492279df877e7d6b8efd05b919 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?L=C3=A9o?= <leopold.chappuis@savoirfairelinux.com>
Date: Mon, 10 Feb 2025 15:16:17 -0500
Subject: [PATCH] admin-panel: UI refactor with layout wrapper and enhancements

Introduced a layout wrapper for the settings page.
 Improved overall UI for better user experience.

Change-Id: I60c483c3f2b8aca923b2ef40c8ab4c4cb677f6c2
---
 .../AdminAccountConfiguration.tsx             | 130 +++++++++---------
 .../AdminSettings/ConfigureOpenId.tsx         |  55 ++++++--
 .../AdminSettings/DownloadLimit.tsx           |  30 ++--
 .../AdminSettings/GuestAuthMethod.tsx         |  53 ++++---
 .../AdminSettings/JamsAuthMethod.tsx          | 109 +++++++--------
 .../AdminSettings/LocalAuthMethod.tsx         |  77 +++++------
 .../AdminSettings/OpenIdAuthMethod.tsx        |  45 +-----
 client/src/components/LayoutSettings.tsx      |  64 +++++++++
 server/locale/en/translation.json             |  13 +-
 server/src/routers/admin-router.ts            |   9 ++
 server/src/storage/admin-account.ts           |   4 +
 11 files changed, 315 insertions(+), 274 deletions(-)
 create mode 100644 client/src/components/LayoutSettings.tsx

diff --git a/client/src/components/AdminSettings/AdminAccountConfiguration.tsx b/client/src/components/AdminSettings/AdminAccountConfiguration.tsx
index 669e6473..03bb1e75 100644
--- a/client/src/components/AdminSettings/AdminAccountConfiguration.tsx
+++ b/client/src/components/AdminSettings/AdminAccountConfiguration.tsx
@@ -15,12 +15,13 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { Box, Button, Input, Typography } from '@mui/material'
+import { Box, Button, Input } from '@mui/material'
 import { AnimatePresence, motion } from 'framer-motion'
 import { useState } from 'react'
 import { useTranslation } from 'react-i18next'
 
 import { useUpdateAdminPasswordMutation } from '../../services/adminQueries'
+import LayoutSettings from '../LayoutSettings'
 
 export default function AuthMethods() {
   const { t } = useTranslation()
@@ -59,68 +60,71 @@ export default function AuthMethods() {
   }
 
   return (
-    <Box
-      sx={{ marginTop: '10%', overflow: 'hidden', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}
-    >
-      <Typography variant="h5" sx={{ display: 'flex', flexDirection: 'row', justifyContent: 'center' }}>
-        {' '}
-        {t('setting_change_admin_password')}{' '}
-      </Typography>
-      <AnimatePresence>
-        <motion.div
-          style={{
-            display: 'flex',
-            flexDirection: 'row',
-            justifyContent: 'center',
-            alignContent: 'center',
-            marginTop: '2%',
-          }}
-          initial={{ x: -20 }}
-          animate={{ x: 0 }}
-          exit={{ x: 20 }}
-          transition={{ duration: 0.5 }}
-        >
-          <Box sx={{ display: 'flex', flexDirection: 'column' }}>
-            <Input
-              type="password"
-              value={oldPassword}
-              onChange={onOldPasswordChange}
-              placeholder={t('admin_page_password_old_placeholder')}
-            ></Input>
-            <Input
-              type="password"
-              value={newPassword}
-              onChange={onNewPasswordChange}
-              placeholder={t('admin_page_password_placeholder')}
-            ></Input>
-            {newPassword !== repeatPassword && newPassword !== '' && repeatPassword !== '' && (
-              <Box sx={{ fontSize: '11px', color: '#CC0022' }}>{t('password_input_helper_text_not_match')}</Box>
-            )}
-            <Input
-              type="password"
-              value={repeatPassword}
-              onChange={onRepeatPasswordChange}
-              placeholder={t('admin_page_password_repeat_placeholder')}
-            ></Input>
-            <Box sx={{ display: 'flex', flexDirection: 'row', justifyContent: 'center', marginTop: '10%' }}>
-              <Button
-                variant="contained"
-                type="submit"
-                onClick={validatePasswordChange}
-                disabled={
-                  oldPassword === '' ||
-                  newPassword === '' ||
-                  repeatPassword === '' ||
-                  newPassword !== repeatPassword ||
-                  oldPassword === newPassword
-                }
-              >
-                {t('admin_page_password_submit')}
-              </Button>
+    <LayoutSettings title={t('setting_change_admin_password')} text={t('setting_change_admin_password_text')}>
+      <Box
+        sx={{
+          overflow: 'hidden',
+          display: 'flex',
+          flexDirection: 'column',
+          justifyContent: 'center',
+          marginTop: '-20px',
+        }}
+      >
+        <AnimatePresence>
+          <motion.div
+            style={{
+              display: 'flex',
+              flexDirection: 'row',
+              justifyContent: 'center',
+              alignContent: 'center',
+            }}
+            initial={{ x: -20 }}
+            animate={{ x: 0 }}
+            exit={{ x: 20 }}
+            transition={{ duration: 0.5 }}
+          >
+            <Box sx={{ display: 'flex', flexDirection: 'column' }}>
+              <Input
+                type="password"
+                value={oldPassword}
+                onChange={onOldPasswordChange}
+                placeholder={t('admin_page_password_old_placeholder')}
+              ></Input>
+              <Input
+                type="password"
+                value={newPassword}
+                onChange={onNewPasswordChange}
+                placeholder={t('admin_page_password_placeholder')}
+              ></Input>
+              {newPassword !== repeatPassword && newPassword !== '' && repeatPassword !== '' && (
+                <Box sx={{ fontSize: '11px', color: '#CC0022' }}>{t('password_input_helper_text_not_match')}</Box>
+              )}
+              <Input
+                type="password"
+                value={repeatPassword}
+                onChange={onRepeatPasswordChange}
+                placeholder={t('admin_page_password_repeat_placeholder')}
+              ></Input>
+              <Box sx={{ display: 'flex', flexDirection: 'row', justifyContent: 'center', marginTop: '10%' }}>
+                <Button
+                  variant="contained"
+                  type="submit"
+                  onClick={validatePasswordChange}
+                  disabled={
+                    oldPassword === '' ||
+                    newPassword === '' ||
+                    repeatPassword === '' ||
+                    newPassword !== repeatPassword ||
+                    oldPassword === newPassword
+                  }
+                >
+                  {t('admin_page_password_submit')}
+                </Button>
+              </Box>
             </Box>
-          </Box>
-        </motion.div>
-      </AnimatePresence>
-    </Box>
+          </motion.div>
+        </AnimatePresence>
+      </Box>
+    </LayoutSettings>
   )
 }
diff --git a/client/src/components/AdminSettings/ConfigureOpenId.tsx b/client/src/components/AdminSettings/ConfigureOpenId.tsx
index f54dcec9..345aadda 100644
--- a/client/src/components/AdminSettings/ConfigureOpenId.tsx
+++ b/client/src/components/AdminSettings/ConfigureOpenId.tsx
@@ -17,6 +17,7 @@
  */
 
 import ContentCopyIcon from '@mui/icons-material/ContentCopy'
+import DescriptionIcon from '@mui/icons-material/Description'
 import {
   Box,
   Button,
@@ -28,6 +29,8 @@ import {
   Input,
   Switch,
   Typography,
+  useMediaQuery,
+  useTheme,
 } from '@mui/material'
 import { AdminSettingSelection } from 'jami-web-common'
 import { useContext, useState } from 'react'
@@ -46,6 +49,8 @@ export default function ConfigureOpenId() {
   const { setAlertContent } = useContext(AlertSnackbarContext)
   const [clientId, setClientId] = useState<string>()
   const [clientSecret, setClientSecret] = useState('')
+  const theme = useTheme()
+  const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
 
   const providerName = location.pathname.split('/').at(-1)
 
@@ -129,22 +134,35 @@ export default function ConfigureOpenId() {
     <Box
       sx={{ display: 'flex', height: '100%', justifyContent: 'center', alignItems: 'center', alignContent: 'center' }}
     >
-      <Card sx={{ width: '420px', height: '580px', marginTop: '-50px' }}>
+      <Card
+        sx={{
+          width: '420px',
+          height: '580px',
+          marginTop: '-50px',
+          boxShadow: isMobile ? 'none' : undefined,
+          border: isMobile ? 'none' : undefined,
+        }}
+      >
         <CardHeader
           title={
-            <Typography sx={{ marginLeft: '-8px' }} variant="h3">
-              {provider.data.displayName}
-            </Typography>
-          }
-          subheader={
-            <Box sx={{ display: 'flex', flexDirection: 'column' }}>
-              {'OAuth2 Provider'}
-              <a href={provider.data.documentation} target="_blank" rel="noopener noreferrer">
-                {'Documentation'}
-              </a>
+            <Box
+              sx={{
+                display: 'flex',
+                flexDirection: 'row',
+                justifyContent: 'flex-start',
+                alignContent: 'center',
+                alignItems: 'center',
+                gap: '10px',
+              }}
+            >
+              <Typography sx={{ marginLeft: '-8px' }} variant="h3">
+                {provider.data.displayName}
+              </Typography>
+              <img src={provider.data.icon} style={{ width: '40px', height: '40px', marginTop: '5px' }} />
             </Box>
           }
-        ></CardHeader>
+          subheader={<Box sx={{ display: 'flex', flexDirection: 'column' }}>{t('oauth2_provider')}</Box>}
+        />
         <CardContent sx={{ display: 'flex', flexDirection: 'column', marginTop: '-16px' }}>
           <Box sx={{ marginBottom: '24px' }}>
             <Typography>{t('configure_oauth_provider_info')}</Typography>
@@ -161,7 +179,7 @@ export default function ConfigureOpenId() {
             }}
           >
             <span style={{ paddingRight: '10%' }}>
-              {t('setting_auth_change', { authMethod: AuthenticationMethods.OPENID })}
+              {t('setting_auth_change', { authMethod: provider.data.displayName })}
             </span>
             <FormGroup style={{ alignItems: 'center' }}>
               <FormControlLabel
@@ -204,9 +222,16 @@ export default function ConfigureOpenId() {
             ></Input>
             <ContentCopyIcon sx={{ ...contentCopyIconStyle, visibility: 'hidden' }} />
           </Box>
-          <Box sx={{ width: '100%', display: 'flex', justifyContent: 'flex-end', marginTop: '4px' }}>
+          <Box sx={{ width: '100%', display: 'flex', justifyContent: 'space-between', marginTop: '20px' }}>
+            <Button
+              color="primary"
+              onClick={() => window.open(provider.data.documentation, '_blank', 'noopener,noreferrer')}
+            >
+              <DescriptionIcon />
+              {t('documentation')}
+            </Button>
             <Button variant="contained" color="primary" onClick={onValidate}>
-              {t('submit')}
+              {t('save_button')}
             </Button>
           </Box>
         </CardContent>
diff --git a/client/src/components/AdminSettings/DownloadLimit.tsx b/client/src/components/AdminSettings/DownloadLimit.tsx
index 04732b35..e5d4f61a 100644
--- a/client/src/components/AdminSettings/DownloadLimit.tsx
+++ b/client/src/components/AdminSettings/DownloadLimit.tsx
@@ -16,13 +16,14 @@
  * <https://www.gnu.org/licenses/>.
  */
 
-import { Box, Button, Input, Typography, useMediaQuery, useTheme } from '@mui/material'
+import { Box, Button, Input } from '@mui/material'
 import { useContext, useEffect, useState } from 'react'
 import { useTranslation } from 'react-i18next'
 
 import { AlertSnackbarContext } from '../../contexts/AlertSnackbarProvider'
 import { useSetAutoDownloadLimit } from '../../services/adminQueries'
 import { useGetAutoDownloadLimit } from '../../services/dataTransferQueries'
+import LayoutSettings from '../LayoutSettings'
 import ProcessingRequest from '../ProcessingRequest'
 
 export default function DownloadLimit() {
@@ -31,8 +32,6 @@ export default function DownloadLimit() {
   const { setAlertContent } = useContext(AlertSnackbarContext)
   const setAutoDownloadLimitMutation = useSetAutoDownloadLimit()
   const { t } = useTranslation()
-  const theme = useTheme()
-  const isMobile = useMediaQuery(theme.breakpoints.down('md'))
 
   const configuredLimit = limitData.data
 
@@ -70,27 +69,14 @@ export default function DownloadLimit() {
   }
 
   return (
-    <Box
-      sx={{
-        display: 'flex',
-        flexDirection: 'column',
-        width: '100%',
-        justifyContent: 'center',
-        marginTop: '5%',
-        alignItems: 'center',
-      }}
-    >
-      <Typography variant="h3" sx={{ textAlign: 'center' }}>
-        {t('download_limit')}
-      </Typography>
-      <Typography variant="body2" sx={{ width: isMobile ? '300px' : '500px', pt: '1%', pb: '1%' }}>
-        {t('download_limit_details')}
-      </Typography>
-      <Box>
+    <LayoutSettings title={t('download_limit')} text={t('download_limit_details')}>
+      <Box sx={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', gap: '10px' }}>
         {' '}
         <Input type="number" inputProps={{ min: '0' }} value={limit} onChange={handleLimitChange}></Input>
-        <Button onClick={onSave}>{t('save_button')}</Button>
+        <Button variant="contained" onClick={onSave}>
+          {t('save_button')}
+        </Button>
       </Box>
-    </Box>
+    </LayoutSettings>
   )
 }
diff --git a/client/src/components/AdminSettings/GuestAuthMethod.tsx b/client/src/components/AdminSettings/GuestAuthMethod.tsx
index 88a2073b..e37649a9 100644
--- a/client/src/components/AdminSettings/GuestAuthMethod.tsx
+++ b/client/src/components/AdminSettings/GuestAuthMethod.tsx
@@ -16,11 +16,11 @@
  * <https://www.gnu.org/licenses/>.
  */
 
-import { Box, FormControl, FormControlLabel, FormGroup, Switch, Typography } from '@mui/material'
+import { Box, FormControl, FormControlLabel, FormGroup, Switch } from '@mui/material'
 import { useTranslation } from 'react-i18next'
 
-import { AuthenticationMethods } from '../../enums/authenticationMethods'
 import { useGetAdminConfigQuery, useUpdateAdminConfigMutation } from '../../services/adminQueries'
+import LayoutSettings from '../LayoutSettings'
 import ProcessingRequest from '../ProcessingRequest'
 
 export default function GuestAuthMethod() {
@@ -38,33 +38,30 @@ export default function GuestAuthMethod() {
   }
 
   return (
-    <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '80%', width: '100%' }}>
-      <Box sx={{ display: 'flex', flexDirection: 'column', width: '300px', gap: '20px' }}>
-        <Box>
-          <Typography variant="h6">{t('guest_authentication')}</Typography>
-        </Box>
-        <Box
-          sx={{
-            display: 'flex',
-            flexDirection: 'row',
-            justifyContent: 'space-between',
-            alignContent: 'center',
-            height: '3rem',
-          }}
-        >
-          <span style={{ paddingRight: '10%' }}>
-            {t('setting_auth_change', { authMethod: AuthenticationMethods.GUEST })}
-          </span>
-          <FormControl>
-            <FormGroup style={{ marginTop: '2%', alignItems: 'center' }}>
-              <FormControlLabel
-                control={<Switch checked={data.guestAccessEnabled} onChange={handleChange} />}
-                label={undefined}
-              />
-            </FormGroup>
-          </FormControl>
+    <LayoutSettings title={t('guest_authentication')} text={t('guest_authentication_info')}>
+      <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '80%', width: '100%' }}>
+        <Box sx={{ display: 'flex', flexDirection: 'column', width: '300px', gap: '20px' }}>
+          <Box
+            sx={{
+              display: 'flex',
+              flexDirection: 'row',
+              justifyContent: 'space-between',
+              alignContent: 'center',
+              height: '3rem',
+            }}
+          >
+            <span style={{ paddingRight: '10%' }}>{t('setting_auth_change', { authMethod: t('guest') })}</span>
+            <FormControl>
+              <FormGroup style={{ marginTop: '2%', alignItems: 'center' }}>
+                <FormControlLabel
+                  control={<Switch checked={data.guestAccessEnabled} onChange={handleChange} />}
+                  label={undefined}
+                />
+              </FormGroup>
+            </FormControl>
+          </Box>
         </Box>
       </Box>
-    </Box>
+    </LayoutSettings>
   )
 }
diff --git a/client/src/components/AdminSettings/JamsAuthMethod.tsx b/client/src/components/AdminSettings/JamsAuthMethod.tsx
index e1f93054..670576c5 100644
--- a/client/src/components/AdminSettings/JamsAuthMethod.tsx
+++ b/client/src/components/AdminSettings/JamsAuthMethod.tsx
@@ -33,14 +33,13 @@ import { useTranslation } from 'react-i18next'
 import { Form, Navigate } from 'react-router-dom'
 
 import { AlertSnackbarContext } from '../../contexts/AlertSnackbarProvider'
-import { AuthenticationMethods } from '../../enums/authenticationMethods'
 import {
   useGetAdminConfigQuery,
   useGetJamsUrl,
-  useRemoveJamsUrl,
   useSetJamsUrl,
   useUpdateAdminConfigMutation,
 } from '../../services/adminQueries'
+import LayoutSettings from '../LayoutSettings'
 import ProcessingRequest from '../ProcessingRequest'
 
 const JamsAuthMethod = () => {
@@ -51,7 +50,6 @@ const JamsAuthMethod = () => {
   const theme: Theme = useTheme()
   const setJamsUrlMutation = useSetJamsUrl()
   const jamsUrlQuery = useGetJamsUrl()
-  const removeJamsUrlMutation = useRemoveJamsUrl()
   const [jamsUrl, setJamsUrl] = useState<string>('')
   const { setAlertContent } = useContext(AlertSnackbarContext)
   const isWrapping = useMediaQuery(theme.breakpoints.down(500))
@@ -81,14 +79,6 @@ const JamsAuthMethod = () => {
     setJamsUrl(event.target.value)
   }
 
-  const removeJamsUrl = async () => {
-    removeJamsUrlMutation.mutate()
-    jamsUrlQuery.refetch()
-    if (jamsCurrentUrl !== undefined) {
-      setJamsUrl(jamsCurrentUrl)
-    }
-  }
-
   const submitNewJamsUrl = async () => {
     // verifies if the string is a server adress or name server
     const regexValidation = /^(https?:\/\/)?(localhost|([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})(?::\d{1,5})?(\/[^\s]*)?$/.test(
@@ -115,60 +105,53 @@ const JamsAuthMethod = () => {
   }
 
   return (
-    <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '80%' }}>
-      <Box sx={{ textAlign: 'start', marginTop: '8%' }}>
-        <Box
-          sx={{
-            display: 'flex',
-            flexDirection: 'row',
-            justifyContent: 'space-between',
-            alignContent: 'center',
-            height: '3rem',
-          }}
-        >
-          <span style={{ paddingRight: '10%' }}>
-            {t('setting_auth_change', { authMethod: AuthenticationMethods.JAMS })}
-          </span>
-          <FormControl>
-            <FormGroup style={{ marginTop: '2%', alignItems: 'center' }}>
-              <FormControlLabel
-                control={<Switch checked={data.jamsAuthEnabled} onChange={handleChange} />}
-                label={undefined}
-              />
-            </FormGroup>
-          </FormControl>
-        </Box>
-        <Box sx={{ textAlign: 'start', alignItems: 'start' }}>
-          <Typography>JAMS URL </Typography>
-          <Form>
-            <Input value={jamsUrl} onChange={handleNameChangeJamsUrl} placeholder={t('enter_jams_server')} />
-            <Button
-              variant="contained"
-              type="submit"
-              onClick={submitNewJamsUrl}
-              sx={{
-                mt: theme.typography.pxToRem(20),
-                marginLeft: theme.typography.pxToRem(10),
-                marginTop: isWrapping ? '5px' : '-15px',
-              }}
-            >
-              {t('save_button')}
-            </Button>
-            <Button
-              variant="contained"
-              onClick={removeJamsUrl}
-              sx={{
-                mt: theme.typography.pxToRem(20),
-                marginLeft: theme.typography.pxToRem(10),
-                marginTop: isWrapping ? '5px' : '-15px',
-              }}
-            >
-              {t('remove_jams_server')}
-            </Button>
-          </Form>
+    <LayoutSettings title={t('jams_authentication')} text={t('jams_authentication_info')}>
+      <Box
+        sx={{ display: 'flex', justifyContent: 'start', alignItems: 'center', height: '80%', paddingBottom: '20px' }}
+      >
+        <Box sx={{ textAlign: 'start', width: '100%', minWidth: '300px' }}>
+          <Box
+            sx={{
+              width: '100%',
+              display: 'flex',
+              flexDirection: 'row',
+              justifyContent: 'space-between',
+              alignContent: 'start',
+            }}
+          >
+            {t('setting_auth_change', { authMethod: t('jams') })}
+            <FormControl>
+              <FormGroup style={{ alignItems: 'start' }}>
+                <FormControlLabel
+                  control={<Switch checked={data.jamsAuthEnabled} onChange={handleChange} />}
+                  label={undefined}
+                />
+              </FormGroup>
+            </FormControl>
+          </Box>
+          <Box sx={{ textAlign: 'start', alignItems: 'start', marginTop: '-40px' }}>
+            <Typography>{t('jams_url')}</Typography>
+            <Form>
+              <Input value={jamsUrl} onChange={handleNameChangeJamsUrl} placeholder={t('enter_jams_server')} />
+            </Form>
+            <Box sx={{ width: '100%', display: 'flex', justifyContent: 'flex-end', marginTop: '20px' }}>
+              <Button
+                variant="contained"
+                type="submit"
+                onClick={submitNewJamsUrl}
+                sx={{
+                  mt: theme.typography.pxToRem(20),
+                  marginLeft: theme.typography.pxToRem(10),
+                  marginTop: isWrapping ? '5px' : '-15px',
+                }}
+              >
+                {t('save_button')}
+              </Button>
+            </Box>
+          </Box>
         </Box>
       </Box>
-    </Box>
+    </LayoutSettings>
   )
 }
 export default JamsAuthMethod
diff --git a/client/src/components/AdminSettings/LocalAuthMethod.tsx b/client/src/components/AdminSettings/LocalAuthMethod.tsx
index 780aa0d9..81679f7e 100644
--- a/client/src/components/AdminSettings/LocalAuthMethod.tsx
+++ b/client/src/components/AdminSettings/LocalAuthMethod.tsx
@@ -33,7 +33,6 @@ import { useTranslation } from 'react-i18next'
 import { Form, Navigate } from 'react-router-dom'
 
 import { AlertSnackbarContext } from '../../contexts/AlertSnackbarProvider'
-import { AuthenticationMethods } from '../../enums/authenticationMethods'
 import {
   useGetAdminConfigQuery,
   useGetNameServer,
@@ -41,6 +40,7 @@ import {
   useSetNameServer,
   useUpdateAdminConfigMutation,
 } from '../../services/adminQueries'
+import LayoutSettings from '../LayoutSettings'
 import ProcessingRequest from '../ProcessingRequest'
 
 const LocalAuthMethod = () => {
@@ -104,64 +104,63 @@ const LocalAuthMethod = () => {
   }
 
   return (
-    <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '80%' }}>
-      <Box sx={{ textAlign: 'start' }}>
-        <Box
-          sx={{
-            display: 'flex',
-            flexDirection: 'row',
-            justifyContent: 'space-between',
-            alignContent: 'center',
-            height: '3rem',
-          }}
-        >
-          <span style={{ paddingRight: '10%' }}>
-            {t('setting_auth_change', { authMethod: AuthenticationMethods.JAMI })}
-          </span>
-          <FormControl>
-            <FormGroup style={{ marginTop: '2%', alignItems: 'center' }}>
-              <FormControlLabel
-                control={<Switch checked={data.localAuthEnabled} onChange={handleChange} />}
-                label={undefined}
-              />
-            </FormGroup>
-          </FormControl>
-        </Box>
-        <Box sx={{ textAlign: 'start', alignItems: 'start' }}>
-          <Typography>{t('setting_name_server')}</Typography>
-          <Form>
-            <Input value={nameServer} onChange={handleNameChangeNameServer} />
+    <LayoutSettings title={t('jami_authentication')} text={t('jami_authentication_info')}>
+      <Box sx={{ display: 'flex', justifyContent: 'start', alignItems: 'start', paddingBottom: '30px' }}>
+        <Box sx={{ textAlign: 'start', width: '100%', minWidth: '300px' }}>
+          <Box
+            sx={{
+              display: 'flex',
+              flexDirection: 'row',
+              justifyContent: 'space-between',
+              alignContent: 'start',
+              marginTop: '-30px',
+            }}
+          >
+            <span style={{ paddingRight: '10%' }}>{t('setting_auth_change', { authMethod: t('jami') })}</span>
+            <FormControl>
+              <FormGroup style={{ alignItems: 'start' }}>
+                <FormControlLabel
+                  control={<Switch checked={data.localAuthEnabled} onChange={handleChange} />}
+                  label={undefined}
+                />
+              </FormGroup>
+            </FormControl>
+          </Box>
+          <Box sx={{ textAlign: 'start', alignItems: 'start', marginTop: '-40px' }}>
+            <Typography>{t('setting_name_server')}</Typography>
+            <Form>
+              <Input value={nameServer} onChange={handleNameChangeNameServer} />
+            </Form>
+          </Box>
+          <Box sx={{ display: 'flex', justifyContent: 'flex-end', marginTop: '30px' }}>
             <Button
               variant="contained"
-              type="submit"
-              onClick={submitNewNameServer}
+              onClick={deleteNameServer}
               sx={{
                 mt: theme.typography.pxToRem(20),
                 marginLeft: theme.typography.pxToRem(10),
                 marginTop: isWrapping ? '5px' : '-15px',
               }}
-              disabled={nameServer === nameServerConfigured}
             >
-              {t('save_button')}
+              {t('reset_nameserver_button')}
             </Button>
             <Button
               variant="contained"
-              onClick={deleteNameServer}
+              type="submit"
+              onClick={submitNewNameServer}
               sx={{
                 mt: theme.typography.pxToRem(20),
                 marginLeft: theme.typography.pxToRem(10),
                 marginTop: isWrapping ? '5px' : '-15px',
               }}
+              disabled={nameServer === nameServerConfigured}
             >
-              {t('reset_nameserver_button')}
+              {t('save_button')}
             </Button>
-          </Form>
+          </Box>
         </Box>
-        {nameServer === nameServerConfigured && (
-          <Box style={{ fontSize: 'smaller', marginTop: '2%' }}>{t('nameserver_already_set')}</Box>
-        )}
       </Box>
-    </Box>
+    </LayoutSettings>
   )
 }
 
diff --git a/client/src/components/AdminSettings/OpenIdAuthMethod.tsx b/client/src/components/AdminSettings/OpenIdAuthMethod.tsx
index 59205c9f..2e895451 100644
--- a/client/src/components/AdminSettings/OpenIdAuthMethod.tsx
+++ b/client/src/components/AdminSettings/OpenIdAuthMethod.tsx
@@ -16,18 +16,7 @@
  * <https://www.gnu.org/licenses/>.
  */
 
-import {
-  Box,
-  Divider,
-  FormControl,
-  FormControlLabel,
-  FormGroup,
-  List,
-  ListItem,
-  ListItemText,
-  Switch,
-  Typography,
-} from '@mui/material'
+import { Box, Divider, List, ListItem, ListItemText, Typography } from '@mui/material'
 import { AnimatePresence, motion } from 'framer-motion'
 import { AdminSettingSelection } from 'jami-web-common'
 import { useTranslation } from 'react-i18next'
@@ -35,15 +24,10 @@ import { useNavigate } from 'react-router-dom'
 
 import { AuthenticationMethods } from '../../enums/authenticationMethods'
 import rightChevron from '../../icons/rightChevron.svg'
-import {
-  useGetAdminConfigQuery,
-  useGetAllOauthClients,
-  useUpdateAdminConfigMutation,
-} from '../../services/adminQueries'
+import { useGetAdminConfigQuery, useGetAllOauthClients } from '../../services/adminQueries'
 import ProcessingRequest from '../ProcessingRequest'
 
 export default function OpenIdAuthMethod() {
-  const saveAdminConfigMutation = useUpdateAdminConfigMutation()
   const navigate = useNavigate()
   const { t } = useTranslation()
   const { data } = useGetAdminConfigQuery()
@@ -83,10 +67,6 @@ export default function OpenIdAuthMethod() {
   const disabledColor = '#CC0022'
   const enabledColor = '#00796B'
 
-  const handleChange = () => {
-    saveAdminConfigMutation.mutate({ openIdAuthEnabled: !data.openIdAuthEnabled })
-  }
-
   const handleSelection = (provider: string) => {
     navigate(`/admin/${AdminSettingSelection.AUTHENTICATION}/${AuthenticationMethods.OPENID}/${provider}`)
   }
@@ -97,27 +77,6 @@ export default function OpenIdAuthMethod() {
         <Box>
           <Typography variant="h6">{t('openid_authentication')}</Typography>
         </Box>
-        <Box
-          sx={{
-            display: 'flex',
-            flexDirection: 'row',
-            justifyContent: 'space-between',
-            alignContent: 'center',
-            height: '3rem',
-          }}
-        >
-          <span style={{ paddingRight: '10%' }}>
-            {t('setting_auth_change', { authMethod: AuthenticationMethods.OPENID })}
-          </span>
-          <FormControl>
-            <FormGroup style={{ marginTop: '2%', alignItems: 'center' }}>
-              <FormControlLabel
-                control={<Switch checked={data.openIdAuthEnabled} onChange={handleChange} />}
-                label={undefined}
-              />
-            </FormGroup>
-          </FormControl>
-        </Box>
         <AnimatePresence>
           <motion.div
             style={{
diff --git a/client/src/components/LayoutSettings.tsx b/client/src/components/LayoutSettings.tsx
new file mode 100644
index 00000000..a44550f5
--- /dev/null
+++ b/client/src/components/LayoutSettings.tsx
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2022-2025 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, Card, CardHeader, Typography, useMediaQuery, useTheme } from '@mui/material'
+
+interface LayoutSettingsProps {
+  children: React.ReactNode
+  title: string
+  text?: string
+}
+
+export default function LayoutSettings({ children, title, text }: LayoutSettingsProps) {
+  const theme = useTheme()
+  const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
+
+  return (
+    <Box
+      sx={{
+        display: 'flex',
+        width: '100%',
+        minHeight: '420px',
+        maxHeight: '600px',
+        justifyContent: 'center',
+        marginTop: isMobile ? '0px' : '80px',
+      }}
+    >
+      <Card
+        sx={{
+          maxWidth: '400px',
+          padding: '20px',
+          boxShadow: isMobile ? 'none' : undefined,
+          border: isMobile ? 'none' : undefined,
+        }}
+      >
+        <CardHeader
+          title={<Typography variant="h5">{title}</Typography>}
+          subheader={
+            <Box sx={{ mt: '40px' }}>
+              <Typography variant="body2">{text}</Typography>
+            </Box>
+          }
+        />
+        <Box sx={{ marginTop: '50px', maxWidth: '100%', display: 'flex', justifyContent: 'center' }}>
+          <Box sx={{ maxWidth: '90%' }}>{children}</Box>
+        </Box>
+      </Card>
+    </Box>
+  )
+}
diff --git a/server/locale/en/translation.json b/server/locale/en/translation.json
index 9912fae5..99674004 100644
--- a/server/locale/en/translation.json
+++ b/server/locale/en/translation.json
@@ -15,10 +15,12 @@
   "admin_account_configuration": "Admin configuration",
   "admin_config_auth_methods_title": "Authentication methods",
   "admin_login_to_main": "User login",
+  "admin_page_accounts_overview": "Accounts overview",
   "admin_page_accounts_overview_title": "Accounts overview - {{count}} active accounts",
   "admin_page_accounts_overview_title_auth": "Authentication method",
   "admin_page_accounts_overview_title_username": "Username",
   "admin_page_accounts_overview_title_storage": "Storage used",
+  "admin_page_authentication_methods": "Authentication methods",
   "admin_page_create_button": "Create admin account",
   "admin_page_login_title": "Jami Admin",
   "admin_page_login_subtitle": "Log in to access the admin panel",
@@ -118,6 +120,7 @@
   "dialog_close": "Close",
   "dialog_confirm_title_default": "Confirm action",
   "disable_tips": "Disable tips",
+  "documentation": "Documentation",
   "download_limit": "Auto download limit",
   "download_limit_details": "The automatic download limit is the maximum file size (in MB) that the application downloads automatically. If a file exceeds the limit, the user is required to accept the download.",
   "edited_message": "Edited ",
@@ -140,6 +143,7 @@
   "go_to_login_page": "Go to login page",
   "guest": "Guest",
   "guest_authentication": "Guest authentication",
+  "guest_authentication_info": "Guest authentication enables users to access a temporary account that is automatically deleted shortly after. This allows them to start chatting without the need to provide any information or credentials.",
   "incoming_call": "Incoming call",
   "incoming_call_audio": "Incoming audio call from {{member0}}",
   "incoming_call_video": "Incoming video call from {{member0}}",
@@ -148,8 +152,13 @@
   "invited": "Invited",
   "jami": "Jami",
   "jami_account": "Jami Account",
+  "jami_authentication": "Jami Local Authentication",
+  "jami_authentication_info": "Local Authentication allows users to register an account by providing a username. The account is created and managed directly by the server. If the server's data is deleted, the account and its access will be permanently lost.",
   "jami_user_id": "Jami user ID",
   "jams": "JAMS",
+  "jams_authentication": "JAMS authentication",
+  "jams_authentication_info": "JAMS is the authentication service provided by Savoir Faire Linux, enabling companies to manage secure access and user authentication efficiently.",
+  "jams_url": "JAMS URL",
   "jams_url_already_set": "JAMS server is already set.",
   "link_new_device": "Link new device",
   "limit_cannot_be_negative": "The limit cannot be negative",
@@ -193,6 +202,7 @@
   "ongoing_call_unmuted": "Ongoing call",
   "openid": "OpenID",
   "openid_authentication": "OpenID authentication",
+  "oauth2_provider": "OAuth2 Provider",
   "outgoing_call": "Outgoing call",
   "password_input_helper_text": "",
   "password_input_helper_text_empty": "The password is missing.",
@@ -242,7 +252,8 @@
   "search_results": "Search Results",
   "see_all_devices": "See all devices",
   "select_placeholder": "Select an option",
-  "setting_auth_change": "{{authMethod}} enabled",
+  "setting_change_admin_password_text": "Change the administrator password",
+  "setting_auth_change": "{{authMethod}} authentication",
   "setting_auth_jami_change": "Enable Jami local",
   "setting_can_be_changed_later": "*You will still have the option to modify this parameter later in the admin configuration panel.",
   "setting_auto_download_limit_error": "An error occurred while updating the auto download limit",
diff --git a/server/src/routers/admin-router.ts b/server/src/routers/admin-router.ts
index 12463199..cfb95168 100644
--- a/server/src/routers/admin-router.ts
+++ b/server/src/routers/admin-router.ts
@@ -461,6 +461,7 @@ adminRouter.get(
       redirectUri: baseUrl + providersSupport.getRedirectUri(provider),
       documentation: providerConfig.documentation,
       displayName: providerConfig.displayName,
+      icon: providerConfig.icon,
     }
 
     res.send(data)
@@ -497,10 +498,18 @@ adminRouter.patch(
       }
     }
 
+    if (adminAccount.getNumberOfEnabledProviders() === 0) {
+      adminAccount.updateConfig({ openIdAuthEnabled: false })
+    } else {
+      adminAccount.updateConfig({ openIdAuthEnabled: true })
+    }
+
     adminAccount.save()
+
     res.sendStatus(HttpStatusCode.Ok)
   }),
 )
+
 adminRouter.post('/autoDownloadLimit', (req, res) => {
   const downloadLimit = Number(req.body.limit)
   if (!downloadLimit) {
diff --git a/server/src/storage/admin-account.ts b/server/src/storage/admin-account.ts
index 1f13cbf1..e263ea81 100644
--- a/server/src/storage/admin-account.ts
+++ b/server/src/storage/admin-account.ts
@@ -137,6 +137,10 @@ export class AdminAccount {
     }
   }
 
+  getNumberOfEnabledProviders() {
+    return Object.values(this.getOpenIdProviders()).filter((provider) => provider.isActive).length
+  }
+
   getLastWizardState(): string {
     return this.adminAccount.lastWizardState
   }
-- 
GitLab