Parity.
This commit is contained in:
@ -1,14 +1,13 @@
|
|||||||
import { policyOptions } from "@goauthentik/admin/applications/PolicyOptions.js";
|
import { policyOptions } from "@goauthentik/admin/applications/PolicyOptions.js";
|
||||||
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
|
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
|
||||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title.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 { camelToSnake } from "@goauthentik/common/utils.js";
|
||||||
import "@goauthentik/components/ak-radio-input";
|
import "@goauthentik/components/ak-radio-input";
|
||||||
import "@goauthentik/components/ak-slug-input";
|
import "@goauthentik/components/ak-slug-input";
|
||||||
import "@goauthentik/components/ak-switch-input";
|
import "@goauthentik/components/ak-switch-input";
|
||||||
import "@goauthentik/components/ak-text-input";
|
import "@goauthentik/components/ak-text-input";
|
||||||
import { type NavigableButton, type WizardButton } from "@goauthentik/components/ak-wizard/types";
|
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/FormGroup";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
|
|
||||||
@ -21,13 +20,25 @@ import { type ApplicationRequest } from "@goauthentik/api";
|
|||||||
|
|
||||||
import { ApplicationWizardStateUpdate, ValidationRecord } from "../types";
|
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<K>} keys - The keys to be plucked from the input object.
|
||||||
|
*/
|
||||||
|
function trimMany<T extends object, K extends keyof T>(input: T, keys: Array<K>): Pick<T, K> {
|
||||||
|
const result: Partial<T> = {};
|
||||||
|
|
||||||
const trimMany = (o: KeyUnknown, vs: string[]) =>
|
for (const key of keys) {
|
||||||
Object.fromEntries(vs.map((v) => [v, autoTrim(o[v])]));
|
const value = input[key];
|
||||||
|
result[key] = (typeof value === "string" ? value.trim() : value) as T[K];
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
return result as Pick<T, K>;
|
||||||
const isStr = (v: any): v is string => typeof v === "string";
|
}
|
||||||
|
|
||||||
@customElement("ak-application-wizard-application-step")
|
@customElement("ak-application-wizard-application-step")
|
||||||
export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||||
@ -54,27 +65,34 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get buttons(): WizardButton[] {
|
get buttons(): WizardButton[] {
|
||||||
return [{ kind: "next", destination: "provider-choice" }, { kind: "cancel" }];
|
return [
|
||||||
|
// ---
|
||||||
|
{ kind: "next", destination: "provider-choice" },
|
||||||
|
{ kind: "cancel" },
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
get valid() {
|
get valid() {
|
||||||
this.errors = new Map();
|
this.errors = new Map();
|
||||||
const values = trimMany(this.formValues ?? {}, ["metaLaunchUrl", "name", "slug"]);
|
|
||||||
|
|
||||||
if (values.name === "") {
|
const trimmed = trimMany((this.formValues || {}) as Partial<ApplicationRequest>, [
|
||||||
|
"name",
|
||||||
|
"slug",
|
||||||
|
"metaLaunchUrl",
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!trimmed.name) {
|
||||||
this.errors.set("name", msg("An application name is required"));
|
this.errors.set("name", msg("An application name is required"));
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
!(
|
if (!isURLInput(trimmed.metaLaunchUrl)) {
|
||||||
isStr(values.metaLaunchUrl) &&
|
|
||||||
(values.metaLaunchUrl === "" || URL.canParse(values.metaLaunchUrl))
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.errors.set("metaLaunchUrl", msg("Not a valid URL"));
|
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"));
|
this.errors.set("slug", msg("Not a valid slug"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.errors.size === 0;
|
return this.errors.size === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,27 +100,39 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
|||||||
if (button.kind === "next") {
|
if (button.kind === "next") {
|
||||||
if (!this.valid) {
|
if (!this.valid) {
|
||||||
this.handleEnabling({
|
this.handleEnabling({
|
||||||
disabled: ["provider-choice", "provider", "bindings", "submit"],
|
disabled: [
|
||||||
|
// ---
|
||||||
|
"provider-choice",
|
||||||
|
"provider",
|
||||||
|
"bindings",
|
||||||
|
"submit",
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const app: Partial<ApplicationRequest> = this.formValues as Partial<ApplicationRequest>;
|
const app: Partial<ApplicationRequest> = this.formValues as Partial<ApplicationRequest>;
|
||||||
|
|
||||||
let payload: ApplicationWizardStateUpdate = {
|
let payload: ApplicationWizardStateUpdate = {
|
||||||
app: this.formValues,
|
app: this.formValues,
|
||||||
errors: this.removeErrors("app"),
|
errors: this.removeErrors("app"),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (app.name && (this.wizard.provider?.name ?? "").trim() === "") {
|
if (app.name && (this.wizard.provider?.name ?? "").trim() === "") {
|
||||||
payload = {
|
payload = {
|
||||||
...payload,
|
...payload,
|
||||||
provider: { name: `Provider for ${app.name}` },
|
provider: { name: `Provider for ${app.name}` },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.handleUpdate(payload, button.destination, {
|
this.handleUpdate(payload, button.destination, {
|
||||||
enable: "provider-choice",
|
enable: "provider-choice",
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
super.handleButton(button);
|
super.handleButton(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,6 +211,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
|||||||
if (!(this.wizard.app && this.wizard.errors)) {
|
if (!(this.wizard.app && this.wizard.errors)) {
|
||||||
throw new Error("Application Step received uninitialized wizard context.");
|
throw new Error("Application Step received uninitialized wizard context.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.renderForm(
|
return this.renderForm(
|
||||||
this.wizard.app as ApplicationRequest,
|
this.wizard.app as ApplicationRequest,
|
||||||
this.wizard.errors?.app ?? {},
|
this.wizard.errors?.app ?? {},
|
||||||
|
|||||||
@ -15,7 +15,9 @@ export const bindModeOptions = [
|
|||||||
{
|
{
|
||||||
label: msg("Direct binding"),
|
label: msg("Direct binding"),
|
||||||
value: LDAPAPIAccessMode.Direct,
|
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"),
|
label: msg("Direct querying"),
|
||||||
value: LDAPAPIAccessMode.Direct,
|
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",
|
||||||
|
)}`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
|||||||
export const ERROR_CLASS = "pf-m-danger";
|
export const ERROR_CLASS = "pf-m-danger";
|
||||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||||
export const CURRENT_CLASS = "pf-m-current";
|
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 TITLE_DEFAULT = "authentik";
|
||||||
export const ROUTE_SEPARATOR = ";";
|
export const ROUTE_SEPARATOR = ";";
|
||||||
|
|
||||||
|
|||||||
@ -25,10 +25,35 @@ export function convertToSlug(text: string): string {
|
|||||||
.replace(/[^\w-]+/g, "");
|
.replace(/[^\w-]+/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSlug(text: string): boolean {
|
/**
|
||||||
const lowered = text.toLowerCase();
|
* Type guard to check if a given string is a valid URL slug, i.e.
|
||||||
const forbidden = /([^\w-]|\s)/.test(lowered);
|
* only containing alphanumeric characters, dashes, and underscores.
|
||||||
return lowered === text && !forbidden;
|
*/
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { msg } from "@lit/localize";
|
|||||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
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 PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||||
import PFTable from "@patternfly/patternfly/components/Table/table.css";
|
import PFTable from "@patternfly/patternfly/components/Table/table.css";
|
||||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
@ -34,6 +35,9 @@ export class SyncStatusTable extends Table<SystemTask> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async apiEndpoint(): Promise<PaginatedResponse<SystemTask>> {
|
async apiEndpoint(): Promise<PaginatedResponse<SystemTask>> {
|
||||||
|
if (this.tasks.length === 1) {
|
||||||
|
this.expandedElements = this.tasks;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
pagination: {
|
pagination: {
|
||||||
next: 0,
|
next: 0,
|
||||||
@ -104,7 +108,7 @@ export class SyncStatusCard extends AKElement {
|
|||||||
triggerSync!: () => Promise<unknown>;
|
triggerSync!: () => Promise<unknown>;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFCard, PFTable];
|
return [PFBase, PFButton, PFCard, PFTable];
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
@ -133,7 +137,20 @@ export class SyncStatusCard extends AKElement {
|
|||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`<div class="pf-c-card">
|
return html`<div class="pf-c-card">
|
||||||
<div class="pf-c-card__title">${msg("Sync status")}</div>
|
<div class="pf-c-card__header">
|
||||||
|
<div class="pf-c-card__actions">
|
||||||
|
<button
|
||||||
|
class="pf-c-button pf-m-plain"
|
||||||
|
type="button"
|
||||||
|
@click=${() => {
|
||||||
|
this.fetch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i class="fa fa-sync"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="pf-c-card__title">${msg("Sync status")}</div>
|
||||||
|
</div>
|
||||||
<div class="pf-c-card__body">${this.renderSyncStatus()}</div>
|
<div class="pf-c-card__body">${this.renderSyncStatus()}</div>
|
||||||
<div class="pf-c-card__footer">
|
<div class="pf-c-card__footer">
|
||||||
<ak-action-button
|
<ak-action-button
|
||||||
|
|||||||
@ -187,7 +187,11 @@ export class Wizard extends ModalButton {
|
|||||||
/**
|
/**
|
||||||
* Reset the wizard to it's initial state.
|
* Reset the wizard to it's initial state.
|
||||||
*/
|
*/
|
||||||
reset = () => {
|
reset = (ev?: Event) => {
|
||||||
|
if (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
this.open = false;
|
this.open = false;
|
||||||
|
|
||||||
this.querySelectorAll("[data-wizardmanaged=true]").forEach((el) => {
|
this.querySelectorAll("[data-wizardmanaged=true]").forEach((el) => {
|
||||||
@ -332,9 +336,7 @@ export class Wizard extends ModalButton {
|
|||||||
<button
|
<button
|
||||||
class="pf-c-button pf-m-link"
|
class="pf-c-button pf-m-link"
|
||||||
type="button"
|
type="button"
|
||||||
@click=${() => {
|
@click=${this.reset}
|
||||||
this.reset();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
${msg("Cancel")}
|
${msg("Cancel")}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -72,7 +72,9 @@ export class BaseStage<
|
|||||||
}
|
}
|
||||||
return this.host?.submit(object as unknown as Tout).then((successful) => {
|
return this.host?.submit(object as unknown as Tout).then((successful) => {
|
||||||
if (successful) {
|
if (successful) {
|
||||||
this.cleanup();
|
this.onSubmitSuccess();
|
||||||
|
} else {
|
||||||
|
this.onSubmitFailure();
|
||||||
}
|
}
|
||||||
return successful;
|
return successful;
|
||||||
});
|
});
|
||||||
@ -80,13 +82,13 @@ export class BaseStage<
|
|||||||
|
|
||||||
renderNonFieldErrors() {
|
renderNonFieldErrors() {
|
||||||
const errors = this.challenge?.responseErrors || {};
|
const errors = this.challenge?.responseErrors || {};
|
||||||
if (!("non_field_errors" in errors)) {
|
|
||||||
return nothing;
|
if (!("non_field_errors" in errors)) return nothing;
|
||||||
}
|
|
||||||
const nonFieldErrors = errors.non_field_errors;
|
const nonFieldErrors = errors.non_field_errors;
|
||||||
if (!nonFieldErrors) {
|
|
||||||
return nothing;
|
if (!nonFieldErrors) return nothing;
|
||||||
}
|
|
||||||
return html`<div class="pf-c-form__alert">
|
return html`<div class="pf-c-form__alert">
|
||||||
${nonFieldErrors.map((err) => {
|
${nonFieldErrors.map((err) => {
|
||||||
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
|
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
|
||||||
@ -124,7 +126,12 @@ export class BaseStage<
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup(): void {
|
onSubmitSuccess(): void {
|
||||||
|
// Method that can be overridden by stages
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmitFailure(): void {
|
||||||
// Method that can be overridden by stages
|
// Method that can be overridden by stages
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
/// <reference types="@hcaptcha/types"/>
|
///<reference types="@hcaptcha/types"/>
|
||||||
import { renderStatic } from "@goauthentik/common/purify";
|
import { renderStatic } from "@goauthentik/common/purify";
|
||||||
import "@goauthentik/elements/EmptyState";
|
import "@goauthentik/elements/EmptyState";
|
||||||
import { akEmptyState } from "@goauthentik/elements/EmptyState";
|
import { akEmptyState } from "@goauthentik/elements/EmptyState";
|
||||||
@ -9,7 +9,7 @@ import { randomId } from "@goauthentik/elements/utils/randomId";
|
|||||||
import "@goauthentik/flow/FormStatic";
|
import "@goauthentik/flow/FormStatic";
|
||||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||||
import { P, match } from "ts-pattern";
|
import { P, match } from "ts-pattern";
|
||||||
import type { TurnstileObject } from "turnstile-types";
|
import type * as _ from "turnstile-types";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
|
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
|
||||||
@ -24,10 +24,6 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
|||||||
|
|
||||||
import { CaptchaChallenge, CaptchaChallengeResponseRequest } from "@goauthentik/api";
|
import { CaptchaChallenge, CaptchaChallengeResponseRequest } from "@goauthentik/api";
|
||||||
|
|
||||||
interface TurnstileWindow extends Window {
|
|
||||||
turnstile: TurnstileObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
type TokenHandler = (token: string) => void;
|
type TokenHandler = (token: string) => void;
|
||||||
|
|
||||||
type Dims = { height: number };
|
type Dims = { height: number };
|
||||||
@ -52,6 +48,8 @@ type CaptchaHandler = {
|
|||||||
name: string;
|
name: string;
|
||||||
interactive: () => Promise<unknown>;
|
interactive: () => Promise<unknown>;
|
||||||
execute: () => Promise<unknown>;
|
execute: () => Promise<unknown>;
|
||||||
|
refreshInteractive: () => Promise<unknown>;
|
||||||
|
refresh: () => Promise<unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// A container iframe for a hosted Captcha, with an event emitter to monitor when the Captcha forces
|
// A container iframe for a hosted Captcha, with an event emitter to monitor when the Captcha forces
|
||||||
@ -119,6 +117,12 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
|||||||
this.host.submit({ component: "ak-stage-captcha", token });
|
this.host.submit({ component: "ak-stage-captcha", token });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
refreshedAt = new Date();
|
||||||
|
|
||||||
|
@state()
|
||||||
|
activeHandler?: CaptchaHandler = undefined;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|
||||||
@ -127,16 +131,22 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
|||||||
name: "grecaptcha",
|
name: "grecaptcha",
|
||||||
interactive: this.renderGReCaptchaFrame,
|
interactive: this.renderGReCaptchaFrame,
|
||||||
execute: this.executeGReCaptcha,
|
execute: this.executeGReCaptcha,
|
||||||
|
refreshInteractive: this.refreshGReCaptchaFrame,
|
||||||
|
refresh: this.refreshGReCaptcha,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "hcaptcha",
|
name: "hcaptcha",
|
||||||
interactive: this.renderHCaptchaFrame,
|
interactive: this.renderHCaptchaFrame,
|
||||||
execute: this.executeHCaptcha,
|
execute: this.executeHCaptcha,
|
||||||
|
refreshInteractive: this.refreshHCaptchaFrame,
|
||||||
|
refresh: this.refreshHCaptcha,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "turnstile",
|
name: "turnstile",
|
||||||
interactive: this.renderTurnstileFrame,
|
interactive: this.renderTurnstileFrame,
|
||||||
execute: this.executeTurnstile,
|
execute: this.executeTurnstile,
|
||||||
|
refreshInteractive: this.refreshTurnstileFrame,
|
||||||
|
refresh: this.refreshTurnstile,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -205,7 +215,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
|||||||
console.debug(`authentik/stages/captcha: Unknown message: ${message}`);
|
console.debug(`authentik/stages/captcha: Unknown message: ${message}`);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.otherwise(() => null);
|
.otherwise(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderGReCaptchaFrame() {
|
async renderGReCaptchaFrame() {
|
||||||
@ -230,6 +240,15 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refreshGReCaptchaFrame() {
|
||||||
|
(this.captchaFrame.contentWindow as typeof window)?.grecaptcha.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshGReCaptcha() {
|
||||||
|
window.grecaptcha.reset();
|
||||||
|
window.grecaptcha.execute();
|
||||||
|
}
|
||||||
|
|
||||||
async renderHCaptchaFrame() {
|
async renderHCaptchaFrame() {
|
||||||
this.renderFrame(
|
this.renderFrame(
|
||||||
html`<div
|
html`<div
|
||||||
@ -251,6 +270,15 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refreshHCaptchaFrame() {
|
||||||
|
(this.captchaFrame.contentWindow as typeof window)?.hcaptcha.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshHCaptcha() {
|
||||||
|
window.hcaptcha.reset();
|
||||||
|
window.hcaptcha.execute();
|
||||||
|
}
|
||||||
|
|
||||||
async renderTurnstileFrame() {
|
async renderTurnstileFrame() {
|
||||||
this.renderFrame(
|
this.renderFrame(
|
||||||
html`<div
|
html`<div
|
||||||
@ -262,13 +290,18 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
async executeTurnstile() {
|
async executeTurnstile() {
|
||||||
return (window as unknown as TurnstileWindow).turnstile.render(
|
return window.turnstile.render(this.captchaDocumentContainer, {
|
||||||
this.captchaDocumentContainer,
|
sitekey: this.challenge.siteKey,
|
||||||
{
|
callback: this.onTokenChange,
|
||||||
sitekey: this.challenge.siteKey,
|
});
|
||||||
callback: this.onTokenChange,
|
}
|
||||||
},
|
|
||||||
);
|
async refreshTurnstileFrame() {
|
||||||
|
(this.captchaFrame.contentWindow as typeof window)?.turnstile.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshTurnstile() {
|
||||||
|
window.turnstile.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderFrame(captchaElement: TemplateResult) {
|
async renderFrame(captchaElement: TemplateResult) {
|
||||||
@ -336,16 +369,19 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
|||||||
const handlers = this.handlers.filter(({ name }) => Object.hasOwn(window, name));
|
const handlers = this.handlers.filter(({ name }) => Object.hasOwn(window, name));
|
||||||
let lastError = undefined;
|
let lastError = undefined;
|
||||||
let found = false;
|
let found = false;
|
||||||
for (const { name, interactive, execute } of handlers) {
|
for (const handler of handlers) {
|
||||||
console.debug(`authentik/stages/captcha: trying handler ${name}`);
|
console.debug(`authentik/stages/captcha: trying handler ${handler.name}`);
|
||||||
try {
|
try {
|
||||||
const runner = this.challenge.interactive ? interactive : execute;
|
const runner = this.challenge.interactive
|
||||||
|
? handler.interactive
|
||||||
|
: handler.execute;
|
||||||
await runner.apply(this);
|
await runner.apply(this);
|
||||||
console.debug(`authentik/stages/captcha[${name}]: handler succeeded`);
|
console.debug(`authentik/stages/captcha[${handler.name}]: handler succeeded`);
|
||||||
found = true;
|
found = true;
|
||||||
|
this.activeHandler = handler;
|
||||||
break;
|
break;
|
||||||
} catch (exc) {
|
} catch (exc) {
|
||||||
console.debug(`authentik/stages/captcha[${name}]: handler failed`);
|
console.debug(`authentik/stages/captcha[${handler.name}]: handler failed`);
|
||||||
console.debug(exc);
|
console.debug(exc);
|
||||||
lastError = exc;
|
lastError = exc;
|
||||||
}
|
}
|
||||||
@ -370,6 +406,19 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
|||||||
document.body.appendChild(this.captchaDocumentContainer);
|
document.body.appendChild(this.captchaDocumentContainer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updated(changedProperties: PropertyValues<this>) {
|
||||||
|
if (!changedProperties.has("refreshedAt") || !this.challenge) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug("authentik/stages/captcha: refresh triggered");
|
||||||
|
if (this.challenge.interactive) {
|
||||||
|
this.activeHandler?.refreshInteractive.apply(this);
|
||||||
|
} else {
|
||||||
|
this.activeHandler?.refresh.apply(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@ -49,6 +49,8 @@ export class IdentificationStage extends BaseStage<
|
|||||||
|
|
||||||
@state()
|
@state()
|
||||||
captchaToken = "";
|
captchaToken = "";
|
||||||
|
@state()
|
||||||
|
captchaRefreshedAt = new Date();
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [
|
return [
|
||||||
@ -136,6 +138,7 @@ export class IdentificationStage extends BaseStage<
|
|||||||
if (ev.key === "Enter") {
|
if (ev.key === "Enter") {
|
||||||
this.submitForm(ev);
|
this.submitForm(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
const el = ev.target as HTMLInputElement;
|
const el = ev.target as HTMLInputElement;
|
||||||
// Because the password field is not actually on this page,
|
// Because the password field is not actually on this page,
|
||||||
// and we want to 'prefill' the password for the user,
|
// and we want to 'prefill' the password for the user,
|
||||||
@ -179,12 +182,16 @@ export class IdentificationStage extends BaseStage<
|
|||||||
this.form.appendChild(totp);
|
this.form.appendChild(totp);
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup(): void {
|
onSubmitSuccess(): void {
|
||||||
if (this.form) {
|
if (this.form) {
|
||||||
this.form.remove();
|
this.form.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSubmitFailure(): void {
|
||||||
|
this.captchaRefreshedAt = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
renderSource(source: LoginSource): TemplateResult {
|
renderSource(source: LoginSource): TemplateResult {
|
||||||
const icon = renderSourceIcon(source.name, source.iconUrl);
|
const icon = renderSourceIcon(source.name, source.iconUrl);
|
||||||
return html`<li class="pf-c-login__main-footer-links-item">
|
return html`<li class="pf-c-login__main-footer-links-item">
|
||||||
@ -287,6 +294,7 @@ export class IdentificationStage extends BaseStage<
|
|||||||
.onTokenChange=${(token: string) => {
|
.onTokenChange=${(token: string) => {
|
||||||
this.captchaToken = token;
|
this.captchaToken = token;
|
||||||
}}
|
}}
|
||||||
|
.refreshedAt=${this.captchaRefreshedAt}
|
||||||
embedded
|
embedded
|
||||||
></ak-stage-captcha>
|
></ak-stage-captcha>
|
||||||
`
|
`
|
||||||
|
|||||||
@ -11,41 +11,41 @@ export const sourceLocale = `en`;
|
|||||||
* lexicographically.
|
* lexicographically.
|
||||||
*/
|
*/
|
||||||
export const targetLocales = [
|
export const targetLocales = [
|
||||||
`de`,
|
`de`,
|
||||||
`en`,
|
`en`,
|
||||||
`es`,
|
`es`,
|
||||||
`fr`,
|
`fr`,
|
||||||
`it`,
|
`it`,
|
||||||
`ko`,
|
`ko`,
|
||||||
`nl`,
|
`nl`,
|
||||||
`pl`,
|
`pl`,
|
||||||
`pseudo-LOCALE`,
|
`pseudo-LOCALE`,
|
||||||
`ru`,
|
`ru`,
|
||||||
`tr`,
|
`tr`,
|
||||||
`zh_TW`,
|
`zh_TW`,
|
||||||
`zh-CN`,
|
`zh-CN`,
|
||||||
`zh-Hans`,
|
`zh-Hans`,
|
||||||
`zh-Hant`,
|
`zh-Hant`,
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All valid project locale codes. Sorted lexicographically.
|
* All valid project locale codes. Sorted lexicographically.
|
||||||
*/
|
*/
|
||||||
export const allLocales = [
|
export const allLocales = [
|
||||||
`de`,
|
`de`,
|
||||||
`en`,
|
`en`,
|
||||||
`en`,
|
`en`,
|
||||||
`es`,
|
`es`,
|
||||||
`fr`,
|
`fr`,
|
||||||
`it`,
|
`it`,
|
||||||
`ko`,
|
`ko`,
|
||||||
`nl`,
|
`nl`,
|
||||||
`pl`,
|
`pl`,
|
||||||
`pseudo-LOCALE`,
|
`pseudo-LOCALE`,
|
||||||
`ru`,
|
`ru`,
|
||||||
`tr`,
|
`tr`,
|
||||||
`zh_TW`,
|
`zh_TW`,
|
||||||
`zh-CN`,
|
`zh-CN`,
|
||||||
`zh-Hans`,
|
`zh-Hans`,
|
||||||
`zh-Hant`,
|
`zh-Hant`,
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import "@formatjs/intl-listformat/locale-data/en";
|
|
||||||
import "@formatjs/intl-listformat/polyfill";
|
|
||||||
import "@webcomponents/webcomponentsjs";
|
|
||||||
import "construct-style-sheets-polyfill";
|
import "construct-style-sheets-polyfill";
|
||||||
|
import "@webcomponents/webcomponentsjs";
|
||||||
|
import "lit/polyfill-support.js";
|
||||||
import "core-js/actual";
|
import "core-js/actual";
|
||||||
|
|
||||||
import "lit/polyfill-support.js";
|
import "@formatjs/intl-listformat/polyfill";
|
||||||
|
import "@formatjs/intl-listformat/locale-data/en";
|
||||||
|
|||||||
Reference in New Issue
Block a user