├── .env.example ├── .gitignore ├── .vscode └── launch.json ├── Middleware ├── authMiddleware.js └── validationMiddleware.js ├── README.md ├── controller ├── authController.js ├── forgetPasswordController.js └── userController.js ├── jest.config.js ├── package-lock.json ├── package.json ├── prisma ├── migrations │ ├── 20250424115155_init │ │ └── migration.sql │ ├── 20250424122723_add_password_field │ │ └── migration.sql │ ├── 20250424123109_add_password_field_without_defult │ │ └── migration.sql │ ├── 20250425092049_add_avatar_to_user │ │ └── migration.sql │ ├── 20250425192630_add_password_reset_fields │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── router ├── authRouter.js ├── forgetPasswordRouter.js └── userRouter.js ├── server.js ├── tests └── userRouter.test.js ├── uploads └── userAvatar │ └── avatar.png └── utils ├── APIError.js ├── APIResponse.js └── sendMail.js /.env.example: -------------------------------------------------------------------------------- 1 | # Application 2 | NODE_ENV=development 3 | PORT=3000 4 | 5 | # Database configuration 6 | DATABASE_URL=mysql://user:password@localhost:3306/your_database_name 7 | 8 | # JWT configuration 9 | JWT_SECRET=your_jwt_secret_key_here 10 | 11 | # Email configuration (Gmail) 12 | EMAIL_HOST=smtp.gmail.com 13 | EMAIL_PORT=587 14 | EMAIL_USER=your_email@gmail.com 15 | EMAIL_PASSWORD=your_app_specific_password -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | uploads/* 4 | !uploads/userAvatar/ 5 | uploads/userAvatar/* 6 | !uploads/userAvatar/avatar.png 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "type": "node", 10 | "request": "launch", 11 | "name": "Launch Program", 12 | "skipFiles": [ 13 | "/**" 14 | ], 15 | "program": "${workspaceFolder}\\controller\\authController.js", 16 | "outFiles": [ 17 | "${workspaceFolder}/**/*.js" 18 | ] 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /Middleware/authMiddleware.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import { PrismaClient } from "@prisma/client"; 3 | import APIResponse from "../utils/APIResponse.js"; 4 | import APIError from "../utils/APIError.js"; 5 | 6 | const prisma = new PrismaClient(); 7 | 8 | const protect = async (req, res, next) => { 9 | let token; 10 | if ( 11 | req.headers.authorization && 12 | req.headers.authorization.startsWith("Bearer") 13 | ) { 14 | try { 15 | token = req.headers.authorization.split(" ")[1]; 16 | const decoded = jwt.verify(token, process.env.JWT_SECRET); 17 | req.user = await prisma.user.findUnique({ 18 | where: { id: decoded.id }, 19 | select: { id: true, name: true, email: true, role: true }, 20 | }); 21 | next(); 22 | } catch (error) { 23 | throw new APIError("Not authorized, token failed", 401); 24 | } 25 | } else { 26 | throw new APIError("Not authorized, no token", 401); 27 | } 28 | }; 29 | 30 | const authorizeRoles = (...roles) => { 31 | return (req, res, next) => { 32 | if (!roles.includes(req.user.role)) { 33 | throw new APIError("Access forbidden: insufficient role", 403); 34 | } else { 35 | next(); 36 | } 37 | }; 38 | }; 39 | 40 | export { protect, authorizeRoles }; -------------------------------------------------------------------------------- /Middleware/validationMiddleware.js: -------------------------------------------------------------------------------- 1 | import { validationResult } from 'express-validator'; 2 | import APIError from '../utils/APIError.js'; 3 | 4 | const validate = (req, res, next) => { 5 | const errors = validationResult(req); 6 | if (!errors.isEmpty()) { 7 | const errorMessages = errors.array().map(error => error.msg); 8 | return next(new APIError(errorMessages.join(', '), 400)); 9 | } 10 | next(); 11 | }; 12 | 13 | export { validate }; 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Express.js Template with MySQL and Authentication 2 | 3 | A robust, production-ready Express.js starter template with MySQL integration using Prisma ORM, featuring comprehensive user authentication, email functionality, and file upload capabilities. 4 | 5 | ## ✨ Features 6 | 7 | - **🔐 User Authentication** 8 | 9 | - JWT-based secure authentication 10 | - Role-based authorization (User/Admin) 11 | - Protected routes with middleware 12 | - Session management with cookies 13 | 14 | - **📧 Email Integration** 15 | 16 | - Gmail SMTP integration 17 | - Beautiful HTML email templates 18 | - Secure password reset workflow 19 | - Transactional email support 20 | 21 | - **📁 File Management** 22 | 23 | - User avatar uploads 24 | - Secure file storage 25 | - Automatic file cleanup 26 | - Default avatar support 27 | 28 | - **🛡️ Security** 29 | 30 | - Role-based access control (RBAC) 31 | - Input validation 32 | - Password hashing with bcrypt 33 | - Secure reset code generation 34 | 35 | - **💾 Database** 36 | 37 | - MySQL with Prisma ORM 38 | - Automated migrations 39 | - Type-safe database queries 40 | - Efficient connection pooling 41 | 42 | - **⚙️ Development Tools** 43 | - Jest testing setup 44 | - Environment configuration 45 | - API error handling 46 | - Standardized response format 47 | 48 | ## 📂 Project Structure 49 | 50 | ``` 51 | ├── config/ # Configuration files 52 | ├── controller/ 53 | │ ├── authController.js # Authentication logic 54 | │ ├── userController.js # User management 55 | │ └── forgetPasswordController.js 56 | ├── Middleware/ 57 | │ ├── authMiddleware.js # JWT authentication 58 | │ └── validationMiddleware.js 59 | ├── prisma/ 60 | │ ├── schema.prisma # Database schema 61 | │ └── migrations/ # Database migrations 62 | ├── router/ 63 | │ ├── authRouter.js 64 | │ ├── userRouter.js 65 | │ └── forgetPasswordRouter.js 66 | ├── utils/ 67 | │ ├── APIError.js # Error handling 68 | │ ├── APIResponse.js # Response formatting 69 | │ └── sendMail.js # Email utility 70 | ├── uploads/ 71 | │ └── userAvatar/ # User avatar storage 72 | └── tests/ # Jest test files 73 | ``` 74 | 75 | ## 🔧 Prerequisites 76 | 77 | - Node.js (v14 or higher) 78 | - MySQL Server (v5.7 or higher) 79 | - npm or yarn 80 | 81 | ## 🚀 Getting Started 82 | 83 | ### 1. Clone the repository 84 | 85 | ```bash 86 | git clone 87 | cd ExpreesTemplateWithSQL 88 | ``` 89 | 90 | ### 2. Install dependencies 91 | 92 | ```bash 93 | npm install 94 | ``` 95 | 96 | ### 3. Configure environment variables 97 | 98 | Create a `.env` file in the root directory: 99 | 100 | ```env 101 | # Application 102 | NODE_ENV=development 103 | PORT=3000 104 | 105 | # Database 106 | DATABASE_URL="mysql://username:password@localhost:3306/your_database" 107 | 108 | # Authentication 109 | JWT_SECRET=your_jwt_secret_key 110 | 111 | # Email Configuration (Gmail) 112 | EMAIL_HOST=smtp.gmail.com 113 | EMAIL_PORT=587 114 | EMAIL_USER=your_email@gmail.com 115 | EMAIL_PASSWORD=your_app_specific_password 116 | ``` 117 | 118 | ### 4. Set up the database 119 | 120 | ```bash 121 | # Create database tables 122 | npx prisma migrate dev 123 | ``` 124 | 125 | ### 5. Start the development server 126 | 127 | ```bash 128 | npm run dev 129 | ``` 130 | 131 | Your API will be available at `http://localhost:3000/api/v1` 132 | 133 | ## 📡 API Endpoints 134 | 135 | ### Authentication 136 | 137 | | Method | Endpoint | Description | Auth Required | 138 | | ------ | ----------------------- | ------------------- | ------------- | 139 | | POST | `/api/v1/auth/register` | Register a new user | No | 140 | | POST | `/api/v1/auth/login` | User login | No | 141 | 142 | ### Password Management 143 | 144 | | Method | Endpoint | Description | Auth Required | 145 | | ------ | ---------------------------------------- | ---------------------- | ------------- | 146 | | POST | `/api/v1/forget-password` | Request password reset | No | 147 | | POST | `/api/v1/forget-password/verify-code` | Verify reset code | No | 148 | | POST | `/api/v1/forget-password/reset-password` | Set new password | No | 149 | 150 | ### User Management 151 | 152 | | Method | Endpoint | Description | Auth Required | 153 | | ------ | -------------------------- | ------------------ | ------------- | 154 | | GET | `/api/v1/users` | Get all users | Admin | 155 | | GET | `/api/v1/users/:id` | Get user by ID | Yes\* | 156 | | PUT | `/api/v1/users/:id` | Update user | Yes\* | 157 | | DELETE | `/api/v1/users/:id` | Delete user | Yes\* | 158 | | PATCH | `/api/v1/users/:id/avatar` | Update user avatar | Yes\* | 159 | 160 | _\* Users can only access their own resources unless they have admin privileges_ 161 | 162 | ## 🔒 Authentication Flow 163 | 164 | ### Registration 165 | 166 | 1. Client sends POST request to `/api/v1/auth/register` with: 167 | 168 | ```json 169 | { 170 | "name": "John Doe", 171 | "email": "john@example.com", 172 | "password": "securePassword123", 173 | "passwordConfirm": "securePassword123" 174 | } 175 | ``` 176 | 177 | 2. Server validates input, hashes password, and creates user 178 | 3. Server returns JWT token and user data 179 | 180 | ### Login 181 | 182 | 1. Client sends POST request to `/api/v1/auth/login` with: 183 | 184 | ```json 185 | { 186 | "email": "john@example.com", 187 | "password": "securePassword123" 188 | } 189 | ``` 190 | 191 | 2. Server validates credentials and issues JWT token 192 | 3. Token is returned in response and set as HTTP-only cookie 193 | 194 | ### Password Reset Flow 195 | 196 | 1. Request reset code: 197 | 198 | ```json 199 | POST /api/v1/forget-password 200 | { 201 | "email": "user@example.com" 202 | } 203 | ``` 204 | 205 | 2. Verify reset code: 206 | 207 | ```json 208 | POST /api/v1/forget-password/verify-code 209 | { 210 | "email": "user@example.com", 211 | "resetCode": "123456" 212 | } 213 | ``` 214 | 215 | 3. Set new password: 216 | ```json 217 | POST /api/v1/forget-password/reset-password 218 | { 219 | "email": "user@example.com", 220 | "newPassword": "newSecurePassword123" 221 | } 222 | ``` 223 | 224 | ## 📧 Email Setup 225 | 226 | ### Using Gmail 227 | 228 | 1. Enable 2-Step Verification in your Google Account 229 | 2. Generate an App Password: 230 | - Go to Google Account Settings → Security → 2-Step Verification → App passwords 231 | - Select "Mail" and "Other" (name it "Express App") 232 | - Use the 16-character password in your `.env` file 233 | 234 | ### HTML Email Templates 235 | 236 | The system includes pre-built HTML email templates for: 237 | - Password reset codes 238 | 239 | 240 | ## 🗃️ Database Schema 241 | 242 | ### User Model 243 | 244 | ```prisma 245 | model User { 246 | id Int @id @default(autoincrement()) 247 | createdAt DateTime @default(now()) 248 | email String @unique 249 | name String? 250 | role Role @default(USER) 251 | password String 252 | avatar String @default("avatar.png") 253 | passwordResetCode String? 254 | passwordResetExpires DateTime? 255 | passwordResetVerify Boolean @default(false) 256 | } 257 | 258 | enum Role { 259 | USER 260 | ADMIN 261 | } 262 | ``` 263 | 264 | ## 🧪 Testing 265 | 266 | ```bash 267 | # Run all tests 268 | npm test 269 | 270 | # Run specific test suite 271 | npm test -- --testPathPattern=auth 272 | ``` 273 | 274 | ## 🤝 Contributing 275 | 276 | 1. Fork the repository 277 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 278 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 279 | 4. Push to the branch (`git push origin feature/amazing-feature`) 280 | 5. Open a Pull Request 281 | 282 | ## 📄 License 283 | 284 | This project is licensed under the MIT License - see the LICENSE file for details. 285 | -------------------------------------------------------------------------------- /controller/authController.js: -------------------------------------------------------------------------------- 1 | //generateToken 2 | //loginUser 3 | //registerUser 4 | import bcrypt from "bcryptjs"; 5 | import { PrismaClient } from "@prisma/client"; 6 | import jwt from "jsonwebtoken"; 7 | const prisma = new PrismaClient(); 8 | import asyncHandler from "express-async-handler"; 9 | import APIResponse from "../utils/APIResponse.js"; 10 | import APIError from "../utils/APIError.js"; 11 | 12 | const generateToken = (id, role) => { 13 | return jwt.sign({ id, role }, process.env.JWT_SECRET, { expiresIn: "30d" }); 14 | }; 15 | 16 | const loginUser = asyncHandler(async (req, res) => { 17 | const { email, password } = req.body; 18 | const user = await prisma.user.findUnique({ where: { email } }); 19 | if (user && (await bcrypt.compare(password, user.password))) { 20 | const responseData = { 21 | id: user.id, 22 | name: user.name, 23 | email: user.email, 24 | role: user.role, 25 | token: generateToken(user.id, user.role), 26 | }; 27 | return APIResponse.send( 28 | res, 29 | APIResponse.success(responseData, 200, "Login successful") 30 | ); 31 | } else { 32 | throw new APIError("Invalid credentials", 401); 33 | } 34 | }); 35 | 36 | const registerUser = asyncHandler(async (req, res) => { 37 | const { name, email, password, role } = req.body; 38 | const userExists = await prisma.user.findFirst({ 39 | where: { email }, 40 | }); 41 | if (userExists) { 42 | throw new APIError("User already exists", 400); 43 | } 44 | const hashedPassword = await bcrypt.hash(password, 12); 45 | const user = await prisma.user.create({ 46 | data: { name, email, password: hashedPassword, role }, 47 | }); 48 | const responseData = { 49 | id: user.id, 50 | name: user.name, 51 | email: user.email, 52 | role: user.role, 53 | token: generateToken(user.id, user.role), 54 | }; 55 | APIResponse.send( 56 | res, 57 | APIResponse.success(responseData, 201, "User registered successfully") 58 | ); 59 | }); 60 | 61 | export { loginUser, registerUser }; 62 | -------------------------------------------------------------------------------- /controller/forgetPasswordController.js: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | import asyncHandler from "express-async-handler"; 3 | import APIResponse from "../utils/APIResponse.js"; 4 | import APIError from "../utils/APIError.js"; 5 | import { PrismaClient } from "@prisma/client"; 6 | import sendEmail from "../utils/sendMail.js"; 7 | import bcrypt from "bcryptjs"; 8 | 9 | const prisma = new PrismaClient(); 10 | 11 | const forgetPassword = asyncHandler(async (req, res) => { 12 | // get user by email 13 | const { email } = req.body; 14 | const user = await prisma.user.findFirst({ where: { email } }); 15 | if (!user) throw new APIError("User not found", 404); 16 | 17 | //if user exist generate reset 6 digit code 18 | const resetCode = Math.floor(100000 + Math.random() * 900000).toString(); 19 | const expires = new Date(Date.now() + 10 * 60 * 1000); 20 | const hasedResetCode = crypto 21 | .createHash("sha256") 22 | .update(resetCode) 23 | .digest("hex"); 24 | 25 | await prisma.user.update({ 26 | where: { email }, 27 | data: { 28 | passwordResetCode: hasedResetCode, 29 | passwordResetExpires: expires, 30 | passwordResetVerify: false, 31 | }, 32 | }); 33 | 34 | const htmlMessage = ` 35 | 36 | 37 | 38 | 75 | 76 | 77 |
78 |
79 |

