exp-node-server 1.1.7 → 2.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.
Files changed (181) hide show
  1. package/README.md +82 -20
  2. package/dist/cli.js +525 -0
  3. package/package.json +29 -9
  4. package/templates/mongoose/.dockerignore +10 -0
  5. package/templates/mongoose/.env.example +56 -7
  6. package/templates/mongoose/.prettierignore +3 -0
  7. package/templates/mongoose/.prettierrc +7 -0
  8. package/templates/mongoose/Dockerfile +33 -0
  9. package/templates/mongoose/_gitignore +32 -0
  10. package/templates/mongoose/docker-compose.yml +21 -0
  11. package/templates/mongoose/eslint.config.js +35 -0
  12. package/templates/mongoose/package-lock.json +6377 -0
  13. package/templates/mongoose/package.json +49 -10
  14. package/templates/mongoose/readme.md +85 -0
  15. package/templates/mongoose/src/app/configs/index.ts +178 -26
  16. package/templates/mongoose/src/app/errors/zodError.ts +16 -16
  17. package/templates/mongoose/src/app/lib/db.ts +18 -0
  18. package/templates/mongoose/src/app/lib/logger.ts +12 -0
  19. package/templates/mongoose/src/app/middlewares/auth.ts +48 -44
  20. package/templates/mongoose/src/app/middlewares/global_error_handler.ts +59 -65
  21. package/templates/mongoose/src/app/middlewares/not_found_api.ts +7 -7
  22. package/templates/mongoose/src/app/middlewares/rate_limiter.ts +153 -0
  23. package/templates/mongoose/src/app/middlewares/request_validator.ts +11 -10
  24. package/templates/mongoose/src/app/middlewares/uploader.ts +17 -13
  25. package/templates/mongoose/src/app/modules/account/account.controller.ts +156 -0
  26. package/templates/mongoose/src/app/modules/account/account.interface.ts +65 -0
  27. package/templates/mongoose/src/app/modules/account/account.route.ts +101 -0
  28. package/templates/mongoose/src/app/modules/account/account.schema.ts +97 -0
  29. package/templates/mongoose/src/app/modules/account/account.service.ts +422 -0
  30. package/templates/mongoose/src/app/modules/account/account.swagger.ts +278 -0
  31. package/templates/mongoose/src/app/modules/account/account.validation.ts +63 -0
  32. package/templates/mongoose/src/app/modules/profile/profile.controller.ts +17 -0
  33. package/templates/mongoose/src/app/modules/profile/profile.interface.ts +7 -0
  34. package/templates/mongoose/src/app/modules/profile/profile.route.ts +28 -0
  35. package/templates/mongoose/src/app/modules/profile/profile.schema.ts +13 -0
  36. package/templates/mongoose/src/app/modules/profile/profile.service.ts +28 -0
  37. package/templates/mongoose/src/app/modules/profile/profile.swagger.ts +34 -0
  38. package/templates/mongoose/src/app/modules/profile/profile.validation.ts +8 -0
  39. package/templates/mongoose/src/app/passport-js/index.ts +87 -0
  40. package/templates/mongoose/src/app/passport-js/oauth-state.ts +150 -0
  41. package/templates/mongoose/src/app/passport-js/providers.ts +258 -0
  42. package/templates/mongoose/src/app/passport-js/social-account.ts +141 -0
  43. package/templates/mongoose/src/app/queues/agenda.ts +24 -0
  44. package/templates/mongoose/src/app/queues/email.queue.ts +53 -0
  45. package/templates/mongoose/src/app/queues/index.ts +13 -0
  46. package/templates/mongoose/src/app/templates/otpEmailTemplate.ts +185 -0
  47. package/templates/mongoose/src/app/templates/resetPasswordEmailTemplate.ts +157 -0
  48. package/templates/mongoose/src/app/types/error.d.ts +7 -7
  49. package/templates/mongoose/src/app/types/express.d.ts +11 -0
  50. package/templates/mongoose/src/app/types/passport-oauth1.d.ts +51 -0
  51. package/templates/mongoose/src/app/utils/JWT.ts +38 -18
  52. package/templates/mongoose/src/app/utils/app_error.ts +10 -10
  53. package/templates/mongoose/src/app/utils/auth_cookie.ts +27 -0
  54. package/templates/mongoose/src/app/utils/catch_async.ts +8 -8
  55. package/templates/mongoose/src/app/utils/cloudinary.ts +26 -59
  56. package/templates/mongoose/src/app/utils/mail_sender.ts +26 -92
  57. package/templates/mongoose/src/app/utils/manage_response.ts +19 -19
  58. package/templates/mongoose/src/app/utils/otp.ts +20 -0
  59. package/templates/mongoose/src/app/utils/session.ts +55 -0
  60. package/templates/mongoose/src/app.ts +73 -42
  61. package/templates/mongoose/src/routes.ts +13 -13
  62. package/templates/mongoose/src/server.ts +38 -18
  63. package/templates/mongoose/src/swaggerOptions.ts +23 -23
  64. package/templates/mongoose/tests/app.test.ts +97 -0
  65. package/templates/mongoose/tests/global-setup.ts +15 -0
  66. package/templates/mongoose/tests/helpers/fixtures.ts +42 -0
  67. package/templates/mongoose/tests/routes/account.test.ts +614 -0
  68. package/templates/mongoose/tests/routes/profile.test.ts +95 -0
  69. package/templates/mongoose/tests/routes/social.test.ts +310 -0
  70. package/templates/mongoose/tests/setup.ts +25 -0
  71. package/templates/mongoose/tsconfig.json +16 -20
  72. package/templates/mongoose/vitest.config.ts +43 -0
  73. package/templates/prisma/.dockerignore +2 -0
  74. package/templates/prisma/.env.example +62 -6
  75. package/templates/prisma/.prettierignore +3 -0
  76. package/templates/prisma/.prettierrc +7 -0
  77. package/templates/prisma/.projectrc.json +1 -1
  78. package/templates/prisma/Dockerfile +21 -7
  79. package/templates/prisma/_gitignore +35 -0
  80. package/templates/prisma/docker-compose.yml +8 -7
  81. package/templates/prisma/eslint.config.js +35 -0
  82. package/templates/prisma/package-lock.json +7327 -0
  83. package/templates/prisma/package.json +47 -9
  84. package/templates/prisma/prisma/schema/account.schema.prisma +92 -10
  85. package/templates/prisma/prisma/schema/profile.schema.prisma +8 -3
  86. package/templates/prisma/prisma/schema/schema.prisma +1 -2
  87. package/templates/prisma/prisma.config.ts +5 -5
  88. package/templates/prisma/readme.md +81 -0
  89. package/templates/prisma/src/app/configs/index.ts +175 -26
  90. package/templates/prisma/src/app/errors/zodError.ts +16 -16
  91. package/templates/prisma/src/app/lib/logger.ts +12 -0
  92. package/templates/prisma/src/app/lib/prisma.ts +4 -6
  93. package/templates/prisma/src/app/middlewares/auth.ts +37 -16
  94. package/templates/prisma/src/app/middlewares/global_error_handler.ts +42 -35
  95. package/templates/prisma/src/app/middlewares/not_found_api.ts +7 -7
  96. package/templates/prisma/src/app/middlewares/rate_limiter.ts +124 -0
  97. package/templates/prisma/src/app/middlewares/request_validator.ts +4 -3
  98. package/templates/prisma/src/app/middlewares/uploader.ts +17 -13
  99. package/templates/prisma/src/app/modules/account/account.controller.ts +74 -34
  100. package/templates/prisma/src/app/modules/account/account.route.ts +68 -22
  101. package/templates/prisma/src/app/modules/account/account.service.ts +329 -309
  102. package/templates/prisma/src/app/modules/account/account.swagger.ts +167 -76
  103. package/templates/prisma/src/app/modules/account/account.validation.ts +41 -19
  104. package/templates/prisma/src/app/modules/profile/profile.controller.ts +4 -4
  105. package/templates/prisma/src/app/modules/profile/profile.route.ts +18 -12
  106. package/templates/prisma/src/app/modules/profile/profile.service.ts +11 -18
  107. package/templates/prisma/src/app/modules/profile/profile.swagger.ts +11 -11
  108. package/templates/prisma/src/app/modules/profile/profile.validation.ts +2 -2
  109. package/templates/prisma/src/app/passport-js/index.ts +87 -0
  110. package/templates/prisma/src/app/passport-js/oauth-state.ts +150 -0
  111. package/templates/prisma/src/app/passport-js/providers.ts +263 -0
  112. package/templates/prisma/src/app/passport-js/social-account.ts +147 -0
  113. package/templates/prisma/src/app/queues/boss.ts +8 -0
  114. package/templates/prisma/src/app/queues/email.queue.ts +56 -0
  115. package/templates/prisma/src/app/queues/index.ts +14 -0
  116. package/templates/prisma/src/app/queues/maintenance.queue.ts +30 -0
  117. package/templates/prisma/src/app/templates/otpEmailTemplate.ts +185 -0
  118. package/templates/prisma/src/app/templates/resetPasswordEmailTemplate.ts +157 -0
  119. package/templates/prisma/src/app/types/error.d.ts +7 -7
  120. package/templates/prisma/src/app/types/express.d.ts +11 -0
  121. package/templates/prisma/src/app/types/passport-oauth1.d.ts +51 -0
  122. package/templates/prisma/src/app/utils/JWT.ts +36 -17
  123. package/templates/prisma/src/app/utils/app_error.ts +10 -10
  124. package/templates/prisma/src/app/utils/auth_cookie.ts +27 -0
  125. package/templates/prisma/src/app/utils/catch_async.ts +8 -8
  126. package/templates/prisma/src/app/utils/cloudinary.ts +26 -59
  127. package/templates/prisma/src/app/utils/mail_sender.ts +26 -92
  128. package/templates/prisma/src/app/utils/manage_response.ts +19 -19
  129. package/templates/prisma/src/app/utils/otp.ts +20 -0
  130. package/templates/prisma/src/app/utils/session.ts +57 -0
  131. package/templates/prisma/src/app.ts +73 -42
  132. package/templates/prisma/src/routes.ts +12 -10
  133. package/templates/prisma/src/server.ts +37 -19
  134. package/templates/prisma/src/swaggerOptions.ts +22 -26
  135. package/templates/prisma/tests/app.test.ts +86 -0
  136. package/templates/prisma/tests/helpers/fixtures.ts +35 -0
  137. package/templates/prisma/tests/helpers/prisma.ts +32 -0
  138. package/templates/prisma/tests/routes/account.test.ts +627 -0
  139. package/templates/prisma/tests/routes/profile.test.ts +91 -0
  140. package/templates/prisma/tests/routes/social.test.ts +317 -0
  141. package/templates/prisma/tests/setup.ts +10 -0
  142. package/templates/prisma/tsconfig.json +16 -23
  143. package/templates/prisma/vitest.config.ts +38 -0
  144. package/bin/cli.js +0 -41
  145. package/src/createProject.js +0 -61
  146. package/src/generateModule.js +0 -93
  147. package/src/utils/getProjectType.js +0 -13
  148. package/src/utils/mongoose/controllerGenerator.js +0 -70
  149. package/src/utils/mongoose/routeGenerator.js +0 -27
  150. package/src/utils/mongoose/schemaGenerator.js +0 -11
  151. package/src/utils/mongoose/serviceGenerator.js +0 -49
  152. package/src/utils/mongoose/swaggerGenerator.js +0 -112
  153. package/src/utils/mongoose/validationGenerator.js +0 -13
  154. package/src/utils/prisma/prismaSchemaGenerator.js +0 -10
  155. package/src/utils/prisma/prismaServiceGenerator.js +0 -47
  156. package/src/utils/routeInjector.js +0 -40
  157. package/src/utils/swaggerInjector.js +0 -52
  158. package/src/utils/validateModuleName.js +0 -17
  159. package/src/utils/validateProjectName.js +0 -15
  160. package/src/utils.js +0 -26
  161. package/templates/mongoose/src/app/errors/castError.ts +0 -23
  162. package/templates/mongoose/src/app/errors/duplicateError.ts +0 -26
  163. package/templates/mongoose/src/app/errors/validationError.ts +0 -25
  164. package/templates/mongoose/src/app/modules/auth/auth.controller.ts +0 -127
  165. package/templates/mongoose/src/app/modules/auth/auth.interface.ts +0 -24
  166. package/templates/mongoose/src/app/modules/auth/auth.route.ts +0 -46
  167. package/templates/mongoose/src/app/modules/auth/auth.schema.ts +0 -19
  168. package/templates/mongoose/src/app/modules/auth/auth.service.ts +0 -324
  169. package/templates/mongoose/src/app/modules/auth/auth.swagger.ts +0 -214
  170. package/templates/mongoose/src/app/modules/auth/auth.validation.ts +0 -37
  171. package/templates/mongoose/src/app/modules/user/user.controller.ts +0 -17
  172. package/templates/mongoose/src/app/modules/user/user.interface.ts +0 -15
  173. package/templates/mongoose/src/app/modules/user/user.route.ts +0 -20
  174. package/templates/mongoose/src/app/modules/user/user.schema.ts +0 -22
  175. package/templates/mongoose/src/app/modules/user/user.service.ts +0 -22
  176. package/templates/mongoose/src/app/modules/user/user.swagger.ts +0 -75
  177. package/templates/mongoose/src/app/modules/user/user.validation.ts +0 -18
  178. package/templates/mongoose/src/app/types/index.d.ts +0 -12
  179. package/templates/mongoose/src/app/utils/isAccountExist.ts +0 -31
  180. package/templates/prisma/src/app/types/index.d.ts +0 -12
  181. package/templates/prisma/src/app/utils/otpGenerator.ts +0 -29
