Compare commits

...

37 Commits

Author SHA1 Message Date
25181d079e ...bandit
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-03-23 21:07:31 +00:00
5b91cb5ff3 yeah
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-03-23 20:51:47 +00:00
8ba5fde5ba fix
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-03-23 20:43:39 +00:00
2802deb497 tests/e2e: don't rely DNS to get host's IP for container access
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-03-23 19:30:24 +00:00
3eccef88aa web: Fix prop. (#13630) 2025-03-22 17:18:07 +01:00
8f50dfa0c5 core, web: update translations (#13628)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2025-03-22 13:28:36 +00:00
8417d8508f web/admin: reworked sync status card (#13625)
* reworked sync status

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* update imports

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add story and fix import

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* format

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-03-21 22:48:28 +00:00
b2c2fc001b core: bump github.com/golang-jwt/jwt/v5 from 5.2.1 to 5.2.2 (#13626)
Bumps [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) from 5.2.1 to 5.2.2.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v5.2.1...v5.2.2)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-21 22:48:16 +00:00
f60312cbbc translate: Updates for file web/xliff/en.xlf in zh-Hans (#13622)
Translate web/xliff/en.xlf in zh-Hans

100% translated source file: 'web/xliff/en.xlf'
on 'zh-Hans'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-03-21 20:16:25 +00:00
7614b17a05 web: bump API Client version (#13623)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-03-21 20:16:17 +00:00
8947376edb translate: Updates for file web/xliff/en.xlf in zh_CN (#13621)
Translate web/xliff/en.xlf in zh_CN

100% translated source file: 'web/xliff/en.xlf'
on 'zh_CN'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-03-21 20:16:12 +00:00
ce23209ae8 events: add configurable headers to webhooks (#13602)
* events: add configurable headers to webhooks

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make it a full thing

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix migration

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-03-21 19:37:15 +00:00
0b806b7130 web: Client-side MDX rendering (#13610)
* web: Allow build errors to propagate.

* web: Refactor MDX for client-side rendering.

* Remove override

Signed-off-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>

* revert css for links and tables

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* web: Move Markdown specific styles.

---------

Signed-off-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-03-21 19:32:52 +00:00
9538cf4690 website/docs: revert token_expiry format in example blueprint (#13582)
* revert token_expiry format in example blueprint

Signed-off-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>

* Revert blueprint change, use hard link to the previous version in the docs

---------

Signed-off-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-03-21 19:13:35 +01:00
63da458fb3 website: update header to match goauthentik.io (#13616)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-03-21 17:33:44 +00:00
873dab29a9 translate: Updates for file web/xliff/en.xlf in fr (#13615)
Translate web/xliff/en.xlf in fr

100% translated source file: 'web/xliff/en.xlf'
on 'fr'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-03-21 16:28:58 +00:00
1e96c80593 core, web: update translations (#13608)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2025-03-21 17:14:08 +01:00
ee4a922234 core: bump astral-sh/uv from 0.6.8 to 0.6.9 (#13612)
Bumps [astral-sh/uv](https://github.com/astral-sh/uv) from 0.6.8 to 0.6.9.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.6.8...0.6.9)

---
updated-dependencies:
- dependency-name: astral-sh/uv
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-21 17:11:54 +01:00
37a2eff716 website: components: add multilinecodeblock component (#13179)
* wip

* wip

* wip

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

wip

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

wip

* wip

* wip

* move css to same folder

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-03-21 10:08:24 -05:00
50e2f1c474 website/docs: add clarification about formatting URLs to Style Guide (#13601)
* a few additions

* codespell fixes??

* clarify URL formatting

* tweak

* Update website/docs/developer-docs/docs/style-guide.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/docs/developer-docs/docs/style-guide.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* conflict of course

* bump build

---------

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.com>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-03-20 20:10:34 -05:00
ab7338b50e core: bump github.com/redis/go-redis/v9 from 9.7.1 to 9.7.3 (#13603)
Bumps [github.com/redis/go-redis/v9](https://github.com/redis/go-redis) from 9.7.1 to 9.7.3.
- [Release notes](https://github.com/redis/go-redis/releases)
- [Changelog](https://github.com/redis/go-redis/blob/master/CHANGELOG.md)
- [Commits](https://github.com/redis/go-redis/compare/v9.7.1...v9.7.3)

---
updated-dependencies:
- dependency-name: github.com/redis/go-redis/v9
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-20 19:41:12 +00:00
bcdc6fcd36 lib: set a default HTTP timeout on outgoing requests (#13599)
* lib: set a default HTTP timeout on outgoing requests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add config

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-03-20 16:44:12 +00:00
98c3e0d68b website/integrations: add miniflux (#13559)
* website/integrations: add miniflux

* fixes authentik capitalisation

* Updated to match suggestions

* Update website/integrations/services/miniflux/index.md

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Dewi Roberts <chwshka@outlook.com>

* Updated to include <kbd> and <em> tags

* fixed codeblock

* Added explanatory text and removed extra EM tags

* fixed prettier issue

* Update website/integrations/services/miniflux/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <chwshka@outlook.com>

* Update website/integrations/services/miniflux/index.md

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

* Update website/integrations/services/miniflux/index.md

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

---------

Signed-off-by: Dewi Roberts <chwshka@outlook.com>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-03-20 08:52:49 -05:00
a2b82b6448 web: bump API Client version (#13585)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-03-20 13:08:26 +00:00
0456ace646 core: bump goauthentik.io/api/v3 from 3.2025022.3 to 3.2025022.5 (#13594)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2025022.3 to 3.2025022.5.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Changelog](https://github.com/goauthentik/client-go/blob/main/model_version_history.go)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2025022.3...v3.2025022.5)

---
updated-dependencies:
- dependency-name: goauthentik.io/api/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-20 13:08:13 +00:00
d3a11ce810 website/docs: style guide & integration template: revamp (#12929)
* website/docs: style guide: revamp

* fix anchor

* Update style-guide.mdx

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update style-guide.mdx

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update style-guide.mdx

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update website/docs/developer-docs/docs/style-guide.mdx

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update website/docs/developer-docs/docs/style-guide.mdx

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update website/docs/developer-docs/docs/style-guide.mdx

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update website/docs/developer-docs/docs/style-guide.mdx

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dominic R <dominic@sdko.org>

* Tana's suggested format 

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

* lint

* wip

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

* wip

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

* wip

lint

* Update website/docs/developer-docs/docs/style-guide.mdx

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

* tana

* fix

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-03-20 07:47:00 -05:00
bfd1445c69 translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#13592)
Translate django.po in zh-Hans

100% translated source file: 'django.po'
on 'zh-Hans'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-03-20 07:58:17 +00:00
c2b3e9b05c translate: Updates for file web/xliff/en.xlf in fr (#13597)
Translate web/xliff/en.xlf in fr

100% translated source file: 'web/xliff/en.xlf'
on 'fr'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-03-20 07:57:31 +00:00
2c7d841e4a translate: Updates for file locale/en/LC_MESSAGES/django.po in fr (#13596)
Translate locale/en/LC_MESSAGES/django.po in fr

100% translated source file: 'locale/en/LC_MESSAGES/django.po'
on 'fr'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-03-20 07:55:47 +00:00
c5d13c4a15 translate: Updates for file web/xliff/en.xlf in zh-Hans (#13593)
Translate web/xliff/en.xlf in zh-Hans

100% translated source file: 'web/xliff/en.xlf'
on 'zh-Hans'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-03-20 08:40:22 +01:00
079ef6e114 translate: Updates for file web/xliff/en.xlf in zh_CN (#13591)
Translate web/xliff/en.xlf in zh_CN

100% translated source file: 'web/xliff/en.xlf'
on 'zh_CN'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-03-20 08:40:02 +01:00
98bfca0b4d translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#13590)
Translate locale/en/LC_MESSAGES/django.po in zh_CN

100% translated source file: 'locale/en/LC_MESSAGES/django.po'
on 'zh_CN'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-03-20 08:40:00 +01:00
a247bd5b9f core, web: update translations (#13584)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2025-03-20 01:07:24 +00:00
27856ec301 brands: add option to set global default flow background (#13079)
* brands: add option to set global default flow background

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* test

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-03-20 01:07:05 +00:00
e4a8c05d25 web/admin: fix diff showing previous false as "-" (#13580)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-03-19 23:06:37 +00:00
cb2e0c6d54 web: bump API Client version (#13581)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-03-19 23:04:21 +00:00
f37e1ca642 brands: migrate custom CSS to brands (#13172)
* brands: migrate custom CSS to brands

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix missing default

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* simpler migration

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add css to brand form

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-03-19 22:52:38 +00:00
97 changed files with 2865 additions and 2841 deletions

View File

@ -94,7 +94,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 5: Download uv
FROM ghcr.io/astral-sh/uv:0.6.8 AS uv
FROM ghcr.io/astral-sh/uv:0.6.9 AS uv
# Stage 6: Base python image
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-base

View File

@ -49,6 +49,8 @@ class BrandSerializer(ModelSerializer):
"branding_title",
"branding_logo",
"branding_favicon",
"branding_custom_css",
"branding_default_flow_background",
"flow_authentication",
"flow_invalidation",
"flow_recovery",
@ -86,6 +88,7 @@ class CurrentBrandSerializer(PassiveSerializer):
branding_title = CharField()
branding_logo = CharField(source="branding_logo_url")
branding_favicon = CharField(source="branding_favicon_url")
branding_custom_css = CharField()
ui_footer_links = ListField(
child=FooterLinkSerializer(),
read_only=True,
@ -125,6 +128,7 @@ class BrandViewSet(UsedByMixin, ModelViewSet):
"branding_title",
"branding_logo",
"branding_favicon",
"branding_default_flow_background",
"flow_authentication",
"flow_invalidation",
"flow_recovery",

View File

@ -0,0 +1,35 @@
# Generated by Django 5.0.12 on 2025-02-22 01:51
from pathlib import Path
from django.db import migrations, models
from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migrate_custom_css(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Brand = apps.get_model("authentik_brands", "brand")
db_alias = schema_editor.connection.alias
path = Path("/web/dist/custom.css")
if not path.exists():
return
with path.read_text() as css:
Brand.objects.using(db_alias).update(branding_custom_css=css)
class Migration(migrations.Migration):
dependencies = [
("authentik_brands", "0007_brand_default_application"),
]
operations = [
migrations.AddField(
model_name="brand",
name="branding_custom_css",
field=models.TextField(blank=True, default=""),
),
migrations.RunPython(migrate_custom_css),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.13 on 2025-03-19 22:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_brands", "0008_brand_branding_custom_css"),
]
operations = [
migrations.AddField(
model_name="brand",
name="branding_default_flow_background",
field=models.TextField(default="/static/dist/assets/images/flow_background.jpg"),
),
]

View File

@ -33,6 +33,10 @@ class Brand(SerializerModel):
branding_logo = models.TextField(default="/static/dist/assets/icons/icon_left_brand.svg")
branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png")
branding_custom_css = models.TextField(default="", blank=True)
branding_default_flow_background = models.TextField(
default="/static/dist/assets/images/flow_background.jpg"
)
flow_authentication = models.ForeignKey(
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_authentication"
@ -84,6 +88,12 @@ class Brand(SerializerModel):
return CONFIG.get("web.path", "/")[:-1] + self.branding_favicon
return self.branding_favicon
def branding_default_flow_background_url(self) -> str:
"""Get branding_default_flow_background with the correct prefix"""
if self.branding_default_flow_background.startswith("/static"):
return CONFIG.get("web.path", "/")[:-1] + self.branding_default_flow_background
return self.branding_default_flow_background
@property
def serializer(self) -> Serializer:
from authentik.brands.api import BrandSerializer

View File

@ -24,6 +24,7 @@ class TestBrands(APITestCase):
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_favicon": "/static/dist/assets/icons/icon.png",
"branding_title": "authentik",
"branding_custom_css": "",
"matched_domain": brand.domain,
"ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC,
@ -43,6 +44,7 @@ class TestBrands(APITestCase):
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_favicon": "/static/dist/assets/icons/icon.png",
"branding_title": "custom",
"branding_custom_css": "",
"matched_domain": "bar.baz",
"ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC,
@ -59,6 +61,7 @@ class TestBrands(APITestCase):
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_favicon": "/static/dist/assets/icons/icon.png",
"branding_title": "authentik",
"branding_custom_css": "",
"matched_domain": "fallback",
"ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC,
@ -121,3 +124,27 @@ class TestBrands(APITestCase):
"subject": None,
},
)
def test_branding_url(self):
"""Test branding attributes return correct values"""
brand = create_test_brand()
brand.branding_default_flow_background = "https://goauthentik.io/img/icon.png"
brand.branding_favicon = "https://goauthentik.io/img/icon.png"
brand.branding_logo = "https://goauthentik.io/img/icon.png"
brand.save()
self.assertEqual(
brand.branding_default_flow_background_url(), "https://goauthentik.io/img/icon.png"
)
self.assertJSONEqual(
self.client.get(reverse("authentik_api:brand-current")).content.decode(),
{
"branding_logo": "https://goauthentik.io/img/icon.png",
"branding_favicon": "https://goauthentik.io/img/icon.png",
"branding_title": "authentik",
"branding_custom_css": "",
"matched_domain": brand.domain,
"ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC,
"default_locale": "",
},
)

View File

@ -16,7 +16,7 @@
{% block head_before %}
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
<style>{{ brand.branding_custom_css }}</style>
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
{% block head %}

View File

@ -4,7 +4,7 @@
{% load i18n %}
{% block head_before %}
<link rel="prefetch" href="{% static 'dist/assets/images/flow_background.jpg' %}" />
<link rel="prefetch" href="{{ request.brand.branding_default_flow_background_url }}" />
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
{% include "base/header_js.html" %}
@ -13,7 +13,7 @@
{% block head %}
<style>
:root {
--ak-flow-background: url("{% static 'dist/assets/images/flow_background.jpg' %}");
--ak-flow-background: url("{{ request.brand.branding_default_flow_background_url }}");
--pf-c-background-image--BackgroundImage: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background);

View File

@ -50,7 +50,8 @@ class NotificationTransportSerializer(ModelSerializer):
"mode",
"mode_verbose",
"webhook_url",
"webhook_mapping",
"webhook_mapping_body",
"webhook_mapping_headers",
"send_once",
]

View File

@ -0,0 +1,43 @@
# Generated by Django 5.0.13 on 2025-03-20 19:54
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0008_event_authentik_e_expires_8c73a8_idx_and_more"),
]
operations = [
migrations.RenameField(
model_name="notificationtransport",
old_name="webhook_mapping",
new_name="webhook_mapping_body",
),
migrations.AlterField(
model_name="notificationtransport",
name="webhook_mapping_body",
field=models.ForeignKey(
default=None,
help_text="Customize the body of the request. Mapping should return data that is JSON-serializable.",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
related_name="+",
to="authentik_events.notificationwebhookmapping",
),
),
migrations.AddField(
model_name="notificationtransport",
name="webhook_mapping_headers",
field=models.ForeignKey(
default=None,
help_text="Configure additional headers to be sent. Mapping should return a dictionary of key-value pairs",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
related_name="+",
to="authentik_events.notificationwebhookmapping",
),
),
]

View File

@ -336,8 +336,27 @@ class NotificationTransport(SerializerModel):
mode = models.TextField(choices=TransportMode.choices, default=TransportMode.LOCAL)
webhook_url = models.TextField(blank=True, validators=[DomainlessURLValidator()])
webhook_mapping = models.ForeignKey(
"NotificationWebhookMapping", on_delete=models.SET_DEFAULT, null=True, default=None
webhook_mapping_body = models.ForeignKey(
"NotificationWebhookMapping",
on_delete=models.SET_DEFAULT,
null=True,
default=None,
related_name="+",
help_text=_(
"Customize the body of the request. "
"Mapping should return data that is JSON-serializable."
),
)
webhook_mapping_headers = models.ForeignKey(
"NotificationWebhookMapping",
on_delete=models.SET_DEFAULT,
null=True,
default=None,
related_name="+",
help_text=_(
"Configure additional headers to be sent. "
"Mapping should return a dictionary of key-value pairs"
),
)
send_once = models.BooleanField(
default=False,
@ -360,8 +379,8 @@ class NotificationTransport(SerializerModel):
def send_local(self, notification: "Notification") -> list[str]:
"""Local notification delivery"""
if self.webhook_mapping:
self.webhook_mapping.evaluate(
if self.webhook_mapping_body:
self.webhook_mapping_body.evaluate(
user=notification.user,
request=None,
notification=notification,
@ -380,9 +399,18 @@ class NotificationTransport(SerializerModel):
if notification.event and notification.event.user:
default_body["event_user_email"] = notification.event.user.get("email", None)
default_body["event_user_username"] = notification.event.user.get("username", None)
if self.webhook_mapping:
headers = {}
if self.webhook_mapping_body:
default_body = sanitize_item(
self.webhook_mapping.evaluate(
self.webhook_mapping_body.evaluate(
user=notification.user,
request=None,
notification=notification,
)
)
if self.webhook_mapping_headers:
headers = sanitize_item(
self.webhook_mapping_headers.evaluate(
user=notification.user,
request=None,
notification=notification,
@ -392,6 +420,7 @@ class NotificationTransport(SerializerModel):
response = get_http_session().post(
self.webhook_url,
json=default_body,
headers=headers,
)
response.raise_for_status()
except RequestException as exc:

View File

@ -120,7 +120,7 @@ class TestEventsNotifications(APITestCase):
)
transport = NotificationTransport.objects.create(
name=generate_id(), webhook_mapping=mapping, mode=TransportMode.LOCAL
name=generate_id(), webhook_mapping_body=mapping, mode=TransportMode.LOCAL
)
NotificationRule.objects.filter(name__startswith="default").delete()
trigger = NotificationRule.objects.create(name=generate_id(), group=self.group)

View File

@ -60,20 +60,25 @@ class TestEventTransports(TestCase):
def test_transport_webhook_mapping(self):
"""Test webhook transport with custom mapping"""
mapping = NotificationWebhookMapping.objects.create(
mapping_body = NotificationWebhookMapping.objects.create(
name=generate_id(), expression="return request.user"
)
mapping_headers = NotificationWebhookMapping.objects.create(
name=generate_id(), expression="""return {"foo": "bar"}"""
)
transport: NotificationTransport = NotificationTransport.objects.create(
name=generate_id(),
mode=TransportMode.WEBHOOK,
webhook_url="http://localhost:1234/test",
webhook_mapping=mapping,
webhook_mapping_body=mapping_body,
webhook_mapping_headers=mapping_headers,
)
with Mocker() as mocker:
mocker.post("http://localhost:1234/test")
transport.send(self.notification)
self.assertEqual(mocker.call_count, 1)
self.assertEqual(mocker.request_history[0].method, "POST")
self.assertEqual(mocker.request_history[0].headers["foo"], "bar")
self.assertJSONEqual(
mocker.request_history[0].body.decode(),
{"email": self.user.email, "pk": self.user.pk, "username": self.user.username},

View File

@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
from uuid import uuid4
from django.db import models
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _
from model_utils.managers import InheritanceManager
from rest_framework.serializers import BaseSerializer
@ -178,14 +179,11 @@ class Flow(SerializerModel, PolicyBindingModel):
help_text=_("Required level of authentication and authorization to access a flow."),
)
@property
def background_url(self) -> str:
def background_url(self, request: HttpRequest) -> str:
"""Get the URL to the background image. If the name is /static or starts with http
it is returned as-is"""
if not self.background:
return (
CONFIG.get("web.path", "/")[:-1] + "/static/dist/assets/images/flow_background.jpg"
)
return request.brand.branding_default_flow_background_url()
if self.background.name.startswith("http"):
return self.background.name
if self.background.name.startswith("/static"):

View File

@ -184,7 +184,7 @@ class ChallengeStageView(StageView):
flow_info = ContextualFlowInfo(
data={
"title": self.format_title(),
"background": self.executor.flow.background_url,
"background": self.executor.flow.background_url(self.request),
"cancel_url": reverse("authentik_flows:cancel"),
"layout": self.executor.flow.layout,
}

View File

@ -27,7 +27,6 @@ class FlowTestCase(APITestCase):
self.assertIsNotNone(raw_response["component"])
if flow:
self.assertIn("flow_info", raw_response)
self.assertEqual(raw_response["flow_info"]["background"], flow.background_url)
self.assertEqual(
raw_response["flow_info"]["cancel_url"], reverse("authentik_flows:cancel")
)

View File

@ -49,7 +49,7 @@ class TestFlowInspector(APITestCase):
"captcha_stage": None,
"component": "ak-stage-identification",
"flow_info": {
"background": flow.background_url,
"background": "/static/dist/assets/images/flow_background.jpg",
"cancel_url": reverse("authentik_flows:cancel"),
"title": flow.title,
"layout": "stacked",

View File

@ -45,6 +45,8 @@ redis:
# url: ""
# transport_options: ""
http_timeout: 30
cache:
# url: ""
timeout: 300

View File

@ -16,7 +16,40 @@ def authentik_user_agent() -> str:
return f"authentik@{get_full_version()}"
class DebugSession(Session):
class TimeoutSession(Session):
"""Always set a default HTTP request timeout"""
def __init__(self, default_timeout=None):
super().__init__()
self.timeout = default_timeout
def send(
self,
request,
*,
stream=...,
verify=...,
proxies=...,
cert=...,
timeout=...,
allow_redirects=...,
**kwargs,
):
if not timeout and self.timeout:
timeout = self.timeout
return super().send(
request,
stream=stream,
verify=verify,
proxies=proxies,
cert=cert,
timeout=timeout,
allow_redirects=allow_redirects,
**kwargs,
)
class DebugSession(TimeoutSession):
"""requests session which logs http requests and responses"""
def send(self, req: PreparedRequest, *args, **kwargs):
@ -42,8 +75,9 @@ class DebugSession(Session):
def get_http_session() -> Session:
"""Get a requests session with common headers"""
session = Session()
session = TimeoutSession()
if CONFIG.get_bool("debug") or CONFIG.get("log_level") == "trace":
session = DebugSession()
session.headers["User-Agent"] = authentik_user_agent()
session.timeout = CONFIG.get_optional_int("http_timeout")
return session

View File

@ -13016,6 +13016,15 @@
"minLength": 1,
"title": "Branding favicon"
},
"branding_custom_css": {
"type": "string",
"title": "Branding custom css"
},
"branding_default_flow_background": {
"type": "string",
"minLength": 1,
"title": "Branding default flow background"
},
"flow_authentication": {
"type": "string",
"format": "uuid",
@ -14897,9 +14906,15 @@
"type": "string",
"title": "Webhook url"
},
"webhook_mapping": {
"webhook_mapping_body": {
"type": "integer",
"title": "Webhook mapping"
"title": "Webhook mapping body",
"description": "Customize the body of the request. Mapping should return data that is JSON-serializable."
},
"webhook_mapping_headers": {
"type": "integer",
"title": "Webhook mapping headers",
"description": "Configure additional headers to be sent. Mapping should return a dictionary of key-value pairs"
},
"send_once": {
"type": "boolean",

6
go.mod
View File

@ -11,7 +11,7 @@ require (
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
github.com/go-ldap/ldap/v3 v3.4.10
github.com/go-openapi/runtime v0.28.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0
github.com/gorilla/handlers v1.5.2
github.com/gorilla/mux v1.8.1
@ -23,13 +23,13 @@ require (
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
github.com/pires/go-proxyproto v0.8.0
github.com/prometheus/client_golang v1.21.1
github.com/redis/go-redis/v9 v9.7.1
github.com/redis/go-redis/v9 v9.7.3
github.com/sethvargo/go-envconfig v1.1.1
github.com/sirupsen/logrus v1.9.3
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.2025022.3
goauthentik.io/api/v3 v3.2025022.5
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.28.0
golang.org/x/sync v0.12.0

12
go.sum
View File

@ -113,8 +113,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -248,8 +248,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc=
github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
@ -299,8 +299,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.2025022.3 h1:cipaxl0il4/s1fU2f6+CD7nzgAktbV0XD7r5qHh0fUc=
goauthentik.io/api/v3 v3.2025022.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2025022.5 h1:+yvpWu7BNvd+EXAbv6PCgjEv1UwEm9yYVIIc7d3fIlM=
goauthentik.io/api/v3 v3.2025022.5/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,6 @@
<link rel="shortcut icon" type="image/png" href="/outpost.goauthentik.io/static/dist/assets/icons/icon.png">
<link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/patternfly.min.css">
<link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/authentik.css">
<link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/custom.css">
<link rel="prefetch" href="/outpost.goauthentik.io/static/dist/assets/images/flow_background.jpg" />
<style>
.pf-c-background-image::before {

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
"POT-Creation-Date: 2025-03-22 00:10+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"
@ -616,6 +616,18 @@ msgstr ""
msgid "Email"
msgstr ""
#: authentik/events/models.py
msgid ""
"Customize the body of the request. Mapping should return data that is JSON-"
"serializable."
msgstr ""
#: authentik/events/models.py
msgid ""
"Configure additional headers to be sent. Mapping should return a dictionary "
"of key-value pairs"
msgstr ""
#: authentik/events/models.py
msgid ""
"Only send notification once, for example when sending a webhook into a chat "
@ -1756,6 +1768,17 @@ msgid ""
"NameIDPolicy of the incoming request will be considered"
msgstr ""
#: authentik/providers/saml/models.py
msgid "AuthnContextClassRef Property Mapping"
msgstr ""
#: authentik/providers/saml/models.py
msgid ""
"Configure how the AuthnContextClassRef value will be created. When left "
"empty, the AuthnContextClassRef will be set based on which authentication "
"methods the user used to authenticate."
msgstr ""
#: authentik/providers/saml/models.py
msgid ""
"Assertion valid not before current time + this value (Format: hours=-1;"

View File

@ -19,7 +19,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
"POT-Creation-Date: 2025-03-20 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Marc Schmitt, 2025\n"
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
@ -1956,6 +1956,20 @@ msgstr ""
"Configure la manière dont la valeur NameID sera créée. Si laissé vide, la "
"NameIDPolicy de la requête entrante sera prise en compte"
#: authentik/providers/saml/models.py
msgid "AuthnContextClassRef Property Mapping"
msgstr "Mappage de propriété AuthnContextClassRef"
#: authentik/providers/saml/models.py
msgid ""
"Configure how the AuthnContextClassRef value will be created. When left "
"empty, the AuthnContextClassRef will be set based on which authentication "
"methods the user used to authenticate."
msgstr ""
"Configure comment la valeur AuthnContextClassRef sera créée. Lorsque non "
"sélectionné, AuthnContextClassRef sera défini en fonction de quelle méthode "
"d'authentification l'utilisateur a utilisé pour s'authentifier."
#: authentik/providers/saml/models.py
msgid ""
"Assertion valid not before current time + this value (Format: "

Binary file not shown.

View File

@ -15,7 +15,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
"POT-Creation-Date: 2025-03-20 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2025\n"
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
@ -1782,6 +1782,18 @@ msgid ""
"NameIDPolicy of the incoming request will be considered"
msgstr "配置如何创建 NameID 值。如果留空,将考虑传入请求的 NameIDPolicy"
#: authentik/providers/saml/models.py
msgid "AuthnContextClassRef Property Mapping"
msgstr "AuthnContextClassRef 属性映射"
#: authentik/providers/saml/models.py
msgid ""
"Configure how the AuthnContextClassRef value will be created. When left "
"empty, the AuthnContextClassRef will be set based on which authentication "
"methods the user used to authenticate."
msgstr ""
"配置如何创建 AuthnContextClassRef 值。留空时,AuthnContextClassRef 会基于用户使用的身份验证方式设置。"
#: authentik/providers/saml/models.py
msgid ""
"Assertion valid not before current time + this value (Format: "

Binary file not shown.

View File

@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-13 00:10+0000\n"
"POT-Creation-Date: 2025-03-20 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2025\n"
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
@ -1781,6 +1781,18 @@ msgid ""
"NameIDPolicy of the incoming request will be considered"
msgstr "配置如何创建 NameID 值。如果留空,将考虑传入请求的 NameIDPolicy"
#: authentik/providers/saml/models.py
msgid "AuthnContextClassRef Property Mapping"
msgstr "AuthnContextClassRef 属性映射"
#: authentik/providers/saml/models.py
msgid ""
"Configure how the AuthnContextClassRef value will be created. When left "
"empty, the AuthnContextClassRef will be set based on which authentication "
"methods the user used to authenticate."
msgstr ""
"配置如何创建 AuthnContextClassRef 值。留空时,AuthnContextClassRef 会基于用户使用的身份验证方式设置。"
#: authentik/providers/saml/models.py
msgid ""
"Assertion valid not before current time + this value (Format: "

View File

@ -4447,6 +4447,10 @@ paths:
schema:
type: string
format: uuid
- in: query
name: branding_default_flow_background
schema:
type: string
- in: query
name: branding_favicon
schema:
@ -41145,6 +41149,10 @@ components:
type: string
branding_favicon:
type: string
branding_custom_css:
type: string
branding_default_flow_background:
type: string
flow_authentication:
type: string
format: uuid
@ -41204,6 +41212,11 @@ components:
branding_favicon:
type: string
minLength: 1
branding_custom_css:
type: string
branding_default_flow_background:
type: string
minLength: 1
flow_authentication:
type: string
format: uuid
@ -42096,6 +42109,8 @@ components:
type: string
branding_favicon:
type: string
branding_custom_css:
type: string
ui_footer_links:
type: array
items:
@ -42122,6 +42137,7 @@ components:
type: string
readOnly: true
required:
- branding_custom_css
- branding_favicon
- branding_logo
- branding_title
@ -46874,10 +46890,18 @@ components:
webhook_url:
type: string
format: uri
webhook_mapping:
webhook_mapping_body:
type: string
format: uuid
nullable: true
description: Customize the body of the request. Mapping should return data
that is JSON-serializable.
webhook_mapping_headers:
type: string
format: uuid
nullable: true
description: Configure additional headers to be sent. Mapping should return
a dictionary of key-value pairs
send_once:
type: boolean
description: Only send notification once, for example when sending a webhook
@ -46905,10 +46929,18 @@ components:
webhook_url:
type: string
format: uri
webhook_mapping:
webhook_mapping_body:
type: string
format: uuid
nullable: true
description: Customize the body of the request. Mapping should return data
that is JSON-serializable.
webhook_mapping_headers:
type: string
format: uuid
nullable: true
description: Configure additional headers to be sent. Mapping should return
a dictionary of key-value pairs
send_once:
type: boolean
description: Only send notification once, for example when sending a webhook
@ -50125,6 +50157,11 @@ components:
branding_favicon:
type: string
minLength: 1
branding_custom_css:
type: string
branding_default_flow_background:
type: string
minLength: 1
flow_authentication:
type: string
format: uuid
@ -51337,10 +51374,18 @@ components:
webhook_url:
type: string
format: uri
webhook_mapping:
webhook_mapping_body:
type: string
format: uuid
nullable: true
description: Customize the body of the request. Mapping should return data
that is JSON-serializable.
webhook_mapping_headers:
type: string
format: uuid
nullable: true
description: Configure additional headers to be sent. Mapping should return
a dictionary of key-value pairs
send_once:
type: boolean
description: Only send notification once, for example when sending a webhook

View File

@ -47,10 +47,12 @@ class TestProviderOAuth2Github(SeleniumTestCase):
"GF_AUTH_GITHUB_AUTH_URL": self.url(
"authentik_providers_oauth2_root:github-authorize"
),
"GF_AUTH_GITHUB_TOKEN_URL": self.url(
"GF_AUTH_GITHUB_TOKEN_URL": self.host_url(
"authentik_providers_oauth2_root:github-access-token"
),
"GF_AUTH_GITHUB_API_URL": self.url("authentik_providers_oauth2_root:github-user"),
"GF_AUTH_GITHUB_API_URL": self.host_url(
"authentik_providers_oauth2_root:github-user"
),
"GF_LOG_LEVEL": "debug",
},
)

View File

@ -53,8 +53,12 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
"GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET": self.client_secret,
"GF_AUTH_GENERIC_OAUTH_SCOPES": "openid email profile",
"GF_AUTH_GENERIC_OAUTH_AUTH_URL": self.url("authentik_providers_oauth2:authorize"),
"GF_AUTH_GENERIC_OAUTH_TOKEN_URL": self.url("authentik_providers_oauth2:token"),
"GF_AUTH_GENERIC_OAUTH_API_URL": self.url("authentik_providers_oauth2:userinfo"),
"GF_AUTH_GENERIC_OAUTH_TOKEN_URL": self.host_url(
"authentik_providers_oauth2:token"
),
"GF_AUTH_GENERIC_OAUTH_API_URL": self.host_url(
"authentik_providers_oauth2:userinfo"
),
"GF_AUTH_SIGNOUT_REDIRECT_URL": self.url(
"authentik_providers_oauth2:end-session",
application_slug=self.app_slug,

View File

@ -42,7 +42,9 @@ class TestSourceSCIM(SeleniumTestCase):
test_launch = session.post(
"http://localhost:8080/test/run",
data={
"endPoint": self.live_server_url + f"/source/scim/{source.slug}/v2",
"endPoint": self.host_url(
"authentik_sources_scim:v2-root", source_slug=source.slug
),
"username": "foo",
"password": source.token.key,
"jwtToken": None,

View File

@ -2,7 +2,6 @@
import json
import os
import socket
from collections.abc import Callable
from functools import lru_cache, wraps
from os import environ
@ -36,8 +35,8 @@ from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id
RETRIES = int(environ.get("RETRIES", "3"))
IS_CI = "CI" in environ
RETRIES = int(environ.get("RETRIES", "3")) if IS_CI else 1
def get_docker_tag() -> str:
@ -51,13 +50,6 @@ def get_docker_tag() -> str:
return f"gh-{branch_name}"
def get_local_ip() -> str:
"""Get the local machine's IP"""
hostname = socket.gethostname()
ip_addr = socket.gethostbyname(hostname)
return ip_addr
class DockerTestCase(TestCase):
"""Mixin for dealing with containers"""
@ -113,6 +105,9 @@ class DockerTestCase(TestCase):
specs["network"] = self.__network.name
specs["labels"] = self.docker_labels
specs["detach"] = True
specs["extra_hosts"] = {
"host.docker.internal": "host-gateway",
}
if hasattr(self, "live_server_url"):
specs.setdefault("environment", {})
specs["environment"]["AUTHENTIK_HOST"] = self.live_server_url
@ -155,7 +150,7 @@ class DockerTestCase(TestCase):
class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
"""StaticLiveServerTestCase which automatically creates a Webdriver instance"""
host = get_local_ip()
host = "0.0.0.0" # nosec Required for containers to reach us directly on the host
wait_timeout: int
user: User
@ -210,6 +205,15 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
f"URL {self.driver.current_url} doesn't match expected URL {desired_url}",
)
def host_url(self, view, query: dict | None = None, **kwargs) -> str:
"""reverse `view` with `**kwargs` into full URL using live_server_url"""
url = f"http://host.docker.internal:{self.server_thread.port}" + reverse(
view, kwargs=kwargs
)
if query:
return url + "?" + urlencode(query)
return url
def url(self, view, query: dict | None = None, **kwargs) -> str:
"""reverse `view` with `**kwargs` into full URL using live_server_url"""
url = self.live_server_url + reverse(view, kwargs=kwargs)

View File

@ -19,5 +19,5 @@
"importOrder": ["^(@?)lit(.*)$", "\\.css$", "^@goauthentik/api$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"importOrderParserPlugins": ["typescript", "classProperties", "decorators-legacy"]
"importOrderParserPlugins": ["typescript", "jsx", "classProperties", "decorators-legacy"]
}

2335
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@
"name": "@goauthentik/web",
"version": "0.0.0",
"dependencies": {
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-python": "^6.1.6",
@ -11,13 +12,12 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.2.2-1742395408",
"@goauthentik/api": "^2025.2.2-1742585853",
"@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4",
"@lit/task": "^1.0.1",
"@mdx-js/esbuild": "^3.1.0",
"@mdx-js/mdx": "^3.1.0",
"@open-wc/lit-helpers": "^0.7.0",
"@patternfly/elements": "^4.0.2",
@ -26,6 +26,7 @@
"@spotlightjs/spotlight": "^2.4.2",
"@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
"change-case": "^5.4.4",
"chart.js": "^4.4.4",
"chartjs-adapter-date-fns": "^3.0.0",
"codemirror": "^6.0.1",
@ -33,18 +34,28 @@
"core-js": "^3.38.1",
"country-flag-icons": "^1.5.13",
"date-fns": "^4.1.0",
"deepmerge-ts": "^7.1.5",
"dompurify": "^3.2.4",
"fuse.js": "^7.0.0",
"guacamole-common-js": "^1.5.0",
"hastscript": "^9.0.1",
"lit": "^3.2.0",
"md-front-matter": "^1.0.4",
"mdx-mermaid": "^2.0.3",
"mermaid": "^11.4.1",
"rapidoc": "^9.3.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"rehype-highlight": "^7.0.2",
"rehype-mermaid": "^3.0.0",
"rehype-parse": "^9.0.1",
"rehype-stringify": "^10.0.1",
"remark-directive": "^4.0.0",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1",
"remark-mdx-frontmatter": "^5.0.0",
"style-mod": "^4.1.2",
"ts-pattern": "^5.4.0",
"unist-util-visit": "^5.0.0",
"webcomponent-qr-code": "^1.2.0",
"yaml": "^2.5.1"
},
@ -71,14 +82,16 @@
"@types/mocha": "^10.0.8",
"@types/node": "^22.7.4",
"@types/react": "^18.3.13",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.8.0",
"@typescript-eslint/parser": "^8.8.0",
"@wdio/browser-runner": "9.4",
"@wdio/cli": "9.4",
"@wdio/spec-reporter": "^9.1.2",
"change-case": "^5.4.4",
"chromedriver": "^131.0.1",
"esbuild": "^0.25.0",
"esbuild-plugin-polyfill-node": "^0.3.0",
"esbuild-plugins-node-modules-polyfill": "^1.7.0",
"eslint": "^9.11.1",
"eslint-plugin-lit": "^1.15.0",
"eslint-plugin-wc": "^2.1.1",
@ -91,13 +104,6 @@
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"pseudolocale": "^2.1.0",
"rehype-highlight": "^7.0.2",
"rehype-parse": "^9.0.1",
"rehype-stringify": "^10.0.1",
"remark-directive": "^4.0.0",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1",
"remark-mdx-frontmatter": "^5.0.0",
"rollup-plugin-modify": "^3.0.0",
"rollup-plugin-postcss-lit": "^2.1.0",
"storybook": "^8.3.4",
@ -182,7 +188,6 @@
"./dist/enterprise/**",
"./dist/poly-*.js",
"./dist/poly-*.js.map",
"./dist/custom.css",
"./dist/theme-dark.css",
"./dist/one-dark.css",
"./dist/patternfly.min.css"

View File

@ -1,5 +1,7 @@
import { execFileSync } from "child_process";
import { deepmerge } from "deepmerge-ts";
import esbuild from "esbuild";
import { polyfillNode } from "esbuild-plugin-polyfill-node";
import findFreePorts from "find-free-ports";
import { copyFileSync, mkdirSync, readFileSync, statSync } from "fs";
import { globSync } from "glob";
@ -52,7 +54,6 @@ const definitions = Object.fromEntries(
const assetsFileMappings = [
["node_modules/@patternfly/patternfly/patternfly.min.css", "."],
["node_modules/@patternfly/patternfly/assets/**", ".", "node_modules/@patternfly/patternfly/"],
["src/custom.css", "."],
["src/common/styles/**", "."],
["src/assets/images/**", "./assets/images"],
["./icons/*", "./assets/icons"],
@ -109,7 +110,7 @@ const entryPoints = [
];
/**
* @satisfies {import("esbuild").BuildOptions}
* @type {import("esbuild").BuildOptions}
*/
const BASE_ESBUILD_OPTIONS = {
bundle: true,
@ -122,11 +123,19 @@ const BASE_ESBUILD_OPTIONS = {
tsconfig: "./tsconfig.json",
loader: {
".css": "text",
".md": "text",
},
plugins: [
polyfillNode({
polyfills: {
path: true,
},
}),
mdxPlugin({
root: authentikProjectRoot,
}),
],
define: definitions,
format: "esm",
plugins: [mdxPlugin()],
logOverride: {
/**
* HACK: Silences issue originating in ESBuild.
@ -163,22 +172,33 @@ function composeVersionID() {
function createEntryPointOptions([source, dest], overrides = {}) {
const outdir = path.join(__dirname, "..", "dist", dest);
return {
...BASE_ESBUILD_OPTIONS,
/**
* @type {esbuild.BuildOptions}
*/
const entryPointConfig = {
entryPoints: [`./src/${source}`],
entryNames: `[dir]/[name]-${composeVersionID()}`,
publicPath: path.join("/static", "dist", dest),
outdir,
...overrides,
};
/**
* @type {esbuild.BuildOptions}
*/
const mergedConfig = deepmerge(BASE_ESBUILD_OPTIONS, entryPointConfig, overrides);
return mergedConfig;
}
/**
* Build all entry points in parallel.
*
* @param {EntryPoint[]} entryPoints
* @returns {Promise<esbuild.BuildResult[]>}
*/
async function buildParallel(entryPoints) {
await Promise.allSettled(
return Promise.all(
entryPoints.map((entryPoint) => {
return esbuild.build(createEntryPointOptions(entryPoint));
}),
@ -210,7 +230,6 @@ async function doWatch() {
return esbuild.context(
createEntryPointOptions(entryPoint, {
plugins: [
...BASE_ESBUILD_OPTIONS.plugins,
buildObserverPlugin({
serverURL,
logPrefix: entryPoint[1],

View File

@ -1,44 +1,13 @@
/**
* @import {Options as HighlightOptions} from 'rehype-highlight'
* @import {CompileOptions} from '@mdx-js/mdx'
* @import {mdxmermaid} from 'mdx-mermaid'
* @import {Message,
* @import {
OnLoadArgs,
OnLoadResult,
Plugin,
PluginBuild
* } from 'esbuild'
*/
import { run as runMDX } from "@mdx-js/mdx";
import { createFormatAwareProcessors } from "@mdx-js/mdx/internal-create-format-aware-processors";
import { extnamesToRegex } from "@mdx-js/mdx/internal-extnames-to-regex";
import apacheGrammar from "highlight.js/lib/languages/apache";
import diffGrammar from "highlight.js/lib/languages/diff";
import confGrammar from "highlight.js/lib/languages/ini";
import nginxGrammar from "highlight.js/lib/languages/nginx";
import { common } from "lowlight";
import mdxMermaid from "mdx-mermaid";
import { Mermaid } from "mdx-mermaid/lib/Mermaid";
import assert from "node:assert";
import fs from "node:fs/promises";
import path from "node:path";
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import * as runtime from "react/jsx-runtime";
import rehypeHighlight from "rehype-highlight";
import remarkDirective from "remark-directive";
import remarkFrontmatter from "remark-frontmatter";
import remarkGFM from "remark-gfm";
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
import remarkParse from "remark-parse";
import { SourceMapGenerator } from "source-map";
import { VFile } from "vfile";
import { VFileMessage } from "vfile-message";
import { remarkAdmonition } from "./remark/remark-admonition.mjs";
import { remarkHeadings } from "./remark/remark-headings.mjs";
import { remarkLinks } from "./remark/remark-links.mjs";
import { remarkLists } from "./remark/remark-lists.mjs";
import * as fs from "node:fs/promises";
import * as path from "node:path";
/**
* @typedef {Omit<OnLoadArgs, 'pluginData'> & LoadDataFields} LoadData
@ -49,77 +18,28 @@ import { remarkLists } from "./remark/remark-lists.mjs";
* @property {PluginData | null | undefined} [pluginData]
* Plugin data.
*
* @typedef {CompileOptions} Options
* Configuration.
*
* Options are the same as `compile` from `@mdx-js/mdx`.
*
* @typedef PluginData
* Extra data passed.
* @property {Buffer | string | null | undefined} [contents]
* File contents.
*
* @typedef State
* Info passed around.
* @property {string} doc
* File value.
* @property {string} name
* Plugin name.
* @property {string} path
* File path.
*/
const eol = /\r\n|\r|\n|\u2028|\u2029/g;
const name = "@mdx-js/esbuild";
const name = "mdx-plugin";
/**
* Compile MDX to HTML.
* *
* @param {Readonly<Options> | null | undefined} [mdxOptions]
* Configuration (optional).
* @return {Plugin}
* Plugin.
* @typedef MDXPluginOptions
*
* @property {string} root Root directory.
*/
export function mdxPlugin(mdxOptions) {
/** @type {mdxmermaid.Config} */
const mermaidConfig = {
output: "svg",
};
/**
* @type {HighlightOptions}
*/
const highlightThemeOptions = {
languages: {
...common,
nginx: nginxGrammar,
apache: apacheGrammar,
conf: confGrammar,
diff: diffGrammar,
},
};
const { extnames, process } = createFormatAwareProcessors({
...mdxOptions,
SourceMapGenerator,
outputFormat: "function-body",
remarkPlugins: [
remarkParse,
remarkDirective,
remarkAdmonition,
remarkGFM,
remarkFrontmatter,
remarkMdxFrontmatter,
remarkHeadings,
remarkLinks,
remarkLists,
[mdxMermaid, mermaidConfig],
],
rehypePlugins: [[rehypeHighlight, highlightThemeOptions]],
});
/**
* Bundle MDX into JSON modules.
*
* @param {MDXPluginOptions} options Options.
* @returns {Plugin} Plugin.
*/
export function mdxPlugin({ root }) {
return { name, setup };
/**
@ -129,7 +49,7 @@ export function mdxPlugin(mdxOptions) {
* Nothing.
*/
function setup(build) {
build.onLoad({ filter: extnamesToRegex(extnames) }, onload);
build.onLoad({ filter: /\.mdx?$/ }, onload);
/**
* @param {LoadData} data
@ -138,7 +58,7 @@ export function mdxPlugin(mdxOptions) {
* Result.
*/
async function onload(data) {
const document = String(
const content = String(
data.pluginData &&
data.pluginData.contents !== null &&
data.pluginData.contents !== undefined
@ -146,154 +66,16 @@ export function mdxPlugin(mdxOptions) {
: await fs.readFile(data.path),
);
/** @type {State} */
const state = {
doc: document,
name,
path: data.path,
};
let file = new VFile({
path: data.path,
value: document,
});
/** @type {string | undefined} */
let value;
/** @type {Array<VFileMessage>} */
let messages = [];
/** @type {Array<Message>} */
const errors = [];
/** @type {Array<Message>} */
const warnings = [];
/**
* @type {React.ComponentType<{children: React.ReactNode, frontmatter: Record<string, string>}>}
*/
const wrapper = ({ children, frontmatter }) => {
const title = frontmatter.title;
const nextChildren = React.Children.toArray(children);
if (title) {
nextChildren.unshift(React.createElement("h1", { key: "title" }, title));
}
return React.createElement(React.Fragment, null, nextChildren);
};
try {
file = await process(file);
const { default: Content, ...mdxExports } = await runMDX(file, {
...runtime,
useMDXComponents: () => {
return {
mermaid: Mermaid,
Mermaid,
};
},
baseUrl: import.meta.url,
});
const { frontmatter = {} } = mdxExports;
const result = renderToStaticMarkup(
Content({
frontmatter,
components: {
wrapper,
},
}),
);
value = result;
messages = file.messages;
} catch (error_) {
const cause = /** @type {VFileMessage | Error} */ (error_);
console.error(cause);
const message =
"reason" in cause
? cause
: new VFileMessage("Cannot process MDX file with esbuild", {
cause,
ruleId: "process-error",
source: "@mdx-js/esbuild",
});
message.fatal = true;
messages.push(message);
}
for (const message of messages) {
const list = message.fatal ? errors : warnings;
list.push(vfileMessageToEsbuild(state, message));
}
// Safety check: the file has a path, so there has to be a `dirname`.
assert(file.dirname, "expected `dirname` to be defined");
const publicPath = path.resolve(
"/",
path.relative(path.join(root, "website"), data.path),
);
const publicDirectory = path.dirname(publicPath);
return {
contents: value || "",
loader: "text",
errors,
resolveDir: path.resolve(file.cwd, file.dirname),
warnings,
contents: JSON.stringify({ content, publicPath, publicDirectory }),
loader: "file",
};
}
}
}
/**
* @param {Readonly<State>} state
* Info passed around.
* @param {Readonly<VFileMessage>} message
* VFile message or error.
* @returns {Message}
* ESBuild message.
*/
function vfileMessageToEsbuild(state, message) {
const place = message.place;
const start = place ? ("start" in place ? place.start : place) : undefined;
const end = place && "end" in place ? place.end : undefined;
let length = 0;
let lineStart = 0;
let line = 0;
let column = 0;
if (start && start.offset !== undefined) {
line = start.line;
column = start.column - 1;
lineStart = start.offset - column;
length = 1;
if (end && end.offset !== undefined) {
length = end.offset - start.offset;
}
}
eol.lastIndex = lineStart;
const match = eol.exec(state.doc);
const lineEnd = match ? match.index : state.doc.length;
return {
detail: message,
id: "",
location: {
column,
file: state.path,
length: Math.min(length, lineEnd),
line,
lineText: state.doc.slice(lineStart, lineEnd),
namespace: "file",
suggestion: "",
},
notes: [],
pluginName: state.name,
text: message.reason,
};
}

View File

@ -1,59 +0,0 @@
/**
* @import {Plugin} from 'unified'
* @import {} from 'mdast-util-directive'
* @import {} from 'mdast-util-to-hast'
* @import {Root, Link} from 'mdast'
* @import {VFile} from 'vfile'
*/
import * as path from "node:path";
import { visit } from "unist-util-visit";
const DOCS_DOMAIN = "https://goauthentik.io";
/**
* Remark plugin to process links
* @type {Plugin<[unknown], Root, VFile>}
*/
export const remarkLinks = () => {
return function transformer(tree, file) {
const docsRoot = path.resolve(file.cwd, "..", "website");
/**
* @param {Link} node
*/
const visitor = (node) => {
node.data = node.data || {};
if (node.url.startsWith("#")) {
node.data.hProperties = {
className: "markdown-heading",
};
return;
}
node.data.hProperties = {
...node.data.hProperties,
rel: "noopener noreferrer",
target: "_blank",
};
if (node.url.startsWith(".") && file.dirname) {
const nextPathname = path.resolve(
"/",
path.relative(docsRoot, file.dirname),
node.url,
);
const nextURL = new URL(nextPathname, DOCS_DOMAIN);
// Remove trailing .md and .mdx, and trailing "index".
nextURL.pathname = nextURL.pathname.replace(/(index)?\.mdx?$/, "");
node.data.hProperties.href = nextURL.toString();
}
};
// @ts-ignore - visit cannot infer the type of the visitor.
visit(tree, "link", visitor);
};
};

View File

@ -5,7 +5,6 @@ import "@goauthentik/components/events/ObjectChangelog";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/Markdown";
import "@goauthentik/elements/PageHeader";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/ModalButton";

View File

@ -3,7 +3,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import MDApplication from "@goauthentik/docs/add-secure-apps/applications/index.md";
import "@goauthentik/elements/AppIcon.js";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import "@goauthentik/elements/Markdown";
import "@goauthentik/elements/ak-mdx";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
@ -89,7 +89,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
return html`<div class="pf-c-sidebar__panel pf-m-width-25">
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-markdown .content=${MDApplication}></ak-markdown>
<ak-mdx .url=${MDApplication}></ak-mdx>
</div>
</div>
</div>`;

View File

@ -94,7 +94,7 @@ export class ApplicationEntitlementsPage extends Table<ApplicationEntitlement> {
}
renderExpanded(item: ApplicationEntitlement): TemplateResult {
return html` <td></td>
return html`<td></td>
<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<div class="pf-c-content">

View File

@ -51,11 +51,7 @@ export class BrandForm extends ModelForm<Brand, string> {
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal
label=${msg("Domain")}
?required=${true}
name="domain"
>
return html` <ak-form-element-horizontal label=${msg("Domain")} required name="domain">
<input
type="text"
value="${first(this.instance?.domain, window.location.host)}"
@ -90,14 +86,10 @@ export class BrandForm extends ModelForm<Brand, string> {
</p>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<ak-form-group>
<span slot="header"> ${msg("Branding settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Title")}
?required=${true}
name="brandingTitle"
>
<ak-form-element-horizontal label=${msg("Title")} required name="brandingTitle">
<input
type="text"
value="${first(
@ -111,11 +103,7 @@ export class BrandForm extends ModelForm<Brand, string> {
${msg("Branding shown in page title and several other places.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Logo")}
?required=${true}
name="brandingLogo"
>
<ak-form-element-horizontal label=${msg("Logo")} required name="brandingLogo">
<input
type="text"
value="${first(this.instance?.brandingLogo, DefaultBrand.brandingLogo)}"
@ -130,7 +118,7 @@ export class BrandForm extends ModelForm<Brand, string> {
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Favicon")}
?required=${true}
required
name="brandingFavicon"
>
<input
@ -148,6 +136,45 @@ export class BrandForm extends ModelForm<Brand, string> {
${msg("Icon shown in the browser tab.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Default flow background")}
?required=${true}
name="brandingDefaultFlowBackground"
>
<input
type="text"
value="${first(
this.instance?.brandingDefaultFlowBackground,
"/static/dist/assets/images/flow_background.jpg",
)}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"Default background used during flow execution. Can be overridden per flow.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Custom CSS")}
required
name="brandingCustomCss"
>
<ak-codemirror
mode=${CodeMirrorMode.CSS}
value="${first(
this.instance?.brandingCustomCss,
DefaultBrand.brandingCustomCss,
)}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">
${msg("Custom CSS to apply to pages when this brand is active.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>

View File

@ -66,7 +66,7 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
return html` <ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
@ -74,7 +74,7 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Mode")} ?required=${true} name="mode">
<ak-form-element-horizontal label=${msg("Mode")} required name="mode">
<ak-radio
@change=${(ev: CustomEvent<{ value: NotificationTransportModeEnum }>) => {
this.onModeChange(ev.detail.value);
@ -106,7 +106,7 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
?hidden=${!this.showWebhook}
label=${msg("Webhook URL")}
name="webhookUrl"
?required=${true}
required
>
<input
type="text"
@ -116,8 +116,8 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
</ak-form-element-horizontal>
<ak-form-element-horizontal
?hidden=${!this.showWebhook}
label=${msg("Webhook Mapping")}
name="webhookMapping"
label=${msg("Webhook Body Mapping")}
name="webhookMappingBody"
>
<ak-search-select
.fetchObjects=${async (
@ -141,9 +141,42 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
return item?.pk;
}}
.selected=${(item: NotificationWebhookMapping): boolean => {
return this.instance?.webhookMapping === item.pk;
return this.instance?.webhookMappingBody === item.pk;
}}
?blankable=${true}
blankable
>
</ak-search-select>
</ak-form-element-horizontal>
<ak-form-element-horizontal
?hidden=${!this.showWebhook}
label=${msg("Webhook Header Mapping")}
name="webhookMappingHeaders"
>
<ak-search-select
.fetchObjects=${async (
query?: string,
): Promise<NotificationWebhookMapping[]> => {
const args: PropertymappingsNotificationListRequest = {
ordering: "name",
};
if (query !== undefined) {
args.search = query;
}
const items = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsNotificationList(args);
return items.results;
}}
.renderElement=${(item: NotificationWebhookMapping): string => {
return item.name;
}}
.value=${(item: NotificationWebhookMapping | undefined): string | undefined => {
return item?.pk;
}}
.selected=${(item: NotificationWebhookMapping): boolean => {
return this.instance?.webhookMappingHeaders === item.pk;
}}
blankable
>
</ak-search-select>
</ak-form-element-horizontal>

View File

@ -7,11 +7,10 @@ import { EVENT_REFRESH } from "@goauthentik/common/constants";
import "@goauthentik/components/ak-status-label";
import "@goauthentik/components/events/ObjectChangelog";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/Markdown";
import "@goauthentik/elements/SyncStatusCard";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/ModalButton";
import "@goauthentik/elements/sync/SyncStatusCard";
import { msg } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";

View File

@ -6,7 +6,6 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import "@goauthentik/components/events/ObjectChangelog";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/Markdown";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/ModalButton";

View File

@ -8,8 +8,8 @@ import MDProviderOAuth2 from "@goauthentik/docs/add-secure-apps/providers/oauth2
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/Markdown";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/ak-mdx";
import "@goauthentik/elements/buttons/ModalButton";
import "@goauthentik/elements/buttons/SpinnerButton";
@ -357,20 +357,20 @@ export class OAuth2ProviderViewPage extends AKElement {
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-12-col-on-2xl"
>
<div class="pf-c-card__body">
<ak-markdown
.content=${MDProviderOAuth2}
<ak-mdx
.url=${MDProviderOAuth2}
.replacers=${[
(input: string) => {
if (!this.provider) {
return input;
}
return input.replaceAll(
"&lt;application slug&gt;",
"<application slug>",
this.provider.assignedApplicationSlug,
);
},
]}
></ak-markdown>
></ak-mdx>
</div>
</div>
</div>`;

View File

@ -16,10 +16,9 @@ import MDTraefikStandalone from "@goauthentik/docs/add-secure-apps/providers/pro
import MDHeaderAuthentication from "@goauthentik/docs/add-secure-apps/providers/proxy/header_authentication.mdx";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/Markdown";
import "@goauthentik/elements/Markdown";
import { Replacer } from "@goauthentik/elements/Markdown";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/ak-mdx";
import type { Replacer } from "@goauthentik/elements/ak-mdx";
import "@goauthentik/elements/buttons/ModalButton";
import "@goauthentik/elements/buttons/SpinnerButton";
import { getURLParam } from "@goauthentik/elements/router/RouteMatch";
@ -127,37 +126,30 @@ export class ProxyProviderViewPage extends AKElement {
{
label: msg("Nginx (Ingress)"),
md: MDNginxIngress,
meta: "providers/proxy/_nginx_ingress.md",
},
{
label: msg("Nginx (Proxy Manager)"),
md: MDNginxPM,
meta: "providers/proxy/_nginx_proxy_manager.md",
},
{
label: msg("Nginx (standalone)"),
md: MDNginxStandalone,
meta: "providers/proxy/_nginx_standalone.md",
},
{
label: msg("Traefik (Ingress)"),
md: MDTraefikIngress,
meta: "providers/proxy/_traefik_ingress.md",
},
{
label: msg("Traefik (Compose)"),
md: MDTraefikCompose,
meta: "providers/proxy/_traefik_compose.md",
},
{
label: msg("Traefik (Standalone)"),
md: MDTraefikStandalone,
meta: "providers/proxy/_traefik_standalone.md",
},
{
label: msg("Caddy (Standalone)"),
md: MDCaddyStandalone,
meta: "providers/proxy/_caddy_standalone.md",
},
];
const replacers: Replacer[] = [
@ -195,11 +187,7 @@ export class ProxyProviderViewPage extends AKElement {
data-tab-title="${server.label}"
class="pf-c-page__main-section pf-m-no-padding-mobile ak-markdown-section"
>
<ak-markdown
.content=${server.md}
.replacers=${replacers}
meta=${server.meta}
></ak-markdown>
<ak-mdx .url=${server.md} .replacers=${replacers}></ak-mdx>
</section>`;
})}</ak-tabs
>`;
@ -265,10 +253,7 @@ export class ProxyProviderViewPage extends AKElement {
</div>
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__body">
<ak-markdown
.content=${MDHeaderAuthentication}
meta="proxy/header_authentication.md"
></ak-markdown>
<ak-mdx .url=${MDHeaderAuthentication}></ak-mdx>
</div>
</div>
</div>`;

View File

@ -9,11 +9,11 @@ import "@goauthentik/components/ak-status-label";
import "@goauthentik/components/events/ObjectChangelog";
import MDSCIMProvider from "@goauthentik/docs/add-secure-apps/providers/scim/index.md";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/Markdown";
import "@goauthentik/elements/SyncStatusCard";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/ak-mdx";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/ModalButton";
import "@goauthentik/elements/sync/SyncStatusCard";
import { msg } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
@ -243,10 +243,7 @@ export class SCIMProviderViewPage extends AKElement {
</div>
<div class="pf-c-card pf-l-grid__item pf-m-5-col">
<div class="pf-c-card__body">
<ak-markdown
.content=${MDSCIMProvider}
meta="providers/scim/index.md"
></ak-markdown>
<ak-mdx .url=${MDSCIMProvider}></ak-mdx>
</div>
</div>
</div>`;

View File

@ -7,7 +7,6 @@ import "@goauthentik/components/events/ObjectChangelog";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/Markdown";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/ModalButton";
import "@goauthentik/elements/buttons/SpinnerButton";

View File

@ -7,12 +7,12 @@ import "@goauthentik/components/events/ObjectChangelog";
import MDSourceKerberosBrowser from "@goauthentik/docs/users-sources/sources/protocols/kerberos/browser.md";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/Markdown";
import "@goauthentik/elements/SyncStatusCard";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/ak-mdx";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/sync/SyncStatusCard";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
@ -186,11 +186,7 @@ export class KerberosSourceViewPage extends AKElement {
${this.renderSyncCards()}
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__body">
<ak-markdown
.content=${MDSourceKerberosBrowser}
meta="users-sources/protocols/kerberos/browser.md"
;
></ak-markdown>
<ak-mdx .url=${MDSourceKerberosBrowser}></ak-mdx>
</div>
</div>
</div>

View File

@ -6,11 +6,11 @@ import { EVENT_REFRESH } from "@goauthentik/common/constants";
import "@goauthentik/components/events/ObjectChangelog";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/SyncStatusCard";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/sync/SyncStatusCard";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";

View File

@ -18,6 +18,7 @@ export const TAG_SENTRY_CAPABILITIES = "authentik.capabilities";
export async function configureSentry(canDoPpi = false): Promise<Config> {
const cfg = await config();
if (cfg.errorReporting.enabled) {
init({
dsn: cfg.errorReporting.sentryDsn,

View File

@ -49,41 +49,6 @@ html > form > input {
/* #endregion */
/* #region Anchors */
a {
--pf-global--link--Color: var(--pf-global--link--Color--light);
--pf-global--link--Color--hover: var(--pf-global--link--Color--light--hover);
--pf-global--link--Color--visited: var(--pf-global--link--Color);
}
/*
Note that order of anchor pseudo-selectors must follow:
1. link
2. visited
3. hover
4. active
*/
a:link {
color: var(--pf-global--link--Color);
}
a:visited {
color: var(--pf-global--link--Color--visited);
}
a:hover {
color: var(--pf-global--link--Color--hover);
}
a:active {
color: var(--pf-global--link--Color);
}
/* #endregion */
/* #region Icons */
.pf-icon {
@ -228,43 +193,6 @@ a:active {
padding-inline-start: 0;
}
/* #region Mermaid */
svg[id^="mermaid-svg-"] {
.rect {
fill: var(
--ak-mermaid-box-background-color,
var(--pf-global--BackgroundColor--light-300)
) !important;
}
.messageText {
stroke-width: 4;
fill: var(--ak-mermaid-message-text) !important;
paint-order: stroke;
}
}
/* #endregion */
/* #region Tables */
table thead,
table tr:nth-child(2n) {
background-color: var(
--ak-table-stripe-background,
var(--pf-global--BackgroundColor--light-200)
);
}
table td,
table th {
border: var(--pf-table-border-width) solid var(--ifm-table-border-color);
padding: var(--pf-global--spacer--md);
}
/* #endregion */
/* #region Code blocks */
pre:has(.hljs) {

View File

@ -271,9 +271,10 @@ export class EventInfo extends AKElement {
<tbody role="rowgroup">
${Object.keys(diff).map((key) => {
const value = diff[key];
const previousCol = value.previous_value
? JSON.stringify(value.previous_value, null, 4)
: msg("-");
const previousCol =
value.previous_value !== null
? JSON.stringify(value.previous_value, null, 4)
: msg("-");
let newCol = html``;
if (value.add || value.remove) {
newCol = html`<ul class="pf-c-list">

View File

@ -1 +0,0 @@
/* User customisable */

View File

@ -24,26 +24,6 @@ type AkInterface = HTMLElement & {
export const rootInterface = <T extends AkInterface>(): T | undefined =>
(document.body.querySelector("[data-ak-interface-root]") as T) ?? undefined;
let css: Promise<string[]> | undefined;
function fetchCustomCSS(): Promise<string[]> {
if (!css) {
css = Promise.all(
Array.of(...document.head.querySelectorAll<HTMLLinkElement>("link[data-inject]")).map(
(link) => {
return fetch(link.href)
.then((res) => {
return res.text();
})
.finally(() => {
return "";
});
},
),
);
}
return css;
}
export const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)";
// Ensure themes are converted to a static instance of CSS Stylesheet, otherwise the
@ -103,15 +83,12 @@ export class AKElement extends LitElement {
}
async _initCustomCSS(root: DocumentOrShadowRoot): Promise<void> {
const sheets = await fetchCustomCSS();
sheets.map((css) => {
if (css === "") {
return;
}
new CSSStyleSheet().replace(css).then((sheet) => {
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
});
});
const brand = globalAK().brand;
if (!brand) {
return;
}
const sheet = await new CSSStyleSheet().replace(brand.brandingCustomCss);
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
}
_applyTheme(root: DocumentOrShadowRoot, theme?: UiThemeEnum): void {

View File

@ -1,4 +1,5 @@
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
import { css as cssLang } from "@codemirror/lang-css";
import { html as htmlLang } from "@codemirror/lang-html";
import { javascript } from "@codemirror/lang-javascript";
import { python } from "@codemirror/lang-python";
@ -27,6 +28,7 @@ export enum CodeMirrorMode {
XML = "xml",
JavaScript = "javascript",
HTML = "html",
CSS = "css",
Python = "python",
YAML = "yaml",
}
@ -147,6 +149,8 @@ export class CodeMirrorTextarea<T> extends AKElement {
return htmlLang();
case CodeMirrorMode.Python:
return python();
case CodeMirrorMode.CSS:
return cssLang();
case CodeMirrorMode.YAML:
return new LanguageSupport(StreamLanguage.define(yamlMode.yaml));
}

View File

@ -1,86 +0,0 @@
import "@goauthentik/elements/Alert";
import { AKElement } from "@goauthentik/elements/Base";
import { CSSResult, PropertyValues, css, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
export type Replacer = (input: string) => string;
@customElement("ak-markdown")
export class Markdown extends AKElement {
@property()
content: string = "";
@property()
meta: string = "";
@property({ attribute: false })
replacers: Replacer[] = [];
resolvedHTML = "";
static get styles(): CSSResult[] {
return [
PFList,
PFContent,
css`
h2:first-of-type {
margin-top: 0;
}
`,
];
}
protected firstUpdated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
const headingLinks =
this.shadowRoot?.querySelectorAll<HTMLAnchorElement>("a.markdown-heading") ?? [];
for (const headingLink of headingLinks) {
headingLink.addEventListener("click", (ev) => {
ev.preventDefault();
const url = new URL(headingLink.href);
const elementID = url.hash.slice(1);
const target = this.shadowRoot?.getElementById(elementID);
if (!target) {
console.warn(`Element with ID ${elementID} not found`);
return;
}
target.scrollIntoView({
behavior: "smooth",
block: "center",
});
});
}
}
willUpdate(properties: PropertyValues<this>) {
if (properties.has("content")) {
this.resolvedHTML = this.replacers.reduce(
(html, replacer) => replacer(html),
this.content,
);
}
}
render() {
if (!this.content) return nothing;
return unsafeHTML(this.resolvedHTML);
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-markdown": Markdown;
}
}

View File

@ -0,0 +1,53 @@
import { createContext, useContext } from "react";
/**
* A parsed JSON module containing MDX content and metadata from ESBuild.
*/
export interface MDXModule {
/**
* The Markdown content of the module.
*/
content: string;
/**
* The public path of the module, typically identical to the docs page path.
*/
publicPath?: string;
/**
* The public directory of the module, used to resolve relative links.
*/
publicDirectory?: string;
}
/**
* Fetches an MDX module from a URL or ESBuild static asset.
*/
export function fetchMDXModule(url: string | URL): Promise<MDXModule> {
return fetch(url)
.then((response) => {
if (!response.ok) {
throw new Error(`Failed to fetch content: ${response.statusText}`);
}
return response.json();
})
.catch((error) => {
console.error("Error fetching content", error);
return { content: "", publicPath: "", publicDirectory: "" };
});
}
/**
* A context for the current MDX module.
*/
export const MDXModuleContext = createContext<MDXModule>({
content: "",
});
MDXModuleContext.displayName = "MDXModuleContext";
/**
* A hook to access the current MDX module.
*/
export function useMDXModule(): MDXModule {
return useContext(MDXModuleContext);
}

View File

@ -0,0 +1,220 @@
import "@goauthentik/elements/Alert";
import { AKElement } from "@goauthentik/elements/Base";
import {
MDXModule,
MDXModuleContext,
fetchMDXModule,
} from "@goauthentik/elements/ak-mdx/MDXModuleContext";
import { MDXAnchor } from "@goauthentik/elements/ak-mdx/components/MDXAnchor";
import { MDXWrapper } from "@goauthentik/elements/ak-mdx/components/MDXWrapper";
import { remarkAdmonition } from "@goauthentik/elements/ak-mdx/remark/remark-admonition";
import { remarkHeadings } from "@goauthentik/elements/ak-mdx/remark/remark-headings";
import { remarkLists } from "@goauthentik/elements/ak-mdx/remark/remark-lists";
import { compile as compileMDX, run as runMDX } from "@mdx-js/mdx";
import apacheGrammar from "highlight.js/lib/languages/apache";
import diffGrammar from "highlight.js/lib/languages/diff";
import confGrammar from "highlight.js/lib/languages/ini";
import nginxGrammar from "highlight.js/lib/languages/nginx";
import { common } from "lowlight";
import { Root, createRoot } from "react-dom/client";
import * as runtime from "react/jsx-runtime";
import rehypeHighlight from "rehype-highlight";
import { Options as HighlightOptions } from "rehype-highlight";
import rehypeMermaid, { RehypeMermaidOptions } from "rehype-mermaid";
import remarkDirective from "remark-directive";
import remarkFrontmatter from "remark-frontmatter";
import remarkGFM from "remark-gfm";
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
import remarkParse from "remark-parse";
import { CSSResult, css } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
import { UiThemeEnum } from "@goauthentik/api";
const highlightThemeOptions: HighlightOptions = {
languages: {
...common,
nginx: nginxGrammar,
apache: apacheGrammar,
conf: confGrammar,
diff: diffGrammar,
},
};
/**
* A replacer function that can be used to modify the output of the MDX component.
*/
export type Replacer = (input: string) => string;
@customElement("ak-mdx")
export class AKMDX extends AKElement {
@property({
reflect: true,
})
url: string = "";
@property()
content: string = "";
@property({ attribute: false })
replacers: Replacer[] = [];
#reactRoot: Root | null = null;
resolvedHTML = "";
static get styles(): CSSResult[] {
return [
PFList,
PFContent,
css`
a {
--pf-global--link--Color: var(--pf-global--link--Color--light);
--pf-global--link--Color--hover: var(--pf-global--link--Color--light--hover);
--pf-global--link--Color--visited: var(--pf-global--link--Color);
}
/*
Note that order of anchor pseudo-selectors must follow:
1. link
2. visited
3. hover
4. active
*/
a:link {
color: var(--pf-global--link--Color);
}
a:visited {
color: var(--pf-global--link--Color--visited);
}
a:hover {
color: var(--pf-global--link--Color--hover);
}
a:active {
color: var(--pf-global--link--Color);
}
h2:first-of-type {
margin-top: 0;
}
table thead,
table tr:nth-child(2n) {
background-color: var(
--ak-table-stripe-background,
var(--pf-global--BackgroundColor--light-200)
);
}
table td,
table th {
border: var(--pf-table-border-width) solid var(--ifm-table-border-color);
padding: var(--pf-global--spacer--md);
}
pre:has(.hljs) {
padding: var(--pf-global--spacer--md);
}
svg[id^="mermaid-svg-"] {
.rect {
fill: var(
--ak-mermaid-box-background-color,
var(--pf-global--BackgroundColor--light-300)
) !important;
}
.messageText {
stroke-width: 4;
fill: var(--ak-mermaid-message-text) !important;
paint-order: stroke;
}
}
`,
];
}
public async connectedCallback() {
super.connectedCallback();
this.#reactRoot = createRoot(this.shadowRoot!);
let nextMDXModule: MDXModule | undefined;
if (this.url) {
nextMDXModule = await fetchMDXModule(this.url);
} else {
nextMDXModule = {
content: this.content,
};
}
return this.delegateRender(nextMDXModule);
}
protected async delegateRender(mdxModule: MDXModule): Promise<void> {
if (!this.#reactRoot) return;
const normalized = this.replacers.reduce(
(content, replacer) => replacer(content),
mdxModule.content,
);
const mdx = await compileMDX(normalized, {
outputFormat: "function-body",
remarkPlugins: [
remarkParse,
remarkDirective,
remarkAdmonition,
remarkGFM,
remarkFrontmatter,
remarkMdxFrontmatter,
remarkHeadings,
remarkLists,
],
rehypePlugins: [
// ---
[rehypeHighlight, highlightThemeOptions],
[
rehypeMermaid,
{
prefix: "mermaid-svg-",
colorScheme: this.activeTheme === UiThemeEnum.Dark ? "dark" : "light",
} satisfies RehypeMermaidOptions,
],
],
});
const { default: Content, ...mdxExports } = await runMDX(mdx, {
...runtime,
baseUrl: import.meta.url,
});
const { frontmatter = {} } = mdxExports;
this.#reactRoot.render(
<MDXModuleContext.Provider value={mdxModule}>
<Content
frontmatter={frontmatter}
components={{
wrapper: MDXWrapper,
a: MDXAnchor,
}}
/>
</MDXModuleContext.Provider>,
);
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-mdx": AKMDX;
}
}

View File

@ -0,0 +1,61 @@
import { useMDXModule } from "@goauthentik/elements/ak-mdx/MDXModuleContext";
import { resolve } from "node:path";
import React, { memo } from "react";
const DOCS_DOMAIN = "https://goauthentik.io";
/**
* A custom anchor element that applies special behavior for MDX content.
*
* - Resolves relative links to the public directory in the public docs domain.
* - Intercepts local links and scrolls to the target element.
*/
export const MDXAnchor = memo<React.AnchorHTMLAttributes<HTMLAnchorElement>>(
({ href, children, ...props }) => {
const { publicDirectory } = useMDXModule();
if (href?.startsWith(".") && publicDirectory) {
const nextPathname = resolve(publicDirectory, href);
const nextURL = new URL(nextPathname, DOCS_DOMAIN);
// Remove trailing .md and .mdx, and trailing "index".
nextURL.pathname = nextURL.pathname.replace(/(index)?\.mdx?$/, "");
href = nextURL.toString();
}
const interceptHeadingLinks = (event: React.MouseEvent<HTMLAnchorElement>) => {
if (!href || !href.startsWith("#")) return;
event.preventDefault();
const rootNode = event.currentTarget.getRootNode() as ShadowRoot;
const elementID = href.slice(1);
const target = rootNode.getElementById(elementID);
if (!target) {
console.warn(`Element with ID ${elementID} not found`);
return;
}
target.scrollIntoView({
behavior: "smooth",
block: "center",
});
};
return (
<a
href={href}
onClick={interceptHeadingLinks}
rel="noopener noreferrer"
target="_blank"
{...props}
>
{children}
</a>
);
},
);
MDXAnchor.displayName = "MDXAnchor";

View File

@ -0,0 +1,20 @@
import React from "react";
export interface MDXWrapperProps {
children: React.ReactNode;
frontmatter: Record<string, string>;
}
/**
* A wrapper component for MDX content that adds a title if one is provided in the frontmatter.
*/
export const MDXWrapper: React.FC<MDXWrapperProps> = ({ children, frontmatter }) => {
const { title } = frontmatter;
const nextChildren = React.Children.toArray(children);
if (title) {
nextChildren.unshift(<h1 key="header-title">{title}</h1>);
}
return <>{nextChildren}</>;
};

View File

@ -0,0 +1,2 @@
export * from "./ak-mdx";
export { AKMDX as default } from "./ak-mdx";

View File

@ -1,25 +1,18 @@
/**
* @import {Plugin} from 'unified'
* @import {Directives} from 'mdast-util-directive'
* @import {} from 'mdast-util-to-hast'
* @import {Root} from 'mdast'
* @import {VFile} from 'vfile'
*/
import { h } from "hastscript";
import type { Root } from "mdast";
import type { Directives } from "mdast-util-directive";
import type { Plugin } from "unified";
import { visit } from "unist-util-visit";
import type { VFile } from "vfile";
const ADMONITION_TYPES = new Set(["info", "warning", "danger", "note"]);
/**
* Remark plugin to process links
* @type {Plugin<[unknown], Root, VFile>}
* Remark plugin to add admonition classes to directives.
*/
export function remarkAdmonition() {
export const remarkAdmonition: Plugin<[unknown], Root, VFile> = () => {
return function transformer(tree) {
/**
* @param {Directives} node
*/
const visitor = (node) => {
const visitor = (node: Directives) => {
if (
node.type === "containerDirective" ||
node.type === "leafDirective" ||
@ -43,4 +36,4 @@ export function remarkAdmonition() {
// @ts-ignore - visit cannot infer the type of the visitor.
visit(tree, visitor);
};
}
};

View File

@ -1,22 +1,16 @@
/**
* @import {Plugin} from 'unified'
* @import {Root, Heading} from 'mdast'
* @import {VFile} from 'vfile'
*/
import { kebabCase } from "change-case";
import { Heading, Root } from "mdast";
import { toString } from "mdast-util-to-string";
import { Plugin } from "unified";
import { visit } from "unist-util-visit";
import { VFile } from "vfile";
/**
* Remark plugin to process links
* @type {Plugin<[unknown], Root, VFile>}
* Remark plugin to add IDs to headings.
*/
export const remarkHeadings = () => {
export const remarkHeadings: Plugin<[unknown], Root, VFile> = () => {
return function transformer(tree) {
/**
* @param {Heading} node
*/
const visitor = (node) => {
const visitor = (node: Heading) => {
const textContent = toString(node);
const id = kebabCase(textContent);

View File

@ -1,20 +1,14 @@
/**
* @import {Plugin} from 'unified'
* @import {Root, List} from 'mdast'
* @import {VFile} from 'vfile'
*/
import type { List, Root } from "mdast";
import type { Plugin } from "unified";
import { visit } from "unist-util-visit";
import type { VFile } from "vfile";
/**
* Remark plugin to process links
* @type {Plugin<[unknown], Root, VFile>}
* Remark plugin to process lists.
*/
export const remarkLists = () => {
export const remarkLists: Plugin<[unknown], Root, VFile> = () => {
return function transformer(tree) {
/**
* @param {List} node
*/
const visitor = (node) => {
const visitor = (node: List) => {
node.data = node.data || {};
node.data.hProperties = {

View File

@ -22,6 +22,7 @@ export const DefaultBrand: CurrentBrand = {
brandingLogo: "/static/dist/assets/icons/icon_left_brand.svg",
brandingFavicon: "/static/dist/assets/icons/icon.png",
brandingTitle: "authentik",
brandingCustomCss: "",
uiFooterLinks: [],
uiTheme: UiThemeEnum.Automatic,
matchedDomain: "",

View File

@ -0,0 +1,157 @@
import type { Meta, StoryObj } from "@storybook/web-components";
import { html } from "lit";
import { LogLevelEnum, SyncStatus, SystemTaskStatusEnum } from "@goauthentik/api";
import "./SyncStatusCard";
const metadata: Meta<SyncStatus> = {
title: "Elements/<ak-sync-status-card>",
component: "ak-sync-status-card",
};
export default metadata;
export const Running: StoryObj = {
args: {
status: {
isRunning: true,
tasks: [],
} as SyncStatus,
},
// @ts-ignore
render: ({ status }: SyncStatus) => {
return html` <div style="background-color: #f0f0f0; padding: 1rem;">
<ak-sync-status-card
.fetch=${async () => {
return status;
}}
></ak-sync-status-card>
</div>`;
},
};
export const SingleTask: StoryObj = {
args: {
status: {
isRunning: false,
tasks: [
{
uuid: "9ff42169-8249-4b67-ae3d-e455d822de2b",
name: "Single task",
fullName: "foo:bar:baz",
status: SystemTaskStatusEnum.Successful,
messages: [
{
logger: "foo",
event: "bar",
attributes: {
foo: "bar",
},
timestamp: new Date(),
logLevel: LogLevelEnum.Info,
},
],
description: "foo",
startTimestamp: new Date(),
finishTimestamp: new Date(),
duration: 0,
},
],
} as SyncStatus,
},
// @ts-ignore
render: ({ status }: SyncStatus) => {
return html` <div style="background-color: #f0f0f0; padding: 1rem;">
<ak-sync-status-card
.fetch=${async () => {
return status;
}}
></ak-sync-status-card>
</div>`;
},
};
export const MultipleTasks: StoryObj = {
args: {
status: {
isRunning: false,
tasks: [
{
uuid: "9ff42169-8249-4b67-ae3d-e455d822de2b",
name: "Single task",
fullName: "foo:bar:baz",
status: SystemTaskStatusEnum.Successful,
messages: [
{
logger: "foo",
event: "bar",
attributes: {
foo: "bar",
},
timestamp: new Date(),
logLevel: LogLevelEnum.Info,
},
],
description: "foo",
startTimestamp: new Date(),
finishTimestamp: new Date(),
duration: 0,
},
{
uuid: "9ff42169-8249-4b67-ae3d-e455d822de2b",
name: "Single task",
fullName: "foo:bar:baz",
status: SystemTaskStatusEnum.Successful,
messages: [
{
logger: "foo",
event: "bar",
attributes: {
foo: "bar",
},
timestamp: new Date(),
logLevel: LogLevelEnum.Info,
},
],
description: "foo",
startTimestamp: new Date(),
finishTimestamp: new Date(),
duration: 0,
},
{
uuid: "9ff42169-8249-4b67-ae3d-e455d822de2b",
name: "Single task",
fullName: "foo:bar:baz",
status: SystemTaskStatusEnum.Successful,
messages: [
{
logger: "foo",
event: "bar",
attributes: {
foo: "bar",
},
timestamp: new Date(),
logLevel: LogLevelEnum.Info,
},
],
description: "foo",
startTimestamp: new Date(),
finishTimestamp: new Date(),
duration: 0,
},
],
} as SyncStatus,
},
// @ts-ignore
render: ({ status }: SyncStatus) => {
return html` <div style="background-color: #f0f0f0; padding: 1rem;">
<ak-sync-status-card
.fetch=${async () => {
return status;
}}
></ak-sync-status-card>
</div>`;
},
};

View File

@ -3,17 +3,92 @@ import { getRelativeTime } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-status-label";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/events/LogViewer";
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table";
import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, html, nothing } from "lit";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFTable from "@patternfly/patternfly/components/Table/table.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { SyncStatus, SystemTask, SystemTaskStatusEnum } from "@goauthentik/api";
@customElement("ak-sync-status-table")
export class SyncStatusTable extends Table<SystemTask> {
@property({ attribute: false })
tasks: SystemTask[] = [];
expandable = true;
static get styles() {
return super.styles.concat(css`
code:not(:last-of-type)::after {
content: "-";
margin: 0 0.25rem;
}
`);
}
async apiEndpoint(): Promise<PaginatedResponse<SystemTask>> {
return {
pagination: {
next: 0,
previous: 0,
count: this.tasks.length,
current: 1,
totalPages: 1,
startIndex: 0,
endIndex: this.tasks.length,
},
results: this.tasks,
};
}
columns(): TableColumn[] {
return [
new TableColumn(msg("Task")),
new TableColumn(msg("Status")),
new TableColumn(msg("Finished")),
];
}
row(item: SystemTask): TemplateResult[] {
const nameParts = item.fullName.split(":");
nameParts.shift();
return [
html`<div>${item.name}</div>
<small>${nameParts.map((part) => html`<code>${part}</code>`)}</small>`,
html`<ak-status-label
?good=${item.status === SystemTaskStatusEnum.Successful}
good-label=${msg("Finished successfully")}
bad-label=${msg("Finished with errors")}
></ak-status-label>`,
html`<div>${getRelativeTime(item.finishTimestamp)}</div>
<small>${item.finishTimestamp.toLocaleString()}</small>`,
];
}
renderExpanded(item: SystemTask): TemplateResult {
return html`<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<ak-log-viewer .logs=${item?.messages}></ak-log-viewer>
</div>
</td>`;
}
renderToolbarContainer() {
return html``;
}
renderTablePagination() {
return html``;
}
}
@customElement("ak-sync-status-card")
export class SyncStatusCard extends AKElement {
@state()
@ -29,7 +104,7 @@ export class SyncStatusCard extends AKElement {
triggerSync!: () => Promise<unknown>;
static get styles(): CSSResult[] {
return [PFBase, PFCard];
return [PFBase, PFCard, PFTable];
}
firstUpdated() {
@ -40,25 +115,6 @@ export class SyncStatusCard extends AKElement {
});
}
renderSyncTask(task: SystemTask): TemplateResult {
return html`<li>
${(this.syncState?.tasks || []).length > 1 ? html`<span>${task.name}</span>` : nothing}
<span
><ak-status-label
?good=${task.status === SystemTaskStatusEnum.Successful}
good-label=${msg("Finished successfully")}
bad-label=${msg("Finished with errors")}
></ak-status-label
></span>
<span
>${msg(
str`Finished ${getRelativeTime(task.finishTimestamp)} (${task.finishTimestamp.toLocaleString()})`,
)}</span
>
<ak-log-viewer .logs=${task?.messages}></ak-log-viewer>
</li> `;
}
renderSyncStatus(): TemplateResult {
if (this.loading) {
return html`<ak-empty-state ?loading=${true}></ak-empty-state>`;
@ -72,13 +128,7 @@ export class SyncStatusCard extends AKElement {
if (this.syncState.tasks.length < 1) {
return html`${msg("Not synced yet.")}`;
}
return html`
<ul class="pf-c-list">
${this.syncState.tasks.map((task) => {
return this.renderSyncTask(task);
})}
</ul>
`;
return html`<ak-sync-status-table .tasks=${this.syncState.tasks}></ak-sync-status-table>`;
}
render(): TemplateResult {
@ -120,6 +170,7 @@ export class SyncStatusCard extends AKElement {
declare global {
interface HTMLElementTagNameMap {
"ak-sync-status-table": SyncStatusTable;
"ak-sync-status-card": SyncStatusCard;
}
}

12
web/src/global.d.ts vendored
View File

@ -2,18 +2,18 @@ declare module "*.css";
declare module "*.md" {
/**
* The HTML content of the markdown file.
* The serialized JSON content of an MD file.
*/
const html: string;
export default html;
const serializedJSON: string;
export default serializedJSON;
}
declare module "*.mdx" {
/**
* The HTML content of the markdown file.
* The serialized JSON content of an MDX file.
*/
const html: string;
export default html;
const serializedJSON: string;
export default serializedJSON;
}
declare namespace Intl {

View File

@ -13,6 +13,7 @@
"expect-webdriverio",
"grecaptcha"
],
"jsx": "react-jsx",
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"experimentalDecorators": true,

View File

@ -5575,11 +5575,6 @@ doesn't pass when either or both of the selected options are equal or above the
<source>Webhook URL</source>
<target>Webhook URL</target>
</trans-unit>
<trans-unit id="s25ec2846f6b88214">
<source>Webhook Mapping</source>
<target>Webhook Zuordnung</target>
</trans-unit>
<trans-unit id="sca2879d96f58a39c">
<source>Send once</source>
@ -8022,10 +8017,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Finished with errors</source>
<target>Mit Fehlern beendet</target>
</trans-unit>
<trans-unit id="sbd12ed8a1053a108">
<source>Finished <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</source>
<target>Beendet <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</target>
</trans-unit>
<trans-unit id="sc3c334d642866997">
<source>Sync currently running</source>
<target>Synchronisation läuft gerade</target>
@ -9063,6 +9054,42 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s054cb3c0a84eefe5">
<source>Alter authentik's behavior for vendor-specific SCIM implementations.</source>
</trans-unit>
<trans-unit id="sc838e297aa5e58ba">
<source>AuthnContextClassRef Property Mapping</source>
</trans-unit>
<trans-unit id="sd7ba1793c3abf4fe">
<source>Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.</source>
</trans-unit>
<trans-unit id="sc83697908e3e2b46">
<source>SSF URL</source>
</trans-unit>
<trans-unit id="s5a1fc8f3b2e072d0">
<source>No assigned application</source>
</trans-unit>
<trans-unit id="sf16aa54bf77362e1">
<source>Custom CSS</source>
</trans-unit>
<trans-unit id="s6171ded034da4e96">
<source>Custom CSS to apply to pages when this brand is active.</source>
</trans-unit>
<trans-unit id="s8e56764914a939d2">
<source>Default flow background</source>
</trans-unit>
<trans-unit id="sea64900dbcf879c8">
<source>Default background used during flow execution. Can be overridden per flow.</source>
</trans-unit>
<trans-unit id="s03ee3cfb0919c62c">
<source>Task</source>
</trans-unit>
<trans-unit id="sbb99d4ec27fbb263">
<source>Finished</source>
</trans-unit>
<trans-unit id="s04fbc36687266544">
<source>Webhook Body Mapping</source>
</trans-unit>
<trans-unit id="s149fd588c9f4b891">
<source>Webhook Header Mapping</source>
</trans-unit>
</body>
</file>

View File

@ -4550,10 +4550,6 @@ doesn't pass when either or both of the selected options are equal or above the
<source>Webhook URL</source>
<target>Webhook URL</target>
</trans-unit>
<trans-unit id="s25ec2846f6b88214">
<source>Webhook Mapping</source>
<target>Webhook Mapping</target>
</trans-unit>
<trans-unit id="sca2879d96f58a39c">
<source>Send once</source>
<target>Send once</target>
@ -6618,9 +6614,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s46f1d86ffec6223c">
<source>Finished with errors</source>
</trans-unit>
<trans-unit id="sbd12ed8a1053a108">
<source>Finished <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</source>
</trans-unit>
<trans-unit id="sc3c334d642866997">
<source>Sync currently running</source>
</trans-unit>
@ -7592,6 +7585,42 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s054cb3c0a84eefe5">
<source>Alter authentik's behavior for vendor-specific SCIM implementations.</source>
</trans-unit>
<trans-unit id="sc838e297aa5e58ba">
<source>AuthnContextClassRef Property Mapping</source>
</trans-unit>
<trans-unit id="sd7ba1793c3abf4fe">
<source>Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.</source>
</trans-unit>
<trans-unit id="sc83697908e3e2b46">
<source>SSF URL</source>
</trans-unit>
<trans-unit id="s5a1fc8f3b2e072d0">
<source>No assigned application</source>
</trans-unit>
<trans-unit id="sf16aa54bf77362e1">
<source>Custom CSS</source>
</trans-unit>
<trans-unit id="s6171ded034da4e96">
<source>Custom CSS to apply to pages when this brand is active.</source>
</trans-unit>
<trans-unit id="s8e56764914a939d2">
<source>Default flow background</source>
</trans-unit>
<trans-unit id="sea64900dbcf879c8">
<source>Default background used during flow execution. Can be overridden per flow.</source>
</trans-unit>
<trans-unit id="s03ee3cfb0919c62c">
<source>Task</source>
</trans-unit>
<trans-unit id="sbb99d4ec27fbb263">
<source>Finished</source>
</trans-unit>
<trans-unit id="s04fbc36687266544">
<source>Webhook Body Mapping</source>
</trans-unit>
<trans-unit id="s149fd588c9f4b891">
<source>Webhook Header Mapping</source>
</trans-unit>
</body>
</file>

View File

@ -5602,11 +5602,6 @@ no se aprueba cuando una o ambas de las opciones seleccionadas son iguales o sup
<source>Webhook URL</source>
<target>URL de webhook</target>
</trans-unit>
<trans-unit id="s25ec2846f6b88214">
<source>Webhook Mapping</source>
<target>Asignación de Webhook</target>
</trans-unit>
<trans-unit id="sca2879d96f58a39c">
<source>Send once</source>
@ -8146,10 +8141,6 @@ Las vinculaciones a grupos o usuarios se comparan con el usuario del evento.</ta
<source>Finished with errors</source>
<target>Finalizado con errores</target>
</trans-unit>
<trans-unit id="sbd12ed8a1053a108">
<source>Finished <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</source>
<target>Terminado <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>) </target>
</trans-unit>
<trans-unit id="sc3c334d642866997">
<source>Sync currently running</source>
<target>Sincronización corriendo actualmente</target>
@ -9155,6 +9146,42 @@ Las vinculaciones a grupos o usuarios se comparan con el usuario del evento.</ta
</trans-unit>
<trans-unit id="s054cb3c0a84eefe5">
<source>Alter authentik's behavior for vendor-specific SCIM implementations.</source>
</trans-unit>
<trans-unit id="sc838e297aa5e58ba">
<source>AuthnContextClassRef Property Mapping</source>
</trans-unit>
<trans-unit id="sd7ba1793c3abf4fe">
<source>Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.</source>
</trans-unit>
<trans-unit id="sc83697908e3e2b46">
<source>SSF URL</source>
</trans-unit>
<trans-unit id="s5a1fc8f3b2e072d0">
<source>No assigned application</source>
</trans-unit>
<trans-unit id="sf16aa54bf77362e1">
<source>Custom CSS</source>
</trans-unit>
<trans-unit id="s6171ded034da4e96">
<source>Custom CSS to apply to pages when this brand is active.</source>
</trans-unit>
<trans-unit id="s8e56764914a939d2">
<source>Default flow background</source>
</trans-unit>
<trans-unit id="sea64900dbcf879c8">
<source>Default background used during flow execution. Can be overridden per flow.</source>
</trans-unit>
<trans-unit id="s03ee3cfb0919c62c">
<source>Task</source>
</trans-unit>
<trans-unit id="sbb99d4ec27fbb263">
<source>Finished</source>
</trans-unit>
<trans-unit id="s04fbc36687266544">
<source>Webhook Body Mapping</source>
</trans-unit>
<trans-unit id="s149fd588c9f4b891">
<source>Webhook Header Mapping</source>
</trans-unit>
</body>
</file>

View File

@ -5687,11 +5687,6 @@ doesn't pass when either or both of the selected options are equal or above the
<source>Webhook URL</source>
<target>URL Webhoo</target>
</trans-unit>
<trans-unit id="s25ec2846f6b88214">
<source>Webhook Mapping</source>
<target>Mappage de Webhook</target>
</trans-unit>
<trans-unit id="sca2879d96f58a39c">
<source>Send once</source>
@ -8367,10 +8362,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<source>Finished with errors</source>
<target>Fini avec erreurs</target>
</trans-unit>
<trans-unit id="sbd12ed8a1053a108">
<source>Finished <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</source>
<target>Fini <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</target>
</trans-unit>
<trans-unit id="sc3c334d642866997">
<source>Sync currently running</source>
<target>Synchronisation en cours</target>
@ -9668,6 +9659,50 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<trans-unit id="s054cb3c0a84eefe5">
<source>Alter authentik's behavior for vendor-specific SCIM implementations.</source>
<target>Change le comportement d'authentik en fonction des spécificités d'implémentations des fournisseurs SCIM.</target>
</trans-unit>
<trans-unit id="sc838e297aa5e58ba">
<source>AuthnContextClassRef Property Mapping</source>
<target>Mappage de propriété AuthnContextClassRef</target>
</trans-unit>
<trans-unit id="sd7ba1793c3abf4fe">
<source>Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.</source>
<target>Configure comment la valeur AuthnContextClassRef sera créée. Lorsque non sélectionné, AuthnContextClassRef sera défini en fonction de quelle méthode d'authentification l'utilisateur a utilisé pour s'authentifier.</target>
</trans-unit>
<trans-unit id="sc83697908e3e2b46">
<source>SSF URL</source>
<target>URL SSF</target>
</trans-unit>
<trans-unit id="s5a1fc8f3b2e072d0">
<source>No assigned application</source>
<target>Aucune application assignée</target>
</trans-unit>
<trans-unit id="sf16aa54bf77362e1">
<source>Custom CSS</source>
<target>CSS personnalisé</target>
</trans-unit>
<trans-unit id="s6171ded034da4e96">
<source>Custom CSS to apply to pages when this brand is active.</source>
<target>CSS personnalisé à appliquer sur les pages lorsque cette marque est active.</target>
</trans-unit>
<trans-unit id="s8e56764914a939d2">
<source>Default flow background</source>
<target>Arrière-plan des flux par défaut</target>
</trans-unit>
<trans-unit id="sea64900dbcf879c8">
<source>Default background used during flow execution. Can be overridden per flow.</source>
<target>Arrière plan utilisé lors de l'exécution des flux. Peut être personnalisé au niveau de chaque flux.</target>
</trans-unit>
<trans-unit id="s03ee3cfb0919c62c">
<source>Task</source>
</trans-unit>
<trans-unit id="sbb99d4ec27fbb263">
<source>Finished</source>
</trans-unit>
<trans-unit id="s04fbc36687266544">
<source>Webhook Body Mapping</source>
</trans-unit>
<trans-unit id="s149fd588c9f4b891">
<source>Webhook Header Mapping</source>
</trans-unit>
</body>
</file>

View File

@ -5686,11 +5686,6 @@ doesn't pass when either or both of the selected options are equal or above the
<source>Webhook URL</source>
<target>Webhook URL</target>
</trans-unit>
<trans-unit id="s25ec2846f6b88214">
<source>Webhook Mapping</source>
<target>Webhook Mapping</target>
</trans-unit>
<trans-unit id="sca2879d96f58a39c">
<source>Send once</source>
@ -8343,10 +8338,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Finished with errors</source>
<target>Finito con errori</target>
</trans-unit>
<trans-unit id="sbd12ed8a1053a108">
<source>Finished <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</source>
<target>Finito <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</target>
</trans-unit>
<trans-unit id="sc3c334d642866997">
<source>Sync currently running</source>
<target>Sincronizzazione attualmente in esecuzione</target>
@ -9506,6 +9497,42 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s054cb3c0a84eefe5">
<source>Alter authentik's behavior for vendor-specific SCIM implementations.</source>
</trans-unit>
<trans-unit id="sc838e297aa5e58ba">
<source>AuthnContextClassRef Property Mapping</source>
</trans-unit>
<trans-unit id="sd7ba1793c3abf4fe">
<source>Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.</source>
</trans-unit>
<trans-unit id="sc83697908e3e2b46">
<source>SSF URL</source>
</trans-unit>
<trans-unit id="s5a1fc8f3b2e072d0">
<source>No assigned application</source>
</trans-unit>
<trans-unit id="sf16aa54bf77362e1">
<source>Custom CSS</source>
</trans-unit>
<trans-unit id="s6171ded034da4e96">
<source>Custom CSS to apply to pages when this brand is active.</source>
</trans-unit>
<trans-unit id="s8e56764914a939d2">
<source>Default flow background</source>
</trans-unit>
<trans-unit id="sea64900dbcf879c8">
<source>Default background used during flow execution. Can be overridden per flow.</source>
</trans-unit>
<trans-unit id="s03ee3cfb0919c62c">
<source>Task</source>
</trans-unit>
<trans-unit id="sbb99d4ec27fbb263">
<source>Finished</source>
</trans-unit>
<trans-unit id="s04fbc36687266544">
<source>Webhook Body Mapping</source>
</trans-unit>
<trans-unit id="s149fd588c9f4b891">
<source>Webhook Header Mapping</source>
</trans-unit>
</body>
</file>

View File

@ -5629,11 +5629,6 @@ doesn't pass when either or both of the selected options are equal or above the
<source>Webhook URL</source>
<target>웹훅 URL</target>
</trans-unit>
<trans-unit id="s25ec2846f6b88214">
<source>Webhook Mapping</source>
<target>웹훅 맵핑</target>
</trans-unit>
<trans-unit id="sca2879d96f58a39c">
<source>Send once</source>
@ -8087,9 +8082,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s46f1d86ffec6223c">
<source>Finished with errors</source>
</trans-unit>
<trans-unit id="sbd12ed8a1053a108">
<source>Finished <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</source>
</trans-unit>
<trans-unit id="sc3c334d642866997">
<source>Sync currently running</source>
</trans-unit>
@ -9062,6 +9054,42 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s054cb3c0a84eefe5">
<source>Alter authentik's behavior for vendor-specific SCIM implementations.</source>
</trans-unit>
<trans-unit id="sc838e297aa5e58ba">
<source>AuthnContextClassRef Property Mapping</source>
</trans-unit>
<trans-unit id="sd7ba1793c3abf4fe">
<source>Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.</source>
</trans-unit>
<trans-unit id="sc83697908e3e2b46">
<source>SSF URL</source>
</trans-unit>
<trans-unit id="s5a1fc8f3b2e072d0">
<source>No assigned application</source>
</trans-unit>
<trans-unit id="sf16aa54bf77362e1">
<source>Custom CSS</source>
</trans-unit>
<trans-unit id="s6171ded034da4e96">
<source>Custom CSS to apply to pages when this brand is active.</source>
</trans-unit>
<trans-unit id="s8e56764914a939d2">
<source>Default flow background</source>
</trans-unit>
<trans-unit id="sea64900dbcf879c8">
<source>Default background used during flow execution. Can be overridden per flow.</source>
</trans-unit>
<trans-unit id="s03ee3cfb0919c62c">
<source>Task</source>
</trans-unit>
<trans-unit id="sbb99d4ec27fbb263">
<source>Finished</source>
</trans-unit>
<trans-unit id="s04fbc36687266544">
<source>Webhook Body Mapping</source>
</trans-unit>
<trans-unit id="s149fd588c9f4b891">
<source>Webhook Header Mapping</source>
</trans-unit>
</body>
</file>

View File

@ -5638,11 +5638,6 @@ slaagt niet wanneer een of beide geselecteerde opties gelijk zijn aan of boven d
<source>Webhook URL</source>
<target>Webhook-URL</target>
</trans-unit>
<trans-unit id="s25ec2846f6b88214">
<source>Webhook Mapping</source>
<target>Webhook-toewijzing</target>
</trans-unit>
<trans-unit id="sca2879d96f58a39c">
<source>Send once</source>
@ -7989,9 +7984,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
<trans-unit id="s46f1d86ffec6223c">
<source>Finished with errors</source>
</trans-unit>
<trans-unit id="sbd12ed8a1053a108">
<source>Finished <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</source>
</trans-unit>
<trans-unit id="sc3c334d642866997">
<source>Sync currently running</source>
</trans-unit>
@ -8964,6 +8956,42 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
</trans-unit>
<trans-unit id="s054cb3c0a84eefe5">
<source>Alter authentik's behavior for vendor-specific SCIM implementations.</source>
</trans-unit>
<trans-unit id="sc838e297aa5e58ba">
<source>AuthnContextClassRef Property Mapping</source>
</trans-unit>
<trans-unit id="sd7ba1793c3abf4fe">
<source>Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.</source>
</trans-unit>
<trans-unit id="sc83697908e3e2b46">
<source>SSF URL</source>
</trans-unit>
<trans-unit id="s5a1fc8f3b2e072d0">
<source>No assigned application</source>
</trans-unit>
<trans-unit id="sf16aa54bf77362e1">
<source>Custom CSS</source>
</trans-unit>
<trans-unit id="s6171ded034da4e96">
<source>Custom CSS to apply to pages when this brand is active.</source>
</trans-unit>
<trans-unit id="s8e56764914a939d2">
<source>Default flow background</source>
</trans-unit>
<trans-unit id="sea64900dbcf879c8">
<source>Default background used during flow execution. Can be overridden per flow.</source>
</trans-unit>
<trans-unit id="s03ee3cfb0919c62c">
<source>Task</source>
</trans-unit>
<trans-unit id="sbb99d4ec27fbb263">
<source>Finished</source>
</trans-unit>
<trans-unit id="s04fbc36687266544">
<source>Webhook Body Mapping</source>
</trans-unit>
<trans-unit id="s149fd588c9f4b891">
<source>Webhook Header Mapping</source>
</trans-unit>
</body>
</file>

View File

@ -5689,11 +5689,6 @@ w toku, tworzony jest nowy użytkownik i zapisywane są do niego dane.</target>
<source>Webhook URL</source>
<target>URL webhooka</target>
</trans-unit>
<trans-unit id="s25ec2846f6b88214">
<source>Webhook Mapping</source>
<target>Mapowanie webhooka</target>
</trans-unit>
<trans-unit id="sca2879d96f58a39c">
<source>Send once</source>
@ -8369,10 +8364,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
<source>Finished with errors</source>
<target>Zakończone z błędami</target>
</trans-unit>
<trans-unit id="sbd12ed8a1053a108">
<source>Finished <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</source>
<target>Zakończono <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</target>
</trans-unit>
<trans-unit id="sc3c334d642866997">
<source>Sync currently running</source>
<target>Synchronizacja jest obecnie uruchomiona.</target>
@ -9392,6 +9383,42 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
</trans-unit>
<trans-unit id="s054cb3c0a84eefe5">
<source>Alter authentik's behavior for vendor-specific SCIM implementations.</source>
</trans-unit>
<trans-unit id="sc838e297aa5e58ba">
<source>AuthnContextClassRef Property Mapping</source>
</trans-unit>
<trans-unit id="sd7ba1793c3abf4fe">
<source>Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.</source>
</trans-unit>
<trans-unit id="sc83697908e3e2b46">
<source>SSF URL</source>
</trans-unit>
<trans-unit id="s5a1fc8f3b2e072d0">
<source>No assigned application</source>
</trans-unit>
<trans-unit id="sf16aa54bf77362e1">
<source>Custom CSS</source>
</trans-unit>
<trans-unit id="s6171ded034da4e96">
<source>Custom CSS to apply to pages when this brand is active.</source>
</trans-unit>
<trans-unit id="s8e56764914a939d2">
<source>Default flow background</source>
</trans-unit>
<trans-unit id="sea64900dbcf879c8">
<source>Default background used during flow execution. Can be overridden per flow.</source>
</trans-unit>
<trans-unit id="s03ee3cfb0919c62c">
<source>Task</source>
</trans-unit>
<trans-unit id="sbb99d4ec27fbb263">
<source>Finished</source>
</trans-unit>
<trans-unit id="s04fbc36687266544">
<source>Webhook Body Mapping</source>
</trans-unit>
<trans-unit id="s149fd588c9f4b891">
<source>Webhook Header Mapping</source>
</trans-unit>
</body>
</file>

View File

@ -5654,11 +5654,6 @@ doesn't pass when either or both of the selected options are equal or above the
<source>Webhook URL</source>
<target>Ŵēƀĥōōķ ŨŔĹ</target>
</trans-unit>
<trans-unit id="s25ec2846f6b88214">
<source>Webhook Mapping</source>
<target>Ŵēƀĥōōķ Ḿàƥƥĩńĝ</target>
</trans-unit>
<trans-unit id="sca2879d96f58a39c">
<source>Send once</source>
@ -8314,10 +8309,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Finished with errors</source>
<target>Ƒĩńĩśĥēď ŵĩţĥ ēŕŕōŕś</target>
</trans-unit>
<trans-unit id="sbd12ed8a1053a108">
<source>Finished <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</source>
<target>Ƒĩńĩśĥēď <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</target>
</trans-unit>
<trans-unit id="sc3c334d642866997">
<source>Sync currently running</source>
<target>Śŷńć ćũŕŕēńţĺŷ ŕũńńĩńĝ</target>
@ -9399,4 +9390,40 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s054cb3c0a84eefe5">
<source>Alter authentik's behavior for vendor-specific SCIM implementations.</source>
</trans-unit>
<trans-unit id="sc838e297aa5e58ba">
<source>AuthnContextClassRef Property Mapping</source>
</trans-unit>
<trans-unit id="sd7ba1793c3abf4fe">
<source>Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.</source>
</trans-unit>
<trans-unit id="sc83697908e3e2b46">
<source>SSF URL</source>
</trans-unit>
<trans-unit id="s5a1fc8f3b2e072d0">
<source>No assigned application</source>
</trans-unit>
<trans-unit id="sf16aa54bf77362e1">
<source>Custom CSS</source>
</trans-unit>
<trans-unit id="s6171ded034da4e96">
<source>Custom CSS to apply to pages when this brand is active.</source>
</trans-unit>
<trans-unit id="s8e56764914a939d2">
<source>Default flow background</source>
</trans-unit>
<trans-unit id="sea64900dbcf879c8">
<source>Default background used during flow execution. Can be overridden per flow.</source>
</trans-unit>
<trans-unit id="s03ee3cfb0919c62c">
<source>Task</source>
</trans-unit>
<trans-unit id="sbb99d4ec27fbb263">
<source>Finished</source>
</trans-unit>
<trans-unit id="s04fbc36687266544">
<source>Webhook Body Mapping</source>
</trans-unit>
<trans-unit id="s149fd588c9f4b891">
<source>Webhook Header Mapping</source>
</trans-unit>
</body></file></xliff>

View File

@ -5688,11 +5688,6 @@ doesn't pass when either or both of the selected options are equal or above the
<source>Webhook URL</source>
<target>URL-адрес вебхука</target>
</trans-unit>
<trans-unit id="s25ec2846f6b88214">
<source>Webhook Mapping</source>
<target>Сопоставление вебхуков</target>
</trans-unit>
<trans-unit id="sca2879d96f58a39c">
<source>Send once</source>
@ -8368,10 +8363,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Finished with errors</source>
<target>Закончено с ошибками</target>
</trans-unit>
<trans-unit id="sbd12ed8a1053a108">
<source>Finished <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</source>
<target>Завершено <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</target>
</trans-unit>
<trans-unit id="sc3c334d642866997">
<source>Sync currently running</source>
<target>Синхронизация текущих запущенных</target>
@ -9425,6 +9416,42 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s054cb3c0a84eefe5">
<source>Alter authentik's behavior for vendor-specific SCIM implementations.</source>
</trans-unit>
<trans-unit id="sc838e297aa5e58ba">
<source>AuthnContextClassRef Property Mapping</source>
</trans-unit>
<trans-unit id="sd7ba1793c3abf4fe">
<source>Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.</source>
</trans-unit>
<trans-unit id="sc83697908e3e2b46">
<source>SSF URL</source>
</trans-unit>
<trans-unit id="s5a1fc8f3b2e072d0">
<source>No assigned application</source>
</trans-unit>
<trans-unit id="sf16aa54bf77362e1">
<source>Custom CSS</source>
</trans-unit>
<trans-unit id="s6171ded034da4e96">
<source>Custom CSS to apply to pages when this brand is active.</source>
</trans-unit>
<trans-unit id="s8e56764914a939d2">
<source>Default flow background</source>
</trans-unit>
<trans-unit id="sea64900dbcf879c8">
<source>Default background used during flow execution. Can be overridden per flow.</source>
</trans-unit>
<trans-unit id="s03ee3cfb0919c62c">
<source>Task</source>
</trans-unit>
<trans-unit id="sbb99d4ec27fbb263">
<source>Finished</source>
</trans-unit>
<trans-unit id="s04fbc36687266544">
<source>Webhook Body Mapping</source>
</trans-unit>
<trans-unit id="s149fd588c9f4b891">
<source>Webhook Header Mapping</source>
</trans-unit>
</body>
</file>

View File

@ -5654,11 +5654,6 @@ Belirlenen seçeneklerden biri veya her ikisi de eşiğe eşit veya eşiğin üz
<source>Webhook URL</source>
<target>Web Kancası URL'si</target>
</trans-unit>
<trans-unit id="s25ec2846f6b88214">
<source>Webhook Mapping</source>
<target>Web Kancası Haritalama</target>
</trans-unit>
<trans-unit id="sca2879d96f58a39c">
<source>Send once</source>
@ -8325,10 +8320,6 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
<source>Finished with errors</source>
<target>Hatalarla tamamlandı</target>
</trans-unit>
<trans-unit id="sbd12ed8a1053a108">
<source>Finished <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</source>
<target><x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>) Tamamlandı</target>
</trans-unit>
<trans-unit id="sc3c334d642866997">
<source>Sync currently running</source>
<target>Senkronizasyon şu anda çalışıyor</target>
@ -9455,6 +9446,42 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
</trans-unit>
<trans-unit id="s054cb3c0a84eefe5">
<source>Alter authentik's behavior for vendor-specific SCIM implementations.</source>
</trans-unit>
<trans-unit id="sc838e297aa5e58ba">
<source>AuthnContextClassRef Property Mapping</source>
</trans-unit>
<trans-unit id="sd7ba1793c3abf4fe">
<source>Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.</source>
</trans-unit>
<trans-unit id="sc83697908e3e2b46">
<source>SSF URL</source>
</trans-unit>
<trans-unit id="s5a1fc8f3b2e072d0">
<source>No assigned application</source>
</trans-unit>
<trans-unit id="sf16aa54bf77362e1">
<source>Custom CSS</source>
</trans-unit>
<trans-unit id="s6171ded034da4e96">
<source>Custom CSS to apply to pages when this brand is active.</source>
</trans-unit>
<trans-unit id="s8e56764914a939d2">
<source>Default flow background</source>
</trans-unit>
<trans-unit id="sea64900dbcf879c8">
<source>Default background used during flow execution. Can be overridden per flow.</source>
</trans-unit>
<trans-unit id="s03ee3cfb0919c62c">
<source>Task</source>
</trans-unit>
<trans-unit id="sbb99d4ec27fbb263">
<source>Finished</source>
</trans-unit>
<trans-unit id="s04fbc36687266544">
<source>Webhook Body Mapping</source>
</trans-unit>
<trans-unit id="s149fd588c9f4b891">
<source>Webhook Header Mapping</source>
</trans-unit>
</body>
</file>

View File

@ -4031,9 +4031,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="s6873bdbfa24615fb">
<source>Webhook URL</source>
</trans-unit>
<trans-unit id="s25ec2846f6b88214">
<source>Webhook Mapping</source>
</trans-unit>
<trans-unit id="sca2879d96f58a39c">
<source>Send once</source>
</trans-unit>
@ -5222,9 +5219,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s46f1d86ffec6223c">
<source>Finished with errors</source>
</trans-unit>
<trans-unit id="sbd12ed8a1053a108">
<source>Finished <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</source>
</trans-unit>
<trans-unit id="sc3c334d642866997">
<source>Sync currently running</source>
</trans-unit>
@ -6197,6 +6191,42 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s054cb3c0a84eefe5">
<source>Alter authentik's behavior for vendor-specific SCIM implementations.</source>
</trans-unit>
<trans-unit id="sc838e297aa5e58ba">
<source>AuthnContextClassRef Property Mapping</source>
</trans-unit>
<trans-unit id="sd7ba1793c3abf4fe">
<source>Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.</source>
</trans-unit>
<trans-unit id="sc83697908e3e2b46">
<source>SSF URL</source>
</trans-unit>
<trans-unit id="s5a1fc8f3b2e072d0">
<source>No assigned application</source>
</trans-unit>
<trans-unit id="sf16aa54bf77362e1">
<source>Custom CSS</source>
</trans-unit>
<trans-unit id="s6171ded034da4e96">
<source>Custom CSS to apply to pages when this brand is active.</source>
</trans-unit>
<trans-unit id="s8e56764914a939d2">
<source>Default flow background</source>
</trans-unit>
<trans-unit id="sea64900dbcf879c8">
<source>Default background used during flow execution. Can be overridden per flow.</source>
</trans-unit>
<trans-unit id="s03ee3cfb0919c62c">
<source>Task</source>
</trans-unit>
<trans-unit id="sbb99d4ec27fbb263">
<source>Finished</source>
</trans-unit>
<trans-unit id="s04fbc36687266544">
<source>Webhook Body Mapping</source>
</trans-unit>
<trans-unit id="s149fd588c9f4b891">
<source>Webhook Header Mapping</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -5688,11 +5688,6 @@ doesn't pass when either or both of the selected options are equal or above the
<source>Webhook URL</source>
<target>Webhook URL</target>
</trans-unit>
<trans-unit id="s25ec2846f6b88214">
<source>Webhook Mapping</source>
<target>Webhook 映射</target>
</trans-unit>
<trans-unit id="sca2879d96f58a39c">
<source>Send once</source>
@ -8368,10 +8363,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Finished with errors</source>
<target>已完成但有错误</target>
</trans-unit>
<trans-unit id="sbd12ed8a1053a108">
<source>Finished <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</source>
<target><x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/>(<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>) 完成</target>
</trans-unit>
<trans-unit id="sc3c334d642866997">
<source>Sync currently running</source>
<target>当前正在同步</target>
@ -9669,6 +9660,50 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s054cb3c0a84eefe5">
<source>Alter authentik's behavior for vendor-specific SCIM implementations.</source>
<target>更改 authentik 的行为,以兼容特定厂商的 SCIM 实现。</target>
</trans-unit>
<trans-unit id="sc838e297aa5e58ba">
<source>AuthnContextClassRef Property Mapping</source>
<target>AuthnContextClassRef 属性映射</target>
</trans-unit>
<trans-unit id="sd7ba1793c3abf4fe">
<source>Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.</source>
<target>配置如何创建 AuthnContextClassRef 值。留空时,AuthnContextClassRef 会基于用户使用的身份验证方式设置。</target>
</trans-unit>
<trans-unit id="sc83697908e3e2b46">
<source>SSF URL</source>
<target>SSF URL</target>
</trans-unit>
<trans-unit id="s5a1fc8f3b2e072d0">
<source>No assigned application</source>
<target>无分配应用</target>
</trans-unit>
<trans-unit id="sf16aa54bf77362e1">
<source>Custom CSS</source>
<target>自定义 CSS</target>
</trans-unit>
<trans-unit id="s6171ded034da4e96">
<source>Custom CSS to apply to pages when this brand is active.</source>
<target>当品牌启用时,应用到页面的自定义 CSS。</target>
</trans-unit>
<trans-unit id="s8e56764914a939d2">
<source>Default flow background</source>
<target>默认流程背景</target>
</trans-unit>
<trans-unit id="sea64900dbcf879c8">
<source>Default background used during flow execution. Can be overridden per flow.</source>
<target>流程执行过程中使用的默认背景。可以按流程单独覆盖。</target>
</trans-unit>
<trans-unit id="s03ee3cfb0919c62c">
<source>Task</source>
</trans-unit>
<trans-unit id="sbb99d4ec27fbb263">
<source>Finished</source>
</trans-unit>
<trans-unit id="s04fbc36687266544">
<source>Webhook Body Mapping</source>
</trans-unit>
<trans-unit id="s149fd588c9f4b891">
<source>Webhook Header Mapping</source>
</trans-unit>
</body>
</file>

View File

@ -4298,10 +4298,6 @@ doesn't pass when either or both of the selected options are equal or above the
<source>Webhook URL</source>
<target>Webhook URL</target>
</trans-unit>
<trans-unit id="s25ec2846f6b88214">
<source>Webhook Mapping</source>
<target>Webhook 映射</target>
</trans-unit>
<trans-unit id="sca2879d96f58a39c">
<source>Send once</source>
<target>发送一次</target>
@ -6318,9 +6314,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s46f1d86ffec6223c">
<source>Finished with errors</source>
</trans-unit>
<trans-unit id="sbd12ed8a1053a108">
<source>Finished <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</source>
</trans-unit>
<trans-unit id="sc3c334d642866997">
<source>Sync currently running</source>
</trans-unit>
@ -7292,6 +7285,42 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s054cb3c0a84eefe5">
<source>Alter authentik's behavior for vendor-specific SCIM implementations.</source>
</trans-unit>
<trans-unit id="sc838e297aa5e58ba">
<source>AuthnContextClassRef Property Mapping</source>
</trans-unit>
<trans-unit id="sd7ba1793c3abf4fe">
<source>Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.</source>
</trans-unit>
<trans-unit id="sc83697908e3e2b46">
<source>SSF URL</source>
</trans-unit>
<trans-unit id="s5a1fc8f3b2e072d0">
<source>No assigned application</source>
</trans-unit>
<trans-unit id="sf16aa54bf77362e1">
<source>Custom CSS</source>
</trans-unit>
<trans-unit id="s6171ded034da4e96">
<source>Custom CSS to apply to pages when this brand is active.</source>
</trans-unit>
<trans-unit id="s8e56764914a939d2">
<source>Default flow background</source>
</trans-unit>
<trans-unit id="sea64900dbcf879c8">
<source>Default background used during flow execution. Can be overridden per flow.</source>
</trans-unit>
<trans-unit id="s03ee3cfb0919c62c">
<source>Task</source>
</trans-unit>
<trans-unit id="sbb99d4ec27fbb263">
<source>Finished</source>
</trans-unit>
<trans-unit id="s04fbc36687266544">
<source>Webhook Body Mapping</source>
</trans-unit>
<trans-unit id="s149fd588c9f4b891">
<source>Webhook Header Mapping</source>
</trans-unit>
</body>
</file>

View File

@ -9669,6 +9669,38 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s054cb3c0a84eefe5">
<source>Alter authentik's behavior for vendor-specific SCIM implementations.</source>
<target>更改 authentik 的行为,以兼容特定厂商的 SCIM 实现。</target>
</trans-unit>
<trans-unit id="sc838e297aa5e58ba">
<source>AuthnContextClassRef Property Mapping</source>
<target>AuthnContextClassRef 属性映射</target>
</trans-unit>
<trans-unit id="sd7ba1793c3abf4fe">
<source>Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.</source>
<target>配置如何创建 AuthnContextClassRef 值。留空时,AuthnContextClassRef 会基于用户使用的身份验证方式设置。</target>
</trans-unit>
<trans-unit id="sc83697908e3e2b46">
<source>SSF URL</source>
<target>SSF URL</target>
</trans-unit>
<trans-unit id="s5a1fc8f3b2e072d0">
<source>No assigned application</source>
<target>无分配应用</target>
</trans-unit>
<trans-unit id="sf16aa54bf77362e1">
<source>Custom CSS</source>
<target>自定义 CSS</target>
</trans-unit>
<trans-unit id="s6171ded034da4e96">
<source>Custom CSS to apply to pages when this brand is active.</source>
<target>当品牌启用时,应用到页面的自定义 CSS。</target>
</trans-unit>
<trans-unit id="s8e56764914a939d2">
<source>Default flow background</source>
<target>默认流程背景</target>
</trans-unit>
<trans-unit id="sea64900dbcf879c8">
<source>Default background used during flow execution. Can be overridden per flow.</source>
<target>流程执行过程中使用的默认背景。可以按流程单独覆盖。</target>
</trans-unit>
</body>
</file>

View File

@ -5618,11 +5618,6 @@ doesn't pass when either or both of the selected options are equal or above the
<source>Webhook URL</source>
<target>Webhook 網址</target>
</trans-unit>
<trans-unit id="s25ec2846f6b88214">
<source>Webhook Mapping</source>
<target>Webhook 對應</target>
</trans-unit>
<trans-unit id="sca2879d96f58a39c">
<source>Send once</source>
@ -8064,9 +8059,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s46f1d86ffec6223c">
<source>Finished with errors</source>
</trans-unit>
<trans-unit id="sbd12ed8a1053a108">
<source>Finished <x id="0" equiv-text="${getRelativeTime(task.finishTimestamp)}"/> (<x id="1" equiv-text="${task.finishTimestamp.toLocaleString()}"/>)</source>
</trans-unit>
<trans-unit id="sc3c334d642866997">
<source>Sync currently running</source>
</trans-unit>
@ -9039,6 +9031,42 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s054cb3c0a84eefe5">
<source>Alter authentik's behavior for vendor-specific SCIM implementations.</source>
</trans-unit>
<trans-unit id="sc838e297aa5e58ba">
<source>AuthnContextClassRef Property Mapping</source>
</trans-unit>
<trans-unit id="sd7ba1793c3abf4fe">
<source>Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.</source>
</trans-unit>
<trans-unit id="sc83697908e3e2b46">
<source>SSF URL</source>
</trans-unit>
<trans-unit id="s5a1fc8f3b2e072d0">
<source>No assigned application</source>
</trans-unit>
<trans-unit id="sf16aa54bf77362e1">
<source>Custom CSS</source>
</trans-unit>
<trans-unit id="s6171ded034da4e96">
<source>Custom CSS to apply to pages when this brand is active.</source>
</trans-unit>
<trans-unit id="s8e56764914a939d2">
<source>Default flow background</source>
</trans-unit>
<trans-unit id="sea64900dbcf879c8">
<source>Default background used during flow execution. Can be overridden per flow.</source>
</trans-unit>
<trans-unit id="s03ee3cfb0919c62c">
<source>Task</source>
</trans-unit>
<trans-unit id="sbb99d4ec27fbb263">
<source>Finished</source>
</trans-unit>
<trans-unit id="s04fbc36687266544">
<source>Webhook Body Mapping</source>
</trans-unit>
<trans-unit id="s149fd588c9f4b891">
<source>Webhook Header Mapping</source>
</trans-unit>
</body>
</file>

View File

@ -42,7 +42,7 @@ By default, the captcha test keys are used. You can get a proper key [here](http
## Recovery with email verification
Flow: right-click [here](/blueprints/example/flows-recovery-email-verification.yaml) and save the file.
Flow: right-click [here](https://version-2024-12.goauthentik.io/assets/files/flows-recovery-email-verification-408d6afeff2fbf276bf43a949e332ef6.yaml) and save the file.
Recovery flow, the user is sent an email after they've identified themselves. After they click on the link in the email, they are prompted for a new password and immediately logged on.

View File

@ -2,151 +2,251 @@
title: Style Guide
---
The Style Guide provides guidelines to ensure that the authentik documentation is easy to read and uses similar phrasing, formatting, and tone.
This Style Guide provides guidelines to ensure that the authentik documentation is consistent, clear, and easy to follow. It standardizes aspects like phrasing, formatting, tone, and structure across all documentation.
We appreciate contributions to our documentation; everything from fixing a typo to adding additional content to writing a completely new topic. To make the review and merging of your contributions faster and easier, please follow the [writing documentation](./writing-documentation.md) guidelines.
We appreciate all contributions to our documentation — whether it's fixing a typo, adding new content, or writing an entirely new topic. To help us review and merge your contributions more efficiently, please follow our [writing documentation](./writing-documentation.md) guidelines. If you notice any inconsistencies, feel free to open an [Issue](https://github.com/goauthentik/authentik/issues) or submit a [Pull Request](https://github.com/goauthentik/authentik/pulls) to fix them.
If you find any documentation that doesn't match these guidelines, feel free to either open an [Issue](https://github.com/goauthentik/authentik/issues) or a [PR](https://github.com/goauthentik/authentik/pulls) so they can be fixed.
- [General Style Guidelines](#general-style-guidelines)
- [Terminology](#terminology)
- [Writing Style](#writing-style)
- [Word Choices](#word-choices)
- [Formatting Guidelines](#formatting-guidelines)
- [Component-Based Formatting](#component-based-formatting)
- [Error Message Formatting and Troubleshooting](#error-message-formatting-and-troubleshooting)
- [Accessibility Best Practices](#accessibility-best-practices)
- [Notes and Warnings](#notes-and-warnings)
---
## General style guidelines
- Try to order the documentation sections in the order that makes it easiest for the user to follow. That is, order the sections in the same order as the actual workflow used to accomplish the task.
### Logical order
- Use headings (sub-titles) to break up long documents, and make it easier to find a specific section.
- Documentation should be structured to follow the natural order of tasks, making it easier for users to follow. Organize sections in a manner that reflects the actual workflow used to complete tasks.
- Add cross-reference links to related content whenever possible.
- When writing procedural documentation (How Tos) the steps should follow the workflow in the UI, specifying the exact pages to navigate and the precise fields, tabs, etc., to select or complete. Present the UI components in the document in the same order they appear in the UI.
- You can use standard [Docusaurus-specific features](https://docusaurus.io/docs/next/markdown-features), which include MDX elements such as tabs and admonitions.
### Headings
Use headings (sub-titles) to break up large blocks of text, making it easier for users to navigate the content and find specific sections quickly.
### Look and feel of the docs
In general, the visual, aesthetics of the technical documentation is intended to be lean and clean. Both the content (shorter sentences, concise instructions, etc) and the layout strive to have a clean, uncluttered look, with restrained use of colors and large call0outs or announcements. Relatedly, the colors used for our Info and Warning callouts, light blue and light yellow respectively, are reserved for those purposes only.
### Cross-references
Always include cross-references to related content. If a concept is referenced elsewhere in the documentation, link to the relevant section to provide users with additional context or instructions.
### Relative vs. absolute paths
Use relative paths when linking to other documentation files. This will ensure links are automatically updated if file paths change in the future. If you are linking between our Integration Guides and our regular technical docs, then use an absolute path.
### Markdown file type
The standard file type for documentation is `.md`. Use `.mdx` only if React components, such as interactive elements, are required.
## Terminology
### authentik names
### authentik product naming conventions
- The product name authentik should always start with a lower-case "a" and end with a "k". Even if it is the first word in a sentence. :-)
- The product name **authentik** should always be written with a lowercase "a" and a "k" at the end, even if it begins a sentence. This consistent style should be followed throughout the documentation.
- The company name is **Authentik Security, Inc.**, but for non-legal documentation, you may shorten it to **Authentik Security**.
- Our company name is Authentik Security, Inc. but in non-legal documentation you can shorten it to Authentik Security.
### Industry terms and technology names
### Industry terms, technology, and other tools
- When referring to external tools, or an industry term or technology, always follow the exact capitalization that the product or company itself uses on their website, in their official documentation, or what the industry uses in consensus.
- Try to avoid using abbreviations if possible.
- Use acronyms where it makes sense (for commonly used terms like SAML or RBAC). If an acronym is less-known, spell it out in parentheses after the first use.
- When referring to external tools or industry terms, always use the exact capitalization and naming conventions that the product or company uses. Refer to their website or official documentation for the proper formatting. For example, use "OAuth", "SAML", or "Docker" as per the official conventions.
- Avoid abbreviations unless they are well-known and widely recognized (e.g., SSO, MFA, RBAC).
- If an acronym is used less frequently, spell out its full meaning when first mentioned, followed by the acronym in parentheses. For instance, "Security Assertion Markup Language (SAML)".
## Writing style
- authentik documentation strives for a friendly, but not overly so, tone. It's ok to be a little bit conversational, and to address the reader in second person: "Next, you need to configure the log in settings."
### Tone
- Our documentation uses American English ("z" not "s").
The tone of the authentik documentation should be friendly but professional. It should be approachable, yet not overly casual. When appropriate, address the reader directly using second-person pronouns (e.g., "Next, you need to configure the login settings").
- Use the present tense and active voice in almost all cases:
### Language
- DON'T: "The Applications page will be loaded."
The documentation uses **American English** spelling conventions (e.g., "customize" instead of "customise").
- DO: "The Applications page displays."
### Voice
- Phrasing should never blame the user, and should be subjective:
Use **active voice** and present tense for clear, direct communication. - **DON'T:** "The Applications page will be loaded." - **DO:** "The Applications page displays."
- DON'T: "Never modify the default file."
### User-friendly phrasing
- DO: "We recommend that you do not modify the default file, because this can result in unexpected issues."
## Formatting
Formatting in documentation is important; it improves comprehension and readability, and allows the brain to more quickly determine what is a command or a configuration setting, what is a field name, or what is a variable.
### Fonts and font styling
- When referring to UI elements or components in the authentik UI, such as field names, labels, etc., use **bold** text.
- When referring to internal components in authentik, like the policy engine, or blueprints, do not use any special formatting. Link to the relevant documentation when possible.
- Use `code` format when referring to:
- commands
- file paths
- file names
- directory names
- code snippets (single line or a block of code)
- Use _italic_ font for variables or placeholders to make it clear they need to be replaced. Choose placeholder names that highlight their purpose, ensuring users understand what to update.
- When handling URLs:
- For URLs entered as values or defined in fields, apply `code formatting` and _italicize_ any variables within them to emphasize that placeholders require user input. Example: `<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."
Clearly indicate whether variables in code snippets need to be defined by the user, are system-provided, or generated.
- When referring to authentik functionality and features, such as flows, stages, sources, or policies, do not capitalize and do not use bold or italic text. When possible link to the corresponding documentation.
### Titles and headers
- Both titles and headers (H1, H2, H3) use sentence style capitalization, meaning that only the first word is capitalized. However, if the title or header includes a proper noun (name of a product, etc) then capitalize those words.
Examples:
- Configure your provider
- Configure the Google Workspace provider
- Make sure the title/header is descriptive, and tells the reader what that section is about. Try to avoid titles or headers like "Overview". Instead say "About authentik policies".
- Use the imperative verb form (not the gerund form) for procedural topics. For example, use "Configure your instance" instead of "Configuring your instance".
### Examples
When you want to show an example (say, a code snippet), use a new line, bold text, and a semi-colon, like this:
**Example**:
This expression policy uses an expression based on the user's name.
```
if request.context["pending_user"].username == "marie":
return True
return False
```
### Notes and warnings
Use the following convention for a note:
```
:::info
Write your note here.
:::
```
:::info
Write your note here.
:::
For a warning, use this:
```
:::warning
Write your note here.
:::
```
:::warning
Write your warning here.
:::
```
```
Avoid phrasing that blames the user. Be subjective and polite when providing instructions. - **DON'T:** "Never modify the default file." - **DO:** "We recommend that you do not modify the default file, as doing so may result in unexpected issues."
## Word choices
- **May** versus **might** versus **can**
Typically, the word "may" is not used in technical writing, because it implies permission (rather than ability) to do something. Instead use the word "can". Use "might" when the scenario could be different in certain environments. Be sparing with your use of "might"; this word implies unpredictability, not our favorite thing with software.
### "May" versus "Might" versus "Can"
- DON'T: "You may use an Expression policy to enforce MFA adherence."
- Typically, avoid using the word "may" in technical writing, as it implies permission rather than ability to perform an action. Instead, use **"can"** to suggest possibility.
- **"Might"** should be used to indicate that something could happen under certain conditions, but use it sparingly. It implies unpredictability, which can be undesirable in software documentation.
- DO: "You can use an Expression policy to enforce MFA adherence."
- **DON'T:** "You may use an Expression policy to enforce MFA adherence."
- **DO:** "You can use an Expression policy to enforce MFA adherence."
- **DO:** "Values might differ depending on the source of the property mappings."
- Do: "Values might differ depending on the source of the property mappings.""
### "Login", "Log in", and "Log in to"
- **Login**, **log in**, and **log in to**
As a descriptive term, use one word: "login". (The login panel.)
As a verb, use "log in", with two words. (This stage prompts the user to log in.)
As a verb with the proposition "to", use "log in to". (Log in to the application.)
- As a noun or descriptive term, use **login** (e.g., "The login panel").
- As a verb, use **"log in"** (e.g., "This stage prompts the user to log in").
- As a verb followed by the proposition **"to"**, use **"log in to"** (e.g., "Log in to the application").
## Formatting guidelines
### Fonts and Font Styling
- When referring to internal components in authentik, like the policy engine, or blueprints, do not use any special formatting. Link to the relevant documentation when possible.
- When referring to authentik functionality and features, such as flows, stages, sources, or policies, do not capitalize and do not use bold or italic text. When possible link to the corresponding documentation.
- Use **bold** to highlight:
- UI elements such as field names, labels, buttons, or options (e.g., **Save** button, **Username** field).
- Key actions in instructions (e.g., **Click Next**).
- 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:
- Commands (e.g., `docker run`).
- File paths, file names, and directory names (e.g., `/usr/local/bin/`).
- Inline code snippets (e.g., `.env`).
- When handling URLs:
- For URLs entered as values or defined in fields _italicize_ any variables within them to emphasize that placeholders 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>
- When mentioning URLs in text or within procedural instructions, omit code formatting. For instance: "In your browser, go to https://example.com."
### Titles and headers
- Titles and headers (H1, H2, H3) should follow **sentence case capitalization**, meaning only the first word is capitalized, except for proper nouns or product names.
- **DO:** "Configure the Google Workspace provider"
- **DON'T:** "CONFIGURE THE GOOGLE WORKSPACE PROVIDER"
- **DON'T:** "Configure The Google Workspace Provider"
- Ensure titles and headers are descriptive and clearly convey the purpose of the section. Avoid vague titles like "Overview." Instead, opt for something more specific, like "About authentik policies."
- Use the **imperative verb form** in procedural topics. For example, use "Configure your instance" instead of "Configuring your instance."
### Examples
When you want to show an example (say, a code snippet), start on a new line, use bold text for the word "Example", and a semi-colon, like this:
**Example**:
This expression policy uses an expression based on the user's name:
```
if request.context["pending_user"].username == "marie":
return True
return False
```
## Component-Based Formatting
This section covers the usage of React components within our documentation. Files that use component-based formatting must use the `.mdx` extension.
### Multiline Code Blocks
For displaying multiline code blocks, especially when variables need to be defined, use the **`IntegrationsMultilineCodeblock`** React component. Insert the following lines wherever you want the code block to appear:
```jsx
import IntegrationsMultilineCodeblock from "@site/src/components/MultilineCodeblock";
<IntegrationsMultilineCodeblock>
{`
OIDC_DISCOVERY_URL=https://<em>authentik.company</em>/application/o/<em>your-application-slug</em>/
`}
</IntegrationsMultilineCodeblock>;
```
This is especially useful for configuration files like JSON, YAML, or `.env` files, where placeholders must be replaced.
### Tabs for Multiple Configurations
Use **Tabs** to display different configurations (e.g., setting up authentication with OIDC vs. SAML) to help users navigate between options. Default to the easier or more common option. Insert the following lines wherever you want the code block to appear:
```jsx
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
<Tabs
defaultValue="oidc"
values={[
{ label: "OIDC", value: "oidc" },
{ label: "SAML", value: "saml" },
]}
>
<TabItem value="oidc">OIDC configuration details go here.</TabItem>
<TabItem value="saml">SAML configuration details go here.</TabItem>
</Tabs>;
```
Tabs improve readability when presenting different configurations or setup options.
## Error Message Formatting and Troubleshooting
When documenting error messages:
- Display the error message
- Explain possible causes
- Offer solutions
Example:
- **Error message:**
```sh
Error: Authentication failed. Invalid credentials.
```
- **Possible causes:**
- Incorrect username or password
- Account is locked due to too many failed login attempts
- **Solution:**
- Verify your username and password
- Reset your password if necessary
- Contact an administrator if your account is locked
## Accessibility Best Practices
- Avoid using color as the sole method of conveying information (e.g., "Click the red button"). Instead, use descriptive labels to ensure accessibility.
- Provide **descriptive link text**. Avoid using generic terms like "Click here". Be specific about where the link will take the user.
- **DON'T:** "Click here."
- **DO:** "See the [Authentication Settings](/) for more details."
## Notes and Warnings
For notes and important information:
**Notes** are formatted using:
```
:::info
Write your note here.
:::
```
**Warnings** are formatted using:
```
:::warning
Write your warning here.
:::
```
Use these conventions to ensure that the reader's attention is drawn to crucial information.

View File

@ -30,6 +30,12 @@ const createConfig = (): Config => {
target: "_self",
},
items: [
{
to: "https://goauthentik.io/features",
label: "Features",
position: "left",
target: "_self",
},
{
to: "https://goauthentik.io/blog",
label: "Blog",

View File

@ -0,0 +1,67 @@
---
title: Integrate with Miniflux
sidebar_label: Miniflux
support_level: community
---
## What is Miniflux
> Miniflux is a minimalist and opinionated RSS feed reader.
>
> -- https://github.com/miniflux/v2
## Preparation
The following placeholders are used in this guide:
- `miniflux.company` is the FQDN of the Miniflux installation.
- `authentik.company` is the FQDN of the authentik installation.
:::note
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
:::
## authentik configuration
To support the integration of Miniflux with authentik, you need to create an application/provider pair in authentik.
### Create an application and provider in authentik
1. Log in to authentik as an admin, and open the authentik Admin interface.
2. Navigate to **Applications** > **Applications** and click **Create with Provider** to create an application and provider pair. (Alternatively you can create only an application, without a provider, by clicking **Create**.)
- **Application**: provide a descriptive name (e.g., `Miniflux`), an optional group for the type of application, the policy engine mode, and optional UI settings.
- **Choose a Provider type**: Select OAuth2/OpenID Provider as the provider type.
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
- **Redirect URI**:
- Strict: <kbd>https://<em>miniflux.company</em>/oauth2/oidc/callback</kbd>
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/flows-stages/bindings/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page.
3. Click **Submit** to save the new application and provider.
## Miniflux configuration
Add the following environment variables to your Miniflux configuration. Make sure to fill in the client ID, client secret, and OpenID Connect well-known URL from your authentik instance.
```sh
OAUTH2_PROVIDER=oidc
OAUTH2_CLIENT_ID=<Client ID from authentik>
OAUTH2_CLIENT_SECRET=<Client Secret from authentik>
OAUTH2_REDIRECT_URL=https://miniflux.company/oauth2/oidc/callback
OAUTH2_OIDC_DISCOVERY_ENDPOINT=https://authentik.company</em>/application/o/<application slug>/
OAUTH2_USER_CREATION=1
```
:::note
The trailing `.well-known/openid-configuration` is not required for `OAUTH2_OIDC_DISCOVERY_ENDPOINT`
:::
Restart the Miniflux service for the changes to take effect.
## Configuration verification
To confirm that authentik is properly configured with Miniflux, log out of Miniflux, locate the "Sign in with OpenID Connect" button on the login page, click on it, and ensure you can successfully log in using Single Sign-On.

View File

@ -21,6 +21,27 @@ The following placeholders are used in this guide:
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
:::
## authentik configuration
To support the integration of _Service_ with authentik, you need to create an application/provider pair in authentik.
_Any specific info about this integration can go here._
### Create an application and provider in authentik
1. Log in to authentik as an admin, and open the authentik Admin interface.
2. Navigate to **Applications** > **Applications** and click **Create with Provider** to create an application and provider pair. (Alternatively you can create only an application, without a provider, by clicking **Create**.)
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings.
- _If there are any specific settings required, list them here. Refer to the [ownCloud integration documentation](https://github.com/goauthentik/authentik/blob/main/website/integrations/services/owncloud/index.md) for a complex requirements example._
- **Choose a Provider type**
- _If there is a specific provider type required, state that here._
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
- _If there are any specific settings required, list them here. Refer to the [ownCloud integration documentation](https://github.com/goauthentik/authentik/blob/main/website/integrations/services/owncloud/index.md) for a complex requirements example._
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/flows-stages/bindings/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page.
3. Click **Submit** to save the new application and provider.
## Service configuration
Insert Service configuration
@ -29,14 +50,6 @@ Insert Service configuration
2. Continue with steps....
## authentik configuration
Insert authentik configuration
1. Write first step here...
2. Continue with steps....
## Configuration verification
Template sentence that you can typically use here: "To confirm that authentik is properly configured with _Service Name_, log out and log back in via authentik."

View File

@ -141,6 +141,7 @@ module.exports = {
"services/immich/index",
"services/jellyfin/index",
"services/komga/index",
"services/miniflux/index",
"services/node-red/index",
"services/open-webui/index",
"services/semaphore/index",

View File

@ -0,0 +1,188 @@
import React, {
ReactNode,
useState,
isValidElement,
useCallback,
useEffect,
useRef,
} from "react";
import createDOMPurify from "dompurify";
import "./styles.css";
type IntegrationsMultilineCodeblockProps = {
children: ReactNode;
className?: string;
};
type CopyButtonState = {
isCopied: boolean;
className: string;
};
// Configuration for allowed HTML tags in the sanitized output
const allowedTags = ["em", "code", "pre"];
/**
* Initializes DOMPurify safely for both browser and server environments
* @returns DOMPurify instance or null
*/
const getDOMPurify = () => {
if (typeof window !== "undefined") {
return createDOMPurify(window);
}
return null;
};
const domPurifyInstance = getDOMPurify();
/**
* Component for rendering secure code blocks with copy functionality
* - Safely sanitizes HTML content
* - Handles both string and JSX content
* - Provides copy-to-clipboard functionality
*/
const IntegrationsMultilineCodeblock: React.FC<
IntegrationsMultilineCodeblockProps
> = ({ children, className = "" }) => {
// State for managing copy button appearance and behavior
const [copyState, setCopyState] = useState<CopyButtonState>({
isCopied: false,
className: "",
});
// State for storing sanitized content after processing
const [sanitizedContent, setSanitizedContent] = useState<string | null>(
null,
);
// Ref to access the actual DOM element for text extraction
const codeRef = useRef<HTMLElement>(null);
/**
* Recursively converts React children to plain text string
* @param nodes - React children nodes to process
* @returns Flattened string representation
*/
const childrenAsString = useCallback((nodes: ReactNode): string => {
return React.Children.toArray(nodes).reduce<string>((acc, node) => {
if (typeof node === "string") {
return acc + node;
} else if (isValidElement(node)) {
return acc + childrenAsString(node.props.children);
} else if (typeof node === "number") {
return acc + String(node);
}
return acc;
}, "");
}, []);
/**
* Sanitizes content while preserving allowed HTML tags
* @param children - React children to process
* @returns Sanitized HTML string
*/
const processContent = useCallback(
(children: ReactNode): string => {
const rawText = childrenAsString(children);
// Client-side sanitization with DOMPurify
if (domPurifyInstance) {
return domPurifyInstance
.sanitize(rawText, {
ALLOWED_TAGS: allowedTags,
KEEP_CONTENT: true,
RETURN_TRUSTED_TYPE: false,
})
.trim();
}
// Server-side fallback (no sanitization, will be re-processed client-side)
return rawText.trim();
},
[childrenAsString],
);
// Process content after component mounts to ensure browser environment
useEffect(() => {
setSanitizedContent(processContent(children));
}, [children, processContent]);
/**
* Handles copy-to-clipboard functionality
*/
const handleCopy = async (): Promise<void> => {
// Get raw text content from DOM element, stripping all HTML tags
const textToCopy = codeRef.current?.textContent || "";
try {
await navigator.clipboard.writeText(textToCopy);
setCopyState({
isCopied: true,
className: "integration-codeblock__copy-btn--copied",
});
setTimeout(() => {
setCopyState((prev) =>
prev.isCopied ? { isCopied: false, className: "" } : prev,
);
}, 2000);
} catch (error) {
console.error("Failed to copy content:", error);
}
};
// SVG icon component for copy buttons
const Icon: React.FC<{ className: string; path: string }> = React.memo(
({ className, path }) => (
<svg viewBox="0 0 24 24" className={className}>
<path fill="currentColor" d={path} />
</svg>
),
);
return (
<pre className={`integration-codeblock ${className}`.trim()}>
{/* Attach ref to access rendered text content */}
<code className="integration-codeblock__content" ref={codeRef}>
{typeof children === "string" ? (
// Render sanitized HTML for string content
<span
dangerouslySetInnerHTML={{
__html: sanitizedContent || children,
}}
/>
) : (
// Directly render JSX children (already safe)
children
)}
</code>
{/* Copy button with state-dependent styling */}
<button
onClick={handleCopy}
className={`integration-codeblock__copy-btn ${copyState.className}`.trim()}
aria-label="Copy code to clipboard"
title="Copy"
type="button"
disabled={copyState.isCopied}
>
<span
className="integration-codeblock__copy-icons"
aria-hidden="true"
>
{/* Copy icon */}
<Icon
className="integration-codeblock__copy-icon"
path="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"
/>
{/* Success checkmark */}
<Icon
className="integration-codeblock__copy-success-icon"
path="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"
/>
</span>
</button>
</pre>
);
};
export default IntegrationsMultilineCodeblock;

View File

@ -0,0 +1,112 @@
/* Base styles */
.integration-codeblock {
position: relative;
background: #f8f8f8;
border-radius: 8px;
font-family: "Menlo", "Consolas", monospace;
margin: 1.5rem 0;
padding: 1rem 2rem 1.5rem 1.5rem;
display: block;
overflow-x: auto;
font-size: 0.9em;
line-height: 1.5;
transition: box-shadow 0.2s ease;
}
.integration-codeblock:hover {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.integration-codeblock code {
margin: 0;
padding: 0;
display: block;
background: transparent !important;
white-space: pre-wrap;
}
/* Copy button */
.integration-codeblock__copy-btn {
position: absolute;
top: 0.75rem;
right: 0.75rem;
padding: 0.5rem;
border: none;
background: none;
cursor: pointer;
color: #666;
border-radius: 4px;
transition: all 0.2s ease;
}
.integration-codeblock__copy-btn:hover {
background: rgba(0, 0, 0, 0.05);
}
.integration-codeblock__copy-btn:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
.integration-codeblock__copy-btn[disabled] {
cursor: not-allowed;
opacity: 0.7;
}
.integration-codeblock__copy-icons {
display: block;
position: relative;
width: 20px;
height: 20px;
}
.integration-codeblock__copy-icon,
.integration-codeblock__copy-success-icon {
position: absolute;
top: 0;
left: 0;
transition:
opacity 0.2s ease,
transform 0.2s ease;
}
.integration-codeblock__copy-success-icon {
opacity: 0;
transform: scale(0.8);
}
.integration-codeblock__copy-btn--copied .integration-codeblock__copy-icon {
opacity: 0;
transform: scale(1.1);
}
.integration-codeblock__copy-btn--copied
.integration-codeblock__copy-success-icon {
opacity: 1;
transform: scale(1);
color: #10b981;
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
.integration-codeblock {
background: #1e1e1e;
color: #f8f8f8;
}
.integration-codeblock:hover {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
.integration-codeblock__copy-btn {
color: #999;
}
.integration-codeblock__copy-btn:hover {
background: rgba(255, 255, 255, 0.05);
}
.integration-codeblock__copy-success-icon {
color: #10b981;
}
}