men-boilerplate 1.0.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 ADDED
@@ -0,0 +1,65 @@
1
+ # MEN Auth Boilerplate (MongoDB, Express, Node.js)
2
+
3
+ A production-grade, ESM-ready authentication and session management boilerplate. Scaffolds a complete auth system in seconds.
4
+
5
+ ## 🚀 Features
6
+
7
+ * **100% ES Modules (ESM)**: Modern, future-proof JavaScript.
8
+ * **JWT Authentication**: Secure Access & Refresh token rotation.
9
+ * **Session Management**: Track and manage active sessions (device, IP mapping).
10
+ * **Persistent Sessions**: Multi-device support with remote revocation (force logout).
11
+ * **Standard CRUD Factory**: Pre-built logic for MongoDB models.
12
+ * **Clean Architecture**: Separation of concerns across controllers, routes, models, and middleware.
13
+
14
+ ## 🛠 Installation
15
+
16
+ Run the following command to scaffold a new project:
17
+
18
+ ```bash
19
+ npx men-setup my-auth-app
20
+ ```
21
+
22
+
23
+ *Note: Replace `men-boilerplate-setup` with the actual package name after publication.*
24
+
25
+ ## 📂 Project Structure
26
+
27
+ ```text
28
+ src/
29
+ ├── auth/ # Middleware and JWT logic
30
+ ├── config/ # DB and environment configuration
31
+ ├── controllers/ # Request handlers (Auth, Sessions)
32
+ ├── crud/ # Reusable CRUD factory
33
+ ├── middleware/ # Global error and log handlers
34
+ ├── models/ # Mongoose schemas (User, Session)
35
+ ├── routes/ # Express routing
36
+ └── utils/ # API Response helpers
37
+ ```
38
+
39
+ ## 🔐 Session Management Endpoints
40
+
41
+ | Method | Endpoint | Description |
42
+ | :--- | :--- | :--- |
43
+ | `POST` | `/api/v1/auth/sign-up` | Create a new user |
44
+ | `POST` | `/api/v1/auth/login` | Login and start a new session |
45
+ | `GET` | `/api/v1/auth/me` | Get current user profile (Protected) |
46
+ | `POST` | `/api/v1/auth/refresh` | Rotate tokens using active session |
47
+ | `GET` | `/api/v1/auth/sessions` | List all active sessions/devices |
48
+ | `DELETE` | `/api/v1/auth/sessions/:id` | Revoke a session (Remote logout) |
49
+ | `POST` | `/api/v1/auth/logout` | Clear current session and cookies |
50
+
51
+ ## ⚙️ Environment Variables
52
+
53
+ Create a `.env` file in the root:
54
+
55
+ ```env
56
+ PORT=5000
57
+ MONGO_URI=mongodb://localhost:27017/auth-db
58
+ JWT_SECRET=your_jwt_secret
59
+ REFRESH_SECRET=your_refresh_secret
60
+ NODE_ENV=development
61
+ ```
62
+
63
+ ## 📝 License
64
+
65
+ ISC
@@ -0,0 +1,13 @@
1
+ {
2
+ "folders": [
3
+ {
4
+ "path": "."
5
+ },
6
+ {
7
+ "path": "../node_modules"
8
+ },
9
+ {
10
+ "path": "../template"
11
+ }
12
+ ]
13
+ }
package/bin/cli.js ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { execSync } from 'child_process';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ // Ensure we are pointing to the RIGHT template folder inside YOUR package
12
+ const templateDir = path.resolve(__dirname, '../template');
13
+ const targetDir = process.cwd();
14
+
15
+ async function init() {
16
+ console.log('🛠️ Starting Scaffolding...');
17
+
18
+ try {
19
+ // Check if template exists before starting
20
+ if (!fs.existsSync(templateDir)) {
21
+ throw new Error(`Template directory not found at: ${templateDir}`);
22
+ }
23
+
24
+ // 1. Copy Files
25
+ console.log(`📂 Copying from ${templateDir} to ${targetDir}`);
26
+ fs.cpSync(templateDir, targetDir, { recursive: true });
27
+
28
+ // 2. Rename _package.json
29
+ const oldPkg = path.join(targetDir, '_package.json');
30
+ const newPkg = path.join(targetDir, 'package.json');
31
+
32
+ if (fs.existsSync(oldPkg)) {
33
+ fs.renameSync(oldPkg, newPkg);
34
+ console.log('✅ Created package.json');
35
+ } else {
36
+ console.warn('⚠️ Warning: _package.json missing from template!');
37
+ }
38
+
39
+ // 3. Install Dependencies
40
+ console.log('📦 Running npm install...');
41
+ // We use { stdio: 'inherit' } to see the actual NPM output in the console
42
+ execSync('npm install', { stdio: 'inherit', cwd: targetDir });
43
+
44
+ console.log('\n🚀 ALL DONE! Happy coding.');
45
+
46
+ } catch (err) {
47
+ console.error('\n❌ CRITICAL ERROR:');
48
+ console.error(err.message);
49
+ process.exit(1); // Force the process to show it failed
50
+ }
51
+ }
52
+
53
+ init();
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "men-boilerplate",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "type": "module",
6
+ "bin": {
7
+ "men-setup": "bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "template"
12
+ ],
13
+ "scripts": {
14
+ "test": "echo \"Error: no test specified\" && exit 1"
15
+ },
16
+ "keywords": [],
17
+ "author": "",
18
+ "license": "ISC",
19
+ "description": "",
20
+ "dependencies": {
21
+ "bcryptjs": "^3.0.3",
22
+ "cookie-parser": "^1.4.7",
23
+ "cors": "^2.8.6",
24
+ "dotenv": "^17.4.0",
25
+ "express": "^5.2.1",
26
+ "jsonwebtoken": "^9.0.3",
27
+ "mongoose": "^9.4.1",
28
+ "morgan": "^1.10.1",
29
+ "winston": "^3.19.0"
30
+ },
31
+ "devDependencies": {
32
+ "jest": "^30.3.0",
33
+ "nodemon": "^3.1.14",
34
+ "supertest": "^7.2.2"
35
+ }
36
+ }
@@ -0,0 +1,7 @@
1
+ PORT=5000
2
+ MONGO_URI=mongodb://localhost:27017/my-auth-db
3
+ JWT_SECRET=replace_me_with_something_secure
4
+ REFRESH_SECRET=replace_me_with_something_even_more_secure
5
+ NODE_ENV=development
6
+ MORGAN_FORMAT=dev
7
+ CORS_ORIGIN=*
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "my-new-auth-app",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "server.js",
6
+ "scripts": {
7
+ "start": "node server.js",
8
+ "dev": "nodemon server.js"
9
+ },
10
+ "dependencies": {
11
+ "bcryptjs": "^2.4.3",
12
+ "cookie-parser": "^1.4.6",
13
+ "cors": "^2.8.5",
14
+ "dotenv": "^16.0.3",
15
+ "express": "^4.18.2",
16
+ "jsonwebtoken": "^9.0.1",
17
+ "mongoose": "^7.5.0",
18
+ "morgan": "^1.10.0"
19
+ },
20
+ "devDependencies": {
21
+ "nodemon": "^3.0.1"
22
+ }
23
+ }
@@ -0,0 +1,39 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import morgan from 'morgan';
4
+ import cookieParser from 'cookie-parser';
5
+
6
+ import config from './src/config/config.js';
7
+ import connectDB from './src/config/db.js';
8
+ import authRoutes from './src/routes/authRoutes.js';
9
+ import globalErrorHandler from './src/middleware/errorHandler.js';
10
+
11
+
12
+ export const bootstrap = async (app, options = {}) => {
13
+
14
+ await connectDB();
15
+
16
+
17
+ app.use(express.json({ limit: '10kb' }));
18
+ app.use(cookieParser());
19
+ app.use(morgan(options.morganFormat || config.morganFormat));
20
+
21
+ app.use(cors({
22
+ origin: options.corsOrigin || config.corsOrigin,
23
+ credentials: true
24
+ }));
25
+
26
+
27
+
28
+ app.use('/api/v1/auth', authRoutes);
29
+
30
+
31
+ app.use(globalErrorHandler);
32
+
33
+ console.log('🛡️ Auth Engine: System Bootstrapped Successfully.');
34
+ };
35
+
36
+
37
+ export { default as factory } from './src/crud/factory.js';
38
+ export { default as ApiResponse } from './src/utils/ApiResponse.js';
39
+ export { default as protect } from './src/auth/authMiddleware.js';
@@ -0,0 +1,19 @@
1
+ import express from 'express';
2
+ import { bootstrap } from './index.js';
3
+
4
+ const app = express();
5
+ const port = process.env.PORT || 5000;
6
+
7
+ const startServer = async () => {
8
+ try {
9
+ await bootstrap(app);
10
+ app.listen(port, () => {
11
+ console.log(`🚀 Server running on http://localhost:${port}`);
12
+ });
13
+ } catch (error) {
14
+ console.error('❌ Failed to start server:', error.message);
15
+ process.exit(1);
16
+ }
17
+ };
18
+
19
+ startServer();
@@ -0,0 +1,22 @@
1
+ import jwt from 'jsonwebtoken';
2
+ import ApiResponse from '../utils/ApiResponse.js';
3
+ import asyncHandler from '../utils/asyncHandler.js';
4
+ import User from '../models/BaseUser.js';
5
+
6
+ const protect = asyncHandler(async (req, res, next) => {
7
+ let token = req.cookies.accessToken;
8
+
9
+ if (!token) {
10
+ return ApiResponse.error(res, 'Not authorized, token missing', 401);
11
+ }
12
+
13
+ try {
14
+ const decoded = jwt.verify(token, process.env.JWT_SECRET);
15
+ req.user = await User.findById(decoded.id);
16
+ next();
17
+ } catch (error) {
18
+ return ApiResponse.error(res, 'Token expired or invalid', 401);
19
+ }
20
+ });
21
+
22
+ export default protect;
@@ -0,0 +1 @@
1
+ // JWT signing and Refresh Token logic
@@ -0,0 +1,41 @@
1
+ import dotenv from 'dotenv';
2
+
3
+
4
+ dotenv.config();
5
+
6
+ const config = {
7
+
8
+ port: process.env.PORT || 5000,
9
+ nodeEnv: process.env.NODE_ENV || 'development',
10
+
11
+
12
+ mongoUri: process.env.MONGO_URI,
13
+
14
+
15
+ jwtSecret: process.env.JWT_SECRET || 'super-secret-key-change-me',
16
+ jwtExpire: process.env.JWT_EXPIRE || '15m',
17
+
18
+ refreshSecret: process.env.REFRESH_SECRET || 'even-more-secret-key-change-me',
19
+ refreshExpire: process.env.REFRESH_EXPIRE || '7d',
20
+
21
+
22
+ cookieExpire: process.env.COOKIE_EXPIRE || 7,
23
+
24
+
25
+ corsOrigin: process.env.CORS_ORIGIN || '*',
26
+
27
+
28
+ morganFormat: process.env.MORGAN_FORMAT || 'dev'
29
+ };
30
+
31
+
32
+ if (config.nodeEnv === 'production') {
33
+ if (!process.env.JWT_SECRET || !process.env.REFRESH_SECRET) {
34
+ console.warn('⚠️ WARNING: JWT_SECRET or REFRESH_SECRET is missing in production!');
35
+ }
36
+ if (!process.env.MONGO_URI) {
37
+ console.error('❌ ERROR: MONGO_URI is required in production!');
38
+ }
39
+ }
40
+
41
+ export default config;
@@ -0,0 +1,19 @@
1
+ import mongoose from 'mongoose';
2
+ import config from './config.js';
3
+
4
+ const connectDB = async () => {
5
+ try {
6
+ const conn = await mongoose.connect(config.mongoUri);
7
+
8
+ console.log(`🍀 MongoDB Connected: ${conn.connection.host}`);
9
+ mongoose.connection.on('error', (err) => {
10
+ console.error(`❌ MongoDB Runtime Error: ${err}`);
11
+ });
12
+
13
+ } catch (error) {
14
+ console.error(`❌ Error connecting to MongoDB: ${error.message}`);
15
+ process.exit(1);
16
+ }
17
+ };
18
+
19
+ export default connectDB;
@@ -0,0 +1,151 @@
1
+ import jwt from 'jsonwebtoken';
2
+ const { sign, verify } = jwt;
3
+
4
+ import User, { create, findOne, findById } from '../models/BaseUser.js';
5
+ import Session from '../models/Session.js';
6
+ import { success, error } from '../utils/ApiResponse.js';
7
+ import asyncHandler from '../utils/asyncHandler.js';
8
+
9
+
10
+
11
+
12
+ const sendTokenResponse = async (user, statusCode, res, req = {}) => {
13
+ const accessToken = sign({ id: user._id }, process.env.JWT_SECRET, {
14
+ expiresIn: process.env.JWT_EXPIRE || '15m'
15
+ });
16
+ const refreshToken = sign({ id: user._id }, process.env.REFRESH_SECRET, {
17
+ expiresIn: process.env.REFRESH_EXPIRE || '7d'
18
+ });
19
+
20
+ // Calculate expiration date for session
21
+ const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days matching REFRESH_EXPIRE
22
+
23
+ // Create a Session record
24
+ await Session.create({
25
+ userId: user._id,
26
+ refreshToken,
27
+ device: req.get ? req.get('User-Agent') : 'Unknown',
28
+ ip: req.ip || 'Unknown',
29
+ expiresAt
30
+ });
31
+
32
+ const cookieOptions = {
33
+ httpOnly: true,
34
+ secure: process.env.NODE_ENV === 'production',
35
+ sameSite: 'Lax',
36
+ };
37
+
38
+ res.cookie('accessToken', accessToken, {
39
+ ...cookieOptions,
40
+ expires: new Date(Date.now() + 15 * 60 * 1000)
41
+ });
42
+
43
+ res.cookie('refreshToken', refreshToken, {
44
+ ...cookieOptions,
45
+ expires: expiresAt
46
+ });
47
+
48
+ return success(res, "Authentication successful", {
49
+ id: user._id,
50
+ email: user.email
51
+ }, statusCode);
52
+ };
53
+
54
+
55
+ export const signUp = asyncHandler(async (req, res, next) => {
56
+ const user = await create(req.body);
57
+ await sendTokenResponse(user, 201, res, req);
58
+ });
59
+
60
+
61
+ export const login = asyncHandler(async (req, res, next) => {
62
+ const { email, password } = req.body;
63
+
64
+ if (!email || !password) {
65
+ return error(res, "Please provide email and password", 400);
66
+ }
67
+
68
+ const user = await findOne({ email }).select('+password');
69
+
70
+ if (!user || !(await user.comparePassword(password, user.password))) {
71
+ return error(res, "Invalid credentials", 401);
72
+ }
73
+
74
+ await sendTokenResponse(user, 200, res, req);
75
+ });
76
+
77
+
78
+ export const refresh = asyncHandler(async (req, res, next) => {
79
+ const { refreshToken } = req.cookies;
80
+
81
+ if (!refreshToken) {
82
+ return error(res, "No refresh token provided", 401);
83
+ }
84
+
85
+ // Find session record
86
+ const session = await Session.findOne({ refreshToken, isValid: true });
87
+ if (!session) {
88
+ return error(res, "Invalid or expired session", 401);
89
+ }
90
+
91
+ const decoded = verify(refreshToken, process.env.REFRESH_SECRET);
92
+ const user = await findById(decoded.id);
93
+
94
+ if (!user) {
95
+ return error(res, "User no longer exists", 401);
96
+ }
97
+
98
+ // Delete old session and create new one
99
+ await session.deleteOne();
100
+ await sendTokenResponse(user, 200, res, req);
101
+ });
102
+
103
+
104
+ export const getMe = asyncHandler(async (req, res, next) => {
105
+
106
+ return success(res, "User profile retrieved", req.user);
107
+ });
108
+
109
+ export const logout = asyncHandler(async (req, res, next) => {
110
+ const { refreshToken } = req.cookies;
111
+
112
+ if (refreshToken) {
113
+ await Session.deleteOne({ refreshToken });
114
+ }
115
+
116
+ res.cookie('accessToken', 'none', { expires: new Date(Date.now() + 10 * 1000), httpOnly: true });
117
+ res.cookie('refreshToken', 'none', { expires: new Date(Date.now() + 10 * 1000), httpOnly: true });
118
+
119
+ return success(res, "Logged out successfully");
120
+ });
121
+
122
+
123
+ export const getSessions = asyncHandler(async (req, res, next) => {
124
+ const sessions = await Session.find({ userId: req.user._id, isValid: true })
125
+ .select('-refreshToken') // Don't expose token in listing
126
+ .sort('-createdAt');
127
+
128
+ return success(res, "Active sessions retrieved", sessions);
129
+ });
130
+
131
+ export const revokeSession = asyncHandler(async (req, res, next) => {
132
+ const sessionId = req.params.id;
133
+
134
+ const session = await Session.findOne({ _id: sessionId, userId: req.user._id });
135
+ if (!session) {
136
+ return error(res, "Session not found", 404);
137
+ }
138
+
139
+ await session.deleteOne();
140
+ return success(res, "Session revoked successfully");
141
+ });
142
+
143
+
144
+ export function healthCheck(req, res) {
145
+ res.status(200).json({
146
+ status: "success",
147
+ message: "Server is healthy",
148
+ uptime: process.uptime(),
149
+ timestamp: new Date().toISOString()
150
+ });
151
+ }
@@ -0,0 +1,79 @@
1
+ import APIFeatures from './queryBuilder.js';
2
+ import ApiResponse from '../utils/ApiResponse.js';
3
+ import asyncHandler from '../utils/asyncHandler.js';
4
+
5
+ /**
6
+ * Factory functions to handle standard CRUD operations
7
+ * @param {import('mongoose').Model} Model - The Mongoose model to use
8
+ */
9
+ const factory = (Model) => ({
10
+
11
+
12
+ getAll: asyncHandler(async (req, res, next) => {
13
+ const features = new APIFeatures(Model.find(), req.query)
14
+ .filter()
15
+ .sort()
16
+ .limitFields()
17
+ .paginate();
18
+
19
+ const docs = await features.query;
20
+
21
+
22
+ const filterObj = new APIFeatures(Model.find(), req.query).filter().query.getFilter();
23
+ const total = await Model.countDocuments(filterObj);
24
+
25
+ const page = Number(req.query.page) || 1;
26
+ const limit = Number(req.query.limit) || 10;
27
+
28
+ return ApiResponse.success(res, "Resources retrieved successfully", docs, 200, {
29
+ total,
30
+ page,
31
+ limit,
32
+ totalPages: Math.ceil(total / limit)
33
+ });
34
+ }),
35
+
36
+
37
+ getOne: asyncHandler(async (req, res, next) => {
38
+ const doc = await Model.findById(req.params.id);
39
+
40
+ if (!doc) {
41
+ return ApiResponse.error(res, "No document found with that ID", 404);
42
+ }
43
+
44
+ return ApiResponse.success(res, "Resource retrieved successfully", doc);
45
+ }),
46
+
47
+
48
+ createOne: asyncHandler(async (req, res, next) => {
49
+ const doc = await Model.create(req.body);
50
+ return ApiResponse.success(res, "Resource created successfully", doc, 201);
51
+ }),
52
+
53
+
54
+ updateOne: asyncHandler(async (req, res, next) => {
55
+ const doc = await Model.findByIdAndUpdate(req.params.id, req.body, {
56
+ new: true,
57
+ runValidators: true
58
+ });
59
+
60
+ if (!doc) {
61
+ return ApiResponse.error(res, "No document found with that ID", 404);
62
+ }
63
+
64
+ return ApiResponse.success(res, "Resource updated successfully", doc);
65
+ }),
66
+
67
+
68
+ deleteOne: asyncHandler(async (req, res, next) => {
69
+ const doc = await Model.findByIdAndDelete(req.params.id);
70
+
71
+ if (!doc) {
72
+ return ApiResponse.error(res, "No document found with that ID", 404);
73
+ }
74
+
75
+ return ApiResponse.success(res, "Resource deleted successfully", null, 204);
76
+ })
77
+ });
78
+
79
+ export default factory;
@@ -0,0 +1,79 @@
1
+ class QueryBuillder {
2
+ constructor(query, queryString) {
3
+ this.query = query
4
+ this.queryString = queryString
5
+ }
6
+
7
+ filter() {
8
+ const queryObj = { ...this.queryString }
9
+
10
+ const excludedFields = ["page", "sort", "limit", "fields", "search"]
11
+
12
+
13
+ let queryStr = queryObj.JSON.stringfy(queryObj)
14
+ excludedFields.forEach(el => delete queryObj[el])
15
+
16
+ queryStr.replace(/\b(gte|gt|lte|lt)\b/g,(match)=> `$${match}`)
17
+
18
+ this.query = this.query.find(JSON.parse(queryStr))
19
+
20
+ return this
21
+
22
+
23
+
24
+ }
25
+
26
+ search(fields = []) {
27
+ if (this.queryString.search) {
28
+ const keyWords = this.queryString.search
29
+
30
+ const searchQuery = {
31
+ $or: fields.map(field => ({[field]:{$regex:keyWords}}))
32
+ }
33
+
34
+ this.query = this.query.find(searchQuery)
35
+
36
+
37
+ }
38
+
39
+ return this
40
+ }
41
+
42
+ sort() {
43
+ if (this.queryString.sort) {
44
+ const sortQuery = this.queryString.sort.split(",").join(" ")
45
+ this.query = this.query.sort(sortQuery)
46
+ }
47
+
48
+ else {
49
+ this.query = this.query.sort('-createdAt')
50
+ }
51
+
52
+ return this
53
+ }
54
+
55
+ limitFields() {
56
+ if (this.queryString.fields) {
57
+ const fields = this.queryString.fields.split(",").join(" ");
58
+ this.query = this.query.select(fields);
59
+ } else {
60
+ this.query = this.query.select("-__v");
61
+ }
62
+
63
+ return this;
64
+ }
65
+
66
+ paginate() {
67
+ const page = parseInt(this.queryString.page) || 1;
68
+ const limit = parseInt(this.queryString.limit) || 10;
69
+ const skip = (page - 1) * limit;
70
+
71
+ this.query = this.query.skip(skip).limit(limit);
72
+ return this;
73
+ }
74
+
75
+
76
+
77
+ }
78
+
79
+ export default QueryBuillder
@@ -0,0 +1,60 @@
1
+ import ApiResponse from '../utils/ApiResponse.js';
2
+
3
+ /**
4
+ * Global Error Handling Middleware
5
+ * Catch-all for every error passed via next(err)
6
+ */
7
+ const globalErrorHandler = (err, req, res, next) => {
8
+
9
+ err.statusCode = err.statusCode || 500;
10
+ err.status = err.status || 'error';
11
+
12
+
13
+
14
+ if (err.name === 'CastError') {
15
+ const message = `Invalid ${err.path}: ${err.value}.`;
16
+ return ApiResponse.error(res, message, 400);
17
+ }
18
+
19
+
20
+
21
+ if (err.code === 11000) {
22
+ const field = Object.keys(err.keyValue)[0];
23
+ const value = Object.values(err.keyValue)[0];
24
+ const message = `Duplicate field value: "${value}". This ${field} is already in use.`;
25
+ return ApiResponse.error(res, message, 400);
26
+ }
27
+
28
+
29
+
30
+ if (err.name === 'ValidationError') {
31
+ const errors = Object.values(err.errors).map(el => el.message);
32
+ const message = `Invalid input data: ${errors.join('. ')}`;
33
+ return ApiResponse.error(res, message, 400);
34
+ }
35
+
36
+
37
+ if (err.name === 'JsonWebTokenError') {
38
+ return ApiResponse.error(res, 'Invalid token. Please log in again.', 401);
39
+ }
40
+
41
+ if (err.name === 'TokenExpiredError') {
42
+ return ApiResponse.error(res, 'Your session has expired. Please refresh your token.', 401);
43
+ }
44
+
45
+
46
+ const message = err.message || 'An unexpected error occurred on the server.';
47
+
48
+
49
+
50
+ const errorDetails = process.env.NODE_ENV === 'development' ? { stack: err.stack } : null;
51
+
52
+ return ApiResponse.error(
53
+ res,
54
+ message,
55
+ err.statusCode,
56
+ errorDetails
57
+ );
58
+ };
59
+
60
+ export default globalErrorHandler;
@@ -0,0 +1 @@
1
+ // Morgan and Winston configuration
@@ -0,0 +1,37 @@
1
+ import mongoose from 'mongoose';
2
+ import bcrypt from 'bcryptjs';
3
+
4
+ const BaseUserSchema = new mongoose.Schema({
5
+ email: {
6
+ type: String,
7
+ required: [true, 'Email is required'],
8
+ unique: true,
9
+ lowercase: true,
10
+ },
11
+ password: {
12
+ type: String,
13
+ required: [true, 'Password is required'],
14
+ select: false,
15
+ }
16
+ }, { timestamps: true });
17
+
18
+
19
+
20
+ BaseUserSchema.pre('save', async function() {
21
+ if (!this.isModified('password')) return;
22
+ this.password = await bcrypt.hash(this.password, 12);
23
+ });
24
+
25
+
26
+
27
+ BaseUserSchema.methods.comparePassword = async function(candidatePassword, userPassword) {
28
+ return await bcrypt.compare(candidatePassword, userPassword);
29
+ };
30
+
31
+ const User = mongoose.model('User', BaseUserSchema);
32
+
33
+ export const create = (...args) => User.create(...args);
34
+ export const findOne = (...args) => User.findOne(...args);
35
+ export const findById = (...args) => User.findById(...args);
36
+
37
+ export default User;
@@ -0,0 +1,36 @@
1
+ import mongoose from 'mongoose';
2
+
3
+ const SessionSchema = new mongoose.Schema({
4
+ userId: {
5
+ type: mongoose.Schema.Types.ObjectId,
6
+ ref: 'User',
7
+ required: true,
8
+ },
9
+ refreshToken: {
10
+ type: String,
11
+ required: true,
12
+ unique: true,
13
+ },
14
+ device: {
15
+ type: String,
16
+ default: 'Unknown Device',
17
+ },
18
+ ip: {
19
+ type: String,
20
+ },
21
+ isValid: {
22
+ type: Boolean,
23
+ default: true,
24
+ },
25
+ expiresAt: {
26
+ type: Date,
27
+ required: true,
28
+ }
29
+ }, { timestamps: true });
30
+
31
+
32
+ SessionSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
33
+
34
+ const Session = mongoose.model('Session', SessionSchema);
35
+
36
+ export default Session;
@@ -0,0 +1,23 @@
1
+ import express from 'express';
2
+ import * as authController from '../controllers/authControllers.js';
3
+ import protect from '../auth/authMiddleware.js';
4
+
5
+ const router = express.Router();
6
+
7
+ // Public Routes
8
+ router.post('/sign-up', authController.signUp);
9
+ router.post('/login', authController.login);
10
+ router.post('/refresh', authController.refresh);
11
+ router.get('/health', authController.healthCheck);
12
+
13
+ // Protected Routes
14
+ router.use(protect); // Applies protection to all routes below this line
15
+ router.get('/me', authController.getMe);
16
+ router.post('/logout', authController.logout);
17
+
18
+ // Session Management
19
+ router.get('/sessions', authController.getSessions);
20
+ router.delete('/sessions/:id', authController.revokeSession);
21
+
22
+
23
+ export default router;
@@ -0,0 +1,46 @@
1
+ class ApiResponse {
2
+ constructor({statusCode = 200, success = true, data = null, message = ""}) {
3
+ this.statusCode = statusCode,
4
+ this.success=success
5
+ this.data = data
6
+ this.message = message
7
+
8
+ }
9
+
10
+ send(res) {
11
+ return res.status(this.statusCode).json(
12
+ {
13
+ success: this.success,
14
+ message:this.message,
15
+ data:this.data,
16
+ }
17
+ )
18
+
19
+ }
20
+
21
+ static success(res, message = "Success", data = null, statusCode = 200, meta = null) {
22
+ const response = {
23
+ success: true,
24
+ message,
25
+ data,
26
+ ...(meta && { meta })
27
+ };
28
+ return res.status(statusCode).json(response);
29
+ }
30
+
31
+ static error(res, message = "Error", statusCode = 500, data = null) {
32
+ const response = {
33
+ success: false,
34
+ message,
35
+ ...(data && { data })
36
+ };
37
+ return res.status(statusCode).json(response);
38
+ }
39
+
40
+ }
41
+
42
+ export default ApiResponse;
43
+
44
+ // Named exports for convenience
45
+ export const success = ApiResponse.success;
46
+ export const error = ApiResponse.error;
@@ -0,0 +1,5 @@
1
+ const asyncHandler = fn => (req, res, next) => {
2
+ return Promise.resolve(fn(req,res,next)).catch(next)
3
+ }
4
+
5
+ export default asyncHandler