@flowerforce/flowerbase 1.7.6-beta.3 → 1.7.6-beta.5

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 (33) hide show
  1. package/dist/auth/providers/anon-user/controller.d.ts.map +1 -1
  2. package/dist/auth/providers/anon-user/controller.js +1 -0
  3. package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
  4. package/dist/auth/providers/custom-function/controller.js +3 -1
  5. package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
  6. package/dist/auth/providers/local-userpass/controller.js +30 -8
  7. package/dist/auth/utils.d.ts +1 -0
  8. package/dist/auth/utils.d.ts.map +1 -1
  9. package/dist/auth/utils.js +1 -0
  10. package/dist/features/endpoints/utils.d.ts.map +1 -1
  11. package/dist/features/endpoints/utils.js +3 -0
  12. package/dist/features/triggers/interface.d.ts +1 -1
  13. package/dist/features/triggers/interface.d.ts.map +1 -1
  14. package/dist/features/triggers/utils.d.ts.map +1 -1
  15. package/dist/features/triggers/utils.js +60 -0
  16. package/dist/utils/context/helpers.d.ts.map +1 -1
  17. package/dist/utils/context/helpers.js +3 -2
  18. package/dist/utils/context/index.d.ts.map +1 -1
  19. package/dist/utils/context/index.js +4 -2
  20. package/package.json +1 -1
  21. package/src/auth/providers/anon-user/controller.ts +1 -0
  22. package/src/auth/providers/custom-function/controller.ts +6 -1
  23. package/src/auth/providers/local-userpass/__tests__/controller.test.ts +200 -0
  24. package/src/auth/providers/local-userpass/controller.ts +44 -9
  25. package/src/auth/utils.ts +1 -0
  26. package/src/features/endpoints/__tests__/utils.test.ts +65 -0
  27. package/src/features/endpoints/utils.ts +3 -0
  28. package/src/features/triggers/interface.ts +1 -1
  29. package/src/features/triggers/utils.ts +60 -0
  30. package/src/utils/__tests__/contextExecuteCompatibility.test.ts +27 -1
  31. package/src/utils/__tests__/generateContextData.test.ts +5 -1
  32. package/src/utils/context/helpers.ts +3 -2
  33. package/src/utils/context/index.ts +4 -3
