@flowerforce/flowerbase 1.7.6-beta.4 → 1.7.6-beta.6
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.
- package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
- package/dist/auth/providers/local-userpass/controller.js +27 -7
- package/dist/auth/utils.d.ts +1 -0
- package/dist/auth/utils.d.ts.map +1 -1
- package/dist/auth/utils.js +1 -0
- package/dist/features/endpoints/utils.d.ts.map +1 -1
- package/dist/features/endpoints/utils.js +3 -0
- package/dist/utils/roles/machines/fieldPermissions.d.ts.map +1 -1
- package/dist/utils/roles/machines/fieldPermissions.js +12 -10
- package/package.json +1 -1
- package/src/auth/providers/local-userpass/__tests__/controller.test.ts +200 -0
- package/src/auth/providers/local-userpass/controller.ts +38 -8
- package/src/auth/utils.ts +1 -0
- package/src/features/endpoints/__tests__/utils.test.ts +65 -0
- package/src/features/endpoints/utils.ts +3 -0
- package/src/utils/__tests__/checkIsValidFieldNameFn.test.ts +28 -4
- package/src/utils/roles/machines/fieldPermissions.ts +10 -8
|
@@ -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;
|
|
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
|
-
|
|
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
|
/**
|
package/dist/auth/utils.d.ts
CHANGED
|
@@ -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 {
|
package/dist/auth/utils.d.ts.map
CHANGED
|
@@ -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"}
|
package/dist/auth/utils.js
CHANGED
|
@@ -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,
|
|
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
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fieldPermissions.d.ts","sourceRoot":"","sources":["../../../../src/utils/roles/machines/fieldPermissions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAElC,OAAO,EAGL,IAAI,EACL,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"fieldPermissions.d.ts","sourceRoot":"","sources":["../../../../src/utils/roles/machines/fieldPermissions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAElC,OAAO,EAGL,IAAI,EACL,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AA0C5C,eAAO,MAAM,0BAA0B,GAAI,OAAO,IAAI,YACN,CAAA;AAEhD,eAAO,MAAM,gCAAgC,GAC3C,SAAS,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC,EACzD,MAAM,MAAM,GAAG,OAAO,EACtB,UAAU;IACR,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB,KACA,OAAO,CAAC,QAAQ,CA6BlB,CAAA"}
|
|
@@ -26,12 +26,9 @@ const getAdditionalFieldPermission = (additionalFields, fieldName) => {
|
|
|
26
26
|
return undefined;
|
|
27
27
|
};
|
|
28
28
|
const canReadField = (context, permission) => __awaiter(void 0, void 0, void 0, function* () {
|
|
29
|
-
if (!permission)
|
|
30
|
-
return
|
|
31
|
-
|
|
32
|
-
if (read)
|
|
33
|
-
return true;
|
|
34
|
-
return yield (0, helpers_1.evaluateExpression)(context.params, permission.write, context.user);
|
|
29
|
+
if (!permission || typeof permission.read === 'undefined')
|
|
30
|
+
return undefined;
|
|
31
|
+
return yield (0, helpers_1.evaluateExpression)(context.params, permission.read, context.user);
|
|
35
32
|
});
|
|
36
33
|
const canWriteField = (context, permission) => __awaiter(void 0, void 0, void 0, function* () {
|
|
37
34
|
if (!permission)
|
|
@@ -53,10 +50,15 @@ const filterDocumentByFieldPermissions = (context, mode, options) => __awaiter(v
|
|
|
53
50
|
const permission = fieldPermission !== null && fieldPermission !== void 0 ? fieldPermission : getAdditionalFieldPermission(additionalFields, key);
|
|
54
51
|
let allowed = (options === null || options === void 0 ? void 0 : options.defaultAllow) === true;
|
|
55
52
|
if (permission) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
if (mode === 'read') {
|
|
54
|
+
const readAllowed = yield canReadField(context, permission);
|
|
55
|
+
if (typeof readAllowed !== 'undefined') {
|
|
56
|
+
allowed = readAllowed;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
allowed = yield canWriteField(context, permission);
|
|
61
|
+
}
|
|
60
62
|
}
|
|
61
63
|
if (allowed) {
|
|
62
64
|
document[key] = value;
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -32,8 +32,7 @@ describe('checkIsValidFieldNameFn', () => {
|
|
|
32
32
|
|
|
33
33
|
const result = await checkIsValidFieldNameFn(context)
|
|
34
34
|
expect(result).toEqual({
|
|
35
|
-
name: 'Alice'
|
|
36
|
-
email: 'alice@example.com'
|
|
35
|
+
name: 'Alice'
|
|
37
36
|
})
|
|
38
37
|
})
|
|
39
38
|
|
|
@@ -56,8 +55,33 @@ describe('checkIsValidFieldNameFn', () => {
|
|
|
56
55
|
|
|
57
56
|
const result = await checkIsValidFieldNameFn(context)
|
|
58
57
|
expect(result).toEqual({
|
|
59
|
-
phone: '123456789'
|
|
60
|
-
|
|
58
|
+
phone: '123456789'
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('keeps fields readable when top-level read is true and the field only defines write rules', async () => {
|
|
63
|
+
const mockedRole = {
|
|
64
|
+
name: 'test',
|
|
65
|
+
apply_when: { '%%true': true },
|
|
66
|
+
read: true,
|
|
67
|
+
fields: {
|
|
68
|
+
avatar: { write: false },
|
|
69
|
+
name: { write: true }
|
|
70
|
+
}
|
|
71
|
+
} as Role
|
|
72
|
+
const context = {
|
|
73
|
+
user: mockUser,
|
|
74
|
+
role: mockedRole,
|
|
75
|
+
params: {
|
|
76
|
+
type: 'read',
|
|
77
|
+
cursor: { avatar: 'avatar.png', name: 'Alice' }
|
|
78
|
+
}
|
|
79
|
+
} as MachineContext
|
|
80
|
+
|
|
81
|
+
const result = await checkIsValidFieldNameFn(context)
|
|
82
|
+
expect(result).toEqual({
|
|
83
|
+
avatar: 'avatar.png',
|
|
84
|
+
name: 'Alice'
|
|
61
85
|
})
|
|
62
86
|
})
|
|
63
87
|
|
|
@@ -35,10 +35,8 @@ const canReadField = async (
|
|
|
35
35
|
context: Pick<MachineContext, 'params' | 'user'>,
|
|
36
36
|
permission?: FieldPermissionExpression
|
|
37
37
|
) => {
|
|
38
|
-
if (!permission) return
|
|
39
|
-
|
|
40
|
-
if (read) return true
|
|
41
|
-
return await evaluateExpression(context.params, permission.write, context.user)
|
|
38
|
+
if (!permission || typeof permission.read === 'undefined') return undefined
|
|
39
|
+
return await evaluateExpression(context.params, permission.read, context.user)
|
|
42
40
|
}
|
|
43
41
|
|
|
44
42
|
const canWriteField = async (
|
|
@@ -71,10 +69,14 @@ export const filterDocumentByFieldPermissions = async (
|
|
|
71
69
|
const permission = fieldPermission ?? getAdditionalFieldPermission(additionalFields, key)
|
|
72
70
|
let allowed = options?.defaultAllow === true
|
|
73
71
|
if (permission) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
if (mode === 'read') {
|
|
73
|
+
const readAllowed = await canReadField(context, permission)
|
|
74
|
+
if (typeof readAllowed !== 'undefined') {
|
|
75
|
+
allowed = readAllowed
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
allowed = await canWriteField(context, permission)
|
|
79
|
+
}
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
if (allowed) {
|