Skip to content

Commit c2a4b38

Browse files
committed
Use nodejs for our edge redirect
This will allow us to use the acm-uiuc/js-shared package
1 parent 170d81e commit c2a4b38

File tree

7 files changed

+217
-119
lines changed

7 files changed

+217
-119
lines changed

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
"workspaces": [
77
"src/api",
88
"src/ui",
9-
"src/archival"
9+
"src/archival",
10+
"src/linkryEdgeFunction"
1011
],
1112
"packageManager": "yarn@1.22.22",
1213
"scripts": {
1314
"postinstall": "npm run setup",
1415
"setup": "git config blame.ignoreRevsFile .git-blame-ignore-revs",
15-
"build": "concurrently --names 'api,ui,archival' 'yarn workspace infra-core-api run build' 'yarn workspace infra-core-ui run build' 'yarn workspace infra-core-archival run build'",
16+
"build": "concurrently --names 'api,ui,archival,linkryEdge' 'yarn workspace infra-core-api run build' 'yarn workspace infra-core-ui run build' 'yarn workspace infra-core-archival run build' 'yarn workspace infra-core-linkry-edge run build'",
1617
"postbuild": "node src/api/createLambdaPackage.js && yarn lockfile-manage",
1718
"dev": "cross-env DISABLE_AUDIT_LOG=true concurrently --names 'api,ui' 'yarn workspace infra-core-api run dev' 'yarn workspace infra-core-ui run dev'",
1819
"lockfile-manage": "synp --with-workspace --source-file yarn.lock",
@@ -94,4 +95,4 @@
9495
"pdfjs-dist": "^4.8.69",
9596
"form-data": "^4.0.4"
9697
}
97-
}
98+
}

src/linkryEdgeFunction/build.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* eslint-disable no-console */
2+
import esbuild from "esbuild";
3+
4+
const commonParams = {
5+
bundle: true,
6+
format: "esm",
7+
minify: true,
8+
outExtension: { ".js": ".mjs" },
9+
loader: {
10+
".png": "file",
11+
".pkpass": "file",
12+
".json": "file",
13+
}, // File loaders
14+
target: "es2022", // Target ES2022
15+
sourcemap: true,
16+
platform: "node",
17+
external: ["@aws-sdk/*"],
18+
banner: {
19+
js: `
20+
import path from 'path';
21+
import { fileURLToPath } from 'url';
22+
import { createRequire as topLevelCreateRequire } from 'module';
23+
const require = topLevelCreateRequire(import.meta.url);
24+
const __filename = fileURLToPath(import.meta.url);
25+
const __dirname = path.dirname(__filename);
26+
`.trim(),
27+
}, // Banner for compatibility with CommonJS
28+
};
29+
30+
esbuild
31+
.build({
32+
...commonParams,
33+
entryPoints: ["linkryEdgeFunction/index.js"],
34+
outdir: "../../dist/linkryEdgeFunction/",
35+
})
36+
.then(() =>
37+
console.log("Linkry Edge Function lambda build completed successfully!"),
38+
)
39+
.catch((error) => {
40+
console.error("Linkry Edge Function lambda build failed:", error);
41+
process.exit(1);
42+
});

