browsertrix/frontend/src/components/account-settings.ts
Sara Tavares bc69d6f0d4
chore(typos): fix typos (#524)
Co-authored-by: stavares843 <stavares843@users.noreply.github.com>
2023-01-26 16:34:47 -08:00

400 lines
9.6 KiB
TypeScript

import { LitElement } from "lit";
import { state, query, property } from "lit/decorators.js";
import { msg, localized } from "@lit/localize";
import { createMachine, interpret, assign } from "@xstate/fsm";
import type { CurrentUser } from "../types/user";
import LiteElement, { html } from "../utils/LiteElement";
import { needLogin } from "../utils/auth";
import type { AuthState, Auth } from "../utils/AuthService";
import AuthService from "../utils/AuthService";
@localized()
class RequestVerify extends LitElement {
@property({ type: String })
email!: string;
@state()
private isRequesting: boolean = false;
@state()
private requestSuccess: boolean = false;
createRenderRoot() {
return this;
}
render() {
if (this.requestSuccess) {
return html`
<div class="text-sm text-gray-400 inline-flex items-center">
<sl-icon class="mr-1" name="check-lg"></sl-icon> ${msg("Sent", {
desc: "Status message after sending verification email",
})}
</div>
`;
}
return html`
<span
class="text-sm text-blue-400 hover:text-blue-500"
role="button"
?disabled=${this.isRequesting}
@click=${this.requestVerification}
>
${this.isRequesting
? msg("Sending...")
: msg("Resend verification email")}
</span>
`;
}
private async requestVerification() {
this.isRequesting = true;
const resp = await fetch("/api/auth/request-verify-token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: this.email,
}),
});
switch (resp.status) {
case 202:
this.requestSuccess = true;
break;
default:
// TODO generic toast error
break;
}
this.isRequesting = false;
}
}
customElements.define("btrix-request-verify", RequestVerify);
type FormContext = {
successMessage?: string;
serverError?: string;
fieldErrors: { [fieldName: string]: string };
};
type FormSuccessEvent = {
type: "SUCCESS";
detail: {
successMessage?: FormContext["successMessage"];
};
};
type FormErrorEvent = {
type: "ERROR";
detail: {
serverError?: FormContext["serverError"];
fieldErrors?: FormContext["fieldErrors"];
};
};
type FormEvent =
| { type: "EDIT" }
| { type: "CANCEL" }
| { type: "SUBMIT" }
| FormSuccessEvent
| FormErrorEvent;
type FormTypestate =
| {
value: "readOnly";
context: FormContext;
}
| {
value: "editingForm";
context: FormContext;
}
| {
value: "submittingForm";
context: FormContext;
};
const initialContext = {
fieldErrors: {},
};
const machine = createMachine<FormContext, FormEvent, FormTypestate>(
{
id: "changePasswordForm",
initial: "readOnly",
context: initialContext,
states: {
["readOnly"]: {
on: {
EDIT: {
target: "editingForm",
actions: "reset",
},
},
},
["editingForm"]: {
on: { CANCEL: "readOnly", SUBMIT: "submittingForm" },
},
["submittingForm"]: {
on: {
SUCCESS: {
target: "readOnly",
actions: "setSuccessMessage",
},
ERROR: {
target: "editingForm",
actions: "setError",
},
},
},
},
},
{
actions: {
reset: assign(() => initialContext),
setSuccessMessage: assign((context, event) => ({
...context,
...(event as FormSuccessEvent).detail,
})),
setError: assign((context, event) => ({
...context,
...(event as FormErrorEvent).detail,
})),
},
}
);
@needLogin
@localized()
export class AccountSettings extends LiteElement {
private formStateService = interpret(machine);
@property({ type: Object })
authState?: AuthState;
@property({ type: Object })
userInfo?: CurrentUser;
@state()
private formState = machine.initialState;
firstUpdated() {
// Enable state machine
this.formStateService.subscribe((state) => {
this.formState = state;
});
this.formStateService.start();
}
disconnectedCallback() {
this.formStateService.stop();
super.disconnectedCallback();
}
render() {
const showForm =
this.formState.value === "editingForm" ||
this.formState.value === "submittingForm";
let successMessage;
let verificationMessage;
if (this.formState.context.successMessage) {
successMessage = html`
<div>
<btrix-alert variant="success"
>${this.formState.context.successMessage}</btrix-alert
>
</div>
`;
}
if (this.userInfo) {
if (this.userInfo.isVerified) {
verificationMessage = html`
<sl-tag variant="success" size="small"
>${msg("verified", {
desc: "Status text when user email is verified",
})}</sl-tag
>
`;
} else {
verificationMessage = html`
<sl-tag class="mr-2" variant="warning" size="small"
>${msg("unverified", {
desc: "Status text when user email is not yet verified",
})}</sl-tag
>
<btrix-request-verify
email=${this.userInfo.email}
></btrix-request-verify>
`;
}
}
return html`<div class="grid gap-4">
<h1 class="text-xl font-semibold">${msg("Account Settings")}</h1>
${successMessage}
<section class="p-4 md:p-8 border rounded-lg grid gap-6">
<div>
<div class="mb-1 text-gray-500">${msg("Name")}</div>
<div class="inline-flex items-center">
<span class="mr-3">${this.userInfo?.name}</span>
</div>
</div>
<div>
<div class="mb-1 text-gray-500">${msg("Email")}</div>
<div class="inline-flex items-center">
<span class="mr-3">${this.userInfo?.email}</span>
${verificationMessage}
</div>
</div>
${showForm
? this.renderChangePasswordForm()
: html`
<div>
<sl-button
variant="primary"
outline
@click=${() => this.formStateService.send("EDIT")}
>${msg("Change password")}</sl-button
>
</div>
`}
</section>
</div>`;
}
renderChangePasswordForm() {
const passwordFieldError = this.formState.context.fieldErrors.password;
let formError;
if (this.formState.context.serverError) {
formError = html`
<div class="mb-5">
<btrix-alert id="formError" variant="danger"
>${this.formState.context.serverError}</btrix-alert
>
</div>
`;
}
return html` <div class="max-w-sm">
<h3 class="font-semibold mb-3">${msg("Change password")}</h3>
<form @submit=${this.onSubmit} aria-describedby="formError">
<div class="mb-5">
<sl-input
id="password"
class="${passwordFieldError ? "text-danger" : ""}"
name="password"
type="password"
label="${msg("Current password")}"
aria-describedby="passwordError"
autocomplete="current-password"
password-toggle
required
>
</sl-input>
${passwordFieldError
? html`<div id="passwordError" class="text-danger" role="alert">
${passwordFieldError}
</div>`
: ""}
</div>
<div class="mb-5">
<sl-input
id="newPassword"
name="newPassword"
type="password"
label="${msg("New password")}"
autocomplete="new-password"
password-toggle
required
>
</sl-input>
</div>
${formError}
<div>
<sl-button
variant="primary"
?loading=${this.formState.value === "submittingForm"}
type="submit"
>${msg("Update password")}</sl-button
>
<sl-button
variant="text"
@click=${() => this.formStateService.send("CANCEL")}
>${msg("Cancel")}</sl-button
>
</div>
</form>
</div>`;
}
async onSubmit(event: any) {
event.preventDefault();
if (!this.authState) return;
this.formStateService.send("SUBMIT");
const formData = new FormData(event.target);
let nextAuthState: Auth | null = null;
try {
nextAuthState = await AuthService.login({
email: this.authState.username,
password: formData.get("password") as string,
});
this.dispatchEvent(AuthService.createLoggedInEvent(nextAuthState));
} catch (e: any) {
console.debug(e);
}
if (!nextAuthState) {
this.formStateService.send({
type: "ERROR",
detail: {
fieldErrors: {
password: msg("Wrong password"),
},
},
});
return;
}
const params = {
password: formData.get("newPassword"),
};
try {
await this.apiFetch("/users/me", nextAuthState, {
method: "PATCH",
body: JSON.stringify(params),
});
this.formStateService.send({
type: "SUCCESS",
detail: {
successMessage: "Successfully updated password",
},
});
} catch (e) {
console.error(e);
this.formStateService.send({
type: "ERROR",
detail: {
serverError: msg("Something went wrong changing password"),
},
});
}
}
}