Compare commits
37 Commits
flows/conc
...
tests/e2e/
| Author | SHA1 | Date | |
|---|---|---|---|
| 25181d079e | |||
| 5b91cb5ff3 | |||
| 8ba5fde5ba | |||
| 2802deb497 | |||
| 3eccef88aa | |||
| 8f50dfa0c5 | |||
| 8417d8508f | |||
| b2c2fc001b | |||
| f60312cbbc | |||
| 7614b17a05 | |||
| 8947376edb | |||
| ce23209ae8 | |||
| 0b806b7130 | |||
| 9538cf4690 | |||
| 63da458fb3 | |||
| 873dab29a9 | |||
| 1e96c80593 | |||
| ee4a922234 | |||
| 37a2eff716 | |||
| 50e2f1c474 | |||
| ab7338b50e | |||
| bcdc6fcd36 | |||
| 98c3e0d68b | |||
| a2b82b6448 | |||
| 0456ace646 | |||
| d3a11ce810 | |||
| bfd1445c69 | |||
| c2b3e9b05c | |||
| 2c7d841e4a | |||
| c5d13c4a15 | |||
| 079ef6e114 | |||
| 98bfca0b4d | |||
| a247bd5b9f | |||
| 27856ec301 | |||
| e4a8c05d25 | |||
| cb2e0c6d54 | |||
| f37e1ca642 |
@ -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
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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),
|
||||
]
|
||||
@ -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"),
|
||||
),
|
||||
]
|
||||
@ -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
|
||||
|
||||
@ -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": "",
|
||||
},
|
||||
)
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -50,7 +50,8 @@ class NotificationTransportSerializer(ModelSerializer):
|
||||
"mode",
|
||||
"mode_verbose",
|
||||
"webhook_url",
|
||||
"webhook_mapping",
|
||||
"webhook_mapping_body",
|
||||
"webhook_mapping_headers",
|
||||
"send_once",
|
||||
]
|
||||
|
||||
|
||||
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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},
|
||||
|
||||
@ -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"):
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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")
|
||||
)
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -45,6 +45,8 @@ redis:
|
||||
# url: ""
|
||||
# transport_options: ""
|
||||
|
||||
http_timeout: 30
|
||||
|
||||
cache:
|
||||
# url: ""
|
||||
timeout: 300
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
6
go.mod
@ -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
12
go.sum
@ -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=
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;"
|
||||
|
||||
@ -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.
@ -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.
@ -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: "
|
||||
|
||||
51
schema.yml
51
schema.yml
@ -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
|
||||
|
||||
@ -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",
|
||||
},
|
||||
)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
2335
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
};
|
||||
@ -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";
|
||||
|
||||
@ -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>`;
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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(
|
||||
"<application slug>",
|
||||
"<application slug>",
|
||||
this.provider.assignedApplicationSlug,
|
||||
);
|
||||
},
|
||||
]}
|
||||
></ak-markdown>
|
||||
></ak-mdx>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
@ -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>`;
|
||||
|
||||
@ -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>`;
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -1 +0,0 @@
|
||||
/* User customisable */
|
||||
@ -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 {
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
53
web/src/elements/ak-mdx/MDXModuleContext.ts
Normal file
53
web/src/elements/ak-mdx/MDXModuleContext.ts
Normal 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);
|
||||
}
|
||||
220
web/src/elements/ak-mdx/ak-mdx.tsx
Normal file
220
web/src/elements/ak-mdx/ak-mdx.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
61
web/src/elements/ak-mdx/components/MDXAnchor.tsx
Normal file
61
web/src/elements/ak-mdx/components/MDXAnchor.tsx
Normal 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";
|
||||
20
web/src/elements/ak-mdx/components/MDXWrapper.tsx
Normal file
20
web/src/elements/ak-mdx/components/MDXWrapper.tsx
Normal 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}</>;
|
||||
};
|
||||
2
web/src/elements/ak-mdx/index.ts
Normal file
2
web/src/elements/ak-mdx/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./ak-mdx";
|
||||
export { AKMDX as default } from "./ak-mdx";
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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);
|
||||
|
||||
@ -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 = {
|
||||
@ -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: "",
|
||||
|
||||
157
web/src/elements/sync/SyncStatusCard.stories.ts
Normal file
157
web/src/elements/sync/SyncStatusCard.stories.ts
Normal 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>`;
|
||||
},
|
||||
};
|
||||
@ -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
12
web/src/global.d.ts
vendored
@ -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 {
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
"expect-webdriverio",
|
||||
"grecaptcha"
|
||||
],
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"experimentalDecorators": true,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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",
|
||||
|
||||
67
website/integrations/services/miniflux/index.md
Normal file
67
website/integrations/services/miniflux/index.md
Normal 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.
|
||||
@ -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."
|
||||
|
||||
@ -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",
|
||||
|
||||
188
website/src/components/MultilineCodeblock/index.tsx
Normal file
188
website/src/components/MultilineCodeblock/index.tsx
Normal 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;
|
||||
112
website/src/components/MultilineCodeblock/styles.css
Normal file
112
website/src/components/MultilineCodeblock/styles.css
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user