diff --git a/README.md b/README.md
index 0153aa96..9ba859f1 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,12 @@ Run this command to create files for distribution.
Result will be created in `/dist`
+## Deploying under a subpath
+By default the app assumes it is served at the root of its domain
+(`https://mail.example.com/`). To run it under a subpath instead
+(`https://mail.example.com/admin/`), only the ` ` tag
+in `public/index.html` needs to be adapted.
# Server-side configuration
diff --git a/conf/web.conf b/conf/web.conf
index df99660c..48a0dbda 100644
--- a/conf/web.conf
+++ b/conf/web.conf
@@ -2,7 +2,7 @@ root /usr/share/grommunio-admin-web;
location / {
#root /usr/share/grommunio-admin-web;
index index.html /index.html;
- try_files $uri /index.html =404;
+ try_files $uri index.html =404;
}
#location /service-worker.js {
# add_header Cache-Control "no-cache, no-store, must-revalidate";
diff --git a/package.json b/package.json
index edc221c5..fb0178eb 100644
--- a/package.json
+++ b/package.json
@@ -2,6 +2,7 @@
"name": "grommunio-admin-web",
"version": "3.0.0",
"private": true,
+ "homepage": ".",
"dependencies": {
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
diff --git a/public/index.html b/public/index.html
index f36f5d8c..f45c552d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -2,6 +2,11 @@
+
+
diff --git a/src/api.ts b/src/api.ts
index 43836570..412e09fd 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -13,7 +13,7 @@ import { NewServer, ServerPolicy, UpdateServer } from './types/servers';
import { FetchSyncParams, RemoteWipeParams } from './types/sync';
import { AddUserFolderPermission, DeleteOrphanedUsersParams, DeleteUserFolderPermission, NewUser, OofSettings, UpdateUser, USER_STATUS } from './types/users';
-const baseUrl = '//' + window.location.host + '/api/v1';
+const baseUrl = 'api/v1';
/**
* Handles fetch api errors
diff --git a/src/components/AuthenticatedDomainRoute.tsx b/src/components/AuthenticatedDomainRoute.tsx
index 996c4f29..ef19a8b1 100644
--- a/src/components/AuthenticatedDomainRoute.tsx
+++ b/src/components/AuthenticatedDomainRoute.tsx
@@ -5,7 +5,7 @@ import { DomainViewProps } from "@/types/common";
import { Domain } from "@/types/domains";
import React from "react";
-import { Navigate } from "react-router-dom";
+import { Navigate, useLocation } from "react-router-dom";
type AuthenticatedDomainRouteProps = {
component: React.ComponentType;
@@ -17,9 +17,10 @@ type AuthenticatedDomainRouteProps = {
}
const AuthenticatedDomainRoute = ({ component: DomainRoute, props: childProps, domain, ...rest }: AuthenticatedDomainRouteProps) => {
+ const location = useLocation();
if(!childProps.authenticated) {
- return
+ return
}
return
diff --git a/src/components/DefaultRedirect.tsx b/src/components/DefaultRedirect.tsx
index e428e5fd..4638d649 100644
--- a/src/components/DefaultRedirect.tsx
+++ b/src/components/DefaultRedirect.tsx
@@ -2,14 +2,15 @@
// SPDX-FileCopyrightText: 2020-2026 grommunio GmbH
/* eslint-disable react/prop-types */
import React from "react";
-import { Navigate } from "react-router-dom";
+import { Navigate, useLocation } from "react-router-dom";
/**
* Default route, which the user is redirected to, if the url does not match any specified route
*/
const DefaultRedirect = () => {
+ const location = useLocation();
return
};
diff --git a/src/config.ts b/src/config.ts
index ec5ca908..7a974508 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -15,7 +15,7 @@ const error = () => {
}
// Fetch config.js on server and merge with default config
-fetch('//' + window.location.host + '/config.json')
+fetch('config.json')
.then(async response => {
if (response.ok) {
try {
diff --git a/src/containers/ContactDetails.tsx b/src/containers/ContactDetails.tsx
index 307206ff..4c672158 100644
--- a/src/containers/ContactDetails.tsx
+++ b/src/containers/ContactDetails.tsx
@@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
import User from '../components/user/User';
import Contact from '../components/user/Contact';
import { editUserData, fetchUserData } from '../actions/users';
-import { useNavigate } from 'react-router';
+import { useNavigate, useParams } from 'react-router';
import HideFromSelect from '../components/HideFromSelect';
import { useAppDispatch } from '../store';
import { UpdateUser, UserProperties } from '@/types/users';
@@ -59,14 +59,14 @@ const ContactDetails = ({ domain }: DomainViewProps) => {
unsaved: false,
});
const navigate = useNavigate();
+ const { userID: userIDParam } = useParams<{ userID: string }>();
const fetch = async (domainID: number, userID: number) => await dispatch(fetchUserData(domainID, userID));
const edit = async (domainID: number, user: UpdateUser) => await dispatch(editUserData(domainID, user));
useEffect(() => {
const inner = async () => {
- const splits = window.location.pathname.split('/');
- const user = await fetch(parseInt(splits[1]), parseInt(splits[3]))
+ const user = await fetch(domain.ID, parseInt(userIDParam as string))
.catch(msg => setState({ ...state, snackbar: msg || 'Unknown error' }));
if(!user) return;
user.syncPolicy = user.syncPolicy || {};
diff --git a/src/containers/DBFile.tsx b/src/containers/DBFile.tsx
index 7dae21de..032cef26 100644
--- a/src/containers/DBFile.tsx
+++ b/src/containers/DBFile.tsx
@@ -19,7 +19,7 @@ import { Add, Delete } from '@mui/icons-material';
import { SYSTEM_ADMIN_WRITE } from '../constants';
import { CapabilityContext } from '../CapabilityContext';
import ViewWrapper from '../components/ViewWrapper';
-import { useNavigate } from 'react-router';
+import { useNavigate, useParams } from 'react-router';
import { ChangeEvent, KeyValuePair } from '@/types/common';
import { useAppDispatch } from '../store';
@@ -61,6 +61,7 @@ const DBFile = () => {
});
const context = useContext(CapabilityContext);
const navigate = useNavigate();
+ const { serviceName, fileName } = useParams<{ serviceName: string; fileName: string }>();
const fetch = async (service: string, filename: string) =>
await dispatch(fetchServiceFile(service, filename));
@@ -69,8 +70,7 @@ const DBFile = () => {
useEffect(() => {
const inner = async () => {
- const splits = window.location.pathname.split('/');
- const file = await fetch(splits[2], splits[3])
+ const file = await fetch(serviceName as string, fileName as string)
.catch((message: string) => setState({ ...state, snackbar: message || 'Unknown error' }));
if(!file?.data) return;
@@ -106,8 +106,7 @@ const DBFile = () => {
}
const handleEdit = () => {
- const splits = window.location.pathname.split('/');
- edit(splits[2], splits[3], { data: formatData(state.data) })
+ edit(serviceName as string, fileName as string, { data: formatData(state.data) })
.then(resp => setState({ ...state, snackbar: 'Success! ' + (resp?.message || '')}))
.catch(message => setState({ ...state, snackbar: message || 'Unknown error' }));
}
diff --git a/src/containers/FolderDetails.tsx b/src/containers/FolderDetails.tsx
index 2bd97cd7..68edebd4 100644
--- a/src/containers/FolderDetails.tsx
+++ b/src/containers/FolderDetails.tsx
@@ -23,7 +23,7 @@ import { CapabilityContext } from '../CapabilityContext';
import ViewWrapper from '../components/ViewWrapper';
import FolderPermissions from '../components/Dialogs/FolderPermissions';
import { Info } from '@mui/icons-material';
-import { useNavigate } from 'react-router';
+import { useNavigate, useParams } from 'react-router';
import { useAppDispatch } from '../store';
import { ChangeEvent, DomainViewProps } from '@/types/common';
import { Folder, UpdateFolder } from '@/types/folders';
@@ -72,6 +72,7 @@ const FolderDetails = ({ domain }: DomainViewProps) => {
});
const context = useContext(CapabilityContext);
const navigate = useNavigate();
+ const { folderID: folderIDParam } = useParams<{ folderID: string }>();
const fetch = async (domainID: number, folderID: string) => await dispatch(fetchFolderDetails(domainID, folderID));
const edit = async (domainID: number, folder: UpdateFolder) => await dispatch(editFolderData(domainID, folder));
@@ -80,8 +81,7 @@ const FolderDetails = ({ domain }: DomainViewProps) => {
useEffect(() => {
const inner = async () => {
- const splits = window.location.pathname.split('/');
- const folderId = splits[3];
+ const folderId = folderIDParam as string;
// If folder is IPM_SUBTREE
if(folderId === IPM_SUBTREE_ID) {
setState({ ...state, folder: IPM_SUBTREE_OBJECT, readonly: true, loading: false });
diff --git a/src/containers/UserDetails.tsx b/src/containers/UserDetails.tsx
index 711ae0bf..c1f0f929 100644
--- a/src/containers/UserDetails.tsx
+++ b/src/containers/UserDetails.tsx
@@ -43,7 +43,7 @@ import { fetchServersData } from '../actions/servers';
import Oof from '../components/user/Oof';
import Tabs from '../components/user/Tabs';
import Altnames from '../components/user/Altnames';
-import { useNavigate } from 'react-router';
+import { useNavigate, useParams } from 'react-router';
import { throttle } from 'lodash';
import { ChangeEvent, DomainViewProps } from '@/types/common';
import { useAppDispatch } from '../store';
@@ -174,6 +174,7 @@ const UserDetails = ({ domain }: DomainViewProps) => {
const [forwardError, setForwardError] = useState(false);
const context = useContext(CapabilityContext);
const navigate = useNavigate();
+ const { userID: userIDParam } = useParams<{ userID: string }>();
const fetch = async (domainID: number, userID: number) => await dispatch(fetchUserData(domainID, userID));
const fetchRoles = async () => await dispatch(fetchRolesData({ sort: 'name,asc', level: 0, limit: 100000 }))
@@ -188,8 +189,7 @@ const UserDetails = ({ domain }: DomainViewProps) => {
useEffect(() => {
const inner = async () => {
- const splits = window.location.pathname.split('/');
- const user = await fetch(parseInt(splits[1]), parseInt(splits[3]))
+ const user = await fetch(domain.ID, parseInt(userIDParam as string))
.catch(msg => setState({ ...state, snackbar: msg || 'Unknown error' }));
if(!user) return;
const defaultPolicy = user.defaultPolicy || {};
diff --git a/src/index.tsx b/src/index.tsx
index 409bd2b0..ec0ffcfc 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -18,12 +18,18 @@ import makeLoadableComponent from './lazy';
// Async loading support.
const LoadableApp = makeLoadableComponent(() => import('./App'));
+// Derive the router basename from the document's , so the app
+// works correctly no matter which subpath it is deployed under.
+// Falls back to "/" if no tag is present.
+const basePathname = new URL(document.baseURI).pathname;
+const basename = basePathname.endsWith('/') ? basePathname.slice(0, -1) : basePathname;
+
const container = document.getElementById('root') || document.createElement("div");
const root = ReactDOM.createRoot(container);
root.render(
-
+