Adjust frontend auth behavior (#24)

This commit is contained in:
sua yoo 2021-11-23 16:57:28 -08:00 committed by GitHub
parent 5b8440f295
commit 04fbe6fc4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 258 additions and 59 deletions

View File

@ -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",

View File

@ -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("<browsertrix-app></browsertrix-app>");
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("<browsertrix-app></browsertrix-app>")) as App;
expect(el.authState).to.eql({
username: "test@example.com",
});
});
});

View File

@ -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 {
<div class="w-full flex flex-col md:flex-row">
<nav class="md:w-80 md:p-4 md:border-r">
<ul class="flex md:flex-col">
${navLink({ href: "/my-account", label: "Archives" })}
${navLink({ href: ROUTES.myAccount, label: "Archives" })}
${navLink({ href: "/users", label: "Users" })}
</ul>
</nav>
@ -161,17 +163,23 @@ export class App extends LiteElement {
case "login":
return html`<log-in
class="w-full md:bg-gray-100 flex items-center justify-center"
@navigate="${this.onNavigateTo}"
@logged-in="${this.onLoggedIn}"
.authState="${this.authState}"
></log-in>`;
case "home":
return html`<div class="w-full flex items-center justify-center">
<sl-button type="primary" size="large" @click="${this.onNeedLogin}">
<sl-button
type="primary"
size="large"
@click="${() => this.navigate("/log-in")}"
>
${msg("Log In")}
</sl-button>
</div>`;
case "my-account":
case "myAccount":
return appLayout(html`<my-account
class="w-full"
@navigate="${this.onNavigateTo}"
@ -206,12 +214,16 @@ export class App extends LiteElement {
headers: { Authorization: event.detail.auth },
};
window.localStorage.setItem("authState", JSON.stringify(this.authState));
this.navigate("/my-account");
this.navigate(ROUTES.myAccount);
}
onNeedLogin() {
onNeedLogin(event?: CustomEvent<{ api: boolean }>) {
this.clearAuthState();
this.navigate("/log-in");
if (event?.detail?.api) {
// TODO refresh instead of redirect
}
this.navigate(ROUTES.login);
}
onNavigateTo(event: NavigateEvent) {

View File

@ -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;
}

View File

@ -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`<btrix-archive-configs
.archive=${{
aid: this.aid!,
authState: this.authState,
}}
.authState=${this.authState}
></btrix-archive-configs>`
: ""}
</div>

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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");
});
});
});

View File

@ -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<T extends { new (...args: any[]): LiteElement }>(
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"));
}
}
};
}

View File

@ -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"