@flowerforce/flowerbase 1.2.0 → 1.2.1-beta.11

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 (125) hide show
  1. package/README.md +28 -3
  2. package/dist/auth/controller.d.ts.map +1 -1
  3. package/dist/auth/controller.js +57 -3
  4. package/dist/auth/plugins/jwt.d.ts.map +1 -1
  5. package/dist/auth/plugins/jwt.js +49 -3
  6. package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
  7. package/dist/auth/providers/custom-function/controller.js +19 -3
  8. package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
  9. package/dist/auth/providers/local-userpass/controller.js +125 -71
  10. package/dist/auth/providers/local-userpass/dtos.d.ts +11 -2
  11. package/dist/auth/providers/local-userpass/dtos.d.ts.map +1 -1
  12. package/dist/auth/utils.d.ts +53 -14
  13. package/dist/auth/utils.d.ts.map +1 -1
  14. package/dist/auth/utils.js +46 -63
  15. package/dist/constants.d.ts +14 -0
  16. package/dist/constants.d.ts.map +1 -1
  17. package/dist/constants.js +18 -5
  18. package/dist/features/functions/controller.d.ts.map +1 -1
  19. package/dist/features/functions/controller.js +32 -3
  20. package/dist/features/functions/dtos.d.ts +3 -0
  21. package/dist/features/functions/dtos.d.ts.map +1 -1
  22. package/dist/features/functions/interface.d.ts +3 -0
  23. package/dist/features/functions/interface.d.ts.map +1 -1
  24. package/dist/features/functions/utils.d.ts +2 -1
  25. package/dist/features/functions/utils.d.ts.map +1 -1
  26. package/dist/features/functions/utils.js +19 -7
  27. package/dist/features/rules/utils.d.ts.map +1 -1
  28. package/dist/features/rules/utils.js +11 -2
  29. package/dist/features/triggers/index.d.ts.map +1 -1
  30. package/dist/features/triggers/index.js +48 -7
  31. package/dist/features/triggers/utils.d.ts.map +1 -1
  32. package/dist/features/triggers/utils.js +118 -27
  33. package/dist/index.d.ts +8 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +57 -21
  36. package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
  37. package/dist/services/mongodb-atlas/index.js +605 -478
  38. package/dist/services/mongodb-atlas/model.d.ts +2 -1
  39. package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
  40. package/dist/services/mongodb-atlas/utils.d.ts +9 -2
  41. package/dist/services/mongodb-atlas/utils.d.ts.map +1 -1
  42. package/dist/services/mongodb-atlas/utils.js +113 -23
  43. package/dist/shared/handleUserRegistration.d.ts.map +1 -1
  44. package/dist/shared/handleUserRegistration.js +4 -1
  45. package/dist/shared/models/handleUserRegistration.model.d.ts +6 -2
  46. package/dist/shared/models/handleUserRegistration.model.d.ts.map +1 -1
  47. package/dist/utils/context/helpers.d.ts +7 -6
  48. package/dist/utils/context/helpers.d.ts.map +1 -1
  49. package/dist/utils/context/helpers.js +3 -0
  50. package/dist/utils/context/index.d.ts +1 -1
  51. package/dist/utils/context/index.d.ts.map +1 -1
  52. package/dist/utils/context/index.js +176 -5
  53. package/dist/utils/context/interface.d.ts +1 -1
  54. package/dist/utils/context/interface.d.ts.map +1 -1
  55. package/dist/utils/crypto/index.d.ts +1 -0
  56. package/dist/utils/crypto/index.d.ts.map +1 -1
  57. package/dist/utils/crypto/index.js +6 -2
  58. package/dist/utils/initializer/exposeRoutes.d.ts.map +1 -1
  59. package/dist/utils/initializer/exposeRoutes.js +11 -4
  60. package/dist/utils/initializer/registerPlugins.d.ts +3 -1
  61. package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
  62. package/dist/utils/initializer/registerPlugins.js +9 -6
  63. package/dist/utils/roles/helpers.js +11 -3
  64. package/dist/utils/roles/machines/commonValidators.d.ts.map +1 -1
  65. package/dist/utils/roles/machines/commonValidators.js +10 -6
  66. package/dist/utils/roles/machines/read/B/validators.d.ts +4 -0
  67. package/dist/utils/roles/machines/read/B/validators.d.ts.map +1 -0
  68. package/dist/utils/roles/machines/read/B/validators.js +8 -0
  69. package/dist/utils/roles/machines/read/C/index.d.ts.map +1 -1
  70. package/dist/utils/roles/machines/read/C/index.js +10 -7
  71. package/dist/utils/roles/machines/read/C/validators.d.ts +5 -0
  72. package/dist/utils/roles/machines/read/C/validators.d.ts.map +1 -0
  73. package/dist/utils/roles/machines/read/C/validators.js +29 -0
  74. package/dist/utils/roles/machines/read/D/index.d.ts.map +1 -1
  75. package/dist/utils/roles/machines/read/D/index.js +13 -11
  76. package/dist/utils/rules.d.ts +1 -1
  77. package/dist/utils/rules.d.ts.map +1 -1
  78. package/dist/utils/rules.js +26 -17
  79. package/jest.config.ts +2 -12
  80. package/jest.setup.ts +28 -0
  81. package/package.json +1 -2
  82. package/src/auth/controller.ts +70 -4
  83. package/src/auth/plugins/jwt.test.ts +93 -0
  84. package/src/auth/plugins/jwt.ts +62 -3
  85. package/src/auth/providers/custom-function/controller.ts +22 -5
  86. package/src/auth/providers/local-userpass/controller.ts +168 -96
  87. package/src/auth/providers/local-userpass/dtos.ts +13 -2
  88. package/src/auth/utils.ts +51 -86
  89. package/src/constants.ts +17 -3
  90. package/src/fastify.d.ts +32 -15
  91. package/src/features/functions/controller.ts +51 -3
  92. package/src/features/functions/dtos.ts +3 -0
  93. package/src/features/functions/interface.ts +3 -0
  94. package/src/features/functions/utils.ts +29 -8
  95. package/src/features/rules/utils.ts +11 -2
  96. package/src/features/triggers/index.ts +43 -1
  97. package/src/features/triggers/utils.ts +146 -38
  98. package/src/index.ts +69 -20
  99. package/src/services/mongodb-atlas/__tests__/findOneAndUpdate.test.ts +95 -0
  100. package/src/services/mongodb-atlas/__tests__/utils.test.ts +141 -0
  101. package/src/services/mongodb-atlas/index.ts +241 -90
  102. package/src/services/mongodb-atlas/model.ts +15 -2
  103. package/src/services/mongodb-atlas/utils.ts +158 -22
  104. package/src/shared/handleUserRegistration.ts +5 -4
  105. package/src/shared/models/handleUserRegistration.model.ts +8 -3
  106. package/src/types/fastify-raw-body.d.ts +22 -0
  107. package/src/utils/__tests__/STEP_B_STATES.test.ts +1 -1
  108. package/src/utils/__tests__/STEP_C_STATES.test.ts +1 -1
  109. package/src/utils/__tests__/STEP_D_STATES.test.ts +2 -2
  110. package/src/utils/__tests__/checkIsValidFieldNameFn.test.ts +9 -4
  111. package/src/utils/__tests__/registerPlugins.test.ts +16 -1
  112. package/src/utils/context/helpers.ts +3 -0
  113. package/src/utils/context/index.ts +238 -13
  114. package/src/utils/context/interface.ts +1 -1
  115. package/src/utils/crypto/index.ts +5 -1
  116. package/src/utils/initializer/exposeRoutes.ts +15 -8
  117. package/src/utils/initializer/registerPlugins.ts +15 -7
  118. package/src/utils/roles/helpers.ts +23 -5
  119. package/src/utils/roles/machines/commonValidators.ts +10 -5
  120. package/src/utils/roles/machines/read/B/validators.ts +8 -0
  121. package/src/utils/roles/machines/read/C/index.ts +11 -7
  122. package/src/utils/roles/machines/read/C/validators.ts +21 -0
  123. package/src/utils/roles/machines/read/D/index.ts +22 -12
  124. package/src/utils/rules.ts +31 -22
  125. package/tsconfig.spec.json +7 -0
