Skip to content

Commit 215e58a

Browse files
committed
fix(pulumi-aws): add support for website redirects
1 parent 32e742e commit 215e58a

File tree

7 files changed

+115
-114
lines changed

7 files changed

+115
-114
lines changed

packages/pulumi-aws/src/apps/tenantRouter.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,58 @@ import { PulumiApp, PulumiAppResource } from "@webiny/pulumi";
55
import { CoreOutput } from "./common";
66
import { LAMBDA_RUNTIME } from "~/constants";
77
import { getEnvVariableAwsRegion } from "~/env/awsRegion";
8+
import type { WebsiteRedirect } from "~/apps/website/index.js";
89

910
interface Params {
1011
region: string;
1112
dynamoDbTable: string;
13+
redirects: WebsiteRedirect[];
1214
}
1315

14-
function createFunctionArchive({ dynamoDbTable, region }: Params) {
16+
const PERMANENT_CACHE = 31536000;
17+
const TEMPORARY_CACHE = 86400;
18+
19+
function createFunctionArchive({ dynamoDbTable, region, redirects }: Params) {
1520
const handler = readFileSync(
1621
__dirname + "/../components/tenantRouter/functions/origin/request.js",
1722
"utf-8"
1823
);
1924

25+
const matchRedirect = readFileSync(
26+
__dirname + "/../components/tenantRouter/functions/origin/matchRedirect.js",
27+
"utf-8"
28+
);
29+
30+
const redirectsMap = redirects.reduce((acc, redirect) => {
31+
const maxAge = redirect.permanent ? PERMANENT_CACHE : TEMPORARY_CACHE;
32+
33+
return {
34+
...acc,
35+
[redirect.from]: {
36+
to: redirect.to,
37+
permanent: redirect.permanent,
38+
maxAge: redirect.maxAge ?? maxAge
39+
}
40+
};
41+
}, {});
42+
2043
const source = handler
2144
.replace("{DB_TABLE_NAME}", dynamoDbTable)
2245
.replace("{DB_TABLE_REGION}", region);
2346

2447
return new pulumi.asset.AssetArchive({
25-
"index.js": new pulumi.asset.StringAsset(source)
48+
"index.js": new pulumi.asset.StringAsset(source),
49+
"matchRedirect.js": new pulumi.asset.StringAsset(matchRedirect),
50+
"redirects.json": new pulumi.asset.StringAsset(JSON.stringify(redirectsMap))
2651
});
2752
}
2853

2954
const PREFIX = "website-router";
3055

3156
export function applyTenantRouter(
3257
app: PulumiApp,
33-
cloudfront: PulumiAppResource<typeof aws.cloudfront.Distribution>
58+
cloudfront: PulumiAppResource<typeof aws.cloudfront.Distribution>,
59+
redirects: WebsiteRedirect[]
3460
) {
3561
const region = getEnvVariableAwsRegion();
3662

@@ -100,7 +126,8 @@ export function applyTenantRouter(
100126
code: dynamoDbTable.apply(dynamoDbTable => {
101127
return createFunctionArchive({
102128
region,
103-
dynamoDbTable
129+
dynamoDbTable,
130+
redirects
104131
});
105132
})
106133
},

packages/pulumi-aws/src/apps/website/createWebsitePulumiApp.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ import { getEnvVariableWebinyProjectName } from "~/env/projectName";
1616

1717
export type WebsitePulumiApp = ReturnType<typeof createWebsitePulumiApp>;
1818

19+
export type WebsiteRedirect = {
20+
from: string;
21+
to: string;
22+
permanent: boolean;
23+
/**
24+
* Cache duration in seconds.
25+
* For permanent redirects, the default value is 1 year (31536000 seconds).
26+
* For temporary redirects, the default value is 1 day (86400 seconds).
27+
*/
28+
maxAge?: number;
29+
};
30+
1931
export interface CreateWebsitePulumiAppParams {
2032
/**
2133
* Custom domain(s) configuration.
@@ -39,6 +51,11 @@ export interface CreateWebsitePulumiAppParams {
3951
*/
4052
pulumi?: (app: WebsitePulumiApp) => void | Promise<void>;
4153

54+
/**
55+
* Define redirects to be handled by website Lambda@Edge function.
56+
*/
57+
redirects?: () => Promise<WebsiteRedirect[]> | WebsiteRedirect[];
58+
4259
/**
4360
* Prefixes names of all Pulumi cloud infrastructure resource with given prefix.
4461
*/
@@ -271,7 +288,11 @@ export const createWebsitePulumiApp = (projectAppParams: CreateWebsitePulumiAppP
271288
process.env.WCP_PROJECT_ENVIRONMENT ||
272289
process.env.WEBINY_MULTI_TENANCY === "true"
273290
) {
274-
const { originLambda } = applyTenantRouter(app, deliveryCloudfront);
291+
const redirects = projectAppParams.redirects
292+
? await projectAppParams.redirects()
293+
: [];
294+
295+
const { originLambda } = applyTenantRouter(app, deliveryCloudfront, redirects);
275296

276297
app.addHandler(() => {
277298
app.addOutputs({

packages/pulumi-aws/src/components/tenantRouter/WebsiteTenantRouter.ts

Lines changed: 0 additions & 107 deletions
This file was deleted.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
exports.matchRedirect = (request, redirects) => {
2+
const headers = request.headers;
3+
4+
// Build the full URL from the request
5+
const protocol = headers["cloudfront-forwarded-proto"]
6+
? headers["cloudfront-forwarded-proto"][0].value
7+
: "https";
8+
const hostname = headers.host[0].value;
9+
const uri = request.uri.replace("/index.html", "");
10+
const querystring = request.querystring ? `?${request.querystring}` : "";
11+
12+
const fullUrl = `${protocol}://${hostname}${uri}${querystring}`;
13+
const fullUrlNoQuery = `${protocol}://${hostname}${uri}`;
14+
15+
// Try to find a match (priority: with query > without query > path only)
16+
let redirectConfig = redirects[fullUrl] || redirects[fullUrlNoQuery] || redirects[uri];
17+
18+
// If no match found, return null
19+
if (!redirectConfig) {
20+
return null;
21+
}
22+
23+
// Return a function that generates the redirect response
24+
return () => {
25+
// Build redirect URL
26+
let redirectTo = redirectConfig.to;
27+
28+
// If redirect.to is a path (not full URL), prepend current domain
29+
if (!redirectTo.startsWith("http://") && !redirectTo.startsWith("https://")) {
30+
redirectTo = `${protocol}://${hostname}${redirectTo}`;
31+
}
32+
33+
console.log("REDIRECTING!", redirectTo);
34+
35+
return {
36+
status: redirectConfig.permanent ? "301" : "302",
37+
statusDescription: redirectConfig.permanent ? "Moved Permanently" : "Found",
38+
headers: {
39+
location: [
40+
{
41+
key: "Location",
42+
value: redirectTo
43+
}
44+
],
45+
"cache-control": [
46+
{
47+
key: "Cache-Control",
48+
value: "max-age=" + redirectConfig.maxAge
49+
}
50+
]
51+
}
52+
};
53+
};
54+
};

packages/pulumi-aws/src/components/tenantRouter/functions/origin/request.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
22
const { DynamoDBDocument, QueryCommand, GetCommand } = require("@aws-sdk/lib-dynamodb");
3+
const redirects = require("./redirects.json");
4+
const { matchRedirect } = require("./matchRedirect.js");
35

46
// Since Lambda@Edge doesn't support ENV variables, the easiest way to pass
57
// config values to it is to inject them into the source code before deploy.
@@ -83,6 +85,11 @@ async function handleOriginRequest(request) {
8385
const requestedDomain = request.headers.host[0].value;
8486
const originDomain = isCustomOrigin ? origin.custom.domainName : origin.s3.domainName;
8587

88+
const redirect = matchRedirect(request, redirects);
89+
if (redirect) {
90+
return redirect();
91+
}
92+
8693
let tenant;
8794
if (await hasMultipleTenants()) {
8895
// Find tenant by domain. This record is stored to the DB using the Tenant Manager app.

packages/pulumi-aws/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
export { WebsiteTenantRouter } from "./components/tenantRouter/WebsiteTenantRouter";
21
export * from "./apps";
32
export * from "./utils";
43
export * from "./constants";

packages/serverless-cms-aws/src/createWebsiteApp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export interface CreateWebsiteAppParams extends CreateWebsitePulumiAppParams {
1616
plugins?: PluginCollection;
1717
}
1818

19-
interface CreateWebsiteAppResult {
19+
export interface CreateWebsiteAppResult {
2020
id: string;
2121
name: string;
2222
description: string;

0 commit comments

Comments
 (0)