web/elements: only render form once instance is loaded (#5049)

* web/elements: only render form once instance is loaded

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* use radio for transport

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* only wait for instance to be loaded if set

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add hook to load additional data in form

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make send an abstract function instead of attribute

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* ensure form is updated after data is loaded

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* remove until for select and multi-selects in forms

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* don't use until for file uploads

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* remove last until from form

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* remove deprecated import

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* prevent form double load, add error handling for PreventFormSubmit

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix double creation of inner element in proxy form

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make PreventFormSubmit work correctly

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L 2023-03-23 14:05:14 +01:00 committed by GitHub
parent 20522558fe
commit 14f0034a0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 900 additions and 995 deletions

View File

@ -1,5 +1,6 @@
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first, groupBy } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/ModalForm";
@ -13,7 +14,6 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
Application,
@ -195,19 +195,13 @@ export class ApplicationForm extends ModelForm<Application, string> {
${t`If checked, the launch URL will open in a new browser tab or window from the user's application library.`}
</p>
</ak-form-element-horizontal>
${until(
config().then((c) => {
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
return html`<ak-form-element-horizontal
label=${t`Icon`}
name="metaIcon"
>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.SaveMedia)
? html`<ak-form-element-horizontal label=${t`Icon`} name="metaIcon">
<input type="file" value="" class="pf-c-form-control" />
${this.instance?.metaIcon
? html`
<p class="pf-c-form__helper-text">
${t`Currently set to:`}
${this.instance?.metaIcon}
${t`Currently set to:`} ${this.instance?.metaIcon}
</p>
`
: html``}
@ -242,12 +236,8 @@ export class ApplicationForm extends ModelForm<Application, string> {
</p>
</ak-form-element-horizontal>
`
: html``}`;
}
return html`<ak-form-element-horizontal
label=${t`Icon`}
name="metaIcon"
>
: html``}`
: html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input
type="text"
value="${first(this.instance?.metaIcon, "")}"
@ -256,9 +246,7 @@ export class ApplicationForm extends ModelForm<Application, string> {
<p class="pf-c-form__helper-text">
${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
</p>
</ak-form-element-horizontal>`;
}),
)}
</ak-form-element-horizontal>`}
<ak-form-element-horizontal label=${t`Publisher`} name="metaPublisher">
<input
type="text"

View File

@ -9,7 +9,6 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
CoreApi,
@ -17,17 +16,26 @@ import {
EventsApi,
Group,
NotificationRule,
PaginatedNotificationTransportList,
SeverityEnum,
} from "@goauthentik/api";
@customElement("ak-event-rule-form")
export class RuleForm extends ModelForm<NotificationRule, string> {
eventTransports?: PaginatedNotificationTransportList;
loadInstance(pk: string): Promise<NotificationRule> {
return new EventsApi(DEFAULT_CONFIG).eventsRulesRetrieve({
pbmUuid: pk,
});
}
async load(): Promise<void> {
this.eventTransports = await new EventsApi(DEFAULT_CONFIG).eventsTransportsList({
ordering: "name",
});
}
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated rule.`;
@ -86,28 +94,14 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Transports`} ?required=${true} name="transports">
<select class="pf-c-form-control" multiple>
${until(
new EventsApi(DEFAULT_CONFIG)
.eventsTransportsList({
ordering: "name",
})
.then((transports) => {
return transports.results.map((transport) => {
const selected = Array.from(
this.instance?.transports || [],
).some((su) => {
${this.eventTransports?.results.map((transport) => {
const selected = Array.from(this.instance?.transports || []).some((su) => {
return su == transport.pk;
});
return html`<option
value=${ifDefined(transport.pk)}
?selected=${selected}
>
return html`<option value=${ifDefined(transport.pk)} ?selected=${selected}>
${transport.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
})}
</select>
<p class="pf-c-form__helper-text">
${t`Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI.`}
@ -120,7 +114,7 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
<ak-radio
.options=${[
{
label: "Alert",
label: t`Alert`,
value: SeverityEnum.Alert,
default: true,
},

View File

@ -2,6 +2,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import { t } from "@lingui/macro";
@ -56,35 +57,6 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
}
};
renderTransportModes(): TemplateResult {
return html`
<option
value=${NotificationTransportModeEnum.Local}
?selected=${this.instance?.mode === NotificationTransportModeEnum.Local}
>
${t`Local (notifications will be created within authentik)`}
</option>
<option
value=${NotificationTransportModeEnum.Email}
?selected=${this.instance?.mode === NotificationTransportModeEnum.Email}
>
${t`Email`}
</option>
<option
value=${NotificationTransportModeEnum.Webhook}
?selected=${this.instance?.mode === NotificationTransportModeEnum.Webhook}
>
${t`Webhook (generic)`}
</option>
<option
value=${NotificationTransportModeEnum.WebhookSlack}
?selected=${this.instance?.mode === NotificationTransportModeEnum.WebhookSlack}
>
${t`Webhook (Slack/Discord)`}
</option>
`;
}
onModeChange(mode: string | undefined): void {
if (
mode === NotificationTransportModeEnum.Webhook ||
@ -107,15 +79,32 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Mode`} ?required=${true} name="mode">
<select
class="pf-c-form-control"
@change=${(ev: Event) => {
const current = (ev.target as HTMLInputElement).value;
this.onModeChange(current);
<ak-radio
@change=${(ev: CustomEvent<NotificationTransportModeEnum>) => {
this.onModeChange(ev.detail);
}}
.options=${[
{
label: t`Local (notifications will be created within authentik)`,
value: NotificationTransportModeEnum.Local,
default: true,
},
{
label: t`Email`,
value: NotificationTransportModeEnum.Email,
},
{
label: t`Webhook (generic)`,
value: NotificationTransportModeEnum.Webhook,
},
{
label: t`Webhook (Slack/Discord)`,
value: NotificationTransportModeEnum.WebhookSlack,
},
]}
.value=${this.instance?.mode}
>
${this.renderTransportModes()}
</select>
</ak-radio>
</ak-form-element-horizontal>
<ak-form-element-horizontal
?hidden=${!this.showWebhook}

View File

@ -2,6 +2,7 @@ import { DesignationToLabel, LayoutToLabel } from "@goauthentik/admin/flows/util
import { AuthenticationEnum } from "@goauthentik/api/dist/models/AuthenticationEnum";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -12,7 +13,6 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
CapabilitiesEnum,
@ -315,22 +315,17 @@ export class FlowForm extends ModelForm<Flow, string> {
</option>
</select>
</ak-form-element-horizontal>
${until(
config().then((c) => {
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
return html`<ak-form-element-horizontal
label=${t`Background`}
name="background"
>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.SaveMedia)
? html`<ak-form-element-horizontal label=${t`Background`} name="background">
<input type="file" value="" class="pf-c-form-control" />
${this.instance?.background
? html`
<p class="pf-c-form__helper-text">
${t`Currently set to:`}
${this.instance?.background}
${t`Currently set to:`} ${this.instance?.background}
</p>
`
: html``}
<p class="pf-c-form__helper-text">
${t`Background shown during execution.`}
</p>
@ -357,7 +352,7 @@ export class FlowForm extends ModelForm<Flow, string> {
</span>
</span>
<span class="pf-c-switch__label">
${t`Clear icon`}
${t`Clear background`}
</span>
</label>
<p class="pf-c-form__helper-text">
@ -365,12 +360,8 @@ export class FlowForm extends ModelForm<Flow, string> {
</p>
</ak-form-element-horizontal>
`
: html``}`;
}
return html`<ak-form-element-horizontal
label=${t`Background`}
name="background"
>
: html``}`
: html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input
type="text"
value="${first(this.instance?.background, "")}"
@ -379,9 +370,7 @@ export class FlowForm extends ModelForm<Flow, string> {
<p class="pf-c-form__helper-text">
${t`Background shown during execution.`}
</p>
</ak-form-element-horizontal>`;
}),
)}
</ak-form-element-horizontal>`}
</div>
</ak-form-group>
</form>`;

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first, groupBy } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -10,11 +11,13 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
Flow,
FlowStageBinding,
FlowsApi,
FlowsInstancesListDesignationEnum,
FlowsInstancesListRequest,
InvalidResponseActionEnum,
PolicyEngineMode,
Stage,
@ -85,23 +88,32 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
`;
}
return html`<ak-form-element-horizontal label=${t`Target`} ?required=${true} name="target">
<select class="pf-c-form-control">
${until(
new FlowsApi(DEFAULT_CONFIG)
.flowsInstancesList({
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
const args: FlowsInstancesListRequest = {
ordering: "slug",
})
.then((flows) => {
return flows.results.map((flow) => {
// No ?selected check here, as this input isn't shown on update forms
return html`<option value=${ifDefined(flow.pk)}>
${flow.name} (${flow.slug})
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
designation: FlowsInstancesListDesignationEnum.Authorization,
};
if (query !== undefined) {
args.search = query;
}
const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(args);
return flows.results;
}}
.renderElement=${(flow: Flow): string => {
return RenderFlowOption(flow);
}}
.renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`;
}}
.value=${(flow: Flow | undefined): string | undefined => {
return flow?.pk;
}}
.selected=${(flow: Flow): boolean => {
return flow.pk === this.instance?.target;
}}
>
</ak-search-select>
</ak-form-element-horizontal>`;
}

View File

@ -10,15 +10,18 @@ import YAML from "yaml";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
Outpost,
OutpostDefaultConfig,
OutpostTypeEnum,
OutpostsApi,
OutpostsServiceConnectionsAllListRequest,
PaginatedLDAPProviderList,
PaginatedProxyProviderList,
PaginatedRadiusProviderList,
ProvidersApi,
ServiceConnection,
} from "@goauthentik/api";
@ -31,6 +34,14 @@ export class OutpostForm extends ModelForm<Outpost, string> {
@property({ type: Boolean })
embedded = false;
@state()
providers?:
| PaginatedProxyProviderList
| PaginatedLDAPProviderList
| PaginatedRadiusProviderList;
defaultConfig?: OutpostDefaultConfig;
async loadInstance(pk: string): Promise<Outpost> {
const o = await new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesRetrieve({
uuid: pk,
@ -39,6 +50,34 @@ export class OutpostForm extends ModelForm<Outpost, string> {
return o;
}
async load(): Promise<void> {
this.defaultConfig = await new OutpostsApi(
DEFAULT_CONFIG,
).outpostsInstancesDefaultSettingsRetrieve();
switch (this.type) {
case OutpostTypeEnum.Proxy:
this.providers = await new ProvidersApi(DEFAULT_CONFIG).providersProxyList({
ordering: "name",
applicationIsnull: false,
});
break;
case OutpostTypeEnum.Ldap:
this.providers = await new ProvidersApi(DEFAULT_CONFIG).providersLdapList({
ordering: "name",
applicationIsnull: false,
});
break;
case OutpostTypeEnum.Radius:
this.providers = await new ProvidersApi(DEFAULT_CONFIG).providersRadiusList({
ordering: "name",
applicationIsnull: false,
});
break;
case OutpostTypeEnum.UnknownDefaultOpenApi:
this.providers = undefined;
}
}
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated outpost.`;
@ -60,78 +99,6 @@ export class OutpostForm extends ModelForm<Outpost, string> {
}
};
renderProviders(): Promise<TemplateResult[]> {
switch (this.type) {
case OutpostTypeEnum.Proxy:
return new ProvidersApi(DEFAULT_CONFIG)
.providersProxyList({
ordering: "name",
applicationIsnull: false,
})
.then((providers) => {
return providers.results.map((provider) => {
const selected = Array.from(this.instance?.providers || []).some(
(sp) => {
return sp == provider.pk;
},
);
return html`<option
value=${ifDefined(provider.pk)}
?selected=${selected}
>
${provider.assignedApplicationName} (${provider.externalHost})
</option>`;
});
});
case OutpostTypeEnum.Ldap:
return new ProvidersApi(DEFAULT_CONFIG)
.providersLdapList({
ordering: "name",
applicationIsnull: false,
})
.then((providers) => {
return providers.results.map((provider) => {
const selected = Array.from(this.instance?.providers || []).some(
(sp) => {
return sp == provider.pk;
},
);
return html`<option
value=${ifDefined(provider.pk)}
?selected=${selected}
>
${provider.assignedApplicationName} (${provider.name})
</option>`;
});
});
case OutpostTypeEnum.Radius:
return new ProvidersApi(DEFAULT_CONFIG)
.providersRadiusList({
ordering: "name",
applicationIsnull: false,
})
.then((providers) => {
return providers.results.map((provider) => {
const selected = Array.from(this.instance?.providers || []).some(
(sp) => {
return sp == provider.pk;
},
);
return html`<option
value=${ifDefined(provider.pk)}
?selected=${selected}
>
${provider.assignedApplicationName} (${provider.name})
</option>`;
});
});
case OutpostTypeEnum.UnknownDefaultOpenApi:
return Promise.resolve([
html` <option value="">${t`Unknown outpost type`}</option>`,
]);
}
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
@ -148,6 +115,7 @@ export class OutpostForm extends ModelForm<Outpost, string> {
@change=${(ev: Event) => {
const target = ev.target as HTMLSelectElement;
this.type = target.selectedOptions[0].value as OutpostTypeEnum;
this.load();
}}
>
<option
@ -162,6 +130,12 @@ export class OutpostForm extends ModelForm<Outpost, string> {
>
${t`LDAP`}
</option>
<option
value=${OutpostTypeEnum.Radius}
?selected=${this.instance?.type === OutpostTypeEnum.Radius}
>
${t`Radius`}
</option>
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Integration`} name="serviceConnection">
@ -213,7 +187,14 @@ export class OutpostForm extends ModelForm<Outpost, string> {
name="providers"
>
<select class="pf-c-form-control" multiple>
${until(this.renderProviders(), html`<option>${t`Loading...`}</option>`)}
${this.providers?.results.map((provider) => {
const selected = Array.from(this.instance?.providers || []).some((sp) => {
return sp == provider.pk;
});
return html`<option value=${ifDefined(provider.pk)} ?selected=${selected}>
${provider.assignedApplicationName} (${provider.name})
</option>`;
})}
</select>
<p class="pf-c-form__helper-text">
${t`You can only select providers that match the type of the outpost.`}
@ -223,19 +204,10 @@ export class OutpostForm extends ModelForm<Outpost, string> {
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Configuration`} name="config">
<!-- @ts-ignore -->
<ak-codemirror
mode="yaml"
value="${until(
new OutpostsApi(DEFAULT_CONFIG)
.outpostsInstancesDefaultSettingsRetrieve()
.then((config) => {
let fc = config.config;
if (this.instance) {
fc = this.instance.config;
}
return YAML.stringify(fc);
}),
value="${YAML.stringify(
this.instance ? this.instance.config : this.defaultConfig?.config,
)}"
></ak-codemirror>
<p class="pf-c-form__helper-text">

View File

@ -10,9 +10,15 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { AdminApi, EventMatcherPolicy, EventsApi, PoliciesApi, TypeCreate } from "@goauthentik/api";
import {
AdminApi,
App,
EventMatcherPolicy,
EventsApi,
PoliciesApi,
TypeCreate,
} from "@goauthentik/api";
@customElement("ak-policy-event-matcher-form")
export class EventMatcherPolicyForm extends ModelForm<EventMatcherPolicy, string> {
@ -22,6 +28,12 @@ export class EventMatcherPolicyForm extends ModelForm<EventMatcherPolicy, string
});
}
async load(): Promise<void> {
this.apps = await new AdminApi(DEFAULT_CONFIG).adminAppsList();
}
apps?: App[];
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated policy.`;
@ -118,19 +130,14 @@ export class EventMatcherPolicyForm extends ModelForm<EventMatcherPolicy, string
<option value="" ?selected=${this.instance?.app === undefined}>
---------
</option>
${until(
new AdminApi(DEFAULT_CONFIG).adminAppsList().then((apps) => {
return apps.map((app) => {
${this.apps?.map((app) => {
return html`<option
value=${app.name}
?selected=${this.instance?.app === app.name}
>
${app.label}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
})}
</select>
<p class="pf-c-form__helper-text">
${t`Match events created by selected application. When left empty, all applications are matched.`}

View File

@ -11,9 +11,8 @@ import "@goauthentik/elements/utils/TimeDeltaHelp";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
CertificateKeyPair,
@ -26,6 +25,8 @@ import {
FlowsInstancesListRequest,
IssuerModeEnum,
OAuth2Provider,
PaginatedOAuthSourceList,
PaginatedScopeMappingList,
PropertymappingsApi,
ProvidersApi,
SourcesApi,
@ -34,19 +35,31 @@ import {
@customElement("ak-provider-oauth2-form")
export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
loadInstance(pk: number): Promise<OAuth2Provider> {
return new ProvidersApi(DEFAULT_CONFIG)
.providersOauth2Retrieve({
propertyMappings?: PaginatedScopeMappingList;
oauthSources?: PaginatedOAuthSourceList;
@state()
showClientSecret = true;
async loadInstance(pk: number): Promise<OAuth2Provider> {
const provider = await new ProvidersApi(DEFAULT_CONFIG).providersOauth2Retrieve({
id: pk,
})
.then((provider) => {
});
this.showClientSecret = provider.clientType === ClientTypeEnum.Confidential;
return provider;
});
}
@property({ type: Boolean })
showClientSecret = true;
async load(): Promise<void> {
this.propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsScopeList({
ordering: "scope_name",
});
this.oauthSources = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthList({
ordering: "name",
hasJwks: true,
});
}
getSuccessMessage(): string {
if (this.instance) {
@ -287,13 +300,7 @@ ${this.instance?.redirectUris}</textarea
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Scopes`} name="propertyMappings">
<select class="pf-c-form-control" multiple>
${until(
new PropertymappingsApi(DEFAULT_CONFIG)
.propertymappingsScopeList({
ordering: "scope_name",
})
.then((scopes) => {
return scopes.results.map((scope) => {
${this.propertyMappings?.results.map((scope) => {
let selected = false;
if (!this.instance?.propertyMappings) {
selected =
@ -301,11 +308,11 @@ ${this.instance?.redirectUris}</textarea
"goauthentik.io/providers/oauth2/scope-",
) || false;
} else {
selected = Array.from(
this.instance?.propertyMappings,
).some((su) => {
selected = Array.from(this.instance?.propertyMappings).some(
(su) => {
return su == scope.pk;
});
},
);
}
return html`<option
value=${ifDefined(scope.pk)}
@ -313,10 +320,7 @@ ${this.instance?.redirectUris}</textarea
>
${scope.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
})}
</select>
<p class="pf-c-form__helper-text">
${t`Select which scopes can be used by the client. The client still has to specify the scope to access the data.`}
@ -413,29 +417,14 @@ ${this.instance?.redirectUris}</textarea
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${t`Trusted OIDC Sources`} name="jwksSources">
<select class="pf-c-form-control" multiple>
${until(
new SourcesApi(DEFAULT_CONFIG)
.sourcesOauthList({
ordering: "name",
hasJwks: true,
})
.then((sources) => {
return sources.results.map((source) => {
const selected = (
this.instance?.jwksSources || []
).some((su) => {
${this.oauthSources?.results.map((source) => {
const selected = (this.instance?.jwksSources || []).some((su) => {
return su == source.pk;
});
return html`<option
value=${source.pk}
?selected=${selected}
>
return html`<option value=${source.pk} ?selected=${selected}>
${source.name} (${source.slug})
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
})}
</select>
<p class="pf-c-form__helper-text">
${t`JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.`}

View File

@ -13,7 +13,6 @@ import { CSSResult, css } from "lit";
import { TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
@ -28,6 +27,8 @@ import {
FlowsApi,
FlowsInstancesListDesignationEnum,
FlowsInstancesListRequest,
PaginatedOAuthSourceList,
PaginatedScopeMappingList,
PropertymappingsApi,
ProvidersApi,
ProxyMode,
@ -51,18 +52,30 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
);
}
loadInstance(pk: number): Promise<ProxyProvider> {
return new ProvidersApi(DEFAULT_CONFIG)
.providersProxyRetrieve({
async loadInstance(pk: number): Promise<ProxyProvider> {
const provider = await new ProvidersApi(DEFAULT_CONFIG).providersProxyRetrieve({
id: pk,
})
.then((provider) => {
});
this.showHttpBasic = first(provider.basicAuthEnabled, true);
this.mode = first(provider.mode, ProxyMode.Proxy);
return provider;
}
async load(): Promise<void> {
this.propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsScopeList({
ordering: "scope_name",
});
this.oauthSources = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthList({
ordering: "name",
hasJwks: true,
});
}
propertyMappings?: PaginatedScopeMappingList;
oauthSources?: PaginatedOAuthSourceList;
@state()
showHttpBasic = true;
@ -392,34 +405,23 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
name="propertyMappings"
>
<select class="pf-c-form-control" multiple>
${until(
new PropertymappingsApi(DEFAULT_CONFIG)
.propertymappingsScopeList({
ordering: "scope_name",
})
.then((scopes) => {
return scopes.results
${this.propertyMappings?.results
.filter((scope) => {
return !scope.managed?.startsWith(
"goauthentik.io/providers",
);
return !scope.managed?.startsWith("goauthentik.io/providers");
})
.map((scope) => {
const selected = (
this.instance?.propertyMappings || []
).some((su) => {
const selected = (this.instance?.propertyMappings || []).some(
(su) => {
return su == scope.pk;
});
},
);
return html`<option
value=${ifDefined(scope.pk)}
?selected=${selected}
>
${scope.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
})}
</select>
<p class="pf-c-form__helper-text">
${t`Additional scope mappings, which are passed to the proxy.`}
@ -497,29 +499,14 @@ ${this.instance?.skipPathRegex}</textarea
${this.showHttpBasic ? this.renderHttpBasic() : html``}
<ak-form-element-horizontal label=${t`Trusted OIDC Sources`} name="jwksSources">
<select class="pf-c-form-control" multiple>
${until(
new SourcesApi(DEFAULT_CONFIG)
.sourcesOauthList({
ordering: "name",
hasJwks: true,
})
.then((sources) => {
return sources.results.map((source) => {
const selected = (
this.instance?.jwksSources || []
).some((su) => {
${this.oauthSources?.results.map((source) => {
const selected = (this.instance?.jwksSources || []).some((su) => {
return su == source.pk;
});
return html`<option
value=${source.pk}
?selected=${selected}
>
return html`<option value=${source.pk} ?selected=${selected}>
${source.name} (${source.slug})
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
})}
</select>
<p class="pf-c-form__helper-text">
${t`JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.`}

View File

@ -9,9 +9,9 @@ import "@goauthentik/elements/forms/SearchSelect";
import { t } from "@lingui/macro";
import { customElement } from "lit-element";
import { TemplateResult, html } from "lit-html";
import { TemplateResult, html } from "lit";
import { ifDefined } from "lit-html/directives/if-defined.js";
import { customElement } from "lit/decorators.js";
import {
Flow,

View File

@ -11,7 +11,8 @@ import "@goauthentik/elements/events/ObjectChangelog";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, customElement, html, property } from "lit-element";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";

View File

@ -12,7 +12,6 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
CertificateKeyPair,
@ -23,6 +22,7 @@ import {
FlowsApi,
FlowsInstancesListDesignationEnum,
FlowsInstancesListRequest,
PaginatedSAMLPropertyMappingList,
PropertymappingsApi,
PropertymappingsSamlListRequest,
ProvidersApi,
@ -40,6 +40,16 @@ export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> {
});
}
async load(): Promise<void> {
this.propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsSamlList({
ordering: "saml_name",
});
}
propertyMappings?: PaginatedSAMLPropertyMappingList;
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated provider.`;
@ -241,13 +251,7 @@ export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> {
name="propertyMappings"
>
<select class="pf-c-form-control" multiple>
${until(
new PropertymappingsApi(DEFAULT_CONFIG)
.propertymappingsSamlList({
ordering: "saml_name",
})
.then((mappings) => {
return mappings.results.map((mapping) => {
${this.propertyMappings?.results.map((mapping) => {
let selected = false;
if (!this.instance?.propertyMappings) {
selected =
@ -255,11 +259,11 @@ export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> {
"goauthentik.io/providers/saml",
) || false;
} else {
selected = Array.from(
this.instance?.propertyMappings,
).some((su) => {
selected = Array.from(this.instance?.propertyMappings).some(
(su) => {
return su == mapping.pk;
});
},
);
}
return html`<option
value=${ifDefined(mapping.pk)}
@ -267,10 +271,7 @@ export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> {
>
${mapping.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
})}
</select>
<p class="pf-c-form__helper-text">
${t`Hold control/command to select multiple items.`}

View File

@ -11,12 +11,12 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
CoreApi,
CoreGroupsListRequest,
Group,
PaginatedSCIMMappingList,
PropertymappingsApi,
ProvidersApi,
SCIMProvider,
@ -30,6 +30,16 @@ export class SCIMProviderFormPage extends ModelForm<SCIMProvider, number> {
});
}
async load(): Promise<void> {
this.propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsScimList({
ordering: "managed",
});
}
propertyMappings?: PaginatedSCIMMappingList;
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated provider.`;
@ -147,25 +157,18 @@ export class SCIMProviderFormPage extends ModelForm<SCIMProvider, number> {
name="propertyMappings"
>
<select class="pf-c-form-control" multiple>
${until(
new PropertymappingsApi(DEFAULT_CONFIG)
.propertymappingsScimList({
ordering: "managed",
})
.then((mappings) => {
return mappings.results.map((mapping) => {
${this.propertyMappings?.results.map((mapping) => {
let selected = false;
if (!this.instance?.propertyMappings) {
selected =
mapping.managed ===
"goauthentik.io/providers/scim/user" ||
mapping.managed === "goauthentik.io/providers/scim/user" ||
false;
} else {
selected = Array.from(
this.instance?.propertyMappings,
).some((su) => {
selected = Array.from(this.instance?.propertyMappings).some(
(su) => {
return su == mapping.pk;
});
},
);
}
return html`<option
value=${ifDefined(mapping.pk)}
@ -173,10 +176,7 @@ export class SCIMProviderFormPage extends ModelForm<SCIMProvider, number> {
>
${mapping.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
})}
</select>
<p class="pf-c-form__helper-text">
${t`Property mappings used to user mapping.`}
@ -191,18 +191,11 @@ export class SCIMProviderFormPage extends ModelForm<SCIMProvider, number> {
name="propertyMappingsGroup"
>
<select class="pf-c-form-control" multiple>
${until(
new PropertymappingsApi(DEFAULT_CONFIG)
.propertymappingsScimList({
ordering: "managed",
})
.then((mappings) => {
return mappings.results.map((mapping) => {
${this.propertyMappings?.results.map((mapping) => {
let selected = false;
if (!this.instance?.propertyMappingsGroup) {
selected =
mapping.managed ===
"goauthentik.io/providers/scim/group";
mapping.managed === "goauthentik.io/providers/scim/group";
} else {
selected = Array.from(
this.instance?.propertyMappingsGroup,
@ -216,10 +209,7 @@ export class SCIMProviderFormPage extends ModelForm<SCIMProvider, number> {
>
${mapping.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
})}
</select>
<p class="pf-c-form__helper-text">
${t`Property mappings used to group creation.`}

View File

@ -10,7 +10,6 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
CertificateKeyPair,
@ -21,6 +20,7 @@ import {
Group,
LDAPSource,
LDAPSourceRequest,
PaginatedLDAPPropertyMappingList,
PropertymappingsApi,
SourcesApi,
} from "@goauthentik/api";
@ -33,6 +33,16 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
});
}
async load(): Promise<void> {
this.propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsLdapList({
ordering: "managed,object_field",
});
}
propertyMappings?: PaginatedLDAPPropertyMappingList;
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated source.`;
@ -241,13 +251,7 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
name="propertyMappings"
>
<select class="pf-c-form-control" multiple>
${until(
new PropertymappingsApi(DEFAULT_CONFIG)
.propertymappingsLdapList({
ordering: "managed,object_field",
})
.then((mappings) => {
return mappings.results.map((mapping) => {
${this.propertyMappings?.results.map((mapping) => {
let selected = false;
if (!this.instance?.propertyMappings) {
selected =
@ -259,11 +263,11 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
) ||
false;
} else {
selected = Array.from(
this.instance?.propertyMappings,
).some((su) => {
selected = Array.from(this.instance?.propertyMappings).some(
(su) => {
return su == mapping.pk;
});
},
);
}
return html`<option
value=${ifDefined(mapping.pk)}
@ -271,10 +275,7 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
>
${mapping.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
})}
</select>
<p class="pf-c-form__helper-text">
${t`Property mappings used to user creation.`}
@ -289,13 +290,7 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
name="propertyMappingsGroup"
>
<select class="pf-c-form-control" multiple>
${until(
new PropertymappingsApi(DEFAULT_CONFIG)
.propertymappingsLdapList({
ordering: "managed,object_field",
})
.then((mappings) => {
return mappings.results.map((mapping) => {
${this.propertyMappings?.results.map((mapping) => {
let selected = false;
if (!this.instance?.propertyMappingsGroup) {
selected =
@ -314,10 +309,7 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
>
${mapping.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
})}
</select>
<p class="pf-c-form__helper-text">
${t`Property mappings used to group creation.`}

View File

@ -2,6 +2,7 @@ import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -13,7 +14,6 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
CapabilitiesEnum,
@ -315,10 +315,8 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
</p>
</ak-form-element-horizontal>
${until(
config().then((c) => {
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.SaveMedia)
? html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input type="file" value="" class="pf-c-form-control" />
${this.instance?.icon
? html`
@ -342,25 +340,18 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i
class="fas fa-check"
aria-hidden="true"
></i>
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">
${t`Clear icon`}
</span>
<span class="pf-c-switch__label"> ${t`Clear icon`} </span>
</label>
<p class="pf-c-form__helper-text">
${t`Delete currently set icon.`}
</p>
</ak-form-element-horizontal>
`
: html``}`;
}
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
: html``}`
: html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input
type="text"
value="${first(this.instance?.icon, "")}"
@ -369,9 +360,7 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
<p class="pf-c-form__helper-text">
${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
</p>
</ak-form-element-horizontal>`;
}),
)}
</ak-form-element-horizontal>`}
<ak-form-group .expanded=${true}>
<span slot="header"> ${t`Protocol settings`} </span>

View File

@ -3,6 +3,7 @@ import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils"
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { PlexAPIClient, PlexResource, popupCenterScreen } from "@goauthentik/common/helpers/plex";
import { first, randomString } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -13,7 +14,6 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
CapabilitiesEnum,
@ -267,10 +267,8 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
</p>
</ak-form-element-horizontal>
${until(
config().then((c) => {
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.SaveMedia)
? html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input type="file" value="" class="pf-c-form-control" />
${this.instance?.icon
? html`
@ -294,24 +292,18 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i
class="fas fa-check"
aria-hidden="true"
></i>
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">
${t`Clear icon`}
</span>
<span class="pf-c-switch__label"> ${t`Clear icon`} </span>
</label>
<p class="pf-c-form__helper-text">
${t`Delete currently set icon.`}
</p>
</ak-form-element-horizontal>
`
: html``}`;
}
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
: html``}`
: html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input
type="text"
value="${first(this.instance?.icon, "")}"
@ -320,10 +312,7 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
<p class="pf-c-form__helper-text">
${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
</p>
</ak-form-element-horizontal>`;
}),
)}
</ak-form-element-horizontal>`}
<ak-form-group .expanded=${true}>
<span slot="header"> ${t`Protocol settings`} </span>
<div slot="body" class="pf-c-form">

View File

@ -2,6 +2,7 @@ import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -13,7 +14,6 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
BindingTypeEnum,
@ -161,10 +161,8 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
</option>
</select>
</ak-form-element-horizontal>
${until(
config().then((c) => {
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.SaveMedia)
? html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input type="file" value="" class="pf-c-form-control" />
${this.instance?.icon
? html`
@ -188,24 +186,18 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i
class="fas fa-check"
aria-hidden="true"
></i>
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">
${t`Clear icon`}
</span>
<span class="pf-c-switch__label"> ${t`Clear icon`} </span>
</label>
<p class="pf-c-form__helper-text">
${t`Delete currently set icon.`}
</p>
</ak-form-element-horizontal>
`
: html``}`;
}
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
: html``}`
: html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input
type="text"
value="${first(this.instance?.icon, "")}"
@ -214,9 +206,7 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
<p class="pf-c-form__helper-text">
${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
</p>
</ak-form-element-horizontal>`;
}),
)}
</ak-form-element-horizontal>`}
<ak-form-group .expanded=${true}>
<span slot="header"> ${t`Protocol settings`} </span>

View File

@ -10,30 +10,35 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
AuthenticatorValidateStage,
DeviceClassesEnum,
NotConfiguredActionEnum,
PaginatedStageList,
StagesApi,
UserVerificationEnum,
} from "@goauthentik/api";
@customElement("ak-stage-authenticator-validate-form")
export class AuthenticatorValidateStageForm extends ModelForm<AuthenticatorValidateStage, string> {
loadInstance(pk: string): Promise<AuthenticatorValidateStage> {
return new StagesApi(DEFAULT_CONFIG)
.stagesAuthenticatorValidateRetrieve({
async loadInstance(pk: string): Promise<AuthenticatorValidateStage> {
const stage = await new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorValidateRetrieve({
stageUuid: pk,
})
.then((stage) => {
});
this.showConfigurationStages =
stage.notConfiguredAction === NotConfiguredActionEnum.Configure;
return stage;
}
async load(): Promise<void> {
this.stages = await new StagesApi(DEFAULT_CONFIG).stagesAllList({
ordering: "name",
});
}
stages?: PaginatedStageList;
@property({ type: Boolean })
showConfigurationStages = true;
@ -216,13 +221,7 @@ export class AuthenticatorValidateStageForm extends ModelForm<AuthenticatorValid
name="configurationStages"
>
<select class="pf-c-form-control" multiple>
${until(
new StagesApi(DEFAULT_CONFIG)
.stagesAllList({
ordering: "name",
})
.then((stages) => {
return stages.results.map((stage) => {
${this.stages?.results.map((stage) => {
const selected = Array.from(
this.instance?.configurationStages || [],
).some((su) => {
@ -234,10 +233,7 @@ export class AuthenticatorValidateStageForm extends ModelForm<AuthenticatorValid
>
${stage.name} (${stage.verboseName})
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
})}
</select>
<p class="pf-c-form__helper-text">
${t`Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again.`}

View File

@ -9,23 +9,25 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { EmailStage, StagesApi } from "@goauthentik/api";
import { EmailStage, StagesApi, TypeCreate } from "@goauthentik/api";
@customElement("ak-stage-email-form")
export class EmailStageForm extends ModelForm<EmailStage, string> {
loadInstance(pk: string): Promise<EmailStage> {
return new StagesApi(DEFAULT_CONFIG)
.stagesEmailRetrieve({
async loadInstance(pk: string): Promise<EmailStage> {
const stage = await new StagesApi(DEFAULT_CONFIG).stagesEmailRetrieve({
stageUuid: pk,
})
.then((stage) => {
});
this.showConnectionSettings = !stage.useGlobalSettings;
return stage;
});
}
async load(): Promise<void> {
this.templates = await new StagesApi(DEFAULT_CONFIG).stagesEmailTemplatesList();
}
templates?: TypeCreate[];
@property({ type: Boolean })
showConnectionSettings = false;
@ -232,23 +234,15 @@ export class EmailStageForm extends ModelForm<EmailStage, string> {
name="template"
>
<select name="users" class="pf-c-form-control">
${until(
new StagesApi(DEFAULT_CONFIG)
.stagesEmailTemplatesList()
.then((templates) => {
return templates.map((template) => {
const selected =
this.instance?.template === template.name;
${this.templates?.map((template) => {
const selected = this.instance?.template === template.name;
return html`<option
value=${ifDefined(template.name)}
?selected=${selected}
>
${template.description}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
})}
</select>
</ak-form-element-horizontal>
</div>

View File

@ -11,7 +11,6 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
Flow,
@ -19,6 +18,7 @@ import {
FlowsInstancesListDesignationEnum,
FlowsInstancesListRequest,
IdentificationStage,
PaginatedSourceList,
SourcesApi,
Stage,
StagesApi,
@ -34,6 +34,14 @@ export class IdentificationStageForm extends ModelForm<IdentificationStage, stri
});
}
async load(): Promise<void> {
this.sources = await new SourcesApi(DEFAULT_CONFIG).sourcesAllList({
ordering: "slug",
});
}
sources?: PaginatedSourceList;
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated stage.`;
@ -80,7 +88,7 @@ export class IdentificationStageForm extends ModelForm<IdentificationStage, stri
<span slot="header"> ${t`Stage-specific settings`} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${t`User fields`} name="userFields">
<select name="users" class="pf-c-form-control" multiple>
<select class="pf-c-form-control" multiple>
<option
value=${UserFieldsEnum.Username}
?selected=${this.isUserFieldSelected(UserFieldsEnum.Username)}
@ -187,22 +195,18 @@ export class IdentificationStageForm extends ModelForm<IdentificationStage, stri
name="sources"
>
<select class="pf-c-form-control" multiple>
${until(
new SourcesApi(DEFAULT_CONFIG)
.sourcesAllList({})
.then((sources) => {
return sources.results.map((source) => {
let selected = Array.from(
this.instance?.sources || [],
).some((su) => {
${this.sources?.results.map((source) => {
let selected = Array.from(this.instance?.sources || []).some(
(su) => {
return su == source.pk;
});
},
);
// Creating a new instance, auto-select built-in source
// Only when no other sources exist
if (
!this.instance &&
source.component === "" &&
sources.results.length < 2
(this.sources?.results || []).length < 2
) {
selected = true;
}
@ -212,10 +216,7 @@ export class IdentificationStageForm extends ModelForm<IdentificationStage, stri
>
${source.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
})}
</select>
<p class="pf-c-form__helper-text">
${t`Select sources should be shown for users to authenticate with. This only affects web-based sources, not LDAP.`}

View File

@ -10,9 +10,14 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { PoliciesApi, PromptStage, StagesApi } from "@goauthentik/api";
import {
PaginatedPolicyList,
PaginatedPromptList,
PoliciesApi,
PromptStage,
StagesApi,
} from "@goauthentik/api";
@customElement("ak-stage-prompt-form")
export class PromptStageForm extends ModelForm<PromptStage, string> {
@ -22,6 +27,18 @@ export class PromptStageForm extends ModelForm<PromptStage, string> {
});
}
async load(): Promise<void> {
this.prompts = await new StagesApi(DEFAULT_CONFIG).stagesPromptPromptsList({
ordering: "field_name",
});
this.policies = await new PoliciesApi(DEFAULT_CONFIG).policiesAllList({
ordering: "name",
});
}
prompts?: PaginatedPromptList;
policies?: PaginatedPolicyList;
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated stage.`;
@ -61,28 +78,19 @@ export class PromptStageForm extends ModelForm<PromptStage, string> {
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${t`Fields`} ?required=${true} name="fields">
<select name="users" class="pf-c-form-control" multiple>
${until(
new StagesApi(DEFAULT_CONFIG)
.stagesPromptPromptsList({
ordering: "field_name",
})
.then((prompts) => {
return prompts.results.map((prompt) => {
const selected = Array.from(
this.instance?.fields || [],
).some((su) => {
${this.prompts?.results.map((prompt) => {
const selected = Array.from(this.instance?.fields || []).some(
(su) => {
return su == prompt.pk;
});
},
);
return html`<option
value=${ifDefined(prompt.pk)}
?selected=${selected}
>
${t`${prompt.name} ("${prompt.fieldKey}", of type ${prompt.type})`}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
})}
</select>
<p class="pf-c-form__helper-text">
${t`Hold control/command to select multiple items.`}
@ -101,13 +109,7 @@ export class PromptStageForm extends ModelForm<PromptStage, string> {
name="validationPolicies"
>
<select name="users" class="pf-c-form-control" multiple>
${until(
new PoliciesApi(DEFAULT_CONFIG)
.policiesAllList({
ordering: "name",
})
.then((policies) => {
return policies.results.map((policy) => {
${this.policies?.results.map((policy) => {
const selected = Array.from(
this.instance?.validationPolicies || [],
).some((su) => {
@ -119,10 +121,7 @@ export class PromptStageForm extends ModelForm<PromptStage, string> {
>
${t`${policy.name} (${policy.verboseName})`}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
})}
</select>
<p class="pf-c-form__helper-text">
${t`Selected policies are executed when the stage is submitted to validate the data.`}

View File

@ -1,6 +1,6 @@
import { tenant } from "@goauthentik/common/api/config";
import { config, tenant } from "@goauthentik/common/api/config";
import { EVENT_LOCALE_CHANGE, EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
import { uiConfig } from "@goauthentik/common/ui/config";
import { UIConfig, uiConfig } from "@goauthentik/common/ui/config";
import { LitElement } from "lit";
import { state } from "lit/decorators.js";
@ -9,13 +9,13 @@ import AKGlobal from "@goauthentik/common/styles/authentik.css";
import ThemeDark from "@goauthentik/common/styles/theme-dark.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { CurrentTenant, UiThemeEnum } from "@goauthentik/api";
import { Config, CurrentTenant, UiThemeEnum } from "@goauthentik/api";
export function rootInterface(): Interface | undefined {
export function rootInterface<T extends Interface>(): T | undefined {
const el = Array.from(document.body.querySelectorAll("*")).filter(
(el) => el instanceof Interface,
);
return el[0] as Interface;
return el[0] as T;
}
let css: Promise<string[]> | undefined;
@ -171,10 +171,17 @@ export class Interface extends AKElement {
@state()
tenant?: CurrentTenant;
@state()
uiConfig?: UIConfig;
@state()
config?: Config;
constructor() {
super();
document.adoptedStyleSheets = [...document.adoptedStyleSheets, PFBase];
tenant().then((tenant) => (this.tenant = tenant));
config().then((config) => (this.config = config));
}
_activateTheme(root: AdoptedStyleSheetsElement, theme: UiThemeEnum): void {
@ -183,7 +190,9 @@ export class Interface extends AKElement {
}
async getTheme(): Promise<UiThemeEnum> {
const config = await uiConfig();
return config.theme?.base || UiThemeEnum.Automatic;
if (!this.uiConfig) {
this.uiConfig = await uiConfig();
}
return this.uiConfig.theme?.base || UiThemeEnum.Automatic;
}
}

View File

@ -7,7 +7,7 @@ import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect";
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { customElement, property, state } from "lit/decorators.js";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -22,7 +22,7 @@ import { ResponseError, ValidationError } from "@goauthentik/api";
export class PreventFormSubmit {
// Stub class which can be returned by form elements to prevent the form from submitting
constructor(public message: string) {}
constructor(public message: string, public element?: HorizontalFormElement) {}
}
export class APIError extends Error {
@ -36,16 +36,15 @@ export interface KeyUnknown {
}
@customElement("ak-form")
export class Form<T> extends AKElement {
export abstract class Form<T> extends AKElement {
abstract send(data: T): Promise<unknown>;
viewportCheck = true;
@property()
successMessage = "";
@property()
send!: (data: T) => Promise<unknown>;
@property({ attribute: false })
@state()
nonFieldErrors?: string[];
static get styles(): CSSResult[] {
@ -177,17 +176,15 @@ export class Form<T> extends AKElement {
json[element.name] = inputElement.checked;
} else if (inputElement.tagName.toLowerCase() === "ak-search-select") {
const select = inputElement as unknown as SearchSelect<unknown>;
let value: unknown;
try {
value = select.toForm();
} catch {
console.debug("authentik/form: SearchSelect.value error");
return;
}
if (value instanceof PreventFormSubmit) {
throw new Error(value.message);
}
const value = select.toForm();
json[element.name] = value;
} catch (exc) {
if (exc instanceof PreventFormSubmit) {
throw new PreventFormSubmit(exc.message, element);
}
throw exc;
}
} else {
this.serializeFieldRecursive(inputElement, inputElement.value, json);
}
@ -215,14 +212,14 @@ export class Form<T> extends AKElement {
parent[nameElements[nameElements.length - 1]] = value;
}
submit(ev: Event): Promise<unknown> | undefined {
async submit(ev: Event): Promise<unknown | undefined> {
ev.preventDefault();
try {
const data = this.serializeForm();
if (!data) {
return;
}
return this.send(data)
.then((r) => {
const response = await this.send(data);
showMessage({
level: MessageLevel.success,
message: this.getSuccessMessage(),
@ -233,12 +230,9 @@ export class Form<T> extends AKElement {
composed: true,
}),
);
return r;
})
.catch(async (ex: Error | ResponseError) => {
if (!(ex instanceof ResponseError)) {
throw ex;
}
return response;
} catch (ex) {
if (ex instanceof ResponseError) {
let msg = ex.response.statusText;
if (ex.response.status > 399 && ex.response.status < 500) {
const errorMessage: ValidationError = await ex.response.json();
@ -277,9 +271,14 @@ export class Form<T> extends AKElement {
message: msg,
level: MessageLevel.error,
});
}
if (ex instanceof PreventFormSubmit && ex.element) {
ex.element.errorMessages = [ex.message];
ex.element.invalid = true;
}
// rethrow the error so the form doesn't close
throw ex;
});
}
}
renderForm(): TemplateResult {

View File

@ -1,27 +1,44 @@
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import "@goauthentik/elements/EmptyState";
import { Form } from "@goauthentik/elements/forms/Form";
import { TemplateResult } from "lit";
import { TemplateResult, html } from "lit";
import { property } from "lit/decorators.js";
export abstract class ModelForm<T, PKT extends string | number> extends Form<T> {
abstract loadInstance(pk: PKT): Promise<T>;
async load(): Promise<void> {
return Promise.resolve();
}
@property({ attribute: false })
set instancePk(value: PKT) {
this._instancePk = value;
if (this.viewportCheck && !this.isInViewport) {
return;
}
if (this._isLoading) {
return;
}
this._isLoading = true;
this.load().then(() => {
this.loadInstance(value).then((instance) => {
this.instance = instance;
this._isLoading = false;
this.requestUpdate();
});
});
}
private _instancePk?: PKT;
// Keep track if we've loaded the model instance
private _initialLoad = false;
// Keep track if we've done the general data loading of load()
private _initialDataLoad = false;
private _isLoading = false;
@property({ attribute: false })
instance?: T = this.defaultInstance;
@ -45,17 +62,29 @@ export abstract class ModelForm<T, PKT extends string | number> extends Form<T>
this._initialLoad = false;
}
renderVisible(): TemplateResult {
if ((this._instancePk && !this.instance) || !this._initialDataLoad) {
return html`<ak-empty-state ?loading=${true}></ak-empty-state>`;
}
return super.renderVisible();
}
render(): TemplateResult {
if (this._instancePk && !this._initialLoad) {
if (
// if we're in viewport now and haven't loaded AND have a PK set, load now
this.isInViewport ||
// Or if we don't check for viewport in some cases
!this.viewportCheck
) {
const viewportVisible = this.isInViewport || !this.viewportCheck;
if (this._instancePk && !this._initialLoad && viewportVisible) {
this.instancePk = this._instancePk;
this._initialLoad = true;
}
} else if (!this._initialDataLoad && viewportVisible) {
// else if since if the above case triggered that will also call this.load(), so
// ensure we don't load again
this.load().then(() => {
this._initialDataLoad = true;
// Class attributes changed in this.load() might not be @property()
// or @state() so let's trigger a re-render to be sure we get updated
this.requestUpdate();
});
}
return super.render();
}

View File

@ -4,7 +4,7 @@ import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
@customElement("ak-proxy-form")
export class ProxyForm extends Form<unknown> {
export abstract class ProxyForm extends Form<unknown> {
@property()
type!: string;
@ -16,7 +16,7 @@ export class ProxyForm extends Form<unknown> {
innerElement?: Form<unknown>;
submit(ev: Event): Promise<unknown> | undefined {
async submit(ev: Event): Promise<unknown | undefined> {
return this.innerElement?.submit(ev);
}
@ -39,7 +39,9 @@ export class ProxyForm extends Form<unknown> {
if (this.type in this.typeMap) {
elementName = this.typeMap[this.type];
}
if (!this.innerElement) {
this.innerElement = document.createElement(elementName) as Form<unknown>;
}
this.innerElement.viewportCheck = this.viewportCheck;
for (const k in this.args) {
this.innerElement.setAttribute(k, this.args[k] as string);

View File

@ -25,11 +25,6 @@ export class Radio<T> extends AKElement {
@property()
value?: T;
@property({ attribute: false })
onChange: (value: T) => void = () => {
return;
};
static get styles(): CSSResult[] {
return [
PFBase,
@ -63,7 +58,13 @@ export class Radio<T> extends AKElement {
id=${elId}
@change=${() => {
this.value = opt.value;
this.onChange(opt.value);
this.dispatchEvent(
new CustomEvent("change", {
bubbles: true,
composed: true,
detail: opt.value,
}),
);
}}
.checked=${opt.value === this.value}
/>

View File

@ -94,7 +94,7 @@ export class SearchSelect<T> extends AKElement {
toForm(): unknown {
if (!this.objects) {
return new PreventFormSubmit(t`Loading options...`);
throw new PreventFormSubmit(t`Loading options...`);
}
return this.value(this.selectedObject) || "";
}

View File

@ -13,13 +13,13 @@ import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-gro
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-wizard-form")
export class WizardForm extends Form<KeyUnknown> {
export abstract class WizardForm extends Form<KeyUnknown> {
viewportCheck = false;
@property({ attribute: false })
nextDataCallback!: (data: KeyUnknown) => Promise<boolean>;
submit(): Promise<boolean> | undefined {
async submit(): Promise<boolean | undefined> {
const data = this.serializeForm();
if (!data) {
return;

View File

@ -5,7 +5,7 @@ import {
EVENT_WS_MESSAGE,
} from "@goauthentik/common/constants";
import { configureSentry } from "@goauthentik/common/sentry";
import { UIConfig, UserDisplay, uiConfig } from "@goauthentik/common/ui/config";
import { UserDisplay } from "@goauthentik/common/ui/config";
import { autoDetectLanguage } from "@goauthentik/common/ui/locale";
import { me } from "@goauthentik/common/users";
import { first } from "@goauthentik/common/utils";
@ -56,9 +56,6 @@ export class UserInterface extends Interface {
@state()
me?: SessionUser;
@state()
config?: UIConfig;
static get styles(): CSSResult[] {
return [
PFBase,
@ -126,7 +123,6 @@ export class UserInterface extends Interface {
async firstUpdated(): Promise<void> {
this.me = await me();
this.config = await uiConfig();
const notifications = await new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({
seen: false,
ordering: "-created",
@ -137,11 +133,11 @@ export class UserInterface extends Interface {
}
render(): TemplateResult {
if (!this.config || !this.me) {
if (!this.uiConfig || !this.me) {
return html``;
}
let userDisplay = "";
switch (this.config.navbar.userDisplay) {
switch (this.uiConfig.navbar.userDisplay) {
case UserDisplay.username:
userDisplay = this.me.user.username;
break;
@ -155,7 +151,7 @@ export class UserInterface extends Interface {
userDisplay = this.me.user.username;
}
return html`<div class="pf-c-page">
<div class="background-wrapper" style="${this.config.theme.background}"></div>
<div class="background-wrapper" style="${this.uiConfig.theme.background}"></div>
<header class="pf-c-page__header">
<div class="pf-c-page__header-brand">
<a href="#/" class="pf-c-page__header-brand-link">
@ -168,7 +164,7 @@ export class UserInterface extends Interface {
</div>
<div class="pf-c-page__header-tools">
<div class="pf-c-page__header-tools-group">
${this.config.enabledFeatures.apiDrawer
${this.uiConfig.enabledFeatures.apiDrawer
? html`<div
class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-lg"
>
@ -186,7 +182,7 @@ export class UserInterface extends Interface {
</button>
</div>`
: html``}
${this.config.enabledFeatures.notificationDrawer
${this.uiConfig.enabledFeatures.notificationDrawer
? html`<div
class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-lg"
>
@ -216,7 +212,7 @@ export class UserInterface extends Interface {
</button>
</div> `
: html``}
${this.config.enabledFeatures.settings
${this.uiConfig.enabledFeatures.settings
? html` <div class="pf-c-page__header-tools-item">
<a class="pf-c-button pf-m-plain" type="button" href="#/settings">
<i class="fas fa-cog" aria-hidden="true"></i>