From 9b03eba08b6ea1dfd8739d7ccb184ad76fa5cad3 Mon Sep 17 00:00:00 2001 From: fehervaryy Date: Wed, 9 Jul 2025 23:08:38 -0400 Subject: [PATCH 1/8] api for apple passes --- package-lock.json | 37 ++++++++++ package.json | 1 + src/assets/passes/.gitignore | 1 + src/assets/passes/apple.pass/icon.png | Bin 0 -> 2069 bytes src/assets/passes/apple.pass/icon@2x.png | Bin 0 -> 7649 bytes src/assets/passes/apple.pass/logo.png | Bin 0 -> 2575 bytes src/assets/passes/apple.pass/logo@2x.png | Bin 0 -> 3032 bytes src/assets/passes/apple.pass/manifest.json | 8 +++ src/assets/passes/apple.pass/pass.json | 59 ++++++++++++++++ src/index.ts | 2 + src/routes/passes.ts | 78 +++++++++++++++++++++ 11 files changed, 186 insertions(+) create mode 100644 src/assets/passes/.gitignore create mode 100644 src/assets/passes/apple.pass/icon.png create mode 100644 src/assets/passes/apple.pass/icon@2x.png create mode 100644 src/assets/passes/apple.pass/logo.png create mode 100644 src/assets/passes/apple.pass/logo@2x.png create mode 100644 src/assets/passes/apple.pass/manifest.json create mode 100644 src/assets/passes/apple.pass/pass.json create mode 100644 src/routes/passes.ts diff --git a/package-lock.json b/package-lock.json index 3cc4a47..6e9d41f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "mongoose-autopopulate": "^1.0.0", "node-cache": "^5.1.2", "node-jose": "^2.2.0", + "passkit-generator": "^3.4.0", "qrcode": "^1.5.4", "short-uuid": "^5.2.0", "simple-oauth2": "^5.1.0", @@ -6778,6 +6779,11 @@ "node": ">=8" } }, + "node_modules/do-not-zip": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/do-not-zip/-/do-not-zip-1.0.0.tgz", + "integrity": "sha512-Pgd81ET43bhAGaN2Hq1zluSX1FmD7kl7KcV9ER/lawiLsRUB9pRA5y8r6us29Xk6BrINZETO8TjhYwtwafWUww==" + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -10610,6 +10616,37 @@ "node": ">= 0.8" } }, + "node_modules/passkit-generator": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/passkit-generator/-/passkit-generator-3.4.0.tgz", + "integrity": "sha512-awWN0twkP2hBymhWtIakv1hdH4ieQVZNC2vHaUIQRIQaCW3ZQOjN0kKO8ii+hqq01682nmsROsvDPZD5u84m7g==", + "dependencies": { + "do-not-zip": "^1.0.0", + "joi": "17.4.2", + "node-forge": "^1.3.1", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/passkit-generator/node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "node_modules/passkit-generator/node_modules/joi": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.4.2.tgz", + "integrity": "sha512-Lm56PP+n0+Z2A2rfRvsfWVDXGEWjXxatPopkQ8qQ5mxCEhwHG+Ettgg5o98FFaxilOxozoa14cFhrE/hOzh/Nw==", + "dependencies": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.0", + "@sideway/formula": "^3.0.0", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", diff --git a/package.json b/package.json index 96c39fb..c59dda6 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "mongoose-autopopulate": "^1.0.0", "node-cache": "^5.1.2", "node-jose": "^2.2.0", + "passkit-generator": "^3.4.0", "qrcode": "^1.5.4", "short-uuid": "^5.2.0", "simple-oauth2": "^5.1.0", diff --git a/src/assets/passes/.gitignore b/src/assets/passes/.gitignore new file mode 100644 index 0000000..bea28e0 --- /dev/null +++ b/src/assets/passes/.gitignore @@ -0,0 +1 @@ +apple/* \ No newline at end of file diff --git a/src/assets/passes/apple.pass/icon.png b/src/assets/passes/apple.pass/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..04153b9fed5d889a2b3b74583a31ccb6a737b3ac GIT binary patch literal 2069 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 literal 0 HcmV?d00001 diff --git a/src/assets/passes/apple.pass/logo@2x.png b/src/assets/passes/apple.pass/logo@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..78015d9d77f9d4bc08a5f3cd9bcbb2c73bc18c5e GIT binary patch literal 3032 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 QvsZQCoW7}Ei4G;|e`L~XegFUf literal 0 HcmV?d00001 diff --git a/src/assets/passes/apple.pass/manifest.json b/src/assets/passes/apple.pass/manifest.json new file mode 100644 index 0000000..3b2bd77 --- /dev/null +++ b/src/assets/passes/apple.pass/manifest.json @@ -0,0 +1,8 @@ +{ +"da39a3ee5e6b4b0d3255bfef95601890afd80709": "", +"ba6c0df424962f4eb5a94c3bbd2cfc65a7b26c92": "", +"31c6114aa84bd6448a92ac49ea82e9be0641db8a": "", +"41428a07f2bad180da2a60b7f3987a3b2ca645db": "", +"e80bfa53a5e6bb765de3a08a8f28ff3bf35f077c": "", +"e4170ceee63549158e5a3fb0c60e00988e9b82a5": "" +} diff --git a/src/assets/passes/apple.pass/pass.json b/src/assets/passes/apple.pass/pass.json new file mode 100644 index 0000000..282b973 --- /dev/null +++ b/src/assets/passes/apple.pass/pass.json @@ -0,0 +1,59 @@ +{ + "formatVersion": 1, + "passTypeIdentifier": "pass.com.hackthe6ix.hackthe6ix2025", + "serialNumber": "123456789", + "teamIdentifier": "GFVM2XT834", + "organizationName": "Hack The 6ix", + "description": "Hack The 6ix 2025", + "logoText": " ", + "backgroundColor": "rgb(207,237,175)", + "foregroundColor": "rgb(78,50,45)", + "labelColor": "rgb(78,50,45)", + "suppressStripShine": true, + "logoImage": "logo.png", + "eventTicket": { + "headerFields": [ + { + "key": "eventDateTime", + "label": "8:00PM", + "value": "July 18, 2025\n8:00PM", + "textAlignment": "PKTextAlignmentRight" + } + ], + "primaryFields": [ + { + "key": "eventName", + "label": "", + "value": "Hack The 6ix 2025", + "textAlignment": "PKTextAlignmentLeft" + } + ], + "secondaryFields": [ + { + "key": "location", + "label": "ADDRESS", + "value": "York University Keele Campus, 83 York Blvd, North York, ON M7A 2C5", + "textAlignment": "PKTextAlignmentLeft" + } + ], + "auxiliaryFields": [ + { + "key": "hacker", + "label": "HACKER", + "value": "Winston Yu", + "textAlignment": "PKTextAlignmentLeft" + }, + { + "key": "additionalInfo", + "label": "ADDITIONAL INFO", + "value": "Accolade East Building", + "textAlignment": "PKTextAlignmentRight" + } + ] + }, + "barcode": { + "format": "PKBarcodeFormatQR", + "message": "https://yourcompany.com/redeem?code=123456", + "messageEncoding": "iso-8859-1" + } +} diff --git a/src/index.ts b/src/index.ts index fdaf023..3151f03 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import apiRouter from './routes/api'; import authRouter from './routes/auth'; import healthRouter from './routes/health'; import nfcRouter from './routes/nfc' +import passesRouter from './routes/passes' import { logResponse, log } from './services/logger'; import './services/environmentValidator'; @@ -51,6 +52,7 @@ app.use('/api/action', actionRouter); app.use('/auth', authRouter); app.use('/health', healthRouter); app.use('/nfc', nfcRouter); +app.use('/passes', passesRouter); app.use(function(err: any, req: express.Request, res: express.Response, next: express.NextFunction) { logResponse(req, res, ( diff --git a/src/routes/passes.ts b/src/routes/passes.ts new file mode 100644 index 0000000..6084791 --- /dev/null +++ b/src/routes/passes.ts @@ -0,0 +1,78 @@ +import express, { Request, Response } from "express"; +import { PKPass } from "passkit-generator"; +import fs from "fs"; +import path from "path"; + +const router = express.Router(); + +const generateApplePass = async (userID: string, userType: string) => { + try { + const wwdr = fs.readFileSync(path.join(process.cwd(), './src/assets/passes/apple/wwdr.pem'), 'utf8'); + const signerCert = fs.readFileSync(path.join(process.cwd(), './src/assets/passes/apple/signerCert.pem'), 'utf8'); + const signerKey = fs.readFileSync(path.join(process.cwd(), './src/assets/passes/apple/signerKey.pem'), 'utf8'); + const pass = await PKPass.from({ + model: path.join(process.cwd(), './src/assets/passes/apple.pass'), + certificates: { + wwdr, + signerCert: signerCert, + signerKey: signerKey, + signerKeyPassphrase: process.env.SIGNER_KEY_PASSPHRASE + } + }); + + const barcodeString = JSON.stringify({ + userID: userID || "test-id", + userType: userType || "test-type", + }) + + pass.setBarcodes(barcodeString); + const buffer = pass.getAsBuffer(); + + return buffer; + + } catch (error) { + console.log(error); + console.log("error"); + throw error; + } +} + + +router.post('/apple/hackathon.pkpass', async (req: Request, res: Response) => { + + const userID = req.body.userID; + const userType = req.body.userType; + + try { + const buffer = await generateApplePass(userID, userType); + res.type("application/vnd.apple.pkpass") + .set("Content-Disposition", 'inline; filename="hackathon.pkpass"') + .send(buffer); + } catch (error) { + console.log(error); + res.status(500).json({ error: "Failed to generate pass" }); + } +}); + +router.post("/test", async (req: Request, res: Response) => { + res.json({ + message: "true" + }); +}); + +router.get("/apple/hackathon.pkpass", async (req: Request, res: Response) => { + const userId = req.query.userId as string; + const userType = req.query.userType as string; + + try { + const buffer = await generateApplePass(userId, userType); + res.type("application/vnd.apple.pkpass") + .set("Content-Disposition", 'inline; filename="hackathon.pkpass"') + .send(buffer); + } catch (error) { + console.log(error); + res.status(500).json({ error: "Failed to generate pass" }); + } +}); + +export default router; From fb94a6ee4d00e537c81cf6cc45452c17e978b8a7 Mon Sep 17 00:00:00 2001 From: fehervaryy Date: Thu, 10 Jul 2025 00:40:10 -0400 Subject: [PATCH 2/8] update manifest --- src/assets/passes/apple.pass/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/passes/apple.pass/manifest.json b/src/assets/passes/apple.pass/manifest.json index 3b2bd77..126bec8 100644 --- a/src/assets/passes/apple.pass/manifest.json +++ b/src/assets/passes/apple.pass/manifest.json @@ -1,6 +1,6 @@ { "da39a3ee5e6b4b0d3255bfef95601890afd80709": "", -"ba6c0df424962f4eb5a94c3bbd2cfc65a7b26c92": "", +"f47f439f18e6c778b91f2ad8d3beeb8292c7f63f": "", "31c6114aa84bd6448a92ac49ea82e9be0641db8a": "", "41428a07f2bad180da2a60b7f3987a3b2ca645db": "", "e80bfa53a5e6bb765de3a08a8f28ff3bf35f077c": "", From 3a9a4250652d9e47a381cdb469a5098d1f261ffc Mon Sep 17 00:00:00 2001 From: fehervaryy Date: Thu, 10 Jul 2025 01:47:04 -0400 Subject: [PATCH 3/8] generate qr codes to redirect to user --- src/controller/UserController.ts | 13 +++++++++++++ src/routes/action.ts | 20 +++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/controller/UserController.ts b/src/controller/UserController.ts index c3f67bd..34aa5ab 100644 --- a/src/controller/UserController.ts +++ b/src/controller/UserController.ts @@ -623,6 +623,19 @@ export const getCheckInQR = ( }); }; +export const getDownloadPassQR = ( + requestUser: string, + userType: AllUserTypes, +): Promise => { + return new Promise((resolve, reject) => { + qrcode.toDataURL(`${process.env.FRONTEND_URL || "https://hackthe6ix.com"}/download-pass?userId=${requestUser}&userType=${userType}`).then((url) => { + return resolve(url); + }).catch((err) => { + return reject(err); + }); + }); +}; + /** * Generate a QR Code for a list of (External) Users * diff --git a/src/routes/action.ts b/src/routes/action.ts index ef18a10..603dec4 100644 --- a/src/routes/action.ts +++ b/src/routes/action.ts @@ -35,7 +35,8 @@ import { addCheckInNotes, removeCheckInNotes, getResumeURL, - syncUserMailingListsByID + syncUserMailingListsByID, + getDownloadPassQR } from '../controller/UserController'; import { logResponse } from '../services/logger'; import sendAllTemplates from '../services/mailer/sendAllTemplates'; @@ -45,6 +46,7 @@ import verifyMailingList from '../services/mailer/verifyMailingList'; import {isAdmin, isAuthenticated, isHacker, isOrganizer, isVolunteer} from '../services/permissions'; import { getStatistics } from '../services/statistics'; import {generateDiscordOAuthUrl} from "../services/discordApi"; +import { AllUserTypes } from '../types/types'; const actionRouter = express.Router(); @@ -262,6 +264,22 @@ actionRouter.get('/checkInQR', isHacker, (req: Request, res:Response) => { ) }); +/** + * (Hacker) + * + * Get QR code to redirect to download pass page + */ +actionRouter.get('/downloadPassQR', (req: Request, res:Response) => { + const { userId, userType } = req.query; + logResponse( + req, + res, + getDownloadPassQR( + userId as string, userType as AllUserTypes + ) + ) +}); + // Volunteer endpoints /** From 74bb9f85d35ebf47c8f76afb16b07fa12d725fbd Mon Sep 17 00:00:00 2001 From: fehervaryy Date: Thu, 10 Jul 2025 03:29:13 -0400 Subject: [PATCH 4/8] api to create google wallet passes --- package-lock.json | 181 ++++++++++++++++++++++++++++++++++++++++--- package.json | 1 + src/routes/passes.ts | 159 +++++++++++++++++++++++++++++++++++++ 3 files changed, 329 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6e9d41f..f3fad4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "express-fileupload": "^1.5.1", "express-mongo-sanitize": "^2.2.0", "file-type": "^21.0.0", + "google-auth-library": "^10.1.0", "jsonwebtoken": "^9.0.2", "jsqr": "^1.4.0", "lodash.clonedeep": "^4.5.0", @@ -5313,7 +5314,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 14" @@ -5861,10 +5861,7 @@ "version": "9.3.0", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==", - "dev": true, "license": "MIT", - "optional": true, - "peer": true, "engines": { "node": "*" } @@ -6635,6 +6632,14 @@ "node": ">= 8" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -7563,10 +7568,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -7678,6 +7680,28 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -7900,6 +7924,17 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -7977,6 +8012,49 @@ "dev": true, "license": "MIT" }, + "node_modules/gaxios": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.1.tgz", + "integrity": "sha512-Odju3uBUJyVCkW64nLD4wKLhbh93bh6vIg/ZIXkWiLPBrdgtc65+tls/qml+un3pr6JqYVFDZbbmLDQT68rTOQ==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/gcp-metadata": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz", + "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8122,6 +8200,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.1.0.tgz", + "integrity": "sha512-GspVjZj1RbyRWpQ9FbAXMKjFGzZwDKnUHi66JJ+tcjcu5/xYAP1pdlWotCuIkMwjfVsxxDvsGZXGLzRt72D0sQ==", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^7.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.1.tgz", + "integrity": "sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A==", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -8140,6 +8243,18 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -8238,7 +8353,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -9309,10 +9423,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "bignumber.js": "^9.0.0" } @@ -9432,6 +9543,25 @@ "integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==", "license": "Apache-2.0" }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", @@ -10220,6 +10350,25 @@ "node": ">= 8.0.0" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -12529,6 +12678,14 @@ "makeerror": "1.0.12" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/package.json b/package.json index c59dda6..25b8d44 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "express-fileupload": "^1.5.1", "express-mongo-sanitize": "^2.2.0", "file-type": "^21.0.0", + "google-auth-library": "^10.1.0", "jsonwebtoken": "^9.0.2", "jsqr": "^1.4.0", "lodash.clonedeep": "^4.5.0", diff --git a/src/routes/passes.ts b/src/routes/passes.ts index 6084791..1f3a5db 100644 --- a/src/routes/passes.ts +++ b/src/routes/passes.ts @@ -2,9 +2,152 @@ import express, { Request, Response } from "express"; import { PKPass } from "passkit-generator"; import fs from "fs"; import path from "path"; +import { GoogleAuth } from "google-auth-library"; +import jwt from "jsonwebtoken"; + const router = express.Router(); +const credentials = process.env.GOOGLE_APPLICATION_CREDENTIALS; +const issuerId = process.env.GOOGLE_ISSUER_ID; +const Google = new GoogleAuth({ + credentials: JSON.parse(credentials || "{}"), + scopes: ["https://www.googleapis.com/auth/wallet_object.issuer"], +}); +const baseUrl = "https://walletobjects.googleapis.com/walletobjects/v1"; +const classId = `${issuerId}.hackathon2025`; + +async function createPassClass(): Promise { + const httpClient = await Google.getClient(); + + let genericClass = { + "id": classId, + "eventName": { + "defaultValue": { + "language": "en-US", + "value": "Hack The 6ix 2025" + } + }, + "issuerName": "Hack The 6ix", + "reviewStatus": "UNDER_REVIEW", + "seatNumberLabel": { + "defaultValue": { + "language": "en-US", + "value": "Team" + } + }, + "locations": [ + { + "latitude": 43.7731, + "longitude": 79.5038 + } + ], + "startDate": "2025-09-12T09:00:00Z", + "endDate": "2025-09-14T18:00:00Z", + "hexBackgroundColor": "#4285F4", + "logo": { + "sourceUri": { "uri": "https://hackthe6ix.com/icon.png?c9f2203f230562e3" }, + "contentDescription": { + "defaultValue": { "language":"en-US","value":"Hackathon Logo" } + } + } + }; + + let response; + try { + // Check if the class exists already + response = await httpClient.request({ + url: `${baseUrl}/genericClass/${classId}`, + method: 'GET' + }); + + console.log('Class already exists'); + console.log(response); + } catch (err: any) { + if (err.response && err.response.status === 404) { + // Class does not exist + // Create it now + response = await httpClient.request({ + url: `${baseUrl}/genericClass`, + method: 'POST', + data: genericClass + }); + + console.log('Class insert response'); + console.log(response); + } else { + // Something else went wrong + console.log(err); + throw err; + } + } +} + +async function createPassObject(userID: string, userType: string): Promise { + + let objectSuffix = `${userID.replace(/[^\w.-]/g, '_')}`; + + let genericObject = { + "id": `${issuerId}.${objectSuffix}`, + "classId": classId, + "state": "ACTIVE", + 'cardTitle': { + 'defaultValue': { + 'language': 'en', + 'value': 'Hack The 6ix 2025' + } + }, + "barcode": { + "type": "QR_CODE", + "value": JSON.stringify({ + userID: userID, + userType: userType, + }), + "alternateText": `${userID}` + }, + 'subheader': { + 'defaultValue': { + 'language': 'en', + 'value': 'Hacker' + } + }, + 'header': { + 'defaultValue': { + 'language': 'en', + 'value': `${userID}` + } + }, + 'heroImage': { + 'sourceUri': { + 'uri': 'https://storage.googleapis.com/wallet-lab-tools-codelab-artifacts-public/google-io-hero-demo-only.jpg' + } + }, + "textModulesData": [ + { + "header": "Check-In Instructions", + "body": "Please show this QR code at the door." + } + ] + } + + const claims = { + iss: JSON.parse(credentials || "{}").client_email, + aud: 'google', + origins: [], + typ: 'savetowallet', + payload: { + genericObjects: [ + genericObject + ] + } + } + + const token = jwt.sign(claims, JSON.parse(credentials || "{}").private_key, { algorithm: 'RS256' }); + const saveUrl = `https://pay.google.com/gp/v/save/${token}`; + + return saveUrl; +} + const generateApplePass = async (userID: string, userType: string) => { try { const wwdr = fs.readFileSync(path.join(process.cwd(), './src/assets/passes/apple/wwdr.pem'), 'utf8'); @@ -37,6 +180,22 @@ const generateApplePass = async (userID: string, userType: string) => { } } +router.get("/google/hackathon.pkpass", async (req: Request, res: Response) => { + const userId = req.query.userId as string; + const userType = req.query.userType as string; + + await createPassClass(); + + try { + const saveUrl = await createPassObject(userId, userType); + res.json({ + saveUrl: saveUrl + }); + } catch (error) { + console.log(error); + res.status(500).json({ error: "Failed to generate pass" }); + } +}); router.post('/apple/hackathon.pkpass', async (req: Request, res: Response) => { From 6cd1726b5baf2db80f2bcfe6b5ab93f3ee680251 Mon Sep 17 00:00:00 2001 From: fehervaryy Date: Thu, 10 Jul 2025 04:34:31 -0400 Subject: [PATCH 5/8] style google wallet pass --- src/routes/passes.ts | 107 ++++++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 48 deletions(-) diff --git a/src/routes/passes.ts b/src/routes/passes.ts index 1f3a5db..f7d8636 100644 --- a/src/routes/passes.ts +++ b/src/routes/passes.ts @@ -5,7 +5,6 @@ import path from "path"; import { GoogleAuth } from "google-auth-library"; import jwt from "jsonwebtoken"; - const router = express.Router(); const credentials = process.env.GOOGLE_APPLICATION_CREDENTIALS; @@ -15,40 +14,39 @@ const Google = new GoogleAuth({ scopes: ["https://www.googleapis.com/auth/wallet_object.issuer"], }); const baseUrl = "https://walletobjects.googleapis.com/walletobjects/v1"; -const classId = `${issuerId}.hackathon2025`; +const classId = `${issuerId}.hackthe6ix`; async function createPassClass(): Promise { const httpClient = await Google.getClient(); let genericClass = { - "id": classId, - "eventName": { - "defaultValue": { - "language": "en-US", - "value": "Hack The 6ix 2025" - } - }, - "issuerName": "Hack The 6ix", - "reviewStatus": "UNDER_REVIEW", - "seatNumberLabel": { - "defaultValue": { - "language": "en-US", - "value": "Team" - } - }, - "locations": [ - { - "latitude": 43.7731, - "longitude": 79.5038 - } - ], - "startDate": "2025-09-12T09:00:00Z", - "endDate": "2025-09-14T18:00:00Z", - "hexBackgroundColor": "#4285F4", - "logo": { - "sourceUri": { "uri": "https://hackthe6ix.com/icon.png?c9f2203f230562e3" }, - "contentDescription": { - "defaultValue": { "language":"en-US","value":"Hackathon Logo" } + 'id': `${classId}`, + 'classTemplateInfo': { + 'cardTemplateOverride': { + 'cardRowTemplateInfos': [ + { + 'twoItems': { + 'startItem': { + 'firstValue': { + 'fields': [ + { + 'fieldPath': 'object.textModulesData[\'address\']' + } + ] + } + }, + 'endItem': { + 'firstValue': { + 'fields': [ + { + 'fieldPath': 'object.textModulesData[\'date\']' + } + ] + } + } + } + } + ] } } }; @@ -88,22 +86,24 @@ async function createPassObject(userID: string, userType: string): Promise Date: Thu, 10 Jul 2025 05:17:24 -0400 Subject: [PATCH 6/8] add hackers name --- src/assets/passes/apple.pass/pass.json | 12 ----- src/controller/UserController.ts | 10 ++-- src/routes/action.ts | 9 +++- src/routes/passes.ts | 68 ++++++++++++++++++++------ 4 files changed, 68 insertions(+), 31 deletions(-) diff --git a/src/assets/passes/apple.pass/pass.json b/src/assets/passes/apple.pass/pass.json index 282b973..1558c96 100644 --- a/src/assets/passes/apple.pass/pass.json +++ b/src/assets/passes/apple.pass/pass.json @@ -37,18 +37,6 @@ } ], "auxiliaryFields": [ - { - "key": "hacker", - "label": "HACKER", - "value": "Winston Yu", - "textAlignment": "PKTextAlignmentLeft" - }, - { - "key": "additionalInfo", - "label": "ADDITIONAL INFO", - "value": "Accolade East Building", - "textAlignment": "PKTextAlignmentRight" - } ] }, "barcode": { diff --git a/src/controller/UserController.ts b/src/controller/UserController.ts index 34aa5ab..46aacd1 100644 --- a/src/controller/UserController.ts +++ b/src/controller/UserController.ts @@ -624,11 +624,15 @@ export const getCheckInQR = ( }; export const getDownloadPassQR = ( - requestUser: string, - userType: AllUserTypes, + user: { + id: string, + type: AllUserTypes, + name: string + } ): Promise => { + const { id, type, name } = user; return new Promise((resolve, reject) => { - qrcode.toDataURL(`${process.env.FRONTEND_URL || "https://hackthe6ix.com"}/download-pass?userId=${requestUser}&userType=${userType}`).then((url) => { + qrcode.toDataURL(`${process.env.FRONTEND_URL || "https://hackthe6ix.com"}/download-pass?userId=${id}&userType=${type}&userName=${name}`).then((url) => { return resolve(url); }).catch((err) => { return reject(err); diff --git a/src/routes/action.ts b/src/routes/action.ts index 603dec4..233b92b 100644 --- a/src/routes/action.ts +++ b/src/routes/action.ts @@ -270,12 +270,17 @@ actionRouter.get('/checkInQR', isHacker, (req: Request, res:Response) => { * Get QR code to redirect to download pass page */ actionRouter.get('/downloadPassQR', (req: Request, res:Response) => { - const { userId, userType } = req.query; + const { userId, userType, userName } = req.query; + const user = { + id: userId as string, + type: userType as AllUserTypes, + name: userName as string + } logResponse( req, res, getDownloadPassQR( - userId as string, userType as AllUserTypes + user ) ) }); diff --git a/src/routes/passes.ts b/src/routes/passes.ts index f7d8636..005416c 100644 --- a/src/routes/passes.ts +++ b/src/routes/passes.ts @@ -7,6 +7,12 @@ import jwt from "jsonwebtoken"; const router = express.Router(); +interface User { + id: string; + type: string; + name: string; +} + const credentials = process.env.GOOGLE_APPLICATION_CREDENTIALS; const issuerId = process.env.GOOGLE_ISSUER_ID; const Google = new GoogleAuth({ @@ -81,9 +87,9 @@ async function createPassClass(): Promise { } } -async function createPassObject(userID: string, userType: string): Promise { +async function createPassObject(user: User): Promise { - let objectSuffix = `${userID.replace(/[^\w.-]/g, '_')}`; + let objectSuffix = `${user.id.replace(/[^\w.-]/g, '_')}`; let genericObject = { 'id': `${issuerId}.${objectSuffix}`, @@ -100,10 +106,10 @@ async function createPassObject(userID: string, userType: string): Promise { +const generateApplePass = async (user: User) => { try { const wwdr = fs.readFileSync(path.join(process.cwd(), './src/assets/passes/apple/wwdr.pem'), 'utf8'); const signerCert = fs.readFileSync(path.join(process.cwd(), './src/assets/passes/apple/signerCert.pem'), 'utf8'); const signerKey = fs.readFileSync(path.join(process.cwd(), './src/assets/passes/apple/signerKey.pem'), 'utf8'); + const pass = await PKPass.from({ model: path.join(process.cwd(), './src/assets/passes/apple.pass'), certificates: { @@ -171,12 +178,30 @@ const generateApplePass = async (userID: string, userType: string) => { signerCert: signerCert, signerKey: signerKey, signerKeyPassphrase: process.env.SIGNER_KEY_PASSPHRASE - } + }, }); + pass.auxiliaryFields.push( + { + "key": "hacker", + "label": "HACKER", + "value": user.name, + "textAlignment": "PKTextAlignmentLeft" + } + ); + + pass.auxiliaryFields.push( + { + "key": "additionalInfo", + "label": "ADDITIONAL INFO", + "value": "Accolade East Building", + "textAlignment": "PKTextAlignmentRight" + } + ); + const barcodeString = JSON.stringify({ - userID: userID || "test-id", - userType: userType || "test-type", + userID: user.id || "test-id", + userType: user.type || "test-type", }) pass.setBarcodes(barcodeString); @@ -194,11 +219,16 @@ const generateApplePass = async (userID: string, userType: string) => { router.get("/google/hackathon.pkpass", async (req: Request, res: Response) => { const userId = req.query.userId as string; const userType = req.query.userType as string; + const userName = req.query.userName as string; await createPassClass(); try { - const saveUrl = await createPassObject(userId, userType); + const saveUrl = await createPassObject({ + id: userId, + type: userType, + name: userName || "" + }); res.json({ saveUrl: saveUrl }); @@ -210,11 +240,16 @@ router.get("/google/hackathon.pkpass", async (req: Request, res: Response) => { router.post('/apple/hackathon.pkpass', async (req: Request, res: Response) => { - const userID = req.body.userID; + const userId = req.body.userId; const userType = req.body.userType; + const userName = req.body.userName; try { - const buffer = await generateApplePass(userID, userType); + const buffer = await generateApplePass({ + id: userId, + type: userType, + name: userName || "" + }); res.type("application/vnd.apple.pkpass") .set("Content-Disposition", 'inline; filename="hackathon.pkpass"') .send(buffer); @@ -233,9 +268,14 @@ router.post("/test", async (req: Request, res: Response) => { router.get("/apple/hackathon.pkpass", async (req: Request, res: Response) => { const userId = req.query.userId as string; const userType = req.query.userType as string; + const userName = req.query.userName as string; try { - const buffer = await generateApplePass(userId, userType); + const buffer = await generateApplePass({ + id: userId, + type: userType, + name: userName || "" + }); res.type("application/vnd.apple.pkpass") .set("Content-Disposition", 'inline; filename="hackathon.pkpass"') .send(buffer); From 7972f9f9012df2d16b3f38761a01e7d4ea45ec08 Mon Sep 17 00:00:00 2001 From: fehervaryy Date: Thu, 10 Jul 2025 05:20:14 -0400 Subject: [PATCH 7/8] remove unused endpoints --- src/routes/passes.ts | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/src/routes/passes.ts b/src/routes/passes.ts index 005416c..43438f1 100644 --- a/src/routes/passes.ts +++ b/src/routes/passes.ts @@ -238,33 +238,6 @@ router.get("/google/hackathon.pkpass", async (req: Request, res: Response) => { } }); -router.post('/apple/hackathon.pkpass', async (req: Request, res: Response) => { - - const userId = req.body.userId; - const userType = req.body.userType; - const userName = req.body.userName; - - try { - const buffer = await generateApplePass({ - id: userId, - type: userType, - name: userName || "" - }); - res.type("application/vnd.apple.pkpass") - .set("Content-Disposition", 'inline; filename="hackathon.pkpass"') - .send(buffer); - } catch (error) { - console.log(error); - res.status(500).json({ error: "Failed to generate pass" }); - } -}); - -router.post("/test", async (req: Request, res: Response) => { - res.json({ - message: "true" - }); -}); - router.get("/apple/hackathon.pkpass", async (req: Request, res: Response) => { const userId = req.query.userId as string; const userType = req.query.userType as string; From d3658e9d4e88e3dae9e4a465705ce6dffac2d451 Mon Sep 17 00:00:00 2001 From: fehervaryy Date: Thu, 10 Jul 2025 16:08:51 -0400 Subject: [PATCH 8/8] fix --- src/routes/action.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/action.ts b/src/routes/action.ts index 6ca67e5..d35bcce 100644 --- a/src/routes/action.ts +++ b/src/routes/action.ts @@ -44,7 +44,7 @@ import { removeCheckInNotes, getResumeURL, syncUserMailingListsByID, - getDownloadPassQR + getDownloadPassQR, updateWaiver, getWaiverURL, } from '../controller/UserController';