import { localized, msg, str } from "@lit/localize"; import type { SlInput } from "@shoelace-style/shoelace"; import type { ZxcvbnResult } from "@zxcvbn-ts/core"; import { customElement, property, query, state } from "lit/decorators.js"; import { when } from "lit/directives/when.js"; import debounce from "lodash/fp/debounce"; import type { UserOrgInviteInfo, UserRegisterResponseData } from "@/types/user"; import type { UnderlyingFunction } from "@/types/utils"; import AuthService from "@/utils/AuthService"; import LiteElement, { html } from "@/utils/LiteElement"; import PasswordService from "@/utils/PasswordService"; export type SignUpSuccessDetail = { orgName?: string; orgSlug?: string; }; const { PASSWORD_MINLENGTH, PASSWORD_MAXLENGTH, PASSWORD_MIN_SCORE } = PasswordService; /** * @event submit * @event success * @event failure * @event authenticated * @event unauthenticated */ @customElement("btrix-sign-up-form") @localized() export class SignUpForm extends LiteElement { /** Optional read-only email, e.g. for invitations */ @property({ type: String }) email?: string; @property({ type: String }) inviteToken?: string; @property({ type: Object }) inviteInfo?: UserOrgInviteInfo; @property({ type: String }) submitLabel?: string; @state() private serverError?: string; @state() private isSubmitting = false; @state() private pwStrengthResults: null | ZxcvbnResult = null; @state() private showLoginLink = false; @query('sl-input[name="password"]') private readonly password?: SlInput | null; protected firstUpdated() { void PasswordService.setOptions(); } render() { let serverError; if (this.serverError) { serverError = html`
${this.serverError} ${this.showLoginLink ? html`

Go to the Log-In Page and try again.

` : ``}
`; } return html`
${serverError}

${msg( "Your full name, nickname, or another name that org collaborators can see.", )}

${this.email ? html`
${msg("Email")}
${this.email}
` : html` `}
} >

${msg( str`Choose a strong password between ${PASSWORD_MINLENGTH}–${PASSWORD_MAXLENGTH} characters.`, )}

${when(this.pwStrengthResults, this.renderPasswordStrength)}
${this.submitLabel || msg("Create Account")}
`; } private readonly renderPasswordStrength = () => html`
`; private readonly onPasswordInput = debounce(150)(async () => { const value = this.password?.value; if (!value || value.length < 4) { this.pwStrengthResults = null; return; } const userInputs: string[] = []; if (this.email) { userInputs.push(this.email); } this.pwStrengthResults = await PasswordService.checkStrength( value, userInputs, ); }); private async onSubmit(event: SubmitEvent) { event.preventDefault(); event.stopPropagation(); this.dispatchEvent(new CustomEvent("submit")); this.serverError = undefined; this.showLoginLink = false; this.isSubmitting = true; const formData = new FormData(event.target as HTMLFormElement); const email = formData.get("email") as string; const password = formData.get("password") as string; const name = formData.get("name") as string; const registerParams: { email: string; password: string; name: string; newOrg: boolean; inviteToken?: string; } = { email, password, name: name || email, newOrg: true, }; if (this.inviteToken) { registerParams.inviteToken = this.inviteToken; registerParams.newOrg = false; } const resp = await fetch("/api/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(registerParams), }); let data; let shouldLogIn = false; switch (resp.status) { case 201: { data = (await resp.json()) as UserRegisterResponseData; if (data.id) { shouldLogIn = true; } break; } case 400: case 422: { const { detail } = (await resp.json()) as { detail: string & { code: string }; }; if ( detail === "user_already_exists" || detail === "user_already_is_org_member" ) { shouldLogIn = true; } else if (detail.code && detail.code === "invalid_password") { this.serverError = msg( "Invalid password. Must be between 8 and 64 characters", ); } else { this.serverError = msg("Invalid email or password"); } break; } default: this.serverError = msg("Something unexpected went wrong"); break; } if (this.serverError) { this.dispatchEvent(new CustomEvent("error")); } else { const org = data && this.inviteInfo && data.orgs.find(({ id }) => this.inviteInfo?.oid === id); this.dispatchEvent( new CustomEvent("success", { detail: { orgName: org?.name, orgSlug: org?.slug, }, }), ); if (shouldLogIn) { try { await this.logIn({ email, password }); } catch { this.serverError = msg( "User is already registered, but with a different password.", ); this.showLoginLink = true; //this.dispatchEvent(new CustomEvent("unauthenticated")); } } } this.isSubmitting = false; } private async logIn({ email, password, }: { email: string; password: string; }) { const data = await AuthService.login({ email, password }); this.dispatchEvent(new CustomEvent("authenticated", { detail: data })); } }