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( - +