From a72b18115c7109520ad377e49c48e5fdeb59a502 Mon Sep 17 00:00:00 2001 From: Pritam Nandi Date: Wed, 3 Jun 2026 12:06:47 +0000 Subject: [PATCH 1/3] refactor: extract shared utilities from duplicated code patterns Backend: - Add utils/response.js (sendSuccess, sendError) to centralize API responses - Add utils/catchAsync.js to eliminate duplicated try-catch error handling - Add middleware/validate.js (validateFields) to DRY input validation - Refactor auth.controller.js and auth.routes.js to use shared utilities - Refactor app.js health check to use response utility Frontend: - Add components/IconLink.jsx for reusable icon+link pattern - Refactor App.jsx to use IconLink component, removing 6 repetitive blocks - Extract social links data into a declarative array Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- backend/src/app.js | 8 +- backend/src/controllers/auth.controller.js | 161 ++++++++----------- backend/src/controllers/ticket.controller.js | 119 +++++--------- backend/src/middleware/auth.js | 11 +- backend/src/middleware/validate.js | 11 ++ backend/src/routes/auth.routes.js | 12 +- backend/src/routes/ticket.routes.js | 3 +- backend/src/utils/catchAsync.js | 10 ++ backend/src/utils/response.js | 16 ++ frontend/src/App.jsx | 73 ++------- frontend/src/components/IconLink.jsx | 19 +++ 11 files changed, 184 insertions(+), 259 deletions(-) create mode 100644 backend/src/middleware/validate.js create mode 100644 backend/src/utils/catchAsync.js create mode 100644 backend/src/utils/response.js create mode 100644 frontend/src/components/IconLink.jsx diff --git a/backend/src/app.js b/backend/src/app.js index c330bc3..9f09216 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -1,6 +1,7 @@ const express = require('express'); const cors = require('cors'); const morgan = require('morgan'); +const { sendSuccess } = require('./utils/response'); const app = express(); const authRoutes = require('./routes/auth.routes'); @@ -17,10 +18,7 @@ app.use('/api/tickets', ticketRoutes); // Health Check app.get('/api/health', (req, res) => { - res.json({ - success: true, - message: 'SmartQueue API is running' - }); + sendSuccess(res, 200, 'SmartQueue API is running'); }); -module.exports = app; \ No newline at end of file +module.exports = app; diff --git a/backend/src/controllers/auth.controller.js b/backend/src/controllers/auth.controller.js index 62e0f83..da61baf 100644 --- a/backend/src/controllers/auth.controller.js +++ b/backend/src/controllers/auth.controller.js @@ -1,122 +1,87 @@ const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const { User } = require('../models'); +const { sendSuccess, sendError } = require('../utils/response'); +const catchAsync = require('../utils/catchAsync'); // ========================================== // REGISTER CONTROLLER // ========================================== -const register = async (req, res) => { - try { - const { name, email, password } = req.body; +const register = catchAsync(async (req, res) => { + const { name, email, password } = req.body; -if (!name || !email || !password) { - return res.status(400).json({ - success: false, - message: 'All fields are required', - }); -} + if (!name || !email || !password) { + return sendError(res, 400, 'All fields are required'); + } - // 1. Check if user already exists - const existingUser = await User.findOne({ - where: { email }, - }); + // 1. Check if user already exists + const existingUser = await User.findOne({ + where: { email }, + }); - if (existingUser) { - return res.status(400).json({ - success: false, - message: 'Email already registered', - }); - } + if (existingUser) { + return sendError(res, 400, 'Email already registered'); + } - // 2. Hash password - const hashedPassword = await bcrypt.hash(password, 10); + // 2. Hash password + const hashedPassword = await bcrypt.hash(password, 10); - // 3. Create user - const user = await User.create({ - name, - email, - password: hashedPassword, - }); + // 3. Create user + const user = await User.create({ + name, + email, + password: hashedPassword, + }); - // 4. Return response - return res.status(201).json({ - success: true, - message: 'User registered successfully', - user: { - id: user.id, - name: user.name, - email: user.email, - }, - }); - } catch (error) { - console.error('Register error:', error); - return res.status(500).json({ - success: false, - message: 'Server error', - }); - } -}; + // 4. Return response + return sendSuccess(res, 201, 'User registered successfully', { + user: { + id: user.id, + name: user.name, + email: user.email, + }, + }); +}); // ========================================== // LOGIN CONTROLLER // ========================================== -const login = async (req, res) => { - try { - const { email, password } = req.body; +const login = catchAsync(async (req, res) => { + const { email, password } = req.body; - // 1. Validate request body - if (!email || !password) { - return res.status(400).json({ - success: false, - message: 'Email and password are required.' - }); - } - - // 2. Find user by email - const user = await User.findOne({ where: { email } }); - if (!user) { - return res.status(401).json({ - success: false, - message: 'Invalid email or password.' - }); - } + if (!email || !password) { + return sendError(res, 400, 'Email and password are required.'); + } - // 3. Compare password with bcrypt - const isPasswordValid = await bcrypt.compare(password, user.password); - if (!isPasswordValid) { - return res.status(401).json({ - success: false, - message: 'Invalid email or password.' - }); - } + // 1. Find user by email + const user = await User.findOne({ where: { email } }); + if (!user) { + return sendError(res, 401, 'Invalid email or password.'); + } - // 4. Generate JWT token - const token = jwt.sign( - { id: user.id, email: user.email }, - process.env.JWT_SECRET, - { expiresIn: process.env.JWT_EXPIRES_IN || '24h' } - ); + // 2. Compare password with bcrypt + const isPasswordValid = await bcrypt.compare(password, user.password); + if (!isPasswordValid) { + return sendError(res, 401, 'Invalid email or password.'); + } - // 5. Return token and user info - return res.status(200).json({ - success: true, - message: 'Login successful.', - token, - user: { - id: user.id, - name: user.name, - email: user.email, - }, - }); + // 3. Generate JWT token + const token = jwt.sign( + { id: user.id, email: user.email }, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRES_IN || '24h' } + ); - } catch (error) { - console.error('Login error:', error); - return res.status(500).json({ - success: false, - message: 'An error occurred during login.' - }); - } -}; + // 4. Return token and user info + return sendSuccess(res, 200, 'Login successful.', { + token, + user: { + id: user.id, + name: user.name, + email: user.email, + }, + }); +}, 'An error occurred during login.'); // ========================================== // EXPORTS @@ -124,4 +89,4 @@ const login = async (req, res) => { module.exports = { register, login, -}; \ No newline at end of file +}; diff --git a/backend/src/controllers/ticket.controller.js b/backend/src/controllers/ticket.controller.js index b97211a..6d488ff 100644 --- a/backend/src/controllers/ticket.controller.js +++ b/backend/src/controllers/ticket.controller.js @@ -1,105 +1,64 @@ const { Ticket, Department } = require('../models'); const { v4: uuidv4 } = require('uuid'); +const { sendSuccess, sendError } = require('../utils/response'); +const catchAsync = require('../utils/catchAsync'); // ========================================== // GENERATE TICKET // ========================================== -const generateTicket = async (req, res) => { - try { - const { departmentId } = req.body; +const generateTicket = catchAsync(async (req, res) => { + const { departmentId } = req.body; - if (!departmentId) { - return res.status(400).json({ - success: false, - message: 'departmentId is required', - }); - } + if (!departmentId) { + return sendError(res, 400, 'departmentId is required'); + } - // Verify department exists - const department = await Department.findByPk(departmentId); - if (!department) { - return res.status(404).json({ - success: false, - message: 'Department not found', - }); - } + // Verify department exists + const department = await Department.findByPk(departmentId); + if (!department) { + return sendError(res, 404, 'Department not found'); + } - // Generate unique ticket number - const ticketNumber = `TKT-${Date.now()}-${uuidv4().replace(/-/g, '').toUpperCase()}`; + // Generate unique ticket number + const ticketNumber = `TKT-${Date.now()}-${uuidv4().replace(/-/g, '').toUpperCase()}`; - const ticket = await Ticket.create({ - ticketNumber, - status: 'waiting', - userId: req.user.id, - departmentId, - }); + const ticket = await Ticket.create({ + ticketNumber, + status: 'waiting', + userId: req.user.id, + departmentId, + }); - return res.status(201).json({ - success: true, - message: 'Ticket generated successfully', - ticket, - }); - } catch (error) { - console.error('Generate ticket error:', error); - return res.status(500).json({ - success: false, - message: 'Server error', - }); - } -}; + return sendSuccess(res, 201, 'Ticket generated successfully', { ticket }); +}); // ========================================== // GET MY TICKETS // ========================================== -const getMyTickets = async (req, res) => { - try { - const tickets = await Ticket.findAll({ - where: { userId: req.user.id }, - order: [['createdAt', 'DESC']], - }); +const getMyTickets = catchAsync(async (req, res) => { + const tickets = await Ticket.findAll({ + where: { userId: req.user.id }, + order: [['createdAt', 'DESC']], + }); - return res.status(200).json({ - success: true, - tickets, - }); - } catch (error) { - console.error('Get my tickets error:', error); - return res.status(500).json({ - success: false, - message: 'Server error', - }); - } -}; + return sendSuccess(res, 200, undefined, { tickets }); +}); // ========================================== // GET TICKET BY ID // ========================================== -const getTicketById = async (req, res) => { - try { - const { id } = req.params; - - const ticket = await Ticket.findOne({ - where: { id, userId: req.user.id }, - }); - if (!ticket) { - return res.status(404).json({ - success: false, - message: 'Ticket not found', - }); - } +const getTicketById = catchAsync(async (req, res) => { + const { id } = req.params; - return res.status(200).json({ - success: true, - ticket, - }); - } catch (error) { - console.error('Get ticket by ID error:', error); - return res.status(500).json({ - success: false, - message: 'Server error', - }); + const ticket = await Ticket.findOne({ + where: { id, userId: req.user.id }, + }); + if (!ticket) { + return sendError(res, 404, 'Ticket not found'); } -}; + + return sendSuccess(res, 200, undefined, { ticket }); +}); // ========================================== // EXPORTS diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js index 2c4d902..abeec48 100644 --- a/backend/src/middleware/auth.js +++ b/backend/src/middleware/auth.js @@ -1,13 +1,11 @@ const jwt = require('jsonwebtoken'); +const { sendError } = require('../utils/response'); const authenticate = (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { - return res.status(401).json({ - success: false, - message: 'Access denied. No token provided.', - }); + return sendError(res, 401, 'Access denied. No token provided.'); } const token = authHeader.split(' ')[1]; @@ -17,10 +15,7 @@ const authenticate = (req, res, next) => { req.user = decoded; next(); } catch (error) { - return res.status(401).json({ - success: false, - message: 'Invalid or expired token.', - }); + return sendError(res, 401, 'Invalid or expired token.'); } }; diff --git a/backend/src/middleware/validate.js b/backend/src/middleware/validate.js new file mode 100644 index 0000000..9d5b677 --- /dev/null +++ b/backend/src/middleware/validate.js @@ -0,0 +1,11 @@ +const { sendError } = require('../utils/response'); + +const validateFields = (requiredFields) => (req, res, next) => { + const missing = requiredFields.filter((field) => !req.body[field]); + if (missing.length > 0) { + return sendError(res, 400, `Missing required fields: ${missing.join(', ')}`); + } + next(); +}; + +module.exports = { validateFields }; diff --git a/backend/src/routes/auth.routes.js b/backend/src/routes/auth.routes.js index 9bd7fd2..4b94dbc 100644 --- a/backend/src/routes/auth.routes.js +++ b/backend/src/routes/auth.routes.js @@ -1,14 +1,12 @@ const express = require('express'); const router = express.Router(); - -// 1. Add 'login' to the destructured import const { register, login } = require('../controllers/auth.controller'); +const { validateFields } = require('../middleware/validate'); // POST /api/auth/register -router.post('/register', register); +router.post('/register', validateFields(['name', 'email', 'password']), register); -// 2. Add the login route definition -// This maps to: POST /api/auth/login -router.post('/login', login); +// POST /api/auth/login +router.post('/login', validateFields(['email', 'password']), login); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/backend/src/routes/ticket.routes.js b/backend/src/routes/ticket.routes.js index 394da64..fec251b 100644 --- a/backend/src/routes/ticket.routes.js +++ b/backend/src/routes/ticket.routes.js @@ -1,10 +1,11 @@ const express = require('express'); const router = express.Router(); const authenticate = require('../middleware/auth'); +const { validateFields } = require('../middleware/validate'); const { generateTicket, getMyTickets, getTicketById } = require('../controllers/ticket.controller'); // POST /api/tickets - Generate a new ticket (authenticated) -router.post('/', authenticate, generateTicket); +router.post('/', authenticate, validateFields(['departmentId']), generateTicket); // GET /api/tickets/my - Get all tickets for logged-in user (authenticated) router.get('/my', authenticate, getMyTickets); diff --git a/backend/src/utils/catchAsync.js b/backend/src/utils/catchAsync.js new file mode 100644 index 0000000..ae65384 --- /dev/null +++ b/backend/src/utils/catchAsync.js @@ -0,0 +1,10 @@ +const { sendError } = require('./response'); + +const catchAsync = (fn, errorMessage = 'Server error') => (req, res, next) => { + return Promise.resolve(fn(req, res, next)).catch((error) => { + console.error(`${fn.name || 'Handler'} error:`, error); + sendError(res, 500, errorMessage); + }); +}; + +module.exports = catchAsync; diff --git a/backend/src/utils/response.js b/backend/src/utils/response.js new file mode 100644 index 0000000..96f70c5 --- /dev/null +++ b/backend/src/utils/response.js @@ -0,0 +1,16 @@ +const sendSuccess = (res, statusCode, message, data = {}) => { + return res.status(statusCode).json({ + success: true, + message, + ...data, + }); +}; + +const sendError = (res, statusCode, message) => { + return res.status(statusCode).json({ + success: false, + message, + }); +}; + +module.exports = { sendSuccess, sendError }; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 4f03aa1..8ffb0f0 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,8 +2,16 @@ import { useState } from 'react' import reactLogo from './assets/react.svg' import viteLogo from './assets/vite.svg' import heroImg from './assets/hero.png' +import IconLink from './components/IconLink' import './App.css' +const socialLinks = [ + { href: 'https://github.com/vitejs/vite', icon: '/icons.svg#github-icon', label: 'GitHub' }, + { href: 'https://chat.vite.dev/', icon: '/icons.svg#discord-icon', label: 'Discord' }, + { href: 'https://x.com/vite_js', icon: '/icons.svg#x-icon', label: 'X.com' }, + { href: 'https://bsky.app/profile/vite.dev', icon: '/icons.svg#bluesky-icon', label: 'Bluesky' }, +] + function App() { const [count, setCount] = useState(0) @@ -40,18 +48,8 @@ function App() {

