web: re-organise frontend and cleanup common code (#3572)
* fix repo in api client Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web: re-organise files to match their interface Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * core: include version in script tags Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * cleanup maybe broken Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * revert rename Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web: get rid of Client.ts Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * move more to common Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * more moving Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * format Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * unfuck files that vscode fucked, thanks Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * move more Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * finish moving (maybe) Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * ok more moving Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * fix more stuff that vs code destroyed Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * get rid "web" prefix for virtual package Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * fix locales Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * use custom base element Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * fix css file Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * don't run autoDetectLanguage when importing locale Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * fix circular dependencies Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web: fix build Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
155
web/src/admin/flows/BoundStagesList.ts
Normal file
155
web/src/admin/flows/BoundStagesList.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import "@goauthentik/admin/flows/StageBindingForm";
|
||||
import "@goauthentik/admin/policies/BoundPoliciesList";
|
||||
import "@goauthentik/admin/stages/StageWizard";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||
import "@goauthentik/elements/forms/ModalForm";
|
||||
import "@goauthentik/elements/forms/ProxyForm";
|
||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
|
||||
|
||||
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 { FlowStageBinding, FlowsApi } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-bound-stages-list")
|
||||
export class BoundStagesList extends Table<FlowStageBinding> {
|
||||
expandable = true;
|
||||
checkbox = true;
|
||||
|
||||
@property()
|
||||
target?: string;
|
||||
|
||||
async apiEndpoint(page: number): Promise<PaginatedResponse<FlowStageBinding>> {
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsList({
|
||||
target: this.target || "",
|
||||
ordering: "order",
|
||||
page: page,
|
||||
pageSize: (await uiConfig()).pagination.perPage,
|
||||
});
|
||||
}
|
||||
|
||||
columns(): TableColumn[] {
|
||||
return [
|
||||
new TableColumn(t`Order`),
|
||||
new TableColumn(t`Name`),
|
||||
new TableColumn(t`Type`),
|
||||
new TableColumn(t`Actions`),
|
||||
];
|
||||
}
|
||||
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${t`Stage binding(s)`}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: FlowStageBinding) => {
|
||||
return [
|
||||
{ key: t`Stage`, value: item.stageObj?.name || "" },
|
||||
{ key: t`Stage type`, value: item.stageObj?.verboseName || "" },
|
||||
];
|
||||
}}
|
||||
.usedBy=${(item: FlowStageBinding) => {
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsUsedByList({
|
||||
fsbUuid: item.pk,
|
||||
});
|
||||
}}
|
||||
.delete=${(item: FlowStageBinding) => {
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsDestroy({
|
||||
fsbUuid: item.pk,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
|
||||
${t`Delete`}
|
||||
</button>
|
||||
</ak-forms-delete-bulk>`;
|
||||
}
|
||||
|
||||
row(item: FlowStageBinding): TemplateResult[] {
|
||||
return [
|
||||
html`${item.order}`,
|
||||
html`${item.stageObj?.name}`,
|
||||
html`${item.stageObj?.verboseName}`,
|
||||
html` <ak-forms-modal>
|
||||
<span slot="submit"> ${t`Update`} </span>
|
||||
<span slot="header"> ${t`Update ${item.stageObj?.verboseName}`} </span>
|
||||
<ak-proxy-form
|
||||
slot="form"
|
||||
.args=${{
|
||||
instancePk: item.stage,
|
||||
}}
|
||||
type=${ifDefined(item.stageObj?.component)}
|
||||
>
|
||||
</ak-proxy-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
${t`Edit Stage`}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit"> ${t`Update`} </span>
|
||||
<span slot="header"> ${t`Update Stage binding`} </span>
|
||||
<ak-stage-binding-form slot="form" .instancePk=${item.pk}>
|
||||
</ak-stage-binding-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
${t`Edit Binding`}
|
||||
</button>
|
||||
</ak-forms-modal>`,
|
||||
];
|
||||
}
|
||||
|
||||
renderExpanded(item: FlowStageBinding): TemplateResult {
|
||||
return html` <td></td>
|
||||
<td role="cell" colspan="4">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<div class="pf-c-content">
|
||||
<p>
|
||||
${t`These bindings control if this stage will be applied to the flow.`}
|
||||
</p>
|
||||
<ak-bound-policies-list .target=${item.policybindingmodelPtrId}>
|
||||
</ak-bound-policies-list>
|
||||
</div>
|
||||
</div>
|
||||
</td>`;
|
||||
}
|
||||
|
||||
renderEmpty(): TemplateResult {
|
||||
return super.renderEmpty(html`<ak-empty-state
|
||||
header=${t`No Stages bound`}
|
||||
icon="pf-icon-module"
|
||||
>
|
||||
<div slot="body">${t`No stages are currently bound to this flow.`}</div>
|
||||
<div slot="primary">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit"> ${t`Create`} </span>
|
||||
<span slot="header"> ${t`Create Stage binding`} </span>
|
||||
<ak-stage-binding-form slot="form" targetPk=${ifDefined(this.target)}>
|
||||
</ak-stage-binding-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||
${t`Bind stage`}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
</div>
|
||||
</ak-empty-state>`);
|
||||
}
|
||||
|
||||
renderToolbar(): TemplateResult {
|
||||
return html`
|
||||
<ak-forms-modal>
|
||||
<span slot="submit"> ${t`Create`} </span>
|
||||
<span slot="header"> ${t`Create Stage binding`} </span>
|
||||
<ak-stage-binding-form slot="form" targetPk=${ifDefined(this.target)}>
|
||||
</ak-stage-binding-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">${t`Bind stage`}</button>
|
||||
</ak-forms-modal>
|
||||
<ak-stage-wizard createText=${t`Create Stage`}></ak-stage-wizard>
|
||||
${super.renderToolbar()}
|
||||
`;
|
||||
}
|
||||
}
|
||||
110
web/src/admin/flows/FlowDiagram.ts
Normal file
110
web/src/admin/flows/FlowDiagram.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import FlowChart from "flowchart.js";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import { FlowsApi } from "@goauthentik/api";
|
||||
|
||||
export const FONT_COLOUR_DARK_MODE = "#fafafa";
|
||||
export const FONT_COLOUR_LIGHT_MODE = "#151515";
|
||||
export const FILL_DARK_MODE = "#18191a";
|
||||
export const FILL_LIGHT_MODE = "#f0f0f0";
|
||||
|
||||
@customElement("ak-flow-diagram")
|
||||
export class FlowDiagram extends AKElement {
|
||||
_flowSlug?: string;
|
||||
|
||||
@property()
|
||||
set flowSlug(value: string) {
|
||||
this._flowSlug = value;
|
||||
this.diagram = undefined;
|
||||
new FlowsApi(DEFAULT_CONFIG)
|
||||
.flowsInstancesDiagramRetrieve({
|
||||
slug: value,
|
||||
})
|
||||
.then((data) => {
|
||||
this.diagram = data.diagram;
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
diagram?: string;
|
||||
|
||||
@property()
|
||||
fontColour: string = FONT_COLOUR_DARK_MODE;
|
||||
|
||||
@property()
|
||||
fill: string = FILL_DARK_MODE;
|
||||
|
||||
handlerBound = false;
|
||||
|
||||
createRenderRoot(): Element | ShadowRoot {
|
||||
return this;
|
||||
}
|
||||
|
||||
get isInViewport(): boolean {
|
||||
const rect = this.getBoundingClientRect();
|
||||
return !(rect.x + rect.y + rect.width + rect.height === 0);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const matcher = window.matchMedia("(prefers-color-scheme: light)");
|
||||
const handler = (ev?: MediaQueryListEvent) => {
|
||||
if (ev?.matches || matcher.matches) {
|
||||
this.fontColour = FONT_COLOUR_LIGHT_MODE;
|
||||
this.fill = FILL_LIGHT_MODE;
|
||||
} else {
|
||||
this.fontColour = FONT_COLOUR_DARK_MODE;
|
||||
this.fill = FILL_DARK_MODE;
|
||||
}
|
||||
this.requestUpdate();
|
||||
};
|
||||
matcher.addEventListener("change", handler);
|
||||
handler();
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
if (this.handlerBound) return;
|
||||
window.addEventListener(EVENT_REFRESH, this.refreshHandler);
|
||||
this.handlerBound = true;
|
||||
}
|
||||
|
||||
refreshHandler = (): void => {
|
||||
if (!this._flowSlug) return;
|
||||
this.flowSlug = this._flowSlug;
|
||||
};
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener(EVENT_REFRESH, this.refreshHandler);
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
this.querySelectorAll("*").forEach((el) => {
|
||||
try {
|
||||
el.remove();
|
||||
} catch {
|
||||
console.debug(`authentik/flow/diagram: failed to remove element ${el}`);
|
||||
}
|
||||
});
|
||||
if (this.diagram) {
|
||||
const diagram = FlowChart.parse(this.diagram);
|
||||
diagram.drawSVG(this, {
|
||||
"font-color": this.fontColour,
|
||||
"line-color": "#bebebe",
|
||||
"element-color": "#bebebe",
|
||||
"fill": this.fill,
|
||||
"yes-text": "Policy passes",
|
||||
"no-text": "Policy denies",
|
||||
});
|
||||
return html``;
|
||||
}
|
||||
return html`<ak-empty-state ?loading=${true}></ak-empty-state>`;
|
||||
}
|
||||
}
|
||||
331
web/src/admin/flows/FlowForm.ts
Normal file
331
web/src/admin/flows/FlowForm.ts
Normal file
@ -0,0 +1,331 @@
|
||||
import { DesignationToLabel, LayoutToLabel } from "@goauthentik/admin/flows/utils";
|
||||
import { DEFAULT_CONFIG, 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 { 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,
|
||||
DeniedActionEnum,
|
||||
Flow,
|
||||
FlowDesignationEnum,
|
||||
FlowsApi,
|
||||
LayoutEnum,
|
||||
PolicyEngineMode,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-flow-form")
|
||||
export class FlowForm extends ModelForm<Flow, string> {
|
||||
loadInstance(pk: string): Promise<Flow> {
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsInstancesRetrieve({
|
||||
slug: pk,
|
||||
});
|
||||
}
|
||||
|
||||
getSuccessMessage(): string {
|
||||
if (this.instance) {
|
||||
return t`Successfully updated flow.`;
|
||||
} else {
|
||||
return t`Successfully created flow.`;
|
||||
}
|
||||
}
|
||||
|
||||
@property({ type: Boolean })
|
||||
clearBackground = false;
|
||||
|
||||
send = async (data: Flow): Promise<void | Flow> => {
|
||||
let flow: Flow;
|
||||
if (this.instance) {
|
||||
flow = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesUpdate({
|
||||
slug: this.instance.slug,
|
||||
flowRequest: data,
|
||||
});
|
||||
} else {
|
||||
flow = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesCreate({
|
||||
flowRequest: data,
|
||||
});
|
||||
}
|
||||
const c = await config();
|
||||
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) {
|
||||
const icon = this.getFormFiles()["background"];
|
||||
if (icon || this.clearBackground) {
|
||||
await new FlowsApi(DEFAULT_CONFIG).flowsInstancesSetBackgroundCreate({
|
||||
slug: flow.slug,
|
||||
file: icon,
|
||||
clear: this.clearBackground,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await new FlowsApi(DEFAULT_CONFIG).flowsInstancesSetBackgroundUrlCreate({
|
||||
slug: flow.slug,
|
||||
filePathRequest: {
|
||||
url: data.background || "",
|
||||
},
|
||||
});
|
||||
}
|
||||
return flow;
|
||||
};
|
||||
|
||||
renderDesignations(): TemplateResult {
|
||||
return html`
|
||||
<option
|
||||
value=${FlowDesignationEnum.Authentication}
|
||||
?selected=${this.instance?.designation === FlowDesignationEnum.Authentication}
|
||||
>
|
||||
${DesignationToLabel(FlowDesignationEnum.Authentication)}
|
||||
</option>
|
||||
<option
|
||||
value=${FlowDesignationEnum.Authorization}
|
||||
?selected=${this.instance?.designation === FlowDesignationEnum.Authorization}
|
||||
>
|
||||
${DesignationToLabel(FlowDesignationEnum.Authorization)}
|
||||
</option>
|
||||
<option
|
||||
value=${FlowDesignationEnum.Enrollment}
|
||||
?selected=${this.instance?.designation === FlowDesignationEnum.Enrollment}
|
||||
>
|
||||
${DesignationToLabel(FlowDesignationEnum.Enrollment)}
|
||||
</option>
|
||||
<option
|
||||
value=${FlowDesignationEnum.Invalidation}
|
||||
?selected=${this.instance?.designation === FlowDesignationEnum.Invalidation}
|
||||
>
|
||||
${DesignationToLabel(FlowDesignationEnum.Invalidation)}
|
||||
</option>
|
||||
<option
|
||||
value=${FlowDesignationEnum.Recovery}
|
||||
?selected=${this.instance?.designation === FlowDesignationEnum.Recovery}
|
||||
>
|
||||
${DesignationToLabel(FlowDesignationEnum.Recovery)}
|
||||
</option>
|
||||
<option
|
||||
value=${FlowDesignationEnum.StageConfiguration}
|
||||
?selected=${this.instance?.designation === FlowDesignationEnum.StageConfiguration}
|
||||
>
|
||||
${DesignationToLabel(FlowDesignationEnum.StageConfiguration)}
|
||||
</option>
|
||||
<option
|
||||
value=${FlowDesignationEnum.Unenrollment}
|
||||
?selected=${this.instance?.designation === FlowDesignationEnum.Unenrollment}
|
||||
>
|
||||
${DesignationToLabel(FlowDesignationEnum.Unenrollment)}
|
||||
</option>
|
||||
`;
|
||||
}
|
||||
|
||||
renderDeniedAction(): TemplateResult {
|
||||
return html` <option
|
||||
value=${DeniedActionEnum.MessageContinue}
|
||||
?selected=${this.instance?.deniedAction === DeniedActionEnum.MessageContinue}
|
||||
>
|
||||
${t`MESSAGE_CONTINUE will follow the ?next parameter if set, otherwise show a message.`}
|
||||
</option>
|
||||
<option
|
||||
value=${DeniedActionEnum.Continue}
|
||||
?selected=${this.instance?.deniedAction === DeniedActionEnum.Continue}
|
||||
>
|
||||
${t`CONTINUE will either follow the ?next parameter or redirect to the default interface.`}
|
||||
</option>
|
||||
<option
|
||||
value=${DeniedActionEnum.Message}
|
||||
?selected=${this.instance?.deniedAction === DeniedActionEnum.Message}
|
||||
>
|
||||
${t`MESSAGE will notify the user the flow isn't applicable.`}
|
||||
</option>`;
|
||||
}
|
||||
|
||||
renderLayout(): TemplateResult {
|
||||
return html`
|
||||
<option
|
||||
value=${LayoutEnum.Stacked}
|
||||
?selected=${this.instance?.layout === LayoutEnum.Stacked}
|
||||
>
|
||||
${LayoutToLabel(LayoutEnum.Stacked)}
|
||||
</option>
|
||||
<option
|
||||
value=${LayoutEnum.ContentLeft}
|
||||
?selected=${this.instance?.layout === LayoutEnum.ContentLeft}
|
||||
>
|
||||
${LayoutToLabel(LayoutEnum.ContentLeft)}
|
||||
</option>
|
||||
<option
|
||||
value=${LayoutEnum.ContentRight}
|
||||
?selected=${this.instance?.layout === LayoutEnum.ContentRight}
|
||||
>
|
||||
${LayoutToLabel(LayoutEnum.ContentRight)}
|
||||
</option>
|
||||
<option
|
||||
value=${LayoutEnum.SidebarLeft}
|
||||
?selected=${this.instance?.layout === LayoutEnum.SidebarLeft}
|
||||
>
|
||||
${LayoutToLabel(LayoutEnum.SidebarLeft)}
|
||||
</option>
|
||||
<option
|
||||
value=${LayoutEnum.SidebarRight}
|
||||
?selected=${this.instance?.layout === LayoutEnum.SidebarRight}
|
||||
>
|
||||
${LayoutToLabel(LayoutEnum.SidebarRight)}
|
||||
</option>
|
||||
`;
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.name)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`Title`} ?required=${true} name="title">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.title)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">${t`Shown as the Title in Flow pages.`}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`Slug`} ?required=${true} name="slug">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.slug)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">${t`Visible in the URL.`}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Policy engine mode`}
|
||||
?required=${true}
|
||||
name="policyEngineMode"
|
||||
>
|
||||
<select class="pf-c-form-control">
|
||||
<option
|
||||
value=${PolicyEngineMode.Any}
|
||||
?selected=${this.instance?.policyEngineMode === PolicyEngineMode.Any}
|
||||
>
|
||||
${t`ANY, any policy must match to grant access.`}
|
||||
</option>
|
||||
<option
|
||||
value=${PolicyEngineMode.All}
|
||||
?selected=${this.instance?.policyEngineMode === PolicyEngineMode.All}
|
||||
>
|
||||
${t`ALL, all policies must match to grant access.`}
|
||||
</option>
|
||||
</select>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Designation`}
|
||||
?required=${true}
|
||||
name="designation"
|
||||
>
|
||||
<select class="pf-c-form-control">
|
||||
<option value="" ?selected=${this.instance?.designation === undefined}>
|
||||
---------
|
||||
</option>
|
||||
${this.renderDesignations()}
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Denied action`}
|
||||
?required=${true}
|
||||
name="deniedAction"
|
||||
>
|
||||
<select class="pf-c-form-control">
|
||||
${this.renderDeniedAction()}
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Decides the response when a policy denies access to this flow for a user.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`Layout`} ?required=${true} name="layout">
|
||||
<select class="pf-c-form-control">
|
||||
${this.renderLayout()}
|
||||
</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"
|
||||
>
|
||||
<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}
|
||||
</p>
|
||||
`
|
||||
: html``}
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Background shown during execution.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
${this.instance?.background
|
||||
? html`
|
||||
<ak-form-element-horizontal>
|
||||
<div class="pf-c-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="pf-c-check__input"
|
||||
@change=${(ev: Event) => {
|
||||
const target = ev.target as HTMLInputElement;
|
||||
this.clearBackground = target.checked;
|
||||
}}
|
||||
/>
|
||||
<label class="pf-c-check__label">
|
||||
${t`Clear background image`}
|
||||
</label>
|
||||
</div>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Delete currently set background image.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
`
|
||||
: html``}`;
|
||||
}
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${t`Background`}
|
||||
name="background"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.background, "")}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Background shown during execution.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>`;
|
||||
}),
|
||||
)}
|
||||
<ak-form-element-horizontal name="compatibilityMode">
|
||||
<div class="pf-c-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="pf-c-check__input"
|
||||
?checked=${first(this.instance?.compatibilityMode, false)}
|
||||
/>
|
||||
<label class="pf-c-check__label"> ${t`Compatibility mode`} </label>
|
||||
</div>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Enable compatibility mode, increases compatibility with password managers on mobile devices.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
40
web/src/admin/flows/FlowImportForm.ts
Normal file
40
web/src/admin/flows/FlowImportForm.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { SentryIgnoredError } from "@goauthentik/common/errors";
|
||||
import { Form } from "@goauthentik/elements/forms/Form";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import { Flow, FlowsApi } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-flow-import-form")
|
||||
export class FlowImportForm extends Form<Flow> {
|
||||
getSuccessMessage(): string {
|
||||
return t`Successfully imported flow.`;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
send = (data: Flow): Promise<void> => {
|
||||
const file = this.getFormFiles()["flow"];
|
||||
if (!file) {
|
||||
throw new SentryIgnoredError("No form data");
|
||||
}
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsInstancesImportFlowCreate({
|
||||
file: file,
|
||||
});
|
||||
};
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${t`Flow`} name="flow">
|
||||
<input type="file" value="" class="pf-c-form-control" />
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`.yaml files, which can be found on goauthentik.io and can be exported by authentik.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
170
web/src/admin/flows/FlowListPage.ts
Normal file
170
web/src/admin/flows/FlowListPage.ts
Normal file
@ -0,0 +1,170 @@
|
||||
import "@goauthentik/admin/flows/FlowForm";
|
||||
import "@goauthentik/admin/flows/FlowImportForm";
|
||||
import { DesignationToLabel } from "@goauthentik/admin/flows/utils";
|
||||
import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||
import { groupBy } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
import "@goauthentik/elements/forms/ConfirmationForm";
|
||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||
import "@goauthentik/elements/forms/ModalForm";
|
||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||
import { TableColumn } from "@goauthentik/elements/table/Table";
|
||||
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
||||
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import { Flow, FlowsApi } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-flow-list")
|
||||
export class FlowListPage extends TablePage<Flow> {
|
||||
searchEnabled(): boolean {
|
||||
return true;
|
||||
}
|
||||
pageTitle(): string {
|
||||
return t`Flows`;
|
||||
}
|
||||
pageDescription(): string {
|
||||
return t`Flows describe a chain of Stages to authenticate, enroll or recover a user. Stages are chosen based on policies applied to them.`;
|
||||
}
|
||||
pageIcon(): string {
|
||||
return "pf-icon pf-icon-process-automation";
|
||||
}
|
||||
|
||||
checkbox = true;
|
||||
|
||||
@property()
|
||||
order = "slug";
|
||||
|
||||
async apiEndpoint(page: number): Promise<PaginatedResponse<Flow>> {
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
|
||||
ordering: this.order,
|
||||
page: page,
|
||||
pageSize: (await uiConfig()).pagination.perPage,
|
||||
search: this.search || "",
|
||||
});
|
||||
}
|
||||
|
||||
groupBy(items: Flow[]): [string, Flow[]][] {
|
||||
return groupBy(items, (flow) => {
|
||||
if (!flow.designation) {
|
||||
return "";
|
||||
}
|
||||
return DesignationToLabel(flow.designation);
|
||||
});
|
||||
}
|
||||
|
||||
columns(): TableColumn[] {
|
||||
return [
|
||||
new TableColumn(t`Identifier`, "slug"),
|
||||
new TableColumn(t`Name`, "name"),
|
||||
new TableColumn(t`Stages`),
|
||||
new TableColumn(t`Policies`),
|
||||
new TableColumn(t`Actions`),
|
||||
];
|
||||
}
|
||||
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${t`Flow(s)`}
|
||||
.objects=${this.selectedElements}
|
||||
.usedBy=${(item: Flow) => {
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsInstancesUsedByList({
|
||||
slug: item.slug,
|
||||
});
|
||||
}}
|
||||
.delete=${(item: Flow) => {
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsInstancesDestroy({
|
||||
slug: item.slug,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
|
||||
${t`Delete`}
|
||||
</button>
|
||||
</ak-forms-delete-bulk>`;
|
||||
}
|
||||
|
||||
row(item: Flow): TemplateResult[] {
|
||||
return [
|
||||
html`<div>
|
||||
<div>
|
||||
<a href="#/flow/flows/${item.slug}">
|
||||
<code>${item.slug}</code>
|
||||
</a>
|
||||
</div>
|
||||
<small>${item.title}</small>
|
||||
</div>`,
|
||||
html`${item.name}`,
|
||||
html`${Array.from(item.stages || []).length}`,
|
||||
html`${Array.from(item.policies || []).length}`,
|
||||
html` <ak-forms-modal>
|
||||
<span slot="submit"> ${t`Update`} </span>
|
||||
<span slot="header"> ${t`Update Flow`} </span>
|
||||
<ak-flow-form slot="form" .instancePk=${item.slug}> </ak-flow-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
<button
|
||||
class="pf-c-button pf-m-plain"
|
||||
@click=${() => {
|
||||
const finalURL = `${window.location.origin}/if/flow/${item.slug}/${AndNext(
|
||||
`${window.location.pathname}#${window.location.hash}`,
|
||||
)}`;
|
||||
window.open(finalURL, "_blank");
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-play"></i>
|
||||
</button>
|
||||
<a class="pf-c-button pf-m-plain" href=${item.exportUrl}>
|
||||
<i class="fas fa-download"></i>
|
||||
</a>`,
|
||||
];
|
||||
}
|
||||
|
||||
renderObjectCreate(): TemplateResult {
|
||||
return html`
|
||||
<ak-forms-modal>
|
||||
<span slot="submit"> ${t`Create`} </span>
|
||||
<span slot="header"> ${t`Create Flow`} </span>
|
||||
<ak-flow-form slot="form"> </ak-flow-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button>
|
||||
</ak-forms-modal>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit"> ${t`Import`} </span>
|
||||
<span slot="header"> ${t`Import Flow`} </span>
|
||||
<ak-flow-import-form slot="form"> </ak-flow-import-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">${t`Import`}</button>
|
||||
</ak-forms-modal>
|
||||
`;
|
||||
}
|
||||
|
||||
renderToolbar(): TemplateResult {
|
||||
return html`
|
||||
${super.renderToolbar()}
|
||||
<ak-forms-confirm
|
||||
successMessage=${t`Successfully cleared flow cache`}
|
||||
errorMessage=${t`Failed to delete flow cache`}
|
||||
action=${t`Clear cache`}
|
||||
.onConfirm=${() => {
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsInstancesCacheClearCreate();
|
||||
}}
|
||||
>
|
||||
<span slot="header"> ${t`Clear Flow cache`} </span>
|
||||
<p slot="body">
|
||||
${t`Are you sure you want to clear the flow cache?
|
||||
This will cause all flows to be re-evaluated on their next usage.`}
|
||||
</p>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary" type="button">
|
||||
${t`Clear cache`}
|
||||
</button>
|
||||
<div slot="modal"></div>
|
||||
</ak-forms-confirm>
|
||||
`;
|
||||
}
|
||||
}
|
||||
254
web/src/admin/flows/FlowViewPage.ts
Normal file
254
web/src/admin/flows/FlowViewPage.ts
Normal file
@ -0,0 +1,254 @@
|
||||
import "@goauthentik/admin/flows/BoundStagesList";
|
||||
import "@goauthentik/admin/flows/FlowDiagram";
|
||||
import "@goauthentik/admin/flows/FlowForm";
|
||||
import "@goauthentik/admin/policies/BoundPoliciesList";
|
||||
import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/PageHeader";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
import "@goauthentik/elements/events/ObjectChangelog";
|
||||
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import AKGlobal from "@goauthentik/common/styles/authentik.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { Flow, FlowsApi, ResponseError } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-flow-view")
|
||||
export class FlowViewPage extends AKElement {
|
||||
@property()
|
||||
set flowSlug(value: string) {
|
||||
new FlowsApi(DEFAULT_CONFIG)
|
||||
.flowsInstancesRetrieve({
|
||||
slug: value,
|
||||
})
|
||||
.then((flow) => {
|
||||
this.flow = flow;
|
||||
});
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
flow!: Flow;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
PFBase,
|
||||
PFPage,
|
||||
PFDescriptionList,
|
||||
PFButton,
|
||||
PFCard,
|
||||
PFContent,
|
||||
PFGrid,
|
||||
AKGlobal,
|
||||
].concat(
|
||||
css`
|
||||
img.pf-icon {
|
||||
max-height: 24px;
|
||||
}
|
||||
ak-tabs {
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.flow) {
|
||||
return html``;
|
||||
}
|
||||
return html`<ak-page-header
|
||||
icon="pf-icon pf-icon-process-automation"
|
||||
header=${this.flow.name}
|
||||
description=${this.flow.title}
|
||||
>
|
||||
</ak-page-header>
|
||||
<ak-tabs>
|
||||
<div
|
||||
slot="page-overview"
|
||||
data-tab-title="${t`Flow Overview`}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
<div
|
||||
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-2-col-on-xl pf-m-2-col-on-2xl"
|
||||
>
|
||||
<div class="pf-c-card__title">${t`Related`}</div>
|
||||
<div class="pf-c-card__body">
|
||||
<dl class="pf-c-description-list">
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${t`Edit`}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit"> ${t`Update`} </span>
|
||||
<span slot="header"> ${t`Update Flow`} </span>
|
||||
<ak-flow-form
|
||||
slot="form"
|
||||
.instancePk=${this.flow.slug}
|
||||
>
|
||||
</ak-flow-form>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-secondary"
|
||||
>
|
||||
${t`Edit`}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
</div>
|
||||
</dd>
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${t`Execute flow`}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<button
|
||||
class="pf-c-button pf-m-primary"
|
||||
@click=${() => {
|
||||
const finalURL = `${
|
||||
window.location.origin
|
||||
}/if/flow/${this.flow.slug}/${AndNext(
|
||||
`${window.location.pathname}#${window.location.hash}`,
|
||||
)}`;
|
||||
window.open(finalURL, "_blank");
|
||||
}}
|
||||
>
|
||||
${t`Normal`}
|
||||
</button>
|
||||
<button
|
||||
class="pf-c-button pf-m-secondary"
|
||||
@click=${() => {
|
||||
new FlowsApi(DEFAULT_CONFIG)
|
||||
.flowsInstancesExecuteRetrieve({
|
||||
slug: this.flow.slug,
|
||||
})
|
||||
.then((link) => {
|
||||
const finalURL = `${
|
||||
link.link
|
||||
}${AndNext(
|
||||
`${window.location.pathname}#${window.location.hash}`,
|
||||
)}`;
|
||||
window.open(finalURL, "_blank");
|
||||
});
|
||||
}}
|
||||
>
|
||||
${t`with current user`}
|
||||
</button>
|
||||
<button
|
||||
class="pf-c-button pf-m-secondary"
|
||||
@click=${() => {
|
||||
new FlowsApi(DEFAULT_CONFIG)
|
||||
.flowsInstancesExecuteRetrieve({
|
||||
slug: this.flow.slug,
|
||||
})
|
||||
.then((link) => {
|
||||
const finalURL = `${
|
||||
link.link
|
||||
}?${encodeURI(
|
||||
`inspector&next=/#${window.location.hash}`,
|
||||
)}`;
|
||||
window.open(finalURL, "_blank");
|
||||
})
|
||||
.catch((exc: ResponseError) => {
|
||||
// This request can return a HTTP 400 when a flow
|
||||
// is not applicable.
|
||||
window.open(
|
||||
exc.response.url,
|
||||
"_blank",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
${t`with inspector`}
|
||||
</button>
|
||||
</div>
|
||||
</dd>
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${t`Export flow`}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<a
|
||||
class="pf-c-button pf-m-secondary"
|
||||
href=${this.flow.exportUrl}
|
||||
>
|
||||
${t`Export`}
|
||||
</a>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-10-col-on-xl pf-m-10-col-on-2xl"
|
||||
>
|
||||
<div class="pf-c-card__title">${t`Diagram`}</div>
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__body">
|
||||
<ak-flow-diagram flowSlug=${this.flow.slug}> </ak-flow-diagram>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-12-col-on-2xl"
|
||||
>
|
||||
<div class="pf-c-card__title">${t`Changelog`}</div>
|
||||
<div class="pf-c-card__body">
|
||||
<ak-object-changelog
|
||||
targetModelPk=${this.flow.pk || ""}
|
||||
targetModelApp="authentik_flows"
|
||||
targetModelName="flow"
|
||||
>
|
||||
</ak-object-changelog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
slot="page-stage-bindings"
|
||||
data-tab-title="${t`Stage Bindings`}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__body">
|
||||
<ak-bound-stages-list .target=${this.flow.pk}> </ak-bound-stages-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
slot="page-policy-bindings"
|
||||
data-tab-title="${t`Policy / Group / User Bindings`}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__title">
|
||||
${t`These bindings control which users can access this flow.`}
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<ak-bound-policies-list .target=${this.flow.policybindingmodelPtrId}>
|
||||
</ak-bound-policies-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ak-tabs>`;
|
||||
}
|
||||
}
|
||||
225
web/src/admin/flows/StageBindingForm.ts
Normal file
225
web/src/admin/flows/StageBindingForm.ts
Normal file
@ -0,0 +1,225 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first, groupBy } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
|
||||
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 {
|
||||
FlowStageBinding,
|
||||
FlowsApi,
|
||||
InvalidResponseActionEnum,
|
||||
PolicyEngineMode,
|
||||
Stage,
|
||||
StagesApi,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-stage-binding-form")
|
||||
export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
|
||||
loadInstance(pk: string): Promise<FlowStageBinding> {
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsRetrieve({
|
||||
fsbUuid: pk,
|
||||
});
|
||||
}
|
||||
|
||||
@property()
|
||||
targetPk?: string;
|
||||
|
||||
getSuccessMessage(): string {
|
||||
if (this.instance) {
|
||||
return t`Successfully updated binding.`;
|
||||
} else {
|
||||
return t`Successfully created binding.`;
|
||||
}
|
||||
}
|
||||
|
||||
send = (data: FlowStageBinding): Promise<FlowStageBinding> => {
|
||||
if (this.instance) {
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsUpdate({
|
||||
fsbUuid: this.instance.pk || "",
|
||||
flowStageBindingRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsCreate({
|
||||
flowStageBindingRequest: data,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
groupStages(stages: Stage[]): TemplateResult {
|
||||
return html`
|
||||
<option value="">---------</option>
|
||||
${groupBy<Stage>(stages, (s) => s.verboseName || "").map(([group, stages]) => {
|
||||
return html`<optgroup label=${group}>
|
||||
${stages.map((stage) => {
|
||||
const selected = this.instance?.stage === stage.pk;
|
||||
return html`<option ?selected=${selected} value=${ifDefined(stage.pk)}>
|
||||
${stage.name}
|
||||
</option>`;
|
||||
})}
|
||||
</optgroup>`;
|
||||
})}
|
||||
`;
|
||||
}
|
||||
|
||||
getOrder(): Promise<number> {
|
||||
if (this.instance) {
|
||||
return Promise.resolve(this.instance.order);
|
||||
}
|
||||
return new FlowsApi(DEFAULT_CONFIG)
|
||||
.flowsBindingsList({
|
||||
target: this.targetPk || "",
|
||||
})
|
||||
.then((bindings) => {
|
||||
const orders = bindings.results.map((binding) => binding.order);
|
||||
if (orders.length < 1) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(...orders) + 1;
|
||||
});
|
||||
}
|
||||
|
||||
renderTarget(): TemplateResult {
|
||||
if (this.instance?.target || this.targetPk) {
|
||||
return html`
|
||||
<input
|
||||
required
|
||||
name="target"
|
||||
type="hidden"
|
||||
value=${ifDefined(this.instance?.target || this.targetPk)}
|
||||
/>
|
||||
`;
|
||||
}
|
||||
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({
|
||||
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>
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
${this.renderTarget()}
|
||||
<ak-form-element-horizontal label=${t`Stage`} ?required=${true} name="stage">
|
||||
<select class="pf-c-form-control">
|
||||
${until(
|
||||
new StagesApi(DEFAULT_CONFIG)
|
||||
.stagesAllList({
|
||||
ordering: "name",
|
||||
})
|
||||
.then((stages) => {
|
||||
return this.groupStages(stages.results);
|
||||
}),
|
||||
html`<option>${t`Loading...`}</option>`,
|
||||
)}
|
||||
</select>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`Order`} ?required=${true} name="order">
|
||||
<!-- @ts-ignore -->
|
||||
<input
|
||||
type="number"
|
||||
value="${until(this.getOrder())}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="evaluateOnPlan">
|
||||
<div class="pf-c-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="pf-c-check__input"
|
||||
?checked=${first(this.instance?.evaluateOnPlan, true)}
|
||||
/>
|
||||
<label class="pf-c-check__label"> ${t`Evaluate on plan`} </label>
|
||||
</div>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Evaluate policies during the Flow planning process. Disable this for input-based policies. Should be used in conjunction with 'Re-evaluate policies', as with both options disabled, policies are **not** evaluated.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="reEvaluatePolicies">
|
||||
<div class="pf-c-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="pf-c-check__input"
|
||||
?checked=${first(this.instance?.reEvaluatePolicies, false)}
|
||||
/>
|
||||
<label class="pf-c-check__label"> ${t`Re-evaluate policies`} </label>
|
||||
</div>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Evaluate policies before the Stage is present to the user.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Invalid response action`}
|
||||
?required=${true}
|
||||
name="invalidResponseAction"
|
||||
>
|
||||
<select class="pf-c-form-control">
|
||||
<option
|
||||
value=${InvalidResponseActionEnum.Retry}
|
||||
?selected=${this.instance?.invalidResponseAction ===
|
||||
InvalidResponseActionEnum.Retry}
|
||||
>
|
||||
${t`RETRY returns the error message and a similar challenge to the executor.`}
|
||||
</option>
|
||||
<option
|
||||
value=${InvalidResponseActionEnum.Restart}
|
||||
?selected=${this.instance?.invalidResponseAction ===
|
||||
InvalidResponseActionEnum.Restart}
|
||||
>
|
||||
${t`RESTART restarts the flow from the beginning.`}
|
||||
</option>
|
||||
<option
|
||||
value=${InvalidResponseActionEnum.RestartWithContext}
|
||||
?selected=${this.instance?.invalidResponseAction ===
|
||||
InvalidResponseActionEnum.RestartWithContext}
|
||||
>
|
||||
${t`RESTART_WITH_CONTEXT restarts the flow from the beginning, while keeping the flow context.`}
|
||||
</option>
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Configure how the flow executor should handle an invalid response to a challenge.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Policy engine mode`}
|
||||
?required=${true}
|
||||
name="policyEngineMode"
|
||||
>
|
||||
<select class="pf-c-form-control">
|
||||
<option
|
||||
value=${PolicyEngineMode.Any}
|
||||
?selected=${this.instance?.policyEngineMode === PolicyEngineMode.Any}
|
||||
>
|
||||
${t`ANY, any policy must match to include this stage access.`}
|
||||
</option>
|
||||
<option
|
||||
value=${PolicyEngineMode.All}
|
||||
?selected=${this.instance?.policyEngineMode === PolicyEngineMode.All}
|
||||
>
|
||||
${t`ALL, all policies must match to include this stage access.`}
|
||||
</option>
|
||||
</select>
|
||||
</ak-form-element-horizontal>
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
37
web/src/admin/flows/utils.ts
Normal file
37
web/src/admin/flows/utils.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
import { FlowDesignationEnum, LayoutEnum } from "@goauthentik/api";
|
||||
|
||||
export function DesignationToLabel(designation: FlowDesignationEnum): string {
|
||||
switch (designation) {
|
||||
case FlowDesignationEnum.Authentication:
|
||||
return t`Authentication`;
|
||||
case FlowDesignationEnum.Authorization:
|
||||
return t`Authorization`;
|
||||
case FlowDesignationEnum.Enrollment:
|
||||
return t`Enrollment`;
|
||||
case FlowDesignationEnum.Invalidation:
|
||||
return t`Invalidation`;
|
||||
case FlowDesignationEnum.Recovery:
|
||||
return t`Recovery`;
|
||||
case FlowDesignationEnum.StageConfiguration:
|
||||
return t`Stage Configuration`;
|
||||
case FlowDesignationEnum.Unenrollment:
|
||||
return t`Unenrollment`;
|
||||
}
|
||||
}
|
||||
|
||||
export function LayoutToLabel(layout: LayoutEnum): string {
|
||||
switch (layout) {
|
||||
case LayoutEnum.Stacked:
|
||||
return t`Stacked`;
|
||||
case LayoutEnum.ContentLeft:
|
||||
return t`Content left`;
|
||||
case LayoutEnum.ContentRight:
|
||||
return t`Content right`;
|
||||
case LayoutEnum.SidebarLeft:
|
||||
return t`Sidebar left`;
|
||||
case LayoutEnum.SidebarRight:
|
||||
return t`Sidebar right`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user