@@ -1 +1 @@
1
- {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../../src/auth/providers/anon-user/controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAOzC;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,eAAe,iBAwE5D"}
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../../src/auth/providers/anon-user/controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAOzC;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,eAAe,iBAyE5D"}
@@ -54,6 +54,7 @@ function anonUserController(app) {
54
54
  email: anonEmail,
55
55
  status: 'confirmed',
56
56
  createdAt: now,
57
+ lastLoginAt: now,
57
58
  custom_data: {},
58
59
  identities: [
59
60
  {
@@ -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;AAUzC;;;;GAIG;AACH,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,eAAe,iBAqIlE"}
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;AAUzC;;;;GAIG;AACH,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,eAAe,iBA0IlE"}
@@ -111,15 +111,17 @@ function customFunctionController(app) {
111
111
  user_data: Object.assign(Object.assign({}, (user || {})), { id: authUser._id.toString(), email: authUser.email }),
112
112
  custom_data: Object.assign({}, (user || {}))
113
113
  };
114
+ const now = new Date();
114
115
  const refreshToken = this.createRefreshToken(currentUserData);
115
116
  const refreshTokenHash = (0, crypto_1.hashToken)(refreshToken);
116
117
  yield authDb.collection(refreshTokensCollection).insertOne({
117
118
  userId: authUser._id,
118
119
  tokenHash: refreshTokenHash,
119
- createdAt: new Date(),
120
+ createdAt: now,
120
121
  expiresAt: new Date(Date.now() + refreshTokenTtlMs),
121
122
  revokedAt: null
122
123
  });
124
+ yield authDb.collection(authCollection).updateOne({ _id: authUser._id }, { $set: { lastLoginAt: now } });
123
125
  return {
124
126
  access_token: this.createAccessToken(currentUserData),
125
127
  refresh_token: refreshToken,
@@ -1 +1 @@
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;AAqCzC;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,eAAe,iBAqXjE"}
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;AAuCzC;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,eAAe,iBAsZjE"}
@@ -81,7 +81,7 @@ function localUserPassController(app) {
81
81
  const currentFunction = functionsList[resetPasswordConfig.resetFunctionName];
82
82
  const baseArgs = { token, tokenId, email, password, username: email };
83
83
  const args = Array.isArray(extraArguments) ? [baseArgs, ...extraArguments] : [baseArgs];
84
- yield (0, context_1.GenerateContext)({
84
+ const response = yield (0, context_1.GenerateContext)({
85
85
  args,
86
86
  app,
87
87
  rules: {},
@@ -89,10 +89,32 @@ function localUserPassController(app) {
89
89
  currentFunction,
90
90
  functionName: resetPasswordConfig.resetFunctionName,
91
91
  functionsList,
92
- services
92
+ services,
93
+ runAsSystem: true
93
94
  });
94
- return;
95
+ const resetStatus = response === null || response === void 0 ? void 0 : response.status;
96
+ if (resetStatus === 'success') {
97
+ if (!password) {
98
+ throw new Error(utils_1.AUTH_ERRORS.INVALID_RESET_FUNCTION_RESPONSE);
99
+ }
100
+ const hashedPassword = yield (0, crypto_1.hashPassword)(password);
101
+ yield authDb.collection(authCollection).updateOne({ email }, {
102
+ $set: {
103
+ password: hashedPassword
104
+ }
105
+ });
106
+ yield (authDb === null || authDb === void 0 ? void 0 : authDb.collection(resetPasswordCollection).deleteOne({ email }));
107
+ return { status: 'success' };
108
+ }
109
+ if (resetStatus === 'pending') {
110
+ return { status: 'pending' };
111
+ }
112
+ if (resetStatus === 'fail') {
113
+ throw new Error(utils_1.AUTH_ERRORS.INVALID_RESET_PARAMS);
114
+ }
115
+ throw new Error(utils_1.AUTH_ERRORS.INVALID_RESET_FUNCTION_RESPONSE);
95
116
  }
117
+ return { status: 'pending' };
96
118
  });
97
119
  /**
98
120
  * Endpoint for user registration.
@@ -214,15 +236,17 @@ function localUserPassController(app) {
214
236
  if (authUser && authUser.status !== 'confirmed') {
215
237
  throw new Error(utils_1.AUTH_ERRORS.USER_NOT_CONFIRMED);
216
238
  }
239
+ const now = new Date();
217
240
  const refreshToken = this.createRefreshToken(userWithCustomData);
218
241
  const refreshTokenHash = (0, crypto_1.hashToken)(refreshToken);
219
242
  yield authDb.collection(refreshTokensCollection).insertOne({
220
243
  userId: authUser._id,
221
244
  tokenHash: refreshTokenHash,
222
- createdAt: new Date(),
245
+ createdAt: now,
223
246
  expiresAt: new Date(Date.now() + refreshTokenTtlMs),
224
247
  revokedAt: null
225
248
  });
249
+ yield authDb.collection(authCollection).updateOne({ _id: authUser._id }, { $set: { lastLoginAt: now } });
226
250
  return {
227
251
  access_token: this.createAccessToken(userWithCustomData),
228
252
  refresh_token: refreshToken,
@@ -271,11 +295,9 @@ function localUserPassController(app) {
271
295
  res.status(429);
272
296
  return { message: 'Too many requests' };
273
297
  }
274
- yield handleResetPasswordRequest(req.body.email, req.body.password, req.body.arguments);
298
+ const result = yield handleResetPasswordRequest(req.body.email, req.body.password, req.body.arguments);
275
299
  res.status(202);
276
- return {
277
- status: 'ok'
278
- };
300
+ return result;
279
301
  });
280
302
  });
281
303
  /**
@@ -146,6 +146,7 @@ export declare enum AUTH_ERRORS {
146
146
  INVALID_TOKEN = "Invalid refresh token provided",
147
147
  INVALID_RESET_PARAMS = "Invalid token or tokenId provided",
148
148
  MISSING_RESET_FUNCTION = "Missing reset function",
149
+ INVALID_RESET_FUNCTION_RESPONSE = "Invalid reset function response",
149
150
  USER_NOT_CONFIRMED = "User not confirmed"
150
151
  }
151
152
  export interface AuthConfig {
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/auth/utils.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,cAAc;;;CAA4C,CAAC;AACxE,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;CAexB,CAAA;AAED,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;CAc7B,CAAA;AAED,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;CAgB7B,CAAA;AAED,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;CAWhC,CAAA;AAED,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;CAU/B,CAAA;AAED,eAAO,MAAM,YAAY;;;;;;;;;;;;;;CAAoB,CAAA;AAE7C,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;CAe/B,CAAA;AAED,oBAAY,cAAc;IACxB,KAAK,WAAW;IAChB,YAAY,cAAc;IAC1B,OAAO,aAAa;IACpB,OAAO,aAAa;IACpB,OAAO,aAAa;IACpB,KAAK,gBAAgB;IACrB,UAAU,gBAAgB;IAC1B,aAAa,WAAW;IACxB,UAAU,sBAAsB;CACjC;AAED,oBAAY,WAAW;IACrB,mBAAmB,wBAAwB;IAC3C,aAAa,mCAAmC;IAChD,oBAAoB,sCAAsC;IAC1D,sBAAsB,2BAA2B;IACjD,kBAAkB,uBAAuB;CAC1C;AAED,MAAM,WAAW,UAAU;IACzB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,gBAAgB,EAAE,aAAa,CAAA;IAC/B,iBAAiB,EAAE,cAAc,CAAA;IACjC,WAAW,CAAC,EAAE,QAAQ,CAAA;CACvB;AAED,UAAU,MAAM;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;CAClB;AACD,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;CACf;AAED,UAAU,cAAc;IACtB,IAAI,EAAE,iBAAiB,CAAC;IACxB,IAAI,EAAE,iBAAiB,CAAC;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE;QACN,kBAAkB,EAAE,MAAM,CAAA;KAC3B,CAAA;CACF;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,WAAW,CAAA;IACjB,IAAI,EAAE,WAAW,CAAA;IACjB,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,MAAM;IACrB,WAAW,EAAE,OAAO,CAAA;IACpB,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC,iBAAiB,EAAE,MAAM,CAAA;IACzB,gBAAgB,EAAE,MAAM,CAAA;IACxB,uBAAuB,EAAE,OAAO,CAAA;IAChC,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAA;IAChB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,aAAa,EAAE,MAAM,CAAA;IACrB,eAAe,EAAE,MAAM,CAAA;IACvB,aAAa,EAAE,MAAM,CAAA;IACrB,8BAA8B,EAAE,MAAM,CAAA;CACvC;AAMD;;;GAGG;AACH,eAAO,MAAM,cAAc,QAAO,UAuCjC,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,QAAO,oBAarC,CAAA;AAED,eAAO,MAAM,gBAAgB,GAAI,eAAW,WAG3C,CAAA"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/auth/utils.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,cAAc;;;CAA4C,CAAC;AACxE,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;CAexB,CAAA;AAED,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;CAc7B,CAAA;AAED,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;CAgB7B,CAAA;AAED,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;CAWhC,CAAA;AAED,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;CAU/B,CAAA;AAED,eAAO,MAAM,YAAY;;;;;;;;;;;;;;CAAoB,CAAA;AAE7C,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;CAe/B,CAAA;AAED,oBAAY,cAAc;IACxB,KAAK,WAAW;IAChB,YAAY,cAAc;IAC1B,OAAO,aAAa;IACpB,OAAO,aAAa;IACpB,OAAO,aAAa;IACpB,KAAK,gBAAgB;IACrB,UAAU,gBAAgB;IAC1B,aAAa,WAAW;IACxB,UAAU,sBAAsB;CACjC;AAED,oBAAY,WAAW;IACrB,mBAAmB,wBAAwB;IAC3C,aAAa,mCAAmC;IAChD,oBAAoB,sCAAsC;IAC1D,sBAAsB,2BAA2B;IACjD,+BAA+B,oCAAoC;IACnE,kBAAkB,uBAAuB;CAC1C;AAED,MAAM,WAAW,UAAU;IACzB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,gBAAgB,EAAE,aAAa,CAAA;IAC/B,iBAAiB,EAAE,cAAc,CAAA;IACjC,WAAW,CAAC,EAAE,QAAQ,CAAA;CACvB;AAED,UAAU,MAAM;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;CAClB;AACD,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;CACf;AAED,UAAU,cAAc;IACtB,IAAI,EAAE,iBAAiB,CAAC;IACxB,IAAI,EAAE,iBAAiB,CAAC;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE;QACN,kBAAkB,EAAE,MAAM,CAAA;KAC3B,CAAA;CACF;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,WAAW,CAAA;IACjB,IAAI,EAAE,WAAW,CAAA;IACjB,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,MAAM;IACrB,WAAW,EAAE,OAAO,CAAA;IACpB,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC,iBAAiB,EAAE,MAAM,CAAA;IACzB,gBAAgB,EAAE,MAAM,CAAA;IACxB,uBAAuB,EAAE,OAAO,CAAA;IAChC,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAA;IAChB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,aAAa,EAAE,MAAM,CAAA;IACrB,eAAe,EAAE,MAAM,CAAA;IACvB,aAAa,EAAE,MAAM,CAAA;IACrB,8BAA8B,EAAE,MAAM,CAAA;CACvC;AAMD;;;GAGG;AACH,eAAO,MAAM,cAAc,QAAO,UAuCjC,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,QAAO,oBAarC,CAAA;AAED,eAAO,MAAM,gBAAgB,GAAI,eAAW,WAG3C,CAAA"}
@@ -115,6 +115,7 @@ var AUTH_ERRORS;
115
115
  AUTH_ERRORS["INVALID_TOKEN"] = "Invalid refresh token provided";
116
116
  AUTH_ERRORS["INVALID_RESET_PARAMS"] = "Invalid token or tokenId provided";
117
117
  AUTH_ERRORS["MISSING_RESET_FUNCTION"] = "Missing reset function";
118
+ AUTH_ERRORS["INVALID_RESET_FUNCTION_RESPONSE"] = "Invalid reset function response";
118
119
  AUTH_ERRORS["USER_NOT_CONFIRMED"] = "User not confirmed";
119
120
  })(AUTH_ERRORS || (exports.AUTH_ERRORS = AUTH_ERRORS = {}));
120
121
  const resolveAppPath = () => { var _a, _b, _c; return (_c = (_a = process.env.FLOWERBASE_APP_PATH) !== null && _a !== void 0 ? _a : (_b = require.main) === null || _b === void 0 ? void 0 : _b.path) !== null && _c !== void 0 ? _c : process.cwd(); };
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/features/endpoints/utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAKvE,OAAO,EAAE,SAAS,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAE9D;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAU,gBAAuB,KAAG,OAAO,CAAC,SAAS,CA+B9E,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAC3B,KAAK,eAAe,EACpB,SAAS,UAAU,CAAC,OAAO,eAAe,CAAC,EAC3C,UAAU,MAAM;;;;;;;CAkDhB,CAAA;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAI,kEAM7B,qBAAqB,MACR,KAAK,cAAc,EAAE,KAAK,YAAY,gBA6CrD,CAAA"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/features/endpoints/utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAKvE,OAAO,EAAE,SAAS,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAE9D;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAU,gBAAuB,KAAG,OAAO,CAAC,SAAS,CA+B9E,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAC3B,KAAK,eAAe,EACpB,SAAS,UAAU,CAAC,OAAO,eAAe,CAAC,EAC3C,UAAU,MAAM;;;;;;;CAkDhB,CAAA;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAI,kEAM7B,qBAAqB,MACR,KAAK,cAAc,EAAE,KAAK,YAAY,gBAgDrD,CAAA"}
@@ -135,6 +135,9 @@ const generateHandler = ({ app, currentFunction, functionName, functionsList, ru
135
135
  setStatusCode: (code) => {
136
136
  res.status(code);
137
137
  },
138
+ setHeader: (name, value) => {
139
+ res.header(name, value);
140
+ },
138
141
  setBody: (body) => {
139
142
  customResponseBody.data = body;
140
143
  }
@@ -22,7 +22,7 @@ type Config = {
22
22
  isAutoTrigger?: boolean;
23
23
  match: Record<string, unknown>;
24
24
  operation_types: string[];
25
- operation_type?: 'CREATE' | 'DELETE' | 'LOGOUT';
25
+ operation_type?: 'CREATE' | 'DELETE' | 'LOGIN' | 'LOGOUT';
26
26
  providers?: string[];
27
27
  project: Record<string, unknown>;
28
28
  service_name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../../src/features/triggers/interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AACnD,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAE5D,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,WAAW,CAAA;IACjB,QAAQ,EAAE,OAAO,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,gBAAgB,EAAE;QAChB,QAAQ,EAAE;YACR,MAAM,EAAE;gBACN,aAAa,EAAE,MAAM,CAAA;aACtB,CAAA;SACF,CAAA;KACF,CAAA;CACF;AAED,KAAK,MAAM,GAAG;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,aAAa,EAAE,OAAO,CAAA;IACtB,2BAA2B,EAAE,OAAO,CAAA;IACpC,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,cAAc,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAA;IAC/C,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;IACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAChC,YAAY,EAAE,MAAM,CAAA;IACpB,mBAAmB,EAAE,OAAO,CAAA;IAC5B,sBAAsB,EAAE,OAAO,CAAA;IAC/B,SAAS,EAAE,OAAO,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,gBAAgB,CAAA;AACrE,MAAM,MAAM,QAAQ,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,EAAE,CAAA;AAE/D,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,QAAQ,CAAA;IACxB,GAAG,EAAE,eAAe,CAAA;IACpB,QAAQ,EAAE,QAAQ,CAAA;IAClB,aAAa,EAAE,SAAS,CAAA;IACxB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,WAAW,CAAA;IACxB,YAAY,EAAE,MAAM,CAAA;CACrB,CAAA"}
1
+ {"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../../src/features/triggers/interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AACnD,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAE5D,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,WAAW,CAAA;IACjB,QAAQ,EAAE,OAAO,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,gBAAgB,EAAE;QAChB,QAAQ,EAAE;YACR,MAAM,EAAE;gBACN,aAAa,EAAE,MAAM,CAAA;aACtB,CAAA;SACF,CAAA;KACF,CAAA;CACF;AAED,KAAK,MAAM,GAAG;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,aAAa,EAAE,OAAO,CAAA;IACtB,2BAA2B,EAAE,OAAO,CAAA;IACpC,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,cAAc,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAA;IACzD,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;IACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAChC,YAAY,EAAE,MAAM,CAAA;IACpB,mBAAmB,EAAE,OAAO,CAAA;IAC5B,sBAAsB,EAAE,OAAO,CAAA;IAC/B,SAAS,EAAE,OAAO,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,gBAAgB,CAAA;AACrE,MAAM,MAAM,QAAQ,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,EAAE,CAAA;AAE/D,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,QAAQ,CAAA;IACxB,GAAG,EAAE,eAAe,CAAA;IACpB,QAAQ,EAAE,QAAQ,CAAA;IAClB,aAAa,EAAE,SAAS,CAAA;IACxB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,WAAW,CAAA;IACxB,YAAY,EAAE,MAAM,CAAA;CACrB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/features/triggers/utils.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,aAAa,EAAW,QAAQ,EAAE,MAAM,aAAa,CAAA;AA0E9D;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,GAAU,gBAAuB,KAAG,OAAO,CAAC,QAAQ,CAkB5E,CAAA;AA+mBD,eAAO,MAAM,gBAAgB;kHAxlB1B,aAAa;iHAggBb,aAAa;uHA/Yb,aAAa;CA2ef,CAAA"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/features/triggers/utils.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,aAAa,EAAW,QAAQ,EAAE,MAAM,aAAa,CAAA;AA0E9D;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,GAAU,gBAAuB,KAAG,OAAO,CAAC,QAAQ,CAkB5E,CAAA;AA2qBD,eAAO,MAAM,gBAAgB;kHAppB1B,aAAa;iHA4jBb,aAAa;uHA1cb,aAAa;CAsiBf,CAAA"}
@@ -170,6 +170,7 @@ const handleCronTrigger = (_a) => __awaiter(void 0, [_a], void 0, function* ({ c
170
170
  const mapOpInverse = {
171
171
  CREATE: ['insert', 'update', 'replace'],
172
172
  DELETE: ['delete'],
173
+ LOGIN: ['insert', 'update'],
173
174
  LOGOUT: ['update'],
174
175
  };
175
176
  const normalizeOperationTypes = (operationTypes = []) => operationTypes.map((op) => op.toLowerCase());
@@ -270,6 +271,8 @@ const handleAuthenticationTrigger = (_a) => __awaiter(void 0, [_a], void 0, func
270
271
  const isUpdate = operationType === 'update';
271
272
  const isReplace = operationType === 'replace';
272
273
  const isDelete = operationType === 'delete';
274
+ const isLoginInsert = isInsert && !!(fullDocument === null || fullDocument === void 0 ? void 0 : fullDocument.lastLoginAt);
275
+ const isLoginUpdate = isUpdate && !!updatedFields && 'lastLoginAt' in updatedFields;
273
276
  const isLogoutUpdate = isUpdate && !!updatedFields && 'lastLogoutAt' in updatedFields;
274
277
  let confirmedCandidate = false;
275
278
  let confirmedDocument = fullDocument;
@@ -353,6 +356,63 @@ const handleAuthenticationTrigger = (_a) => __awaiter(void 0, [_a], void 0, func
353
356
  }
354
357
  return;
355
358
  }
359
+ if (operation_type === 'LOGIN') {
360
+ if (!isLoginInsert && !isLoginUpdate) {
361
+ return;
362
+ }
363
+ let loginDocument = fullDocument !== null && fullDocument !== void 0 ? fullDocument : confirmedDocument;
364
+ if (!loginDocument && (documentKey === null || documentKey === void 0 ? void 0 : documentKey._id)) {
365
+ loginDocument = (yield collection.findOne({
366
+ _id: documentKey._id
367
+ }));
368
+ }
369
+ if (!matchesProviderFilter(loginDocument, providerFilter)) {
370
+ return;
371
+ }
372
+ const userData = buildUserData(loginDocument);
373
+ if (!userData) {
374
+ return;
375
+ }
376
+ const op = {
377
+ operationType: 'LOGIN',
378
+ fullDocument,
379
+ fullDocumentBeforeChange,
380
+ documentKey,
381
+ updateDescription
382
+ };
383
+ try {
384
+ emitTriggerEvent({
385
+ status: 'fired',
386
+ triggerName,
387
+ triggerType,
388
+ functionName,
389
+ meta: Object.assign(Object.assign({}, baseMeta), { event: 'LOGIN' })
390
+ });
391
+ yield (0, context_1.GenerateContext)({
392
+ args: [Object.assign({ user: userData }, op)],
393
+ app,
394
+ rules: state_1.StateManager.select("rules"),
395
+ user: {}, // TODO from currentUser ??
396
+ currentFunction: triggerHandler,
397
+ functionName,
398
+ functionsList,
399
+ services,
400
+ runAsSystem: true
401
+ });
402
+ }
403
+ catch (error) {
404
+ emitTriggerEvent({
405
+ status: 'error',
406
+ triggerName,
407
+ triggerType,
408
+ functionName,
409
+ meta: Object.assign(Object.assign({}, baseMeta), { event: 'LOGIN' }),
410
+ error
411
+ });
412
+ console.log("🚀 ~ handleAuthenticationTrigger ~ error:", error);
413
+ }
414
+ return;
415
+ }
356
416
  if (isDelete) {
357
417
  if (isAutoTrigger || operation_type !== 'DELETE') {
358
418
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/utils/context/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAG1C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAE3C,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAA;AAEvD,KAAK,QAAQ,GAAG;IACd,MAAM,EAAE,CACN,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GAAG,MAAM,EACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACzC,MAAM,CAAA;IACX,MAAM,EAAE,CACN,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GAAG,MAAM,EACpB,YAAY,CAAC,EAAE,OAAO,EACtB,sBAAsB,CAAC,EAAE,MAAM,EAAE,KAC9B,OAAO,CAAA;CACb,CAAA;AAgFD;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,GAAI,4GAUjC,yBAAyB;;;;;;;;;;;;;uBA4DP,SAAS;yBAGP,SAAS;;;;;;;;;;;;;;;;;;uBAcb,MAAM;;;;;;+BA5CU,MAAM,OAAO,QAAQ;;;;sCA1HrC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BAsGT,CAAC;iCAAa,CAAC;;;;;;;;;;;;;;;;;;;kCAtGP,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BAsGT,CAAC;6BAAa,CAAC;;;;;;;;;;;;;;;;;;kCAtGP,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BAsGT,CAAC;6BAAa,CAAC;;;;;;;;;;;;;;;4BAyEF,MAAM,OAAO,aAAa,WAAW,SAAS;;;CAiBrE,CAAA"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/utils/context/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAG1C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAE3C,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAA;AAEvD,KAAK,QAAQ,GAAG;IACd,MAAM,EAAE,CACN,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GAAG,MAAM,EACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACzC,MAAM,CAAA;IACX,MAAM,EAAE,CACN,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GAAG,MAAM,EACpB,YAAY,CAAC,EAAE,OAAO,EACtB,sBAAsB,CAAC,EAAE,MAAM,EAAE,KAC9B,OAAO,CAAA;CACb,CAAA;AAgFD;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,GAAI,4GAUjC,yBAAyB;;;;;;;;;;;;;uBA4DP,SAAS;yBAGP,SAAS;;;;;;;;;;;;;;;;;;uBAcb,MAAM;;;;;;+BA5CU,MAAM,OAAO,QAAQ;;;;sCA1HrC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BAsGT,CAAC;iCAAa,CAAC;;;;;;;;;;;;;;;;;;;kCAtGP,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BAsGT,CAAC;6BAAa,CAAC;;;;;;;;;;;;;;;;;;kCAtGP,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BAsGT,CAAC;6BAAa,CAAC;;;;;;;;;;;;;;;4BAyEF,MAAM,OAAO,aAAa,WAAW,SAAS;;;CAkBrE,CAAA"}
@@ -176,16 +176,17 @@ const generateContextData = ({ user, services, app, rules, currentFunction, func
176
176
  https: getService('api'),
177
177
  functions: {
178
178
  execute: (name, ...args) => {
179
- const currentFunction = functionsList[name];
179
+ const targetFunction = functionsList[name];
180
180
  return GenerateContextSync({
181
181
  args,
182
182
  app,
183
183
  rules,
184
184
  user,
185
- currentFunction,
185
+ currentFunction: targetFunction,
186
186
  functionName: String(name),
187
187
  functionsList,
188
188
  services,
189
+ runAsSystem: currentFunction.run_as_system,
189
190
  deserializeArgs: false
190
191
  });
191
192
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/context/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AA4JnD;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CAAC,EACpC,IAAI,EACJ,GAAG,EACH,KAAK,EACL,IAAI,EACJ,eAAe,EACf,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,eAAsB,EACtB,OAAO,EACP,OAAO,EACR,EAAE,qBAAqB,GAAG,OAAO,CAAC,OAAO,CAAC,CA2G1C;AAED,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,GAAG,EACH,KAAK,EACL,IAAI,EACJ,eAAe,EACf,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,eAAsB,EACtB,OAAO,EACR,EAAE,qBAAqB,GAAG,OAAO,CA4BjC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/context/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AA4JnD;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CAAC,EACpC,IAAI,EACJ,GAAG,EACH,KAAK,EACL,IAAI,EACJ,eAAe,EACf,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,eAAsB,EACtB,OAAO,EACP,OAAO,EACR,EAAE,qBAAqB,GAAG,OAAO,CAAC,OAAO,CAAC,CA2G1C;AAED,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,GAAG,EACH,KAAK,EACL,IAAI,EACJ,eAAe,EACf,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,eAAsB,EACtB,OAAO,EACR,EAAE,qBAAqB,GAAG,OAAO,CA6BjC"}
@@ -153,7 +153,8 @@ function GenerateContext(_a) {
153
153
  if (!currentFunction)
154
154
  return;
155
155
  const functionsQueue = state_1.StateManager.select("functionsQueue");
156
- const functionToRun = Object.assign({ run_as_system: runAsSystem }, currentFunction);
156
+ const effectiveRunAsSystem = Boolean(runAsSystem || currentFunction.run_as_system);
157
+ const functionToRun = Object.assign(Object.assign({}, currentFunction), { run_as_system: effectiveRunAsSystem });
157
158
  const run = () => __awaiter(this, void 0, void 0, function* () {
158
159
  var _a;
159
160
  const contextData = (0, helpers_1.generateContextData)({
@@ -232,7 +233,8 @@ function GenerateContextSync({ args, app, rules, user, currentFunction, function
232
233
  var _a;
233
234
  if (!currentFunction)
234
235
  return;
235
- const functionToRun = Object.assign({ run_as_system: runAsSystem }, currentFunction);
236
+ const effectiveRunAsSystem = Boolean(runAsSystem || currentFunction.run_as_system);
237
+ const functionToRun = Object.assign(Object.assign({}, currentFunction), { run_as_system: effectiveRunAsSystem });
236
238
  const contextData = (0, helpers_1.generateContextData)({
237
239
  user,
238
240
  services,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowerforce/flowerbase",
3
- "version": "1.7.6-beta.3",
3
+ "version": "1.7.6-beta.5",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -50,6 +50,7 @@ export async function anonUserController(app: FastifyInstance) {
50
50
  email: anonEmail,
51
51
  status: 'confirmed',
52
52
  createdAt: now,
53
+ lastLoginAt: now,
53
54
  custom_data: {},
54
55
  identities: [
55
56
  {
@@ -128,15 +128,20 @@ export async function customFunctionController(app: FastifyInstance) {
128
128
  ...(user || {})
129
129
  }
130
130
  }
131
+ const now = new Date()
131
132
  const refreshToken = this.createRefreshToken(currentUserData)
132
133
  const refreshTokenHash = hashToken(refreshToken)
133
134
  await authDb.collection(refreshTokensCollection).insertOne({
134
135
  userId: authUser._id,
135
136
  tokenHash: refreshTokenHash,
136
- createdAt: new Date(),
137
+ createdAt: now,
137
138
  expiresAt: new Date(Date.now() + refreshTokenTtlMs),
138
139
  revokedAt: null
139
140
  })
141
+ await authDb.collection(authCollection!).updateOne(
142
+ { _id: authUser._id },
143
+ { $set: { lastLoginAt: now } }
144
+ )
140
145
  return {
141
146
  access_token: this.createAccessToken(currentUserData),
142
147
  refresh_token: refreshToken,
@@ -0,0 +1,200 @@
1
+ jest.mock('../../../../constants', () => ({
2
+ AUTH_CONFIG: {
3
+ authCollection: 'auth_users',
4
+ refreshTokensCollection: 'refresh_tokens',
5
+ resetPasswordCollection: 'reset_password_requests',
6
+ userCollection: 'users',
7
+ user_id_field: 'id',
8
+ authProviders: {
9
+ 'local-userpass': {
10
+ disabled: false
11
+ }
12
+ },
13
+ resetPasswordConfig: {
14
+ runResetFunction: true,
15
+ resetFunctionName: 'customReset'
16
+ }
17
+ },
18
+ AUTH_DB_NAME: 'test-auth-db',
19
+ DB_NAME: 'test-db',
20
+ DEFAULT_CONFIG: {
21
+ RESET_PASSWORD_TTL_SECONDS: 3600,
22
+ AUTH_RATE_LIMIT_WINDOW_MS: 60000,
23
+ AUTH_LOGIN_MAX_ATTEMPTS: 5,
24
+ AUTH_REGISTER_MAX_ATTEMPTS: 5,
25
+ AUTH_RESET_MAX_ATTEMPTS: 5,
26
+ REFRESH_TOKEN_TTL_DAYS: 1
27
+ }
28
+ }))
29
+
30
+ jest.mock('../../../../state', () => ({
31
+ StateManager: {
32
+ select: jest.fn((key: string) => {
33
+ if (key === 'functions') {
34
+ return {
35
+ customReset: { name: 'customReset', code: 'exports = async () => ({ status: "success" })' }
36
+ }
37
+ }
38
+ if (key === 'services') {
39
+ return {}
40
+ }
41
+ return {}
42
+ })
43
+ }
44
+ }))
45
+
46
+ jest.mock('../../../../utils/context', () => ({
47
+ GenerateContext: jest.fn()
48
+ }))
49
+
50
+ jest.mock('../../../../utils/crypto', () => ({
51
+ comparePassword: jest.fn(),
52
+ generateToken: jest.fn(() => 'generated-token'),
53
+ hashPassword: jest.fn(async (password: string) => `hashed:${password}`),
54
+ hashToken: jest.fn(() => 'hashed-token')
55
+ }))
56
+
57
+ import { AUTH_ERRORS } from '../../../utils'
58
+ import { localUserPassController } from '../controller'
59
+ import { GenerateContext } from '../../../../utils/context'
60
+ import { hashPassword } from '../../../../utils/crypto'
61
+
62
+ describe('localUserPassController reset call', () => {
63
+ const buildApp = () => {
64
+ let resetCallHandler:
65
+ | ((req: { body: { email: string; password: string; arguments?: unknown[] }; ip: string }, res: { status: jest.Mock }) => Promise<unknown>)
66
+ | undefined
67
+
68
+ const authUsersCollection = {
69
+ findOne: jest.fn().mockResolvedValue({
70
+ _id: 'auth-user-1',
71
+ email: 'john@doe.com',
72
+ password: 'old-hash'
73
+ }),
74
+ updateOne: jest.fn().mockResolvedValue({ acknowledged: true })
75
+ }
76
+ const resetCollection = {
77
+ createIndex: jest.fn().mockResolvedValue('ok'),
78
+ updateOne: jest.fn().mockResolvedValue({ acknowledged: true }),
79
+ deleteOne: jest.fn().mockResolvedValue({ acknowledged: true }),
80
+ findOne: jest.fn()
81
+ }
82
+ const refreshCollection = {
83
+ createIndex: jest.fn().mockResolvedValue('ok'),
84
+ insertOne: jest.fn()
85
+ }
86
+ const usersCollection = {
87
+ findOne: jest.fn()
88
+ }
89
+ const db = {
90
+ collection: jest.fn((name: string) => {
91
+ if (name === 'auth_users') return authUsersCollection
92
+ if (name === 'reset_password_requests') return resetCollection
93
+ if (name === 'refresh_tokens') return refreshCollection
94
+ if (name === 'users') return usersCollection
95
+ return {}
96
+ })
97
+ }
98
+ const app = {
99
+ mongo: { client: { db: jest.fn().mockReturnValue(db) } },
100
+ post: jest.fn((path: string, _opts: unknown, handler: typeof resetCallHandler) => {
101
+ if (path === '/reset/call') {
102
+ resetCallHandler = handler
103
+ }
104
+ })
105
+ }
106
+
107
+ return { app, authUsersCollection, resetCollection, resetCallHandlerRef: () => resetCallHandler }
108
+ }
109
+
110
+ beforeEach(() => {
111
+ jest.clearAllMocks()
112
+ })
113
+
114
+ it('hashes and applies the password when the custom reset function returns success', async () => {
115
+ ;(GenerateContext as jest.Mock).mockResolvedValue({ status: 'success' })
116
+ const { app, authUsersCollection, resetCollection, resetCallHandlerRef } = buildApp()
117
+
118
+ await localUserPassController(app as never)
119
+
120
+ const res = { status: jest.fn() }
121
+ const result = await resetCallHandlerRef()?.(
122
+ {
123
+ body: { email: 'john@doe.com', password: 'new-secret', arguments: ['extra'] },
124
+ ip: '127.0.0.1'
125
+ },
126
+ res
127
+ )
128
+
129
+ expect(GenerateContext).toHaveBeenCalledWith(expect.objectContaining({
130
+ args: [
131
+ {
132
+ token: 'generated-token',
133
+ tokenId: 'generated-token',
134
+ email: 'john@doe.com',
135
+ password: 'new-secret',
136
+ username: 'john@doe.com'
137
+ },
138
+ 'extra'
139
+ ],
140
+ runAsSystem: true
141
+ }))
142
+ expect(hashPassword).toHaveBeenCalledWith('new-secret')
143
+ expect(authUsersCollection.updateOne).toHaveBeenCalledWith(
144
+ { email: 'john@doe.com' },
145
+ { $set: { password: 'hashed:new-secret' } }
146
+ )
147
+ expect(resetCollection.deleteOne).toHaveBeenCalledWith({ email: 'john@doe.com' })
148
+ expect(res.status).toHaveBeenCalledWith(202)
149
+ expect(result).toEqual({ status: 'success' })
150
+ })
151
+
152
+ it('returns pending without changing the password when the custom reset function returns pending', async () => {
153
+ ;(GenerateContext as jest.Mock).mockResolvedValue({ status: 'pending' })
154
+ const { app, authUsersCollection, resetCollection, resetCallHandlerRef } = buildApp()
155
+
156
+ await localUserPassController(app as never)
157
+
158
+ const res = { status: jest.fn() }
159
+ const result = await resetCallHandlerRef()?.(
160
+ {
161
+ body: { email: 'john@doe.com', password: 'new-secret' },
162
+ ip: '127.0.0.1'
163
+ },
164
+ res
165
+ )
166
+
167
+ expect(hashPassword).not.toHaveBeenCalled()
168
+ expect(authUsersCollection.updateOne).not.toHaveBeenCalledWith(
169
+ { email: 'john@doe.com' },
170
+ expect.objectContaining({ $set: { password: expect.any(String) } })
171
+ )
172
+ expect(resetCollection.deleteOne).not.toHaveBeenCalled()
173
+ expect(res.status).toHaveBeenCalledWith(202)
174
+ expect(result).toEqual({ status: 'pending' })
175
+ })
176
+
177
+ it('rejects the request when the custom reset function returns fail', async () => {
178
+ ;(GenerateContext as jest.Mock).mockResolvedValue({ status: 'fail' })
179
+ const { app, authUsersCollection, resetCollection, resetCallHandlerRef } = buildApp()
180
+
181
+ await localUserPassController(app as never)
182
+
183
+ const res = { status: jest.fn() }
184
+
185
+ await expect(
186
+ resetCallHandlerRef()?.(
187
+ {
188
+ body: { email: 'john@doe.com', password: 'new-secret' },
189
+ ip: '127.0.0.1'
190
+ },
191
+ res
192
+ )
193
+ ).rejects.toThrow(AUTH_ERRORS.INVALID_RESET_PARAMS)
194
+
195
+ expect(hashPassword).not.toHaveBeenCalled()
196
+ expect(authUsersCollection.updateOne).not.toHaveBeenCalled()
197
+ expect(resetCollection.deleteOne).not.toHaveBeenCalled()
198
+ expect(res.status).not.toHaveBeenCalled()
199
+ })
200
+ })
@@ -27,6 +27,8 @@ import {
27
27
 
28
28
  const rateLimitStore = new Map<string, number[]>()
29
29
 
30
+ type ResetFunctionResult = { status?: 'success' | 'pending' | 'fail' }
31
+
30
32
  const isRateLimited = (key: string, maxAttempts: number, windowMs: number) => {
31
33
  const now = Date.now()
32
34
  const existing = rateLimitStore.get(key) ?? []
@@ -106,7 +108,7 @@ export async function localUserPassController(app: FastifyInstance) {
106
108
  const currentFunction = functionsList[resetPasswordConfig.resetFunctionName]
107
109
  const baseArgs = { token, tokenId, email, password, username: email }
108
110
  const args = Array.isArray(extraArguments) ? [baseArgs, ...extraArguments] : [baseArgs]
109
- await GenerateContext({
111
+ const response = await GenerateContext({
110
112
  args,
111
113
  app,
112
114
  rules: {},
@@ -114,11 +116,41 @@ export async function localUserPassController(app: FastifyInstance) {
114
116
  currentFunction,
115
117
  functionName: resetPasswordConfig.resetFunctionName,
116
118
  functionsList,
117
- services
118
- })
119
- return
119
+ services,
120
+ runAsSystem: true
121
+ }) as ResetFunctionResult
122
+ const resetStatus = response?.status
123
+
124
+ if (resetStatus === 'success') {
125
+ if (!password) {
126
+ throw new Error(AUTH_ERRORS.INVALID_RESET_FUNCTION_RESPONSE)
127
+ }
128
+
129
+ const hashedPassword = await hashPassword(password)
130
+ await authDb.collection(authCollection!).updateOne(
131
+ { email },
132
+ {
133
+ $set: {
134
+ password: hashedPassword
135
+ }
136
+ }
137
+ )
138
+ await authDb?.collection(resetPasswordCollection).deleteOne({ email })
139
+ return { status: 'success' as const }
140
+ }
141
+
142
+ if (resetStatus === 'pending') {
143
+ return { status: 'pending' as const }
144
+ }
145
+
146
+ if (resetStatus === 'fail') {
147
+ throw new Error(AUTH_ERRORS.INVALID_RESET_PARAMS)
148
+ }
149
+
150
+ throw new Error(AUTH_ERRORS.INVALID_RESET_FUNCTION_RESPONSE)
120
151
  }
121
152
 
153
+ return { status: 'pending' as const }
122
154
  }
123
155
 
124
156
  /**
@@ -283,15 +315,20 @@ export async function localUserPassController(app: FastifyInstance) {
283
315
  throw new Error(AUTH_ERRORS.USER_NOT_CONFIRMED)
284
316
  }
285
317
 
318
+ const now = new Date()
286
319
  const refreshToken = this.createRefreshToken(userWithCustomData)
287
320
  const refreshTokenHash = hashToken(refreshToken)
288
321
  await authDb.collection(refreshTokensCollection).insertOne({
289
322
  userId: authUser._id,
290
323
  tokenHash: refreshTokenHash,
291
- createdAt: new Date(),
324
+ createdAt: now,
292
325
  expiresAt: new Date(Date.now() + refreshTokenTtlMs),
293
326
  revokedAt: null
294
327
  })
328
+ await authDb.collection(authCollection!).updateOne(
329
+ { _id: authUser._id },
330
+ { $set: { lastLoginAt: now } }
331
+ )
295
332
 
296
333
  return {
297
334
  access_token: this.createAccessToken(userWithCustomData),
@@ -347,15 +384,13 @@ export async function localUserPassController(app: FastifyInstance) {
347
384
  res.status(429)
348
385
  return { message: 'Too many requests' }
349
386
  }
350
- await handleResetPasswordRequest(
387
+ const result = await handleResetPasswordRequest(
351
388
  req.body.email,
352
389
  req.body.password,
353
390
  req.body.arguments
354
391
  )
355
392
  res.status(202)
356
- return {
357
- status: 'ok'
358
- }
393
+ return result
359
394
  }
360
395
  )
361
396
 
package/src/auth/utils.ts CHANGED
@@ -117,6 +117,7 @@ export enum AUTH_ERRORS {
117
117
  INVALID_TOKEN = 'Invalid refresh token provided',
118
118
  INVALID_RESET_PARAMS = 'Invalid token or tokenId provided',
119
119
  MISSING_RESET_FUNCTION = 'Missing reset function',
120
+ INVALID_RESET_FUNCTION_RESPONSE = 'Invalid reset function response',
120
121
  USER_NOT_CONFIRMED = 'User not confirmed'
121
122
  }
122
123
 
@@ -0,0 +1,65 @@
1
+ import { GenerateContext } from '../../../utils/context'
2
+ import { generateHandler } from '../utils'
3
+
4
+ jest.mock('../../../utils/context', () => ({
5
+ GenerateContext: jest.fn()
6
+ }))
7
+
8
+ const mockedGenerateContext = jest.mocked(GenerateContext)
9
+
10
+ describe('generateHandler', () => {
11
+ beforeEach(() => {
12
+ mockedGenerateContext.mockReset()
13
+ })
14
+
15
+ it('allows endpoint functions to set custom response headers', async () => {
16
+ mockedGenerateContext.mockImplementation(async ({ args }) => {
17
+ const [, response] = args as [
18
+ { body: { text: () => string; rawBody: Buffer | string | undefined } },
19
+ {
20
+ setStatusCode: (code: number) => void
21
+ setHeader: (name: string, value: string | number | readonly string[]) => void
22
+ setBody: (body: unknown) => void
23
+ }
24
+ ]
25
+
26
+ response.setStatusCode(201)
27
+ response.setHeader('Content-Type', 'application/json')
28
+ response.setHeader('Cache-Control', 'no-store')
29
+ response.setBody(JSON.stringify({ ok: true }))
30
+
31
+ return { ignored: true }
32
+ })
33
+
34
+ const handler = generateHandler({
35
+ app: {} as any,
36
+ currentFunction: { code: 'module.exports = function () {}' } as any,
37
+ functionName: 'endpointHandler',
38
+ functionsList: {
39
+ endpointHandler: { code: 'module.exports = function () {}' }
40
+ } as any,
41
+ http_method: 'POST',
42
+ rulesList: {} as any
43
+ })
44
+
45
+ const res = {
46
+ status: jest.fn(),
47
+ header: jest.fn(),
48
+ send: jest.fn((body) => body)
49
+ } as any
50
+
51
+ const response = await handler({
52
+ body: { hello: 'world' },
53
+ headers: { accept: 'application/json' },
54
+ query: { page: '1' },
55
+ rawBody: '{"hello":"world"}',
56
+ user: { id: 'user-1' }
57
+ } as any, res)
58
+
59
+ expect(res.status).toHaveBeenCalledWith(201)
60
+ expect(res.header).toHaveBeenCalledWith('Content-Type', 'application/json')
61
+ expect(res.header).toHaveBeenCalledWith('Cache-Control', 'no-store')
62
+ expect(res.send).toHaveBeenCalledWith(JSON.stringify({ ok: true }))
63
+ expect(response).toBe(JSON.stringify({ ok: true }))
64
+ })
65
+ })
@@ -138,6 +138,9 @@ export const generateHandler = ({
138
138
  setStatusCode: (code: number) => {
139
139
  res.status(code)
140
140
  },
141
+ setHeader: (name: string, value: string | number | readonly string[]) => {
142
+ res.header(name, value)
143
+ },
141
144
  setBody: (body: unknown) => {
142
145
  customResponseBody.data = body
143
146
  }
@@ -24,7 +24,7 @@ type Config = {
24
24
  isAutoTrigger?: boolean
25
25
  match: Record<string, unknown>
26
26
  operation_types: string[]
27
- operation_type?: 'CREATE' | 'DELETE' | 'LOGOUT'
27
+ operation_type?: 'CREATE' | 'DELETE' | 'LOGIN' | 'LOGOUT'
28
28
  providers?: string[]
29
29
  project: Record<string, unknown>
30
30
  service_name: string
@@ -172,6 +172,7 @@ const handleCronTrigger = async ({
172
172
  const mapOpInverse = {
173
173
  CREATE: ['insert', 'update', 'replace'],
174
174
  DELETE: ['delete'],
175
+ LOGIN: ['insert', 'update'],
175
176
  LOGOUT: ['update'],
176
177
  }
177
178
 
@@ -306,6 +307,8 @@ const handleAuthenticationTrigger = async ({
306
307
  const isUpdate = operationType === 'update'
307
308
  const isReplace = operationType === 'replace'
308
309
  const isDelete = operationType === 'delete'
310
+ const isLoginInsert = isInsert && !!(fullDocument as Record<string, unknown> | null)?.lastLoginAt
311
+ const isLoginUpdate = isUpdate && !!updatedFields && 'lastLoginAt' in updatedFields
309
312
  const isLogoutUpdate = isUpdate && !!updatedFields && 'lastLogoutAt' in updatedFields
310
313
 
311
314
  let confirmedCandidate = false
@@ -397,6 +400,63 @@ const handleAuthenticationTrigger = async ({
397
400
  return
398
401
  }
399
402
 
403
+ if (operation_type === 'LOGIN') {
404
+ if (!isLoginInsert && !isLoginUpdate) {
405
+ return
406
+ }
407
+ let loginDocument = fullDocument ?? confirmedDocument
408
+ if (!loginDocument && documentKey?._id) {
409
+ loginDocument = await collection.findOne({
410
+ _id: documentKey._id
411
+ }) as Record<string, unknown> | null
412
+ }
413
+ if (!matchesProviderFilter(loginDocument, providerFilter)) {
414
+ return
415
+ }
416
+ const userData = buildUserData(loginDocument)
417
+ if (!userData) {
418
+ return
419
+ }
420
+ const op = {
421
+ operationType: 'LOGIN',
422
+ fullDocument,
423
+ fullDocumentBeforeChange,
424
+ documentKey,
425
+ updateDescription
426
+ }
427
+ try {
428
+ emitTriggerEvent({
429
+ status: 'fired',
430
+ triggerName,
431
+ triggerType,
432
+ functionName,
433
+ meta: { ...baseMeta, event: 'LOGIN' }
434
+ })
435
+ await GenerateContext({
436
+ args: [{ user: userData, ...op }],
437
+ app,
438
+ rules: StateManager.select("rules"),
439
+ user: {}, // TODO from currentUser ??
440
+ currentFunction: triggerHandler,
441
+ functionName,
442
+ functionsList,
443
+ services,
444
+ runAsSystem: true
445
+ })
446
+ } catch (error) {
447
+ emitTriggerEvent({
448
+ status: 'error',
449
+ triggerName,
450
+ triggerType,
451
+ functionName,
452
+ meta: { ...baseMeta, event: 'LOGIN' },
453
+ error
454
+ })
455
+ console.log("🚀 ~ handleAuthenticationTrigger ~ error:", error)
456
+ }
457
+ return
458
+ }
459
+
400
460
  if (isDelete) {
401
461
  if (isAutoTrigger || operation_type !== 'DELETE') {
402
462
  return
@@ -4,7 +4,7 @@ import { Functions } from '../../features/functions/interface'
4
4
  const mockServices = {
5
5
  api: jest.fn().mockReturnValue({}),
6
6
  aws: jest.fn().mockReturnValue({}),
7
- 'mongodb-atlas': jest.fn().mockReturnValue({})
7
+ 'mongodb-atlas': jest.fn((_app, options) => options ?? {})
8
8
  } as any
9
9
 
10
10
  describe('context.functions.execute compatibility', () => {
@@ -91,4 +91,30 @@ describe('context.functions.execute compatibility', () => {
91
91
 
92
92
  expect(result).toBe(true)
93
93
  })
94
+
95
+ it('propagates run_as_system to child functions executed through context.functions.execute', () => {
96
+ const functionsList = {
97
+ caller: {
98
+ run_as_system: true,
99
+ code: 'module.exports = function() { return context.functions.execute("target") }'
100
+ },
101
+ target: {
102
+ run_as_system: false,
103
+ code: 'module.exports = function() { return context.services.get("mongodb-atlas").run_as_system }'
104
+ }
105
+ } as Functions
106
+
107
+ const result = GenerateContextSync({
108
+ args: [],
109
+ app: {} as any,
110
+ rules: {} as any,
111
+ user: {} as any,
112
+ currentFunction: functionsList.caller,
113
+ functionsList,
114
+ services: mockServices,
115
+ functionName: 'caller'
116
+ })
117
+
118
+ expect(result).toBe(true)
119
+ })
94
120
  })
@@ -79,7 +79,11 @@ describe('generateContextData', () => {
79
79
  mockErrorLog.mockRestore()
80
80
 
81
81
  context.functions.execute('test')
82
- expect(GenerateContextSyncMock).toHaveBeenCalled()
82
+ expect(GenerateContextSyncMock).toHaveBeenCalledWith(expect.objectContaining({
83
+ currentFunction,
84
+ functionName: 'test',
85
+ runAsSystem: currentFunction.run_as_system
86
+ }))
83
87
 
84
88
  const token = jwt.sign(
85
89
  { sub: 'user', role: 'admin' },
@@ -205,16 +205,17 @@ export const generateContextData = ({
205
205
  https: getService('api'),
206
206
  functions: {
207
207
  execute: (name: keyof typeof functionsList, ...args: Arguments) => {
208
- const currentFunction = functionsList[name] as Function
208
+ const targetFunction = functionsList[name] as Function
209
209
  return GenerateContextSync({
210
210
  args,
211
211
  app,
212
212
  rules,
213
213
  user,
214
- currentFunction,
214
+ currentFunction: targetFunction,
215
215
  functionName: String(name),
216
216
  functionsList,
217
217
  services,
218
+ runAsSystem: currentFunction.run_as_system,
218
219
  deserializeArgs: false
219
220
  })
220
221
  }
@@ -189,8 +189,8 @@ export async function GenerateContext({
189
189
  if (!currentFunction) return
190
190
 
191
191
  const functionsQueue = StateManager.select("functionsQueue")
192
-
193
- const functionToRun = { run_as_system: runAsSystem, ...currentFunction }
192
+ const effectiveRunAsSystem = Boolean(runAsSystem || currentFunction.run_as_system)
193
+ const functionToRun = { ...currentFunction, run_as_system: effectiveRunAsSystem }
194
194
 
195
195
  const run = async () => {
196
196
 
@@ -309,7 +309,8 @@ export function GenerateContextSync({
309
309
  }: GenerateContextParams): unknown {
310
310
  if (!currentFunction) return
311
311
 
312
- const functionToRun = { run_as_system: runAsSystem, ...currentFunction }
312
+ const effectiveRunAsSystem = Boolean(runAsSystem || currentFunction.run_as_system)
313
+ const functionToRun = { ...currentFunction, run_as_system: effectiveRunAsSystem }
313
314
  const contextData = generateContextData({
314
315
  user,
315
316
  services,