feat: Sprint 2 Phase 1 - Auth Core Complete
✅ Implemented: - Password hashing with bcrypt (salt rounds = 10) - JWT token generation (access 15m, refresh 7d) - Updated login endpoint to return access + refresh tokens - Added refresh and logout endpoints - Updated seed script with hashed passwords - Added test users for all roles (OWNER, MANAGER, GROWER, STAFF) 📝 Files Added/Modified: - backend/src/utils/password.ts (NEW) - backend/src/utils/jwt.ts (NEW) - backend/src/controllers/auth.controller.ts (UPDATED) - backend/src/routes/auth.routes.ts (UPDATED) - backend/prisma/seed.js (UPDATED - now hashes passwords) - CREDENTIALS.md (UPDATED - all test users documented) 🔐 Test Users: - admin@runfoo.run (OWNER) - manager@runfoo.run (MANAGER) - grower@runfoo.run (GROWER) - staff@runfoo.run (STAFF) All passwords: password123 ⏭️ Next: Auth middleware + RBAC
This commit is contained in:
parent
54d2d2f387
commit
9dc0586d67
9 changed files with 594 additions and 141 deletions
|
|
@ -16,6 +16,30 @@
|
||||||
- **Permissions**: Full access to all features
|
- **Permissions**: Full access to all features
|
||||||
- **Hourly Rate**: $50.00
|
- **Hourly Rate**: $50.00
|
||||||
|
|
||||||
|
### Manager Account
|
||||||
|
|
||||||
|
- **Email**: `manager@runfoo.run`
|
||||||
|
- **Password**: `password123`
|
||||||
|
- **Role**: MANAGER
|
||||||
|
- **Permissions**: Full access except user management
|
||||||
|
- **Hourly Rate**: $35.00
|
||||||
|
|
||||||
|
### Grower Account
|
||||||
|
|
||||||
|
- **Email**: `grower@runfoo.run`
|
||||||
|
- **Password**: `password123`
|
||||||
|
- **Role**: GROWER
|
||||||
|
- **Permissions**: Read/write batches, rooms, tasks
|
||||||
|
- **Hourly Rate**: $30.00
|
||||||
|
|
||||||
|
### Staff Account
|
||||||
|
|
||||||
|
- **Email**: `staff@runfoo.run`
|
||||||
|
- **Password**: `password123`
|
||||||
|
- **Role**: STAFF
|
||||||
|
- **Permissions**: Read-only + timeclock
|
||||||
|
- **Hourly Rate**: $20.00
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Seeded Data
|
## Seeded Data
|
||||||
|
|
|
||||||
296
backend/package-lock.json
generated
296
backend/package-lock.json
generated
|
|
@ -8,11 +8,15 @@
|
||||||
"name": "ca-grow-ops-backend",
|
"name": "ca-grow-ops-backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/jwt": "^7.2.4",
|
||||||
"@prisma/client": "^5.7.0",
|
"@prisma/client": "^5.7.0",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"fastify": "^4.25.0",
|
"fastify": "^4.25.0",
|
||||||
"fastify-jwt": "^4.2.0",
|
"fastify-plugin": "^4.5.0",
|
||||||
"fastify-plugin": "^4.5.0"
|
"jsonwebtoken": "^9.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^20.10.0",
|
||||||
|
|
@ -690,6 +694,34 @@
|
||||||
"fast-json-stringify": "^5.7.0"
|
"fast-json-stringify": "^5.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fastify/jwt": {
|
||||||
|
"version": "7.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fastify/jwt/-/jwt-7.2.4.tgz",
|
||||||
|
"integrity": "sha512-aWJzVb3iZb9xIPjfut8YOrkNEKrZA9xyF2C2Hv9nTheFp7CQPGIZMNTyf3848BsD27nw0JLk8jVLZ2g2DfJOoQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/error": "^3.0.0",
|
||||||
|
"@lukeed/ms": "^2.0.0",
|
||||||
|
"fast-jwt": "^3.3.2",
|
||||||
|
"fastify-plugin": "^4.0.0",
|
||||||
|
"steed": "^1.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fastify/jwt/node_modules/fast-jwt": {
|
||||||
|
"version": "3.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-3.3.3.tgz",
|
||||||
|
"integrity": "sha512-oS3P8bRI24oPLJUePt2OgF64FBQib5TlgHLFQxYNoHYEEZe0gU3cKjJAVqpB5XKV/zjxmq4Hzbk3fgfW/wRz8Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@lukeed/ms": "^2.0.1",
|
||||||
|
"asn1.js": "^5.4.1",
|
||||||
|
"ecdsa-sig-formatter": "^1.0.11",
|
||||||
|
"mnemonist": "^0.39.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 <22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@fastify/merge-json-schemas": {
|
"node_modules/@fastify/merge-json-schemas": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz",
|
||||||
|
|
@ -1417,6 +1449,15 @@
|
||||||
"@babel/types": "^7.28.2"
|
"@babel/types": "^7.28.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/bcrypt": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/graceful-fs": {
|
"node_modules/@types/graceful-fs": {
|
||||||
"version": "4.1.9",
|
"version": "4.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
||||||
|
|
@ -1454,11 +1495,26 @@
|
||||||
"@types/istanbul-lib-report": "*"
|
"@types/istanbul-lib-report": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/ms": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.19.26",
|
"version": "20.19.26",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.26.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.26.tgz",
|
||||||
"integrity": "sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==",
|
"integrity": "sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
|
|
@ -1880,6 +1936,20 @@
|
||||||
"baseline-browser-mapping": "dist/cli.js"
|
"baseline-browser-mapping": "dist/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bcrypt": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-addon-api": "^8.3.0",
|
||||||
|
"node-gyp-build": "^4.8.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
|
|
@ -1967,6 +2037,12 @@
|
||||||
"node-int64": "^0.4.0"
|
"node-int64": "^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==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/buffer-from": {
|
"node_modules/buffer-from": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
|
|
@ -2273,15 +2349,6 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/depd": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/detect-newline": {
|
"node_modules/detect-newline": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
|
||||||
|
|
@ -2726,20 +2793,6 @@
|
||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-jwt": {
|
|
||||||
"version": "1.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-1.5.1.tgz",
|
|
||||||
"integrity": "sha512-XmsUqmyGoyMH5JB8dLFkMs7v2aEYdNj9UdZ/SZTyd211KVkvojnd7p9HT/9UXzMdF2s6IS0cSb7gUcOQrJq2GQ==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"asn1.js": "^5.3.0",
|
|
||||||
"ecdsa-sig-formatter": "^1.0.11",
|
|
||||||
"mnemonist": "^0.39.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fast-levenshtein": {
|
"node_modules/fast-levenshtein": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||||
|
|
@ -2808,46 +2861,6 @@
|
||||||
"toad-cache": "^3.3.0"
|
"toad-cache": "^3.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fastify-jwt": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/fastify-jwt/-/fastify-jwt-4.2.0.tgz",
|
|
||||||
"integrity": "sha512-wS0Wvg+5/7ZGaOJVFFIIWoMyc1Ashq9zUuVzq757R36fvXtsrEnEAaEIVFImAsDrAUIr9+PnrthMSGdVZgDk/g==",
|
|
||||||
"deprecated": "Please use @fastify/jwt@5.0.0 instead",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"fastify-jwt-deprecated": "npm:fastify-jwt@4.1.3",
|
|
||||||
"process-warning": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fastify-jwt-deprecated": {
|
|
||||||
"name": "fastify-jwt",
|
|
||||||
"version": "4.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/fastify-jwt/-/fastify-jwt-4.1.3.tgz",
|
|
||||||
"integrity": "sha512-SmcXjwgO6x8Kw4ybiZHOhk2Sp5MCvA/qWI3aXZPXrICWi1z6bQBSXJw8PiyiiGt84w/YcikpgntkWZiEP04EEg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@lukeed/ms": "^2.0.0",
|
|
||||||
"fast-jwt": "^1.4.0",
|
|
||||||
"fastify-plugin": "^3.0.0",
|
|
||||||
"http-errors": "^2.0.0",
|
|
||||||
"steed": "^1.1.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fastify-jwt-deprecated/node_modules/fastify-plugin": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-qKcDXmuZadJqdTm6vlCqioEbyewF60b/0LOFCcYN1B6BIZGlYJumWWOYs70SFYLDAH4YqdE1cxH/RKMG7rFxgA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/fastify-jwt/node_modules/process-warning": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/fastify-plugin": {
|
"node_modules/fastify-plugin": {
|
||||||
"version": "4.5.1",
|
"version": "4.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz",
|
||||||
|
|
@ -3151,26 +3164,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/http-errors": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"depd": "~2.0.0",
|
|
||||||
"inherits": "~2.0.4",
|
|
||||||
"setprototypeof": "~1.2.0",
|
|
||||||
"statuses": "~2.0.2",
|
|
||||||
"toidentifier": "~1.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/express"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/human-signals": {
|
"node_modules/human-signals": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||||
|
|
@ -4121,6 +4114,49 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsonwebtoken": {
|
||||||
|
"version": "9.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||||
|
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jws": "^4.0.1",
|
||||||
|
"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/jwa": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||||
|
"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": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^2.0.1",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
|
|
@ -4199,6 +4235,42 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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==",
|
||||||
|
"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.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
|
|
@ -4206,6 +4278,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"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": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
|
|
@ -4335,7 +4413,6 @@
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/natural-compare": {
|
"node_modules/natural-compare": {
|
||||||
|
|
@ -4345,6 +4422,26 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-addon-api": {
|
||||||
|
"version": "8.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
|
||||||
|
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18 || ^20 || >= 21"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-gyp-build": {
|
||||||
|
"version": "4.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||||
|
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"node-gyp-build": "bin.js",
|
||||||
|
"node-gyp-build-optional": "optional.js",
|
||||||
|
"node-gyp-build-test": "build-test.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-int64": {
|
"node_modules/node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
|
|
@ -5095,12 +5192,6 @@
|
||||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/setprototypeof": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
|
@ -5217,15 +5308,6 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/statuses": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
|
||||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/steed": {
|
"node_modules/steed": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/steed/-/steed-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/steed/-/steed-1.1.3.tgz",
|
||||||
|
|
@ -5400,15 +5482,6 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/toidentifier": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tree-kill": {
|
"node_modules/tree-kill": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||||
|
|
@ -5599,7 +5672,6 @@
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
|
|
|
||||||
|
|
@ -13,18 +13,22 @@
|
||||||
"seed": "node prisma/seed.js"
|
"seed": "node prisma/seed.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/jwt": "^7.2.4",
|
||||||
"@prisma/client": "^5.7.0",
|
"@prisma/client": "^5.7.0",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
"fastify": "^4.25.0",
|
"fastify": "^4.25.0",
|
||||||
"fastify-plugin": "^4.5.0",
|
"fastify-plugin": "^4.5.0",
|
||||||
"@fastify/jwt": "^7.2.4",
|
"jsonwebtoken": "^9.0.3"
|
||||||
"dotenv": "^16.3.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.3.3",
|
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^20.10.0",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
"prisma": "^5.7.0",
|
"prisma": "^5.7.0",
|
||||||
"ts-node-dev": "^2.0.0",
|
"ts-node-dev": "^2.0.0",
|
||||||
"jest": "^29.7.0",
|
"typescript": "^5.3.3"
|
||||||
"eslint": "^8.56.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const { PrismaClient } = require('@prisma/client');
|
const { PrismaClient } = require('@prisma/client');
|
||||||
|
const bcrypt = require('bcrypt');
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
|
@ -21,21 +22,47 @@ const RoomType = {
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log('Seeding database...');
|
console.log('Seeding database...');
|
||||||
|
|
||||||
// Create Owner
|
// Hash password once for all users
|
||||||
const ownerEmail = 'admin@runfoo.run';
|
const hashedPassword = await bcrypt.hash('password123', 10);
|
||||||
const existingOwner = await prisma.user.findUnique({ where: { email: ownerEmail } });
|
|
||||||
|
|
||||||
if (!existingOwner) {
|
// Create test users for each role
|
||||||
await prisma.user.create({
|
const users = [
|
||||||
data: {
|
{
|
||||||
email: ownerEmail,
|
email: 'admin@runfoo.run',
|
||||||
passwordHash: 'password123', // In real app, hash this
|
passwordHash: hashedPassword,
|
||||||
name: 'Facility Owner',
|
name: 'Facility Owner',
|
||||||
role: Role.OWNER,
|
role: Role.OWNER,
|
||||||
rate: 50.00
|
rate: 50.00
|
||||||
}
|
},
|
||||||
});
|
{
|
||||||
console.log('Created Owner: admin@runfoo.run / password123');
|
email: 'manager@runfoo.run',
|
||||||
|
passwordHash: hashedPassword,
|
||||||
|
name: 'Operations Manager',
|
||||||
|
role: Role.MANAGER,
|
||||||
|
rate: 35.00
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: 'grower@runfoo.run',
|
||||||
|
passwordHash: hashedPassword,
|
||||||
|
name: 'Head Grower',
|
||||||
|
role: Role.GROWER,
|
||||||
|
rate: 30.00
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: 'staff@runfoo.run',
|
||||||
|
passwordHash: hashedPassword,
|
||||||
|
name: 'Floor Staff',
|
||||||
|
role: Role.STAFF,
|
||||||
|
rate: 20.00
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const userData of users) {
|
||||||
|
const existing = await prisma.user.findUnique({ where: { email: userData.email } });
|
||||||
|
if (!existing) {
|
||||||
|
await prisma.user.create({ data: userData });
|
||||||
|
console.log(`Created ${userData.role}: ${userData.email} / password123`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create Default Rooms
|
// Create Default Rooms
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { comparePassword } from '../utils/password';
|
||||||
|
import { generateAccessToken, generateRefreshToken, verifyToken } from '../utils/jwt';
|
||||||
|
|
||||||
export const login = async (request: FastifyRequest, reply: FastifyReply) => {
|
export const login = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
const { email, password } = request.body as any;
|
const { email, password } = request.body as any;
|
||||||
|
|
@ -15,20 +17,66 @@ export const login = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
return reply.code(401).send({ message: 'Invalid credentials' });
|
return reply.code(401).send({ message: 'Invalid credentials' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Use bcrypt.compare
|
// Compare password with bcrypt
|
||||||
// For now (Foundation), simple check (assuming seed uses cleartext or we fix later)
|
const isValid = await comparePassword(password, user.passwordHash);
|
||||||
// In real app, verify passwordHash
|
|
||||||
if (user.passwordHash !== password) {
|
if (!isValid) {
|
||||||
return reply.code(401).send({ message: 'Invalid credentials' });
|
return reply.code(401).send({ message: 'Invalid credentials' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = request.server.jwt.sign({
|
// Generate tokens
|
||||||
id: user.id,
|
const payload = {
|
||||||
|
userId: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
role: user.role
|
role: user.role
|
||||||
});
|
};
|
||||||
|
|
||||||
return { token, user: { id: user.id, email: user.email, role: user.role } };
|
const accessToken = generateAccessToken(payload);
|
||||||
|
const refreshToken = generateRefreshToken(payload);
|
||||||
|
|
||||||
|
// TODO: Store refresh token in Redis for invalidation on logout
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
role: user.role
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const refresh = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
const { refreshToken } = request.body as any;
|
||||||
|
|
||||||
|
if (!refreshToken) {
|
||||||
|
return reply.code(400).send({ message: 'Refresh token required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verify refresh token
|
||||||
|
const payload = verifyToken(refreshToken);
|
||||||
|
|
||||||
|
// TODO: Check if refresh token is in Redis (not revoked)
|
||||||
|
|
||||||
|
// Generate new access token
|
||||||
|
const newAccessToken = generateAccessToken({
|
||||||
|
userId: payload.userId,
|
||||||
|
email: payload.email,
|
||||||
|
role: payload.role
|
||||||
|
});
|
||||||
|
|
||||||
|
return { accessToken: newAccessToken };
|
||||||
|
} catch (error) {
|
||||||
|
return reply.code(401).send({ message: 'Invalid or expired refresh token' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logout = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
// TODO: Invalidate refresh token in Redis
|
||||||
|
return { message: 'Logged out successfully' };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const me = async (request: FastifyRequest, reply: FastifyReply) => {
|
export const me = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
|
@ -39,3 +87,4 @@ export const me = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
reply.send(err);
|
reply.send(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { FastifyInstance } from 'fastify';
|
import { FastifyInstance } from 'fastify';
|
||||||
import { login, me } from '../controllers/auth.controller';
|
import { login, refresh, logout, me } from '../controllers/auth.controller';
|
||||||
|
|
||||||
export async function authRoutes(server: FastifyInstance) {
|
export async function authRoutes(server: FastifyInstance) {
|
||||||
server.post('/login', login);
|
server.post('/login', login);
|
||||||
|
server.post('/refresh', refresh);
|
||||||
|
server.post('/logout', logout);
|
||||||
server.get('/me', me);
|
server.get('/me', me);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
38
backend/src/utils/jwt.ts
Normal file
38
backend/src/utils/jwt.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'supersecret';
|
||||||
|
|
||||||
|
export interface TokenPayload {
|
||||||
|
userId: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an access token (short-lived)
|
||||||
|
*/
|
||||||
|
export function generateAccessToken(payload: TokenPayload): string {
|
||||||
|
return jwt.sign(payload, JWT_SECRET, {
|
||||||
|
expiresIn: '15m',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a refresh token (long-lived)
|
||||||
|
*/
|
||||||
|
export function generateRefreshToken(payload: TokenPayload): string {
|
||||||
|
return jwt.sign(payload, JWT_SECRET, {
|
||||||
|
expiresIn: '7d',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify and decode a token
|
||||||
|
*/
|
||||||
|
export function verifyToken(token: string): TokenPayload {
|
||||||
|
try {
|
||||||
|
return jwt.verify(token, JWT_SECRET) as TokenPayload;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Invalid or expired token');
|
||||||
|
}
|
||||||
|
}
|
||||||
17
backend/src/utils/password.ts
Normal file
17
backend/src/utils/password.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
|
||||||
|
const SALT_ROUNDS = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash a plaintext password
|
||||||
|
*/
|
||||||
|
export async function hashPassword(password: string): Promise<string> {
|
||||||
|
return bcrypt.hash(password, SALT_ROUNDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare a plaintext password with a hashed password
|
||||||
|
*/
|
||||||
|
export async function comparePassword(password: string, hash: string): Promise<boolean> {
|
||||||
|
return bcrypt.compare(password, hash);
|
||||||
|
}
|
||||||
220
docs/SPRINT-2-AUTH.md
Normal file
220
docs/SPRINT-2-AUTH.md
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
# Sprint 2: Authentication & RBAC
|
||||||
|
|
||||||
|
**Date**: 2025-12-09
|
||||||
|
**Status**: 🟡 In Progress
|
||||||
|
**Duration**: 8-10 hours
|
||||||
|
**Priority**: 🔴 Critical
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Objective
|
||||||
|
|
||||||
|
Implement secure authentication and role-based access control (RBAC) to protect all API endpoints and enforce permissions based on user roles.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Current State
|
||||||
|
|
||||||
|
### What Works
|
||||||
|
|
||||||
|
- ✅ Basic login endpoint exists (`/api/auth/login`)
|
||||||
|
- ✅ JWT plugin registered in Fastify
|
||||||
|
- ✅ User model with Role enum (OWNER, MANAGER, GROWER, STAFF)
|
||||||
|
- ✅ Frontend AuthContext exists
|
||||||
|
|
||||||
|
### What's Missing
|
||||||
|
|
||||||
|
- ❌ Password hashing (currently plaintext)
|
||||||
|
- ❌ JWT token generation and validation
|
||||||
|
- ❌ Auth middleware to protect routes
|
||||||
|
- ❌ RBAC middleware for role-based permissions
|
||||||
|
- ❌ Refresh token logic
|
||||||
|
- ❌ Proper error handling for auth failures
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Backend Auth Core (3-4 hours)
|
||||||
|
|
||||||
|
#### Task 2.1: Password Hashing
|
||||||
|
|
||||||
|
- [ ] Install bcrypt: `npm install bcrypt @types/bcrypt`
|
||||||
|
- [ ] Create password utility functions (hash, compare)
|
||||||
|
- [ ] Update seed script to hash passwords
|
||||||
|
- [ ] Update login controller to compare hashed passwords
|
||||||
|
|
||||||
|
#### Task 2.2: JWT Token Generation
|
||||||
|
|
||||||
|
- [ ] Create JWT utility functions (generate access token, generate refresh token)
|
||||||
|
- [ ] Update login endpoint to return JWT tokens
|
||||||
|
- [ ] Add token expiry configuration (15min access, 7d refresh)
|
||||||
|
- [ ] Store refresh tokens in Redis
|
||||||
|
|
||||||
|
#### Task 2.3: Auth Middleware
|
||||||
|
|
||||||
|
- [ ] Create `authenticate` middleware (verify JWT)
|
||||||
|
- [ ] Add user info to request object
|
||||||
|
- [ ] Handle expired tokens
|
||||||
|
- [ ] Handle invalid tokens
|
||||||
|
|
||||||
|
#### Task 2.4: RBAC Middleware
|
||||||
|
|
||||||
|
- [ ] Create `authorize(...roles)` middleware
|
||||||
|
- [ ] Check user role against allowed roles
|
||||||
|
- [ ] Return 403 Forbidden if unauthorized
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Protected Routes (2-3 hours)
|
||||||
|
|
||||||
|
#### Task 2.5: Apply Auth to Routes
|
||||||
|
|
||||||
|
- [ ] Protect `/api/rooms` (all roles)
|
||||||
|
- [ ] Protect `/api/batches` (all roles)
|
||||||
|
- [ ] Protect `/api/timeclock` (all roles)
|
||||||
|
- [ ] Add role restrictions where needed
|
||||||
|
|
||||||
|
#### Task 2.6: Refresh Token Endpoint
|
||||||
|
|
||||||
|
- [ ] Create `/api/auth/refresh` endpoint
|
||||||
|
- [ ] Validate refresh token
|
||||||
|
- [ ] Generate new access token
|
||||||
|
- [ ] Return new tokens
|
||||||
|
|
||||||
|
#### Task 2.7: Logout Endpoint
|
||||||
|
|
||||||
|
- [ ] Create `/api/auth/logout` endpoint
|
||||||
|
- [ ] Invalidate refresh token in Redis
|
||||||
|
- [ ] Clear tokens from client
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Frontend Integration (2-3 hours)
|
||||||
|
|
||||||
|
#### Task 2.8: Update AuthContext
|
||||||
|
|
||||||
|
- [ ] Store tokens in localStorage
|
||||||
|
- [ ] Add token refresh logic
|
||||||
|
- [ ] Add automatic logout on 401
|
||||||
|
- [ ] Add token to all API requests
|
||||||
|
|
||||||
|
#### Task 2.9: Update API Client
|
||||||
|
|
||||||
|
- [ ] Create axios instance with interceptors
|
||||||
|
- [ ] Add Authorization header to requests
|
||||||
|
- [ ] Handle 401 (refresh token or logout)
|
||||||
|
- [ ] Handle 403 (show permission error)
|
||||||
|
|
||||||
|
#### Task 2.10: Update Login Flow
|
||||||
|
|
||||||
|
- [ ] Store tokens on successful login
|
||||||
|
- [ ] Redirect to dashboard
|
||||||
|
- [ ] Handle login errors
|
||||||
|
- [ ] Add loading states
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Testing & Polish (1-2 hours)
|
||||||
|
|
||||||
|
#### Task 2.11: Manual Testing
|
||||||
|
|
||||||
|
- [ ] Test login with correct credentials
|
||||||
|
- [ ] Test login with wrong credentials
|
||||||
|
- [ ] Test protected routes without token
|
||||||
|
- [ ] Test protected routes with valid token
|
||||||
|
- [ ] Test token expiry and refresh
|
||||||
|
- [ ] Test logout
|
||||||
|
|
||||||
|
#### Task 2.12: Add More Test Users
|
||||||
|
|
||||||
|
- [ ] Create users for each role (MANAGER, GROWER, STAFF)
|
||||||
|
- [ ] Update seed script
|
||||||
|
- [ ] Document test credentials
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Security Considerations
|
||||||
|
|
||||||
|
### Password Security
|
||||||
|
|
||||||
|
- Use bcrypt with salt rounds = 10
|
||||||
|
- Never log passwords
|
||||||
|
- Never return password hashes in API responses
|
||||||
|
|
||||||
|
### Token Security
|
||||||
|
|
||||||
|
- Access tokens: Short-lived (15 minutes)
|
||||||
|
- Refresh tokens: Longer-lived (7 days)
|
||||||
|
- Store refresh tokens in httpOnly cookies (future enhancement)
|
||||||
|
- Invalidate refresh tokens on logout
|
||||||
|
|
||||||
|
### RBAC Rules
|
||||||
|
|
||||||
|
- **OWNER**: Full access to everything
|
||||||
|
- **MANAGER**: Full access except user management
|
||||||
|
- **GROWER**: Read/write batches, rooms, tasks
|
||||||
|
- **STAFF**: Read-only + timeclock
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Success Criteria
|
||||||
|
|
||||||
|
- [ ] All API endpoints require authentication
|
||||||
|
- [ ] Passwords are hashed with bcrypt
|
||||||
|
- [ ] JWT tokens working (access + refresh)
|
||||||
|
- [ ] Role-based permissions enforced
|
||||||
|
- [ ] Frontend stores and uses tokens correctly
|
||||||
|
- [ ] Token refresh works automatically
|
||||||
|
- [ ] Logout invalidates tokens
|
||||||
|
- [ ] Multiple test users available (one per role)
|
||||||
|
- [ ] No security vulnerabilities (no plaintext passwords, no exposed secrets)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing Checklist
|
||||||
|
|
||||||
|
### Backend Tests
|
||||||
|
|
||||||
|
- [ ] Login with valid credentials returns tokens
|
||||||
|
- [ ] Login with invalid credentials returns 401
|
||||||
|
- [ ] Protected route without token returns 401
|
||||||
|
- [ ] Protected route with valid token returns data
|
||||||
|
- [ ] Protected route with expired token returns 401
|
||||||
|
- [ ] Refresh token endpoint works
|
||||||
|
- [ ] Logout invalidates refresh token
|
||||||
|
- [ ] RBAC blocks unauthorized roles
|
||||||
|
|
||||||
|
### Frontend Tests
|
||||||
|
|
||||||
|
- [ ] Login form submits correctly
|
||||||
|
- [ ] Tokens stored in localStorage
|
||||||
|
- [ ] API requests include Authorization header
|
||||||
|
- [ ] 401 triggers token refresh
|
||||||
|
- [ ] Logout clears tokens and redirects
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Files to Modify
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
- `backend/package.json` - Add bcrypt dependency
|
||||||
|
- `backend/src/utils/password.ts` - New file (hash, compare)
|
||||||
|
- `backend/src/utils/jwt.ts` - New file (generate, verify)
|
||||||
|
- `backend/src/middleware/auth.ts` - New file (authenticate, authorize)
|
||||||
|
- `backend/src/controllers/auth.controller.ts` - Update login, add refresh, logout
|
||||||
|
- `backend/src/routes/*.routes.ts` - Add auth middleware
|
||||||
|
- `backend/prisma/seed.js` - Hash passwords
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
- `frontend/src/context/AuthContext.tsx` - Update token handling
|
||||||
|
- `frontend/src/lib/api.ts` - New file (axios instance with interceptors)
|
||||||
|
- `frontend/src/pages/LoginPage.tsx` - Update login flow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Let's Begin
|
||||||
|
|
||||||
|
Starting with **Task 2.1: Password Hashing**...
|
||||||
Loading…
Add table
Reference in a new issue