Files
authentik/web/src/elements/controllers/SessionContextController.ts
Teffen Ellis b88d082947 web/a11y: Modals, Command Palette (Merge branch) (#17812)
* Use project relative paths.

* Fix tests.

* Fix types.

* Clean up admin imports.

* Move admin import.

* Remove or replace references to admin.

* Typo fix.

* Flesh out ak-modal, about modal.

* Flesh out lazy modal.

* Fix portal elements not using dialog scope.

* Fix url parameters, wizards.

* Fix invokers, lazy load.

* Fix theming.

* Add placeholders, help.

* Flesh out command palette.

Flesh out styles, command invokers.

Continue clean up.

Allow slotted content.

Flesh out.

* Flesh out edit invoker. Prep groups.

* Fix odd labeling, legacy situations.

* Prepare deprecation of table modal. Clean up serialization.

* Tidy types.

* Port provider select modal.

* Port member select form.

* Flesh out role modal. Fix loading state.

* Port user group form.

* Fix spellcheck.

* Fix dialog detection.

* Revise types.

* Port rac launch modal.

* Remove deprecated table modal.

* Consistent form action placement.

* Consistent casing.

* Consistent alignment.

* Use more appropriate description.

* Flesh out icon. Fix alignment, colors.

* Flesh out user search.

* Consistent save button.

* Clean up labels.

* Reduce warning noise.

* Clean up label.

* Use attribute e2e expects.

* Use directive. Fix lifecycle

* Fix frequent un-memoized entries.

* Fix up closedBy detection.

* Tidy alignment.

* Fix types, composition.

* Fix labels, tests.

* Fix up impersonation, labels.

* Flesh out. Fix refresh after submit.

* Flesh out basic modal test.

* Fix ARIA.

* Flesh out roles test.

* Revise selectors.

* Clean up selectors.

* Fix impersonation labels, form references.

* Fix messages appearing under modals.

* Ensure reason is parsed.

* Flesh out impersonation test.

* Flesh out impersonate test.

* Flesh out application tests. Clean up toolbar header, ARIA.

* Flesh out wizard test.

* Refine weight, order.

* Fix up initial values, selectors.

* Fix tests.

* Fix selector.
2026-03-25 06:07:29 +00:00

203 lines
6.4 KiB
TypeScript

import { DEFAULT_CONFIG } from "#common/api/config";
import { type APIResult, isAPIResultReady } from "#common/api/responses";
import { globalAK } from "#common/global";
import { applyThemeChoice, formatColorScheme } from "#common/theme";
import { createUIConfig, DefaultUIConfig } from "#common/ui/config";
import { autoDetectLanguage } from "#common/ui/locale/utils";
import { me } from "#common/users";
import {
CommandPaletteState,
PaletteCommandDefinitionInit,
PaletteCommandNamespace,
} from "#elements/commands/shared";
import { ReactiveContextController } from "#elements/controllers/ReactiveContextController";
import { AKConfigMixin, kAKConfig } from "#elements/mixins/config";
import { kAKLocale, type LocaleMixin } from "#elements/mixins/locale";
import {
canAccessAdmin,
SessionContext,
SessionMixin,
UIConfigContext,
} from "#elements/mixins/session";
import { AKDrawerChangeEvent } from "#elements/notifications/events";
import type { ReactiveElementHost } from "#elements/types";
import { CoreApi, SessionUser } from "@goauthentik/api";
import { setUser } from "@sentry/browser";
import { ContextProvider } from "@lit/context";
import { msg } from "@lit/localize";
/**
* A controller that provides the session information to the element.
*
* @see {@linkcode SessionMixin}
*/
export class SessionContextController extends ReactiveContextController<APIResult<SessionUser>> {
protected static override logPrefix = "session";
public host: ReactiveElementHost<LocaleMixin & SessionMixin & AKConfigMixin>;
public context: ContextProvider<SessionContext>;
protected uiConfigContext: ContextProvider<UIConfigContext>;
constructor(
host: ReactiveElementHost<SessionMixin & AKConfigMixin>,
initialValue?: APIResult<SessionUser>,
) {
super();
this.host = host;
this.context = new ContextProvider(this.host, {
context: SessionContext,
initialValue: initialValue ?? { loading: true, error: null },
});
this.uiConfigContext = new ContextProvider(this.host, {
context: UIConfigContext,
initialValue: DefaultUIConfig,
});
}
protected apiEndpoint(requestInit?: RequestInit) {
return me(requestInit);
}
#refreshCommandsFrameID = -1;
#commands = new CommandPaletteState({
target: this.host,
});
protected doRefresh(session: APIResult<SessionUser>): void {
this.context.setValue(session);
this.host.session = session;
if (!isAPIResultReady(session)) return;
const localeHint: string | undefined = session.user.settings.locale;
if (localeHint) {
const locale = autoDetectLanguage(localeHint);
this.logger.info(`Activating user's configured locale '${locale}'`);
this.host[kAKLocale]?.setLocale(locale);
}
const { settings = {} } = session.user || {};
const nextUIConfig = createUIConfig(settings);
this.uiConfigContext.setValue(nextUIConfig);
this.host.uiConfig = nextUIConfig;
const colorScheme = formatColorScheme(nextUIConfig.theme.base);
applyThemeChoice(colorScheme, this.host.ownerDocument);
const config = this.host[kAKConfig];
if (config?.errorReporting.sendPii) {
this.logger.info("Sentry with PII enabled.");
setUser({ email: session.user.email });
}
this.#refreshCommandsFrameID = requestAnimationFrame(this.#refreshCommands);
}
#refreshCommands = (): void => {
const session = this.context.value;
if (!isAPIResultReady(session)) {
this.#commands.clear();
return;
}
const base = globalAK().api.base;
const group = msg("Session");
const weight = 0.5;
const commands: PaletteCommandDefinitionInit[] = [
{
namespace: PaletteCommandNamespace.Navigation,
label: msg("User settings"),
prefix: msg("Navigate to", { id: "command-palette.prefix.navigate" }),
group,
weight,
action: () => {
window.location.assign(`${base}if/user/#/settings`);
},
},
];
const { notificationDrawer, apiDrawer } = this.host.uiConfig?.enabledFeatures ?? {};
const drawerGroup = msg("Interface");
if (apiDrawer) {
commands.push({
label: msg("API requests drawer", {
id: "command-palette.label.api-requests-drawer",
}),
prefix: msg("Toggle", { id: "command-palette.prefix.toggle" }),
group: drawerGroup,
weight,
action: AKDrawerChangeEvent.dispatchAPIToggle,
});
}
if (notificationDrawer) {
commands.push({
label: msg("Notifications drawer", {
id: "command-palette.label.notifications-drawer",
}),
prefix: msg("Toggle", { id: "command-palette.prefix.toggle" }),
group: drawerGroup,
weight,
action: AKDrawerChangeEvent.dispatchNotificationsToggle,
});
}
if (canAccessAdmin(session.user)) {
commands.push({
label: msg("Admin interface"),
prefix: msg("Navigate to", { id: "command-palette.prefix.navigate" }),
group,
weight,
action: () => {
window.location.assign(`${base}if/admin/`);
},
});
}
if (session.original) {
commands.push({
label: msg("Stop impersonation"),
suffix: msg("Reloads page", { id: "command-palette.prefix.reloads-page" }),
group,
weight,
action: async () => {
await new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve();
window.location.reload();
},
});
}
this.#commands.set(commands);
};
public override hostConnected() {
this.logger.debug("Host connected, refreshing session");
this.refresh();
}
public override hostDisconnected() {
this.context.clearCallbacks();
cancelAnimationFrame(this.#refreshCommandsFrameID);
super.hostDisconnected();
}
}