From 722d5b3155f0110df080400aa4758c4d5c02509a Mon Sep 17 00:00:00 2001 From: daxantz Date: Sun, 5 Oct 2025 13:15:16 -0400 Subject: [PATCH 01/15] install jwt and cookie parser packages --- package-lock.json | 150 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 3 + 2 files changed, 152 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index b4ee142..50d8a36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,12 @@ "dependencies": { "@prisma/client": "^6.16.2", "compression": "^1.7.4", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", "pg": "^8.16.3", "pino": "^8.19.0" }, @@ -23,6 +25,7 @@ "@types/dotenv": "^8.2.0", "@types/express": "^4.17.21", "@types/jest": "^30.0.0", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^20.11.30", "@types/supertest": "^6.0.3", "@typescript-eslint/eslint-plugin": "^7.3.1", @@ -1912,6 +1915,17 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -1925,6 +1939,13 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.11.30", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", @@ -3065,6 +3086,12 @@ "ieee754": "^1.2.1" } }, + "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==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3496,6 +3523,28 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -3772,6 +3821,15 @@ "dev": true, "license": "MIT" }, + "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==", + "license": "Apache-2.0", + "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", @@ -6194,6 +6252,55 @@ "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==", + "license": "MIT", + "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/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "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==", + "license": "MIT", + "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", @@ -6246,6 +6353,42 @@ "node": ">=8" } }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -6259,6 +6402,12 @@ "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==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -7515,7 +7664,6 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" diff --git a/package.json b/package.json index 2b5f1f2..02520e2 100644 --- a/package.json +++ b/package.json @@ -29,10 +29,12 @@ "dependencies": { "@prisma/client": "^6.16.2", "compression": "^1.7.4", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", "pg": "^8.16.3", "pino": "^8.19.0" }, @@ -41,6 +43,7 @@ "@types/dotenv": "^8.2.0", "@types/express": "^4.17.21", "@types/jest": "^30.0.0", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^20.11.30", "@types/supertest": "^6.0.3", "@typescript-eslint/eslint-plugin": "^7.3.1", From 72f5b7cccceee83ff2a464f5cc1d2caf9171605b Mon Sep 17 00:00:00 2001 From: daxantz Date: Sun, 5 Oct 2025 13:21:21 -0400 Subject: [PATCH 02/15] add routes for auth endpoint --- src/resources/auth/routes.ts | 9 +++++++++ src/resources/locations/routes.ts | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 src/resources/auth/routes.ts diff --git a/src/resources/auth/routes.ts b/src/resources/auth/routes.ts new file mode 100644 index 0000000..89ed3cc --- /dev/null +++ b/src/resources/auth/routes.ts @@ -0,0 +1,9 @@ +import { Router } from 'express' +import authController from './controller' + +const router = Router({ mergeParams: true }) + +// define routes +router.post('/login', authController.login) + +export default router diff --git a/src/resources/locations/routes.ts b/src/resources/locations/routes.ts index 1893b73..3ed40f5 100644 --- a/src/resources/locations/routes.ts +++ b/src/resources/locations/routes.ts @@ -2,6 +2,7 @@ import { Router } from 'express' import locationController from './controller' import entranceRouter from '../entrances/routes' import employeeRouter from '../employee/routes' +import authRouter from '../auth/routes' const router = Router() // define routes @@ -10,7 +11,7 @@ router.delete('/:id', locationController.removeLocation) router.get('/', locationController.getLocations) router.get('/:id', locationController.getSingleLocation) router.put('/:id', locationController.updateLocation) - +router.use('/:locationId/auth', authRouter) router.use('/:locationId/employee', employeeRouter) router.use('/:locationId/entrance', entranceRouter) From 7096c9d76644b053ea041d01cd09bc40cb47e936 Mon Sep 17 00:00:00 2001 From: daxantz Date: Sun, 5 Oct 2025 13:22:10 -0400 Subject: [PATCH 03/15] add login function and test for login --- __tests__/auth.test.ts | 24 ++++++++++++++ src/resources/auth/controller.ts | 56 ++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 __tests__/auth.test.ts create mode 100644 src/resources/auth/controller.ts diff --git a/__tests__/auth.test.ts b/__tests__/auth.test.ts new file mode 100644 index 0000000..e66bfc2 --- /dev/null +++ b/__tests__/auth.test.ts @@ -0,0 +1,24 @@ +import request from 'supertest' +import app from '../src/app' +import prisma from '../src/services/prisma' + +test('Login with valid credentials', async () => { + // First, create a test location and employee in the database + const location = await prisma.location.create({ data: { name: 'Test Location' } }) + const employee = await prisma.employee.create({ + data: { name: 'Test Employee', pin: '1234', locationId: location.id }, + }) + + const res = await request(app).post(`/v1/location/${location.id}/auth/login`).send({ pin: '1234' }) + + expect(res.status).toBe(200) + expect(res.body).toHaveProperty('message', 'Login successful') + expect(res.body).toHaveProperty('employee') + expect(res.body.employee).toHaveProperty('id', employee.id) + expect(res.body.employee).toHaveProperty('name', 'Test Employee') + expect(res.headers['set-cookie']).toBeDefined() + + // Clean up + await prisma.employee.delete({ where: { id: employee.id } }) + await prisma.location.delete({ where: { id: location.id } }) +}) diff --git a/src/resources/auth/controller.ts b/src/resources/auth/controller.ts new file mode 100644 index 0000000..b50f7d0 --- /dev/null +++ b/src/resources/auth/controller.ts @@ -0,0 +1,56 @@ +import { Request, Response, NextFunction } from 'express' +import jwt from 'jsonwebtoken' +import prisma from '../../services/prisma' + +const login = async (req: Request, res: Response, next: NextFunction) => { + try { + const { locationId } = req.params + const { pin } = req.body + + if (!locationId) { + return res.status(400).json({ error: 'Location ID is required' }) + } + if (!pin) { + return res.status(400).json({ error: 'PIN is required' }) + } + + const employee = await prisma.employee.findFirst({ + where: { locationId: parseInt(locationId), pin }, + }) + + if (!employee) { + return res.status(401).json({ error: 'Invalid PIN or location' }) + } + + // Ensure JWT secret is defined + const jwtSecret = process.env.JWT_SECRET + if (!jwtSecret) { + console.error('JWT_SECRET not defined') + return res.status(500).json({ error: 'Server configuration error' }) + } + + // Sign JWT synchronously (safe for small payloads) + const token = jwt.sign({ employeeId: employee.id }, jwtSecret, { expiresIn: '8h' }) + + // Set cookie safely for dev and prod + res.cookie('token', token, { + httpOnly: true, + secure: false, // will only be secure in production + maxAge: 8 * 60 * 60 * 1000, // 8 hours + sameSite: 'lax', + }) + + // Always send JSON response + return res.status(200).json({ + message: 'Login successful', + employee: { id: employee.id, name: employee.name }, + }) + } catch (error) { + console.error('Login error:', error) + return next(error) + } +} + +export default { + login, +} From a540e77167f36e50a5c4c9c13db22534bffe4766 Mon Sep 17 00:00:00 2001 From: daxantz Date: Sun, 5 Oct 2025 13:23:06 -0400 Subject: [PATCH 04/15] change configs for cros and helmet and add error and auth middleware --- src/app.ts | 16 +++++++++++++--- src/middlewares/authenicated.ts | 25 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 src/middlewares/authenicated.ts diff --git a/src/app.ts b/src/app.ts index 649ff07..e1906d8 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,7 +4,7 @@ import helmet from 'helmet' import compression from 'compression' import routes from './common/routes' import unknownEndpoint from './middlewares/unknownEndpoint' - +import cookieParser from 'cookie-parser' // to use env variables import './common/env' @@ -12,8 +12,12 @@ const app: Application = express() // middleware app.disable('x-powered-by') -app.use(cors()) -app.use(helmet()) +app.use(cors({ credentials: true, origin: 'http://localhost:3000' })) +app.use( + helmet({ + crossOriginResourcePolicy: false, + }), +) app.use(compression()) app.use( express.urlencoded({ @@ -22,6 +26,7 @@ app.use( }), ) app.use(express.json()) +app.use(cookieParser()) // health check // app.get('/', (req: Request, res: Response) => { @@ -39,4 +44,9 @@ app.use('/v1', routes) // Handle unknown endpoints app.use('*', unknownEndpoint) +app.use((err, req: Request, res: Response) => { + console.error(err) + res.status(500).json({ error: 'Something went wrong' }) +}) + export default app diff --git a/src/middlewares/authenicated.ts b/src/middlewares/authenicated.ts new file mode 100644 index 0000000..cc0d029 --- /dev/null +++ b/src/middlewares/authenicated.ts @@ -0,0 +1,25 @@ +import { NextFunction, Request, Response } from 'express' +import jwt from 'jsonwebtoken' +export const authenticate = (req: Request, res: Response, next: NextFunction) => { + try { + console.log('Authenticating request...') + const authHeader = req.headers.authorization + if (!authHeader) { + return res.status(401).json({ error: 'Authorization header missing' }) + } + + const token = authHeader.split(' ')[1] + if (!token) { + return res.status(401).json({ error: 'Token missing' }) + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { employeeId: number } + req.cookies = decoded.employeeId + + next() + } catch (error) { + return res.status(401).json({ error: 'Invalid or expired token' }) + } +} + +export default authenticate From 29862630b9ac2407f659af75e1b4640c10fd8fd1 Mon Sep 17 00:00:00 2001 From: daxantz Date: Sun, 5 Oct 2025 14:29:32 -0400 Subject: [PATCH 05/15] add binary targets to client generator --- prisma/schema.prisma | 2 ++ 1 file changed, 2 insertions(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8431a44..7ece374 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -7,6 +7,8 @@ generator client { provider = "prisma-client-js" // output = "../src/generated/prisma" + binaryTargets = ["native", "linux-musl-arm64-openssl-3.0.x"] + } datasource db { From bbf3c9a943a8837c43789e3b49f2bb73f91cb156 Mon Sep 17 00:00:00 2001 From: daxantz Date: Sun, 5 Oct 2025 14:29:53 -0400 Subject: [PATCH 06/15] add .env variables to docker file --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 40749bf..727645b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,5 +7,8 @@ RUN npm install COPY . . + +COPY .env .env +RUN npx prisma generate EXPOSE 3000 CMD ["npm", "run", "dev"] \ No newline at end of file From b249e4ab524beb95e7ed7b8191d8b048fa539f49 Mon Sep 17 00:00:00 2001 From: daxantz Date: Tue, 7 Oct 2025 21:29:43 -0400 Subject: [PATCH 07/15] create endpoint for logging into a location --- src/common/routes.ts | 2 ++ src/resources/login/controller.ts | 50 +++++++++++++++++++++++++++++++ src/resources/login/routes.ts | 7 +++++ 3 files changed, 59 insertions(+) create mode 100644 src/resources/login/controller.ts create mode 100644 src/resources/login/routes.ts diff --git a/src/common/routes.ts b/src/common/routes.ts index 1c2b9b8..f3f6431 100644 --- a/src/common/routes.ts +++ b/src/common/routes.ts @@ -3,9 +3,11 @@ import { Router } from 'express' const router: Router = Router() // import routes +import loginRouter from '../resources/login/routes' import locationRouter from '../resources/locations/routes' // Higher level routes definition +router.use('/login', loginRouter) router.use('/location', locationRouter) export default router diff --git a/src/resources/login/controller.ts b/src/resources/login/controller.ts new file mode 100644 index 0000000..806e702 --- /dev/null +++ b/src/resources/login/controller.ts @@ -0,0 +1,50 @@ +import { Request, Response, NextFunction } from 'express' +import prisma from '../../services/prisma' +import jwt from 'jsonwebtoken' +// import { getLocationById, getAllLocations, updateLocationName } from './service' +// import { Location } from '@prisma/client' + +const loginLocation = async (req: Request, res: Response, next: NextFunction) => { + try { + const { name } = req.body + + if (!name) { + return res.status(400).json({ error: 'Location name is required' }) + } + // get location by name + const location = await prisma.location.findFirst({ + where: { name }, + }) + if (!location) { + return res.status(404).json({ error: 'Location not found' }) + } + // Ensure JWT secret is defined + const jwtSecret = process.env.JWT_SECRET + if (!jwtSecret) { + console.error('JWT_SECRET not defined') + return res.status(500).json({ error: 'Server configuration error' }) + } + + // Sign JWT synchronously (safe for small payloads) + const token = jwt.sign({ locationId: location.id }, jwtSecret, { expiresIn: '8h' }) + + // Set cookie safely for dev and prod + res.cookie('token', token, { + httpOnly: true, + secure: false, // will only be secure in production + maxAge: 8 * 60 * 60 * 1000, // 8 hours + sameSite: 'lax', + }) + + // Always send JSON response + return res.status(200).json({ + message: 'Login successful', + location: { id: location.id, name: location.name }, + }) + } catch (error) { + console.error('Login error:', error) + return next(error) + } +} + +export default { loginLocation } diff --git a/src/resources/login/routes.ts b/src/resources/login/routes.ts new file mode 100644 index 0000000..77fb1b2 --- /dev/null +++ b/src/resources/login/routes.ts @@ -0,0 +1,7 @@ +import { Router } from 'express' +import loginController from './controller' +const router = Router() + +router.post('/', loginController.loginLocation) + +export default router From 08c5b9af03c0b302cf8a49f550d7c9e4da48aaa6 Mon Sep 17 00:00:00 2001 From: daxantz Date: Tue, 7 Oct 2025 21:30:51 -0400 Subject: [PATCH 08/15] create middleware functions for verifying location token and authenticating the user --- src/middlewares/authenicated.ts | 5 ++--- src/middlewares/verifyLocationToken.ts | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 src/middlewares/verifyLocationToken.ts diff --git a/src/middlewares/authenicated.ts b/src/middlewares/authenicated.ts index cc0d029..89becdb 100644 --- a/src/middlewares/authenicated.ts +++ b/src/middlewares/authenicated.ts @@ -2,7 +2,6 @@ import { NextFunction, Request, Response } from 'express' import jwt from 'jsonwebtoken' export const authenticate = (req: Request, res: Response, next: NextFunction) => { try { - console.log('Authenticating request...') const authHeader = req.headers.authorization if (!authHeader) { return res.status(401).json({ error: 'Authorization header missing' }) @@ -13,9 +12,9 @@ export const authenticate = (req: Request, res: Response, next: NextFunction) => return res.status(401).json({ error: 'Token missing' }) } - const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { employeeId: number } + const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as jwt.JwtPayload req.cookies = decoded.employeeId - + console.log(decoded) next() } catch (error) { return res.status(401).json({ error: 'Invalid or expired token' }) diff --git a/src/middlewares/verifyLocationToken.ts b/src/middlewares/verifyLocationToken.ts new file mode 100644 index 0000000..c59b40b --- /dev/null +++ b/src/middlewares/verifyLocationToken.ts @@ -0,0 +1,23 @@ +import { Request, Response, NextFunction } from 'express' +import jwt from 'jsonwebtoken' +/* eslint-disable @typescript-eslint/no-explicit-any */ +const verifyLocationToken = (req: Request, res: Response, next: NextFunction) => { + try { + const token = req.headers.authorization?.split(' ')[1] + if (!token) { + return res.status(401).json({ error: 'Invalid or expired token' }) + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as jwt.JwtPayload + if (!decoded) { + return res.status(401).json({ error: 'Invalid token payload' }) + } + ;(req as any).location = decoded.locationId + console.log(decoded) + next() + } catch (error) { + return res.status(401).json({ error: 'Invalid or expired token' }) + } +} + +export default verifyLocationToken From eb69ba983d7c4c7ad5f9ebbedfbddf3091e85a17 Mon Sep 17 00:00:00 2001 From: daxantz Date: Tue, 7 Oct 2025 21:31:57 -0400 Subject: [PATCH 09/15] create test to test invalid pins and a test for middleware --- __tests__/auth.test.ts | 65 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/__tests__/auth.test.ts b/__tests__/auth.test.ts index e66bfc2..1d2764c 100644 --- a/__tests__/auth.test.ts +++ b/__tests__/auth.test.ts @@ -1,7 +1,10 @@ import request from 'supertest' import app from '../src/app' import prisma from '../src/services/prisma' +import jwt from 'jsonwebtoken' +import authMiddleware from '../src/middlewares/authenicated' +import { Request, Response, NextFunction } from 'express' test('Login with valid credentials', async () => { // First, create a test location and employee in the database const location = await prisma.location.create({ data: { name: 'Test Location' } }) @@ -22,3 +25,65 @@ test('Login with valid credentials', async () => { await prisma.employee.delete({ where: { id: employee.id } }) await prisma.location.delete({ where: { id: location.id } }) }) + +test('Login with invalid PIN', async () => { + // First, create a test location and employee in the database + const location = await prisma.location.create({ data: { name: 'Test Location' } }) + await prisma.employee.create({ + data: { name: 'Test Employee', pin: '1234', locationId: location.id }, + }) + + const res = await request(app).post(`/v1/location/${location.id}/auth/login`).send({ pin: 'wrongpin' }) + + expect(res.status).toBe(401) + expect(res.body).toHaveProperty('error', 'Invalid PIN or location') + + // Clean up + await prisma.employee.deleteMany({ where: { locationId: location.id } }) + await prisma.location.delete({ where: { id: location.id } }) +}) + +describe('Auth Middleware', () => { + it('calls next() when token is valid', () => { + const token = jwt.sign({ employeeId: 1 }, process.env.JWT_SECRET!) + const req = { + headers: { authorization: `Bearer ${token}` }, + } as Partial + const res: Partial = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + } + const next = jest.fn() as NextFunction + + authMiddleware(req as Request, res as Response, next) + expect(next).toHaveBeenCalled() + }) + + it('returns 401 when token is missing', () => { + const req = { cookies: {} } as Partial + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + } as Partial + const next = jest.fn() as NextFunction + + authMiddleware(req as Request, res as Response, next) + expect(res.status).toHaveBeenCalledWith(401) + expect(res.json).toHaveBeenCalledWith({ error: 'Invalid or expired token' }) + expect(next).not.toHaveBeenCalled() + }) + + it('returns 401 when token is invalid', () => { + const req = { cookies: { token: 'invalidtoken' } } as Partial + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + } as Partial + const next = jest.fn() as NextFunction + + authMiddleware(req as Request, res as Response, next) + expect(res.status).toHaveBeenCalledWith(401) + expect(res.json).toHaveBeenCalledWith({ error: 'Invalid or expired token' }) + expect(next).not.toHaveBeenCalled() + }) +}) From adf9ba11b5257199b4a82e509ca5f7bee170517e Mon Sep 17 00:00:00 2001 From: daxantz Date: Tue, 7 Oct 2025 21:33:18 -0400 Subject: [PATCH 10/15] change locationId to be recieved from location set by location middleware and remove uneeded property in jwt payload --- src/resources/auth/controller.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/resources/auth/controller.ts b/src/resources/auth/controller.ts index b50f7d0..400b63e 100644 --- a/src/resources/auth/controller.ts +++ b/src/resources/auth/controller.ts @@ -2,10 +2,15 @@ import { Request, Response, NextFunction } from 'express' import jwt from 'jsonwebtoken' import prisma from '../../services/prisma' -const login = async (req: Request, res: Response, next: NextFunction) => { +interface AuthenticatedRequest extends Request { + location?: string +} + +const login = async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { - const { locationId } = req.params + // const { locationId } = req.params const { pin } = req.body + const locationId = req.location if (!locationId) { return res.status(400).json({ error: 'Location ID is required' }) @@ -30,7 +35,7 @@ const login = async (req: Request, res: Response, next: NextFunction) => { } // Sign JWT synchronously (safe for small payloads) - const token = jwt.sign({ employeeId: employee.id }, jwtSecret, { expiresIn: '8h' }) + const token = jwt.sign({ employeeId: employee.id, foo: 'bar' }, jwtSecret, { expiresIn: '8h' }) // Set cookie safely for dev and prod res.cookie('token', token, { From 1a2ce6a6c5bdc88b1b1fb62625cb531143e3ff56 Mon Sep 17 00:00:00 2001 From: daxantz Date: Tue, 7 Oct 2025 21:33:58 -0400 Subject: [PATCH 11/15] add middleware to check location token before employee logs in --- src/resources/auth/routes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/resources/auth/routes.ts b/src/resources/auth/routes.ts index 89ed3cc..e8883ac 100644 --- a/src/resources/auth/routes.ts +++ b/src/resources/auth/routes.ts @@ -1,9 +1,10 @@ import { Router } from 'express' import authController from './controller' +import verifyLocationToken from '../../middlewares/verifyLocationToken' const router = Router({ mergeParams: true }) // define routes -router.post('/login', authController.login) +router.post('/login', verifyLocationToken, authController.login) export default router From b5859c21642c3f082fb5a413543bbab199998caf Mon Sep 17 00:00:00 2001 From: daxantz Date: Wed, 8 Oct 2025 18:31:14 -0400 Subject: [PATCH 12/15] add jwt authentication to tests --- __tests__/auth.test.ts | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/__tests__/auth.test.ts b/__tests__/auth.test.ts index 1d2764c..0e6a9e4 100644 --- a/__tests__/auth.test.ts +++ b/__tests__/auth.test.ts @@ -5,6 +5,13 @@ import jwt from 'jsonwebtoken' import authMiddleware from '../src/middlewares/authenicated' import { Request, Response, NextFunction } from 'express' + +beforeEach(async () => { + await prisma.entrance.deleteMany() + await prisma.employee.deleteMany() + await prisma.location.deleteMany() +}) + test('Login with valid credentials', async () => { // First, create a test location and employee in the database const location = await prisma.location.create({ data: { name: 'Test Location' } }) @@ -12,7 +19,12 @@ test('Login with valid credentials', async () => { data: { name: 'Test Employee', pin: '1234', locationId: location.id }, }) - const res = await request(app).post(`/v1/location/${location.id}/auth/login`).send({ pin: '1234' }) + const token = jwt.sign({ locationId: location.id }, process.env.JWT_SECRET!) + + const res = await request(app) + .post(`/v1/location/${location.id}/auth/login`) + .set('Authorization', `Bearer ${token}`) + .send({ pin: '1234' }) expect(res.status).toBe(200) expect(res.body).toHaveProperty('message', 'Login successful') @@ -22,8 +34,8 @@ test('Login with valid credentials', async () => { expect(res.headers['set-cookie']).toBeDefined() // Clean up - await prisma.employee.delete({ where: { id: employee.id } }) - await prisma.location.delete({ where: { id: location.id } }) + // await prisma.employee.delete({ where: { id: employee.id } }) + // await prisma.location.delete({ where: { id: location.id } }) }) test('Login with invalid PIN', async () => { @@ -32,8 +44,11 @@ test('Login with invalid PIN', async () => { await prisma.employee.create({ data: { name: 'Test Employee', pin: '1234', locationId: location.id }, }) - - const res = await request(app).post(`/v1/location/${location.id}/auth/login`).send({ pin: 'wrongpin' }) + const token = jwt.sign({ locationId: location.id }, process.env.JWT_SECRET!) + const res = await request(app) + .post(`/v1/location/${location.id}/auth/login`) + .set('Authorization', `Bearer ${token}`) + .send({ pin: 'wrongpin' }) expect(res.status).toBe(401) expect(res.body).toHaveProperty('error', 'Invalid PIN or location') From 71f93e8a70473abca8dca7d5fc593f4f525defa2 Mon Sep 17 00:00:00 2001 From: daxantz Date: Wed, 8 Oct 2025 18:38:16 -0400 Subject: [PATCH 13/15] change name of middleware import --- __tests__/auth.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/__tests__/auth.test.ts b/__tests__/auth.test.ts index 0e6a9e4..523402b 100644 --- a/__tests__/auth.test.ts +++ b/__tests__/auth.test.ts @@ -2,7 +2,7 @@ import request from 'supertest' import app from '../src/app' import prisma from '../src/services/prisma' import jwt from 'jsonwebtoken' -import authMiddleware from '../src/middlewares/authenicated' +import authenticate from '../src/middlewares/authenicated' import { Request, Response, NextFunction } from 'express' @@ -70,7 +70,7 @@ describe('Auth Middleware', () => { } const next = jest.fn() as NextFunction - authMiddleware(req as Request, res as Response, next) + authenticate(req as Request, res as Response, next) expect(next).toHaveBeenCalled() }) @@ -82,7 +82,7 @@ describe('Auth Middleware', () => { } as Partial const next = jest.fn() as NextFunction - authMiddleware(req as Request, res as Response, next) + authenticate(req as Request, res as Response, next) expect(res.status).toHaveBeenCalledWith(401) expect(res.json).toHaveBeenCalledWith({ error: 'Invalid or expired token' }) expect(next).not.toHaveBeenCalled() @@ -96,7 +96,7 @@ describe('Auth Middleware', () => { } as Partial const next = jest.fn() as NextFunction - authMiddleware(req as Request, res as Response, next) + authenticate(req as Request, res as Response, next) expect(res.status).toHaveBeenCalledWith(401) expect(res.json).toHaveBeenCalledWith({ error: 'Invalid or expired token' }) expect(next).not.toHaveBeenCalled() From 8d9f930bc459e9e120c45955695f3c069d2e94c0 Mon Sep 17 00:00:00 2001 From: daxantz Date: Wed, 8 Oct 2025 18:53:01 -0400 Subject: [PATCH 14/15] change env jwt secret to hard coded value for tests --- __tests__/auth.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/__tests__/auth.test.ts b/__tests__/auth.test.ts index 523402b..e95c754 100644 --- a/__tests__/auth.test.ts +++ b/__tests__/auth.test.ts @@ -19,7 +19,7 @@ test('Login with valid credentials', async () => { data: { name: 'Test Employee', pin: '1234', locationId: location.id }, }) - const token = jwt.sign({ locationId: location.id }, process.env.JWT_SECRET!) + const token = jwt.sign({ locationId: location.id }, 'supersecretkey') const res = await request(app) .post(`/v1/location/${location.id}/auth/login`) @@ -44,7 +44,7 @@ test('Login with invalid PIN', async () => { await prisma.employee.create({ data: { name: 'Test Employee', pin: '1234', locationId: location.id }, }) - const token = jwt.sign({ locationId: location.id }, process.env.JWT_SECRET!) + const token = jwt.sign({ locationId: location.id }, 'supersecretkey') const res = await request(app) .post(`/v1/location/${location.id}/auth/login`) .set('Authorization', `Bearer ${token}`) @@ -60,7 +60,7 @@ test('Login with invalid PIN', async () => { describe('Auth Middleware', () => { it('calls next() when token is valid', () => { - const token = jwt.sign({ employeeId: 1 }, process.env.JWT_SECRET!) + const token = jwt.sign({ employeeId: 1 }, 'supersecretkey') const req = { headers: { authorization: `Bearer ${token}` }, } as Partial From 1e74e8d799eb5b85f1f42ca4d2206df4c08d9344 Mon Sep 17 00:00:00 2001 From: daxantz Date: Wed, 8 Oct 2025 19:25:55 -0400 Subject: [PATCH 15/15] change jwt secret to the correct value and change workflow to allow github actions to recognize env variable --- .github/workflows/testing.yml | 2 ++ __tests__/auth.test.ts | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index e97d036..0435bba 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -15,6 +15,7 @@ jobs: POSTGRES_USER: postgres POSTGRES_PASSWORD: Callofduty2497 POSTGRES_DB: valetapptest + options: >- --health-cmd pg_isready --health-interval 10s @@ -49,4 +50,5 @@ jobs: env: NODE_ENV: test DATABASE_URL: postgres://postgres:Callofduty2497@localhost:5433/valetapptest + JWT_SECRET: ${{ secrets.JWT_SECRET }} run: npm test diff --git a/__tests__/auth.test.ts b/__tests__/auth.test.ts index e95c754..a1e6b4c 100644 --- a/__tests__/auth.test.ts +++ b/__tests__/auth.test.ts @@ -19,7 +19,7 @@ test('Login with valid credentials', async () => { data: { name: 'Test Employee', pin: '1234', locationId: location.id }, }) - const token = jwt.sign({ locationId: location.id }, 'supersecretkey') + const token = jwt.sign({ locationId: location.id }, process.env.JWT_SECRET!) const res = await request(app) .post(`/v1/location/${location.id}/auth/login`) @@ -44,7 +44,7 @@ test('Login with invalid PIN', async () => { await prisma.employee.create({ data: { name: 'Test Employee', pin: '1234', locationId: location.id }, }) - const token = jwt.sign({ locationId: location.id }, 'supersecretkey') + const token = jwt.sign({ locationId: location.id }, process.env.JWT_SECRET!) const res = await request(app) .post(`/v1/location/${location.id}/auth/login`) .set('Authorization', `Bearer ${token}`) @@ -60,7 +60,7 @@ test('Login with invalid PIN', async () => { describe('Auth Middleware', () => { it('calls next() when token is valid', () => { - const token = jwt.sign({ employeeId: 1 }, 'supersecretkey') + const token = jwt.sign({ employeeId: 1 }, process.env.JWT_SECRET) const req = { headers: { authorization: `Bearer ${token}` }, } as Partial