Merge branch 'main' into web/monorepo-stage-1

* main:
  web: Replace  calls to `rootInterface()?.tenant?` with a contextual `this.tenant` object (#7778)
  web: abstract `rootInterface()?.config?.capabilities.includes()` into `.can()` (#7737)
This commit is contained in:
Ken Sternberg 2024-01-08 15:44:50 -08:00
commit b053c17fb0
36 changed files with 417 additions and 186 deletions

View File

@ -3,6 +3,92 @@
This is the default UI for the authentik server. The documentation is going to be a little sparse This is the default UI for the authentik server. The documentation is going to be a little sparse
for awhile, but at least let's get started. for awhile, but at least let's get started.
# The Theory of the authentik UI
In Peter Naur's 1985 essay [Programming as Theory
Building](https://pages.cs.wisc.edu/~remzi/Naur.pdf), programming is described as creating a mental
model of how a program _should_ run, then writing the code to test if the program _can_ run that
way.
The mental model for the authentik UI is straightforward. There are five "applications" within the
UI, each with its own base URL, router, and responsibilities, and each application needs as many as
three contexts in which to run.
The three contexts corresponds to objects in the API's `model` section, so let's use those names.
- The root `Config`. The root configuration object of the server, containing mostly caching and
error reporting information. This is misleading, however; the `Config` object contains some user
information, specifically a list of permissions the current user (or "no user") has.
- The root `CurrentTenant`. This describes the `Brand` information UIs should use, such as themes,
logos, favicon, and specific default flows for logging in, logging out, and recovering a user
password.
- The current `SessionUser`, the person logged in: username, display name, and various states.
(Note: the authentik server permits administrators to "impersonate" any other user in order to
debug their authentikation experience. If impersonation is active, the `user` field reflects that
user, but it also includes a field, `original`, with the administrator's information.)
(There is a fourth context object, Version, but its use is limited to displaying version information
and checking for upgrades. Just be aware that you will see it, but you will probably never interact
with it.)
There are five applications. Two (`loading` and `api-browser`) are trivial applications whose
insides are provided by third-party libraries (Patternfly and Rapidoc, respectively). The other
three are actual applications. The descriptions below are wholly from the view of the user's
experience:
- `Flow`: From a given URL, displays a form that requests information from the user to accomplish a
task. Some tasks require the user to be logged in, but many (such as logging in itself!)
obviously do not.
- `User`: Provides the user with access to the applications they can access, plus a few user
settings.
- `Admin`: Provides someone with super-user permissions access to the administrative functions of
the authentik server.
**Mental Model**
- Upon initialization, _every_ authentik UI application fetches `Config` and `CurrentTenant`. `User`
and `Admin` will also attempt to load the `SessionUser`; if there is none, the user is kicked out
to the `Flow` for logging into authentik itself.
- `Config`, `CurrentTenant`, and `SessionUser`, are provided by the `@goauthentik/api` application,
not by the codebase under `./web`. (Where you are now).
- `Flow`, `User`, and `Admin` are all called `Interfaces` and are found in
`./web/src/flow/FlowInterface`, `./web/src/user/UserInterface`, `./web/src/admin/AdminInterface`,
respectively.
Inside each of these you will find, in a hierarchal order:
- The context layer described above
- A theme managing layer
- The orchestration layer:
- web socket handler for server-generated events
- The router
- Individual routes for each vertical slice and its relationship to other objects:
Each slice corresponds to an object table on the server, and each slice _usually_ consists of the
following:
- A paginated collection display, usually using the `Table` foundation (found in
`./web/src/elements/Table`)
- The ability to view an individual object from the collection, which you may be able to:
- Edit
- Delete
- A form for creating a new object
- Tabs showing that object's relationship to other objects
- Interactive elements for changing or deleting those relationships, or creating new ones.
- The ability to create new objects with which to have that relationship, if they're not part of
the core objects (such as User->MFA authenticator apps, since the latter is not a "core" object
and has no tab of its own).
We are still a bit "all over the place" with respect to sub-units and common units; there are
folders `common`, `elements`, and `components`, and ideally they would be:
- `common`: non-UI related libraries all of our applications need
- `elements`: UI elements shared among multiple applications that do not need context
- `components`: UI elements shared among multiple that use one or more context
... but at the moment there are some context-sensitive elements, and some UI-related stuff in
`common`.
# Comments # Comments
**NOTE:** The comments in this section are for specific changes to this repository that cannot be **NOTE:** The comments in this section are for specific changes to this repository that cannot be

View File

@ -7,7 +7,7 @@ import {
import { configureSentry } from "@goauthentik/common/sentry"; import { configureSentry } from "@goauthentik/common/sentry";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { WebsocketClient } from "@goauthentik/common/ws"; import { WebsocketClient } from "@goauthentik/common/ws";
import { Interface } from "@goauthentik/elements/Base"; import { Interface } from "@goauthentik/elements/Interface";
import "@goauthentik/elements/ak-locale-context"; import "@goauthentik/elements/ak-locale-context";
import "@goauthentik/elements/enterprise/EnterpriseStatusBanner"; import "@goauthentik/elements/enterprise/EnterpriseStatusBanner";
import "@goauthentik/elements/messages/MessageContainer"; import "@goauthentik/elements/messages/MessageContainer";

View File

@ -1,23 +1,25 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_SIDEBAR_TOGGLE, VERSION } from "@goauthentik/common/constants"; import { EVENT_SIDEBAR_TOGGLE, VERSION } from "@goauthentik/common/constants";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route"; import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route";
import { getRootStyle } from "@goauthentik/elements/utils/getRootStyle"; import { getRootStyle } from "@goauthentik/elements/utils/getRootStyle";
import { spread } from "@open-wc/lit-helpers"; import { spread } from "@open-wc/lit-helpers";
import { consume } from "@lit-labs/context";
import { msg, str } from "@lit/localize"; import { msg, str } from "@lit/localize";
import { TemplateResult, html, nothing } from "lit"; import { TemplateResult, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import { map } from "lit/directives/map.js"; import { map } from "lit/directives/map.js";
import { AdminApi, CapabilitiesEnum, CoreApi, UiThemeEnum, Version } from "@goauthentik/api"; import { AdminApi, CoreApi, UiThemeEnum, Version } from "@goauthentik/api";
import type { Config, SessionUser, UserSelf } from "@goauthentik/api"; import type { SessionUser, UserSelf } from "@goauthentik/api";
@customElement("ak-admin-sidebar") @customElement("ak-admin-sidebar")
export class AkAdminSidebar extends AKElement { export class AkAdminSidebar extends WithCapabilitiesConfig(AKElement) {
@property({ type: Boolean, reflect: true }) @property({ type: Boolean, reflect: true })
open = true; open = true;
@ -27,9 +29,6 @@ export class AkAdminSidebar extends AKElement {
@state() @state()
impersonation: UserSelf["username"] | null = null; impersonation: UserSelf["username"] | null = null;
@consume({ context: authentikConfigContext })
public config!: Config;
constructor() { constructor() {
super(); super();
new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve().then((version) => { new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve().then((version) => {
@ -200,7 +199,7 @@ export class AkAdminSidebar extends AKElement {
} }
renderEnterpriseMessage() { renderEnterpriseMessage() {
return this.config?.capabilities.includes(CapabilitiesEnum.IsEnterprise) return this.can(CapabilitiesEnum.IsEnterprise)
? html` ? html`
<ak-sidebar-item> <ak-sidebar-item>
<span slot="label">${msg("Enterprise")}</span> <span slot="label">${msg("Enterprise")}</span>

View File

@ -74,10 +74,7 @@ export class AdminOverviewPage extends AKElement {
} }
render(): TemplateResult { render(): TemplateResult {
let name = this.user?.user.username; const name = this.user?.user.name ?? this.user?.user.username;
if (this.user?.user.name) {
name = this.user.user.name;
}
return html`<ak-page-header icon="" header="" description=${msg("General system status")}> return html`<ak-page-header icon="" header="" description=${msg("General system status")}>
<span slot="header"> ${msg(str`Welcome, ${name}.`)} </span> <span slot="header"> ${msg(str`Welcome, ${name}.`)} </span>
</ak-page-header> </ak-page-header>

View File

@ -1,13 +1,16 @@
import "@goauthentik/admin/applications/ProviderSelectModal"; import "@goauthentik/admin/applications/ProviderSelectModal";
import { iconHelperText } from "@goauthentik/admin/helperText"; import { iconHelperText } from "@goauthentik/admin/helperText";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-file-input"; import "@goauthentik/components/ak-file-input";
import "@goauthentik/components/ak-radio-input"; import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input"; import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input"; import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input"; import "@goauthentik/components/ak-textarea-input";
import { rootInterface } from "@goauthentik/elements/Base"; import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/forms/ModalForm";
@ -22,13 +25,7 @@ import { TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { import { Application, CoreApi, PolicyEngineMode, Provider } from "@goauthentik/api";
Application,
CapabilitiesEnum,
CoreApi,
PolicyEngineMode,
Provider,
} from "@goauthentik/api";
import "./components/ak-backchannel-input"; import "./components/ak-backchannel-input";
import "./components/ak-provider-search-input"; import "./components/ak-provider-search-input";
@ -48,7 +45,7 @@ export const policyOptions = [
]; ];
@customElement("ak-application-form") @customElement("ak-application-form")
export class ApplicationForm extends ModelForm<Application, string> { export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Application, string>) {
constructor() { constructor() {
super(); super();
this.handleConfirmBackchannelProviders = this.handleConfirmBackchannelProviders.bind(this); this.handleConfirmBackchannelProviders = this.handleConfirmBackchannelProviders.bind(this);
@ -93,8 +90,7 @@ export class ApplicationForm extends ModelForm<Application, string> {
applicationRequest: data, applicationRequest: data,
}); });
} }
const c = await config(); if (this.can(CapabilitiesEnum.CanSaveMedia)) {
if (c.capabilities.includes(CapabilitiesEnum.CanSaveMedia)) {
const icon = this.getFormFiles()["metaIcon"]; const icon = this.getFormFiles()["metaIcon"];
if (icon || this.clearIcon) { if (icon || this.clearIcon) {
await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconCreate({ await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconCreate({
@ -140,21 +136,21 @@ export class ApplicationForm extends ModelForm<Application, string> {
return html`<form class="pf-c-form pf-m-horizontal"> return html`<form class="pf-c-form pf-m-horizontal">
<ak-text-input <ak-text-input
name="name" name="name"
.value=${this.instance?.name} value=${ifDefined(this.instance?.name)}
label=${msg("Name")} label=${msg("Name")}
required required
help=${msg("Application's display Name.")} help=${msg("Application's display Name.")}
></ak-text-input> ></ak-text-input>
<ak-text-input <ak-text-input
name="slug" name="slug"
.value=${this.instance?.slug} value=${ifDefined(this.instance?.slug)}
label=${msg("Slug")} label=${msg("Slug")}
required required
help=${msg("Internal application name used in URLs.")} help=${msg("Internal application name used in URLs.")}
></ak-text-input> ></ak-text-input>
<ak-text-input <ak-text-input
name="group" name="group"
.value=${this.instance?.group} value=${ifDefined(this.instance?.group)}
label=${msg("Group")} label=${msg("Group")}
help=${msg( help=${msg(
"Optionally enter a group name. Applications with identical groups are shown grouped together.", "Optionally enter a group name. Applications with identical groups are shown grouped together.",
@ -163,7 +159,7 @@ export class ApplicationForm extends ModelForm<Application, string> {
<ak-provider-search-input <ak-provider-search-input
name="provider" name="provider"
label=${msg("Provider")} label=${msg("Provider")}
.value=${this.instance?.provider} value=${ifDefined(this.instance?.provider ?? undefined)}
help=${msg("Select a provider that this application should use.")} help=${msg("Select a provider that this application should use.")}
blankable blankable
></ak-provider-search-input> ></ak-provider-search-input>
@ -209,11 +205,11 @@ export class ApplicationForm extends ModelForm<Application, string> {
)} )}
> >
</ak-switch-input> </ak-switch-input>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanSaveMedia) ${this.can(CapabilitiesEnum.CanSaveMedia)
? html`<ak-file-input ? html`<ak-file-input
label="${msg("Icon")}" label="${msg("Icon")}"
name="metaIcon" name="metaIcon"
.value=${this.instance?.metaIcon} value=${ifDefined(this.instance?.metaIcon ?? undefined)}
current=${msg("Currently set to:")} current=${msg("Currently set to:")}
></ak-file-input> ></ak-file-input>
${this.instance?.metaIcon ${this.instance?.metaIcon

View File

@ -7,7 +7,7 @@ import "@goauthentik/components/ak-number-input";
import "@goauthentik/components/ak-radio-input"; import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input"; import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input"; import "@goauthentik/components/ak-text-input";
import { rootInterface } from "@goauthentik/elements/Base"; import { WithTenantConfig } from "@goauthentik/elements/Interface/tenantProvider";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
@ -32,7 +32,7 @@ import {
} from "./LDAPOptionsAndHelp"; } from "./LDAPOptionsAndHelp";
@customElement("ak-application-wizard-authentication-by-ldap") @customElement("ak-application-wizard-authentication-by-ldap")
export class ApplicationWizardApplicationDetails extends BaseProviderPanel { export class ApplicationWizardApplicationDetails extends WithTenantConfig(BaseProviderPanel) {
render() { render() {
const provider = this.wizard.provider as LDAPProvider | undefined; const provider = this.wizard.provider as LDAPProvider | undefined;
const errors = this.wizard.errors.provider; const errors = this.wizard.errors.provider;
@ -57,7 +57,7 @@ export class ApplicationWizardApplicationDetails extends BaseProviderPanel {
<ak-tenanted-flow-search <ak-tenanted-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication} flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authorizationFlow} .currentFlow=${provider?.authorizationFlow}
.tenantFlow=${rootInterface()?.tenant?.flowAuthentication} .tenantFlow=${this.tenant.flowAuthentication}
required required
></ak-tenanted-flow-search> ></ak-tenanted-flow-search>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">

View File

@ -3,7 +3,7 @@ import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search"; import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils"; import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-text-input"; import "@goauthentik/components/ak-text-input";
import { rootInterface } from "@goauthentik/elements/Base"; import { WithTenantConfig } from "@goauthentik/elements/Interface/tenantProvider";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
@ -17,7 +17,7 @@ import { FlowsInstancesListDesignationEnum, RadiusProvider } from "@goauthentik/
import BaseProviderPanel from "../BaseProviderPanel"; import BaseProviderPanel from "../BaseProviderPanel";
@customElement("ak-application-wizard-authentication-by-radius") @customElement("ak-application-wizard-authentication-by-radius")
export class ApplicationWizardAuthenticationByRadius extends BaseProviderPanel { export class ApplicationWizardAuthenticationByRadius extends WithTenantConfig(BaseProviderPanel) {
render() { render() {
const provider = this.wizard.provider as RadiusProvider | undefined; const provider = this.wizard.provider as RadiusProvider | undefined;
const errors = this.wizard.errors.provider; const errors = this.wizard.errors.provider;
@ -42,7 +42,7 @@ export class ApplicationWizardAuthenticationByRadius extends BaseProviderPanel {
<ak-tenanted-flow-search <ak-tenanted-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication} flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authorizationFlow} .currentFlow=${provider?.authorizationFlow}
.tenantFlow=${rootInterface()?.tenant?.flowAuthentication} .tenantFlow=${this.tenant.flowAuthentication}
required required
></ak-tenanted-flow-search> ></ak-tenanted-flow-search>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">

View File

@ -1,8 +1,11 @@
import { DesignationToLabel, LayoutToLabel } from "@goauthentik/admin/flows/utils"; import { DesignationToLabel, LayoutToLabel } from "@goauthentik/admin/flows/utils";
import { AuthenticationEnum } from "@goauthentik/api/dist/models/AuthenticationEnum"; import { AuthenticationEnum } from "@goauthentik/api/dist/models/AuthenticationEnum";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base"; import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -14,7 +17,6 @@ import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { import {
CapabilitiesEnum,
DeniedActionEnum, DeniedActionEnum,
Flow, Flow,
FlowDesignationEnum, FlowDesignationEnum,
@ -24,7 +26,7 @@ import {
} from "@goauthentik/api"; } from "@goauthentik/api";
@customElement("ak-flow-form") @customElement("ak-flow-form")
export class FlowForm extends ModelForm<Flow, string> { export class FlowForm extends WithCapabilitiesConfig(ModelForm<Flow, string>) {
async loadInstance(pk: string): Promise<Flow> { async loadInstance(pk: string): Promise<Flow> {
const flow = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesRetrieve({ const flow = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesRetrieve({
slug: pk, slug: pk,
@ -54,8 +56,8 @@ export class FlowForm extends ModelForm<Flow, string> {
flowRequest: data, flowRequest: data,
}); });
} }
const c = await config();
if (c.capabilities.includes(CapabilitiesEnum.CanSaveMedia)) { if (this.can(CapabilitiesEnum.CanSaveMedia)) {
const icon = this.getFormFiles()["background"]; const icon = this.getFormFiles()["background"];
if (icon || this.clearBackground) { if (icon || this.clearBackground) {
await new FlowsApi(DEFAULT_CONFIG).flowsInstancesSetBackgroundCreate({ await new FlowsApi(DEFAULT_CONFIG).flowsInstancesSetBackgroundCreate({
@ -340,7 +342,7 @@ export class FlowForm extends ModelForm<Flow, string> {
</option> </option>
</select> </select>
</ak-form-element-horizontal> </ak-form-element-horizontal>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanSaveMedia) ${this.can(CapabilitiesEnum.CanSaveMedia)
? html`<ak-form-element-horizontal ? html`<ak-form-element-horizontal
label=${msg("Background")} label=${msg("Background")}
name="background" name="background"

View File

@ -118,7 +118,7 @@ export class GroupViewPage extends AKElement {
<div class="pf-c-description-list__text"> <div class="pf-c-description-list__text">
<ak-status-label <ak-status-label
type="warning" type="warning"
?good=${this.group.isSuperuser} ?good${this.group.isSuperuser}
></ak-status-label> ></ak-status-label>
</div> </div>
</dd> </dd>

View File

@ -9,7 +9,11 @@ import { MessageLevel } from "@goauthentik/common/messages";
import { uiConfig } from "@goauthentik/common/ui/config"; import { uiConfig } from "@goauthentik/common/ui/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-status-label"; import "@goauthentik/components/ak-status-label";
import { rootInterface } from "@goauthentik/elements/Base"; import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import { WithTenantConfig } from "@goauthentik/elements/Interface/tenantProvider";
import "@goauthentik/elements/buttons/ActionButton"; import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/Dropdown"; import "@goauthentik/elements/buttons/Dropdown";
import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/DeleteBulkForm";
@ -33,7 +37,6 @@ import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import { import {
CapabilitiesEnum,
CoreApi, CoreApi,
CoreUsersListTypeEnum, CoreUsersListTypeEnum,
Group, Group,
@ -107,7 +110,7 @@ export class RelatedUserAdd extends Form<{ users: number[] }> {
} }
@customElement("ak-user-related-list") @customElement("ak-user-related-list")
export class RelatedUserList extends Table<User> { export class RelatedUserList extends WithTenantConfig(WithCapabilitiesConfig(Table<User>)) {
expandable = true; expandable = true;
checkbox = true; checkbox = true;
@ -188,8 +191,7 @@ export class RelatedUserList extends Table<User> {
row(item: User): TemplateResult[] { row(item: User): TemplateResult[] {
const canImpersonate = const canImpersonate =
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) && this.can(CapabilitiesEnum.CanImpersonate) && item.pk !== this.me?.user.pk;
item.pk !== this.me?.user.pk;
return [ return [
html`<a href="#/identity/users/${item.pk}"> html`<a href="#/identity/users/${item.pk}">
<div>${item.username}</div> <div>${item.username}</div>
@ -293,7 +295,7 @@ export class RelatedUserList extends Table<User> {
${msg("Set password")} ${msg("Set password")}
</button> </button>
</ak-forms-modal> </ak-forms-modal>
${rootInterface()?.tenant?.flowRecovery ${this.tenant?.flowRecovery
? html` ? html`
<ak-action-button <ak-action-button
class="pf-m-secondary" class="pf-m-secondary"

View File

@ -3,7 +3,7 @@ import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search";
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm"; import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base"; import { WithTenantConfig } from "@goauthentik/elements/Interface/tenantProvider";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio"; import "@goauthentik/elements/forms/Radio";
@ -25,7 +25,7 @@ import {
} from "@goauthentik/api"; } from "@goauthentik/api";
@customElement("ak-provider-ldap-form") @customElement("ak-provider-ldap-form")
export class LDAPProviderFormPage extends BaseProviderForm<LDAPProvider> { export class LDAPProviderFormPage extends WithTenantConfig(BaseProviderForm<LDAPProvider>) {
async loadInstance(pk: number): Promise<LDAPProvider> { async loadInstance(pk: number): Promise<LDAPProvider> {
return new ProvidersApi(DEFAULT_CONFIG).providersLdapRetrieve({ return new ProvidersApi(DEFAULT_CONFIG).providersLdapRetrieve({
id: pk, id: pk,
@ -68,7 +68,7 @@ export class LDAPProviderFormPage extends BaseProviderForm<LDAPProvider> {
<ak-tenanted-flow-search <ak-tenanted-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication} flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${this.instance?.authorizationFlow} .currentFlow=${this.instance?.authorizationFlow}
.tenantFlow=${rootInterface()?.tenant?.flowAuthentication} .tenantFlow=${this.tenant?.flowAuthentication}
required required
></ak-tenanted-flow-search> ></ak-tenanted-flow-search>
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p> <p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>

View File

@ -1,7 +1,7 @@
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm"; import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils"; import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base"; import { WithTenantConfig } from "@goauthentik/elements/Interface/tenantProvider";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect"; import "@goauthentik/elements/forms/SearchSelect";
@ -14,7 +14,7 @@ import { customElement } from "lit/decorators.js";
import { FlowsInstancesListDesignationEnum, ProvidersApi, RadiusProvider } from "@goauthentik/api"; import { FlowsInstancesListDesignationEnum, ProvidersApi, RadiusProvider } from "@goauthentik/api";
@customElement("ak-provider-radius-form") @customElement("ak-provider-radius-form")
export class RadiusProviderFormPage extends BaseProviderForm<RadiusProvider> { export class RadiusProviderFormPage extends WithTenantConfig(BaseProviderForm<RadiusProvider>) {
loadInstance(pk: number): Promise<RadiusProvider> { loadInstance(pk: number): Promise<RadiusProvider> {
return new ProvidersApi(DEFAULT_CONFIG).providersRadiusRetrieve({ return new ProvidersApi(DEFAULT_CONFIG).providersRadiusRetrieve({
id: pk, id: pk,
@ -57,7 +57,7 @@ export class RadiusProviderFormPage extends BaseProviderForm<RadiusProvider> {
<ak-tenanted-flow-search <ak-tenanted-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication} flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${this.instance?.authorizationFlow} .currentFlow=${this.instance?.authorizationFlow}
.tenantFlow=${rootInterface()?.tenant?.flowAuthentication} .tenantFlow=${this.tenant?.flowAuthentication}
required required
></ak-tenanted-flow-search> ></ak-tenanted-flow-search>
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p> <p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>

View File

@ -4,9 +4,12 @@ import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils"; import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror"; import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror"; import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect"; import "@goauthentik/elements/forms/SearchSelect";
@ -17,7 +20,6 @@ import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { import {
CapabilitiesEnum,
FlowsInstancesListDesignationEnum, FlowsInstancesListDesignationEnum,
OAuthSource, OAuthSource,
OAuthSourceRequest, OAuthSourceRequest,
@ -28,7 +30,7 @@ import {
} from "@goauthentik/api"; } from "@goauthentik/api";
@customElement("ak-source-oauth-form") @customElement("ak-source-oauth-form")
export class OAuthSourceForm extends BaseSourceForm<OAuthSource> { export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuthSource>) {
async loadInstance(pk: string): Promise<OAuthSource> { async loadInstance(pk: string): Promise<OAuthSource> {
const source = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthRetrieve({ const source = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthRetrieve({
slug: pk, slug: pk,
@ -318,7 +320,7 @@ export class OAuthSourceForm extends BaseSourceForm<OAuthSource> {
/> />
<p class="pf-c-form__helper-text">${placeholderHelperText}</p> <p class="pf-c-form__helper-text">${placeholderHelperText}</p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanSaveMedia) ${this.can(CapabilitiesEnum.CanSaveMedia)
? html`<ak-form-element-horizontal label=${msg("Icon")} name="icon"> ? html`<ak-form-element-horizontal label=${msg("Icon")} name="icon">
<input type="file" value="" class="pf-c-form-control" /> <input type="file" value="" class="pf-c-form-control" />
${this.instance?.icon ${this.instance?.icon

View File

@ -2,10 +2,13 @@ import "@goauthentik/admin/common/ak-flow-search/ak-source-flow-search";
import { iconHelperText, placeholderHelperText } from "@goauthentik/admin/helperText"; import { iconHelperText, placeholderHelperText } from "@goauthentik/admin/helperText";
import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm"; import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils"; import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PlexAPIClient, PlexResource, popupCenterScreen } from "@goauthentik/common/helpers/plex"; import { PlexAPIClient, PlexResource, popupCenterScreen } from "@goauthentik/common/helpers/plex";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils"; import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base"; import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect"; import "@goauthentik/elements/forms/SearchSelect";
@ -16,7 +19,6 @@ import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { import {
CapabilitiesEnum,
FlowsInstancesListDesignationEnum, FlowsInstancesListDesignationEnum,
PlexSource, PlexSource,
SourcesApi, SourcesApi,
@ -24,7 +26,7 @@ import {
} from "@goauthentik/api"; } from "@goauthentik/api";
@customElement("ak-source-plex-form") @customElement("ak-source-plex-form")
export class PlexSourceForm extends BaseSourceForm<PlexSource> { export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSource>) {
async loadInstance(pk: string): Promise<PlexSource> { async loadInstance(pk: string): Promise<PlexSource> {
const source = await new SourcesApi(DEFAULT_CONFIG).sourcesPlexRetrieve({ const source = await new SourcesApi(DEFAULT_CONFIG).sourcesPlexRetrieve({
slug: pk, slug: pk,
@ -63,8 +65,7 @@ export class PlexSourceForm extends BaseSourceForm<PlexSource> {
plexSourceRequest: data, plexSourceRequest: data,
}); });
} }
const c = await config(); if (this.can(CapabilitiesEnum.CanSaveMedia)) {
if (c.capabilities.includes(CapabilitiesEnum.CanSaveMedia)) {
const icon = this.getFormFiles()["icon"]; const icon = this.getFormFiles()["icon"];
if (icon || this.clearIcon) { if (icon || this.clearIcon) {
await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconCreate({ await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconCreate({
@ -255,7 +256,7 @@ export class PlexSourceForm extends BaseSourceForm<PlexSource> {
/> />
<p class="pf-c-form__helper-text">${placeholderHelperText}</p> <p class="pf-c-form__helper-text">${placeholderHelperText}</p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanSaveMedia) ${this.can(CapabilitiesEnum.CanSaveMedia)
? html`<ak-form-element-horizontal label=${msg("Icon")} name="icon"> ? html`<ak-form-element-horizontal label=${msg("Icon")} name="icon">
<input type="file" value="" class="pf-c-form-control" /> <input type="file" value="" class="pf-c-form-control" />
${this.instance?.icon ${this.instance?.icon

View File

@ -5,7 +5,10 @@ import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils"; import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base"; import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio"; import "@goauthentik/elements/forms/Radio";
@ -18,7 +21,6 @@ import { ifDefined } from "lit/directives/if-defined.js";
import { import {
BindingTypeEnum, BindingTypeEnum,
CapabilitiesEnum,
DigestAlgorithmEnum, DigestAlgorithmEnum,
FlowsInstancesListDesignationEnum, FlowsInstancesListDesignationEnum,
NameIdPolicyEnum, NameIdPolicyEnum,
@ -29,7 +31,7 @@ import {
} from "@goauthentik/api"; } from "@goauthentik/api";
@customElement("ak-source-saml-form") @customElement("ak-source-saml-form")
export class SAMLSourceForm extends BaseSourceForm<SAMLSource> { export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSource>) {
@state() @state()
clearIcon = false; clearIcon = false;
@ -149,7 +151,7 @@ export class SAMLSourceForm extends BaseSourceForm<SAMLSource> {
</option> </option>
</select> </select>
</ak-form-element-horizontal> </ak-form-element-horizontal>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanSaveMedia) ${this.can(CapabilitiesEnum.CanSaveMedia)
? html`<ak-form-element-horizontal label=${msg("Icon")} name="icon"> ? html`<ak-form-element-horizontal label=${msg("Icon")} name="icon">
<input type="file" value="" class="pf-c-form-control" /> <input type="file" value="" class="pf-c-form-control" />
${this.instance?.icon ${this.instance?.icon

View File

@ -12,6 +12,11 @@ import { DefaultUIConfig, uiConfig } from "@goauthentik/common/ui/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-status-label"; import "@goauthentik/components/ak-status-label";
import { rootInterface } from "@goauthentik/elements/Base"; import { rootInterface } from "@goauthentik/elements/Base";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import { WithTenantConfig } from "@goauthentik/elements/Interface/tenantProvider";
import { PFSize } from "@goauthentik/elements/Spinner"; import { PFSize } from "@goauthentik/elements/Spinner";
import "@goauthentik/elements/TreeView"; import "@goauthentik/elements/TreeView";
import "@goauthentik/elements/buttons/ActionButton"; import "@goauthentik/elements/buttons/ActionButton";
@ -33,14 +38,7 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css"; import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import { import { CoreApi, ResponseError, SessionUser, User, UserPath } from "@goauthentik/api";
CapabilitiesEnum,
CoreApi,
ResponseError,
SessionUser,
User,
UserPath,
} from "@goauthentik/api";
export const requestRecoveryLink = (user: User) => export const requestRecoveryLink = (user: User) =>
new CoreApi(DEFAULT_CONFIG) new CoreApi(DEFAULT_CONFIG)
@ -93,7 +91,7 @@ const recoveryButtonStyles = css`
`; `;
@customElement("ak-user-list") @customElement("ak-user-list")
export class UserListPage extends TablePage<User> { export class UserListPage extends WithTenantConfig(WithCapabilitiesConfig(TablePage<User>)) {
expandable = true; expandable = true;
checkbox = true; checkbox = true;
@ -244,8 +242,7 @@ export class UserListPage extends TablePage<User> {
row(item: User): TemplateResult[] { row(item: User): TemplateResult[] {
const canImpersonate = const canImpersonate =
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) && this.can(CapabilitiesEnum.CanImpersonate) && item.pk !== this.me?.user.pk;
item.pk !== this.me?.user.pk;
return [ return [
html`<a href="#/identity/users/${item.pk}"> html`<a href="#/identity/users/${item.pk}">
<div>${item.username}</div> <div>${item.username}</div>
@ -355,7 +352,7 @@ export class UserListPage extends TablePage<User> {
${msg("Set password")} ${msg("Set password")}
</button> </button>
</ak-forms-modal> </ak-forms-modal>
${rootInterface()?.tenant?.flowRecovery ${this.tenant.flowRecovery
? html` ? html`
<ak-action-button <ak-action-button
class="pf-m-secondary" class="pf-m-secondary"

View File

@ -22,8 +22,9 @@ import {
import "@goauthentik/components/ak-status-label"; import "@goauthentik/components/ak-status-label";
import "@goauthentik/components/events/ObjectChangelog"; import "@goauthentik/components/events/ObjectChangelog";
import "@goauthentik/components/events/UserEvents"; import "@goauthentik/components/events/UserEvents";
import { AKElement, rootInterface } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror"; import "@goauthentik/elements/CodeMirror";
import { WithCapabilitiesConfig } from "@goauthentik/elements/Interface/capabilitiesProvider";
import "@goauthentik/elements/PageHeader"; import "@goauthentik/elements/PageHeader";
import { PFSize } from "@goauthentik/elements/Spinner"; import { PFSize } from "@goauthentik/elements/Spinner";
import "@goauthentik/elements/Tabs"; import "@goauthentik/elements/Tabs";
@ -60,7 +61,7 @@ import {
import "./UserDevicesTable"; import "./UserDevicesTable";
@customElement("ak-user-view") @customElement("ak-user-view")
export class UserViewPage extends AKElement { export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
@property({ type: Number }) @property({ type: Number })
set userId(id: number) { set userId(id: number) {
me().then((me) => { me().then((me) => {
@ -163,8 +164,7 @@ export class UserViewPage extends AKElement {
renderActionButtons(user: User) { renderActionButtons(user: User) {
const canImpersonate = const canImpersonate =
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) && this.can(CapabilitiesEnum.CanImpersonate) && user.pk !== this.me?.user.pk;
user.pk !== this.me?.user.pk;
return html`<div class="ak-button-collection"> return html`<div class="ak-button-collection">
<ak-forms-modal> <ak-forms-modal>

View File

@ -1,7 +1,11 @@
import { createContext } from "@lit-labs/context"; import { createContext } from "@lit-labs/context";
import { type Config } from "@goauthentik/api"; import type { Config, CurrentTenant } from "@goauthentik/api";
export const authentikConfigContext = createContext<Config>(Symbol("authentik-config-context")); export const authentikConfigContext = createContext<Config>(Symbol("authentik-config-context"));
export const authentikTenantContext = createContext<CurrentTenant>(
Symbol("authentik-tenant-context"),
);
export default authentikConfigContext; export default authentikConfigContext;

View File

@ -1,20 +1,18 @@
import { config, tenant } from "@goauthentik/common/api/config";
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants"; import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
import { UIConfig, uiConfig } from "@goauthentik/common/ui/config"; import { UIConfig } from "@goauthentik/common/ui/config";
import { adaptCSS } from "@goauthentik/common/utils"; import { adaptCSS } from "@goauthentik/common/utils";
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts"; import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
import { ContextProvider } from "@lit-labs/context";
import { localized } from "@lit/localize"; import { localized } from "@lit/localize";
import { CSSResult, LitElement } from "lit"; import { LitElement } from "lit";
import { state } from "lit/decorators.js";
import AKGlobal from "@goauthentik/common/styles/authentik.css"; import AKGlobal from "@goauthentik/common/styles/authentik.css";
import ThemeDark from "@goauthentik/common/styles/theme-dark.css"; import ThemeDark from "@goauthentik/common/styles/theme-dark.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { Config, CurrentTenant, UiThemeEnum } from "@goauthentik/api"; import { Config, CurrentTenant, UiThemeEnum } from "@goauthentik/api";
import { AdoptedStyleSheetsElement } from "./types";
type AkInterface = HTMLElement & { type AkInterface = HTMLElement & {
getTheme: () => Promise<UiThemeEnum>; getTheme: () => Promise<UiThemeEnum>;
tenant?: CurrentTenant; tenant?: CurrentTenant;
@ -25,13 +23,6 @@ type AkInterface = HTMLElement & {
export const rootInterface = <T extends AkInterface>(): T | undefined => export const rootInterface = <T extends AkInterface>(): T | undefined =>
(document.body.querySelector("[data-ak-interface-root]") as T) ?? undefined; (document.body.querySelector("[data-ak-interface-root]") as T) ?? undefined;
export function ensureCSSStyleSheet(css: CSSStyleSheet | CSSResult): CSSStyleSheet {
if (css instanceof CSSResult) {
return css.styleSheet!;
}
return css;
}
let css: Promise<string[]> | undefined; let css: Promise<string[]> | undefined;
function fetchCustomCSS(): Promise<string[]> { function fetchCustomCSS(): Promise<string[]> {
if (!css) { if (!css) {
@ -52,10 +43,6 @@ function fetchCustomCSS(): Promise<string[]> {
return css; return css;
} }
export interface AdoptedStyleSheetsElement {
adoptedStyleSheets: readonly CSSStyleSheet[];
}
const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)"; const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)";
@localized() @localized()
@ -175,49 +162,3 @@ export class AKElement extends LitElement {
this.requestUpdate(); this.requestUpdate();
} }
} }
export class Interface extends AKElement implements AkInterface {
@state()
tenant?: CurrentTenant;
@state()
uiConfig?: UIConfig;
_configContext = new ContextProvider(this, {
context: authentikConfigContext,
initialValue: undefined,
});
_config?: Config;
@state()
set config(c: Config) {
this._config = c;
this._configContext.setValue(c);
this.requestUpdate();
}
get config(): Config | undefined {
return this._config;
}
constructor() {
super();
document.adoptedStyleSheets = [...document.adoptedStyleSheets, ensureCSSStyleSheet(PFBase)];
tenant().then((tenant) => (this.tenant = tenant));
config().then((config) => (this.config = config));
this.dataset.akInterfaceRoot = "true";
}
_activateTheme(root: AdoptedStyleSheetsElement, theme: UiThemeEnum): void {
super._activateTheme(root, theme);
super._activateTheme(document, theme);
}
async getTheme(): Promise<UiThemeEnum> {
if (!this.uiConfig) {
this.uiConfig = await uiConfig();
}
return this.uiConfig.theme?.base || UiThemeEnum.Automatic;
}
}

View File

@ -0,0 +1,85 @@
import { config, tenant } from "@goauthentik/common/api/config";
import { UIConfig, uiConfig } from "@goauthentik/common/ui/config";
import {
authentikConfigContext,
authentikTenantContext,
} from "@goauthentik/elements/AuthentikContexts";
import type { AdoptedStyleSheetsElement } from "@goauthentik/elements/types";
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
import { ContextProvider } from "@lit-labs/context";
import { state } from "lit/decorators.js";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { Config, CurrentTenant, UiThemeEnum } from "@goauthentik/api";
import { AKElement } from "../Base";
type AkInterface = HTMLElement & {
getTheme: () => Promise<UiThemeEnum>;
tenant?: CurrentTenant;
uiConfig?: UIConfig;
config?: Config;
};
export class Interface extends AKElement implements AkInterface {
@state()
uiConfig?: UIConfig;
_configContext = new ContextProvider(this, {
context: authentikConfigContext,
initialValue: undefined,
});
_config?: Config;
@state()
set config(c: Config) {
this._config = c;
this._configContext.setValue(c);
this.requestUpdate();
}
get config(): Config | undefined {
return this._config;
}
_tenantContext = new ContextProvider(this, {
context: authentikTenantContext,
initialValue: undefined,
});
_tenant?: CurrentTenant;
@state()
set tenant(c: CurrentTenant) {
this._tenant = c;
this._tenantContext.setValue(c);
this.requestUpdate();
}
get tenant(): CurrentTenant | undefined {
return this._tenant;
}
constructor() {
super();
document.adoptedStyleSheets = [...document.adoptedStyleSheets, ensureCSSStyleSheet(PFBase)];
tenant().then((tenant) => (this.tenant = tenant));
config().then((config) => (this.config = config));
this.dataset.akInterfaceRoot = "true";
}
_activateTheme(root: AdoptedStyleSheetsElement, theme: UiThemeEnum): void {
super._activateTheme(root, theme);
super._activateTheme(document, theme);
}
async getTheme(): Promise<UiThemeEnum> {
if (!this.uiConfig) {
this.uiConfig = await uiConfig();
}
return this.uiConfig.theme?.base || UiThemeEnum.Automatic;
}
}

View File

@ -0,0 +1,20 @@
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import { consume } from "@lit-labs/context";
import type { LitElement } from "lit";
import type { Config } from "@goauthentik/api";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<T = object> = new (...args: any[]) => T;
export function WithAuthentikConfig<T extends Constructor<LitElement>>(
superclass: T,
subscribe = true,
) {
abstract class WithAkConfigProvider extends superclass {
@consume({ context: authentikConfigContext, subscribe })
public authentikConfig!: Config;
}
return WithAkConfigProvider;
}

View File

@ -0,0 +1,69 @@
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import { consume } from "@lit-labs/context";
import type { LitElement } from "lit";
import { CapabilitiesEnum } from "@goauthentik/api";
import { Config } from "@goauthentik/api";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<T = object> = abstract new (...args: any[]) => T;
// Using a unique, lexically scoped, and locally static symbol as the field name for the context
// means that it's inaccessible to any child class looking for it. It's one of the strongest privacy
// guarantees in JavaScript.
class WCC {
public static readonly capabilitiesConfig: unique symbol = Symbol();
}
/**
* withCapabilitiesContext mixes in a single method to any LitElement, `can()`, which takes a
* CapabilitiesEnum and returns true or false.
*
* Usage:
*
* After importing, simply mixin this function:
*
* ```
* export class AkMyNiftyNewFeature extends withCapabilitiesContext(AKElement) {
* ```
*
* And then if you need to check on a capability:
*
* ```
* if (this.can(CapabilitiesEnum.IsEnterprise) { ... }
* ```
*
* This code re-exports CapabilitiesEnum, so you won't have to import it on a separate line if you
* don't need anything else from the API.
*
* Passing `true` as the second mixin argument will cause the inheriting class to subscribe to the
* configuration context. Should the context be explicitly reset, all active web components that are
* currently active and subscribed to the context will automatically have a `requestUpdate()`
* triggered with the new configuration.
*
*/
export function WithCapabilitiesConfig<T extends Constructor<LitElement>>(
superclass: T,
subscribe = true,
) {
abstract class CapabilitiesContext extends superclass {
@consume({ context: authentikConfigContext, subscribe })
private [WCC.capabilitiesConfig]!: Config;
can(c: CapabilitiesEnum) {
if (!this[WCC.capabilitiesConfig]) {
throw new Error(
"ConfigContext: Attempted to access site configuration before initialization.",
);
}
return this[WCC.capabilitiesConfig].capabilities.includes(c);
}
}
return CapabilitiesContext;
}
export { CapabilitiesEnum };

View File

@ -0,0 +1,4 @@
import { Interface } from "./Interface";
export { Interface };
export default Interface;

View File

@ -0,0 +1,20 @@
import { authentikTenantContext } from "@goauthentik/elements/AuthentikContexts";
import { consume } from "@lit-labs/context";
import type { LitElement } from "lit";
import type { CurrentTenant } from "@goauthentik/api";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<T = object> = abstract new (...args: any[]) => T;
export function WithTenantConfig<T extends Constructor<LitElement>>(
superclass: T,
subscribe = true,
) {
abstract class WithTenantProvider extends superclass {
@consume({ context: authentikTenantContext, subscribe })
public tenant!: CurrentTenant;
}
return WithTenantProvider;
}

View File

@ -8,7 +8,8 @@ import {
} from "@goauthentik/common/constants"; } from "@goauthentik/common/constants";
import { currentInterface } from "@goauthentik/common/sentry"; import { currentInterface } from "@goauthentik/common/sentry";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { AKElement, rootInterface } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { WithTenantConfig } from "@goauthentik/elements/Interface/tenantProvider";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
@ -23,7 +24,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { EventsApi } from "@goauthentik/api"; import { EventsApi } from "@goauthentik/api";
@customElement("ak-page-header") @customElement("ak-page-header")
export class PageHeader extends AKElement { export class PageHeader extends WithTenantConfig(AKElement) {
@property() @property()
icon?: string; icon?: string;
@ -35,9 +36,8 @@ export class PageHeader extends AKElement {
@property() @property()
set header(value: string) { set header(value: string) {
const tenant = rootInterface()?.tenant;
const currentIf = currentInterface(); const currentIf = currentInterface();
let title = tenant?.brandingTitle || TITLE_DEFAULT; let title = this.tenant?.brandingTitle || TITLE_DEFAULT;
if (currentIf === "admin") { if (currentIf === "admin") {
title = `${msg("Admin")} - ${title}`; title = `${msg("Admin")} - ${title}`;
} }

View File

@ -1,6 +1,6 @@
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants"; import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
import { first } from "@goauthentik/common/utils"; import { AKElement } from "@goauthentik/elements/Base";
import { AKElement, rootInterface } from "@goauthentik/elements/Base"; import { WithTenantConfig } from "@goauthentik/elements/Interface/tenantProvider";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
@ -27,7 +27,7 @@ export const DefaultTenant: CurrentTenant = {
}; };
@customElement("ak-sidebar-brand") @customElement("ak-sidebar-brand")
export class SidebarBrand extends AKElement { export class SidebarBrand extends WithTenantConfig(AKElement) {
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
PFBase, PFBase,
@ -85,10 +85,7 @@ export class SidebarBrand extends AKElement {
<a href="#/" class="pf-c-page__header-brand-link"> <a href="#/" class="pf-c-page__header-brand-link">
<div class="pf-c-brand ak-brand"> <div class="pf-c-brand ak-brand">
<img <img
src="${first( src=${this.tenant?.brandingLogo ?? DefaultTenant.brandingLogo}
rootInterface()?.tenant?.brandingLogo,
DefaultTenant.brandingLogo,
)}"
alt="authentik Logo" alt="authentik Logo"
loading="lazy" loading="lazy"
/> />

View File

@ -0,0 +1,3 @@
export interface AdoptedStyleSheetsElement {
adoptedStyleSheets: readonly CSSStyleSheet[];
}

View File

@ -0,0 +1,4 @@
import { CSSResult } from "lit";
export const ensureCSSStyleSheet = (css: CSSStyleSheet | CSSResult): CSSStyleSheet =>
css instanceof CSSResult ? css.styleSheet! : css;

View File

@ -1,5 +1,5 @@
import { TITLE_DEFAULT } from "@goauthentik/app/common/constants"; import { TITLE_DEFAULT } from "@goauthentik/app/common/constants";
import { Interface } from "@goauthentik/elements/Base"; import { Interface } from "@goauthentik/elements/Interface";
import "@goauthentik/elements/LoadingOverlay"; import "@goauthentik/elements/LoadingOverlay";
import Guacamole from "guacamole-common-js"; import Guacamole from "guacamole-common-js";

View File

@ -8,7 +8,7 @@ import { globalAK } from "@goauthentik/common/global";
import { configureSentry } from "@goauthentik/common/sentry"; import { configureSentry } from "@goauthentik/common/sentry";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import { WebsocketClient } from "@goauthentik/common/ws"; import { WebsocketClient } from "@goauthentik/common/ws";
import { Interface } from "@goauthentik/elements/Base"; import { Interface } from "@goauthentik/elements/Interface";
import "@goauthentik/elements/LoadingOverlay"; import "@goauthentik/elements/LoadingOverlay";
import "@goauthentik/elements/ak-locale-context"; import "@goauthentik/elements/ak-locale-context";
import "@goauthentik/flow/sources/apple/AppleLoginInit"; import "@goauthentik/flow/sources/apple/AppleLoginInit";

View File

@ -1,6 +1,9 @@
import { rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/Divider"; import "@goauthentik/elements/Divider";
import "@goauthentik/elements/EmptyState"; import "@goauthentik/elements/EmptyState";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import { LOCALES } from "@goauthentik/elements/ak-locale-context/definitions"; import { LOCALES } from "@goauthentik/elements/ak-locale-context/definitions";
import "@goauthentik/elements/forms/FormElement"; import "@goauthentik/elements/forms/FormElement";
import { BaseStage } from "@goauthentik/flow/stages/base"; import { BaseStage } from "@goauthentik/flow/stages/base";
@ -20,7 +23,6 @@ import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { import {
CapabilitiesEnum,
PromptChallenge, PromptChallenge,
PromptChallengeResponseRequest, PromptChallengeResponseRequest,
PromptTypeEnum, PromptTypeEnum,
@ -28,7 +30,9 @@ import {
} from "@goauthentik/api"; } from "@goauthentik/api";
@customElement("ak-stage-prompt") @customElement("ak-stage-prompt")
export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeResponseRequest> { export class PromptStage extends WithCapabilitiesConfig(
BaseStage<PromptChallenge, PromptChallengeResponseRequest>,
) {
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
PFBase, PFBase,
@ -193,10 +197,7 @@ ${prompt.initialValue}</textarea
</div> `; </div> `;
})}`; })}`;
case PromptTypeEnum.AkLocale: { case PromptTypeEnum.AkLocale: {
const inDebug = rootInterface()?.config?.capabilities.includes( const locales = this.can(CapabilitiesEnum.CanDebug)
CapabilitiesEnum.CanDebug,
);
const locales = inDebug
? LOCALES ? LOCALES
: LOCALES.filter((locale) => locale.code !== "debug"); : LOCALES.filter((locale) => locale.code !== "debug");
const options = locales.map( const options = locales.map(

View File

@ -2,7 +2,7 @@ import { CSRFHeaderName } from "@goauthentik/common/api/middleware";
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants"; import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { first, getCookie } from "@goauthentik/common/utils"; import { first, getCookie } from "@goauthentik/common/utils";
import { Interface } from "@goauthentik/elements/Base"; import { Interface } from "@goauthentik/elements/Interface";
import "@goauthentik/elements/ak-locale-context"; import "@goauthentik/elements/ak-locale-context";
import { DefaultTenant } from "@goauthentik/elements/sidebar/SidebarBrand"; import { DefaultTenant } from "@goauthentik/elements/sidebar/SidebarBrand";
import "rapidoc"; import "rapidoc";

View File

@ -1,5 +1,5 @@
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { Interface } from "@goauthentik/elements/Base"; import { Interface } from "@goauthentik/elements/Interface";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";

View File

@ -1,4 +1,4 @@
import { Interface } from "@goauthentik/app/elements/Base"; import { Interface } from "@goauthentik/app/elements/Interface";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";

View File

@ -9,7 +9,7 @@ import { UserDisplay } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import { WebsocketClient } from "@goauthentik/common/ws"; import { WebsocketClient } from "@goauthentik/common/ws";
import { Interface } from "@goauthentik/elements/Base"; import { Interface } from "@goauthentik/elements/Interface";
import "@goauthentik/elements/ak-locale-context"; import "@goauthentik/elements/ak-locale-context";
import "@goauthentik/elements/buttons/ActionButton"; import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/enterprise/EnterpriseStatusBanner"; import "@goauthentik/elements/enterprise/EnterpriseStatusBanner";

View File

@ -2,14 +2,15 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { MessageLevel } from "@goauthentik/common/messages"; import { MessageLevel } from "@goauthentik/common/messages";
import { refreshMe } from "@goauthentik/common/users"; import { refreshMe } from "@goauthentik/common/users";
import { AKElement, rootInterface } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { WithTenantConfig } from "@goauthentik/elements/Interface/tenantProvider";
import { showMessage } from "@goauthentik/elements/messages/MessageContainer"; import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
import { StageHost } from "@goauthentik/flow/stages/base"; import { StageHost } from "@goauthentik/flow/stages/base";
import "@goauthentik/user/user-settings/details/stages/prompt/PromptStage"; import "@goauthentik/user/user-settings/details/stages/prompt/PromptStage";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit"; import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -21,7 +22,6 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { import {
ChallengeChoices, ChallengeChoices,
ChallengeTypes, ChallengeTypes,
CurrentTenant,
FlowChallengeResponseRequest, FlowChallengeResponseRequest,
FlowErrorChallenge, FlowErrorChallenge,
FlowsApi, FlowsApi,
@ -31,13 +31,13 @@ import {
} from "@goauthentik/api"; } from "@goauthentik/api";
@customElement("ak-user-settings-flow-executor") @customElement("ak-user-settings-flow-executor")
export class UserSettingsFlowExecutor extends AKElement implements StageHost { export class UserSettingsFlowExecutor
extends WithTenantConfig(AKElement, true)
implements StageHost
{
@property() @property()
flowSlug?: string; flowSlug?: string;
@state()
tenant?: CurrentTenant;
private _challenge?: ChallengeTypes; private _challenge?: ChallengeTypes;
@property({ attribute: false }) @property({ attribute: false })
@ -87,7 +87,6 @@ export class UserSettingsFlowExecutor extends AKElement implements StageHost {
} }
firstUpdated(): void { firstUpdated(): void {
this.tenant = rootInterface()?.tenant;
this.flowSlug = this.tenant?.flowUserSettings; this.flowSlug = this.tenant?.flowUserSettings;
if (!this.flowSlug) { if (!this.flowSlug) {
return; return;