Skip to content

Commit 67eb04d

Browse files
committed
a super basic pass issuer
1 parent ba0d6b1 commit 67eb04d

File tree

21 files changed

+602
-41
lines changed

21 files changed

+602
-41
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,4 @@ __pycache__
141141
/playwright-report/
142142
/blob-report/
143143
/playwright/.cache/
144+
dist_devel/

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ clean:
5252
build: src/ cloudformation/ docs/
5353
yarn -D
5454
VITE_BUILD_HASH=$(GIT_HASH) yarn build
55+
cp -r src/api/resources/ dist/api/resources
5556
sam build --template-file cloudformation/main.yml
5657

5758
local:

cloudformation/main.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ Resources:
127127
Minify: true
128128
OutExtension:
129129
- .js=.mjs
130+
Loader:
131+
- .png=file
132+
- .pkpass=file
133+
- .json=file
130134
Target: "es2022"
131135
Sourcemap: false
132136
EntryPoints:

generate_jwt.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const payload = {
1818
groups: ["0"],
1919
idp: "https://login.microsoftonline.com",
2020
ipaddr: "192.168.1.1",
21-
name: "John Doe",
21+
name: "Singh, Dev",
2222
oid: "00000000-0000-0000-0000-000000000000",
2323
rh: "rh-value",
2424
scp: "user_impersonation",

src/api/esbuild.config.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { build, context } from 'esbuild';
2+
import { readFileSync } from 'fs';
3+
import { resolve } from 'path';
4+
5+
const isWatching = !!process.argv.includes('--watch')
6+
const nodePackage = JSON.parse(readFileSync(resolve(process.cwd(), 'package.json'), 'utf8'));
7+
8+
const buildOptions = {
9+
entryPoints: [resolve(process.cwd(), 'index.ts')],
10+
outfile: resolve(process.cwd(), '../', '../', 'dist_devel', 'index.js'),
11+
bundle: true,
12+
platform: 'node',
13+
format: 'esm',
14+
external: [
15+
Object.keys(nodePackage.dependencies ?? {}),
16+
Object.keys(nodePackage.peerDependencies ?? {}),
17+
Object.keys(nodePackage.devDependencies ?? {}),
18+
].flat(),
19+
loader: {
20+
'.png': 'file', // Add this line to specify a loader for .png files
21+
},
22+
};
23+
24+
if (isWatching) {
25+
context(buildOptions).then(ctx => {
26+
if (isWatching) {
27+
ctx.watch();
28+
} else {
29+
ctx.rebuild();
30+
}
31+
});
32+
} else {
33+
build(buildOptions)
34+
}

src/api/functions/discord.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import { type EventPostRequest } from "../routes/events.js";
1212
import moment from "moment-timezone";
1313

1414
import { FastifyBaseLogger } from "fastify";
15-
import { DiscordEventError } from "../../common/errors/index.js";
15+
import {
16+
DiscordEventError,
17+
InternalServerError,
18+
} from "../../common/errors/index.js";
1619
import { getSecretValue } from "../plugins/auth.js";
1720
import { genericConfig } from "../../common/config.js";
1821
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
@@ -30,8 +33,15 @@ export const updateDiscord = async (
3033
isDelete: boolean = false,
3134
logger: FastifyBaseLogger,
3235
): Promise<null | GuildScheduledEventCreateOptions> => {
33-
const secretApiConfig =
34-
(await getSecretValue(smClient, genericConfig.ConfigSecretName)) || {};
36+
const secretApiConfig = await getSecretValue(
37+
smClient,
38+
genericConfig.ConfigSecretName,
39+
);
40+
if (!secretApiConfig) {
41+
throw new InternalServerError({
42+
message: "Could not find credentials for Discord.",
43+
});
44+
}
3545
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
3646
let payload: GuildScheduledEventCreateOptions | null = null;
3747

src/api/functions/entraId.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ export async function getEntraIdToken(
3030
clientId: string,
3131
scopes: string[] = ["https://graph.microsoft.com/.default"],
3232
) {
33-
const secretApiConfig =
34-
(await getSecretValue(
35-
fastify.secretsManagerClient,
36-
genericConfig.ConfigSecretName,
37-
)) || {};
33+
const secretApiConfig = await getSecretValue(
34+
fastify.secretsManagerClient,
35+
genericConfig.ConfigSecretName,
36+
);
3837
if (
38+
!secretApiConfig ||
3939
!secretApiConfig.entra_id_private_key ||
4040
!secretApiConfig.entra_id_thumbprint
4141
) {

src/api/functions/mobileWallet.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { getSecretValue } from "../plugins/auth.js";
2+
import { genericConfig } from "../../common/config.js";
3+
import {
4+
InternalServerError,
5+
UnauthorizedError,
6+
} from "../../common/errors/index.js";
7+
import { FastifyInstance, FastifyRequest } from "fastify";
8+
// these make sure that esbuild includes the files
9+
import icon from "../resources/MembershipPass.pkpass/icon.png";
10+
import logo from "../resources/MembershipPass.pkpass/logo.png";
11+
import strip from "../resources/MembershipPass.pkpass/strip.png";
12+
import pass from "../resources/MembershipPass.pkpass/pass.js";
13+
import { PKPass } from "passkit-generator";
14+
import { promises as fs } from "fs";
15+
16+
function trim(s: string) {
17+
return (s || "").replace(/^\s+|\s+$/g, "");
18+
}
19+
20+
function convertName(name: string): string {
21+
if (!name.includes(",")) {
22+
return name;
23+
}
24+
return `${trim(name.split(",")[1])} ${name.split(",")[0]}`;
25+
}
26+
27+
export async function issueAppleWalletMembershipCard(
28+
app: FastifyInstance,
29+
request: FastifyRequest,
30+
email: string,
31+
name: string,
32+
) {
33+
if (!email.endsWith("@illinois.edu")) {
34+
throw new UnauthorizedError({
35+
message:
36+
"Cannot issue membership pass for emails not on the illinois.edu domain.",
37+
});
38+
}
39+
const secretApiConfig = await getSecretValue(
40+
app.secretsManagerClient,
41+
genericConfig.ConfigSecretName,
42+
);
43+
if (!secretApiConfig) {
44+
throw new InternalServerError({
45+
message: "Could not retrieve signing data",
46+
});
47+
}
48+
const signerCert = Buffer.from(
49+
secretApiConfig.acm_passkit_signerCert_base64,
50+
"base64",
51+
).toString("utf-8");
52+
const signerKey = Buffer.from(
53+
secretApiConfig.acm_passkit_signerKey_base64,
54+
"base64",
55+
).toString("utf-8");
56+
const wwdr = Buffer.from(
57+
secretApiConfig.apple_signing_cert_base64,
58+
"base64",
59+
).toString("utf-8");
60+
pass["passTypeIdentifier"] = app.environmentConfig["PasskitIdentifier"];
61+
62+
const pkpass = new PKPass(
63+
{
64+
"icon.png": await fs.readFile(icon),
65+
"logo.png": await fs.readFile(logo),
66+
"strip.png": await fs.readFile(strip),
67+
"pass.json": Buffer.from(JSON.stringify(pass)),
68+
},
69+
{
70+
wwdr,
71+
signerCert,
72+
signerKey,
73+
},
74+
{
75+
// logoText: app.runEnvironment === "dev" ? "INVALID Membership Pass" : "Membership Pass",
76+
serialNumber: app.environmentConfig["PasskitSerialNumber"],
77+
},
78+
);
79+
pkpass.setBarcodes({
80+
altText: email.split("@")[0],
81+
format: "PKBarcodeFormatPDF417",
82+
message: app.runEnvironment === "dev" ? `INVALID${email}INVALID` : email,
83+
});
84+
const iat = new Date().toLocaleDateString("en-US", {
85+
day: "2-digit",
86+
month: "2-digit",
87+
year: "numeric",
88+
});
89+
if (name && name !== "") {
90+
pkpass.secondaryFields.push({
91+
label: "Member Name",
92+
key: "name",
93+
value: convertName(name),
94+
});
95+
}
96+
if (app.runEnvironment === "prod") {
97+
pkpass.backFields.push({
98+
label: "Verification URL",
99+
key: "iss",
100+
value: `https://membership.acm.illinois.edu/verify/${email.split("@")[0]}`,
101+
});
102+
} else {
103+
pkpass.backFields.push({
104+
label: "TESTING ONLY Pass",
105+
key: "iss",
106+
value: `Do not honor!`,
107+
});
108+
}
109+
pkpass.backFields.push({ label: "Pass Created On", key: "iat", value: iat });
110+
pkpass.backFields.push({ label: "Membership ID", key: "id", value: email });
111+
const buffer = pkpass.getAsBuffer();
112+
request.log.info(
113+
{ type: "audit", actor: email, target: email },
114+
"Created membership verification pass",
115+
);
116+
return buffer;
117+
}

src/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
2121
import NodeCache from "node-cache";
2222
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2323
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
24+
import mobileWalletRoute from "./routes/mobileWallet.js";
2425

2526
dotenv.config();
2627

@@ -82,6 +83,7 @@ async function init() {
8283
app.nodeCache = new NodeCache({ checkperiod: 30 });
8384
app.dynamoClient = dynamoClient;
8485
app.secretsManagerClient = secretsManagerClient;
86+
app.secretsManagerData = null;
8587
app.addHook("onRequest", (req, _, done) => {
8688
req.startTime = now();
8789
const hostname = req.hostname;
@@ -110,6 +112,7 @@ async function init() {
110112
api.register(organizationsPlugin, { prefix: "/organizations" });
111113
api.register(icalPlugin, { prefix: "/ical" });
112114
api.register(iamRoutes, { prefix: "/iam" });
115+
api.register(mobileWalletRoute, { prefix: "/mobile" });
113116
api.register(ticketsPlugin, { prefix: "/tickets" });
114117
if (app.runEnvironment === "dev") {
115118
api.register(vendingPlugin, { prefix: "/vending" });

src/api/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"type": "module",
99
"scripts": {
1010
"build": "tsc",
11-
"dev": "cross-env LOG_LEVEL=debug tsx watch index.ts",
11+
"dev": "cross-env LOG_LEVEL=debug node esbuild.config.js --watch & nodemon ../../dist/index.js",
1212
"typecheck": "tsc --noEmit",
1313
"lint": "eslint . --ext .ts --cache",
1414
"prettier": "prettier --check *.ts **/*.ts",
@@ -36,12 +36,14 @@
3636
"moment": "^2.30.1",
3737
"moment-timezone": "^0.5.45",
3838
"node-cache": "^5.1.2",
39+
"passkit-generator": "^3.3.1",
3940
"pluralize": "^8.0.0",
4041
"zod": "^3.23.8",
4142
"zod-to-json-schema": "^3.23.2",
4243
"zod-validation-error": "^3.3.1"
4344
},
4445
"devDependencies": {
45-
"@tsconfig/node22": "^22.0.0"
46+
"@tsconfig/node22": "^22.0.0",
47+
"nodemon": "^3.1.9"
4648
}
4749
}

0 commit comments

Comments
 (0)