create-frontify-backend 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +59 -0
- package/package.json +31 -0
- package/templates/backend/my-backend-template/.env.example +13 -0
- package/templates/backend/my-backend-template/README.md +177 -0
- package/templates/backend/my-backend-template/eslint.config.js +30 -0
- package/templates/backend/my-backend-template/eslint.config.mjs +35 -0
- package/templates/backend/my-backend-template/package-lock.json +4738 -0
- package/templates/backend/my-backend-template/package.json +43 -0
- package/templates/backend/my-backend-template/server.js +11 -0
- package/templates/backend/my-backend-template/src/app.js +57 -0
- package/templates/backend/my-backend-template/src/config/config.js +24 -0
- package/templates/backend/my-backend-template/src/config/db.js +18 -0
- package/templates/backend/my-backend-template/src/config/email.config.js +19 -0
- package/templates/backend/my-backend-template/src/constants/constants.js +5 -0
- package/templates/backend/my-backend-template/src/controllers/auth.controller.js +196 -0
- package/templates/backend/my-backend-template/src/dao/user.dao.js +86 -0
- package/templates/backend/my-backend-template/src/loggers/morgan.logger.js +11 -0
- package/templates/backend/my-backend-template/src/loggers/winston.logger.js +64 -0
- package/templates/backend/my-backend-template/src/middlewares/auth.middleware.js +64 -0
- package/templates/backend/my-backend-template/src/middlewares/error.handler.js +28 -0
- package/templates/backend/my-backend-template/src/middlewares/rateLimiter.middleware.js +31 -0
- package/templates/backend/my-backend-template/src/middlewares/validator.middleware.js +66 -0
- package/templates/backend/my-backend-template/src/models/user.model.js +54 -0
- package/templates/backend/my-backend-template/src/routes/auth.routes.js +68 -0
- package/templates/backend/my-backend-template/src/services/userServices.js +179 -0
- package/templates/backend/my-backend-template/src/utils/appError.js +17 -0
- package/templates/backend/my-backend-template/src/utils/asyncHandler.js +5 -0
- package/templates/backend/my-backend-template/src/utils/password.js +20 -0
- package/templates/backend/my-backend-template/src/utils/sendEmail.js +17 -0
- package/templates/backend/my-backend-template/src/validators/auth.validator.js +99 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { validationResult } from "express-validator";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validate request using express-validator rules
|
|
5
|
+
*/
|
|
6
|
+
export const validate = (rules = []) => {
|
|
7
|
+
return async (req, res, next) => {
|
|
8
|
+
// Run all validation rules
|
|
9
|
+
for (const rule of rules) {
|
|
10
|
+
await rule.run(req);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Collect validation errors
|
|
14
|
+
const errors = validationResult(req);
|
|
15
|
+
|
|
16
|
+
// If no errors, continue
|
|
17
|
+
if (errors.isEmpty()) {
|
|
18
|
+
return next();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Format errors: field -> message
|
|
22
|
+
const formattedErrors = {};
|
|
23
|
+
for (const err of errors.array()) {
|
|
24
|
+
if (!formattedErrors[err.path]) {
|
|
25
|
+
formattedErrors[err.path] = err.msg;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return res.status(422).json({
|
|
30
|
+
success: false,
|
|
31
|
+
message: "Validation failed",
|
|
32
|
+
errors: formattedErrors,
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Custom Validators
|
|
39
|
+
* -----------------
|
|
40
|
+
* Only validators actually used in the package
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
export const customValidators = {
|
|
44
|
+
/**
|
|
45
|
+
* Strong password validator
|
|
46
|
+
* - Min 8 chars
|
|
47
|
+
* - At least 1 uppercase
|
|
48
|
+
* - At least 1 lowercase
|
|
49
|
+
* - At least 1 number
|
|
50
|
+
*/
|
|
51
|
+
strongPassword(value) {
|
|
52
|
+
if (!value) return false;
|
|
53
|
+
|
|
54
|
+
const hasUppercase = /[A-Z]/.test(value);
|
|
55
|
+
const hasLowercase = /[a-z]/.test(value);
|
|
56
|
+
const hasNumber = /\d/.test(value);
|
|
57
|
+
const hasMinLength = value.length >= 8;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
hasUppercase &&
|
|
61
|
+
hasLowercase &&
|
|
62
|
+
hasNumber &&
|
|
63
|
+
hasMinLength
|
|
64
|
+
);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* User Schema
|
|
5
|
+
* Handles authentication & authorization
|
|
6
|
+
*/
|
|
7
|
+
const userSchema = new mongoose.Schema(
|
|
8
|
+
{
|
|
9
|
+
username: {
|
|
10
|
+
type: String,
|
|
11
|
+
required: true,
|
|
12
|
+
unique: true,
|
|
13
|
+
trim: true,
|
|
14
|
+
minlength: 3,
|
|
15
|
+
maxlength: 30,
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
email: {
|
|
19
|
+
type: String,
|
|
20
|
+
required: true,
|
|
21
|
+
unique: true,
|
|
22
|
+
lowercase: true,
|
|
23
|
+
trim: true,
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
name: {
|
|
27
|
+
type: String,
|
|
28
|
+
required: true,
|
|
29
|
+
trim: true,
|
|
30
|
+
maxlength: 50,
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
password: {
|
|
34
|
+
type: String,
|
|
35
|
+
required: true,
|
|
36
|
+
minlength: 8,
|
|
37
|
+
select: false, // 🚨 very important (won't return by default)
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
role: {
|
|
41
|
+
type: String,
|
|
42
|
+
enum: ["user", "admin"],
|
|
43
|
+
default: "user",
|
|
44
|
+
select: false,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
timestamps: true, // createdAt & updatedAt
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const User = mongoose.model("User", userSchema);
|
|
53
|
+
|
|
54
|
+
export default User;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
|
|
3
|
+
import authController from "../controllers/auth.controller.js";
|
|
4
|
+
import { protect } from "../middlewares/auth.middleware.js";
|
|
5
|
+
import { authRateLimiter} from '../middlewares/rateLimiter.middleware.js'
|
|
6
|
+
import { validate } from "../middlewares/validator.middleware.js";
|
|
7
|
+
import {
|
|
8
|
+
registerValidator,
|
|
9
|
+
loginValidator,
|
|
10
|
+
verifyEmailValidator,
|
|
11
|
+
verifyEmailTokenValidator
|
|
12
|
+
} from '../validators/auth.validator.js'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
const router = express.Router();
|
|
16
|
+
|
|
17
|
+
router.use(authRateLimiter)
|
|
18
|
+
|
|
19
|
+
// Register user
|
|
20
|
+
router.post(
|
|
21
|
+
"/register",
|
|
22
|
+
validate(registerValidator),
|
|
23
|
+
authController.register
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Login user
|
|
27
|
+
router.post(
|
|
28
|
+
"/login",
|
|
29
|
+
validate(loginValidator),
|
|
30
|
+
authController.login
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Logout user (stateless)
|
|
34
|
+
router.post(
|
|
35
|
+
"/logout",
|
|
36
|
+
protect,
|
|
37
|
+
authController.logout
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Get current user
|
|
41
|
+
router.get(
|
|
42
|
+
"/get-me",
|
|
43
|
+
protect,
|
|
44
|
+
authController.getMe
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Generate new access token
|
|
48
|
+
router.post(
|
|
49
|
+
"/refresh-token",
|
|
50
|
+
authController.refreshAccessToken
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
// Send verification email
|
|
55
|
+
router.post(
|
|
56
|
+
"/verify-email",
|
|
57
|
+
validate(verifyEmailValidator),
|
|
58
|
+
authController.verifyEmail
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Verify email using token
|
|
62
|
+
router.get(
|
|
63
|
+
"/verify-email",
|
|
64
|
+
validate(verifyEmailTokenValidator),
|
|
65
|
+
authController.verifyEmailToken
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
export default router;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import jwt from "jsonwebtoken";
|
|
2
|
+
|
|
3
|
+
import userDAO from "../dao/user.dao.js";
|
|
4
|
+
import appError from '../utils/appError.js';
|
|
5
|
+
import config from "../config/config.js";
|
|
6
|
+
import {hashPassword, comparePassword} from '../utils/password.js'
|
|
7
|
+
import * as CONSTANT from "../constants/constants.js";
|
|
8
|
+
import logger from "../loggers/winston.logger.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* User Service
|
|
12
|
+
* ------------
|
|
13
|
+
* Business logic only
|
|
14
|
+
* Function-based
|
|
15
|
+
* Beginner + Production ready
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const userService = {
|
|
19
|
+
/**
|
|
20
|
+
* Register new user
|
|
21
|
+
*/
|
|
22
|
+
async register(userData) {
|
|
23
|
+
const emailExists = await userDAO.findByEmail(userData.email);
|
|
24
|
+
if (emailExists) {
|
|
25
|
+
throw appError("Email already registered", 400);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const usernameExists = await userDAO.findByUsername(userData.username);
|
|
29
|
+
if (usernameExists) {
|
|
30
|
+
throw appError("Username already taken", 400);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const hashedPassword = await hashPassword(userData.password);
|
|
34
|
+
|
|
35
|
+
const user = await userDAO.create({
|
|
36
|
+
username: userData.username,
|
|
37
|
+
email: userData.email,
|
|
38
|
+
password: hashedPassword,
|
|
39
|
+
name: userData.name || "",
|
|
40
|
+
role: userData.role || "user"
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
user.password = undefined;
|
|
44
|
+
return user;
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Login user
|
|
49
|
+
*/
|
|
50
|
+
async login(email, password) {
|
|
51
|
+
const user = await userDAO.findByEmail(email);
|
|
52
|
+
|
|
53
|
+
if (!user) {
|
|
54
|
+
throw appError("Invalid email or password", 401);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const isPasswordValid = await comparePassword(password, user.password);
|
|
58
|
+
if (!isPasswordValid) {
|
|
59
|
+
throw appError("Invalid email or password", 401);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
user.password = undefined;
|
|
64
|
+
return user;
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get logged-in user
|
|
69
|
+
*/
|
|
70
|
+
async getMe(userId) {
|
|
71
|
+
return userDAO.findById(userId);
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Generate access token
|
|
76
|
+
*/
|
|
77
|
+
generateAccessToken({ userId, username, email }) {
|
|
78
|
+
return jwt.sign(
|
|
79
|
+
{ id: userId, username, email },
|
|
80
|
+
config.JWT_SECRET,
|
|
81
|
+
{ expiresIn: CONSTANT.ACCESS_TOKEN_EXPIRATION }
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate refresh token (stateless)
|
|
87
|
+
*/
|
|
88
|
+
generateRefreshToken({ userId }) {
|
|
89
|
+
return jwt.sign(
|
|
90
|
+
{ id: userId },
|
|
91
|
+
config.JWT_SECRET,
|
|
92
|
+
{ expiresIn: CONSTANT.REFRESH_TOKEN_EXPIRATION }
|
|
93
|
+
);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Verify refresh token (stateless)
|
|
98
|
+
*/
|
|
99
|
+
verifyRefreshToken(refreshToken) {
|
|
100
|
+
try {
|
|
101
|
+
const decoded = jwt.verify(refreshToken, config.JWT_SECRET);
|
|
102
|
+
return decoded;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
logger.warn("Refresh token verification failed", {
|
|
105
|
+
error: error.message,
|
|
106
|
+
});
|
|
107
|
+
throw appError("Invalid or expired refresh token", 401);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Reset password
|
|
113
|
+
*/
|
|
114
|
+
async resetPassword(userId, newPassword) {
|
|
115
|
+
const user = await userDAO.findById(userId);
|
|
116
|
+
if (!user) throw appError("User not found", 404);
|
|
117
|
+
|
|
118
|
+
user.password = newPassword;
|
|
119
|
+
await user.save(); // triggers hashing
|
|
120
|
+
return true;
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Update user profile
|
|
125
|
+
*/
|
|
126
|
+
async updateProfile(userId, updates) {
|
|
127
|
+
if (updates.password) {
|
|
128
|
+
throw appError("Password update not allowed here", 400);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return userDAO.updateById(userId, updates);
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Generate email verification token
|
|
136
|
+
*/
|
|
137
|
+
async generateVerificationToken(email) {
|
|
138
|
+
const user = await userDAO.findByEmail(email);
|
|
139
|
+
if (!user) throw appError("User not found", 404);
|
|
140
|
+
|
|
141
|
+
const token = jwt.sign(
|
|
142
|
+
{ id: user._id },
|
|
143
|
+
config.JWT_SECRET,
|
|
144
|
+
{ expiresIn: CONSTANT.VERIFICATION_TOKEN_EXPIRATION }
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
user.emailVerificationToken = token;
|
|
148
|
+
await user.save();
|
|
149
|
+
|
|
150
|
+
return token;
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Verify email
|
|
155
|
+
*/
|
|
156
|
+
async verifyEmail(token) {
|
|
157
|
+
try {
|
|
158
|
+
const decoded = jwt.verify(token, config.JWT_SECRET);
|
|
159
|
+
const user = await userDAO.findById(decoded.id);
|
|
160
|
+
|
|
161
|
+
if (!user || user.emailVerificationToken !== token) {
|
|
162
|
+
throw appError("Invalid verification token", 401);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
user.isEmailVerified = true;
|
|
166
|
+
user.emailVerificationToken = undefined;
|
|
167
|
+
await user.save();
|
|
168
|
+
|
|
169
|
+
return user;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
logger.warn("Email verification failed", {
|
|
172
|
+
error: error.message,
|
|
173
|
+
});
|
|
174
|
+
throw appError("Invalid or expired verification token", 401);
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export default Object.freeze(userService);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory function to create operational errors
|
|
3
|
+
* CI/CD & ESLint safe version
|
|
4
|
+
*/
|
|
5
|
+
const appError = (message, statusCode = 500) => {
|
|
6
|
+
const error = new Error(message);
|
|
7
|
+
|
|
8
|
+
error.statusCode = statusCode;
|
|
9
|
+
error.status = statusCode >= 400 && statusCode < 500 ? 'fail' : 'error';
|
|
10
|
+
error.isOperational = true;
|
|
11
|
+
|
|
12
|
+
Error.captureStackTrace(error, appError);
|
|
13
|
+
|
|
14
|
+
return error;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default appError;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import bcrypt from 'bcrypt';
|
|
2
|
+
import appError from './appError.js';
|
|
3
|
+
|
|
4
|
+
const saltRounds = 12;
|
|
5
|
+
|
|
6
|
+
export const hashPassword = (password) => {
|
|
7
|
+
try {
|
|
8
|
+
return bcrypt.hashSync(password, saltRounds);
|
|
9
|
+
} catch {
|
|
10
|
+
throw appError('Could not hash password', 500);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const comparePassword = (password, hashedPassword) => {
|
|
15
|
+
try {
|
|
16
|
+
return bcrypt.compareSync(password, hashedPassword);
|
|
17
|
+
} catch {
|
|
18
|
+
throw appError('Could not compare password', 500);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createTransporter } from '../config/email.config.js';
|
|
2
|
+
import config from '../config/config.js';
|
|
3
|
+
|
|
4
|
+
export const sendVerificationEmail = async (to, verificationLink) => {
|
|
5
|
+
const transporter = await createTransporter();
|
|
6
|
+
const mailOptions = {
|
|
7
|
+
from: `"Backend Starter" <${config.GMAIL_USER}>`,
|
|
8
|
+
to,
|
|
9
|
+
subject: 'Verify your email',
|
|
10
|
+
html: `
|
|
11
|
+
<p>Click the link below to verify your email:</p>
|
|
12
|
+
<a href="${verificationLink}">${verificationLink}</a>
|
|
13
|
+
`,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return transporter.sendMail(mailOptions);
|
|
17
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { body, query } from 'express-validator';
|
|
2
|
+
import { customValidators } from '../middlewares/validator.middleware.js';
|
|
3
|
+
|
|
4
|
+
export const registerValidator = [
|
|
5
|
+
body('username')
|
|
6
|
+
.optional({ nullable: true })
|
|
7
|
+
.isLength({ min: 3, max: 30 })
|
|
8
|
+
.withMessage('Username must be between 3 and 30 characters')
|
|
9
|
+
.matches(/^[a-zA-Z0-9_]+$/)
|
|
10
|
+
.withMessage('Username can only contain letters, numbers, and underscores')
|
|
11
|
+
.trim()
|
|
12
|
+
.escape(),
|
|
13
|
+
body('name')
|
|
14
|
+
.optional()
|
|
15
|
+
.isLength({ min: 2, max: 50 })
|
|
16
|
+
.withMessage('Name must be between 2 and 50 characters')
|
|
17
|
+
.trim(),
|
|
18
|
+
body('email')
|
|
19
|
+
.notEmpty()
|
|
20
|
+
.withMessage('Email is required')
|
|
21
|
+
.isEmail()
|
|
22
|
+
.withMessage('Please provide a valid email')
|
|
23
|
+
.normalizeEmail(),
|
|
24
|
+
body('password')
|
|
25
|
+
.notEmpty()
|
|
26
|
+
.withMessage('Password is required')
|
|
27
|
+
.isLength({ min: 8 })
|
|
28
|
+
.withMessage('Password must be at least 8 characters')
|
|
29
|
+
.custom((value) => {
|
|
30
|
+
if (!customValidators.strongPassword(value)) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character'
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}),
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export const loginValidator = [
|
|
40
|
+
body('email')
|
|
41
|
+
.notEmpty()
|
|
42
|
+
.withMessage('Email is required')
|
|
43
|
+
.isEmail()
|
|
44
|
+
.withMessage('Please provide a valid email')
|
|
45
|
+
.normalizeEmail(),
|
|
46
|
+
body('password')
|
|
47
|
+
.notEmpty()
|
|
48
|
+
.withMessage('Password is required')
|
|
49
|
+
.isLength({ min: 8 })
|
|
50
|
+
.withMessage('Password must be at least 8 characters'),
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export const verifyEmailValidator = [
|
|
54
|
+
body('email')
|
|
55
|
+
.notEmpty()
|
|
56
|
+
.withMessage('Email is required')
|
|
57
|
+
.isEmail()
|
|
58
|
+
.withMessage('Please provide a valid email')
|
|
59
|
+
.normalizeEmail(),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
export const verifyEmailTokenValidator = [
|
|
63
|
+
query('token').notEmpty().withMessage('Token is required'),
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
export const forgotPasswordValidator = [
|
|
67
|
+
body('email')
|
|
68
|
+
.notEmpty()
|
|
69
|
+
.withMessage('Email is required')
|
|
70
|
+
.isEmail()
|
|
71
|
+
.withMessage('Please provide a valid email')
|
|
72
|
+
.normalizeEmail(),
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
export const resetPasswordValidator = [
|
|
76
|
+
body('token').notEmpty().withMessage('Token is required'),
|
|
77
|
+
body('password')
|
|
78
|
+
.notEmpty()
|
|
79
|
+
.withMessage('Password is required')
|
|
80
|
+
.isLength({ min: 8 })
|
|
81
|
+
.withMessage('Password must be at least 8 characters')
|
|
82
|
+
.custom((value) => {
|
|
83
|
+
if (!customValidators.strongPassword(value)) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}),
|
|
90
|
+
body('confirmPassword')
|
|
91
|
+
.notEmpty()
|
|
92
|
+
.withMessage('Confirm password is required')
|
|
93
|
+
.custom((value, { req }) => {
|
|
94
|
+
if (value !== req.body.password) {
|
|
95
|
+
throw new Error('Passwords do not match');
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}),
|
|
99
|
+
];
|