Documentation

Your questions, answered

@@ -61,54 +59,9 @@ function App() {

Connect with us

Join the Vite community

diff --git a/frontend/src/components/IconLink.jsx b/frontend/src/components/IconLink.jsx new file mode 100644 index 0000000..7d7c613 --- /dev/null +++ b/frontend/src/components/IconLink.jsx @@ -0,0 +1,19 @@ +function IconLink({ href, icon, label }) { + const isImage = typeof icon === 'string' && !icon.startsWith('/icons.svg'); + return ( +
  • + + {isImage ? ( + + ) : ( + + )} + {label} + +
  • + ); +} + +export default IconLink; From f7d4d1cb6ebd601009fec7513d70c64261748524 Mon Sep 17 00:00:00 2001 From: Pritam Nandi Date: Thu, 4 Jun 2026 17:44:26 +0000 Subject: [PATCH 2/3] fix: address CodeRabbit review feedback - Guard against undefined req.body in validateFields middleware - Add rel='noopener noreferrer' to IconLink for security - Use link.href as stable React key instead of link.label Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- backend/src/middleware/validate.js | 3 ++- frontend/src/App.jsx | 2 +- frontend/src/components/IconLink.jsx | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/src/middleware/validate.js b/backend/src/middleware/validate.js index 9d5b677..3247f99 100644 --- a/backend/src/middleware/validate.js +++ b/backend/src/middleware/validate.js @@ -1,7 +1,8 @@ const { sendError } = require('../utils/response'); const validateFields = (requiredFields) => (req, res, next) => { - const missing = requiredFields.filter((field) => !req.body[field]); + const body = req.body || {}; + const missing = requiredFields.filter((field) => !body[field]); if (missing.length > 0) { return sendError(res, 400, `Missing required fields: ${missing.join(', ')}`); } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8ffb0f0..c3a98c3 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -60,7 +60,7 @@ function App() {

    Join the Vite community

    diff --git a/frontend/src/components/IconLink.jsx b/frontend/src/components/IconLink.jsx index 7d7c613..8f23cfe 100644 --- a/frontend/src/components/IconLink.jsx +++ b/frontend/src/components/IconLink.jsx @@ -2,7 +2,7 @@ function IconLink({ href, icon, label }) { const isImage = typeof icon === 'string' && !icon.startsWith('/icons.svg'); return (
  • - + {isImage ? ( ) : ( From 8838b3bf4cfcc47f340cadbe9e62ee0f9b9d05c1 Mon Sep 17 00:00:00 2001 From: Pritam Nandi Date: Thu, 4 Jun 2026 17:49:43 +0000 Subject: [PATCH 3/3] fix: add uuid as direct dependency in backend/package.json Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- backend/package-lock.json | 26 ++++++++++++++++++++------ backend/package.json | 3 ++- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 024dfb5..1791895 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,7 +16,8 @@ "jsonwebtoken": "^9.0.3", "morgan": "^1.11.0", "mysql2": "^3.22.4", - "sequelize": "^6.37.8" + "sequelize": "^6.37.8", + "uuid": "^11.1.1" }, "devDependencies": { "jest": "^30.4.2", @@ -5725,6 +5726,16 @@ "node": ">= 10.0.0" } }, + "node_modules/sequelize/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/serve-static": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", @@ -6704,13 +6715,16 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/v8-to-istanbul": { diff --git a/backend/package.json b/backend/package.json index b030568..602cca8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,7 +19,8 @@ "jsonwebtoken": "^9.0.3", "morgan": "^1.11.0", "mysql2": "^3.22.4", - "sequelize": "^6.37.8" + "sequelize": "^6.37.8", + "uuid": "^11.1.1" }, "devDependencies": { "jest": "^30.4.2",