Skip to content

Commit

Permalink
feat: custom headers per branch or site (#195)
Browse files Browse the repository at this point in the history
  • Loading branch information
Geoffroy Empain committed Mar 10, 2021
1 parent bc5e3ff commit c8645a7
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 36 deletions.
29 changes: 17 additions & 12 deletions src/caddy/config/sites/generate-site-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,6 @@ function get401ErrorRoute() {
};
}

// https://caddyserver.com/docs/json/apps/http/servers/routes/handle/headers/
const cacheHandler = {
handler: 'headers',
response: {
set: {
'Cache-Control': ['public', 'max-age=0', 'must-revalidate'],
},
},
};

// https://caddyserver.com/docs/json/apps/http/servers/routes/handle/encode/encodings/gzip/
// https://caddy.community/t/gzip-headers-when-using-encode-handler/11781
const gzipHandler = {
Expand All @@ -106,6 +96,21 @@ function getPrimaryRoute(site: Site, branch: Branch) {
root: branchDirInCaddy,
};

// https://caddyserver.com/docs/json/apps/http/servers/routes/handle/headers/
const headersHandler = {
handler: 'headers',
response: {
set: {
'Cache-Control': ['public', 'max-age=0', 'must-revalidate'],
...[...(site.headers || []), ...(branch.headers || [])].reduce((prev, { name, value }) => {
// TODO should this be split by comma ?
prev[name] = [value];
return prev;
}, {}),
},
},
};

if (site.spa) {
return {
match: [{
Expand All @@ -122,7 +127,7 @@ function getPrimaryRoute(site: Site, branch: Branch) {
handler: 'rewrite',
uri: '{http.matchers.file.relative}',
},
cacheHandler,
headersHandler,
gzipHandler,
fileHandler,
],
Expand All @@ -131,7 +136,7 @@ function getPrimaryRoute(site: Site, branch: Branch) {

return {
handle: [
cacheHandler,
headersHandler,
gzipHandler,
fileHandler,
],
Expand Down
2 changes: 2 additions & 0 deletions src/entities/api/api-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export enum ApiScope {
site_read = 'site.read',
site_update = 'site.update',
site_delete = 'site.delete',
site_headers_set = 'site.headers.set',
site_logo_set = 'site.logo.set',
site_logo_remove = 'site.logo.remove',
site_logo_get = 'site.logo.get',
Expand All @@ -67,6 +68,7 @@ export enum ApiScope {
site_branch_password_remove = 'site.branch.password.remove',
site_branch_redirects_read = 'site.branch.redirects.read',
site_branch_redirects_set = 'site.branch.redirects.set',
site_branch_headers_set = 'site.branch.headers.set',
site_hook_list = 'site.hook.list',
site_hook_create = 'site.hook.create',
site_hook_read = 'site.hook.read',
Expand Down
2 changes: 2 additions & 0 deletions src/entities/sites/branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { string } from 'joi';
import { STRING_MAX_LENGTH } from '../../constants';
import { Redirect } from './redirect';
import { Password } from './password';
import { Header } from './header';

export interface Branch {
_id: string;
Expand All @@ -10,6 +11,7 @@ export interface Branch {
release?: string;
password?: Password;
redirects?: Redirect[];
headers?: Header[];
}

export const $branchName = string().required().max(STRING_MAX_LENGTH);
Original file line number Diff line number Diff line change
@@ -1,50 +1,38 @@
import { Request, Response } from 'express';
import { branchExistsGuard } from '../../guards/branch-exists-guard';
import { object, string } from 'joi';
import { STRING_MAX_LENGTH } from '../../../../constants';
import { array, object } from 'joi';
import { ARRAY_MAX } from '../../../../constants';
import { emitEvent } from '../../../../events/emit-event';
import { EventType } from '../../../../events/event-type';
import { wrapAsyncMiddleware } from '../../../../commons/utils/wrap-async-middleware';
import { body } from '../../../../commons/express-joi/body';
import { BadRequestError } from '../../../../commons/errors/bad-request-error';
import { canAdminSiteGuard } from '../../guards/can-admin-site-guard';
import { Sites } from '../../site';
import { serializeBranch } from '../../serialize-branch';
import { configureSiteBranchInCaddy } from '../../../../caddy/configuration';
import { Logger } from '../../../../commons/logger/logger';

async function releaseExists(siteId: string, branchId: string): Promise<boolean> {
const count = await Sites().countDocuments({
_id: siteId,
'branches._id': branchId,
}, {
limit: 1,
});
return count === 1;
}
import { $header } from '../../header';

const validators = [
body(object({
release: string().optional().max(STRING_MAX_LENGTH),
headers: array().min(0).max(ARRAY_MAX).optional()
.default([])
.items($header)
.unique((a, b) => a.name.toLowerCase() === b.name.toLowerCase()),
})),
];

const logger = new Logger('meli.api:updateBranch');
const logger = new Logger('meli.api:setBranchHeaders');

async function handler(req: Request, res: Response): Promise<void> {
const { siteId, branchId } = req.params;

const releaseBranchExists = await releaseExists(siteId, req.body.mainBranch);
if (!releaseBranchExists) {
throw new BadRequestError('Release not found');
}

await Sites().updateOne({
_id: siteId,
'branches._id': branchId,
}, {
$set: {
'branches.$.release': req.body.release,
'branches.$.headers': req.body.headers,
},
});

Expand All @@ -57,14 +45,15 @@ async function handler(req: Request, res: Response): Promise<void> {
logger.error(err);
});

emitEvent(EventType.site_updated, {
emitEvent(EventType.site_branch_updated, {
site,
branch,
});

res.json(serializeBranch(site, branch));
}

export const updateBranch = [
export const setBranchHeaders = [
...branchExistsGuard,
...canAdminSiteGuard,
...validators,
Expand Down
11 changes: 11 additions & 0 deletions src/entities/sites/header.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { object, string } from 'joi';

export interface Header {
name: string;
value: string;
}

export const $header = object({
name: string().required(),
value: string().optional().empty(''),
});
20 changes: 20 additions & 0 deletions src/entities/sites/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { removeSitePassword } from './handlers/remove-site-password';
import { removeSiteLogo } from './handlers/remove-site-logo';
import { setSiteLogo } from './handlers/set-site-logo';
import { getSiteLogo } from './handlers/get-site-logo';
import { setBranchHeaders } from './handlers/branches/set-branch-headers';
import { setSiteHeaders } from './set-site-headers';

const router = Router();

Expand Down Expand Up @@ -74,6 +76,15 @@ apiEndpoint({
apiScope: ApiScope.site_delete,
router,
});
apiEndpoint({
name: 'set site headers',
method: 'put',
path: '/api/v1/sites/:siteId/headers',
handler: setSiteHeaders,
auth: true,
apiScope: ApiScope.site_headers_set,
router,
});

// logo
apiEndpoint({
Expand Down Expand Up @@ -269,6 +280,15 @@ apiEndpoint({
apiScope: ApiScope.site_branch_redirects_set,
router,
});
apiEndpoint({
name: 'set branch headers',
method: 'put',
path: '/api/v1/sites/:siteId/branches/:branchId/headers',
handler: setBranchHeaders,
auth: true,
apiScope: ApiScope.site_branch_headers_set,
router,
});

// hooks
apiEndpoint({
Expand Down
1 change: 1 addition & 0 deletions src/entities/sites/serialize-branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ export function serializeBranch(site: Site, branch: Branch) {
release: branch.release,
hasPassword: !!branch.password,
url: getBranchUrl(site, branch),
headers: branch.headers || [],
};
}
1 change: 1 addition & 0 deletions src/entities/sites/serialize-site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ export function serializeSite(site: Site): any {
url: getSiteUrl(site),
spa: site.spa,
hasPassword: !!site.password,
headers: site.headers || [],
};
}
58 changes: 58 additions & 0 deletions src/entities/sites/set-site-headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Request, Response } from 'express';
import { array, object } from 'joi';
import { ARRAY_MAX } from '../../constants';
import { $header } from './header';
import { emitEvent } from '../../events/emit-event';
import { EventType } from '../../events/event-type';
import { wrapAsyncMiddleware } from '../../commons/utils/wrap-async-middleware';
import { configureSiteInCaddy } from '../../caddy/configuration';
import { body } from '../../commons/express-joi/body';
import { canAdminSiteGuard } from './guards/can-admin-site-guard';
import { Sites } from './site';
import { Logger } from '../../commons/logger/logger';
import { serializeSite } from './serialize-site';
import { siteExistsGuard } from './guards/site-exists-guard';

const validators = [
body(object({
headers: array().min(0).max(ARRAY_MAX).optional()
.default([])
.items($header)
.unique((a, b) => a.name.toLowerCase() === b.name.toLowerCase()),
})),
];

const logger = new Logger('meli.api:setBranchHeaders');

async function handler(req: Request, res: Response): Promise<void> {
const { siteId } = req.params;

await Sites().updateOne({
_id: siteId,
}, {
$set: {
headers: req.body.headers,
},
});

const site = await Sites().findOne({
_id: siteId,
});

configureSiteInCaddy(site).catch(err => {
logger.error(err);
});

emitEvent(EventType.site_updated, {
site,
});

res.json(serializeSite(site));
}

export const setSiteHeaders = [
...siteExistsGuard,
...canAdminSiteGuard,
...validators,
wrapAsyncMiddleware(handler),
];
7 changes: 6 additions & 1 deletion src/entities/sites/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AppDb } from '../../db/db';
import { Branch } from './branch';
import { Password } from './password';
import { StoredFile } from '../../storage/store-file';
import { $header, Header } from './header';

export interface SiteToken {
_id: string;
Expand Down Expand Up @@ -49,6 +50,7 @@ export interface Site {
hooks: string[];
spa?: boolean;
password?: Password;
headers?: Header[];
}

export const Sites = () => AppDb.db.collection<Site>('sites');
Expand Down Expand Up @@ -78,7 +80,7 @@ export const $siteDomain = object({
exposeBranches: boolean().optional().default(false),
});

export const $site = object({
export const $site = object<Site>({
name: $siteName,
color: string().required().regex(COLOR_PATTERN),
mainBranch: string()
Expand All @@ -88,4 +90,7 @@ export const $site = object({
.default([])
.items($siteDomain),
spa: boolean().optional().default(false),
headers: array().min(0).max(ARRAY_MAX).optional()
.default([])
.items($header),
});

0 comments on commit c8645a7

Please sign in to comment.