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,69 +1,63 @@
1
- import { Request, Response, NextFunction, ErrorRequestHandler } from 'express'
2
- import { ZodError } from 'zod'
3
- import handleZodError from '../errors/zodError'
4
- import handleValidationError from '../errors/validationError'
5
- import handleCastError from '../errors/castError'
6
- import { TErrorSources } from '../types/error'
7
- import handleDuplicateError from '../errors/duplicateError'
8
- import { AppError } from '../utils/app_error'
9
- import { configs } from '../configs'
1
+ import type { ErrorRequestHandler } from 'express';
2
+ import mongoose from 'mongoose';
3
+ import multer from 'multer';
4
+ import { ZodError } from 'zod';
5
+ import { configs } from '../configs/index.js';
6
+ import handleZodError from '../errors/zodError.js';
7
+ import { logger } from '../lib/logger.js';
8
+ import type { TErrorSources } from '../types/error.js';
9
+ import { AppError } from '../utils/app_error.js';
10
10
 
11
- const globalErrorHandler: ErrorRequestHandler = (err, req, res, next) => {
12
- let statusCode = 500
13
- let message = 'Something went wrong!'
14
- let errorSources: TErrorSources = [
15
- {
16
- path: '',
17
- message: 'Something went wrong'
18
- }
19
- ]
11
+ const globalErrorHandler: ErrorRequestHandler = (err, req, res, _next) => {
12
+ let statusCode = 500;
13
+ let message = 'Something went wrong!';
14
+ let errorSources: TErrorSources = [];
20
15
 
21
- if (err instanceof ZodError) {
22
- const simplifiedError = handleZodError(err)
23
- statusCode = simplifiedError?.statusCode
24
- message = simplifiedError?.message
25
- errorSources = simplifiedError?.errorSources
26
- } else if (err?.name === 'ValidationError') {
27
- const simplifiedError = handleValidationError(err)
28
- statusCode = simplifiedError?.statusCode
29
- message = simplifiedError?.message
30
- errorSources = simplifiedError?.errorSources
31
- } else if (err?.name === 'CastError') {
32
- const simplifiedError = handleCastError(err)
33
- statusCode = simplifiedError?.statusCode
34
- message = simplifiedError?.message
35
- errorSources = simplifiedError?.errorSources
36
- } else if (err?.code === 11000) {
37
- const simplifiedError = handleDuplicateError(err)
38
- statusCode = simplifiedError?.statusCode
39
- message = simplifiedError?.message
40
- errorSources = simplifiedError?.errorSources
41
- } else if (err instanceof AppError) {
42
- statusCode = err?.statusCode
43
- message = err.message
44
- errorSources = [
45
- {
46
- path: '',
47
- message: err?.message
48
- }
49
- ]
50
- } else if (err instanceof Error) {
51
- message = err.message
52
- errorSources = [
53
- {
54
- path: '',
55
- message: err?.message
56
- }
57
- ]
58
- }
16
+ if (err instanceof ZodError) {
17
+ ({ statusCode, message, errorSources } = handleZodError(err));
18
+ } else if (err instanceof AppError) {
19
+ statusCode = err.statusCode;
20
+ message = err.message;
21
+ } else if (err instanceof multer.MulterError) {
22
+ statusCode = 400;
23
+ message = err.message;
24
+ } else if (err?.code === 11000) {
25
+ // duplicate key; the offending value is not echoed back
26
+ statusCode = 409;
27
+ message = 'Resource already exists';
28
+ } else if (err instanceof mongoose.Error.CastError) {
29
+ statusCode = 400;
30
+ message = `Invalid ${err.path}`;
31
+ } else if (err instanceof mongoose.Error.ValidationError) {
32
+ statusCode = 400;
33
+ message = 'Validation Error';
34
+ errorSources = Object.values(err.errors).map((e) => ({ path: e.path, message: e.message }));
35
+ } else if (err?.status === 401 || err?.name === 'AuthenticationError') {
36
+ // passport authentication failures
37
+ statusCode = 401;
38
+ message = 'Authentication failed';
39
+ } else if (err instanceof SyntaxError && 'body' in err) {
40
+ // malformed JSON body
41
+ statusCode = 400;
42
+ message = 'Invalid JSON body';
43
+ } else if (err?.expose && typeof err.status === 'number' && err.status < 500) {
44
+ // client errors from body-parser & co (body too large, unsupported charset, ...)
45
+ statusCode = err.status;
46
+ message = err.message;
47
+ }
59
48
 
60
- res.status(statusCode).json({
61
- success: false,
62
- message,
63
- errorSources,
64
- err,
65
- stack: configs.env === 'development' ? err?.stack : null
66
- })
67
- }
49
+ if (statusCode >= 500) {
50
+ logger.error({ err, method: req.method, url: req.originalUrl }, 'Unhandled error');
51
+ // don't leak internals (SQL, file paths, ...) to clients in production
52
+ if (!configs.is_production && err instanceof Error) message = err.message;
53
+ }
68
54
 
69
- export default globalErrorHandler
55
+ res.status(statusCode).json({
56
+ success: false,
57
+ message,
58
+ errorSources: errorSources.length ? errorSources : [{ path: '', message }],
59
+ ...(configs.is_production ? {} : { stack: err?.stack }),
60
+ });
61
+ };
62
+
63
+ export default globalErrorHandler;
@@ -1,10 +1,10 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
2
 
