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,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,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
|
+
});
|