mirror of
https://github.com/goauthentik/authentik
synced 2026-05-05 22:52:42 +02:00
Compare commits
17 Commits
a11y-modal
...
2025-12-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a64762dd6 | ||
|
|
61f42de07e | ||
|
|
e759d0d7d9 | ||
|
|
ade566126b | ||
|
|
024def44e5 | ||
|
|
429da7242f | ||
|
|
a7ebd6dc1d | ||
|
|
c6d94e9a7e | ||
|
|
400c0f56e8 | ||
|
|
617eaf1175 | ||
|
|
b6c559c05a | ||
|
|
efca157d80 | ||
|
|
464b4d2bb2 | ||
|
|
bbdf9df3fd | ||
|
|
fb2355b4db | ||
|
|
0bdd97530c | ||
|
|
9966cfedf3 |
@@ -6,7 +6,7 @@ import { globalAK } from "#common/global";
|
||||
import { ModalButton } from "#elements/buttons/ModalButton";
|
||||
import { WithBrandConfig } from "#elements/mixins/branding";
|
||||
import { WithLicenseSummary } from "#elements/mixins/license";
|
||||
import { renderImage } from "#elements/utils/images";
|
||||
import { ThemedImage } from "#elements/utils/images";
|
||||
|
||||
import { AdminApi, CapabilitiesEnum, LicenseSummaryStatusEnum } from "@goauthentik/api";
|
||||
|
||||
@@ -95,11 +95,12 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
|
||||
aria-labelledby="modal-title"
|
||||
>
|
||||
<div class="pf-c-about-modal-box__brand">
|
||||
${renderImage(
|
||||
this.brandingFavicon,
|
||||
msg("authentik Logo"),
|
||||
"pf-c-about-modal-box__brand-image",
|
||||
)}
|
||||
${ThemedImage({
|
||||
src: this.brandingFavicon,
|
||||
alt: msg("authentik Logo"),
|
||||
className: "pf-c-about-modal-box__brand-image",
|
||||
theme: this.activeTheme,
|
||||
})}
|
||||
</div>
|
||||
<div class="pf-c-about-modal-box__close">
|
||||
<button class="pf-c-button pf-m-plain" type="button" @click=${this.close}>
|
||||
|
||||
@@ -3,5 +3,5 @@ import type { TypeCreate } from "@goauthentik/api";
|
||||
import { createContext } from "@lit/context";
|
||||
|
||||
export const applicationWizardProvidersContext = createContext<TypeCreate[]>(
|
||||
Symbol.for("ak-application-wizard-providers-context"),
|
||||
Symbol("ak-application-wizard-providers-context"),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AKRequestPostEvent, APIRequestInfo } from "#common/api/events";
|
||||
import { autoDetectLanguage } from "#common/ui/locale/utils";
|
||||
import { formatAcceptLanguageHeader } from "#common/ui/locale/utils";
|
||||
import { getCookie } from "#common/utils";
|
||||
|
||||
import { ConsoleLogger, Logger } from "#logger/browser";
|
||||
@@ -74,11 +74,11 @@ export class LocaleMiddleware implements Middleware, Disposable {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#locale = event.detail.readyLocale;
|
||||
this.#locale = formatAcceptLanguageHeader(event.detail.readyLocale);
|
||||
};
|
||||
|
||||
constructor(localeHint?: string) {
|
||||
this.#locale = autoDetectLanguage(localeHint);
|
||||
constructor(languageTagHint: Intl.UnicodeBCP47LocaleIdentifier) {
|
||||
this.#locale = formatAcceptLanguageHeader(languageTagHint);
|
||||
|
||||
window.addEventListener(LOCALE_STATUS_EVENT, this.#localeStatusListener);
|
||||
}
|
||||
|
||||
@@ -338,7 +338,7 @@ export function applyBackgroundImageProperty(
|
||||
/**
|
||||
* Returns the root interface element of the page.
|
||||
*
|
||||
* @todo Can this be handled with a Lit Mixin?
|
||||
* @deprecated Use context controllers to access the interface root instead.
|
||||
*/
|
||||
export function rootInterface<T extends HTMLElement = HTMLElement>(): T {
|
||||
const element = document.body.querySelector<T>("[data-test-id=interface-root]");
|
||||
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
import { safeParseLocale } from "#common/ui/locale/utils";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { repeat } from "lit/directives/repeat.js";
|
||||
|
||||
/**
|
||||
* Safely get a minimized locale ID, with fallback for older browsers.
|
||||
@@ -198,34 +196,41 @@ export function formatLocaleDisplayNames(
|
||||
return entries.sort(createIntlCollator(activeLocaleTag, collatorOptions));
|
||||
}
|
||||
|
||||
export function renderLocaleDisplayNames(
|
||||
entries: LocaleDisplay[],
|
||||
activeLocaleTag: TargetLanguageTag | null,
|
||||
export function formatRelativeLocaleDisplayName(
|
||||
languageTag: TargetLanguageTag,
|
||||
localizedDisplayName: string,
|
||||
relativeDisplayName: string,
|
||||
) {
|
||||
return repeat(
|
||||
entries,
|
||||
([languageTag]) => languageTag,
|
||||
([languageTag, localizedDisplayName, relativeDisplayName]) => {
|
||||
const pseudo = languageTag === PseudoLanguageTag;
|
||||
const pseudo = languageTag === PseudoLanguageTag;
|
||||
|
||||
const same =
|
||||
relativeDisplayName &&
|
||||
normalizeDisplayName(relativeDisplayName) ===
|
||||
normalizeDisplayName(localizedDisplayName);
|
||||
const same =
|
||||
relativeDisplayName &&
|
||||
normalizeDisplayName(relativeDisplayName) === normalizeDisplayName(localizedDisplayName);
|
||||
|
||||
let localizedMessage = localizedDisplayName;
|
||||
if (same || pseudo) {
|
||||
return localizedDisplayName;
|
||||
}
|
||||
|
||||
if (!same && !pseudo) {
|
||||
localizedMessage = msg(str`${relativeDisplayName} (${localizedDisplayName})`, {
|
||||
id: "locale-option-localized-label",
|
||||
desc: "Locale option label showing the localized language name along with the native language name in parentheses.",
|
||||
});
|
||||
}
|
||||
|
||||
return html`${pseudo ? html`<hr />` : null}
|
||||
<option value=${languageTag} ?selected=${languageTag === activeLocaleTag}>
|
||||
${localizedMessage}
|
||||
</option>`;
|
||||
},
|
||||
);
|
||||
return msg(str`${relativeDisplayName} (${localizedDisplayName})`, {
|
||||
id: "locale-option-localized-label",
|
||||
desc: "Locale option label showing the localized language name along with the native language name in parentheses.",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the display name for the auto-detect locale option.
|
||||
*
|
||||
* @param detectedLocale The detected locale display, if any.
|
||||
*/
|
||||
export function formatAutoDetectLocaleDisplayName(detectedLocale?: LocaleDisplay | null) {
|
||||
const prefix = msg("Auto-detect", {
|
||||
id: "locale-auto-detect-option",
|
||||
desc: "Label for the auto-detect locale option in language selection dropdown",
|
||||
});
|
||||
|
||||
if (!detectedLocale) {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
return `${prefix} (${formatRelativeLocaleDisplayName(...detectedLocale)})`;
|
||||
}
|
||||
|
||||
@@ -157,8 +157,8 @@ export function getSessionLocale(): string | null {
|
||||
/**
|
||||
* Auto-detect the best locale to use from several sources.
|
||||
*
|
||||
* @param localeHint An optional locale code hint.
|
||||
* @param fallbackLocaleCode An optional fallback locale code.
|
||||
* @param languageTagHint An optional locale code hint.
|
||||
* @param fallbackLanguageTag An optional fallback locale code.
|
||||
* @returns The best-matching supported locale code.
|
||||
*
|
||||
* @remarks
|
||||
@@ -172,8 +172,8 @@ export function getSessionLocale(): string | null {
|
||||
* 6. The source locale (English)
|
||||
*/
|
||||
export function autoDetectLanguage(
|
||||
localeHint?: string,
|
||||
fallbackLocaleCode?: string,
|
||||
languageTagHint?: Intl.UnicodeBCP47LocaleIdentifier,
|
||||
fallbackLanguageTag?: Intl.UnicodeBCP47LocaleIdentifier,
|
||||
): TargetLanguageTag {
|
||||
let localeParam: string | null = null;
|
||||
|
||||
@@ -188,9 +188,9 @@ export function autoDetectLanguage(
|
||||
const candidates = [
|
||||
localeParam,
|
||||
sessionLocale,
|
||||
localeHint,
|
||||
self.navigator?.language,
|
||||
fallbackLocaleCode,
|
||||
languageTagHint,
|
||||
...(self.navigator?.languages || []),
|
||||
fallbackLanguageTag,
|
||||
].filter((item): item is string => !!item);
|
||||
|
||||
const firstSupportedLocale = findSupportedLocale(candidates);
|
||||
@@ -198,8 +198,8 @@ export function autoDetectLanguage(
|
||||
if (!firstSupportedLocale) {
|
||||
console.debug(`authentik/locale: Falling back to source locale`, {
|
||||
SourceLanguageTag,
|
||||
localeHint,
|
||||
fallbackLocaleCode,
|
||||
languageTagHint,
|
||||
fallbackLanguageTag,
|
||||
candidates,
|
||||
});
|
||||
|
||||
@@ -208,3 +208,28 @@ export function autoDetectLanguage(
|
||||
|
||||
return firstSupportedLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a locale code, format it for use in an `Accept-Language` header.
|
||||
*/
|
||||
export function formatAcceptLanguageHeader(languageTag: Intl.UnicodeBCP47LocaleIdentifier): string {
|
||||
const [preferredLanguageTag, ...languageTags] = new Set([
|
||||
languageTag,
|
||||
...(self.navigator?.languages || []),
|
||||
SourceLanguageTag,
|
||||
"*",
|
||||
]);
|
||||
|
||||
const fallbackCount = languageTags.length;
|
||||
|
||||
return [
|
||||
preferredLanguageTag,
|
||||
...languageTags.map((tag, idx) => {
|
||||
const weight = ((fallbackCount - idx) / (fallbackCount + 1)).toFixed(
|
||||
fallbackCount > 9 ? 2 : 1,
|
||||
);
|
||||
|
||||
return `${tag};q=${weight}`;
|
||||
}),
|
||||
].join(", ");
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AKElement } from "#elements/Base";
|
||||
import { WithBrandConfig } from "#elements/mixins/branding";
|
||||
import { WithSession } from "#elements/mixins/session";
|
||||
import { isAdminRoute } from "#elements/router/utils";
|
||||
import { renderImage } from "#elements/utils/images";
|
||||
import { ThemedImage } from "#elements/utils/images";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
|
||||
@@ -377,7 +377,11 @@ export class AKPageNavbar
|
||||
<aside role="presentation" class="brand ${this.open ? "" : "pf-m-collapsed"}">
|
||||
<a aria-label="${msg("Home")}" href="#/">
|
||||
<div class="logo">
|
||||
${renderImage(this.brandingLogo, msg("authentik Logo"), "")}
|
||||
${ThemedImage({
|
||||
src: this.brandingLogo,
|
||||
alt: msg("authentik Logo"),
|
||||
theme: this.activeTheme,
|
||||
})}
|
||||
</div>
|
||||
</a>
|
||||
</aside>
|
||||
|
||||
@@ -3,5 +3,5 @@ import type { WizardStepState } from "./types.js";
|
||||
import { createContext } from "@lit/context";
|
||||
|
||||
export const wizardStepContext = createContext<WizardStepState>(
|
||||
Symbol.for("authentik-wizard-step-labels"),
|
||||
Symbol("authentik-wizard-step-labels"),
|
||||
);
|
||||
|
||||
@@ -3,16 +3,18 @@ import { NotificationsContextController } from "#elements/controllers/Notificati
|
||||
import { SessionContextController } from "#elements/controllers/SessionContextController";
|
||||
import { VersionContextController } from "#elements/controllers/VersionContextController";
|
||||
import { Interface } from "#elements/Interface";
|
||||
import { LicenseContext } from "#elements/mixins/license";
|
||||
import { NotificationsContext } from "#elements/mixins/notifications";
|
||||
import { SessionContext } from "#elements/mixins/session";
|
||||
import { VersionContext } from "#elements/mixins/version";
|
||||
|
||||
export class AuthenticatedInterface extends Interface {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.addController(new LicenseContextController(this));
|
||||
this.addController(new LicenseContextController(this), LicenseContext);
|
||||
this.addController(new SessionContextController(this), SessionContext);
|
||||
this.addController(new VersionContextController(this));
|
||||
this.addController(new VersionContextController(this), VersionContext);
|
||||
this.addController(new NotificationsContextController(this), NotificationsContext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import { ConfigContextController } from "#elements/controllers/ConfigContextCont
|
||||
import { ContextControllerRegistry } from "#elements/controllers/ContextControllerRegistry";
|
||||
import { LocaleContextController } from "#elements/controllers/LocaleContextController";
|
||||
import { ModalOrchestrationController } from "#elements/controllers/ModalOrchestrationController";
|
||||
import { ReactiveContextController } from "#elements/types";
|
||||
import { ReactiveContextController } from "#elements/controllers/ReactiveContextController";
|
||||
import { BrandingContext } from "#elements/mixins/branding";
|
||||
import { AuthentikConfigContext } from "#elements/mixins/config";
|
||||
|
||||
import { Context, ContextType } from "@lit/context";
|
||||
import { ReactiveController } from "lit";
|
||||
@@ -16,6 +18,14 @@ import { ReactiveController } from "lit";
|
||||
* The base interface element for the application.
|
||||
*/
|
||||
export abstract class Interface extends AKElement {
|
||||
/**
|
||||
* Private map of controllers to their registry keys.
|
||||
*
|
||||
* This is used to track which controllers have been registered,
|
||||
* and to unregister them when removed.
|
||||
*/
|
||||
#registryKeys = new WeakMap<ReactiveController, ContextType<Context<unknown, unknown>>>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@@ -24,24 +34,44 @@ export abstract class Interface extends AKElement {
|
||||
createUIThemeEffect(applyDocumentTheme);
|
||||
|
||||
this.addController(new LocaleContextController(this, locale));
|
||||
this.addController(new ConfigContextController(this, config));
|
||||
this.addController(new BrandingContextController(this, brand));
|
||||
this.addController(new ConfigContextController(this, config), AuthentikConfigContext);
|
||||
this.addController(new BrandingContextController(this, brand), BrandingContext);
|
||||
this.addController(new ModalOrchestrationController());
|
||||
|
||||
this.dataset.testId = "interface-root";
|
||||
}
|
||||
|
||||
public override addController(
|
||||
controller: ReactiveController,
|
||||
registryKey?: ContextType<Context<unknown, unknown>>,
|
||||
): void {
|
||||
if (controller instanceof ReactiveContextController) {
|
||||
if (!registryKey) {
|
||||
throw new TypeError(
|
||||
`ReactiveContextController (${controller.constructor.name}) requires a registry key.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.#registryKeys.has(controller)) {
|
||||
throw new Error(
|
||||
`Controller (${controller.constructor.name}) is already registered.`,
|
||||
);
|
||||
}
|
||||
|
||||
this.#registryKeys.set(controller, registryKey);
|
||||
ContextControllerRegistry.set(registryKey, controller);
|
||||
}
|
||||
super.addController(controller);
|
||||
}
|
||||
|
||||
public override removeController(controller: ReactiveController): void {
|
||||
super.removeController(controller);
|
||||
|
||||
const registryKey = this.#registryKeys.get(controller);
|
||||
|
||||
if (registryKey) {
|
||||
ContextControllerRegistry.set(registryKey, controller as ReactiveContextController);
|
||||
ContextControllerRegistry.delete(registryKey);
|
||||
this.#registryKeys.delete(controller);
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.dataset.testId = "interface-root";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,34 @@
|
||||
import { type ContextControllerRegistryMap } from "#elements/types";
|
||||
|
||||
/**
|
||||
* Check if the environment supports Symbol-keyed WeakMaps.
|
||||
*
|
||||
* @see {@link https://caniuse.com/mdn-javascript_builtins_weakmap_symbol_as_keys | Can I use}
|
||||
*
|
||||
* @todo Re-evaluate browser coverage after 2027-01-01
|
||||
*/
|
||||
function supportsSymbolKeyedWeakMap(): boolean {
|
||||
const testKey = Symbol("test");
|
||||
const wm = new WeakMap();
|
||||
|
||||
try {
|
||||
wm.set(testKey, "value");
|
||||
return wm.has(testKey);
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A constructor for either WeakMap or Map, depending on environment support.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* A preference for `WeakMap` is optional at the moment.
|
||||
* However, if we ever support short-lived context controllers, such as
|
||||
*/
|
||||
const ContextControllerConstructor = supportsSymbolKeyedWeakMap() ? WeakMap : Map;
|
||||
|
||||
/**
|
||||
* A registry of context controllers added to the Interface.
|
||||
*
|
||||
@@ -9,4 +38,5 @@ import { type ContextControllerRegistryMap } from "#elements/types";
|
||||
*
|
||||
* This is exported separately to avoid circular dependencies.
|
||||
*/
|
||||
export const ContextControllerRegistry = new WeakMap() as ContextControllerRegistryMap;
|
||||
export const ContextControllerRegistry =
|
||||
new ContextControllerConstructor() as ContextControllerRegistryMap;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { TargetLanguageTag } from "#common/ui/locale/definitions";
|
||||
import { formatLocaleDisplayNames, renderLocaleDisplayNames } from "#common/ui/locale/format";
|
||||
import { formatLocaleDisplayNames } from "#common/ui/locale/format";
|
||||
import { setSessionLocale } from "#common/ui/locale/utils";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import Styles from "#elements/locale/ak-locale-select.css";
|
||||
import { LocaleOptions } from "#elements/locale/utils";
|
||||
import { WithCapabilitiesConfig } from "#elements/mixins/capabilities";
|
||||
import { WithLocale } from "#elements/mixins/locale";
|
||||
|
||||
@@ -159,7 +160,7 @@ export class AKLocaleSelect extends WithLocale(WithCapabilitiesConfig(AKElement)
|
||||
class="pf-c-form-control ak-m-capitalize"
|
||||
name="locale"
|
||||
>
|
||||
${renderLocaleDisplayNames(entries, activeLocaleTag)}
|
||||
${LocaleOptions({ entries, activeLocaleTag })}
|
||||
</select>`;
|
||||
});
|
||||
}
|
||||
|
||||
36
web/src/elements/locale/utils.ts
Normal file
36
web/src/elements/locale/utils.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { PseudoLanguageTag, TargetLanguageTag } from "#common/ui/locale/definitions";
|
||||
import { formatRelativeLocaleDisplayName, LocaleDisplay } from "#common/ui/locale/format";
|
||||
|
||||
import type { LitFC, SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { html } from "lit";
|
||||
import { repeat } from "lit/directives/repeat.js";
|
||||
|
||||
export interface LocaleOptionsProps {
|
||||
entries: Iterable<LocaleDisplay>;
|
||||
activeLocaleTag: TargetLanguageTag | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render locale display name options for a select element.
|
||||
*/
|
||||
export const LocaleOptions: LitFC<LocaleOptionsProps> = ({ entries, activeLocaleTag }) => {
|
||||
return repeat(
|
||||
entries,
|
||||
([languageTag]) => languageTag,
|
||||
([languageTag, localizedDisplayName, relativeDisplayName]) => {
|
||||
const pseudo = languageTag === PseudoLanguageTag;
|
||||
|
||||
const localizedMessage = formatRelativeLocaleDisplayName(
|
||||
languageTag,
|
||||
localizedDisplayName,
|
||||
relativeDisplayName,
|
||||
);
|
||||
|
||||
return html`${pseudo ? html`<hr />` : null}
|
||||
<option value=${languageTag} ?selected=${languageTag === activeLocaleTag}>
|
||||
${localizedMessage}
|
||||
</option>`;
|
||||
},
|
||||
) as SlottedTemplateResult;
|
||||
};
|
||||
@@ -13,9 +13,7 @@ import { consume, Context, createContext } from "@lit/context";
|
||||
* @see {@linkcode BrandingMixin}
|
||||
* @see {@linkcode WithBrandConfig}
|
||||
*/
|
||||
export const BrandingContext = createContext<CurrentBrand>(
|
||||
Symbol.for("authentik-branding-context"),
|
||||
);
|
||||
export const BrandingContext = createContext<CurrentBrand>(Symbol("authentik-branding-context"));
|
||||
|
||||
export type BrandingContext = Context<symbol, CurrentBrand>;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export const kAKConfig = Symbol("kAKConfig");
|
||||
* @see {@linkcode AKConfigMixin}
|
||||
* @see {@linkcode WithAuthentikConfig}
|
||||
*/
|
||||
export const AuthentikConfigContext = createContext<Config>(Symbol.for("authentik-config-context"));
|
||||
export const AuthentikConfigContext = createContext<Config>(Symbol("authentik-config-context"));
|
||||
|
||||
export type AuthentikConfigContext = Context<symbol, Config>;
|
||||
|
||||
|
||||
@@ -4,9 +4,7 @@ import { type LicenseSummary, LicenseSummaryStatusEnum } from "@goauthentik/api"
|
||||
|
||||
import { consume, Context, createContext } from "@lit/context";
|
||||
|
||||
export const LicenseContext = createContext<LicenseSummary>(
|
||||
Symbol.for("authentik-license-context"),
|
||||
);
|
||||
export const LicenseContext = createContext<LicenseSummary>(Symbol("authentik-license-context"));
|
||||
|
||||
export type LicenseContext = Context<symbol, LicenseSummary>;
|
||||
|
||||
|
||||
@@ -16,9 +16,7 @@ export const kAKLocale = Symbol("kAKLocale");
|
||||
* @see {@linkcode LocaleMixin}
|
||||
* @see {@linkcode WithLocale}
|
||||
*/
|
||||
export const LocaleContext = createContext<LocaleContextValue>(
|
||||
Symbol.for("authentik-locale-context"),
|
||||
);
|
||||
export const LocaleContext = createContext<LocaleContextValue>(Symbol("authentik-locale-context"));
|
||||
|
||||
export type LocaleContext = typeof LocaleContext;
|
||||
|
||||
|
||||
@@ -13,9 +13,7 @@ import { property } from "lit/decorators.js";
|
||||
* @see {@linkcode WithVersion}
|
||||
*/
|
||||
|
||||
export const VersionContext = createContext<Version | null>(
|
||||
Symbol.for("authentik-version-context"),
|
||||
);
|
||||
export const VersionContext = createContext<Version | null>(Symbol("authentik-version-context"));
|
||||
|
||||
export type VersionContext = typeof VersionContext;
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ export interface ContextControllerRegistryMap {
|
||||
key: ContextType<T>,
|
||||
controller: ReactiveContextController<T, object>,
|
||||
): void;
|
||||
delete<T extends Context<unknown, unknown>>(key: ContextType<T>): void;
|
||||
}
|
||||
|
||||
export interface ReactiveControllerHostRegistry extends ReactiveControllerHost {
|
||||
|
||||
@@ -1,58 +1,48 @@
|
||||
import { resolveUITheme, rootInterface } from "#common/theme";
|
||||
import { ResolvedUITheme } from "#common/theme";
|
||||
|
||||
import type { AKElement } from "#elements/Base";
|
||||
import type { SlottedTemplateResult } from "#elements/types";
|
||||
import type { LitFC } from "#elements/types";
|
||||
import { ifPresent } from "#elements/utils/attributes";
|
||||
|
||||
import { spread } from "@open-wc/lit-helpers";
|
||||
import { ImgHTMLAttributes } from "react";
|
||||
|
||||
import { html, nothing } from "lit";
|
||||
|
||||
export const FontAwesomeProtocol = "fa://";
|
||||
|
||||
export function themeImage(rawPath: string) {
|
||||
const enabledTheme = rootInterface<AKElement>()?.activeTheme || resolveUITheme();
|
||||
|
||||
return rawPath.replaceAll("%(theme)s", enabledTheme);
|
||||
export function themeImage(rawPath: string, theme: ResolvedUITheme) {
|
||||
return rawPath.replaceAll("%(theme)s", theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an image that can be a regular URL, Font Awesome icon (fa://), or themed image
|
||||
*
|
||||
* @param imagePath - URL, fa:// icon, or path with %(theme)s placeholder
|
||||
* @param alt - Alt text for the image
|
||||
* @param className - CSS classes to apply
|
||||
* @returns TemplateResult with either <img> or <i> element
|
||||
*/
|
||||
export function renderImage(
|
||||
imagePath: string,
|
||||
alt?: string,
|
||||
className?: string,
|
||||
): SlottedTemplateResult {
|
||||
if (!imagePath) {
|
||||
export interface ThemedImageProps extends ImgHTMLAttributes<HTMLImageElement> {
|
||||
/**
|
||||
* The image path, which can be:
|
||||
* - A regular URL
|
||||
* - A Font Awesome icon (fa://icon-name)
|
||||
* - A themed image path with %(theme)s placeholder
|
||||
*/
|
||||
src: string;
|
||||
theme: ResolvedUITheme;
|
||||
}
|
||||
|
||||
export const ThemedImage: LitFC<ThemedImageProps> = ({ src, className, theme, ...props }) => {
|
||||
if (!src) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
// Handle Font Awesome icons (same logic as ak-app-icon)
|
||||
if (imagePath.startsWith(FontAwesomeProtocol)) {
|
||||
const classes = [
|
||||
className,
|
||||
"font-awesome",
|
||||
"fas",
|
||||
imagePath.slice(FontAwesomeProtocol.length),
|
||||
]
|
||||
if (src.startsWith(FontAwesomeProtocol)) {
|
||||
const classes = [className, "font-awesome", "fas", src.slice(FontAwesomeProtocol.length)]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
return html`<i
|
||||
part="icon font-awesome"
|
||||
role="img"
|
||||
aria-label=${ifPresent(alt)}
|
||||
class=${classes}
|
||||
></i>`;
|
||||
|
||||
return html`<i part="icon font-awesome" role="img" class=${classes} ${spread(props)}></i>`;
|
||||
}
|
||||
|
||||
const src = themeImage(imagePath);
|
||||
const themedSrc = themeImage(src, theme);
|
||||
|
||||
return html`<img src=${src} alt=${ifPresent(alt)} class=${ifPresent(className)} />`;
|
||||
}
|
||||
return html`<img src=${themedSrc} class=${ifPresent(className)} ${spread(props)} />`;
|
||||
};
|
||||
|
||||
export function isDefaultAvatar(path?: string | null): boolean {
|
||||
return !!path?.endsWith("user_default.png");
|
||||
|
||||
@@ -26,7 +26,7 @@ import { WithBrandConfig } from "#elements/mixins/branding";
|
||||
import { WithCapabilitiesConfig } from "#elements/mixins/capabilities";
|
||||
import { LitPropertyRecord } from "#elements/types";
|
||||
import { exportParts } from "#elements/utils/attributes";
|
||||
import { renderImage } from "#elements/utils/images";
|
||||
import { ThemedImage } from "#elements/utils/images";
|
||||
|
||||
import { AKFlowAdvanceEvent, AKFlowInspectorChangeEvent } from "#flow/events";
|
||||
import { BaseStage, StageHost, SubmitOptions } from "#flow/stages/base";
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
|
||||
import { spread } from "@open-wc/lit-helpers";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { LOCALE_STATUS_EVENT, LocaleStatusEventDetail, msg } from "@lit/localize";
|
||||
import { CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { guard } from "lit/directives/guard.js";
|
||||
@@ -85,20 +85,26 @@ export class FlowExecutor
|
||||
@property({ type: String, attribute: "slug", useDefault: true })
|
||||
public flowSlug: string = window.location.pathname.split("/")[3];
|
||||
|
||||
#challenge?: ChallengeTypes;
|
||||
#challenge: ChallengeTypes | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
public set challenge(value: ChallengeTypes | undefined) {
|
||||
public set challenge(value: ChallengeTypes | null) {
|
||||
const previousValue = this.#challenge;
|
||||
const previousTitle = previousValue?.flowInfo?.title;
|
||||
const nextTitle = value?.flowInfo?.title;
|
||||
|
||||
this.#challenge = value;
|
||||
if (value?.flowInfo?.title) {
|
||||
document.title = `${value.flowInfo?.title} - ${this.brandingTitle}`;
|
||||
} else {
|
||||
|
||||
if (!nextTitle) {
|
||||
document.title = this.brandingTitle;
|
||||
} else if (nextTitle !== previousTitle) {
|
||||
document.title = `${nextTitle} - ${this.brandingTitle}`;
|
||||
}
|
||||
this.requestUpdate();
|
||||
|
||||
this.requestUpdate("challenge", previousValue);
|
||||
}
|
||||
|
||||
public get challenge(): ChallengeTypes | undefined {
|
||||
public get challenge(): ChallengeTypes | null {
|
||||
return this.#challenge;
|
||||
}
|
||||
|
||||
@@ -117,7 +123,7 @@ export class FlowExecutor
|
||||
@property({ type: Boolean })
|
||||
public inspectorAvailable?: boolean;
|
||||
|
||||
@property({ type: String, attribute: "data-layout", useDefault: true })
|
||||
@property({ type: String, attribute: "data-layout", useDefault: true, reflect: true })
|
||||
public layout: FlowLayoutEnum = FlowExecutor.DefaultLayout;
|
||||
|
||||
@state()
|
||||
@@ -159,6 +165,8 @@ export class FlowExecutor
|
||||
});
|
||||
}
|
||||
|
||||
//#region Listeners
|
||||
|
||||
@listen(AKSessionAuthenticatedEvent)
|
||||
protected sessionAuthenticatedListener = () => {
|
||||
if (!document.hidden) {
|
||||
@@ -169,17 +177,22 @@ export class FlowExecutor
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
@listen(LOCALE_STATUS_EVENT)
|
||||
protected localeStatusListener = (event: CustomEvent<LocaleStatusEventDetail>) => {
|
||||
if (event.detail.status !== "ready") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
};
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
|
||||
WebsocketClient.close();
|
||||
}
|
||||
|
||||
public async firstUpdated(): Promise<void> {
|
||||
if (this.can(CapabilitiesEnum.CanDebug)) {
|
||||
this.inspectorAvailable = true;
|
||||
}
|
||||
|
||||
protected refresh = () => {
|
||||
this.loading = true;
|
||||
|
||||
return new FlowsApi(DEFAULT_CONFIG)
|
||||
@@ -187,11 +200,7 @@ export class FlowExecutor
|
||||
flowSlug: this.flowSlug,
|
||||
query: window.location.search.substring(1),
|
||||
})
|
||||
.then((challenge: ChallengeTypes) => {
|
||||
if (this.inspectorOpen) {
|
||||
window.dispatchEvent(new AKFlowAdvanceEvent());
|
||||
}
|
||||
|
||||
.then((challenge) => {
|
||||
this.challenge = challenge;
|
||||
|
||||
if (this.challenge.flowInfo) {
|
||||
@@ -210,6 +219,20 @@ export class FlowExecutor
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
};
|
||||
|
||||
public async firstUpdated(changed: PropertyValues<this>): Promise<void> {
|
||||
super.firstUpdated(changed);
|
||||
|
||||
if (this.can(CapabilitiesEnum.CanDebug)) {
|
||||
this.inspectorAvailable = true;
|
||||
}
|
||||
|
||||
this.refresh().then(() => {
|
||||
if (this.inspectorOpen) {
|
||||
window.dispatchEvent(new AKFlowAdvanceEvent());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// DOM post-processing has to happen after the render.
|
||||
@@ -489,7 +512,12 @@ export class FlowExecutor
|
||||
part="main"
|
||||
>
|
||||
<div class="pf-c-login__main-header pf-c-brand" part="branding">
|
||||
${renderImage(this.brandingLogo, msg("authentik Logo"), "branding-logo")}
|
||||
${ThemedImage({
|
||||
src: this.brandingLogo,
|
||||
alt: msg("authentik Logo"),
|
||||
className: "branding-logo",
|
||||
theme: this.activeTheme,
|
||||
})}
|
||||
</div>
|
||||
${this.loading && this.challenge
|
||||
? html`<ak-loading-overlay></ak-loading-overlay>`
|
||||
|
||||
@@ -2,13 +2,12 @@ import "#elements/EmptyState";
|
||||
import "#elements/Expand";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { AKRequestPostEvent } from "#common/api/events";
|
||||
import { APIError, parseAPIResponseError, pluckErrorDetail } from "#common/errors/network";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { listen } from "#elements/decorators/listen";
|
||||
|
||||
import { AKFlowInspectorChangeEvent } from "#flow/events";
|
||||
import { AKFlowAdvanceEvent, AKFlowInspectorChangeEvent } from "#flow/events";
|
||||
import Styles from "#flow/FlowInspector.css";
|
||||
|
||||
import { FlowInspection, FlowsApi, Stage } from "@goauthentik/api";
|
||||
@@ -56,7 +55,7 @@ export class FlowInspector extends AKElement {
|
||||
|
||||
//#endregion
|
||||
|
||||
@listen(AKRequestPostEvent)
|
||||
@listen(AKFlowAdvanceEvent)
|
||||
protected advanceHandler = (): void => {
|
||||
new FlowsApi(DEFAULT_CONFIG)
|
||||
.flowsInspectorGet({
|
||||
|
||||
@@ -1,22 +1,39 @@
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { LitFC } from "#elements/types";
|
||||
import { ifPresent } from "#elements/utils/attributes";
|
||||
import { isDefaultAvatar } from "#elements/utils/images";
|
||||
|
||||
import {
|
||||
AccessDeniedChallenge,
|
||||
AuthenticatorDuoChallenge,
|
||||
AuthenticatorEmailChallenge,
|
||||
AuthenticatorStaticChallenge,
|
||||
AuthenticatorTOTPChallenge,
|
||||
AuthenticatorWebAuthnChallenge,
|
||||
CaptchaChallenge,
|
||||
ConsentChallenge,
|
||||
PasswordChallenge,
|
||||
SessionEndChallenge,
|
||||
UserLoginChallenge,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { css, CSSResult, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { guard } from "lit/directives/guard.js";
|
||||
|
||||
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
|
||||
|
||||
@customElement("ak-form-static")
|
||||
export class FormStatic extends AKElement {
|
||||
export class AKFormStatic extends AKElement {
|
||||
public override role = "banner";
|
||||
public override ariaLabel = msg("User information");
|
||||
|
||||
@property()
|
||||
userAvatar?: string;
|
||||
@property({ type: String })
|
||||
public avatar: string = "";
|
||||
|
||||
@property()
|
||||
user?: string;
|
||||
@property({ type: String })
|
||||
public username: string = "";
|
||||
|
||||
static styles: CSSResult[] = [
|
||||
PFAvatar,
|
||||
@@ -64,21 +81,27 @@ export class FormStatic extends AKElement {
|
||||
`,
|
||||
];
|
||||
|
||||
render() {
|
||||
if (!this.user) {
|
||||
protected override render() {
|
||||
if (!this.username) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="primary-content">
|
||||
${this.userAvatar && !isDefaultAvatar(this.userAvatar)
|
||||
${this.avatar && !isDefaultAvatar(this.avatar)
|
||||
? html`<img
|
||||
class="pf-c-avatar"
|
||||
src=${this.userAvatar}
|
||||
alt=${this.user ? msg(str`Avatar for ${this.user}`) : msg("User avatar")}
|
||||
src=${this.avatar}
|
||||
alt=${this.username
|
||||
? msg(str`Avatar for ${this.username}`, {
|
||||
id: "avatar.alt-text-for-user",
|
||||
})
|
||||
: msg("User avatar", {
|
||||
id: "avatar.alt-text",
|
||||
})}
|
||||
/>`
|
||||
: nothing}
|
||||
<div class="username" aria-description=${msg("Username")}>${this.user}</div>
|
||||
<div class="username" aria-description=${msg("Username")}>${this.username}</div>
|
||||
</div>
|
||||
<div class="links">
|
||||
<slot name="link"></slot>
|
||||
@@ -87,8 +110,51 @@ export class FormStatic extends AKElement {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type FormStaticChallenge =
|
||||
| SessionEndChallenge
|
||||
| AccessDeniedChallenge
|
||||
| AuthenticatorDuoChallenge
|
||||
| AuthenticatorEmailChallenge
|
||||
| AuthenticatorStaticChallenge
|
||||
| AuthenticatorTOTPChallenge
|
||||
| AuthenticatorWebAuthnChallenge
|
||||
| CaptchaChallenge
|
||||
| ConsentChallenge
|
||||
| PasswordChallenge
|
||||
| UserLoginChallenge;
|
||||
|
||||
export interface FlowUserDetailsProps {
|
||||
challenge?: Partial<
|
||||
Pick<FormStaticChallenge, "pendingUserAvatar" | "pendingUser" | "flowInfo">
|
||||
>;
|
||||
}
|
||||
|
||||
export const FlowUserDetails: LitFC<FlowUserDetailsProps> = ({ challenge }) => {
|
||||
const { pendingUserAvatar, pendingUser, flowInfo } = challenge || {};
|
||||
return guard(
|
||||
[pendingUserAvatar, pendingUser, flowInfo],
|
||||
() =>
|
||||
html`<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
avatar=${ifPresent(pendingUserAvatar)}
|
||||
username=${ifPresent(pendingUser)}
|
||||
>
|
||||
${flowInfo?.cancelUrl
|
||||
? html`
|
||||
<div slot="link">
|
||||
<a href=${flowInfo.cancelUrl}>${msg("Not you?")}</a>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</ak-form-static>`,
|
||||
);
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-form-static": FormStatic;
|
||||
"ak-form-static": AKFormStatic;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import "#flow/components/ak-flow-card";
|
||||
|
||||
import { globalAK } from "#common/global";
|
||||
|
||||
import { FlowUserDetails } from "#flow/FormStatic";
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
|
||||
import { SessionEndChallenge } from "@goauthentik/api";
|
||||
@@ -10,7 +11,6 @@ import { SessionEndChallenge } from "@goauthentik/api";
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, html, nothing, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
@@ -26,17 +26,8 @@ export class SessionEnd extends BaseStage<SessionEndChallenge, unknown> {
|
||||
render(): TemplateResult {
|
||||
return html`<ak-flow-card .challenge=${this.challenge}>
|
||||
<form class="pf-c-form">
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
${FlowUserDetails({ challenge: this.challenge })}
|
||||
|
||||
<p>
|
||||
${msg(
|
||||
str`You've logged out of ${this.challenge.applicationName}. You can go back to the overview to launch another application, or log out of your authentik account.`,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "#flow/FormStatic";
|
||||
import "#flow/components/ak-flow-card";
|
||||
|
||||
import { FlowUserDetails } from "#flow/FormStatic";
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
|
||||
import { AccessDeniedChallenge, FlowChallengeResponseRequest } from "@goauthentik/api";
|
||||
@@ -8,35 +9,31 @@ import { AccessDeniedChallenge, FlowChallengeResponseRequest } from "@goauthenti
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, nothing, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
@customElement("ak-stage-access-denied")
|
||||
export class AccessDeniedStage extends BaseStage<
|
||||
AccessDeniedChallenge,
|
||||
FlowChallengeResponseRequest
|
||||
> {
|
||||
static styles: CSSResult[] = [PFBase, PFLogin, PFForm, PFTitle, PFFormControl];
|
||||
static styles: CSSResult[] = [
|
||||
// ---
|
||||
PFLogin,
|
||||
PFForm,
|
||||
PFTitle,
|
||||
PFFormControl,
|
||||
PFButton,
|
||||
];
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<ak-flow-card .challenge=${this.challenge}>
|
||||
<form class="pf-c-form">
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge?.pendingUserAvatar}"
|
||||
user=${this.challenge?.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
${FlowUserDetails({ challenge: this.challenge })}
|
||||
<ak-empty-state icon="fa-times"
|
||||
><span>${msg("Request has been denied.")}</span>
|
||||
${this.challenge.errorMessage
|
||||
@@ -47,6 +44,20 @@ export class AccessDeniedStage extends BaseStage<
|
||||
`
|
||||
: nothing}
|
||||
</ak-empty-state>
|
||||
${this.challenge.flowInfo?.cancelUrl
|
||||
? html`<fieldset class="pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<a
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
href=${this.challenge.flowInfo?.cancelUrl}
|
||||
>
|
||||
${msg("Go back", {
|
||||
id: "flow.navigation.go-back",
|
||||
})}
|
||||
</a>
|
||||
</fieldset>`
|
||||
: nothing}
|
||||
}
|
||||
</form>
|
||||
</ak-flow-card>`;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import "#flow/components/ak-flow-card";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { FlowUserDetails } from "#flow/FormStatic";
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
|
||||
import {
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
@@ -67,17 +67,8 @@ export class AuthenticatorDuoStage extends BaseStage<
|
||||
render(): TemplateResult {
|
||||
return html`<ak-flow-card .challenge=${this.challenge}>
|
||||
<form class="pf-c-form" @submit=${this.submitForm}>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
${FlowUserDetails({ challenge: this.challenge })}
|
||||
|
||||
<img src=${this.challenge.activationBarcode} alt=${msg("Duo activation QR code")} />
|
||||
<p>
|
||||
${msg(
|
||||
|
||||
@@ -4,6 +4,7 @@ import "#flow/components/ak-flow-card";
|
||||
import { AKFormErrors } from "#components/ak-field-errors";
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { FlowUserDetails } from "#flow/FormStatic";
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
|
||||
import {
|
||||
@@ -11,10 +12,9 @@ import {
|
||||
AuthenticatorEmailChallengeResponseRequest,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
@@ -44,17 +44,8 @@ export class AuthenticatorEmailStage extends BaseStage<
|
||||
renderEmailInput(): TemplateResult {
|
||||
return html`<ak-flow-card .challenge=${this.challenge}>
|
||||
<form class="pf-c-form" @submit=${this.submitForm}>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
${FlowUserDetails({ challenge: this.challenge })}
|
||||
|
||||
<div class="pf-c-form__group">
|
||||
${AKLabel(
|
||||
{ required: true, htmlFor: "email-input" },
|
||||
@@ -88,20 +79,25 @@ export class AuthenticatorEmailStage extends BaseStage<
|
||||
}
|
||||
|
||||
renderEmailOTPInput(): TemplateResult {
|
||||
const { email } = this.challenge;
|
||||
|
||||
return html`<ak-flow-card .challenge=${this.challenge}>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
A verification token has been sent to your configured email address
|
||||
${ifDefined(this.challenge.email)}
|
||||
${FlowUserDetails({ challenge: this.challenge })}
|
||||
|
||||
<p>
|
||||
${email
|
||||
? msg(
|
||||
str`A verification token has been sent to your configured email address: ${email}`,
|
||||
{
|
||||
id: "stage.authenticator.email.sent-to-address",
|
||||
desc: "Displayed when a verification token has been sent to the user's configured email address.",
|
||||
},
|
||||
)
|
||||
: msg("A verification token has been sent to your email address.", {
|
||||
id: "stage.authenticator.email.sent",
|
||||
desc: "Displayed when a verification token has been sent to the user's email address.",
|
||||
})}
|
||||
</p>
|
||||
<form class="pf-c-form" @submit=${this.submitForm}>
|
||||
<div class="pf-c-form__group">
|
||||
${AKLabel({ required: true, htmlFor: "code-input" }, msg("Code"))}
|
||||
|
||||
@@ -4,6 +4,7 @@ import "#flow/components/ak-flow-card";
|
||||
import { AKFormErrors } from "#components/ak-field-errors";
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { FlowUserDetails } from "#flow/FormStatic";
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
|
||||
import {
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
@@ -44,17 +44,8 @@ export class AuthenticatorSMSStage extends BaseStage<
|
||||
renderPhoneNumber(): TemplateResult {
|
||||
return html`<ak-flow-card .challenge=${this.challenge}>
|
||||
<form class="pf-c-form" @submit=${this.submitForm}>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar=${this.challenge.pendingUserAvatar}
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
${FlowUserDetails({ challenge: this.challenge })}
|
||||
|
||||
<div class="pf-c-form__group">
|
||||
${AKLabel(
|
||||
{ required: true, htmlFor: "phone-number-input" },
|
||||
@@ -90,17 +81,7 @@ export class AuthenticatorSMSStage extends BaseStage<
|
||||
renderCode(): TemplateResult {
|
||||
return html`<ak-flow-card .challenge=${this.challenge}>
|
||||
<form class="pf-c-form" @submit=${this.submitForm}>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
${FlowUserDetails({ challenge: this.challenge })}
|
||||
<div class="pf-c-form__group">
|
||||
${AKLabel({ required: true, htmlFor: "sms-code-input" }, msg("Code"))}
|
||||
<input
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "#flow/FormStatic";
|
||||
import "#flow/components/ak-flow-card";
|
||||
|
||||
import { FlowUserDetails } from "#flow/FormStatic";
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
|
||||
import {
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
@@ -53,17 +53,8 @@ export class AuthenticatorStaticStage extends BaseStage<
|
||||
render(): TemplateResult {
|
||||
return html`<ak-flow-card .challenge=${this.challenge}>
|
||||
<form class="pf-c-form" @submit=${this.submitForm}>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
${FlowUserDetails({ challenge: this.challenge })}
|
||||
|
||||
<ul class="pf-c-form__group token-list">
|
||||
${this.challenge.codes.map((token) => {
|
||||
return html`<li>${token}</li>`;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { showMessage } from "#elements/messages/MessageContainer";
|
||||
import { AKFormErrors } from "#components/ak-field-errors";
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { FlowUserDetails } from "#flow/FormStatic";
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
|
||||
import {
|
||||
@@ -19,7 +20,6 @@ import {
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
@@ -54,17 +54,8 @@ export class AuthenticatorTOTPStage extends BaseStage<
|
||||
render(): TemplateResult {
|
||||
return html`<ak-flow-card .challenge=${this.challenge}>
|
||||
<form class="pf-c-form" @submit=${this.submitForm}>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
${FlowUserDetails({ challenge: this.challenge })}
|
||||
|
||||
<input type="hidden" name="otp_uri" value=${this.challenge.configUrl} />
|
||||
|
||||
<div class="pf-c-form__group">
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
transformNewAssertionForServer,
|
||||
} from "#common/helpers/webauthn";
|
||||
|
||||
import { FlowUserDetails } from "#flow/FormStatic";
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
|
||||
import {
|
||||
@@ -19,7 +20,6 @@ import {
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
@@ -116,17 +116,8 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
|
||||
render(): TemplateResult {
|
||||
return html`<ak-flow-card .challenge=${this.challenge}>
|
||||
<form class="pf-c-form">
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
${FlowUserDetails({ challenge: this.challenge })}
|
||||
|
||||
<ak-empty-state ?loading="${this.registerRunning}" icon="fa-times">
|
||||
<span
|
||||
>${this.registerRunning
|
||||
|
||||
@@ -7,12 +7,14 @@ import { intersectionObserver } from "#elements/decorators/intersection-observer
|
||||
import { WithLocale } from "#elements/mixins/locale";
|
||||
import { FocusTarget } from "#elements/utils/focus";
|
||||
|
||||
import { FlowUserDetails } from "#flow/FormStatic";
|
||||
|
||||
import { ConsoleLogger } from "#logger/browser";
|
||||
|
||||
import { ContextualFlowInfo, CurrentBrand, ErrorDetail } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
export interface SubmitOptions {
|
||||
invisible: boolean;
|
||||
@@ -65,6 +67,8 @@ export abstract class BaseStage<
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
protected logger = ConsoleLogger.prefix(`flow:${this.tagName.toLowerCase()}`);
|
||||
|
||||
// TODO: Should have a property but this needs some refactoring first.
|
||||
// @property({ attribute: false })
|
||||
public host!: StageHost;
|
||||
@@ -137,11 +141,11 @@ export abstract class BaseStage<
|
||||
}
|
||||
}
|
||||
|
||||
return this.host?.submit(payload).then((successful) => {
|
||||
return this.host?.submit(payload).then(async (successful) => {
|
||||
if (successful) {
|
||||
this.onSubmitSuccess();
|
||||
await this.onSubmitSuccess?.(payload);
|
||||
} else {
|
||||
this.onSubmitFailure();
|
||||
await this.onSubmitFailure?.(payload);
|
||||
}
|
||||
|
||||
return successful;
|
||||
@@ -178,17 +182,7 @@ export abstract class BaseStage<
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
${FlowUserDetails({ challenge: this.challenge })}
|
||||
<input
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
@@ -198,12 +192,17 @@ export abstract class BaseStage<
|
||||
`;
|
||||
}
|
||||
|
||||
onSubmitSuccess(): void {
|
||||
// Method that can be overridden by stages
|
||||
return;
|
||||
}
|
||||
onSubmitFailure(): void {
|
||||
// Method that can be overridden by stages
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Callback method for successful form submission.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
protected onSubmitSuccess?(payload: Record<string, unknown>): void | Promise<void>;
|
||||
|
||||
/**
|
||||
* Callback method for failed form submission.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
protected onSubmitFailure?(payload: Record<string, unknown>): void | Promise<void>;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ifPresent } from "#elements/utils/attributes";
|
||||
import { ListenerController } from "#elements/utils/listenerController";
|
||||
import { randomId } from "#elements/utils/randomId";
|
||||
|
||||
import { FlowUserDetails } from "#flow/FormStatic";
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
import { CaptchaHandler, CaptchaProvider, iframeTemplate } from "#flow/stages/captcha/shared";
|
||||
|
||||
@@ -20,7 +21,6 @@ import { match } from "ts-pattern";
|
||||
import { LOCALE_STATUS_EVENT, LocaleStatusEventDetail, msg } from "@lit/localize";
|
||||
import { css, CSSResult, html, nothing, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { createRef, ref } from "lit/directives/ref.js";
|
||||
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
@@ -351,18 +351,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
renderMain() {
|
||||
return html`<ak-flow-card .challenge=${this.challenge}>
|
||||
<form class="pf-c-form">
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
${this.renderBody()}
|
||||
${FlowUserDetails({ challenge: this.challenge })} ${this.renderBody()}
|
||||
</form>
|
||||
</ak-flow-card>`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "#flow/FormStatic";
|
||||
import "#flow/components/ak-flow-card";
|
||||
|
||||
import { FlowUserDetails } from "#flow/FormStatic";
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
|
||||
import {
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, nothing, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
@@ -113,17 +113,7 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
${FlowUserDetails({ challenge: this.challenge })}
|
||||
${this.challenge.additionalPermissions.length > 0
|
||||
? this.renderAdditional()
|
||||
: this.renderNoPrevious()}
|
||||
|
||||
@@ -307,11 +307,11 @@ export class IdentificationStage extends BaseStage<
|
||||
|
||||
//#endregion
|
||||
|
||||
onSubmitSuccess(): void {
|
||||
protected override onSubmitSuccess(): void {
|
||||
this.#form?.remove();
|
||||
}
|
||||
|
||||
onSubmitFailure(): void {
|
||||
protected override onSubmitFailure(): void {
|
||||
const captchaInput = this.#captchaInputRef.value;
|
||||
|
||||
if (captchaInput) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import "#flow/components/ak-flow-password-input";
|
||||
|
||||
import { ErrorProp } from "#components/ak-field-errors";
|
||||
|
||||
import { FlowUserDetails } from "#flow/FormStatic";
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
import { PasswordManagerPrefill } from "#flow/stages/identification/IdentificationStage";
|
||||
|
||||
@@ -12,7 +13,6 @@ import { PasswordChallenge, PasswordChallengeResponseRequest } from "@goauthenti
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
@@ -43,17 +43,8 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng
|
||||
render(): TemplateResult {
|
||||
return html`<ak-flow-card .challenge=${this.challenge}>
|
||||
<form class="pf-c-form" @submit=${this.submitForm}>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
${FlowUserDetails({ challenge: this.challenge })}
|
||||
|
||||
<input
|
||||
name="username"
|
||||
type="text"
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import "#elements/Divider";
|
||||
import "#flow/components/ak-flow-card";
|
||||
|
||||
import { formatLocaleDisplayNames, renderLocaleDisplayNames } from "#common/ui/locale/format";
|
||||
import { getBestMatchLocale } from "#common/ui/locale/utils";
|
||||
|
||||
import { WithCapabilitiesConfig } from "#elements/mixins/capabilities";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { AKFormErrors } from "#components/ak-field-errors";
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
import { LocalePrompt } from "#flow/stages/prompt/components/locale";
|
||||
|
||||
import {
|
||||
CapabilitiesEnum,
|
||||
PromptChallenge,
|
||||
PromptChallengeResponseRequest,
|
||||
PromptTypeEnum,
|
||||
@@ -33,9 +33,6 @@ import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
// Fixes horizontal rule <hr> warning in select dropdowns.
|
||||
/* eslint-disable lit/no-invalid-html */
|
||||
|
||||
@customElement("ak-stage-prompt")
|
||||
export class PromptStage extends WithCapabilitiesConfig(
|
||||
BaseStage<PromptChallenge, PromptChallengeResponseRequest>,
|
||||
@@ -59,7 +56,7 @@ export class PromptStage extends WithCapabilitiesConfig(
|
||||
`,
|
||||
];
|
||||
|
||||
renderPromptInner(prompt: StagePrompt, disabled = false): TemplateResult {
|
||||
renderPromptInner(prompt: StagePrompt, disabled = false): SlottedTemplateResult {
|
||||
const fieldId = `field-${prompt.fieldKey}`;
|
||||
|
||||
switch (prompt.type) {
|
||||
@@ -233,31 +230,13 @@ ${prompt.initialValue}</textarea
|
||||
</div> `;
|
||||
})}`;
|
||||
case PromptTypeEnum.AkLocale: {
|
||||
const entries = formatLocaleDisplayNames(this.activeLanguageTag);
|
||||
|
||||
const currentLanguageTag = prompt.initialValue
|
||||
? getBestMatchLocale(prompt.initialValue)
|
||||
: null;
|
||||
|
||||
return html`<select
|
||||
class="pf-c-form-control ak-m-capitalize"
|
||||
id=${fieldId}
|
||||
name="${prompt.fieldKey}"
|
||||
?disabled=${disabled}
|
||||
aria-label=${msg("Select language", {
|
||||
id: "language-selector-label",
|
||||
desc: "Label for the language selection dropdown",
|
||||
})}
|
||||
>
|
||||
<option value="" ?selected=${!currentLanguageTag}>
|
||||
${msg("Auto-detect", {
|
||||
id: "locale-auto-detect-option",
|
||||
desc: "Label for the auto-detect locale option in language selection dropdown",
|
||||
})}
|
||||
</option>
|
||||
<hr />
|
||||
${renderLocaleDisplayNames(entries, currentLanguageTag)}
|
||||
</select>`;
|
||||
return LocalePrompt({
|
||||
activeLanguageTag: this.activeLanguageTag,
|
||||
prompt,
|
||||
fieldId,
|
||||
disabled,
|
||||
debug: this.can(CapabilitiesEnum.CanDebug),
|
||||
});
|
||||
}
|
||||
default:
|
||||
return html`<p>invalid type '${prompt.type}'</p>`;
|
||||
|
||||
78
web/src/flow/stages/prompt/components/locale.ts
Normal file
78
web/src/flow/stages/prompt/components/locale.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { TargetLanguageTag } from "#common/ui/locale/definitions";
|
||||
import {
|
||||
formatAutoDetectLocaleDisplayName,
|
||||
formatLocaleDisplayNames,
|
||||
LocaleDisplay,
|
||||
} from "#common/ui/locale/format";
|
||||
import { getBestMatchLocale, getSessionLocale } from "#common/ui/locale/utils";
|
||||
|
||||
import { LocaleOptions } from "#elements/locale/utils";
|
||||
import { LitFC } from "#elements/types";
|
||||
|
||||
import { StagePrompt } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { guard } from "lit/directives/guard.js";
|
||||
|
||||
// Fixes horizontal rule <hr> warning in select dropdowns.
|
||||
/* eslint-disable lit/no-invalid-html */
|
||||
|
||||
export interface LocalePromptProps {
|
||||
activeLanguageTag: TargetLanguageTag;
|
||||
prompt: StagePrompt;
|
||||
fieldId: string;
|
||||
disabled?: boolean;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
export const LocalePrompt: LitFC<LocalePromptProps> = ({
|
||||
activeLanguageTag,
|
||||
prompt,
|
||||
disabled,
|
||||
debug,
|
||||
fieldId,
|
||||
}) => {
|
||||
const sessionLocale = getSessionLocale();
|
||||
|
||||
return guard(
|
||||
[activeLanguageTag, prompt.fieldKey, prompt.initialValue, disabled, sessionLocale],
|
||||
() => {
|
||||
const entries = formatLocaleDisplayNames(activeLanguageTag, {
|
||||
debug,
|
||||
});
|
||||
|
||||
const languagesByTag = new Map<TargetLanguageTag, LocaleDisplay>(
|
||||
entries.map((entry) => [entry[0], entry]),
|
||||
);
|
||||
|
||||
const selectedLanguageTag = prompt.initialValue
|
||||
? getBestMatchLocale(prompt.initialValue)
|
||||
: null;
|
||||
|
||||
/**
|
||||
* This is a bit subtle.
|
||||
*
|
||||
* -
|
||||
*/
|
||||
const autoDetectedLocale = formatAutoDetectLocaleDisplayName(
|
||||
sessionLocale ? languagesByTag.get(selectedLanguageTag || activeLanguageTag) : null,
|
||||
);
|
||||
|
||||
return html`<select
|
||||
class="pf-c-form-control ak-m-capitalize"
|
||||
id=${fieldId}
|
||||
name=${prompt.fieldKey}
|
||||
?disabled=${disabled}
|
||||
aria-label=${msg("Select language", {
|
||||
id: "language-selector-label",
|
||||
desc: "Label for the language selection dropdown",
|
||||
})}
|
||||
>
|
||||
<option value="" ?selected=${!selectedLanguageTag}>${autoDetectedLocale}</option>
|
||||
<hr />
|
||||
${LocaleOptions({ entries, activeLocaleTag: selectedLanguageTag })}
|
||||
</select>`;
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import "#flow/FormStatic";
|
||||
import "#flow/components/ak-flow-card";
|
||||
|
||||
import { FlowUserDetails } from "#flow/FormStatic";
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
|
||||
import { UserLoginChallenge, UserLoginChallengeResponseRequest } from "@goauthentik/api";
|
||||
@@ -8,7 +9,6 @@ import { UserLoginChallenge, UserLoginChallengeResponseRequest } from "@goauthen
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
@@ -48,17 +48,8 @@ export class PasswordStage extends BaseStage<
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${msg("Not you?")}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
${FlowUserDetails({ challenge: this.challenge })}
|
||||
|
||||
<div class="pf-c-form__group">
|
||||
<h3 data-test-id="stage-heading" class="pf-c-title pf-m-xl pf-u-mb-xl">
|
||||
${msg("Stay signed in?")}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getCookie } from "#common/utils";
|
||||
|
||||
import { Interface } from "#elements/Interface";
|
||||
import { WithBrandConfig } from "#elements/mixins/branding";
|
||||
import { renderImage } from "#elements/utils/images";
|
||||
import { ThemedImage } from "#elements/utils/images";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, TemplateResult } from "lit";
|
||||
@@ -101,7 +101,12 @@ export class APIBrowser extends WithBrandConfig(Interface) {
|
||||
show-method-in-nav-bar="as-colored-text"
|
||||
>
|
||||
<div slot="nav-logo">
|
||||
${renderImage(this.brandingLogo, msg("authentik Logo"), "logo")}
|
||||
${ThemedImage({
|
||||
src: this.brandingLogo,
|
||||
alt: msg("authentik Logo"),
|
||||
className: "logo",
|
||||
theme: this.activeTheme,
|
||||
})}
|
||||
</div>
|
||||
</rapi-doc>
|
||||
`;
|
||||
|
||||
@@ -187,8 +187,7 @@
|
||||
|
||||
.branding-logo {
|
||||
display: block;
|
||||
width: clamp(75%, calc(var(--ak-login--MaxWidth) / 2), 90%);
|
||||
min-height: 4rem;
|
||||
max-width: clamp(75%, calc(var(--ak-login--MaxWidth) / 2), 90%);
|
||||
}
|
||||
|
||||
/* Ensure Font Awesome logos scale similarly to image logos */
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
renderNotificationDrawerPanel,
|
||||
} from "#elements/notifications/utils";
|
||||
import { ifPresent } from "#elements/utils/attributes";
|
||||
import { renderImage } from "#elements/utils/images";
|
||||
import { ThemedImage } from "#elements/utils/images";
|
||||
|
||||
import Styles from "#user/index.entrypoint.css";
|
||||
import { ROUTES } from "#user/Routes";
|
||||
@@ -150,7 +150,12 @@ class UserInterface extends WithBrandConfig(WithSession(AuthenticatedInterface))
|
||||
<header part="page__header" class="pf-c-page__header">
|
||||
<div part="brand" class="pf-c-page__header-brand">
|
||||
<a href="#/" class="pf-c-page__header-brand-link">
|
||||
${renderImage(this.brandingLogo, this.brandingTitle, "pf-c-brand")}
|
||||
${ThemedImage({
|
||||
src: this.brandingLogo,
|
||||
alt: this.brandingTitle,
|
||||
className: "pf-c-brand",
|
||||
theme: this.activeTheme,
|
||||
})}
|
||||
</a>
|
||||
</div>
|
||||
<ak-nav-buttons>${this.renderAdminInterfaceLink()}</ak-nav-buttons>
|
||||
|
||||
@@ -40,16 +40,19 @@ export class UserSettingsFlowExecutor
|
||||
@property()
|
||||
flowSlug = this.brand?.flowUserSettings;
|
||||
|
||||
private _challenge?: ChallengeTypes;
|
||||
#challenge: ChallengeTypes | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
set challenge(value: ChallengeTypes | undefined) {
|
||||
this._challenge = value;
|
||||
this.requestUpdate();
|
||||
set challenge(value: ChallengeTypes | null) {
|
||||
const previousValue = this.#challenge;
|
||||
|
||||
this.#challenge = value;
|
||||
|
||||
this.requestUpdate("challenge", previousValue);
|
||||
}
|
||||
|
||||
get challenge(): ChallengeTypes | undefined {
|
||||
return this._challenge;
|
||||
get challenge(): ChallengeTypes | null {
|
||||
return this.#challenge;
|
||||
}
|
||||
|
||||
@property({ type: Boolean })
|
||||
|
||||
@@ -3,6 +3,9 @@ import "#flow/components/ak-flow-card";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { globalAK } from "#common/global";
|
||||
import { autoDetectLanguage, setSessionLocale } from "#common/ui/locale/utils";
|
||||
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
@@ -58,7 +61,7 @@ export class UserSettingsPromptStage extends PromptStage {
|
||||
return super.renderPromptHelpText(prompt);
|
||||
}
|
||||
|
||||
renderPromptInner(prompt: StagePrompt): TemplateResult {
|
||||
renderPromptInner(prompt: StagePrompt): SlottedTemplateResult {
|
||||
if (prompt.type === PromptTypeEnum.Checkbox) {
|
||||
return html`<input
|
||||
type="checkbox"
|
||||
@@ -135,6 +138,38 @@ export class UserSettingsPromptStage extends PromptStage {
|
||||
</div>
|
||||
</ak-flow-card>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if the locale was changed in a prompt stage and updates the session accordingly.
|
||||
*/
|
||||
protected override onSubmitSuccess(payload: Record<string, unknown>): void {
|
||||
super.onSubmitSuccess?.(payload);
|
||||
|
||||
if (this.challenge.component !== "ak-stage-prompt") return;
|
||||
|
||||
const localeField = this.challenge.fields.find(
|
||||
(field) => field.type === PromptTypeEnum.AkLocale,
|
||||
);
|
||||
|
||||
if (!localeField) return;
|
||||
|
||||
const previousLanguageTag = localeField.initialValue;
|
||||
const languageTag = localeField?.fieldKey ? payload[localeField.fieldKey] : null;
|
||||
|
||||
if (typeof languageTag !== "string") return;
|
||||
|
||||
// Remove the temporary session locale...
|
||||
setSessionLocale(null);
|
||||
|
||||
if (languageTag !== this.activeLanguageTag) {
|
||||
this.logger.info("A prompt stage changed the locale", {
|
||||
languageTag,
|
||||
previousLanguageTag,
|
||||
});
|
||||
|
||||
this.activeLanguageTag = autoDetectLanguage(languageTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
9863
web/xliff/cs_CZ.xlf
9863
web/xliff/cs_CZ.xlf
File diff suppressed because it is too large
Load Diff
9902
web/xliff/de_DE.xlf
9902
web/xliff/de_DE.xlf
File diff suppressed because it is too large
Load Diff
9823
web/xliff/es_ES.xlf
9823
web/xliff/es_ES.xlf
File diff suppressed because it is too large
Load Diff
10086
web/xliff/fi_FI.xlf
10086
web/xliff/fi_FI.xlf
File diff suppressed because it is too large
Load Diff
10330
web/xliff/fr_FR.xlf
10330
web/xliff/fr_FR.xlf
File diff suppressed because it is too large
Load Diff
9772
web/xliff/it_IT.xlf
9772
web/xliff/it_IT.xlf
File diff suppressed because it is too large
Load Diff
10068
web/xliff/ja_JP.xlf
10068
web/xliff/ja_JP.xlf
File diff suppressed because it is too large
Load Diff
9396
web/xliff/ko_KR.xlf
9396
web/xliff/ko_KR.xlf
File diff suppressed because it is too large
Load Diff
9426
web/xliff/pl_PL.xlf
9426
web/xliff/pl_PL.xlf
File diff suppressed because it is too large
Load Diff
10057
web/xliff/pt_BR.xlf
10057
web/xliff/pt_BR.xlf
File diff suppressed because it is too large
Load Diff
9514
web/xliff/ru_RU.xlf
9514
web/xliff/ru_RU.xlf
File diff suppressed because it is too large
Load Diff
9492
web/xliff/tr_TR.xlf
9492
web/xliff/tr_TR.xlf
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user