This repository has been archived on 2024-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
authentik/web/src/standalone/api-browser/index.ts

114 lines
4 KiB
TypeScript
Raw Normal View History

import { CSRFHeaderName } from "@goauthentik/common/api/middleware";
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { first, getCookie } from "@goauthentik/common/utils";
web: abstract `rootInterface()?.config?.capabilities.includes()` into `.can()` (#7737) * This commit abstracts access to the object `rootInterface()?.config?` into a single accessor, `authentikConfig`, that can be mixed into any AKElement object that requires access to it. Since access to `rootInterface()?.config?` is _universally_ used for a single (and repetitive) boolean check, a separate accessor has been provided that converts all calls of the form: ``` javascript rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) ``` into: ``` javascript this.can(CapabilitiesEnum.CanImpersonate) ``` It does this via a Mixin, `WithCapabilitiesConfig`, which understands that these calls only make sense in the context of a running, fully configured authentik instance, and that their purpose is to inform authentik components of a user’s capabilities. The latter is why I don’t feel uncomfortable turning a function call into a method; we should make it explicit that this is a relationship between components. The mixin has a single single field, `[WCC.capabilitiesConfig]`, where its association with the upper-level configuration is made. If that syntax looks peculiar to you, good! I’ve used an explict unique symbol as the field name; it is inaccessable an innumerable in the object list. The debugger shows it only as: Symbol(): { cacheTimeout: 300 cacheTimeoutFlows: 300 cacheTimeoutPolicies: 300 cacheTimeoutReputation: 300 capabilities: (5) ['can_save_media', 'can_geo_ip', 'can_impersonate', 'can_debug', 'is_enterprise'] } Since you can’t reference it by identity, you can’t write to it. Until every browser supports actual private fields, this is the best we can do; it does guarantee that field name collisions are impossible, which is a win. The mixin takes a second optional boolean; setting this to true will cause any web component using the mixin to automatically schedule a re-render if the capabilities list changes. The mixin is also generic; despite the "...into a Lit-Context" in the title, the internals of the Mixin can be replaced with anything so long as the signature of `.can()` is preserved. Because this work builds off the work I did to give the Sidebar access to the configuration without ad-hoc retrieval or prop-drilling, it wasn’t necessary to create a new context for it. That will be necessary for the following: TODO: ``` javascript rootInterface()?.uiConfig; rootInterface()?.tenant; me(); ``` * web: Added a README with a description of the applications' "mental model," essentially an architectural description. * web: prettier had opinions about the README * web: Jens requested that subscription be by default, and it's the right call. * This commit abstracts access to the object `rootInterface()?.config?` into a single accessor, `authentikConfig`, that can be mixed into any AKElement object that requires access to it. Since access to `rootInterface()?.config?` is _universally_ used for a single (and repetitive) boolean check, a separate accessor has been provided that converts all calls of the form: ``` javascript rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) ``` into: ``` javascript this.can(CapabilitiesEnum.CanImpersonate) ``` It does this via a Mixin, `WithCapabilitiesConfig`, which understands that these calls only make sense in the context of a running, fully configured authentik instance, and that their purpose is to inform authentik components of a user’s capabilities. The latter is why I don’t feel uncomfortable turning a function call into a method; we should make it explicit that this is a relationship between components. The mixin has a single single field, `[WCC.capabilitiesConfig]`, where its association with the upper-level configuration is made. If that syntax looks peculiar to you, good! I’ve used an explict unique symbol as the field name; it is inaccessable an innumerable in the object list. The debugger shows it only as: Symbol(): { cacheTimeout: 300 cacheTimeoutFlows: 300 cacheTimeoutPolicies: 300 cacheTimeoutReputation: 300 capabilities: (5) ['can_save_media', 'can_geo_ip', 'can_impersonate', 'can_debug', 'is_enterprise'] } Since you can’t reference it by identity, you can’t write to it. Until every browser supports actual private fields, this is the best we can do; it does guarantee that field name collisions are impossible, which is a win. The mixin takes a second optional boolean; setting this to true will cause any web component using the mixin to automatically schedule a re-render if the capabilities list changes. The mixin is also generic; despite the "...into a Lit-Context" in the title, the internals of the Mixin can be replaced with anything so long as the signature of `.can()` is preserved. Because this work builds off the work I did to give the Sidebar access to the configuration without ad-hoc retrieval or prop-drilling, it wasn’t necessary to create a new context for it. That will be necessary for the following: TODO: ``` javascript rootInterface()?.uiConfig; rootInterface()?.tenant; me(); ``` * web: Added a README with a description of the applications' "mental model," essentially an architectural description. * web: prettier had opinions about the README * web: Jens requested that subscription be by default, and it's the right call. * web: adjust RAC to point to the (now independent) Interface. - Also, removed redundant check.
2024-01-08 18:22:52 +00:00
import { Interface } from "@goauthentik/elements/Interface";
import "@goauthentik/elements/ak-locale-context";
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
import "rapidoc";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { UiThemeEnum } from "@goauthentik/api";
@customElement("ak-api-browser")
export class APIBrowser extends Interface {
@property()
schemaPath?: string;
static get styles(): CSSResult[] {
return [
css`
img.logo {
width: 100%;
padding: 1rem 0.5rem 1.5rem 0.5rem;
min-height: 48px;
}
`,
];
}
@state()
bgColor = "#000000";
@state()
textColor = "#000000";
firstUpdated(): void {
this.addEventListener(EVENT_THEME_CHANGE, ((ev: CustomEvent<UiThemeEnum>) => {
const style = getComputedStyle(document.documentElement);
if (ev.detail === UiThemeEnum.Light) {
this.bgColor = style
.getPropertyValue("--pf-global--BackgroundColor--light-300")
.trim();
this.textColor = style.getPropertyValue("--pf-global--Color--300").trim();
} else {
this.bgColor = style.getPropertyValue("--ak-dark-background").trim();
this.textColor = style.getPropertyValue("--ak-dark-foreground").trim();
}
}) as EventListener);
this.dispatchEvent(
new CustomEvent(EVENT_THEME_CHANGE, {
bubbles: true,
composed: true,
detail: UiThemeEnum.Automatic,
}),
);
}
async getTheme(): Promise<UiThemeEnum> {
return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic;
}
render(): TemplateResult {
return html`
<ak-locale-context>
<rapi-doc
spec-url=${ifDefined(this.schemaPath)}
heading-text=""
theme="light"
render-style="read"
default-schema-tab="schema"
primary-color="#fd4b2d"
nav-bg-color="#212427"
bg-color=${this.bgColor}
text-color=${this.textColor}
nav-text-color="#ffffff"
nav-hover-bg-color="#3c3f42"
nav-accent-color="#4f5255"
nav-hover-text-color="#ffffff"
use-path-in-nav-bar="true"
nav-item-spacing="relaxed"
allow-server-selection="false"
show-header="false"
allow-spec-url-load="false"
allow-spec-file-load="false"
show-method-in-nav-bar="as-colored-text"
@before-try=${(
e: CustomEvent<{
request: {
headers: Headers;
};
}>,
) => {
e.detail.request.headers.append(
CSRFHeaderName,
getCookie("authentik_csrf"),
);
}}
>
<div slot="nav-logo">
<img
alt="authentik Logo"
class="logo"
src="${first(this.brand?.brandingLogo, DefaultBrand.brandingLogo)}"
/>
</div>
</rapi-doc>
</ak-locale-context>
`;
}
}