package/README.md CHANGED
@@ -96,6 +96,13 @@ Ensure the following environment variables are set in your .env file or deployme
96
96
  | `APP_SECRET` | Secret used to sign and verify JWT tokens (choose a strong secret). | `supersecretkey123!` |
97
97
  | `HOST` | The host address the server binds to (usually `0.0.0.0` for public access). | `0.0.0.0` |
98
98
  | `HTTPS_SCHEMA` | The schema for your server requests (usually `https` or `http`). | `http` |
99
+ | `RESET_PASSWORD_TTL_SECONDS` | Time-to-live for password reset tokens (in seconds). | `3600` |
100
+ | `AUTH_RATE_LIMIT_WINDOW_MS` | Rate limit window for auth endpoints (in ms). | `900000` |
101
+ | `AUTH_LOGIN_MAX_ATTEMPTS` | Max login attempts per window. | `10` |
102
+ | `AUTH_RESET_MAX_ATTEMPTS` | Max reset requests per window. | `5` |
103
+ | `REFRESH_TOKEN_TTL_DAYS` | Refresh token time-to-live (in days). | `60` |
104
+ | `SWAGGER_UI_USER` | Basic Auth username for Swagger UI (optional). | `admin` |
105
+ | `SWAGGER_UI_PASSWORD` | Basic Auth password for Swagger UI (optional). | `change-me` |
99
106
 
100
107
 
101
108
  Example:
@@ -106,6 +113,13 @@ DB_CONNECTION_STRING=mongodb+srv://username:password@cluster.mongodb.net/dbname
106
113
  APP_SECRET=your-jwt-secret
107
114
  HOST=0.0.0.0
108
115
  HTTPS_SCHEMA=http
