import "@goauthentik/admin/applications/wizard/ak-wizard-title.js"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { parseAPIResponseError } from "@goauthentik/common/errors/network"; import { WizardNavigationEvent } from "@goauthentik/components/ak-wizard/events.js"; import { type WizardButton } from "@goauthentik/components/ak-wizard/types"; import { showAPIErrorMessage } from "@goauthentik/elements/messages/MessageContainer"; import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; import { P, match } from "ts-pattern"; import { msg } from "@lit/localize"; import { TemplateResult, css, html, nothing } from "lit"; import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; // import { map } from "lit/directives/map.js"; import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css"; import PFProgressStepper from "@patternfly/patternfly/components/ProgressStepper/progress-stepper.css"; import PFTitle from "@patternfly/patternfly/components/Title/title.css"; import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css"; import { type ApplicationRequest, CoreApi, type ModelRequest, type PolicyBinding, ProviderModelEnum, ProxyMode, type ProxyProviderRequest, type TransactionApplicationRequest, type TransactionApplicationResponse, type TransactionPolicyBindingRequest, instanceOfValidationError, } from "@goauthentik/api"; import { ApplicationWizardStep } from "../ApplicationWizardStep.js"; import { OneOfProvider, isApplicationTransactionValidationError } from "../types.js"; import { providerRenderers } from "./SubmitStepOverviewRenderers.js"; const _submitStates = ["reviewing", "running", "submitted"] as const; type SubmitStates = (typeof _submitStates)[number]; type StrictProviderModelEnum = Exclude; const providerMap: Map = Object.values(ProviderModelEnum) .filter((value) => /^authentik_providers_/.test(value) && /provider$/.test(value)) .reduce((acc: Map, value) => { acc.set(value.split(".")[1], value); return acc; }, new Map()); type NonEmptyArray = [T, ...T[]]; type MaybeTemplateResult = TemplateResult | typeof nothing; // eslint-disable-next-line @typescript-eslint/no-explicit-any const isNotEmpty = (arr: any): arr is NonEmptyArray => Array.isArray(arr) && arr.length > 0; const cleanApplication = (app: Partial): ApplicationRequest => ({ name: "", slug: "", ...app, }); const cleanBinding = (binding: PolicyBinding): TransactionPolicyBindingRequest => ({ policy: binding.policy, group: binding.group, user: binding.user, negate: binding.negate, enabled: binding.enabled, order: binding.order, timeout: binding.timeout, failureResult: binding.failureResult, }); @customElement("ak-application-wizard-submit-step") export class ApplicationWizardSubmitStep extends CustomEmitterElement(ApplicationWizardStep) { static get styles() { return [ ...ApplicationWizardStep.styles, PFBullseye, PFEmptyState, PFTitle, PFProgressStepper, PFDescriptionList, css` .ak-wizard-main-content .pf-c-title { padding-bottom: var(--pf-global--spacer--md); padding-top: var(--pf-global--spacer--md); } `, ]; } label = msg("Review and Submit Application"); @state() state: SubmitStates = "reviewing"; async send() { const app = this.wizard.app; const provider = this.wizard.provider as ModelRequest; if (app === undefined) { throw new Error("Reached the submit state with the app undefined"); } if (provider === undefined) { throw new Error("Reached the submit state with the provider undefined"); } // Stringly-based API. Not the best, but it works. Just be aware that it is // stringly-based. const providerModel = providerMap.get(this.wizard.providerModel) as StrictProviderModelEnum; provider.providerModel = providerModel; // Special case for the Proxy provider. if (this.wizard.providerModel === "proxyprovider") { (provider as ProxyProviderRequest).mode = this.wizard.proxyMode; if ((provider as ProxyProviderRequest).mode !== ProxyMode.ForwardDomain) { (provider as ProxyProviderRequest).cookieDomain = ""; } } const request: TransactionApplicationRequest = { app: cleanApplication(this.wizard.app), providerModel, provider, policyBindings: (this.wizard.bindings ?? []).map(cleanBinding), }; this.state = "running"; return new CoreApi(DEFAULT_CONFIG) .coreTransactionalApplicationsUpdate({ transactionApplicationRequest: request, }) .then((_response: TransactionApplicationResponse) => { this.dispatchCustomEvent(EVENT_REFRESH); this.state = "submitted"; }) .catch(async (error) => { const parsedError = await parseAPIResponseError(error); if (!instanceOfValidationError(parsedError)) { showAPIErrorMessage(parsedError); return; } if (isApplicationTransactionValidationError(parsedError)) { // THIS is a really gross special case; if the user is duplicating the name of an existing provider, the error appears on the `app` (!) error object. // We have to move that to the `provider.name` error field so it shows up in the right place. if (Array.isArray(parsedError.app?.provider)) { const providerError = parsedError.app.provider; parsedError.provider = { ...parsedError.provider, name: providerError, }; delete parsedError.app.provider; if (Object.keys(parsedError.app).length === 0) { delete parsedError.app; } } } this.handleUpdate({ errors: parsedError }); this.state = "reviewing"; }); } override handleButton(button: WizardButton) { match([button.kind, this.state]) .with([P.union("back", "cancel"), P._], () => { super.handleButton(button); }) .with(["close", "submitted"], () => { super.handleButton(button); }) .with(["next", "reviewing"], () => { this.send(); }) .with([P._, "running"], () => { throw new Error("No buttons should be showing when running submit phase"); }) .otherwise(() => { throw new Error( `Submit step received incoherent button/state combination: ${[button.kind, state]}`, ); }); } get buttons(): WizardButton[] { const forReview: WizardButton[] = [ { kind: "next", label: msg("Submit"), destination: "here" }, { kind: "back", destination: "bindings" }, { kind: "cancel" }, ]; const forSubmit: WizardButton[] = [{ kind: "close" }]; return match(this.state) .with("submitted", () => forSubmit) .with("running", () => []) .with("reviewing", () => forReview) .exhaustive(); } renderInfo( state: string, label: string, icons: string[], extraInfo: MaybeTemplateResult = nothing, ) { const icon = classMap(icons.reduce((acc, icon) => ({ ...acc, [icon]: true }), {})); return html`

