From 03ca2a46470a7e8b380e66c0b980058987d8ca2f Mon Sep 17 00:00:00 2001 From: Ben Turner Date: Wed, 14 Feb 2024 04:50:56 +0000 Subject: [PATCH] add access token to qr generator --- README.md | 2 +- jest.config.ts | 2 + package-lock.json | 128 ++++++++++++++++++++-- package.json | 2 + src/__tests__/setup.ts | 3 + src/__tests__/{test-utils.ts => utils.ts} | 0 src/routers/qr-code.router.test.ts | 18 +++ src/routers/qr-code.router.ts | 16 ++- src/server.test.ts | 37 +------ src/server.ts | 2 +- src/services/qr-code.service.ts | 30 ++++- src/types.ts | 6 +- 12 files changed, 189 insertions(+), 57 deletions(-) create mode 100644 src/__tests__/setup.ts rename src/__tests__/{test-utils.ts => utils.ts} (100%) create mode 100644 src/routers/qr-code.router.test.ts diff --git a/README.md b/README.md index 89c6641..5c6795f 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# VS-attendance-api +# VS-attendance-api \ No newline at end of file diff --git a/jest.config.ts b/jest.config.ts index d89a07d..2b4163d 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -5,6 +5,8 @@ const config: Config = { testEnvironment: "node", preset: "ts-jest", testMatch: ["**/*.test.ts"], + clearMocks: true, + setupFiles: ['/src/__tests__/setup.ts'] }; export default config; diff --git a/package-lock.json b/package-lock.json index eeb472e..d4532e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,9 @@ "dependencies": { "dotenv": "^16.4.2", "express": "^4.18.2", + "express-jwt": "^8.4.1", "jimp": "^0.22.10", + "jsonwebtoken": "^9.0.2", "jsqr": "^1.4.0", "qrcode": "^1.5.3" }, @@ -1968,6 +1970,14 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -1984,7 +1994,6 @@ "version": "20.11.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -2935,6 +2944,11 @@ "node": ">=0.4.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3439,6 +3453,14 @@ "url": "https://dotenvx.com" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3832,6 +3854,24 @@ "node": ">= 0.10.0" } }, + "node_modules/express-jwt": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-8.4.1.tgz", + "integrity": "sha512-IZoZiDv2yZJAb3QrbaSATVtTCYT11OcqgFGoTN4iKVyN6NBkBkhtVIixww5fmakF0Upt5HfOxJuS6ZmJVeOtTQ==", + "dependencies": { + "@types/jsonwebtoken": "^9", + "express-unless": "^2.1.3", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/express-unless": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-2.1.3.tgz", + "integrity": "sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ==" + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -5325,11 +5365,51 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsqr": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz", "integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==" }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5416,6 +5496,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -5428,6 +5538,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5581,8 +5696,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -6628,7 +6742,6 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -6643,7 +6756,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -6654,8 +6766,7 @@ "node_modules/semver/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/send": { "version": "0.18.0", @@ -7453,8 +7564,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/unpipe": { "version": "1.0.0", diff --git a/package.json b/package.json index 75d47f8..689c8fa 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "dependencies": { "dotenv": "^16.4.2", "express": "^4.18.2", + "express-jwt": "^8.4.1", "jimp": "^0.22.10", + "jsonwebtoken": "^9.0.2", "jsqr": "^1.4.0", "qrcode": "^1.5.3" }, diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts new file mode 100644 index 0000000..810487f --- /dev/null +++ b/src/__tests__/setup.ts @@ -0,0 +1,3 @@ +import { config } from "dotenv"; + +config(); \ No newline at end of file diff --git a/src/__tests__/test-utils.ts b/src/__tests__/utils.ts similarity index 100% rename from src/__tests__/test-utils.ts rename to src/__tests__/utils.ts diff --git a/src/routers/qr-code.router.test.ts b/src/routers/qr-code.router.test.ts new file mode 100644 index 0000000..69694a1 --- /dev/null +++ b/src/routers/qr-code.router.test.ts @@ -0,0 +1,18 @@ +import { mockServer } from "../__tests__/utils"; +import { decodeQRCodeDataUrl } from "../services/qr-code.service"; + +jest.mock("jsonwebtoken", () => ({ sign: () => "ACCESS_TOKEN" })); + +describe("qr-code-router.ts", () => { + it("Should generate a QR code as a base64 encoded png", async () => { + const response = await mockServer().get("/api/qr-code/generate"); + expect(response.headers["content-type"]).toContain("application/json"); + expect(response.body.dataUrl).toContain( + "data:image/png;base64,iVBORw0KGgo", + ); + const decoded = await decodeQRCodeDataUrl(response.body.dataUrl); + expect(decoded).toBe( + `${process.env.CLIENT_BASE_URL}?access_token=ACCESS_TOKEN`, + ); + }); +}); diff --git a/src/routers/qr-code.router.ts b/src/routers/qr-code.router.ts index 5a18654..8c6b471 100644 --- a/src/routers/qr-code.router.ts +++ b/src/routers/qr-code.router.ts @@ -1,11 +1,23 @@ import ex from "express"; import { generateQRCode } from "../services/qr-code.service"; +import { sign } from "jsonwebtoken"; const qrCodeRouter = ex.Router(); -qrCodeRouter.post("/generate", async (req, res) => { - const dataUrl = await generateQRCode(req.body.userData); +qrCodeRouter.get("/generate", async (req, res) => { + const accessToken = sign( + "" + new Date().getTime(), + process.env.SECRET as string, + { algorithm: "HS256", expiresIn: 1000 * 60 * 5 }, + ); + const payload = encodeURI( + `${process.env.CLIENT_BASE_URL}?access_token=${accessToken}`, + ); + const dataUrl = await generateQRCode(payload); res.status(200).send({ dataUrl }); }); +// generate qr code with access token (5 min expiry time) encoded in url parameters +// landing page on website where key is decoded, validates against api, then adds entry to spreadsheet + export { qrCodeRouter }; diff --git a/src/server.test.ts b/src/server.test.ts index 959b5d3..efed722 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -1,8 +1,5 @@ -import { mockServer } from "./__tests__/test-utils"; -import Jimp from "jimp"; -import jsqr, { QRCode } from "jsqr"; -import { config } from "dotenv"; -config(); +import { mockServer } from "./__tests__/utils"; + describe("server.ts", () => { it("should run", async () => { @@ -11,38 +8,10 @@ describe("server.ts", () => { }); it("should serve documentation site", async () => { - const response = await mockServer().get("/docs"); + const response = await mockServer().get("/"); expect(response.headers["content-type"]).toContain("text/html"); expect(response.text).toContain( "V School Attendance API Documentation", ); }); - - it("Should generate a QR code as a base64 encoded png", async () => { - const response = await mockServer() - .post("/api/qr-code/generate") - .send({ - userData: { email: "test@test.com", name: "test user", id: "123" }, - }); - expect(response.headers["content-type"]).toContain("application/json"); - expect(response.body.dataUrl).toContain( - "data:image/png;base64,iVBORw0KGgo", - ); - const buffer = Buffer.from( - response.body.dataUrl.replace(/^data:image\/[a-z]+;base64,/, ""), - "base64", - ); - const image = await Jimp.read(buffer); - const decoded = jsqr( - new Uint8ClampedArray(image.bitmap.data), - image.bitmap.width, - image.bitmap.height, - ) as QRCode; - expect(decoded.data).toBe( - `${process.env.ADMIN_CLIENT_BASE_URL}?email=test@test.com&name=test%20user&id=123`, - ); - }); - - // add student to google sheets list - // update sign out time }); diff --git a/src/server.ts b/src/server.ts index d74e0a7..c623a27 100644 --- a/src/server.ts +++ b/src/server.ts @@ -8,7 +8,7 @@ server.use(ex.json()); server.use(ex.static(path.resolve(__dirname, "..", "public"))); server.get("/ping", (req, res) => res.status(200).send({ message: "pong" })); -server.get("/docs", (req, res) => { +server.get(["/", "/docs"], (req, res) => { res .status(200) .sendFile(path.resolve(__dirname, "..", "public", "documentation.html")); diff --git a/src/services/qr-code.service.ts b/src/services/qr-code.service.ts index de743e1..64fb1b8 100644 --- a/src/services/qr-code.service.ts +++ b/src/services/qr-code.service.ts @@ -1,9 +1,29 @@ -import { QRCodeData } from "../types"; import QRCode from "qrcode"; +import Jimp from "jimp"; +import jsqr, { QRCode as QRC } from "jsqr"; -export const generateQRCode = async (userData: QRCodeData) => { - const payload = encodeURI( - `${process.env.ADMIN_CLIENT_BASE_URL}?email=${userData.email}&name=${userData.name}&id=${userData.id}`, - ); +export const generateQRCode = async

(payload: P) => { return await QRCode.toDataURL(payload, { width: 250 }); }; + +export const decodeQRCodeDataUrl = async ( + dataUrl: string, +): Promise => { + try { + const buffer = Buffer.from( + dataUrl.replace(/^data:image\/[a-z]+;base64,/, ""), + "base64", + ); + const image = await Jimp.read(buffer); + const decoded = jsqr( + new Uint8ClampedArray(image.bitmap.data), + image.bitmap.width, + image.bitmap.height, + ) as QRC; + + return decoded.data; + } catch (err) { + console.error(Error("Error: QR Decoding failed")); + console.error(err); + } +}; diff --git a/src/types.ts b/src/types.ts index f2c7ca9..cb0ff5c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1 @@ -export interface QRCodeData { - email: string; - name: string; - id: string; -} +export {};