From ff3e59c8e2f82f4a8ee100044a4cb166035ccade Mon Sep 17 00:00:00 2001 From: Teffen Ellis Date: Tue, 1 Apr 2025 05:16:56 +0200 Subject: [PATCH] Parity. --- .../ak-application-wizard-application-step.ts | 67 ++++++++++---- .../providers/ldap/LDAPOptionsAndHelp.ts | 8 +- web/src/common/constants.ts | 2 +- web/src/common/utils.ts | 33 ++++++- web/src/elements/sync/SyncStatusCard.ts | 21 ++++- web/src/elements/wizard/Wizard.ts | 10 ++- web/src/flow/stages/base.ts | 23 +++-- web/src/flow/stages/captcha/CaptchaStage.ts | 87 +++++++++++++++---- .../identification/IdentificationStage.ts | 10 ++- web/src/locale-codes.ts | 62 ++++++------- web/src/polyfill/poly.ts | 9 +- 11 files changed, 237 insertions(+), 95 deletions(-) diff --git a/web/src/admin/applications/wizard/steps/ak-application-wizard-application-step.ts b/web/src/admin/applications/wizard/steps/ak-application-wizard-application-step.ts index 80d57f81b2..35c44d1ee8 100644 --- a/web/src/admin/applications/wizard/steps/ak-application-wizard-application-step.ts +++ b/web/src/admin/applications/wizard/steps/ak-application-wizard-application-step.ts @@ -1,14 +1,13 @@ import { policyOptions } from "@goauthentik/admin/applications/PolicyOptions.js"; import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js"; import "@goauthentik/admin/applications/wizard/ak-wizard-title.js"; -import { isSlug } from "@goauthentik/common/utils.js"; +import { isSlug, isURLInput } from "@goauthentik/common/utils.js"; import { camelToSnake } from "@goauthentik/common/utils.js"; import "@goauthentik/components/ak-radio-input"; import "@goauthentik/components/ak-slug-input"; import "@goauthentik/components/ak-switch-input"; import "@goauthentik/components/ak-text-input"; import { type NavigableButton, type WizardButton } from "@goauthentik/components/ak-wizard/types"; -import { type KeyUnknown } from "@goauthentik/elements/forms/Form"; import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/HorizontalFormElement"; @@ -21,13 +20,25 @@ import { type ApplicationRequest } from "@goauthentik/api"; import { ApplicationWizardStateUpdate, ValidationRecord } from "../types"; -const autoTrim = (v: unknown) => (typeof v === "string" ? v.trim() : v); +/** + * Plucks the specified keys from an object, trimming their values if they are strings. + * + * @template T - The type of the input object. + * @template K - The keys to be plucked from the input object. + * + * @param {T} input - The input object. + * @param {Array} keys - The keys to be plucked from the input object. + */ +function trimMany(input: T, keys: Array): Pick { + const result: Partial = {}; -const trimMany = (o: KeyUnknown, vs: string[]) => - Object.fromEntries(vs.map((v) => [v, autoTrim(o[v])])); + for (const key of keys) { + const value = input[key]; + result[key] = (typeof value === "string" ? value.trim() : value) as T[K]; + } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const isStr = (v: any): v is string => typeof v === "string"; + return result as Pick; +} @customElement("ak-application-wizard-application-step") export class ApplicationWizardApplicationStep extends ApplicationWizardStep { @@ -54,27 +65,34 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep { } get buttons(): WizardButton[] { - return [{ kind: "next", destination: "provider-choice" }, { kind: "cancel" }]; + return [ + // --- + { kind: "next", destination: "provider-choice" }, + { kind: "cancel" }, + ]; } get valid() { this.errors = new Map(); - const values = trimMany(this.formValues ?? {}, ["metaLaunchUrl", "name", "slug"]); - if (values.name === "") { + const trimmed = trimMany((this.formValues || {}) as Partial, [ + "name", + "slug", + "metaLaunchUrl", + ]); + + if (!trimmed.name) { this.errors.set("name", msg("An application name is required")); } - if ( - !( - isStr(values.metaLaunchUrl) && - (values.metaLaunchUrl === "" || URL.canParse(values.metaLaunchUrl)) - ) - ) { + + if (!isURLInput(trimmed.metaLaunchUrl)) { this.errors.set("metaLaunchUrl", msg("Not a valid URL")); } - if (!(isStr(values.slug) && values.slug !== "" && isSlug(values.slug))) { + + if (!isSlug(trimmed.slug)) { this.errors.set("slug", msg("Not a valid slug")); } + return this.errors.size === 0; } @@ -82,27 +100,39 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep { if (button.kind === "next") { if (!this.valid) { this.handleEnabling({ - disabled: ["provider-choice", "provider", "bindings", "submit"], + disabled: [ + // --- + "provider-choice", + "provider", + "bindings", + "submit", + ], }); + return; } + const app: Partial = this.formValues as Partial; let payload: ApplicationWizardStateUpdate = { app: this.formValues, errors: this.removeErrors("app"), }; + if (app.name && (this.wizard.provider?.name ?? "").trim() === "") { payload = { ...payload, provider: { name: `Provider for ${app.name}` }, }; } + this.handleUpdate(payload, button.destination, { enable: "provider-choice", }); + return; } + super.handleButton(button); } @@ -181,6 +211,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep { if (!(this.wizard.app && this.wizard.errors)) { throw new Error("Application Step received uninitialized wizard context."); } + return this.renderForm( this.wizard.app as ApplicationRequest, this.wizard.errors?.app ?? {}, diff --git a/web/src/admin/providers/ldap/LDAPOptionsAndHelp.ts b/web/src/admin/providers/ldap/LDAPOptionsAndHelp.ts index da967a9a50..5265abf049 100644 --- a/web/src/admin/providers/ldap/LDAPOptionsAndHelp.ts +++ b/web/src/admin/providers/ldap/LDAPOptionsAndHelp.ts @@ -15,7 +15,9 @@ export const bindModeOptions = [ { label: msg("Direct binding"), value: LDAPAPIAccessMode.Direct, - description: html`${msg("Always execute the configured bind flow to authenticate the user")}`, + description: html`${msg( + "Always execute the configured bind flow to authenticate the user", + )}`, }, ]; @@ -31,7 +33,9 @@ export const searchModeOptions = [ { label: msg("Direct querying"), value: LDAPAPIAccessMode.Direct, - description: html`${msg("Always returns the latest data, but slower than cached querying")}`, + description: html`${msg( + "Always returns the latest data, but slower than cached querying", + )}`, }, ]; diff --git a/web/src/common/constants.ts b/web/src/common/constants.ts index 7e7c73a111..713f5c61e6 100644 --- a/web/src/common/constants.ts +++ b/web/src/common/constants.ts @@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success"; export const ERROR_CLASS = "pf-m-danger"; export const PROGRESS_CLASS = "pf-m-in-progress"; export const CURRENT_CLASS = "pf-m-current"; -export const VERSION = "2025.2.2"; +export const VERSION = "2025.2.3"; export const TITLE_DEFAULT = "authentik"; export const ROUTE_SEPARATOR = ";"; diff --git a/web/src/common/utils.ts b/web/src/common/utils.ts index dcb0947a59..f8beb9d08b 100644 --- a/web/src/common/utils.ts +++ b/web/src/common/utils.ts @@ -25,10 +25,35 @@ export function convertToSlug(text: string): string { .replace(/[^\w-]+/g, ""); } -export function isSlug(text: string): boolean { - const lowered = text.toLowerCase(); - const forbidden = /([^\w-]|\s)/.test(lowered); - return lowered === text && !forbidden; +/** + * Type guard to check if a given string is a valid URL slug, i.e. + * only containing alphanumeric characters, dashes, and underscores. + */ +export function isSlug(input: unknown): input is string { + if (typeof input !== "string") return false; + if (!input) return false; + + const lowered = input.toLowerCase(); + if (input !== lowered) return false; + + return /([^\w-]|\s)/.test(lowered); +} + +/** + * Type guard to check if a given input is parsable as a URL. + * + * ```js + * isURLInput("https://example.com") // true + * isURLInput("invalid-url") // false + * isURLInput(new URL("https://example.com")) // true + * ``` + */ +export function isURLInput(input: unknown): input is string | URL { + if (typeof input !== "string" && !(input instanceof URL)) return false; + + if (!input) return false; + + return URL.canParse(input); } /** diff --git a/web/src/elements/sync/SyncStatusCard.ts b/web/src/elements/sync/SyncStatusCard.ts index ce329dd61a..fb75d9995a 100644 --- a/web/src/elements/sync/SyncStatusCard.ts +++ b/web/src/elements/sync/SyncStatusCard.ts @@ -11,6 +11,7 @@ import { msg } from "@lit/localize"; import { CSSResult, TemplateResult, css, html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; +import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFCard from "@patternfly/patternfly/components/Card/card.css"; import PFTable from "@patternfly/patternfly/components/Table/table.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; @@ -34,6 +35,9 @@ export class SyncStatusTable extends Table { } async apiEndpoint(): Promise> { + if (this.tasks.length === 1) { + this.expandedElements = this.tasks; + } return { pagination: { next: 0, @@ -104,7 +108,7 @@ export class SyncStatusCard extends AKElement { triggerSync!: () => Promise; static get styles(): CSSResult[] { - return [PFBase, PFCard, PFTable]; + return [PFBase, PFButton, PFCard, PFTable]; } firstUpdated() { @@ -133,7 +137,20 @@ export class SyncStatusCard extends AKElement { render(): TemplateResult { return html`
-
${msg("Sync status")}
+
+
+ +
+
${msg("Sync status")}
+
${this.renderSyncStatus()}