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,43 @@
1
+ {
2
+ "name": "backend-project",
3
+ "private": true,
4
+ "version": "1.3.0",
5
+ "description": "",
6
+ "main": "server.js",
7
+ "scripts": {
8
+ "test": "echo \"Error: no test specified\" && exit 1",
9
+ "start": "node server.js",
10
+ "start-watch": "node --inspect --watch-path=./ ./server.js",
11
+ "dev": "nodemon server.js",
12
+ "lint": "eslint ."
13
+ },
14
+ "keywords": [],
15
+ "author": "",
16
+ "license": "ISC",
17
+ "type": "module",
18
+ "dependencies": {
19
+ "bcrypt": "^6.0.0",
20
+ "bcryptjs": "^3.0.3",
21
+ "cookie-parser": "^1.4.7",
22
+ "cors": "^2.8.5",
23
+ "dotenv": "^17.2.3",
24
+ "express": "^5.2.1",
25
+ "express-rate-limit": "^8.2.1",
26
+ "express-validator": "^7.3.1",
27
+ "helmet": "^8.1.0",
28
+ "jsonwebtoken": "^9.0.3",
29
+ "mongoose": "^9.0.1",
30
+ "morgan": "^1.10.1",
31
+ "nodemailer": "^7.0.11",
32
+ "winston": "^3.19.0"
33
+ },
34
+ "devDependencies": {
35
+ "@eslint/js": "^9.39.2",
36
+ "eslint": "^9.39.2",
37
+ "eslint-config-prettier": "^10.1.8",
38
+ "eslint-plugin-import": "^2.32.0",
39
+ "eslint-plugin-prettier": "^5.5.4",
40
+ "globals": "^16.5.0",
41
+ "nodemon": "^3.1.11"
42
+ }
43
+ }
@@ -0,0 +1,11 @@
1
+ import app from './src/app.js'
2
+ import logger from './src/loggers/winston.logger.js'
3
+ import config from './src/config/config.js';
4
+ import connectedToDatabase from './src/config/db.js';
5
+
6
+ connectedToDatabase();
7
+
8
+ app.listen(config.PORT, () => {
9
+ logger.info(`Server is running on port ${config.PORT}`);
10
+ logger.info(`Environment: ${config.NODE_ENV || 'development'}`);
11
+ });
@@ -0,0 +1,57 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import helmet from 'helmet';
4
+ import cookieParser from 'cookie-parser'
5
+ import { generalRateLimiter } from './middlewares/rateLimiter.middleware.js'
6
+ import morganLogger from './loggers/morgan.logger.js'
7
+ import config from './config/config.js'
8
+
9
+ const app = express();
10
+
11
+ app.use(
12
+ cors(
13
+ {
14
+ origin: config.FRONTEND_URL,
15
+ credentials: true,
16
+ }
17
+ ));
18
+ app.use(morganLogger);
19
+ app.use(helmet());
20
+ app.use(express.json({ limit: '100kb' }));
21
+ app.use(express.urlencoded({ extended: true, limit: '100kb' }));
22
+ app.use(cookieParser());
23
+
24
+ app.use(generalRateLimiter)
25
+
26
+ // import routes
27
+ import authRoutes from "./routes/auth.routes.js";
28
+ import errorHandler from './middlewares/error.handler.js'
29
+
30
+ // Auth Routes
31
+ app.use('/api/v1/auth', authRoutes)
32
+
33
+
34
+
35
+ // // Simple route for checking server status
36
+ app.get('/', (req, res) => {
37
+ res.status(200).json({
38
+ status: 'success',
39
+ message: 'Welcome to the Backend Starter',
40
+ environment: config.NODE_ENV,
41
+ documentation: 'docs.testdog.in',
42
+ });
43
+ });
44
+
45
+ // // 404 route handler for undefined routes
46
+ app.all('*name', (req, res, next) => {
47
+ const err = new Error(`Can't find ${req.originalUrl} on this server!`);
48
+ err.statusCode = 404;
49
+ err.status = 'fail';
50
+ next(err);
51
+ });
52
+
53
+ app.use(errorHandler)
54
+
55
+
56
+
57
+ export default app;
@@ -0,0 +1,24 @@
1
+ import dotenv from 'dotenv';
2
+
3
+ dotenv.config();
4
+
5
+ const _config = {
6
+ NODE_ENV: process.env.NODE_ENV || 'development',
7
+ PORT: process.env.PORT || 3000,
8
+ WEB_URL: process.env.WEB_URL || 'https://yourdomain.com',
9
+ FRONTEND_URL: process.env.FRONTEND_URL || 'http://localhost:5173',
10
+ DB_URL: process.env.DB_URL || 'mongodb://localhost:27017/mydatabase',
11
+ JWT_SECRET: process.env.JWT_SECRET,
12
+ GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
13
+ GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
14
+ GOOGLE_REFRESH_TOKEN: process.env.GOOGLE_REFRESH_TOKEN,
15
+ GOOGLE_CALLBACK_URL: process.env.GOOGLE_CALLBACK_URL,
16
+ GMAIL_USER: process.env.GMAIL_USER,
17
+ EMAIL_HOST: process.env.EMAIL_HOST,
18
+ EMAIL_PORT: process.env.EMAIL_PORT,
19
+ EMAIL_USER: process.env.EMAIL_USER,
20
+ EMAIL_PASSWORD: process.env.EMAIL_PASSWORD,
21
+ };
22
+
23
+
24
+ export default _config;
@@ -0,0 +1,18 @@
1
+ import mongoose from 'mongoose';
2
+ import logger from '../loggers/winston.logger.js'
3
+ import config from './config.js'
4
+
5
+ const connectedToDatabase = () => {
6
+ const dbUrl = config.DB_URL;
7
+
8
+ mongoose
9
+ .connect(dbUrl)
10
+ .then(() => {
11
+ logger.info('Connected to MongoDB')
12
+ })
13
+ .catch((err) => {
14
+ logger.error('Error connecting to MongoDB', err)
15
+ })
16
+ }
17
+
18
+ export default connectedToDatabase;
@@ -0,0 +1,19 @@
1
+ import nodemailer from "nodemailer";
2
+ import config from "./config.js";
3
+
4
+ /**
5
+ * Create Nodemailer transporter
6
+ * -----------------------------
7
+ * Uses simple SMTP (no Google OAuth)
8
+ */
9
+ export const createTransporter = () => {
10
+ return nodemailer.createTransport({
11
+ host: config.EMAIL_HOST, // e.g. smtp.gmail.com
12
+ port: config.EMAIL_PORT, // 587
13
+ secure: false, // true for 465, false for 587
14
+ auth: {
15
+ user: config.EMAIL_USER, // your email
16
+ pass: config.EMAIL_PASSWORD, // app password
17
+ },
18
+ });
19
+ };
@@ -0,0 +1,5 @@
1
+ export const ACCESS_TOKEN_EXPIRATION = '1h';
2
+ export const REFRESH_TOKEN_EXPIRATION = '30d';
3
+ export const FORGOT_PASSWORD_TOKEN_EXPIRATION = '15m';
4
+ export const VERIFICATION_TOKEN_EXPIRATION = '10m';
5
+ export const ISSUER = "Backend Starter";
@@ -0,0 +1,196 @@
1
+ import asyncHandler from "../utils/asyncHandler.js";
2
+ import config from "../config/config.js";
3
+ import userService from "../services/userServices.js";
4
+ import { sendVerificationEmail } from "../utils/sendEmail.js";
5
+ import appError from '../utils/appError.js';
6
+
7
+
8
+
9
+
10
+ const authController = {
11
+ /**
12
+ * Register user
13
+ */
14
+ register: asyncHandler(async (req, res) => {
15
+ const user = await userService.register(req.body);
16
+
17
+ const accessToken = userService.generateAccessToken({
18
+ userId: user._id,
19
+ username: user.username,
20
+ email: user.email,
21
+ });
22
+
23
+ const refreshToken = userService.generateRefreshToken({
24
+ userId: user._id,
25
+ });
26
+
27
+ res.cookie("refreshToken", refreshToken, {
28
+ httpOnly: true,
29
+ secure: true, // Use secure cookies in production
30
+ sameSite: 'none',
31
+ });
32
+
33
+ res.cookie("accessToken", accessToken, {
34
+ httpOnly: true,
35
+ secure: true, // Use secure cookies in production
36
+ sameSite: 'none',
37
+ });
38
+
39
+ res.status(201).json({
40
+ success: true,
41
+ data: user,
42
+ accessToken,
43
+ refreshToken,
44
+ });
45
+ }),
46
+
47
+ /**
48
+ * Login user
49
+ */
50
+ login: asyncHandler(async (req, res, next) => {
51
+ const { email, password } = req.body;
52
+
53
+ if (!email || !password) {
54
+ return next(appError("Email and password are required", 400));
55
+ }
56
+
57
+ const user = await userService.login(email, password);
58
+
59
+ const accessToken = userService.generateAccessToken({
60
+ userId: user._id,
61
+ username: user.username,
62
+ email: user.email,
63
+ });
64
+
65
+ const refreshToken = userService.generateRefreshToken({
66
+ userId: user._id,
67
+ });
68
+
69
+ res.cookie("refreshToken", refreshToken, {
70
+ httpOnly: true,
71
+ secure: true, // Use secure cookies in production
72
+ sameSite: 'none',
73
+ });
74
+ res.cookie("accessToken", accessToken, {
75
+ httpOnly: true,
76
+ secure: true, // Use secure cookies in production
77
+ sameSite: 'none',
78
+ });
79
+
80
+ res.status(200).json({
81
+ success: true,
82
+ data: user,
83
+ accessToken,
84
+ refreshToken,
85
+ });
86
+ }),
87
+
88
+ /**
89
+ * Get current user
90
+ */
91
+ getMe: asyncHandler(async (req, res, next) => {
92
+ if (!req.user) {
93
+ return next(appError("Unauthorized", 401));
94
+ }
95
+
96
+ const user = await userService.getMe(req.user._id);
97
+
98
+ if (!user) {
99
+ return next(appError("User not found", 404));
100
+ }
101
+
102
+ res.status(200).json({
103
+ success: true,
104
+ data: user,
105
+ });
106
+ }),
107
+
108
+ /**
109
+ * Refresh access token
110
+ */
111
+ refreshAccessToken: asyncHandler(async (req, res, next) => {
112
+
113
+ let refreshToken;
114
+
115
+ if (req.cookies.refreshToken) {
116
+ refreshToken = req.cookies.refreshToken;
117
+ } else if (req.body.refreshToken) {
118
+ refreshToken = req.body.refreshToken;
119
+ } else {
120
+ return next(appError("Refresh token is required", 401));
121
+ }
122
+
123
+ if (!refreshToken) {
124
+ return next(appError("Refresh token is required", 401));
125
+ }
126
+
127
+ const decoded = userService.verifyRefreshToken(refreshToken);
128
+
129
+ const accessToken = userService.generateAccessToken({
130
+ userId: decoded.id,
131
+ });
132
+
133
+ res.status(200).json({
134
+ success: true,
135
+ accessToken,
136
+ });
137
+ }),
138
+
139
+ /**
140
+ * Logout (stateless)
141
+ */
142
+ logout: asyncHandler(async (req, res) => {
143
+ res.clearCookie("refreshToken");
144
+ res.clearCookie("accessToken");
145
+ res.status(200).json({
146
+ success: true,
147
+ message: "Logged out successfully",
148
+ });
149
+ }),
150
+
151
+ /**
152
+ * Send verification email
153
+ */
154
+ verifyEmail: asyncHandler(async (req, res, next) => {
155
+ const { email } = req.body;
156
+
157
+ if (!email) {
158
+ return next(appError("Email is required", 400));
159
+ }
160
+
161
+ const token = await userService.generateVerificationToken(email);
162
+
163
+ const verifyUrl = `${config.NODE_ENV === "production"
164
+ ? `${config.WEB_URL}`
165
+ : "http://localhost:3000"
166
+ }/verify-email?token=${token}`;
167
+
168
+ await sendVerificationEmail(email, verifyUrl);
169
+
170
+ res.status(200).json({
171
+ success: true,
172
+ message: "Verification email sent",
173
+ });
174
+ }),
175
+
176
+ /**
177
+ * Verify email token
178
+ */
179
+ verifyEmailToken: asyncHandler(async (req, res, next) => {
180
+ const { token } = req.query;
181
+
182
+ if (!token) {
183
+ return next(appError("Token is required", 400));
184
+ }
185
+
186
+ const user = await userService.verifyEmail(token);
187
+
188
+ res.status(200).json({
189
+ success: true,
190
+ message: "Email verified successfully",
191
+ user,
192
+ });
193
+ }),
194
+ };
195
+
196
+ export default Object.freeze(authController);
@@ -0,0 +1,86 @@
1
+ import User from "../models/user.model.js";
2
+
3
+ /**
4
+ * User Data Access Object
5
+ * ----------------------
6
+ * Single default export
7
+ * Multiple methods attached to one object
8
+ */
9
+
10
+ const userDAO = {
11
+ /**
12
+ * Create a new user
13
+ */
14
+ async create(data) {
15
+ return await User.create(data);
16
+ },
17
+
18
+ /**
19
+ * Find user by email (login use-case)
20
+ * Password included explicitly
21
+ */
22
+ async findByEmail(email) {
23
+ return await User.findOne({ email }).select("+password");
24
+ },
25
+
26
+ async findByUsername(username) {
27
+ return await User.findOne({ username }).select("+password");
28
+ },
29
+
30
+ /**
31
+ * Find user by ID (safe fields)
32
+ */
33
+ async findById(userId) {
34
+ return await User.findById(userId).select("-password -__v");
35
+ },
36
+
37
+ /**
38
+ * Update user by ID
39
+ */
40
+ async updateById(userId, updates) {
41
+ return await User.findByIdAndUpdate(userId, updates, {
42
+ new: true,
43
+ runValidators: true,
44
+ }).select("-password -__v");
45
+ },
46
+
47
+ /**
48
+ * Delete user by ID
49
+ */
50
+ async deleteById(userId) {
51
+ return await User.findByIdAndDelete(userId);
52
+ },
53
+
54
+ /**
55
+ * Paginated users list
56
+ */
57
+ async findAll({ page = 1, limit = 20 } = {}) {
58
+ const skip = (page - 1) * limit;
59
+
60
+ const [users, total] = await Promise.all([
61
+ User.find()
62
+ .select("-password -__v")
63
+ .skip(skip)
64
+ .limit(limit)
65
+ .sort({ createdAt: -1 }),
66
+ User.countDocuments(),
67
+ ]);
68
+
69
+ return {
70
+ users,
71
+ total,
72
+ page,
73
+ totalPages: Math.ceil(total / limit),
74
+ };
75
+ },
76
+
77
+ /**
78
+ * Check email existence
79
+ */
80
+ async isEmailTaken(email) {
81
+ const user = await User.findOne({ email }).lean();
82
+ return Boolean(user);
83
+ },
84
+ };
85
+
86
+ export default userDAO;
@@ -0,0 +1,11 @@
1
+ import morgan from 'morgan';
2
+ import logger from './winston.logger.js';
3
+
4
+ const format =
5
+ ':remote-addr :method :url :status :res[content-length] - :response-time ms';
6
+
7
+ const morganLogger = morgan(format, {
8
+ stream: logger.stream,
9
+ });
10
+
11
+ export default morganLogger;
@@ -0,0 +1,64 @@
1
+ import winston from "winston";
2
+ const { createLogger, format, transports } = winston;
3
+
4
+ /*
5
+ |--------------------------------------------------------------------------
6
+ | Custom log format
7
+ |--------------------------------------------------------------------------
8
+ */
9
+ const logFormat = format.combine(
10
+ format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
11
+ format.colorize(),
12
+ format.printf(({ timestamp, level, message }) => {
13
+ return `${timestamp} [${level}]: ${message}`;
14
+ })
15
+ );
16
+
17
+ /*
18
+ |--------------------------------------------------------------------------
19
+ | Create logger instance
20
+ |--------------------------------------------------------------------------
21
+ */
22
+ const logger = createLogger({
23
+ level: process.env.NODE_ENV === "production" ? "error" : "info",
24
+ format: logFormat,
25
+ transports: [
26
+ // 🔴 Error logs
27
+ new transports.File({
28
+ filename: "logs/error.log",
29
+ level: "error",
30
+ }),
31
+
32
+ // 🟢 All logs
33
+ new transports.File({
34
+ filename: "logs/combined.log",
35
+ }),
36
+ ],
37
+ });
38
+
39
+ /*
40
+ |--------------------------------------------------------------------------
41
+ | Console logs only in development
42
+ |--------------------------------------------------------------------------
43
+ */
44
+ if (process.env.NODE_ENV !== "production") {
45
+ logger.add(
46
+ new transports.Console({
47
+ format: logFormat,
48
+ })
49
+ );
50
+ }
51
+
52
+ /*
53
+ |--------------------------------------------------------------------------
54
+ | 🔥 REQUIRED FOR MORGAN (IMPORTANT)
55
+ |--------------------------------------------------------------------------
56
+ | Morgan calls: stream.write(message)
57
+ */
58
+ logger.stream = {
59
+ write: (message) => {
60
+ logger.info(message.trim());
61
+ },
62
+ };
63
+
64
+ export default logger;
@@ -0,0 +1,64 @@
1
+ import jwt from "jsonwebtoken";
2
+ import appError from '../utils/appError.js';
3
+ import config from "../config/config.js";
4
+ import User from "../models/user.model.js";
5
+
6
+ /**
7
+ * Protect middleware
8
+ * Checks if user is authenticated using JWT
9
+ */
10
+ export const protect = async (req, res, next) => {
11
+ try {
12
+ let token;
13
+
14
+ // 1️⃣ Get token from Authorization header
15
+ if (
16
+ req.headers.authorization &&
17
+ req.headers.authorization.startsWith("Bearer") ||
18
+ req.cookies.accessToken
19
+ ) {
20
+ token = req.cookies.accessToken || req.headers.authorization.split(" ")[1];
21
+ }
22
+
23
+
24
+
25
+ // 2️⃣ If token not found
26
+ if (!token) {
27
+ return next(
28
+ appError("You are not logged in. Please log in to continue.", 401)
29
+ );
30
+ }
31
+
32
+ // 3️⃣ Verify token
33
+ const decoded = jwt.verify(token, config.JWT_SECRET);
34
+
35
+ // decoded = { id, iat, exp }
36
+
37
+ // 4️⃣ Check if user still exists
38
+ const user = await User.findById(decoded.id).select("role");
39
+
40
+ if (!user) {
41
+ return next(
42
+ appError("The user belonging to this token no longer exists.", 401)
43
+ );
44
+ }
45
+
46
+ // 5️⃣ Attach user to request
47
+ req.user = user;
48
+
49
+ next();
50
+ } catch (error) {
51
+ // 6️⃣ Token errors handling
52
+ if (error.name === "JsonWebTokenError") {
53
+ return next(appError("Invalid token. Please log in again.", 401));
54
+ }
55
+
56
+ if (error.name === "TokenExpiredError") {
57
+ return next(
58
+ appError("Your session has expired. Please log in again.", 401)
59
+ );
60
+ }
61
+
62
+ next(error);
63
+ }
64
+ };
@@ -0,0 +1,28 @@
1
+ import logger from "../loggers/winston.logger.js";
2
+ import config from "../config/config.js";
3
+
4
+ const errorHandler = (err, req, res) => {
5
+ const statusCode = err.statusCode || 500;
6
+
7
+ const nodeEnv = config.NODE_ENV;
8
+
9
+ // Backend logging (FULL DETAILS)
10
+ logger.error(err.message, {
11
+ statusCode,
12
+ method: req.method,
13
+ path: req.originalUrl,
14
+ stack: nodeEnv === "development" ? err.stack : undefined,
15
+ });
16
+
17
+ // Frontend-safe response
18
+ res.status(statusCode).json({
19
+ success: false,
20
+ message:
21
+ nodeEnv === "production"
22
+ ? "Internal Server Error"
23
+ : err.message,
24
+ stack: nodeEnv === "development" ? err.stack : undefined,
25
+ });
26
+ };
27
+
28
+ export default errorHandler;
@@ -0,0 +1,31 @@
1
+ // middleware/rateLimiter.js
2
+ import rateLimit from "express-rate-limit";
3
+
4
+ export const generalRateLimiter = rateLimit({
5
+ windowMs: 15 * 60 * 1000, // 15 minutes
6
+ max: 100, // max 100 requests per IP
7
+ standardHeaders: true,
8
+ legacyHeaders: false,
9
+
10
+ handler: (req, res) => {
11
+ res.status(429).json({
12
+ success: false,
13
+ message: "Too many requests. Please try again later.",
14
+ });
15
+ },
16
+ });
17
+
18
+ export const authRateLimiter = rateLimit({
19
+ windowMs: 15 * 60 * 1000, // 15 minutes
20
+ max: 10, // only 10 login attempts
21
+ standardHeaders: true,
22
+ legacyHeaders: false,
23
+
24
+ handler: (req, res) => {
25
+ res.status(429).json({
26
+ success: false,
27
+ message:
28
+ "Too many login attempts. Please try again after some time.",
29
+ });
30
+ },
31
+ });