@@ -1,102 +1,36 @@
1
1
  import nodemailer from 'nodemailer';
2
- import { configs } from '../configs';
2
+ import { configs } from '../configs/index.js';
3
+
3
4
  type TMailContent = {
4
- to: string,
5
- subject: string,
6
- textBody: string,
7
- htmlBody: string,
8
- name?: string
9
- }
5
+ to: string;
6
+ subject: string;
7
+ textBody: string;
8
+ htmlBody: string;
9
+ };
10
10
 
11
11
  const transporter = nodemailer.createTransport({
12
- host: "smtp.gmail.com",
13
- port: 465,
14
- secure: true, // true for 465, false for other ports
15
- auth: {
16
- user: configs.email.app_email!,
17
- pass: configs.email.app_password!,
18
- },
12
+ host: 'smtp.gmail.com',
13
+ port: 465,
14
+ secure: true, // true for 465, false for other ports
15
+ auth: {
16
+ user: configs.email.app_email,
17
+ pass: configs.email.app_password,
18
+ },
19
19
  });
20
20
 
21
- // ✅ Email Sender Function
21
+ // Email Sender Function
22
22
  const sendMail = async (payload: TMailContent) => {
23
- const info = await transporter.sendMail({
24
- from: 'info@digitalcreditai.com',
25
- to: payload.to,
26
- subject: payload.subject,
27
- text: payload.textBody,
28
- html: `
29
- <!DOCTYPE html>
30
- <html lang="en">
31
-
32
- <head>
33
- <meta charset="UTF-8">
34
- <title>Welcome Email</title>
35
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
36
- <style>
37
- * {
38
- margin: 0;
39
- padding: 0;
40
- box-sizing: border-box;
41
- }
42
-
43
- /* Fallback styles for unsupported clients (some email clients ignore <style> tags) */
44
- @media only screen and (max-width: 600px) {
45
- .container {
46
- padding: 20px !important;
47
- }
48
-
49
- .btn {
50
- padding: 12px 18px !important;
51
- font-size: 16px !important;
52
- }
53
- }
54
- </style>
55
- </head>
56
-
57
- <body
58
- style="margin: 0; padding: 0; font-family: Arial, sans-serif;">
59
-
60
- <div style="max-width: 600px; margin: 40px auto; background-color: #f4f4f4; padding: 40px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);"
61
- class="container">
62
-
63
- <div style="font-size: 16px; color: #555555; line-height: 1.6;">
64
- <p style="margin-bottom: 30px;">Hi <strong>${payload?.name || ""}</strong>,</p>
65
-
66
- ${payload?.htmlBody}
67
-
68
- <div
69
- style=" margin-top: 60px; text-align: center;">
70
-
71
- <img style="width: 50px; height: 50px; border-radius: 50%;"
72
- src="https://imgs.search.brave.com/IZoN38NQxnIIuB1I9E70bW6q5OvbEtz68YaxTe1j-o0/rs:fit:860:0:0:0/g:ce/aHR0cHM6Ly9lbGVt/ZW50cy1yZXNpemVk/LmVudmF0b3VzZXJj/b250ZW50LmNvbS9l/bGVtZW50cy1jb3Zl/ci1pbWFnZXMvMjhi/NmVjMTQtMGMwOS00/NGY1LWE5NGUtNmIy/OTM5NTZkMDM2P3c9/NDMzJmNmX2ZpdD1z/Y2FsZS1kb3duJnE9/ODUmZm9ybWF0PWF1/dG8mcz04Mjc0OWYy/ZDUyMmJiM2NlMjNi/OWNhNjhlZmFhNjdk/MTg5OGI4NWIwNzBh/MjQ1NjM4NmI1ZmFj/NWVmNmM5ZTNl"
73
- alt="">
74
-
75
- <p style="font-size: 12px;">The Support Team</p>
76
- <h3>Company Name</h3>
77
- </div>
78
- </div>
79
- <p style="font-size: 14px; color: #999999; margin-top: 20px; margin-bottom: 10px; text-align: center;">
80
- This is an automated message — please do not reply to this email.
81
- <br>
82
- If you need assistance, feel free to contact our support team.
83
- <br><br>
84
- Thank you for choosing us!
85
- </p>
86
-
87
- <hr>
88
- <div style="text-align: center; font-size: 12px; color: #999999; margin-top: 20px;">
89
- &copy; {{year}} Your Company. All rights reserved.
90
- </div>
91
-
92
- </div>
93
- </body>
94
-
95
- </html>
96
-
97
- `,
98
- });
99
- return info
23
+ if (!configs.email.app_email || !configs.email.app_password) {
24
+ throw new Error('Email is not configured. Set APP_USER_EMAIL and APP_PASSWORD.');
25
+ }
26
+
27
+ return transporter.sendMail({
28
+ from: configs.email.from,
29
+ to: payload.to,
30
+ subject: payload.subject,
31
+ text: payload.textBody,
32
+ html: payload.htmlBody,
33
+ });
100
34
  };
