Skip to content

Commit

Permalink
[WIP] feat: improve form validation (#919)
Browse files Browse the repository at this point in the history
* feat: improve form validation in emailpassword flow

* misc: increase package version and run build-pretty

* docs: update changelog

* fix: use for of instead of for each to support promises

* test: add test case to cover the functionality of optional fields being truly optional
  • Loading branch information
akashrajum7 authored Sep 2, 2024
1 parent f51a72c commit 38b49b7
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 47 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

- Optional form fields are now truly optional, can be omitted from the payload.

## [20.0.4] - 2024-08-30

- Improves thirdParty debug logging to help with debugging issues with JSON parsing.
Expand Down
55 changes: 34 additions & 21 deletions lib/build/recipe/emailpassword/api/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,37 +50,50 @@ function newBadRequestError(message) {
message,
});
}
// We check that the number of fields in input and config form field is the same.
// We check that each item in the config form field is also present in the input form field
// We check to make sure we are validating each required form field
// and also validate optional form fields only when present
async function validateFormOrThrowError(inputs, configFormFields, tenantId, userContext) {
let validationErrors = [];
if (configFormFields.length !== inputs.length) {
let requiredFormFields = new Set();
configFormFields.forEach((formField) => {
if (!formField.optional) {
requiredFormFields.add(formField.id);
}
});
if (inputs.length < requiredFormFields.size || inputs.length > configFormFields.length) {
throw newBadRequestError("Are you sending too many / too few formFields?");
}
// Loop through all form fields.
for (let i = 0; i < configFormFields.length; i++) {
const field = configFormFields[i];
// Find corresponding input value.
const input = inputs.find((i) => i.id === field.id);
// Absent or not optional empty field
if (input === undefined || (input.value === "" && !field.optional)) {
validationErrors.push({
error: "Field is not optional",
id: field.id,
});
for (const formField of configFormFields) {
const input = inputs.find((input) => input.id === formField.id);
if (formField.optional) {
// Validate optional inputs only when they are present
if (input && input.value.length > 0) {
const error = await formField.validate(input.value, tenantId, userContext);
if (error) {
validationErrors.push({
error,
id: formField.id,
});
}
}
} else {
// Otherwise, use validate function.
const error = await field.validate(input.value, tenantId, userContext);
// If error, add it.
if (error !== undefined) {
if (input && input.value.length > 0) {
const error = await formField.validate(input.value, tenantId, userContext);
if (error) {
validationErrors.push({
error,
id: formField.id,
});
}
} else {
validationErrors.push({
error,
id: field.id,
error: "Field is not optional",
id: formField.id,
});
}
}
}
if (validationErrors.length !== 0) {
if (validationErrors.length > 0) {
throw new error_1.default({
type: error_1.default.FIELD_ERROR,
payload: validationErrors,
Expand Down
2 changes: 1 addition & 1 deletion lib/build/version.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/build/version.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 35 additions & 22 deletions lib/ts/recipe/emailpassword/api/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ function newBadRequestError(message: string) {
});
}

// We check that the number of fields in input and config form field is the same.
// We check that each item in the config form field is also present in the input form field
// We check to make sure we are validating each required form field
// and also validate optional form fields only when present
async function validateFormOrThrowError(
inputs: {
id: string;
Expand All @@ -94,38 +94,51 @@ async function validateFormOrThrowError(
userContext: UserContext
) {
let validationErrors: { id: string; error: string }[] = [];
let requiredFormFields = new Set();

if (configFormFields.length !== inputs.length) {
configFormFields.forEach((formField) => {
if (!formField.optional) {
requiredFormFields.add(formField.id);
}
});

if (inputs.length < requiredFormFields.size || inputs.length > configFormFields.length) {
throw newBadRequestError("Are you sending too many / too few formFields?");
}

// Loop through all form fields.
for (let i = 0; i < configFormFields.length; i++) {
const field = configFormFields[i];
for (const formField of configFormFields) {
const input = inputs.find((input) => input.id === formField.id);

// Find corresponding input value.
const input = inputs.find((i) => i.id === field.id);

// Absent or not optional empty field
if (input === undefined || (input.value === "" && !field.optional)) {
validationErrors.push({
error: "Field is not optional",
id: field.id,
});
if (formField.optional) {
// Validate optional inputs only when they are present
if (input && input.value.length > 0) {
const error = await formField.validate(input.value, tenantId, userContext);
if (error) {
validationErrors.push({
error,
id: formField.id,
});
}
}
} else {
// Otherwise, use validate function.
const error = await field.validate(input.value, tenantId, userContext);
// If error, add it.
if (error !== undefined) {
if (input && input.value.length > 0) {
const error = await formField.validate(input.value, tenantId, userContext);
if (error) {
validationErrors.push({
error,
id: formField.id,
});
}
} else {
validationErrors.push({
error,
id: field.id,
error: "Field is not optional",
id: formField.id,
});
}
}
}

if (validationErrors.length !== 0) {
if (validationErrors.length > 0) {
throw new STError({
type: STError.FIELD_ERROR,
payload: validationErrors,
Expand Down
2 changes: 1 addition & 1 deletion lib/ts/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
export const version = "20.0.4";
export const version = "20.0.5";

export const cdiSupported = ["5.1"];

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "supertokens-node",
"version": "20.0.4",
"version": "20.0.5",
"description": "NodeJS driver for SuperTokens core",
"main": "index.js",
"scripts": {
Expand Down
66 changes: 66 additions & 0 deletions test/emailpassword/signupFeature.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,72 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js]
assert(response.formFields[0].id === "testField2");
});

// Custom optional field missing in the payload should not throw an error
it("Custom optional field missing in the payload should not throw an error", async function () {
const connectionURI = await startST();
STExpress.init({
supertokens: {
connectionURI,
},
appInfo: {
apiDomain: "api.supertokens.io",
appName: "SuperTokens",
websiteDomain: "supertokens.io",
},
recipeList: [
EmailPassword.init({
signUpFeature: {
formFields: [
{
id: "testField",
optional: true,
},
{
id: "testField1",
optional: true,
},
],
},
}),
Session.init({ getTokenTransferMethod: () => "cookie" }),
],
});
const app = express();

app.use(middleware());

app.use(errorHandler());

let response = await new Promise((resolve) =>
request(app)
.post("/auth/signup")
.send({
formFields: [
{
id: "password",
value: "validpass123",
},
{
id: "email",
value: "random@gmail.com",
},
],
})
.expect(200)
.end((err, res) => {
if (err) {
resolve(undefined);
} else {
resolve(JSON.parse(res.text));
}
})
);

assert(response.status === "OK");
assert(response.user.id !== undefined);
assert(response.user.emails[0] === "random@gmail.com");
});

// Test custom field validation error (one and two custom fields)
it("test custom field validation error", async function () {
const connectionURI = await startST();
Expand Down

0 comments on commit 38b49b7

Please sign in to comment.