Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] feat: improve form validation #919

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: "[email protected]",
},
],
})
.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] === "[email protected]");
});

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