diff --git a/frontend/src/components/account-settings.ts b/frontend/src/components/account-settings.ts
index ed06d0f8..9aa1124f 100644
--- a/frontend/src/components/account-settings.ts
+++ b/frontend/src/components/account-settings.ts
@@ -40,7 +40,9 @@ class RequestVerify extends LitElement {
?disabled=${this.isRequesting}
@click=${this.requestVerification}
>
- ${msg("Resend verification email")}
+ ${this.isRequesting
+ ? msg("Sending...")
+ : msg("Resend verification email")}
`;
}
diff --git a/frontend/src/index.ts b/frontend/src/index.ts
index ffa203d0..9d545b76 100644
--- a/frontend/src/index.ts
+++ b/frontend/src/index.ts
@@ -32,6 +32,14 @@ const ROUTES = {
"archive-info-tab": "/archive/:aid/:tab",
} as const;
+/**
+ * @event navigate
+ * @event notify
+ * @event need-login
+ * @event logged-in
+ * @event log-out
+ * @event user-info-change
+ */
@localized()
export class App extends LiteElement {
private router: APIRouter = new APIRouter(ROUTES);
@@ -230,6 +238,11 @@ export class App extends LiteElement {
return html``;
case "login":
@@ -295,8 +308,8 @@ export class App extends LiteElement {
}
}
- onLogOut(event: CustomEvent<{ redirect?: boolean }>) {
- const { detail } = event;
+ onLogOut(event: CustomEvent<{ redirect?: boolean } | null>) {
+ const detail = event.detail || {};
const redirect = detail.redirect !== false;
this.clearAuthState();
@@ -334,6 +347,61 @@ export class App extends LiteElement {
this.navigate(event.detail);
}
+ onUserInfoChange(event: CustomEvent>) {
+ // @ts-ignore
+ this.userInfo = {
+ ...this.userInfo,
+ ...event.detail,
+ };
+ }
+
+ onNotify(
+ event: CustomEvent<{
+ title?: string;
+ message?: string;
+ type?: "success" | "warning" | "danger" | "primary";
+ icon?: string;
+ duration?: number;
+ }>
+ ) {
+ const {
+ title,
+ message,
+ type = "primary",
+ icon = "info-circle",
+ duration = 5000,
+ } = event.detail;
+
+ const escapeHtml = (html: any) => {
+ const div = document.createElement("div");
+ div.textContent = html;
+ return div.innerHTML;
+ };
+
+ const alert = Object.assign(document.createElement("sl-alert"), {
+ type: type,
+ closable: true,
+ duration: duration,
+ style: [
+ "--sl-panel-background-color: var(--sl-color-neutral-1000)",
+ "--sl-color-neutral-700: var(--sl-color-neutral-0)",
+ // "--sl-panel-border-width: 0px",
+ "--sl-spacing-large: var(--sl-spacing-medium)",
+ ].join(";"),
+ innerHTML: `
+
+
+ ${title ? `${escapeHtml(title)}` : ""}
+ ${message ? `${escapeHtml(message)}
` : ""}
+
+
+ `,
+ });
+
+ document.body.append(alert);
+ alert.toast();
+ }
+
clearAuthState() {
this.authState = null;
window.localStorage.setItem("authState", "");
diff --git a/frontend/src/pages/verify.ts b/frontend/src/pages/verify.ts
index 403406ae..3cefc654 100644
--- a/frontend/src/pages/verify.ts
+++ b/frontend/src/pages/verify.ts
@@ -1,10 +1,14 @@
import { state, property } from "lit/decorators.js";
import { msg, localized } from "@lit/localize";
+import { AuthState } from "../types/auth";
import LiteElement, { html } from "../utils/LiteElement";
@localized()
export class Verify extends LiteElement {
+ @property({ type: Object })
+ authState?: AuthState;
+
@property({ type: String })
token?: string;
@@ -24,7 +28,7 @@ export class Verify extends LiteElement {
return html`
`;
}
- private async verify() {
+ private async verify(): Promise {
const resp = await fetch("/api/auth/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -33,19 +37,61 @@ export class Verify extends LiteElement {
}),
});
+ const data = await resp.json();
+
switch (resp.status) {
case 200:
- this.navTo("/log-in");
- break;
+ return this.onVerificationComplete(data);
case 400:
- const { detail } = await resp.json();
+ const { detail } = data;
if (detail === "VERIFY_USER_BAD_TOKEN") {
this.serverError = msg("This verification email is not valid.");
break;
}
+
+ if (detail === "VERIFY_USER_ALREADY_VERIFIED") {
+ return this.onVerificationComplete(data);
+ }
default:
this.serverError = msg("Something unexpected went wrong");
break;
}
}
+
+ private onVerificationComplete(data: {
+ email: string;
+ is_verified: boolean;
+ }) {
+ const isLoggedIn = Boolean(this.authState);
+ const shouldLogOut = isLoggedIn && this.authState?.username !== data.email;
+
+ this.dispatchEvent(
+ new CustomEvent("notify", {
+ detail: {
+ title: msg("Email address verified"),
+ message:
+ isLoggedIn && !shouldLogOut ? "" : msg("Log in to continue."),
+ type: "success",
+ icon: "check2-circle",
+ duration: 10000,
+ },
+ })
+ );
+
+ if (shouldLogOut) {
+ this.dispatchEvent(new CustomEvent("log-out"));
+ } else {
+ if (isLoggedIn) {
+ this.dispatchEvent(
+ new CustomEvent("user-info-change", {
+ detail: {
+ isVerified: data.is_verified,
+ },
+ })
+ );
+ }
+
+ this.navTo("/log-in");
+ }
+ }
}
diff --git a/frontend/src/shoelace.ts b/frontend/src/shoelace.ts
index afbba84d..edc9861f 100644
--- a/frontend/src/shoelace.ts
+++ b/frontend/src/shoelace.ts
@@ -4,6 +4,7 @@
*/
import { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js";
import "@shoelace-style/shoelace/dist/themes/light.css";
+import "@shoelace-style/shoelace/dist/components/alert/alert";
import "@shoelace-style/shoelace/dist/components/button/button";
import "@shoelace-style/shoelace/dist/components/form/form";
import "@shoelace-style/shoelace/dist/components/icon/icon";
diff --git a/frontend/src/theme.ts b/frontend/src/theme.ts
index f3585c6f..aa251d59 100644
--- a/frontend/src/theme.ts
+++ b/frontend/src/theme.ts
@@ -50,6 +50,11 @@ const theme = css`
--sl-input-label-font-size-medium: var(--sl-font-size-small);
--sl-input-label-font-size-large: var(--sl-font-size-medium);
}
+
+ .sl-toast-stack {
+ bottom: 0;
+ top: auto;
+ }
`;
export default theme;