Password Reset Request

80 |
81 |
82 |

Hello,

83 |

We received a request to reset your password. Here is your password reset code:

84 |
${resetCode}
85 |

Please use this code to reset your password. This code will expire in 10 minutes.

86 |

If you didn't request a password reset, please ignore this email or contact support if you have concerns.

87 |
88 |
89 | 90 | 91 | `; 92 | 93 | // send email 94 | await sendEmail({ 95 | to: email, 96 | subject: "Password Reset Code - Valid for 10 minutes", 97 | message: `Your password reset code is: ${resetCode}. Valid for 10 minutes.`, 98 | html: htmlMessage, 99 | }); 100 | 101 | APIResponse.send( 102 | res, 103 | APIResponse.success(null, 200, "Reset code sent to your email") 104 | ); 105 | }); 106 | 107 | const verifyResetCode = asyncHandler(async (req, res) => { 108 | const { email, resetCode } = req.body; 109 | 110 | // Hash the reset code to compare with stored hash 111 | const hashedResetCode = crypto 112 | .createHash("sha256") 113 | .update(resetCode) 114 | .digest("hex"); 115 | 116 | const user = await prisma.user.findFirst({ 117 | where: { 118 | email, 119 | passwordResetCode: hashedResetCode, 120 | passwordResetExpires: { 121 | gt: new Date(), 122 | }, 123 | }, 124 | }); 125 | 126 | if (!user) { 127 | throw new APIError("Invalid or expired reset code", 400); 128 | } 129 | 130 | // Mark the reset code as verified 131 | await prisma.user.update({ 132 | where: { email }, 133 | data: { passwordResetVerify: true }, 134 | }); 135 | 136 | APIResponse.send( 137 | res, 138 | APIResponse.success(null, 200, "Reset code verified successfully") 139 | ); 140 | }); 141 | 142 | const resetPassword = asyncHandler(async (req, res) => { 143 | const { email, newPassword } = req.body; 144 | 145 | const user = await prisma.user.findFirst({ 146 | where: { 147 | email, 148 | passwordResetVerify: true, 149 | }, 150 | }); 151 | 152 | if (!user) { 153 | throw new APIError("Reset code not verified or expired", 400); 154 | } 155 | 156 | // Hash the new password 157 | const hashedPassword = await bcrypt.hash(newPassword, 12); 158 | 159 | // Update password and clear reset fields 160 | await prisma.user.update({ 161 | where: { email }, 162 | data: { 163 | password: hashedPassword, 164 | passwordResetCode: null, 165 | passwordResetExpires: null, 166 | passwordResetVerify: false, 167 | }, 168 | }); 169 | 170 | APIResponse.send( 171 | res, 172 | APIResponse.success(null, 200, "Password reset successfully") 173 | ); 174 | }); 175 | 176 | export { forgetPassword, verifyResetCode, resetPassword }; 177 | -------------------------------------------------------------------------------- /controller/userController.js: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import asyncHandler from "express-async-handler"; 3 | import bcrypt from "bcryptjs"; 4 | const prisma = new PrismaClient(); 5 | import multer from "multer"; 6 | import { v4 as uuid } from "uuid"; 7 | import APIResponse from "../utils/APIResponse.js"; 8 | import APIError from "../utils/APIError.js"; 9 | 10 | const storage = multer.diskStorage({ 11 | destination: function (req, file, cb) { 12 | cb(null, "uploads/userAvatar"); 13 | }, 14 | filename: function (req, file, cb) { 15 | const ext = file.mimetype.split("/")[1]; 16 | const name = `avatar-${uuid()}-${Date.now()}.${ext}`; 17 | cb(null, name); 18 | }, 19 | }); 20 | const multerFilter = function (_, file, cb) { 21 | if (file.mimetype.startsWith("image")) { 22 | cb(null, true); 23 | } else { 24 | cb(new APIError("Only image files are allowed", 400), false); 25 | } 26 | }; 27 | const upload = multer({ storage: storage, fileFilter: multerFilter }); 28 | const imageUpload = upload.single("avatar"); 29 | 30 | const createUser = asyncHandler(async (req, res) => { 31 | const { name, email, role, password } = req.body; 32 | const userExists = await prisma.user.findFirst({ 33 | where: { email }, 34 | }); 35 | if (userExists) { 36 | throw new APIError("User with this email already exists", 400); 37 | } 38 | 39 | const hashedPassword = await bcrypt.hash(password, 12); 40 | const avatar = req.file ? req.file.filename : "avatar.png"; 41 | 42 | const user = await prisma.user.create({ 43 | data: { 44 | name, 45 | email, 46 | role, 47 | password: hashedPassword, 48 | avatar, 49 | }, 50 | }); 51 | 52 | const { password: _, ...userWithoutPassword } = user; 53 | APIResponse.send( 54 | res, 55 | APIResponse.success(userWithoutPassword, 201, "User created successfully") 56 | ); 57 | }); 58 | 59 | const getAllUsers = asyncHandler(async (req, res) => { 60 | const users = await prisma.user.findMany({ 61 | select: { 62 | id: true, 63 | name: true, 64 | email: true, 65 | role: true, 66 | avatar: true, 67 | password: false, 68 | }, 69 | }); 70 | APIResponse.send( 71 | res, 72 | APIResponse.success(users, 200, "Users retrieved successfully") 73 | ); 74 | }); 75 | 76 | const getUserById = asyncHandler(async (req, res) => { 77 | const user = await prisma.user.findFirst({ 78 | where: { id: Number(req.params.id) }, 79 | select: { 80 | id: true, 81 | name: true, 82 | email: true, 83 | role: true, 84 | avatar: true, 85 | password: false, 86 | }, 87 | }); 88 | 89 | if (!user) { 90 | throw new APIError("User not found", 404); 91 | } 92 | 93 | APIResponse.send( 94 | res, 95 | APIResponse.success(user, 200, "User retrieved successfully") 96 | ); 97 | }); 98 | 99 | const updateUser = asyncHandler(async (req, res) => { 100 | const userExists = await prisma.user.findUnique({ 101 | where: { id: Number(req.params.id) }, 102 | }); 103 | 104 | if (!userExists) { 105 | throw new APIError("User not found", 404); 106 | } 107 | 108 | const data = { ...req.body }; 109 | if (data.password) data.password = await bcrypt.hash(data.password, 12); 110 | if (req.file) { 111 | data.avatar = req.file.filename; 112 | } 113 | 114 | try { 115 | const user = await prisma.user.update({ 116 | where: { id: Number(req.params.id) }, 117 | data, 118 | select: { 119 | id: true, 120 | name: true, 121 | email: true, 122 | role: true, 123 | avatar: true, 124 | password: false, 125 | }, 126 | }); 127 | APIResponse.send( 128 | res, 129 | APIResponse.success(user, 200, "User updated successfully") 130 | ); 131 | } catch (error) { 132 | throw new APIError("Error updating user", 400); 133 | } 134 | }); 135 | 136 | const deleteUser = asyncHandler(async (req, res) => { 137 | const userExists = await prisma.user.findUnique({ 138 | where: { id: Number(req.params.id) }, 139 | }); 140 | 141 | if (!userExists) { 142 | throw new APIError("User not found", 404); 143 | } 144 | 145 | try { 146 | const user = await prisma.user.delete({ 147 | where: { id: Number(req.params.id) }, 148 | }); 149 | APIResponse.send( 150 | res, 151 | APIResponse.success({ message: "User deleted successfully" }, 200) 152 | ); 153 | } catch (error) { 154 | throw new APIError("Error deleting user", 400); 155 | } 156 | }); 157 | 158 | export { 159 | imageUpload, 160 | createUser, 161 | getAllUsers, 162 | getUserById, 163 | updateUser, 164 | deleteUser, 165 | }; 166 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | testEnvironment: "node", 3 | transform: {}, 4 | moduleNameMapper: { 5 | "^(\\.{1,2}/.*)\\.js$": "$1", 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "scripts": { 4 | "dev": "nodemon server.js", 5 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" 6 | }, 7 | "dependencies": { 8 | "@prisma/client": "^6.6.0", 9 | "bcryptjs": "^3.0.2", 10 | "dotenv": "^16.5.0", 11 | "express": "^5.1.0", 12 | "express-async-handler": "^1.2.0", 13 | "express-validator": "^7.2.1", 14 | "jsonwebtoken": "^9.0.2", 15 | "morgan": "^1.10.0", 16 | "multer": "^1.4.5-lts.2", 17 | "nodemailer": "^6.10.1", 18 | "nodemon": "^3.1.10", 19 | "uuid": "^11.1.0" 20 | }, 21 | "devDependencies": { 22 | "jest": "^29.7.0", 23 | "supertest": "^7.1.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /prisma/migrations/20250424115155_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `User` ( 3 | `id` INTEGER NOT NULL AUTO_INCREMENT, 4 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 5 | `email` VARCHAR(191) NOT NULL, 6 | `name` VARCHAR(191) NULL, 7 | `role` ENUM('USER', 'ADMIN') NOT NULL DEFAULT 'USER', 8 | 9 | UNIQUE INDEX `User_email_key`(`email`), 10 | PRIMARY KEY (`id`) 11 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 12 | -------------------------------------------------------------------------------- /prisma/migrations/20250424122723_add_password_field/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `user` ADD COLUMN `password` VARCHAR(191) NOT NULL DEFAULT 'default123'; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250424123109_add_password_field_without_defult/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `user` ALTER COLUMN `password` DROP DEFAULT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250425092049_add_avatar_to_user/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `user` ADD COLUMN `avatar` VARCHAR(191) NOT NULL DEFAULT 'avatar.png'; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250425192630_add_password_reset_fields/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `user` ADD COLUMN `passwordResetCode` VARCHAR(191) NULL, 3 | ADD COLUMN `passwordResetExpires` DATETIME(3) NULL, 4 | ADD COLUMN `passwordResetVerify` BOOLEAN NOT NULL DEFAULT false; 5 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "mysql" 4 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "mysql" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | output = "../node_modules/.prisma/client" 9 | } 10 | 11 | model User { 12 | id Int @id @default(autoincrement()) 13 | createdAt DateTime @default(now()) 14 | email String @unique 15 | name String? 16 | role Role @default(USER) 17 | password String 18 | avatar String @default("avatar.png") 19 | passwordResetCode String? 20 | passwordResetExpires DateTime? 21 | passwordResetVerify Boolean @default(false) 22 | } 23 | 24 | enum Role { 25 | USER 26 | ADMIN 27 | } 28 | -------------------------------------------------------------------------------- /router/authRouter.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { loginUser, registerUser } from "../controller/authController.js"; 3 | 4 | const router = express.Router(); 5 | 6 | router.post("/register", registerUser); 7 | router.post("/login", loginUser); 8 | 9 | export default router; -------------------------------------------------------------------------------- /router/forgetPasswordRouter.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | forgetPassword, 4 | verifyResetCode, 5 | resetPassword, 6 | } from "../controller/forgetPasswordController.js"; 7 | 8 | const router = express.Router(); 9 | 10 | router.post("/", forgetPassword); 11 | router.post("/verify-code", verifyResetCode); 12 | router.post("/reset-password", resetPassword); 13 | 14 | export default router; 15 | -------------------------------------------------------------------------------- /router/userRouter.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | createUser, 4 | getAllUsers, 5 | getUserById, 6 | updateUser, 7 | deleteUser, 8 | imageUpload, 9 | } from "../controller/userController.js"; 10 | import { protect, authorizeRoles } from "../Middleware/authMiddleware.js"; 11 | const router = express.Router(); 12 | 13 | // Protected routes (require authentication) 14 | router.get("/", protect, getAllUsers); 15 | router.get("/:id", protect, getUserById); 16 | 17 | // Admin-only routes 18 | router.post("/", protect, imageUpload, authorizeRoles("ADMIN"), createUser); 19 | router.put("/:id", protect, imageUpload, authorizeRoles("ADMIN"), updateUser); 20 | router.delete("/:id", protect, authorizeRoles("ADMIN"), deleteUser); 21 | 22 | export default router; 23 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import morgan from "morgan"; 3 | import dotenv from "dotenv"; 4 | import userRouter from "./router/userRouter.js"; 5 | import authRouter from "./router/authRouter.js"; 6 | import forgetPasswordRouter from "./router/forgetPasswordRouter.js"; 7 | import path from "path"; 8 | import { fileURLToPath } from "url"; 9 | dotenv.config(); 10 | const app = express(); 11 | 12 | app.use(express.json()); 13 | const __filename = fileURLToPath(import.meta.url); 14 | const __dirname = path.dirname(__filename); 15 | 16 | app.use(express.static(path.join(__dirname, "uploads"))); 17 | if (process.env.NODE_ENV === "development") { 18 | console.log("======================================"); 19 | console.log(`Mode: ${process.env.NODE_ENV}`); 20 | app.use(morgan("dev")); 21 | console.log("======================================"); 22 | } 23 | 24 | const port = process.env.PORT || 3000; 25 | app.listen(port, () => { 26 | console.log(`Server is running on port ${port}`); 27 | }); 28 | 29 | app.use("/api/v1/user", userRouter); 30 | app.use("/api/v1/auth", authRouter); 31 | app.use("/api/v1/forget-password", forgetPasswordRouter); 32 | 33 | // Import APIResponse utility 34 | import APIResponse from "./utils/APIResponse.js"; 35 | import APIError from "./utils/APIError.js"; 36 | 37 | // Handle 404s for API routes 38 | app.use("/api", (req, res) => { 39 | APIResponse.send( 40 | res, 41 | APIResponse.error(`Can't find ${req.originalUrl} on this server`, 404) 42 | ); 43 | }); 44 | 45 | // Handle 404s for other routes (including favicon.ico) 46 | app.all(/(.*)/, (req, res, next) => { 47 | if (req.accepts("json")) { 48 | APIResponse.send( 49 | res, 50 | APIResponse.error(`Can't find ${req.originalUrl} on this server`, 404) 51 | ); 52 | } else { 53 | res.status(404).send(`Can't find ${req.originalUrl} on this server`); 54 | } 55 | }); 56 | 57 | // Global error handler 58 | app.use((err, req, res, next) => { 59 | // Get status code and message 60 | const statusCode = err.statusCode || 500; 61 | const message = statusCode === 500 ? "Something went wrong!" : err.message; 62 | 63 | // Check if error is operational (expected) or programming error 64 | const isOperational = err.isOperational || false; 65 | 66 | // Log non-operational errors for debugging 67 | if (!isOperational) { 68 | console.error("ERROR 💥", err); 69 | } 70 | 71 | // Send response based on accepted content type 72 | if (req.accepts("json")) { 73 | // Use APIResponse for consistent formatting 74 | APIResponse.send( 75 | res, 76 | APIResponse.error( 77 | message, 78 | statusCode, 79 | isOperational ? null : { stack: err.stack } 80 | ) 81 | ); 82 | } else { 83 | res.status(statusCode).send(message); 84 | } 85 | }); 86 | -------------------------------------------------------------------------------- /tests/userRouter.test.js: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import express from "express"; 3 | import userRouter from "../router/userRouter.js"; 4 | import authRouter from "../router/authRouter.js"; 5 | import dotenv from "dotenv"; 6 | import path from "path"; 7 | import { fileURLToPath } from "url"; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | 12 | dotenv.config(); 13 | 14 | const app = express(); 15 | app.use(express.json()); 16 | app.use("/api/v1/user", userRouter); 17 | app.use("/api/v1/auth", authRouter); 18 | 19 | let token; 20 | let createdUserId; 21 | 22 | beforeAll(async () => { 23 | const adminUser = { 24 | name: "Admin User", 25 | email: `admin${Date.now()}@example.com`, 26 | password: "AdminPass123!", 27 | role: "ADMIN", 28 | }; 29 | 30 | const res = await request(app).post("/api/v1/auth/register").send(adminUser); 31 | token = res.body.data.token; 32 | }); 33 | 34 | describe("userRouter", () => { 35 | it("should create a new user with default avatar", async () => { 36 | const userData = { 37 | name: "John Doe", 38 | email: `john${Date.now()}@example.com`, 39 | role: "USER", 40 | password: "securepassword123", 41 | }; 42 | 43 | const res = await request(app) 44 | .post("/api/v1/user") 45 | .set("Authorization", `Bearer ${token}`) 46 | .send(userData); 47 | 48 | expect(res.status).toBe(201); 49 | expect(res.body.status).toBe("success"); 50 | expect(res.body.data).toHaveProperty("id"); 51 | expect(res.body.data.avatar).toBe("avatar.png"); 52 | createdUserId = res.body.data.id; 53 | }); 54 | 55 | it("should create a new user with custom avatar", async () => { 56 | const userData = { 57 | name: "Jane Doe", 58 | email: `jane${Date.now()}@example.com`, 59 | role: "USER", 60 | password: "securepassword123", 61 | }; 62 | 63 | const res = await request(app) 64 | .post("/api/v1/user") 65 | .set("Authorization", `Bearer ${token}`) 66 | .field("name", userData.name) 67 | .field("email", userData.email) 68 | .field("role", userData.role) 69 | .field("password", userData.password) 70 | .attach( 71 | "avatar", 72 | path.join(__dirname, "../uploads/userAvatar/avatar.png") 73 | ); 74 | 75 | expect(res.status).toBe(201); 76 | expect(res.body.status).toBe("success"); 77 | expect(res.body.data).toHaveProperty("id"); 78 | expect(res.body.data.avatar).toMatch(/^avatar-.*\.(jpeg|jpg|png)$/); 79 | }); 80 | 81 | it("should get all users with avatar fields", async () => { 82 | const res = await request(app) 83 | .get("/api/v1/user") 84 | .set("Authorization", `Bearer ${token}`); 85 | expect(res.status).toBe(200); 86 | expect(res.body.status).toBe("success"); 87 | expect(Array.isArray(res.body.data)).toBe(true); 88 | expect(res.body.data[0]).toHaveProperty("avatar"); 89 | }); 90 | 91 | it("should get a user by ID with avatar", async () => { 92 | const res = await request(app) 93 | .get(`/api/v1/user/${createdUserId}`) 94 | .set("Authorization", `Bearer ${token}`); 95 | expect(res.status).toBe(200); 96 | expect(res.body.status).toBe("success"); 97 | expect(res.body.data.id).toBe(createdUserId); 98 | expect(res.body.data).toHaveProperty("avatar"); 99 | }); 100 | 101 | it("should update a user with new avatar", async () => { 102 | const res = await request(app) 103 | .put(`/api/v1/user/${createdUserId}`) 104 | .set("Authorization", `Bearer ${token}`) 105 | .field("name", "John Updated") 106 | .field("email", `updated${Date.now()}@example.com`) 107 | .field("role", "ADMIN") 108 | .field("password", "newPassword123") 109 | .attach( 110 | "avatar", 111 | path.join(__dirname, "../uploads/userAvatar/avatar.png") 112 | ); 113 | 114 | expect(res.status).toBe(200); 115 | expect(res.body.status).toBe("success"); 116 | expect(res.body.data.name).toBe("John Updated"); 117 | expect(res.body.data.avatar).toMatch(/^avatar-.*\.(jpeg|jpg|png)$/); 118 | }); 119 | 120 | it("should delete a user", async () => { 121 | const res = await request(app) 122 | .delete(`/api/v1/user/${createdUserId}`) 123 | .set("Authorization", `Bearer ${token}`); 124 | expect(res.status).toBe(200); 125 | expect(res.body.status).toBe("success"); 126 | expect(res.body.data.message).toBe("User deleted successfully"); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /uploads/userAvatar/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrXrobot26/ExpreesTemplateWithSQL/79345d46a61173d09dc20d498df40b099cd81920/uploads/userAvatar/avatar.png -------------------------------------------------------------------------------- /utils/APIError.js: -------------------------------------------------------------------------------- 1 | class APIError extends Error { 2 | constructor(message, statusCode) { 3 | super(message); 4 | this.statusCode = statusCode; 5 | this.status = statusCode.toString().startsWith("4") ? "Fail" : "Error"; 6 | this.isOperational = true; 7 | } 8 | } 9 | 10 | export default APIError; 11 | -------------------------------------------------------------------------------- /utils/APIResponse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * APIResponse class for standardizing API responses following JSend specification 3 | * https://github.com/omniti-labs/jsend 4 | */ 5 | class APIResponse { 6 | /** 7 | * Creates a success response object 8 | * @param {any} data - The data to be returned 9 | * @param {number} statusCode - HTTP status code (default: 200) 10 | * @param {string} message - Optional success message 11 | * @returns {Object} JSend formatted success response 12 | */ 13 | static success(data, statusCode = 200, message = null) { 14 | const response = { 15 | status: "success", 16 | data, 17 | }; 18 | 19 | if (message) { 20 | response.message = message; 21 | } 22 | 23 | return { 24 | statusCode, 25 | body: response, 26 | }; 27 | } 28 | 29 | /** 30 | * Creates a fail response object (client error) 31 | * @param {string|Object} data - Error details or validation errors 32 | * @param {number} statusCode - HTTP status code (default: 400) 33 | * @returns {Object} JSend formatted fail response 34 | */ 35 | static fail(data, statusCode = 400) { 36 | return { 37 | statusCode, 38 | body: { 39 | status: "fail", 40 | data: typeof data === "string" ? { message: data } : data, 41 | }, 42 | }; 43 | } 44 | 45 | /** 46 | * Creates an error response object (server error) 47 | * @param {string} message - Error message 48 | * @param {number} statusCode - HTTP status code (default: 500) 49 | * @param {Object} data - Optional additional error data 50 | * @returns {Object} JSend formatted error response 51 | */ 52 | static error(message, statusCode = 500, data = null) { 53 | const response = { 54 | status: "error", 55 | message, 56 | }; 57 | 58 | if (data) { 59 | response.data = data; 60 | } 61 | 62 | return { 63 | statusCode, 64 | body: response, 65 | }; 66 | } 67 | 68 | /** 69 | * Sends the response to the client 70 | * @param {Object} res - Express response object 71 | * @param {Object} responseObj - Response object from success/fail/error methods 72 | */ 73 | static send(res, responseObj) { 74 | return res.status(responseObj.statusCode).json(responseObj.body); 75 | } 76 | } 77 | 78 | export default APIResponse; 79 | -------------------------------------------------------------------------------- /utils/sendMail.js: -------------------------------------------------------------------------------- 1 | import nodemailer from "nodemailer"; 2 | /** 3 | * @async 4 | * @example 5 | * await sendEmail({ 6 | * to: "recipient@example.com", 7 | * subject: "Hello", 8 | * message: "This is a test email", 9 | * html: "

This is HTML content

" 10 | * }); 11 | */ 12 | const sendEmail = async (options) => { 13 | const transporter = nodemailer.createTransport({ 14 | host: process.env.EMAIL_HOST, 15 | port: process.env.EMAIL_PORT, 16 | secure: false, 17 | auth: { 18 | user: process.env.EMAIL_USER, 19 | pass: process.env.EMAIL_PASSWORD, 20 | }, 21 | }); 22 | 23 | const mailOptions = { 24 | from: "Bessa 3mk aka(BESS GATES)", 25 | to: options.to, 26 | subject: options.subject, 27 | text: options.message, 28 | html: options.html || options.message, 29 | }; 30 | 31 | await transporter.sendMail(mailOptions); 32 | }; 33 | 34 | export default sendEmail; 35 | --------------------------------------------------------------------------------