@codenokami/node-api-master 1.4.0
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/README.md +164 -0
- package/dist/index.d.mts +436 -0
- package/dist/index.d.ts +436 -0
- package/dist/index.js +415 -0
- package/dist/index.mjs +372 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
ďťż# @cnk/node-api-master đ
|
|
2
|
+
|
|
3
|
+
A robust, professional-grade utility package for Express.js APIs. This package simplifies database connections, authentication, real-time communication with Socket.io, error handling, and request validation.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## đŚ Installation
|
|
8
|
+
|
|
9
|
+
This package is designed to work with **mongoose** and **socket.io** as peer dependencies to avoid version conflicts.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Install the core package
|
|
13
|
+
npm install @cnk/node-api-master
|
|
14
|
+
|
|
15
|
+
# Install required peer dependencies
|
|
16
|
+
npm install mongoose socket.io
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## đ Features
|
|
23
|
+
|
|
24
|
+
- **đĄ Auth & Admin Middleware**: JWT-based route protection and role-based access control.
|
|
25
|
+
- **đ Socket.io Helper**: Integrated real-time communication with JWT authentication support.
|
|
26
|
+
- **đ¨ Global Error Handler**: Centralized error management using the `AppError` class.
|
|
27
|
+
- **⥠CatchAsync Utility**: Eliminate repetitive `try-catch` blocks in your controllers.
|
|
28
|
+
- **â
Joi Validation**: Pre-defined and custom schemas for request validation.
|
|
29
|
+
- **đ DB Helper**: Simplified MongoDB connection setup.
|
|
30
|
+
- **đ Standardized Responses**: Uniform success and error API response formats.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## đ Detailed Usage Guide
|
|
35
|
+
|
|
36
|
+
### 1. Database Connection & Server Setup
|
|
37
|
+
|
|
38
|
+
Handle dynamic ports and database connections efficiently.
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
const { connectDB, STATUS_CODES } = require("@cnk/node-api-master");
|
|
42
|
+
|
|
43
|
+
// Start DB
|
|
44
|
+
connectDB(process.env.MONGO_URI);
|
|
45
|
+
|
|
46
|
+
// Dynamic Port
|
|
47
|
+
const PORT = process.env.PORT || 5000;
|
|
48
|
+
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### 2. Socket.io with Authentication
|
|
54
|
+
|
|
55
|
+
Setup a secure real-time server. The middleware automatically verifies tokens from `socket.handshake.auth.token`.
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
const http = require("http");
|
|
59
|
+
const { socket } = require("@cnk/node-api-master");
|
|
60
|
+
|
|
61
|
+
const server = http.createServer(app);
|
|
62
|
+
|
|
63
|
+
// Initialize Socket with JWT protection
|
|
64
|
+
const io = socket.initSocket(server, {
|
|
65
|
+
authRequired: true,
|
|
66
|
+
jwtSecret: process.env.JWT_SECRET,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
io.on("connection", (s) => {
|
|
70
|
+
console.log("Authenticated User ID:", s.user.id);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
server.listen(PORT);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### 3. Authentication Middleware (HTTP)
|
|
79
|
+
|
|
80
|
+
Protect your Express routes using JWT.
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
const { auth, admin } = require("@cnk/node-api-master");
|
|
84
|
+
|
|
85
|
+
// Protect a route
|
|
86
|
+
router.get("/me", auth(), userController.getMe);
|
|
87
|
+
|
|
88
|
+
// Admin only route
|
|
89
|
+
router.delete("/users/:id", auth(), admin, userController.deleteUser);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### 4. Request Validation (Joi)
|
|
95
|
+
|
|
96
|
+
Validate incoming request bodies, params, or queries.
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
const { validate, schemas, Joi } = require("@cnk/node-api-master");
|
|
100
|
+
|
|
101
|
+
// Use pre-built schemas
|
|
102
|
+
router.post("/login", validate(schemas.user.login), authController.login);
|
|
103
|
+
|
|
104
|
+
// Use custom schema
|
|
105
|
+
const profileSchema = Joi.object({
|
|
106
|
+
bio: Joi.string().max(200),
|
|
107
|
+
website: Joi.string().uri(),
|
|
108
|
+
});
|
|
109
|
+
router.put("/profile", validate(profileSchema), userController.updateProfile);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
### 5. Standardized Responses & Error Handling
|
|
115
|
+
|
|
116
|
+
Keep your API responses consistent across the entire project.
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
const {
|
|
120
|
+
catchAsync,
|
|
121
|
+
AppError,
|
|
122
|
+
apiResponse,
|
|
123
|
+
STATUS_CODES,
|
|
124
|
+
} = require("@cnk/node-api-master");
|
|
125
|
+
|
|
126
|
+
exports.getUser = catchAsync(async (req, res, next) => {
|
|
127
|
+
const user = await User.findById(req.params.id);
|
|
128
|
+
|
|
129
|
+
if (!user) {
|
|
130
|
+
return next(new AppError("User not found!", STATUS_CODES.NOT_FOUND));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
apiResponse.success(res, user, "User retrieved successfully");
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
### 6. Global Error Middleware
|
|
140
|
+
|
|
141
|
+
**Mandatory:** Register this at the very end of your middleware stack in `app.js`.
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
const { errorMiddleware } = require("@cnk/node-api-master");
|
|
145
|
+
|
|
146
|
+
// After all your routes
|
|
147
|
+
app.use(errorMiddleware);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## đ Project Structure
|
|
153
|
+
|
|
154
|
+
- `src/db`: Mongoose connection helpers.
|
|
155
|
+
- `src/middleware`: Auth, Admin, Validation, and Global Error handlers.
|
|
156
|
+
- `src/socket`: Socket.io initialization and JWT middleware.
|
|
157
|
+
- `src/utils`: `AppError`, `catchAsync`, and API response helpers.
|
|
158
|
+
- `src/validations`: Reusable Joi schemas.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## đ License
|
|
163
|
+
|
|
164
|
+
MIT Š CNK
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import Joi from 'joi';
|
|
2
|
+
export { default as Joi } from 'joi';
|
|
3
|
+
import mongoose from 'mongoose';
|
|
4
|
+
import jwt from 'jsonwebtoken';
|
|
5
|
+
import bcrypt from 'bcryptjs';
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
import { Server } from 'socket.io';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Database áááŻáˇ ááťáááşáááşááąá¸ááąáŹ function
|
|
11
|
+
* @param {string} url - MongoDB Connection String
|
|
12
|
+
*/
|
|
13
|
+
const connectDB = async (url) => {
|
|
14
|
+
if (!url) {
|
|
15
|
+
console.error(
|
|
16
|
+
"â Error: MongoDB URL is required to connect to the database.",
|
|
17
|
+
);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const conn = await mongoose.connect(url);
|
|
23
|
+
console.log(`â
MongoDB Connected: ${conn.connection.host}`);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(`â Error: ${error.message}`);
|
|
26
|
+
process.exit(1); // ááťáááşáááááş Server ááᯠáááşáá
áşáááş
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Custom Error Class for API Errors
|
|
32
|
+
*/
|
|
33
|
+
class AppError extends Error {
|
|
34
|
+
constructor(message, statusCode) {
|
|
35
|
+
super(message);
|
|
36
|
+
|
|
37
|
+
this.statusCode = statusCode;
|
|
38
|
+
// status á 4xx áááŻáááş 'fail', 5xx áááŻáááş 'error' áááŻáˇ áááşáážááşáááş
|
|
39
|
+
this.status = `${statusCode}`.startsWith("4") ? "fail" : "error";
|
|
40
|
+
|
|
41
|
+
// ááŤá ááťá˝ááşááąáŹáşáááŻáˇáááŻááşáááŻááş áááşáážááşáááŻááşáá˛áˇ Error ááźá
áşááźáąáŹááşá¸ áĄáážááşáĄááŹá¸ááźáŻááŹááŤ
|
|
42
|
+
this.isOperational = true;
|
|
43
|
+
|
|
44
|
+
// Error áááşááąááŹáážáŹ ááźá
áşáááşáááŻáá˛áˇ stack trace ááᯠááááşá¸ááŹá¸áááş
|
|
45
|
+
Error.captureStackTrace(this, this.constructor);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* catchAsync - try-catch block áá˝áąááᯠáĄá
áŹá¸áááŻá¸áááş
|
|
51
|
+
* @param {Function} fn - Async controller function
|
|
52
|
+
*/
|
|
53
|
+
const catchAsync = (fn) => {
|
|
54
|
+
return (req, res, next) => {
|
|
55
|
+
// ááąá¸áááŻááşáá˛áˇ function ááᯠrun áááşá error áááşáááş .catch() áááą next(err) ááᯠáááŻáˇááąá¸áááş
|
|
56
|
+
fn(req, res, next).catch(next);
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Auth Middleware - Supports Bearer Token & Cookies
|
|
62
|
+
* @param {string} secret - JWT Secret (Default is process.env.JWT_SECRET)
|
|
63
|
+
*/
|
|
64
|
+
const auth = (secret = process.env.JWT_SECRET) =>
|
|
65
|
+
catchAsync(async (req, res, next) => {
|
|
66
|
+
let token;
|
|
67
|
+
|
|
68
|
+
// 1) Header áááą Bearer Token ááᯠá
á
áşááąá¸ááźááşá¸
|
|
69
|
+
if (
|
|
70
|
+
req.headers.authorization &&
|
|
71
|
+
req.headers.authorization.startsWith("Bearer")
|
|
72
|
+
) {
|
|
73
|
+
token = req.headers.authorization.split(" ")[1];
|
|
74
|
+
}
|
|
75
|
+
// 2) Cookie áááą Token ááᯠá
á
áşááąá¸ááźááşá¸
|
|
76
|
+
else if (req.cookies && req.cookies.jwt) {
|
|
77
|
+
token = req.cookies.jwt;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Secret ááážááááş Developer ááᯠáááááąá¸áááŻáˇ Error ááźáááş
|
|
81
|
+
if (!secret) {
|
|
82
|
+
return next(
|
|
83
|
+
new AppError("JWT Secret is not defined in environment variables", 500),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!token) {
|
|
88
|
+
return next(
|
|
89
|
+
new AppError(
|
|
90
|
+
"You are not logged in! Please log in to get access.",
|
|
91
|
+
401,
|
|
92
|
+
),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Verify token
|
|
97
|
+
try {
|
|
98
|
+
const decoded = jwt.verify(token, secret);
|
|
99
|
+
req.user = decoded;
|
|
100
|
+
next();
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return next(
|
|
103
|
+
new AppError("Invalid token or expired. Please log in again.", 401),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Admin Middleware
|
|
110
|
+
*/
|
|
111
|
+
const admin = (req, res, next) => {
|
|
112
|
+
if (req.user && req.user.role === "admin") {
|
|
113
|
+
next();
|
|
114
|
+
} else {
|
|
115
|
+
return next(
|
|
116
|
+
new AppError("You do not have permission to perform this action", 403),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Joi Schema Validation Middleware
|
|
123
|
+
* @param {Object} schema - Joi validation schema
|
|
124
|
+
*/
|
|
125
|
+
const validate = (schema) => (req, res, next) => {
|
|
126
|
+
// body, query, params áĄáŹá¸ááŻáśá¸ááᯠá
á
áşáááŻáˇááĄáąáŹááş req object áá
áşááŻááŻáśá¸ááᯠschema áá˛áˇ áááŻááşá
á
áşáááş
|
|
127
|
+
const { error, value } = schema.validate(req.body, {
|
|
128
|
+
abortEarly: false, // Error áĄáŹá¸ááŻáśá¸ááᯠáá
áşááŤáááşá¸ ááźáááş
|
|
129
|
+
allowUnknown: true, // Schema áá˛áááŤáá˛áˇ field áá˝áąááŤááŹáááş áááşááśáááş
|
|
130
|
+
stripUnknown: true, // Schema áá˛áááŤáá˛áˇ field áá˝áąááᯠáááşááŻááşáá
áşáááş
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (error) {
|
|
134
|
+
// Error message áá˝áąááᯠá
áŻá
ááşá¸ááźáŽá¸ format ááŻááşáááş
|
|
135
|
+
const errorMessage = error.details
|
|
136
|
+
.map((detail) => detail.message.replace(/"/g, ""))
|
|
137
|
+
.join(", ");
|
|
138
|
+
|
|
139
|
+
return next(new AppError(errorMessage, 400));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// á
á
áşááąá¸ááźáŽá¸ááŹá¸ data (sanitized data) ááᯠreq.body áᲠááźááşáááˇáşáááş
|
|
143
|
+
req.body = value;
|
|
144
|
+
next();
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Standard HTTP Status Codes
|
|
149
|
+
*/
|
|
150
|
+
const PORT = process.env.PORT || 5000;
|
|
151
|
+
|
|
152
|
+
const STATUS_CODES = {
|
|
153
|
+
// --- 2xx Success ---
|
|
154
|
+
OK: 200, // Request áĄáąáŹááşááźááşáááş
|
|
155
|
+
CREATED: 201, // Data áĄáá
áş áááşááąáŹááşáážáŻ áĄáąáŹááşááźááşáááş
|
|
156
|
+
ACCEPTED: 202, // áááşááśááážááááş (ááąáŹááşááž áĄááŻááşááŻááşáááş)
|
|
157
|
+
NO_CONTENT: 204, // áĄáąáŹááşááźááşáááşá áááŻáˇááąáŹáş ááźá
áᏠData ááážá (áĽáááŹ- Delete ááŻááşááźáŽá¸ááťáááş)
|
|
158
|
+
|
|
159
|
+
// --- 3xx Redirection ---
|
|
160
|
+
MOVED_PERMANENTLY: 301,
|
|
161
|
+
FOUND: 302,
|
|
162
|
+
|
|
163
|
+
// --- 4xx Client Errors ---
|
|
164
|
+
BAD_REQUEST: 400, // ááąá¸áááŻáˇáááŻááşááąáŹ Data Format áážáŹá¸ááąáááş (Validation error)
|
|
165
|
+
UNAUTHORIZED: 401, // Login áááşáááş áááŻáĄááşáááş (áááŻáˇáááŻááş Token áážáŹá¸áááş)
|
|
166
|
+
FORBIDDEN: 403, // ááŻááşáááŻááşáá˝ááˇáşááážá (áĽáááŹ- Admin áááŻááşáᲠAdmin panel áááşááźááşá¸)
|
|
167
|
+
NOT_FOUND: 404, // áážáŹáá˝áąááąááąáŹ Resource ááážáááŤ
|
|
168
|
+
METHOD_NOT_ALLOWED: 405, // API Method (GET, POST, etc.) áážáŹá¸ááąáááş
|
|
169
|
+
CONFLICT: 409, // Data áááşááąáááş (áĽáááŹ- áážáááźáŽá¸ááŹá¸ Email áá˛áˇ Register ááŻááşááźááşá¸)
|
|
170
|
+
UNPROCESSABLE_ENTITY: 422, // Validation Error ááťáŹá¸áĄáá˝ááş áĄááŻáśá¸ááťáŹá¸áááş
|
|
171
|
+
TOO_MANY_REQUESTS: 429, // API ááᯠáááá áááşáááŻááşááąáŤáşááźááşá¸ (Rate limiting)
|
|
172
|
+
|
|
173
|
+
// --- 5xx Server Errors ---
|
|
174
|
+
INTERNAL_SERVER_ERROR: 500, // Server áá˛áá˝ááş Code áážáŹá¸áá˝ááşá¸ááźááşá¸
|
|
175
|
+
NOT_IMPLEMENTED: 501,
|
|
176
|
+
BAD_GATEWAY: 502,
|
|
177
|
+
SERVICE_UNAVAILABLE: 503, // Server ááąáášáááááşááŹá¸áááş
|
|
178
|
+
GATEWAY_TIMEOUT: 504,
|
|
179
|
+
|
|
180
|
+
PORT,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Global Error Handling Middleware
|
|
185
|
+
*/
|
|
186
|
+
const errorMiddleware = (err, req, res, next) => {
|
|
187
|
+
err.statusCode = err.statusCode || STATUS_CODES.INTERNAL_SERVER_ERROR;
|
|
188
|
+
err.status = err.status || "error";
|
|
189
|
+
|
|
190
|
+
// Development mode áážáŹ Error áĄááźááˇáşáĄá
áŻáśááźááźáŽá¸ Production áážáŹ message áᲠááźáááş
|
|
191
|
+
if (process.env.NODE_ENV === "development") {
|
|
192
|
+
res.status(err.statusCode).json({
|
|
193
|
+
status: err.status,
|
|
194
|
+
error: err,
|
|
195
|
+
message: err.message,
|
|
196
|
+
stack: err.stack,
|
|
197
|
+
});
|
|
198
|
+
} else {
|
|
199
|
+
// Production Mode
|
|
200
|
+
// áĄáááşá ááŤá ááťá˝ááşááąáŹáşáááŻáˇ áááşáážááşááŹá¸áá˛áˇ Operational Error (AppError) áááŻáááş
|
|
201
|
+
if (err.isOperational) {
|
|
202
|
+
res.status(err.statusCode).json({
|
|
203
|
+
status: err.status,
|
|
204
|
+
message: err.message,
|
|
205
|
+
});
|
|
206
|
+
} else {
|
|
207
|
+
// ááááşáážááşááŹá¸áá˛áˇ Programming error áá˝áąááźá
áşáááş (áĽáááŹ- Library áá
áşááŻá áááşáá˛áˇ error)
|
|
208
|
+
console.error("ERROR đĽ", err);
|
|
209
|
+
res.status(STATUS_CODES.INTERNAL_SERVER_ERROR).json({
|
|
210
|
+
status: "error",
|
|
211
|
+
message: "Something went very wrong!",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Standard API Response Wrapper
|
|
219
|
+
*/
|
|
220
|
+
const apiResponse = {
|
|
221
|
+
// áĄáąáŹááşááźááşáá˛áˇ response ááąá¸áááŻáˇáááş
|
|
222
|
+
success: (res, data, message = "Success", statusCode = STATUS_CODES.OK) => {
|
|
223
|
+
return res.status(statusCode).json({
|
|
224
|
+
status: "success",
|
|
225
|
+
message,
|
|
226
|
+
data,
|
|
227
|
+
});
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
// Error response ááąá¸áááŻáˇáááş (Operational error ááťáŹá¸áĄáá˝ááş)
|
|
231
|
+
error: (
|
|
232
|
+
res,
|
|
233
|
+
message = "Internal Server Error",
|
|
234
|
+
statusCode = STATUS_CODES.INTERNAL_SERVER_ERROR,
|
|
235
|
+
) => {
|
|
236
|
+
return res.status(statusCode).json({
|
|
237
|
+
status: "error",
|
|
238
|
+
message,
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
declare namespace response {
|
|
244
|
+
export { apiResponse as default };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Password ááᯠHash ááŻááşáááş
|
|
249
|
+
*/
|
|
250
|
+
const hashPassword = async (password) => {
|
|
251
|
+
const salt = await bcrypt.genSalt(10);
|
|
252
|
+
return await bcrypt.hash(password, salt);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Password áážááş/ááážááş á
á
áşááąá¸áááş
|
|
257
|
+
*/
|
|
258
|
+
const comparePassword = async (password, hashedPassword) => {
|
|
259
|
+
return await bcrypt.compare(password, hashedPassword);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* JWT Token ááŻááşááąá¸ááźáŽá¸ Cookie áá˛áááŻáˇ áááşá¸áááˇáşááąá¸áááˇáş Helper
|
|
264
|
+
* @param {Object} payload - Token áá˛áá˝ááş ááááşá¸áááŻááąáŹ áĄááťááşáĄáááş (e.g., { userId })
|
|
265
|
+
* @param {Object} res - Express Response Object
|
|
266
|
+
* @param {Object} options - á
áááşááźááŻááşááźááşáááşáááŻááşááąáŹ options ááťáŹá¸
|
|
267
|
+
*/
|
|
268
|
+
const generateToken = (payload, res, options = {}) => {
|
|
269
|
+
const secret = options.secret || process.env.JWT_SECRET;
|
|
270
|
+
const expiresIn = options.expiresIn || "30d";
|
|
271
|
+
const cookieName = options.cookieName || "jwt";
|
|
272
|
+
|
|
273
|
+
// 1. Token Generate ááŻááşááźááşá¸
|
|
274
|
+
const token = jwt.sign(payload, secret, {
|
|
275
|
+
expiresIn: expiresIn,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// 2. Cookie Options áááşáážááşááźááşá¸
|
|
279
|
+
const cookieOptions = {
|
|
280
|
+
maxAge: options.maxAge || 30 * 24 * 60 * 60 * 1000, // Default 30 days
|
|
281
|
+
httpOnly: true, // JavaScript ááž áááşáááĄáąáŹááş (Prevent XSS)
|
|
282
|
+
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", // CSRF Protection
|
|
283
|
+
secure: process.env.NODE_ENV === "production", // Production áá˝ááş HTTPS ááŻáśá¸áážáᏠáĄááŻááşááŻááşáááş
|
|
284
|
+
...options.extraCookieOptions, // áááźáŹá¸ áĄááᯠoptions ááťáŹá¸áážáááťážááş áááˇáşáááş
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// 3. Cookie áá˛áááŻáˇ áááˇáşááźááşá¸
|
|
288
|
+
res.cookie(cookieName, token, cookieOptions);
|
|
289
|
+
|
|
290
|
+
return token;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* ááááˇáşááᯠáááŻááşááśáˇááźáŽá¸ Secure ááźá
áşáá˛áˇ UUID v4 ááŻááşááąá¸áááş
|
|
295
|
+
* (RFC 4122 Standard Compliant)
|
|
296
|
+
*/
|
|
297
|
+
const generateUUID = () => {
|
|
298
|
+
// 1. ááąááşáá
áş Node.js (v14.17.0+) áĄáá˝ááş áááŻááşáááŻááşááŻáśá¸áááş
|
|
299
|
+
if (typeof crypto.randomUUID === "function") {
|
|
300
|
+
return crypto.randomUUID();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 2. Fallback: Node ááŹá¸áážááşá¸áĄááááˇáşáá˝áąáĄáá˝ááş Cryptographically Secure ááźá
áşáĄáąáŹááş áááŻááşáááŻááşááŻááşáááş
|
|
304
|
+
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
|
|
305
|
+
(c ^ (crypto.randomBytes(1).readUInt8() & (15 >> (c / 4)))).toString(16),
|
|
306
|
+
);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* User Validation Schemas
|
|
311
|
+
*/
|
|
312
|
+
const userSchema = {
|
|
313
|
+
// Standard Register Schema
|
|
314
|
+
register: Joi.object({
|
|
315
|
+
username: Joi.string().alphanum().min(3).max(30).required().messages({
|
|
316
|
+
"string.min": "Username must be at least 3 characters long",
|
|
317
|
+
"any.required": "Username is a required field",
|
|
318
|
+
}),
|
|
319
|
+
email: Joi.string()
|
|
320
|
+
.email({ minDomainSegments: 2, tlds: { allow: ["com", "net", "org"] } })
|
|
321
|
+
.required()
|
|
322
|
+
.messages({
|
|
323
|
+
"string.email": "Please provide a valid email address",
|
|
324
|
+
}),
|
|
325
|
+
password: Joi.string().min(8).required().messages({
|
|
326
|
+
"string.min": "Password must be at least 8 characters long",
|
|
327
|
+
}),
|
|
328
|
+
confirmPassword: Joi.any()
|
|
329
|
+
.equal(Joi.ref("password"))
|
|
330
|
+
.required()
|
|
331
|
+
.messages({ "any.only": "Passwords do not match" }),
|
|
332
|
+
role: Joi.string().valid("user", "admin").default("user"),
|
|
333
|
+
}),
|
|
334
|
+
|
|
335
|
+
// Login Schema
|
|
336
|
+
login: Joi.object({
|
|
337
|
+
email: Joi.string().email().required(),
|
|
338
|
+
password: Joi.string().required(),
|
|
339
|
+
}),
|
|
340
|
+
|
|
341
|
+
// Password Update Schema
|
|
342
|
+
updatePassword: Joi.object({
|
|
343
|
+
currentPassword: Joi.string().required(),
|
|
344
|
+
newPassword: Joi.string().min(8).required(),
|
|
345
|
+
}),
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* á
áááşááźááŻááş schema áĄáá
áşááąáŹááşááťááşáááş ááŻáśá¸áááş (Dynamic Flexibility)
|
|
349
|
+
* @param {Object} schemaDefinition - Joi object definition
|
|
350
|
+
*/
|
|
351
|
+
custom: (schemaDefinition) => Joi.object(schemaDefinition),
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Common Joi Schemas
|
|
356
|
+
*/
|
|
357
|
+
const commonSchema = {
|
|
358
|
+
// MongoDB ObjectId á
á
áşááąá¸áááş (24 hex characters)
|
|
359
|
+
objectId: Joi.string()
|
|
360
|
+
.regex(/^[0-9a-fA-F]{24}$/)
|
|
361
|
+
.messages({
|
|
362
|
+
"string.pattern.base": "Invalid ID format. Must be a valid ObjectId.",
|
|
363
|
+
}),
|
|
364
|
+
|
|
365
|
+
// Pagination áĄáá˝ááş (Query strings)
|
|
366
|
+
pagination: Joi.object({
|
|
367
|
+
page: Joi.number().integer().min(1).default(1),
|
|
368
|
+
limit: Joi.number().integer().min(1).max(100).default(10),
|
|
369
|
+
sort: Joi.string().optional(),
|
|
370
|
+
fields: Joi.string().optional(),
|
|
371
|
+
}),
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const schemas = {
|
|
375
|
+
user: userSchema,
|
|
376
|
+
common: commonSchema,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
let io;
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Socket Authentication Middleware
|
|
383
|
+
*/
|
|
384
|
+
const socketAuth = (secret) => {
|
|
385
|
+
return (socket, next) => {
|
|
386
|
+
const token =
|
|
387
|
+
socket.handshake.auth?.token || socket.handshake.headers?.token;
|
|
388
|
+
|
|
389
|
+
if (!token) {
|
|
390
|
+
return next(new Error("Authentication error: Token missing"));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
const decoded = jwt.verify(token, secret || process.env.JWT_SECRET);
|
|
395
|
+
socket.user = decoded; // socket áá˛áážáŹ user data ááááşá¸ááŹá¸áááş
|
|
396
|
+
next();
|
|
397
|
+
} catch (err) {
|
|
398
|
+
next(new Error("Authentication error: Invalid token"));
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Socket Initialize ááŻááşááźááşá¸
|
|
405
|
+
*/
|
|
406
|
+
const initSocket = (server, options = {}) => {
|
|
407
|
+
io = new Server(server, {
|
|
408
|
+
cors: {
|
|
409
|
+
origin: options.origin || "*",
|
|
410
|
+
},
|
|
411
|
+
...options,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// JWT Middleware ááᯠáĄááŻáśá¸ááźáŻááźááşá¸
|
|
415
|
+
if (options.authRequired) {
|
|
416
|
+
io.use(socketAuth(options.jwtSecret));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return io;
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Initialize ááźáŽá¸ááŹá¸ IO object ááᯠááźááşáá°ááźááşá¸
|
|
424
|
+
*/
|
|
425
|
+
const getIO = () => {
|
|
426
|
+
if (!io) throw new Error("Socket.io not initialized!");
|
|
427
|
+
return io;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
declare const index_getIO: typeof getIO;
|
|
431
|
+
declare const index_initSocket: typeof initSocket;
|
|
432
|
+
declare namespace index {
|
|
433
|
+
export { index_getIO as getIO, index_initSocket as initSocket };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export { AppError, STATUS_CODES, admin, response as apiResponse, auth, catchAsync, comparePassword, connectDB, errorMiddleware, generateToken, generateUUID, hashPassword, schemas, index as socket, validate };
|