diff --git a/frontend/package.json b/frontend/package.json
index e5a729d5..862e5afa 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -31,7 +31,9 @@
"devDependencies": {
"@esm-bundle/chai": "^4.3.4-fix.0",
"@lit/localize-tools": "^0.5.0",
+ "@open-wc/testing": "^3.0.3",
"@types/color": "^3.0.2",
+ "@types/sinon": "^10.0.6",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"@web/dev-server-esbuild": "^0.2.16",
diff --git a/frontend/src/index.test.ts b/frontend/src/index.test.ts
index 621f7a26..72572679 100644
--- a/frontend/src/index.test.ts
+++ b/frontend/src/index.test.ts
@@ -1,9 +1,27 @@
-import { expect } from "@esm-bundle/chai";
+import { spy, stub } from "sinon";
+import { fixture, expect } from "@open-wc/testing";
+// import { expect } from "@esm-bundle/chai";
import { App } from "./index";
-describe("App", () => {
- it("should exist", () => {
- expect(App).to.exist;
+describe("browsertrix-app", () => {
+ it("is defined", async () => {
+ const el = await fixture("");
+ expect(el).instanceOf(App);
+ });
+
+ it("gets auth state from local storage", async () => {
+ stub(window.localStorage, "getItem").callsFake((key) => {
+ if (key === "authState")
+ return JSON.stringify({
+ username: "test@example.com",
+ });
+ return null;
+ });
+ const el = (await fixture("")) as App;
+
+ expect(el.authState).to.eql({
+ username: "test@example.com",
+ });
});
});
diff --git a/frontend/src/index.ts b/frontend/src/index.ts
index a538e9d1..551e5891 100644
--- a/frontend/src/index.ts
+++ b/frontend/src/index.ts
@@ -1,4 +1,5 @@
import type { TemplateResult } from "lit";
+import { state } from "lit/decorators.js";
import { msg, updateWhenLocaleChanges } from "@lit/localize";
import "./shoelace";
@@ -13,10 +14,22 @@ import type { ViewState, NavigateEvent } from "./utils/APIRouter";
import type { AuthState } from "./types/auth";
import theme from "./theme";
+const ROUTES = {
+ home: "/",
+ login: "/log-in",
+ myAccount: "/my-account",
+ "archive-info": "/archive/:aid",
+ "archive-info-tab": "/archive/:aid/:tab",
+} as const;
+
// ===========================================================================
export class App extends LiteElement {
- authState: AuthState | null;
router: APIRouter;
+
+ @state()
+ authState: AuthState | null = null;
+
+ @state()
viewState: ViewState & {
aid?: string;
// TODO common tab type
@@ -31,31 +44,16 @@ export class App extends LiteElement {
// history navigation.
updateWhenLocaleChanges(this);
- this.authState = null;
-
const authState = window.localStorage.getItem("authState");
if (authState) {
this.authState = JSON.parse(authState);
}
- this.router = new APIRouter({
- home: "/",
- login: "/log-in",
- "my-account": "/my-account",
- "archive-info": "/archive/:aid",
- "archive-info-tab": "/archive/:aid/:tab",
- });
+ this.router = new APIRouter(ROUTES);
this.viewState = this.router.match(window.location.pathname);
}
- static get properties() {
- return {
- viewState: { type: Object },
- authState: { type: Object },
- };
- }
-
firstUpdated() {
window.addEventListener("popstate", (event) => {
// if (event.state.view) {
@@ -71,10 +69,14 @@ export class App extends LiteElement {
if (newView.startsWith("http")) {
newView = new URL(newView).pathname;
}
- this.viewState = this.router.match(newView);
- if (this.viewState._route === "login") {
- this.clearAuthState();
+
+ if (newView === "/log-in" && this.authState) {
+ // Redirect to logged in home page
+ this.viewState = this.router.match(ROUTES.myAccount);
+ } else {
+ this.viewState = this.router.match(newView);
}
+
//console.log(this.view._route, window.location.href);
window.history.pushState(this.viewState, "", this.viewState._path);
}
@@ -149,7 +151,7 @@ export class App extends LiteElement {
@@ -161,17 +163,23 @@ export class App extends LiteElement {
case "login":
return html`
`;
case "home":
return html`
-
+ this.navigate("/log-in")}"
+ >
${msg("Log In")}
`;
- case "my-account":
+ case "myAccount":
return appLayout(html`
) {
this.clearAuthState();
- this.navigate("/log-in");
+
+ if (event?.detail?.api) {
+ // TODO refresh instead of redirect
+ }
+ this.navigate(ROUTES.login);
}
onNavigateTo(event: NavigateEvent) {
diff --git a/frontend/src/pages/archive-info-tab.ts b/frontend/src/pages/archive-info-tab.ts
index 15a7b905..ed4bbcd1 100644
--- a/frontend/src/pages/archive-info-tab.ts
+++ b/frontend/src/pages/archive-info-tab.ts
@@ -1,11 +1,12 @@
import LiteElement, { html } from "../utils/LiteElement";
+import { needLogin } from "../utils/auth";
import type { Archive, ArchiveConfig } from "../types/archives";
import type { AuthState } from "../types/auth";
+@needLogin
export class ArchiveConfigsPage extends LiteElement {
- archive!: Archive & {
- authState: AuthState;
- };
+ archive!: Archive;
+ authState!: AuthState;
configs: ArchiveConfig;
static get properties() {
@@ -16,14 +17,9 @@ export class ArchiveConfigsPage extends LiteElement {
}
async firstUpdated() {
- if (!this.archive?.authState) {
- // TODO
- return;
- }
-
const res = await this.apiFetch(
`/archives/${this.archive.aid}/crawlconfigs`,
- this.archive.authState
+ this.authState!
);
this.configs = res.crawl_configs;
}
diff --git a/frontend/src/pages/archive-info.ts b/frontend/src/pages/archive-info.ts
index 912a16cd..ff91d741 100644
--- a/frontend/src/pages/archive-info.ts
+++ b/frontend/src/pages/archive-info.ts
@@ -1,7 +1,9 @@
import LiteElement, { html } from "../utils/LiteElement";
+import { needLogin } from "../utils/auth";
import type { Archive } from "../types/archives";
import type { AuthState } from "../types/auth";
+@needLogin
export class ArchivePage extends LiteElement {
authState: AuthState = null;
aid?: Archive["aid"];
@@ -48,8 +50,8 @@ export class ArchivePage extends LiteElement {
? html``
: ""}
diff --git a/frontend/src/pages/log-in.ts b/frontend/src/pages/log-in.ts
index 0baffd30..e1179290 100644
--- a/frontend/src/pages/log-in.ts
+++ b/frontend/src/pages/log-in.ts
@@ -3,9 +3,6 @@ import LiteElement, { html } from "../utils/LiteElement";
import type { Auth } from "../types/auth";
export class LogInPage extends LiteElement {
- @property({ type: Object })
- auth?: Auth;
-
@state()
isLoggingIn: boolean = false;
@@ -90,10 +87,6 @@ export class LogInPage extends LiteElement {
console.error(e);
}
- if (!this.auth) {
- this.loginError = "Unknown login response";
- }
-
this.isLoggingIn = false;
}
}
diff --git a/frontend/src/pages/my-account.ts b/frontend/src/pages/my-account.ts
index 27f699c4..b07851f7 100644
--- a/frontend/src/pages/my-account.ts
+++ b/frontend/src/pages/my-account.ts
@@ -1,7 +1,9 @@
import LiteElement, { html } from "../utils/LiteElement";
+import { needLogin } from "../utils/auth";
import type { Archive } from "../types/archives";
import type { AuthState } from "../types/auth";
+@needLogin
export class MyAccountPage extends LiteElement {
archiveList: Archive[] = [];
authState: AuthState = null;
@@ -15,15 +17,10 @@ export class MyAccountPage extends LiteElement {
}
async firstUpdated() {
- if (!this.authState) {
- this.dispatchEvent(new CustomEvent("need-login"));
- return;
- }
-
- const data = await this.apiFetch("/archives", this.authState);
+ const data = await this.apiFetch("/archives", this.authState!);
this.archiveList = data.archives;
- const data2 = await this.apiFetch("/users/me", this.authState);
+ const data2 = await this.apiFetch("/users/me", this.authState!);
this.id = data2.id;
}
diff --git a/frontend/src/utils/LiteElement.ts b/frontend/src/utils/LiteElement.ts
index 3a8d6aae..685cca79 100644
--- a/frontend/src/utils/LiteElement.ts
+++ b/frontend/src/utils/LiteElement.ts
@@ -27,10 +27,27 @@ export default class LiteElement extends LitElement {
async apiFetch(path: string, auth: Auth) {
const resp = await fetch("/api" + path, { headers: auth.headers });
+
if (resp.status !== 200) {
- this.navTo("/log-in");
- throw new Error("logged out");
+ if (resp.status === 401) {
+ this.dispatchEvent(
+ new CustomEvent("need-login", {
+ detail: { api: true },
+ })
+ );
+ }
+
+ // TODO get error details
+ let errorMessage: string;
+
+ try {
+ errorMessage = (await resp.json()).detail;
+ } catch {
+ errorMessage = "Unknown API error";
+ }
+ throw new Error(errorMessage);
}
+
return await resp.json();
}
}
diff --git a/frontend/src/utils/auth.test.ts b/frontend/src/utils/auth.test.ts
new file mode 100644
index 00000000..4c34c85d
--- /dev/null
+++ b/frontend/src/utils/auth.test.ts
@@ -0,0 +1,26 @@
+import { spy } from "sinon";
+import { expect } from "@esm-bundle/chai";
+
+import * as auth from "./auth";
+
+describe("auth", () => {
+ describe("needLogin", () => {
+ it("dispatches the correct event on need log in", () => {
+ const dispatchEventSpy = spy();
+ class LiteElementMock {
+ dispatchEvent = dispatchEventSpy;
+ }
+
+ const Element = auth.needLogin(
+ class extends LiteElementMock {
+ authState = null;
+ } as any
+ );
+
+ const element = new Element();
+ element.connectedCallback();
+
+ expect(dispatchEventSpy.getCall(0).firstArg.type).to.equal("need-login");
+ });
+ });
+});
diff --git a/frontend/src/utils/auth.ts b/frontend/src/utils/auth.ts
new file mode 100644
index 00000000..e194fea2
--- /dev/null
+++ b/frontend/src/utils/auth.ts
@@ -0,0 +1,33 @@
+import LiteElement from "../utils/LiteElement";
+import type { AuthState } from "../types/auth";
+
+/**
+ * Block rendering and dispatch event if user is not logged in
+ *
+ * Usage example:
+ * ```ts
+ * @needLogin
+ * MyComponent extends LiteElement {}
+ * ```
+ */
+export function needLogin(
+ constructor: T
+) {
+ return class extends constructor {
+ authState?: AuthState;
+
+ static get properties() {
+ return {
+ authState: { type: Object },
+ };
+ }
+
+ connectedCallback() {
+ if (this.authState) {
+ super.connectedCallback();
+ } else {
+ this.dispatchEvent(new CustomEvent("need-login"));
+ }
+ }
+ };
+}
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index a2e9df72..6ac3aac4 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -59,6 +59,13 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
+"@esm-bundle/chai@^4.3.4":
+ version "4.3.4"
+ resolved "https://registry.yarnpkg.com/@esm-bundle/chai/-/chai-4.3.4.tgz#74ed4a0794b3a9f9517ff235744ac6f4be0d34dc"
+ integrity sha512-6Tx35wWiNw7X0nLY9RMx8v3EL8SacCFW+eEZOE9Hc+XxmU5HFE2AFEg+GehUZpiyDGwVvPH75ckGlqC7coIPnA==
+ dependencies:
+ "@types/chai" "^4.2.12"
+
"@esm-bundle/chai@^4.3.4-fix.0":
version "4.3.4-fix.0"
resolved "https://registry.yarnpkg.com/@esm-bundle/chai/-/chai-4.3.4-fix.0.tgz#3084cff7eb46d741749f47f3a48dbbdcbaf30a92"
@@ -176,6 +183,63 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
+"@open-wc/chai-dom-equals@^0.12.36":
+ version "0.12.36"
+ resolved "https://registry.yarnpkg.com/@open-wc/chai-dom-equals/-/chai-dom-equals-0.12.36.tgz#ed0eb56b9e98c4d7f7280facce6215654aae9f4c"
+ integrity sha512-Gt1fa37h4rtWPQGETSU4n1L678NmMi9KwHM1sH+JCGcz45rs8DBPx7MUVeGZ+HxRlbEI5t9LU2RGGv6xT2OlyA==
+ dependencies:
+ "@open-wc/semantic-dom-diff" "^0.13.16"
+ "@types/chai" "^4.1.7"
+
+"@open-wc/dedupe-mixin@^1.3.0":
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/@open-wc/dedupe-mixin/-/dedupe-mixin-1.3.0.tgz#0df5d438285fc3482838786ee81895318f0ff778"
+ integrity sha512-UfdK1MPnR6T7f3svzzYBfu3qBkkZ/KsPhcpc3JYhsUY4hbpwNF9wEQtD4Z+/mRqMTJrKg++YSxIxE0FBhY3RIw==
+
+"@open-wc/scoped-elements@^2.0.1":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@open-wc/scoped-elements/-/scoped-elements-2.0.1.tgz#6b1c3535f809bd90710574db80093a81e3a1fc2d"
+ integrity sha512-JS6ozxUFwFX3+Er91v9yQzNIaFn7OnE0iESKTbFvkkKdNwvAPtp1fpckBKIvWk8Ae9ZcoI9DYZuT2DDbMPcadA==
+ dependencies:
+ "@lit/reactive-element" "^1.0.0"
+ "@open-wc/dedupe-mixin" "^1.3.0"
+ "@webcomponents/scoped-custom-element-registry" "^0.0.3"
+
+"@open-wc/semantic-dom-diff@^0.13.16":
+ version "0.13.21"
+ resolved "https://registry.yarnpkg.com/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.13.21.tgz#718b9ec5f9a98935fc775e577ad094ae8d8b7dea"
+ integrity sha512-BONpjHcGX2zFa9mfnwBCLEmlDsOHzT+j6Qt1yfK3MzFXFtAykfzFjAgaxPetu0YbBlCfXuMlfxI4vlRGCGMvFg==
+
+"@open-wc/semantic-dom-diff@^0.19.5":
+ version "0.19.5"
+ resolved "https://registry.yarnpkg.com/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.19.5.tgz#8d3d7f69140b9ba477a4adf8099c79e0efe18955"
+ integrity sha512-Wi0Fuj3dzqlWClU0y+J4k/nqTcH0uwgOWxZXPyeyG3DdvuyyjgiT4L4I/s6iVShWQvvEsyXnj7yVvixAo3CZvg==
+ dependencies:
+ "@types/chai" "^4.2.11"
+ "@web/test-runner-commands" "^0.5.7"
+
+"@open-wc/testing-helpers@^2.0.2":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@open-wc/testing-helpers/-/testing-helpers-2.0.2.tgz#ca1833bf76036d9bdc03547415e79b6d502c78f6"
+ integrity sha512-wJlvDmWo+fIbgykRP21YSP9I9Pf/fo2+dZGaWG77Hw0sIuyB+7sNUDJDkL6kMkyyRecPV6dVRmbLt6HuOwvZ1w==
+ dependencies:
+ "@open-wc/scoped-elements" "^2.0.1"
+ lit "^2.0.0"
+
+"@open-wc/testing@^3.0.3":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@open-wc/testing/-/testing-3.0.3.tgz#9d122933ef69e06cc9cb3b0d4e425b90117bb9e5"
+ integrity sha512-xJYckO8X9yfWc+ltPlDZjHGTh4ldNmnYsnxNriuUUEEhV5ASdsc+5WEsIS2+9m4lQELj89rNQ7YvhYhawDorhg==
+ dependencies:
+ "@esm-bundle/chai" "^4.3.4"
+ "@open-wc/chai-dom-equals" "^0.12.36"
+ "@open-wc/semantic-dom-diff" "^0.19.5"
+ "@open-wc/testing-helpers" "^2.0.2"
+ "@types/chai" "^4.2.11"
+ "@types/chai-dom" "^0.0.9"
+ "@types/sinon-chai" "^3.2.3"
+ chai-a11y-axe "^1.3.2"
+
"@popperjs/core@^2.7.0":
version "2.10.2"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590"
@@ -226,7 +290,7 @@
dependencies:
type-detect "4.0.8"
-"@sinonjs/fake-timers@^7.0.4":
+"@sinonjs/fake-timers@^7.0.4", "@sinonjs/fake-timers@^7.1.0":
version "7.1.2"
resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz#2524eae70c4910edccf99b2f4e6efc5894aff7b5"
integrity sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==
@@ -274,7 +338,14 @@
"@types/connect" "*"
"@types/node" "*"
-"@types/chai@^4.2.12":
+"@types/chai-dom@^0.0.9":
+ version "0.0.9"
+ resolved "https://registry.yarnpkg.com/@types/chai-dom/-/chai-dom-0.0.9.tgz#77379354efec2568284ca355fff6a4f85f5a66f4"
+ integrity sha512-jj4F2NJog2/GBYsyJ8+NvhnWUBbPY4MUAKLdPJE6+568rw12GGXvj0ycUuP5nndVrnJgozmJAoMTvxvjJATXWw==
+ dependencies:
+ "@types/chai" "*"
+
+"@types/chai@*", "@types/chai@^4.1.7", "@types/chai@^4.2.11", "@types/chai@^4.2.12":
version "4.2.22"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.22.tgz#47020d7e4cf19194d43b5202f35f75bd2ad35ce7"
integrity sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==
@@ -518,6 +589,21 @@
"@types/mime" "^1"
"@types/node" "*"
+"@types/sinon-chai@^3.2.3":
+ version "3.2.5"
+ resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.5.tgz#df21ae57b10757da0b26f512145c065f2ad45c48"
+ integrity sha512-bKQqIpew7mmIGNRlxW6Zli/QVyc3zikpGzCa797B/tRnD9OtHvZ/ts8sYXV+Ilj9u3QRaUEM8xrjgd1gwm1BpQ==
+ dependencies:
+ "@types/chai" "*"
+ "@types/sinon" "*"
+
+"@types/sinon@*", "@types/sinon@^10.0.6":
+ version "10.0.6"
+ resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.6.tgz#bc3faff5154e6ecb69b797d311b7cf0c1b523a1d"
+ integrity sha512-6EF+wzMWvBNeGrfP3Nx60hhx+FfwSg1JJBLAAP/IdIUq0EYkqCYf70VT3PhuhPX9eLD+Dp+lNdpb/ZeHG8Yezg==
+ dependencies:
+ "@sinonjs/fake-timers" "^7.1.0"
+
"@types/trusted-types@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756"
@@ -718,7 +804,7 @@
chrome-launcher "^0.15.0"
puppeteer-core "^11.0.0"
-"@web/test-runner-commands@^0.5.10":
+"@web/test-runner-commands@^0.5.10", "@web/test-runner-commands@^0.5.7":
version "0.5.13"
resolved "https://registry.yarnpkg.com/@web/test-runner-commands/-/test-runner-commands-0.5.13.tgz#57ea472c00ee2ada99eb9bb5a0371200922707c2"
integrity sha512-FXnpUU89ALbRlh9mgBd7CbSn5uzNtr8gvnQZPOvGLDAJ7twGvZdUJEAisPygYx2BLPSFl3/Mre8pH8zshJb8UQ==
@@ -928,6 +1014,11 @@
"@webassemblyjs/ast" "1.11.1"
"@xtuc/long" "4.2.2"
+"@webcomponents/scoped-custom-element-registry@^0.0.3":
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/@webcomponents/scoped-custom-element-registry/-/scoped-custom-element-registry-0.0.3.tgz#774591a886b0b0e4914717273ba53fd8d5657522"
+ integrity sha512-lpSzgDCGbM99dytb3+J3Suo4+Bk1E13MPnWB42JK8GwxSAxFz+tC7TTv2hhDSIE2IirGNKNKCf3m08ecu6eAsQ==
+
"@webpack-cli/configtest@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.0.4.tgz#f03ce6311c0883a83d04569e2c03c6238316d2aa"
@@ -1145,6 +1236,11 @@ autoprefixer@^10.3.6:
normalize-range "^0.1.2"
postcss-value-parser "^4.1.0"
+axe-core@^4.3.3:
+ version "4.3.5"
+ resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5"
+ integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA==
+
axios@^0.22.0:
version "0.22.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.22.0.tgz#bf702c41fb50fbca4539589d839a077117b79b25"
@@ -1314,6 +1410,13 @@ caniuse-lite@^1.0.30001260, caniuse-lite@^1.0.30001261:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001263.tgz#7ce7a6fb482a137585cbc908aaf38e90c53a16a4"
integrity sha512-doiV5dft6yzWO1WwU19kt8Qz8R0/8DgEziz6/9n2FxUasteZNwNNYSmJO3GLBH8lCVE73AB1RPDPAeYbcO5Cvw==
+chai-a11y-axe@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/chai-a11y-axe/-/chai-a11y-axe-1.3.2.tgz#77dc5f503901fed4f6097b5b0213ddb00cc891ea"
+ integrity sha512-/jYczmhGUoCfEcsrkJwjecy3PJ31T9FxFdu2BDlAwR/sX1nN3L2XmuPP3tw8iYk6LPqdF7K11wwFr3yUZMv5MA==
+ dependencies:
+ axe-core "^4.3.3"
+
chalk@^2.0.0, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"