gristlabs_grist-core/app/client/lib/formUtils.ts
George Gevoian 0f4f0d3dad (core) Migrate to SRP and add change password dialog
Summary:
Moves some auth-related UI components, like MFAConfig, out
of core, and adds a new ChangePasswordDialog component for
allowing direct password changes, replacing the old reset password
link to hosted Cognito.

Updates all MFA endpoints to use SRP for authentication.

Also refactors MFAConfig into smaller files, and polishes up some parts
of the UI to be more consistent with the login pages.

Test Plan: New server and deployment tests. Updated existing tests.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D3311
2022-03-16 21:35:06 -07:00

73 lines
2.4 KiB
TypeScript

import {reportError} from 'app/client/models/errors';
import {ApiError} from 'app/common/ApiError';
import {BaseAPI} from 'app/common/BaseAPI';
import {dom, Observable} from 'grainjs';
/**
* Handles submission of an HTML form element.
*
* When the form is submitted, `onSubmit` will be called, followed by
* either `onSuccess` or `onError`, depending on whether `onSubmit` threw any
* unhandled errors. The `pending` observable is set to true until `onSubmit`
* resolves.
*/
export function handleSubmit<T>(
pending: Observable<boolean>,
onSubmit: (fields: { [key: string]: string }, form: HTMLFormElement) => Promise<T> = submitForm,
onSuccess: (v: T) => void = () => { /* noop */ },
onError: (e: unknown) => void = (e) => reportError(e as string | Error)
): (elem: HTMLFormElement) => void {
return dom.on('submit', async (e, form) => {
e.preventDefault();
try {
if (pending.get()) { return; }
pending.set(true);
const result = await onSubmit(formDataToObj(form), form).finally(() => pending.set(false));
onSuccess(result);
} catch (err) {
onError(err);
}
});
}
/**
* Convert a form to a JSON-stringifiable object, ignoring any File fields.
*/
export function formDataToObj(formElem: HTMLFormElement): { [key: string]: string } {
// Use FormData to collect values (rather than e.g. finding <input> elements) to ensure we get
// values from all form items correctly (e.g. checkboxes and textareas).
const formData = new FormData(formElem);
const data: { [key: string]: string } = {};
for (const [name, value] of formData.entries()) {
if (typeof value === 'string') {
data[name] = value;
}
}
return data;
}
/**
* Submit a form using BaseAPI. Send inputs as JSON, and interpret any reply as JSON.
*/
export async function submitForm(fields: { [key: string]: string }, form: HTMLFormElement): Promise<any> {
return BaseAPI.requestJson(form.action, {method: 'POST', body: JSON.stringify(fields)});
}
/**
* Sets the error details on `errObs` if `err` is a 4XX error (except 401). Otherwise, reports the
* error via the Notifier instance.
*/
export function handleFormError(err: unknown, errObs: Observable<string|null>) {
if (
err instanceof ApiError &&
err.status !== 401 &&
err.status >= 400 &&
err.status < 500
) {
errObs.set(err.details?.userError ?? err.message);
} else {
reportError(err as Error|string);
}
}