fix: preserve base path in email asset URLs (#3327)

This commit is contained in:
Christopher Ryan
2026-09-11 20:44:05 +10:00
committed by GitHub
parent 20ae849a1a
commit 5603a9e59d
10 changed files with 50 additions and 44 deletions
@@ -1,6 +1,7 @@
import { Plural, Trans } from '@lingui/react/macro';
import { Heading, Img, Section, Text } from '../components';
import { getEmailAssetUrl } from '../utils/asset-url';
export type TemplateAccessAuth2FAProps = {
documentTitle: string;
@@ -18,13 +19,9 @@ export const TemplateAccessAuth2FA = ({
expiresInMinutes,
assetBaseUrl = 'http://localhost:3002',
}: TemplateAccessAuth2FAProps) => {
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return (
<div>
<Img src={getAssetUrl('/static/document.png')} alt="Document" className="mx-auto h-12 w-12" />
<Img src={getEmailAssetUrl(assetBaseUrl, 'static/document.png')} alt="Document" className="mx-auto h-12 w-12" />
<Section className="mt-8">
<Heading className="text-center font-semibold text-foreground text-lg">
@@ -1,5 +1,6 @@
import { Img, Link } from '../components';
import { useBranding } from '../providers/branding';
import { getEmailAssetUrl } from '../utils/asset-url';
import { getSafeBrandingUrl } from '../utils/branding-url';
export type TemplateBrandingLogoProps = {
@@ -20,7 +21,7 @@ export const TemplateBrandingLogo = ({ assetBaseUrl, className = 'mb-4 h-6' }: T
const hasCustomBrandingLogo = branding.brandingEnabled && Boolean(branding.brandingLogo);
if (!hasCustomBrandingLogo) {
const documensoLogoUrl = new URL('/static/logo.png', assetBaseUrl).toString();
const documensoLogoUrl = getEmailAssetUrl(assetBaseUrl, 'static/logo.png');
return <Img src={documensoLogoUrl} alt="Documenso Logo" className={className} />;
}
@@ -1,6 +1,7 @@
import { Trans } from '@lingui/react/macro';
import { Button, Column, Img, Section, Text } from '../components';
import { getEmailAssetUrl } from '../utils/asset-url';
import { TemplateDocumentImage } from './template-document-image';
export interface TemplateDocumentCompletedProps {
@@ -16,10 +17,6 @@ export const TemplateDocumentCompleted = ({
assetBaseUrl,
customBody,
}: TemplateDocumentCompletedProps) => {
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return (
<>
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
@@ -29,7 +26,7 @@ export const TemplateDocumentCompleted = ({
<Column align="center">
<Text className="font-semibold text-base text-foreground">
<Img
src={getAssetUrl('/static/completed.png')}
src={getEmailAssetUrl(assetBaseUrl, 'static/completed.png')}
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
alt=""
/>
@@ -51,7 +48,11 @@ export const TemplateDocumentCompleted = ({
className="rounded-lg border border-border border-solid px-4 py-2 text-center font-medium text-foreground text-sm no-underline"
href={downloadLink}
>
<Img src={getAssetUrl('/static/download.png')} className="mr-2 mb-0.5 inline h-5 w-5 align-middle" alt="" />
<Img
src={getEmailAssetUrl(assetBaseUrl, 'static/download.png')}
className="mr-2 mb-0.5 inline h-5 w-5 align-middle"
alt=""
/>
<Trans>Download</Trans>
</Button>
</Section>
@@ -1,4 +1,5 @@
import { Column, Img, Row, Section } from '../components';
import { getEmailAssetUrl } from '../utils/asset-url';
export interface TemplateDocumentImageProps {
assetBaseUrl: string;
@@ -6,17 +7,13 @@ export interface TemplateDocumentImageProps {
}
export const TemplateDocumentImage = ({ assetBaseUrl, className }: TemplateDocumentImageProps) => {
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return (
<Section className={className}>
<Row className="table-fixed">
<Column />
<Column>
<Img className="mx-auto h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
<Img className="mx-auto h-42" src={getEmailAssetUrl(assetBaseUrl, 'static/document.png')} alt="Documenso" />
</Column>
<Column />
@@ -1,6 +1,7 @@
import { Trans } from '@lingui/react/macro';
import { Column, Img, Section, Text } from '../components';
import { getEmailAssetUrl } from '../utils/asset-url';
import { TemplateDocumentImage } from './template-document-image';
export interface TemplateDocumentPendingProps {
@@ -9,10 +10,6 @@ export interface TemplateDocumentPendingProps {
}
export const TemplateDocumentPending = ({ documentName, assetBaseUrl }: TemplateDocumentPendingProps) => {
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return (
<>
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
@@ -21,7 +18,11 @@ export const TemplateDocumentPending = ({ documentName, assetBaseUrl }: Template
<Section className="mb-4">
<Column align="center">
<Text className="font-semibold text-base text-foreground">
<Img src={getAssetUrl('/static/clock.png')} className="-mt-0.5 mr-2 inline h-7 w-7 align-middle" alt="" />
<Img
src={getEmailAssetUrl(assetBaseUrl, 'static/clock.png')}
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
alt=""
/>
<Trans>Waiting for others</Trans>
</Text>
</Column>
@@ -1,6 +1,7 @@
import { Trans } from '@lingui/react/macro';
import { Column, Img, Section, Text } from '../components';
import { getEmailAssetUrl } from '../utils/asset-url';
import { TemplateDocumentImage } from './template-document-image';
export interface TemplateDocumentRecipientSignedProps {
@@ -16,10 +17,6 @@ export const TemplateDocumentRecipientSigned = ({
recipientEmail,
assetBaseUrl,
}: TemplateDocumentRecipientSignedProps) => {
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
const recipientReference = recipientName || recipientEmail;
return (
@@ -31,7 +28,7 @@ export const TemplateDocumentRecipientSigned = ({
<Column align="center">
<Text className="font-semibold text-base text-foreground">
<Img
src={getAssetUrl('/static/completed.png')}
src={getEmailAssetUrl(assetBaseUrl, 'static/completed.png')}
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
alt=""
/>
@@ -2,6 +2,7 @@ import { env } from '@documenso/lib/utils/env';
import { Trans } from '@lingui/react/macro';
import { Button, Column, Img, Link, Section, Text } from '../components';
import { getEmailAssetUrl } from '../utils/asset-url';
import { TemplateDocumentImage } from './template-document-image';
export interface TemplateDocumentSelfSignedProps {
@@ -14,10 +15,6 @@ export const TemplateDocumentSelfSigned = ({ documentName, assetBaseUrl }: Templ
const signUpUrl = `${NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signup`;
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return (
<>
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
@@ -27,7 +24,7 @@ export const TemplateDocumentSelfSigned = ({ documentName, assetBaseUrl }: Templ
<Column align="center">
<Text className="font-semibold text-base text-foreground">
<Img
src={getAssetUrl('/static/completed.png')}
src={getEmailAssetUrl(assetBaseUrl, 'static/completed.png')}
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
alt=""
/>
@@ -56,7 +53,7 @@ export const TemplateDocumentSelfSigned = ({ documentName, assetBaseUrl }: Templ
className="mr-4 rounded-lg border border-border border-solid px-4 py-2 text-center font-medium text-foreground text-sm no-underline"
>
<Img
src={getAssetUrl('/static/user-plus.png')}
src={getEmailAssetUrl(assetBaseUrl, 'static/user-plus.png')}
className="mr-2 mb-0.5 inline h-5 w-5 align-middle"
alt=""
/>
@@ -67,7 +64,11 @@ export const TemplateDocumentSelfSigned = ({ documentName, assetBaseUrl }: Templ
className="rounded-lg border border-border border-solid px-4 py-2 text-center font-medium text-foreground text-sm no-underline"
href="https://documenso.com/pricing"
>
<Img src={getAssetUrl('/static/review.png')} className="mr-2 mb-0.5 inline h-5 w-5 align-middle" alt="" />
<Img
src={getEmailAssetUrl(assetBaseUrl, 'static/review.png')}
className="mr-2 mb-0.5 inline h-5 w-5 align-middle"
alt=""
/>
<Trans>View plans</Trans>
</Button>
</Section>
@@ -1,4 +1,5 @@
import { Img } from '../components';
import { getEmailAssetUrl } from '../utils/asset-url';
export interface TemplateImageProps {
assetBaseUrl: string;
@@ -7,11 +8,7 @@ export interface TemplateImageProps {
}
export const TemplateImage = ({ assetBaseUrl, className, staticAsset }: TemplateImageProps) => {
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return <Img className={className} src={getAssetUrl(`/static/${staticAsset}`)} alt="" />;
return <Img className={className} src={getEmailAssetUrl(assetBaseUrl, `static/${staticAsset}`)} alt="" />;
};
export default TemplateImage;
@@ -5,6 +5,7 @@ import { Body, Container, Head, Html, Img, Preview, Section } from '../component
import type { TemplateAdminUserCreatedProps } from '../template-components/template-admin-user-created';
import { TemplateAdminUserCreated } from '../template-components/template-admin-user-created';
import { TemplateFooter } from '../template-components/template-footer';
import { getEmailAssetUrl } from '../utils/asset-url';
export const AdminUserCreatedTemplate = ({
resetPasswordLink,
@@ -14,10 +15,6 @@ export const AdminUserCreatedTemplate = ({
const previewText = msg`Set your password for Documenso`;
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return (
<Html>
<Head />
@@ -27,7 +24,7 @@ export const AdminUserCreatedTemplate = ({
<Section>
<Container className="mx-auto mt-8 mb-2 max-w-xl rounded-lg border border-border border-solid p-4 backdrop-blur-sm">
<Section>
<Img src={getAssetUrl('/static/logo.png')} alt="Documenso Logo" className="mb-4 h-6" />
<Img src={getEmailAssetUrl(assetBaseUrl, 'static/logo.png')} alt="Documenso Logo" className="mb-4 h-6" />
<TemplateAdminUserCreated resetPasswordLink={resetPasswordLink} assetBaseUrl={assetBaseUrl} />
</Section>
+17
View File
@@ -0,0 +1,17 @@
/**
* Resolve a static email asset path against the asset base URL.
*
* The base is normalised to end with a trailing slash and the path is
* normalised to have no leading slash, so a sub-path in the base URL
* (e.g. "/ESign") is preserved. Passing a root-absolute path straight to
* `new URL()` would otherwise replace the base pathname entirely.
*
* `getEmailAssetUrl('https://host/ESign', 'static/logo.png')` -> `https://host/ESign/static/logo.png`
* `getEmailAssetUrl('https://host/ESign/', '/static/logo.png')` -> `https://host/ESign/static/logo.png`
*/
export const getEmailAssetUrl = (assetBaseUrl: string, path: string): string => {
const base = assetBaseUrl.endsWith('/') ? assetBaseUrl : `${assetBaseUrl}/`;
const relativePath = path.startsWith('/') ? path.slice(1) : path;
return new URL(relativePath, base).toString();
};