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/stretchr/testify v1.10.0
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2025024.8
goauthentik.io/api/v3 v3.2025024.9
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.29.0
golang.org/x/sync v0.13.0

4
go.sum
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.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2025024.8 h1:2mG4CqGSsmZq2CtRehxpDjsER43U/JQSoTOn5VC1ui4=
goauthentik.io/api/v3 v3.2025024.8/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2025024.9 h1:i3tbkyotE32ZpJ729BsPWTuLQUdtZ54Li4aP1amZzsM=
goauthentik.io/api/v3 v3.2025024.9/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-22 13:40+0000\n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -1255,20 +1255,6 @@ msgstr ""
msgid "Reputation Scores"
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr ""
#: authentik/policies/templates/policies/denied.html
msgid "Permission denied"
msgstr ""

View File

@ -4,8 +4,12 @@ import {
EventMiddleware,
LoggingMiddleware,
} from "@goauthentik/common/api/middleware";
import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants";
import { VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import {
EVENT_LOCALE_REQUEST,
LocaleContextEventDetail,
} from "@goauthentik/elements/ak-locale-context/events.js";
import { Config, Configuration, CoreApi, CurrentBrand, RootApi } from "@goauthentik/api";
@ -44,7 +48,7 @@ export function brandSetLocale(brand: CurrentBrand) {
}
console.debug("authentik/locale: setting locale from brand default");
window.dispatchEvent(
new CustomEvent(EVENT_LOCALE_REQUEST, {
new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, {
composed: true,
bubbles: true,
detail: { locale: brand.defaultLocale },

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_WS_MESSAGE = "ak-ws-message";
export const EVENT_FLOW_ADVANCE = "ak-flow-advance";
export const EVENT_LOCALE_CHANGE = "ak-locale-change";
export const EVENT_LOCALE_REQUEST = "ak-locale-request";
export const EVENT_REQUEST_POST = "ak-request-post";
export const EVENT_MESSAGE = "ak-message";
export const EVENT_THEME_CHANGE = "ak-theme-change";

View File

@ -1,6 +1,9 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
import { isResponseErrorLike } from "@goauthentik/common/errors/network";
import {
EVENT_LOCALE_REQUEST,
LocaleContextEventDetail,
} from "@goauthentik/elements/ak-locale-context/events.js";
import { CoreApi, SessionUser } from "@goauthentik/api";
@ -57,7 +60,7 @@ export async function me(): Promise<SessionUser> {
console.debug(`authentik/locale: Activating user's configured locale '${locale}'`);
window.dispatchEvent(
new CustomEvent(EVENT_LOCALE_REQUEST, {
new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, {
composed: true,
bubbles: true,
detail: { locale },

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

View File

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

View File

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

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

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 { LOCALES as RAW_LOCALES, enLocale } from "./definitions";
import { AkLocale } from "./types";
import { AKLocalDefinitions } from "./definitions.js";
import { AKLocaleDefinition } from "./types.js";
export const DEFAULT_LOCALE = "en";
export const EVENT_REQUEST_LOCALE = "ak-request-locale";
const TOMBSTONE = "⛼⛼tombstone⛼⛼";
/**
* Find the locale definition for a given language code.
*/
export function findLocaleDefinition(languageCode: string): AKLocaleDefinition | null {
for (const locale of AKLocalDefinitions) {
if (locale.pattern.test(languageCode)) {
return locale;
}
}
// NOTE: This is the definition of the LOCALES table that most of the code uses. The 'definitions'
// file is relatively pure, but here we establish that we want the English locale to loaded when an
// application is first instantiated.
export const LOCALES = RAW_LOCALES.map((locale) =>
locale.code === "en" ? { ...locale, locale: async () => enLocale } : locale,
);
export function getBestMatchLocale(locale: string): AkLocale | undefined {
return LOCALES.find((l) => l.match.test(locale));
return null;
}
// This looks weird, but it's sensible: we have several candidates, and we want to find the first
// one that has a supported locale. Then, from *that*, we have to extract that first supported
// locale.
export function findSupportedLocale(candidates: string[]) {
const candidate = candidates.find((candidate: string) => getBestMatchLocale(candidate));
return candidate ? getBestMatchLocale(candidate) : undefined;
export function findSupportedLocale(candidates: string[]): AKLocaleDefinition | null {
for (const candidate of candidates) {
const locale = findLocaleDefinition(candidate);
if (locale) return locale;
}
return null;
}
export function localeCodeFromUrl(param = "locale") {
const url = new URL(window.location.href);
return url.searchParams.get(param) || "";
export function localeCodeFromURL(param = "locale") {
const searchParams = new URLSearchParams(window.location.search);
return searchParams.get(param);
}
// Get all locales we can, in order
// - Global authentik settings (contains user settings)
// - URL parameter
// - A requested code passed in, if any
// - Navigator
// - Fallback (en)
function isLocaleCodeCandidate(input: unknown): input is string {
if (typeof input !== "string") return false;
const isLocaleCandidate = (v: unknown): v is string =>
typeof v === "string" && v !== "" && v !== TOMBSTONE;
return !!input;
}
export function autoDetectLanguage(userReq = TOMBSTONE, brandReq = TOMBSTONE): string {
const localeCandidates: string[] = [
localeCodeFromUrl("locale"),
userReq,
window.navigator?.language ?? TOMBSTONE,
brandReq,
globalAK()?.locale ?? TOMBSTONE,
DEFAULT_LOCALE,
].filter(isLocaleCandidate);
/**
* Auto-detect the most appropriate locale.
*
* @remarks
*
* The order of precedence is:
*
* 1. URL parameter `locale`.
* 2. User's preferred locale, if any.
* 3. Browser's preferred locale, if any.
* 4. Brand's preferred locale, if any.
* 5. Default locale.
*
* @param requestedLanguageCode - The user's preferred locale, if any.
* @param brandLanguageCode - The brand's preferred locale, if any.
*
* @returns The most appropriate locale.
*/
export function autoDetectLanguage(
requestedLanguageCode?: string,
brandLanguageCode?: string,
): string {
const localeCandidates = [
localeCodeFromURL("locale"),
requestedLanguageCode,
window.navigator?.language,
brandLanguageCode,
globalAK()?.locale,
].filter(isLocaleCodeCandidate);
const firstSupportedLocale = findSupportedLocale(localeCandidates);
@ -61,10 +82,11 @@ export function autoDetectLanguage(userReq = TOMBSTONE, brandReq = TOMBSTONE): s
console.debug(
`authentik/locale: No locale found for '[${localeCandidates}.join(',')]', falling back to ${DEFAULT_LOCALE}`,
);
return DEFAULT_LOCALE;
}
return firstSupportedLocale.code;
return firstSupportedLocale.languageCode;
}
export default autoDetectLanguage;

View File

@ -1,10 +1,21 @@
import type { LocaleModule } from "@lit/localize";
export type LocaleRow = [string, RegExp, () => string, () => Promise<LocaleModule>];
/**
* - ISO 639-1 code for the locale.
* - Pattern to match the user-supplied locale.
* - Human-readable label for the locale.
* - Locale loader.
*/
export type LocaleRow = [
languageCode: string,
pattern: RegExp,
formatLabel: () => string,
fetch: () => Promise<LocaleModule>,
];
export type AkLocale = {
code: string;
match: RegExp;
label: () => string;
locale: () => Promise<LocaleModule>;
};
export interface AKLocaleDefinition {
languageCode: string;
pattern: RegExp;
formatLabel(): string;
fetch(): Promise<LocaleModule>;
}

View File

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

View File

@ -146,7 +146,6 @@ When writing out steps in a procedural topic, avoid starting with "Once...". Ins
- Use _italic_ for:
- Variables or placeholders to indicate that the value should be replaced by the user (e.g., _your-domain.com_). Clearly indicate whether variables in code snippets need to be defined by the user, are system-provided, or generated.
- Emphasis, but sparingly, to avoid overuse. For example, you can use italics for important terms or concepts on first mention in a section.
- Use `code formatting` for:
@ -157,11 +156,9 @@ When writing out steps in a procedural topic, avoid starting with "Once...". Ins
- When handling URLs:
- For URLs entered as values or defined in fields _italicize_ any variables within them to emphasize that placeholders require user input.
- For URLs entered as values or defined in fields, enclose any variables inside angle brackets (`< >`) to clearly indicate that these are placeholders that require user input.
In Markdown, use this syntax: `<kbd>https://<em>company-domain</em>/source/oauth/callback/<em>source-slug</em></kbd>`
Rendered formatting: <kbd>https://<em>company-domain</em>/source/oauth/callback/<em>source-slug</em></kbd>
For example: `https://authentik.company/application/o/<slug>/.well-known/openid-configuration`
- When mentioning URLs in text or within procedural instructions, omit code formatting. For instance: "In your browser, go to https://example.com."

View File

@ -7,41 +7,43 @@ title: User properties and attributes
The User object has the following properties:
- `username`: User's username.
- `email` User's email.
- `uid` User's unique ID
- `name` User's display name.
- `is_staff` Boolean field if user is staff.
- `is_active` Boolean field if user is active.
- `date_joined` Date user joined/was created.
- `password_change_date` Date password was last changed.
- `path` User's path, see [Path](#path)
- `attributes` Dynamic attributes, see [Attributes](#attributes)
- `group_attributes()` Merged attributes of all groups the user is member of and the user's own attributes.
- `ak_groups` This is a queryset of all the user's groups.
You can do additional filtering like:
```python
user.ak_groups.filter(name__startswith='test')
```
For Django field lookups, see [here](https://docs.djangoproject.com/en/4.2/ref/models/querysets/#id4).
To get the name of all groups, you can use this command:
```python
[group.name for group in user.ak_groups.all()]
```
- `email`: User's email.
- `uid`: User's unique ID. Read-only.
- `name`: User's display name.
- `is_staff`: Boolean field defining if user is staff.
- `is_active`: Boolean field defining if user is active.
- `date_joined`: Date user joined/was created. Read-only.
- `password_change_date`: Date password was last changed. Read-only.
- `path`: User's path, see [Path](#path)
- `attributes`: Dynamic attributes, see [Attributes](#attributes)
- `group_attributes()`: Merged attributes of all groups the user is member of and the user's own attributes. Ready-only.
- `ak_groups`: This is a queryset of all the user's groups.
## Examples
List all the User's group names:
These are examples of how User objects can be used within Policies and Property Mappings.
### List a user's group memberships
Use the following example to list all groups that a User object is a member of:
```python
for group in user.ak_groups.all():
yield group.name
```
### List a user's group memberships and filter based on group name
Use the following example to list groups that a User object is a member of, but filter based on group name:
```python
user.ak_groups.filter(name__startswith='test')
```
:::info
For Django field lookups, see the [Django documentation](https://docs.djangoproject.com/en/stable/ref/models/querysets/#id4).
:::
## Path
Paths can be used to organize users into folders depending on which source created them or organizational structure. Paths may not start or end with a slash, but they can contain any other character as path segments. The paths are currently purely used for organization, it does not affect their permissions, group memberships, or anything else.
@ -87,7 +89,7 @@ This field is only used by the Proxy Provider.
Some applications can be configured to create new users using header information forwarded from authentik. You can forward additional header information by adding each header
underneath `additionalHeaders`:
#### Example:
#### Example
```yaml
additionalHeaders:

View File

@ -66,7 +66,7 @@ environment:
"client_id": "<Client ID>",
"secret": "<Client Secret>",
"settings": {
"server_url": "https://authentik.company/application/o/paperless/.well-known/openid-configuration"
"server_url": "https://authentik.company/application/o/<slug>/.well-known/openid-configuration"
}
}
],