From 9dc0586d67a117233fb965d53aaa891aa80ea842 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Tue, 9 Dec 2025 13:52:54 -0800 Subject: [PATCH] feat: Sprint 2 Phase 1 - Auth Core Complete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit โœ… 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 --- CREDENTIALS.md | 24 ++ backend/package-lock.json | 296 +++++++++++++-------- backend/package.json | 16 +- backend/prisma/seed.js | 55 +++- backend/src/controllers/auth.controller.ts | 65 ++++- backend/src/routes/auth.routes.ts | 4 +- backend/src/utils/jwt.ts | 38 +++ backend/src/utils/password.ts | 17 ++ docs/SPRINT-2-AUTH.md | 220 +++++++++++++++ 9 files changed, 594 insertions(+), 141 deletions(-) create mode 100644 backend/src/utils/jwt.ts create mode 100644 backend/src/utils/password.ts create mode 100644 docs/SPRINT-2-AUTH.md diff --git a/CREDENTIALS.md b/CREDENTIALS.md index e7a1a3f..fff5e27 100644 --- a/CREDENTIALS.md +++ b/CREDENTIALS.md @@ -16,6 +16,30 @@ - **Permissions**: Full access to all features - **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 diff --git a/backend/package-lock.json b/backend/package-lock.json index 664123d..ab5d4ae 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -8,11 +8,15 @@ "name": "ca-grow-ops-backend", "version": "1.0.0", "dependencies": { + "@fastify/jwt": "^7.2.4", "@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-jwt": "^4.2.0", - "fastify-plugin": "^4.5.0" + "fastify-plugin": "^4.5.0", + "jsonwebtoken": "^9.0.3" }, "devDependencies": { "@types/node": "^20.10.0", @@ -690,6 +694,34 @@ "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": { "version": "0.1.1", "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" } }, + "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": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -1454,11 +1495,26 @@ "@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": { "version": "20.19.26", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.26.tgz", "integrity": "sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -1880,6 +1936,20 @@ "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": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1967,6 +2037,12 @@ "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": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2273,15 +2349,6 @@ "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": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2726,20 +2793,6 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "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": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -2808,46 +2861,6 @@ "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": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", @@ -3151,26 +3164,6 @@ "dev": true, "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": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -4121,6 +4114,49 @@ "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": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4199,6 +4235,42 @@ "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": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4206,6 +4278,12 @@ "dev": true, "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": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -4335,7 +4413,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/natural-compare": { @@ -4345,6 +4422,26 @@ "dev": true, "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": { "version": "0.4.0", "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==", "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5217,15 +5308,6 @@ "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": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/steed/-/steed-1.1.3.tgz", @@ -5400,15 +5482,6 @@ "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": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -5599,7 +5672,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/update-browserslist-db": { diff --git a/backend/package.json b/backend/package.json index 9b67b6f..739f69a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,18 +13,22 @@ "seed": "node prisma/seed.js" }, "dependencies": { + "@fastify/jwt": "^7.2.4", "@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-plugin": "^4.5.0", - "@fastify/jwt": "^7.2.4", - "dotenv": "^16.3.1" + "jsonwebtoken": "^9.0.3" }, "devDependencies": { - "typescript": "^5.3.3", "@types/node": "^20.10.0", + "eslint": "^8.56.0", + "jest": "^29.7.0", "prisma": "^5.7.0", "ts-node-dev": "^2.0.0", - "jest": "^29.7.0", - "eslint": "^8.56.0" + "typescript": "^5.3.3" } -} \ No newline at end of file +} diff --git a/backend/prisma/seed.js b/backend/prisma/seed.js index b04df94..6ad901f 100644 --- a/backend/prisma/seed.js +++ b/backend/prisma/seed.js @@ -1,4 +1,5 @@ const { PrismaClient } = require('@prisma/client'); +const bcrypt = require('bcrypt'); const prisma = new PrismaClient(); @@ -21,21 +22,47 @@ const RoomType = { async function main() { console.log('Seeding database...'); - // Create Owner - const ownerEmail = 'admin@runfoo.run'; - const existingOwner = await prisma.user.findUnique({ where: { email: ownerEmail } }); + // Hash password once for all users + const hashedPassword = await bcrypt.hash('password123', 10); - if (!existingOwner) { - await prisma.user.create({ - data: { - email: ownerEmail, - passwordHash: 'password123', // In real app, hash this - name: 'Facility Owner', - role: Role.OWNER, - rate: 50.00 - } - }); - console.log('Created Owner: admin@runfoo.run / password123'); + // Create test users for each role + const users = [ + { + email: 'admin@runfoo.run', + passwordHash: hashedPassword, + name: 'Facility Owner', + role: Role.OWNER, + rate: 50.00 + }, + { + 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 diff --git a/backend/src/controllers/auth.controller.ts b/backend/src/controllers/auth.controller.ts index 7a12979..f44b451 100644 --- a/backend/src/controllers/auth.controller.ts +++ b/backend/src/controllers/auth.controller.ts @@ -1,4 +1,6 @@ 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) => { 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' }); } - // TODO: Use bcrypt.compare - // For now (Foundation), simple check (assuming seed uses cleartext or we fix later) - // In real app, verify passwordHash - if (user.passwordHash !== password) { + // Compare password with bcrypt + const isValid = await comparePassword(password, user.passwordHash); + + if (!isValid) { return reply.code(401).send({ message: 'Invalid credentials' }); } - const token = request.server.jwt.sign({ - id: user.id, + // Generate tokens + const payload = { + userId: user.id, email: user.email, 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) => { @@ -39,3 +87,4 @@ export const me = async (request: FastifyRequest, reply: FastifyReply) => { reply.send(err); } }; + diff --git a/backend/src/routes/auth.routes.ts b/backend/src/routes/auth.routes.ts index cad0c6d..a7c44d2 100644 --- a/backend/src/routes/auth.routes.ts +++ b/backend/src/routes/auth.routes.ts @@ -1,7 +1,9 @@ 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) { server.post('/login', login); + server.post('/refresh', refresh); + server.post('/logout', logout); server.get('/me', me); } diff --git a/backend/src/utils/jwt.ts b/backend/src/utils/jwt.ts new file mode 100644 index 0000000..1d7eb07 --- /dev/null +++ b/backend/src/utils/jwt.ts @@ -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'); + } +} diff --git a/backend/src/utils/password.ts b/backend/src/utils/password.ts new file mode 100644 index 0000000..551b545 --- /dev/null +++ b/backend/src/utils/password.ts @@ -0,0 +1,17 @@ +import bcrypt from 'bcrypt'; + +const SALT_ROUNDS = 10; + +/** + * Hash a plaintext password + */ +export async function hashPassword(password: string): Promise { + return bcrypt.hash(password, SALT_ROUNDS); +} + +/** + * Compare a plaintext password with a hashed password + */ +export async function comparePassword(password: string, hash: string): Promise { + return bcrypt.compare(password, hash); +} diff --git a/docs/SPRINT-2-AUTH.md b/docs/SPRINT-2-AUTH.md new file mode 100644 index 0000000..fa5792a --- /dev/null +++ b/docs/SPRINT-2-AUTH.md @@ -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**...