Cherry-pick API_URL changes
This commit is contained in:
@@ -46,6 +46,10 @@ Create an `.env` file with the following:
|
|||||||
DATABASE_URL=connection-url
|
DATABASE_URL=connection-url
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Optional: set `API_URL` to change the base URL used by internal UI API calls.
|
||||||
|
Relative paths are served under `BASE_PATH`; absolute URLs are called directly by the browser.
|
||||||
|
For example, `API_URL=/internal-api` or `API_URL=https://api.example.com/api`.
|
||||||
|
|
||||||
The connection URL format:
|
The connection URL format:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+39
-1
@@ -8,6 +8,7 @@ const TRACKER_SCRIPT = '/script.js';
|
|||||||
|
|
||||||
const isProd = process.env.NODE_ENV === 'production';
|
const isProd = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
|
const apiUrl = process.env.API_URL || '';
|
||||||
const basePath = process.env.BASE_PATH || '';
|
const basePath = process.env.BASE_PATH || '';
|
||||||
const cloudMode = process.env.CLOUD_MODE || '';
|
const cloudMode = process.env.CLOUD_MODE || '';
|
||||||
const cloudUrl = process.env.CLOUD_URL || '';
|
const cloudUrl = process.env.CLOUD_URL || '';
|
||||||
@@ -22,12 +23,32 @@ const trackerScriptURL = process.env.TRACKER_SCRIPT_URL || '';
|
|||||||
const selfTrack = process.env.UMAMI_SELF_TRACK || '';
|
const selfTrack = process.env.UMAMI_SELF_TRACK || '';
|
||||||
const selfRecord = process.env.UMAMI_SELF_RECORD || '';
|
const selfRecord = process.env.UMAMI_SELF_RECORD || '';
|
||||||
|
|
||||||
|
function getUrlOrigin(url: string) {
|
||||||
|
try {
|
||||||
|
return new URL(url).origin;
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRelativeUrl(url: string) {
|
||||||
|
return Boolean(url && !/^https?:\/\//i.test(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePath(url: string) {
|
||||||
|
return `/${url.replace(/^\/+|\/+$/g, '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiUrlOrigin = getUrlOrigin(apiUrl);
|
||||||
|
const connectSrc = ["'self'", 'https:', apiUrlOrigin].filter(Boolean).join(' ');
|
||||||
|
|
||||||
const contentSecurityPolicy = `
|
const contentSecurityPolicy = `
|
||||||
default-src 'self';
|
default-src 'self';
|
||||||
img-src 'self' https: data:;
|
img-src 'self' https: data:;
|
||||||
script-src 'self' 'unsafe-eval' 'unsafe-inline';
|
script-src 'self' 'unsafe-eval' 'unsafe-inline';
|
||||||
style-src 'self' 'unsafe-inline';
|
style-src 'self' 'unsafe-inline';
|
||||||
connect-src 'self' https:;
|
connect-src ${connectSrc};
|
||||||
|
frame-src 'self' http: https:;
|
||||||
frame-ancestors 'self' ${frameAncestors};
|
frame-ancestors 'self' ${frameAncestors};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -122,6 +143,22 @@ if (collectApiEndpoint) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isRelativeUrl(apiUrl)) {
|
||||||
|
const normalizedApiUrl = normalizePath(apiUrl);
|
||||||
|
|
||||||
|
if (normalizedApiUrl !== '/' && normalizedApiUrl !== '/api') {
|
||||||
|
headers.push({
|
||||||
|
source: `${normalizedApiUrl}/:path*`,
|
||||||
|
headers: apiHeaders,
|
||||||
|
});
|
||||||
|
|
||||||
|
rewrites.push({
|
||||||
|
source: `${normalizedApiUrl}/:path*`,
|
||||||
|
destination: '/api/:path*',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const redirects = [
|
const redirects = [
|
||||||
{
|
{
|
||||||
source: '/teams/:id/dashboard/edit',
|
source: '/teams/:id/dashboard/edit',
|
||||||
@@ -187,6 +224,7 @@ if (isProd && cloudMode) {
|
|||||||
export default withNextIntl({
|
export default withNextIntl({
|
||||||
reactStrictMode: false,
|
reactStrictMode: false,
|
||||||
env: {
|
env: {
|
||||||
|
apiUrl,
|
||||||
basePath,
|
basePath,
|
||||||
cloudMode,
|
cloudMode,
|
||||||
cloudUrl,
|
cloudUrl,
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ DATABASE_TYPE=postgresql
|
|||||||
# A secret string used by Umami (replace with a strong random string)
|
# A secret string used by Umami (replace with a strong random string)
|
||||||
APP_SECRET=replace-me-with-a-random-string
|
APP_SECRET=replace-me-with-a-random-string
|
||||||
|
|
||||||
|
# Optional API base URL for internal UI API calls. Relative paths are served under BASE_PATH;
|
||||||
|
# absolute URLs are called directly by the browser.
|
||||||
|
# Examples: /internal-api or https://api.example.com/api
|
||||||
|
API_URL=
|
||||||
|
|
||||||
# Postgres container defaults.
|
# Postgres container defaults.
|
||||||
POSTGRES_DB=umami
|
POSTGRES_DB=umami
|
||||||
POSTGRES_USER=umami
|
POSTGRES_USER=umami
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { getApiUrl } from '@/lib/api-url';
|
||||||
import { getClientAuthToken } from '@/lib/client';
|
import { getClientAuthToken } from '@/lib/client';
|
||||||
import { SHARE_CONTEXT_HEADER, SHARE_TOKEN_HEADER } from '@/lib/constants';
|
import { SHARE_CONTEXT_HEADER, SHARE_TOKEN_HEADER } from '@/lib/constants';
|
||||||
import { type FetchResponse, httpDelete, httpGet, httpPost, httpPut } from '@/lib/fetch';
|
import { type FetchResponse, httpDelete, httpGet, httpPost, httpPut } from '@/lib/fetch';
|
||||||
@@ -31,10 +32,8 @@ export function useApi() {
|
|||||||
authorization: `Bearer ${getClientAuthToken()}`,
|
authorization: `Bearer ${getClientAuthToken()}`,
|
||||||
...shareHeaders,
|
...shareHeaders,
|
||||||
};
|
};
|
||||||
const basePath = process.env.basePath;
|
|
||||||
|
|
||||||
const getUrl = (url: string) => {
|
const getUrl = (url: string) => {
|
||||||
return url.startsWith('http') ? url : `${basePath || ''}/api${url}`;
|
return getApiUrl(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getHeaders = (headers: any = {}) => {
|
const getHeaders = (headers: any = {}) => {
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
type ApiUrlOptions = {
|
||||||
|
apiUrl?: string;
|
||||||
|
basePath?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const APP_ROUTE_PATTERNS: RegExp[] = [
|
||||||
|
/^\/auth(\/|$)/,
|
||||||
|
/^\/config(\/|$)/,
|
||||||
|
];
|
||||||
|
|
||||||
|
function trimTrailingSlash(value: string) {
|
||||||
|
return value.replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimLeadingSlash(value: string) {
|
||||||
|
return value.replace(/^\/+/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinPath(base: string, path: string) {
|
||||||
|
if (!base) {
|
||||||
|
return `/${trimLeadingSlash(path)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${trimTrailingSlash(base)}/${trimLeadingSlash(path)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAbsoluteUrl(url: string) {
|
||||||
|
return /^https?:\/\//i.test(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAppRoute(url: string) {
|
||||||
|
const path = `/${trimLeadingSlash(url.split('?')[0])}`;
|
||||||
|
return APP_ROUTE_PATTERNS.some(re => re.test(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getApiUrl(url: string, options: ApiUrlOptions = {}) {
|
||||||
|
if (isAbsoluteUrl(url)) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { apiUrl = process.env.apiUrl || '', basePath = process.env.basePath || '' } = options;
|
||||||
|
const useApiUrl = apiUrl && !isAppRoute(url);
|
||||||
|
const baseUrl = useApiUrl
|
||||||
|
? isAbsoluteUrl(apiUrl)
|
||||||
|
? apiUrl
|
||||||
|
: joinPath(basePath, apiUrl)
|
||||||
|
: joinPath(basePath, '/api');
|
||||||
|
|
||||||
|
return joinPath(baseUrl, url);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user