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.
Files changed (30) hide show
  1. package/bin/index.js +59 -0
  2. package/package.json +31 -0
  3. package/templates/backend/my-backend-template/.env.example +13 -0
  4. package/templates/backend/my-backend-template/README.md +177 -0
  5. package/templates/backend/my-backend-template/eslint.config.js +30 -0
  6. package/templates/backend/my-backend-template/eslint.config.mjs +35 -0
  7. package/templates/backend/my-backend-template/package-lock.json +4738 -0
  8. package/templates/backend/my-backend-template/package.json +43 -0
  9. package/templates/backend/my-backend-template/server.js +11 -0
  10. package/templates/backend/my-backend-template/src/app.js +57 -0
  11. package/templates/backend/my-backend-template/src/config/config.js +24 -0
  12. package/templates/backend/my-backend-template/src/config/db.js +18 -0
  13. package/templates/backend/my-backend-template/src/config/email.config.js +19 -0
  14. package/templates/backend/my-backend-template/src/constants/constants.js +5 -0
  15. package/templates/backend/my-backend-template/src/controllers/auth.controller.js +196 -0
  16. package/templates/backend/my-backend-template/src/dao/user.dao.js +86 -0
  17. package/templates/backend/my-backend-template/src/loggers/morgan.logger.js +11 -0
  18. package/templates/backend/my-backend-template/src/loggers/winston.logger.js +64 -0
  19. package/templates/backend/my-backend-template/src/middlewares/auth.middleware.js +64 -0
  20. package/templates/backend/my-backend-template/src/middlewares/error.handler.js +28 -0
  21. package/templates/backend/my-backend-template/src/middlewares/rateLimiter.middleware.js +31 -0
  22. package/templates/backend/my-backend-template/src/middlewares/validator.middleware.js +66 -0
  23. package/templates/backend/my-backend-template/src/models/user.model.js +54 -0
  24. package/templates/backend/my-backend-template/src/routes/auth.routes.js +68 -0
  25. package/templates/backend/my-backend-template/src/services/userServices.js +179 -0
  26. package/templates/backend/my-backend-template/src/utils/appError.js +17 -0
  27. package/templates/backend/my-backend-template/src/utils/asyncHandler.js +5 -0
  28. package/templates/backend/my-backend-template/src/utils/password.js +20 -0
  29. package/templates/backend/my-backend-template/src/utils/sendEmail.js +17 -0
  30. 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,5 @@
1
+ const asyncHandler = (fn) => (req, res, next) => {
2
+ Promise.resolve(fn(req, res, next)).catch(next);
3
+ };
4
+
5
+ export default asyncHandler;
@@ -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
+ ];