101
35
 
102
36
  export default sendMail;
@@ -1,24 +1,24 @@
1
- import { Response } from "express"
1
+ import { Response } from 'express';
2
2
  interface IResponse<T> {
3
- success: boolean,
4
- statusCode: number,
5
- message: string,
6
- data?: T,
7
- meta?: {
8
- page?: number,
9
- limit?: number,
10
- skip?: number,
11
- total?: number
12
- }
3
+ success: boolean;
4
+ statusCode: number;
5
+ message: string;
6
+ data?: T;
7
+ meta?: {
8
+ page?: number;
9
+ limit?: number;
10
+ skip?: number;
11
+ total?: number;
12
+ };
13
13
  }
14
14
 
15
15
  const manageResponse = <T>(res: Response, payload: IResponse<T>) => {
16
- res.status(payload.statusCode).json({
17
- success: payload.success,
18
- message: payload.message,
19
- data: payload.data || undefined || null,
20
- meta: payload.meta || undefined || null
21
- })
22
- }
16
+ res.status(payload.statusCode).json({
17
+ success: payload.success,
18
+ message: payload.message,
19
+ data: payload.data ?? null,
20
+ meta: payload.meta ?? null,
21
+ });
22
+ };
23
23
 
24
- export default manageResponse
24
+ export default manageResponse;
@@ -0,0 +1,20 @@
1
+ import { createHmac, randomInt, timingSafeEqual } from 'node:crypto';
2
+ import { configs } from '../configs/index.js';
3
+
4
+ export const OTP_EXPIRY_MS = 5 * 60 * 1000;
5
+ export const OTP_MAX_ATTEMPTS = 5;
6
+ export const OTP_RESEND_COOLDOWN_MS = 60 * 1000;
7
+
8
+ /** A cryptographically random 6-digit code. */
9
+ export const generateOtp = (): string => randomInt(0, 1_000_000).toString().padStart(6, '0');
10
+
11
+ /** OTPs are stored as an HMAC, never in plain text. */
12
+ export const hashOtp = (otp: string): string =>
13
+ createHmac('sha256', configs.jwt.verified_token).update(otp).digest('hex');
14
+
15
+ export const isOtpMatch = (otp: string, storedHash: string | null): boolean => {
16
+ if (!storedHash) return false;
17
+ const a = Buffer.from(hashOtp(otp), 'hex');
18
+ const b = Buffer.from(storedHash, 'hex');
19
+ return a.length === b.length && timingSafeEqual(a, b);
20
+ };
@@ -0,0 +1,55 @@
1
+ import type { CookieOptions, Request, Response } from 'express';
2
+ import { createHash, randomBytes, randomUUID } from 'node:crypto';
3
+ import { configs } from '../configs/index.js';
4
+ import { refresh_token_model } from '../modules/account/account.schema.js';
5
+ import { clearAccessToken, issueAccessToken } from './auth_cookie.js';
6
+ import type { TokenUser } from './JWT.js';
7
+
8
+ export const REFRESH_TOKEN_COOKIE = 'refresh_token';
9
+
10
+ const refreshCookieOptions: CookieOptions = {
11
+ httpOnly: true,
12
+ secure: configs.is_production,
13
+ sameSite: 'lax',
14
+ // only sent to the auth routes (refresh, sign out), never to the rest of the API
15
+ path: '/api/auth',
16
+ };
17
+
18
+ export const hashRefreshToken = (token: string) => createHash('sha256').update(token).digest('hex');
19
+
20
+ /** The refresh token from the cookie (browsers) or the body (mobile / other clients). */
21
+ export const readRefreshToken = (req: Request): string | undefined => {
22
+ const token = req.cookies?.[REFRESH_TOKEN_COOKIE] ?? req.body?.refreshToken;
23
+ return typeof token === 'string' && token ? token : undefined;
24
+ };
25
+
26
+ /**
27
+ * Signs `user` in: a new access token plus a refresh token, both set as httpOnly
28
+ * cookies and returned. Pass `familyId` when rotating, so the new refresh token
29
+ * belongs to the same session.
30
+ */
31
+ export const createSession = async (
32
+ res: Response,
33
+ user: TokenUser,
34
+ familyId: string = randomUUID(),
35
+ ) => {
36
+ const refreshToken = randomBytes(32).toString('base64url');
37
+ const expiresAt = new Date(Date.now() + configs.jwt.refresh_expires_days * 24 * 60 * 60 * 1000);
38
+
39
+ await refresh_token_model.create({
40
+ _id: hashRefreshToken(refreshToken),
41
+ accountId: user.accountId,
42
+ familyId,
43
+ tokenVersion: user.tokenVersion,
44
+ expiresAt,
45
+ });
46
+
47
+ res.cookie(REFRESH_TOKEN_COOKIE, refreshToken, { ...refreshCookieOptions, expires: expiresAt });
48
+ const accessToken = issueAccessToken(res, user);
49
+ return { accessToken, refreshToken };
50
+ };
51
+
52
+ export const clearSessionCookies = (res: Response) => {
53
+ clearAccessToken(res);
54
+ res.clearCookie(REFRESH_TOKEN_COOKIE, refreshCookieOptions);
55
+ };
@@ -1,42 +1,73 @@
1
- import cookieParser from 'cookie-parser';
2
- import cors from 'cors';
3
- import express, { Request, Response } from 'express';
4
- import swaggerJSDoc from 'swagger-jsdoc';
5
- import swaggerUi from "swagger-ui-express";
6
- import globalErrorHandler from './app/middlewares/global_error_handler';
7
- import notFound from './app/middlewares/not_found_api';
8
- import appRouter from './routes';
9
- import { swaggerOptions } from './swaggerOptions';
10
-
11
- // define app
12
- const app = express()
13
- const swaggerSpec = swaggerJSDoc(swaggerOptions);
14
- app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
15
-
16
- // middleware
17
- app.use(cors({
18
- origin: ["http://localhost:3000"],
19
- methods: ["GET", "POST", "PATCH", "DELETE", "PUT"],
20
- credentials: true
21
- }))
22
- app.use(express.json({ limit: "100mb" }))
23
- app.use(express.raw())
24
- app.use(cookieParser())
25
- app.use(express.urlencoded({ extended: true }));
26
- app.use("/api", appRouter)
27
-
28
- // stating point
29
- app.get('/', (req: Request, res: Response) => {
30
- res.status(200).json({
31
- status: 'success',
32
- message: 'Server is running successful !!',
33
- data: null,
34
- });
35
- });
36
-
37
- // global error handler
38
- app.use(globalErrorHandler);
39
- app.use(notFound);
40
-
41
- // export app
42
- export default app;
1
+ import cookieParser from 'cookie-parser';
2
+ import cors from 'cors';
3
+ import express, { type Request, type Response } from 'express';
4
+ import helmet from 'helmet';
5
+ import swaggerJSDoc from 'swagger-jsdoc';
6
+ import swaggerUi from 'swagger-ui-express';
7
+ import { configs } from './app/configs/index.js';
8
+ import globalErrorHandler from './app/middlewares/global_error_handler.js';
9
+ import notFound from './app/middlewares/not_found_api.js';
10
+ import { apiRateLimiter } from './app/middlewares/rate_limiter.js';
11
+ import { setupPassport } from './app/passport-js/index.js';
12
+ import manageResponse from './app/utils/manage_response.js';
13
+ import appRouter from './routes.js';
14
+ import { swaggerOptions } from './swaggerOptions.js';
15
+
16
+ // define app
17
+ const app = express();
18
+
19
+ // number of reverse proxies in front of the app (e.g. 1 behind Caddy/Nginx), so
20
+ // req.ip and rate limiting see the real client IP. Keep 0 when exposed directly.
21
+ app.set('trust proxy', configs.trust_proxy);
22
+ app.disable('x-powered-by');
23
+
24
+ // security headers
25
+ app.use(helmet());
26
+
27
+ // swagger docs (disabled in production unless ENABLE_SWAGGER=true)
28
+ if (configs.enable_swagger) {
29
+ const swaggerSpec = swaggerJSDoc(swaggerOptions);
30
+ app.use(
31
+ '/docs',
32
+ helmet({ contentSecurityPolicy: false }),
33
+ swaggerUi.serve,
34
+ swaggerUi.setup(swaggerSpec),
35
+ );
36
+ }
37
+
38
+ // cors middleware
39
+ app.use(
40
+ cors({
41
+ origin: configs.cors_origins,
42
+ methods: ['GET', 'POST', 'PATCH', 'DELETE', 'PUT'],
43
+ credentials: true,
44
+ }),
45
+ );
46
+
47
+ // express middleware
48
+ app.use(express.json({ limit: '1mb' }));
49
+ app.use(express.urlencoded({ extended: true, limit: '1mb' }));
50
+ app.use(cookieParser());
51
+
52
+ // passport js (social login)
53
+ setupPassport(app);
54
+
55
+ // Main router (stricter per-route limits live on the auth routes)
56
+ app.use('/api', apiRateLimiter, appRouter);
57
+
58
+ // stating point
59
+ app.get('/', (_req: Request, res: Response) => {
60
+ manageResponse(res, {
61
+ statusCode: 200,
62
+ success: true,
63
+ message: 'Server is running successful !!',
64
+ data: null,
65
+ });
66
+ });
67
+
68
+ // not found, then global error handler (must be last)
69
+ app.use(notFound);
70
+ app.use(globalErrorHandler);
71
+
72
+ // export app
73
+ export default app;
@@ -1,13 +1,13 @@
1
- import { Router } from "express";
2
- import authRoute from "./app/modules/auth/auth.route";
3
- import userRoute from "./app/modules/user/user.route";
4
-
5
- const appRouter = Router();
6
-
7
- const moduleRoutes = [
8
- { path: "/auth", route: authRoute },
9
- { path: "/user", route: userRoute },
10
- ];
11
-
12
- moduleRoutes.forEach((route) => appRouter.use(route.path, route.route));
13
- export default appRouter;
1
+ import { Router } from 'express';
2
+ import accountRouter from './app/modules/account/account.route.js';
3
+ import profileRoute from './app/modules/profile/profile.route.js';
4
+
5
+ const appRouter = Router();
6
+
7
+ const moduleRoutes = [
8
+ { path: '/profile', route: profileRoute },
9
+ { path: '/auth', route: accountRouter },
10
+ ];
11
+
12
+ moduleRoutes.forEach((route) => appRouter.use(route.path, route.route));
13
+ export default appRouter;
@@ -1,18 +1,38 @@
1
- import mongoose from "mongoose";
2
- import app from "./app";
3
- import { configs } from "./app/configs/index";
4
-
5
- async function main() {
6
- try {
7
- app.listen(configs.port, async () => {
8
- await mongoose.connect(configs.db_url as string);
9
- console.log(`Server is running on port ${configs.port}`);
10
- });
11
- } catch (error) {
12
- console.error("Error starting server:", error);
13
- }
14
- }
15
-
16
- main().catch((error) => {
17
- console.error("Error in main function:", error);
18
- });
1
+ import type { Server } from 'node:http';
2
+ import app from './app.js';
3
+ import { configs } from './app/configs/index.js';
4
+ import { connectDatabase, disconnectDatabase } from './app/lib/db.js';
5
+ import { logger } from './app/lib/logger.js';
6
+ import { startQueues, stopQueues } from './app/queues/index.js';
7
+
8
+ let server: Server | undefined;
9
+
10
+ async function main() {
11
+ // connect before accepting requests, so nothing hits a database that isn't there
12
+ await connectDatabase();
13
+ await startQueues();
14
+
15
+ server = app.listen(configs.port, '0.0.0.0', () => {
16
+ logger.info(`Server is running on port ${configs.port}`);
17
+ });
18
+ }
19
+
20
+ async function shutdown(signal: string) {
21
+ logger.info(`${signal} received, shutting down...`);
22
+ // stop accepting connections and let in-flight requests finish
23
+ await new Promise<void>((resolve) => (server ? server.close(() => resolve()) : resolve()));
24
+ await stopQueues().catch((err) => logger.error(err, 'Error stopping queues'));
25
+ await disconnectDatabase();
26
+ process.exit(0);
27
+ }
28
+
29
+ process.on('SIGTERM', () => void shutdown('SIGTERM'));
30
+ process.on('SIGINT', () => void shutdown('SIGINT'));
31
+ process.on('unhandledRejection', (reason) => {
32
+ logger.error(reason, 'Unhandled promise rejection');
33
+ });
34
+
35
+ main().catch((error) => {
36
+ logger.error(error, 'Failed to start server');
37
+ process.exit(1);
38
+ });
@@ -1,31 +1,36 @@
1
- import path from "path";
2
- import { configs } from "./app/configs";
3
- import { authSwaggerDocs } from "./app/modules/auth/auth.swagger";
4
- import { userSwaggerDocs } from "./app/modules/user/user.swagger";
1
+ import { configs } from './app/configs/index.js';
2
+ import { accountSwaggerDocs } from './app/modules/account/account.swagger.js';
3
+ import { profileSwaggerDocs } from './app/modules/profile/profile.swagger.js';
5
4
 
