Use root-relative base url for app and extensions (#6923)

* Add Url util class

* Use relative base url for app and extensions

Also use utils/url when working with PUBLIC_URL in other places.
This commit is contained in:
Nicola Krumschmidt
2021-08-26 23:11:21 +02:00
committed by GitHub
parent 5970a7b473
commit 7dfc5dc6af
5 changed files with 102 additions and 20 deletions

View File

@@ -45,15 +45,13 @@ import { validateStorage } from './utils/validate-storage';
import { register as registerWebhooks } from './webhooks';
import { session } from './middleware/session';
import { flushCaches } from './cache';
import { URL } from 'url';
import { Url } from './utils/url';
export default async function createApp(): Promise<express.Application> {
validateEnv(['KEY', 'SECRET']);
try {
new URL(env.PUBLIC_URL);
} catch {
logger.warn('PUBLIC_URL is not a valid URL');
if (!new Url(env.PUBLIC_URL).isAbsolute()) {
logger.warn('PUBLIC_URL should be a full URL');
}
await validateStorage();
@@ -126,11 +124,14 @@ export default async function createApp(): Promise<express.Application> {
if (env.SERVE_APP) {
const adminPath = require.resolve('@directus/app/dist/index.html');
const publicUrl = env.PUBLIC_URL.endsWith('/') ? env.PUBLIC_URL : env.PUBLIC_URL + '/';
const adminUrl = new Url(env.PUBLIC_URL).addPath('admin');
// Set the App's base path according to the APIs public URL
let html = fse.readFileSync(adminPath, 'utf-8');
html = html.replace(/<meta charset="utf-8" \/>/, `<meta charset="utf-8" />\n\t\t<base href="${publicUrl}admin/">`);
html = html.replace(
/<meta charset="utf-8" \/>/,
`<meta charset="utf-8" />\n\t\t<base href="${adminUrl.toString({ rootRelative: true })}/">`
);
app.get('/admin', (req, res) => res.send(html));
app.use('/admin', express.static(path.join(adminPath, '..')));

View File

@@ -32,6 +32,7 @@ import { rollup } from 'rollup';
// @ts-expect-error
import virtual from '@rollup/plugin-virtual';
import alias from '@rollup/plugin-alias';
import { Url } from './utils/url';
let extensions: Extension[] = [];
let extensionBundles: Partial<Record<AppExtensionType, string>> = {};
@@ -120,14 +121,15 @@ async function generateExtensionBundles() {
async function getSharedDepsMapping(deps: string[]) {
const appDir = await fse.readdir(path.join(resolvePackage('@directus/app'), 'dist'));
const adminUrl = env.PUBLIC_URL.endsWith('/') ? env.PUBLIC_URL + 'admin' : env.PUBLIC_URL + '/admin';
const depsMapping: Record<string, string> = {};
for (const dep of deps) {
const depName = appDir.find((file) => dep.replace(/\//g, '_') === file.substring(0, file.indexOf('.')));
if (depName) {
depsMapping[dep] = `${adminUrl}/${depName}`;
const depUrl = new Url(env.PUBLIC_URL).addPath('admin', depName);
depsMapping[dep] = depUrl.toString({ rootRelative: true });
} else {
logger.warn(`Couldn't find shared extension dependency "${dep}"`);
}

View File

@@ -11,6 +11,7 @@ import { Accountability } from '@directus/shared/types';
import getMailer from '../../mailer';
import { Transporter, SendMailOptions } from 'nodemailer';
import prettier from 'prettier';
import { Url } from '../../utils/url';
const liquidEngine = new Liquid({
root: [path.resolve(env.EXTENSIONS_PATH, 'templates'), path.resolve(__dirname, 'templates')],
@@ -100,16 +101,15 @@ export class MailService {
};
function getProjectLogoURL(logoID?: string) {
let projectLogoURL = env.PUBLIC_URL;
if (projectLogoURL.endsWith('/') === false) {
projectLogoURL += '/';
}
const projectLogoUrl = new Url(env.PUBLIC_URL);
if (logoID) {
projectLogoURL += `assets/${logoID}`;
projectLogoUrl.addPath('assets', logoID);
} else {
projectLogoURL += `admin/img/directus-white.png`;
projectLogoUrl.addPath('admin', 'img', 'directus-white.png');
}
return projectLogoURL;
return projectLogoUrl.toString();
}
}
}

View File

@@ -17,6 +17,7 @@ import { AbstractServiceOptions, Item, PrimaryKey, Query, SchemaOverview } from
import { Accountability } from '@directus/shared/types';
import isUrlAllowed from '../utils/is-url-allowed';
import { toArray } from '@directus/shared/utils';
import { Url } from '../utils/url';
import { AuthenticationService } from './authentication';
import { ItemsService, MutationOptions } from './items';
import { MailService } from './mail';
@@ -305,9 +306,9 @@ export class UsersService extends ItemsService {
const payload = { email, scope: 'invite' };
const token = jwt.sign(payload, env.SECRET as string, { expiresIn: '7d' });
const inviteURL = url ?? env.PUBLIC_URL + '/admin/accept-invite';
const acceptURL = inviteURL + '?token=' + token;
const subjectLine = subject ? subject : "You've been invited";
const subjectLine = subject ?? "You've been invited";
const inviteURL = url ? new Url(url) : new Url(env.PUBLIC_URL).addPath('admin', 'accept-invite');
inviteURL.setQuery('token', token);
await mailService.send({
to: email,
@@ -315,7 +316,7 @@ export class UsersService extends ItemsService {
template: {
name: 'user-invitation',
data: {
url: acceptURL,
url: inviteURL.toString(),
email,
},
},

78
api/src/utils/url.ts Normal file
View File

@@ -0,0 +1,78 @@
import { URL } from 'url';
export class Url {
protocol: string | null;
host: string | null;
port: string | null;
path: string[];
query: Record<string, string>;
hash: string | null;
constructor(url: string) {
const parsedUrl = new URL(url, 'http://localhost');
const isProtocolRelative = /^\/\//.test(url);
const isRootRelative = /^\/$|^\/[^/]/.test(url);
const isPathRelative = /^\./.test(url);
this.protocol =
!isProtocolRelative && !isRootRelative && !isPathRelative
? parsedUrl.protocol.substring(0, parsedUrl.protocol.length - 1)
: null;
this.host = !isRootRelative && !isPathRelative ? parsedUrl.host : null;
this.port = parsedUrl.port !== '' ? parsedUrl.port : null;
this.path = parsedUrl.pathname.split('/').filter((p) => p !== '');
this.query = Object.fromEntries(parsedUrl.searchParams.entries());
this.hash = parsedUrl.hash !== '' ? parsedUrl.hash.substring(1) : null;
}
public isAbsolute(): boolean {
return this.protocol !== null && this.host !== null;
}
public isProtocolRelative(): boolean {
return this.protocol === null && this.host !== null;
}
public isRootRelative(): boolean {
return this.protocol === null && this.host === null;
}
public addPath(...paths: string[]): Url {
const pathToAdd = paths.flatMap((p) => p.split('/')).filter((p) => p !== '');
for (const pathSegment of pathToAdd) {
if (pathSegment === '..') {
this.path.pop();
} else if (pathSegment !== '.') {
this.path.push(pathSegment);
}
}
return this;
}
public setQuery(key: string, value: string): Url {
this.query[key] = value;
return this;
}
public toString({ rootRelative } = { rootRelative: false }): string {
const protocol = this.protocol !== null ? `${this.protocol}:` : '';
const host = this.host ?? '';
const port = this.port !== null ? `:${this.port}` : '';
const origin = `${this.host !== null ? `${protocol}//` : ''}${host}${port}`;
const path = `/${this.path.join('/')}`;
const query =
Object.keys(this.query).length !== 0
? `?${Object.entries(this.query)
.map(([k, v]) => `${k}=${v}`)
.join('&')}`
: '';
const hash = this.hash !== null ? `#${this.hash}` : '';
return `${!rootRelative ? origin : ''}${path}${query}${hash}`;
}
}