3
- const notFound = (req: Request, res: Response, next: NextFunction) => {
4
- res.status(404).json({
5
- message: 'Sorry Route is not found!! 😴😴😴',
6
- success: false,
7
- error: '',
8
- });
3
+ const notFound = (req: Request, res: Response, _next: NextFunction) => {
4
+ res.status(404).json({
5
+ message: 'Sorry Route is not found!! 😴😴😴',
6
+ success: false,
7
+ error: '',
8
+ });
9
9
  };
10
- export default notFound;
10
+ export default notFound;
@@ -0,0 +1,153 @@
1
+ import type { Request } from 'express';
2
+ import rateLimit, {
3
+ type IncrementResponse,
4
+ ipKeyGenerator,
5
+ type Options,
6
+ type Store,
7
+ } from 'express-rate-limit';
8
+ import type { Collection } from 'mongodb';
9
+ import { model, Schema } from 'mongoose';
10
+
11
+ type RateLimitDoc = { _id: string; hits: number; resetAt: Date };
12
+
13
+ const rateLimitModel = model(
14
+ 'rate_limit',
15
+ new Schema<RateLimitDoc>(
16
+ {
17
+ _id: { type: String, required: true },
18
+ hits: { type: Number, required: true },
19
+ // TTL index: MongoDB deletes counters once their window is over
20
+ resetAt: { type: Date, required: true, expires: 0 },
21
+ },
22
+ { versionKey: false },
23
+ ),
24
+ );
25
+
26
+ const isDuplicateKey = (err: unknown) => (err as { code?: number })?.code === 11000;
27
+
28
+ /**
29
+ * Keeps hit counters in MongoDB, so limits are shared by every app instance and
30
+ * survive restarts (no Redis needed).
31
+ */
32
+ class MongoStore implements Store {
33
+ windowMs = 60_000;
34
+ // `prefix` keeps counters of different limiters apart in the same collection
35
+ constructor(public prefix: string) {}
36
+
37
+ init(options: Options) {
38
+ this.windowMs = options.windowMs;
39
+ }
40
+
41
+ async increment(key: string): Promise<IncrementResponse> {
42
+ // one atomic update: start a new window if the old one is over, else count the hit
43
+ const update = [
44
+ {
45
+ $set: {
46
+ hits: { $cond: [{ $gt: ['$resetAt', '$$NOW'] }, { $add: ['$hits', 1] }, 1] },
47
+ resetAt: {
48
+ $cond: [{ $gt: ['$resetAt', '$$NOW'] }, '$resetAt', { $add: ['$$NOW', this.windowMs] }],
49
+ },
50
+ },
51
+ },
52
+ ];
53
+ const collection = rateLimitModel.collection as unknown as Collection<RateLimitDoc>;
54
+ const run = () =>
55
+ collection.findOneAndUpdate({ _id: this.prefix + key }, update, {
56
+ upsert: true,
57
+ returnDocument: 'after',
58
+ includeResultMetadata: false,
59
+ });
60
+
61
+ // two first hits at the same moment can race on the upsert; the loser retries
62
+ const row = await run().catch((err) => (isDuplicateKey(err) ? run() : Promise.reject(err)));
63
+ return { totalHits: row!.hits, resetTime: row!.resetAt };
64
+ }
65
+
66
+ async decrement(key: string) {
67
+ await rateLimitModel.updateOne(
68
+ { _id: this.prefix + key, hits: { $gt: 0 } },
69
+ { $inc: { hits: -1 } },
70
+ );
71
+ }
72
+
73
+ async resetKey(key: string) {
74
+ await rateLimitModel.deleteOne({ _id: this.prefix + key });
75
+ }
76
+ }
77
+
78
+ const message = { success: false, message: 'Too many attempts, please try again later.' };
79
+
80
+ /** Client IP, with IPv6 addresses grouped by subnet so they can't rotate around the limit. */
81
+ const ip = (req: Request) => ipKeyGenerator(req.ip ?? '');
82
+
83
+ /** Email from the (not yet validated) body, normalised like the validators do. */
84
+ const email = (req: Request) =>
85
+ typeof req.body?.email === 'string' ? req.body.email.trim().toLowerCase() : '';
86
+
87
+ type LimiterOptions = {
88
+ windowMs: number;
89
+ limit: number;
90
+ keyGenerator?: (req: Request) => string;
91
+ };
92
+
93
+ /** A limiter backed by MongoDB. `name` must be unique. */
94
+ const createLimiter = (name: string, { windowMs, limit, keyGenerator = ip }: LimiterOptions) =>
95
+ rateLimit({
96
+ windowMs,
97
+ limit,
98
+ keyGenerator,
99
+ standardHeaders: 'draft-8',
100
+ legacyHeaders: false,
101
+ message,
102
+ store: new MongoStore(`${name}:`),
103
+ });
104
+
105
+ const MINUTE = 60 * 1000;
106
+
107
+ /**
108
+ * Coarse limit for the whole API. Kept in memory (per instance) so normal traffic
109
+ * doesn't pay for a database round trip; the auth limiters below are the strict ones.
110
+ */
111
+ export const apiRateLimiter = rateLimit({
112
+ windowMs: MINUTE,
113
+ limit: 300,
114
+ standardHeaders: 'draft-8',
115
+ legacyHeaders: false,
116
+ message,
117
+ });
118
+
119
+ export const rateLimiters = {
120
+ /** Account creation, per IP. */
121
+ signUp: createLimiter('sign-up', { windowMs: 60 * MINUTE, limit: 10 }),
122
+
123
+ /** Password guessing: per IP+email for one account, and per IP across many accounts. */
124
+ signIn: createLimiter('sign-in', {
125
+ windowMs: 15 * MINUTE,
126
+ limit: 10,
127
+ keyGenerator: (req) => `${ip(req)}:${email(req)}`,
128
+ }),
129
+ signInIp: createLimiter('sign-in-ip', { windowMs: 15 * MINUTE, limit: 50 }),
130
+
131
+ /** OTP / link / reset token guessing, per IP. */
132
+ verify: createLimiter('verify', { windowMs: 15 * MINUTE, limit: 20 }),
133
+
134
+ /**
135
+ * Endpoints that send an email. Limited per recipient so nobody can flood an
136
+ * inbox from many IPs, and per IP so one client can't spray many addresses.
137
+ */
138
+ emailPerAddress: createLimiter('email-address', {
139
+ windowMs: 15 * MINUTE,
140
+ limit: 3,
141
+ keyGenerator: email,
142
+ }),
143
+ emailPerIp: createLimiter('email-ip', { windowMs: 15 * MINUTE, limit: 10 }),
144
+
145
+ /** Password changes for signed-in users, per IP. */
146
+ changePassword: createLimiter('change-password', { windowMs: 15 * MINUTE, limit: 10 }),
147
+
148
+ /** Token refreshes, per IP (tokens are unguessable; this only caps abuse). */
149
+ refresh: createLimiter('refresh', { windowMs: 15 * MINUTE, limit: 300 }),
150
+
151
+ /** Starting a social login and exchanging its code, per IP. */
152
+ social: createLimiter('social', { windowMs: 15 * MINUTE, limit: 30 }),
153
+ };
@@ -1,14 +1,15 @@
1
- import { NextFunction, Request, Response } from 'express';
1
+ import type { NextFunction, Request, Response } from 'express';
2
+ import type { ZodType } from 'zod';
2
3
 
3
- const RequestValidator = (schema: any) => {
4
- return async (req: Request, res: Response, next: NextFunction) => {
5
- try {
6
- req.body = await schema.parseAsync(req.body);
7
- next();
8
- } catch (err) {
9
- next(err);
10
- }
11
- };
4
+ const RequestValidator = (schema: ZodType) => {
5
+ return async (req: Request, _res: Response, next: NextFunction) => {
6
+ try {
7
+ req.body = await schema.parseAsync(req.body);
8
+ next();
9
+ } catch (err) {
10
+ next(err);
11
+ }
12
+ };
12
13
  };
13
14
 
14
15
  export default RequestValidator;
@@ -1,16 +1,20 @@
1
- import multer from "multer";
2
- import path from "path";
1
+ import multer from 'multer';
2
+ import { AppError } from '../utils/app_error.js';
3
3
 
4
- const storage = multer.diskStorage({
5
- destination: function (req, file, cb) {
6
- cb(null, path.join(process.cwd(), "uploads"))
7
- },
8
- filename: function (req, file, cb) {
4
+ const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
5
+ const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
9
6
 
10
- cb(null, file.originalname)
11
- }
12
- })
7
+ /**
8
+ * Keeps the (size limited) file in memory and streams it to Cloudinary, so nothing
9
+ * is ever left behind on disk when validation or the upload fails.
10
+ */
11
+ const uploader = multer({
12
+ storage: multer.memoryStorage(),
13
+ limits: { fileSize: MAX_FILE_SIZE, files: 1, fields: 10 },
14
+ fileFilter: (_req, file, cb) => {
15
+ if (ALLOWED_IMAGE_TYPES.includes(file.mimetype)) return cb(null, true);
16
+ cb(new AppError(`Only ${ALLOWED_IMAGE_TYPES.join(', ')} files are allowed`, 400));
17
+ },
18
+ });
13
19
 
14
- const uploader = multer({ storage: storage })
15
-
16
- export default uploader;
20
+ export default uploader;
@@ -0,0 +1,156 @@
1
+ import catchAsync from '../../utils/catch_async.js';
2
+ import manageResponse from '../../utils/manage_response.js';
3
+ import { clearSessionCookies, createSession } from '../../utils/session.js';
4
+ import { account_services } from './account.service.js';
5
+
6
+ const create_account = catchAsync(async (req, res) => {
7
+ const result = await account_services.create_account_into_db(req);
8
+ manageResponse(res, {
9
+ statusCode: 201,
10
+ success: true,
11
+ message: 'Account created successfully',
12
+ data: result,
13
+ });
14
+ });
15
+
16
+ const verify_account_using_otp = catchAsync(async (req, res) => {
17
+ const result = await account_services.verify_account_using_otp_into_db(req);
18
+ manageResponse(res, {
19
+ statusCode: 200,
20
+ success: true,
21
+ message: 'Otp verification successful',
22
+ data: result,
23
+ });
24
+ });
25
+
26
+ const verify_account_using_link = catchAsync(async (req, res) => {
27
+ const result = await account_services.verify_account_using_link_into_db(req);
28
+ manageResponse(res, {
29
+ statusCode: 200,
30
+ success: true,
31
+ message: 'Account verification successful',
32
+ data: result,
33
+ });
34
+ });
35
+
36
+ const login_user = catchAsync(async (req, res) => {
37
+ const user = await account_services.login_user_into_db(req);
38
+ const tokens = await createSession(res, user);
39
+ manageResponse(res, {
40
+ statusCode: 200,
41
+ success: true,
42
+ message: 'User logged in successfully',
43
+ data: tokens,
44
+ });
45
+ });
46
+
47
+ const get_user_account = catchAsync(async (req, res) => {
48
+ const result = await account_services.get_user_account_from_db(req);
49
+
50
+ manageResponse(res, {
51
+ statusCode: 200,
52
+ success: true,
53
+ message: 'Account fetched successfully',
54
+ data: result,
55
+ });
56
+ });
57
+
58
+ const change_password = catchAsync(async (req, res) => {
59
+ const user = await account_services.change_password_into_db(req);
60
+ // every session was revoked, so give this one a fresh start
61
+ const tokens = await createSession(res, user);
62
+ manageResponse(res, {
63
+ statusCode: 200,
64
+ success: true,
65
+ message: 'Password changed successfully',
66
+ data: tokens,
67
+ });
68
+ });
69
+
70
+ const resend_otp_and_verification_link = catchAsync(async (req, res) => {
71
+ const result = await account_services.resend_otp_and_verification_link_from_db(req);
72
+ manageResponse(res, {
73
+ statusCode: 200,
74
+ success: true,
75
+ message: 'If the account exists and is not verified, a new OTP has been sent',
76
+ data: result,
77
+ });
78
+ });
79
+
80
+ const forget_password_generate_reset_token = catchAsync(async (req, res) => {
81
+ const result = await account_services.forget_password_generate_reset_token_from_db(req);
82
+ manageResponse(res, {
83
+ statusCode: 200,
84
+ success: true,
85
+ message: 'If an account exists for this email, a password reset link has been sent',
86
+ data: result,
87
+ });
88
+ });
89
+
90
+ const reset_password_using_token = catchAsync(async (req, res) => {
91
+ const result = await account_services.reset_password_using_token_into_db(req);
92
+ manageResponse(res, {
93
+ statusCode: 200,
94
+ success: true,
95
+ message: 'Password reset successfully',
96
+ data: result,
97
+ });
98
+ });
99
+
100
+ const refresh_token = catchAsync(async (req, res) => {
101
+ const { user, familyId } = await account_services.refresh_session_from_db(req);
102
+ const tokens = await createSession(res, user, familyId);
103
+ manageResponse(res, {
104
+ statusCode: 200,
105
+ success: true,
106
+ message: 'Tokens refreshed successfully',
107
+ data: tokens,
108
+ });
109
+ });
110
+
111
+ const sign_out = catchAsync(async (req, res) => {
112
+ await account_services.sign_out_from_db(req);
113
+ clearSessionCookies(res);
114
+ manageResponse(res, {
115
+ statusCode: 200,
116
+ success: true,
117
+ message: 'Signed out successfully',
118
+ });
119
+ });
120
+
121
+ const sign_out_everywhere = catchAsync(async (req, res) => {
122
+ await account_services.sign_out_everywhere_from_db(req);
123
+ clearSessionCookies(res);
124
+ manageResponse(res, {
125
+ statusCode: 200,
126
+ success: true,
127
+ message: 'Signed out from all devices',
128
+ });
129
+ });
130
+
131
+ const exchange_social_login_code = catchAsync(async (req, res) => {
132
+ const user = await account_services.exchange_social_login_code_from_db(req);
133
+ const tokens = await createSession(res, user);
134
+ manageResponse(res, {
135
+ statusCode: 200,
136
+ success: true,
137
+ message: 'User logged in successfully',
138
+ data: tokens,
139
+ });
140
+ });
141
+
142
+ export const account_controller = {
143
+ create_account,
144
+ login_user,
145
+ get_user_account,
146
+ change_password,
147
+ verify_account_using_otp,
148
+ resend_otp_and_verification_link,
149
+ verify_account_using_link,
150
+ forget_password_generate_reset_token,
151
+ reset_password_using_token,
152
+ refresh_token,
153
+ sign_out,
154
+ sign_out_everywhere,
155
+ exchange_social_login_code,
156
+ };
@@ -0,0 +1,65 @@
1
+ import type { Types } from 'mongoose';
2
+
3
+ export const ROLES = ['ADMIN', 'USER'] as const;
4
+ export type ROLE = (typeof ROLES)[number];
5
+
6
+ export const PROVIDERS = [
7
+ 'LOCAL',
8
+ 'GOOGLE',
9
+ 'FACEBOOK',
10
+ 'GITHUB',
11
+ 'TWITTER',
12
+ 'LINKEDIN',
13
+ 'DISCORD',
14
+ ] as const;
15
+ export type PROVIDER = (typeof PROVIDERS)[number];
16
+
17
+ export type T_account = {
18
+ email: string;
19
+ password: string | null;
20
+ // provider the account was created with
21
+ provider: PROVIDER;
22
+ role: ROLE;
23
+ isDeleted: boolean;
24
+ isAccountVerified: boolean;
25
+ // bumped on password change/reset and "sign out everywhere"; access tokens
26
+ // and reset links carrying an older version stop working
27
+ tokenVersion: number;
28
+ // email verification (the OTP is stored as an HMAC, never in plain text)
29
+ lastOtp: string | null;
30
+ lastOtpSendingTime: Date | null;
31
+ otpAttempts: number;
32
+ createdAt: Date;
33
+ updatedAt: Date;
34
+ };
35
+
36
+ /** A social identity linked to an account, found by the provider's stable user id. */
37
+ export type T_social_account = {
38
+ accountId: Types.ObjectId;
39
+ provider: PROVIDER;
40
+ providerUserId: string;
41
+ createdAt: Date;
42
+ };
43
+
44
+ /** Single-use code exchanged for an access token after a social login (hash only). */
45
+ export type T_social_login_code = {
46
+ _id: string; // sha256 of the code
47
+ accountId: Types.ObjectId;
48
+ expiresAt: Date;
49
+ };
50
+
51
+ /**
52
+ * Refresh tokens are opaque random strings; only their hash is stored. Every refresh
53
+ * rotates the token, and all tokens of one sign-in share a `familyId`, so a stolen
54
+ * token that gets reused can revoke the whole session.
55
+ */
56
+ export type T_refresh_token = {
57
+ _id: string; // sha256 of the token
58
+ accountId: Types.ObjectId;
59
+ familyId: string;
60
+ // the account's tokenVersion when issued; a password change makes it stale
61
+ tokenVersion: number;
62
+ expiresAt: Date;
63
+ // set when the token is rotated; kept until expiry to detect reuse
64
+ usedAt: Date | null;
65
+ };
@@ -0,0 +1,101 @@
1
+ import { Router } from 'express';
2
+ import auth from '../../middlewares/auth.js';
3
+ import { rateLimiters } from '../../middlewares/rate_limiter.js';
4
+ import RequestValidator from '../../middlewares/request_validator.js';
5
+ import { socialAuthRouter } from '../../passport-js/index.js';
6
+ import { account_controller } from './account.controller.js';
7
+ import { account_validation } from './account.validation.js';
8
+
9
+ const accountRouter = Router();
10
+
11
+ accountRouter.post(
12
+ '/sign-up',
13
+ rateLimiters.signUp,
14
+ rateLimiters.emailPerAddress,
15
+ RequestValidator(account_validation.sign_up),
16
+ account_controller.create_account,
17
+ );
18
+
19
+ accountRouter.post(
20
+ '/sign-in',
21
+ rateLimiters.signInIp,
22
+ rateLimiters.signIn,
23
+ RequestValidator(account_validation.sign_in),
24
+ account_controller.login_user,
25
+ );
26
+
27
+ accountRouter.post(
28
+ '/refresh-token',
29
+ rateLimiters.refresh,
30
+ RequestValidator(account_validation.refresh_token),
31
+ account_controller.refresh_token,
32
+ );
33
+
34
+ accountRouter.post(
35
+ '/sign-out',
36
+ RequestValidator(account_validation.refresh_token),
37
+ account_controller.sign_out,
38
+ );
39
+
40
+ accountRouter.post('/sign-out-all', auth(), account_controller.sign_out_everywhere);
41
+
42
+ accountRouter.put(
43
+ '/verify-otp',
44
+ rateLimiters.verify,
45
+ RequestValidator(account_validation.verify_otp),
46
+ account_controller.verify_account_using_otp,
47
+ );
48
+
49
+ accountRouter.put(
50
+ '/verify-link',
51
+ rateLimiters.verify,
52
+ RequestValidator(account_validation.verify_link),
53
+ account_controller.verify_account_using_link,
54
+ );
55
+
56
+ accountRouter.get('/me', auth(), account_controller.get_user_account);
57
+
58
+ accountRouter.put(
59
+ '/change-password',
60
+ rateLimiters.changePassword,
61
+ auth(),
62
+ RequestValidator(account_validation.change_password),
63
+ account_controller.change_password,
64
+ );
65
+
66
+ accountRouter.put(
67
+ '/resend-otp',
68
+ rateLimiters.emailPerIp,
69
+ rateLimiters.emailPerAddress,
70
+ RequestValidator(account_validation.resend_otp),
71
+ account_controller.resend_otp_and_verification_link,
72
+ );
73
+
74
+ accountRouter.put(
75
+ '/forget-password',
76
+ rateLimiters.emailPerIp,
77
+ rateLimiters.emailPerAddress,
78
+ RequestValidator(account_validation.forget_password),
79
+ account_controller.forget_password_generate_reset_token,
80
+ );
81
+
82
+ accountRouter.put(
83
+ '/reset-password',
84
+ rateLimiters.verify,
85
+ RequestValidator(account_validation.reset_pass),
86
+ account_controller.reset_password_using_token,
87
+ );
88
+
89
+ // Social login: the front-end trades the `?code=` from the redirect for a token
90
+ accountRouter.post(
91
+ '/social/exchange',
92
+ rateLimiters.social,
93
+ RequestValidator(account_validation.social_exchange),
94
+ account_controller.exchange_social_login_code,
95
+ );
96
+
97
+ // GET /social/providers, /social/<provider> and /social/<provider>/callback
98
+ // for every provider enabled in .env (see passport-js/providers.ts)
99
+ accountRouter.use('/social', socialAuthRouter);
100
+
101
+ export default accountRouter;