Compare commits

..

7 Commits

Author SHA1 Message Date
9deed34479 web: Fix issue stemming from locale initialization triggering UI repeat reloads. 2025-04-23 14:29:20 +02:00
2033d52dc2 core, web: update translations (#14187)
Co-authored-by: melizeche <484773+melizeche@users.noreply.github.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-04-23 10:57:09 +00:00
be00f47ddc core: bump goauthentik.io/api/v3 from 3.2025024.8 to 3.2025024.9 (#14189)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-23 12:44:09 +02:00
2cc5f4b273 website/docs: update user object doc (#14132)
* Updated formatting, changed examples, added headers, updated django doc link to stable

* Prettier fix

* Update website/docs/users-sources/user/user_ref.mdx

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/docs/users-sources/user/user_ref.mdx

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-04-23 08:26:10 +01:00
4e8f3407a4 website/docs: dev-docs: style guide: no longer using italic for vars (#14185)
We no longer use italic for variables

Signed-off-by: Dominic R <dominic@sdko.org>
2025-04-22 17:30:46 -05:00
7f861cc2a1 website/docs: dev docs: style guide: update style conventions for urls (#14184)
* website/docs: dev docs: style guide: update style conventions for urls

Updates URL styling conventions to use angle bracket surrounded values instead of <em>s and <kbd>s

Part of https://www.notion.so/authentiksecurity/Check-ins-17caee05b24e80a0aec6c7d508406435?pvs=4#1ddaee05b24e80138155e120174c3502

Signed-off-by: Dominic R <dominic@sdko.org>

* yep

Signed-off-by: Dominic R <dominic@sdko.org>

---------

Signed-off-by: Dominic R <dominic@sdko.org>
2025-04-22 17:30:02 -05:00
7bf58d0ba2 website/integrations: paperless: use <slug>. instead of hardcoded slug value (#14183)
Closes https://github.com/goauthentik/authentik/issues/13778

Signed-off-by: Dominic R <dominic@sdko.org>
2025-04-22 16:55:53 -05:00
26 changed files with 465 additions and 665 deletions

2
go.mod
View File

@ -27,7 +27,7 @@ require (
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/wwt/guac v1.3.2 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/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.29.0 golang.org/x/oauth2 v0.29.0
golang.org/x/sync v0.13.0 golang.org/x/sync v0.13.0

4
go.sum
View File

@ -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.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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 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.9 h1:i3tbkyotE32ZpJ729BsPWTuLQUdtZ54Li4aP1amZzsM=
goauthentik.io/api/v3 v3.2025024.8/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= 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-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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -1255,20 +1255,6 @@ msgstr ""
msgid "Reputation Scores" msgid "Reputation Scores"
msgstr "" 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 #: authentik/policies/templates/policies/denied.html
msgid "Permission denied" msgid "Permission denied"
msgstr "" msgstr ""

View File

@ -4,7 +4,6 @@ import { ROUTES } from "@goauthentik/admin/Routes";
import { import {
EVENT_API_DRAWER_TOGGLE, EVENT_API_DRAWER_TOGGLE,
EVENT_NOTIFICATION_DRAWER_TOGGLE, EVENT_NOTIFICATION_DRAWER_TOGGLE,
EVENT_SIDEBAR_TOGGLE,
} from "@goauthentik/common/constants"; } from "@goauthentik/common/constants";
import { configureSentry } from "@goauthentik/common/sentry"; import { configureSentry } from "@goauthentik/common/sentry";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
@ -12,8 +11,6 @@ import { WebsocketClient } from "@goauthentik/common/ws";
import { AuthenticatedInterface } from "@goauthentik/elements/Interface"; import { AuthenticatedInterface } from "@goauthentik/elements/Interface";
import "@goauthentik/elements/ak-locale-context"; import "@goauthentik/elements/ak-locale-context";
import "@goauthentik/elements/banner/EnterpriseStatusBanner"; import "@goauthentik/elements/banner/EnterpriseStatusBanner";
import "@goauthentik/elements/banner/EnterpriseStatusBanner";
import "@goauthentik/elements/banner/VersionBanner";
import "@goauthentik/elements/banner/VersionBanner"; import "@goauthentik/elements/banner/VersionBanner";
import "@goauthentik/elements/messages/MessageContainer"; import "@goauthentik/elements/messages/MessageContainer";
import "@goauthentik/elements/messages/MessageContainer"; import "@goauthentik/elements/messages/MessageContainer";
@ -43,8 +40,6 @@ if (process.env.NODE_ENV === "development") {
@customElement("ak-interface-admin") @customElement("ak-interface-admin")
export class AdminInterface extends AuthenticatedInterface { export class AdminInterface extends AuthenticatedInterface {
//#region Properties
@property({ type: Boolean }) @property({ type: Boolean })
notificationDrawerOpen = getURLParam("notificationDrawerOpen", false); notificationDrawerOpen = getURLParam("notificationDrawerOpen", false);
@ -59,17 +54,6 @@ export class AdminInterface extends AuthenticatedInterface {
@query("ak-about-modal") @query("ak-about-modal")
aboutModal?: AboutModal; aboutModal?: AboutModal;
@state()
sidebarVisible = false;
#toggleSidebar = () => {
this.sidebarVisible = !this.sidebarVisible;
};
//#endregion
//#region Styles
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
PFBase, PFBase,
@ -83,30 +67,23 @@ export class AdminInterface extends AuthenticatedInterface {
z-index: auto !important; z-index: auto !important;
background-color: transparent; background-color: transparent;
} }
.display-none { .display-none {
display: none; display: none;
} }
.pf-c-page { .pf-c-page {
background-color: var(--pf-c-page--BackgroundColor) !important; background-color: var(--pf-c-page--BackgroundColor) !important;
} }
/* Global page background colour */
:host([theme="dark"]) { :host([theme="dark"]) .pf-c-page {
/* Global page background colour */ --pf-c-page--BackgroundColor: var(--ak-dark-background);
.pf-c-page {
--pf-c-page--BackgroundColor: var(--ak-dark-background);
}
} }
ak-enterprise-status,
ak-page-navbar { ak-version-banner {
grid-area: header; grid-area: header;
} }
ak-admin-sidebar { ak-admin-sidebar {
grid-area: nav; grid-area: nav;
} }
.pf-c-drawer__panel { .pf-c-drawer__panel {
z-index: var(--pf-global--ZIndex--xl); z-index: var(--pf-global--ZIndex--xl);
} }
@ -114,10 +91,6 @@ export class AdminInterface extends AuthenticatedInterface {
]; ];
} }
//#endregion
//#region Lifecycle
constructor() { constructor() {
super(); super();
this.ws = new WebsocketClient(); 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 { render(): TemplateResult {
const sidebarClasses = { const sidebarClasses = {
"pf-m-light": this.activeTheme === UiThemeEnum.Light, "pf-m-light": this.activeTheme === UiThemeEnum.Light,
"pf-m-expanded": !this.sidebarVisible,
"pf-m-collapsed": this.sidebarVisible,
}; };
const drawerOpen = this.notificationDrawerOpen || this.apiDrawerOpen; const drawerOpen = this.notificationDrawerOpen || this.apiDrawerOpen;
const drawerClasses = { const drawerClasses = {
"pf-m-expanded": drawerOpen, "pf-m-expanded": drawerOpen,
"pf-m-collapsed": !drawerOpen, "pf-m-collapsed": !drawerOpen,
@ -177,16 +136,11 @@ export class AdminInterface extends AuthenticatedInterface {
return html` <ak-locale-context> return html` <ak-locale-context>
<div class="pf-c-page"> <div class="pf-c-page">
<ak-page-navbar> <ak-enterprise-status interface="admin"></ak-enterprise-status>
<ak-version-banner></ak-version-banner> <ak-version-banner></ak-version-banner>
<ak-enterprise-status interface="admin"></ak-enterprise-status>
</ak-page-navbar>
<ak-admin-sidebar <ak-admin-sidebar
class="pf-c-page__sidebar class="pf-c-page__sidebar ${classMap(sidebarClasses)}"
${classMap(sidebarClasses)}"
></ak-admin-sidebar> ></ak-admin-sidebar>
<div class="pf-c-page__drawer"> <div class="pf-c-page__drawer">
<div class="pf-c-drawer ${classMap(drawerClasses)}"> <div class="pf-c-drawer ${classMap(drawerClasses)}">
<div class="pf-c-drawer__main"> <div class="pf-c-drawer__main">

View File

@ -1,3 +1,4 @@
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { import {
@ -30,9 +31,16 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement
me().then((user: SessionUser) => { me().then((user: SessionUser) => {
this.impersonation = user.original ? user.user.username : null; this.impersonation = user.original ? user.user.username : null;
}); });
this.toggleOpen = this.toggleOpen.bind(this);
this.checkWidth = this.checkWidth.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() { checkWidth() {
// This works just fine, but it assumes that the `--ak-sidebar--minimum-auto-width` is in // 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. // 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() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.toggleOpen);
window.addEventListener("resize", this.checkWidth); window.addEventListener("resize", this.checkWidth);
// After connecting to the DOM, we can now perform this check to see if the sidebar should // After connecting to the DOM, we can now perform this check to see if the sidebar should
// be open by default. // be open by default.
@ -55,6 +63,7 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement
// connection, and removing them before disconnection. // connection, and removing them before disconnection.
disconnectedCallback() { disconnectedCallback() {
window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.toggleOpen);
window.removeEventListener("resize", this.checkWidth); window.removeEventListener("resize", this.checkWidth);
super.disconnectedCallback(); super.disconnectedCallback();
} }
@ -62,9 +71,8 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement
render() { render() {
return html` return html`
<ak-sidebar <ak-sidebar
class="pf-c-page__sidebar class="pf-c-page__sidebar ${this.open ? "pf-m-expanded" : "pf-m-collapsed"} ${this
${this.open ? "pf-m-expanded" : "pf-m-collapsed"} ${this.activeTheme === .activeTheme === UiThemeEnum.Light
UiThemeEnum.Light
? "pf-m-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 { renderSidebarItems(): TemplateResult {
// The second attribute type is of string[] to help with the 'activeWhen' control, which was // The second attribute type is of string[] to help with the 'activeWhen' control, which was
// commonplace and singular enough to merit its own handler. // commonplace and singular enough to merit its own handler.

View File

@ -94,13 +94,10 @@ export class AdminOverviewPage extends AdminOverviewBase {
} }
render(): TemplateResult { 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 return html`<ak-page-header description=${msg("General system status")} ?hasIcon=${false}>
header=${msg(str`Welcome, ${username || ""}.`)} <span slot="header"> ${msg(str`Welcome, ${name || ""}.`)} </span>
description=${msg("General system status")}
?hasIcon=${false}
>
</ak-page-header> </ak-page-header>
<section class="pf-c-page__main-section"> <section class="pf-c-page__main-section">
<div class="pf-l-grid pf-m-gutter"> <div class="pf-l-grid pf-m-gutter">

View File

@ -83,10 +83,13 @@ export class AdminSettingsPage extends AKElement {
} }
render() { render() {
if (!this.settings) return nothing; if (!this.settings) {
return nothing;
}
return html` 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"> <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">
<div class="pf-c-card__body"> <div class="pf-c-card__body">

View File

@ -4,8 +4,12 @@ import {
EventMiddleware, EventMiddleware,
LoggingMiddleware, LoggingMiddleware,
} from "@goauthentik/common/api/middleware"; } 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 { 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"; 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"); console.debug("authentik/locale: setting locale from brand default");
window.dispatchEvent( window.dispatchEvent(
new CustomEvent(EVENT_LOCALE_REQUEST, { new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, {
composed: true, composed: true,
bubbles: true, bubbles: true,
detail: { locale: brand.defaultLocale }, detail: { locale: brand.defaultLocale },

View File

@ -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_SIDEBAR_TOGGLE = "ak-sidebar-toggle";
export const EVENT_WS_MESSAGE = "ak-ws-message"; export const EVENT_WS_MESSAGE = "ak-ws-message";
export const EVENT_FLOW_ADVANCE = "ak-flow-advance"; 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_REQUEST_POST = "ak-request-post";
export const EVENT_MESSAGE = "ak-message"; export const EVENT_MESSAGE = "ak-message";
export const EVENT_THEME_CHANGE = "ak-theme-change"; export const EVENT_THEME_CHANGE = "ak-theme-change";

View File

@ -17,13 +17,6 @@
/* Minimum width after which the sidebar becomes automatic */ /* Minimum width after which the sidebar becomes automatic */
--ak-sidebar--minimum-auto-width: 80rem; --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) { @supports selector(::-webkit-scrollbar) {

View File

@ -1,6 +1,9 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
import { isResponseErrorLike } from "@goauthentik/common/errors/network"; 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"; 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}'`); console.debug(`authentik/locale: Activating user's configured locale '${locale}'`);
window.dispatchEvent( window.dispatchEvent(
new CustomEvent(EVENT_LOCALE_REQUEST, { new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, {
composed: true, composed: true,
bubbles: true, bubbles: true,
detail: { locale }, detail: { locale },

View File

@ -67,12 +67,6 @@ export class NavigationButtons extends AKElement {
:host([theme="light"]) .pf-c-page__header-tools-group .pf-c-button { :host([theme="light"]) .pf-c-page__header-tools-group .pf-c-button {
color: var(--ak-global--Color--100) !important; 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() { renderImpersonation() {
if (!this.me?.original) return nothing; if (!this.me?.original) {
return nothing;
}
const onClick = async () => { const onClick = async () => {
await new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve(); await new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve();
@ -179,14 +175,6 @@ export class NavigationButtons extends AKElement {
</div>`; </div>`;
} }
renderAvatar() {
return html`<img
class="pf-c-avatar"
src=${ifDefined(this.me?.user.avatar)}
alt="${msg("Avatar image")}"
/>`;
}
get userDisplayName() { get userDisplayName() {
return match<UserDisplay | undefined, string | undefined>(this.uiConfig?.navbar.userDisplay) return match<UserDisplay | undefined, string | undefined>(this.uiConfig?.navbar.userDisplay)
.with(UserDisplay.username, () => this.me?.user.username) .with(UserDisplay.username, () => this.me?.user.username)
@ -224,7 +212,11 @@ export class NavigationButtons extends AKElement {
</div> </div>
</div>` </div>`
: nothing} : nothing}
${this.renderAvatar()} <img
class="pf-c-avatar"
src=${ifDefined(this.me?.user.avatar)}
alt="${msg("Avatar image")}"
/>
</div>`; </div>`;
} }
} }

View File

@ -10,18 +10,15 @@ import { me } from "@goauthentik/common/users";
import "@goauthentik/components/ak-nav-buttons"; import "@goauthentik/components/ak-nav-buttons";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; 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 "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize"; 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 { customElement, property, state } from "lit/decorators.js";
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css"; import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFContent from "@patternfly/patternfly/components/Content/content.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 PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css";
import PFNotificationBadge from "@patternfly/patternfly/components/NotificationBadge/notification-badge.css"; import PFNotificationBadge from "@patternfly/patternfly/components/NotificationBadge/notification-badge.css";
import PFPage from "@patternfly/patternfly/components/Page/page.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"; import { SessionUser } from "@goauthentik/api";
//#region Page Navbar @customElement("ak-page-header")
export class PageHeader extends WithBrandConfig(AKElement) {
export interface PageNavbarDetails { @property()
header?: string;
description?: string;
icon?: string; icon?: string;
iconImage?: boolean;
}
/** @property({ type: Boolean })
* A global navbar component at the top of the page. iconImage = false;
*
* 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
private static elementRef: AKPageNavbar | null = null; @property()
header = "";
static readonly setNavbarDetails = (detail: Partial<PageNavbarDetails>): void => { @property()
const { elementRef } = AKPageNavbar; description?: string;
if (!elementRef) {
console.debug(
`ak-page-header: Could not find ak-page-navbar, skipping event dispatch.`,
);
return;
}
const { header, description, icon, iconImage } = detail; @property({ type: Boolean })
hasIcon = true;
elementRef.header = header; @state()
elementRef.description = description; me?: SessionUser;
elementRef.icon = icon;
elementRef.iconImage = iconImage || false; @state()
elementRef.hasIcon = !!icon; uiConfig!: UIConfig;
};
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
PFBase, PFBase,
PFButton, PFButton,
PFPage, PFPage,
PFDrawer,
PFNotificationBadge, PFNotificationBadge,
PFContent, PFContent,
PFAvatar, PFAvatar,
@ -84,212 +63,55 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb
position: sticky; position: sticky;
top: 0; top: 0;
z-index: var(--pf-global--ZIndex--lg); 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
);
} }
.bar {
: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 {
border-bottom: var(--pf-global--BorderWidth--sm); border-bottom: var(--pf-global--BorderWidth--sm);
border-bottom-style: solid; border-bottom-style: solid;
border-bottom-color: var(--pf-global--BorderColor--100); border-bottom-color: var(--pf-global--BorderColor--100);
background-color: var(--pf-c-page--BackgroundColor);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
min-height: 6rem; min-height: 114px;
max-height: 114px;
display: grid; background-color: var(--pf-c-page--BackgroundColor);
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%;
}
} }
.pf-c-page__main-section.pf-m-light {
.items { background-color: transparent;
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 {
.brand { flex-grow: 1;
grid-area: brand; flex-shrink: 1;
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);
display: flex; display: flex;
flex-direction: column;
justify-content: center; justify-content: center;
&.pf-m-collapsed {
display: none;
}
@media (max-width: 1279px) {
display: none;
}
} }
img.pf-icon {
.sidebar-trigger { max-height: 24px;
grid-area: toggle;
height: 100%;
} }
.logo {
flex: 0 0 auto;
height: var(--ak-brand-logo-height);
& img {
height: 100%;
}
}
.sidebar-trigger, .sidebar-trigger,
.notification-trigger { .notification-trigger {
font-size: 1.5rem; font-size: 24px;
} }
.notification-trigger.has-notifications { .notification-trigger.has-notifications {
color: var(--pf-global--active-color--100); color: var(--pf-global--active-color--100);
} }
.page-title {
display: flex;
gap: var(--pf-global--spacer--xs);
}
h1 { h1 {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center !important; 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() { constructor() {
super(); super();
window.addEventListener(EVENT_WS_MESSAGE, () => { window.addEventListener(EVENT_WS_MESSAGE, () => {
@ -297,23 +119,13 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb
}); });
} }
connectedCallback(): void { async firstUpdated() {
super.connectedCallback();
AKPageNavbar.elementRef = this;
}
disconnectedCallback(): void {
super.disconnectedCallback();
AKPageNavbar.elementRef = null;
}
public async firstUpdated() {
this.me = await me(); this.me = await me();
this.uiConfig = await uiConfig(); this.uiConfig = await uiConfig();
this.uiConfig.navbar.userDisplay = UserDisplay.none; this.uiConfig.navbar.userDisplay = UserDisplay.none;
} }
#setTitle(header?: string) { setTitle(header?: string) {
const currentIf = currentInterface(); const currentIf = currentInterface();
let title = this.brand?.brandingTitle || TITLE_DEFAULT; let title = this.brand?.brandingTitle || TITLE_DEFAULT;
if (currentIf === "admin") { if (currentIf === "admin") {
@ -329,146 +141,65 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb
willUpdate() { willUpdate() {
// Always update title, even if there's no header value set, // Always update title, even if there's no header value set,
// as in that case we still need to return to the generic title // as in that case we still need to return to the generic title
this.#setTitle(this.header); this.setTitle(this.header);
} }
//#region Render
renderIcon() { renderIcon() {
if (this.icon) { if (this.icon) {
if (this.iconImage && !this.icon.startsWith("fa://")) { 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 "); const icon = this.icon.replaceAll("fa://", "fa ");
return html`<i class=${icon}></i>`;
return html`<i class="accent-icon ${icon}"></i>`;
} }
return nothing; return nothing;
} }
render(): TemplateResult { render(): TemplateResult {
return html`<navbar aria-label="Main" class="navbar"> return html`<div class="bar">
<aside class="brand ${this.open ? "" : "pf-m-collapsed"}"> <button
<a href="#/"> class="sidebar-trigger pf-c-button pf-m-plain"
<div class="logo"> @click=${() => {
<img this.dispatchEvent(
src=${themeImage( new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
this.brand?.brandingLogo ?? DefaultBrand.brandingLogo, bubbles: true,
)} composed: true,
alt="${msg("authentik Logo")}" }),
loading="lazy" );
/> }}
</div> >
</a> <i class="fas fa-bars"></i>
</aside> </button>
<button <section class="pf-c-page__main-section pf-m-light">
class="sidebar-trigger pf-c-button pf-m-plain" <div class="pf-c-content">
@click=${this.#toggleSidebar} <h1>
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">
${this.hasIcon ${this.hasIcon
? html`<slot name="icon">${this.renderIcon()}</slot>` ? html`<slot name="icon">${this.renderIcon()}</slot>&nbsp;`
: nothing} : nothing}
${this.header} <slot name="header">${this.header}</slot>
</h1> </h1>
</section> ${this.description ? html`<p>${this.description}</p>` : html``}
${this.description </div>
? html`<section class="items page-description pf-c-content"> </section>
<p>${this.description}</p> <div class="pf-c-page__header-tools">
</section>` <div class="pf-c-page__header-tools-group">
: nothing} <ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.me}>
<a
<section class="items secondary"> class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md"
<div class="pf-c-page__header-tools-group"> href="${globalAK().api.base}if/user/"
<ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.me}> slot="extra"
<a >
class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md" ${msg("User interface")}
href="${globalAK().api.base}if/user/" </a>
slot="extra" </ak-nav-buttons>
> </div>
${msg("User interface")} </div>
</a> </div>`;
</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,
});
} }
} }
//#endregion
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ak-page-header": AKPageHeader; "ak-page-header": PageHeader;
"ak-page-navbar": AKPageNavbar;
} }
} }

View File

@ -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 { localized, msg } from "@lit/localize";
import { LitElement, html } from "lit"; import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import "./ak-locale-context"; import "./ak-locale-context";
import { EVENT_LOCALE_REQUEST, LocaleContextEventDetail } from "./events.js";
export default { export default {
title: "Elements / Shell / Locale Context", title: "Elements / Shell / Locale Context",
@ -37,10 +35,18 @@ export const InFrench = () =>
</div>`; </div>`;
export const SwitchingBackAndForth = () => { export const SwitchingBackAndForth = () => {
let lang = "en"; let languageCode = "en";
window.setInterval(() => { window.setInterval(() => {
lang = lang === "en" ? "fr" : "en"; languageCode = languageCode === "en" ? "fr" : "en";
window.dispatchEvent(customEvent(EVENT_LOCALE_REQUEST, { locale: lang }));
window.dispatchEvent(
new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, {
composed: true,
bubbles: true,
detail: { locale: languageCode },
}),
);
}, 1000); }, 1000);
return html`<div style="background: #fff; padding: 4em"> return html`<div style="background: #fff; padding: 4em">

View File

@ -1,19 +1,18 @@
import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { customEvent } from "@goauthentik/elements/utils/customEvents";
import { html } from "lit"; import { html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { WithBrandConfig } from "../Interface/brandProvider"; import { WithBrandConfig } from "../Interface/brandProvider";
import { initializeLocalization } from "./configureLocale"; import { initializeLocalization } from "./configureLocale.js";
import type { LocaleGetter, LocaleSetter } from "./configureLocale"; import type { GetLocale, SetLocale } from "./configureLocale.js";
import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers"; 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. * A component to manage your locale settings.
* *
* ## Details * @remarks
* *
* This component exists to take a locale setting from several different places, find the * 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 * 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") @customElement("ak-locale-context")
export class LocaleContext extends WithBrandConfig(AKElement) { 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 }) @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 }) @property({ attribute: true, type: String })
param = "locale"; public param = "locale";
getLocale: LocaleGetter; protected readonly getLocale: GetLocale;
protected readonly setLocale: SetLocale;
setLocale: LocaleSetter;
constructor(code = DEFAULT_LOCALE) { constructor(code = DEFAULT_LOCALE) {
super(); super();
this.notifyApplication = this.notifyApplication.bind(this);
this.updateLocaleHandler = this.updateLocaleHandler.bind(this); if (LocaleContext.singleton) {
try { throw new Error(`Developer error: Must have only one locale context per session`);
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}`);
} }
LocaleContext.singleton = this;
const [getLocale, setLocale] = initializeLocalization();
this.getLocale = getLocale;
this.setLocale = setLocale;
this.setLocale(code).then(this.#notifyApplication);
} }
connectedCallback() { connectedCallback() {
super.connectedCallback(); this.#updateLocale();
this.updateLocale();
window.addEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler as EventListener); window.addEventListener(EVENT_LOCALE_REQUEST, this.#localeUpdateListener as EventListener);
} }
disconnectedCallback() { disconnectedCallback() {
window.removeEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler as EventListener); LocaleContext.singleton = null;
window.removeEventListener(
EVENT_LOCALE_REQUEST,
this.#localeUpdateListener as EventListener,
);
super.disconnectedCallback(); super.disconnectedCallback();
} }
updateLocaleHandler(ev: CustomEvent<{ locale: string }>) { #localeUpdateListener = (ev: CustomEvent<LocaleContextEventDetail>) => {
console.debug("authentik/locale: Locale update request received."); 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) { if (!locale) {
console.warn(`authentik/locale: failed to find locale for code ${localeRequest}`); console.warn(`authentik/locale: failed to find locale for code ${localeRequest}`);
return; return;
} }
locale.locale().then(() => {
console.debug(`authentik/locale: Setting Locale to ${locale.label()} (${locale.code})`); return locale.fetch().then(() => {
this.setLocale(locale.code).then(() => { console.debug(
window.setTimeout(this.notifyApplication, 0); `authentik/locale: Setting Locale to ${locale.formatLabel()} (${locale.languageCode})`,
}); );
this.setLocale(locale.languageCode).then(this.#notifyApplication);
}); });
} }
notifyApplication() { #notifyFrameID = -1;
// You will almost never have cause to catch this event. Lit's own `@localized()` decorator
// works just fine for almost every use case. #notifyApplication = () => {
this.dispatchEvent(customEvent(EVENT_LOCALE_CHANGE)); 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() { render() {
return html`<slot></slot>`; return html`<slot></slot>`;

View File

@ -1,39 +1,44 @@
import { configureLocalization } from "@lit/localize"; import { configureLocalization } from "@lit/localize";
import { sourceLocale, targetLocales } from "../../locale-codes"; import { sourceLocale, targetLocales } from "../../locale-codes.js";
import { getBestMatchLocale } from "./helpers"; import { findLocaleDefinition } from "./helpers.js";
type LocaleGetter = ReturnType<typeof configureLocalization>["getLocale"]; export type ConfigureLocalizationResult = ReturnType<typeof configureLocalization>;
type LocaleSetter = ReturnType<typeof configureLocalization>["setLocale"];
// Internal use only. export type GetLocale = ConfigureLocalizationResult["getLocale"];
// export type SetLocale = ConfigureLocalizationResult["setLocale"];
// This is where the lit-localization module is initialized with our loader, which associates our
// collection of locales with its getter and setter functions.
let getLocale: LocaleGetter | undefined = undefined; export type LocaleState = [GetLocale, SetLocale];
let setLocale: LocaleSetter | undefined = undefined;
export function initializeLocalization(): [LocaleGetter, LocaleSetter] { let cachedLocaleState: LocaleState | undefined = undefined;
if (getLocale && setLocale) {
return [getLocale, setLocale];
}
({ 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, sourceLocale,
targetLocales, targetLocales,
loadLocale: async (locale: string) => { loadLocale: (languageCode) => {
const localeDef = getBestMatchLocale(locale); const localeDef = findLocaleDefinition(languageCode);
if (!localeDef) {
console.warn(`Unrecognized locale: ${localeDef}`);
return Promise.reject("");
}
return localeDef.locale();
},
}));
return [getLocale, setLocale]; if (!localeDef) {
throw new Error(`Unrecognized locale: ${localeDef}`);
}
return localeDef.fetch();
},
});
cachedLocaleState = [getLocale, setLocale];
return cachedLocaleState;
} }
export default initializeLocalization; export default initializeLocalization;
export type { LocaleGetter, LocaleSetter };

View File

@ -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 type { LocaleModule } from "@lit/localize";
import { msg } 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; /**
* The default English locale module.
export { enLocale }; */
export const DefaultLocaleModule: LocaleModule = EnglishLocaleModule;
// NOTE: This table cannot be made any shorter, despite all the repetition of syntax. Bundlers look // 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 // for the `await import` string as a *string target* for doing alias substitution, so putting
@ -35,34 +39,44 @@ export { enLocale };
// - Text Label // - Text Label
// - Locale loader. // - Locale loader.
// prettier-ignore
const debug: LocaleRow = [ 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 // prettier-ignore
const LOCALE_TABLE: LocaleRow[] = [ const LOCALE_TABLE: readonly LocaleRow[] = [
["de", /^de([_-]|$)/i, () => msg("German"), async () => await import("@goauthentik/locales/de")], // English loaded when the application is first instantiated.
["en", /^en([_-]|$)/i, () => msg("English"), async () => await import("@goauthentik/locales/en")], ["en", /^en([_-]|$)/i, () => msg("English"), () => Promise.resolve(DefaultLocaleModule)],
["es", /^es([_-]|$)/i, () => msg("Spanish"), async () => await import("@goauthentik/locales/es")], ["de", /^de([_-]|$)/i, () => msg("German"), () => import("@goauthentik/locales/de")],
["fr", /^fr([_-]|$)/i, () => msg("French"), async () => await import("@goauthentik/locales/fr")], ["es", /^es([_-]|$)/i, () => msg("Spanish"), () => import("@goauthentik/locales/es")],
["it", /^it([_-]|$)/i, () => msg("Italian"), async () => await import("@goauthentik/locales/it")], ["fr", /^fr([_-]|$)/i, () => msg("French"), () => import("@goauthentik/locales/fr")],
["ko", /^ko([_-]|$)/i, () => msg("Korean"), async () => await import("@goauthentik/locales/ko")], ["it", /^it([_-]|$)/i, () => msg("Italian"), () => import("@goauthentik/locales/it")],
["nl", /^nl([_-]|$)/i, () => msg("Dutch"), async () => await import("@goauthentik/locales/nl")], ["ko", /^ko([_-]|$)/i, () => msg("Korean"), () => import("@goauthentik/locales/ko")],
["pl", /^pl([_-]|$)/i, () => msg("Polish"), async () => await import("@goauthentik/locales/pl")], ["nl", /^nl([_-]|$)/i, () => msg("Dutch"), () => import("@goauthentik/locales/nl")],
["ru", /^ru([_-]|$)/i, () => msg("Russian"), async () => await import("@goauthentik/locales/ru")], ["pl", /^pl([_-]|$)/i, () => msg("Polish"), () => import("@goauthentik/locales/pl")],
["tr", /^tr([_-]|$)/i, () => msg("Turkish"), async () => await import("@goauthentik/locales/tr")], ["ru", /^ru([_-]|$)/i, () => msg("Russian"), () => import("@goauthentik/locales/ru")],
["zh_TW", /^zh[_-]TW$/i, () => msg("Taiwanese Mandarin"), async () => await import("@goauthentik/locales/zh_TW")], ["tr", /^tr([_-]|$)/i, () => msg("Turkish"), () => import("@goauthentik/locales/tr")],
["zh-Hans", /^zh(\b|_)/i, () => msg("Chinese (simplified)"), async () => await import("@goauthentik/locales/zh-Hans")], ["zh_TW", /^zh[_-]TW$/i, () => msg("Taiwanese Mandarin"), () => import("@goauthentik/locales/zh_TW")],
["zh-Hant", /^zh[_-](HK|Hant)/i, () => msg("Chinese (traditional)"), async () => await import("@goauthentik/locales/zh-Hant")], ["zh-Hans", /^zh(\b|_)/i, () => msg("Chinese (simplified)"), () => import("@goauthentik/locales/zh-Hans")],
debug ["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, * Available locales, identified by their ISO 639-1 language code.
match, */
label, export const AKLocalDefinitions: readonly AKLocaleDefinition[] = LOCALE_TABLE.map(
locale, ([languageCode, pattern, formatLabel, fetch]) => {
})); return {
languageCode,
pattern,
formatLabel,
fetch,
};
},
);
export default LOCALES; export default AKLocalDefinitions;

View 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;
}

View File

@ -1,59 +1,80 @@
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { LOCALES as RAW_LOCALES, enLocale } from "./definitions"; import { AKLocalDefinitions } from "./definitions.js";
import { AkLocale } from "./types"; import { AKLocaleDefinition } from "./types.js";
export const DEFAULT_LOCALE = "en"; export const DEFAULT_LOCALE = "en";
export const EVENT_REQUEST_LOCALE = "ak-request-locale"; 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' return null;
// 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));
} }
// This looks weird, but it's sensible: we have several candidates, and we want to find the first // 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 // one that has a supported locale. Then, from *that*, we have to extract that first supported
// locale. // locale.
export function findSupportedLocale(candidates: string[]) { export function findSupportedLocale(candidates: string[]): AKLocaleDefinition | null {
const candidate = candidates.find((candidate: string) => getBestMatchLocale(candidate)); for (const candidate of candidates) {
return candidate ? getBestMatchLocale(candidate) : undefined; const locale = findLocaleDefinition(candidate);
if (locale) return locale;
}
return null;
} }
export function localeCodeFromUrl(param = "locale") { export function localeCodeFromURL(param = "locale") {
const url = new URL(window.location.href); const searchParams = new URLSearchParams(window.location.search);
return url.searchParams.get(param) || "";
return searchParams.get(param);
} }
// Get all locales we can, in order function isLocaleCodeCandidate(input: unknown): input is string {
// - Global authentik settings (contains user settings) if (typeof input !== "string") return false;
// - URL parameter
// - A requested code passed in, if any
// - Navigator
// - Fallback (en)
const isLocaleCandidate = (v: unknown): v is string => return !!input;
typeof v === "string" && v !== "" && v !== TOMBSTONE; }
export function autoDetectLanguage(userReq = TOMBSTONE, brandReq = TOMBSTONE): string { /**
const localeCandidates: string[] = [ * Auto-detect the most appropriate locale.
localeCodeFromUrl("locale"), *
userReq, * @remarks
window.navigator?.language ?? TOMBSTONE, *
brandReq, * The order of precedence is:
globalAK()?.locale ?? TOMBSTONE, *
DEFAULT_LOCALE, * 1. URL parameter `locale`.
].filter(isLocaleCandidate); * 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); const firstSupportedLocale = findSupportedLocale(localeCandidates);
@ -61,10 +82,11 @@ export function autoDetectLanguage(userReq = TOMBSTONE, brandReq = TOMBSTONE): s
console.debug( console.debug(
`authentik/locale: No locale found for '[${localeCandidates}.join(',')]', falling back to ${DEFAULT_LOCALE}`, `authentik/locale: No locale found for '[${localeCandidates}.join(',')]', falling back to ${DEFAULT_LOCALE}`,
); );
return DEFAULT_LOCALE; return DEFAULT_LOCALE;
} }
return firstSupportedLocale.code; return firstSupportedLocale.languageCode;
} }
export default autoDetectLanguage; export default autoDetectLanguage;

View File

@ -1,10 +1,21 @@
import type { LocaleModule } from "@lit/localize"; 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 = { export interface AKLocaleDefinition {
code: string; languageCode: string;
match: RegExp; pattern: RegExp;
label: () => string; formatLabel(): string;
locale: () => Promise<LocaleModule>; fetch(): Promise<LocaleModule>;
}; }

View File

@ -35,7 +35,10 @@ export class Sidebar extends AKElement {
.pf-c-nav__section + .pf-c-nav__section { .pf-c-nav__section + .pf-c-nav__section {
--pf-c-nav__section--section--MarginTop: var(--pf-global--spacer--sm); --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 { nav {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -67,6 +70,7 @@ export class Sidebar extends AKElement {
class="pf-c-nav ${this.activeTheme === UiThemeEnum.Light ? "pf-m-light" : ""}" class="pf-c-nav ${this.activeTheme === UiThemeEnum.Light ? "pf-m-light" : ""}"
aria-label=${msg("Global")} aria-label=${msg("Global")}
> >
<ak-sidebar-brand></ak-sidebar-brand>
<ul class="pf-c-nav__list"> <ul class="pf-c-nav__list">
<slot></slot> <slot></slot>
</ul> </ul>

View File

@ -1,3 +1,4 @@
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import { themeImage } from "@goauthentik/elements/utils/images"; import { themeImage } from "@goauthentik/elements/utils/images";
@ -41,16 +42,22 @@ export class SidebarBrand extends WithBrandConfig(AKElement) {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
height: var(--ak-navbar-height); height: 114px;
min-height: 114px;
border-bottom: var(--pf-global--BorderWidth--sm); border-bottom: var(--pf-global--BorderWidth--sm);
border-bottom-style: solid; border-bottom-style: solid;
border-bottom-color: var(--pf-global--BorderColor--100); border-bottom-color: var(--pf-global--BorderColor--100);
} }
.pf-c-brand img { .pf-c-brand img {
padding: 0 0.5rem; padding: 0 0.5rem;
height: 42px; 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 { render(): TemplateResult {
return html` <a href="#/" class="pf-c-page__header-brand-link"> return html` ${window.innerWidth <= MIN_WIDTH
<div class="pf-c-brand ak-brand"> ? html`
<img <button
src=${themeImage(this.brand?.brandingLogo ?? DefaultBrand.brandingLogo)} class="sidebar-trigger pf-c-button"
alt="${msg("authentik Logo")}" @click=${() => {
loading="lazy" this.dispatchEvent(
/> new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
</div> bubbles: true,
</a>`; 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>`;
} }
} }

View File

@ -4,7 +4,7 @@ import {
CapabilitiesEnum, CapabilitiesEnum,
WithCapabilitiesConfig, WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider"; } 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 "@goauthentik/elements/forms/FormElement";
import { BaseStage } from "@goauthentik/flow/stages/base"; import { BaseStage } from "@goauthentik/flow/stages/base";
@ -199,15 +199,15 @@ ${prompt.initialValue}</textarea
})}`; })}`;
case PromptTypeEnum.AkLocale: { case PromptTypeEnum.AkLocale: {
const locales = this.can(CapabilitiesEnum.CanDebug) const locales = this.can(CapabilitiesEnum.CanDebug)
? LOCALES ? AKLocalDefinitions
: LOCALES.filter((locale) => locale.code !== "debug"); : AKLocalDefinitions.filter((locale) => locale.languageCode !== "debug");
const options = locales.map( const options = locales.map(
(locale) => (locale) =>
html`<option html`<option
value=${locale.code} value=${locale.languageCode}
?selected=${locale.code === prompt.initialValue} ?selected=${locale.languageCode === prompt.initialValue}
> >
${locale.code.toUpperCase()} - ${locale.label()} ${locale.languageCode.toUpperCase()} - ${locale.formatLabel()}
</option> `, </option> `,
); );

View File

@ -146,7 +146,6 @@ When writing out steps in a procedural topic, avoid starting with "Once...". Ins
- Use _italic_ for: - 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. - 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: - Use `code formatting` for:
@ -157,11 +156,9 @@ When writing out steps in a procedural topic, avoid starting with "Once...". Ins
- When handling URLs: - 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>` For example: `https://authentik.company/application/o/<slug>/.well-known/openid-configuration`
Rendered formatting: <kbd>https://<em>company-domain</em>/source/oauth/callback/<em>source-slug</em></kbd>
- When mentioning URLs in text or within procedural instructions, omit code formatting. For instance: "In your browser, go to https://example.com." - When mentioning URLs in text or within procedural instructions, omit code formatting. For instance: "In your browser, go to https://example.com."

View File

@ -7,41 +7,43 @@ title: User properties and attributes
The User object has the following properties: The User object has the following properties:
- `username`: User's username. - `username`: User's username.
- `email` User's email. - `email`: User's email.
- `uid` User's unique ID - `uid`: User's unique ID. Read-only.
- `name` User's display name. - `name`: User's display name.
- `is_staff` Boolean field if user is staff. - `is_staff`: Boolean field defining if user is staff.
- `is_active` Boolean field if user is active. - `is_active`: Boolean field defining if user is active.
- `date_joined` Date user joined/was created. - `date_joined`: Date user joined/was created. Read-only.
- `password_change_date` Date password was last changed. - `password_change_date`: Date password was last changed. Read-only.
- `path` User's path, see [Path](#path) - `path`: User's path, see [Path](#path)
- `attributes` Dynamic attributes, see [Attributes](#attributes) - `attributes`: Dynamic attributes, see [Attributes](#attributes)
- `group_attributes()` Merged attributes of all groups the user is member of and the user's own 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. - `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()]
```
## Examples ## 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 ```python
for group in user.ak_groups.all(): for group in user.ak_groups.all():
yield group.name 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 ## 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. 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 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`: underneath `additionalHeaders`:
#### Example: #### Example
```yaml ```yaml
additionalHeaders: additionalHeaders:

View File

@ -66,7 +66,7 @@ environment:
"client_id": "<Client ID>", "client_id": "<Client ID>",
"secret": "<Client Secret>", "secret": "<Client Secret>",
"settings": { "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"
} }
} }
], ],