web: Clean up error handling. Prep for permission checks. - Add clearer reporting for API and network errors. - Tidy error checking. - Partial type safety for events.
375 lines
15 KiB
TypeScript
375 lines
15 KiB
TypeScript
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<ProviderModelEnum, "11184809">;
|
|
|
|
const providerMap: Map<string, string> = Object.values(ProviderModelEnum)
|
|
.filter((value) => /^authentik_providers_/.test(value) && /provider$/.test(value))
|
|
.reduce((acc: Map<string, string>, value) => {
|
|
acc.set(value.split(".")[1], value);
|
|
return acc;
|
|
}, new Map());
|
|
|
|
type NonEmptyArray<T> = [T, ...T[]];
|
|
|
|
type MaybeTemplateResult = TemplateResult | typeof nothing;
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const isNotEmpty = (arr: any): arr is NonEmptyArray<any> => Array.isArray(arr) && arr.length > 0;
|
|
|
|
const cleanApplication = (app: Partial<ApplicationRequest>): 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`<div data-ouid-component-state=${this.state} class="ak-wizard-main-content">
|
|
<div class="pf-l-bullseye">
|
|
<div class="pf-c-empty-state pf-m-lg">
|
|
<div class="pf-c-empty-state__content">
|
|
<i class="fas ${icon} pf-c-empty-state__icon" aria-hidden="true"></i>
|
|
<h1 data-ouia-commit-state=${state} class="pf-c-title pf-m-lg">${label}</h1>
|
|
${extraInfo}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
renderError() {
|
|
const { errors } = this.wizard;
|
|
|
|
if (Object.keys(errors).length === 0) return nothing;
|
|
|
|
return html` <hr class="pf-c-divider" />
|
|
${match(errors)
|
|
.with(
|
|
{ app: P.nonNullable },
|
|
() =>
|
|
html`<p>${msg("There was an error in the application.")}</p>
|
|
<p>
|
|
<a @click=${WizardNavigationEvent.toListener(this, "application")}>
|
|
${msg("Review the application.")}
|
|
</a>
|
|
</p>`,
|
|
)
|
|
.with(
|
|
{ provider: P.nonNullable },
|
|
() =>
|
|
html`<p>${msg("There was an error in the provider.")}</p>
|
|
<p>
|
|
<a @click=${WizardNavigationEvent.toListener(this, "provider")}
|
|
>${msg("Review the provider.")}</a
|
|
>
|
|
</p>`,
|
|
)
|
|
.with(
|
|
{ detail: P.nonNullable },
|
|
() =>
|
|
html`<p>
|
|
${msg(
|
|
"There was an error. Please go back and review the application.",
|
|
)}:
|
|
${errors.detail}
|
|
</p>`,
|
|
)
|
|
.with(
|
|
{
|
|
nonFieldErrors: P.when(isNotEmpty),
|
|
},
|
|
() =>
|
|
html`<p>${msg("There was an error:")}:</p>
|
|
<ul>
|
|
${(errors.nonFieldErrors ?? []).map(
|
|
(reason) => html`<li>${reason}</li>`,
|
|
)}
|
|
</ul>
|
|
<p>${msg("Please go back and review the application.")}</p>`,
|
|
)
|
|
.otherwise(
|
|
() =>
|
|
html`<p>
|
|
${msg(
|
|
"There was an error creating the application, but no error message was sent. Please review the server logs.",
|
|
)}
|
|
</p>`,
|
|
)}`;
|
|
}
|
|
|
|
renderReview(app: Partial<ApplicationRequest>, 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`
|
|
<div class="ak-wizard-main-content">
|
|
<ak-wizard-title>${msg("Review the Application and Provider")}</ak-wizard-title>
|
|
<h2 class="pf-c-title pf-m-xl">${msg("Application")}</h2>
|
|
<dl class="pf-c-description-list">
|
|
<div class="pf-c-description-list__group">
|
|
<dt class="pf-c-description-list__term">${msg("Name")}</dt>
|
|
<dt class="pf-c-description-list__description">${app.name}</dt>
|
|
</div>
|
|
<div class="pf-c-description-list__group">
|
|
<dt class="pf-c-description-list__term">${msg("Group")}</dt>
|
|
<dt class="pf-c-description-list__description">${app.group || msg("-")}</dt>
|
|
</div>
|
|
<div class="pf-c-description-list__group">
|
|
<dt class="pf-c-description-list__term">${msg("Policy engine mode")}</dt>
|
|
<dt class="pf-c-description-list__description">
|
|
${app.policyEngineMode?.toUpperCase()}
|
|
</dt>
|
|
</div>
|
|
${(app.metaLaunchUrl ?? "").trim() !== ""
|
|
? html` <div class="pf-c-description-list__group">
|
|
<dt class="pf-c-description-list__term">${msg("Launch URL")}</dt>
|
|
<dt class="pf-c-description-list__description">
|
|
${app.metaLaunchUrl}
|
|
</dt>
|
|
</div>`
|
|
: nothing}
|
|
</dl>
|
|
${renderer
|
|
? html` <h2 class="pf-c-title pf-m-xl pf-u-pt-xl">${msg("Provider")}</h2>
|
|
${renderer(provider)}`
|
|
: nothing}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|