From 208f6a2a57550efbc8ce05f318aa92ed94f94702 Mon Sep 17 00:00:00 2001 From: RTS-Dev Date: Thu, 2 Oct 2025 11:58:12 +0530 Subject: [PATCH] Refactor project structure to enhance modularity and improve user management - Updated main entry point in package.json to server.js. - Reorganized server setup into src/app.js for better structure. - Removed deprecated middleware and routes, consolidating user authentication logic. - Introduced user service and repository layers for cleaner data handling. - Added global error handling and response utilities for consistent API responses. - Implemented user routes with authentication middleware for secure access. --- middleware/cors.js | 12 -- middleware/index.js | 2 - package.json | 2 +- routes/auth.js | 41 ------- routes/index.js | 2 - routes/users.js | 20 ---- server.js | 16 +-- src/app.js | 36 ++++++ lib/prisma.js => src/config/db.js | 4 +- src/controllers/userController.js | 107 ++++++++++++++++++ {middleware => src/middlewares}/auth.js | 19 +++- src/middlewares/cors.js | 15 +++ src/middlewares/errorHandler.js | 55 ++++++++++ src/middlewares/index.js | 3 + src/models/index.js | 2 + src/repositories/userRepository.js | 92 ++++++++++++++++ src/routes/authRoutes.js | 9 ++ src/routes/index.js | 2 + src/routes/userRoutes.js | 25 +++++ src/services/userService.js | 139 ++++++++++++++++++++++++ src/utils/response.js | 32 ++++++ 21 files changed, 535 insertions(+), 100 deletions(-) delete mode 100644 middleware/cors.js delete mode 100644 middleware/index.js delete mode 100644 routes/auth.js delete mode 100644 routes/index.js delete mode 100644 routes/users.js create mode 100644 src/app.js rename lib/prisma.js => src/config/db.js (72%) create mode 100644 src/controllers/userController.js rename {middleware => src/middlewares}/auth.js (51%) create mode 100644 src/middlewares/cors.js create mode 100644 src/middlewares/errorHandler.js create mode 100644 src/middlewares/index.js create mode 100644 src/models/index.js create mode 100644 src/repositories/userRepository.js create mode 100644 src/routes/authRoutes.js create mode 100644 src/routes/index.js create mode 100644 src/routes/userRoutes.js create mode 100644 src/services/userService.js create mode 100644 src/utils/response.js diff --git a/middleware/cors.js b/middleware/cors.js deleted file mode 100644 index 091de44..0000000 --- a/middleware/cors.js +++ /dev/null @@ -1,12 +0,0 @@ -import cors from 'cors'; - -function buildCors() { - const origins = process.env.CORS_ORIGIN - ? process.env.CORS_ORIGIN.split(',').map(s => s.trim()) - : '*'; - return cors({ origin: origins, credentials: true }); -} - -export { buildCors }; - - diff --git a/middleware/index.js b/middleware/index.js deleted file mode 100644 index dd14b20..0000000 --- a/middleware/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { authRequired } from './auth.js'; -export { buildCors } from './cors.js'; \ No newline at end of file diff --git a/package.json b/package.json index e279407..691364a 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "license": "ISC", "author": "RTS-Dev", "type": "module", - "main": "index.js", + "main": "server.js", "scripts": { "dev": "nodemon server.js", "start": "node server.js", diff --git a/routes/auth.js b/routes/auth.js deleted file mode 100644 index baf907d..0000000 --- a/routes/auth.js +++ /dev/null @@ -1,41 +0,0 @@ -import express from 'express'; -import bcrypt from 'bcryptjs'; -import jwt from 'jsonwebtoken'; -import { prisma } from '../lib/prisma.js'; - -const authRoutes = express.Router(); - -authRoutes.post('/login', async (req, res) => { - try { - const { email, password } = req.body; - if (!email || !password) { - return res.status(400).json({ error: 'Email and password are required' }); - } - - const user = await prisma.super_admin.findUnique({ where: { email } }); - if (!user || !user.password) { - return res.status(401).json({ error: 'Invalid credentials' }); - } - - const isPasswordValid = await bcrypt.compare(password, user.password); - if (!isPasswordValid) { - return res.status(401).json({ error: 'Invalid credentials' }); - } - - const token = jwt.sign( - { sub: user.id, email: user.email, role: user.role || 'ADMIN' }, - process.env.JWT_SECRET || 'dev-secret', - { expiresIn: process.env.JWT_EXPIRES_IN || '1h' } - ); - - // Exclude sensitive fields - const { password: _, ...userData } = user; - - return res.json({ user: userData, token }); - } catch (error) { - console.error('Login error:', error); - return res.status(500).json({ error: 'Internal server error' }); - } -}); - -export default authRoutes; diff --git a/routes/index.js b/routes/index.js deleted file mode 100644 index a145b5a..0000000 --- a/routes/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as authRoutes } from './auth.js'; -export { default as userRoutes } from './users.js'; \ No newline at end of file diff --git a/routes/users.js b/routes/users.js deleted file mode 100644 index 6a2b28e..0000000 --- a/routes/users.js +++ /dev/null @@ -1,20 +0,0 @@ -import express from 'express'; -import { prisma } from '../lib/prisma.js'; -import { authRequired } from '../middleware/auth.js'; - -const userRoutes = express.Router(); - -userRoutes.get('/', authRequired, async (req, res) => { - const super_admin = await prisma.super_admin.findMany(); - const response = super_admin.map((user) => ({ - id: user.id, - email: user.email, - name: user.name, - username: user.username, - role: user.role, - createdAt: user.createdAt, - })); - res.json(response); -}); - -export default userRoutes; \ No newline at end of file diff --git a/server.js b/server.js index 7c56bb5..0f34430 100644 --- a/server.js +++ b/server.js @@ -1,20 +1,6 @@ -import 'dotenv/config'; -import express from 'express'; -import { authRoutes, userRoutes } from './routes/index.js'; -import { buildCors, authRequired } from './middleware/index.js'; +import app from './src/app.js'; const PORT = process.env.PORT || 3000; -const app = express(); - -app.use(express.json()); -app.use(express.urlencoded({ extended: true })); -app.use(buildCors()); - -app.use('/', authRoutes); - -app.use(authRequired); - -app.use('/users', userRoutes); app.listen(PORT, () => { console.log(`API listening on http://localhost:${PORT}`); diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..03b9edb --- /dev/null +++ b/src/app.js @@ -0,0 +1,36 @@ +import 'dotenv/config'; +import express from 'express'; +import { authRoutes, userRoutes } from './routes/index.js'; +import { buildCors, errorHandler } from './middlewares/index.js'; +import { errorResponse, successResponse } from './utils/response.js'; + +/** + * Express application setup + */ +const app = express(); + +// Middleware +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(buildCors()); + +// Auth Routes +app.use('/', authRoutes); + +// User Routes +app.use('/users', userRoutes); + +// Test endpoint +app.get('/test', (req, res) => { + return successResponse(res, 'Server is running'); +}); + +// 404 handler +app.use((req, res) => { + return errorResponse(res, 'Route not found', 404); +}); + +// Global error handler +app.use(errorHandler); + +export default app; diff --git a/lib/prisma.js b/src/config/db.js similarity index 72% rename from lib/prisma.js rename to src/config/db.js index 961e862..54040ad 100644 --- a/lib/prisma.js +++ b/src/config/db.js @@ -1,4 +1,4 @@ -import { PrismaClient } from '../generated/prisma/index.js'; +import { PrismaClient } from '../../generated/prisma/index.js'; let prisma; @@ -11,4 +11,4 @@ if (process.env.NODE_ENV === 'production') { prisma = globalThis.__prisma; } -export { prisma }; \ No newline at end of file +export { prisma }; diff --git a/src/controllers/userController.js b/src/controllers/userController.js new file mode 100644 index 0000000..41af051 --- /dev/null +++ b/src/controllers/userController.js @@ -0,0 +1,107 @@ +import * as UserService from '../services/userService.js'; +import { successResponse, errorResponse } from '../utils/response.js'; + +/** + * User controller - handles HTTP requests and responses + */ + +/** + * Login user + */ +export async function login(req, res) { + try { + const { email, password } = req.body; + + if (!email || !password) { + return errorResponse(res, 'Email and password are required', 400); + } + + const result = await UserService.authenticateUser(email, password); + return successResponse(res, result, 'Login successful'); + } catch (error) { + console.error('Login error:', error); + return errorResponse(res, error.message, 401); + } +} + +/** + * Get all users + */ +export async function getAllUsers(req, res) { + try { + const users = await UserService.getAllUsers(); + return successResponse(res, users, 'Users retrieved successfully'); + } catch (error) { + console.error('Get users error:', error); + return errorResponse(res, 'Failed to retrieve users', 500); + } +} + +/** + * Get user by ID + */ +export async function getUserById(req, res) { + try { + const { id } = req.params; + const user = await UserService.getUserById(id); + return successResponse(res, user, 'User retrieved successfully'); + } catch (error) { + console.error('Get user error:', error); + if (error.message === 'User not found') { + return errorResponse(res, error.message, 404); + } + return errorResponse(res, 'Failed to retrieve user', 500); + } +} + +/** + * Create new user + */ +export async function createUser(req, res) { + try { + const userData = req.body; + const user = await UserService.createUser(userData); + return successResponse(res, user, 'User created successfully', 201); + } catch (error) { + console.error('Create user error:', error); + if (error.message.includes('already exists') || error.message.includes('already taken')) { + return errorResponse(res, error.message, 409); + } + return errorResponse(res, 'Failed to create user', 500); + } +} + +/** + * Update user + */ +export async function updateUser(req, res) { + try { + const { id } = req.params; + const userData = req.body; + const user = await UserService.updateUser(id, userData); + return successResponse(res, user, 'User updated successfully'); + } catch (error) { + console.error('Update user error:', error); + if (error.message === 'User not found') { + return errorResponse(res, error.message, 404); + } + return errorResponse(res, 'Failed to update user', 500); + } +} + +/** + * Delete user + */ +export async function deleteUser(req, res) { + try { + const { id } = req.params; + await UserService.deleteUser(id); + return successResponse(res, null, 'User deleted successfully'); + } catch (error) { + console.error('Delete user error:', error); + if (error.message === 'User not found') { + return errorResponse(res, error.message, 404); + } + return errorResponse(res, 'Failed to delete user', 500); + } +} diff --git a/middleware/auth.js b/src/middlewares/auth.js similarity index 51% rename from middleware/auth.js rename to src/middlewares/auth.js index 602fa18..3af4773 100644 --- a/middleware/auth.js +++ b/src/middlewares/auth.js @@ -1,18 +1,27 @@ import jwt from 'jsonwebtoken'; -function authRequired(req, res, next) { +/** + * Authentication middleware + */ +export function authRequired(req, res, next) { const auth = req.headers.authorization || ''; const token = auth.startsWith('Bearer ') ? auth.slice(7) : null; + if (!token) { - return res.status(401).json({ error: 'Unauthorized' }); + return res.status(401).json({ + success: false, + error: 'Unauthorized - No token provided' + }); } + try { const payload = jwt.verify(token, process.env.JWT_SECRET || 'dev-secret'); req.user = payload; return next(); } catch (err) { - return res.status(401).json({ error: 'Invalid token' }); + return res.status(401).json({ + success: false, + error: 'Invalid token' + }); } } - -export { authRequired }; \ No newline at end of file diff --git a/src/middlewares/cors.js b/src/middlewares/cors.js new file mode 100644 index 0000000..a7ce039 --- /dev/null +++ b/src/middlewares/cors.js @@ -0,0 +1,15 @@ +import cors from 'cors'; + +/** + * CORS middleware configuration + */ +export function buildCors() { + const origins = process.env.CORS_ORIGIN + ? process.env.CORS_ORIGIN.split(',').map(s => s.trim()) + : '*'; + + return cors({ + origin: origins, + credentials: true + }); +} diff --git a/src/middlewares/errorHandler.js b/src/middlewares/errorHandler.js new file mode 100644 index 0000000..b084784 --- /dev/null +++ b/src/middlewares/errorHandler.js @@ -0,0 +1,55 @@ +/** + * Global error handling middleware + */ +export function errorHandler(err, req, res, next) { + console.error('Error:', err); + + // Default error + let error = { + success: false, + message: 'Internal server error', + statusCode: 500 + }; + + // Mongoose validation error + if (err.name === 'ValidationError') { + const messages = Object.values(err.errors).map(val => val.message); + error.message = messages.join(', '); + error.statusCode = 400; + } + + // Mongoose duplicate key error + if (err.code === 11000) { + const field = Object.keys(err.keyValue)[0]; + error.message = `${field} already exists`; + error.statusCode = 400; + } + + // JWT errors + if (err.name === 'JsonWebTokenError') { + error.message = 'Invalid token'; + error.statusCode = 401; + } + + if (err.name === 'TokenExpiredError') { + error.message = 'Token expired'; + error.statusCode = 401; + } + + // Prisma errors + if (err.code === 'P2002') { + error.message = 'Duplicate entry'; + error.statusCode = 400; + } + + if (err.code === 'P2025') { + error.message = 'Record not found'; + error.statusCode = 404; + } + + res.status(error.statusCode).json({ + success: false, + message: error.message, + ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) + }); +} diff --git a/src/middlewares/index.js b/src/middlewares/index.js new file mode 100644 index 0000000..9704503 --- /dev/null +++ b/src/middlewares/index.js @@ -0,0 +1,3 @@ +export { authRequired } from './auth.js'; +export { buildCors } from './cors.js'; +export { errorHandler } from './errorHandler.js'; diff --git a/src/models/index.js b/src/models/index.js new file mode 100644 index 0000000..567fb76 --- /dev/null +++ b/src/models/index.js @@ -0,0 +1,2 @@ +// Re-export Prisma client for use throughout the application +export { prisma } from '../config/db.js'; diff --git a/src/repositories/userRepository.js b/src/repositories/userRepository.js new file mode 100644 index 0000000..d173e21 --- /dev/null +++ b/src/repositories/userRepository.js @@ -0,0 +1,92 @@ +import { prisma } from '../models/index.js'; + +/** + * User repository - handles direct Prisma queries for user operations + */ + +/** + * Find user by email + */ +export async function findByEmail(email) { + return await prisma.super_admin.findUnique({ + where: { email } + }); +} + +/** + * Find user by ID + */ +export async function findById(id) { + return await prisma.super_admin.findUnique({ + where: { id } + }); +} + +/** + * Find user by username + */ +export async function findByUsername(username) { + return await prisma.super_admin.findUnique({ + where: { username } + }); +} + +/** + * Get all users + */ +export async function findAll() { + return await prisma.super_admin.findMany({ + select: { + id: true, + email: true, + name: true, + username: true, + role: true, + createdAt: true + } + }); +} + +/** + * Create new user + */ +export async function create(userData) { + return await prisma.super_admin.create({ + data: userData, + select: { + id: true, + email: true, + name: true, + username: true, + role: true, + createdAt: true + } + }); +} + +/** + * Update user by ID + */ +export async function updateById(id, userData) { + return await prisma.super_admin.update({ + where: { id }, + data: userData, + select: { + id: true, + email: true, + name: true, + username: true, + role: true, + createdAt: true + } + }); +} + +/** + * Delete user by ID + */ +export async function deleteById(id) { + return await prisma.super_admin.delete({ + where: { id } + }); +} diff --git a/src/routes/authRoutes.js b/src/routes/authRoutes.js new file mode 100644 index 0000000..aeac859 --- /dev/null +++ b/src/routes/authRoutes.js @@ -0,0 +1,9 @@ +import express from 'express'; +import { login } from '../controllers/userController.js'; + +const router = express.Router(); + +// POST /login +router.post('/login', login); + +export default router; diff --git a/src/routes/index.js b/src/routes/index.js new file mode 100644 index 0000000..728911d --- /dev/null +++ b/src/routes/index.js @@ -0,0 +1,2 @@ +export { default as authRoutes } from './authRoutes.js'; +export { default as userRoutes } from './userRoutes.js'; diff --git a/src/routes/userRoutes.js b/src/routes/userRoutes.js new file mode 100644 index 0000000..0116b3c --- /dev/null +++ b/src/routes/userRoutes.js @@ -0,0 +1,25 @@ +import express from 'express'; +import { getAllUsers, getUserById, createUser, updateUser, deleteUser } from '../controllers/userController.js'; +import { authRequired } from '../middlewares/auth.js'; + +const router = express.Router(); + +// Apply authentication middleware to all routes +router.use(authRequired); + +// GET /users - Get all users +router.get('/', getAllUsers); + +// GET /users/:id - Get user by ID +router.get('/:id', getUserById); + +// POST /users - Create new user +router.post('/', createUser); + +// PUT /users/:id - Update user +router.put('/:id', updateUser); + +// DELETE /users/:id - Delete user +router.delete('/:id', deleteUser); + +export default router; diff --git a/src/services/userService.js b/src/services/userService.js new file mode 100644 index 0000000..c0e50b6 --- /dev/null +++ b/src/services/userService.js @@ -0,0 +1,139 @@ +import bcrypt from 'bcryptjs'; +import jwt from 'jsonwebtoken'; +import * as UserRepository from '../repositories/userRepository.js'; + +/** + * User service - handles business logic for user operations + */ + +/** + * Authenticate user login + */ +export async function authenticateUser(email, password) { + try { + const user = await UserRepository.findByEmail(email); + + if (!user || !user.password) { + throw new Error('Invalid credentials'); + } + + const isPasswordValid = await bcrypt.compare(password, user.password); + if (!isPasswordValid) { + throw new Error('Invalid credentials'); + } + + // Generate JWT token + const token = jwt.sign( + { + sub: user.id, + email: user.email, + role: user.role || 'ADMIN' + }, + process.env.JWT_SECRET || 'dev-secret', + { expiresIn: process.env.JWT_EXPIRES_IN || '1h' } + ); + + // Return user data without password + const { password: _, ...userData } = user; + + return { + user: userData, + token + }; + } catch (error) { + throw error; + } +} + +/** + * Get all users + */ +export async function getAllUsers() { + try { + return await UserRepository.findAll(); + } catch (error) { + throw error; + } +} + +/** + * Get user by ID + */ +export async function getUserById(id) { + try { + const user = await UserRepository.findById(id); + if (!user) { + throw new Error('User not found'); + } + return user; + } catch (error) { + throw error; + } +} + +/** + * Create new user + */ +export async function createUser(userData) { + try { + // Check if user already exists + const existingUser = await UserRepository.findByEmail(userData.email); + if (existingUser) { + throw new Error('User with this email already exists'); + } + + // Check if username already exists + if (userData.username) { + const existingUsername = await UserRepository.findByUsername(userData.username); + if (existingUsername) { + throw new Error('Username already taken'); + } + } + + // Hash password if provided + if (userData.password) { + userData.password = await bcrypt.hash(userData.password, 12); + } + + return await UserRepository.create(userData); + } catch (error) { + throw error; + } +} + +/** + * Update user + */ +export async function updateUser(id, userData) { + try { + const user = await UserRepository.findById(id); + if (!user) { + throw new Error('User not found'); + } + + // Hash password if provided + if (userData.password) { + userData.password = await bcrypt.hash(userData.password, 12); + } + + return await UserRepository.updateById(id, userData); + } catch (error) { + throw error; + } +} + +/** + * Delete user + */ +export async function deleteUser(id) { + try { + const user = await UserRepository.findById(id); + if (!user) { + throw new Error('User not found'); + } + + return await UserRepository.deleteById(id); + } catch (error) { + throw error; + } +} diff --git a/src/utils/response.js b/src/utils/response.js new file mode 100644 index 0000000..1a4d3b5 --- /dev/null +++ b/src/utils/response.js @@ -0,0 +1,32 @@ +/** + * Standard response helper functions + */ + +export const successResponse = (res, data, message = 'Success', statusCode = 200) => { + return res.status(statusCode).json({ + success: true, + message, + data + }); +}; + +export const errorResponse = (res, message = 'Internal server error', statusCode = 500, error = null) => { + const response = { + success: false, + message + }; + + if (error && process.env.NODE_ENV === 'development') { + response.error = error; + } + + return res.status(statusCode).json(response); +}; + +export const validationErrorResponse = (res, errors) => { + return res.status(400).json({ + success: false, + message: 'Validation failed', + errors + }); +};