create-theta-code 1.0.1

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 (68) hide show
  1. package/bin/create.js +9 -0
  2. package/package.json +34 -0
  3. package/src/cli.js +21 -0
  4. package/src/generators/scaffoldProject.js +46 -0
  5. package/src/prompts/getProjectName.js +30 -0
  6. package/src/prompts/getTemplateChoice.js +39 -0
  7. package/src/utils/logger.js +29 -0
  8. package/templates/mongo-js/.env +12 -0
  9. package/templates/mongo-js/.env.example +13 -0
  10. package/templates/mongo-js/.eslintrc.json +24 -0
  11. package/templates/mongo-js/.prettierrc +9 -0
  12. package/templates/mongo-js/README.md +429 -0
  13. package/templates/mongo-js/_env.example +13 -0
  14. package/templates/mongo-js/_gitignore +22 -0
  15. package/templates/mongo-js/package-lock.json +4671 -0
  16. package/templates/mongo-js/package.json +48 -0
  17. package/templates/mongo-js/server.js +67 -0
  18. package/templates/mongo-js/src/config/app.config.js +72 -0
  19. package/templates/mongo-js/src/config/db.config.js +32 -0
  20. package/templates/mongo-js/src/config/env.config.js +49 -0
  21. package/templates/mongo-js/src/config/rateLimiter.config.js +32 -0
  22. package/templates/mongo-js/src/middlewares/auth.middleware.js +20 -0
  23. package/templates/mongo-js/src/middlewares/error.middleware.js +61 -0
  24. package/templates/mongo-js/src/middlewares/notFound.middleware.js +11 -0
  25. package/templates/mongo-js/src/middlewares/requestId.middleware.js +10 -0
  26. package/templates/mongo-js/src/middlewares/requireRole.middleware.js +13 -0
  27. package/templates/mongo-js/src/middlewares/validate.middleware.js +21 -0
  28. package/templates/mongo-js/src/modules/user/user.controller.js +88 -0
  29. package/templates/mongo-js/src/modules/user/user.model.js +45 -0
  30. package/templates/mongo-js/src/modules/user/user.repository.js +47 -0
  31. package/templates/mongo-js/src/modules/user/user.routes.js +32 -0
  32. package/templates/mongo-js/src/modules/user/user.service.js +87 -0
  33. package/templates/mongo-js/src/modules/user/user.validator.js +28 -0
  34. package/templates/mongo-js/src/utils/AppError.js +15 -0
  35. package/templates/mongo-js/src/utils/apiResponse.js +23 -0
  36. package/templates/mongo-js/src/utils/asyncHandler.js +7 -0
  37. package/templates/mongo-js/src/utils/constants.js +16 -0
  38. package/templates/mongo-js/src/utils/jwt.utils.js +40 -0
  39. package/templates/mongo-js/tests/integration/user.routes.test.js +111 -0
  40. package/templates/mongo-js/tests/unit/user.service.test.js +96 -0
  41. package/templates/pg-js/.eslintrc.json +24 -0
  42. package/templates/pg-js/.prettierrc +9 -0
  43. package/templates/pg-js/_env.example +7 -0
  44. package/templates/pg-js/_gitignore +20 -0
  45. package/templates/pg-js/package.json +50 -0
  46. package/templates/pg-js/prisma/schema.prisma +23 -0
  47. package/templates/pg-js/server.js +63 -0
  48. package/templates/pg-js/src/config/app.config.js +48 -0
  49. package/templates/pg-js/src/config/db.config.js +30 -0
  50. package/templates/pg-js/src/config/env.config.js +36 -0
  51. package/templates/pg-js/src/config/rateLimiter.config.js +22 -0
  52. package/templates/pg-js/src/middlewares/auth.middleware.js +32 -0
  53. package/templates/pg-js/src/middlewares/error.middleware.js +50 -0
  54. package/templates/pg-js/src/middlewares/notFound.middleware.js +11 -0
  55. package/templates/pg-js/src/middlewares/validate.middleware.js +21 -0
  56. package/templates/pg-js/src/modules/user/user.controller.js +57 -0
  57. package/templates/pg-js/src/modules/user/user.model.js +20 -0
  58. package/templates/pg-js/src/modules/user/user.repository.js +105 -0
  59. package/templates/pg-js/src/modules/user/user.routes.js +27 -0
  60. package/templates/pg-js/src/modules/user/user.service.js +81 -0
  61. package/templates/pg-js/src/modules/user/user.validator.js +22 -0
  62. package/templates/pg-js/src/utils/AppError.js +14 -0
  63. package/templates/pg-js/src/utils/apiResponse.js +23 -0
  64. package/templates/pg-js/src/utils/asyncHandler.js +7 -0
  65. package/templates/pg-js/src/utils/constants.js +24 -0
  66. package/templates/pg-js/src/utils/jwt.utils.js +39 -0
  67. package/templates/pg-js/tests/integration/user.routes.test.js +95 -0
  68. package/templates/pg-js/tests/unit/user.service.test.js +96 -0
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "my-awesome-app",
3
+ "version": "1.0.0",
4
+ "description": "Production-grade Node.js API built with create-theta-code",
5
+ "type": "module",
6
+ "main": "server.js",
7
+ "scripts": {
8
+ "start": "node server.js",
9
+ "dev": "nodemon server.js",
10
+ "test": "vitest",
11
+ "test:coverage": "vitest --coverage",
12
+ "lint": "eslint src/",
13
+ "lint:fix": "eslint src/ --fix",
14
+ "format": "prettier --write \"src/**/*.js\"",
15
+ "migrate": "echo 'Run migrations here'"
16
+ },
17
+ "keywords": [
18
+ "nodejs",
19
+ "express",
20
+ "mongodb",
21
+ "api"
22
+ ],
23
+ "author": "Arpit Shukla <arpit@techvault.in>",
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "bcryptjs": "^2.4.3",
27
+ "cookie-parser": "^1.4.7",
28
+ "cors": "^2.8.5",
29
+ "dotenv": "^16.3.1",
30
+ "express": "^4.18.2",
31
+ "express-rate-limit": "^7.1.5",
32
+ "helmet": "^7.1.0",
33
+ "jsonwebtoken": "^9.0.2",
34
+ "mongoose": "^8.0.0",
35
+ "zod": "^3.22.4"
36
+ },
37
+ "devDependencies": {
38
+ "eslint": "^8.54.0",
39
+ "morgan": "^1.10.0",
40
+ "nodemon": "^3.0.1",
41
+ "prettier": "^3.1.0",
42
+ "supertest": "^6.3.3",
43
+ "vitest": "^0.34.6"
44
+ },
45
+ "engines": {
46
+ "node": ">=18.0.0"
47
+ }
48
+ }
@@ -0,0 +1,67 @@
1
+ // server.js — HTTP server startup and graceful shutdown. Zero other logic.
2
+
3
+ import { createApp } from './src/config/app.config.js';
4
+ import { connectDB, disconnectDB } from './src/config/db.config.js';
5
+ import envConfig from './src/config/env.config.js';
6
+
7
+ let server;
8
+
9
+ async function startServer() {
10
+ try {
11
+ // Connect to database
12
+ await connectDB();
13
+
14
+ // Create Express app
15
+ const app = createApp();
16
+
17
+ // Start HTTP server
18
+ server = app.listen(envConfig.PORT, () => {
19
+ console.log(`✅ Server running on port ${envConfig.PORT}`);
20
+ });
21
+ } catch (err) {
22
+ console.error('❌ Failed to start server:', err.message);
23
+ process.exit(1);
24
+ }
25
+ }
26
+
27
+ const shutdown = async (signal) => {
28
+ console.log(`\n${signal} received. Shutting down gracefully...`);
29
+
30
+ // Force exit if graceful shutdown takes too long
31
+ const forceExit = setTimeout(() => {
32
+ console.error('Forced shutdown: graceful shutdown timed out after 10s');
33
+ process.exit(1);
34
+ }, 10000);
35
+ forceExit.unref(); // Don't keep process alive just for this timer
36
+
37
+ if (server) {
38
+ server.close(async () => {
39
+ console.log('HTTP server closed.');
40
+ await disconnectDB();
41
+ clearTimeout(forceExit);
42
+ process.exit(0);
43
+ });
44
+ } else {
45
+ await disconnectDB();
46
+ clearTimeout(forceExit);
47
+ process.exit(0);
48
+ }
49
+ };
50
+
51
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
52
+ process.on('SIGINT', () => shutdown('SIGINT'));
53
+
54
+ // Unhandled promise rejections
55
+ process.on('unhandledRejection', (reason, promise) => {
56
+ console.error('❌ Unhandled Rejection at:', promise, 'reason:', reason);
57
+ process.exit(1);
58
+ });
59
+
60
+ // Uncaught exceptions
61
+ process.on('uncaughtException', (err) => {
62
+ console.error('❌ Uncaught Exception:', err);
63
+ process.exit(1);
64
+ });
65
+
66
+ // Start server
67
+ startServer();
@@ -0,0 +1,72 @@
1
+ // src/config/app.config.js — Express app factory. Middleware registration. Middleware order sacred.
2
+
3
+ import express from 'express';
4
+ import helmet from 'helmet';
5
+ import cors from 'cors';
6
+ import morgan from 'morgan';
7
+ import cookieParser from 'cookie-parser';
8
+ import envConfig from './env.config.js';
9
+ import { globalRateLimiter } from './rateLimiter.config.js';
10
+ import { notFoundMiddleware } from '../middlewares/notFound.middleware.js';
11
+ import { errorMiddleware } from '../middlewares/error.middleware.js';
12
+ import { requestIdMiddleware } from '../middlewares/requestId.middleware.js';
13
+ import { REQUEST_BODY_LIMIT } from '../utils/constants.js';
14
+ import userRoutes from '../modules/user/user.routes.js';
15
+
16
+ function createApp() {
17
+ const app = express();
18
+
19
+ // Request correlation ID middleware must be first for tracing
20
+ app.use(requestIdMiddleware);
21
+
22
+ // Security middleware
23
+ app.use(helmet());
24
+ app.use(cookieParser());
25
+ app.use(cors({
26
+ origin: (origin, callback) => {
27
+ if (!origin) return callback(null, true); // allow server-to-server
28
+ const allowed = envConfig.CORS_ORIGIN.split(',').map(o => o.trim());
29
+ if (allowed.includes('*') || allowed.includes(origin)) {
30
+ return callback(null, true);
31
+ }
32
+ return callback(new Error(`CORS: origin ${origin} not allowed`));
33
+ },
34
+ credentials: true,
35
+ }));
36
+
37
+ // Body parser with 10kb limit
38
+ app.use(express.json({ limit: REQUEST_BODY_LIMIT }));
39
+ app.use(express.urlencoded({ limit: REQUEST_BODY_LIMIT }));
40
+
41
+ // Logging in development only
42
+ if (envConfig.NODE_ENV === 'development') {
43
+ app.use(morgan('dev'));
44
+ }
45
+
46
+ // Health check (before rate limiter)
47
+ app.get('/health', (_req, res) => {
48
+ res.status(200).json({
49
+ status: 'UP',
50
+ environment: envConfig.NODE_ENV,
51
+ uptime: process.uptime(),
52
+ timestamp: new Date().toISOString(),
53
+ memory: process.memoryUsage(),
54
+ });
55
+ });
56
+
57
+ // Rate limiter on all /api routes
58
+ app.use('/api/', globalRateLimiter);
59
+
60
+ // API routes
61
+ app.use('/api/v1/users', userRoutes);
62
+
63
+ // 404 handler
64
+ app.use(notFoundMiddleware);
65
+
66
+ // Global error handler (must be last)
67
+ app.use(errorMiddleware);
68
+
69
+ return app;
70
+ }
71
+
72
+ export { createApp };
@@ -0,0 +1,32 @@
1
+ // src/config/db.config.js — MongoDB connection. Mongoose initialization and connection pooling.
2
+
3
+ import mongoose from 'mongoose';
4
+ import envConfig from './env.config.js';
5
+
6
+ async function connectDB() {
7
+ try {
8
+ const mongooseInstance = await mongoose.connect(envConfig.MONGODB_URI, {
9
+ maxPoolSize: 10,
10
+ minPoolSize: 5,
11
+ serverSelectionTimeoutMS: 5000,
12
+ socketTimeoutMS: 45000,
13
+ });
14
+
15
+ console.log('✅ MongoDB connected successfully');
16
+ return mongooseInstance;
17
+ } catch (err) {
18
+ console.error('❌ MongoDB connection failed:', err.message);
19
+ process.exit(1);
20
+ }
21
+ }
22
+
23
+ async function disconnectDB() {
24
+ try {
25
+ await mongoose.disconnect();
26
+ console.log('✅ MongoDB disconnected');
27
+ } catch (err) {
28
+ console.error('❌ MongoDB disconnection failed:', err.message);
29
+ }
30
+ }
31
+
32
+ export { connectDB, disconnectDB };
@@ -0,0 +1,49 @@
1
+ // src/config/env.config.js — Validates ALL required env vars at startup. App refuses to boot if missing.
2
+
3
+ import 'dotenv/config';
4
+ import { z } from 'zod';
5
+
6
+ const envSchema = z.object({
7
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
8
+ PORT: z.string().transform(Number).default('3000'),
9
+ MONGODB_URI: z.string().url('Invalid MongoDB URI'),
10
+ JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
11
+ JWT_REFRESH_SECRET: z.string().min(32, 'JWT_REFRESH_SECRET must be at least 32 characters'),
12
+ JWT_EXPIRES_IN: z.string().default('7d'),
13
+ JWT_REFRESH_EXPIRES_IN: z.string().default('30d'),
14
+ BCRYPT_SALT_ROUNDS: z.string().transform(Number).default('12'),
15
+ RATE_LIMIT_WINDOW_MS: z.string().transform(Number).default('900000'),
16
+ RATE_LIMIT_MAX: z.string().transform(Number).default('100'),
17
+ AUTH_RATE_LIMIT_MAX: z.string().transform(Number).default('10'),
18
+ CORS_ORIGIN: z.string().default('http://localhost:3000'),
19
+ LOG_LEVEL: z.enum(['error', 'warn', 'info', 'debug']).default('info'),
20
+ });
21
+
22
+ let envConfig;
23
+
24
+ try {
25
+ const rawEnv = {
26
+ NODE_ENV: process.env.NODE_ENV,
27
+ PORT: process.env.PORT,
28
+ MONGODB_URI: process.env.MONGODB_URI,
29
+ JWT_SECRET: process.env.JWT_SECRET,
30
+ JWT_REFRESH_SECRET: process.env.JWT_REFRESH_SECRET,
31
+ JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN,
32
+ JWT_REFRESH_EXPIRES_IN: process.env.JWT_REFRESH_EXPIRES_IN,
33
+ BCRYPT_SALT_ROUNDS: process.env.BCRYPT_SALT_ROUNDS,
34
+ RATE_LIMIT_WINDOW_MS: process.env.RATE_LIMIT_WINDOW_MS,
35
+ RATE_LIMIT_MAX: process.env.RATE_LIMIT_MAX,
36
+ AUTH_RATE_LIMIT_MAX: process.env.AUTH_RATE_LIMIT_MAX,
37
+ CORS_ORIGIN: process.env.CORS_ORIGIN,
38
+ LOG_LEVEL: process.env.LOG_LEVEL,
39
+ };
40
+
41
+ envConfig = envSchema.parse(rawEnv);
42
+ Object.freeze(envConfig);
43
+ } catch (err) {
44
+ console.error('❌ Environment validation failed:');
45
+ console.error(err.errors || err.message);
46
+ process.exit(1);
47
+ }
48
+
49
+ export default envConfig;
@@ -0,0 +1,32 @@
1
+ // src/config/rateLimiter.config.js — Global and auth-specific rate limiters. Non-negotiable.
2
+
3
+ import rateLimit from 'express-rate-limit';
4
+ import envConfig from './env.config.js';
5
+
6
+ const globalRateLimiter = rateLimit({
7
+ windowMs: envConfig.RATE_LIMIT_WINDOW_MS,
8
+ max: envConfig.RATE_LIMIT_MAX,
9
+ message: {
10
+ success: false,
11
+ message: 'Too many requests, please try again later.',
12
+ data: null,
13
+ timestamp: new Date().toISOString(),
14
+ },
15
+ standardHeaders: true,
16
+ legacyHeaders: false,
17
+ });
18
+
19
+ const authRateLimiter = rateLimit({
20
+ windowMs: envConfig.RATE_LIMIT_WINDOW_MS,
21
+ max: envConfig.AUTH_RATE_LIMIT_MAX,
22
+ message: {
23
+ success: false,
24
+ message: 'Too many login attempts, please try again later.',
25
+ data: null,
26
+ timestamp: new Date().toISOString(),
27
+ },
28
+ standardHeaders: true,
29
+ legacyHeaders: false,
30
+ });
31
+
32
+ export { globalRateLimiter, authRateLimiter };
@@ -0,0 +1,20 @@
1
+ // src/middlewares/auth.middleware.js — Verifies JWT from Authorization header. Attaches req.user.
2
+
3
+ import { HTTP_STATUS } from '../utils/constants.js';
4
+ import { AppError } from '../utils/AppError.js';
5
+ import { verifyToken } from '../utils/jwt.utils.js';
6
+ import { asyncHandler } from '../utils/asyncHandler.js';
7
+
8
+ const authMiddleware = asyncHandler((req, res, next) => {
9
+ const token = req.cookies?.accessToken || req.headers.authorization?.replace('Bearer ', '');
10
+
11
+ if (!token) {
12
+ throw new AppError('Authentication required', HTTP_STATUS.UNAUTHORIZED);
13
+ }
14
+
15
+ const decoded = verifyToken(token);
16
+ req.user = decoded;
17
+ next();
18
+ });
19
+
20
+ export { authMiddleware };
@@ -0,0 +1,61 @@
1
+ // src/middlewares/error.middleware.js — Global error handler. Operational vs programmer errors. No stack in prod.
2
+
3
+ import { HTTP_STATUS } from '../utils/constants.js';
4
+ import { sendError } from '../utils/apiResponse.js';
5
+
6
+ const errorMiddleware = (err, req, res, next) => {
7
+ const isDevelopment = process.env.NODE_ENV === 'development';
8
+
9
+ if (req?.requestId) {
10
+ res.setHeader('X-Request-Id', req.requestId);
11
+ }
12
+
13
+ // Mongoose CastError
14
+ if (err.name === 'CastError') {
15
+ return sendError(
16
+ res,
17
+ HTTP_STATUS.BAD_REQUEST,
18
+ `Invalid ${err.path}: ${err.value}`,
19
+ isDevelopment ? err : null
20
+ );
21
+ }
22
+
23
+ // Mongoose ValidationError
24
+ if (err.name === 'ValidationError') {
25
+ const messages = Object.values(err.errors).map((e) => e.message);
26
+ return sendError(res, HTTP_STATUS.BAD_REQUEST, messages.join(', '), isDevelopment ? err : null);
27
+ }
28
+
29
+ // Mongoose duplicate key (11000)
30
+ if (err.code === 11000) {
31
+ const field = Object.keys(err.keyValue)[0];
32
+ const message = `${field} already in use`;
33
+ return sendError(res, HTTP_STATUS.CONFLICT, message, isDevelopment ? err : null);
34
+ }
35
+
36
+ // JWT TokenExpiredError
37
+ if (err.name === 'TokenExpiredError') {
38
+ return sendError(res, HTTP_STATUS.UNAUTHORIZED, 'Token expired', isDevelopment ? err : null);
39
+ }
40
+
41
+ // JWT JsonWebTokenError
42
+ if (err.name === 'JsonWebTokenError') {
43
+ return sendError(res, HTTP_STATUS.UNAUTHORIZED, 'Invalid token', isDevelopment ? err : null);
44
+ }
45
+
46
+ // AppError (operational)
47
+ if (err.isOperational) {
48
+ return sendError(res, err.statusCode, err.message, isDevelopment ? { stack: err.stack } : null);
49
+ }
50
+
51
+ // Programmer error (unhandled)
52
+ console.error('UNHANDLED ERROR:', err);
53
+ return sendError(
54
+ res,
55
+ HTTP_STATUS.INTERNAL_SERVER_ERROR,
56
+ 'Internal server error',
57
+ isDevelopment ? { error: err.message, stack: err.stack } : null
58
+ );
59
+ };
60
+
61
+ export { errorMiddleware };
@@ -0,0 +1,11 @@
1
+ // src/middlewares/notFound.middleware.js — Catches 404s. Creates AppError, passes to error handler.
2
+
3
+ import { HTTP_STATUS } from '../utils/constants.js';
4
+ import { AppError } from '../utils/AppError.js';
5
+
6
+ const notFoundMiddleware = (req, res, next) => {
7
+ const error = new AppError(`Route ${req.originalUrl} not found`, HTTP_STATUS.NOT_FOUND);
8
+ next(error);
9
+ };
10
+
11
+ export { notFoundMiddleware };
@@ -0,0 +1,10 @@
1
+ // src/middlewares/requestId.middleware.js — Adds request correlation ID for tracing.
2
+
3
+ import crypto from 'node:crypto';
4
+
5
+ export const requestIdMiddleware = (req, res, next) => {
6
+ const requestId = crypto.randomUUID();
7
+ req.requestId = requestId;
8
+ res.setHeader('X-Request-Id', requestId);
9
+ next();
10
+ };
@@ -0,0 +1,13 @@
1
+ // src/middlewares/requireRole.middleware.js — Role-based access control guard.
2
+ // Responsibility: Verify req.user.role matches allowed roles.
3
+ // Must be used AFTER authMiddleware — never standalone.
4
+
5
+ import { AppError } from '../utils/AppError.js';
6
+ import { HTTP_STATUS } from '../utils/constants.js';
7
+
8
+ export const requireRole = (...roles) => (req, res, next) => {
9
+ if (!req.user || !roles.includes(req.user.role)) {
10
+ return next(new AppError('Access denied. Insufficient permissions.', HTTP_STATUS.FORBIDDEN));
11
+ }
12
+ next();
13
+ };
@@ -0,0 +1,21 @@
1
+ // src/middlewares/validate.middleware.js — Validates req.body with Zod schema. 400 on fail.
2
+
3
+ import { HTTP_STATUS } from '../utils/constants.js';
4
+ import { sendError } from '../utils/apiResponse.js';
5
+
6
+ const validate = (schema) => (req, res, next) => {
7
+ const result = schema.safeParse(req.body);
8
+
9
+ if (!result.success) {
10
+ const errors = result.error.errors.map((err) => ({
11
+ field: err.path.join('.'),
12
+ message: err.message,
13
+ }));
14
+ return sendError(res, HTTP_STATUS.BAD_REQUEST, 'Validation failed', errors);
15
+ }
16
+
17
+ req.body = result.data;
18
+ next();
19
+ };
20
+
21
+ export { validate };
@@ -0,0 +1,88 @@
1
+ // src/modules/user/user.controller.js — HTTP only. Read req, call service, send res. No business logic.
2
+
3
+ import { HTTP_STATUS } from '../../utils/constants.js';
4
+ import { sendSuccess, sendError } from '../../utils/apiResponse.js';
5
+ import { asyncHandler } from '../../utils/asyncHandler.js';
6
+ import envConfig from '../../config/env.config.js';
7
+ import * as userService from './user.service.js';
8
+
9
+ const register = asyncHandler(async (req, res) => {
10
+ const { email, firstName, lastName, password } = req.body;
11
+
12
+ const user = await userService.register(email, firstName, lastName, password);
13
+
14
+ return sendSuccess(res, HTTP_STATUS.CREATED, 'User registered successfully', {
15
+ user,
16
+ });
17
+ });
18
+
19
+ const login = asyncHandler(async (req, res) => {
20
+ const { email, password } = req.body;
21
+
22
+ const result = await userService.login(email, password);
23
+
24
+ res.cookie('accessToken', result.token, {
25
+ httpOnly: true,
26
+ secure: envConfig.NODE_ENV === 'production',
27
+ sameSite: 'strict',
28
+ maxAge: 7 * 24 * 60 * 60 * 1000,
29
+ });
30
+
31
+ res.cookie('refreshToken', result.refreshToken, {
32
+ httpOnly: true,
33
+ secure: envConfig.NODE_ENV === 'production',
34
+ sameSite: 'strict',
35
+ maxAge: 30 * 24 * 60 * 60 * 1000,
36
+ });
37
+
38
+ return sendSuccess(res, HTTP_STATUS.OK, 'Login successful', {
39
+ user: result.user,
40
+ });
41
+ });
42
+
43
+ const getAllUsers = asyncHandler(async (req, res) => {
44
+ const rawLimit = Number(req.query.limit);
45
+ const rawSkip = Number(req.query.skip);
46
+ const limit = Math.min(isNaN(rawLimit) || rawLimit < 1 ? 10 : rawLimit, 100);
47
+ const skip = isNaN(rawSkip) || rawSkip < 0 ? 0 : rawSkip;
48
+
49
+ const users = await userService.getAllUsers(limit, skip);
50
+
51
+ return sendSuccess(res, HTTP_STATUS.OK, 'Users retrieved', { users });
52
+ });
53
+
54
+ const logout = asyncHandler(async (req, res) => {
55
+ res.clearCookie('accessToken');
56
+ res.clearCookie('refreshToken');
57
+
58
+ return sendSuccess(res, HTTP_STATUS.OK, 'Logged out successfully', null);
59
+ });
60
+
61
+ const getProfile = asyncHandler(async (req, res) => {
62
+ const user = await userService.getUserById(req.user.id);
63
+
64
+ return sendSuccess(res, HTTP_STATUS.OK, 'User profile retrieved', {
65
+ user,
66
+ });
67
+ });
68
+
69
+ const updateProfile = asyncHandler(async (req, res) => {
70
+ const { firstName, lastName } = req.body;
71
+
72
+ const user = await userService.updateUser(req.user.id, {
73
+ firstName,
74
+ lastName,
75
+ });
76
+
77
+ return sendSuccess(res, HTTP_STATUS.OK, 'User profile updated', {
78
+ user,
79
+ });
80
+ });
81
+
82
+ const deleteAccount = asyncHandler(async (req, res) => {
83
+ await userService.deleteUser(req.user.id);
84
+
85
+ return sendSuccess(res, HTTP_STATUS.OK, 'Account deleted successfully', null);
86
+ });
87
+
88
+ export { register, login, logout, getAllUsers, getProfile, updateProfile, deleteAccount };
@@ -0,0 +1,45 @@
1
+ // src/modules/user/user.model.js — Mongoose schema. DB layer only. No business logic.
2
+
3
+ import mongoose from 'mongoose';
4
+
5
+ const userSchema = new mongoose.Schema(
6
+ {
7
+ email: {
8
+ type: String,
9
+ required: [true, 'Email is required'],
10
+ unique: true,
11
+ lowercase: true,
12
+ trim: true,
13
+ match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, 'Invalid email format'],
14
+ },
15
+ firstName: {
16
+ type: String,
17
+ required: [true, 'First name is required'],
18
+ trim: true,
19
+ },
20
+ lastName: {
21
+ type: String,
22
+ required: [true, 'Last name is required'],
23
+ trim: true,
24
+ },
25
+ password: {
26
+ type: String,
27
+ required: [true, 'Password is required'],
28
+ select: false,
29
+ },
30
+ role: {
31
+ type: String,
32
+ enum: ['user', 'admin'],
33
+ default: 'user',
34
+ },
35
+ isActive: {
36
+ type: Boolean,
37
+ default: true,
38
+ },
39
+ },
40
+ { timestamps: true, versionKey: false, collection: 'users' }
41
+ );
42
+
43
+ const User = mongoose.model('User', userSchema);
44
+
45
+ export { User };
@@ -0,0 +1,47 @@
1
+ // src/modules/user/user.repository.js — DB queries only. Never business logic, never HTTP.
2
+
3
+ import { User } from './user.model.js';
4
+
5
+ async function findById(id) {
6
+ return User.findById(id).select('-password');
7
+ }
8
+
9
+ async function findByEmail(email) {
10
+ return User.findOne({ email }).select('+password');
11
+ }
12
+
13
+ async function findByEmailPublic(email) {
14
+ return User.findOne({ email }).select('-password');
15
+ }
16
+
17
+ async function create(userData) {
18
+ const user = new User(userData);
19
+ await user.save();
20
+ return user.toObject({ transform: (doc, ret) => { delete ret.password; return ret; } });
21
+ }
22
+
23
+ async function updateById(id, updateData) {
24
+ return User.findByIdAndUpdate(id, updateData, { new: true, runValidators: true }).select('-password');
25
+ }
26
+
27
+ async function deleteById(id) {
28
+ return User.findByIdAndDelete(id);
29
+ }
30
+
31
+ async function findAll(limit = 10, skip = 0) {
32
+ return User.find()
33
+ .select('-password')
34
+ .limit(limit)
35
+ .skip(skip)
36
+ .sort({ createdAt: -1 });
37
+ }
38
+
39
+ export {
40
+ findById,
41
+ findByEmail,
42
+ findByEmailPublic,
43
+ create,
44
+ updateById,
45
+ deleteById,
46
+ findAll,
47
+ };
@@ -0,0 +1,32 @@
1
+ // src/modules/user/user.routes.js — Route definitions. Middleware chain per route. Validation before controller.
2
+
3
+ import { Router } from 'express';
4
+ import { validate } from '../../middlewares/validate.middleware.js';
5
+ import { authMiddleware } from '../../middlewares/auth.middleware.js';
6
+ import { requireRole } from '../../middlewares/requireRole.middleware.js';
7
+ import { authRateLimiter } from '../../config/rateLimiter.config.js';
8
+ import { registerSchema, loginSchema, updateUserSchema } from './user.validator.js';
9
+ import {
10
+ register,
11
+ login,
12
+ logout,
13
+ getAllUsers,
14
+ getProfile,
15
+ updateProfile,
16
+ deleteAccount,
17
+ } from './user.controller.js';
18
+
19
+ const router = Router();
20
+
21
+ // Public routes
22
+ router.post('/register', authRateLimiter, validate(registerSchema), register);
23
+ router.post('/login', authRateLimiter, validate(loginSchema), login);
24
+
25
+ // Protected routes
26
+ router.get('/users', authMiddleware, requireRole('admin'), getAllUsers);
27
+ router.post('/logout', authMiddleware, logout);
28
+ router.get('/profile', authMiddleware, getProfile);
29
+ router.put('/profile', authMiddleware, validate(updateUserSchema), updateProfile);
30
+ router.delete('/account', authMiddleware, deleteAccount);
31
+
32
+ export default router;