Skip to content

Commit

Permalink
Allow users to sign up through UI (#30)
Browse files Browse the repository at this point in the history
closes #28
  • Loading branch information
SuaYoo committed Nov 30, 2021
1 parent 50e9372 commit 3fa85c8
Show file tree
Hide file tree
Showing 8 changed files with 453 additions and 15 deletions.
105 changes: 100 additions & 5 deletions frontend/src/components/account-settings.ts
Original file line number Diff line number Diff line change
@@ -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 { 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 { 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 = {
successMessage?: string;
serverError?: string;
Expand Down Expand Up @@ -98,14 +162,19 @@ const machine = createMachine<FormContext, FormEvent, FormTypestate>(
@needLogin
@localized()
export class AccountSettings extends LiteElement {
private formStateService = interpret(machine);

@property({ type: Object })
authState?: AuthState;

private formStateService = interpret(machine);
@property({ type: Object })
userInfo?: CurrentUser;

@state()
private formState = machine.initialState;

firstUpdated() {
// Enable state machine
this.formStateService.subscribe((state) => {
this.formState = state;
});
Expand All @@ -122,6 +191,7 @@ export class AccountSettings extends LiteElement {
this.formState.value === "editingForm" ||
this.formState.value === "submittingForm";
let successMessage;
let verificationMessage;

if (this.formState.context.successMessage) {
successMessage = html`
Expand All @@ -133,15 +203,40 @@ 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">
<h1 class="text-xl font-bold">${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">Email</div>
<div>${this.authState!.username}</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
Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/alert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export class Alert extends LitElement {
`;

render() {
console.log("id:", this.id);
return html`
<div class="${this.type}" role="alert">
<slot></slot>
Expand Down
37 changes: 33 additions & 4 deletions frontend/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,56 @@
import { spy, stub } from "sinon";
import { spy, stub, mock, restore } from "sinon";
import { fixture, expect } from "@open-wc/testing";
// import { expect } from "@esm-bundle/chai";

import { App } from "./index";

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 () => {
const el = await fixture("<browsertrix-app></browsertrix-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) => {
if (key === "authState")
return JSON.stringify({
username: "test@example.com",
username: "test-auth@example.com",
});
return null;
});
const el = (await fixture("<browsertrix-app></browsertrix-app>")) as App;

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,
});
});
});
76 changes: 71 additions & 5 deletions frontend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import "./shoelace";
import { LocalePicker } from "./components/locale-picker";
import { Alert } from "./components/alert";
import { AccountSettings } from "./components/account-settings";
import { SignUp } from "./pages/sign-up";
import { Verify } from "./pages/verify";
import { LogInPage } from "./pages/log-in";
import { ResetPassword } from "./pages/reset-password";
import { MyAccountPage } from "./pages/my-account";
Expand All @@ -14,11 +16,13 @@ import { ArchiveConfigsPage } from "./pages/archive-info-tab";
import LiteElement, { html } from "./utils/LiteElement";
import APIRouter 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";

const ROUTES = {
home: "/",
signUp: "/sign-up",
verify: "/verify?token",
login: "/log-in",
forgotPassword: "/log-in/forgot-password",
resetPassword: "/reset-password?token",
Expand All @@ -35,6 +39,9 @@ export class App extends LiteElement {
@state()
authState: AuthState | null = null;

@state()
userInfo?: CurrentUser;

@state()
viewState!: ViewState & {
aid?: string;
Expand Down Expand Up @@ -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) {
if (newViewPath.startsWith("http")) {
const url = new URL(newViewPath);
Expand Down Expand Up @@ -127,7 +160,7 @@ export class App extends LiteElement {
><h1 class="text-base px-2">${msg("Browsertrix Cloud")}</h1></a
>
</div>
<div>
<div class="grid grid-flow-col gap-5 items-center">
${this.authState
? html` <sl-dropdown>
<div class="p-2" role="button" slot="trigger">
Expand All @@ -147,7 +180,12 @@ export class App extends LiteElement {
>
</sl-menu>
</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>
</nav>
`;
Expand Down Expand Up @@ -179,6 +217,21 @@ export class App extends LiteElement {
`;

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 "forgotPassword":
return html`<log-in
Expand Down Expand Up @@ -223,6 +276,7 @@ export class App extends LiteElement {
@navigate="${this.onNavigateTo}"
@need-login="${this.onNeedLogin}"
.authState="${this.authState}"
.userInfo="${this.userInfo}"
></btrix-account-settings>`);

case "archive-info":
Expand All @@ -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.navigate("/");

if (redirect) {
this.navigate("/");
}
}

onLoggedIn(
Expand Down Expand Up @@ -278,11 +338,17 @@ export class App extends LiteElement {
this.authState = null;
window.localStorage.setItem("authState", "");
}

getUserInfo() {
return this.apiFetch("/users/me", this.authState!);
}
}

customElements.define("bt-alert", Alert);
customElements.define("bt-locale-picker", LocalePicker);
customElements.define("browsertrix-app", App);
customElements.define("btrix-sign-up", SignUp);
customElements.define("btrix-verify", Verify);
customElements.define("log-in", LogInPage);
customElements.define("my-account", MyAccountPage);
customElements.define("btrix-archive", ArchivePage);
Expand Down
Loading

0 comments on commit 3fa85c8

Please sign in to comment.