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/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 diff --git a/__tests__/auth.test.ts b/__tests__/auth.test.ts new file mode 100644 index 0000000..a1e6b4c --- /dev/null +++ b/__tests__/auth.test.ts @@ -0,0 +1,104 @@ +import request from 'supertest' +import app from '../src/app' +import prisma from '../src/services/prisma' +import jwt from 'jsonwebtoken' +import authenticate 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' } }) + const employee = 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 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') + 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 } }) +}) + +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 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') + + // 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 + + authenticate(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 + + 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() + }) + + 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 + + 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() + }) +}) 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", 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 { 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/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/middlewares/authenicated.ts b/src/middlewares/authenicated.ts new file mode 100644 index 0000000..89becdb --- /dev/null +++ b/src/middlewares/authenicated.ts @@ -0,0 +1,24 @@ +import { NextFunction, Request, Response } from 'express' +import jwt from 'jsonwebtoken' +export const authenticate = (req: Request, res: Response, next: NextFunction) => { + try { + 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 jwt.JwtPayload + req.cookies = decoded.employeeId + console.log(decoded) + next() + } catch (error) { + return res.status(401).json({ error: 'Invalid or expired token' }) + } +} + +export default authenticate 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 diff --git a/src/resources/auth/controller.ts b/src/resources/auth/controller.ts new file mode 100644 index 0000000..400b63e --- /dev/null +++ b/src/resources/auth/controller.ts @@ -0,0 +1,61 @@ +import { Request, Response, NextFunction } from 'express' +import jwt from 'jsonwebtoken' +import prisma from '../../services/prisma' + +interface AuthenticatedRequest extends Request { + location?: string +} + +const login = async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { + try { + // const { locationId } = req.params + const { pin } = req.body + const locationId = req.location + + 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, foo: 'bar' }, 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, +} diff --git a/src/resources/auth/routes.ts b/src/resources/auth/routes.ts new file mode 100644 index 0000000..e8883ac --- /dev/null +++ b/src/resources/auth/routes.ts @@ -0,0 +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', verifyLocationToken, 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) 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