diff --git a/package-lock.json b/package-lock.json index 3cc4a47..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", @@ -38,6 +39,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", @@ -5312,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" @@ -5860,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": "*" } @@ -6634,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", @@ -6778,6 +6784,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", @@ -7557,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", @@ -7672,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", @@ -7894,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", @@ -7971,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", @@ -8116,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", @@ -8134,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", @@ -8232,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", @@ -9303,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" } @@ -9426,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", @@ -10214,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", @@ -10610,6 +10765,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", @@ -12492,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 96c39fb..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", @@ -56,6 +57,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 0000000..04153b9 Binary files /dev/null and b/src/assets/passes/apple.pass/icon.png differ diff --git a/src/assets/passes/apple.pass/icon@2x.png b/src/assets/passes/apple.pass/icon@2x.png new file mode 100644 index 0000000..d8603e3 Binary files /dev/null and b/src/assets/passes/apple.pass/icon@2x.png differ diff --git a/src/assets/passes/apple.pass/logo.png b/src/assets/passes/apple.pass/logo.png new file mode 100644 index 0000000..e7a2681 Binary files /dev/null and b/src/assets/passes/apple.pass/logo.png differ 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 0000000..78015d9 Binary files /dev/null and b/src/assets/passes/apple.pass/logo@2x.png differ diff --git a/src/assets/passes/apple.pass/manifest.json b/src/assets/passes/apple.pass/manifest.json new file mode 100644 index 0000000..126bec8 --- /dev/null +++ b/src/assets/passes/apple.pass/manifest.json @@ -0,0 +1,8 @@ +{ +"da39a3ee5e6b4b0d3255bfef95601890afd80709": "", +"f47f439f18e6c778b91f2ad8d3beeb8292c7f63f": "", +"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..1558c96 --- /dev/null +++ b/src/assets/passes/apple.pass/pass.json @@ -0,0 +1,47 @@ +{ + "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": [ + ] + }, + "barcode": { + "format": "PKBarcodeFormatQR", + "message": "https://yourcompany.com/redeem?code=123456", + "messageEncoding": "iso-8859-1" + } +} diff --git a/src/controller/UserController.ts b/src/controller/UserController.ts index 8d98191..e86aadd 100644 --- a/src/controller/UserController.ts +++ b/src/controller/UserController.ts @@ -661,6 +661,23 @@ export const getCheckInQR = ( }); }; +export const getDownloadPassQR = ( + 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=${id}&userType=${type}&userName=${name}`).then((url) => { + return resolve(url); + }).catch((err) => { + return reject(err); + }); + }); +}; + /** * Generate a QR Code for a list of (External) Users * 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/action.ts b/src/routes/action.ts index 91e8422..d35bcce 100644 --- a/src/routes/action.ts +++ b/src/routes/action.ts @@ -44,6 +44,7 @@ import { removeCheckInNotes, getResumeURL, syncUserMailingListsByID, + getDownloadPassQR, updateWaiver, getWaiverURL, } from '../controller/UserController'; @@ -60,6 +61,7 @@ import { isVolunteer, } from '../services/permissions'; import { getStatistics } from '../services/statistics'; +import { AllUserTypes } from '../types/types'; import { generateDiscordOAuthUrl } from '../services/discordApi'; const actionRouter = express.Router(); @@ -223,6 +225,27 @@ actionRouter.get('/checkInQR', isHacker, (req: Request, res: Response) => { logResponse(req, res, getCheckInQR(req.executor!._id, 'User')); }); +/** + * (Hacker) + * + * Get QR code to redirect to download pass page + */ +actionRouter.get('/downloadPassQR', (req: Request, res:Response) => { + const { userId, userType, userName } = req.query; + const user = { + id: userId as string, + type: userType as AllUserTypes, + name: userName as string + } + logResponse( + req, + res, + getDownloadPassQR( + user + ) + ) +}); + // Volunteer endpoints /** diff --git a/src/routes/passes.ts b/src/routes/passes.ts new file mode 100644 index 0000000..43438f1 --- /dev/null +++ b/src/routes/passes.ts @@ -0,0 +1,261 @@ +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(); + +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({ + credentials: JSON.parse(credentials || "{}"), + scopes: ["https://www.googleapis.com/auth/wallet_object.issuer"], +}); +const baseUrl = "https://walletobjects.googleapis.com/walletobjects/v1"; +const classId = `${issuerId}.hackthe6ix`; + +async function createPassClass(): Promise { + const httpClient = await Google.getClient(); + + let genericClass = { + 'id': `${classId}`, + 'classTemplateInfo': { + 'cardTemplateOverride': { + 'cardRowTemplateInfos': [ + { + 'twoItems': { + 'startItem': { + 'firstValue': { + 'fields': [ + { + 'fieldPath': 'object.textModulesData[\'address\']' + } + ] + } + }, + 'endItem': { + 'firstValue': { + 'fields': [ + { + 'fieldPath': 'object.textModulesData[\'date\']' + } + ] + } + } + } + } + ] + } + } + }; + + 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(user: User): Promise { + + let objectSuffix = `${user.id.replace(/[^\w.-]/g, '_')}`; + + let genericObject = { + 'id': `${issuerId}.${objectSuffix}`, + 'classId': classId, + 'genericType': 'GENERIC_TYPE_UNSPECIFIED', + 'state': 'ACTIVE', + 'cardTitle': { + 'defaultValue': { + 'language': 'en', + 'value': 'Hack The 6ix 2025' + } + }, + 'hexBackgroundColor': '#CFEDAF', + 'barcode': { + 'type': 'QR_CODE', + 'value': JSON.stringify({ + 'userID': user.id, + 'userType': user.type, + }), + 'alternateText': `${user.id}` + }, + 'subheader': { + 'defaultValue': { + 'language': 'en', + 'value': 'Hacker' + } + }, + 'header': { + 'defaultValue': { + 'language': 'en', + 'value': `${user.name}` + } + }, + 'heroImage': { + 'sourceUri': { + 'uri': 'https://miro.medium.com/v2/resize:fit:1400/1*IMRytrOprJjmRPKEdtt6Aw.png' + } + }, + 'logo': { + 'sourceUri': { + 'uri': 'https://hackthe6ix.com/icon.png?c9f2203f230562e3' + } + }, + 'textModulesData': [ + { + 'id': 'address', + 'header': 'Address', + 'body': 'York University Keele Campus,\n Accolade East Building' + }, + { + 'id': 'date', + 'header': 'July 18, 2025', + 'body': '8:00 PM' + } + ] + } + + 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 (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: { + wwdr, + 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: user.id || "test-id", + userType: user.type || "test-type", + }) + + pass.setBarcodes(barcodeString); + const buffer = pass.getAsBuffer(); + + return buffer; + + } catch (error) { + console.log(error); + console.log("error"); + throw error; + } +} + +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({ + id: userId, + type: userType, + name: userName || "" + }); + res.json({ + saveUrl: saveUrl + }); + } catch (error) { + console.log(error); + res.status(500).json({ error: "Failed to generate pass" }); + } +}); + +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({ + 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" }); + } +}); + +export default router;