Compare commits
7 Commits
safari-cra
...
safari-loc
| Author | SHA1 | Date | |
|---|---|---|---|
| 9deed34479 | |||
| 2033d52dc2 | |||
| be00f47ddc | |||
| 2cc5f4b273 | |||
| 4e8f3407a4 | |||
| 7f861cc2a1 | |||
| 7bf58d0ba2 |
2
go.mod
2
go.mod
@ -27,7 +27,7 @@ require (
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2025024.8
|
||||
goauthentik.io/api/v3 v3.2025024.9
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.29.0
|
||||
golang.org/x/sync v0.13.0
|
||||
|
||||
4
go.sum
4
go.sum
@ -290,8 +290,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
goauthentik.io/api/v3 v3.2025024.8 h1:2mG4CqGSsmZq2CtRehxpDjsER43U/JQSoTOn5VC1ui4=
|
||||
goauthentik.io/api/v3 v3.2025024.8/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2025024.9 h1:i3tbkyotE32ZpJ729BsPWTuLQUdtZ54Li4aP1amZzsM=
|
||||
goauthentik.io/api/v3 v3.2025024.9/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
||||
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-04-22 13:40+0000\n"
|
||||
"POT-Creation-Date: 2025-04-23 09:00+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -1255,20 +1255,6 @@ msgstr ""
|
||||
msgid "Reputation Scores"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/policies/templates/policies/buffer.html
|
||||
msgid "Waiting for authentication..."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/policies/templates/policies/buffer.html
|
||||
msgid ""
|
||||
"You're already authenticating in another tab. This page will refresh once "
|
||||
"authentication is completed."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/policies/templates/policies/buffer.html
|
||||
msgid "Authenticate in this tab"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Permission denied"
|
||||
msgstr ""
|
||||
|
||||
@ -4,7 +4,6 @@ import { ROUTES } from "@goauthentik/admin/Routes";
|
||||
import {
|
||||
EVENT_API_DRAWER_TOGGLE,
|
||||
EVENT_NOTIFICATION_DRAWER_TOGGLE,
|
||||
EVENT_SIDEBAR_TOGGLE,
|
||||
} from "@goauthentik/common/constants";
|
||||
import { configureSentry } from "@goauthentik/common/sentry";
|
||||
import { me } from "@goauthentik/common/users";
|
||||
@ -12,8 +11,6 @@ import { WebsocketClient } from "@goauthentik/common/ws";
|
||||
import { AuthenticatedInterface } from "@goauthentik/elements/Interface";
|
||||
import "@goauthentik/elements/ak-locale-context";
|
||||
import "@goauthentik/elements/banner/EnterpriseStatusBanner";
|
||||
import "@goauthentik/elements/banner/EnterpriseStatusBanner";
|
||||
import "@goauthentik/elements/banner/VersionBanner";
|
||||
import "@goauthentik/elements/banner/VersionBanner";
|
||||
import "@goauthentik/elements/messages/MessageContainer";
|
||||
import "@goauthentik/elements/messages/MessageContainer";
|
||||
@ -43,8 +40,6 @@ if (process.env.NODE_ENV === "development") {
|
||||
|
||||
@customElement("ak-interface-admin")
|
||||
export class AdminInterface extends AuthenticatedInterface {
|
||||
//#region Properties
|
||||
|
||||
@property({ type: Boolean })
|
||||
notificationDrawerOpen = getURLParam("notificationDrawerOpen", false);
|
||||
|
||||
@ -59,17 +54,6 @@ export class AdminInterface extends AuthenticatedInterface {
|
||||
@query("ak-about-modal")
|
||||
aboutModal?: AboutModal;
|
||||
|
||||
@state()
|
||||
sidebarVisible = false;
|
||||
|
||||
#toggleSidebar = () => {
|
||||
this.sidebarVisible = !this.sidebarVisible;
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Styles
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
PFBase,
|
||||
@ -83,30 +67,23 @@ export class AdminInterface extends AuthenticatedInterface {
|
||||
z-index: auto !important;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.display-none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pf-c-page {
|
||||
background-color: var(--pf-c-page--BackgroundColor) !important;
|
||||
}
|
||||
|
||||
:host([theme="dark"]) {
|
||||
/* Global page background colour */
|
||||
.pf-c-page {
|
||||
--pf-c-page--BackgroundColor: var(--ak-dark-background);
|
||||
}
|
||||
/* Global page background colour */
|
||||
:host([theme="dark"]) .pf-c-page {
|
||||
--pf-c-page--BackgroundColor: var(--ak-dark-background);
|
||||
}
|
||||
|
||||
ak-page-navbar {
|
||||
ak-enterprise-status,
|
||||
ak-version-banner {
|
||||
grid-area: header;
|
||||
}
|
||||
|
||||
ak-admin-sidebar {
|
||||
grid-area: nav;
|
||||
}
|
||||
|
||||
.pf-c-drawer__panel {
|
||||
z-index: var(--pf-global--ZIndex--xl);
|
||||
}
|
||||
@ -114,10 +91,6 @@ export class AdminInterface extends AuthenticatedInterface {
|
||||
];
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.ws = new WebsocketClient();
|
||||
@ -150,26 +123,12 @@ export class AdminInterface extends AuthenticatedInterface {
|
||||
}
|
||||
}
|
||||
|
||||
async connectedCallback(): Promise<void> {
|
||||
super.connectedCallback();
|
||||
|
||||
window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar);
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
const sidebarClasses = {
|
||||
"pf-m-light": this.activeTheme === UiThemeEnum.Light,
|
||||
"pf-m-expanded": !this.sidebarVisible,
|
||||
"pf-m-collapsed": this.sidebarVisible,
|
||||
};
|
||||
|
||||
const drawerOpen = this.notificationDrawerOpen || this.apiDrawerOpen;
|
||||
|
||||
const drawerClasses = {
|
||||
"pf-m-expanded": drawerOpen,
|
||||
"pf-m-collapsed": !drawerOpen,
|
||||
@ -177,16 +136,11 @@ export class AdminInterface extends AuthenticatedInterface {
|
||||
|
||||
return html` <ak-locale-context>
|
||||
<div class="pf-c-page">
|
||||
<ak-page-navbar>
|
||||
<ak-version-banner></ak-version-banner>
|
||||
<ak-enterprise-status interface="admin"></ak-enterprise-status>
|
||||
</ak-page-navbar>
|
||||
|
||||
<ak-enterprise-status interface="admin"></ak-enterprise-status>
|
||||
<ak-version-banner></ak-version-banner>
|
||||
<ak-admin-sidebar
|
||||
class="pf-c-page__sidebar
|
||||
${classMap(sidebarClasses)}"
|
||||
class="pf-c-page__sidebar ${classMap(sidebarClasses)}"
|
||||
></ak-admin-sidebar>
|
||||
|
||||
<div class="pf-c-page__drawer">
|
||||
<div class="pf-c-drawer ${classMap(drawerClasses)}">
|
||||
<div class="pf-c-drawer__main">
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
|
||||
import { me } from "@goauthentik/common/users";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import {
|
||||
@ -30,9 +31,16 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement
|
||||
me().then((user: SessionUser) => {
|
||||
this.impersonation = user.original ? user.user.username : null;
|
||||
});
|
||||
this.toggleOpen = this.toggleOpen.bind(this);
|
||||
this.checkWidth = this.checkWidth.bind(this);
|
||||
}
|
||||
|
||||
// This has to be a bound method so the event listener can be removed on disconnection as
|
||||
// needed.
|
||||
toggleOpen() {
|
||||
this.open = !this.open;
|
||||
}
|
||||
|
||||
checkWidth() {
|
||||
// This works just fine, but it assumes that the `--ak-sidebar--minimum-auto-width` is in
|
||||
// REMs. If that changes, this code will have to be adjusted as well.
|
||||
@ -44,7 +52,7 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.toggleOpen);
|
||||
window.addEventListener("resize", this.checkWidth);
|
||||
// After connecting to the DOM, we can now perform this check to see if the sidebar should
|
||||
// be open by default.
|
||||
@ -55,6 +63,7 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement
|
||||
// connection, and removing them before disconnection.
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.toggleOpen);
|
||||
window.removeEventListener("resize", this.checkWidth);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
@ -62,9 +71,8 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement
|
||||
render() {
|
||||
return html`
|
||||
<ak-sidebar
|
||||
class="pf-c-page__sidebar
|
||||
${this.open ? "pf-m-expanded" : "pf-m-collapsed"} ${this.activeTheme ===
|
||||
UiThemeEnum.Light
|
||||
class="pf-c-page__sidebar ${this.open ? "pf-m-expanded" : "pf-m-collapsed"} ${this
|
||||
.activeTheme === UiThemeEnum.Light
|
||||
? "pf-m-light"
|
||||
: ""}"
|
||||
>
|
||||
@ -73,6 +81,19 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement
|
||||
`;
|
||||
}
|
||||
|
||||
updated() {
|
||||
// This is permissible as`:host.classList` is not one of the properties Lit uses as a
|
||||
// scheduling trigger. This sort of shenanigans can trigger an loop, in that it will trigger
|
||||
// a browser reflow, which may trigger some other styling the application is monitoring,
|
||||
// triggering a re-render which triggers a browser reflow, ad infinitum. But we've been
|
||||
// living with that since jQuery, and it's both well-known and fortunately rare.
|
||||
|
||||
// eslint-disable-next-line wc/no-self-class
|
||||
this.classList.remove("pf-m-expanded", "pf-m-collapsed");
|
||||
// eslint-disable-next-line wc/no-self-class
|
||||
this.classList.add(this.open ? "pf-m-expanded" : "pf-m-collapsed");
|
||||
}
|
||||
|
||||
renderSidebarItems(): TemplateResult {
|
||||
// The second attribute type is of string[] to help with the 'activeWhen' control, which was
|
||||
// commonplace and singular enough to merit its own handler.
|
||||
|
||||
@ -94,13 +94,10 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
const username = this.user?.user.name || this.user?.user.username;
|
||||
const name = this.user?.user.name ?? this.user?.user.username;
|
||||
|
||||
return html` <ak-page-header
|
||||
header=${msg(str`Welcome, ${username || ""}.`)}
|
||||
description=${msg("General system status")}
|
||||
?hasIcon=${false}
|
||||
>
|
||||
return html`<ak-page-header description=${msg("General system status")} ?hasIcon=${false}>
|
||||
<span slot="header"> ${msg(str`Welcome, ${name || ""}.`)} </span>
|
||||
</ak-page-header>
|
||||
<section class="pf-c-page__main-section">
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
|
||||
@ -83,10 +83,13 @@ export class AdminSettingsPage extends AKElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.settings) return nothing;
|
||||
|
||||
if (!this.settings) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ak-page-header icon="fa fa-cog" header="${msg("System settings")}"> </ak-page-header>
|
||||
<ak-page-header icon="fa fa-cog" header="" description="">
|
||||
<span slot="header"> ${msg("System settings")} </span>
|
||||
</ak-page-header>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter">
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__body">
|
||||
|
||||
@ -4,8 +4,12 @@ import {
|
||||
EventMiddleware,
|
||||
LoggingMiddleware,
|
||||
} from "@goauthentik/common/api/middleware";
|
||||
import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants";
|
||||
import { VERSION } from "@goauthentik/common/constants";
|
||||
import { globalAK } from "@goauthentik/common/global";
|
||||
import {
|
||||
EVENT_LOCALE_REQUEST,
|
||||
LocaleContextEventDetail,
|
||||
} from "@goauthentik/elements/ak-locale-context/events.js";
|
||||
|
||||
import { Config, Configuration, CoreApi, CurrentBrand, RootApi } from "@goauthentik/api";
|
||||
|
||||
@ -44,7 +48,7 @@ export function brandSetLocale(brand: CurrentBrand) {
|
||||
}
|
||||
console.debug("authentik/locale: setting locale from brand default");
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(EVENT_LOCALE_REQUEST, {
|
||||
new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, {
|
||||
composed: true,
|
||||
bubbles: true,
|
||||
detail: { locale: brand.defaultLocale },
|
||||
|
||||
@ -14,8 +14,6 @@ export const EVENT_FLOW_INSPECTOR_TOGGLE = "ak-flow-inspector-toggle";
|
||||
export const EVENT_SIDEBAR_TOGGLE = "ak-sidebar-toggle";
|
||||
export const EVENT_WS_MESSAGE = "ak-ws-message";
|
||||
export const EVENT_FLOW_ADVANCE = "ak-flow-advance";
|
||||
export const EVENT_LOCALE_CHANGE = "ak-locale-change";
|
||||
export const EVENT_LOCALE_REQUEST = "ak-locale-request";
|
||||
export const EVENT_REQUEST_POST = "ak-request-post";
|
||||
export const EVENT_MESSAGE = "ak-message";
|
||||
export const EVENT_THEME_CHANGE = "ak-theme-change";
|
||||
|
||||
@ -17,13 +17,6 @@
|
||||
|
||||
/* Minimum width after which the sidebar becomes automatic */
|
||||
--ak-sidebar--minimum-auto-width: 80rem;
|
||||
|
||||
/**
|
||||
* The height of the navbar and branded sidebar.
|
||||
* @todo This shouldn't be necessary. The sidebar can instead use a grid layout
|
||||
* ensuring they share the same height.
|
||||
*/
|
||||
--ak-navbar--height: 7rem;
|
||||
}
|
||||
|
||||
@supports selector(::-webkit-scrollbar) {
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
|
||||
import { isResponseErrorLike } from "@goauthentik/common/errors/network";
|
||||
import {
|
||||
EVENT_LOCALE_REQUEST,
|
||||
LocaleContextEventDetail,
|
||||
} from "@goauthentik/elements/ak-locale-context/events.js";
|
||||
|
||||
import { CoreApi, SessionUser } from "@goauthentik/api";
|
||||
|
||||
@ -57,7 +60,7 @@ export async function me(): Promise<SessionUser> {
|
||||
console.debug(`authentik/locale: Activating user's configured locale '${locale}'`);
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(EVENT_LOCALE_REQUEST, {
|
||||
new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, {
|
||||
composed: true,
|
||||
bubbles: true,
|
||||
detail: { locale },
|
||||
|
||||
@ -67,12 +67,6 @@ export class NavigationButtons extends AKElement {
|
||||
:host([theme="light"]) .pf-c-page__header-tools-group .pf-c-button {
|
||||
color: var(--ak-global--Color--100) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pf-c-avatar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
@ -162,7 +156,9 @@ export class NavigationButtons extends AKElement {
|
||||
}
|
||||
|
||||
renderImpersonation() {
|
||||
if (!this.me?.original) return nothing;
|
||||
if (!this.me?.original) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const onClick = async () => {
|
||||
await new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve();
|
||||
@ -179,14 +175,6 @@ export class NavigationButtons extends AKElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
renderAvatar() {
|
||||
return html`<img
|
||||
class="pf-c-avatar"
|
||||
src=${ifDefined(this.me?.user.avatar)}
|
||||
alt="${msg("Avatar image")}"
|
||||
/>`;
|
||||
}
|
||||
|
||||
get userDisplayName() {
|
||||
return match<UserDisplay | undefined, string | undefined>(this.uiConfig?.navbar.userDisplay)
|
||||
.with(UserDisplay.username, () => this.me?.user.username)
|
||||
@ -224,7 +212,11 @@ export class NavigationButtons extends AKElement {
|
||||
</div>
|
||||
</div>`
|
||||
: nothing}
|
||||
${this.renderAvatar()}
|
||||
<img
|
||||
class="pf-c-avatar"
|
||||
src=${ifDefined(this.me?.user.avatar)}
|
||||
alt="${msg("Avatar image")}"
|
||||
/>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,18 +10,15 @@ import { me } from "@goauthentik/common/users";
|
||||
import "@goauthentik/components/ak-nav-buttons";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
||||
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
|
||||
import { themeImage } from "@goauthentik/elements/utils/images";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, LitElement, TemplateResult, css, html, nothing } from "lit";
|
||||
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
|
||||
import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css";
|
||||
import PFNotificationBadge from "@patternfly/patternfly/components/NotificationBadge/notification-badge.css";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
@ -29,52 +26,34 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { SessionUser } from "@goauthentik/api";
|
||||
|
||||
//#region Page Navbar
|
||||
|
||||
export interface PageNavbarDetails {
|
||||
header?: string;
|
||||
description?: string;
|
||||
@customElement("ak-page-header")
|
||||
export class PageHeader extends WithBrandConfig(AKElement) {
|
||||
@property()
|
||||
icon?: string;
|
||||
iconImage?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A global navbar component at the top of the page.
|
||||
*
|
||||
* Internally, this component listens for the `ak-page-header` event, which is
|
||||
* dispatched by the `ak-page-header` component.
|
||||
*/
|
||||
@customElement("ak-page-navbar")
|
||||
export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavbarDetails {
|
||||
//#region Static Properties
|
||||
@property({ type: Boolean })
|
||||
iconImage = false;
|
||||
|
||||
private static elementRef: AKPageNavbar | null = null;
|
||||
@property()
|
||||
header = "";
|
||||
|
||||
static readonly setNavbarDetails = (detail: Partial<PageNavbarDetails>): void => {
|
||||
const { elementRef } = AKPageNavbar;
|
||||
if (!elementRef) {
|
||||
console.debug(
|
||||
`ak-page-header: Could not find ak-page-navbar, skipping event dispatch.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@property()
|
||||
description?: string;
|
||||
|
||||
const { header, description, icon, iconImage } = detail;
|
||||
@property({ type: Boolean })
|
||||
hasIcon = true;
|
||||
|
||||
elementRef.header = header;
|
||||
elementRef.description = description;
|
||||
elementRef.icon = icon;
|
||||
elementRef.iconImage = iconImage || false;
|
||||
elementRef.hasIcon = !!icon;
|
||||
};
|
||||
@state()
|
||||
me?: SessionUser;
|
||||
|
||||
@state()
|
||||
uiConfig!: UIConfig;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
PFBase,
|
||||
PFButton,
|
||||
PFPage,
|
||||
PFDrawer,
|
||||
|
||||
PFNotificationBadge,
|
||||
PFContent,
|
||||
PFAvatar,
|
||||
@ -84,212 +63,55 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: var(--pf-global--ZIndex--lg);
|
||||
--pf-c-page__header-tools--MarginRight: 0;
|
||||
--ak-brand-logo-height: var(--pf-global--FontSize--4xl, 2.25rem);
|
||||
--ak-brand-background-color: var(
|
||||
--pf-c-page__sidebar--m-light--BackgroundColor
|
||||
);
|
||||
}
|
||||
|
||||
:host([theme="dark"]) {
|
||||
--ak-brand-background-color: var(--pf-c-page__sidebar--BackgroundColor);
|
||||
--pf-c-page__sidebar--BackgroundColor: var(--ak-dark-background-light);
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
navbar {
|
||||
.bar {
|
||||
border-bottom: var(--pf-global--BorderWidth--sm);
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: var(--pf-global--BorderColor--100);
|
||||
background-color: var(--pf-c-page--BackgroundColor);
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: 6rem;
|
||||
|
||||
display: grid;
|
||||
row-gap: var(--pf-global--spacer--sm);
|
||||
column-gap: var(--pf-global--spacer--sm);
|
||||
grid-template-columns: [brand] auto [toggle] auto [primary] 1fr [secondary] auto;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-areas:
|
||||
"brand toggle primary secondary"
|
||||
"brand toggle description secondary";
|
||||
|
||||
@media (max-width: 768px) {
|
||||
row-gap: var(--pf-global--spacer--xs);
|
||||
|
||||
align-items: center;
|
||||
grid-template-areas:
|
||||
"toggle primary secondary"
|
||||
"toggle description description";
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
min-height: 114px;
|
||||
max-height: 114px;
|
||||
background-color: var(--pf-c-page--BackgroundColor);
|
||||
}
|
||||
|
||||
.items {
|
||||
display: block;
|
||||
|
||||
&.primary {
|
||||
grid-column: primary;
|
||||
grid-row: primary / description;
|
||||
|
||||
align-content: center;
|
||||
padding-block: var(--pf-global--spacer--md);
|
||||
|
||||
@media (min-width: 426px) {
|
||||
&.block-sibling {
|
||||
padding-block-end: 0;
|
||||
grid-row: primary;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding-block: var(--pf-global--spacer--sm);
|
||||
}
|
||||
|
||||
.accent-icon {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.page-description {
|
||||
grid-area: description;
|
||||
padding-block-end: var(--pf-global--spacer--md);
|
||||
|
||||
@media (max-width: 425px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
grid-area: secondary;
|
||||
flex: 0 0 auto;
|
||||
justify-self: end;
|
||||
padding-block: var(--pf-global--spacer--sm);
|
||||
padding-inline-end: var(--pf-global--spacer--sm);
|
||||
|
||||
@media (min-width: 769px) {
|
||||
align-content: center;
|
||||
padding-block: var(--pf-global--spacer--md);
|
||||
padding-inline-end: var(--pf-global--spacer--xl);
|
||||
}
|
||||
}
|
||||
.pf-c-page__main-section.pf-m-light {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.brand {
|
||||
grid-area: brand;
|
||||
background-color: var(--ak-brand-background-color);
|
||||
height: 100%;
|
||||
width: var(--pf-c-page__sidebar--Width);
|
||||
align-items: center;
|
||||
padding-inline: var(--pf-global--spacer--sm);
|
||||
|
||||
.pf-c-page__main-section {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
&.pf-m-collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1279px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-trigger {
|
||||
grid-area: toggle;
|
||||
height: 100%;
|
||||
img.pf-icon {
|
||||
max-height: 24px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
flex: 0 0 auto;
|
||||
height: var(--ak-brand-logo-height);
|
||||
|
||||
& img {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-trigger,
|
||||
.notification-trigger {
|
||||
font-size: 1.5rem;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.notification-trigger.has-notifications {
|
||||
color: var(--pf-global--active-color--100);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: flex;
|
||||
gap: var(--pf-global--spacer--xs);
|
||||
}
|
||||
|
||||
h1 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center !important;
|
||||
}
|
||||
.pf-c-page__header-tools {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.pf-c-page__header-tools-group {
|
||||
height: 100%;
|
||||
}
|
||||
:host([theme="dark"]) .pf-c-page__header-tools {
|
||||
color: var(--ak-dark-foreground) !important;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Properties
|
||||
|
||||
@property({ type: String })
|
||||
icon?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
iconImage = false;
|
||||
|
||||
@property({ type: String })
|
||||
header?: string;
|
||||
|
||||
@property({ type: String })
|
||||
description?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
hasIcon = true;
|
||||
|
||||
@property({ type: Boolean })
|
||||
open = true;
|
||||
|
||||
@state()
|
||||
me?: SessionUser;
|
||||
|
||||
@state()
|
||||
uiConfig!: UIConfig;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#endregion
|
||||
//#region Methods
|
||||
|
||||
#toggleSidebar() {
|
||||
this.open = !this.open;
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
//#region Constructor
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
window.addEventListener(EVENT_WS_MESSAGE, () => {
|
||||
@ -297,23 +119,13 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb
|
||||
});
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
AKPageNavbar.elementRef = this;
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
AKPageNavbar.elementRef = null;
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
async firstUpdated() {
|
||||
this.me = await me();
|
||||
this.uiConfig = await uiConfig();
|
||||
this.uiConfig.navbar.userDisplay = UserDisplay.none;
|
||||
}
|
||||
|
||||
#setTitle(header?: string) {
|
||||
setTitle(header?: string) {
|
||||
const currentIf = currentInterface();
|
||||
let title = this.brand?.brandingTitle || TITLE_DEFAULT;
|
||||
if (currentIf === "admin") {
|
||||
@ -329,146 +141,65 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb
|
||||
willUpdate() {
|
||||
// Always update title, even if there's no header value set,
|
||||
// as in that case we still need to return to the generic title
|
||||
this.#setTitle(this.header);
|
||||
this.setTitle(this.header);
|
||||
}
|
||||
|
||||
//#region Render
|
||||
|
||||
renderIcon() {
|
||||
if (this.icon) {
|
||||
if (this.iconImage && !this.icon.startsWith("fa://")) {
|
||||
return html`<img class="accent-icon pf-icon" src="${this.icon}" alt="page icon" />`;
|
||||
return html`<img class="pf-icon" src="${this.icon}" alt="page icon" />`;
|
||||
}
|
||||
|
||||
const icon = this.icon.replaceAll("fa://", "fa ");
|
||||
|
||||
return html`<i class="accent-icon ${icon}"></i>`;
|
||||
return html`<i class=${icon}></i>`;
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<navbar aria-label="Main" class="navbar">
|
||||
<aside class="brand ${this.open ? "" : "pf-m-collapsed"}">
|
||||
<a href="#/">
|
||||
<div class="logo">
|
||||
<img
|
||||
src=${themeImage(
|
||||
this.brand?.brandingLogo ?? DefaultBrand.brandingLogo,
|
||||
)}
|
||||
alt="${msg("authentik Logo")}"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</aside>
|
||||
<button
|
||||
class="sidebar-trigger pf-c-button pf-m-plain"
|
||||
@click=${this.#toggleSidebar}
|
||||
aria-label=${msg("Toggle sidebar")}
|
||||
aria-expanded=${this.open ? "true" : "false"}
|
||||
>
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
|
||||
<section
|
||||
class="items primary pf-c-content ${this.description ? "block-sibling" : ""}"
|
||||
>
|
||||
<h1 class="page-title">
|
||||
return html`<div class="bar">
|
||||
<button
|
||||
class="sidebar-trigger pf-c-button pf-m-plain"
|
||||
@click=${() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
${this.hasIcon
|
||||
? html`<slot name="icon">${this.renderIcon()}</slot>`
|
||||
? html`<slot name="icon">${this.renderIcon()}</slot> `
|
||||
: nothing}
|
||||
${this.header}
|
||||
<slot name="header">${this.header}</slot>
|
||||
</h1>
|
||||
</section>
|
||||
${this.description
|
||||
? html`<section class="items page-description pf-c-content">
|
||||
<p>${this.description}</p>
|
||||
</section>`
|
||||
: nothing}
|
||||
|
||||
<section class="items secondary">
|
||||
<div class="pf-c-page__header-tools-group">
|
||||
<ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.me}>
|
||||
<a
|
||||
class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md"
|
||||
href="${globalAK().api.base}if/user/"
|
||||
slot="extra"
|
||||
>
|
||||
${msg("User interface")}
|
||||
</a>
|
||||
</ak-nav-buttons>
|
||||
</div>
|
||||
</section>
|
||||
</navbar>
|
||||
<slot></slot>`;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Page Header
|
||||
|
||||
/**
|
||||
* A page header component, used to display the page title and description.
|
||||
*
|
||||
* Internally, this component dispatches the `ak-page-header` event, which is
|
||||
* listened to by the `ak-page-navbar` component.
|
||||
*
|
||||
* @singleton
|
||||
*/
|
||||
@customElement("ak-page-header")
|
||||
export class AKPageHeader extends LitElement implements PageNavbarDetails {
|
||||
@property({ type: String })
|
||||
header?: string;
|
||||
|
||||
@property({ type: String })
|
||||
description?: string;
|
||||
|
||||
@property({ type: String })
|
||||
icon?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
iconImage = false;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
css`
|
||||
:host {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
AKPageNavbar.setNavbarDetails({
|
||||
header: this.header,
|
||||
description: this.description,
|
||||
icon: this.icon,
|
||||
iconImage: this.iconImage,
|
||||
});
|
||||
}
|
||||
|
||||
updated(): void {
|
||||
AKPageNavbar.setNavbarDetails({
|
||||
header: this.header,
|
||||
description: this.description,
|
||||
icon: this.icon,
|
||||
iconImage: this.iconImage,
|
||||
});
|
||||
${this.description ? html`<p>${this.description}</p>` : html``}
|
||||
</div>
|
||||
</section>
|
||||
<div class="pf-c-page__header-tools">
|
||||
<div class="pf-c-page__header-tools-group">
|
||||
<ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.me}>
|
||||
<a
|
||||
class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md"
|
||||
href="${globalAK().api.base}if/user/"
|
||||
slot="extra"
|
||||
>
|
||||
${msg("User interface")}
|
||||
</a>
|
||||
</ak-nav-buttons>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-page-header": AKPageHeader;
|
||||
"ak-page-navbar": AKPageNavbar;
|
||||
"ak-page-header": PageHeader;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
|
||||
import { customEvent } from "@goauthentik/elements/utils/customEvents";
|
||||
|
||||
import { localized, msg } from "@lit/localize";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import "./ak-locale-context";
|
||||
import { EVENT_LOCALE_REQUEST, LocaleContextEventDetail } from "./events.js";
|
||||
|
||||
export default {
|
||||
title: "Elements / Shell / Locale Context",
|
||||
@ -37,10 +35,18 @@ export const InFrench = () =>
|
||||
</div>`;
|
||||
|
||||
export const SwitchingBackAndForth = () => {
|
||||
let lang = "en";
|
||||
let languageCode = "en";
|
||||
|
||||
window.setInterval(() => {
|
||||
lang = lang === "en" ? "fr" : "en";
|
||||
window.dispatchEvent(customEvent(EVENT_LOCALE_REQUEST, { locale: lang }));
|
||||
languageCode = languageCode === "en" ? "fr" : "en";
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, {
|
||||
composed: true,
|
||||
bubbles: true,
|
||||
detail: { locale: languageCode },
|
||||
}),
|
||||
);
|
||||
}, 1000);
|
||||
|
||||
return html`<div style="background: #fff; padding: 4em">
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { customEvent } from "@goauthentik/elements/utils/customEvents";
|
||||
|
||||
import { html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import { WithBrandConfig } from "../Interface/brandProvider";
|
||||
import { initializeLocalization } from "./configureLocale";
|
||||
import type { LocaleGetter, LocaleSetter } from "./configureLocale";
|
||||
import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers";
|
||||
import { initializeLocalization } from "./configureLocale.js";
|
||||
import type { GetLocale, SetLocale } from "./configureLocale.js";
|
||||
import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST, LocaleContextEventDetail } from "./events.js";
|
||||
import { DEFAULT_LOCALE, autoDetectLanguage, findLocaleDefinition } from "./helpers.js";
|
||||
|
||||
/**
|
||||
* A component to manage your locale settings.
|
||||
*
|
||||
* ## Details
|
||||
* @remarks
|
||||
*
|
||||
* This component exists to take a locale setting from several different places, find the
|
||||
* appropriate locale file in our catalog of locales, and set the lit-localization context
|
||||
@ -25,70 +24,98 @@ import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helper
|
||||
*/
|
||||
@customElement("ak-locale-context")
|
||||
export class LocaleContext extends WithBrandConfig(AKElement) {
|
||||
/// @attribute The text representation of the current locale */
|
||||
protected static singleton: LocaleContext | null = null;
|
||||
|
||||
/**
|
||||
* The text representation of the current locale
|
||||
* @attribute
|
||||
*/
|
||||
@property({ attribute: true, type: String })
|
||||
locale = DEFAULT_LOCALE;
|
||||
public locale = DEFAULT_LOCALE;
|
||||
|
||||
/// @attribute The URL parameter to look for (if any)
|
||||
/**
|
||||
* The URL parameter to look for (if any)
|
||||
* @attribute
|
||||
*/
|
||||
@property({ attribute: true, type: String })
|
||||
param = "locale";
|
||||
public param = "locale";
|
||||
|
||||
getLocale: LocaleGetter;
|
||||
|
||||
setLocale: LocaleSetter;
|
||||
protected readonly getLocale: GetLocale;
|
||||
protected readonly setLocale: SetLocale;
|
||||
|
||||
constructor(code = DEFAULT_LOCALE) {
|
||||
super();
|
||||
this.notifyApplication = this.notifyApplication.bind(this);
|
||||
this.updateLocaleHandler = this.updateLocaleHandler.bind(this);
|
||||
try {
|
||||
const [getLocale, setLocale] = initializeLocalization();
|
||||
this.getLocale = getLocale;
|
||||
this.setLocale = setLocale;
|
||||
this.setLocale(code).then(() => {
|
||||
window.setTimeout(this.notifyApplication, 0);
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(`Developer error: Must have only one locale context per session: ${e}`);
|
||||
|
||||
if (LocaleContext.singleton) {
|
||||
throw new Error(`Developer error: Must have only one locale context per session`);
|
||||
}
|
||||
|
||||
LocaleContext.singleton = this;
|
||||
|
||||
const [getLocale, setLocale] = initializeLocalization();
|
||||
|
||||
this.getLocale = getLocale;
|
||||
this.setLocale = setLocale;
|
||||
|
||||
this.setLocale(code).then(this.#notifyApplication);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.updateLocale();
|
||||
window.addEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler as EventListener);
|
||||
this.#updateLocale();
|
||||
|
||||
window.addEventListener(EVENT_LOCALE_REQUEST, this.#localeUpdateListener as EventListener);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler as EventListener);
|
||||
LocaleContext.singleton = null;
|
||||
|
||||
window.removeEventListener(
|
||||
EVENT_LOCALE_REQUEST,
|
||||
this.#localeUpdateListener as EventListener,
|
||||
);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
updateLocaleHandler(ev: CustomEvent<{ locale: string }>) {
|
||||
#localeUpdateListener = (ev: CustomEvent<LocaleContextEventDetail>) => {
|
||||
console.debug("authentik/locale: Locale update request received.");
|
||||
this.updateLocale(ev.detail.locale);
|
||||
}
|
||||
this.#updateLocale(ev.detail.locale);
|
||||
};
|
||||
|
||||
#updateLocale(requestedLanguageCode?: string) {
|
||||
const localeRequest = autoDetectLanguage(requestedLanguageCode, this.brand?.defaultLocale);
|
||||
|
||||
const locale = findLocaleDefinition(localeRequest);
|
||||
|
||||
updateLocale(requestedLocale: string | undefined = undefined) {
|
||||
const localeRequest = autoDetectLanguage(requestedLocale, this.brand?.defaultLocale);
|
||||
const locale = getBestMatchLocale(localeRequest);
|
||||
if (!locale) {
|
||||
console.warn(`authentik/locale: failed to find locale for code ${localeRequest}`);
|
||||
return;
|
||||
}
|
||||
locale.locale().then(() => {
|
||||
console.debug(`authentik/locale: Setting Locale to ${locale.label()} (${locale.code})`);
|
||||
this.setLocale(locale.code).then(() => {
|
||||
window.setTimeout(this.notifyApplication, 0);
|
||||
});
|
||||
|
||||
return locale.fetch().then(() => {
|
||||
console.debug(
|
||||
`authentik/locale: Setting Locale to ${locale.formatLabel()} (${locale.languageCode})`,
|
||||
);
|
||||
|
||||
this.setLocale(locale.languageCode).then(this.#notifyApplication);
|
||||
});
|
||||
}
|
||||
|
||||
notifyApplication() {
|
||||
// You will almost never have cause to catch this event. Lit's own `@localized()` decorator
|
||||
// works just fine for almost every use case.
|
||||
this.dispatchEvent(customEvent(EVENT_LOCALE_CHANGE));
|
||||
}
|
||||
#notifyFrameID = -1;
|
||||
|
||||
#notifyApplication = () => {
|
||||
cancelAnimationFrame(this.#notifyFrameID);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
// You will almost never have cause to catch this event.
|
||||
// Lit's own `@localized()` decorator works just fine for almost every use case.
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_LOCALE_CHANGE, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`<slot></slot>`;
|
||||
|
||||
@ -1,39 +1,44 @@
|
||||
import { configureLocalization } from "@lit/localize";
|
||||
|
||||
import { sourceLocale, targetLocales } from "../../locale-codes";
|
||||
import { getBestMatchLocale } from "./helpers";
|
||||
import { sourceLocale, targetLocales } from "../../locale-codes.js";
|
||||
import { findLocaleDefinition } from "./helpers.js";
|
||||
|
||||
type LocaleGetter = ReturnType<typeof configureLocalization>["getLocale"];
|
||||
type LocaleSetter = ReturnType<typeof configureLocalization>["setLocale"];
|
||||
export type ConfigureLocalizationResult = ReturnType<typeof configureLocalization>;
|
||||
|
||||
// Internal use only.
|
||||
//
|
||||
// This is where the lit-localization module is initialized with our loader, which associates our
|
||||
// collection of locales with its getter and setter functions.
|
||||
export type GetLocale = ConfigureLocalizationResult["getLocale"];
|
||||
export type SetLocale = ConfigureLocalizationResult["setLocale"];
|
||||
|
||||
let getLocale: LocaleGetter | undefined = undefined;
|
||||
let setLocale: LocaleSetter | undefined = undefined;
|
||||
export type LocaleState = [GetLocale, SetLocale];
|
||||
|
||||
export function initializeLocalization(): [LocaleGetter, LocaleSetter] {
|
||||
if (getLocale && setLocale) {
|
||||
return [getLocale, setLocale];
|
||||
}
|
||||
let cachedLocaleState: LocaleState | undefined = undefined;
|
||||
|
||||
({ getLocale, setLocale } = configureLocalization({
|
||||
/**
|
||||
* This is where the lit-localization module is initialized with our loader,
|
||||
* which associates our collection of locales with its getter and setter functions.
|
||||
*
|
||||
* @returns A tuple of getter and setter functions.
|
||||
* @internal
|
||||
*/
|
||||
export function initializeLocalization(): LocaleState {
|
||||
if (cachedLocaleState) return cachedLocaleState;
|
||||
|
||||
const { getLocale, setLocale } = configureLocalization({
|
||||
sourceLocale,
|
||||
targetLocales,
|
||||
loadLocale: async (locale: string) => {
|
||||
const localeDef = getBestMatchLocale(locale);
|
||||
if (!localeDef) {
|
||||
console.warn(`Unrecognized locale: ${localeDef}`);
|
||||
return Promise.reject("");
|
||||
}
|
||||
return localeDef.locale();
|
||||
},
|
||||
}));
|
||||
loadLocale: (languageCode) => {
|
||||
const localeDef = findLocaleDefinition(languageCode);
|
||||
|
||||
return [getLocale, setLocale];
|
||||
if (!localeDef) {
|
||||
throw new Error(`Unrecognized locale: ${localeDef}`);
|
||||
}
|
||||
|
||||
return localeDef.fetch();
|
||||
},
|
||||
});
|
||||
|
||||
cachedLocaleState = [getLocale, setLocale];
|
||||
|
||||
return cachedLocaleState;
|
||||
}
|
||||
|
||||
export default initializeLocalization;
|
||||
export type { LocaleGetter, LocaleSetter };
|
||||
|
||||
@ -1,15 +1,19 @@
|
||||
import * as _enLocale from "@goauthentik/locales/en";
|
||||
import * as EnglishLocaleModule from "@goauthentik/locales/en";
|
||||
|
||||
import type { LocaleModule } from "@lit/localize";
|
||||
import { msg } from "@lit/localize";
|
||||
|
||||
import { AkLocale, LocaleRow } from "./types";
|
||||
import { AKLocaleDefinition, LocaleRow } from "./types.js";
|
||||
|
||||
export const DEFAULT_FALLBACK = "en";
|
||||
/**
|
||||
* The default ISO 639-1 language code.
|
||||
*/
|
||||
export const DEFAULT_LANGUAGE_CODE = "en";
|
||||
|
||||
const enLocale: LocaleModule = _enLocale;
|
||||
|
||||
export { enLocale };
|
||||
/**
|
||||
* The default English locale module.
|
||||
*/
|
||||
export const DefaultLocaleModule: LocaleModule = EnglishLocaleModule;
|
||||
|
||||
// NOTE: This table cannot be made any shorter, despite all the repetition of syntax. Bundlers look
|
||||
// for the `await import` string as a *string target* for doing alias substitution, so putting
|
||||
@ -35,34 +39,44 @@ export { enLocale };
|
||||
// - Text Label
|
||||
// - Locale loader.
|
||||
|
||||
// prettier-ignore
|
||||
const debug: LocaleRow = [
|
||||
"pseudo-LOCALE", /^pseudo/i, () => msg("Pseudolocale (for testing)"), async () => await import("@goauthentik/locales/pseudo-LOCALE"),
|
||||
"pseudo-LOCALE",
|
||||
/^pseudo/i,
|
||||
() => msg("Pseudolocale (for testing)"),
|
||||
() => import("@goauthentik/locales/pseudo-LOCALE"),
|
||||
];
|
||||
|
||||
// prettier-ignore
|
||||
const LOCALE_TABLE: LocaleRow[] = [
|
||||
["de", /^de([_-]|$)/i, () => msg("German"), async () => await import("@goauthentik/locales/de")],
|
||||
["en", /^en([_-]|$)/i, () => msg("English"), async () => await import("@goauthentik/locales/en")],
|
||||
["es", /^es([_-]|$)/i, () => msg("Spanish"), async () => await import("@goauthentik/locales/es")],
|
||||
["fr", /^fr([_-]|$)/i, () => msg("French"), async () => await import("@goauthentik/locales/fr")],
|
||||
["it", /^it([_-]|$)/i, () => msg("Italian"), async () => await import("@goauthentik/locales/it")],
|
||||
["ko", /^ko([_-]|$)/i, () => msg("Korean"), async () => await import("@goauthentik/locales/ko")],
|
||||
["nl", /^nl([_-]|$)/i, () => msg("Dutch"), async () => await import("@goauthentik/locales/nl")],
|
||||
["pl", /^pl([_-]|$)/i, () => msg("Polish"), async () => await import("@goauthentik/locales/pl")],
|
||||
["ru", /^ru([_-]|$)/i, () => msg("Russian"), async () => await import("@goauthentik/locales/ru")],
|
||||
["tr", /^tr([_-]|$)/i, () => msg("Turkish"), async () => await import("@goauthentik/locales/tr")],
|
||||
["zh_TW", /^zh[_-]TW$/i, () => msg("Taiwanese Mandarin"), async () => await import("@goauthentik/locales/zh_TW")],
|
||||
["zh-Hans", /^zh(\b|_)/i, () => msg("Chinese (simplified)"), async () => await import("@goauthentik/locales/zh-Hans")],
|
||||
["zh-Hant", /^zh[_-](HK|Hant)/i, () => msg("Chinese (traditional)"), async () => await import("@goauthentik/locales/zh-Hant")],
|
||||
debug
|
||||
const LOCALE_TABLE: readonly LocaleRow[] = [
|
||||
// English loaded when the application is first instantiated.
|
||||
["en", /^en([_-]|$)/i, () => msg("English"), () => Promise.resolve(DefaultLocaleModule)],
|
||||
["de", /^de([_-]|$)/i, () => msg("German"), () => import("@goauthentik/locales/de")],
|
||||
["es", /^es([_-]|$)/i, () => msg("Spanish"), () => import("@goauthentik/locales/es")],
|
||||
["fr", /^fr([_-]|$)/i, () => msg("French"), () => import("@goauthentik/locales/fr")],
|
||||
["it", /^it([_-]|$)/i, () => msg("Italian"), () => import("@goauthentik/locales/it")],
|
||||
["ko", /^ko([_-]|$)/i, () => msg("Korean"), () => import("@goauthentik/locales/ko")],
|
||||
["nl", /^nl([_-]|$)/i, () => msg("Dutch"), () => import("@goauthentik/locales/nl")],
|
||||
["pl", /^pl([_-]|$)/i, () => msg("Polish"), () => import("@goauthentik/locales/pl")],
|
||||
["ru", /^ru([_-]|$)/i, () => msg("Russian"), () => import("@goauthentik/locales/ru")],
|
||||
["tr", /^tr([_-]|$)/i, () => msg("Turkish"), () => import("@goauthentik/locales/tr")],
|
||||
["zh_TW", /^zh[_-]TW$/i, () => msg("Taiwanese Mandarin"), () => import("@goauthentik/locales/zh_TW")],
|
||||
["zh-Hans", /^zh(\b|_)/i, () => msg("Chinese (simplified)"), () => import("@goauthentik/locales/zh-Hans")],
|
||||
["zh-Hant", /^zh[_-](HK|Hant)/i, () => msg("Chinese (traditional)"), () => import("@goauthentik/locales/zh-Hant")],
|
||||
debug,
|
||||
];
|
||||
|
||||
export const LOCALES: AkLocale[] = LOCALE_TABLE.map(([code, match, label, locale]) => ({
|
||||
code,
|
||||
match,
|
||||
label,
|
||||
locale,
|
||||
}));
|
||||
/**
|
||||
* Available locales, identified by their ISO 639-1 language code.
|
||||
*/
|
||||
export const AKLocalDefinitions: readonly AKLocaleDefinition[] = LOCALE_TABLE.map(
|
||||
([languageCode, pattern, formatLabel, fetch]) => {
|
||||
return {
|
||||
languageCode,
|
||||
pattern,
|
||||
formatLabel,
|
||||
fetch,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export default LOCALES;
|
||||
export default AKLocalDefinitions;
|
||||
|
||||
6
web/src/elements/ak-locale-context/events.ts
Normal file
6
web/src/elements/ak-locale-context/events.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export const EVENT_LOCALE_REQUEST = "ak-locale-request";
|
||||
export const EVENT_LOCALE_CHANGE = "ak-locale-change";
|
||||
|
||||
export interface LocaleContextEventDetail {
|
||||
locale: string;
|
||||
}
|
||||
@ -1,59 +1,80 @@
|
||||
import { globalAK } from "@goauthentik/common/global";
|
||||
|
||||
import { LOCALES as RAW_LOCALES, enLocale } from "./definitions";
|
||||
import { AkLocale } from "./types";
|
||||
import { AKLocalDefinitions } from "./definitions.js";
|
||||
import { AKLocaleDefinition } from "./types.js";
|
||||
|
||||
export const DEFAULT_LOCALE = "en";
|
||||
|
||||
export const EVENT_REQUEST_LOCALE = "ak-request-locale";
|
||||
|
||||
const TOMBSTONE = "⛼⛼tombstone⛼⛼";
|
||||
/**
|
||||
* Find the locale definition for a given language code.
|
||||
*/
|
||||
export function findLocaleDefinition(languageCode: string): AKLocaleDefinition | null {
|
||||
for (const locale of AKLocalDefinitions) {
|
||||
if (locale.pattern.test(languageCode)) {
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: This is the definition of the LOCALES table that most of the code uses. The 'definitions'
|
||||
// file is relatively pure, but here we establish that we want the English locale to loaded when an
|
||||
// application is first instantiated.
|
||||
|
||||
export const LOCALES = RAW_LOCALES.map((locale) =>
|
||||
locale.code === "en" ? { ...locale, locale: async () => enLocale } : locale,
|
||||
);
|
||||
|
||||
export function getBestMatchLocale(locale: string): AkLocale | undefined {
|
||||
return LOCALES.find((l) => l.match.test(locale));
|
||||
return null;
|
||||
}
|
||||
|
||||
// This looks weird, but it's sensible: we have several candidates, and we want to find the first
|
||||
// one that has a supported locale. Then, from *that*, we have to extract that first supported
|
||||
// locale.
|
||||
|
||||
export function findSupportedLocale(candidates: string[]) {
|
||||
const candidate = candidates.find((candidate: string) => getBestMatchLocale(candidate));
|
||||
return candidate ? getBestMatchLocale(candidate) : undefined;
|
||||
export function findSupportedLocale(candidates: string[]): AKLocaleDefinition | null {
|
||||
for (const candidate of candidates) {
|
||||
const locale = findLocaleDefinition(candidate);
|
||||
|
||||
if (locale) return locale;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function localeCodeFromUrl(param = "locale") {
|
||||
const url = new URL(window.location.href);
|
||||
return url.searchParams.get(param) || "";
|
||||
export function localeCodeFromURL(param = "locale") {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
return searchParams.get(param);
|
||||
}
|
||||
|
||||
// Get all locales we can, in order
|
||||
// - Global authentik settings (contains user settings)
|
||||
// - URL parameter
|
||||
// - A requested code passed in, if any
|
||||
// - Navigator
|
||||
// - Fallback (en)
|
||||
function isLocaleCodeCandidate(input: unknown): input is string {
|
||||
if (typeof input !== "string") return false;
|
||||
|
||||
const isLocaleCandidate = (v: unknown): v is string =>
|
||||
typeof v === "string" && v !== "" && v !== TOMBSTONE;
|
||||
return !!input;
|
||||
}
|
||||
|
||||
export function autoDetectLanguage(userReq = TOMBSTONE, brandReq = TOMBSTONE): string {
|
||||
const localeCandidates: string[] = [
|
||||
localeCodeFromUrl("locale"),
|
||||
userReq,
|
||||
window.navigator?.language ?? TOMBSTONE,
|
||||
brandReq,
|
||||
globalAK()?.locale ?? TOMBSTONE,
|
||||
DEFAULT_LOCALE,
|
||||
].filter(isLocaleCandidate);
|
||||
/**
|
||||
* Auto-detect the most appropriate locale.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* The order of precedence is:
|
||||
*
|
||||
* 1. URL parameter `locale`.
|
||||
* 2. User's preferred locale, if any.
|
||||
* 3. Browser's preferred locale, if any.
|
||||
* 4. Brand's preferred locale, if any.
|
||||
* 5. Default locale.
|
||||
*
|
||||
* @param requestedLanguageCode - The user's preferred locale, if any.
|
||||
* @param brandLanguageCode - The brand's preferred locale, if any.
|
||||
*
|
||||
* @returns The most appropriate locale.
|
||||
*/
|
||||
export function autoDetectLanguage(
|
||||
requestedLanguageCode?: string,
|
||||
brandLanguageCode?: string,
|
||||
): string {
|
||||
const localeCandidates = [
|
||||
localeCodeFromURL("locale"),
|
||||
requestedLanguageCode,
|
||||
window.navigator?.language,
|
||||
brandLanguageCode,
|
||||
globalAK()?.locale,
|
||||
].filter(isLocaleCodeCandidate);
|
||||
|
||||
const firstSupportedLocale = findSupportedLocale(localeCandidates);
|
||||
|
||||
@ -61,10 +82,11 @@ export function autoDetectLanguage(userReq = TOMBSTONE, brandReq = TOMBSTONE): s
|
||||
console.debug(
|
||||
`authentik/locale: No locale found for '[${localeCandidates}.join(',')]', falling back to ${DEFAULT_LOCALE}`,
|
||||
);
|
||||
|
||||
return DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
return firstSupportedLocale.code;
|
||||
return firstSupportedLocale.languageCode;
|
||||
}
|
||||
|
||||
export default autoDetectLanguage;
|
||||
|
||||
@ -1,10 +1,21 @@
|
||||
import type { LocaleModule } from "@lit/localize";
|
||||
|
||||
export type LocaleRow = [string, RegExp, () => string, () => Promise<LocaleModule>];
|
||||
/**
|
||||
* - ISO 639-1 code for the locale.
|
||||
* - Pattern to match the user-supplied locale.
|
||||
* - Human-readable label for the locale.
|
||||
* - Locale loader.
|
||||
*/
|
||||
export type LocaleRow = [
|
||||
languageCode: string,
|
||||
pattern: RegExp,
|
||||
formatLabel: () => string,
|
||||
fetch: () => Promise<LocaleModule>,
|
||||
];
|
||||
|
||||
export type AkLocale = {
|
||||
code: string;
|
||||
match: RegExp;
|
||||
label: () => string;
|
||||
locale: () => Promise<LocaleModule>;
|
||||
};
|
||||
export interface AKLocaleDefinition {
|
||||
languageCode: string;
|
||||
pattern: RegExp;
|
||||
formatLabel(): string;
|
||||
fetch(): Promise<LocaleModule>;
|
||||
}
|
||||
|
||||
@ -35,7 +35,10 @@ export class Sidebar extends AKElement {
|
||||
.pf-c-nav__section + .pf-c-nav__section {
|
||||
--pf-c-nav__section--section--MarginTop: var(--pf-global--spacer--sm);
|
||||
}
|
||||
|
||||
.pf-c-nav__list .sidebar-brand {
|
||||
max-height: 82px;
|
||||
margin-bottom: -0.5rem;
|
||||
}
|
||||
nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -67,6 +70,7 @@ export class Sidebar extends AKElement {
|
||||
class="pf-c-nav ${this.activeTheme === UiThemeEnum.Light ? "pf-m-light" : ""}"
|
||||
aria-label=${msg("Global")}
|
||||
>
|
||||
<ak-sidebar-brand></ak-sidebar-brand>
|
||||
<ul class="pf-c-nav__list">
|
||||
<slot></slot>
|
||||
</ul>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
||||
import { themeImage } from "@goauthentik/elements/utils/images";
|
||||
@ -41,16 +42,22 @@ export class SidebarBrand extends WithBrandConfig(AKElement) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: var(--ak-navbar-height);
|
||||
height: 114px;
|
||||
min-height: 114px;
|
||||
border-bottom: var(--pf-global--BorderWidth--sm);
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: var(--pf-global--BorderColor--100);
|
||||
}
|
||||
|
||||
.pf-c-brand img {
|
||||
padding: 0 0.5rem;
|
||||
height: 42px;
|
||||
}
|
||||
button.pf-c-button.sidebar-trigger {
|
||||
background-color: transparent;
|
||||
border-radius: 0px;
|
||||
height: 100%;
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
@ -63,15 +70,32 @@ export class SidebarBrand extends WithBrandConfig(AKElement) {
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html` <a href="#/" class="pf-c-page__header-brand-link">
|
||||
<div class="pf-c-brand ak-brand">
|
||||
<img
|
||||
src=${themeImage(this.brand?.brandingLogo ?? DefaultBrand.brandingLogo)}
|
||||
alt="${msg("authentik Logo")}"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</a>`;
|
||||
return html` ${window.innerWidth <= MIN_WIDTH
|
||||
? html`
|
||||
<button
|
||||
class="sidebar-trigger pf-c-button"
|
||||
@click=${() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
`
|
||||
: html``}
|
||||
<a href="#/" class="pf-c-page__header-brand-link">
|
||||
<div class="pf-c-brand ak-brand">
|
||||
<img
|
||||
src=${themeImage(this.brand?.brandingLogo ?? DefaultBrand.brandingLogo)}
|
||||
alt="${msg("authentik Logo")}"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
CapabilitiesEnum,
|
||||
WithCapabilitiesConfig,
|
||||
} from "@goauthentik/elements/Interface/capabilitiesProvider";
|
||||
import { LOCALES } from "@goauthentik/elements/ak-locale-context/definitions";
|
||||
import { AKLocalDefinitions } from "@goauthentik/elements/ak-locale-context/definitions";
|
||||
import "@goauthentik/elements/forms/FormElement";
|
||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||
|
||||
@ -199,15 +199,15 @@ ${prompt.initialValue}</textarea
|
||||
})}`;
|
||||
case PromptTypeEnum.AkLocale: {
|
||||
const locales = this.can(CapabilitiesEnum.CanDebug)
|
||||
? LOCALES
|
||||
: LOCALES.filter((locale) => locale.code !== "debug");
|
||||
? AKLocalDefinitions
|
||||
: AKLocalDefinitions.filter((locale) => locale.languageCode !== "debug");
|
||||
const options = locales.map(
|
||||
(locale) =>
|
||||
html`<option
|
||||
value=${locale.code}
|
||||
?selected=${locale.code === prompt.initialValue}
|
||||
value=${locale.languageCode}
|
||||
?selected=${locale.languageCode === prompt.initialValue}
|
||||
>
|
||||
${locale.code.toUpperCase()} - ${locale.label()}
|
||||
${locale.languageCode.toUpperCase()} - ${locale.formatLabel()}
|
||||
</option> `,
|
||||
);
|
||||
|
||||
|
||||
@ -146,7 +146,6 @@ When writing out steps in a procedural topic, avoid starting with "Once...". Ins
|
||||
|
||||
- Use _italic_ for:
|
||||
|
||||
- Variables or placeholders to indicate that the value should be replaced by the user (e.g., _your-domain.com_). Clearly indicate whether variables in code snippets need to be defined by the user, are system-provided, or generated.
|
||||
- Emphasis, but sparingly, to avoid overuse. For example, you can use italics for important terms or concepts on first mention in a section.
|
||||
|
||||
- Use `code formatting` for:
|
||||
@ -157,11 +156,9 @@ When writing out steps in a procedural topic, avoid starting with "Once...". Ins
|
||||
|
||||
- When handling URLs:
|
||||
|
||||
- For URLs entered as values or defined in fields _italicize_ any variables within them to emphasize that placeholders require user input.
|
||||
- For URLs entered as values or defined in fields, enclose any variables inside angle brackets (`< >`) to clearly indicate that these are placeholders that require user input.
|
||||
|
||||
In Markdown, use this syntax: `<kbd>https://<em>company-domain</em>/source/oauth/callback/<em>source-slug</em></kbd>`
|
||||
|
||||
Rendered formatting: <kbd>https://<em>company-domain</em>/source/oauth/callback/<em>source-slug</em></kbd>
|
||||
For example: `https://authentik.company/application/o/<slug>/.well-known/openid-configuration`
|
||||
|
||||
- When mentioning URLs in text or within procedural instructions, omit code formatting. For instance: "In your browser, go to https://example.com."
|
||||
|
||||
|
||||
@ -7,41 +7,43 @@ title: User properties and attributes
|
||||
The User object has the following properties:
|
||||
|
||||
- `username`: User's username.
|
||||
- `email` User's email.
|
||||
- `uid` User's unique ID
|
||||
- `name` User's display name.
|
||||
- `is_staff` Boolean field if user is staff.
|
||||
- `is_active` Boolean field if user is active.
|
||||
- `date_joined` Date user joined/was created.
|
||||
- `password_change_date` Date password was last changed.
|
||||
- `path` User's path, see [Path](#path)
|
||||
- `attributes` Dynamic attributes, see [Attributes](#attributes)
|
||||
- `group_attributes()` Merged attributes of all groups the user is member of and the user's own attributes.
|
||||
- `ak_groups` This is a queryset of all the user's groups.
|
||||
|
||||
You can do additional filtering like:
|
||||
|
||||
```python
|
||||
user.ak_groups.filter(name__startswith='test')
|
||||
```
|
||||
|
||||
For Django field lookups, see [here](https://docs.djangoproject.com/en/4.2/ref/models/querysets/#id4).
|
||||
|
||||
To get the name of all groups, you can use this command:
|
||||
|
||||
```python
|
||||
[group.name for group in user.ak_groups.all()]
|
||||
```
|
||||
- `email`: User's email.
|
||||
- `uid`: User's unique ID. Read-only.
|
||||
- `name`: User's display name.
|
||||
- `is_staff`: Boolean field defining if user is staff.
|
||||
- `is_active`: Boolean field defining if user is active.
|
||||
- `date_joined`: Date user joined/was created. Read-only.
|
||||
- `password_change_date`: Date password was last changed. Read-only.
|
||||
- `path`: User's path, see [Path](#path)
|
||||
- `attributes`: Dynamic attributes, see [Attributes](#attributes)
|
||||
- `group_attributes()`: Merged attributes of all groups the user is member of and the user's own attributes. Ready-only.
|
||||
- `ak_groups`: This is a queryset of all the user's groups.
|
||||
|
||||
## Examples
|
||||
|
||||
List all the User's group names:
|
||||
These are examples of how User objects can be used within Policies and Property Mappings.
|
||||
|
||||
### List a user's group memberships
|
||||
|
||||
Use the following example to list all groups that a User object is a member of:
|
||||
|
||||
```python
|
||||
for group in user.ak_groups.all():
|
||||
yield group.name
|
||||
```
|
||||
|
||||
### List a user's group memberships and filter based on group name
|
||||
|
||||
Use the following example to list groups that a User object is a member of, but filter based on group name:
|
||||
|
||||
```python
|
||||
user.ak_groups.filter(name__startswith='test')
|
||||
```
|
||||
|
||||
:::info
|
||||
For Django field lookups, see the [Django documentation](https://docs.djangoproject.com/en/stable/ref/models/querysets/#id4).
|
||||
:::
|
||||
|
||||
## Path
|
||||
|
||||
Paths can be used to organize users into folders depending on which source created them or organizational structure. Paths may not start or end with a slash, but they can contain any other character as path segments. The paths are currently purely used for organization, it does not affect their permissions, group memberships, or anything else.
|
||||
@ -87,7 +89,7 @@ This field is only used by the Proxy Provider.
|
||||
Some applications can be configured to create new users using header information forwarded from authentik. You can forward additional header information by adding each header
|
||||
underneath `additionalHeaders`:
|
||||
|
||||
#### Example:
|
||||
#### Example
|
||||
|
||||
```yaml
|
||||
additionalHeaders:
|
||||
|
||||
@ -66,7 +66,7 @@ environment:
|
||||
"client_id": "<Client ID>",
|
||||
"secret": "<Client Secret>",
|
||||
"settings": {
|
||||
"server_url": "https://authentik.company/application/o/paperless/.well-known/openid-configuration"
|
||||
"server_url": "https://authentik.company/application/o/<slug>/.well-known/openid-configuration"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user