6
5
  export const swaggerOptions = {
7
6
  definition: {
8
- openapi: "3.0.0",
7
+ openapi: '3.0.0',
9
8
  info: {
10
- title: "API Doc - Build with Express CLI",
11
- version: "1.0.0-Mahid",
12
- description: "Express API with auto-generated Swagger docs",
9
+ title: 'API Doc - Build with exp-node-server',
10
+ version: '1.0.0',
11
+ description: 'Express + Mongoose API with auto-generated Swagger docs',
13
12
  },
14
13
  paths: {
15
- ...authSwaggerDocs,
16
- ...userSwaggerDocs,
14
+ ...accountSwaggerDocs,
15
+ ...profileSwaggerDocs,
17
16
  },
18
17
  servers:
19
- configs.env === "production"
20
- ? [{ url: "https://live-url.com" }, { url: "http://localhost:5000" }]
21
- : [{ url: "http://localhost:5000" }, { url: "https://live-url.com" }],
18
+ configs.env === 'production'
19
+ ? [
20
+ { url: configs.swagger_config.production_base_url },
21
+ { url: configs.swagger_config.development_base_url },
22
+ ]
23
+ : [
24
+ { url: configs.swagger_config.development_base_url },
25
+ { url: configs.swagger_config.production_base_url },
26
+ ],
22
27
  components: {
23
28
  securitySchemes: {
24
29
  AuthorizationToken: {
25
- type: "apiKey",
26
- in: "header",
27
- name: "Authorization",
28
- description: "Put your accessToken here ",
30
+ type: 'apiKey',
31
+ in: 'header',
32
+ name: 'Authorization',
33
+ description: 'Put your accessToken here ',
29
34
  },
30
35
  },
31
36
  },
@@ -35,10 +40,5 @@ export const swaggerOptions = {
35
40
  },
36
41
  ],
37
42
  },
