From c995d0579de00e6793ef413ba16a9854a32233fa Mon Sep 17 00:00:00 2001 From: Gunjan Verma Date: Wed, 22 Oct 2025 17:28:11 +0530 Subject: [PATCH 1/8] add pagination and filter based on status --- src/controllers/taskController.js | 35 ++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/controllers/taskController.js b/src/controllers/taskController.js index f508afe..f795828 100644 --- a/src/controllers/taskController.js +++ b/src/controllers/taskController.js @@ -3,9 +3,38 @@ const { Task } = require('../models'); exports.getTasks = async (req, res, next) => { try { - const tasks = await Task.findAll({ where: { userId: req.user.id } }); - res.json(tasks); - } catch (err) { next(err); } + // Extract query params + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + const status = req.query.status; + + const offset = (page - 1) * limit; + + // Build where condition + let userTaskStatus = { userId: req.user.id }; + if (status) { + userTaskStatus.status = status; + } + + // Fetch tasks + total tasks count + const { rows: tasks, count: totalTasks } = await Task.findAndCountAll({ + where: userTaskStatus, + limit, + offset, + order: [['createdAt', 'DESC']], + }); + + res.json({ + page, + limit, + totalTasks, + totalPages: Math.ceil(totalTasks / limit), + tasks, + }); + + } catch (err) { + next(err); + } }; exports.createTask = async (req, res, next) => { From 0fceefda220ca8e88a6510bdf168a992c5bb226d Mon Sep 17 00:00:00 2001 From: Gunjan Verma Date: Wed, 22 Oct 2025 18:42:46 +0530 Subject: [PATCH 2/8] add proper validation for status --- src/controllers/taskController.js | 4 ++-- src/models/task.js | 10 ++++++++++ src/routes/authRoutes.js | 12 +++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/controllers/taskController.js b/src/controllers/taskController.js index f795828..f8e1919 100644 --- a/src/controllers/taskController.js +++ b/src/controllers/taskController.js @@ -41,8 +41,8 @@ exports.createTask = async (req, res, next) => { try { const errors = validationResult(req); if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); - const { title, description } = req.body; - const task = await Task.create({ title, description, userId: req.user.id }); + const { title, description, status } = req.body; + const task = await Task.create({ title, description,status, userId: req.user.id }); res.status(201).json(task); } catch (err) { next(err); } }; diff --git a/src/models/task.js b/src/models/task.js index 374907c..c5af45b 100644 --- a/src/models/task.js +++ b/src/models/task.js @@ -8,7 +8,17 @@ const Task = sequelize.define('Task', { status: { type: DataTypes.ENUM('pending', 'in-progress', 'done'), defaultValue: 'pending', + validate: { + isIn: { + args: [['pending', 'in-progress', 'done']], + msg: 'Status must be one of: pending, in-progress, done' + } + } }, +}, { + paranoid: true, // Enables soft delete + timestamps: true, + deletedAt: 'deletedAt', }); Task.belongsTo(User, { foreignKey: 'userId', onDelete: 'CASCADE' }); diff --git a/src/routes/authRoutes.js b/src/routes/authRoutes.js index 34b61e8..a08b1cb 100644 --- a/src/routes/authRoutes.js +++ b/src/routes/authRoutes.js @@ -3,6 +3,16 @@ const { body } = require('express-validator'); const router = express.Router(); const authController = require('../controllers/authController'); -router.post('/register', [body('username').notEmpty(), body('password').isLength({ min: 5 })], authController.register); +router.post( + '/register', + [ + body('username') + .notEmpty().withMessage('Username is required') + .isString().withMessage('Username must be a string'), + body('password') + .isLength({ min: 5 }).withMessage('Password must be at least 5 characters') + ], + authController.register + ); router.post('/login', authController.login); module.exports = router; \ No newline at end of file From 383ca6954c526411b47b99c3b66b2fb519e31858 Mon Sep 17 00:00:00 2001 From: Gunjan Verma Date: Wed, 22 Oct 2025 19:40:52 +0530 Subject: [PATCH 3/8] add role based access control and request logging --- package-lock.json | 71 +++++++++++++++++++++++++++++++ package.json | 3 +- src/app.js | 2 + src/controllers/authController.js | 4 +- src/controllers/taskController.js | 15 ++++--- src/middleware/checkRoles.js | 12 ++++++ src/models/user.js | 5 +++ src/routes/authRoutes.js | 3 +- src/services/authService.js | 10 +++-- 9 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 src/middleware/checkRoles.js diff --git a/package-lock.json b/package-lock.json index 2b4ee5b..90cb25a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "express": "^4.21.1", "express-validator": "^7.2.1", "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.1", "pg": "^8.13.1", "pg-hstore": "^2.3.4", "sequelize": "^6.37.3" @@ -1732,6 +1733,24 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", @@ -4723,6 +4742,49 @@ "node": "*" } }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4921,6 +4983,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/package.json b/package.json index 9befbd1..f48dd37 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "express": "^4.21.1", "express-validator": "^7.2.1", "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.1", "pg": "^8.13.1", "pg-hstore": "^2.3.4", "sequelize": "^6.37.3" @@ -29,4 +30,4 @@ "prettier": "^3.3.3", "supertest": "^7.1.1" } -} \ No newline at end of file +} diff --git a/src/app.js b/src/app.js index 2453aff..87f6525 100644 --- a/src/app.js +++ b/src/app.js @@ -1,9 +1,11 @@ const express = require('express'); +const morgan = require('morgan'); const app = express(); const authRoutes = require('./routes/authRoutes'); const taskRoutes = require('./routes/taskRoutes'); const errorHandler = require('./middleware/errorHandler'); +app.use(morgan('dev')); app.use(express.json()); app.use('/api/auth', authRoutes); app.use('/api/tasks', taskRoutes); diff --git a/src/controllers/authController.js b/src/controllers/authController.js index 92291a7..57aa9af 100644 --- a/src/controllers/authController.js +++ b/src/controllers/authController.js @@ -5,8 +5,8 @@ exports.register = async (req, res, next) => { try { const errors = validationResult(req); if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); - const { username, password } = req.body; - const user = await authService.register(username, password); + const { username, password, role } = req.body; + const user = await authService.register(username, password, role); res.status(201).json({ message: 'User created', user }); } catch (err) { next(err); } }; diff --git a/src/controllers/taskController.js b/src/controllers/taskController.js index f8e1919..c785f0f 100644 --- a/src/controllers/taskController.js +++ b/src/controllers/taskController.js @@ -3,25 +3,29 @@ const { Task } = require('../models'); exports.getTasks = async (req, res, next) => { try { - // Extract query params const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 10; + const offset = (page - 1) * limit; const status = req.query.status; - const offset = (page - 1) * limit; + let userTaskStatus = {}; + + if (req.user.role !== 'admin') { + // Non-admin: filter by their own tasks + userTaskStatus.userId = req.user.id; + } - // Build where condition - let userTaskStatus = { userId: req.user.id }; if (status) { + // Apply status filter for everyone userTaskStatus.status = status; } - // Fetch tasks + total tasks count const { rows: tasks, count: totalTasks } = await Task.findAndCountAll({ where: userTaskStatus, limit, offset, order: [['createdAt', 'DESC']], + paranoid: false }); res.json({ @@ -31,7 +35,6 @@ exports.getTasks = async (req, res, next) => { totalPages: Math.ceil(totalTasks / limit), tasks, }); - } catch (err) { next(err); } diff --git a/src/middleware/checkRoles.js b/src/middleware/checkRoles.js new file mode 100644 index 0000000..4d77155 --- /dev/null +++ b/src/middleware/checkRoles.js @@ -0,0 +1,12 @@ +module.exports = (requiredRole) => { + return (req, res, next) => { + if (!req.user) return res.status(401).json({ message: 'Unauthorized' }); + + if (req.user.role !== requiredRole) { + return res.status(403).json({ message: 'Forbidden: insufficient privileges' }); + } + + next(); + }; + }; + \ No newline at end of file diff --git a/src/models/user.js b/src/models/user.js index afebaad..af06b0c 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -5,6 +5,11 @@ const bcrypt = require('bcrypt'); const User = sequelize.define('User', { username: { type: DataTypes.STRING, allowNull: false, unique: true }, password: { type: DataTypes.STRING, allowNull: false }, + role: { + type: DataTypes.ENUM('user', 'admin'), + defaultValue: 'user', + allowNull: false + } }); User.beforeCreate(async (user) => { diff --git a/src/routes/authRoutes.js b/src/routes/authRoutes.js index a08b1cb..26f31be 100644 --- a/src/routes/authRoutes.js +++ b/src/routes/authRoutes.js @@ -10,7 +10,8 @@ router.post( .notEmpty().withMessage('Username is required') .isString().withMessage('Username must be a string'), body('password') - .isLength({ min: 5 }).withMessage('Password must be at least 5 characters') + .isLength({ min: 5 }).withMessage('Password must be at least 5 characters'), + body('role').optional().isIn(['user', 'admin']).withMessage('Role must be either user or admin') ], authController.register ); diff --git a/src/services/authService.js b/src/services/authService.js index 4865903..da803f2 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -2,10 +2,14 @@ const jwt = require('jsonwebtoken'); const bcrypt = require('bcrypt'); const { User } = require('../models'); -exports.register = async (username, password) => { +exports.register = async (username, password, role = 'user') => { const existing = await User.findOne({ where: { username } }); if (existing) throw new Error('Username already exists'); - return await User.create({ username, password }); + + if (!['user', 'admin'].includes(role)) { + throw new Error('Invalid role'); + } + return await User.create({ username, password, role }); }; exports.login = async (username, password) => { @@ -14,7 +18,7 @@ exports.login = async (username, password) => { const valid = await bcrypt.compare(password, user.password); if (!valid) throw new Error('Invalid credentials'); const token = jwt.sign( - { id: user.id, username: user.username }, + { id: user.id, username: user.username, role: user.role }, process.env.JWT_SECRET, { expiresIn: '1h' } ); From a9675a6fe64e7c05624a2c0cff173d64100fab69 Mon Sep 17 00:00:00 2001 From: Gunjan Verma Date: Wed, 22 Oct 2025 19:46:49 +0530 Subject: [PATCH 4/8] added rate limiter per IP --- package-lock.json | 28 ++++++++++++++++++++++++++++ package.json | 1 + src/middleware/rateLimiter.js | 14 ++++++++++++++ src/routes/authRoutes.js | 3 ++- 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 src/middleware/rateLimiter.js diff --git a/package-lock.json b/package-lock.json index 90cb25a..a8fa27e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "bcrypt": "^5.1.1", "dotenv": "^16.4.5", "express": "^4.21.1", + "express-rate-limit": "^8.1.0", "express-validator": "^7.2.1", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.1", @@ -2840,6 +2841,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.1.0.tgz", + "integrity": "sha512-4nLnATuKupnmwqiJc27b4dCFmB/T60ExgmtDD7waf4LdrbJ8CPZzZRHYErDYNhoz+ql8fUdYwM/opf90PoPAQA==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/express-validator": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", @@ -3487,6 +3506,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/package.json b/package.json index f48dd37..cfa02d2 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "bcrypt": "^5.1.1", "dotenv": "^16.4.5", "express": "^4.21.1", + "express-rate-limit": "^8.1.0", "express-validator": "^7.2.1", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.1", diff --git a/src/middleware/rateLimiter.js b/src/middleware/rateLimiter.js new file mode 100644 index 0000000..5b0905e --- /dev/null +++ b/src/middleware/rateLimiter.js @@ -0,0 +1,14 @@ +const rateLimit = require('express-rate-limit'); + +// Limit login attempts to 5 per 15 minutes per IP +const loginLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 5, + message: { + message: 'Too many login attempts from this IP. Please try again after 15 minutes.' + }, + standardHeaders: true, + legacyHeaders: false, +}); + +module.exports = loginLimiter; diff --git a/src/routes/authRoutes.js b/src/routes/authRoutes.js index 26f31be..d5e36e0 100644 --- a/src/routes/authRoutes.js +++ b/src/routes/authRoutes.js @@ -2,6 +2,7 @@ const express = require('express'); const { body } = require('express-validator'); const router = express.Router(); const authController = require('../controllers/authController'); +const loginLimiter = require('../middleware/rateLimiter') router.post( '/register', @@ -15,5 +16,5 @@ router.post( ], authController.register ); -router.post('/login', authController.login); +router.post('/login',loginLimiter, authController.login); module.exports = router; \ No newline at end of file From 45ab437cd684bca0255450d06974dfdab383ef10 Mon Sep 17 00:00:00 2001 From: Gunjan Verma Date: Wed, 22 Oct 2025 20:07:04 +0530 Subject: [PATCH 5/8] add swagger docs for task creation and authentication --- package-lock.json | 235 ++++++++++++++++++++++++++++++++++++++- package.json | 4 +- src/app.js | 3 + src/config/swagger.js | 38 +++++++ src/routes/authRoutes.js | 59 ++++++++++ src/routes/taskRoutes.js | 123 +++++++++++++++++++- 6 files changed, 454 insertions(+), 8 deletions(-) create mode 100644 src/config/swagger.js diff --git a/package-lock.json b/package-lock.json index a8fa27e..0f755b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,9 @@ "morgan": "^1.10.1", "pg": "^8.13.1", "pg-hstore": "^2.3.4", - "sequelize": "^6.37.3" + "sequelize": "^6.37.3", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" }, "devDependencies": { "cross-env": "^7.0.3", @@ -28,6 +30,50 @@ "supertest": "^7.1.1" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1199,6 +1245,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -1242,6 +1294,13 @@ "@noble/hashes": "^1.1.5" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1371,7 +1430,6 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, "license": "MIT" }, "node_modules/@types/ms": { @@ -1569,7 +1627,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-flatten": { @@ -1936,6 +1993,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2149,6 +2212,15 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/component-emitter": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", @@ -2401,6 +2473,18 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -2730,7 +2814,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -4313,7 +4396,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4492,6 +4574,13 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -4504,6 +4593,13 @@ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "license": "MIT" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", @@ -4535,6 +4631,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "license": "MIT" + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -5045,6 +5147,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6251,6 +6360,83 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.29.5", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.29.5.tgz", + "integrity": "sha512-2zFnjONgLXlz8gLToRKvXHKJdqXF6UGgCmv65i8T6i/UrjDNyV1fIQ7FauZA40SaivlGKEvW2tw9XDyDhfcXqQ==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -6649,6 +6835,15 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -6690,6 +6885,36 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } } } } diff --git a/package.json b/package.json index cfa02d2..dda4941 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,9 @@ "morgan": "^1.10.1", "pg": "^8.13.1", "pg-hstore": "^2.3.4", - "sequelize": "^6.37.3" + "sequelize": "^6.37.3", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" }, "devDependencies": { "cross-env": "^7.0.3", diff --git a/src/app.js b/src/app.js index 87f6525..f7ce7b4 100644 --- a/src/app.js +++ b/src/app.js @@ -4,11 +4,14 @@ const app = express(); const authRoutes = require('./routes/authRoutes'); const taskRoutes = require('./routes/taskRoutes'); const errorHandler = require('./middleware/errorHandler'); +const swaggerDocs = require('./config/swagger'); + app.use(morgan('dev')); app.use(express.json()); app.use('/api/auth', authRoutes); app.use('/api/tasks', taskRoutes); app.use(errorHandler); +swaggerDocs(app); module.exports = app; \ No newline at end of file diff --git a/src/config/swagger.js b/src/config/swagger.js new file mode 100644 index 0000000..46e7dca --- /dev/null +++ b/src/config/swagger.js @@ -0,0 +1,38 @@ +// swagger.js +const swaggerJsdoc = require('swagger-jsdoc'); +const swaggerUi = require('swagger-ui-express'); +const path = require('path'); + +const options = { + definition: { + openapi: '3.0.0', + info: { + title: 'Task Manager API', + version: '1.0.0', + description: 'API documentation for Task Manager', + }, + servers: [ + { url: 'http://localhost:3000' } + ], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + }, + security: [{ bearerAuth: [] }], + }, + apis: [path.join(__dirname, '../routes/*.js')], +}; + +const specs = swaggerJsdoc(options); + +const swaggerDocs = (app) => { + app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(specs)); + console.log('Swagger docs available at /api/docs'); +}; + +module.exports = swaggerDocs; \ No newline at end of file diff --git a/src/routes/authRoutes.js b/src/routes/authRoutes.js index d5e36e0..5c98c70 100644 --- a/src/routes/authRoutes.js +++ b/src/routes/authRoutes.js @@ -4,6 +4,40 @@ const router = express.Router(); const authController = require('../controllers/authController'); const loginLimiter = require('../middleware/rateLimiter') +/** + * @swagger + * tags: + * name: Authentication + * description: User registration and login + */ + +/** + * @swagger + * /api/auth/register: + * post: + * summary: Register a new user + * tags: [Authentication] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * username: + * type: string + * password: + * type: string + * role: + * type: string + * enum: [user, admin] + * responses: + * 201: + * description: User created successfully + * 400: + * description: Validation error + */ + router.post( '/register', [ @@ -16,5 +50,30 @@ router.post( ], authController.register ); + +/** + * @swagger + * /api/auth/login: + * post: + * summary: Login a user and get JWT token + * tags: [Authentication] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * username: + * type: string + * password: + * type: string + * responses: + * 200: + * description: JWT token returned + * 400: + * description: Invalid credentials + */ + router.post('/login',loginLimiter, authController.login); module.exports = router; \ No newline at end of file diff --git a/src/routes/taskRoutes.js b/src/routes/taskRoutes.js index 264132a..4a3288e 100644 --- a/src/routes/taskRoutes.js +++ b/src/routes/taskRoutes.js @@ -4,9 +4,128 @@ const router = express.Router(); const taskController = require('../controllers/taskController'); const auth = require('../middleware/authMiddleware'); +/** + * @swagger + * tags: + * name: Tasks + * description: Task management + */ + router.use(auth); + +/** + * @swagger + * /api/tasks: + * get: + * summary: Get all tasks for the authenticated user + * tags: [Tasks] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: List of tasks + * content: + * application/json: + * schema: + * type: object + * properties: + * page: + * type: integer + * limit: + * type: integer + * totalTasks: + * type: integer + * totalPages: + * type: integer + * tasks: + * type: array + * items: + * type: object + */ router.get('/', taskController.getTasks); -router.post('/', [body('title').notEmpty()], taskController.createTask); + +/** + * @swagger + * /api/tasks: + * post: + * summary: Create a new task + * tags: [Tasks] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - title + * properties: + * title: + * type: string + * description: + * type: string + * status: + * type: string + * enum: [pending, in-progress, completed] + * responses: + * 201: + * description: Task created successfully + */ +router.post('/', [body('title').notEmpty().withMessage('Title is required')], taskController.createTask); + +/** + * @swagger + * /api/tasks/{id}: + * put: + * summary: Update a task by ID + * tags: [Tasks] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * title: + * type: string + * description: + * type: string + * status: + * type: string + * enum: [pending, in-progress, completed] + * responses: + * 200: + * description: Task updated successfully + */ router.put('/:id', taskController.updateTask); + +/** + * @swagger + * /api/tasks/{id}: + * delete: + * summary: Delete a task by ID + * tags: [Tasks] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Task deleted successfully + */ router.delete('/:id', taskController.deleteTask); -module.exports = router; \ No newline at end of file + +module.exports = router; From e36a60da379eed884cac99ff0f16cc16ccd52c4f Mon Sep 17 00:00:00 2001 From: Gunjan Verma Date: Thu, 23 Oct 2025 00:37:32 +0530 Subject: [PATCH 6/8] docerize the application and add email notification services by using docker and redis --- Dockerfile | 20 + docker-compose.yml | 55 +++ package-lock.json | 687 +++++++++++++----------------- package.json | 5 +- src/config/db.js | 6 +- src/controllers/authController.js | 4 +- src/controllers/taskController.js | 29 +- src/models/index.js | 6 +- src/models/user.js | 3 +- src/queues/emailQueue.js | 11 + src/server.js | 2 +- src/services/authService.js | 23 +- src/workers/emailWorker.js | 38 ++ 13 files changed, 475 insertions(+), 414 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 src/queues/emailQueue.js create mode 100644 src/workers/emailWorker.js diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ddbc134 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +# Use official Node.js LTS image +FROM node:20 + +# Set working directory +WORKDIR /usr/src/app + +# Copy package.json and package-lock.json +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy the rest of the app +COPY . . + +# Expose app port +EXPOSE 3000 + +# Start the app +CMD ["npm", "start"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..48772d1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,55 @@ +version: "3.9" + +services: + app: + build: . + container_name: node-app + ports: + - "${PORT}:${PORT}" + env_file: + - .env + depends_on: + - postgres + - redis + + worker: + build: . + container_name: email-worker + env_file: + - .env + depends_on: + - redis + - postgres + command: node src/workers/emailWorker.js + + postgres: + image: postgres:15 + container_name: postgres + restart: always + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME} + ports: + - "${DB_PORT}:${DB_PORT}" + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7 + container_name: redis + ports: + - "${REDIS_PORT}:${REDIS_PORT}" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + pgdata: diff --git a/package-lock.json b/package-lock.json index 0f755b5..bafb36d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,16 @@ "name": "nodejs-task-manager", "version": "1.0.0", "dependencies": { - "bcrypt": "^5.1.1", + "bcryptjs": "^3.0.2", + "bullmq": "^5.61.0", "dotenv": "^16.4.5", "express": "^4.21.1", "express-rate-limit": "^8.1.0", "express-validator": "^7.2.1", + "ioredis": "^5.8.2", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.1", + "nodemailer": "^7.0.9", "pg": "^8.13.1", "pg-hstore": "^2.3.4", "sequelize": "^6.37.3", @@ -786,6 +789,12 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@ioredis/commands": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", + "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", + "license": "MIT" + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1251,25 +1260,83 @@ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", "license": "MIT" }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "license": "BSD-3-Clause", - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@noble/hashes": { "version": "1.8.0", @@ -1477,12 +1544,6 @@ "dev": true, "license": "MIT" }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "license": "ISC" - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -1519,18 +1580,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1568,6 +1617,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1603,26 +1653,6 @@ "node": ">= 8" } }, - "node_modules/aproba": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", - "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", - "license": "ISC" - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1809,18 +1839,13 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, - "node_modules/bcrypt": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", - "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.11", - "node-addon-api": "^5.0.0" - }, - "engines": { - "node": ">= 10.0.0" + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" } }, "node_modules/binary-extensions": { @@ -1955,6 +1980,34 @@ "dev": true, "license": "MIT" }, + "node_modules/bullmq": { + "version": "5.61.0", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.61.0.tgz", + "integrity": "sha512-khaTjc1JnzaYFl4FrUtsSsqugAW/urRrcZ9Q0ZE+REAw8W+gkHFqxbGlutOu6q7j7n91wibVaaNlOUMdiEvoSQ==", + "license": "MIT", + "dependencies": { + "cron-parser": "^4.9.0", + "ioredis": "^5.4.1", + "msgpackr": "^1.11.2", + "node-abort-controller": "^3.1.1", + "semver": "^7.5.4", + "tslib": "^2.0.0", + "uuid": "^11.1.0" + } + }, + "node_modules/bullmq/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2105,15 +2158,6 @@ "node": ">= 6" } }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -2152,6 +2196,15 @@ "node": ">=12" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -2190,15 +2243,6 @@ "dev": true, "license": "MIT" }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "license": "ISC", - "bin": { - "color-support": "bin.js" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2237,12 +2281,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "license": "ISC" - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -2315,6 +2353,18 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -2408,11 +2458,14 @@ "node": ">=0.4.0" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "license": "MIT" + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } }, "node_modules/depd": { "version": "2.0.0", @@ -2438,6 +2491,7 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", + "optional": true, "engines": { "node": ">=8" } @@ -2556,6 +2610,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -3158,36 +3213,6 @@ "node": ">= 0.6" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3218,27 +3243,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3324,6 +3328,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -3423,12 +3428,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "license": "ISC" - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -3464,19 +3463,6 @@ "node": ">= 0.8" } }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -3589,6 +3575,30 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ioredis": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz", + "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.4.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ip-address": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", @@ -3657,6 +3667,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4574,6 +4585,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -4587,6 +4604,12 @@ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -4653,28 +4676,13 @@ "yallist": "^3.0.2" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node": ">=12" } }, "node_modules/makeerror": { @@ -4799,58 +4807,6 @@ "node": "*" } }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -4921,6 +4877,37 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/msgpackr": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", + "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4937,30 +4924,25 @@ "node": ">= 0.6" } }, - "node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", "license": "MIT" }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", "license": "MIT", + "optional": true, "dependencies": { - "whatwg-url": "^5.0.0" + "detect-libc": "^2.0.1" }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" } }, "node_modules/node-int64": { @@ -4977,6 +4959,15 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.9.tgz", + "integrity": "sha512-9/Qm0qXIByEP8lEV2qOqcAW7bRpL8CR9jcTwk3NBnHJNmP9fIJ86g2fgmIXqHY+nj55ZEMwWqYAT2QTDpRUYiQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", @@ -5029,21 +5020,6 @@ "node": ">=4" } }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "license": "ISC", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -5067,28 +5043,6 @@ "node": ">=8" } }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -5697,20 +5651,6 @@ "dev": true, "license": "MIT" }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -5724,6 +5664,27 @@ "node": ">=8.10.0" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5804,22 +5765,6 @@ "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", "license": "MIT" }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5992,12 +5937,6 @@ "node": ">= 0.8.0" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -6103,6 +6042,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, "license": "ISC" }, "node_modules/simple-update-notifier": { @@ -6195,6 +6135,12 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -6204,15 +6150,6 @@ "node": ">= 0.8" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -6231,6 +6168,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -6245,6 +6183,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -6437,29 +6376,6 @@ "express": ">=4.0.0 || >=5.0.0-beta" } }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -6520,11 +6436,11 @@ "nodetouch": "bin/nodetouch.js" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", @@ -6644,12 +6560,6 @@ "punycode": "^2.1.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -6711,22 +6621,6 @@ "makeerror": "1.0.12" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6743,15 +6637,6 @@ "node": ">= 8" } }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "license": "ISC", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, "node_modules/wkx": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", diff --git a/package.json b/package.json index dda4941..85da92a 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,16 @@ "format": "prettier --write ." }, "dependencies": { - "bcrypt": "^5.1.1", + "bcryptjs": "^3.0.2", + "bullmq": "^5.61.0", "dotenv": "^16.4.5", "express": "^4.21.1", "express-rate-limit": "^8.1.0", "express-validator": "^7.2.1", + "ioredis": "^5.8.2", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.1", + "nodemailer": "^7.0.9", "pg": "^8.13.1", "pg-hstore": "^2.3.4", "sequelize": "^6.37.3", diff --git a/src/config/db.js b/src/config/db.js index f3dd8c1..cc181f7 100644 --- a/src/config/db.js +++ b/src/config/db.js @@ -3,11 +3,13 @@ require('dotenv').config(); const sequelize = new Sequelize( process.env.DB_NAME, process.env.DB_USER, - process.env.DB_PASS, + process.env.DB_PASSWORD, { - host: process.env.DB_HOST, + host: process.env.DB_HOST || 'postgres', + port: process.env.DB_PORT || 5432, dialect: 'postgres', logging: false, } ); + module.exports = sequelize; \ No newline at end of file diff --git a/src/controllers/authController.js b/src/controllers/authController.js index 57aa9af..122f78c 100644 --- a/src/controllers/authController.js +++ b/src/controllers/authController.js @@ -5,8 +5,8 @@ exports.register = async (req, res, next) => { try { const errors = validationResult(req); if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); - const { username, password, role } = req.body; - const user = await authService.register(username, password, role); + const { username, password, email, role } = req.body; + const user = await authService.register(username, password, email, role); res.status(201).json({ message: 'User created', user }); } catch (err) { next(err); } }; diff --git a/src/controllers/taskController.js b/src/controllers/taskController.js index c785f0f..b66d8a1 100644 --- a/src/controllers/taskController.js +++ b/src/controllers/taskController.js @@ -1,5 +1,6 @@ const { validationResult } = require('express-validator'); -const { Task } = require('../models'); +const { Task, User } = require('../models'); +const emailQueue = require('../queues/emailQueue') exports.getTasks = async (req, res, next) => { try { @@ -46,6 +47,19 @@ exports.createTask = async (req, res, next) => { if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); const { title, description, status } = req.body; const task = await Task.create({ title, description,status, userId: req.user.id }); + + // Fetch user's email + const user = await User.findByPk(req.user.id); + + if (user && user.email) { + // Add a job to email queue + await emailQueue.add('taskCreated', { + to: user.email, + subject: 'New Task Created', + text: `Your task "${task.title}" has been created.`, + }); + } + res.status(201).json(task); } catch (err) { next(err); } }; @@ -56,6 +70,19 @@ exports.updateTask = async (req, res, next) => { if (!task) return res.status(404).json({ message: 'Task not found' }); if (task.userId !== req.user.id) return res.status(403).json({ message: 'Forbidden' }); await task.update(req.body); + + // Fetch user's email + const user = await User.findByPk(req.user.id); + + if (user && user.email) { + // Add a job to email queue + await emailQueue.add('taskUpdated', { + to: user.email, + subject: 'Task Updated', + text: `Your task "${task.title}" has been updated.`, + }); + } + res.json(task); } catch (err) { next(err); } }; diff --git a/src/models/index.js b/src/models/index.js index 47a453a..47429d7 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -1,4 +1,8 @@ const sequelize = require('../config/db'); const User = require('./user'); const Task = require('./task'); -module.exports = { sequelize, User, Task }; \ No newline at end of file +module.exports = { + sequelize, + User, + Task, +}; diff --git a/src/models/user.js b/src/models/user.js index af06b0c..57f9737 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -1,9 +1,10 @@ const { DataTypes } = require('sequelize'); const sequelize = require('../config/db'); -const bcrypt = require('bcrypt'); +const bcrypt = require('bcryptjs'); const User = sequelize.define('User', { username: { type: DataTypes.STRING, allowNull: false, unique: true }, + email: { type: DataTypes.STRING, allowNull: false, unique: true }, password: { type: DataTypes.STRING, allowNull: false }, role: { type: DataTypes.ENUM('user', 'admin'), diff --git a/src/queues/emailQueue.js b/src/queues/emailQueue.js new file mode 100644 index 0000000..99b885f --- /dev/null +++ b/src/queues/emailQueue.js @@ -0,0 +1,11 @@ +const { Queue } = require('bullmq'); +const Redis = require('ioredis'); + +const connection = new Redis({ + host: process.env.REDIS_HOST, + port: process.env.REDIS_PORT, +}); + +const emailQueue = new Queue('emailQueue', { connection }); + +module.exports = emailQueue; diff --git a/src/server.js b/src/server.js index ad3d796..084f34e 100644 --- a/src/server.js +++ b/src/server.js @@ -6,7 +6,7 @@ const PORT = process.env.PORT || 4000; (async () => { try { - await sequelize.sync(); + await sequelize.sync({ alter: true }); app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`)); } catch (err) { console.error('DB Connection failed:', err); diff --git a/src/services/authService.js b/src/services/authService.js index da803f2..58858f8 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -1,15 +1,30 @@ -const jwt = require('jsonwebtoken'); -const bcrypt = require('bcrypt'); const { User } = require('../models'); +const jwt = require('jsonwebtoken'); +const bcrypt = require('bcryptjs'); +const emailQueue = require('../queues/emailQueue') -exports.register = async (username, password, role = 'user') => { +exports.register = async (username, password, email, role = 'user') => { const existing = await User.findOne({ where: { username } }); if (existing) throw new Error('Username already exists'); + const existingEmail = await User.findOne({ where: { email } }); + if (existingEmail) throw new Error('Email already exists'); + if (!['user', 'admin'].includes(role)) { throw new Error('Invalid role'); } - return await User.create({ username, password, role }); + const user = await User.create({ username, password, email, role }); + + // Add a job to email queue + if (user.email) { + await emailQueue.add('userRegistered', { + to: user.email, + subject: 'Welcome to Task Manager!', + text: `Hi ${user.username},\n\nThank you for registering. Your account has been successfully created.`, + }); + } + + return user; }; exports.login = async (username, password) => { diff --git a/src/workers/emailWorker.js b/src/workers/emailWorker.js new file mode 100644 index 0000000..9c05cd4 --- /dev/null +++ b/src/workers/emailWorker.js @@ -0,0 +1,38 @@ +const { Worker } = require('bullmq'); +const Redis = require('ioredis'); +const nodemailer = require('nodemailer'); +require('dotenv').config(); + +const connection = new Redis({ + host: process.env.REDIS_HOST, + port: process.env.REDIS_PORT, + maxRetriesPerRequest: null, +}); + +const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, +}); + +new Worker( + 'emailQueue', + async (job) => { + const { to, subject, text } = job.data; + try { + const info = await transporter.sendMail({ + from: process.env.EMAIL_USER, + to, + subject, + text, + }); + console.log(`Email sent to ${to} (job ${job.id})`); + console.log('Message ID:', info.messageId); + } catch (err) { + console.error('Failed to send email:', err); + } + }, + { connection } +); From cb2de568995c0f1e1d3767f70dc6874fd6c9e373 Mon Sep 17 00:00:00 2001 From: Gunjan Verma Date: Thu, 23 Oct 2025 09:21:27 +0530 Subject: [PATCH 7/8] added test cases for tasks and auth using jest --- README.md | 15 ++++++++++++++ tests/auth.test.js | 38 +++++++++++++++++++++++++++++++++++ tests/task.test.js | 50 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 tests/auth.test.js diff --git a/README.md b/README.md index 58dc55c..446be07 100644 --- a/README.md +++ b/README.md @@ -193,3 +193,18 @@ Here are some suggestions for the next set of tasks for candidates to further en - **Continuous Integration (CI)**: - Set up a CI pipeline using GitHub Actions to automatically run tests and lint checks on every push and pull request. ``` +### Docker Setup + +- The project uses Docker Compose with 4 services: +app – Node.js server (src/server.js) +worker – Email worker (src/workers/emailWorker.js) +postgres – PostgreSQL database with persistent volume +redis – Redis server for BullMQ queues + +- install docker from [docker](https://docs.docker.com/) +- add env variable in .env + REDIS_HOST=redis + REDIS_PORT=6379 + - for the node mailer add + EMAIL_USER + EMAIL_PASS diff --git a/tests/auth.test.js b/tests/auth.test.js new file mode 100644 index 0000000..c6dd2a0 --- /dev/null +++ b/tests/auth.test.js @@ -0,0 +1,38 @@ +jest.mock('../src/queues/emailQueue', () => ({ + add: jest.fn().mockResolvedValue(true), + })); + + jest.mock('../src/services/authService'); + + const request = require('supertest'); + const app = require('../src/app'); + const authService = require('../src/services/authService'); + + describe('Auth API', () => { + it('POST /api/auth/register should create a user', async () => { + authService.register.mockResolvedValue({ id: 1, username: 'test' }); + + const res = await request(app) + .post('/api/auth/register') + .send({ username: 'test', password: '123456', email: 'test@mail.com' }); + + expect(res.statusCode).toBe(201); + expect(res.body.message).toBe('User created'); + }); + + it('POST /api/auth/login should return token', async () => { + authService.login.mockResolvedValue('mocked-jwt'); + + const res = await request(app) + .post('/api/auth/login') + .send({ username: 'test', password: '123456' }); + + expect(res.statusCode).toBe(200); + expect(res.body.token).toBe('mocked-jwt'); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + }); + \ No newline at end of file diff --git a/tests/task.test.js b/tests/task.test.js index 8db0828..17c0b24 100644 --- a/tests/task.test.js +++ b/tests/task.test.js @@ -1,9 +1,57 @@ const request = require('supertest'); const app = require('../src/app'); +const jwt = require('jsonwebtoken'); +const { Task, User } = require('../src/models'); + +jest.mock('../src/models'); +jest.mock('../src/queues/emailQueue', () => ({ + add: jest.fn().mockResolvedValue(true), +})); describe('Task API', () => { + let token; + + beforeAll(() => { + token = jwt.sign({ id: 1, role: 'user' }, process.env.JWT_SECRET || 'testsecret'); + }); + it('GET /api/tasks should return 401 if no token provided', async () => { const res = await request(app).get('/api/tasks'); expect(res.statusCode).toBe(401); }); -}); \ No newline at end of file + + it('GET /api/tasks should return tasks for authenticated user', async () => { + Task.findAndCountAll = jest.fn().mockResolvedValue({ + rows: [{ id: 1, title: 'Test Task' }], + count: 1, + }); + + const res = await request(app) + .get('/api/tasks') + .set('Authorization', `Bearer ${token}`); + + expect(res.statusCode).toBe(200); + expect(res.body.tasks.length).toBe(1); + }); + + it('POST /api/tasks should create a task', async () => { + Task.create = jest.fn().mockResolvedValue({ + id: 1, + title: 'New Task', + userId: 1, + }); + + User.findByPk = jest.fn().mockResolvedValue({ email: 'test@example.com' }); + + const res = await request(app) + .post('/api/tasks') + .set('Authorization', `Bearer ${token}`) + .send({ title: 'New Task', description: 'Test' }); + + expect(res.statusCode).toBe(201); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); +}); From 7c7ed5955a45af0a6f3cfeadbb47066d2b61494b Mon Sep 17 00:00:00 2001 From: Gunjan Verma Date: Thu, 23 Oct 2025 09:43:19 +0530 Subject: [PATCH 8/8] add configuration in readme for docker --- README.md | 47 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 446be07..3a6efda 100644 --- a/README.md +++ b/README.md @@ -193,18 +193,35 @@ Here are some suggestions for the next set of tasks for candidates to further en - **Continuous Integration (CI)**: - Set up a CI pipeline using GitHub Actions to automatically run tests and lint checks on every push and pull request. ``` -### Docker Setup - -- The project uses Docker Compose with 4 services: -app – Node.js server (src/server.js) -worker – Email worker (src/workers/emailWorker.js) -postgres – PostgreSQL database with persistent volume -redis – Redis server for BullMQ queues - -- install docker from [docker](https://docs.docker.com/) -- add env variable in .env - REDIS_HOST=redis - REDIS_PORT=6379 - - for the node mailer add - EMAIL_USER - EMAIL_PASS +## Running the Project with Docker + +### Prerequisites +Before running the application in Docker, ensure the following are installed: + +| Requirement | Required? | Instructions | +|------------|----------|--------------| +| **Docker Desktop (Windows/macOS)** https://www.docker.com/products/docker-desktop/ | +| **Docker Compose** Comes bundled with Docker Desktop / Docker Engine | + +> **Windows/macOS users must start Docker Desktop before running any docker-compose commands.** + +--- + +### Start Docker (Windows/macOS users) +1. Open **Docker Desktop** from the Start Menu. +2. Wait until it shows: **"Docker is running"**. +3. build and run containers + docker-compose up --build + +### This project uses Nodemailer to send email notifications (e.g., for task creation and updation, user registration confirmation) + +# Nodemailer Email Configuration (add in .env) +EMAIL_PASS=your-email-password +EMAIL_USER= + +## This project uses Redis for job queues (via BullMQ). When running with Docker, Redis is included as a service. +# The default host and port in Docker Compose are: +REDIS_HOST=redis +REDIS_PORT=6379 + +