Skip to content

Commit

Permalink
feat: Move admin route to own namespace (#2405)
Browse files Browse the repository at this point in the history
Resolves #2382

## Changes
- Moves superadmin to `/admin` URL namespace
- Removes superadmin views from main webpack chunks
  • Loading branch information
SuaYoo authored Feb 21, 2025
1 parent 8db80f5 commit 06f6d9d
Show file tree
Hide file tree
Showing 13 changed files with 139 additions and 98 deletions.
2 changes: 1 addition & 1 deletion frontend/src/components/orgs-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,7 +725,7 @@ export class OrgsList extends BtrixElement {
<btrix-table-cell class="p-2" rowClickTarget="a">
<a
class=${org.readOnly ? "text-neutral-500" : "text-neutral-900"}
href="/orgs/${org.slug}"
href="/orgs/${org.slug}/dashboard"
@click=${this.navigate.link}
aria-disabled="${!isUserOrg}"
>
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe("browsertrix-app", () => {
expect(el.shadowRoot?.childElementCount).to.equal(0);
});

it("renders home when authenticated", async () => {
it("renders org when authenticated", async () => {
stub(AuthService, "initSessionStorage").returns(
Promise.resolve({
headers: { Authorization: "_fake_headers_" },
Expand All @@ -89,14 +89,15 @@ describe("browsertrix-app", () => {
);
// @ts-expect-error checkFreshness is private
stub(AuthService.prototype, "checkFreshness");
AppStateService.updateOrgSlug("fake-org");
const el = await fixture<App>(
html` <browsertrix-app .settings=${mockAppSettings}></browsertrix-app>`,
);
await el.updateComplete;
expect(el.shadowRoot?.querySelector("btrix-home")).to.exist;
expect(el.shadowRoot?.querySelector("btrix-org")).to.exist;
});

it("renders home when not authenticated", async () => {
it("renders log in when not authenticated", async () => {
stub(AuthService, "initSessionStorage").returns(Promise.resolve(null));
// @ts-expect-error checkFreshness is private
stub(AuthService.prototype, "checkFreshness");
Expand All @@ -110,7 +111,7 @@ describe("browsertrix-app", () => {
const el = await fixture<App>(
html` <browsertrix-app .settings=${mockAppSettings}></browsertrix-app>`,
);
expect(el.shadowRoot?.querySelector("btrix-home")).to.exist;
expect(el.shadowRoot?.querySelector("btrix-log-in")).to.exist;
});

// TODO move tests to AuthService
Expand Down
118 changes: 72 additions & 46 deletions frontend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
import { html, nothing, type TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { when } from "lit/directives/when.js";
import isEqual from "lodash/fp/isEqual";

Expand All @@ -24,10 +25,10 @@ import "./assets/fonts/Recursive/recursive.css";
import "./styles.css";

import { viewStateContext } from "./context/view-state";
import { OrgTab, RouteNamespace, ROUTES } from "./routes";
import { OrgTab, RouteNamespace } from "./routes";
import type { UserInfo, UserOrg } from "./types/user";
import { pageView, type AnalyticsTrackProps } from "./utils/analytics";
import APIRouter, { type ViewState } from "./utils/APIRouter";
import { type ViewState } from "./utils/APIRouter";
import AuthService, {
type AuthEventDetail,
type LoggedInEventDetail,
Expand All @@ -47,6 +48,7 @@ import { type AppSettings } from "@/utils/app";
import { DEFAULT_MAX_SCALE } from "@/utils/crawler";
import localize from "@/utils/localize";
import { toast } from "@/utils/notify";
import router, { urlForName } from "@/utils/router";
import { AppStateService } from "@/utils/state";
import { formatAPIUser } from "@/utils/user";
import brandLockupColor from "~assets/brand/browsertrix-lockup-color.svg";
Expand Down Expand Up @@ -94,7 +96,9 @@ export class App extends BtrixElement {
@property({ type: Object })
settings?: AppSettings;

private readonly router = new APIRouter(ROUTES);
// TODO Refactor into context
private readonly router = router;

authService = new AuthService();

@state()
Expand All @@ -120,6 +124,22 @@ export class App extends BtrixElement {
return this.viewState.params.slug || "";
}

private get homePath() {
let path = "/log-in";
if (this.authState) {
if (this.userInfo?.isSuperAdmin) {
path = `/${RouteNamespace.Superadmin}`;
} else if (this.appState.orgSlug) {
path = `${this.navigate.orgBasePath}/${OrgTab.Dashboard}`;
} else if (this.userInfo?.orgs[0]) {
path = `/${RouteNamespace.PrivateOrgs}/${this.userInfo.orgs[0].slug}/${OrgTab.Dashboard}`;
} else {
path = "/account/settings";
}
}
return path;
}

get isUserInCurrentOrg(): boolean {
const slug = this.orgSlugInPath;
if (!this.userInfo || !slug) return false;
Expand Down Expand Up @@ -201,6 +221,10 @@ export class App extends BtrixElement {
}
break;
}
case "home":
// Redirect base URL
this.routeTo(this.homePath);
break;
default:
break;
}
Expand Down Expand Up @@ -268,7 +292,7 @@ export class App extends BtrixElement {
this.authService.authState,
);
this.clearUser();
this.routeTo(ROUTES.login);
this.routeTo(urlForName("login"));
}
}
}
Expand Down Expand Up @@ -429,10 +453,6 @@ export class App extends BtrixElement {

private renderNavBar() {
const isSuperAdmin = this.userInfo?.isSuperAdmin;
let homeHref = "/";
if (!isSuperAdmin && this.appState.orgSlug && this.authState) {
homeHref = `${this.navigate.orgBasePath}/${OrgTab.Dashboard}`;
}

const showFullLogo =
this.viewState.route === "login" || !this.authService.authState;
Expand All @@ -446,7 +466,7 @@ export class App extends BtrixElement {
<a
class="items-between flex gap-2"
aria-label="home"
href=${homeHref}
href=${this.homePath}
@click=${(e: MouseEvent) => {
if (isSuperAdmin) {
this.clearSelectedOrg();
Expand Down Expand Up @@ -474,7 +494,7 @@ export class App extends BtrixElement {
></div>
<a
class="flex items-center gap-2 font-medium text-primary-700 transition-colors hover:text-primary"
href="/"
href="/${RouteNamespace.Superadmin}"
@click=${(e: MouseEvent) => {
this.clearSelectedOrg();
this.navigate.link(e);
Expand Down Expand Up @@ -538,7 +558,8 @@ export class App extends BtrixElement {
</sl-menu-item>
${this.userInfo?.isSuperAdmin
? html` <sl-menu-item
@click=${() => this.routeTo(ROUTES.usersInvite)}
@click=${() =>
this.routeTo(urlForName("adminUsersInvite"))}
>
<sl-icon slot="prefix" name="person-plus"></sl-icon>
${msg("Invite Users")}
Expand Down Expand Up @@ -580,7 +601,7 @@ export class App extends BtrixElement {
>
<a
class="font-medium text-neutral-500 hover:text-primary"
href="/crawls"
href=${urlForName("adminCrawls")}
@click=${this.navigate.link}
>${msg("Running Crawls")}</a
>
Expand Down Expand Up @@ -817,8 +838,12 @@ export class App extends BtrixElement {
.viewState=${this.viewState}
></btrix-reset-password>`;

case "home":
return html`<btrix-home class="w-full md:bg-neutral-50"></btrix-home>`;
case "admin":
return this.renderAdminPage(
() => html`
<btrix-admin class="w-full md:bg-neutral-50"></btrix-admin>
`,
);

case "orgs":
return html`<btrix-orgs class="w-full md:bg-neutral-50"></btrix-orgs>`;
Expand Down Expand Up @@ -869,36 +894,25 @@ export class App extends BtrixElement {
tab=${this.viewState.params.settingsTab}
></btrix-account-settings>`;

case "usersInvite": {
if (this.userInfo) {
if (this.userInfo.isSuperAdmin) {
return html`<btrix-users-invite
case "adminUsers":
case "adminUsersInvite":
return this.renderAdminPage(
() =>
html`<btrix-users-invite
class="mx-auto box-border w-full max-w-screen-desktop p-2 md:py-8"
></btrix-users-invite>`;
} else {
return this.renderNotFoundPage();
}
} else {
return this.renderSpinner();
}
}
></btrix-users-invite>`,
);

case "crawls":
case "crawl": {
if (this.userInfo) {
if (this.userInfo.isSuperAdmin) {
return html`<btrix-crawls
case "adminCrawls":
case "adminCrawl":
return this.renderAdminPage(
() =>
html`<btrix-crawls
class="w-full"
@notify=${this.onNotify}
crawlId=${this.viewState.params.crawlId}
></btrix-crawls>`;
} else {
return this.renderNotFoundPage();
}
} else {
return this.renderSpinner();
}
}
></btrix-crawls>`,
);

case "awpUploadRedirect": {
const { orgId, uploadId } = this.viewState.params;
Expand All @@ -915,6 +929,21 @@ export class App extends BtrixElement {
}
}

private renderAdminPage(renderer: () => TemplateResult) {
// if (!this.userInfo) return this.renderSpinner();

if (this.userInfo?.isSuperAdmin) {
// Dynamically import admin pages
return until(
import(/* webpackChunkName: "admin" */ "@/pages/admin/index").then(
renderer,
),
);
}

return this.renderNotFoundPage();
}

private renderSpinner() {
return html`
<div class="flex w-full items-center justify-center text-3xl">
Expand Down Expand Up @@ -962,7 +991,7 @@ export class App extends BtrixElement {
this.clearUser();

if (redirect) {
this.routeTo(ROUTES.login);
this.routeTo(urlForName("login"));
}
}

Expand All @@ -976,10 +1005,7 @@ export class App extends BtrixElement {
});

if (!detail.api) {
this.routeTo(
detail.redirectUrl ||
`${this.navigate.orgBasePath}/${OrgTab.Dashboard}`,
);
this.routeTo(detail.redirectUrl || this.homePath);
}

if (detail.firstLogin) {
Expand All @@ -1000,7 +1026,7 @@ export class App extends BtrixElement {

this.clearUser();
const redirectUrl = e.detail.redirectUrl;
this.routeTo(ROUTES.login, {
this.routeTo(urlForName("login"), {
redirectUrl,
});
if (redirectUrl && redirectUrl !== "/") {
Expand Down Expand Up @@ -1105,7 +1131,7 @@ export class App extends BtrixElement {
this.syncViewState();
} else {
this.clearUser();
this.routeTo(ROUTES.login);
this.routeTo(urlForName("login"));
}
}
}
Expand Down
45 changes: 7 additions & 38 deletions frontend/src/pages/admin.ts → frontend/src/pages/admin/admin.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { localized, msg, str } from "@lit/localize";
import type { SlInput, SlInputEvent } from "@shoelace-style/shoelace";
import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js";
import { html, type PropertyValues } from "lit";
import { html } from "lit";
import { customElement, state } from "lit/decorators.js";

import { BtrixElement } from "@/classes/BtrixElement";
import needLogin from "@/decorators/needLogin";
import type { InviteSuccessDetail } from "@/features/accounts/invite-form";
import type { APIUser } from "@/index";
import { OrgTab, RouteNamespace } from "@/routes";
import type { APIPaginatedList } from "@/types/api";
import { isApiError } from "@/utils/api";
import { maxLengthValidator } from "@/utils/form";
Expand All @@ -17,16 +17,11 @@ import { AppStateService } from "@/utils/state";
import { formatAPIUser } from "@/utils/user";

/**
* Home page when org is not selected.
*
* Uses custom redirect instead of needLogin decorator to suppress "need login"
* message when accessing root URL.
*
* Only accessed by superadmins. Regular users will be redirected their org.
* See https://github.com/webrecorder/browsertrix/issues/1972
* Browsertrix superadmin dashboard
*/
@customElement("btrix-home")
@customElement("btrix-admin")
@localized()
@needLogin
export class Admin extends BtrixElement {
@state()
private orgList?: OrgData[];
Expand All @@ -52,38 +47,12 @@ export class Admin extends BtrixElement {

private readonly validateOrgNameMax = maxLengthValidator(40);

connectedCallback() {
if (this.authState) {
if (this.slug) {
this.navigate.to(`/orgs/${this.slug}`);
} else {
super.connectedCallback();
}
} else {
this.navigate.to("/log-in");
}
}

willUpdate(changedProperties: PropertyValues) {
if (changedProperties.has("appState.userInfo") && this.userInfo) {
if (this.userInfo.isSuperAdmin) {
this.initSuperAdmin();
} else if (this.userInfo.orgs.length) {
this.navigate.to(
`/${RouteNamespace.PrivateOrgs}/${this.userInfo.orgs[0].slug}/${OrgTab.Dashboard}`,
);
} else {
this.navigate.to(`/account/settings`);
}
}
}

protected firstUpdated(): void {
this.initSuperAdmin();
}

private initSuperAdmin() {
if (this.userInfo?.isSuperAdmin && !this.orgList) {
if (this.userInfo?.isSuperAdmin) {
if (this.userInfo.orgs.length) {
void this.fetchOrgs();
} else {
Expand All @@ -94,7 +63,7 @@ export class Admin extends BtrixElement {
}

render() {
if (!this.userInfo || !this.userInfo.isSuperAdmin) {
if (!this.userInfo?.isSuperAdmin) {
return;
}

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/pages/admin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import "./admin";
import "./users-invite";
File renamed without changes.
Loading

0 comments on commit 06f6d9d

Please sign in to comment.