38
- apis: [
39
- path.join(
40
- __dirname,
41
- configs.env === "production" ? "./**/*.js" : "./**/*.ts",
42
- ),
43
- ],
43
+ apis: [],
44
44
  };
@@ -0,0 +1,97 @@
1
+ import request from 'supertest';
2
+ import { describe, expect, it } from 'vitest';
3
+ import app from '../src/app.js';
4
+ import { createAccount } from './helpers/fixtures.js';
5
+
6
+ describe('app', () => {
7
+ it('GET / responds with security headers', async () => {
8
+ const res = await request(app).get('/');
9
+
10
+ expect(res.status).toBe(200);
11
+ expect(res.headers['x-powered-by']).toBeUndefined();
12
+ expect(res.headers['x-content-type-options']).toBe('nosniff');
13
+ expect(res.headers['content-security-policy']).toBeDefined();
14
+ });
15
+
16
+ it('unknown routes are 404', async () => {
17
+ const res = await request(app).get('/api/nope');
18
+ expect(res.status).toBe(404);
19
+ });
20
+
21
+ it('malformed JSON is a 400, not a 500', async () => {
22
+ const res = await request(app)
23
+ .post('/api/auth/sign-in')
24
+ .set('Content-Type', 'application/json')
25
+ .send('{"email":');
26
+
27
+ expect(res.status).toBe(400);
28
+ expect(res.body.message).toBe('Invalid JSON body');
29
+ });
30
+
31
+ it('rejects bodies over 1mb', async () => {
32
+ const res = await request(app)
33
+ .post('/api/auth/sign-in')
34
+ .send({ email: 'a@b.c', password: 'x'.repeat(1_100_000) });
35
+
36
+ expect(res.status).toBe(413);
37
+ });
38
+
39
+ it('only allows the configured CORS origin', async () => {
40
+ const allowed = await request(app).get('/').set('Origin', 'http://localhost:5173');
41
+ const other = await request(app).get('/').set('Origin', 'https://evil.example');
42
+
43
+ expect(allowed.headers['access-control-allow-origin']).toBe('http://localhost:5173');
44
+ expect(other.headers['access-control-allow-origin']).toBeUndefined();
45
+ });
46
+
47
+ it('never sends error internals to the client', async () => {
48
+ const res = await request(app).get('/api/auth/me').set('Authorization', 'Bearer garbage');
49
+
50
+ expect(res.status).toBe(401);
51
+ expect(res.body).not.toHaveProperty('err');
52
+ });
53
+ });
54
+
55
+ describe('rate limiting (counters stored in MongoDB)', () => {
56
+ it('limits sign-in attempts per email', async () => {
57
+ await createAccount({ email: 'target@example.com' });
58
+ const attempt = (email = 'target@example.com') =>
59
+ request(app).post('/api/auth/sign-in').send({ email, password: 'guess-guess' });
60
+
61
+ for (let i = 0; i < 10; i++) {
62
+ expect((await attempt()).status).toBe(401);
63
+ }
64
+ const blocked = await attempt();
65
+
66
+ expect(blocked.status).toBe(429);
67
+ expect(blocked.headers['ratelimit-policy']).toBeDefined();
68
+
69
+ // another account from the same client is still allowed
70
+ expect((await attempt('someone-else@example.com')).status).toBe(401);
71
+ });
72
+
73
+ it('limits emails sent to one address, whatever the endpoint', async () => {
74
+ const email = 'victim@example.com';
75
+
76
+ await request(app).put('/api/auth/forget-password').send({ email }).expect(200);
77
+ await request(app).put('/api/auth/resend-otp').send({ email }).expect(200);
78
+ await request(app)
79
+ .put('/api/auth/forget-password')
80
+ .send({ email: email.toUpperCase() })
81
+ .expect(200);
82
+
83
+ const blocked = await request(app).put('/api/auth/forget-password').send({ email });
84
+ expect(blocked.status).toBe(429);
85
+ });
86
+
87
+ it('counts parallel requests exactly', async () => {
88
+ const results = await Promise.all(
89
+ Array.from({ length: 6 }, () =>
90
+ request(app).put('/api/auth/forget-password').send({ email: 'burst@example.com' }),
91
+ ),
92
+ );
93
+
94
+ const statuses = results.map((r) => r.status).sort();
95
+ expect(statuses).toEqual([200, 200, 200, 429, 429, 429]);
96
+ });
97
+ });
@@ -0,0 +1,15 @@
1
+ import { MongoMemoryServer } from 'mongodb-memory-server';
2
+ import type { TestProject } from 'vitest/node';
3
+
4
+ declare module 'vitest' {
5
+ export interface ProvidedContext {
6
+ mongoUri: string;
7
+ }
8
+ }
9
+
10
+ /** One in-memory MongoDB for the run; every test file gets its own database. */
11
+ export default async function setup(project: TestProject) {
12
+ const mongod = await MongoMemoryServer.create();
13
+ project.provide('mongoUri', mongod.getUri());
14
+ return () => mongod.stop();
15
+ }