116
+ RESET_PASSWORD_TTL_SECONDS=3600
117
+ AUTH_RATE_LIMIT_WINDOW_MS=900000
118
+ AUTH_LOGIN_MAX_ATTEMPTS=10
119
+ AUTH_RESET_MAX_ATTEMPTS=5
120
+ REFRESH_TOKEN_TTL_DAYS=60
121
+ SWAGGER_UI_USER=admin
122
+ SWAGGER_UI_PASSWORD=change-me
109
123
  ```
110
124
 
111
125
  🛡️ Note: Never commit .env files to source control. Use a .gitignore file to exclude it.
@@ -406,6 +420,13 @@ Ensure the following environment variables are set in your .env file or deployme
406
420
  | `APP_SECRET` | Secret used to sign and verify JWT tokens (choose a strong secret). | `supersecretkey123!` |
407
421
  | `HOST` | The host address the server binds to (usually `0.0.0.0` for public access). | `0.0.0.0` |
408
422
  | `HTTPS_SCHEMA` | The schema for your server requests (usually `https` or `http`). | `http` |
423
+ | `RESET_PASSWORD_TTL_SECONDS` | Time-to-live for password reset tokens (in seconds). | `3600` |
424
+ | `AUTH_RATE_LIMIT_WINDOW_MS` | Rate limit window for auth endpoints (in ms). | `900000` |
425
+ | `AUTH_LOGIN_MAX_ATTEMPTS` | Max login attempts per window. | `10` |
426
+ | `AUTH_RESET_MAX_ATTEMPTS` | Max reset requests per window. | `5` |
427
+ | `REFRESH_TOKEN_TTL_DAYS` | Refresh token time-to-live (in days). | `60` |
428
+ | `SWAGGER_UI_USER` | Basic Auth username for Swagger UI (optional). | `admin` |
429
+ | `SWAGGER_UI_PASSWORD` | Basic Auth password for Swagger UI (optional). | `change-me` |
409
430
 
410
431
 
411
432
  Example:
@@ -416,6 +437,13 @@ DB_CONNECTION_STRING=mongodb+srv://username:password@cluster.mongodb.net/dbname
416
437
  APP_SECRET=your-jwt-secret
417
438
  HOST=0.0.0.0
418
439
  HTTPS_SCHEMA=http
440
+ RESET_PASSWORD_TTL_SECONDS=3600
441
+ AUTH_RATE_LIMIT_WINDOW_MS=900000
442
+ AUTH_LOGIN_MAX_ATTEMPTS=10
443
+ AUTH_RESET_MAX_ATTEMPTS=5
444
+ REFRESH_TOKEN_TTL_DAYS=60
445
+ SWAGGER_UI_USER=admin
446
+ SWAGGER_UI_PASSWORD=change-me
419
447
  ```
420
448
 
421
449
  🛡️ Note: Never commit .env files to source control. Use a .gitignore file to exclude it.
@@ -472,6 +500,3 @@ export default app;
472
500
 
473
501
  >🔗 The baseUrl should point to the backend URL you deployed earlier using Flowerbase.
474
502
  This tells the frontend SDK where to send authentication and data requests.
