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`
`;
}
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 }));
}
}