src/linkryEdgeFunction/index.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import {
2+
DynamoDBClient,
3+
QueryCommand,
4+
QueryCommandInput,
5+
} from "@aws-sdk/client-dynamodb";
6+
import type {
7+
CloudFrontRequestEvent,
8+
CloudFrontRequestResult,
9+
} from "aws-lambda";
10+
11+
const DEFAULT_AWS_REGION = "us-east-2";
12+
const AVAILABLE_REPLICAS = ["us-west-2"];
13+
const DYNAMODB_TABLE = "infra-core-api-linkry";
14+
const FALLBACK_URL = process.env.FALLBACK_URL || "https://acm.illinois.edu/404";
15+
const DEFAULT_URL = process.env.DEFAULT_URL || "https://www.acm.illinois.edu";
16+
const CACHE_TTL = "30"; // seconds to hold response in PoP
17+
18+
/**
19+
* Determine which DynamoDB replica to use based on Lambda execution region
20+
*/
21+
function selectReplica(lambdaRegion: string): string {
22+
// First check if Lambda is running in a replica region
23+
if (AVAILABLE_REPLICAS.includes(lambdaRegion)) {
24+
return lambdaRegion;
25+
}
26+
27+
// Otherwise, find nearest replica by region prefix matching
28+
const regionPrefix = lambdaRegion.split("-").slice(0, 2).join("-");
29+
if (regionPrefix === "us") {
30+
return DEFAULT_AWS_REGION;
31+
}
32+
33+
for (const replica of AVAILABLE_REPLICAS) {
34+
if (replica.startsWith(regionPrefix)) {
35+
return replica;
36+
}
37+
}
38+
39+
return DEFAULT_AWS_REGION;
40+
}
41+
42+
const currentRegion = process.env.AWS_REGION || DEFAULT_AWS_REGION;
43+
const targetRegion = selectReplica(currentRegion);
44+
const dynamodb = new DynamoDBClient({ region: targetRegion });
45+
46+
console.log(`Lambda in ${currentRegion}, routing DynamoDB to ${targetRegion}`);
47+
48+
export const handler = async (
49+
event: CloudFrontRequestEvent,
50+
): Promise<CloudFrontRequestResult> => {
51+
const request = event.Records[0].cf.request;
52+
const path = request.uri.replace(/^\/+/, "");
53+
54+
console.log(`Processing path: ${path}`);
55+
56+
if (!path) {
57+
return {
58+
status: "301",
59+
statusDescription: "Moved Permanently",
60+
headers: {
61+
location: [{ key: "Location", value: DEFAULT_URL }],
62+
"cache-control": [
63+
{ key: "Cache-Control", value: `public, max-age=${CACHE_TTL}` },
64+
],
65+
},
66+
};
67+
}
68+
69+
// Query DynamoDB for records with PK=path and SK starting with "OWNER#"
70+
try {
71+
const queryParams: QueryCommandInput = {
72+
TableName: DYNAMODB_TABLE,
73+
KeyConditionExpression:
74+
"slug = :slug AND begins_with(access, :owner_prefix)",
75+
ExpressionAttributeValues: {
76+
":slug": { S: path },
77+
":owner_prefix": { S: "OWNER#" },
78+
},
79+
ProjectionExpression: "redirect",
80+
Limit: 1, // We only need one result
81+
};
82+
83+
const response = await dynamodb.send(new QueryCommand(queryParams));
84+
85+
if (response.Items && response.Items.length > 0) {
86+
const item = response.Items[0];
87+
88+
// Extract the redirect URL from the item
89+
const redirectUrl = item.redirect?.S;
90+
91+
if (redirectUrl) {
92+
console.log(`Found redirect: ${path} -> ${redirectUrl}`);
93+
return {
94+
status: "302",
95+
statusDescription: "Found",
96+
headers: {
97+
location: [{ key: "Location", value: redirectUrl }],
98+
"cache-control": [
99+
{ key: "Cache-Control", value: `public, max-age=${CACHE_TTL}` },
100+
],
101+
},
102+
};
103+
}
104+
console.log(`Item found but no redirect attribute for path: ${path}`);
105+
} else {
106+
console.log(`No items found for path: ${path}`);
107+
}
108+
} catch (error) {
109+
if (error instanceof Error) {
110+
console.error(
111+
`DynamoDB query failed for ${path} in region ${targetRegion}:`,
112+
error.message,
113+
);
114+
} else {
115+
console.error(`Unexpected error:`, error);
116+
}
117+
}
118+
119+
// Not found - redirect to fallback
120+
return {
121+
status: "307",
122+
statusDescription: "Temporary Redirect",
123+
headers: {
124+
location: [{ key: "Location", value: FALLBACK_URL }],
125+
"cache-control": [
126+
{ key: "Cache-Control", value: `public, max-age=${CACHE_TTL}` },
127+
],
128+
},
129+
};
130+
};

src/linkryEdgeFunction/main.py

Lines changed: 0 additions & 113 deletions
This file was deleted.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "infra-core-linkry-edge",
3+
"version": "1.0.0",
4+
"description": "Handles edge redirects",
5+
"type": "module",
6+
"main": "index.js",
7+
"author": "ACM@UIUC",
8+
"license": "BSD-3-Clause",
9+
"scripts": {
10+
"build": "tsc && node build.js",
11+
"prettier": "prettier --check *.ts **/*.ts",
12+
"lint": "eslint . --ext .ts --cache",
13+
"prettier:write": "prettier --write *.ts **/*.ts"
14+
},
15+
"devDependencies": {
16+
"@types/aws-lambda": "^8.10.138",
17+
"@types/node": "^24.3.0",
18+
"typescript": "^5.9.2",
19+
"esbuild": "^0.25.12"
20+
},
21+
"dependencies": {
22+
"@aws-sdk/client-dynamodb": "^3.922.0"
23+
}
24+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"extends": "@tsconfig/node22/tsconfig.json",
3+
"compilerOptions": {
4+
"module": "Node16",
5+
"rootDir": "../",
6+
"outDir": "../../dist",
7+
"baseUrl": "../"
8+
},
9+
"ts-node": {
10+
"esm": true
11+
},
12+
"include": ["../api/**/*.ts", "../common/**/*.ts"],
13+
"exclude": ["../../node_modules", "../../dist"]
14+
}

terraform/modules/lambdas/main.tf

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ data "archive_file" "sqs_lambda_code" {
1212

1313
data "archive_file" "linkry_edge_lambda_code" {
1414
type = "zip"
15-
source_dir = "${path.module}/../../../src/linkryEdgeFunction/"
15+
source_dir = "${path.module}/../../../dist/linkryEdgeFunction/"
1616
output_path = "${path.module}/../../../dist/terraform/linkryEdgeFunction.zip"
1717
}
1818

@@ -509,8 +509,8 @@ resource "aws_lambda_function" "linkry_edge" {
509509
filename = data.archive_file.linkry_edge_lambda_code.output_path
510510
function_name = "${var.ProjectId}-linkry-edge"
511511
role = aws_iam_role.linkry_lambda_edge_role[0].arn
512-
handler = "main.handler"
513-
runtime = "python3.12"
512+
handler = "index.handler"
513+
runtime = "nodejs22.x"
514514
publish = true
515515
timeout = 5
516516
memory_size = 128

0 commit comments

Comments
 (0)