diff --git a/web/package.json b/web/package.json index a81048023..7f5a86692 100644 --- a/web/package.json +++ b/web/package.json @@ -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 .", diff --git a/web/src/admin/applications/wizard/oauth/TypeOAuthCodeApplicationWizardPage.ts b/web/src/admin/applications/wizard/oauth/TypeOAuthCodeApplicationWizardPage.ts index 6a7b5c473..458def24b 100644 --- a/web/src/admin/applications/wizard/oauth/TypeOAuthCodeApplicationWizardPage.ts +++ b/web/src/admin/applications/wizard/oauth/TypeOAuthCodeApplicationWizardPage.ts @@ -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" > - => { - 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; - }} - > - +

${msg("Flow used when users access this application.")}

diff --git a/web/src/admin/common/ak-flow-search/FlowSearch.ts b/web/src/admin/common/ak-flow-search/FlowSearch.ts new file mode 100644 index 000000000..484df6d6e --- /dev/null +++ b/web/src/admin/common/ak-flow-search/FlowSearch.ts @@ -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 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; + + @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 { + 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` + + + `; + } +} + +export default FlowSearch; diff --git a/web/src/admin/common/ak-flow-search/ak-flow-search-no-default.ts b/web/src/admin/common/ak-flow-search/ak-flow-search-no-default.ts new file mode 100644 index 000000000..4942d1590 --- /dev/null +++ b/web/src/admin/common/ak-flow-search/ak-flow-search-no-default.ts @@ -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 extends FlowSearch { + render() { + return html` + + + `; + } +} + +export default AkFlowSearchNoDefault; diff --git a/web/src/admin/common/ak-flow-search/ak-flow-search.ts b/web/src/admin/common/ak-flow-search/ak-flow-search.ts new file mode 100644 index 000000000..57fd7270d --- /dev/null +++ b/web/src/admin/common/ak-flow-search/ak-flow-search.ts @@ -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 extends FlowSearch {} + +export default AkFlowSearch; diff --git a/web/src/admin/common/ak-flow-search/ak-source-flow-search.ts b/web/src/admin/common/ak-flow-search/ak-source-flow-search.ts new file mode 100644 index 000000000..56624c0d1 --- /dev/null +++ b/web/src/admin/common/ak-flow-search/ak-source-flow-search.ts @@ -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 extends FlowSearch { + /** + * 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; diff --git a/web/src/admin/common/ak-flow-search/ak-tenanted-flow-search.ts b/web/src/admin/common/ak-flow-search/ak-tenanted-flow-search.ts new file mode 100644 index 000000000..6c89e1baa --- /dev/null +++ b/web/src/admin/common/ak-flow-search/ak-tenanted-flow-search.ts @@ -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 extends FlowSearch { + /** + * 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; diff --git a/web/src/admin/flows/StageBindingForm.ts b/web/src/admin/flows/StageBindingForm.ts index d1f736cb8..8c5492be2 100644 --- a/web/src/admin/flows/StageBindingForm.ts +++ b/web/src/admin/flows/StageBindingForm.ts @@ -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 { ?required=${true} name="target" > - => { - 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; - }} - > - + `; } diff --git a/web/src/admin/providers/ldap/LDAPProviderForm.ts b/web/src/admin/providers/ldap/LDAPProviderForm.ts index 0a4df7ada..c7adc1002 100644 --- a/web/src/admin/providers/ldap/LDAPProviderForm.ts +++ b/web/src/admin/providers/ldap/LDAPProviderForm.ts @@ -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 { } } + // 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`
@@ -73,36 +76,12 @@ export class LDAPProviderFormPage extends ModelForm { ?required=${true} name="authorizationFlow" > - => { - 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; - }} - > - +

${msg("Flow used for users to authenticate.")}

diff --git a/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts b/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts index 87d06d7d3..8e9bdbb02 100644 --- a/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts +++ b/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts @@ -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 { label=${msg("Authentication flow")} name="authenticationFlow" > - => { - 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; - }} - > - +

${msg("Flow used when a user access this provider and is not authenticated.")}

@@ -130,32 +106,11 @@ export class OAuth2ProviderFormPage extends ModelForm { ?required=${true} name="authorizationFlow" > - => { - 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; - }} - > - +

${msg("Flow used when authorizing this provider.")}

diff --git a/web/src/admin/providers/proxy/ProxyProviderForm.ts b/web/src/admin/providers/proxy/ProxyProviderForm.ts index 86e2cd024..e3d76c752 100644 --- a/web/src/admin/providers/proxy/ProxyProviderForm.ts +++ b/web/src/admin/providers/proxy/ProxyProviderForm.ts @@ -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 { ?required=${false} name="authenticationFlow" > - => { - 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; - }} - > - +

${msg("Flow used when a user access this provider and is not authenticated.")}

@@ -375,32 +351,11 @@ export class ProxyProviderFormPage extends ModelForm { ?required=${true} name="authorizationFlow" > - => { - 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; - }} - > - +

${msg("Flow used when authorizing this provider.")}

diff --git a/web/src/admin/providers/radius/RadiusProviderForm.ts b/web/src/admin/providers/radius/RadiusProviderForm.ts index 33841858c..9b1dc5452 100644 --- a/web/src/admin/providers/radius/RadiusProviderForm.ts +++ b/web/src/admin/providers/radius/RadiusProviderForm.ts @@ -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 { @@ -50,6 +42,12 @@ export class RadiusProviderFormPage extends ModelForm { } } + // 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` @@ -65,36 +63,12 @@ export class RadiusProviderFormPage extends ModelForm { ?required=${true} name="authorizationFlow" > - => { - 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; - }} - > - +

${msg("Flow used for users to authenticate.")}

diff --git a/web/src/admin/providers/saml/SAMLProviderForm.ts b/web/src/admin/providers/saml/SAMLProviderForm.ts index eee46e3bf..2d92d3a08 100644 --- a/web/src/admin/providers/saml/SAMLProviderForm.ts +++ b/web/src/admin/providers/saml/SAMLProviderForm.ts @@ -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 { ?required=${false} name="authenticationFlow" > - => { - 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; - }} - > - +

${msg("Flow used when a user access this provider and is not authenticated.")}

@@ -120,32 +96,11 @@ export class SAMLProviderFormPage extends ModelForm { ?required=${true} name="authorizationFlow" > - => { - 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; - }} - > - +

${msg("Flow used when authorizing this provider.")}

diff --git a/web/src/admin/providers/saml/SAMLProviderImportForm.ts b/web/src/admin/providers/saml/SAMLProviderImportForm.ts index d2730a708..b9eb8efce 100644 --- a/web/src/admin/providers/saml/SAMLProviderImportForm.ts +++ b/web/src/admin/providers/saml/SAMLProviderImportForm.ts @@ -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 { @@ -45,29 +38,10 @@ export class SAMLProviderImportForm extends Form { ?required=${true} name="authorizationFlow" > - => { - 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; - }} - > - +

${msg("Flow used when authorizing this provider.")}

diff --git a/web/src/admin/sources/oauth/OAuthSourceForm.ts b/web/src/admin/sources/oauth/OAuthSourceForm.ts index 63dddcea0..fdf4d22b0 100644 --- a/web/src/admin/sources/oauth/OAuthSourceForm.ts +++ b/web/src/admin/sources/oauth/OAuthSourceForm.ts @@ -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 { ?required=${true} name="authenticationFlow" > - => { - 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} - > - +

${msg("Flow to use when authenticating existing users.")}

@@ -459,43 +425,12 @@ export class OAuthSourceForm extends ModelForm { ?required=${true} name="enrollmentFlow" > - => { - 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} - > - +

${msg("Flow to use when enrolling new users.")}

diff --git a/web/src/admin/sources/plex/PlexSourceForm.ts b/web/src/admin/sources/plex/PlexSourceForm.ts index a84b987d1..729654edb 100644 --- a/web/src/admin/sources/plex/PlexSourceForm.ts +++ b/web/src/admin/sources/plex/PlexSourceForm.ts @@ -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 { ?required=${true} name="authenticationFlow" > - => { - 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} - > - +

${msg("Flow to use when authenticating existing users.")}

@@ -385,43 +351,12 @@ export class PlexSourceForm extends ModelForm { ?required=${true} name="enrollmentFlow" > - => { - 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} - > - +

${msg("Flow to use when enrolling new users.")}

diff --git a/web/src/admin/sources/saml/SAMLSourceForm.ts b/web/src/admin/sources/saml/SAMLSourceForm.ts index d41b3de62..86e7e3bb8 100644 --- a/web/src/admin/sources/saml/SAMLSourceForm.ts +++ b/web/src/admin/sources/saml/SAMLSourceForm.ts @@ -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 { ?required=${true} name="preAuthenticationFlow" > - => { - 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} - > - +

${msg("Flow used before authentication.")}

@@ -568,43 +533,12 @@ export class SAMLSourceForm extends ModelForm { ?required=${true} name="authenticationFlow" > - => { - 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} - > - +

${msg("Flow to use when authenticating existing users.")}

@@ -614,43 +548,12 @@ export class SAMLSourceForm extends ModelForm { ?required=${true} name="enrollmentFlow" > - => { - 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} - > - +

${msg("Flow to use when enrolling new users.")}

diff --git a/web/src/admin/stages/identification/IdentificationStageForm.ts b/web/src/admin/stages/identification/IdentificationStageForm.ts index c3c5ee137..2a808d123 100644 --- a/web/src/admin/stages/identification/IdentificationStageForm.ts +++ b/web/src/admin/stages/identification/IdentificationStageForm.ts @@ -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 - => { - 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} - > - +

${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 - => { - 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} - > - + +

${msg( "Optional enrollment flow, which is linked at the bottom of the page.", @@ -340,35 +288,10 @@ export class IdentificationStageForm extends ModelForm - => { - 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} - > - +

${msg( "Optional recovery flow, which is linked at the bottom of the page.", diff --git a/web/src/admin/stages/invitation/InvitationForm.ts b/web/src/admin/stages/invitation/InvitationForm.ts index 42080eed1..0ee755389 100644 --- a/web/src/admin/stages/invitation/InvitationForm.ts +++ b/web/src/admin/stages/invitation/InvitationForm.ts @@ -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 { @@ -75,33 +68,10 @@ export class InvitationForm extends ModelForm { /> - => { - 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} - > - +

${msg( "When selected, the invite will only be usable with the flow. By default the invite is accepted on all flows with invitation stages.", diff --git a/web/src/admin/tenants/TenantForm.ts b/web/src/admin/tenants/TenantForm.ts index 1095be2bb..34420b367 100644 --- a/web/src/admin/tenants/TenantForm.ts +++ b/web/src/admin/tenants/TenantForm.ts @@ -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 { label=${msg("Authentication flow")} name="flowAuthentication" > - => { - 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} - > - +

${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 { label=${msg("Invalidation flow")} name="flowInvalidation" > - => { - 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} - > - +

${msg( @@ -230,35 +177,10 @@ export class TenantForm extends ModelForm {

- => { - 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} - > - +

${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 { label=${msg("Unenrollment flow")} name="flowUnenrollment" > - => { - 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} - > - +

${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 { label=${msg("User settings flow")} name="flowUserSettings" > - => { - 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} - > - +

${msg("If set, users are able to configure details of their profile.")}

@@ -346,36 +217,10 @@ export class TenantForm extends ModelForm { label=${msg("Device code flow")} name="flowDeviceCode" > - => { - 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} - > - +

${msg( "If set, the OAuth Device Code profile can be used, and the selected flow will be used to enter the code.", diff --git a/web/src/elements/forms/Form.ts b/web/src/elements/forms/Form.ts index 734e6eb51..c3f3f4b7a 100644 --- a/web/src/elements/forms/Form.ts +++ b/web/src/elements/forms/Form.ts @@ -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 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; try { diff --git a/web/src/elements/forms/SearchSelect.ts b/web/src/elements/forms/SearchSelect.ts index 6c803c243..02d1f1fc2 100644 --- a/web/src/elements/forms/SearchSelect.ts +++ b/web/src/elements/forms/SearchSelect.ts @@ -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 extends AKElement { +export class SearchSelect extends CustomEmitterElement(AKElement) { @property() query?: string; @@ -91,6 +92,16 @@ export class SearchSelect extends AKElement { this.dropdownUID = `dropdown-${randomString(10, ascii_letters + digits)}`; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + shouldUpdate(changedProperties: Map) { + if (changedProperties.has("selectedObject")) { + this.dispatchCustomEvent("ak-change", { + value: this.selectedObject, + }); + } + return true; + } + toForm(): unknown { if (!this.objects) { throw new PreventFormSubmit(msg("Loading options...")); diff --git a/web/src/elements/utils/eventEmitter.ts b/web/src/elements/utils/eventEmitter.ts index 2d62b5676..9afa8be3b 100644 --- a/web/src/elements/utils/eventEmitter.ts +++ b/web/src/elements/utils/eventEmitter.ts @@ -25,10 +25,66 @@ export function CustomEmitterElement>(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; + export function CustomListenerElement>(superclass: T) { return class ListenerElementHandler extends superclass { - addCustomListener(eventName: string, handler: (ev: CustomEvent) => void) { - this.addEventListener(eventName, (ev: Event) => { + private [HK.listenHandlers] = new Map(); + + 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>(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); } }; }