parent
50e93724fc
commit
3fa85c83f2
@ -1,11 +1,75 @@
|
|||||||
import { state, query } from "lit/decorators.js";
|
import { LitElement } from "lit";
|
||||||
|
import { state, query, property } from "lit/decorators.js";
|
||||||
import { msg, localized } from "@lit/localize";
|
import { msg, localized } from "@lit/localize";
|
||||||
import { createMachine, interpret, assign } from "@xstate/fsm";
|
import { createMachine, interpret, assign } from "@xstate/fsm";
|
||||||
|
|
||||||
import type { AuthState } from "../types/auth";
|
import type { AuthState, CurrentUser } from "../types/auth";
|
||||||
import LiteElement, { html } from "../utils/LiteElement";
|
import LiteElement, { html } from "../utils/LiteElement";
|
||||||
import { needLogin } from "../utils/auth";
|
import { needLogin } from "../utils/auth";
|
||||||
|
|
||||||
|
@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}
|
||||||
|
>
|
||||||
|
${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("bt-request-verify", RequestVerify);
|
||||||
|
|
||||||
type FormContext = {
|
type FormContext = {
|
||||||
successMessage?: string;
|
successMessage?: string;
|
||||||
serverError?: string;
|
serverError?: string;
|
||||||
@ -98,14 +162,19 @@ const machine = createMachine<FormContext, FormEvent, FormTypestate>(
|
|||||||
@needLogin
|
@needLogin
|
||||||
@localized()
|
@localized()
|
||||||
export class AccountSettings extends LiteElement {
|
export class AccountSettings extends LiteElement {
|
||||||
|
private formStateService = interpret(machine);
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
authState?: AuthState;
|
authState?: AuthState;
|
||||||
|
|
||||||
private formStateService = interpret(machine);
|
@property({ type: Object })
|
||||||
|
userInfo?: CurrentUser;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private formState = machine.initialState;
|
private formState = machine.initialState;
|
||||||
|
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
|
// Enable state machine
|
||||||
this.formStateService.subscribe((state) => {
|
this.formStateService.subscribe((state) => {
|
||||||
this.formState = state;
|
this.formState = state;
|
||||||
});
|
});
|
||||||
@ -122,6 +191,7 @@ export class AccountSettings extends LiteElement {
|
|||||||
this.formState.value === "editingForm" ||
|
this.formState.value === "editingForm" ||
|
||||||
this.formState.value === "submittingForm";
|
this.formState.value === "submittingForm";
|
||||||
let successMessage;
|
let successMessage;
|
||||||
|
let verificationMessage;
|
||||||
|
|
||||||
if (this.formState.context.successMessage) {
|
if (this.formState.context.successMessage) {
|
||||||
successMessage = html`
|
successMessage = html`
|
||||||
@ -133,6 +203,28 @@ export class AccountSettings extends LiteElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.userInfo) {
|
||||||
|
if (this.userInfo.isVerified) {
|
||||||
|
verificationMessage = html`
|
||||||
|
<sl-tag type="success" size="small"
|
||||||
|
>${msg("verified", {
|
||||||
|
desc: "Status text when user email is verified",
|
||||||
|
})}</sl-tag
|
||||||
|
>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
verificationMessage = html`
|
||||||
|
<sl-tag class="mr-2" type="warning" size="small"
|
||||||
|
>${msg("unverified", {
|
||||||
|
desc: "Status text when user email is not yet verified",
|
||||||
|
})}</sl-tag
|
||||||
|
>
|
||||||
|
|
||||||
|
<bt-request-verify email=${this.userInfo.email}></bt-request-verify>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return html`<div class="grid gap-4">
|
return html`<div class="grid gap-4">
|
||||||
<h1 class="text-xl font-bold">${msg("Account settings")}</h1>
|
<h1 class="text-xl font-bold">${msg("Account settings")}</h1>
|
||||||
|
|
||||||
@ -140,8 +232,11 @@ export class AccountSettings extends LiteElement {
|
|||||||
|
|
||||||
<section class="p-4 md:p-8 border rounded-lg grid gap-6">
|
<section class="p-4 md:p-8 border rounded-lg grid gap-6">
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-1 text-gray-500">Email</div>
|
<div class="mb-1 text-gray-500">${msg("Email")}</div>
|
||||||
<div>${this.authState!.username}</div>
|
<div class="inline-flex items-center">
|
||||||
|
<span class="mr-3">${this.userInfo?.email}</span>
|
||||||
|
${verificationMessage}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${showForm
|
${showForm
|
||||||
|
|||||||
@ -42,7 +42,6 @@ export class Alert extends LitElement {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
console.log("id:", this.id);
|
|
||||||
return html`
|
return html`
|
||||||
<div class="${this.type}" role="alert">
|
<div class="${this.type}" role="alert">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
|||||||
@ -1,27 +1,56 @@
|
|||||||
import { spy, stub } from "sinon";
|
import { spy, stub, mock, restore } from "sinon";
|
||||||
import { fixture, expect } from "@open-wc/testing";
|
import { fixture, expect } from "@open-wc/testing";
|
||||||
// import { expect } from "@esm-bundle/chai";
|
// import { expect } from "@esm-bundle/chai";
|
||||||
|
|
||||||
import { App } from "./index";
|
import { App } from "./index";
|
||||||
|
|
||||||
describe("browsertrix-app", () => {
|
describe("browsertrix-app", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
stub(App.prototype, "getUserInfo").callsFake(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
email: "test-user@example.com",
|
||||||
|
is_verified: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
restore();
|
||||||
|
});
|
||||||
|
|
||||||
it("is defined", async () => {
|
it("is defined", async () => {
|
||||||
const el = await fixture("<browsertrix-app></browsertrix-app>");
|
const el = await fixture("<browsertrix-app></browsertrix-app>");
|
||||||
expect(el).instanceOf(App);
|
expect(el).instanceOf(App);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("gets auth state from local storage", async () => {
|
it("sets auth state from local storage", async () => {
|
||||||
stub(window.localStorage, "getItem").callsFake((key) => {
|
stub(window.localStorage, "getItem").callsFake((key) => {
|
||||||
if (key === "authState")
|
if (key === "authState")
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
username: "test@example.com",
|
username: "test-auth@example.com",
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
const el = (await fixture("<browsertrix-app></browsertrix-app>")) as App;
|
const el = (await fixture("<browsertrix-app></browsertrix-app>")) as App;
|
||||||
|
|
||||||
expect(el.authState).to.eql({
|
expect(el.authState).to.eql({
|
||||||
username: "test@example.com",
|
username: "test-auth@example.com",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets user info", async () => {
|
||||||
|
stub(window.localStorage, "getItem").callsFake((key) => {
|
||||||
|
if (key === "authState")
|
||||||
|
return JSON.stringify({
|
||||||
|
username: "test-auth@example.com",
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
const el = (await fixture("<browsertrix-app></browsertrix-app>")) as App;
|
||||||
|
|
||||||
|
expect(el.userInfo).to.eql({
|
||||||
|
email: "test-user@example.com",
|
||||||
|
isVerified: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import "./shoelace";
|
|||||||
import { LocalePicker } from "./components/locale-picker";
|
import { LocalePicker } from "./components/locale-picker";
|
||||||
import { Alert } from "./components/alert";
|
import { Alert } from "./components/alert";
|
||||||
import { AccountSettings } from "./components/account-settings";
|
import { AccountSettings } from "./components/account-settings";
|
||||||
|
import { SignUp } from "./pages/sign-up";
|
||||||
|
import { Verify } from "./pages/verify";
|
||||||
import { LogInPage } from "./pages/log-in";
|
import { LogInPage } from "./pages/log-in";
|
||||||
import { ResetPassword } from "./pages/reset-password";
|
import { ResetPassword } from "./pages/reset-password";
|
||||||
import { MyAccountPage } from "./pages/my-account";
|
import { MyAccountPage } from "./pages/my-account";
|
||||||
@ -14,11 +16,13 @@ import { ArchiveConfigsPage } from "./pages/archive-info-tab";
|
|||||||
import LiteElement, { html } from "./utils/LiteElement";
|
import LiteElement, { html } from "./utils/LiteElement";
|
||||||
import APIRouter from "./utils/APIRouter";
|
import APIRouter from "./utils/APIRouter";
|
||||||
import type { ViewState, NavigateEvent } from "./utils/APIRouter";
|
import type { ViewState, NavigateEvent } from "./utils/APIRouter";
|
||||||
import type { AuthState } from "./types/auth";
|
import type { AuthState, CurrentUser } from "./types/auth";
|
||||||
import theme from "./theme";
|
import theme from "./theme";
|
||||||
|
|
||||||
const ROUTES = {
|
const ROUTES = {
|
||||||
home: "/",
|
home: "/",
|
||||||
|
signUp: "/sign-up",
|
||||||
|
verify: "/verify?token",
|
||||||
login: "/log-in",
|
login: "/log-in",
|
||||||
forgotPassword: "/log-in/forgot-password",
|
forgotPassword: "/log-in/forgot-password",
|
||||||
resetPassword: "/reset-password?token",
|
resetPassword: "/reset-password?token",
|
||||||
@ -35,6 +39,9 @@ export class App extends LiteElement {
|
|||||||
@state()
|
@state()
|
||||||
authState: AuthState | null = null;
|
authState: AuthState | null = null;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
userInfo?: CurrentUser;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
viewState!: ViewState & {
|
viewState!: ViewState & {
|
||||||
aid?: string;
|
aid?: string;
|
||||||
@ -80,6 +87,32 @@ export class App extends LiteElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updated(changedProperties: any) {
|
||||||
|
if (changedProperties.has("authState") && this.authState) {
|
||||||
|
const prevAuthState = changedProperties.get("authState");
|
||||||
|
|
||||||
|
if (this.authState.username !== prevAuthState?.username) {
|
||||||
|
this.updateUserInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateUserInfo() {
|
||||||
|
try {
|
||||||
|
const data = await this.getUserInfo();
|
||||||
|
|
||||||
|
this.userInfo = {
|
||||||
|
email: data.email,
|
||||||
|
isVerified: data.is_verified,
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err?.message === "Unauthorized") {
|
||||||
|
this.clearAuthState();
|
||||||
|
this.navigate(ROUTES.login);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
navigate(newViewPath: string) {
|
navigate(newViewPath: string) {
|
||||||
if (newViewPath.startsWith("http")) {
|
if (newViewPath.startsWith("http")) {
|
||||||
const url = new URL(newViewPath);
|
const url = new URL(newViewPath);
|
||||||
@ -127,7 +160,7 @@ export class App extends LiteElement {
|
|||||||
><h1 class="text-base px-2">${msg("Browsertrix Cloud")}</h1></a
|
><h1 class="text-base px-2">${msg("Browsertrix Cloud")}</h1></a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="grid grid-flow-col gap-5 items-center">
|
||||||
${this.authState
|
${this.authState
|
||||||
? html` <sl-dropdown>
|
? html` <sl-dropdown>
|
||||||
<div class="p-2" role="button" slot="trigger">
|
<div class="p-2" role="button" slot="trigger">
|
||||||
@ -147,7 +180,12 @@ export class App extends LiteElement {
|
|||||||
>
|
>
|
||||||
</sl-menu>
|
</sl-menu>
|
||||||
</sl-dropdown>`
|
</sl-dropdown>`
|
||||||
: html` <a href="/log-in"> ${msg("Log In")} </a> `}
|
: html`
|
||||||
|
<a href="/log-in"> ${msg("Log In")} </a>
|
||||||
|
<sl-button outline @click="${() => this.navigate("/sign-up")}">
|
||||||
|
<span class="text-white">${msg("Sign up")}</span>
|
||||||
|
</sl-button>
|
||||||
|
`}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
`;
|
`;
|
||||||
@ -179,6 +217,21 @@ export class App extends LiteElement {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
switch (this.viewState.route) {
|
switch (this.viewState.route) {
|
||||||
|
case "signUp":
|
||||||
|
return html`<btrix-sign-up
|
||||||
|
class="w-full md:bg-gray-100 flex items-center justify-center"
|
||||||
|
@navigate="${this.onNavigateTo}"
|
||||||
|
@logged-in="${this.onLoggedIn}"
|
||||||
|
@log-out="${this.onLogOut}"
|
||||||
|
.authState="${this.authState}"
|
||||||
|
></btrix-sign-up>`;
|
||||||
|
|
||||||
|
case "verify":
|
||||||
|
return html`<btrix-verify
|
||||||
|
class="w-full flex items-center justify-center"
|
||||||
|
token="${this.viewState.params.token}"
|
||||||
|
></btrix-verify>`;
|
||||||
|
|
||||||
case "login":
|
case "login":
|
||||||
case "forgotPassword":
|
case "forgotPassword":
|
||||||
return html`<log-in
|
return html`<log-in
|
||||||
@ -223,6 +276,7 @@ export class App extends LiteElement {
|
|||||||
@navigate="${this.onNavigateTo}"
|
@navigate="${this.onNavigateTo}"
|
||||||
@need-login="${this.onNeedLogin}"
|
@need-login="${this.onNeedLogin}"
|
||||||
.authState="${this.authState}"
|
.authState="${this.authState}"
|
||||||
|
.userInfo="${this.userInfo}"
|
||||||
></btrix-account-settings>`);
|
></btrix-account-settings>`);
|
||||||
|
|
||||||
case "archive-info":
|
case "archive-info":
|
||||||
@ -241,9 +295,15 @@ export class App extends LiteElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onLogOut() {
|
onLogOut(event: CustomEvent<{ redirect?: boolean }>) {
|
||||||
|
const { detail } = event;
|
||||||
|
const redirect = detail.redirect !== false;
|
||||||
|
|
||||||
this.clearAuthState();
|
this.clearAuthState();
|
||||||
this.navigate("/");
|
|
||||||
|
if (redirect) {
|
||||||
|
this.navigate("/");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoggedIn(
|
onLoggedIn(
|
||||||
@ -278,11 +338,17 @@ export class App extends LiteElement {
|
|||||||
this.authState = null;
|
this.authState = null;
|
||||||
window.localStorage.setItem("authState", "");
|
window.localStorage.setItem("authState", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserInfo() {
|
||||||
|
return this.apiFetch("/users/me", this.authState!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("bt-alert", Alert);
|
customElements.define("bt-alert", Alert);
|
||||||
customElements.define("bt-locale-picker", LocalePicker);
|
customElements.define("bt-locale-picker", LocalePicker);
|
||||||
customElements.define("browsertrix-app", App);
|
customElements.define("browsertrix-app", App);
|
||||||
|
customElements.define("btrix-sign-up", SignUp);
|
||||||
|
customElements.define("btrix-verify", Verify);
|
||||||
customElements.define("log-in", LogInPage);
|
customElements.define("log-in", LogInPage);
|
||||||
customElements.define("my-account", MyAccountPage);
|
customElements.define("my-account", MyAccountPage);
|
||||||
customElements.define("btrix-archive", ArchivePage);
|
customElements.define("btrix-archive", ArchivePage);
|
||||||
|
|||||||
192
frontend/src/pages/sign-up.ts
Normal file
192
frontend/src/pages/sign-up.ts
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import { state, property, query } from "lit/decorators.js";
|
||||||
|
import { msg, localized } from "@lit/localize";
|
||||||
|
|
||||||
|
import type { AuthState } from "../types/auth";
|
||||||
|
import LiteElement, { html } from "../utils/LiteElement";
|
||||||
|
|
||||||
|
@localized()
|
||||||
|
export class SignUp extends LiteElement {
|
||||||
|
@property({ type: Object })
|
||||||
|
authState?: AuthState;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
isSignUpComplete?: boolean;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
serverError?: string;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
isSubmitting: boolean = false;
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let serverError;
|
||||||
|
|
||||||
|
if (this.serverError) {
|
||||||
|
serverError = html`
|
||||||
|
<div class="mb-5">
|
||||||
|
<bt-alert id="formError" type="danger">${this.serverError}</bt-alert>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<article class="w-full max-w-sm grid gap-5">
|
||||||
|
<main class="md:bg-white md:shadow-xl md:rounded-lg md:px-12 md:py-12">
|
||||||
|
${this.isSignUpComplete
|
||||||
|
? html`
|
||||||
|
<div
|
||||||
|
class="text-2xl font-semibold mb-5 text-primary"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
${msg("Successfully signed up")}
|
||||||
|
</div>
|
||||||
|
<p class="text-lg">
|
||||||
|
${msg(
|
||||||
|
"Click the link in the verification email we sent you to log in."
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<h1 class="text-3xl font-semibold mb-5">${msg("Sign up")}</h1>
|
||||||
|
|
||||||
|
<sl-form
|
||||||
|
@sl-submit="${this.onSubmit}"
|
||||||
|
aria-describedby="formError"
|
||||||
|
>
|
||||||
|
<div class="mb-5">
|
||||||
|
<sl-input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
label=${msg("Email")}
|
||||||
|
placeholder=${msg("you@email.com")}
|
||||||
|
autocomplete="username"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</sl-input>
|
||||||
|
</div>
|
||||||
|
<div class="mb-5">
|
||||||
|
<sl-input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
label=${msg("Password")}
|
||||||
|
autocomplete="new-password"
|
||||||
|
toggle-password
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</sl-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${serverError}
|
||||||
|
|
||||||
|
<sl-button
|
||||||
|
class="w-full"
|
||||||
|
type="primary"
|
||||||
|
?loading=${this.isSubmitting}
|
||||||
|
submit
|
||||||
|
>${msg("Sign up")}</sl-button
|
||||||
|
>
|
||||||
|
</sl-form>
|
||||||
|
`}
|
||||||
|
</main>
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onSubmit(event: { detail: { formData: FormData } }) {
|
||||||
|
this.isSubmitting = true;
|
||||||
|
|
||||||
|
if (this.authState) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("log-out", { detail: { redirect: false } })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { formData } = event.detail;
|
||||||
|
const email = formData.get("email") as string;
|
||||||
|
const password = formData.get("password") as string;
|
||||||
|
const registerParams = {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
newArchive: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const resp = await fetch("/api/auth/register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(registerParams),
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (resp.status) {
|
||||||
|
case 201:
|
||||||
|
const data = await resp.json();
|
||||||
|
|
||||||
|
if (data.is_active) {
|
||||||
|
// Log in right away
|
||||||
|
try {
|
||||||
|
await this.logIn({ email, password });
|
||||||
|
} catch {
|
||||||
|
// Fallback to sign up message
|
||||||
|
this.isSignUpComplete = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isSignUpComplete = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 400:
|
||||||
|
case 422:
|
||||||
|
const { detail } = await resp.json();
|
||||||
|
if (detail === "REGISTER_USER_ALREADY_EXISTS") {
|
||||||
|
// Try logging user in
|
||||||
|
try {
|
||||||
|
await this.logIn({ email, password });
|
||||||
|
} catch {
|
||||||
|
this.serverError = msg("Invalid email address or password");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO show validation details
|
||||||
|
this.serverError = msg("Invalid email address or password");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.serverError = msg("Something unexpected went wrong");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isSubmitting = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async logIn({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}) {
|
||||||
|
const loginParams = new URLSearchParams();
|
||||||
|
loginParams.set("grant_type", "password");
|
||||||
|
loginParams.set("username", email);
|
||||||
|
loginParams.set("password", password);
|
||||||
|
|
||||||
|
const resp = await fetch("/api/auth/jwt/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
body: loginParams.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
throw new Error(resp.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO consolidate with log-in method
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.token_type === "bearer" && data.access_token) {
|
||||||
|
const auth = "Bearer " + data.access_token;
|
||||||
|
const detail = { auth, username: email };
|
||||||
|
this.dispatchEvent(new CustomEvent("logged-in", { detail }));
|
||||||
|
} else {
|
||||||
|
throw new Error("Unknown authorization type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
frontend/src/pages/verify.ts
Normal file
51
frontend/src/pages/verify.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { state, property } from "lit/decorators.js";
|
||||||
|
import { msg, localized } from "@lit/localize";
|
||||||
|
|
||||||
|
import LiteElement, { html } from "../utils/LiteElement";
|
||||||
|
|
||||||
|
@localized()
|
||||||
|
export class Verify extends LiteElement {
|
||||||
|
@property({ type: String })
|
||||||
|
token?: string;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private serverError?: string;
|
||||||
|
|
||||||
|
firstUpdated() {
|
||||||
|
if (this.token) {
|
||||||
|
this.verify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.serverError) {
|
||||||
|
return html`<bt-alert type="danger">${this.serverError}</bt-alert>`;
|
||||||
|
}
|
||||||
|
return html` <div class="text-4xl"><sl-spinner></sl-spinner></div> `;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async verify() {
|
||||||
|
const resp = await fetch("/api/auth/verify", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
token: this.token,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (resp.status) {
|
||||||
|
case 200:
|
||||||
|
this.navTo("/log-in");
|
||||||
|
break;
|
||||||
|
case 400:
|
||||||
|
const { detail } = await resp.json();
|
||||||
|
if (detail === "VERIFY_USER_BAD_TOKEN") {
|
||||||
|
this.serverError = msg("This verification email is not valid.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
this.serverError = msg("Something unexpected went wrong");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,5 +11,6 @@ import "@shoelace-style/shoelace/dist/components/input/input";
|
|||||||
import "@shoelace-style/shoelace/dist/components/menu/menu";
|
import "@shoelace-style/shoelace/dist/components/menu/menu";
|
||||||
import "@shoelace-style/shoelace/dist/components/menu-item/menu-item";
|
import "@shoelace-style/shoelace/dist/components/menu-item/menu-item";
|
||||||
import "@shoelace-style/shoelace/dist/components/select/select";
|
import "@shoelace-style/shoelace/dist/components/select/select";
|
||||||
|
import "@shoelace-style/shoelace/dist/components/spinner/spinner";
|
||||||
|
|
||||||
setBasePath("/shoelace");
|
setBasePath("/shoelace");
|
||||||
|
|||||||
@ -6,3 +6,8 @@ export type Auth = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type AuthState = Auth | null;
|
export type AuthState = Auth | null;
|
||||||
|
|
||||||
|
export type CurrentUser = {
|
||||||
|
email: string;
|
||||||
|
isVerified: boolean;
|
||||||
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user