475
-
476
-
477
-
@@ -1 +1 @@
1
- {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/auth/controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAOzC;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,eAAe,iBA2ExD"}
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/auth/controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAQzC;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,eAAe,iBA4IxD"}
@@ -13,6 +13,7 @@ exports.authController = authController;
13
13
  const bson_1 = require("bson");
14
14
  const constants_1 = require("../constants");
15
15
  const utils_1 = require("./utils");
16
+ const crypto_1 = require("../utils/crypto");
16
17
  const HANDLER_TYPE = 'preHandler';
17
18
  /**
18
19
  * Controller for handling user authentication, profile retrieval, and session management.
@@ -21,8 +22,15 @@ const HANDLER_TYPE = 'preHandler';
21
22
  */
22
23
  function authController(app) {
23
24
  return __awaiter(this, void 0, void 0, function* () {
24
- const { authCollection, userCollection } = constants_1.AUTH_CONFIG;
25
+ const { authCollection, userCollection, refreshTokensCollection } = constants_1.AUTH_CONFIG;
25
26
  const db = app.mongo.client.db(constants_1.DB_NAME);
27
+ const refreshTokenTtlMs = constants_1.DEFAULT_CONFIG.REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000;
28
+ try {
29
+ yield db.collection(refreshTokensCollection).createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
30
+ }
31
+ catch (error) {
32
+ console.error('Failed to ensure refresh token TTL index', error);
33
+ }
26
34
  app.addHook(HANDLER_TYPE, app.jwtAuthentication);
27
35
  /**
28
36
  * Endpoint to retrieve the authenticated user's profile.
@@ -33,6 +41,9 @@ function authController(app) {
33
41
  */
34
42
  app.get(utils_1.AUTH_ENDPOINTS.PROFILE, function (req) {
35
43
  return __awaiter(this, void 0, void 0, function* () {
44
+ if (req.user.typ !== 'access') {
45
+ throw new Error('Access token required');
46
+ }
36
47
  const user = yield db
37
48
  .collection(authCollection)
38
49
  .findOne({ _id: bson_1.ObjectId.createFromHexString(req.user.id) });
@@ -61,6 +72,20 @@ function authController(app) {
61
72
  if (req.user.typ !== 'refresh') {
62
73
  throw new Error(utils_1.AUTH_ERRORS.INVALID_TOKEN);
63
74
  }
75
+ const authHeader = req.headers.authorization;
76
+ if (!(authHeader === null || authHeader === void 0 ? void 0 : authHeader.startsWith('Bearer '))) {
77
+ throw new Error(utils_1.AUTH_ERRORS.INVALID_TOKEN);
78
+ }
79
+ const refreshToken = authHeader.slice('Bearer '.length).trim();
80
+ const refreshTokenHash = (0, crypto_1.hashToken)(refreshToken);
81
+ const storedToken = yield db.collection(refreshTokensCollection).findOne({
82
+ tokenHash: refreshTokenHash,
83
+ revokedAt: null,
84
+ expiresAt: { $gt: new Date() }
85
+ });
86
+ if (!storedToken) {
87
+ throw new Error(utils_1.AUTH_ERRORS.INVALID_TOKEN);
88
+ }
64
89
  const auth_user = yield (db === null || db === void 0 ? void 0 : db.collection(authCollection).findOne({ _id: new this.mongo.ObjectId(req.user.sub) }));
65
90
  if (!auth_user) {
66
91
  throw new Error(`User with ID ${req.user.sub} not found`);
@@ -77,9 +102,38 @@ function authController(app) {
77
102
  /**
78
103
  * Endpoint to destroy the existing session.
79
104
  */
80
- app.delete(utils_1.AUTH_ENDPOINTS.SESSION, function () {
105
+ app.delete(utils_1.AUTH_ENDPOINTS.SESSION, function (req, res) {
81
106
  return __awaiter(this, void 0, void 0, function* () {
82
- return { status: "ok" };
107
+ var _a, _b;
108
+ const authHeader = req.headers.authorization;
109
+ if (!(authHeader === null || authHeader === void 0 ? void 0 : authHeader.startsWith('Bearer '))) {
110
+ res.status(204);
111
+ return;
112
+ }
113
+ const refreshToken = authHeader.slice('Bearer '.length).trim();
114
+ const refreshTokenHash = (0, crypto_1.hashToken)(refreshToken);
115
+ const now = new Date();
116
+ const expiresAt = new Date(Date.now() + refreshTokenTtlMs);
117
+ const updateResult = yield db.collection(refreshTokensCollection).findOneAndUpdate({ tokenHash: refreshTokenHash }, {
118
+ $set: {
119
+ revokedAt: now,
120
+ expiresAt
121
+ }
122
+ }, { returnDocument: 'after' });
123
+ const fromToken = (_a = req.user) === null || _a === void 0 ? void 0 : _a.sub;
124
+ let userId = (_b = updateResult === null || updateResult === void 0 ? void 0 : updateResult.value) === null || _b === void 0 ? void 0 : _b.userId;
125
+ if (!userId && fromToken) {
126
+ try {
127
+ userId = new bson_1.ObjectId(fromToken);
128
+ }
129
+ catch (_c) {
130
+ userId = fromToken;
131
+ }
132
+ }
133
+ if (userId && authCollection) {
134
+ yield db.collection(authCollection).updateOne({ _id: userId }, { $set: { lastLogoutAt: now } });
135
+ }
136
+ return { status: 'ok' };
83
137
  });
84
138
  });
85
139
  });
@@ -1 +1 @@
1
- {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../../src/auth/plugins/jwt.ts"],"names":[],"mappings":"AAIA,KAAK,OAAO,GAAG;IACb,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED;;;;;;;GAOG;iUAC8C,OAAO;AAAxD,wBAwDE"}
1
+ {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../../src/auth/plugins/jwt.ts"],"names":[],"mappings":"AAKA,KAAK,OAAO,GAAG;IACb,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAQD;;;;;;;GAOG;iUAC8C,OAAO;AAAxD,wBA4GE"}
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  const jwt_1 = __importDefault(require("@fastify/jwt"));
16
16
  const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
17
17
  const mongodb_1 = require("mongodb");
18
+ const constants_1 = require("../../constants");
18
19
  /**
19
20
  * This module is a Fastify plugin that sets up JWT-based authentication and token creation.
20
21
  * It registers JWT authentication, and provides methods to create access and refresh tokens.
@@ -31,12 +32,57 @@ exports.default = (0, fastify_plugin_1.default)(function (fastify, opts) {
31
32
  });
32
33
  fastify.decorate('jwtAuthentication', function (request, reply) {
33
34
  return __awaiter(this, void 0, void 0, function* () {
35
+ var _a, _b, _c;
34
36
  try {
35
37
  yield request.jwtVerify();
36
38
  }
37
39
  catch (err) {
38
- // TODO: handle error
39
- reply.send(err);
40
+ fastify.log.warn({ err }, 'JWT authentication failed');
41
+ reply.code(401).send({ message: 'Unauthorized' });
42
+ return;
43
+ }
44
+ if (((_a = request.user) === null || _a === void 0 ? void 0 : _a.typ) !== 'access') {
45
+ return;
46
+ }
47
+ const db = (_c = (_b = fastify.mongo) === null || _b === void 0 ? void 0 : _b.client) === null || _c === void 0 ? void 0 : _c.db(constants_1.DB_NAME);
48
+ if (!db) {
49
+ fastify.log.warn('Mongo client unavailable while checking logout state');
50
+ return;
51
+ }
52
+ if (!request.user.sub) {
53
+ reply.code(401).send({ message: 'Unauthorized' });
54
+ return;
55
+ }
56
+ let authUser;
57
+ try {
58
+ authUser = yield db
59
+ .collection(constants_1.AUTH_CONFIG.authCollection)
60
+ .findOne({ _id: new mongodb_1.ObjectId(request.user.sub) });
61
+ }
62
+ catch (err) {
63
+ fastify.log.warn({ err }, 'Failed to lookup user during JWT authentication');
64
+ reply.code(401).send({ message: 'Unauthorized' });
65
+ return;
66
+ }
67
+ if (!authUser) {
68
+ reply.code(401).send({ message: 'Unauthorized' });
69
+ return;
70
+ }
71
+ const lastLogoutAt = authUser.lastLogoutAt ? new Date(authUser.lastLogoutAt) : null;
72
+ const accessUser = request.user;
73
+ const rawIssuedAt = accessUser.iat;
74
+ const issuedAt = typeof rawIssuedAt === 'number'
75
+ ? rawIssuedAt
76
+ : typeof rawIssuedAt === 'string'
77
+ ? Number(rawIssuedAt)
78
+ : undefined;
79
+ if (lastLogoutAt &&
80
+ !Number.isNaN(lastLogoutAt.getTime()) &&
81
+ typeof issuedAt === 'number' &&
82
+ !Number.isNaN(issuedAt) &&
83
+ lastLogoutAt.getTime() >= issuedAt * 1000) {
84
+ reply.code(401).send({ message: 'Unauthorized' });
85
+ return;
40
86
  }
41
87
  });
42
88
  });
@@ -63,7 +109,7 @@ exports.default = (0, fastify_plugin_1.default)(function (fastify, opts) {
63
109
  baas_id: BAAS_ID
64
110
  }, {
65
111
  sub: user._id.toJSON(),
66
- expiresIn: '60d'
112
+ expiresIn: `${constants_1.DEFAULT_CONFIG.REFRESH_TOKEN_TTL_DAYS}d`
67
113
  });
68
114
  });
69
115
  });
@@ -1 +1 @@
1
- {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../../src/auth/providers/custom-function/controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAezC;;;;GAIG;AACH,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,eAAe,iBA8ElE"}
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../../src/auth/providers/custom-function/controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAgBzC;;;;GAIG;AACH,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,eAAe,iBA8FlE"}
@@ -18,6 +18,7 @@ const handleUserRegistration_1 = __importDefault(require("../../../shared/handle
18
18
  const handleUserRegistration_model_1 = require("../../../shared/models/handleUserRegistration.model");
19
19
  const state_1 = require("../../../state");
20
20
  const context_1 = require("../../../utils/context");
21
+ const crypto_1 = require("../../../utils/crypto");
21
22
  const utils_1 = require("../../utils");
22
23
  const schema_1 = require("./schema");
23
24
  /**
@@ -29,6 +30,9 @@ function customFunctionController(app) {
29
30
  return __awaiter(this, void 0, void 0, function* () {
30
31
  const functionsList = state_1.StateManager.select('functions');
31
32
  const services = state_1.StateManager.select('services');
33
+ const db = app.mongo.client.db(constants_1.DB_NAME);
34
+ const { refreshTokensCollection } = constants_1.AUTH_CONFIG;
35
+ const refreshTokenTtlMs = constants_1.DEFAULT_CONFIG.REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000;
32
36
  /**
33
37
  * Endpoint for user login.
34
38
  *
@@ -68,17 +72,29 @@ function customFunctionController(app) {
68
72
  });
69
73
  if (res.id) {
70
74
  const user = yield (0, handleUserRegistration_1.default)(app, { run_as_system: true, skipUserCheck: true, provider: handleUserRegistration_model_1.PROVIDER.CUSTOM_FUNCTION })({ email: res.id, password: (0, utils_1.generatePassword)() });
75
+ if (!(user === null || user === void 0 ? void 0 : user.insertedId)) {
76
+ throw new Error('Failed to register custom user');
77
+ }
71
78
  const currentUserData = {
72
79
  _id: user.insertedId,
73
80
  user_data: {
74
- _id: user.insertedId,
81
+ _id: user.insertedId
75
82
  }
76
83
  };
84
+ const refreshToken = this.createRefreshToken(currentUserData);
85
+ const refreshTokenHash = (0, crypto_1.hashToken)(refreshToken);
86
+ yield db.collection(refreshTokensCollection).insertOne({
87
+ userId: user.insertedId,
88
+ tokenHash: refreshTokenHash,
89
+ createdAt: new Date(),
90
+ expiresAt: new Date(Date.now() + refreshTokenTtlMs),
91
+ revokedAt: null
92
+ });
77
93
  return {
78
94
  access_token: this.createAccessToken(currentUserData),
79
- refresh_token: this.createRefreshToken(currentUserData),
95
+ refresh_token: refreshToken,
80
96
  device_id: '',
81
- user_id: user.insertedId.toString(),
97
+ user_id: user.insertedId.toString()
82
98
  };
83
99
  }
84
100
  throw new Error("Authentication Failed");
@@ -1 +1 @@
1
- {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../../src/auth/providers/local-userpass/controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAuBzC;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,eAAe,iBAkOjE"}
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../../src/auth/providers/local-userpass/controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAmCzC;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,eAAe,iBA+RjE"}
@@ -13,15 +13,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.localUserPassController = localUserPassController;
16
- const mail_1 = __importDefault(require("@sendgrid/mail"));
17
16
  const constants_1 = require("../../../constants");
18
- const services_1 = require("../../../services");
19
17
  const handleUserRegistration_1 = __importDefault(require("../../../shared/handleUserRegistration"));
20
18
  const handleUserRegistration_model_1 = require("../../../shared/models/handleUserRegistration.model");
21
19
  const state_1 = require("../../../state");
22
20
  const context_1 = require("../../../utils/context");
23
21
  const crypto_1 = require("../../../utils/crypto");
24
22
  const utils_1 = require("../../utils");
23
+ const rateLimitStore = new Map();
24
+ const isRateLimited = (key, maxAttempts, windowMs) => {
25
+ var _a;
26
+ const now = Date.now();
27
+ const existing = (_a = rateLimitStore.get(key)) !== null && _a !== void 0 ? _a : [];
28
+ const recent = existing.filter((timestamp) => now - timestamp < windowMs);
29
+ recent.push(now);
30
+ rateLimitStore.set(key, recent);
31
+ return recent.length > maxAttempts;
32
+ };
25
33
  /**
26
34
  * Controller for handling local user registration and login.
27
35
  * @testable
@@ -29,9 +37,59 @@ const utils_1 = require("../../utils");
29
37
  */
30
38
  function localUserPassController(app) {
31
39
  return __awaiter(this, void 0, void 0, function* () {
32
- const functionsList = state_1.StateManager.select('functions');
33
- const { authCollection, userCollection, user_id_field, on_user_creation_function_name } = constants_1.AUTH_CONFIG;
40
+ const { authCollection, userCollection, user_id_field } = constants_1.AUTH_CONFIG;
41
+ const { resetPasswordCollection } = constants_1.AUTH_CONFIG;
42
+ const { refreshTokensCollection } = constants_1.AUTH_CONFIG;
34
43
  const db = app.mongo.client.db(constants_1.DB_NAME);
44
+ const resetPasswordTtlSeconds = constants_1.DEFAULT_CONFIG.RESET_PASSWORD_TTL_SECONDS;
45
+ const rateLimitWindowMs = constants_1.DEFAULT_CONFIG.AUTH_RATE_LIMIT_WINDOW_MS;
46
+ const loginMaxAttempts = constants_1.DEFAULT_CONFIG.AUTH_LOGIN_MAX_ATTEMPTS;
47
+ const resetMaxAttempts = constants_1.DEFAULT_CONFIG.AUTH_RESET_MAX_ATTEMPTS;
48
+ const refreshTokenTtlMs = constants_1.DEFAULT_CONFIG.REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000;
49
+ try {
50
+ yield db.collection(resetPasswordCollection).createIndex({ createdAt: 1 }, { expireAfterSeconds: resetPasswordTtlSeconds });
51
+ }
52
+ catch (error) {
53
+ console.error('Failed to ensure reset password TTL index', error);
54
+ }
55
+ try {
56
+ yield db.collection(refreshTokensCollection).createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
57
+ }
58
+ catch (error) {
59
+ console.error('Failed to ensure refresh token TTL index', error);
60
+ }
61
+ const handleResetPasswordRequest = (email, password, extraArguments) => __awaiter(this, void 0, void 0, function* () {
62
+ const { resetPasswordConfig } = constants_1.AUTH_CONFIG;
63
+ const authUser = yield db.collection(authCollection).findOne({
64
+ email
65
+ });
66
+ if (!authUser) {
67
+ return;
68
+ }
69
+ const token = (0, crypto_1.generateToken)();
70
+ const tokenId = (0, crypto_1.generateToken)();
71
+ yield (db === null || db === void 0 ? void 0 : db.collection(resetPasswordCollection).updateOne({ email }, { $set: { token, tokenId, email, createdAt: new Date() } }, { upsert: true }));
72
+ if (!resetPasswordConfig.runResetFunction && !resetPasswordConfig.resetFunctionName) {
73
+ throw new Error(utils_1.AUTH_ERRORS.MISSING_RESET_FUNCTION);
74
+ }
75
+ if (resetPasswordConfig.runResetFunction && resetPasswordConfig.resetFunctionName) {
76
+ const functionsList = state_1.StateManager.select('functions');
77
+ const services = state_1.StateManager.select('services');
78
+ const currentFunction = functionsList[resetPasswordConfig.resetFunctionName];
79
+ const baseArgs = { token, tokenId, email, password, username: email };
80
+ const args = Array.isArray(extraArguments) ? [baseArgs, ...extraArguments] : [baseArgs];
81
+ yield (0, context_1.GenerateContext)({
82
+ args,
83
+ app,
84
+ rules: {},
85
+ user: {},
86
+ currentFunction,
87
+ functionsList,
88
+ services
89
+ });
90
+ return;
91
+ }
92
+ });
35
93
  /**
36
94
  * Endpoint for user registration.
37
95
  *
@@ -44,8 +102,12 @@ function localUserPassController(app) {
44
102
  schema: utils_1.REGISTRATION_SCHEMA
45
103
  }, (req, res) => __awaiter(this, void 0, void 0, function* () {
46
104
  const result = yield (0, handleUserRegistration_1.default)(app, { run_as_system: true, provider: handleUserRegistration_model_1.PROVIDER.LOCAL_USERPASS })({ email: req.body.email.toLowerCase(), password: req.body.password });
105
+ if (!(result === null || result === void 0 ? void 0 : result.insertedId)) {
106
+ res === null || res === void 0 ? void 0 : res.status(500);
107
+ throw new Error('Failed to register user');
108
+ }
47
109
  res === null || res === void 0 ? void 0 : res.status(201);
48
- return { userId: result === null || result === void 0 ? void 0 : result.insertedId.toString() };
110
+ return { userId: result.insertedId.toString() };
49
111
  }));
50
112
  /**
51
113
  * Endpoint for user login.
@@ -56,8 +118,13 @@ function localUserPassController(app) {
56
118
  */
57
119
  app.post(utils_1.AUTH_ENDPOINTS.LOGIN, {
58
120
  schema: utils_1.LOGIN_SCHEMA
59
- }, function (req) {
121
+ }, function (req, res) {
60
122
  return __awaiter(this, void 0, void 0, function* () {
123
+ const key = `login:${req.ip}`;
124
+ if (isRateLimited(key, loginMaxAttempts, rateLimitWindowMs)) {
125
+ res.status(429).send({ message: 'Too many requests' });
126
+ return;
127
+ }
61
128
  const authUser = yield db.collection(authCollection).findOne({
62
129
  email: req.body.username
63
130
  });
@@ -74,7 +141,7 @@ function localUserPassController(app) {
74
141
  .findOne({ [user_id_field]: authUser._id.toString() })
75
142
  : {};
76
143
  authUser === null || authUser === void 0 ? true : delete authUser.password;
77
- const userWithCustomData = Object.assign(Object.assign({}, authUser), { user_data: user, id: authUser._id.toString() });
144
+ const userWithCustomData = Object.assign(Object.assign({}, authUser), { user_data: Object.assign(Object.assign({}, (user || {})), { _id: authUser._id }), data: { email: authUser.email }, id: authUser._id.toString() });
78
145
  if (authUser && authUser.status === 'pending') {
79
146
  try {
80
147
  yield (db === null || db === void 0 ? void 0 : db.collection(authCollection).updateOne({ _id: authUser._id }, {
@@ -87,35 +154,18 @@ function localUserPassController(app) {
87
154
  console.log('>>> 🚀 ~ localUserPassController ~ error:', error);
88
155
  }
89
156
  }
90
- if (authUser &&
91
- authUser.status === 'pending' &&
92
- on_user_creation_function_name &&
93
- functionsList[on_user_creation_function_name]) {
94
- try {
95
- yield (0, context_1.GenerateContext)({
96
- args: [
97
- {
98
- operationType: 'CREATE',
99
- providers: 'local-userpass',
100
- user: userWithCustomData,
101
- time: new Date().getTime()
102
- }
103
- ],
104
- app,
105
- rules: {},
106
- user: userWithCustomData,
107
- currentFunction: functionsList[on_user_creation_function_name],
108
- functionsList,
109
- services: services_1.services
110
- });
111
- }
112
- catch (error) {
113
- console.log('localUserPassController - /login - GenerateContext - CATCH:', error);
114
- }
115
- }
157
+ const refreshToken = this.createRefreshToken(userWithCustomData);
158
+ const refreshTokenHash = (0, crypto_1.hashToken)(refreshToken);
159
+ yield db.collection(refreshTokensCollection).insertOne({
160
+ userId: authUser._id,
161
+ tokenHash: refreshTokenHash,
162
+ createdAt: new Date(),
163
+ expiresAt: new Date(Date.now() + refreshTokenTtlMs),
164
+ revokedAt: null
165
+ });
116
166
  return {
117
167
  access_token: this.createAccessToken(userWithCustomData),
118
- refresh_token: this.createRefreshToken(userWithCustomData),
168
+ refresh_token: refreshToken,
119
169
  device_id: '',
120
170
  user_id: authUser._id.toString()
121
171
  };
@@ -124,48 +174,40 @@ function localUserPassController(app) {
124
174
  /**
125
175
  * Endpoint for reset password.
126
176
  *
127
- * @route {POST} /reset/call
177
+ * @route {POST} /reset/send
128
178
  * @param {ResetPasswordDto} req - The request object with th reset request.
129
179
  * @returns {Promise<void>}
130
180
  */
131
181
  app.post(utils_1.AUTH_ENDPOINTS.RESET, {
132
- schema: utils_1.RESET_SCHEMA
133
- }, function (req) {
182
+ schema: utils_1.RESET_SEND_SCHEMA
183
+ }, function (req, res) {
134
184
  return __awaiter(this, void 0, void 0, function* () {
135
- const { resetPasswordCollection, resetPasswordConfig } = constants_1.AUTH_CONFIG;
136
- const email = req.body.email;
137
- const authUser = yield db.collection(authCollection).findOne({
138
- email
139
- });
140
- if (!authUser) {
141
- throw new Error(utils_1.AUTH_ERRORS.INVALID_CREDENTIALS);
185
+ const key = `reset:${req.ip}`;
186
+ if (isRateLimited(key, resetMaxAttempts, rateLimitWindowMs)) {
187
+ res.status(429);
188
+ return { message: 'Too many requests' };
142
189
  }
143
- const token = (0, crypto_1.generateToken)();
144
- const tokenId = (0, crypto_1.generateToken)();
145
- yield (db === null || db === void 0 ? void 0 : db.collection(resetPasswordCollection).updateOne({ email }, { $set: { token, tokenId, email, createdAt: new Date() } }, { upsert: true }));
146
- if (resetPasswordConfig.runResetFunction && resetPasswordConfig.resetFunctionName) {
147
- const functionsList = state_1.StateManager.select('functions');
148
- const services = state_1.StateManager.select('services');
149
- const currentFunction = functionsList[resetPasswordConfig.resetFunctionName];
150
- yield (0, context_1.GenerateContext)({
151
- args: [{ token, tokenId, email }],
152
- app,
153
- rules: {},
154
- user: {},
155
- currentFunction,
156
- functionsList,
157
- services
158
- });
159
- return;
190
+ yield handleResetPasswordRequest(req.body.email);
191
+ res.status(202);
192
+ return {
193
+ status: 'ok'
194
+ };
195
+ });
196
+ });
197
+ app.post(utils_1.AUTH_ENDPOINTS.RESET_CALL, {
198
+ schema: utils_1.RESET_CALL_SCHEMA
199
+ }, function (req, res) {
200
+ return __awaiter(this, void 0, void 0, function* () {
201
+ const key = `reset:${req.ip}`;
202
+ if (isRateLimited(key, resetMaxAttempts, rateLimitWindowMs)) {
203
+ res.status(429);
204
+ return { message: 'Too many requests' };
160
205
  }
161
- const { from, subject, mailToken, body } = (0, utils_1.getMailConfig)(resetPasswordConfig, token, tokenId);
162
- mail_1.default.setApiKey(mailToken);
163
- yield mail_1.default.send({
164
- to: email,
165
- from,
166
- subject,
167
- html: body
168
- });
206
+ yield handleResetPasswordRequest(req.body.email, req.body.password, req.body.arguments);
207
+ res.status(202);
208
+ return {
209
+ status: 'ok'
210
+ };
169
211
  });
170
212
  });
171
213
  /**
@@ -177,14 +219,26 @@ function localUserPassController(app) {
177
219
  */
178
220
  app.post(utils_1.AUTH_ENDPOINTS.CONFIRM_RESET, {
179
221
  schema: utils_1.CONFIRM_RESET_SCHEMA
180
- }, function (req) {
222
+ }, function (req, res) {
181
223
  return __awaiter(this, void 0, void 0, function* () {
182
- const { resetPasswordCollection } = constants_1.AUTH_CONFIG;
224
+ const key = `reset-confirm:${req.ip}`;
225
+ if (isRateLimited(key, resetMaxAttempts, rateLimitWindowMs)) {
226
+ res.status(429);
227
+ return { message: 'Too many requests' };
228
+ }
183
229
  const { token, tokenId, password } = req.body;
184
230
  const resetRequest = yield (db === null || db === void 0 ? void 0 : db.collection(resetPasswordCollection).findOne({ token, tokenId }));
185
231
  if (!resetRequest) {
186
232
  throw new Error(utils_1.AUTH_ERRORS.INVALID_RESET_PARAMS);
187
233
  }
234
+ const createdAt = resetRequest.createdAt ? new Date(resetRequest.createdAt) : null;
235
+ const isExpired = !createdAt ||
236
+ Number.isNaN(createdAt.getTime()) ||
237
+ Date.now() - createdAt.getTime() > resetPasswordTtlSeconds * 1000;
238
+ if (isExpired) {
239
+ yield (db === null || db === void 0 ? void 0 : db.collection(resetPasswordCollection).deleteOne({ _id: resetRequest._id }));
240
+ throw new Error(utils_1.AUTH_ERRORS.INVALID_RESET_PARAMS);
241
+ }
188
242
  const hashedPassword = yield (0, crypto_1.hashPassword)(password);
189
243
  yield db.collection(authCollection).updateOne({ email: resetRequest.email }, {
190
244
  $set: {
@@ -12,17 +12,26 @@ export type LoginSuccessDto = {
12
12
  refresh_token: string;
13
13
  user_id: string;
14
14
  };
15
+ export type ErrorResponseDto = {
16
+ message: string;
17
+ };
15
18
  export interface RegistrationDto {
16
19
  Body: RegisterUserDto;
17
20
  }
18
21
  export interface LoginDto {
19
22
  Body: LoginUserDto;
20
- Reply: LoginSuccessDto;
23
+ Reply: LoginSuccessDto | ErrorResponseDto;
24
+ }
25
+ export interface ResetPasswordSendDto {
26
+ Body: {
27
+ email: string;
28
+ };
21
29
  }
22
- export interface ResetPasswordDto {
30
+ export interface ResetPasswordCallDto {
23
31
  Body: {
24
32
  email: string;
25
33
  password: string;
34
+ arguments?: unknown[];
26
35
  };
27
36
  }
28
37
  export interface ConfirmResetPasswordDto {
@@ -1 +1 @@
1
- {"version":3,"file":"dtos.d.ts","sourceRoot":"","sources":["../../../../src/auth/providers/local-userpass/dtos.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,eAAe,CAAA;CACtB;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,YAAY,CAAA;IAClB,KAAK,EAAE,eAAe,CAAA;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAA;QACb,QAAQ,EAAE,MAAM,CAAA;KACjB,CAAA;CACF;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAA;QACb,OAAO,EAAE,MAAM,CAAA;QACf,QAAQ,EAAE,MAAM,CAAA;KACjB,CAAA;CACF"}
1
+ {"version":3,"file":"dtos.d.ts","sourceRoot":"","sources":["../../../../src/auth/providers/local-userpass/dtos.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,eAAe,CAAA;CACtB;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,YAAY,CAAA;IAClB,KAAK,EAAE,eAAe,GAAG,gBAAgB,CAAA;CAC1C;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAA;KACd,CAAA;CACF;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAA;QACb,QAAQ,EAAE,MAAM,CAAA;QAChB,SAAS,CAAC,EAAE,OAAO,EAAE,CAAA;KACtB,CAAA;CACF;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAA;QACb,OAAO,EAAE,MAAM,CAAA;QACf,QAAQ,EAAE,MAAM,CAAA;KACjB,CAAA;CACF"}