This repository has been archived on 2024-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
authentik/web/src/admin/applications/ApplicationForm.ts
Ken Sternberg cc1c66aa13
Web: bugfix: broken backchannel selector (#7480)
* web: break circular dependency between AKElement & Interface.

This commit changes the way the root node of the web application shell is
discovered by child components, such that the base class shared by both
no longer results in a circular dependency between the two models.

I've run this in isolation and have seen no failures of discovery; the identity
token exists as soon as the Interface is constructed and is found by every item
on the page.

* web: fix broken typescript references

This built... and then it didn't?  Anyway, the current fix is to
provide type information the AkInterface for the data that consumers
require.

* web: rollback dependabot's upgrade of context

The most frustrating part of this is that I RAN THIS, dammit, with the updated
context and the current Wizard, and it finished the End-to-End tests without
complaint.

* web: bugfix: broken backchannel selector

There were two bugs here, both of them introduced by me because I didn't understand the
system well enough the first time through, and because I didn't test thoroughly enough.

The first is that I was calling the wrong confirmation code; the resulting syntax survived
because `confirm()` is actually a legitimate function call in the context of the DOM Window,
a legacy survivor similar to `alert()` but with a yes/no return value. Bleah.

The second is that the confirm code doesn't appear to pass back a dictionary with the
`{ items: Array<Provider> }` list, it passes back just the `items` as an Array.
2023-11-07 18:10:43 +00:00

253 lines
9.7 KiB
TypeScript

import "@goauthentik/admin/applications/ProviderSelectModal";
import { iconHelperText } from "@goauthentik/admin/helperText";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-file-input";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
import { rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/ModalForm";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/ProxyForm";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
Application,
CapabilitiesEnum,
CoreApi,
PolicyEngineMode,
Provider,
} from "@goauthentik/api";
import "./components/ak-backchannel-input";
import "./components/ak-provider-search-input";
export const policyOptions = [
{
label: "any",
value: PolicyEngineMode.Any,
default: true,
description: html`${msg("Any policy must match to grant access")}`,
},
{
label: "all",
value: PolicyEngineMode.All,
description: html`${msg("All policies must match to grant access")}`,
},
];
@customElement("ak-application-form")
export class ApplicationForm extends ModelForm<Application, string> {
constructor() {
super();
this.handleConfirmBackchannelProviders = this.handleConfirmBackchannelProviders.bind(this);
this.makeRemoveBackchannelProviderHandler =
this.makeRemoveBackchannelProviderHandler.bind(this);
}
async loadInstance(pk: string): Promise<Application> {
const app = await new CoreApi(DEFAULT_CONFIG).coreApplicationsRetrieve({
slug: pk,
});
this.clearIcon = false;
this.backchannelProviders = app.backchannelProvidersObj || [];
return app;
}
@property({ attribute: false })
provider?: number;
@state()
backchannelProviders: Provider[] = [];
@property({ type: Boolean })
clearIcon = false;
getSuccessMessage(): string {
if (this.instance) {
return msg("Successfully updated application.");
} else {
return msg("Successfully created application.");
}
}
async send(data: Application): Promise<Application | void> {
let app: Application;
data.backchannelProviders = this.backchannelProviders.map((p) => p.pk);
if (this.instance) {
app = await new CoreApi(DEFAULT_CONFIG).coreApplicationsUpdate({
slug: this.instance.slug,
applicationRequest: data,
});
} else {
app = await new CoreApi(DEFAULT_CONFIG).coreApplicationsCreate({
applicationRequest: data,
});
}
const c = await config();
if (c.capabilities.includes(CapabilitiesEnum.CanSaveMedia)) {
const icon = this.getFormFiles()["metaIcon"];
if (icon || this.clearIcon) {
await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconCreate({
slug: app.slug,
file: icon,
clear: this.clearIcon,
});
}
} else {
await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconUrlCreate({
slug: app.slug,
filePathRequest: {
url: data.metaIcon || "",
},
});
}
return app;
}
handleConfirmBackchannelProviders(items: Provider[]) {
this.backchannelProviders = items;
this.requestUpdate();
return Promise.resolve();
}
makeRemoveBackchannelProviderHandler(provider: Provider) {
return () => {
const idx = this.backchannelProviders.indexOf(provider);
this.backchannelProviders.splice(idx, 1);
this.requestUpdate();
};
}
handleClearIcon(ev: Event) {
ev.stopPropagation();
if (!(ev instanceof InputEvent) || !ev.target) {
return;
}
this.clearIcon = !!(ev.target as HTMLInputElement).checked;
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-text-input
name="name"
value=${this.instance?.name}
label=${msg("Name")}
required
help=${msg("Application's display Name.")}
></ak-text-input>
<ak-text-input
name="slug"
value=${this.instance?.slug}
label=${msg("Slug")}
required
help=${msg("Internal application name used in URLs.")}
></ak-text-input>
<ak-text-input
name="group"
value=${this.instance?.group}
label=${msg("Group")}
help=${msg(
"Optionally enter a group name. Applications with identical groups are shown grouped together.",
)}
></ak-text-input>
<ak-provider-search-input
name="provider"
label=${msg("Provider")}
value=${this.instance?.provider}
help=${msg("Select a provider that this application should use.")}
blankable
></ak-provider-search-input>
<ak-backchannel-providers-input
name="backchannelProviders"
label=${msg("Backchannel Providers")}
help=${msg(
"Select backchannel providers which augment the functionality of the main provider.",
)}
.providers=${this.backchannelProviders}
.confirm=${this.handleConfirmBackchannelProviders}
.remover=${this.makeRemoveBackchannelProviderHandler}
.tooltip=${html`<pf-tooltip
position="top"
content=${msg("Add provider")}
></pf-tooltip>`}
>
</ak-backchannel-providers-input>
<ak-radio-input
label=${msg("Policy engine mode")}
required
name="policyEngineMode"
.options=${policyOptions}
.value=${this.instance?.policyEngineMode}
></ak-radio-input>
<ak-form-group>
<span slot="header"> ${msg("UI settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="metaLaunchUrl"
label=${msg("Launch URL")}
value=${ifDefined(this.instance?.metaLaunchUrl)}
help=${msg(
"If left empty, authentik will try to extract the launch URL based on the selected provider.",
)}
></ak-text-input>
<ak-switch-input
name="openInNewTab"
?checked=${first(this.instance?.openInNewTab, false)}
label=${msg("Open in new tab")}
help=${msg(
"If checked, the launch URL will open in a new browser tab or window from the user's application library.",
)}
>
</ak-switch-input>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanSaveMedia)
? html`<ak-file-input
label="${msg("Icon")}"
name="metaIcon"
value=${this.instance?.metaIcon}
current=${msg("Currently set to:")}
></ak-file-input>
${this.instance?.metaIcon
? html`
<ak-switch-input
name=""
label=${msg("Clear icon")}
help=${msg("Delete currently set icon.")}
@change=${this.handleClearIcon}
></ak-switch-input>
`
: html``}`
: html` <ak-text-input
label=${msg("Icon")}
name="metaIcon"
value=${first(this.instance?.metaIcon, "")}
help=${iconHelperText}
>
</ak-text-input>`}
<ak-text-input
label=${msg("Publisher")}
name="metaPublisher"
value="${ifDefined(this.instance?.metaPublisher)}"
></ak-text-input>
<ak-textarea-input
label=${msg("Description")}
name="metaDescription"
value=${ifDefined(this.instance?.metaDescription)}
></ak-textarea-input>
</div>
</ak-form-group>
</form>`;
}
}