web/flows: resize captcha iframes (#12260) * web: Add InvalidationFlow to Radius Provider dialogues ## What - Bugfix: adds the InvalidationFlow to the Radius Provider dialogues - Repairs: `{"invalidation_flow":["This field is required."]}` message, which was *not* propagated to the Notification. - Nitpick: Pretties `?foo=${true}` expressions: `s/\?([^=]+)=\$\{true\}/\1/` ## Note Yes, I know I'm going to have to do more magic when we harmonize the forms, and no, I didn't add the Property Mappings to the wizard, and yes, I know I'm going to have pain with the *new* version of the wizard. But this is a serious bug; you can't make Radius servers with *either* of the current dialogues at the moment. * web: streamline CaptchaStage # What This commit: 1. Replaces the mass of `if () { if() { if() } }` with two state tables: - One for `render()` - One for `renderBody()` 2. Breaks each Captcha out into "interactive" and "executive" versions 3. Creates a handler table for each Captcha type 4. Replaces the `.forEach` expression with a `for` loop. 5. Move `updated` to the end of the class. 6. Make captchDocument and captchaFrame constructed-on-demand with a cache. 7. Remove a lot of `@state` handlers 8. Give IframeMessageEvent its own type. 9. Removed `this.scriptElement` 10. Replaced `window.removeEventListener` with an `AbortController()` # Why 1. **Replacing `if` trees with a state table.** The logic of the original was really hard to follow. With the state table, we can clearly see now that for the `render()` function, we care about the Boolean flags `[embedded, challenged, interactive]` and have appropriate effects for each. With `renderBody()`, we can see that we care about the Boolean flags `[hasError, challenged]`, and can see the appropriate effects for each one. 2. (and 3.) **Breaking each Captcha clause into separate handlers.** Again, the logic was convoluted, when what we really care about is "Does this captcha have a corresponding handler attached to `window`, and, if so, should we run the interactive or executive version of it?" By putting all of that into a table of `[name, challenge, execute]`, we can clearly see what's being handled when. 4. **Replacing `foreach()` with `for()`**: [You cannot use a `forEach()` with async expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach#:~:text=does%20not%20wait%20for%20promises). If you need asynchronous behavior in an ordered loop, `for()` is the safest way to handle it; if you need asynchronous behavior from multiple promises, `Promise.allSettled(handlers.map())` is the way to go. I tried to tell if this function *meant* to run every handler it found simultaneously, or if it meant to test them in order; I went with the second option, breaking and exiting the loop once a handler had run successfully. 5. **Reordered the code a bit**. We're trying to evolve a pattern in our source code: styles, properties, states, internal variables, constructor, getters & setters that are not `@property()` or `@state()`, DOM-related lifecycle handlers, event handlers, pre-render lifecycle handlers, renderers, and post-render lifecycle handlers. Helper methods (including subrenderers) go above the method(s) they help. 6. **Constructing Elements on demand with a cache**. It is not guaranteed that we will actually need either of those. Constructing them on demand with a cache is both performant and cleaner. Likewise, I removed these from the Lit object's `state()` table, since they're constructed once and never change over the lifetime of an instance of `ak-stage-captcha`. 9. **Remove `this.scriptElement`**: It was never referenced outside the one function where it was used. 10. **Remove `removeEventListener()`**: `AbortController()` is a bit more verbose for small event handler collections, but it's considered much more reliable and much cleaner. * Didn't save the extracted ListenerController. Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
This commit is contained in:
![98988430+gcp-cherry-pick-bot[bot]@users.noreply.github.com](/assets/img/avatar_default.png)
committed by
GitHub

parent
efac5ce7bd
commit
0e4b153e7f
41
web/src/elements/utils/listenerController.ts
Normal file
41
web/src/elements/utils/listenerController.ts
Normal file
@ -0,0 +1,41 @@
|
||||
// This is a more modern way to handle disconnecting listeners on demand.
|
||||
|
||||
// example usage:
|
||||
|
||||
/*
|
||||
export class MyElement extends LitElement {
|
||||
|
||||
this.listenerController = new ListenerController();
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("event-1", handler1, { signal: this.listenerController.signal });
|
||||
window.addEventListener("event-2", handler2, { signal: this.listenerController.signal });
|
||||
window.addEventListener("event-3", handler3, { signal: this.listenerController.signal });
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
// This will disconnect *all* the event listeners at once, and resets the listenerController,
|
||||
// releasing the memory used for the signal as well. No more trying to map all the
|
||||
// `addEventListener` to `removeEventListener` tediousness!
|
||||
this.listenerController.abort();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
export class ListenerController {
|
||||
listenerController?: AbortController;
|
||||
|
||||
get signal() {
|
||||
if (!this.listenerController) {
|
||||
this.listenerController = new AbortController();
|
||||
}
|
||||
return this.listenerController.signal;
|
||||
}
|
||||
|
||||
abort() {
|
||||
this.listenerController?.abort();
|
||||
this.listenerController = undefined;
|
||||
}
|
||||
}
|
@ -1,14 +1,18 @@
|
||||
///<reference types="@hcaptcha/types"/>
|
||||
import { renderStatic } from "@goauthentik/common/purify";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import { akEmptyState } from "@goauthentik/elements/EmptyState";
|
||||
import { bound } from "@goauthentik/elements/decorators/bound";
|
||||
import "@goauthentik/elements/forms/FormElement";
|
||||
import { ListenerController } from "@goauthentik/elements/utils/listenerController.js";
|
||||
import { randomId } from "@goauthentik/elements/utils/randomId";
|
||||
import "@goauthentik/flow/FormStatic";
|
||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||
import { P, match } from "ts-pattern";
|
||||
import type { TurnstileObject } from "turnstile-types";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, PropertyValues, TemplateResult, css, html } from "lit";
|
||||
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@ -23,8 +27,72 @@ import { CaptchaChallenge, CaptchaChallengeResponseRequest } from "@goauthentik/
|
||||
interface TurnstileWindow extends Window {
|
||||
turnstile: TurnstileObject;
|
||||
}
|
||||
|
||||
type TokenHandler = (token: string) => void;
|
||||
|
||||
type Dims = { height: number };
|
||||
|
||||
type IframeCaptchaMessage = {
|
||||
source?: string;
|
||||
context?: string;
|
||||
message: "captcha";
|
||||
token: string;
|
||||
};
|
||||
|
||||
type IframeResizeMessage = {
|
||||
source?: string;
|
||||
context?: string;
|
||||
message: "resize";
|
||||
size: Dims;
|
||||
};
|
||||
|
||||
type IframeMessageEvent = MessageEvent<IframeCaptchaMessage | IframeResizeMessage>;
|
||||
|
||||
type CaptchaHandler = {
|
||||
name: string;
|
||||
interactive: () => Promise<unknown>;
|
||||
execute: () => Promise<unknown>;
|
||||
};
|
||||
|
||||
// A container iframe for a hosted Captcha, with an event emitter to monitor when the Captcha forces
|
||||
// a resize. Because the Captcha is itself in an iframe, the reported height is often off by some
|
||||
// margin, so adding 2rem of height to our container adds padding and prevents scroll bars or hidden
|
||||
// rendering.
|
||||
|
||||
const iframeTemplate = (captchaElement: TemplateResult, challengeUrl: string) =>
|
||||
html`<!doctype html>
|
||||
<head>
|
||||
<html>
|
||||
<body style="display:flex;flex-direction:row;justify-content:center;">
|
||||
${captchaElement}
|
||||
<script>
|
||||
new ResizeObserver((entries) => {
|
||||
const height =
|
||||
document.body.offsetHeight +
|
||||
parseFloat(getComputedStyle(document.body).fontSize) * 2;
|
||||
window.parent.postMessage({
|
||||
message: "resize",
|
||||
source: "goauthentik.io",
|
||||
context: "flow-executor",
|
||||
size: { height },
|
||||
});
|
||||
}).observe(document.querySelector(".ak-captcha-container"));
|
||||
</script>
|
||||
<script src=${challengeUrl}></script>
|
||||
<script>
|
||||
function callback(token) {
|
||||
window.parent.postMessage({
|
||||
message: "captcha",
|
||||
source: "goauthentik.io",
|
||||
context: "flow-executor",
|
||||
token: token,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</head>`;
|
||||
|
||||
@customElement("ak-stage-captcha")
|
||||
export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
|
||||
static get styles(): CSSResult[] {
|
||||
@ -37,26 +105,12 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
css`
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 73px; /* tmp */
|
||||
height: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
handlers = [this.handleGReCaptcha, this.handleHCaptcha, this.handleTurnstile];
|
||||
|
||||
@state()
|
||||
error?: string;
|
||||
|
||||
@state()
|
||||
captchaFrame: HTMLIFrameElement;
|
||||
|
||||
@state()
|
||||
captchaDocumentContainer: HTMLDivElement;
|
||||
|
||||
@state()
|
||||
scriptElement?: HTMLScriptElement;
|
||||
|
||||
@property({ type: Boolean })
|
||||
embedded = false;
|
||||
|
||||
@ -65,209 +119,177 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
this.host.submit({ component: "ak-stage-captcha", token });
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.captchaFrame = document.createElement("iframe");
|
||||
this.captchaFrame.src = "about:blank";
|
||||
this.captchaFrame.id = `ak-captcha-${randomId()}`;
|
||||
@state()
|
||||
error?: string;
|
||||
|
||||
this.captchaDocumentContainer = document.createElement("div");
|
||||
this.captchaDocumentContainer.id = `ak-captcha-${randomId()}`;
|
||||
this.messageCallback = this.messageCallback.bind(this);
|
||||
}
|
||||
handlers: CaptchaHandler[] = [
|
||||
{
|
||||
name: "grecaptcha",
|
||||
interactive: this.renderGReCaptchaFrame,
|
||||
execute: this.executeGReCaptcha,
|
||||
},
|
||||
{
|
||||
name: "hcaptcha",
|
||||
interactive: this.renderHCaptchaFrame,
|
||||
execute: this.executeHCaptcha,
|
||||
},
|
||||
{
|
||||
name: "turnstile",
|
||||
interactive: this.renderTurnstileFrame,
|
||||
execute: this.executeTurnstile,
|
||||
},
|
||||
];
|
||||
|
||||
_captchaFrame?: HTMLIFrameElement;
|
||||
_captchaDocumentContainer?: HTMLDivElement;
|
||||
_listenController = new ListenerController();
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("message", this.messageCallback);
|
||||
window.addEventListener("message", this.onIframeMessage, {
|
||||
signal: this._listenController.signal,
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("message", this.messageCallback);
|
||||
if (!this.challenge.interactive) {
|
||||
document.body.removeChild(this.captchaDocumentContainer);
|
||||
this._listenController.abort();
|
||||
if (!this.challenge?.interactive) {
|
||||
if (document.body.contains(this.captchaDocumentContainer)) {
|
||||
document.body.removeChild(this.captchaDocumentContainer);
|
||||
}
|
||||
}
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
messageCallback(
|
||||
ev: MessageEvent<{
|
||||
source?: string;
|
||||
context?: string;
|
||||
message: string;
|
||||
token: string;
|
||||
}>,
|
||||
) {
|
||||
const msg = ev.data;
|
||||
if (msg.source !== "goauthentik.io" || msg.context !== "flow-executor") {
|
||||
return;
|
||||
get captchaDocumentContainer() {
|
||||
if (this._captchaDocumentContainer) {
|
||||
return this._captchaDocumentContainer;
|
||||
}
|
||||
if (msg.message !== "captcha") {
|
||||
return;
|
||||
this._captchaDocumentContainer = document.createElement("div");
|
||||
this._captchaDocumentContainer.id = `ak-captcha-${randomId()}`;
|
||||
return this._captchaDocumentContainer;
|
||||
}
|
||||
|
||||
get captchaFrame() {
|
||||
if (this._captchaFrame) {
|
||||
return this._captchaFrame;
|
||||
}
|
||||
this.onTokenChange(msg.token);
|
||||
this._captchaFrame = document.createElement("iframe");
|
||||
this._captchaFrame.src = "about:blank";
|
||||
this._captchaFrame.id = `ak-captcha-${randomId()}`;
|
||||
return this._captchaFrame;
|
||||
}
|
||||
|
||||
onFrameResize({ height }: Dims) {
|
||||
this.captchaFrame.style.height = `${height}px`;
|
||||
}
|
||||
|
||||
// ADR: Did not to put anything into `otherwise` or `exhaustive` here because iframe messages
|
||||
// that were not of interest to us also weren't necessarily corrupt or suspicious. For example,
|
||||
// during testing Storybook throws a lot of cross-iframe messages that we don't care about.
|
||||
|
||||
@bound
|
||||
onIframeMessage({ data }: IframeMessageEvent) {
|
||||
match(data)
|
||||
.with(
|
||||
{ source: "goauthentik.io", context: "flow-executor", message: "captcha" },
|
||||
({ token }) => this.onTokenChange(token),
|
||||
)
|
||||
.with(
|
||||
{ source: "goauthentik.io", context: "flow-executor", message: "resize" },
|
||||
({ size }) => this.onFrameResize(size),
|
||||
)
|
||||
.with(
|
||||
{ source: "goauthentik.io", context: "flow-executor", message: P.any },
|
||||
({ message }) => {
|
||||
console.debug(`authentik/stages/captcha: Unknown message: ${message}`);
|
||||
},
|
||||
)
|
||||
.otherwise(() => {});
|
||||
}
|
||||
|
||||
async renderGReCaptchaFrame() {
|
||||
this.renderFrame(
|
||||
html`<div
|
||||
class="g-recaptcha ak-captcha-container"
|
||||
data-sitekey="${this.challenge.siteKey}"
|
||||
data-callback="callback"
|
||||
></div>`,
|
||||
);
|
||||
}
|
||||
|
||||
async executeGReCaptcha() {
|
||||
return grecaptcha.ready(() => {
|
||||
grecaptcha.execute(
|
||||
grecaptcha.render(this.captchaDocumentContainer, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: this.onTokenChange,
|
||||
size: "invisible",
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async renderHCaptchaFrame() {
|
||||
this.renderFrame(
|
||||
html`<div
|
||||
class="h-captcha ak-captcha-container"
|
||||
data-sitekey="${this.challenge.siteKey}"
|
||||
data-theme="${this.activeTheme ? this.activeTheme : "light"}"
|
||||
data-callback="callback"
|
||||
></div> `,
|
||||
);
|
||||
}
|
||||
|
||||
async executeHCaptcha() {
|
||||
return hcaptcha.execute(
|
||||
hcaptcha.render(this.captchaDocumentContainer, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: this.onTokenChange,
|
||||
size: "invisible",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async renderTurnstileFrame() {
|
||||
this.renderFrame(
|
||||
html`<div
|
||||
class="cf-turnstile ak-captcha-container"
|
||||
data-sitekey="${this.challenge.siteKey}"
|
||||
data-callback="callback"
|
||||
></div>`,
|
||||
);
|
||||
}
|
||||
|
||||
async executeTurnstile() {
|
||||
return (window as unknown as TurnstileWindow).turnstile.render(
|
||||
this.captchaDocumentContainer,
|
||||
{
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: this.onTokenChange,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async renderFrame(captchaElement: TemplateResult) {
|
||||
this.captchaFrame.contentWindow?.document.open();
|
||||
this.captchaFrame.contentWindow?.document.write(
|
||||
await renderStatic(
|
||||
html`<!doctype html>
|
||||
<html>
|
||||
<body style="display:flex;flex-direction:row;justify-content:center;">
|
||||
${captchaElement}
|
||||
<script src=${this.challenge.jsUrl}></script>
|
||||
<script>
|
||||
function callback(token) {
|
||||
window.parent.postMessage({
|
||||
message: "captcha",
|
||||
source: "goauthentik.io",
|
||||
context: "flow-executor",
|
||||
token: token,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`,
|
||||
),
|
||||
await renderStatic(iframeTemplate(captchaElement, this.challenge.jsUrl)),
|
||||
);
|
||||
this.captchaFrame.contentWindow?.document.close();
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("challenge") && this.challenge !== undefined) {
|
||||
this.scriptElement = document.createElement("script");
|
||||
this.scriptElement.src = this.challenge.jsUrl;
|
||||
this.scriptElement.async = true;
|
||||
this.scriptElement.defer = true;
|
||||
this.scriptElement.dataset.akCaptchaScript = "true";
|
||||
this.scriptElement.onload = async () => {
|
||||
console.debug("authentik/stages/captcha: script loaded");
|
||||
let found = false;
|
||||
let lastError = undefined;
|
||||
this.handlers.forEach(async (handler) => {
|
||||
let handlerFound = false;
|
||||
try {
|
||||
console.debug(`authentik/stages/captcha[${handler.name}]: trying handler`);
|
||||
handlerFound = await handler.apply(this);
|
||||
if (handlerFound) {
|
||||
console.debug(
|
||||
`authentik/stages/captcha[${handler.name}]: handler succeeded`,
|
||||
);
|
||||
found = true;
|
||||
}
|
||||
} catch (exc) {
|
||||
console.debug(
|
||||
`authentik/stages/captcha[${handler.name}]: handler failed: ${exc}`,
|
||||
);
|
||||
if (handlerFound) {
|
||||
lastError = exc;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!found && lastError) {
|
||||
this.error = (lastError as Error).toString();
|
||||
}
|
||||
};
|
||||
document.head
|
||||
.querySelectorAll("[data-ak-captcha-script=true]")
|
||||
.forEach((el) => el.remove());
|
||||
document.head.appendChild(this.scriptElement);
|
||||
if (!this.challenge.interactive) {
|
||||
document.body.appendChild(this.captchaDocumentContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleGReCaptcha(): Promise<boolean> {
|
||||
if (!Object.hasOwn(window, "grecaptcha")) {
|
||||
return false;
|
||||
}
|
||||
if (this.challenge.interactive) {
|
||||
this.renderFrame(
|
||||
html`<div
|
||||
class="g-recaptcha"
|
||||
data-sitekey="${this.challenge.siteKey}"
|
||||
data-callback="callback"
|
||||
></div>`,
|
||||
);
|
||||
} else {
|
||||
grecaptcha.ready(() => {
|
||||
const captchaId = grecaptcha.render(this.captchaDocumentContainer, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: this.onTokenChange,
|
||||
size: "invisible",
|
||||
});
|
||||
grecaptcha.execute(captchaId);
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async handleHCaptcha(): Promise<boolean> {
|
||||
if (!Object.hasOwn(window, "hcaptcha")) {
|
||||
return false;
|
||||
}
|
||||
if (this.challenge.interactive) {
|
||||
this.renderFrame(
|
||||
html`<div
|
||||
class="h-captcha"
|
||||
data-sitekey="${this.challenge.siteKey}"
|
||||
data-theme="${this.activeTheme ? this.activeTheme : "light"}"
|
||||
data-callback="callback"
|
||||
></div> `,
|
||||
);
|
||||
} else {
|
||||
const captchaId = hcaptcha.render(this.captchaDocumentContainer, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: this.onTokenChange,
|
||||
size: "invisible",
|
||||
});
|
||||
hcaptcha.execute(captchaId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async handleTurnstile(): Promise<boolean> {
|
||||
if (!Object.hasOwn(window, "turnstile")) {
|
||||
return false;
|
||||
}
|
||||
if (this.challenge.interactive) {
|
||||
this.renderFrame(
|
||||
html`<div
|
||||
class="cf-turnstile"
|
||||
data-sitekey="${this.challenge.siteKey}"
|
||||
data-callback="callback"
|
||||
></div>`,
|
||||
);
|
||||
} else {
|
||||
(window as unknown as TurnstileWindow).turnstile.render(this.captchaDocumentContainer, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: this.onTokenChange,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
renderBody() {
|
||||
if (this.error) {
|
||||
return html`<ak-empty-state icon="fa-times" header=${this.error}> </ak-empty-state>`;
|
||||
}
|
||||
if (this.challenge.interactive) {
|
||||
return html`${this.captchaFrame}`;
|
||||
}
|
||||
return html`<ak-empty-state loading header=${msg("Verifying...")}></ak-empty-state>`;
|
||||
// [hasError, isInteractive]
|
||||
// prettier-ignore
|
||||
return match([Boolean(this.error), Boolean(this.challenge?.interactive)])
|
||||
.with([true, P.any], () => akEmptyState({ icon: "fa-times", header: this.error }))
|
||||
.with([false, true], () => html`${this.captchaFrame}`)
|
||||
.with([false, false], () => akEmptyState({ loading: true, header: msg("Verifying...") }))
|
||||
.exhaustive();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.embedded) {
|
||||
if (!this.challenge.interactive) {
|
||||
return html``;
|
||||
}
|
||||
return this.renderBody();
|
||||
}
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state loading> </ak-empty-state>`;
|
||||
}
|
||||
renderMain() {
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||
</header>
|
||||
@ -291,6 +313,63 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
<ul class="pf-c-login__main-footer-links"></ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
// [isEmbedded, hasChallenge, isInteractive]
|
||||
// prettier-ignore
|
||||
return match([this.embedded, Boolean(this.challenge), Boolean(this.challenge?.interactive)])
|
||||
.with([true, false, P.any], () => nothing)
|
||||
.with([true, true, false], () => nothing)
|
||||
.with([true, true, true], () => this.renderBody())
|
||||
.with([false, false, P.any], () => akEmptyState({ loading: true }))
|
||||
.with([false, true, P.any], () => this.renderMain())
|
||||
.exhaustive();
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
if (!(changedProperties.has("challenge") && this.challenge !== undefined)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attachCaptcha = async () => {
|
||||
console.debug("authentik/stages/captcha: script loaded");
|
||||
const handlers = this.handlers.filter(({ name }) => Object.hasOwn(window, name));
|
||||
let lastError = undefined;
|
||||
let found = false;
|
||||
for (const { name, interactive, execute } of handlers) {
|
||||
console.debug(`authentik/stages/captcha: trying handler ${name}`);
|
||||
try {
|
||||
const runner = this.challenge.interactive ? interactive : execute;
|
||||
await runner.apply(this);
|
||||
console.debug(`authentik/stages/captcha[${name}]: handler succeeded`);
|
||||
found = true;
|
||||
break;
|
||||
} catch (exc) {
|
||||
console.debug(`authentik/stages/captcha[${name}]: handler failed`);
|
||||
console.debug(exc);
|
||||
lastError = exc;
|
||||
}
|
||||
}
|
||||
this.error = found ? undefined : (lastError ?? "Unspecified error").toString();
|
||||
};
|
||||
|
||||
const scriptElement = document.createElement("script");
|
||||
scriptElement.src = this.challenge.jsUrl;
|
||||
scriptElement.async = true;
|
||||
scriptElement.defer = true;
|
||||
scriptElement.dataset.akCaptchaScript = "true";
|
||||
scriptElement.onload = attachCaptcha;
|
||||
|
||||
document.head
|
||||
.querySelectorAll("[data-ak-captcha-script=true]")
|
||||
.forEach((el) => el.remove());
|
||||
|
||||
document.head.appendChild(scriptElement);
|
||||
|
||||
if (!this.challenge.interactive) {
|
||||
document.body.appendChild(this.captchaDocumentContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
Reference in New Issue
Block a user