blob: 8c2d45fc11c65627a280aa76026903cef2d99ed9 [file]
/**
* Where the app is mounted.
*
* The calendar usually lives at the root of a host, but it can be mounted in a
* sub-directory instead - `https://calendar.apache.org/calendar/` rather than
* `https://calendar.apache.org/`. The prefix is a deployment decision, not a
* build-time one: `frontend/dist` is committed to the repository and shared
* between deployments, so the same build has to work either way.
*
* The backend therefore rewrites the `<base href>` in index.html to match
* `server.base_path` from its config, and everything here reads the answer back
* out of `document.baseURI`. The browser uses the same tag to resolve the
* script, stylesheet and favicon, which is why relative URLs in the page keep
* working on a nested route such as /calendar/e/AbCd2345.
*/
/** The mount point, with no trailing slash: "" at the root, or "/calendar". */
export function basePath(): string {
const path = baseUrl().pathname;
return path === "/" ? "" : path.replace(/\/+$/, "");
}
/** The absolute base URL the document resolves relative URLs against. */
export function baseUrl(): URL {
const href = globalThis.document?.baseURI;
try {
return new URL(href ?? "/", globalThis.location?.href ?? "http://localhost/");
} catch {
return new URL("http://localhost/");
}
}
/** An absolute path within the app: appPath("/help") -> "/calendar/help". */
export function appPath(path: string): string {
const suffix = path.startsWith("/") ? path : `/${path}`;
return `${basePath()}${suffix}` || "/";
}
/** An absolute path to an API endpoint. */
export function apiUrl(path: string): string {
return appPath(`/api${path.startsWith("/") ? path : `/${path}`}`);
}
/** An absolute path to a file shipped in the build, such as the logo. */
export function assetUrl(name: string): string {
return appPath(`/${name.replace(/^\/+/, "")}`);
}
/**
* Strips the mount point off a browser path, so route matching does not have to
* know about it. Returns null when the path is not inside the app at all.
*/
export function withoutBase(pathname: string): string | null {
const prefix = basePath();
if (!prefix) return pathname || "/";
if (pathname === prefix) return "/";
if (pathname.startsWith(`${prefix}/`)) return pathname.slice(prefix.length) || "/";
return null;
}
/**
* asfquart's OAuth endpoint, as mounted by this app.
*
* The backend hands back the real login and logout URLs in /api/session, and
* those always win, because a deployment can move the endpoint with the
* `oauth.uri` config key. These are the fallbacks used before that request has
* come back, or if it fails - which is exactly when someone is most likely to
* be reaching for the login link.
*
* They are not URL-encoded, for the same reason the backend does not encode
* them: a mount point is restricted to unreserved URL characters.
*/
const DEFAULT_OAUTH_PATH = "/auth";
export function loginUrl(returnTo: string = appPath("/")): string {
return `${appPath(DEFAULT_OAUTH_PATH)}?login=${returnTo}`;
}
export function logoutUrl(returnTo: string = appPath("/")): string {
return `${appPath(DEFAULT_OAUTH_PATH)}?logout=${returnTo}`;
}