@flowerforce/flowerbase 1.7.6-beta.4 → 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.
@@ -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,iBA0XjE"}
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.
@@ -273,11 +295,9 @@ function localUserPassController(app) {
273
295
  res.status(429);
274
296
  return { message: 'Too many requests' };
275
297
  }
276
- 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);
277
299
  res.status(202);
278
- return {
279
- status: 'ok'
280
- };
300
+ return result;
281
301
  });
282
302
  });
283
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowerforce/flowerbase",
3
- "version": "1.7.6-beta.4",
3
+ "version": "1.7.6-beta.5",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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
  /**
@@ -352,15 +384,13 @@ export async function localUserPassController(app: FastifyInstance) {
352
384
  res.status(429)
353
385
  return { message: 'Too many requests' }
354
386
  }
355
- await handleResetPasswordRequest(
387
+ const result = await handleResetPasswordRequest(
356
388
  req.body.email,
357
389
  req.body.password,
358
390
  req.body.arguments
359
391
  )
360
392
  res.status(202)
361
- return {
362
- status: 'ok'
363
- }
393
+ return result
364
394
  }
365
395
  )
366
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
  }