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
17 changed files with 294 additions and 213 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,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

@ -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

@ -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

@ -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"
} }
} }
], ],