${label}

${extraInfo}
`; } renderError() { const { errors } = this.wizard; if (Object.keys(errors).length === 0) return nothing; return html`
${match(errors) .with( { app: P.nonNullable }, () => html`

${msg("There was an error in the application.")}

${msg("Review the application.")}

`, ) .with( { provider: P.nonNullable }, () => html`

${msg("There was an error in the provider.")}

${msg("Review the provider.")}

`, ) .with( { detail: P.nonNullable }, () => html`

${msg( "There was an error. Please go back and review the application.", )}: ${errors.detail}

`, ) .with( { nonFieldErrors: P.when(isNotEmpty), }, () => html`

${msg("There was an error:")}:

    ${(errors.nonFieldErrors ?? []).map( (reason) => html`
  • ${reason}
  • `, )}

${msg("Please go back and review the application.")}

`, ) .otherwise( () => html`

${msg( "There was an error creating the application, but no error message was sent. Please review the server logs.", )}

`, )}`; } renderReview(app: Partial, provider: OneOfProvider) { const renderer = providerRenderers.get(this.wizard.providerModel); if (!renderer) { throw new Error( `Provider ${this.wizard.providerModel ?? "-- undefined --"} has no summary renderer.`, ); } return html`
${msg("Review the Application and Provider")}

${msg("Application")}

${msg("Name")}
${app.name}
${msg("Group")}
${app.group || msg("-")}
${msg("Policy engine mode")}
${app.policyEngineMode?.toUpperCase()}
${(app.metaLaunchUrl ?? "").trim() !== "" ? html`
${msg("Launch URL")}
${app.metaLaunchUrl}
` : nothing}
${renderer ? html`

${msg("Provider")}

${renderer(provider)}` : nothing}
`; } renderMain() { const app = this.wizard.app; const provider = this.wizard.provider; if (!(this.wizard && app && provider)) { throw new Error("Submit step received uninitialized wizard context"); } // An empty object is truthy, an empty array is falsey. *WAT JavaScript*. const keys = Object.keys(this.wizard.errors); return match([this.state, keys]) .with(["submitted", P._], () => this.renderInfo("success", msg("Your application has been saved"), [ "fa-check-circle", "pf-m-success", ]), ) .with(["running", P._], () => this.renderInfo("running", msg("Saving application..."), ["fa-cogs", "pf-m-info"]), ) .with(["reviewing", []], () => this.renderReview(app, provider)) .with(["reviewing", [P.any, ...P.array()]], () => this.renderInfo( "error", msg("authentik was unable to complete this process."), ["fa-times-circle", "pf-m-danger"], this.renderError(), ), ) .exhaustive(); } } declare global { interface HTMLElementTagNameMap { "ak-application-wizard-submit-step": ApplicationWizardSubmitStep; } }