From b16cc7122c181cdb601fce4109c849cdcd382d31 Mon Sep 17 00:00:00 2001 From: Kavinda L Jayarathna Date: Sun, 29 Mar 2026 14:11:57 +0530 Subject: [PATCH 1/2] feat(auth): implement centralized JWT authentication and role-based middleware --- server.js | 79 ++-- src/controllers/issueController.js | 236 +++++++--- src/middleware/auth.js | 45 ++ src/routes/auth.js | 714 ++++++++++++++--------------- src/routes/branch.js | 20 +- src/routes/issues.js | 32 +- src/services/issueService.js | 435 +++++++++++------- 7 files changed, 902 insertions(+), 659 deletions(-) create mode 100644 src/middleware/auth.js diff --git a/server.js b/server.js index 1588c22..f9e571a 100644 --- a/server.js +++ b/server.js @@ -1,17 +1,21 @@ -import 'dotenv/config'; -import express from 'express'; +import "dotenv/config"; +import express from "express"; import http from "http"; -import cors from 'cors'; -import { initializeDatabase, initializeStorage } from './src/services/connectionService.js'; -import healthRoutes from './src/routes/health.js'; -import { setupSocket } from './src/socket/socket.js'; -import issueRoutes from './src/routes/issues.js'; -import userRoutes from './src/routes/users.js'; -import messageRoutes from './src/routes/messages.js'; -import branchRoutes from './src/routes/branch.js'; -import thirdPartiesRoutes from './src/routes/thirdparties.js'; -import cashRequestRoutes from './src/routes/cashRequestRoutes.js'; -import authRoutes from './src/routes/auth.js'; +import cors from "cors"; +import { + initializeDatabase, + initializeStorage, +} from "./src/services/connectionService.js"; +import healthRoutes from "./src/routes/health.js"; +import { setupSocket } from "./src/socket/socket.js"; +import issueRoutes from "./src/routes/issues.js"; +import userRoutes from "./src/routes/users.js"; +import messageRoutes from "./src/routes/messages.js"; +import branchRoutes from "./src/routes/branch.js"; +import thirdPartiesRoutes from "./src/routes/thirdparties.js"; +import cashRequestRoutes from "./src/routes/cashRequestRoutes.js"; +import authRoutes from "./src/routes/auth.js"; +import { authenticateToken } from "./src/middleware/auth.js"; const app = express(); const server = http.createServer(app); @@ -21,24 +25,28 @@ app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Enable CORS -app.use(cors({ origin: '*' })); +app.use(cors({ origin: "*" })); // Routes -app.use('/api/health', healthRoutes); -app.use('/api/v1/auth', authRoutes); -app.use('/api/v1/cash-requests', cashRequestRoutes); -app.use('/api/v1/issues', issueRoutes); -app.use('/api/v1/users', userRoutes); -app.use('/api/v1/messages', messageRoutes); -app.use('/api/v1/branches', branchRoutes); -app.use('/api/v1/thirdparties', thirdPartiesRoutes); +app.use("/api/health", healthRoutes); +app.use("/api/v1/auth", authRoutes); + +// Protected routes - require authentication +app.use(authenticateToken); + +app.use("/api/v1/cash-requests", cashRequestRoutes); +app.use("/api/v1/issues", issueRoutes); +app.use("/api/v1/users", userRoutes); +// app.use("/api/v1/messages", messageRoutes); +app.use("/api/v1/branches", branchRoutes); +app.use("/api/v1/thirdparties", thirdPartiesRoutes); // Basic route -app.get('/api/', (req, res) => { +app.get("/api/", (req, res) => { res.json({ - message: 'FixPoint API Server is running!', - version: '1.0.0', - timestamp: new Date().toISOString() + message: "FixPoint API Server is running!", + version: "1.0.0", + timestamp: new Date().toISOString(), }); }); @@ -47,14 +55,14 @@ async function startServer() { const PORT = process.env.PORT || 5000; try { - console.log('Starting FixPoint Server...'); + console.log("Starting FixPoint Server..."); // Initialize database connection - console.log('Initializing database connection...'); + console.log("Initializing database connection..."); await initializeDatabase(); // Initialize MinIO storage - console.log('Initializing MinIO storage...'); + console.log("Initializing MinIO storage..."); await initializeStorage(); //Socket.io @@ -65,15 +73,18 @@ async function startServer() { console.log(`Server is running on port ${PORT}`); console.log(`Server URL: http://localhost:${PORT}`); console.log(`Health check: http://localhost:${PORT}/api/health`); - console.log(`Database health: http://localhost:${PORT}/api/health/database`); - console.log(`Storage health: http://localhost:${PORT}/api/health/storage`); + console.log( + `Database health: http://localhost:${PORT}/api/health/database`, + ); + console.log( + `Storage health: http://localhost:${PORT}/api/health/storage`, + ); }); - } catch (error) { - console.error('Failed to start server:', error.message); + console.error("Failed to start server:", error.message); process.exit(1); } } // Start the server -startServer(); \ No newline at end of file +startServer(); diff --git a/src/controllers/issueController.js b/src/controllers/issueController.js index a64a57e..23f6093 100644 --- a/src/controllers/issueController.js +++ b/src/controllers/issueController.js @@ -1,26 +1,32 @@ -import issueService from '../services/issueService.js'; -import { notifyNewIssue, notifyAssign, makeDynamicNamespace, issueRealtimeUpdate, removeDynamicNamespace } from '../socket/socket.js'; +import issueService from "../services/issueService.js"; +import { + notifyNewIssue, + notifyAssign, + makeDynamicNamespace, + issueRealtimeUpdate, + removeDynamicNamespace, +} from "../socket/socket.js"; class IssueController { // POST /api/issues - Create new issue async createIssue(req, res) { try { - const { - branch_id, - title, - manager_id, - description, - maintenance_executive_id, - technician_id, + const { + branch_id, + title, + manager_id, + description, + maintenance_executive_id, + technician_id, status, - third_party_id + third_party_id, } = req.body; // Validation if (!branch_id || !title || !manager_id) { return res.status(400).json({ success: false, - message: 'Branch ID, title, and manager ID are required' + message: "Branch ID, title, and manager ID are required", }); } @@ -28,7 +34,7 @@ class IssueController { branch_id: parseInt(branch_id), title, manager_id: parseInt(manager_id), - description + description, }; // Add optional fields if provided @@ -49,13 +55,17 @@ class IssueController { if (result.success) { // Notify all Maintenance Executives about the new issue(real-time) - try { notifyNewIssue(result); } catch (e) { console.error(e);} + try { + notifyNewIssue(result); + } catch (e) { + console.error(e); + } try { // Create dynamic namespaces for real-time communication makeDynamicNamespace(result.data.id); } catch (nsErr) { - console.error('makeDynamicNamespace failed:', nsErr); + console.error("makeDynamicNamespace failed:", nsErr); } return res.status(201).json(result); @@ -65,8 +75,8 @@ class IssueController { } catch (error) { return res.status(500).json({ success: false, - message: 'Internal server error', - error: error.message + message: "Internal server error", + error: error.message, }); } } @@ -74,30 +84,52 @@ class IssueController { // GET /api/issues - Get all issues async getAllIssues(req, res) { try { - const { - branch_id, - manager_id, - technician_id, + const { + branch_id, + manager_id, + technician_id, maintenance_executive_id, third_party_id, status, - search, - limit, + search, + limit, offset, - include_relations + include_relations, } = req.query; const filters = {}; if (branch_id) filters.branch_id = parseInt(branch_id); if (manager_id) filters.manager_id = parseInt(manager_id); if (technician_id) filters.technician_id = parseInt(technician_id); - if (maintenance_executive_id) filters.maintenance_executive_id = parseInt(maintenance_executive_id); + if (maintenance_executive_id) + filters.maintenance_executive_id = parseInt(maintenance_executive_id); if (third_party_id) filters.third_party_id = parseInt(third_party_id); if (status) filters.status = status; if (search) filters.search = search; if (limit) filters.limit = parseInt(limit); if (offset) filters.offset = parseInt(offset); - if (include_relations) filters.include_relations = include_relations === 'true'; + if (include_relations) + filters.include_relations = include_relations === "true"; + + // Strict Role-Based Access Control Filtering + const { role, roleSpecificId } = req.user; + + if (role === "maintenance_executive") { + // Maintenance Executives can view all issues by default. + // They can still use query filters if they want to narrow down results. + } else if (role === "technician") { + // Strictly restricted to issues assigned to them + filters.technician_id = roleSpecificId; + } else if (role === "branch_manager") { + // Strictly restricted to issues they created/manage + filters.manager_id = roleSpecificId; + } else { + // Safety: If an unknown role somehow accesses this, return empty results or error + return res.status(403).json({ + success: false, + message: "Access denied: Unauthorized role", + }); + } const result = await issueService.getAllIssues(filters); @@ -109,8 +141,8 @@ class IssueController { } catch (error) { return res.status(500).json({ success: false, - message: 'Internal server error', - error: error.message + message: "Internal server error", + error: error.message, }); } } @@ -124,22 +156,56 @@ class IssueController { if (!id || isNaN(id)) { return res.status(400).json({ success: false, - message: 'Valid issue ID is required' + message: "Valid issue ID is required", }); } - const result = await issueService.getIssueById(parseInt(id), include_relations === 'true'); + const result = await issueService.getIssueById( + parseInt(id), + include_relations === "true", + ); + + if (result.success && result.data) { + // Authorization check + const { role, roleSpecificId } = req.user; + const issue = result.data; + + // Strict Check + if (role === "maintenance_executive") { + // Allowed: See all + } else if (role === "technician") { + if (issue.technician_id !== roleSpecificId) { + return res.status(403).json({ + success: false, + message: "Access denied: You are not assigned to this issue", + }); + } + } else if (role === "branch_manager") { + if (issue.manager_id !== roleSpecificId) { + return res.status(403).json({ + success: false, + message: + "Access denied: This issue does not belong to your branch", + }); + } + } else { + return res.status(403).json({ + success: false, + message: "Access denied: Unauthorized role", + }); + } - if (result.success) { return res.status(200).json(result); - } else { + } else if (result.success && !result.data) { return res.status(404).json(result); + } else { + return res.status(500).json(result); } } catch (error) { return res.status(500).json({ success: false, - message: 'Internal server error', - error: error.message + message: "Internal server error", + error: error.message, }); } } @@ -153,12 +219,18 @@ class IssueController { if (!id || isNaN(id)) { return res.status(400).json({ success: false, - message: 'Valid issue ID is required' + message: "Valid issue ID is required", }); } // Convert string IDs to integers if provided - ['branch_id', 'manager_id', 'technician_id', 'maintenance_executive_id', 'third_party_id'].forEach(field => { + [ + "branch_id", + "manager_id", + "technician_id", + "maintenance_executive_id", + "third_party_id", + ].forEach((field) => { if (updateData[field]) { updateData[field] = parseInt(updateData[field]); } @@ -170,10 +242,9 @@ class IssueController { // Notify via realtime update that the issue has been updated issueRealtimeUpdate(result.data.id, result); } catch (err) { - console.error('issueRealtimeUpdate error after updating issue:', err); + console.error("issueRealtimeUpdate error after updating issue:", err); } - if (result.success) { return res.status(200).json(result); } else { @@ -182,8 +253,8 @@ class IssueController { } catch (error) { return res.status(500).json({ success: false, - message: 'Internal server error', - error: error.message + message: "Internal server error", + error: error.message, }); } } @@ -196,7 +267,7 @@ class IssueController { if (!id || isNaN(id)) { return res.status(400).json({ success: false, - message: 'Valid issue ID is required' + message: "Valid issue ID is required", }); } @@ -212,8 +283,8 @@ class IssueController { } catch (error) { return res.status(500).json({ success: false, - message: 'Internal server error', - error: error.message + message: "Internal server error", + error: error.message, }); } } @@ -227,32 +298,35 @@ class IssueController { if (!id || isNaN(id)) { return res.status(400).json({ success: false, - message: 'Valid issue ID is required' + message: "Valid issue ID is required", }); } if (!technician_id) { return res.status(400).json({ success: false, - message: 'Technician ID is required' + message: "Technician ID is required", }); } - const result = await issueService.assignTechnician(parseInt(id), parseInt(technician_id)); + const result = await issueService.assignTechnician( + parseInt(id), + parseInt(technician_id), + ); if (result.success) { // Notify via realtime update that the technician has been assigned try { issueRealtimeUpdate(parseInt(id), result); } catch (emitErr) { - console.error('issueRealtimeUpdate failed:', emitErr); + console.error("issueRealtimeUpdate failed:", emitErr); } // broadcast assignment to the assigned technician (real-time) try { notifyAssign(parseInt(technician_id), result); } catch (emitErr) { - console.error('notifyAssign failed:', emitErr); + console.error("notifyAssign failed:", emitErr); } return res.status(200).json(result); @@ -262,8 +336,8 @@ class IssueController { } catch (error) { return res.status(500).json({ success: false, - message: 'Internal server error', - error: error.message + message: "Internal server error", + error: error.message, }); } } @@ -277,29 +351,36 @@ class IssueController { if (!id || isNaN(id)) { return res.status(400).json({ success: false, - message: 'Valid issue ID is required' + message: "Valid issue ID is required", }); } if (!maintenance_executive_id) { return res.status(400).json({ success: false, - message: 'Maintenance Executive ID is required' + message: "Maintenance Executive ID is required", }); } - const result = await issueService.assignMaintenanceExecutive(parseInt(id), parseInt(maintenance_executive_id)); + const result = await issueService.assignMaintenanceExecutive( + parseInt(id), + parseInt(maintenance_executive_id), + ); if (result.success) { // Notify via realtime update that the maintenance executive has been assigned try { issueRealtimeUpdate(parseInt(id), result); } catch (emitErr) { - console.error('issueRealtimeUpdate failed:', emitErr); + console.error("issueRealtimeUpdate failed:", emitErr); } // Notify all Maintenance Executives about the new issue(real-time) update with assigned ME - try { notifyNewIssue(result); } catch (e) { console.error(e);} + try { + notifyNewIssue(result); + } catch (e) { + console.error(e); + } return res.status(200).json(result); } else { @@ -308,8 +389,8 @@ class IssueController { } catch (error) { return res.status(500).json({ success: false, - message: 'Internal server error', - error: error.message + message: "Internal server error", + error: error.message, }); } } @@ -323,25 +404,28 @@ class IssueController { if (!id || isNaN(id)) { return res.status(400).json({ success: false, - message: 'Valid issue ID is required' + message: "Valid issue ID is required", }); } if (!third_party_id) { return res.status(400).json({ success: false, - message: 'Third Party ID is required' + message: "Third Party ID is required", }); } - const result = await issueService.assignThirdParty(parseInt(id), parseInt(third_party_id)); + const result = await issueService.assignThirdParty( + parseInt(id), + parseInt(third_party_id), + ); if (result.success) { // Notify via realtime update that the third party has been assigned try { issueRealtimeUpdate(parseInt(id), result); } catch (emitErr) { - console.error('issueRealtimeUpdate failed:', emitErr); + console.error("issueRealtimeUpdate failed:", emitErr); } return res.status(200).json(result); @@ -351,8 +435,8 @@ class IssueController { } catch (error) { return res.status(500).json({ success: false, - message: 'Internal server error', - error: error.message + message: "Internal server error", + error: error.message, }); } } @@ -366,22 +450,22 @@ class IssueController { if (!id || isNaN(id)) { return res.status(400).json({ success: false, - message: 'Valid issue ID is required' + message: "Valid issue ID is required", }); } if (!status) { return res.status(400).json({ success: false, - message: 'Status is required' + message: "Status is required", }); } - const validStatuses = ['Open', 'In Progress', 'Done', 'Closed']; + const validStatuses = ["Open", "In Progress", "Done", "Closed"]; if (!validStatuses.includes(status)) { return res.status(400).json({ success: false, - message: `Status must be one of: ${validStatuses.join(', ')}` + message: `Status must be one of: ${validStatuses.join(", ")}`, }); } @@ -392,17 +476,25 @@ class IssueController { try { issueRealtimeUpdate(parseInt(id), result); } catch (emitErr) { - console.error('issueRealtimeUpdate failed:', emitErr); + console.error("issueRealtimeUpdate failed:", emitErr); } // Notify Maintenance Executives about the issue status update(real-time in home dashboard) - try { notifyNewIssue(result); } catch (e) { console.error(e);} + try { + notifyNewIssue(result); + } catch (e) { + console.error(e); + } // Notify assigned technician about the issue status update(real-time in home dashboard) - try { notifyAssign(result.data.technician_id, result); } catch (e) { console.error(e);} + try { + notifyAssign(result.data.technician_id, result); + } catch (e) { + console.error(e); + } // Remove all connections and delete the namespace for the issue - if (result.data.status === 'Closed' || result.data.status === 'Done') { + if (result.data.status === "Closed" || result.data.status === "Done") { removeDynamicNamespace(result.data.id); } @@ -413,8 +505,8 @@ class IssueController { } catch (error) { return res.status(500).json({ success: false, - message: 'Internal server error', - error: error.message + message: "Internal server error", + error: error.message, }); } } diff --git a/src/middleware/auth.js b/src/middleware/auth.js new file mode 100644 index 0000000..4fc6e6e --- /dev/null +++ b/src/middleware/auth.js @@ -0,0 +1,45 @@ +import jwt from 'jsonwebtoken'; + +const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; + +/** + * Middleware to authenticate JWT token + */ +export const authenticateToken = (req, res, next) => { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN + + if (!token) { + return res.status(401).json({ + success: false, + message: 'Access token is required' + }); + } + + jwt.verify(token, JWT_SECRET, (err, user) => { + if (err) { + return res.status(403).json({ + success: false, + message: 'Invalid or expired token' + }); + } + + req.user = user; + next(); + }); +}; + +/** + * Middleware to check user roles + */ +export const authorizeRoles = (...allowedRoles) => { + return (req, res, next) => { + if (!req.user || !allowedRoles.includes(req.user.role)) { + return res.status(403).json({ + success: false, + message: 'Access denied: You do not have the required permissions.' + }); + } + next(); + }; +}; diff --git a/src/routes/auth.js b/src/routes/auth.js index 9080dd6..5800f21 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -1,229 +1,247 @@ -import express from 'express'; -import jwt from 'jsonwebtoken'; -import bcrypt from 'bcryptjs'; -import userService from '../services/userService.js'; +import express from "express"; +import jwt from "jsonwebtoken"; +import bcrypt from "bcryptjs"; +import userService from "../services/userService.js"; +import { authenticateToken } from "../middleware/auth.js"; const router = express.Router(); // JWT Configuration from environment variables if (!process.env.JWT_SECRET) { - console.warn('⚠️ WARNING: JWT_SECRET not set in environment variables. Using default (INSECURE!).'); + console.warn( + "⚠️ WARNING: JWT_SECRET not set in environment variables. Using default (INSECURE!).", + ); } -const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; -const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d'; +const JWT_SECRET = + process.env.JWT_SECRET || "your-secret-key-change-in-production"; +const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "7d"; // Helper: get role-specific profile id (if present) from a user object function getRoleSpecificId(user) { - if (!user || !user.role) return null; + if (!user || !user.role) return null; + + // Profiles are included by userService with these aliases + if ( + user.role === "technician" && + user.technicianProfile && + user.technicianProfile.id + ) { + return user.technicianProfile.id; + } + + if ( + user.role === "branch_manager" && + user.branchManagerProfile && + user.branchManagerProfile.id + ) { + return user.branchManagerProfile.id; + } + + if ( + user.role === "maintenance_executive" && + user.maintenanceExecutiveProfile && + user.maintenanceExecutiveProfile.id + ) { + return user.maintenanceExecutiveProfile.id; + } + + return null; +} - // Profiles are included by userService with these aliases - if (user.role === 'technician' && user.technicianProfile && user.technicianProfile.id) { - return user.technicianProfile.id; +/** + * @route POST /api/v1/auth/register + * @desc Register a new user + * @access Public + */ +router.post("/register", async (req, res) => { + try { + const { email, password, name, role } = req.body; + + // Validate input + if (!email || !password || !name || !role) { + return res.status(400).json({ + success: false, + message: "Email, password, name, and role are required", + }); } - if (user.role === 'branch_manager' && user.branchManagerProfile && user.branchManagerProfile.id) { - return user.branchManagerProfile.id; + // Validate email format + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return res.status(400).json({ + success: false, + message: "Please provide a valid email address", + }); } - if (user.role === 'maintenance_executive' && user.maintenanceExecutiveProfile && user.maintenanceExecutiveProfile.id) { - return user.maintenanceExecutiveProfile.id; + // Validate password length + if (password.length < 6) { + return res.status(400).json({ + success: false, + message: "Password must be at least 6 characters long", + }); } - return null; -} + // Validate role + const validRoles = [ + "technician", + "branch_manager", + "maintenance_executive", + ]; + if (!validRoles.includes(role)) { + return res.status(400).json({ + success: false, + message: + "Invalid role. Must be technician, branch_manager, or maintenance_executive", + }); + } -/** - * @route POST /api/v1/auth/register - * @desc Register a new user - * @access Public - */ -router.post('/register', async (req, res) => { - try { - const { email, password, name, role } = req.body; - - // Validate input - if (!email || !password || !name || !role) { - return res.status(400).json({ - success: false, - message: 'Email, password, name, and role are required' - }); - } - - // Validate email format - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(email)) { - return res.status(400).json({ - success: false, - message: 'Please provide a valid email address' - }); - } - - // Validate password length - if (password.length < 6) { - return res.status(400).json({ - success: false, - message: 'Password must be at least 6 characters long' - }); - } - - // Validate role - const validRoles = ['technician', 'branch_manager', 'maintenance_executive']; - if (!validRoles.includes(role)) { - return res.status(400).json({ - success: false, - message: 'Invalid role. Must be technician, branch_manager, or maintenance_executive' - }); - } - - // Check if user already exists - const existingUser = await userService.getUserByEmail(email); - if (existingUser) { - return res.status(409).json({ - success: false, - message: 'User with this email already exists' - }); - } - - // Hash password with bcrypt before storing - const hashedPassword = await bcrypt.hash(password, 10); - - // Create user with role-specific profile - const userData = { - email, - password: hashedPassword, - name, - role, - isActive: true - }; - - // Role-specific profile data (can be extended) - const profileData = {}; - - const newUser = await userService.createUser(userData, profileData); - - // Generate JWT token - // include the role-specific profile id in the token as `roleId` - const roleSpecificId = getRoleSpecificId(newUser); - const token = jwt.sign( - { - id: newUser.id, - email: newUser.email, - role: newUser.role, - roleSpecificId: roleSpecificId - }, - JWT_SECRET, - { expiresIn: JWT_EXPIRES_IN } - ); - - // Convert to plain object and remove password - const userObject = newUser.get({ plain: true }); - const { password: _, ...userWithoutPassword } = userObject; - - res.status(201).json({ - success: true, - message: 'Registration successful', - token, - data: userWithoutPassword - }); - - } catch (error) { - console.error('Registration error:', error); - - // Handle unique constraint errors - if (error.message.includes('already exists')) { - return res.status(409).json({ - success: false, - message: error.message - }); - } - - res.status(500).json({ - success: false, - message: 'Registration failed. Please try again.' - }); + // Check if user already exists + const existingUser = await userService.getUserByEmail(email); + if (existingUser) { + return res.status(409).json({ + success: false, + message: "User with this email already exists", + }); } -}); + // Hash password with bcrypt before storing + const hashedPassword = await bcrypt.hash(password, 10); + + // Create user with role-specific profile + const userData = { + email, + password: hashedPassword, + name, + role, + isActive: true, + }; + + // Role-specific profile data (can be extended) + const profileData = {}; + + const newUser = await userService.createUser(userData, profileData); + + // Generate JWT token + // include the role-specific profile id in the token as `roleId` + const roleSpecificId = getRoleSpecificId(newUser); + const token = jwt.sign( + { + id: newUser.id, + email: newUser.email, + role: newUser.role, + roleSpecificId: roleSpecificId, + }, + JWT_SECRET, + { expiresIn: JWT_EXPIRES_IN }, + ); + + // Convert to plain object and remove password + const userObject = newUser.get({ plain: true }); + const { password: _, ...userWithoutPassword } = userObject; + + res.status(201).json({ + success: true, + message: "Registration successful", + token, + data: userWithoutPassword, + }); + } catch (error) { + console.error("Registration error:", error); + + // Handle unique constraint errors + if (error.message.includes("already exists")) { + return res.status(409).json({ + success: false, + message: error.message, + }); + } + + res.status(500).json({ + success: false, + message: "Registration failed. Please try again.", + }); + } +}); /** * @route POST /api/v1/auth/login * @desc Login user and return JWT token * @access Public */ -router.post('/login', async (req, res) => { - try { - const { email, password } = req.body; - - // Validate input - if (!email || !password) { - return res.status(400).json({ - success: false, - message: 'Email and password are required' - }); - } - - // Find user by email - const user = await userService.getUserByEmail(email); - - if (!user) { - return res.status(401).json({ - success: false, - message: 'Invalid email or password' - }); - } - - // Check if user is active - if (!user.isActive) { - return res.status(401).json({ - success: false, - message: 'Account is inactive. Please contact support.' - }); - } - - // Verify password using bcrypt - const isPasswordValid = await bcrypt.compare(password, user.password); - - if (!isPasswordValid) { - return res.status(401).json({ - success: false, - message: 'Invalid email or password' - }); - } - - // Generate JWT token - // include the role-specific profile id in the token as `roleId` - const roleSpecificId = getRoleSpecificId(user); - const token = jwt.sign( - { - id: user.id, - email: user.email, - role: user.role, - roleSpecificId: roleSpecificId - }, - JWT_SECRET, - { expiresIn: JWT_EXPIRES_IN } - ); - - // Convert Sequelize model to plain object to avoid circular reference - const userObject = user.get({ plain: true }); - - // Remove password from response - const { password: _, ...userWithoutPassword } = userObject; - - res.status(200).json({ - success: true, - message: 'Login successful', - token, - data: userWithoutPassword - }); - - } catch (error) { - console.error('Login error:', error); - res.status(500).json({ - success: false, - message: 'Login failed. Please try again.' - }); +router.post("/login", async (req, res) => { + try { + const { email, password } = req.body; + + // Validate input + if (!email || !password) { + return res.status(400).json({ + success: false, + message: "Email and password are required", + }); + } + + // Find user by email + const user = await userService.getUserByEmail(email); + + if (!user) { + return res.status(401).json({ + success: false, + message: "Invalid email or password", + }); + } + + // Check if user is active + if (!user.isActive) { + return res.status(401).json({ + success: false, + message: "Account is inactive. Please contact support.", + }); + } + + // Verify password using bcrypt + const isPasswordValid = await bcrypt.compare(password, user.password); + + if (!isPasswordValid) { + return res.status(401).json({ + success: false, + message: "Invalid email or password", + }); } + + // Generate JWT token + // include the role-specific profile id in the token as `roleId` + const roleSpecificId = getRoleSpecificId(user); + const token = jwt.sign( + { + id: user.id, + email: user.email, + role: user.role, + roleSpecificId: roleSpecificId, + }, + JWT_SECRET, + { expiresIn: JWT_EXPIRES_IN }, + ); + + // Convert Sequelize model to plain object to avoid circular reference + const userObject = user.get({ plain: true }); + + // Remove password from response + const { password: _, ...userWithoutPassword } = userObject; + + res.status(200).json({ + success: true, + message: "Login successful", + token, + data: userWithoutPassword, + }); + } catch (error) { + console.error("Login error:", error); + res.status(500).json({ + success: false, + message: "Login failed. Please try again.", + }); + } }); /** @@ -231,36 +249,35 @@ router.post('/login', async (req, res) => { * @desc Get current logged-in user * @access Protected (requires valid JWT token) */ -router.get('/me', authenticateToken, async (req, res) => { - try { - // req.user is set by the authenticateToken middleware - const user = await userService.getUserById(req.user.id); - - if (!user) { - return res.status(404).json({ - success: false, - message: 'User not found' - }); - } - - // Convert Sequelize model to plain object to avoid circular reference - const userObject = user.get({ plain: true }); - - // Remove password from response (already excluded by getUserById, but just in case) - const { password: _, ...userWithoutPassword } = userObject; - - res.status(200).json({ - success: true, - data: userWithoutPassword - }); - - } catch (error) { - console.error('Get current user error:', error); - res.status(500).json({ - success: false, - message: 'Failed to fetch user data' - }); +router.get("/me", authenticateToken, async (req, res) => { + try { + // req.user is set by the authenticateToken middleware + const user = await userService.getUserById(req.user.id); + + if (!user) { + return res.status(404).json({ + success: false, + message: "User not found", + }); } + + // Convert Sequelize model to plain object to avoid circular reference + const userObject = user.get({ plain: true }); + + // Remove password from response (already excluded by getUserById, but just in case) + const { password: _, ...userWithoutPassword } = userObject; + + res.status(200).json({ + success: true, + data: userWithoutPassword, + }); + } catch (error) { + console.error("Get current user error:", error); + res.status(500).json({ + success: false, + message: "Failed to fetch user data", + }); + } }); /** @@ -268,59 +285,60 @@ router.get('/me', authenticateToken, async (req, res) => { * @desc Initiate password reset process (generate reset token) * @access Public */ -router.post('/forgot-password', async (req, res) => { - try { - const { email } = req.body; - - // Validate input - if (!email) { - return res.status(400).json({ - success: false, - message: 'Email is required' - }); - } - - // Find user by email - const user = await userService.getUserByEmail(email); - - // Don't reveal if user exists or not (security best practice) - if (!user) { - return res.status(200).json({ - success: true, - message: 'If an account exists with this email, a password reset link has been sent.' - }); - } - - // Generate a password reset token (valid for 1 hour) - const resetToken = jwt.sign( - { id: user.id, email: user.email }, - JWT_SECRET, - { expiresIn: '1h' } - ); - - // In a production app, you would: - // 1. Save this token to the database with an expiry time - // 2. Send an email with a reset link containing the token - // For now, we'll return the token directly (for demo purposes) - - // TODO: Send email with reset link - // const resetLink = `${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`; - // await sendEmail(user.email, 'Password Reset', resetLink); - - res.status(200).json({ - success: true, - message: 'If an account exists with this email, a password reset link has been sent.', - // For demo: return token (remove in production!) - resetToken: resetToken - }); - - } catch (error) { - console.error('Forgot password error:', error); - res.status(500).json({ - success: false, - message: 'Failed to process password reset request' - }); +router.post("/forgot-password", async (req, res) => { + try { + const { email } = req.body; + + // Validate input + if (!email) { + return res.status(400).json({ + success: false, + message: "Email is required", + }); } + + // Find user by email + const user = await userService.getUserByEmail(email); + + // Don't reveal if user exists or not (security best practice) + if (!user) { + return res.status(200).json({ + success: true, + message: + "If an account exists with this email, a password reset link has been sent.", + }); + } + + // Generate a password reset token (valid for 1 hour) + const resetToken = jwt.sign( + { id: user.id, email: user.email }, + JWT_SECRET, + { expiresIn: "1h" }, + ); + + // In a production app, you would: + // 1. Save this token to the database with an expiry time + // 2. Send an email with a reset link containing the token + // For now, we'll return the token directly (for demo purposes) + + // TODO: Send email with reset link + // const resetLink = `${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`; + // await sendEmail(user.email, 'Password Reset', resetLink); + + res.status(200).json({ + success: true, + message: + "If an account exists with this email, a password reset link has been sent.", + // For demo: return token (remove in production!) + resetToken: resetToken, + }); + } catch (error) { + console.error("Forgot password error:", error); + res.status(500).json({ + success: false, + message: "Failed to process password reset request", + }); + } }); /** @@ -328,93 +346,65 @@ router.post('/forgot-password', async (req, res) => { * @desc Reset password using reset token * @access Public */ -router.post('/reset-password', async (req, res) => { +router.post("/reset-password", async (req, res) => { + try { + const { token, newPassword } = req.body; + + // Validate input + if (!token || !newPassword) { + return res.status(400).json({ + success: false, + message: "Token and new password are required", + }); + } + + // Validate password length + if (newPassword.length < 6) { + return res.status(400).json({ + success: false, + message: "Password must be at least 6 characters long", + }); + } + + // Verify the reset token + let decoded; try { - const { token, newPassword } = req.body; - - // Validate input - if (!token || !newPassword) { - return res.status(400).json({ - success: false, - message: 'Token and new password are required' - }); - } - - // Validate password length - if (newPassword.length < 6) { - return res.status(400).json({ - success: false, - message: 'Password must be at least 6 characters long' - }); - } - - // Verify the reset token - let decoded; - try { - decoded = jwt.verify(token, JWT_SECRET); - } catch (err) { - return res.status(400).json({ - success: false, - message: 'Invalid or expired reset token' - }); - } - - // Find the user - const user = await userService.getUserById(decoded.id); - - if (!user) { - return res.status(404).json({ - success: false, - message: 'User not found' - }); - } - - // Hash the new password - const hashedPassword = await bcrypt.hash(newPassword, 10); - - // Update the user's password - await userService.updateUser(decoded.id, { password: hashedPassword }, {}); - - res.status(200).json({ - success: true, - message: 'Password has been reset successfully. You can now login with your new password.' - }); - - } catch (error) { - console.error('Reset password error:', error); - res.status(500).json({ - success: false, - message: 'Failed to reset password' - }); + decoded = jwt.verify(token, JWT_SECRET); + } catch (err) { + return res.status(400).json({ + success: false, + message: "Invalid or expired reset token", + }); } -}); -/** - * Middleware to authenticate JWT token - */ -function authenticateToken(req, res, next) { - const authHeader = req.headers['authorization']; - const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN - - if (!token) { - return res.status(401).json({ - success: false, - message: 'Access token is required' - }); + // Find the user + const user = await userService.getUserById(decoded.id); + + if (!user) { + return res.status(404).json({ + success: false, + message: "User not found", + }); } - jwt.verify(token, JWT_SECRET, (err, user) => { - if (err) { - return res.status(403).json({ - success: false, - message: 'Invalid or expired token' - }); - } + // Hash the new password + const hashedPassword = await bcrypt.hash(newPassword, 10); + + // Update the user's password + await userService.updateUser(decoded.id, { password: hashedPassword }, {}); - req.user = user; - next(); + res.status(200).json({ + success: true, + message: + "Password has been reset successfully. You can now login with your new password.", }); -} + } catch (error) { + console.error("Reset password error:", error); + res.status(500).json({ + success: false, + message: "Failed to reset password", + }); + } +}); -export { authenticateToken }; export default router; diff --git a/src/routes/branch.js b/src/routes/branch.js index d220806..bff3040 100644 --- a/src/routes/branch.js +++ b/src/routes/branch.js @@ -1,22 +1,26 @@ // src/routes/branchRoutes.js -import { Router } from 'express'; -import branchController from '../controllers/branchController.js'; +import { Router } from "express"; +import branchController from "../controllers/branchController.js"; +import { authorizeRoles } from "../middleware/auth.js"; const router = Router(); +// Only maintenance_executive can access branch routes +router.use(authorizeRoles("maintenance_executive")); + // POST /api/v1/branches - Add a new branch -router.post('/', branchController.addBranch); +router.post("/", branchController.addBranch); // GET /api/v1/branches - Get all branches -router.get('/', branchController.getAllBranches); +router.get("/", branchController.getAllBranches); // GET /api/v1/branches/:id - Get branch by ID -router.get('/:id', branchController.getBranchById); +router.get("/:id", branchController.getBranchById); // PUT /api/v1/branches/:id - Update branch by ID -router.put('/:id', branchController.updateBranch); +router.put("/:id", branchController.updateBranch); // DELETE /api/v1/branches/:id - Delete branch by ID -router.delete('/:id', branchController.deleteBranch); +router.delete("/:id", branchController.deleteBranch); -export default router; \ No newline at end of file +export default router; diff --git a/src/routes/issues.js b/src/routes/issues.js index 985f669..724eb08 100644 --- a/src/routes/issues.js +++ b/src/routes/issues.js @@ -1,33 +1,41 @@ -import { Router } from 'express'; -import issueController from '../controllers/issueController.js'; +import { Router } from "express"; +import issueController from "../controllers/issueController.js"; +import { authorizeRoles } from "../middleware/auth.js"; const router = Router(); // POST /api/v1/issues - Create new issue -router.post('/', issueController.createIssue); +router.post( + "/", + authorizeRoles("maintenance_executive", "branch_manager"), + issueController.createIssue, +); // GET /api/v1/issues - Get all issues with filtering -router.get('/', issueController.getAllIssues); +router.get("/", issueController.getAllIssues); // GET /api/v1/issues/:id - Get issue by ID -router.get('/:id', issueController.getIssueById); +router.get("/:id", issueController.getIssueById); // PUT /api/v1/issues/:id - Update issue -router.put('/:id', issueController.updateIssue); +router.put("/:id", issueController.updateIssue); // DELETE /api/v1/issues/:id - Delete issue -router.delete('/:id', issueController.deleteIssue); +router.delete("/:id", issueController.deleteIssue); // POST /api/v1/issues/:id/assign-technician - Assign technician to issue -router.post('/:id/assign-technician', issueController.assignTechnician); +router.post("/:id/assign-technician", issueController.assignTechnician); // POST /api/v1/issues/:id/assign-maintenance-executive - Assign maintenance executive to issue -router.post('/:id/assign-maintenance-executive', issueController.assignMaintenanceExecutive); +router.post( + "/:id/assign-maintenance-executive", + issueController.assignMaintenanceExecutive, +); // POST /api/v1/issues/:id/assign-third-party - Assign third party to issue -router.post('/:id/assign-third-party', issueController.assignThirdParty); +router.post("/:id/assign-third-party", issueController.assignThirdParty); // PUT /api/v1/issues/:id/status - Update issue status -router.put('/:id/status', issueController.updateStatus); +router.put("/:id/status", issueController.updateStatus); -export default router; \ No newline at end of file +export default router; diff --git a/src/services/issueService.js b/src/services/issueService.js index 58688a2..726801b 100644 --- a/src/services/issueService.js +++ b/src/services/issueService.js @@ -1,60 +1,75 @@ -import models from '../models/index.js'; - -const { Issue, Branch, BranchManager, Technician, MaintenanceExecutive, ThirdParty, Message, PettyCashRequest, User } = models; -import { Op } from 'sequelize'; +import models from "../models/index.js"; + +const { + Issue, + Branch, + BranchManager, + Technician, + MaintenanceExecutive, + ThirdParty, + Message, + PettyCashRequest, + User, +} = models; +import { Op } from "sequelize"; class IssueService { // Create a new issue async createIssue(issueData) { try { const { manager_id, branch_id } = issueData; - + // Validate branch exists const branch = await Branch.findByPk(branch_id); if (!branch) { return { success: false, - message: 'Invalid branch ID' + message: "Invalid branch ID", }; } - + // Validate branch manager exists const branchManager = await BranchManager.findByPk(manager_id); if (!branchManager) { return { success: false, - message: 'Invalid branch manager ID' + message: "Invalid branch manager ID", }; } - + // Validate that branch manager is assigned to the specified branch - if (branchManager.branchId && branchManager.branchId !== parseInt(branch_id)) { + if ( + branchManager.branchId && + branchManager.branchId !== parseInt(branch_id) + ) { return { success: false, - message: `Branch manager is assigned to branch ${branchManager.branchId}, but issue is for branch ${branch_id}` + message: `Branch manager is assigned to branch ${branchManager.branchId}, but issue is for branch ${branch_id}`, }; } - + // If branch manager has no assigned branch, that might be okay or might be an error // depending on your business logic. For now, we'll allow it. if (!branchManager.branchId) { - console.warn(`Branch manager ${manager_id} has no assigned branch, but creating issue for branch ${branch_id}`); + console.warn( + `Branch manager ${manager_id} has no assigned branch, but creating issue for branch ${branch_id}`, + ); } - + // Create the issue const issue = await Issue.create(issueData); - + return { success: true, - message: 'Issue created successfully', - data: issue + message: "Issue created successfully", + data: issue, }; } catch (error) { - console.error('Error creating issue:', error); + console.error("Error creating issue:", error); return { success: false, - message: 'Failed to create issue', - error: error.message + message: "Failed to create issue", + error: error.message, }; } } @@ -63,36 +78,36 @@ class IssueService { async getAllIssues(filters = {}) { try { const where = {}; - + // Apply filters if provided if (filters.branch_id) { where.branch_id = filters.branch_id; } - + if (filters.manager_id) { where.manager_id = filters.manager_id; } - + if (filters.technician_id) { where.technician_id = filters.technician_id; } - + if (filters.maintenance_executive_id) { where.maintenance_executive_id = filters.maintenance_executive_id; } - + if (filters.third_party_id) { where.third_party_id = filters.third_party_id; } - + if (filters.status) { where.status = filters.status; } - + if (filters.search) { where[Op.or] = [ { title: { [Op.iLike]: `%${filters.search}%` } }, - { description: { [Op.iLike]: `%${filters.search}%` } } + { description: { [Op.iLike]: `%${filters.search}%` } }, ]; } @@ -102,54 +117,60 @@ class IssueService { include.push( { model: Branch, - as: 'branch', - attributes: ['id', 'name', 'location'] + as: "branch", + attributes: ["id", "name", "location"], }, { model: BranchManager, - as: 'manager', - attributes: ['id'], - include: [{ - model: models.User, - as: 'user', - attributes: ['id', 'name', 'email'] - }] + as: "manager", + attributes: ["id"], + include: [ + { + model: models.User, + as: "user", + attributes: ["id", "name", "email"], + }, + ], }, { model: Technician, - as: 'technician', - attributes: ['id', 'specialization'], - include: [{ - model: models.User, - as: 'user', - attributes: ['id', 'name', 'email'] - }] + as: "technician", + attributes: ["id", "specialization"], + include: [ + { + model: models.User, + as: "user", + attributes: ["id", "name", "email"], + }, + ], }, { model: MaintenanceExecutive, - as: 'maintenanceExecutive', - attributes: ['id'], - include: [{ - model: models.User, - as: 'user', - attributes: ['id', 'name', 'email'] - }] + as: "maintenanceExecutive", + attributes: ["id"], + include: [ + { + model: models.User, + as: "user", + attributes: ["id", "name", "email"], + }, + ], }, { model: ThirdParty, - as: 'thirdParty', - attributes: ['id', 'organization', 'email', 'worktype'] - } + as: "thirdParty", + attributes: ["id", "organization", "email", "worktype"], + }, ); } const { count, rows: issues } = await Issue.findAndCountAll({ where, include, - order: [['createdAt', 'DESC']], + order: [["createdAt", "DESC"]], limit: filters.limit || 100, offset: filters.offset || 0, - distinct: true + distinct: true, }); return { @@ -157,13 +178,13 @@ class IssueService { data: issues, count: issues.length, total: count, - message: 'Issues retrieved successfully' + message: "Issues retrieved successfully", }; } catch (error) { return { success: false, error: error.message, - message: 'Failed to retrieve issues' + message: "Failed to retrieve issues", }; } } @@ -172,84 +193,113 @@ class IssueService { async getIssueById(id, includeRelations = false) { try { const include = []; - + if (includeRelations) { include.push( { model: Branch, - as: 'branch', - attributes: ['id', 'name', 'location'] + as: "branch", + attributes: ["id", "name", "location"], }, { model: BranchManager, - as: 'manager', - attributes: ['id'], - include: [{ - model: models.User, - as: 'user', - attributes: ['id', 'name', 'email', 'phone'] - }] + as: "manager", + attributes: ["id"], + include: [ + { + model: models.User, + as: "user", + attributes: ["id", "name", "email", "phone"], + }, + ], }, { model: Technician, - as: 'technician', - attributes: ['id', 'specialization', 'experienceYears', 'isAvailable'], - include: [{ - model: models.User, - as: 'user', - attributes: ['id', 'name', 'email', 'phone'] - }] + as: "technician", + attributes: [ + "id", + "specialization", + "experienceYears", + "isAvailable", + ], + include: [ + { + model: models.User, + as: "user", + attributes: ["id", "name", "email", "phone"], + }, + ], }, { model: MaintenanceExecutive, - as: 'maintenanceExecutive', - attributes: ['id'], - include: [{ - model: models.User, - as: 'user', - attributes: ['id', 'name', 'email', 'phone'] - }] + as: "maintenanceExecutive", + attributes: ["id"], + include: [ + { + model: models.User, + as: "user", + attributes: ["id", "name", "email", "phone"], + }, + ], }, { model: ThirdParty, - as: 'thirdParty', - attributes: ['id', 'organization', 'email', 'worktype', 'location', 'phone'] + as: "thirdParty", + attributes: [ + "id", + "organization", + "email", + "worktype", + "location", + "phone", + ], }, { model: Message, - as: 'messages', - attributes: ['id', 'body', 'sender_id', 'receiver_id', 'createdAt'], + as: "messages", + attributes: ["id", "body", "sender_id", "receiver_id", "createdAt"], include: [ { model: models.User, - as: 'sender', - attributes: ['id', 'name', 'email'] + as: "sender", + attributes: ["id", "name", "email"], }, { model: models.User, - as: 'receiver', - attributes: ['id', 'name', 'email'] - } - ] + as: "receiver", + attributes: ["id", "name", "email"], + }, + ], }, { model: PettyCashRequest, - as: 'pettyCashRequests', - attributes: ['id', 'technician_id', 'amount', 'description', 'status', 'createdAt'] - } + as: "pettyCashRequests", + attributes: [ + "id", + "technician_id", + "amount", + "description", + "status", + "createdAt", + ], + }, ); } - const queryOptions = { + const queryOptions = { include, - order: [] + order: [], }; // Add ordering for nested associations when including relations if (includeRelations) { queryOptions.order = [ - [{ model: Message, as: 'messages' }, 'createdAt', 'ASC'], - [{ model: PettyCashRequest, as: 'pettyCashRequests' }, 'createdAt', 'DESC'] + [{ model: Message, as: "messages" }, "createdAt", "ASC"], + [ + { model: PettyCashRequest, as: "pettyCashRequests" }, + "createdAt", + "DESC", + ], ]; } @@ -258,20 +308,20 @@ class IssueService { if (!issue) { return { success: false, - message: 'Issue not found' + message: "Issue not found", }; } return { success: true, data: issue, - message: 'Issue retrieved successfully' + message: "Issue retrieved successfully", }; } catch (error) { return { success: false, error: error.message, - message: 'Failed to retrieve issue' + message: "Failed to retrieve issue", }; } } @@ -284,7 +334,7 @@ class IssueService { if (!issue) { return { success: false, - message: 'Issue not found' + message: "Issue not found", }; } @@ -295,37 +345,39 @@ class IssueService { include: [ { model: Branch, - as: 'branch', - attributes: ['id', 'name', 'location'] + as: "branch", + attributes: ["id", "name", "location"], }, { model: BranchManager, - as: 'manager', - attributes: ['id'], - include: [{ - model: models.User, - as: 'user', - attributes: ['id', 'name', 'email'] - }] + as: "manager", + attributes: ["id"], + include: [ + { + model: models.User, + as: "user", + attributes: ["id", "name", "email"], + }, + ], }, { model: ThirdParty, - as: 'thirdParty', - attributes: ['id', 'organization', 'email'] - } - ] + as: "thirdParty", + attributes: ["id", "organization", "email"], + }, + ], }); return { success: true, data: updatedIssue, - message: 'Issue updated successfully' + message: "Issue updated successfully", }; } catch (error) { return { success: false, error: error.message, - message: 'Failed to update issue' + message: "Failed to update issue", }; } } @@ -338,7 +390,7 @@ class IssueService { if (!issue) { return { success: false, - message: 'Issue not found' + message: "Issue not found", }; } @@ -346,13 +398,13 @@ class IssueService { return { success: true, - message: 'Issue deleted successfully' + message: "Issue deleted successfully", }; } catch (error) { return { success: false, error: error.message, - message: 'Failed to delete issue' + message: "Failed to delete issue", }; } } @@ -365,7 +417,7 @@ class IssueService { if (!issue) { return { success: false, - message: 'Issue not found' + message: "Issue not found", }; } @@ -374,40 +426,52 @@ class IssueService { if (!technician) { return { success: false, - message: 'Technician not found' + message: "Technician not found", }; } - await issue.update({ + await issue.update({ technician_id: technicianId, - technician_assigned_at: new Date() + technician_assigned_at: new Date(), }); // Fetch updated issue with technician info and assignment timestamp const updatedIssue = await Issue.findByPk(issueId, { - attributes: ['id', 'title', 'description', 'status', 'technician_id', 'technician_assigned_at', 'updatedAt'], - include: [{ - model: Technician, - as: 'technician', - attributes: ['id', 'specialization', 'createdAt', 'updatedAt'], - include: [{ - model: models.User, - as: 'user', - attributes: ['id', 'name', 'email', 'createdAt'] - }] - }] + attributes: [ + "id", + "title", + "description", + "status", + "technician_id", + "technician_assigned_at", + "updatedAt", + ], + include: [ + { + model: Technician, + as: "technician", + attributes: ["id", "specialization", "createdAt", "updatedAt"], + include: [ + { + model: models.User, + as: "user", + attributes: ["id", "name", "email", "createdAt"], + }, + ], + }, + ], }); return { success: true, data: updatedIssue, - message: 'Technician assigned successfully' + message: "Technician assigned successfully", }; } catch (error) { return { success: false, error: error.message, - message: 'Failed to assign technician' + message: "Failed to assign technician", }; } } @@ -420,7 +484,7 @@ class IssueService { if (!issue) { return { success: false, - message: 'Issue not found' + message: "Issue not found", }; } @@ -429,40 +493,52 @@ class IssueService { if (!executive) { return { success: false, - message: 'Maintenance Executive not found' + message: "Maintenance Executive not found", }; } - await issue.update({ + await issue.update({ maintenance_executive_id: executiveId, - maintenance_executive_assigned_at: new Date() + maintenance_executive_assigned_at: new Date(), }); // Fetch updated issue with executive info and assignment timestamp const updatedIssue = await Issue.findByPk(issueId, { - attributes: ['id', 'title', 'description', 'status', 'maintenance_executive_id', 'maintenance_executive_assigned_at', 'updatedAt'], - include: [{ - model: MaintenanceExecutive, - as: 'maintenanceExecutive', - attributes: ['id', 'createdAt', 'updatedAt'], - include: [{ - model: models.User, - as: 'user', - attributes: ['id', 'name', 'email', 'createdAt'] - }] - }] + attributes: [ + "id", + "title", + "description", + "status", + "maintenance_executive_id", + "maintenance_executive_assigned_at", + "updatedAt", + ], + include: [ + { + model: MaintenanceExecutive, + as: "maintenanceExecutive", + attributes: ["id", "createdAt", "updatedAt"], + include: [ + { + model: models.User, + as: "user", + attributes: ["id", "name", "email", "createdAt"], + }, + ], + }, + ], }); return { success: true, data: updatedIssue, - message: 'Maintenance Executive assigned successfully' + message: "Maintenance Executive assigned successfully", }; } catch (error) { return { success: false, error: error.message, - message: 'Failed to assign maintenance executive' + message: "Failed to assign maintenance executive", }; } } @@ -475,7 +551,7 @@ class IssueService { if (!issue) { return { success: false, - message: 'Issue not found' + message: "Issue not found", }; } @@ -484,35 +560,52 @@ class IssueService { if (!thirdParty) { return { success: false, - message: 'Third Party not found' + message: "Third Party not found", }; } - await issue.update({ + await issue.update({ third_party_id: thirdPartyId, - third_party_assigned_at: new Date() + third_party_assigned_at: new Date(), }); // Fetch updated issue with third party info and assignment timestamp const updatedIssue = await Issue.findByPk(issueId, { - attributes: ['id', 'title', 'description', 'status', 'third_party_id', 'third_party_assigned_at', 'updatedAt'], - include: [{ - model: ThirdParty, - as: 'thirdParty', - attributes: ['id', 'organization', 'email', 'worktype', 'createdAt', 'updatedAt'] - }] + attributes: [ + "id", + "title", + "description", + "status", + "third_party_id", + "third_party_assigned_at", + "updatedAt", + ], + include: [ + { + model: ThirdParty, + as: "thirdParty", + attributes: [ + "id", + "organization", + "email", + "worktype", + "createdAt", + "updatedAt", + ], + }, + ], }); return { success: true, data: updatedIssue, - message: 'Third Party assigned successfully' + message: "Third Party assigned successfully", }; } catch (error) { return { success: false, error: error.message, - message: 'Failed to assign third party' + message: "Failed to assign third party", }; } } @@ -525,7 +618,7 @@ class IssueService { if (!issue) { return { success: false, - message: 'Issue not found' + message: "Issue not found", }; } @@ -534,16 +627,16 @@ class IssueService { return { success: true, data: issue, - message: 'Issue status updated successfully' + message: "Issue status updated successfully", }; } catch (error) { return { success: false, error: error.message, - message: 'Failed to update issue status' + message: "Failed to update issue status", }; } } } -export default new IssueService(); \ No newline at end of file +export default new IssueService(); From ef4b38d54a6eedb6c69d35a82114b65f750682ad Mon Sep 17 00:00:00 2001 From: Kavinda L Jayarathna Date: Wed, 1 Apr 2026 20:07:37 +0530 Subject: [PATCH 2/2] fix: gpm / gdm issue --- src/services/userService.js | 51 ++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/src/services/userService.js b/src/services/userService.js index 77e2bf3..4d1114b 100644 --- a/src/services/userService.js +++ b/src/services/userService.js @@ -1,4 +1,5 @@ import models from '../models/index.js'; +import branchService from './branchService.js'; const { User, Technician, BranchManager, MaintenanceExecutive } = models; import { getSequelizeInstance } from './connectionService.js'; @@ -264,13 +265,57 @@ class UserService { return { message: 'User deleted successfully' }; } + // /** + // * Get users by role with profiles + // * @param {string} role - User role + // * @returns {Promise} List of users with profiles + // */ + // async getUsersByRole(role) { + // return this.getAllUsers(role); + // } + /** - * Get users by role with profiles + * Get users by role with ONLY their specific profile + * If branch_manager, manually attaches branch data using BranchService * @param {string} role - User role - * @returns {Promise} List of users with profiles + * @returns {Promise} List of users */ async getUsersByRole(role) { - return this.getAllUsers(role); + const include = []; + + if (role === 'technician') { + include.push({ model: Technician, as: 'technicianProfile' }); + } else if (role === 'branch_manager') { + include.push({ model: BranchManager, as: 'branchManagerProfile' }); + } else if (role === 'maintenance_executive') { + include.push({ model: MaintenanceExecutive, as: 'maintenanceExecutiveProfile' }); + } + + const users = await User.findAll({ + where: { role, isActive: true }, + attributes: { exclude: ['password'] }, + include, + order: [['createdAt', 'DESC']] + }); + + // If role is branch_manager, iterate and fetch branch details manually + if (role === 'branch_manager') { + const usersWithBranch = await Promise.all(users.map(async (user) => { + const userJson = user.toJSON(); + const branchId = userJson.branchManagerProfile?.branchId; + + if (branchId) { + const branchResult = await branchService.getBranchById(branchId); + if (branchResult.success) { + userJson.Branch = branchResult.data; // Attaching branch data + } + } + return userJson; + })); + return usersWithBranch; + } + + return users; } }