web: weightloss program, part 1: FlowSearch (#6332)
* web: weightloss program, part 1: FlowSearch This commit extracts the multiple uses of SearchSelect for Flow lookups in the `providers` collection and replaces them with a slightly more legible format, from: ```HTML <ak-search-select .fetchObjects=${async (query?: string): Promise<Flow[]> => { const args: FlowsInstancesListRequest = { ordering: "slug", designation: FlowsInstancesListDesignationEnum.Authentication, }; 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?.authenticationFlow; }} > </ak-search-select> ``` ... to: ```HTML <ak-flow-search flowType=${FlowsInstancesListDesignationEnum.Authentication} .currentFlow=${this.instance?.authenticationFlow} required ></ak-flow-search> ``` All of those middle methods, like `renderElement`, `renderDescription`, etc, are *completely the same* for *all* of the searches, and there are something like 25 of them; this commit only covers the 8 in `providers`, but the next commit should carry all of them. The topmost example has been extracted into its own Web Component, `ak-flow-search`, that takes only two arguments: the type of `FlowInstanceListDesignation` and the current instance of the flow. The static methods for `renderElement`, `renderDescription` and `value` (which are all the same in all 25 instances of `FlowInstancesListRequest`) have been made into standalone functions. `fetchObjects` has been made into a method that takes the parameter from the `designation` property, and `selected` has been turned into a method that takes the comparator instance from the `currentFlow` property. That's it. That's the whole of it. `SearchSelect` now emits an event whenever the user changes the field, and `ak-flow-search` intercepts that event to mirror the value locally. `Form` has been adapted to recognize the `ak-flow-search` element and extract the current value. There are a number of legibility issues remaining, even with this fix. The Authentik Form manager is dependent upon a component named `ak-form-element-horizontal`, which is a container for a single displayed element in a form: ```HTML <ak-form-element-horizontal label=${msg("Authorization flow")} ?required=${true} name="authorizationFlow" > <ak-flow-search flowType=${FlowsInstancesListDesignationEnum.Authorization} .currentFlow=${this.instance?.authorizationFlow} required ></ak-flow-search> <p class="pf-c-form__helper-text"> ${msg("Flow used when authorizing this provider.")} </p> </ak-form-element-horizontal> ``` Imagine, instead, if we could write: ```HTML <ak-form-element-flow-search flowType=${FlowsInstancesListDesignationEnum.Authorization} .currentFlow=${this.instance?.authorizationFlow} required name="authorizationFlow"> <label slot="label">${msg("Authorization flow")}</label> <span slot="help">${msg("Flow used when authorizing this provider.")}</span> <ak-form-element-flow-search> ``` Starting with a superclass that understands the need for `label` and `help` slots, it would automatically configure the input object that would be used. We've already specified multiple identical copies of this thing in multiple different places; centralizing their definition and then re-using them would be classic code re-use. Even better, since the Authorization flow is used 10 times in the whole of our code base, and the Authentication flow 8 times, and they are *all identical*, it would be fitting if we just created wrappers: ```HTML <ak-form-element-flow-search flowType=${FlowsInstancesListDesignationEnum.Authorization}> <ak-form-element-flow-search> ``` That's really all that's needed. There are *hundreds* (about 470 total) cases where nine or more lines of repetitious HTML could be replaced with a one-liner like the above. A "narrow waist" design is one that allows for a system to communicate between two different components through a small but consistent collection of calls. The Form manager needs to be narrowed hard. The `ak-form-element-horizontal` is a wrapper around an input object, and it has this at its core for extracting that information. This forwards the name component to the containing input object so that when the input object generates an event, we can identify the field it's associated with. ```Javascript this.querySelectorAll("*").forEach((input) => { switch (input.tagName.toLowerCase()) { case "input": case "textarea": case "select": case "ak-codemirror": case "ak-chip-group": case "ak-search-select": case "ak-radio": input.setAttribute("name", this.name); break; default: return; } ``` A *temporary* variant of this is in the `ak-flow-search` component, to support this API without having to modify `ak-form-element-horizontal`. And then `ak-form` itself has this: ```Javascript if ( inputElement.tagName.toLowerCase() === "select" && "multiple" in inputElement.attributes ) { const selectElement = inputElement as unknown as HTMLSelectElement; json[element.name] = Array.from(selectElement.selectedOptions).map((v) => v.value); } else if ( inputElement.tagName.toLowerCase() === "input" && inputElement.type === "date" ) { json[element.name] = inputElement.valueAsDate; } else if ( inputElement.tagName.toLowerCase() === "input" && inputElement.type === "datetime-local" ) { json[element.name] = new Date(inputElement.valueAsNumber); } // ... another 20 lines removed ``` This ought to read: ```Javascript const json = elements.filter((element => element instanceof AkFormComponent) .reduce((acc, element) => ({ ...acc, [element.name]: element.value] }); ``` Where, instead of hand-writing all the different input objects for date and datetime and checkbox into our forms, and then having to craft custom value extractors for each and every one of them, just write *one* version of each with all the wrappers and bells and whistles already attached, and have each one of them have a `value` getter descriptor that returns the value expected by our form handler. A back-of-the-envelope estimation is that there's about four *thousand* lines that could disappear if we did this right. More importantly, it would be possible to create new `AkFormComponent`s without having to register them or define them for `ak-form`; as long as they conformed to the AkFormComponent's expectations for "what is a source of values for a Form", `ak-form` would understand how to handle it. Ultimately, what I want is to be able to do this: ``` HTML <ak-input-form itemtype="ak-search" itemid="ak-authentication" itemprop=${this.instance}></ak-inputform> ``` And it will (1) go out and find the right kind of search to put there, (2) conduct the right kind of fetch to fill that search, (3) pre-configure it with the user's current choice in that locale. I don't think this is possible-- for one thing, it would be very expensive in terms of development, and it may break the "narrow waist" ideal by require that the `ak-input-form` object know all the different kinds of searches that are available. The old Midgardian dream was that the object would have *just* the identity triple (A table, a row of that table, a field of that row), and the Javascript would go out and, using the identity, *find* the right object for CRUD (Creating, Retrieving, Updating, and Deleting) it. But that inspiration, as unreachable as it is, is where I'm headed. Where our objects are both *smart* and *standalone*. Where they're polite citizens in an ordered universe, capable of independence sufficient to be tested and validated and trusted, but working in concert to achieve our aims. * web: unravel the search-select for flows completely. This commit removes *all* instances of the search-select for flows, classifying them into four different categories: - a search with no default - a search with a default - a search with a default and a fallback to a static default if non specified - a search with a default and a fallback to the tenant's preferred default if this is a new instance and no flow specified. It's not humanly possible to test all the instances where this has been committed, but the linters are very happy with the results, and I'm going to eyeball every one of them in the github presentation before I move this out of draft. * web: several were declared 'required' that were not. * web: I can't believe this was rejected because of a misspelling in a code comment. Well done\! * web: another codespell fix for a comment. * web: adding 'codespell' to the pre-commit command. Fixed spelling error in eventEmitter.
This commit is contained in:
parent
033ebf9332
commit
3f02534eb1
|
@ -15,8 +15,9 @@
|
|||
"build-proxy": "run-s build-locales rollup:build-proxy",
|
||||
"watch": "run-s build-locales rollup:watch",
|
||||
"lint": "eslint . --max-warnings 0 --fix",
|
||||
"lint:spelling": "codespell -D - -D ../.github/codespell-dictionary.txt -I ../.github/codespell-words.txt -S './src/locales/**' ./src -s",
|
||||
"lit-analyse": "lit-analyzer src",
|
||||
"precommit": "run-s tsc lit-analyse lint prettier",
|
||||
"precommit": "run-s tsc lit-analyse lint lint:spelling prettier",
|
||||
"prettier-check": "prettier --check .",
|
||||
"prettier": "prettier --write .",
|
||||
"tsc:execute": "tsc --noEmit -p .",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search-no-default";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { KeyUnknown } from "@goauthentik/elements/forms/Form";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
@ -12,10 +12,7 @@ import { TemplateResult, html } from "lit";
|
|||
|
||||
import {
|
||||
ClientTypeEnum,
|
||||
Flow,
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
FlowsInstancesListRequest,
|
||||
OAuth2ProviderRequest,
|
||||
ProvidersApi,
|
||||
} from "@goauthentik/api";
|
||||
|
@ -47,29 +44,10 @@ export class TypeOAuthCodeApplicationWizardPage extends WizardFormPage {
|
|||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
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;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-flow-search-no-default
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
required
|
||||
></ak-flow-search-no-default>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when users access this application.")}
|
||||
</p>
|
||||
|
|
131
web/src/admin/common/ak-flow-search/FlowSearch.ts
Normal file
131
web/src/admin/common/ak-flow-search/FlowSearch.ts
Normal file
|
@ -0,0 +1,131 @@
|
|||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect";
|
||||
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
import { html } from "lit";
|
||||
import { property, query } from "lit/decorators.js";
|
||||
|
||||
import { FlowsApi, FlowsInstancesListDesignationEnum } from "@goauthentik/api";
|
||||
import type { Flow, FlowsInstancesListRequest } from "@goauthentik/api";
|
||||
|
||||
export function renderElement(flow: Flow) {
|
||||
return RenderFlowOption(flow);
|
||||
}
|
||||
|
||||
export function renderDescription(flow: Flow) {
|
||||
return html`${flow.slug}`;
|
||||
}
|
||||
|
||||
export function getFlowValue(flow: Flow | undefined): string | undefined {
|
||||
return flow?.pk;
|
||||
}
|
||||
|
||||
/**
|
||||
* FlowSearch
|
||||
*
|
||||
* A wrapper around SearchSelect that understands the basic semantics of querying about Flows. This
|
||||
* code eliminates the long blocks of unreadable invocation that were embedded in every provider, as well as in
|
||||
* sources, tenants, and applications.
|
||||
*
|
||||
*/
|
||||
|
||||
export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement) {
|
||||
/**
|
||||
* The type of flow we're looking for.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String })
|
||||
flowType?: FlowsInstancesListDesignationEnum;
|
||||
|
||||
/**
|
||||
* The id of the current flow, if any. For stages where the flow is already defined.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
currentFlow: string | undefined;
|
||||
|
||||
/**
|
||||
* If true, it is not valid to leave the flow blank.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
required?: boolean = false;
|
||||
|
||||
@query("ak-search-select")
|
||||
search!: SearchSelect<T>;
|
||||
|
||||
@property({ type: String })
|
||||
name: string | null | undefined;
|
||||
|
||||
selectedFlow?: T;
|
||||
|
||||
get value() {
|
||||
return this.selectedFlow ? getFlowValue(this.selectedFlow) : undefined;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.fetchObjects = this.fetchObjects.bind(this);
|
||||
this.selected = this.selected.bind(this);
|
||||
this.handleSearchUpdate = this.handleSearchUpdate.bind(this);
|
||||
this.addCustomListener("ak-change", this.handleSearchUpdate);
|
||||
}
|
||||
|
||||
handleSearchUpdate(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this.selectedFlow = ev.detail.value;
|
||||
}
|
||||
|
||||
async fetchObjects(query?: string): Promise<Flow[]> {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: this.flowType,
|
||||
...(query !== undefined ? { search: query } : {}),
|
||||
};
|
||||
const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(args);
|
||||
return flows.results;
|
||||
}
|
||||
|
||||
/* This is the most commonly overridden method of this class. About half of the Flow Searches
|
||||
* use this method, but several have more complex needs, such as relating to the tenant, or just
|
||||
* returning false.
|
||||
*/
|
||||
|
||||
selected(flow: Flow): boolean {
|
||||
return this.currentFlow === flow.pk;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const horizontalContainer = this.closest("ak-form-element-horizontal[name]");
|
||||
if (!horizontalContainer) {
|
||||
throw new Error("This search can only be used in a named ak-form-element-horizontal");
|
||||
}
|
||||
const name = horizontalContainer.getAttribute("name");
|
||||
const myName = this.getAttribute("name");
|
||||
if (name !== null && name !== myName) {
|
||||
this.setAttribute("name", name);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<ak-search-select
|
||||
.fetchObjects=${this.fetchObjects}
|
||||
.selected=${this.selected}
|
||||
.renderElement=${renderElement}
|
||||
.renderDescription=${renderDescription}
|
||||
.value=${getFlowValue}
|
||||
?blankable=${!this.required}
|
||||
>
|
||||
</ak-search-select>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default FlowSearch;
|
|
@ -0,0 +1,34 @@
|
|||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
|
||||
import { html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import type { Flow } from "@goauthentik/api";
|
||||
|
||||
import { FlowSearch, getFlowValue, renderDescription, renderElement } from "./FlowSearch";
|
||||
|
||||
/**
|
||||
* @element ak-flow-search-no-default
|
||||
*
|
||||
* A variant of the Flow Search that doesn't look for a current flow-of-flowtype according to the
|
||||
* user's settings because there shouldn't be one. Currently only used for uploading providers via
|
||||
* metadata, as that scenario can only happen when no current instance is available.
|
||||
*/
|
||||
|
||||
@customElement("ak-flow-search-no-default")
|
||||
export class AkFlowSearchNoDefault<T extends Flow> extends FlowSearch<T> {
|
||||
render() {
|
||||
return html`
|
||||
<ak-search-select
|
||||
.fetchObjects=${this.fetchObjects}
|
||||
.renderElement=${renderElement}
|
||||
.renderDescription=${renderDescription}
|
||||
.value=${getFlowValue}
|
||||
?blankable=${!this.required}
|
||||
>
|
||||
</ak-search-select>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkFlowSearchNoDefault;
|
16
web/src/admin/common/ak-flow-search/ak-flow-search.ts
Normal file
16
web/src/admin/common/ak-flow-search/ak-flow-search.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import type { Flow } from "@goauthentik/api";
|
||||
|
||||
import FlowSearch from "./FlowSearch";
|
||||
|
||||
/**
|
||||
* @element ak-flow-search
|
||||
*
|
||||
* The default flow search experience.
|
||||
*/
|
||||
|
||||
@customElement("ak-flow-search")
|
||||
export class AkFlowSearch<T extends Flow> extends FlowSearch<T> {}
|
||||
|
||||
export default AkFlowSearch;
|
50
web/src/admin/common/ak-flow-search/ak-source-flow-search.ts
Normal file
50
web/src/admin/common/ak-flow-search/ak-source-flow-search.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { customElement } from "lit/decorators.js";
|
||||
import { property } from "lit/decorators.js";
|
||||
|
||||
import type { Flow } from "@goauthentik/api";
|
||||
|
||||
import FlowSearch from "./FlowSearch";
|
||||
|
||||
/**
|
||||
* Search for flows that connect to user sources
|
||||
*
|
||||
* @element ak-source-flow-search
|
||||
*
|
||||
*/
|
||||
|
||||
@customElement("ak-source-flow-search")
|
||||
export class AkSourceFlowSearch<T extends Flow> extends FlowSearch<T> {
|
||||
/**
|
||||
* The fallback flow if none specified AND the instance has no set flow and the instance is new.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
|
||||
@property({ type: String })
|
||||
fallback: string | undefined;
|
||||
|
||||
/**
|
||||
* The primary key of the Source (not the Flow). Mostly the instancePk itself, used to affirm
|
||||
* that we're working on a new stage and so falling back to the default is appropriate.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String })
|
||||
instanceId: string | undefined;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.selected = this.selected.bind(this);
|
||||
}
|
||||
|
||||
// If there's no instance or no currentFlowId for it and the flow resembles the fallback,
|
||||
// otherwise defer to the parent class.
|
||||
selected(flow: Flow): boolean {
|
||||
return (
|
||||
(!this.instanceId && !this.currentFlow && flow.slug === this.fallback) ||
|
||||
super.selected(flow)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AkSourceFlowSearch;
|
|
@ -0,0 +1,34 @@
|
|||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import type { Flow } from "@goauthentik/api";
|
||||
|
||||
import FlowSearch from "./FlowSearch";
|
||||
|
||||
/**
|
||||
* Search for flows that may have a fallback specified by the tenant settings
|
||||
*
|
||||
* @element ak-tenanted-flow-search
|
||||
*
|
||||
*/
|
||||
|
||||
@customElement("ak-tenanted-flow-search")
|
||||
export class AkTenantedFlowSearch<T extends Flow> extends FlowSearch<T> {
|
||||
/**
|
||||
* The Associated ID of the flow the tenant has, to compare if possible
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ attribute: false, type: String })
|
||||
tenantFlow?: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.selected = this.selected.bind(this);
|
||||
}
|
||||
|
||||
selected(flow: Flow): boolean {
|
||||
return super.selected(flow) || flow.pk === this.tenantFlow;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkTenantedFlowSearch;
|
|
@ -1,4 +1,3 @@
|
|||
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";
|
||||
|
@ -11,11 +10,9 @@ import { TemplateResult, html } from "lit";
|
|||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
import {
|
||||
Flow,
|
||||
FlowStageBinding,
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
FlowsInstancesListRequest,
|
||||
InvalidResponseActionEnum,
|
||||
PolicyEngineMode,
|
||||
Stage,
|
||||
|
@ -86,32 +83,11 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
|
|||
?required=${true}
|
||||
name="target"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
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-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${this.instance?.target}
|
||||
required
|
||||
></ak-flow-search>
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import { rootInterface } from "@goauthentik/elements/Base";
|
||||
|
@ -19,10 +19,7 @@ import {
|
|||
CoreGroupsListRequest,
|
||||
CryptoApi,
|
||||
CryptoCertificatekeypairsListRequest,
|
||||
Flow,
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
FlowsInstancesListRequest,
|
||||
Group,
|
||||
LDAPAPIAccessMode,
|
||||
LDAPProvider,
|
||||
|
@ -58,6 +55,12 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
|
|||
}
|
||||
}
|
||||
|
||||
// All Provider objects have an Authorization flow, but not all providers have an Authentication
|
||||
// flow. LDAP needs only one field, but it is not an Authorization field, it is an
|
||||
// Authentication field. So, yeah, we're using the authorization field to store the
|
||||
// authentication information, which is why the ak-tenanted-flow-search call down there looks so
|
||||
// weird-- we're looking up Authentication flows, but we're storing them in the Authorization
|
||||
// field of the target Provider.
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||
|
@ -73,36 +76,12 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
|
|||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Authentication,
|
||||
};
|
||||
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.slug}`;
|
||||
}}
|
||||
.value=${(flow: Flow | undefined): string | undefined => {
|
||||
return flow?.pk;
|
||||
}}
|
||||
.selected=${(flow: Flow): boolean => {
|
||||
let selected = flow.pk === rootInterface()?.tenant?.flowAuthentication;
|
||||
if (this.instance?.authorizationFlow === flow.pk) {
|
||||
selected = true;
|
||||
}
|
||||
return selected;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-tenanted-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
.tenantFlow=${rootInterface()?.tenant?.flowAuthentication}
|
||||
required
|
||||
></ak-tenanted-flow-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Search group")} name="searchGroup">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
|
@ -18,10 +18,7 @@ import {
|
|||
ClientTypeEnum,
|
||||
CryptoApi,
|
||||
CryptoCertificatekeypairsListRequest,
|
||||
Flow,
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
FlowsInstancesListRequest,
|
||||
IssuerModeEnum,
|
||||
OAuth2Provider,
|
||||
PaginatedOAuthSourceList,
|
||||
|
@ -95,32 +92,11 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
|
|||
label=${msg("Authentication flow")}
|
||||
name="authenticationFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Authentication,
|
||||
};
|
||||
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?.authenticationFlow;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authenticationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when a user access this provider and is not authenticated.")}
|
||||
</p>
|
||||
|
@ -130,32 +106,11 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
|
|||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
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?.authorizationFlow;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
|
@ -22,10 +22,7 @@ import {
|
|||
CertificateKeyPair,
|
||||
CryptoApi,
|
||||
CryptoCertificatekeypairsListRequest,
|
||||
Flow,
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
FlowsInstancesListRequest,
|
||||
PaginatedOAuthSourceList,
|
||||
PaginatedScopeMappingList,
|
||||
PropertymappingsApi,
|
||||
|
@ -340,32 +337,11 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
|
|||
?required=${false}
|
||||
name="authenticationFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Authentication,
|
||||
};
|
||||
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?.authenticationFlow;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authenticationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when a user access this provider and is not authenticated.")}
|
||||
</p>
|
||||
|
@ -375,32 +351,11 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
|
|||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
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?.authorizationFlow;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
|
||||
import { rootInterface } from "@goauthentik/elements/Base";
|
||||
|
@ -12,14 +11,7 @@ import { TemplateResult, html } from "lit";
|
|||
import { ifDefined } from "lit-html/directives/if-defined.js";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import {
|
||||
Flow,
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
FlowsInstancesListRequest,
|
||||
ProvidersApi,
|
||||
RadiusProvider,
|
||||
} from "@goauthentik/api";
|
||||
import { FlowsInstancesListDesignationEnum, ProvidersApi, RadiusProvider } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-provider-radius-form")
|
||||
export class RadiusProviderFormPage extends ModelForm<RadiusProvider, number> {
|
||||
|
@ -50,6 +42,12 @@ export class RadiusProviderFormPage extends ModelForm<RadiusProvider, number> {
|
|||
}
|
||||
}
|
||||
|
||||
// All Provider objects have an Authorization flow, but not all providers have an Authentication
|
||||
// flow. Radius needs only one field, but it is not the Authorization field, it is an
|
||||
// Authentication field. So, yeah, we're using the authorization field to store the
|
||||
// authentication information, which is why the ak-tenanted-flow-search call down there looks so
|
||||
// weird-- we're looking up Authentication flows, but we're storing them in the Authorization
|
||||
// field of the target Provider.
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||
|
@ -65,36 +63,12 @@ export class RadiusProviderFormPage extends ModelForm<RadiusProvider, number> {
|
|||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Authentication,
|
||||
};
|
||||
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.slug}`;
|
||||
}}
|
||||
.value=${(flow: Flow | undefined): string | undefined => {
|
||||
return flow?.pk;
|
||||
}}
|
||||
.selected=${(flow: Flow): boolean => {
|
||||
let selected = flow.pk === rootInterface()?.tenant?.flowAuthentication;
|
||||
if (this.instance?.authorizationFlow === flow.pk) {
|
||||
selected = true;
|
||||
}
|
||||
return selected;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-tenanted-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
.tenantFlow=${rootInterface()?.tenant?.flowAuthentication}
|
||||
required
|
||||
></ak-tenanted-flow-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
@ -17,10 +17,7 @@ import {
|
|||
CryptoApi,
|
||||
CryptoCertificatekeypairsListRequest,
|
||||
DigestAlgorithmEnum,
|
||||
Flow,
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
FlowsInstancesListRequest,
|
||||
PaginatedSAMLPropertyMappingList,
|
||||
PropertymappingsApi,
|
||||
PropertymappingsSamlListRequest,
|
||||
|
@ -85,32 +82,11 @@ export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> {
|
|||
?required=${false}
|
||||
name="authenticationFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Authentication,
|
||||
};
|
||||
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?.authenticationFlow;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authenticationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when a user access this provider and is not authenticated.")}
|
||||
</p>
|
||||
|
@ -120,32 +96,11 @@ export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> {
|
|||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
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?.authorizationFlow;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search-no-default";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { SentryIgnoredError } from "@goauthentik/common/errors";
|
||||
import { Form } from "@goauthentik/elements/forms/Form";
|
||||
|
@ -9,14 +9,7 @@ import { msg } from "@lit/localize";
|
|||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import {
|
||||
Flow,
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
FlowsInstancesListRequest,
|
||||
ProvidersApi,
|
||||
SAMLProvider,
|
||||
} from "@goauthentik/api";
|
||||
import { FlowsInstancesListDesignationEnum, ProvidersApi, SAMLProvider } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-provider-saml-import-form")
|
||||
export class SAMLProviderImportForm extends Form<SAMLProvider> {
|
||||
|
@ -45,29 +38,10 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> {
|
|||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
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?.slug;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-flow-search-no-default
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
required
|
||||
></ak-flow-search-no-default>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-source-flow-search";
|
||||
import { iconHelperText, placeholderHelperText } from "@goauthentik/admin/helperText";
|
||||
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
|
||||
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
||||
|
@ -17,10 +17,7 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
|||
|
||||
import {
|
||||
CapabilitiesEnum,
|
||||
Flow,
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
FlowsInstancesListRequest,
|
||||
OAuthSource,
|
||||
OAuthSourceRequest,
|
||||
ProviderTypeEnum,
|
||||
|
@ -413,43 +410,12 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
|
|||
?required=${true}
|
||||
name="authenticationFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Authentication,
|
||||
};
|
||||
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 => {
|
||||
let selected = this.instance?.authenticationFlow === flow.pk;
|
||||
if (
|
||||
!this.instance?.pk &&
|
||||
!this.instance?.authenticationFlow &&
|
||||
flow.slug === "default-source-authentication"
|
||||
) {
|
||||
selected = true;
|
||||
}
|
||||
return selected;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-source-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authenticationFlow}
|
||||
.instanceId=${this.instance?.pk}
|
||||
fallback="default-source-authentication"
|
||||
></ak-source-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow to use when authenticating existing users.")}
|
||||
</p>
|
||||
|
@ -459,43 +425,12 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
|
|||
?required=${true}
|
||||
name="enrollmentFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Enrollment,
|
||||
};
|
||||
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 => {
|
||||
let selected = this.instance?.enrollmentFlow === flow.pk;
|
||||
if (
|
||||
!this.instance?.pk &&
|
||||
!this.instance?.enrollmentFlow &&
|
||||
flow.slug === "default-source-enrollment"
|
||||
) {
|
||||
selected = true;
|
||||
}
|
||||
return selected;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-source-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Enrollment}
|
||||
.currentFlow=${this.instance?.enrollmentFlow}
|
||||
.instanceId=${this.instance?.pk}
|
||||
fallback="default-source-enrollment"
|
||||
></ak-source-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow to use when enrolling new users.")}
|
||||
</p>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-source-flow-search";
|
||||
import { iconHelperText, placeholderHelperText } from "@goauthentik/admin/helperText";
|
||||
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
|
||||
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
||||
|
@ -17,10 +17,7 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
|||
|
||||
import {
|
||||
CapabilitiesEnum,
|
||||
Flow,
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
FlowsInstancesListRequest,
|
||||
PlexSource,
|
||||
SourcesApi,
|
||||
UserMatchingModeEnum,
|
||||
|
@ -339,43 +336,12 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
|
|||
?required=${true}
|
||||
name="authenticationFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Authentication,
|
||||
};
|
||||
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 => {
|
||||
let selected = this.instance?.authenticationFlow === flow.pk;
|
||||
if (
|
||||
!this.instance?.pk &&
|
||||
!this.instance?.authenticationFlow &&
|
||||
flow.slug === "default-source-authentication"
|
||||
) {
|
||||
selected = true;
|
||||
}
|
||||
return selected;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-source-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authenticationFlow}
|
||||
.instanceId=${this.instance?.pk}
|
||||
fallback="default-source-authentication"
|
||||
></ak-source-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow to use when authenticating existing users.")}
|
||||
</p>
|
||||
|
@ -385,43 +351,12 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
|
|||
?required=${true}
|
||||
name="enrollmentFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Enrollment,
|
||||
};
|
||||
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 => {
|
||||
let selected = this.instance?.enrollmentFlow === flow.pk;
|
||||
if (
|
||||
!this.instance?.pk &&
|
||||
!this.instance?.enrollmentFlow &&
|
||||
flow.slug === "default-source-enrollment"
|
||||
) {
|
||||
selected = true;
|
||||
}
|
||||
return selected;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-source-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Enrollment}
|
||||
.currentFlow=${this.instance?.enrollmentFlow}
|
||||
.instanceId=${this.instance?.pk}
|
||||
fallback="default-source-enrollment"
|
||||
></ak-source-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow to use when enrolling new users.")}
|
||||
</p>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-source-flow-search";
|
||||
import { iconHelperText, placeholderHelperText } from "@goauthentik/admin/helperText";
|
||||
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
|
||||
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
||||
|
@ -22,10 +22,7 @@ import {
|
|||
CryptoApi,
|
||||
CryptoCertificatekeypairsListRequest,
|
||||
DigestAlgorithmEnum,
|
||||
Flow,
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
FlowsInstancesListRequest,
|
||||
NameIdPolicyEnum,
|
||||
SAMLSource,
|
||||
SignatureAlgorithmEnum,
|
||||
|
@ -521,44 +518,12 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
|
|||
?required=${true}
|
||||
name="preAuthenticationFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation:
|
||||
FlowsInstancesListDesignationEnum.StageConfiguration,
|
||||
};
|
||||
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 => {
|
||||
let selected = this.instance?.preAuthenticationFlow === flow.pk;
|
||||
if (
|
||||
!this.instance?.pk &&
|
||||
!this.instance?.preAuthenticationFlow &&
|
||||
flow.slug === "default-source-pre-authentication"
|
||||
) {
|
||||
selected = true;
|
||||
}
|
||||
return selected;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-source-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.StageConfiguration}
|
||||
.currentFlow=${this.instance?.preAuthenticationFlow}
|
||||
.instanceId=${this.instance?.pk}
|
||||
fallback="default-source-pre-authentication"
|
||||
></ak-source-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used before authentication.")}
|
||||
</p>
|
||||
|
@ -568,43 +533,12 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
|
|||
?required=${true}
|
||||
name="authenticationFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Authentication,
|
||||
};
|
||||
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 => {
|
||||
let selected = this.instance?.authenticationFlow === flow.pk;
|
||||
if (
|
||||
!this.instance?.pk &&
|
||||
!this.instance?.authenticationFlow &&
|
||||
flow.slug === "default-source-authentication"
|
||||
) {
|
||||
selected = true;
|
||||
}
|
||||
return selected;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-source-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authenticationFlow}
|
||||
.instanceId=${this.instance?.pk}
|
||||
fallback="default-source-authentication"
|
||||
></ak-source-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow to use when authenticating existing users.")}
|
||||
</p>
|
||||
|
@ -614,43 +548,12 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
|
|||
?required=${true}
|
||||
name="enrollmentFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Enrollment,
|
||||
};
|
||||
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 => {
|
||||
let selected = this.instance?.enrollmentFlow === flow.pk;
|
||||
if (
|
||||
!this.instance?.pk &&
|
||||
!this.instance?.enrollmentFlow &&
|
||||
flow.slug === "default-source-enrollment"
|
||||
) {
|
||||
selected = true;
|
||||
}
|
||||
return selected;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-source-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Enrollment}
|
||||
.currentFlow=${this.instance?.enrollmentFlow}
|
||||
.instanceId=${this.instance?.pk}
|
||||
fallback="default-source-enrollment"
|
||||
></ak-source-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow to use when enrolling new users.")}
|
||||
</p>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first, groupBy } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
|
@ -12,10 +12,7 @@ import { customElement } from "lit/decorators.js";
|
|||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
Flow,
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
FlowsInstancesListRequest,
|
||||
IdentificationStage,
|
||||
PaginatedSourceList,
|
||||
SourcesApi,
|
||||
|
@ -265,35 +262,10 @@ export class IdentificationStageForm extends ModelForm<IdentificationStage, stri
|
|||
label=${msg("Passwordless flow")}
|
||||
name="passwordlessFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Authentication,
|
||||
};
|
||||
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 this.instance?.passwordlessFlow == flow.pk;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.passwordlessFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Optional passwordless flow, which is linked at the bottom of the page. When configured, users can use this flow to authenticate with a WebAuthn authenticator, without entering any details.",
|
||||
|
@ -304,35 +276,11 @@ export class IdentificationStageForm extends ModelForm<IdentificationStage, stri
|
|||
label=${msg("Enrollment flow")}
|
||||
name="enrollmentFlow"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Enrollment,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(
|
||||
args,
|
||||
);
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
}}
|
||||
.value=${(flow: Flow | undefined): string | undefined => {
|
||||
return flow?.pk;
|
||||
}}
|
||||
.selected=${(flow: Flow): boolean => {
|
||||
return this.instance?.enrollmentFlow == flow.pk;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Enrollment}
|
||||
.currentFlow=${this.instance?.enrollmentFlow}
|
||||
></ak-flow-search>
|
||||
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Optional enrollment flow, which is linked at the bottom of the page.",
|
||||
|
@ -340,35 +288,10 @@ export class IdentificationStageForm extends ModelForm<IdentificationStage, stri
|
|||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Recovery flow")} name="recoveryFlow">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Recovery,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(
|
||||
args,
|
||||
);
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
return flow.slug;
|
||||
}}
|
||||
.renderDescription=${(flow: Flow): TemplateResult => {
|
||||
return html`${flow.name}`;
|
||||
}}
|
||||
.value=${(flow: Flow | undefined): string | undefined => {
|
||||
return flow?.pk;
|
||||
}}
|
||||
.selected=${(flow: Flow): boolean => {
|
||||
return this.instance?.recoveryFlow == flow.pk;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Recovery}
|
||||
.currentFlow=${this.instance?.recoveryFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Optional recovery flow, which is linked at the bottom of the page.",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { dateTimeLocal, first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
|
@ -11,14 +11,7 @@ import { msg } from "@lit/localize";
|
|||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import {
|
||||
Flow,
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
FlowsInstancesListRequest,
|
||||
Invitation,
|
||||
StagesApi,
|
||||
} from "@goauthentik/api";
|
||||
import { FlowsInstancesListDesignationEnum, Invitation, StagesApi } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-invitation-form")
|
||||
export class InvitationForm extends ModelForm<Invitation, string> {
|
||||
|
@ -75,33 +68,10 @@ export class InvitationForm extends ModelForm<Invitation, string> {
|
|||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Flow")} ?required=${true} name="flow">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Enrollment,
|
||||
};
|
||||
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?.flow;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Enrollment}
|
||||
.currentFlow=${this.instance?.flow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When selected, the invite will only be usable with the flow. By default the invite is accepted on all flows with invitation stages.",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
|
@ -18,10 +18,7 @@ import {
|
|||
CoreApi,
|
||||
CryptoApi,
|
||||
CryptoCertificatekeypairsListRequest,
|
||||
Flow,
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
FlowsInstancesListRequest,
|
||||
Tenant,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
|
@ -154,35 +151,10 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
|||
label=${msg("Authentication flow")}
|
||||
name="flowAuthentication"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Authentication,
|
||||
};
|
||||
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 this.instance?.flowAuthentication === flow.pk;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.flowAuthentication}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used to authenticate users. If left empty, the first applicable flow sorted by the slug is used.",
|
||||
|
@ -193,35 +165,10 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
|||
label=${msg("Invalidation flow")}
|
||||
name="flowInvalidation"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Invalidation,
|
||||
};
|
||||
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 this.instance?.flowInvalidation === flow.pk;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${this.instance?.flowInvalidation}
|
||||
></ak-flow-search>
|
||||
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
|
@ -230,35 +177,10 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
|||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Recovery flow")} name="flowRecovery">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Recovery,
|
||||
};
|
||||
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 this.instance?.flowRecovery === flow.pk;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Recovery}
|
||||
.currentFlow=${this.instance?.flowRecovery}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Recovery flow. If left empty, the first applicable flow sorted by the slug is used.",
|
||||
|
@ -269,35 +191,10 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
|||
label=${msg("Unenrollment flow")}
|
||||
name="flowUnenrollment"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Unenrollment,
|
||||
};
|
||||
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 this.instance?.flowUnenrollment === flow.pk;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Unenrollment}
|
||||
.currentFlow=${this.instance?.flowUnenrollment}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"If set, users are able to unenroll themselves using this flow. If no flow is set, option is not shown.",
|
||||
|
@ -308,36 +205,10 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
|||
label=${msg("User settings flow")}
|
||||
name="flowUserSettings"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation:
|
||||
FlowsInstancesListDesignationEnum.StageConfiguration,
|
||||
};
|
||||
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 this.instance?.flowUserSettings === flow.pk;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.StageConfiguration}
|
||||
.currentFlow=${this.instance?.flowUserSettings}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("If set, users are able to configure details of their profile.")}
|
||||
</p>
|
||||
|
@ -346,36 +217,10 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
|||
label=${msg("Device code flow")}
|
||||
name="flowDeviceCode"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation:
|
||||
FlowsInstancesListDesignationEnum.StageConfiguration,
|
||||
};
|
||||
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 this.instance?.flowDeviceCode === flow.pk;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.StageConfiguration}
|
||||
.currentFlow=${this.instance?.flowDeviceCode}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"If set, the OAuth Device Code profile can be used, and the selected flow will be used to enter the code.",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { FlowSearch } from "@goauthentik/admin/common/ak-flow-search/FlowSearch";
|
||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import { MessageLevel } from "@goauthentik/common/messages";
|
||||
import { camelToSnake, convertToSlug } from "@goauthentik/common/utils";
|
||||
|
@ -178,6 +179,8 @@ export abstract class Form<T> extends AKElement {
|
|||
inputElement.type === "checkbox"
|
||||
) {
|
||||
json[element.name] = inputElement.checked;
|
||||
} else if (inputElement instanceof FlowSearch) {
|
||||
json[element.name] = inputElement.value;
|
||||
} else if (inputElement.tagName.toLowerCase() === "ak-search-select") {
|
||||
const select = inputElement as unknown as SearchSelect<unknown>;
|
||||
try {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
|||
import { ascii_letters, digits, groupBy, randomString } from "@goauthentik/common/utils";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { PreventFormSubmit } from "@goauthentik/elements/forms/Form";
|
||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, html, render } from "lit";
|
||||
|
@ -14,7 +15,7 @@ import PFSelect from "@patternfly/patternfly/components/Select/select.css";
|
|||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
@customElement("ak-search-select")
|
||||
export class SearchSelect<T> extends AKElement {
|
||||
export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
|
||||
@property()
|
||||
query?: string;
|
||||
|
||||
|
@ -91,6 +92,16 @@ export class SearchSelect<T> extends AKElement {
|
|||
this.dropdownUID = `dropdown-${randomString(10, ascii_letters + digits)}`;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
shouldUpdate(changedProperties: Map<string, any>) {
|
||||
if (changedProperties.has("selectedObject")) {
|
||||
this.dispatchCustomEvent("ak-change", {
|
||||
value: this.selectedObject,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
toForm(): unknown {
|
||||
if (!this.objects) {
|
||||
throw new PreventFormSubmit(msg("Loading options..."));
|
||||
|
|
|
@ -25,10 +25,66 @@ export function CustomEmitterElement<T extends Constructor<LitElement>>(supercla
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixin that enables Lit Elements to handle custom events in a more straightforward manner.
|
||||
*
|
||||
*/
|
||||
|
||||
// This is a neat trick: this static "class" is just a namespace for these unique symbols. Because
|
||||
// of all the constraints on them, they're legal field names in Typescript objects! Which means that
|
||||
// we can use them as identifiers for internal references in a Typescript class with absolutely no
|
||||
// risk that a future user who wants a name like 'addHandler' or 'removeHandler' will override any
|
||||
// of those, either in this mixin or in any class that this is mixed into, past or present along the
|
||||
// chain of inheritance.
|
||||
|
||||
class HK {
|
||||
public static readonly listenHandlers: unique symbol = Symbol();
|
||||
public static readonly addHandler: unique symbol = Symbol();
|
||||
public static readonly removeHandler: unique symbol = Symbol();
|
||||
public static readonly getHandler: unique symbol = Symbol();
|
||||
}
|
||||
|
||||
type EventHandler = (ev: CustomEvent) => void;
|
||||
type EventMap = WeakMap<EventHandler, EventHandler>;
|
||||
|
||||
export function CustomListenerElement<T extends Constructor<LitElement>>(superclass: T) {
|
||||
return class ListenerElementHandler extends superclass {
|
||||
addCustomListener(eventName: string, handler: (ev: CustomEvent) => void) {
|
||||
this.addEventListener(eventName, (ev: Event) => {
|
||||
private [HK.listenHandlers] = new Map<string, EventMap>();
|
||||
|
||||
private [HK.getHandler](eventName: string, handler: EventHandler) {
|
||||
const internalMap = this[HK.listenHandlers].get(eventName);
|
||||
return internalMap ? internalMap.get(handler) : undefined;
|
||||
}
|
||||
|
||||
// For every event NAME, we create a WeakMap that pairs the event handler given to us by the
|
||||
// class that uses this method to the custom, wrapped handler we create to manage the types
|
||||
// and handlings. If the wrapped handler disappears due to garbage collection, no harm done;
|
||||
// meanwhile, this allows us to remove it from the event listeners if it's still around
|
||||
// using the original handler's identity as the key.
|
||||
//
|
||||
private [HK.addHandler](
|
||||
eventName: string,
|
||||
handler: EventHandler,
|
||||
internalHandler: EventHandler,
|
||||
) {
|
||||
if (!this[HK.listenHandlers].has(eventName)) {
|
||||
this[HK.listenHandlers].set(eventName, new WeakMap());
|
||||
}
|
||||
const internalMap = this[HK.listenHandlers].get(eventName);
|
||||
if (internalMap) {
|
||||
internalMap.set(handler, internalHandler);
|
||||
}
|
||||
}
|
||||
|
||||
private [HK.removeHandler](eventName: string, handler: EventHandler) {
|
||||
const internalMap = this[HK.listenHandlers].get(eventName);
|
||||
if (internalMap) {
|
||||
internalMap.delete(handler);
|
||||
}
|
||||
}
|
||||
|
||||
addCustomListener(eventName: string, handler: EventHandler) {
|
||||
const internalHandler = (ev: Event) => {
|
||||
if (!isCustomEvent(ev)) {
|
||||
console.error(
|
||||
`Received a standard event for custom event ${eventName}; event will not be handled.`,
|
||||
|
@ -36,7 +92,20 @@ export function CustomListenerElement<T extends Constructor<LitElement>>(supercl
|
|||
return;
|
||||
}
|
||||
handler(ev);
|
||||
});
|
||||
};
|
||||
this[HK.addHandler](eventName, handler, internalHandler);
|
||||
this.addEventListener(eventName, internalHandler);
|
||||
}
|
||||
|
||||
removeCustomListener(eventName: string, handler: EventHandler) {
|
||||
const realHandler = this[HK.getHandler](eventName, handler);
|
||||
if (realHandler) {
|
||||
this.removeEventListener(
|
||||
eventName,
|
||||
realHandler as EventListenerOrEventListenerObject,
|
||||
);
|
||||
}
|
||||
this[HK.removeHandler](eventName, handler);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
Reference in a new issue