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:
Ken Sternberg 2023-07-28 13:57:14 -07:00 committed by GitHub
parent 033ebf9332
commit 3f02534eb1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 519 additions and 913 deletions

View file

@ -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 .",

View file

@ -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>

View 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;

View file

@ -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;

View 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;

View 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;

View file

@ -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;

View file

@ -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>`;
}

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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.",

View file

@ -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.",

View file

@ -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.",

View file

@ -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 {

View file

@ -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..."));

View file

@ -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);
}
};
}