diff --git a/server.js b/server.js index b87eec5..f6de22a 100644 --- a/server.js +++ b/server.js @@ -1,19 +1,22 @@ -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 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 branchRoutes from "./src/routes/branch.js"; +import thirdPartiesRoutes from "./src/routes/thirdparties.js"; +import cashRequestRoutes from "./src/routes/cashRequestRoutes.js"; import ahpRoutes from './src/routes/ahpRoutes.js'; -import authRoutes from './src/routes/auth.js'; import outsidePartyRequestRoutes from './src/routes/outsidePartyRequests.js'; +import authRoutes from "./src/routes/auth.js"; +import { authenticateToken } from "./src/middleware/auth.js"; const app = express(); const server = http.createServer(app); @@ -23,27 +26,31 @@ 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/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); app.use('/api/v1/petty-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/v1/ahp', ahpRoutes); app.use('/api/v1/outside-party-requests', outsidePartyRequestRoutes); // 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(), }); }); @@ -52,14 +59,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 @@ -70,15 +77,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 6f21226..17f47b5 100644 --- a/src/controllers/issueController.js +++ b/src/controllers/issueController.js @@ -1,5 +1,11 @@ -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 @@ -13,21 +19,22 @@ class IssueController { maintenance_executive_id, technician_id, status, - third_party_id + third_party_id, } = req.body; // Validation if (!branch_id || !title) { return res.status(400).json({ success: false, - message: 'Branch ID and title are required' + message: "Branch ID, title, and manager ID are required", }); } const issueData = { branch_id: parseInt(branch_id), title, - description + manager_id: parseInt(manager_id), + description, }; // Include manager_id only if it is a valid non-zero integer @@ -55,13 +62,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); @@ -71,8 +82,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, }); } } @@ -90,20 +101,42 @@ class IssueController { 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); @@ -115,8 +148,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, }); } } @@ -130,22 +163,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, }); } } @@ -159,12 +226,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]); } @@ -176,10 +249,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 { @@ -188,8 +260,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, }); } } @@ -202,7 +274,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", }); } @@ -218,8 +290,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, }); } } @@ -233,32 +305,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); @@ -268,8 +343,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, }); } } @@ -283,29 +358,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 { @@ -314,8 +396,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, }); } } @@ -329,25 +411,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); @@ -357,8 +442,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, }); } } @@ -372,22 +457,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', 'Pending Resolution', 'Pending Close', '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(", ")}`, }); } @@ -398,17 +483,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); } @@ -419,8 +512,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 24fe7a6..917ea4f 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -1,37 +1,52 @@ -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"; import emailService from '../services/emailService.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; - - // 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; + 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; } /** @@ -39,193 +54,195 @@ function getRoleSpecificId(user) { * @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' - }); - } +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", + }); + } - // Hash password with bcrypt before storing - const hashedPassword = await bcrypt.hash(password, 10); + // 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", + }); + } - // 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 - }); + // Validate password length + if (password.length < 6) { + return res.status(400).json({ + success: false, + message: "Password must be at least 6 characters long", + }); + } - } catch (error) { - console.error('Registration error:', error); + // 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", + }); + } - // Handle unique constraint errors - if (error.message.includes('already exists')) { - return res.status(409).json({ - success: false, - message: error.message - }); - } + // 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", + }); + } - res.status(500).json({ - success: false, - message: 'Registration failed. Please try again.' - }); + // 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.' - }); - } +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", + }); + } - // Verify password using bcrypt - const isPasswordValid = await bcrypt.compare(password, user.password); + // Find user by email + const user = await userService.getUserByEmail(email); - if (!isPasswordValid) { - return res.status(401).json({ - success: false, - message: 'Invalid email or password' - }); - } + if (!user) { + 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; + // Check if user is active + if (!user.isActive) { + return res.status(401).json({ + success: false, + message: "Account is inactive. Please contact support.", + }); + } - res.status(200).json({ - success: true, - message: 'Login successful', - token, - data: userWithoutPassword - }); + // Verify password using bcrypt + const isPasswordValid = await bcrypt.compare(password, user.password); - } catch (error) { - console.error('Login error:', error); - res.status(500).json({ - success: false, - message: 'Login failed. Please try again.' - }); + 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.", + }); + } }); /** @@ -233,36 +250,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 }); +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", + }); + } - // Remove password from response (already excluded by getUserById, but just in case) - const { password: _, ...userWithoutPassword } = userObject; + // Convert Sequelize model to plain object to avoid circular reference + const userObject = user.get({ plain: true }); - res.status(200).json({ - success: true, - data: userWithoutPassword - }); + // Remove password from response (already excluded by getUserById, but just in case) + const { password: _, ...userWithoutPassword } = userObject; - } catch (error) { - console.error('Get current user error:', error); - res.status(500).json({ - success: false, - message: 'Failed to fetch user data' - }); - } + 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", + }); + } }); /** @@ -270,20 +286,20 @@ 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' - }); - } +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); + // Find user by email + const user = await userService.getUserByEmail(email); // Don't reveal if user exists or not (security best practice) if (!user) { @@ -387,8 +403,8 @@ router.post('/reset-password', async (req, res) => { }); } - // Hash the new password - const hashedPassword = await bcrypt.hash(newPassword, 10); + // Hash the new password + const hashedPassword = await bcrypt.hash(newPassword, 10); // Update the user's password and clear OTP await userService.updateUser(user.id, { @@ -397,46 +413,18 @@ router.post('/reset-password', async (req, res) => { resetOTPExpiry: null }); - 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' - }); - } -}); - -/** - * 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' - }); - } - - 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(); + 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 70d5e60..c85fbb7 100644 --- a/src/routes/issues.js +++ b/src/routes/issues.js @@ -1,35 +1,43 @@ -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"; import statusController from '../controllers/statusController.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); // GET /api/v1/issues/:id/statuses - Get all status update logs for an issue router.get('/:id/statuses', statusController.getStatusUpdates); diff --git a/src/services/issueService.js b/src/services/issueService.js index 67b5bf8..f5f2409 100644 --- a/src/services/issueService.js +++ b/src/services/issueService.js @@ -1,64 +1,60 @@ -import models from '../models/index.js'; - -const { Issue, Branch, BranchManager, Technician, MaintenanceExecutive, ThirdParty, Message, PettyCashRequest, User, Status } = models; +import models from "../models/index.js"; + +const { + Issue, + Branch, + BranchManager, + Technician, + MaintenanceExecutive, + ThirdParty, + Message, + PettyCashRequest, + User, Status, +} = models; import OutsidePartyRequest from '../models/outsidePartyRequest.js'; -import { Op } from 'sequelize'; +import { Op } from "sequelize"; class IssueService { // Create a new issue async createIssue(issueData) { try { - let { manager_id, branch_id } = issueData; + 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", }; } - // If manager_id is not provided or is 0, auto-assign a branch manager - if (!manager_id) { - // Try to find a manager assigned to this specific branch first - let autoManager = await BranchManager.findOne({ where: { branchId: parseInt(branch_id) } }); - - // If no branch-specific manager found, pick any available branch manager - if (!autoManager) { - autoManager = await BranchManager.findOne(); - } - - if (!autoManager) { - return { - success: false, - message: 'No branch manager available to assign. Please contact support.' - }; - } - - manager_id = autoManager.id; - issueData = { ...issueData, manager_id }; - } else { - // Validate provided branch manager exists - const branchManager = await BranchManager.findByPk(manager_id); - if (!branchManager) { - return { - success: false, - message: 'Invalid branch manager ID' - }; - } - - // Validate that branch manager is assigned to the specified branch - 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}` - }; - } - - if (!branchManager.branchId) { - console.warn(`Branch manager ${manager_id} has no assigned branch, but creating issue for branch ${branch_id}`); - } + // Validate branch manager exists + const branchManager = await BranchManager.findByPk(manager_id); + if (!branchManager) { + return { + success: false, + message: "Invalid branch manager ID", + }; + } + + // Validate that branch manager is assigned to the specified branch + 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}`, + }; + } + + // 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}`, + ); } // Create the issue @@ -66,15 +62,15 @@ class IssueService { 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, }; } } @@ -112,7 +108,7 @@ class IssueService { if (filters.search) { where[Op.or] = [ { title: { [Op.iLike]: `%${filters.search}%` } }, - { description: { [Op.iLike]: `%${filters.search}%` } } + { description: { [Op.iLike]: `%${filters.search}%` } }, ]; } @@ -122,54 +118,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 { @@ -177,13 +179,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", }; } } @@ -197,65 +199,90 @@ 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', '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", + ], }, { model: OutsidePartyRequest, @@ -271,13 +298,13 @@ class IssueService { as: 'user', attributes: ['id', 'name', 'email', 'profilePicture'] }] - } + }, ); } const queryOptions = { include, - order: [] + order: [], }; // Add ordering for nested associations when including relations @@ -295,20 +322,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", }; } } @@ -321,25 +348,50 @@ class IssueService { if (!issue) { return { success: false, - message: 'Issue not found' + message: "Issue not found", }; } await issue.update(updateData); - // Fetch full updated issue to return to the client - const updatedIssueResult = await this.getIssueById(id, true); + // Fetch updated issue with relations + const updatedIssue = await Issue.findByPk(id, { + include: [ + { + model: Branch, + as: "branch", + attributes: ["id", "name", "location"], + }, + { + model: BranchManager, + as: "manager", + attributes: ["id"], + include: [ + { + model: models.User, + as: "user", + attributes: ["id", "name", "email"], + }, + ], + }, + { + model: ThirdParty, + as: "thirdParty", + attributes: ["id", "organization", "email"], + }, + ], + }); return { success: true, - data: updatedIssueResult.data, - message: 'Issue updated successfully' + data: updatedIssue, + message: "Issue updated successfully", }; } catch (error) { return { success: false, error: error.message, - message: 'Failed to update issue' + message: "Failed to update issue", }; } } @@ -352,7 +404,7 @@ class IssueService { if (!issue) { return { success: false, - message: 'Issue not found' + message: "Issue not found", }; } @@ -360,13 +412,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", }; } } @@ -379,7 +431,7 @@ class IssueService { if (!issue) { return { success: false, - message: 'Issue not found' + message: "Issue not found", }; } @@ -388,28 +440,52 @@ class IssueService { if (!technician) { return { success: false, - message: 'Technician not found' + message: "Technician not found", }; } await issue.update({ technician_id: technicianId, - technician_assigned_at: new Date() + technician_assigned_at: new Date(), }); - // Fetch full updated issue to return to the client - const updatedIssueResult = await this.getIssueById(issueId, true); + // 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"], + }, + ], + }, + ], + }); return { success: true, - data: updatedIssueResult.data, - message: 'Technician assigned successfully' + data: updatedIssue, + message: "Technician assigned successfully", }; } catch (error) { return { success: false, error: error.message, - message: 'Failed to assign technician' + message: "Failed to assign technician", }; } } @@ -422,7 +498,7 @@ class IssueService { if (!issue) { return { success: false, - message: 'Issue not found' + message: "Issue not found", }; } @@ -431,28 +507,52 @@ class IssueService { if (!executive) { return { success: false, - message: 'Maintenance Executive not found' + message: "Maintenance Executive not found", }; } await issue.update({ maintenance_executive_id: executiveId, - maintenance_executive_assigned_at: new Date() + maintenance_executive_assigned_at: new Date(), }); - // Fetch full updated issue to return to the client - const updatedIssueResult = await this.getIssueById(issueId, true); + // 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"], + }, + ], + }, + ], + }); return { success: true, - data: updatedIssueResult.data, - message: 'Maintenance Executive assigned successfully' + data: updatedIssue, + 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", }; } } @@ -465,7 +565,7 @@ class IssueService { if (!issue) { return { success: false, - message: 'Issue not found' + message: "Issue not found", }; } @@ -474,28 +574,52 @@ class IssueService { if (!thirdParty) { return { success: false, - message: 'Third Party not found' + message: "Third Party not found", }; } await issue.update({ third_party_id: thirdPartyId, - third_party_assigned_at: new Date() + third_party_assigned_at: new Date(), }); - // Fetch full updated issue to return to the client - const updatedIssueResult = await this.getIssueById(issueId, true); + // 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", + ], + }, + ], + }); return { success: true, - data: updatedIssueResult.data, - message: 'Third Party assigned successfully' + data: updatedIssue, + 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", }; } } @@ -508,7 +632,7 @@ class IssueService { if (!issue) { return { success: false, - message: 'Issue not found' + message: "Issue not found", }; } @@ -519,17 +643,17 @@ class IssueService { return { success: true, - data: updatedIssueResult.data, - message: 'Issue status updated successfully' + data: issue, + 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(); 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; } }