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.
This commit is contained in:
parent
c9dc500a2b
commit
d555c0db41
|
@ -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
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -10,6 +10,10 @@ 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 { rootInterface } from "@goauthentik/elements/Base";
|
||||||
|
import {
|
||||||
|
CapabilitiesEnum,
|
||||||
|
WithCapabilitiesConfig,
|
||||||
|
} from "@goauthentik/elements/Interface/capabilitiesProvider";
|
||||||
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 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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -12,6 +12,10 @@ 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 { 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 +37,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 +90,7 @@ const recoveryButtonStyles = css`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@customElement("ak-user-list")
|
@customElement("ak-user-list")
|
||||||
export class UserListPage extends TablePage<User> {
|
export class UserListPage extends WithCapabilitiesConfig(TablePage<User>) {
|
||||||
expandable = true;
|
expandable = true;
|
||||||
checkbox = true;
|
checkbox = true;
|
||||||
|
|
||||||
|
@ -244,8 +241,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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
67
web/src/elements/Interface/Interface.ts
Normal file
67
web/src/elements/Interface/Interface.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { config, tenant } from "@goauthentik/common/api/config";
|
||||||
|
import { UIConfig, uiConfig } from "@goauthentik/common/ui/config";
|
||||||
|
import { authentikConfigContext } 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()
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
20
web/src/elements/Interface/authentikConfigProvider.ts
Normal file
20
web/src/elements/Interface/authentikConfigProvider.ts
Normal 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,
|
||||||
|
) {
|
||||||
|
class WithAkConfigProvider extends superclass {
|
||||||
|
@consume({ context: authentikConfigContext, subscribe })
|
||||||
|
public authentikConfig!: Config;
|
||||||
|
}
|
||||||
|
return WithAkConfigProvider;
|
||||||
|
}
|
69
web/src/elements/Interface/capabilitiesProvider.ts
Normal file
69
web/src/elements/Interface/capabilitiesProvider.ts
Normal 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 };
|
4
web/src/elements/Interface/index.ts
Normal file
4
web/src/elements/Interface/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { Interface } from "./Interface";
|
||||||
|
|
||||||
|
export { Interface };
|
||||||
|
export default Interface;
|
3
web/src/elements/types.ts
Normal file
3
web/src/elements/types.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export interface AdoptedStyleSheetsElement {
|
||||||
|
adoptedStyleSheets: readonly CSSStyleSheet[];
|
||||||
|
}
|
4
web/src/elements/utils/ensureCSSStyleSheet.ts
Normal file
4
web/src/elements/utils/ensureCSSStyleSheet.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { CSSResult } from "lit";
|
||||||
|
|
||||||
|
export const ensureCSSStyleSheet = (css: CSSStyleSheet | CSSResult): CSSStyleSheet =>
|
||||||
|
css instanceof CSSResult ? css.styleSheet! : css;
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -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";
|
||||||
|
|
Reference in a new issue