mirror of
https://github.com/goauthentik/authentik
synced 2026-05-05 06:32:15 +02:00
Compare commits
11 Commits
sdko/postg
...
web/style/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62a407ebbc | ||
|
|
e966159692 | ||
|
|
b5eab028ee | ||
|
|
25d7ba59fd | ||
|
|
f7096a2c84 | ||
|
|
4273b15320 | ||
|
|
27a550b18d | ||
|
|
a09cf62bac | ||
|
|
cf2ab7f701 | ||
|
|
cc4ce19ccc | ||
|
|
bd304e76c8 |
@@ -35,43 +35,14 @@
|
|||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script>
|
<script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script>
|
||||||
<style data-id="flow-css">
|
|
||||||
:root {
|
|
||||||
--ak-global--background-image: url("{{ flow_background_url }}");
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<ak-skip-to-content></ak-skip-to-content>
|
<ak-skip-to-content></ak-skip-to-content>
|
||||||
<ak-message-container></ak-message-container>
|
<ak-message-container></ak-message-container>
|
||||||
|
<ak-flow
|
||||||
<div class="pf-c-page__drawer">
|
slug="{{ flow.slug }}"
|
||||||
<div class="pf-c-drawer pf-m-collapsed" id="flow-drawer">
|
layout={{ flow.layout|default:'stacked' }}"
|
||||||
<div class="pf-c-drawer__main">
|
style='--ak-global--background-image: url("{{ flow_background_url }}")'
|
||||||
<div class="pf-c-drawer__content">
|
></ak-flow>
|
||||||
<div class="pf-c-drawer__body">
|
|
||||||
<ak-flow-executor
|
|
||||||
slug="{{ flow.slug }}"
|
|
||||||
class="pf-c-login"
|
|
||||||
data-layout="{{ flow.layout|default:'stacked' }}"
|
|
||||||
loading
|
|
||||||
>
|
|
||||||
{% include "base/placeholder.html" %}
|
|
||||||
|
|
||||||
<ak-brand-links name="flow-links" slot="footer"></ak-brand-links>
|
|
||||||
</ak-flow-executor>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ak-flow-inspector
|
|
||||||
id="flow-inspector"
|
|
||||||
data-registration="lazy"
|
|
||||||
class="pf-c-drawer__panel pf-m-width-33"
|
|
||||||
slug="{{ flow.slug }}"
|
|
||||||
></ak-flow-inspector>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
103
web/src/elements/directives/light.ts
Normal file
103
web/src/elements/directives/light.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { html, nothing, render, TemplateResult } from "lit";
|
||||||
|
import { AsyncDirective, DirectiveResult } from "lit/async-directive.js";
|
||||||
|
import { ChildPart, directive, PartInfo, PartType } from "lit/directive.js";
|
||||||
|
import { RootPart } from "lit/html.js";
|
||||||
|
|
||||||
|
export interface LightChildOptions {
|
||||||
|
// Optional alternative target for any `@event`-style handlers passed into the template. NOTE:
|
||||||
|
// this only works if the handlers do not already have a `this` bound to them, so only ordinary
|
||||||
|
// functions and methods will respond to this parameter; arrow function class fields and bound
|
||||||
|
// functions will use the `this` to which they were bound.
|
||||||
|
|
||||||
|
host?: Element;
|
||||||
|
slotName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class LightChildDirective extends AsyncDirective {
|
||||||
|
#slotName: string | null = null;
|
||||||
|
#slot: HTMLSlotElement | null = null;
|
||||||
|
#host: Element | null = null;
|
||||||
|
#rootPart: RootPart | null = null;
|
||||||
|
#sentinel: Comment | null = null;
|
||||||
|
|
||||||
|
constructor(partInfo: PartInfo) {
|
||||||
|
super(partInfo);
|
||||||
|
if (partInfo.type !== PartType.CHILD) {
|
||||||
|
throw new Error("The `light()` directive can only be use in child position");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(_template?: TemplateResult | DirectiveResult, options?: LightChildOptions) {
|
||||||
|
this.#slotName ??= options?.slotName ?? `lc-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
return html`<slot name="${this.#slotName}"></slot>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(
|
||||||
|
part: ChildPart,
|
||||||
|
[template, options = {}]: [TemplateResult | DirectiveResult, LightChildOptions],
|
||||||
|
) {
|
||||||
|
this.#slotName ??= options?.slotName ?? `lc-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
|
||||||
|
// This places a comment in the LightDom that belongs to this directive. Comments are not
|
||||||
|
// part of the DOM tree for the purposes of CSS, so it will be possible to style this child
|
||||||
|
// directly without a wrapper.
|
||||||
|
|
||||||
|
if (!this.#sentinel) {
|
||||||
|
const rootNode = part.parentNode.getRootNode();
|
||||||
|
this.#host ??= (part.options?.host ||
|
||||||
|
(rootNode instanceof ShadowRoot ? rootNode.host : null)) as Element | null;
|
||||||
|
|
||||||
|
if (!this.#host) {
|
||||||
|
throw new Error(
|
||||||
|
"light() must be used inside a shadow root or a valid options.host",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#sentinel = document.createComment("");
|
||||||
|
this.#host.appendChild(this.#sentinel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.#sentinel.parentNode) {
|
||||||
|
throw new Error("Could not assign sentinel to element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderOptions = Object.fromEntries(
|
||||||
|
Object.entries(options).filter(([key]) => ["host"].includes(key)),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.#rootPart = render(template, this.#sentinel.parentNode as HTMLElement, {
|
||||||
|
renderBefore: this.#sentinel,
|
||||||
|
...renderOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rendered = this.#sentinel.previousSibling;
|
||||||
|
if (rendered instanceof Element) {
|
||||||
|
rendered.slot = this.#slotName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (this.#slot ??= Object.assign(document.createElement("slot"), {
|
||||||
|
name: this.#slotName,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnected() {
|
||||||
|
if (this.#sentinel?.parentNode && this.#host?.isConnected) {
|
||||||
|
// The content being rendered this way, with the `render()` *function*, has its own Lit
|
||||||
|
// VDOM comment nodes in the HTML unrelated to the `host` context. Rendering `nothing`
|
||||||
|
// here ensures that any children of the lightDOM component receive clean-up signals and
|
||||||
|
// correctly disconnect (including listeners, etc.) from the current display as well.
|
||||||
|
// This is what lets us receive other DirectiveResults as template content.
|
||||||
|
|
||||||
|
render(nothing, this.#sentinel.parentNode as HTMLElement, {
|
||||||
|
renderBefore: this.#sentinel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.#rootPart?.setConnected(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnected() {
|
||||||
|
this.#rootPart?.setConnected(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const light = directive(LightChildDirective);
|
||||||
84
web/src/flow/Flow.ts
Normal file
84
web/src/flow/Flow.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import "#flow/FlowExecutor";
|
||||||
|
import "#flow/inspector/FlowInspector";
|
||||||
|
import "#flow/components/ak-brand-footer";
|
||||||
|
|
||||||
|
import { light } from "#elements/directives/light";
|
||||||
|
import { Interface } from "#elements/Interface";
|
||||||
|
|
||||||
|
import AKPlaceholder from "#styles/authentik/base/placeholder.css";
|
||||||
|
|
||||||
|
import { msg } from "@lit/localize";
|
||||||
|
import { html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
import { classMap } from "lit/directives/class-map.js";
|
||||||
|
|
||||||
|
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
|
||||||
|
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||||
|
import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.css";
|
||||||
|
|
||||||
|
@customElement("ak-flow")
|
||||||
|
export class Flow extends Interface {
|
||||||
|
static readonly styles = [PFDrawer, PFLogin, PFSpinner, AKPlaceholder];
|
||||||
|
|
||||||
|
@property()
|
||||||
|
public slug?: string;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
public layout?: string = "stacked";
|
||||||
|
|
||||||
|
protected renderPlacehonder() {
|
||||||
|
return html`<div class="ak-c-placeholder" id="ak-placeholder" slot="placeholder">
|
||||||
|
<span class="pf-c-spinner" role="progressbar" aria-valuetext=${msg("Loading...")}>
|
||||||
|
<span class="pf-c-spinner__clipper"></span>
|
||||||
|
<span class="pf-c-spinner__lead-ball"></span>
|
||||||
|
<span class="pf-c-spinner__tail-ball"></span>
|
||||||
|
</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { slug, layout } = this;
|
||||||
|
const footerClasses = classMap({
|
||||||
|
"pf-c-login_footer": true,
|
||||||
|
"pf-m-dark": this.layout === FlowLayoutEnum.Stacked,
|
||||||
|
});
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="pf-c-page__drawer">
|
||||||
|
<div class="pf-c-drawer pf-m-collapsed" id="flow-drawer">
|
||||||
|
<div class="pf-c-drawer__main">
|
||||||
|
<div class="pf-c-drawer__content">
|
||||||
|
<div class="pf-c-drawer__body">
|
||||||
|
${light(
|
||||||
|
html`<ak-flow-executor
|
||||||
|
slug="${slug}"
|
||||||
|
class="pf-c-login"
|
||||||
|
data-layout="${layout}"
|
||||||
|
loading
|
||||||
|
>
|
||||||
|
${this.renderPlaceholder()}
|
||||||
|
</ak-flow-executor>`
|
||||||
|
)}
|
||||||
|
<footer
|
||||||
|
aria-label=${msg("Site footer")}
|
||||||
|
name="site-footer"
|
||||||
|
part="footer"
|
||||||
|
class=${footerClasses}
|
||||||
|
>
|
||||||
|
<ak-brand-links name="flow-links"></ak-brand-links>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ak-flow-inspector
|
||||||
|
id="flow-inspector"
|
||||||
|
data-registration="lazy"
|
||||||
|
class="pf-c-drawer__panel pf-m-width-33"
|
||||||
|
slug=${slug}
|
||||||
|
></ak-flow-inspector>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,8 +15,8 @@ import { applyBackgroundImageProperty } from "#common/theme";
|
|||||||
import { AKSessionAuthenticatedEvent } from "#common/ws/events";
|
import { AKSessionAuthenticatedEvent } from "#common/ws/events";
|
||||||
import { WebsocketClient } from "#common/ws/WebSocketClient";
|
import { WebsocketClient } from "#common/ws/WebSocketClient";
|
||||||
|
|
||||||
|
import { AKElement } from "#elements/Base";
|
||||||
import { listen } from "#elements/decorators/listen";
|
import { listen } from "#elements/decorators/listen";
|
||||||
import { Interface } from "#elements/Interface";
|
|
||||||
import { showAPIErrorMessage } from "#elements/messages/MessageContainer";
|
import { showAPIErrorMessage } from "#elements/messages/MessageContainer";
|
||||||
import { WithBrandConfig } from "#elements/mixins/branding";
|
import { WithBrandConfig } from "#elements/mixins/branding";
|
||||||
import { LitPropertyRecord, SlottedTemplateResult } from "#elements/types";
|
import { LitPropertyRecord, SlottedTemplateResult } from "#elements/types";
|
||||||
@@ -78,7 +78,7 @@ import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
|||||||
* @part locale-select-select - The select element of the locale select component.
|
* @part locale-select-select - The select element of the locale select component.
|
||||||
*/
|
*/
|
||||||
@customElement("ak-flow-executor")
|
@customElement("ak-flow-executor")
|
||||||
export class FlowExecutor extends WithBrandConfig(Interface) implements StageHost {
|
export class FlowExecutor extends WithBrandConfig(AKElement) implements StageHost {
|
||||||
public static readonly DefaultLayout: FlowLayoutEnum =
|
public static readonly DefaultLayout: FlowLayoutEnum =
|
||||||
globalAK()?.flow?.layout || FlowLayoutEnum.Stacked;
|
globalAK()?.flow?.layout || FlowLayoutEnum.Stacked;
|
||||||
|
|
||||||
@@ -98,7 +98,11 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
|
|||||||
|
|
||||||
//#region Properties
|
//#region Properties
|
||||||
|
|
||||||
@property({ type: String, attribute: "slug", useDefault: true })
|
@property({
|
||||||
|
type: String,
|
||||||
|
attribute: "slug",
|
||||||
|
useDefault: true,
|
||||||
|
})
|
||||||
public flowSlug: string = window.location.pathname.split("/")[3];
|
public flowSlug: string = window.location.pathname.split("/")[3];
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@@ -107,7 +111,12 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
|
|||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
public loading = false;
|
public loading = false;
|
||||||
|
|
||||||
@property({ type: String, attribute: "data-layout", useDefault: true, reflect: true })
|
@property({
|
||||||
|
type: String,
|
||||||
|
attribute: "data-layout",
|
||||||
|
useDefault: true,
|
||||||
|
reflect: true,
|
||||||
|
})
|
||||||
public layout: FlowLayoutEnum = FlowExecutor.DefaultLayout;
|
public layout: FlowLayoutEnum = FlowExecutor.DefaultLayout;
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
@@ -195,7 +204,9 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
|
|||||||
? this.closest<HTMLDivElement>(".docs-story")
|
? this.closest<HTMLDivElement>(".docs-story")
|
||||||
: this.ownerDocument.body;
|
: this.ownerDocument.body;
|
||||||
|
|
||||||
applyBackgroundImageProperty(background, { target });
|
applyBackgroundImageProperty(background, {
|
||||||
|
target,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region Listeners
|
//#region Listeners
|
||||||
@@ -283,7 +294,7 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
|
|||||||
|
|
||||||
public submit = async (
|
public submit = async (
|
||||||
payload?: FlowChallengeResponseRequest,
|
payload?: FlowChallengeResponseRequest,
|
||||||
options?: SubmitOptions,
|
options?: SubmitOptions
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
if (!payload) throw new Error("No payload provided");
|
if (!payload) throw new Error("No payload provided");
|
||||||
if (!this.challenge) throw new Error("No challenge provided");
|
if (!this.challenge) throw new Error("No challenge provided");
|
||||||
@@ -336,7 +347,7 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.renderChallengeError(
|
return this.renderChallengeError(
|
||||||
`No stage found for component: ${challenge.component}`,
|
`No stage found for component: ${challenge.component}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,11 +375,16 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
|
|||||||
const props = spread(
|
const props = spread(
|
||||||
match(variant)
|
match(variant)
|
||||||
.with("challenge", () => challengeProps)
|
.with("challenge", () => challengeProps)
|
||||||
.with("standard", () => ({ ...challengeProps, ...litParts }))
|
.with("standard", () => ({
|
||||||
.exhaustive(),
|
...challengeProps,
|
||||||
|
...litParts,
|
||||||
|
}))
|
||||||
|
.exhaustive()
|
||||||
);
|
);
|
||||||
|
|
||||||
return staticHTML`<${unsafeStatic(tag)} ${props}></${unsafeStatic(tag)}>`;
|
return html`<div part="challange">
|
||||||
|
${light(staticHTML`<${unsafeStatic(tag)} ${props}></${unsafeStatic(tag)}>`)}
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected renderChallengeError(error: unknown): SlottedTemplateResult {
|
protected renderChallengeError(error: unknown): SlottedTemplateResult {
|
||||||
@@ -394,59 +410,10 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
|
|||||||
return html`<slot name="placeholder"></slot>`;
|
return html`<slot name="placeholder"></slot>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected renderFrameBackground(): SlottedTemplateResult {
|
|
||||||
return guard([this.layout, this.challenge], () => {
|
|
||||||
if (
|
|
||||||
this.layout !== FlowLayoutEnum.SidebarLeftFrameBackground &&
|
|
||||||
this.layout !== FlowLayoutEnum.SidebarRightFrameBackground
|
|
||||||
) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const src = this.challenge?.flowInfo?.background;
|
|
||||||
|
|
||||||
if (!src) return nothing;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="ak-c-login__content" part="content">
|
|
||||||
<iframe
|
|
||||||
class="ak-c-login__content-iframe"
|
|
||||||
part="content-iframe"
|
|
||||||
name="flow-content-frame"
|
|
||||||
src=${src}
|
|
||||||
></iframe>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected renderFooter(): SlottedTemplateResult {
|
|
||||||
return guard([this.layout], () => {
|
|
||||||
return html`<footer
|
|
||||||
aria-label=${msg("Site footer")}
|
|
||||||
name="site-footer"
|
|
||||||
part="footer"
|
|
||||||
class="pf-c-login__footer ${this.layout === FlowLayoutEnum.Stacked
|
|
||||||
? "pf-m-dark"
|
|
||||||
: ""}"
|
|
||||||
>
|
|
||||||
<slot name="footer"></slot>
|
|
||||||
</footer>`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override render(): SlottedTemplateResult {
|
protected override render(): SlottedTemplateResult {
|
||||||
const { challenge, loading } = this;
|
const { challenge, loading } = this;
|
||||||
|
|
||||||
return html`<ak-locale-select
|
return html`
|
||||||
part="locale-select"
|
|
||||||
exportparts="label:locale-select-label,select:locale-select-select"
|
|
||||||
class="pf-m-dark"
|
|
||||||
></ak-locale-select>
|
|
||||||
|
|
||||||
<header class="pf-c-login__header">
|
|
||||||
<ak-flow-inspector-button></ak-flow-inspector-button>
|
|
||||||
</header>
|
|
||||||
<main
|
<main
|
||||||
data-layout=${this.layout}
|
data-layout=${this.layout}
|
||||||
class="pf-c-login__main"
|
class="pf-c-login__main"
|
||||||
@@ -469,13 +436,14 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
|
|||||||
: this.renderLoading();
|
: this.renderLoading();
|
||||||
})}
|
})}
|
||||||
</main>
|
</main>
|
||||||
${this.renderFooter()}`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
g;
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"ak-flow-executor": FlowExecutor;
|
"ak-flow-executor": FlowExecutor;
|
||||||
}
|
}
|
||||||
|
|||||||
130
web/src/flow/controllers/FlowMessageController.ts
Normal file
130
web/src/flow/controllers/FlowMessageController.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import type { Interface } from "#elements/Interface";
|
||||||
|
|
||||||
|
type MessageHost = ReactiveControllerHost & Interface;
|
||||||
|
|
||||||
|
export class FlowMessageController implements ReactiveController {
|
||||||
|
// eslint-disable-next-line no-useless-constructor
|
||||||
|
constructor(private host: RememberMeHost) {
|
||||||
|
/* no op */
|
||||||
|
}
|
||||||
|
|
||||||
|
hostConnected() {
|
||||||
|
try {
|
||||||
|
const sessionId = localStorage.getItem("authentik-remember-me-session");
|
||||||
|
if (!!this.localSession && sessionId === this.localSession) {
|
||||||
|
this.username = undefined;
|
||||||
|
localStorage?.removeItem("authentik-remember-me-user");
|
||||||
|
}
|
||||||
|
localStorage?.setItem("authentik-remember-me-session", this.localSession);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (_e: any) {
|
||||||
|
this.username = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get localSession() {
|
||||||
|
return (getCookie("authentik_csrf") ?? "").substring(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
get usernameField() {
|
||||||
|
return this.host.renderRoot.querySelector(
|
||||||
|
'input[name="uidField"]'
|
||||||
|
) as HTMLInputElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get rememberMeToggle() {
|
||||||
|
return this.host.renderRoot.querySelector(
|
||||||
|
"#authentik-remember-me"
|
||||||
|
) as HTMLInputElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isValidChallenge() {
|
||||||
|
return !(
|
||||||
|
this.host.challenge?.responseErrors &&
|
||||||
|
this.host.challenge.responseErrors.non_field_errors &&
|
||||||
|
this.host.challenge.responseErrors.non_field_errors.find(
|
||||||
|
(cre) => cre.code === "invalid"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get submitButton() {
|
||||||
|
return this.host.renderRoot.querySelector('button[type="submit"]') as HTMLButtonElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isEnabled() {
|
||||||
|
return this.host.challenge?.enableRememberMe && typeof localStorage !== "undefined";
|
||||||
|
}
|
||||||
|
|
||||||
|
get canAutoSubmit() {
|
||||||
|
return (
|
||||||
|
!!this.host.challenge &&
|
||||||
|
!!this.username &&
|
||||||
|
!!this.usernameField?.value &&
|
||||||
|
!this.host.challenge.passwordFields &&
|
||||||
|
!this.host.challenge.passwordlessUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before the page is updated, try to extract the username from localstorage.
|
||||||
|
hostUpdate() {
|
||||||
|
if (!this.isEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.username = localStorage.getItem("authentik-remember-me-user") || undefined;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (_e: any) {
|
||||||
|
this.username = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After the page is updated, if everything is ready to go, do the autosubmit.
|
||||||
|
hostUpdated() {
|
||||||
|
if (this.isEnabled && this.canAutoSubmit) {
|
||||||
|
this.submitButton?.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trackRememberMe() {
|
||||||
|
if (!this.usernameField || this.usernameField.value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.username = this.usernameField.value;
|
||||||
|
localStorage?.setItem("authentik-remember-me-user", this.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When active, save current details and record every keystroke to the username.
|
||||||
|
// When inactive, clear all fields and remove keystroke recorder.
|
||||||
|
toggleRememberMe() {
|
||||||
|
if (!this.rememberMeToggle || !this.rememberMeToggle.checked) {
|
||||||
|
localStorage?.removeItem("authentik-remember-me-user");
|
||||||
|
localStorage?.removeItem("authentik-remember-me-session");
|
||||||
|
this.username = undefined;
|
||||||
|
this.usernameField?.removeEventListener("keyup", this.trackRememberMe);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.usernameField) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localStorage?.setItem("authentik-remember-me-user", this.usernameField.value);
|
||||||
|
localStorage?.setItem("authentik-remember-me-session", this.localSession);
|
||||||
|
this.usernameField.addEventListener("keyup", this.trackRememberMe);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return this.isEnabled
|
||||||
|
? html` <label class="pf-c-switch remember-me-switch">
|
||||||
|
<input
|
||||||
|
class="pf-c-switch__input"
|
||||||
|
id="authentik-remember-me"
|
||||||
|
@click=${this.toggleRememberMe}
|
||||||
|
type="checkbox"
|
||||||
|
?checked=${!!this.username}
|
||||||
|
/>
|
||||||
|
<span class="pf-c-form__label">${msg("Remember me on this device")}</span>
|
||||||
|
</label>`
|
||||||
|
: nothing;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import "#elements/messages/MessageContainer";
|
import "#elements/messages/MessageContainer";
|
||||||
import "#flow/FlowExecutor";
|
import "#flow/Flow";
|
||||||
// Statically import some stages to speed up load speed
|
// Statically import some stages to speed up load speed
|
||||||
import "#flow/stages/access_denied/AccessDeniedStage";
|
import "#flow/stages/access_denied/AccessDeniedStage";
|
||||||
// Import webauthn-related stages to prevent issues on safari
|
// Import webauthn-related stages to prevent issues on safari
|
||||||
@@ -15,5 +15,7 @@ import "#flow/stages/password/PasswordStage";
|
|||||||
// end of stage import
|
// end of stage import
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
await import("@goauthentik/esbuild-plugin-live-reload/client");
|
await import(
|
||||||
|
"@goauthentik/esbuild-plugin-live-reload/client"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user