Compare commits

..

1 Commits

118 changed files with 7225 additions and 13193 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2025.4.0 current_version = 2025.2.4
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?

View File

@ -70,18 +70,22 @@ jobs:
- name: checkout stable - name: checkout stable
run: | run: |
# Copy current, latest config to local # Copy current, latest config to local
# Temporarly comment the .github backup while migrating to uv
cp authentik/lib/default.yml local.env.yml cp authentik/lib/default.yml local.env.yml
cp -R .github .. # cp -R .github ..
cp -R scripts .. cp -R scripts ..
git checkout $(git tag --sort=version:refname | grep '^version/' | grep -vE -- '-rc[0-9]+$' | tail -n1) git checkout $(git tag --sort=version:refname | grep '^version/' | grep -vE -- '-rc[0-9]+$' | tail -n1)
rm -rf .github/ scripts/ # rm -rf .github/ scripts/
mv ../.github ../scripts . # mv ../.github ../scripts .
rm -rf scripts/
mv ../scripts .
- name: Setup authentik env (stable) - name: Setup authentik env (stable)
uses: ./.github/actions/setup uses: ./.github/actions/setup
with: with:
postgresql_version: ${{ matrix.psql }} postgresql_version: ${{ matrix.psql }}
continue-on-error: true
- name: run migrations to stable - name: run migrations to stable
run: uv run python -m lifecycle.migrate run: poetry run python -m lifecycle.migrate
- name: checkout current code - name: checkout current code
run: | run: |
set -x set -x
@ -228,6 +232,7 @@ jobs:
needs: needs:
- lint - lint
- test-migrations - test-migrations
- test-migrations-from-stable
- test-unittest - test-unittest
- test-integration - test-integration
- test-e2e - test-e2e

View File

@ -3,10 +3,10 @@ on:
push: push:
branches: [main] branches: [main]
paths: paths:
- packages/docusaurus-config/** - packages/docusaurus-config
- packages/eslint-config/** - packages/eslint-config
- packages/prettier-config/** - packages/prettier-config
- packages/tsconfig/** - packages/tsconfig
workflow_dispatch: workflow_dispatch:
jobs: jobs:
publish: publish:

View File

@ -40,8 +40,7 @@ COPY ./web /work/web/
COPY ./website /work/website/ COPY ./website /work/website/
COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
RUN npm run build && \ RUN npm run build
npm run build:sfe
# Stage 3: Build go proxy # Stage 3: Build go proxy
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder

View File

@ -20,8 +20,8 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
| Version | Supported | | Version | Supported |
| --------- | --------- | | --------- | --------- |
| 2024.12.x | ✅ |
| 2025.2.x | ✅ | | 2025.2.x | ✅ |
| 2025.4.x | ✅ |
## Reporting a Vulnerability ## Reporting a Vulnerability

View File

@ -2,7 +2,7 @@
from os import environ from os import environ
__version__ = "2025.4.0" __version__ = "2025.2.4"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -16,7 +16,7 @@ def migrate_custom_css(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
if not path.exists(): if not path.exists():
return return
css = path.read_text() css = path.read_text()
Brand.objects.using(db_alias).all().update(branding_custom_css=css) Brand.objects.using(db_alias).update(branding_custom_css=css)
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -99,8 +99,9 @@ class GroupSerializer(ModelSerializer):
if superuser if superuser
else "authentik_core.disable_group_superuser" else "authentik_core.disable_group_superuser"
) )
if self.instance or superuser: has_perm = user.has_perm(perm)
has_perm = user.has_perm(perm) or user.has_perm(perm, self.instance) if self.instance and not has_perm:
has_perm = user.has_perm(perm, self.instance)
if not has_perm: if not has_perm:
raise ValidationError( raise ValidationError(
_( _(

View File

@ -2,7 +2,6 @@
from django.apps import apps from django.apps import apps
from django.contrib.auth.management import create_permissions from django.contrib.auth.management import create_permissions
from django.core.management import call_command
from django.core.management.base import BaseCommand, no_translations from django.core.management.base import BaseCommand, no_translations
from guardian.management import create_anonymous_user from guardian.management import create_anonymous_user
@ -17,10 +16,6 @@ class Command(BaseCommand):
"""Check permissions for all apps""" """Check permissions for all apps"""
for tenant in Tenant.objects.filter(ready=True): for tenant in Tenant.objects.filter(ready=True):
with tenant: with tenant:
# See https://code.djangoproject.com/ticket/28417
# Remove potential lingering old permissions
call_command("remove_stale_contenttypes", "--no-input")
for app in apps.get_app_configs(): for app in apps.get_app_configs():
self.stdout.write(f"Checking app {app.name} ({app.label})\n") self.stdout.write(f"Checking app {app.name} ({app.label})\n")
create_permissions(app, verbosity=0) create_permissions(app, verbosity=0)

View File

@ -31,10 +31,7 @@ class PickleSerializer:
def loads(self, data): def loads(self, data):
"""Unpickle data to be loaded from redis""" """Unpickle data to be loaded from redis"""
try:
return pickle.loads(data) # nosec return pickle.loads(data) # nosec
except Exception:
return {}
def _migrate_session( def _migrate_session(

View File

@ -1,27 +0,0 @@
# Generated by Django 5.1.9 on 2025-05-14 11:15
from django.apps.registry import Apps
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def remove_old_authenticated_session_content_type(
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
):
db_alias = schema_editor.connection.alias
ContentType = apps.get_model("contenttypes", "ContentType")
ContentType.objects.using(db_alias).filter(model="oldauthenticatedsession").delete()
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0047_delete_oldauthenticatedsession"),
]
operations = [
migrations.RunPython(
code=remove_old_authenticated_session_content_type,
),
]

View File

@ -124,16 +124,6 @@ class TestGroupsAPI(APITestCase):
{"is_superuser": ["User does not have permission to set superuser status to True."]}, {"is_superuser": ["User does not have permission to set superuser status to True."]},
) )
def test_superuser_no_perm_no_superuser(self):
"""Test creating a group without permission and without superuser flag"""
assign_perm("authentik_core.add_group", self.login_user)
self.client.force_login(self.login_user)
res = self.client.post(
reverse("authentik_api:group-list"),
data={"name": generate_id(), "is_superuser": False},
)
self.assertEqual(res.status_code, 201)
def test_superuser_update_no_perm(self): def test_superuser_update_no_perm(self):
"""Test updating a superuser group without permission""" """Test updating a superuser group without permission"""
group = Group.objects.create(name=generate_id(), is_superuser=True) group = Group.objects.create(name=generate_id(), is_superuser=True)

View File

@ -132,14 +132,13 @@ class LicenseKey:
"""Get a summarized version of all (not expired) licenses""" """Get a summarized version of all (not expired) licenses"""
total = LicenseKey(get_license_aud(), 0, "Summarized license", 0, 0) total = LicenseKey(get_license_aud(), 0, "Summarized license", 0, 0)
for lic in License.objects.all(): for lic in License.objects.all():
if lic.is_valid:
total.internal_users += lic.internal_users total.internal_users += lic.internal_users
total.external_users += lic.external_users total.external_users += lic.external_users
total.license_flags.extend(lic.status.license_flags)
exp_ts = int(mktime(lic.expiry.timetuple())) exp_ts = int(mktime(lic.expiry.timetuple()))
if total.exp == 0: if total.exp == 0:
total.exp = exp_ts total.exp = exp_ts
total.exp = max(total.exp, exp_ts) total.exp = max(total.exp, exp_ts)
total.license_flags.extend(lic.status.license_flags)
return total return total
@staticmethod @staticmethod

View File

@ -39,10 +39,6 @@ class License(SerializerModel):
internal_users = models.BigIntegerField() internal_users = models.BigIntegerField()
external_users = models.BigIntegerField() external_users = models.BigIntegerField()
@property
def is_valid(self) -> bool:
return self.expiry >= now()
@property @property
def serializer(self) -> type[BaseSerializer]: def serializer(self) -> type[BaseSerializer]:
from authentik.enterprise.api import LicenseSerializer from authentik.enterprise.api import LicenseSerializer

View File

@ -8,7 +8,6 @@ from django.test import TestCase
from django.utils.timezone import now from django.utils.timezone import now
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from authentik.core.models import User
from authentik.enterprise.license import LicenseKey from authentik.enterprise.license import LicenseKey
from authentik.enterprise.models import ( from authentik.enterprise.models import (
THRESHOLD_READ_ONLY_WEEKS, THRESHOLD_READ_ONLY_WEEKS,
@ -72,9 +71,9 @@ class TestEnterpriseLicense(TestCase):
) )
def test_valid_multiple(self): def test_valid_multiple(self):
"""Check license verification""" """Check license verification"""
lic = License.objects.create(key=generate_id(), expiry=expiry_valid) lic = License.objects.create(key=generate_id())
self.assertTrue(lic.status.status().is_valid) self.assertTrue(lic.status.status().is_valid)
lic2 = License.objects.create(key=generate_id(), expiry=expiry_valid) lic2 = License.objects.create(key=generate_id())
self.assertTrue(lic2.status.status().is_valid) self.assertTrue(lic2.status.status().is_valid)
total = LicenseKey.get_total() total = LicenseKey.get_total()
self.assertEqual(total.internal_users, 200) self.assertEqual(total.internal_users, 200)
@ -233,9 +232,7 @@ class TestEnterpriseLicense(TestCase):
) )
def test_expiry_expired(self): def test_expiry_expired(self):
"""Check license verification""" """Check license verification"""
User.objects.all().delete() License.objects.create(key=generate_id())
License.objects.all().delete()
License.objects.create(key=generate_id(), expiry=expiry_expired)
self.assertEqual(LicenseKey.get_total().summary().status, LicenseUsageStatus.EXPIRED) self.assertEqual(LicenseKey.get_total().summary().status, LicenseUsageStatus.EXPIRED)
@patch( @patch(

View File

@ -15,7 +15,6 @@
{% endblock %} {% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/sfe/bootstrap.min.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/sfe/bootstrap.min.css' %}">
<meta name="sentry-trace" content="{{ sentry_trace }}" /> <meta name="sentry-trace" content="{{ sentry_trace }}" />
<link rel="prefetch" href="{{ flow_background_url }}" />
{% include "base/header_js.html" %} {% include "base/header_js.html" %}
<style> <style>
html, html,
@ -23,7 +22,7 @@
height: 100%; height: 100%;
} }
body { body {
background-image: url("{{ flow_background_url }}"); background-image: url("{{ flow.background_url }}");
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
} }

View File

@ -5,7 +5,7 @@
{% block head_before %} {% block head_before %}
{{ block.super }} {{ block.super }}
<link rel="prefetch" href="{{ flow_background_url }}" /> <link rel="prefetch" href="{{ flow.background_url }}" />
{% if flow.compatibility_mode and not inspector %} {% if flow.compatibility_mode and not inspector %}
<script>ShadyDOM = { force: !navigator.webdriver };</script> <script>ShadyDOM = { force: !navigator.webdriver };</script>
{% endif %} {% endif %}
@ -21,7 +21,7 @@ window.authentik.flow = {
<script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script> <script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script>
<style> <style>
:root { :root {
--ak-flow-background: url("{{ flow_background_url }}"); --ak-flow-background: url("{{ flow.background_url }}");
} }
</style> </style>
{% endblock %} {% endblock %}

View File

@ -69,7 +69,6 @@ SESSION_KEY_APPLICATION_PRE = "authentik/flows/application_pre"
SESSION_KEY_GET = "authentik/flows/get" SESSION_KEY_GET = "authentik/flows/get"
SESSION_KEY_POST = "authentik/flows/post" SESSION_KEY_POST = "authentik/flows/post"
SESSION_KEY_HISTORY = "authentik/flows/history" SESSION_KEY_HISTORY = "authentik/flows/history"
SESSION_KEY_AUTH_STARTED = "authentik/flows/auth_started"
QS_KEY_TOKEN = "flow_token" # nosec QS_KEY_TOKEN = "flow_token" # nosec
QS_QUERY = "query" QS_QUERY = "query"
@ -454,7 +453,6 @@ class FlowExecutorView(APIView):
SESSION_KEY_APPLICATION_PRE, SESSION_KEY_APPLICATION_PRE,
SESSION_KEY_PLAN, SESSION_KEY_PLAN,
SESSION_KEY_GET, SESSION_KEY_GET,
SESSION_KEY_AUTH_STARTED,
# We might need the initial POST payloads for later requests # We might need the initial POST payloads for later requests
# SESSION_KEY_POST, # SESSION_KEY_POST,
# We don't delete the history on purpose, as a user might # We don't delete the history on purpose, as a user might

View File

@ -6,23 +6,14 @@ from django.shortcuts import get_object_or_404
from ua_parser.user_agent_parser import Parse from ua_parser.user_agent_parser import Parse
from authentik.core.views.interface import InterfaceView from authentik.core.views.interface import InterfaceView
from authentik.flows.models import Flow, FlowDesignation from authentik.flows.models import Flow
from authentik.flows.views.executor import SESSION_KEY_AUTH_STARTED
class FlowInterfaceView(InterfaceView): class FlowInterfaceView(InterfaceView):
"""Flow interface""" """Flow interface"""
def get_context_data(self, **kwargs: Any) -> dict[str, Any]: def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
flow = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug")) kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
if (
not self.request.user.is_authenticated
and flow.designation == FlowDesignation.AUTHENTICATION
):
self.request.session[SESSION_KEY_AUTH_STARTED] = True
self.request.session.save()
kwargs["flow"] = flow
kwargs["flow_background_url"] = flow.background_url(self.request)
kwargs["inspector"] = "inspector" in self.request.GET kwargs["inspector"] = "inspector" in self.request.GET
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)

View File

@ -363,9 +363,6 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
pool_options = config.get_dict_from_b64_json("postgresql.pool_options", True) pool_options = config.get_dict_from_b64_json("postgresql.pool_options", True)
if not pool_options: if not pool_options:
pool_options = True pool_options = True
# FIXME: Temporarily force pool to be deactivated.
# See https://github.com/goauthentik/authentik/issues/14320
pool_options = False
db = { db = {
"default": { "default": {

View File

@ -494,88 +494,86 @@ class TestConfig(TestCase):
}, },
) )
# FIXME: Temporarily force pool to be deactivated. def test_db_pool(self):
# See https://github.com/goauthentik/authentik/issues/14320 """Test DB Config with pool"""
# def test_db_pool(self): config = ConfigLoader()
# """Test DB Config with pool""" config.set("postgresql.host", "foo")
# config = ConfigLoader() config.set("postgresql.name", "foo")
# config.set("postgresql.host", "foo") config.set("postgresql.user", "foo")
# config.set("postgresql.name", "foo") config.set("postgresql.password", "foo")
# config.set("postgresql.user", "foo") config.set("postgresql.port", "foo")
# config.set("postgresql.password", "foo") config.set("postgresql.test.name", "foo")
# config.set("postgresql.port", "foo") config.set("postgresql.use_pool", True)
# config.set("postgresql.test.name", "foo") conf = django_db_config(config)
# config.set("postgresql.use_pool", True) self.assertEqual(
# conf = django_db_config(config) conf,
# self.assertEqual( {
# conf, "default": {
# { "ENGINE": "authentik.root.db",
# "default": { "HOST": "foo",
# "ENGINE": "authentik.root.db", "NAME": "foo",
# "HOST": "foo", "OPTIONS": {
# "NAME": "foo", "pool": True,
# "OPTIONS": { "sslcert": None,
# "pool": True, "sslkey": None,
# "sslcert": None, "sslmode": None,
# "sslkey": None, "sslrootcert": None,
# "sslmode": None, },
# "sslrootcert": None, "PASSWORD": "foo",
# }, "PORT": "foo",
# "PASSWORD": "foo", "TEST": {"NAME": "foo"},
# "PORT": "foo", "USER": "foo",
# "TEST": {"NAME": "foo"}, "CONN_MAX_AGE": 0,
# "USER": "foo", "CONN_HEALTH_CHECKS": False,
# "CONN_MAX_AGE": 0, "DISABLE_SERVER_SIDE_CURSORS": False,
# "CONN_HEALTH_CHECKS": False, }
# "DISABLE_SERVER_SIDE_CURSORS": False, },
# } )
# },
# )
# def test_db_pool_options(self): def test_db_pool_options(self):
# """Test DB Config with pool""" """Test DB Config with pool"""
# config = ConfigLoader() config = ConfigLoader()
# config.set("postgresql.host", "foo") config.set("postgresql.host", "foo")
# config.set("postgresql.name", "foo") config.set("postgresql.name", "foo")
# config.set("postgresql.user", "foo") config.set("postgresql.user", "foo")
# config.set("postgresql.password", "foo") config.set("postgresql.password", "foo")
# config.set("postgresql.port", "foo") config.set("postgresql.port", "foo")
# config.set("postgresql.test.name", "foo") config.set("postgresql.test.name", "foo")
# config.set("postgresql.use_pool", True) config.set("postgresql.use_pool", True)
# config.set( config.set(
# "postgresql.pool_options", "postgresql.pool_options",
# base64.b64encode( base64.b64encode(
# dumps( dumps(
# { {
# "max_size": 15, "max_size": 15,
# } }
# ).encode() ).encode()
# ).decode(), ).decode(),
# ) )
# conf = django_db_config(config) conf = django_db_config(config)
# self.assertEqual( self.assertEqual(
# conf, conf,
# { {
# "default": { "default": {
# "ENGINE": "authentik.root.db", "ENGINE": "authentik.root.db",
# "HOST": "foo", "HOST": "foo",
# "NAME": "foo", "NAME": "foo",
# "OPTIONS": { "OPTIONS": {
# "pool": { "pool": {
# "max_size": 15, "max_size": 15,
# }, },
# "sslcert": None, "sslcert": None,
# "sslkey": None, "sslkey": None,
# "sslmode": None, "sslmode": None,
# "sslrootcert": None, "sslrootcert": None,
# }, },
# "PASSWORD": "foo", "PASSWORD": "foo",
# "PORT": "foo", "PORT": "foo",
# "TEST": {"NAME": "foo"}, "TEST": {"NAME": "foo"},
# "USER": "foo", "USER": "foo",
# "CONN_MAX_AGE": 0, "CONN_MAX_AGE": 0,
# "CONN_HEALTH_CHECKS": False, "CONN_HEALTH_CHECKS": False,
# "DISABLE_SERVER_SIDE_CURSORS": False, "DISABLE_SERVER_SIDE_CURSORS": False,
# } }
# }, },
# ) )

View File

@ -39,4 +39,3 @@ class AuthentikPoliciesConfig(ManagedAppConfig):
label = "authentik_policies" label = "authentik_policies"
verbose_name = "authentik Policies" verbose_name = "authentik Policies"
default = True default = True
mountpoint = "policy/"

View File

@ -1,89 +0,0 @@
{% extends 'login/base_full.html' %}
{% load static %}
{% load i18n %}
{% block head %}
{{ block.super }}
<script>
let redirecting = false;
const checkAuth = async () => {
if (redirecting) return true;
const url = "{{ check_auth_url }}";
console.debug("authentik/policies/buffer: Checking authentication...");
try {
const result = await fetch(url, {
method: "HEAD",
});
if (result.status >= 400) {
return false
}
console.debug("authentik/policies/buffer: Continuing");
redirecting = true;
if ("{{ auth_req_method }}" === "post") {
document.querySelector("form").submit();
} else {
window.location.assign("{{ continue_url|escapejs }}");
}
} catch {
return false;
}
};
let timeout = 100;
let offset = 20;
let attempt = 0;
const main = async () => {
attempt += 1;
await checkAuth();
console.debug(`authentik/policies/buffer: Waiting ${timeout}ms...`);
setTimeout(main, timeout);
timeout += (offset * attempt);
if (timeout >= 2000) {
timeout = 2000;
}
}
document.addEventListener("visibilitychange", async () => {
if (document.hidden) return;
console.debug("authentik/policies/buffer: Checking authentication on tab activate...");
await checkAuth();
});
main();
</script>
{% endblock %}
{% block title %}
{% trans 'Waiting for authentication...' %} - {{ brand.branding_title }}
{% endblock %}
{% block card_title %}
{% trans 'Waiting for authentication...' %}
{% endblock %}
{% block card %}
<form class="pf-c-form" method="{{ auth_req_method }}" action="{{ continue_url }}">
{% if auth_req_method == "post" %}
{% for key, value in auth_req_body.items %}
<input type="hidden" name="{{ key }}" value="{{ value }}" />
{% endfor %}
{% endif %}
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<div class="pf-c-empty-state__icon">
<span class="pf-c-spinner pf-m-xl" role="progressbar">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
</div>
<h1 class="pf-c-title pf-m-lg">
{% trans "You're already authenticating in another tab. This page will refresh once authentication is completed." %}
</h1>
</div>
</div>
<div class="pf-c-form__group pf-m-action">
<a href="{{ auth_req_url }}" class="pf-c-button pf-m-primary pf-m-block">
{% trans "Authenticate in this tab" %}
</a>
</div>
</form>
{% endblock %}

View File

@ -1,121 +0,0 @@
from django.contrib.auth.models import AnonymousUser
from django.contrib.sessions.middleware import SessionMiddleware
from django.http import HttpResponse
from django.test import RequestFactory, TestCase
from django.urls import reverse
from authentik.core.models import Application, Provider
from authentik.core.tests.utils import create_test_flow, create_test_user
from authentik.flows.models import FlowDesignation
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import dummy_get_response
from authentik.policies.views import (
QS_BUFFER_ID,
SESSION_KEY_BUFFER,
BufferedPolicyAccessView,
BufferView,
PolicyAccessView,
)
class TestPolicyViews(TestCase):
"""Test PolicyAccessView"""
def setUp(self):
super().setUp()
self.factory = RequestFactory()
self.user = create_test_user()
def test_pav(self):
"""Test simple policy access view"""
provider = Provider.objects.create(
name=generate_id(),
)
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
class TestView(PolicyAccessView):
def resolve_provider_application(self):
self.provider = provider
self.application = app
def get(self, *args, **kwargs):
return HttpResponse("foo")
req = self.factory.get("/")
req.user = self.user
res = TestView.as_view()(req)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.content, b"foo")
def test_pav_buffer(self):
"""Test simple policy access view"""
provider = Provider.objects.create(
name=generate_id(),
)
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
class TestView(BufferedPolicyAccessView):
def resolve_provider_application(self):
self.provider = provider
self.application = app
def get(self, *args, **kwargs):
return HttpResponse("foo")
req = self.factory.get("/")
req.user = AnonymousUser()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(req)
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
req.session.save()
res = TestView.as_view()(req)
self.assertEqual(res.status_code, 302)
self.assertTrue(res.url.startswith(reverse("authentik_policies:buffer")))
def test_pav_buffer_skip(self):
"""Test simple policy access view (skip buffer)"""
provider = Provider.objects.create(
name=generate_id(),
)
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
class TestView(BufferedPolicyAccessView):
def resolve_provider_application(self):
self.provider = provider
self.application = app
def get(self, *args, **kwargs):
return HttpResponse("foo")
req = self.factory.get("/?skip_buffer=true")
req.user = AnonymousUser()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(req)
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
req.session.save()
res = TestView.as_view()(req)
self.assertEqual(res.status_code, 302)
self.assertTrue(res.url.startswith(reverse("authentik_flows:default-authentication")))
def test_buffer(self):
"""Test buffer view"""
uid = generate_id()
req = self.factory.get(f"/?{QS_BUFFER_ID}={uid}")
req.user = AnonymousUser()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(req)
ts = generate_id()
req.session[SESSION_KEY_BUFFER % uid] = {
"method": "get",
"body": {},
"url": f"/{ts}",
}
req.session.save()
res = BufferView.as_view()(req)
self.assertEqual(res.status_code, 200)
self.assertIn(ts, res.render().content.decode())

View File

@ -1,14 +1,7 @@
"""API URLs""" """API URLs"""
from django.urls import path
from authentik.policies.api.bindings import PolicyBindingViewSet from authentik.policies.api.bindings import PolicyBindingViewSet
from authentik.policies.api.policies import PolicyViewSet from authentik.policies.api.policies import PolicyViewSet
from authentik.policies.views import BufferView
urlpatterns = [
path("buffer", BufferView.as_view(), name="buffer"),
]
api_urlpatterns = [ api_urlpatterns = [
("policies/all", PolicyViewSet), ("policies/all", PolicyViewSet),

View File

@ -1,37 +1,23 @@
"""authentik access helper classes""" """authentik access helper classes"""
from typing import Any from typing import Any
from uuid import uuid4
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import AccessMixin from django.contrib.auth.mixins import AccessMixin
from django.contrib.auth.views import redirect_to_login from django.contrib.auth.views import redirect_to_login
from django.http import HttpRequest, HttpResponse, QueryDict from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.http import urlencode
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic.base import TemplateView, View from django.views.generic.base import View
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import Application, Provider, User from authentik.core.models import Application, Provider, User
from authentik.flows.models import Flow, FlowDesignation from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_POST
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import (
SESSION_KEY_APPLICATION_PRE,
SESSION_KEY_AUTH_STARTED,
SESSION_KEY_PLAN,
SESSION_KEY_POST,
)
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
from authentik.policies.denied import AccessDeniedResponse from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyRequest, PolicyResult from authentik.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger() LOGGER = get_logger()
QS_BUFFER_ID = "af_bf_id"
QS_SKIP_BUFFER = "skip_buffer"
SESSION_KEY_BUFFER = "authentik/policies/pav_buffer/%s"
class RequestValidationError(SentryIgnoredException): class RequestValidationError(SentryIgnoredException):
@ -139,65 +125,3 @@ class PolicyAccessView(AccessMixin, View):
for message in result.messages: for message in result.messages:
messages.error(self.request, _(message)) messages.error(self.request, _(message))
return result return result
def url_with_qs(url: str, **kwargs):
"""Update/set querystring of `url` with the parameters in `kwargs`. Original query string
parameters are retained"""
if "?" not in url:
return url + f"?{urlencode(kwargs)}"
url, _, qs = url.partition("?")
qs = QueryDict(qs, mutable=True)
qs.update(kwargs)
return url + f"?{urlencode(qs.items())}"
class BufferView(TemplateView):
"""Buffer view"""
template_name = "policies/buffer.html"
def get_context_data(self, **kwargs):
buf_id = self.request.GET.get(QS_BUFFER_ID)
buffer: dict = self.request.session.get(SESSION_KEY_BUFFER % buf_id)
kwargs["auth_req_method"] = buffer["method"]
kwargs["auth_req_body"] = buffer["body"]
kwargs["auth_req_url"] = url_with_qs(buffer["url"], **{QS_SKIP_BUFFER: True})
kwargs["check_auth_url"] = reverse("authentik_api:user-me")
kwargs["continue_url"] = url_with_qs(buffer["url"], **{QS_BUFFER_ID: buf_id})
return super().get_context_data(**kwargs)
class BufferedPolicyAccessView(PolicyAccessView):
"""PolicyAccessView which buffers access requests in case the user is not logged in"""
def handle_no_permission(self):
plan: FlowPlan | None = self.request.session.get(SESSION_KEY_PLAN)
authenticating = self.request.session.get(SESSION_KEY_AUTH_STARTED)
if plan:
flow = Flow.objects.filter(pk=plan.flow_pk).first()
if not flow or flow.designation != FlowDesignation.AUTHENTICATION:
LOGGER.debug("Not buffering request, no flow or flow not for authentication")
return super().handle_no_permission()
if not plan and authenticating is None:
LOGGER.debug("Not buffering request, no flow plan active")
return super().handle_no_permission()
if self.request.GET.get(QS_SKIP_BUFFER):
LOGGER.debug("Not buffering request, explicit skip")
return super().handle_no_permission()
buffer_id = str(uuid4())
LOGGER.debug("Buffering access request", bf_id=buffer_id)
self.request.session[SESSION_KEY_BUFFER % buffer_id] = {
"body": self.request.POST,
"url": self.request.build_absolute_uri(self.request.get_full_path()),
"method": self.request.method.lower(),
}
return redirect(
url_with_qs(reverse("authentik_policies:buffer"), **{QS_BUFFER_ID: buffer_id})
)
def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs)
if QS_BUFFER_ID in self.request.GET:
self.request.session.pop(SESSION_KEY_BUFFER % self.request.GET[QS_BUFFER_ID], None)
return response

View File

@ -30,7 +30,7 @@ from authentik.flows.stage import StageView
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message
from authentik.policies.types import PolicyRequest from authentik.policies.types import PolicyRequest
from authentik.policies.views import BufferedPolicyAccessView, RequestValidationError from authentik.policies.views import PolicyAccessView, RequestValidationError
from authentik.providers.oauth2.constants import ( from authentik.providers.oauth2.constants import (
PKCE_METHOD_PLAIN, PKCE_METHOD_PLAIN,
PKCE_METHOD_S256, PKCE_METHOD_S256,
@ -326,7 +326,7 @@ class OAuthAuthorizationParams:
return code return code
class AuthorizationFlowInitView(BufferedPolicyAccessView): class AuthorizationFlowInitView(PolicyAccessView):
"""OAuth2 Flow initializer, checks access to application and starts flow""" """OAuth2 Flow initializer, checks access to application and starts flow"""
params: OAuthAuthorizationParams params: OAuthAuthorizationParams

View File

@ -18,11 +18,11 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
from authentik.flows.stage import RedirectStage from authentik.flows.stage import RedirectStage
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
from authentik.policies.views import BufferedPolicyAccessView from authentik.policies.views import PolicyAccessView
from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider
class RACStartView(BufferedPolicyAccessView): class RACStartView(PolicyAccessView):
"""Start a RAC connection by checking access and creating a connection token""" """Start a RAC connection by checking access and creating a connection token"""
endpoint: Endpoint endpoint: Endpoint

View File

@ -35,8 +35,8 @@ REQUEST_KEY_SAML_SIG_ALG = "SigAlg"
REQUEST_KEY_SAML_RESPONSE = "SAMLResponse" REQUEST_KEY_SAML_RESPONSE = "SAMLResponse"
REQUEST_KEY_RELAY_STATE = "RelayState" REQUEST_KEY_RELAY_STATE = "RelayState"
PLAN_CONTEXT_SAML_AUTH_N_REQUEST = "authentik/providers/saml/authn_request" SESSION_KEY_AUTH_N_REQUEST = "authentik/providers/saml/authn_request"
PLAN_CONTEXT_SAML_LOGOUT_REQUEST = "authentik/providers/saml/logout_request" SESSION_KEY_LOGOUT_REQUEST = "authentik/providers/saml/logout_request"
# This View doesn't have a URL on purpose, as its called by the FlowExecutor # This View doesn't have a URL on purpose, as its called by the FlowExecutor
@ -50,11 +50,10 @@ class SAMLFlowFinalView(ChallengeStageView):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
application: Application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION] application: Application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
provider: SAMLProvider = get_object_or_404(SAMLProvider, pk=application.provider_id) provider: SAMLProvider = get_object_or_404(SAMLProvider, pk=application.provider_id)
if PLAN_CONTEXT_SAML_AUTH_N_REQUEST not in self.executor.plan.context: if SESSION_KEY_AUTH_N_REQUEST not in self.request.session:
self.logger.warning("No AuthNRequest in context")
return self.executor.stage_invalid() return self.executor.stage_invalid()
auth_n_request: AuthNRequest = self.executor.plan.context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST] auth_n_request: AuthNRequest = self.request.session.pop(SESSION_KEY_AUTH_N_REQUEST)
try: try:
response = AssertionProcessor(provider, request, auth_n_request).build_response() response = AssertionProcessor(provider, request, auth_n_request).build_response()
except SAMLException as exc: except SAMLException as exc:
@ -107,3 +106,6 @@ class SAMLFlowFinalView(ChallengeStageView):
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
# We'll never get here since the challenge redirects to the SP # We'll never get here since the challenge redirects to the SP
return HttpResponseBadRequest() return HttpResponseBadRequest()
def cleanup(self):
self.request.session.pop(SESSION_KEY_AUTH_N_REQUEST, None)

View File

@ -19,9 +19,9 @@ from authentik.providers.saml.exceptions import CannotHandleAssertion
from authentik.providers.saml.models import SAMLProvider from authentik.providers.saml.models import SAMLProvider
from authentik.providers.saml.processors.logout_request_parser import LogoutRequestParser from authentik.providers.saml.processors.logout_request_parser import LogoutRequestParser
from authentik.providers.saml.views.flows import ( from authentik.providers.saml.views.flows import (
PLAN_CONTEXT_SAML_LOGOUT_REQUEST,
REQUEST_KEY_RELAY_STATE, REQUEST_KEY_RELAY_STATE,
REQUEST_KEY_SAML_REQUEST, REQUEST_KEY_SAML_REQUEST,
SESSION_KEY_LOGOUT_REQUEST,
) )
LOGGER = get_logger() LOGGER = get_logger()
@ -33,10 +33,6 @@ class SAMLSLOView(PolicyAccessView):
flow: Flow flow: Flow
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.plan_context = {}
def resolve_provider_application(self): def resolve_provider_application(self):
self.application = get_object_or_404(Application, slug=self.kwargs["application_slug"]) self.application = get_object_or_404(Application, slug=self.kwargs["application_slug"])
self.provider: SAMLProvider = get_object_or_404( self.provider: SAMLProvider = get_object_or_404(
@ -63,7 +59,6 @@ class SAMLSLOView(PolicyAccessView):
request, request,
{ {
PLAN_CONTEXT_APPLICATION: self.application, PLAN_CONTEXT_APPLICATION: self.application,
**self.plan_context,
}, },
) )
plan.append_stage(in_memory_stage(SessionEndStage)) plan.append_stage(in_memory_stage(SessionEndStage))
@ -88,7 +83,7 @@ class SAMLSLOBindingRedirectView(SAMLSLOView):
self.request.GET[REQUEST_KEY_SAML_REQUEST], self.request.GET[REQUEST_KEY_SAML_REQUEST],
relay_state=self.request.GET.get(REQUEST_KEY_RELAY_STATE, None), relay_state=self.request.GET.get(REQUEST_KEY_RELAY_STATE, None),
) )
self.plan_context[PLAN_CONTEXT_SAML_LOGOUT_REQUEST] = logout_request self.request.session[SESSION_KEY_LOGOUT_REQUEST] = logout_request
except CannotHandleAssertion as exc: except CannotHandleAssertion as exc:
Event.new( Event.new(
EventAction.CONFIGURATION_ERROR, EventAction.CONFIGURATION_ERROR,
@ -116,7 +111,7 @@ class SAMLSLOBindingPOSTView(SAMLSLOView):
payload[REQUEST_KEY_SAML_REQUEST], payload[REQUEST_KEY_SAML_REQUEST],
relay_state=payload.get(REQUEST_KEY_RELAY_STATE, None), relay_state=payload.get(REQUEST_KEY_RELAY_STATE, None),
) )
self.plan_context[PLAN_CONTEXT_SAML_LOGOUT_REQUEST] = logout_request self.request.session[SESSION_KEY_LOGOUT_REQUEST] = logout_request
except CannotHandleAssertion as exc: except CannotHandleAssertion as exc:
LOGGER.info(str(exc)) LOGGER.info(str(exc))
return bad_request_message(self.request, str(exc)) return bad_request_message(self.request, str(exc))

View File

@ -15,16 +15,16 @@ from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.views.executor import SESSION_KEY_POST from authentik.flows.views.executor import SESSION_KEY_POST
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message
from authentik.policies.views import BufferedPolicyAccessView from authentik.policies.views import PolicyAccessView
from authentik.providers.saml.exceptions import CannotHandleAssertion from authentik.providers.saml.exceptions import CannotHandleAssertion
from authentik.providers.saml.models import SAMLBindings, SAMLProvider from authentik.providers.saml.models import SAMLBindings, SAMLProvider
from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser
from authentik.providers.saml.views.flows import ( from authentik.providers.saml.views.flows import (
PLAN_CONTEXT_SAML_AUTH_N_REQUEST,
REQUEST_KEY_RELAY_STATE, REQUEST_KEY_RELAY_STATE,
REQUEST_KEY_SAML_REQUEST, REQUEST_KEY_SAML_REQUEST,
REQUEST_KEY_SAML_SIG_ALG, REQUEST_KEY_SAML_SIG_ALG,
REQUEST_KEY_SAML_SIGNATURE, REQUEST_KEY_SAML_SIGNATURE,
SESSION_KEY_AUTH_N_REQUEST,
SAMLFlowFinalView, SAMLFlowFinalView,
) )
from authentik.stages.consent.stage import ( from authentik.stages.consent.stage import (
@ -35,14 +35,10 @@ from authentik.stages.consent.stage import (
LOGGER = get_logger() LOGGER = get_logger()
class SAMLSSOView(BufferedPolicyAccessView): class SAMLSSOView(PolicyAccessView):
"""SAML SSO Base View, which plans a flow and injects our final stage. """SAML SSO Base View, which plans a flow and injects our final stage.
Calls get/post handler.""" Calls get/post handler."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.plan_context = {}
def resolve_provider_application(self): def resolve_provider_application(self):
self.application = get_object_or_404(Application, slug=self.kwargs["application_slug"]) self.application = get_object_or_404(Application, slug=self.kwargs["application_slug"])
self.provider: SAMLProvider = get_object_or_404( self.provider: SAMLProvider = get_object_or_404(
@ -72,7 +68,6 @@ class SAMLSSOView(BufferedPolicyAccessView):
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.") PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
% {"application": self.application.name}, % {"application": self.application.name},
PLAN_CONTEXT_CONSENT_PERMISSIONS: [], PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
**self.plan_context,
}, },
) )
except FlowNonApplicableException: except FlowNonApplicableException:
@ -88,7 +83,7 @@ class SAMLSSOView(BufferedPolicyAccessView):
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse: def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
"""GET and POST use the same handler, but we can't """GET and POST use the same handler, but we can't
override .dispatch easily because BufferedPolicyAccessView's dispatch""" override .dispatch easily because PolicyAccessView's dispatch"""
return self.get(request, application_slug) return self.get(request, application_slug)
@ -108,7 +103,7 @@ class SAMLSSOBindingRedirectView(SAMLSSOView):
self.request.GET.get(REQUEST_KEY_SAML_SIGNATURE), self.request.GET.get(REQUEST_KEY_SAML_SIGNATURE),
self.request.GET.get(REQUEST_KEY_SAML_SIG_ALG), self.request.GET.get(REQUEST_KEY_SAML_SIG_ALG),
) )
self.plan_context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST] = auth_n_request self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
except CannotHandleAssertion as exc: except CannotHandleAssertion as exc:
Event.new( Event.new(
EventAction.CONFIGURATION_ERROR, EventAction.CONFIGURATION_ERROR,
@ -142,7 +137,7 @@ class SAMLSSOBindingPOSTView(SAMLSSOView):
payload[REQUEST_KEY_SAML_REQUEST], payload[REQUEST_KEY_SAML_REQUEST],
payload.get(REQUEST_KEY_RELAY_STATE), payload.get(REQUEST_KEY_RELAY_STATE),
) )
self.plan_context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST] = auth_n_request self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
except CannotHandleAssertion as exc: except CannotHandleAssertion as exc:
LOGGER.info(str(exc)) LOGGER.info(str(exc))
return bad_request_message(self.request, str(exc)) return bad_request_message(self.request, str(exc))
@ -156,4 +151,4 @@ class SAMLSSOBindingInitView(SAMLSSOView):
"""Create SAML Response from scratch""" """Create SAML Response from scratch"""
LOGGER.debug("No SAML Request, using IdP-initiated flow.") LOGGER.debug("No SAML Request, using IdP-initiated flow.")
auth_n_request = AuthNRequestParser(self.provider).idp_initiated() auth_n_request = AuthNRequestParser(self.provider).idp_initiated()
self.plan_context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST] = auth_n_request self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request

View File

@ -99,7 +99,6 @@ class RBACPermissionViewSet(ReadOnlyModelViewSet):
filterset_class = PermissionFilter filterset_class = PermissionFilter
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
search_fields = [ search_fields = [
"name",
"codename", "codename",
"content_type__model", "content_type__model",
"content_type__app_label", "content_type__app_label",

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema", "$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json", "$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object", "type": "object",
"title": "authentik 2025.4.0 Blueprint schema", "title": "authentik 2025.2.4 Blueprint schema",
"required": [ "required": [
"version", "version",
"entries" "entries"

View File

@ -31,7 +31,7 @@ services:
volumes: volumes:
- redis:/data - redis:/data
server: server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.0} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.4}
restart: unless-stopped restart: unless-stopped
command: server command: server
environment: environment:
@ -55,7 +55,7 @@ services:
redis: redis:
condition: service_healthy condition: service_healthy
worker: worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.0} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.4}
restart: unless-stopped restart: unless-stopped
command: worker command: worker
environment: environment:

View File

@ -29,4 +29,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion()) return fmt.Sprintf("authentik@%s", FullVersion())
} }
const VERSION = "2025.4.0" const VERSION = "2025.2.4"

View File

@ -56,7 +56,6 @@ EXPOSE 3389 6636 9300
USER 1000 USER 1000
ENV TMPDIR=/dev/shm/ \ ENV GOFIPS=1
GOFIPS=1
ENTRYPOINT ["/ldap"] ENTRYPOINT ["/ldap"]

View File

@ -62,8 +62,7 @@ function prepare_debug {
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
apt-get update apt-get update
apt-get install -y --no-install-recommends krb5-kdc krb5-user krb5-admin-server libkrb5-dev gcc apt-get install -y --no-install-recommends krb5-kdc krb5-user krb5-admin-server libkrb5-dev gcc
source "${VENV_PATH}/bin/activate" VIRTUAL_ENV=/ak-root/.venv uv sync --frozen
uv sync --active --frozen
touch /unittest.xml touch /unittest.xml
chown authentik:authentik /unittest.xml chown authentik:authentik /unittest.xml
} }
@ -97,7 +96,6 @@ elif [[ "$1" == "test-all" ]]; then
elif [[ "$1" == "healthcheck" ]]; then elif [[ "$1" == "healthcheck" ]]; then
run_authentik healthcheck $(cat $MODE_FILE) run_authentik healthcheck $(cat $MODE_FILE)
elif [[ "$1" == "dump_config" ]]; then elif [[ "$1" == "dump_config" ]]; then
shift
exec python -m authentik.lib.config $@ exec python -m authentik.lib.config $@
elif [[ "$1" == "debug" ]]; then elif [[ "$1" == "debug" ]]; then
exec sleep infinity exec sleep infinity

View File

@ -26,7 +26,7 @@ Parameters:
Description: authentik Docker image Description: authentik Docker image
AuthentikVersion: AuthentikVersion:
Type: String Type: String
Default: 2025.4.0 Default: 2025.2.4
Description: authentik Docker image tag Description: authentik Docker image tag
AuthentikServerCPU: AuthentikServerCPU:
Type: Number Type: Number

Binary file not shown.

View File

@ -8,6 +8,7 @@
# Jens L. <jens@goauthentik.io>, 2022 # Jens L. <jens@goauthentik.io>, 2022
# Lars Lehmann <lars@lars-lehmann.net>, 2023 # Lars Lehmann <lars@lars-lehmann.net>, 2023
# Johannes —/—, 2023 # Johannes —/—, 2023
# Dominic Wagner <mail@dominic-wagner.de>, 2023
# fde4f289d99ed356ff5cfdb762dc44aa_a8a971d, 2023 # fde4f289d99ed356ff5cfdb762dc44aa_a8a971d, 2023
# Christian Foellmann <foellmann@foe-services.de>, 2023 # Christian Foellmann <foellmann@foe-services.de>, 2023
# kidhab, 2023 # kidhab, 2023
@ -29,18 +30,17 @@
# Alexander Möbius, 2025 # Alexander Möbius, 2025
# Jonas, 2025 # Jonas, 2025
# Niklas Kroese, 2025 # Niklas Kroese, 2025
# datenschmutz, 2025
# 97cce0ae0cad2a2cc552d3165d04643e_de3d740, 2025 # 97cce0ae0cad2a2cc552d3165d04643e_de3d740, 2025
# Dominic Wagner <mail@dominic-wagner.de>, 2025 # datenschmutz, 2025
# #
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n" "POT-Creation-Date: 2025-04-11 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Dominic Wagner <mail@dominic-wagner.de>, 2025\n" "Last-Translator: datenschmutz, 2025\n"
"Language-Team: German (https://app.transifex.com/authentik/teams/119923/de/)\n" "Language-Team: German (https://app.transifex.com/authentik/teams/119923/de/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@ -214,7 +214,6 @@ msgid "User's display name."
msgstr "Anzeigename" msgstr "Anzeigename"
#: authentik/core/models.py authentik/providers/oauth2/models.py #: authentik/core/models.py authentik/providers/oauth2/models.py
#: authentik/rbac/models.py
msgid "User" msgid "User"
msgstr "Benutzer" msgstr "Benutzer"
@ -403,18 +402,6 @@ msgstr "Eigenschaft"
msgid "Property Mappings" msgid "Property Mappings"
msgstr "Eigenschaften" msgstr "Eigenschaften"
#: authentik/core/models.py
msgid "session data"
msgstr ""
#: authentik/core/models.py
msgid "Session"
msgstr "Sitzung"
#: authentik/core/models.py
msgid "Sessions"
msgstr "Sitzungen"
#: authentik/core/models.py #: authentik/core/models.py
msgid "Authenticated Session" msgid "Authenticated Session"
msgstr "Authentifizierte Sitzung" msgstr "Authentifizierte Sitzung"
@ -524,38 +511,6 @@ msgstr "Lizenzverwendung"
msgid "License Usage Records" msgid "License Usage Records"
msgstr "Lizenzverwendung Aufzeichnungen" msgstr "Lizenzverwendung Aufzeichnungen"
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
"Zu prüfender Feldschlüssel, die in den Aufforderungsstufen definierten "
"Feldschlüssel sind verfügbar."
#: authentik/enterprise/policies/unique_password/models.py
msgid "Number of passwords to check against."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "Passwort nicht im Kontext festgelegt"
#: authentik/enterprise/policies/unique_password/models.py
msgid "This password has been used previously. Please choose a different one."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policy"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policies"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "User Password History"
msgstr ""
#: authentik/enterprise/policy.py #: authentik/enterprise/policy.py
msgid "Enterprise required to access this feature." msgid "Enterprise required to access this feature."
msgstr "Enterprise ist erforderlich, um auf diese Funktion zuzugreifen." msgstr "Enterprise ist erforderlich, um auf diese Funktion zuzugreifen."
@ -1348,6 +1303,12 @@ msgstr "Richtlinien Cache Metriken anzeigen"
msgid "Clear Policy's cache metrics" msgid "Clear Policy's cache metrics"
msgstr "Richtlinien Cache Metriken löschen" msgstr "Richtlinien Cache Metriken löschen"
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
"Zu prüfender Feldschlüssel, die in den Aufforderungsstufen definierten "
"Feldschlüssel sind verfügbar."
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "How many times the password hash is allowed to be on haveibeenpwned" msgid "How many times the password hash is allowed to be on haveibeenpwned"
msgstr "Wie häufig der Passwort-Hash auf haveibeenpwned vertreten sein darf" msgstr "Wie häufig der Passwort-Hash auf haveibeenpwned vertreten sein darf"
@ -1359,6 +1320,10 @@ msgstr ""
"Die Richtlinie wird verweigert, wenn die zxcvbn-Bewertung gleich oder " "Die Richtlinie wird verweigert, wenn die zxcvbn-Bewertung gleich oder "
"kleiner diesem Wert ist." "kleiner diesem Wert ist."
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "Passwort nicht im Kontext festgelegt"
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "Invalid password." msgid "Invalid password."
msgstr "Ungültiges Passwort." msgstr "Ungültiges Passwort."
@ -1400,6 +1365,20 @@ msgstr "Reputationswert"
msgid "Reputation Scores" msgid "Reputation Scores"
msgstr "Reputationswert" msgstr "Reputationswert"
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr ""
#: authentik/policies/templates/policies/denied.html #: authentik/policies/templates/policies/denied.html
msgid "Permission denied" msgid "Permission denied"
msgstr "Erlaubnis verweigert" msgstr "Erlaubnis verweigert"
@ -2229,10 +2208,6 @@ msgstr "Rolle"
msgid "Roles" msgid "Roles"
msgstr "Rollen" msgstr "Rollen"
#: authentik/rbac/models.py
msgid "Initial Permissions"
msgstr ""
#: authentik/rbac/models.py #: authentik/rbac/models.py
msgid "System permission" msgid "System permission"
msgstr "Systemberechtigung" msgstr "Systemberechtigung"
@ -2503,22 +2478,6 @@ msgstr "LDAP Quelle Eigenschafts-Zuordnung"
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "LDAP Quelle Eigenschafts-Zuordnungen" msgstr "LDAP Quelle Eigenschafts-Zuordnungen"
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/signals.py #: authentik/sources/ldap/signals.py
msgid "Password does not match Active Directory Complexity." msgid "Password does not match Active Directory Complexity."
msgstr "" msgstr ""
@ -2528,14 +2487,6 @@ msgstr ""
msgid "No token received." msgid "No token received."
msgstr "Kein Token empfangen." msgstr "Kein Token empfangen."
#: authentik/sources/oauth/models.py
msgid "HTTP Basic Authentication"
msgstr ""
#: authentik/sources/oauth/models.py
msgid "Include the client ID and secret as request parameters"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "Request Token URL" msgid "Request Token URL"
msgstr "Token-URL anfordern" msgstr "Token-URL anfordern"
@ -2577,12 +2528,6 @@ msgstr ""
msgid "Additional Scopes" msgid "Additional Scopes"
msgstr "zusätzliche Scopes" msgstr "zusätzliche Scopes"
#: authentik/sources/oauth/models.py
msgid ""
"How to perform authentication during an authorization_code token request "
"flow"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "OAuth Source" msgid "OAuth Source"
msgstr "Outh Quelle" msgstr "Outh Quelle"
@ -3489,12 +3434,6 @@ msgstr ""
"Wenn aktiviert, wird die Phase auch dann erfolgreich abgeschlossen und " "Wenn aktiviert, wird die Phase auch dann erfolgreich abgeschlossen und "
"fortgesetzt, wenn falsche Benutzerdaten eingegeben wurden." "fortgesetzt, wenn falsche Benutzerdaten eingegeben wurden."
#: authentik/stages/identification/models.py
msgid ""
"Show the user the 'Remember me on this device' toggle, allowing repeat users"
" to skip straight to entering their password."
msgstr ""
#: authentik/stages/identification/models.py #: authentik/stages/identification/models.py
msgid "Optional enrollment flow, which is linked at the bottom of the page." msgid "Optional enrollment flow, which is linked at the bottom of the page."
msgstr "Optionaler Registrierungs-Flow, der unten auf der Seite verlinkt ist." msgstr "Optionaler Registrierungs-Flow, der unten auf der Seite verlinkt ist."
@ -3887,14 +3826,6 @@ msgstr ""
"Die Ereignisse werden nach dieser Dauer gelöscht (Format: " "Die Ereignisse werden nach dieser Dauer gelöscht (Format: "
"Wochen=3;Tage=2;Stunden=3,Sekunden=2)." "Wochen=3;Tage=2;Stunden=3,Sekunden=2)."
#: authentik/tenants/models.py
msgid "Reputation cannot decrease lower than this value. Zero or negative."
msgstr ""
#: authentik/tenants/models.py
msgid "Reputation cannot increase higher than this value. Zero or positive."
msgstr ""
#: authentik/tenants/models.py #: authentik/tenants/models.py
msgid "The option configures the footer links on the flow executor pages." msgid "The option configures the footer links on the flow executor pages."
msgstr "" msgstr ""

Binary file not shown.

View File

@ -15,7 +15,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n" "POT-Creation-Date: 2025-04-11 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n" "Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n"
"Language-Team: Spanish (https://app.transifex.com/authentik/teams/119923/es/)\n" "Language-Team: Spanish (https://app.transifex.com/authentik/teams/119923/es/)\n"
@ -190,7 +190,6 @@ msgid "User's display name."
msgstr "Nombre para mostrar del usuario." msgstr "Nombre para mostrar del usuario."
#: authentik/core/models.py authentik/providers/oauth2/models.py #: authentik/core/models.py authentik/providers/oauth2/models.py
#: authentik/rbac/models.py
msgid "User" msgid "User"
msgstr "Usuario" msgstr "Usuario"
@ -379,18 +378,6 @@ msgstr "Asignación de Propiedades"
msgid "Property Mappings" msgid "Property Mappings"
msgstr "Asignaciones de Propiedades" msgstr "Asignaciones de Propiedades"
#: authentik/core/models.py
msgid "session data"
msgstr ""
#: authentik/core/models.py
msgid "Session"
msgstr "Sesión"
#: authentik/core/models.py
msgid "Sessions"
msgstr "Sesiones"
#: authentik/core/models.py #: authentik/core/models.py
msgid "Authenticated Session" msgid "Authenticated Session"
msgstr "Sesión autenticada" msgstr "Sesión autenticada"
@ -498,38 +485,6 @@ msgstr "Uso de Licencias"
msgid "License Usage Records" msgid "License Usage Records"
msgstr "Registro de Uso de Licencias" msgstr "Registro de Uso de Licencias"
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
"Clave de campo a verificar, las claves de campo definidas en las etapas de "
"Solicitud están disponibles."
#: authentik/enterprise/policies/unique_password/models.py
msgid "Number of passwords to check against."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "La contraseña no se ha establecido en contexto"
#: authentik/enterprise/policies/unique_password/models.py
msgid "This password has been used previously. Please choose a different one."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policy"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policies"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "User Password History"
msgstr ""
#: authentik/enterprise/policy.py #: authentik/enterprise/policy.py
msgid "Enterprise required to access this feature." msgid "Enterprise required to access this feature."
msgstr "Se requiere de Enterprise para acceder esta característica." msgstr "Se requiere de Enterprise para acceder esta característica."
@ -1313,6 +1268,12 @@ msgstr "Ver las métricas de caché de la Política"
msgid "Clear Policy's cache metrics" msgid "Clear Policy's cache metrics"
msgstr "Borrar las métricas de caché de la Política" msgstr "Borrar las métricas de caché de la Política"
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
"Clave de campo a verificar, las claves de campo definidas en las etapas de "
"Solicitud están disponibles."
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "How many times the password hash is allowed to be on haveibeenpwned" msgid "How many times the password hash is allowed to be on haveibeenpwned"
msgstr "" msgstr ""
@ -1326,6 +1287,10 @@ msgstr ""
"Si la puntuación zxcvbn es igual o menor que este valor, la política " "Si la puntuación zxcvbn es igual o menor que este valor, la política "
"fallará." "fallará."
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "La contraseña no se ha establecido en contexto"
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "Invalid password." msgid "Invalid password."
msgstr "Contraseña inválida." msgstr "Contraseña inválida."
@ -1367,6 +1332,20 @@ msgstr "Puntuación de Reputacion"
msgid "Reputation Scores" msgid "Reputation Scores"
msgstr "Puntuaciones de Reputacion" msgstr "Puntuaciones de Reputacion"
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr ""
#: authentik/policies/templates/policies/denied.html #: authentik/policies/templates/policies/denied.html
msgid "Permission denied" msgid "Permission denied"
msgstr "Permiso denegado" msgstr "Permiso denegado"
@ -2196,10 +2175,6 @@ msgstr "Rol"
msgid "Roles" msgid "Roles"
msgstr "Roles" msgstr "Roles"
#: authentik/rbac/models.py
msgid "Initial Permissions"
msgstr ""
#: authentik/rbac/models.py #: authentik/rbac/models.py
msgid "System permission" msgid "System permission"
msgstr "Permiso de sistema" msgstr "Permiso de sistema"
@ -2468,22 +2443,6 @@ msgstr "Asignación de Propiedades de Fuente de LDAP"
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "Asignaciones de Propiedades de Fuente de LDAP" msgstr "Asignaciones de Propiedades de Fuente de LDAP"
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/signals.py #: authentik/sources/ldap/signals.py
msgid "Password does not match Active Directory Complexity." msgid "Password does not match Active Directory Complexity."
msgstr "La contraseña no coincide con la complejidad de Active Directory." msgstr "La contraseña no coincide con la complejidad de Active Directory."
@ -2492,14 +2451,6 @@ msgstr "La contraseña no coincide con la complejidad de Active Directory."
msgid "No token received." msgid "No token received."
msgstr "No se recibió ningún token." msgstr "No se recibió ningún token."
#: authentik/sources/oauth/models.py
msgid "HTTP Basic Authentication"
msgstr ""
#: authentik/sources/oauth/models.py
msgid "Include the client ID and secret as request parameters"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "Request Token URL" msgid "Request Token URL"
msgstr "Solicitar URL de token" msgstr "Solicitar URL de token"
@ -2540,12 +2491,6 @@ msgstr "URL utilizada por authentik para obtener información del usuario."
msgid "Additional Scopes" msgid "Additional Scopes"
msgstr "Alcances Adicionales" msgstr "Alcances Adicionales"
#: authentik/sources/oauth/models.py
msgid ""
"How to perform authentication during an authorization_code token request "
"flow"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "OAuth Source" msgid "OAuth Source"
msgstr "Fuente de OAuth" msgstr "Fuente de OAuth"
@ -3462,12 +3407,6 @@ msgstr ""
"Cuando está habilitado, la etapa tendrá éxito y continuará incluso cuando se" "Cuando está habilitado, la etapa tendrá éxito y continuará incluso cuando se"
" ingrese información de usuario incorrecta." " ingrese información de usuario incorrecta."
#: authentik/stages/identification/models.py
msgid ""
"Show the user the 'Remember me on this device' toggle, allowing repeat users"
" to skip straight to entering their password."
msgstr ""
#: authentik/stages/identification/models.py #: authentik/stages/identification/models.py
msgid "Optional enrollment flow, which is linked at the bottom of the page." msgid "Optional enrollment flow, which is linked at the bottom of the page."
msgstr "" msgstr ""
@ -3855,14 +3794,6 @@ msgstr ""
"Los Eventos serán eliminados después de este periodo. (Formato: " "Los Eventos serán eliminados después de este periodo. (Formato: "
"weeks=3;days=2;hours=3,seconds=2)." "weeks=3;days=2;hours=3,seconds=2)."
#: authentik/tenants/models.py
msgid "Reputation cannot decrease lower than this value. Zero or negative."
msgstr ""
#: authentik/tenants/models.py
msgid "Reputation cannot increase higher than this value. Zero or positive."
msgstr ""
#: authentik/tenants/models.py #: authentik/tenants/models.py
msgid "The option configures the footer links on the flow executor pages." msgid "The option configures the footer links on the flow executor pages."
msgstr "" msgstr ""

Binary file not shown.

View File

@ -15,7 +15,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n" "POT-Creation-Date: 2025-04-11 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Ville Ranki, 2025\n" "Last-Translator: Ville Ranki, 2025\n"
"Language-Team: Finnish (https://app.transifex.com/authentik/teams/119923/fi/)\n" "Language-Team: Finnish (https://app.transifex.com/authentik/teams/119923/fi/)\n"
@ -186,7 +186,6 @@ msgid "User's display name."
msgstr "Käyttäjän näytettävä nimi" msgstr "Käyttäjän näytettävä nimi"
#: authentik/core/models.py authentik/providers/oauth2/models.py #: authentik/core/models.py authentik/providers/oauth2/models.py
#: authentik/rbac/models.py
msgid "User" msgid "User"
msgstr "Käyttäjä" msgstr "Käyttäjä"
@ -372,18 +371,6 @@ msgstr "Ominaisuuskytkentä"
msgid "Property Mappings" msgid "Property Mappings"
msgstr "Ominaisuuskytkennät" msgstr "Ominaisuuskytkennät"
#: authentik/core/models.py
msgid "session data"
msgstr ""
#: authentik/core/models.py
msgid "Session"
msgstr "Istunto"
#: authentik/core/models.py
msgid "Sessions"
msgstr ""
#: authentik/core/models.py #: authentik/core/models.py
msgid "Authenticated Session" msgid "Authenticated Session"
msgstr "Autentikoitu istunto" msgstr "Autentikoitu istunto"
@ -491,38 +478,6 @@ msgstr "Lisenssin käyttö"
msgid "License Usage Records" msgid "License Usage Records"
msgstr "Lisenssin käyttötiedot" msgstr "Lisenssin käyttötiedot"
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
"Kentän avain, joka tarkistetaan. Kysymysvaiheissa määritellyt kenttien "
"avaimet ovat käytettävissä."
#: authentik/enterprise/policies/unique_password/models.py
msgid "Number of passwords to check against."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "Salasanaa ei ole asetettu kontekstissa"
#: authentik/enterprise/policies/unique_password/models.py
msgid "This password has been used previously. Please choose a different one."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policy"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policies"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "User Password History"
msgstr ""
#: authentik/enterprise/policy.py #: authentik/enterprise/policy.py
msgid "Enterprise required to access this feature." msgid "Enterprise required to access this feature."
msgstr "Tämän ominaisuuden käyttöön tarvitaan Enterprise-versiota." msgstr "Tämän ominaisuuden käyttöön tarvitaan Enterprise-versiota."
@ -1296,6 +1251,12 @@ msgstr "Näytä käytäntövälimuistitilastot"
msgid "Clear Policy's cache metrics" msgid "Clear Policy's cache metrics"
msgstr "Tyhjennä käytäntövälimuistitilastot" msgstr "Tyhjennä käytäntövälimuistitilastot"
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
"Kentän avain, joka tarkistetaan. Kysymysvaiheissa määritellyt kenttien "
"avaimet ovat käytettävissä."
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "How many times the password hash is allowed to be on haveibeenpwned" msgid "How many times the password hash is allowed to be on haveibeenpwned"
msgstr "" msgstr ""
@ -1308,6 +1269,10 @@ msgstr ""
"Jos zxcvbn-pistemäärä on tämä arvo tai pienempi, käytännön suorittaminen " "Jos zxcvbn-pistemäärä on tämä arvo tai pienempi, käytännön suorittaminen "
"epäonnistuu." "epäonnistuu."
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "Salasanaa ei ole asetettu kontekstissa"
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "Invalid password." msgid "Invalid password."
msgstr "Virheellinen salasana." msgstr "Virheellinen salasana."
@ -1349,6 +1314,20 @@ msgstr "Mainepistemäärä"
msgid "Reputation Scores" msgid "Reputation Scores"
msgstr "Mainepistemäärät" msgstr "Mainepistemäärät"
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr ""
#: authentik/policies/templates/policies/denied.html #: authentik/policies/templates/policies/denied.html
msgid "Permission denied" msgid "Permission denied"
msgstr "Käyttö evätty" msgstr "Käyttö evätty"
@ -2176,10 +2155,6 @@ msgstr "Rooli"
msgid "Roles" msgid "Roles"
msgstr "Roolit" msgstr "Roolit"
#: authentik/rbac/models.py
msgid "Initial Permissions"
msgstr ""
#: authentik/rbac/models.py #: authentik/rbac/models.py
msgid "System permission" msgid "System permission"
msgstr "Järjestelmän käyttöoikeus" msgstr "Järjestelmän käyttöoikeus"
@ -2445,22 +2420,6 @@ msgstr "LDAP-lähteen ominaisuuskytkentä"
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "LDAP-lähteen ominaisuuskytkennät" msgstr "LDAP-lähteen ominaisuuskytkennät"
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/signals.py #: authentik/sources/ldap/signals.py
msgid "Password does not match Active Directory Complexity." msgid "Password does not match Active Directory Complexity."
msgstr "Salasana ei vastaa Active Directoryn monimutkaisuusmääritystä." msgstr "Salasana ei vastaa Active Directoryn monimutkaisuusmääritystä."
@ -2469,14 +2428,6 @@ msgstr "Salasana ei vastaa Active Directoryn monimutkaisuusmääritystä."
msgid "No token received." msgid "No token received."
msgstr "Tunnistetta ei saatu." msgstr "Tunnistetta ei saatu."
#: authentik/sources/oauth/models.py
msgid "HTTP Basic Authentication"
msgstr ""
#: authentik/sources/oauth/models.py
msgid "Include the client ID and secret as request parameters"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "Request Token URL" msgid "Request Token URL"
msgstr "Pyyntötunnisteen URL" msgstr "Pyyntötunnisteen URL"
@ -2517,12 +2468,6 @@ msgstr "URL, jota authentik käyttää käyttäjätiedon hakemiseksi."
msgid "Additional Scopes" msgid "Additional Scopes"
msgstr "Lisäkäyttöalueet" msgstr "Lisäkäyttöalueet"
#: authentik/sources/oauth/models.py
msgid ""
"How to perform authentication during an authorization_code token request "
"flow"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "OAuth Source" msgid "OAuth Source"
msgstr "OAuth-lähde" msgstr "OAuth-lähde"
@ -3432,12 +3377,6 @@ msgstr ""
"Kun tämä on käytössä, vaihe onnistuu ja suoritus jatkuu, vaikka olisi " "Kun tämä on käytössä, vaihe onnistuu ja suoritus jatkuu, vaikka olisi "
"syötetty virheelliset käyttäjätiedot." "syötetty virheelliset käyttäjätiedot."
#: authentik/stages/identification/models.py
msgid ""
"Show the user the 'Remember me on this device' toggle, allowing repeat users"
" to skip straight to entering their password."
msgstr ""
#: authentik/stages/identification/models.py #: authentik/stages/identification/models.py
msgid "Optional enrollment flow, which is linked at the bottom of the page." msgid "Optional enrollment flow, which is linked at the bottom of the page."
msgstr "" msgstr ""
@ -3815,14 +3754,6 @@ msgstr ""
"Tapahtumat poistetaan tämän ajan jälkeen. (Muoto: " "Tapahtumat poistetaan tämän ajan jälkeen. (Muoto: "
"weeks=3;days=2;hours=3;seconds=2)." "weeks=3;days=2;hours=3;seconds=2)."
#: authentik/tenants/models.py
msgid "Reputation cannot decrease lower than this value. Zero or negative."
msgstr ""
#: authentik/tenants/models.py
msgid "Reputation cannot increase higher than this value. Zero or positive."
msgstr ""
#: authentik/tenants/models.py #: authentik/tenants/models.py
msgid "The option configures the footer links on the flow executor pages." msgid "The option configures the footer links on the flow executor pages."
msgstr "" msgstr ""

Binary file not shown.

View File

@ -12,17 +12,17 @@
# tmassimi, 2024 # tmassimi, 2024
# Marc Schmitt, 2024 # Marc Schmitt, 2024
# albanobattistella <albanobattistella@gmail.com>, 2024 # albanobattistella <albanobattistella@gmail.com>, 2024
# Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025
# Matteo Piccina <altermatte@gmail.com>, 2025 # Matteo Piccina <altermatte@gmail.com>, 2025
# Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025
# #
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n" "POT-Creation-Date: 2025-04-11 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Matteo Piccina <altermatte@gmail.com>, 2025\n" "Last-Translator: Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025\n"
"Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n" "Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@ -194,7 +194,6 @@ msgid "User's display name."
msgstr "Nome visualizzato dell'utente." msgstr "Nome visualizzato dell'utente."
#: authentik/core/models.py authentik/providers/oauth2/models.py #: authentik/core/models.py authentik/providers/oauth2/models.py
#: authentik/rbac/models.py
msgid "User" msgid "User"
msgstr "Utente" msgstr "Utente"
@ -381,18 +380,6 @@ msgstr "Mappatura della proprietà"
msgid "Property Mappings" msgid "Property Mappings"
msgstr "Mappatura delle proprietà" msgstr "Mappatura delle proprietà"
#: authentik/core/models.py
msgid "session data"
msgstr ""
#: authentik/core/models.py
msgid "Session"
msgstr "Sessione"
#: authentik/core/models.py
msgid "Sessions"
msgstr "Sessioni"
#: authentik/core/models.py #: authentik/core/models.py
msgid "Authenticated Session" msgid "Authenticated Session"
msgstr "Sessione Autenticata" msgstr "Sessione Autenticata"
@ -500,38 +487,6 @@ msgstr "Utilizzo della licenza"
msgid "License Usage Records" msgid "License Usage Records"
msgstr "Registri sull'utilizzo della licenza" msgstr "Registri sull'utilizzo della licenza"
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
"Chiave di campo da verificare, sono disponibili le chiavi di campo definite "
"nelle fasi Richiesta."
#: authentik/enterprise/policies/unique_password/models.py
msgid "Number of passwords to check against."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "Password non impostata nel contesto"
#: authentik/enterprise/policies/unique_password/models.py
msgid "This password has been used previously. Please choose a different one."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policy"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policies"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "User Password History"
msgstr ""
#: authentik/enterprise/policy.py #: authentik/enterprise/policy.py
msgid "Enterprise required to access this feature." msgid "Enterprise required to access this feature."
msgstr "Versione Enterprise richiesta per accedere a questa funzione" msgstr "Versione Enterprise richiesta per accedere a questa funzione"
@ -1319,6 +1274,12 @@ msgstr "Visualizza le metriche della cache della Policy"
msgid "Clear Policy's cache metrics" msgid "Clear Policy's cache metrics"
msgstr "Cancellare le metriche della cache della Policy" msgstr "Cancellare le metriche della cache della Policy"
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
"Chiave di campo da verificare, sono disponibili le chiavi di campo definite "
"nelle fasi Richiesta."
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "How many times the password hash is allowed to be on haveibeenpwned" msgid "How many times the password hash is allowed to be on haveibeenpwned"
msgstr "" msgstr ""
@ -1331,6 +1292,10 @@ msgstr ""
"Se il punteggio zxcvbn è inferiore o uguale a questo valore, il criterio non" "Se il punteggio zxcvbn è inferiore o uguale a questo valore, il criterio non"
" verrà soddisfatto." " verrà soddisfatto."
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "Password non impostata nel contesto"
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "Invalid password." msgid "Invalid password."
msgstr "Password invalida." msgstr "Password invalida."
@ -1372,6 +1337,22 @@ msgstr "Punteggio di reputazione"
msgid "Reputation Scores" msgid "Reputation Scores"
msgstr "Punteggi di reputazione" msgstr "Punteggi di reputazione"
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr "In attesa di autenticazione..."
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
"Ti stai già autenticando in un'altra scheda. Questa pagina si aggiornerà una"
" volta completata l'autenticazione."
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr "Autenticati in questa scheda"
#: authentik/policies/templates/policies/denied.html #: authentik/policies/templates/policies/denied.html
msgid "Permission denied" msgid "Permission denied"
msgstr "Permesso negato" msgstr "Permesso negato"
@ -2201,10 +2182,6 @@ msgstr "Ruolo"
msgid "Roles" msgid "Roles"
msgstr "Ruoli" msgstr "Ruoli"
#: authentik/rbac/models.py
msgid "Initial Permissions"
msgstr ""
#: authentik/rbac/models.py #: authentik/rbac/models.py
msgid "System permission" msgid "System permission"
msgstr "Autorizzazione di sistema" msgstr "Autorizzazione di sistema"
@ -2475,22 +2452,6 @@ msgstr "Mappatura delle proprietà sorgente LDAP"
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "Mappature delle proprietà della sorgente LDAP" msgstr "Mappature delle proprietà della sorgente LDAP"
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/signals.py #: authentik/sources/ldap/signals.py
msgid "Password does not match Active Directory Complexity." msgid "Password does not match Active Directory Complexity."
msgstr "La password non soddisfa la complessità Active Directory." msgstr "La password non soddisfa la complessità Active Directory."
@ -2499,14 +2460,6 @@ msgstr "La password non soddisfa la complessità Active Directory."
msgid "No token received." msgid "No token received."
msgstr "Nessun token ricevuto." msgstr "Nessun token ricevuto."
#: authentik/sources/oauth/models.py
msgid "HTTP Basic Authentication"
msgstr ""
#: authentik/sources/oauth/models.py
msgid "Include the client ID and secret as request parameters"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "Request Token URL" msgid "Request Token URL"
msgstr "URL di Richiesta Token" msgstr "URL di Richiesta Token"
@ -2547,12 +2500,6 @@ msgstr "URL utilizzato da authentik per ottenere le informazioni dell'utente."
msgid "Additional Scopes" msgid "Additional Scopes"
msgstr "Ambiti aggiuntivi" msgstr "Ambiti aggiuntivi"
#: authentik/sources/oauth/models.py
msgid ""
"How to perform authentication during an authorization_code token request "
"flow"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "OAuth Source" msgid "OAuth Source"
msgstr "Sorgente OAuth" msgstr "Sorgente OAuth"
@ -3479,12 +3426,6 @@ msgstr ""
"Quando abilitato, la fase avrà successo e continuerà anche quando vengono " "Quando abilitato, la fase avrà successo e continuerà anche quando vengono "
"inserite informazioni utente errate." "inserite informazioni utente errate."
#: authentik/stages/identification/models.py
msgid ""
"Show the user the 'Remember me on this device' toggle, allowing repeat users"
" to skip straight to entering their password."
msgstr ""
#: authentik/stages/identification/models.py #: authentik/stages/identification/models.py
msgid "Optional enrollment flow, which is linked at the bottom of the page." msgid "Optional enrollment flow, which is linked at the bottom of the page."
msgstr "Flusso di iscrizione opzionale, che è collegato in fondo alla pagina." msgstr "Flusso di iscrizione opzionale, che è collegato in fondo alla pagina."
@ -3871,14 +3812,6 @@ msgstr ""
"Gli eventi saranno cancellati dopo questa durata. (Formato: " "Gli eventi saranno cancellati dopo questa durata. (Formato: "
"weeks=3;days=2;hours=3,seconds=2)." "weeks=3;days=2;hours=3,seconds=2)."
#: authentik/tenants/models.py
msgid "Reputation cannot decrease lower than this value. Zero or negative."
msgstr ""
#: authentik/tenants/models.py
msgid "Reputation cannot increase higher than this value. Zero or positive."
msgstr ""
#: authentik/tenants/models.py #: authentik/tenants/models.py
msgid "The option configures the footer links on the flow executor pages." msgid "The option configures the footer links on the flow executor pages."
msgstr "" msgstr ""

View File

@ -12,7 +12,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n" "POT-Creation-Date: 2025-03-31 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: NavyStack, 2023\n" "Last-Translator: NavyStack, 2023\n"
"Language-Team: Korean (https://app.transifex.com/authentik/teams/119923/ko/)\n" "Language-Team: Korean (https://app.transifex.com/authentik/teams/119923/ko/)\n"
@ -176,7 +176,6 @@ msgid "User's display name."
msgstr "사용자의 표시 이름" msgstr "사용자의 표시 이름"
#: authentik/core/models.py authentik/providers/oauth2/models.py #: authentik/core/models.py authentik/providers/oauth2/models.py
#: authentik/rbac/models.py
msgid "User" msgid "User"
msgstr "사용자" msgstr "사용자"
@ -345,18 +344,6 @@ msgstr "속성 매핑"
msgid "Property Mappings" msgid "Property Mappings"
msgstr "속성 매핑" msgstr "속성 매핑"
#: authentik/core/models.py
msgid "session data"
msgstr ""
#: authentik/core/models.py
msgid "Session"
msgstr ""
#: authentik/core/models.py
msgid "Sessions"
msgstr ""
#: authentik/core/models.py #: authentik/core/models.py
msgid "Authenticated Session" msgid "Authenticated Session"
msgstr "인증된 세션" msgstr "인증된 세션"
@ -460,36 +447,6 @@ msgstr "라이선스 사용"
msgid "License Usage Records" msgid "License Usage Records"
msgstr "라이선스 사용 기록" msgstr "라이선스 사용 기록"
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr "확인하려는 필드 키, 프롬프트 스테이지에서 정의된 필드 키를 사용할 수 있습니다."
#: authentik/enterprise/policies/unique_password/models.py
msgid "Number of passwords to check against."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "비밀번호가 컨텍스트에 설정되지 않음"
#: authentik/enterprise/policies/unique_password/models.py
msgid "This password has been used previously. Please choose a different one."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policy"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policies"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "User Password History"
msgstr ""
#: authentik/enterprise/policy.py #: authentik/enterprise/policy.py
msgid "Enterprise required to access this feature." msgid "Enterprise required to access this feature."
msgstr "" msgstr ""
@ -1225,6 +1182,10 @@ msgstr "정책의 캐시 메트릭 보기"
msgid "Clear Policy's cache metrics" msgid "Clear Policy's cache metrics"
msgstr "정책의 캐시 메트릭 삭제" msgstr "정책의 캐시 메트릭 삭제"
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr "확인하려는 필드 키, 프롬프트 스테이지에서 정의된 필드 키를 사용할 수 있습니다."
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "How many times the password hash is allowed to be on haveibeenpwned" msgid "How many times the password hash is allowed to be on haveibeenpwned"
msgstr "비밀번호 해시가 허용되는 해시 횟수" msgstr "비밀번호 해시가 허용되는 해시 횟수"
@ -1234,6 +1195,10 @@ msgid ""
"If the zxcvbn score is equal or less than this value, the policy will fail." "If the zxcvbn score is equal or less than this value, the policy will fail."
msgstr "만약 zxcvbn 점수가 이 값과 같거나 이 값보다 작다면, 정책이 실패합니다." msgstr "만약 zxcvbn 점수가 이 값과 같거나 이 값보다 작다면, 정책이 실패합니다."
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "비밀번호가 컨텍스트에 설정되지 않음"
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "Invalid password." msgid "Invalid password."
msgstr "" msgstr ""
@ -1275,6 +1240,20 @@ msgstr "평판 점수"
msgid "Reputation Scores" msgid "Reputation Scores"
msgstr "평판 점수" msgstr "평판 점수"
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr ""
#: authentik/policies/templates/policies/denied.html #: authentik/policies/templates/policies/denied.html
msgid "Permission denied" msgid "Permission denied"
msgstr "권한 거부됨" msgstr "권한 거부됨"
@ -2034,10 +2013,6 @@ msgstr "역할"
msgid "Roles" msgid "Roles"
msgstr "역할" msgstr "역할"
#: authentik/rbac/models.py
msgid "Initial Permissions"
msgstr ""
#: authentik/rbac/models.py #: authentik/rbac/models.py
msgid "System permission" msgid "System permission"
msgstr "시스템 권한" msgstr "시스템 권한"
@ -2256,13 +2231,6 @@ msgid ""
"enabled on a single LDAP source." "enabled on a single LDAP source."
msgstr "사용자가 비밀번호를 변경하면 LDAP로 다시 동기화합니다. 이 기능은 단일의 LDAP 소스에서만 활성화할 수 있습니다." msgstr "사용자가 비밀번호를 변경하면 LDAP로 다시 동기화합니다. 이 기능은 단일의 LDAP 소스에서만 활성화할 수 있습니다."
#: authentik/sources/ldap/models.py
msgid ""
"Lookup group membership based on a user attribute instead of a group "
"attribute. This allows nested group resolution on systems like FreeIPA and "
"Active Directory"
msgstr ""
#: authentik/sources/ldap/models.py #: authentik/sources/ldap/models.py
msgid "LDAP Source" msgid "LDAP Source"
msgstr "LDAP 소스" msgstr "LDAP 소스"
@ -2279,22 +2247,6 @@ msgstr ""
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "" msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/signals.py #: authentik/sources/ldap/signals.py
msgid "Password does not match Active Directory Complexity." msgid "Password does not match Active Directory Complexity."
msgstr "비밀번호가 Active Directory 복잡도와 일치하지 않습니다." msgstr "비밀번호가 Active Directory 복잡도와 일치하지 않습니다."
@ -2303,14 +2255,6 @@ msgstr "비밀번호가 Active Directory 복잡도와 일치하지 않습니다.
msgid "No token received." msgid "No token received."
msgstr "수신된 토큰이 없습니다." msgstr "수신된 토큰이 없습니다."
#: authentik/sources/oauth/models.py
msgid "HTTP Basic Authentication"
msgstr ""
#: authentik/sources/oauth/models.py
msgid "Include the client ID and secret as request parameters"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "Request Token URL" msgid "Request Token URL"
msgstr "토큰 요청 URL" msgstr "토큰 요청 URL"
@ -2349,12 +2293,6 @@ msgstr "사용자 정보를 가져오기 위해 authentik에서 사용하는 URL
msgid "Additional Scopes" msgid "Additional Scopes"
msgstr "추가 스코프" msgstr "추가 스코프"
#: authentik/sources/oauth/models.py
msgid ""
"How to perform authentication during an authorization_code token request "
"flow"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "OAuth Source" msgid "OAuth Source"
msgstr "OAuth 소스" msgstr "OAuth 소스"
@ -3211,12 +3149,6 @@ msgid ""
"info is entered." "info is entered."
msgstr "활성화되면 잘못된 사용자 정보가 입력되더라도 단계가 성공하고 계속됩니다." msgstr "활성화되면 잘못된 사용자 정보가 입력되더라도 단계가 성공하고 계속됩니다."
#: authentik/stages/identification/models.py
msgid ""
"Show the user the 'Remember me on this device' toggle, allowing repeat users"
" to skip straight to entering their password."
msgstr ""
#: authentik/stages/identification/models.py #: authentik/stages/identification/models.py
msgid "Optional enrollment flow, which is linked at the bottom of the page." msgid "Optional enrollment flow, which is linked at the bottom of the page."
msgstr "페이지 하단에 링크된, 선택적 등록 플로우를 참조하세요." msgstr "페이지 하단에 링크된, 선택적 등록 플로우를 참조하세요."
@ -3568,14 +3500,6 @@ msgid ""
"weeks=3;days=2;hours=3,seconds=2)." "weeks=3;days=2;hours=3,seconds=2)."
msgstr "이 기간이 지나면 이벤트가 삭제됩니다. (서식: hours=-1;minutes=-2;seconds=-3)" msgstr "이 기간이 지나면 이벤트가 삭제됩니다. (서식: hours=-1;minutes=-2;seconds=-3)"
#: authentik/tenants/models.py
msgid "Reputation cannot decrease lower than this value. Zero or negative."
msgstr ""
#: authentik/tenants/models.py
msgid "Reputation cannot increase higher than this value. Zero or positive."
msgstr ""
#: authentik/tenants/models.py #: authentik/tenants/models.py
msgid "The option configures the footer links on the flow executor pages." msgid "The option configures the footer links on the flow executor pages."
msgstr "" msgstr ""

View File

@ -7,18 +7,18 @@
# Bartosz Karpiński, 2023 # Bartosz Karpiński, 2023
# Michał Jastrzębski, 2024 # Michał Jastrzębski, 2024
# Tomci 12 <drizztes@gmail.com>, 2024 # Tomci 12 <drizztes@gmail.com>, 2024
# Darek “NeroPcStation” NeroPcStation <dareknowacki2001@gmail.com>, 2024
# Marc Schmitt, 2025 # Marc Schmitt, 2025
# Jens L. <jens@goauthentik.io>, 2025 # Jens L. <jens@goauthentik.io>, 2025
# Darek “NeroPcStation” NeroPcStation <dareknowacki2001@gmail.com>, 2025
# #
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n" "POT-Creation-Date: 2025-04-11 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n" "Last-Translator: Darek “NeroPcStation” NeroPcStation <dareknowacki2001@gmail.com>, 2025\n"
"Language-Team: Polish (https://app.transifex.com/authentik/teams/119923/pl/)\n" "Language-Team: Polish (https://app.transifex.com/authentik/teams/119923/pl/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@ -189,7 +189,6 @@ msgid "User's display name."
msgstr "Wyświetlana nazwa użytkownika." msgstr "Wyświetlana nazwa użytkownika."
#: authentik/core/models.py authentik/providers/oauth2/models.py #: authentik/core/models.py authentik/providers/oauth2/models.py
#: authentik/rbac/models.py
msgid "User" msgid "User"
msgstr "Użytkownik" msgstr "Użytkownik"
@ -372,18 +371,6 @@ msgstr "Mapowanie właściwości"
msgid "Property Mappings" msgid "Property Mappings"
msgstr "Mapowanie właściwości" msgstr "Mapowanie właściwości"
#: authentik/core/models.py
msgid "session data"
msgstr ""
#: authentik/core/models.py
msgid "Session"
msgstr "Sesja"
#: authentik/core/models.py
msgid "Sessions"
msgstr "Sesje"
#: authentik/core/models.py #: authentik/core/models.py
msgid "Authenticated Session" msgid "Authenticated Session"
msgstr "Sesja uwierzytelniona" msgstr "Sesja uwierzytelniona"
@ -492,38 +479,6 @@ msgstr "Wykorzystanie licencji"
msgid "License Usage Records" msgid "License Usage Records"
msgstr "Rejestr wykorzystania licencji" msgstr "Rejestr wykorzystania licencji"
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
"Klucz pola do sprawdzenia, dostępne są klucze pola zdefiniowane w etapach "
"monitu."
#: authentik/enterprise/policies/unique_password/models.py
msgid "Number of passwords to check against."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "Hasło nie jest ustawione w kontekście"
#: authentik/enterprise/policies/unique_password/models.py
msgid "This password has been used previously. Please choose a different one."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policy"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policies"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "User Password History"
msgstr ""
#: authentik/enterprise/policy.py #: authentik/enterprise/policy.py
msgid "Enterprise required to access this feature." msgid "Enterprise required to access this feature."
msgstr "Wymagane jest konto Enterprise, aby uzyskać dostęp do tej funkcji." msgstr "Wymagane jest konto Enterprise, aby uzyskać dostęp do tej funkcji."
@ -1302,6 +1257,12 @@ msgstr "Wyświetl metryki pamięci podręcznej Zasady"
msgid "Clear Policy's cache metrics" msgid "Clear Policy's cache metrics"
msgstr "Wyczyść metryki pamięci podręcznej Zasady" msgstr "Wyczyść metryki pamięci podręcznej Zasady"
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
"Klucz pola do sprawdzenia, dostępne są klucze pola zdefiniowane w etapach "
"monitu."
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "How many times the password hash is allowed to be on haveibeenpwned" msgid "How many times the password hash is allowed to be on haveibeenpwned"
msgstr "Ile razy skrót hasła może być na haveibeenpwned" msgstr "Ile razy skrót hasła może być na haveibeenpwned"
@ -1313,6 +1274,10 @@ msgstr ""
"Jeśli wynik zxcvbn jest równy lub mniejszy od tej wartości, zasada zakończy " "Jeśli wynik zxcvbn jest równy lub mniejszy od tej wartości, zasada zakończy "
"się niepowodzeniem." "się niepowodzeniem."
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "Hasło nie jest ustawione w kontekście"
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "Invalid password." msgid "Invalid password."
msgstr "" msgstr ""
@ -1354,6 +1319,20 @@ msgstr "Punkty reputacji"
msgid "Reputation Scores" msgid "Reputation Scores"
msgstr "Punkty reputacji" msgstr "Punkty reputacji"
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr "Oczekiwanie na uwierzytelnienie..."
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr ""
#: authentik/policies/templates/policies/denied.html #: authentik/policies/templates/policies/denied.html
msgid "Permission denied" msgid "Permission denied"
msgstr "Odmowa uprawnień" msgstr "Odmowa uprawnień"
@ -2162,10 +2141,6 @@ msgstr "Rola"
msgid "Roles" msgid "Roles"
msgstr "Role" msgstr "Role"
#: authentik/rbac/models.py
msgid "Initial Permissions"
msgstr ""
#: authentik/rbac/models.py #: authentik/rbac/models.py
msgid "System permission" msgid "System permission"
msgstr "Uprawnienie systemowe" msgstr "Uprawnienie systemowe"
@ -2415,22 +2390,6 @@ msgstr ""
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "" msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/signals.py #: authentik/sources/ldap/signals.py
msgid "Password does not match Active Directory Complexity." msgid "Password does not match Active Directory Complexity."
msgstr "Hasło nie pasuje do złożoności usługi Active Directory." msgstr "Hasło nie pasuje do złożoności usługi Active Directory."
@ -2439,14 +2398,6 @@ msgstr "Hasło nie pasuje do złożoności usługi Active Directory."
msgid "No token received." msgid "No token received."
msgstr "Nie otrzymano tokena." msgstr "Nie otrzymano tokena."
#: authentik/sources/oauth/models.py
msgid "HTTP Basic Authentication"
msgstr ""
#: authentik/sources/oauth/models.py
msgid "Include the client ID and secret as request parameters"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "Request Token URL" msgid "Request Token URL"
msgstr "URL żądania tokena" msgstr "URL żądania tokena"
@ -2489,12 +2440,6 @@ msgstr "URL używany przez authentik do uzyskania informacji o użytkowniku."
msgid "Additional Scopes" msgid "Additional Scopes"
msgstr "Dodatkowe zakresy" msgstr "Dodatkowe zakresy"
#: authentik/sources/oauth/models.py
msgid ""
"How to perform authentication during an authorization_code token request "
"flow"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "OAuth Source" msgid "OAuth Source"
msgstr "Źródło OAuth" msgstr "Źródło OAuth"
@ -3399,12 +3344,6 @@ msgstr ""
"Po włączeniu tej opcji etap zakończy się powodzeniem i będzie kontynuowany " "Po włączeniu tej opcji etap zakończy się powodzeniem i będzie kontynuowany "
"nawet po wprowadzeniu nieprawidłowych danych użytkownika." "nawet po wprowadzeniu nieprawidłowych danych użytkownika."
#: authentik/stages/identification/models.py
msgid ""
"Show the user the 'Remember me on this device' toggle, allowing repeat users"
" to skip straight to entering their password."
msgstr ""
#: authentik/stages/identification/models.py #: authentik/stages/identification/models.py
msgid "Optional enrollment flow, which is linked at the bottom of the page." msgid "Optional enrollment flow, which is linked at the bottom of the page."
msgstr "" msgstr ""
@ -3788,14 +3727,6 @@ msgstr ""
"Zdarzenia zostaną usunięte po upływie tego czasu. (Format: " "Zdarzenia zostaną usunięte po upływie tego czasu. (Format: "
"weeks=3;days=2;hours=3,seconds=2)." "weeks=3;days=2;hours=3,seconds=2)."
#: authentik/tenants/models.py
msgid "Reputation cannot decrease lower than this value. Zero or negative."
msgstr ""
#: authentik/tenants/models.py
msgid "Reputation cannot increase higher than this value. Zero or positive."
msgstr ""
#: authentik/tenants/models.py #: authentik/tenants/models.py
msgid "The option configures the footer links on the flow executor pages." msgid "The option configures the footer links on the flow executor pages."
msgstr "Opcja ta konfiguruje łącza stopki na stronach wykonawców przepływu." msgstr "Opcja ta konfiguruje łącza stopki na stronach wykonawców przepływu."

View File

@ -18,7 +18,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n" "POT-Creation-Date: 2025-04-11 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Gil Poiares-Oliveira, 2025\n" "Last-Translator: Gil Poiares-Oliveira, 2025\n"
"Language-Team: Portuguese (Brazil) (https://app.transifex.com/authentik/teams/119923/pt_BR/)\n" "Language-Team: Portuguese (Brazil) (https://app.transifex.com/authentik/teams/119923/pt_BR/)\n"
@ -192,7 +192,6 @@ msgid "User's display name."
msgstr "Nome de exibição do usuário." msgstr "Nome de exibição do usuário."
#: authentik/core/models.py authentik/providers/oauth2/models.py #: authentik/core/models.py authentik/providers/oauth2/models.py
#: authentik/rbac/models.py
msgid "User" msgid "User"
msgstr "Usuário" msgstr "Usuário"
@ -377,18 +376,6 @@ msgstr "Mapeamento de propriedades"
msgid "Property Mappings" msgid "Property Mappings"
msgstr "Mapeamentos de propriedades" msgstr "Mapeamentos de propriedades"
#: authentik/core/models.py
msgid "session data"
msgstr ""
#: authentik/core/models.py
msgid "Session"
msgstr ""
#: authentik/core/models.py
msgid "Sessions"
msgstr ""
#: authentik/core/models.py #: authentik/core/models.py
msgid "Authenticated Session" msgid "Authenticated Session"
msgstr "Sessão Autenticada" msgstr "Sessão Autenticada"
@ -496,38 +483,6 @@ msgstr "Uso de licença"
msgid "License Usage Records" msgid "License Usage Records"
msgstr "Registros de uso de licença" msgstr "Registros de uso de licença"
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
"Chave de campo para verificar, as chaves de campo definidas nos estágios de "
"prompt estão disponíveis."
#: authentik/enterprise/policies/unique_password/models.py
msgid "Number of passwords to check against."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "Senha não definida no contexto"
#: authentik/enterprise/policies/unique_password/models.py
msgid "This password has been used previously. Please choose a different one."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policy"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policies"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "User Password History"
msgstr ""
#: authentik/enterprise/policy.py #: authentik/enterprise/policy.py
msgid "Enterprise required to access this feature." msgid "Enterprise required to access this feature."
msgstr "Entrerprise é necessário para acessar essa funcionalidade" msgstr "Entrerprise é necessário para acessar essa funcionalidade"
@ -1297,6 +1252,12 @@ msgstr ""
msgid "Clear Policy's cache metrics" msgid "Clear Policy's cache metrics"
msgstr "" msgstr ""
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
"Chave de campo para verificar, as chaves de campo definidas nos estágios de "
"prompt estão disponíveis."
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "How many times the password hash is allowed to be on haveibeenpwned" msgid "How many times the password hash is allowed to be on haveibeenpwned"
msgstr "Quantas vezes o hash da senha pode estar em haveibeenpwned" msgstr "Quantas vezes o hash da senha pode estar em haveibeenpwned"
@ -1307,6 +1268,10 @@ msgid ""
msgstr "" msgstr ""
"Se a pontuação zxcvbn for igual ou menor que esse valor, a política falhará." "Se a pontuação zxcvbn for igual ou menor que esse valor, a política falhará."
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "Senha não definida no contexto"
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "Invalid password." msgid "Invalid password."
msgstr "" msgstr ""
@ -1348,6 +1313,20 @@ msgstr "Pontuação de reputação"
msgid "Reputation Scores" msgid "Reputation Scores"
msgstr "Pontuações de reputação" msgstr "Pontuações de reputação"
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr ""
#: authentik/policies/templates/policies/denied.html #: authentik/policies/templates/policies/denied.html
msgid "Permission denied" msgid "Permission denied"
msgstr "Permissão negada" msgstr "Permissão negada"
@ -2162,10 +2141,6 @@ msgstr ""
msgid "Roles" msgid "Roles"
msgstr "" msgstr ""
#: authentik/rbac/models.py
msgid "Initial Permissions"
msgstr ""
#: authentik/rbac/models.py #: authentik/rbac/models.py
msgid "System permission" msgid "System permission"
msgstr "Permissão do sistema" msgstr "Permissão do sistema"
@ -2412,22 +2387,6 @@ msgstr ""
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "" msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/signals.py #: authentik/sources/ldap/signals.py
msgid "Password does not match Active Directory Complexity." msgid "Password does not match Active Directory Complexity."
msgstr "A senha não corresponde à complexidade do Active Directory." msgstr "A senha não corresponde à complexidade do Active Directory."
@ -2436,14 +2395,6 @@ msgstr "A senha não corresponde à complexidade do Active Directory."
msgid "No token received." msgid "No token received."
msgstr "Nenhum token recebido." msgstr "Nenhum token recebido."
#: authentik/sources/oauth/models.py
msgid "HTTP Basic Authentication"
msgstr ""
#: authentik/sources/oauth/models.py
msgid "Include the client ID and secret as request parameters"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "Request Token URL" msgid "Request Token URL"
msgstr "URL do token de solicitação" msgstr "URL do token de solicitação"
@ -2484,12 +2435,6 @@ msgstr "URL usado pelo authentik para obter informações do usuário."
msgid "Additional Scopes" msgid "Additional Scopes"
msgstr "Escopos Adicionais" msgstr "Escopos Adicionais"
#: authentik/sources/oauth/models.py
msgid ""
"How to perform authentication during an authorization_code token request "
"flow"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "OAuth Source" msgid "OAuth Source"
msgstr "Fonte OAuth" msgstr "Fonte OAuth"
@ -3373,12 +3318,6 @@ msgid ""
"info is entered." "info is entered."
msgstr "" msgstr ""
#: authentik/stages/identification/models.py
msgid ""
"Show the user the 'Remember me on this device' toggle, allowing repeat users"
" to skip straight to entering their password."
msgstr ""
#: authentik/stages/identification/models.py #: authentik/stages/identification/models.py
msgid "Optional enrollment flow, which is linked at the bottom of the page." msgid "Optional enrollment flow, which is linked at the bottom of the page."
msgstr "Optional enrollment flow, which is linked at the bottom of the page." msgstr "Optional enrollment flow, which is linked at the bottom of the page."
@ -3739,14 +3678,6 @@ msgstr ""
"Os eventos serão excluídos após esta duração.(Formato: " "Os eventos serão excluídos após esta duração.(Formato: "
"semanas=3;dias=2;horas=3,segundos=2)." "semanas=3;dias=2;horas=3,segundos=2)."
#: authentik/tenants/models.py
msgid "Reputation cannot decrease lower than this value. Zero or negative."
msgstr ""
#: authentik/tenants/models.py
msgid "Reputation cannot increase higher than this value. Zero or positive."
msgstr ""
#: authentik/tenants/models.py #: authentik/tenants/models.py
msgid "The option configures the footer links on the flow executor pages." msgid "The option configures the footer links on the flow executor pages."
msgstr "" msgstr ""

View File

@ -18,7 +18,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n" "POT-Creation-Date: 2025-04-11 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Marc Schmitt, 2025\n" "Last-Translator: Marc Schmitt, 2025\n"
"Language-Team: Russian (https://app.transifex.com/authentik/teams/119923/ru/)\n" "Language-Team: Russian (https://app.transifex.com/authentik/teams/119923/ru/)\n"
@ -191,7 +191,6 @@ msgid "User's display name."
msgstr "Отображаемое имя пользователя." msgstr "Отображаемое имя пользователя."
#: authentik/core/models.py authentik/providers/oauth2/models.py #: authentik/core/models.py authentik/providers/oauth2/models.py
#: authentik/rbac/models.py
msgid "User" msgid "User"
msgstr "Пользователь" msgstr "Пользователь"
@ -380,18 +379,6 @@ msgstr "Сопоставление свойств"
msgid "Property Mappings" msgid "Property Mappings"
msgstr "Сопоставление свойств" msgstr "Сопоставление свойств"
#: authentik/core/models.py
msgid "session data"
msgstr ""
#: authentik/core/models.py
msgid "Session"
msgstr ""
#: authentik/core/models.py
msgid "Sessions"
msgstr ""
#: authentik/core/models.py #: authentik/core/models.py
msgid "Authenticated Session" msgid "Authenticated Session"
msgstr "Аутентифицированная Сессия" msgstr "Аутентифицированная Сессия"
@ -500,37 +487,6 @@ msgstr "Использование лицензии"
msgid "License Usage Records" msgid "License Usage Records"
msgstr "Записи использования лицензии" msgstr "Записи использования лицензии"
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
"Ключ поля для проверки, доступны ключи поля, определенные в этапах запроса."
#: authentik/enterprise/policies/unique_password/models.py
msgid "Number of passwords to check against."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "Пароль не задан в контексте"
#: authentik/enterprise/policies/unique_password/models.py
msgid "This password has been used previously. Please choose a different one."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policy"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policies"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "User Password History"
msgstr ""
#: authentik/enterprise/policy.py #: authentik/enterprise/policy.py
msgid "Enterprise required to access this feature." msgid "Enterprise required to access this feature."
msgstr "Для доступа к этой функции требуется Enterprise." msgstr "Для доступа к этой функции требуется Enterprise."
@ -1311,6 +1267,11 @@ msgstr "Просмотр показателей кэша политики"
msgid "Clear Policy's cache metrics" msgid "Clear Policy's cache metrics"
msgstr "Очистка показателей кэша политики" msgstr "Очистка показателей кэша политики"
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
"Ключ поля для проверки, доступны ключи поля, определенные в этапах запроса."
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "How many times the password hash is allowed to be on haveibeenpwned" msgid "How many times the password hash is allowed to be on haveibeenpwned"
msgstr "Как часто хэш пароля может быть представлен на haveibeenpwned" msgstr "Как часто хэш пароля может быть представлен на haveibeenpwned"
@ -1322,6 +1283,10 @@ msgstr ""
"Если показатель zxcvbn равен или меньше этого значения, политика будет " "Если показатель zxcvbn равен или меньше этого значения, политика будет "
"провалена." "провалена."
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "Пароль не задан в контексте"
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "Invalid password." msgid "Invalid password."
msgstr "Неправильный пароль" msgstr "Неправильный пароль"
@ -1363,6 +1328,20 @@ msgstr "Оценка репутации"
msgid "Reputation Scores" msgid "Reputation Scores"
msgstr "Оценка репутации" msgstr "Оценка репутации"
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr ""
#: authentik/policies/templates/policies/denied.html #: authentik/policies/templates/policies/denied.html
msgid "Permission denied" msgid "Permission denied"
msgstr "Доступ запрещен" msgstr "Доступ запрещен"
@ -2185,10 +2164,6 @@ msgstr "Роль"
msgid "Roles" msgid "Roles"
msgstr "Роли" msgstr "Роли"
#: authentik/rbac/models.py
msgid "Initial Permissions"
msgstr ""
#: authentik/rbac/models.py #: authentik/rbac/models.py
msgid "System permission" msgid "System permission"
msgstr "Системное разрешение" msgstr "Системное разрешение"
@ -2446,22 +2421,6 @@ msgstr "Сопоставление свойства LDAP источника"
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "Сопоставление свойств LDAP источника" msgstr "Сопоставление свойств LDAP источника"
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/signals.py #: authentik/sources/ldap/signals.py
msgid "Password does not match Active Directory Complexity." msgid "Password does not match Active Directory Complexity."
msgstr "Пароль не соответствует сложности Active Directory." msgstr "Пароль не соответствует сложности Active Directory."
@ -2470,14 +2429,6 @@ msgstr "Пароль не соответствует сложности Active D
msgid "No token received." msgid "No token received."
msgstr "Токен не был получен." msgstr "Токен не был получен."
#: authentik/sources/oauth/models.py
msgid "HTTP Basic Authentication"
msgstr ""
#: authentik/sources/oauth/models.py
msgid "Include the client ID and secret as request parameters"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "Request Token URL" msgid "Request Token URL"
msgstr "URL-адрес запроса токена" msgstr "URL-адрес запроса токена"
@ -2520,12 +2471,6 @@ msgstr ""
msgid "Additional Scopes" msgid "Additional Scopes"
msgstr "Дополнительные области" msgstr "Дополнительные области"
#: authentik/sources/oauth/models.py
msgid ""
"How to perform authentication during an authorization_code token request "
"flow"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "OAuth Source" msgid "OAuth Source"
msgstr "Источник OAuth" msgstr "Источник OAuth"
@ -3431,12 +3376,6 @@ msgstr ""
"При включении этап будет завершаться успешно и продолжаться даже в случае " "При включении этап будет завершаться успешно и продолжаться даже в случае "
"ввода неправильной информации о пользователе." "ввода неправильной информации о пользователе."
#: authentik/stages/identification/models.py
msgid ""
"Show the user the 'Remember me on this device' toggle, allowing repeat users"
" to skip straight to entering their password."
msgstr ""
#: authentik/stages/identification/models.py #: authentik/stages/identification/models.py
msgid "Optional enrollment flow, which is linked at the bottom of the page." msgid "Optional enrollment flow, which is linked at the bottom of the page."
msgstr "" msgstr ""
@ -3828,14 +3767,6 @@ msgstr ""
"По истечении этого времени события будут удалены. (Формат: недели=3; дни=2; " "По истечении этого времени события будут удалены. (Формат: недели=3; дни=2; "
"часы=3, секунды=2)." "часы=3, секунды=2)."
#: authentik/tenants/models.py
msgid "Reputation cannot decrease lower than this value. Zero or negative."
msgstr ""
#: authentik/tenants/models.py
msgid "Reputation cannot increase higher than this value. Zero or positive."
msgstr ""
#: authentik/tenants/models.py #: authentik/tenants/models.py
msgid "The option configures the footer links on the flow executor pages." msgid "The option configures the footer links on the flow executor pages."
msgstr "" msgstr ""

Binary file not shown.

View File

@ -13,7 +13,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n" "POT-Creation-Date: 2025-03-31 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n" "Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n"
"Language-Team: Turkish (https://app.transifex.com/authentik/teams/119923/tr/)\n" "Language-Team: Turkish (https://app.transifex.com/authentik/teams/119923/tr/)\n"
@ -187,7 +187,6 @@ msgid "User's display name."
msgstr "Kullanıcının görünen adı." msgstr "Kullanıcının görünen adı."
#: authentik/core/models.py authentik/providers/oauth2/models.py #: authentik/core/models.py authentik/providers/oauth2/models.py
#: authentik/rbac/models.py
msgid "User" msgid "User"
msgstr "Kullanıcı" msgstr "Kullanıcı"
@ -373,18 +372,6 @@ msgstr "Özellik Eşleme"
msgid "Property Mappings" msgid "Property Mappings"
msgstr "Özellik Eşlemeleri" msgstr "Özellik Eşlemeleri"
#: authentik/core/models.py
msgid "session data"
msgstr ""
#: authentik/core/models.py
msgid "Session"
msgstr "Oturum"
#: authentik/core/models.py
msgid "Sessions"
msgstr "Oturumlar"
#: authentik/core/models.py #: authentik/core/models.py
msgid "Authenticated Session" msgid "Authenticated Session"
msgstr "Kimliği Doğrulanmış Oturum" msgstr "Kimliği Doğrulanmış Oturum"
@ -492,38 +479,6 @@ msgstr "Lisans Kullanımı"
msgid "License Usage Records" msgid "License Usage Records"
msgstr "Lisans Kullanım Kayıtları" msgstr "Lisans Kullanım Kayıtları"
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
"Alan tuşu kontrol etmek için, İstem aşamalarında tanımlanan alan tuşları "
"mevcuttur."
#: authentik/enterprise/policies/unique_password/models.py
msgid "Number of passwords to check against."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "Parola bağlam içinde ayarlanmamış"
#: authentik/enterprise/policies/unique_password/models.py
msgid "This password has been used previously. Please choose a different one."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policy"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policies"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "User Password History"
msgstr ""
#: authentik/enterprise/policy.py #: authentik/enterprise/policy.py
msgid "Enterprise required to access this feature." msgid "Enterprise required to access this feature."
msgstr "Bu özelliğe erişmek için Kurumsal Paket gereklidir." msgstr "Bu özelliğe erişmek için Kurumsal Paket gereklidir."
@ -1298,6 +1253,12 @@ msgstr "İlke'nin önbellek ölçümlerini görüntüleme"
msgid "Clear Policy's cache metrics" msgid "Clear Policy's cache metrics"
msgstr "İlke'nin önbellek ölçümlerini temizleyin" msgstr "İlke'nin önbellek ölçümlerini temizleyin"
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
"Alan tuşu kontrol etmek için, İstem aşamalarında tanımlanan alan tuşları "
"mevcuttur."
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "How many times the password hash is allowed to be on haveibeenpwned" msgid "How many times the password hash is allowed to be on haveibeenpwned"
msgstr "" msgstr ""
@ -1310,6 +1271,10 @@ msgstr ""
"Eğer zxcvbn puanı bu değere eşit veya daha az ise, politika başarısız " "Eğer zxcvbn puanı bu değere eşit veya daha az ise, politika başarısız "
"olacaktır." "olacaktır."
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "Parola bağlam içinde ayarlanmamış"
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "Invalid password." msgid "Invalid password."
msgstr "" msgstr ""
@ -1351,6 +1316,20 @@ msgstr "İtibar Puanı"
msgid "Reputation Scores" msgid "Reputation Scores"
msgstr "İtibar Puanları" msgstr "İtibar Puanları"
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr ""
#: authentik/policies/templates/policies/denied.html #: authentik/policies/templates/policies/denied.html
msgid "Permission denied" msgid "Permission denied"
msgstr "İzin reddedildi" msgstr "İzin reddedildi"
@ -2176,10 +2155,6 @@ msgstr "Rol"
msgid "Roles" msgid "Roles"
msgstr "Roller" msgstr "Roller"
#: authentik/rbac/models.py
msgid "Initial Permissions"
msgstr ""
#: authentik/rbac/models.py #: authentik/rbac/models.py
msgid "System permission" msgid "System permission"
msgstr "Sistem yetkisi" msgstr "Sistem yetkisi"
@ -2423,13 +2398,6 @@ msgstr ""
"Bir kullanıcı parolasını değiştirdiğinde, parolayı LDAP ile geri eşitleyin. " "Bir kullanıcı parolasını değiştirdiğinde, parolayı LDAP ile geri eşitleyin. "
"Bu yalnızca tek bir LDAP kaynağında etkinleştirilebilir." "Bu yalnızca tek bir LDAP kaynağında etkinleştirilebilir."
#: authentik/sources/ldap/models.py
msgid ""
"Lookup group membership based on a user attribute instead of a group "
"attribute. This allows nested group resolution on systems like FreeIPA and "
"Active Directory"
msgstr ""
#: authentik/sources/ldap/models.py #: authentik/sources/ldap/models.py
msgid "LDAP Source" msgid "LDAP Source"
msgstr "LDAP Kaynağı" msgstr "LDAP Kaynağı"
@ -2446,22 +2414,6 @@ msgstr "LDAP Kaynak Özellik Eşlemesi"
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "LDAP Kaynak Özellik Eşlemeleri" msgstr "LDAP Kaynak Özellik Eşlemeleri"
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/signals.py #: authentik/sources/ldap/signals.py
msgid "Password does not match Active Directory Complexity." msgid "Password does not match Active Directory Complexity."
msgstr "Parola Active Directory Karmaşıklığıyla eşleşmiyor." msgstr "Parola Active Directory Karmaşıklığıyla eşleşmiyor."
@ -2470,14 +2422,6 @@ msgstr "Parola Active Directory Karmaşıklığıyla eşleşmiyor."
msgid "No token received." msgid "No token received."
msgstr "Jeton alınmadı." msgstr "Jeton alınmadı."
#: authentik/sources/oauth/models.py
msgid "HTTP Basic Authentication"
msgstr ""
#: authentik/sources/oauth/models.py
msgid "Include the client ID and secret as request parameters"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "Request Token URL" msgid "Request Token URL"
msgstr "Jeton URL'si İste" msgstr "Jeton URL'si İste"
@ -2518,12 +2462,6 @@ msgstr "Kullanıcı bilgilerini almak için authentik tarafından kullanılan UR
msgid "Additional Scopes" msgid "Additional Scopes"
msgstr "Ek Kapsamlar" msgstr "Ek Kapsamlar"
#: authentik/sources/oauth/models.py
msgid ""
"How to perform authentication during an authorization_code token request "
"flow"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "OAuth Source" msgid "OAuth Source"
msgstr "OAuth Kaynağı" msgstr "OAuth Kaynağı"
@ -3422,12 +3360,6 @@ msgstr ""
"Etkinleştirildiğinde, yanlış kullanıcı bilgisi girilse bile aşama başarılı " "Etkinleştirildiğinde, yanlış kullanıcı bilgisi girilse bile aşama başarılı "
"olur ve devam eder." "olur ve devam eder."
#: authentik/stages/identification/models.py
msgid ""
"Show the user the 'Remember me on this device' toggle, allowing repeat users"
" to skip straight to entering their password."
msgstr ""
#: authentik/stages/identification/models.py #: authentik/stages/identification/models.py
msgid "Optional enrollment flow, which is linked at the bottom of the page." msgid "Optional enrollment flow, which is linked at the bottom of the page."
msgstr "Sayfanın alt kısmında bağlanan isteğe bağlı kayıt akışı." msgstr "Sayfanın alt kısmında bağlanan isteğe bağlı kayıt akışı."
@ -3802,14 +3734,6 @@ msgstr ""
"Olaylar bu süreden sonra silinecektir (Format: " "Olaylar bu süreden sonra silinecektir (Format: "
"weeks=3;days=2;hours=3,seconds=2)." "weeks=3;days=2;hours=3,seconds=2)."
#: authentik/tenants/models.py
msgid "Reputation cannot decrease lower than this value. Zero or negative."
msgstr ""
#: authentik/tenants/models.py
msgid "Reputation cannot increase higher than this value. Zero or positive."
msgstr ""
#: authentik/tenants/models.py #: authentik/tenants/models.py
msgid "The option configures the footer links on the flow executor pages." msgid "The option configures the footer links on the flow executor pages."
msgstr "" msgstr ""

Binary file not shown.

View File

@ -14,7 +14,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-23 09:00+0000\n" "POT-Creation-Date: 2025-04-11 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: 刘松, 2025\n" "Last-Translator: 刘松, 2025\n"
"Language-Team: Chinese (Taiwan) (https://app.transifex.com/authentik/teams/119923/zh_TW/)\n" "Language-Team: Chinese (Taiwan) (https://app.transifex.com/authentik/teams/119923/zh_TW/)\n"
@ -178,7 +178,6 @@ msgid "User's display name."
msgstr "使用者的顯示名稱。" msgstr "使用者的顯示名稱。"
#: authentik/core/models.py authentik/providers/oauth2/models.py #: authentik/core/models.py authentik/providers/oauth2/models.py
#: authentik/rbac/models.py
msgid "User" msgid "User"
msgstr "使用者" msgstr "使用者"
@ -345,18 +344,6 @@ msgstr "屬性對應"
msgid "Property Mappings" msgid "Property Mappings"
msgstr "屬性對應" msgstr "屬性對應"
#: authentik/core/models.py
msgid "session data"
msgstr ""
#: authentik/core/models.py
msgid "Session"
msgstr "会话"
#: authentik/core/models.py
msgid "Sessions"
msgstr "会话"
#: authentik/core/models.py #: authentik/core/models.py
msgid "Authenticated Session" msgid "Authenticated Session"
msgstr "已認證會談" msgstr "已認證會談"
@ -460,36 +447,6 @@ msgstr "授權使用情況"
msgid "License Usage Records" msgid "License Usage Records"
msgstr "授權使用紀錄" msgstr "授權使用紀錄"
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr "要檢查的欄位鍵,在提示階段中有可用的已定義欄位鍵。"
#: authentik/enterprise/policies/unique_password/models.py
msgid "Number of passwords to check against."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "未在上下文中設定密碼"
#: authentik/enterprise/policies/unique_password/models.py
msgid "This password has been used previously. Please choose a different one."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policy"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policies"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "User Password History"
msgstr ""
#: authentik/enterprise/policy.py #: authentik/enterprise/policy.py
msgid "Enterprise required to access this feature." msgid "Enterprise required to access this feature."
msgstr "企業版才能存取此功能。" msgstr "企業版才能存取此功能。"
@ -1219,6 +1176,10 @@ msgstr "檢視原則的快取指標"
msgid "Clear Policy's cache metrics" msgid "Clear Policy's cache metrics"
msgstr "清除原則的快取指標" msgstr "清除原則的快取指標"
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr "要檢查的欄位鍵,在提示階段中有可用的已定義欄位鍵。"
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "How many times the password hash is allowed to be on haveibeenpwned" msgid "How many times the password hash is allowed to be on haveibeenpwned"
msgstr "密碼雜湊在 haveibeenpwned 上允許出現的次數" msgstr "密碼雜湊在 haveibeenpwned 上允許出現的次數"
@ -1228,6 +1189,10 @@ msgid ""
"If the zxcvbn score is equal or less than this value, the policy will fail." "If the zxcvbn score is equal or less than this value, the policy will fail."
msgstr "如果 zxcvbn 分數等於或小於此值,則該政策將失敗。" msgstr "如果 zxcvbn 分數等於或小於此值,則該政策將失敗。"
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr "未在上下文中設定密碼"
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "Invalid password." msgid "Invalid password."
msgstr "" msgstr ""
@ -1269,6 +1234,20 @@ msgstr "信譽分數"
msgid "Reputation Scores" msgid "Reputation Scores"
msgstr "信譽分數" msgstr "信譽分數"
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr ""
#: authentik/policies/templates/policies/denied.html #: authentik/policies/templates/policies/denied.html
msgid "Permission denied" msgid "Permission denied"
msgstr "權限不足。" msgstr "權限不足。"
@ -2020,10 +1999,6 @@ msgstr "角色"
msgid "Roles" msgid "Roles"
msgstr "角色" msgstr "角色"
#: authentik/rbac/models.py
msgid "Initial Permissions"
msgstr ""
#: authentik/rbac/models.py #: authentik/rbac/models.py
msgid "System permission" msgid "System permission"
msgstr "系統權限" msgstr "系統權限"
@ -2265,22 +2240,6 @@ msgstr ""
msgid "LDAP Source Property Mappings" msgid "LDAP Source Property Mappings"
msgstr "" msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "User LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connection"
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Group LDAP Source Connections"
msgstr ""
#: authentik/sources/ldap/signals.py #: authentik/sources/ldap/signals.py
msgid "Password does not match Active Directory Complexity." msgid "Password does not match Active Directory Complexity."
msgstr "密碼不符合 Active Directory 的複雜性要求。" msgstr "密碼不符合 Active Directory 的複雜性要求。"
@ -2289,14 +2248,6 @@ msgstr "密碼不符合 Active Directory 的複雜性要求。"
msgid "No token received." msgid "No token received."
msgstr "未收到權杖。" msgstr "未收到權杖。"
#: authentik/sources/oauth/models.py
msgid "HTTP Basic Authentication"
msgstr ""
#: authentik/sources/oauth/models.py
msgid "Include the client ID and secret as request parameters"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "Request Token URL" msgid "Request Token URL"
msgstr "請求權杖的網址" msgstr "請求權杖的網址"
@ -2335,12 +2286,6 @@ msgstr "authentik 用來擷取使用者資訊的網址。"
msgid "Additional Scopes" msgid "Additional Scopes"
msgstr "附加範圍" msgstr "附加範圍"
#: authentik/sources/oauth/models.py
msgid ""
"How to perform authentication during an authorization_code token request "
"flow"
msgstr ""
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "OAuth Source" msgid "OAuth Source"
msgstr "OAuth 來源" msgstr "OAuth 來源"
@ -3192,12 +3137,6 @@ msgid ""
"info is entered." "info is entered."
msgstr "" msgstr ""
#: authentik/stages/identification/models.py
msgid ""
"Show the user the 'Remember me on this device' toggle, allowing repeat users"
" to skip straight to entering their password."
msgstr ""
#: authentik/stages/identification/models.py #: authentik/stages/identification/models.py
msgid "Optional enrollment flow, which is linked at the bottom of the page." msgid "Optional enrollment flow, which is linked at the bottom of the page."
msgstr "可選的註冊流程,連結在頁面的底部。" msgstr "可選的註冊流程,連結在頁面的底部。"
@ -3542,14 +3481,6 @@ msgid ""
"weeks=3;days=2;hours=3,seconds=2)." "weeks=3;days=2;hours=3,seconds=2)."
msgstr "事件將在此期間後刪除。格式weeks=3;days=2;hours=3,seconds=2" msgstr "事件將在此期間後刪除。格式weeks=3;days=2;hours=3,seconds=2"
#: authentik/tenants/models.py
msgid "Reputation cannot decrease lower than this value. Zero or negative."
msgstr ""
#: authentik/tenants/models.py
msgid "Reputation cannot increase higher than this value. Zero or positive."
msgstr ""
#: authentik/tenants/models.py #: authentik/tenants/models.py
msgid "The option configures the footer links on the flow executor pages." msgid "The option configures the footer links on the flow executor pages."
msgstr "" msgstr ""

View File

@ -1,5 +1,5 @@
{ {
"name": "@goauthentik/authentik", "name": "@goauthentik/authentik",
"version": "2025.4.0", "version": "2025.2.4",
"private": true "private": true
} }

View File

@ -76,7 +76,6 @@ EXPOSE 9000 9300 9443
USER 1000 USER 1000
ENV TMPDIR=/dev/shm/ \ ENV GOFIPS=1
GOFIPS=1
ENTRYPOINT ["/proxy"] ENTRYPOINT ["/proxy"]

View File

@ -1,6 +1,6 @@
[project] [project]
name = "authentik" name = "authentik"
version = "2025.4.0" version = "2025.2.4"
description = "" description = ""
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }] authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
requires-python = "==3.12.*" requires-python = "==3.12.*"

View File

@ -56,7 +56,6 @@ HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "/rac", "healthch
USER 1000 USER 1000
ENV TMPDIR=/dev/shm/ \ ENV GOFIPS=1
GOFIPS=1
ENTRYPOINT ["/rac"] ENTRYPOINT ["/rac"]

View File

@ -56,7 +56,6 @@ EXPOSE 1812/udp 9300
USER 1000 USER 1000
ENV TMPDIR=/dev/shm/ \ ENV GOFIPS=1
GOFIPS=1
ENTRYPOINT ["/radius"] ENTRYPOINT ["/radius"]

View File

@ -1,7 +1,7 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: authentik title: authentik
version: 2025.4.0 version: 2025.2.4
description: Making authentication simple. description: Making authentication simple.
contact: contact:
email: hello@goauthentik.io email: hello@goauthentik.io

View File

@ -410,77 +410,3 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text, self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
"Permission denied", "Permission denied",
) )
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint("default/flow-default-provider-authorization-implicit-consent.yaml")
@apply_blueprint("system/providers-oauth2.yaml")
@reconcile_app("authentik_crypto")
def test_authorization_consent_implied_parallel(self):
"""test OpenID Provider flow (default authorization flow with implied consent)"""
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
)
provider = OAuth2Provider.objects.create(
name=generate_id(),
client_type=ClientTypes.CONFIDENTIAL,
client_id=self.client_id,
client_secret=self.client_secret,
signing_key=create_test_cert(),
redirect_uris=[
RedirectURI(
RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth"
)
],
authorization_flow=authorization_flow,
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
scope_name__in=[
SCOPE_OPENID,
SCOPE_OPENID_EMAIL,
SCOPE_OPENID_PROFILE,
SCOPE_OFFLINE_ACCESS,
]
)
)
Application.objects.create(
name=generate_id(),
slug=self.app_slug,
provider=provider,
)
self.driver.get(self.live_server_url)
login_window = self.driver.current_window_handle
self.driver.switch_to.new_window("tab")
grafana_window = self.driver.current_window_handle
self.driver.get("http://localhost:3000")
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
self.driver.switch_to.window(login_window)
self.login()
self.driver.switch_to.window(grafana_window)
self.wait_for_url("http://localhost:3000/?orgId=1")
self.driver.get("http://localhost:3000/profile")
self.assertEqual(
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
self.user.name,
)
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute("value"),
self.user.name,
)
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "input[name=email]").get_attribute("value"),
self.user.email,
)
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "input[name=login]").get_attribute("value"),
self.user.email,
)

View File

@ -20,7 +20,7 @@ from tests.e2e.utils import SeleniumTestCase, retry
class TestProviderSAML(SeleniumTestCase): class TestProviderSAML(SeleniumTestCase):
"""test SAML Provider flow""" """test SAML Provider flow"""
def setup_client(self, provider: SAMLProvider, force_post: bool = False, **kwargs): def setup_client(self, provider: SAMLProvider, force_post: bool = False):
"""Setup client saml-sp container which we test SAML against""" """Setup client saml-sp container which we test SAML against"""
metadata_url = ( metadata_url = (
self.url( self.url(
@ -40,7 +40,6 @@ class TestProviderSAML(SeleniumTestCase):
"SP_ENTITY_ID": provider.issuer, "SP_ENTITY_ID": provider.issuer,
"SP_SSO_BINDING": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", "SP_SSO_BINDING": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
"SP_METADATA_URL": metadata_url, "SP_METADATA_URL": metadata_url,
**kwargs,
}, },
) )
@ -112,74 +111,6 @@ class TestProviderSAML(SeleniumTestCase):
[self.user.email], [self.user.email],
) )
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"default/flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"system/providers-saml.yaml",
)
@reconcile_app("authentik_crypto")
def test_sp_initiated_implicit_post(self):
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
)
provider: SAMLProvider = SAMLProvider.objects.create(
name="saml-test",
acs_url="http://localhost:9009/saml/acs",
audience="authentik-e2e",
issuer="authentik-e2e",
sp_binding=SAMLBindings.POST,
authorization_flow=authorization_flow,
signing_kp=create_test_cert(),
)
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
provider.save()
Application.objects.create(
name="SAML",
slug="authentik-saml",
provider=provider,
)
self.setup_client(provider, True)
self.driver.get("http://localhost:9009")
self.login()
self.wait_for_url("http://localhost:9009/")
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
self.assertEqual(
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],
[self.user.name],
)
self.assertEqual(
body["attr"][
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
],
[self.user.username],
)
self.assertEqual(
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/username"],
[self.user.username],
)
self.assertEqual(
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/uid"],
[str(self.user.pk)],
)
self.assertEqual(
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
[self.user.email],
)
self.assertEqual(
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"],
[self.user.email],
)
@retry() @retry()
@apply_blueprint( @apply_blueprint(
"default/flow-default-authentication-flow.yaml", "default/flow-default-authentication-flow.yaml",
@ -519,81 +450,3 @@ class TestProviderSAML(SeleniumTestCase):
lambda driver: driver.current_url.startswith(should_url), lambda driver: driver.current_url.startswith(should_url),
f"URL {self.driver.current_url} doesn't match expected URL {should_url}", f"URL {self.driver.current_url} doesn't match expected URL {should_url}",
) )
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"default/flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"system/providers-saml.yaml",
)
@reconcile_app("authentik_crypto")
def test_sp_initiated_implicit_post_buffer(self):
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
)
provider: SAMLProvider = SAMLProvider.objects.create(
name="saml-test",
acs_url=f"http://{self.host}:9009/saml/acs",
audience="authentik-e2e",
issuer="authentik-e2e",
sp_binding=SAMLBindings.POST,
authorization_flow=authorization_flow,
signing_kp=create_test_cert(),
)
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
provider.save()
Application.objects.create(
name="SAML",
slug="authentik-saml",
provider=provider,
)
self.setup_client(provider, True, SP_ROOT_URL=f"http://{self.host}:9009")
self.driver.get(self.live_server_url)
login_window = self.driver.current_window_handle
self.driver.switch_to.new_window("tab")
client_window = self.driver.current_window_handle
# We need to access the SP on the same host as the IdP for SameSite cookies
self.driver.get(f"http://{self.host}:9009")
self.driver.switch_to.window(login_window)
self.login()
self.driver.switch_to.window(client_window)
self.wait_for_url(f"http://{self.host}:9009/")
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
self.assertEqual(
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],
[self.user.name],
)
self.assertEqual(
body["attr"][
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
],
[self.user.username],
)
self.assertEqual(
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/username"],
[self.user.username],
)
self.assertEqual(
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/uid"],
[str(self.user.pk)],
)
self.assertEqual(
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
[self.user.email],
)
self.assertEqual(
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"],
[self.user.email],
)

14
uv.lock generated
View File

@ -165,7 +165,7 @@ wheels = [
[[package]] [[package]]
name = "authentik" name = "authentik"
version = "2025.4.0" version = "2025.2.4"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "argon2-cffi" }, { name = "argon2-cffi" },
@ -1436,11 +1436,11 @@ wheels = [
[[package]] [[package]]
name = "h11" name = "h11"
version = "0.16.0" version = "0.14.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
] ]
[[package]] [[package]]
@ -1467,15 +1467,15 @@ wheels = [
[[package]] [[package]]
name = "httpcore" name = "httpcore"
version = "1.0.9" version = "1.0.8"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
{ name = "h11" }, { name = "h11" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } sdist = { url = "https://files.pythonhosted.org/packages/9f/45/ad3e1b4d448f22c0cff4f5692f5ed0666658578e358b8d58a19846048059/httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad", size = 85385 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, { url = "https://files.pythonhosted.org/packages/18/8d/f052b1e336bb2c1fc7ed1aaed898aa570c0b61a09707b108979d9fc6e308/httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be", size = 78732 },
] ]
[[package]] [[package]]

12198
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,8 +12,7 @@
"@floating-ui/dom": "^1.6.11", "@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7", "@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.2.4-1745519715", "@goauthentik/api": "^2025.2.4-1745325566",
"@lit-labs/ssr": "3.2.2",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2", "@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4", "@lit/reactive-element": "^2.0.4",
@ -54,6 +53,7 @@
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"remark-mdx-frontmatter": "^5.0.0", "remark-mdx-frontmatter": "^5.0.0",
"style-mod": "^4.1.2", "style-mod": "^4.1.2",
"trusted-types": "^2.0.0",
"ts-pattern": "^5.4.0", "ts-pattern": "^5.4.0",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"webcomponent-qr-code": "^1.2.0", "webcomponent-qr-code": "^1.2.0",

View File

@ -47,16 +47,7 @@ class SimpleFlowExecutor {
return `${ak().api.base}api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`; return `${ak().api.base}api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`;
} }
loading() {
this.container.innerHTML = `<div class="d-flex justify-content-center">
<div class="spinner-border spinner-border-md" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>`;
}
start() { start() {
this.loading();
$.ajax({ $.ajax({
type: "GET", type: "GET",
url: this.apiURL, url: this.apiURL,

View File

@ -4,13 +4,17 @@ import { ROUTES } from "@goauthentik/admin/Routes";
import { import {
EVENT_API_DRAWER_TOGGLE, EVENT_API_DRAWER_TOGGLE,
EVENT_NOTIFICATION_DRAWER_TOGGLE, EVENT_NOTIFICATION_DRAWER_TOGGLE,
EVENT_SIDEBAR_TOGGLE,
} from "@goauthentik/common/constants"; } from "@goauthentik/common/constants";
import { configureSentry } from "@goauthentik/common/sentry"; import { configureSentry } from "@goauthentik/common/sentry";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { WebsocketClient } from "@goauthentik/common/ws"; import { WebsocketClient } from "@goauthentik/common/ws";
import { AuthenticatedInterface } from "@goauthentik/elements/Interface"; import { AuthenticatedInterface } from "@goauthentik/elements/Interface";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js";
import "@goauthentik/elements/ak-locale-context"; import "@goauthentik/elements/ak-locale-context";
import "@goauthentik/elements/banner/EnterpriseStatusBanner"; import "@goauthentik/elements/banner/EnterpriseStatusBanner";
import "@goauthentik/elements/banner/EnterpriseStatusBanner";
import "@goauthentik/elements/banner/VersionBanner";
import "@goauthentik/elements/banner/VersionBanner"; import "@goauthentik/elements/banner/VersionBanner";
import "@goauthentik/elements/messages/MessageContainer"; import "@goauthentik/elements/messages/MessageContainer";
import "@goauthentik/elements/messages/MessageContainer"; import "@goauthentik/elements/messages/MessageContainer";
@ -21,25 +25,32 @@ import "@goauthentik/elements/router/RouterOutlet";
import "@goauthentik/elements/sidebar/Sidebar"; import "@goauthentik/elements/sidebar/Sidebar";
import "@goauthentik/elements/sidebar/SidebarItem"; import "@goauthentik/elements/sidebar/SidebarItem";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators.js"; import { customElement, property, query, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js"; import { classMap } from "lit/directives/class-map.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css"; import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
import PFNav from "@patternfly/patternfly/components/Nav/nav.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css"; import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { SessionUser, UiThemeEnum } from "@goauthentik/api"; import { LicenseSummaryStatusEnum, SessionUser, UiThemeEnum } from "@goauthentik/api";
import "./AdminSidebar"; import {
AdminSidebarEnterpriseEntries,
AdminSidebarEntries,
renderSidebarItems,
} from "./AdminSidebar.js";
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
await import("@goauthentik/esbuild-plugin-live-reload/client"); await import("@goauthentik/esbuild-plugin-live-reload/client");
} }
@customElement("ak-interface-admin") @customElement("ak-interface-admin")
export class AdminInterface extends AuthenticatedInterface { export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
//#region Properties
@property({ type: Boolean }) @property({ type: Boolean })
notificationDrawerOpen = getURLParam("notificationDrawerOpen", false); notificationDrawerOpen = getURLParam("notificationDrawerOpen", false);
@ -54,12 +65,29 @@ export class AdminInterface extends AuthenticatedInterface {
@query("ak-about-modal") @query("ak-about-modal")
aboutModal?: AboutModal; aboutModal?: AboutModal;
@property({ type: Boolean, reflect: true })
public sidebarOpen: boolean;
#toggleSidebar = () => {
this.sidebarOpen = !this.sidebarOpen;
};
#sidebarMatcher: MediaQueryList;
#sidebarListener = (event: MediaQueryListEvent) => {
this.sidebarOpen = event.matches;
};
//#endregion
//#region Styles
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
PFBase, PFBase,
PFPage, PFPage,
PFButton, PFButton,
PFDrawer, PFDrawer,
PFNav,
css` css`
.pf-c-page__main, .pf-c-page__main,
.pf-c-drawer__content, .pf-c-drawer__content,
@ -67,23 +95,30 @@ export class AdminInterface extends AuthenticatedInterface {
z-index: auto !important; z-index: auto !important;
background-color: transparent; background-color: transparent;
} }
.display-none { .display-none {
display: none; display: none;
} }
.pf-c-page { .pf-c-page {
background-color: var(--pf-c-page--BackgroundColor) !important; background-color: var(--pf-c-page--BackgroundColor) !important;
} }
:host([theme="dark"]) {
/* Global page background colour */ /* Global page background colour */
:host([theme="dark"]) .pf-c-page { .pf-c-page {
--pf-c-page--BackgroundColor: var(--ak-dark-background); --pf-c-page--BackgroundColor: var(--ak-dark-background);
} }
ak-enterprise-status, }
ak-version-banner {
ak-page-navbar {
grid-area: header; grid-area: header;
} }
ak-admin-sidebar {
.ak-sidebar {
grid-area: nav; grid-area: nav;
} }
.pf-c-drawer__panel { .pf-c-drawer__panel {
z-index: var(--pf-global--ZIndex--xl); z-index: var(--pf-global--ZIndex--xl);
} }
@ -91,10 +126,23 @@ export class AdminInterface extends AuthenticatedInterface {
]; ];
} }
//#endregion
//#region Lifecycle
constructor() { constructor() {
super(); super();
this.ws = new WebsocketClient(); this.ws = new WebsocketClient();
this.#sidebarMatcher = window.matchMedia("(min-width: 1200px)");
this.sidebarOpen = this.#sidebarMatcher.matches;
}
public connectedCallback() {
super.connectedCallback();
window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar);
window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => { window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => {
this.notificationDrawerOpen = !this.notificationDrawerOpen; this.notificationDrawerOpen = !this.notificationDrawerOpen;
updateURLParams({ updateURLParams({
@ -108,6 +156,14 @@ export class AdminInterface extends AuthenticatedInterface {
apiDrawerOpen: this.apiDrawerOpen, apiDrawerOpen: this.apiDrawerOpen,
}); });
}); });
this.#sidebarMatcher.addEventListener("change", this.#sidebarListener);
}
public disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar);
this.#sidebarMatcher.removeEventListener("change", this.#sidebarListener);
} }
async firstUpdated(): Promise<void> { async firstUpdated(): Promise<void> {
@ -118,6 +174,7 @@ export class AdminInterface extends AuthenticatedInterface {
this.user.user.isSuperuser || this.user.user.isSuperuser ||
// TODO: somehow add `access_admin_interface` to the API schema // TODO: somehow add `access_admin_interface` to the API schema
this.user.user.systemPermissions.includes("access_admin_interface"); this.user.user.systemPermissions.includes("access_admin_interface");
if (!canAccessAdmin && this.user.user.pk > 0) { if (!canAccessAdmin && this.user.user.pk > 0) {
window.location.assign("/if/user/"); window.location.assign("/if/user/");
} }
@ -125,10 +182,14 @@ export class AdminInterface extends AuthenticatedInterface {
render(): TemplateResult { render(): TemplateResult {
const sidebarClasses = { const sidebarClasses = {
"pf-c-page__sidebar": true,
"pf-m-light": this.activeTheme === UiThemeEnum.Light, "pf-m-light": this.activeTheme === UiThemeEnum.Light,
"pf-m-expanded": this.sidebarOpen,
"pf-m-collapsed": !this.sidebarOpen,
}; };
const drawerOpen = this.notificationDrawerOpen || this.apiDrawerOpen; const drawerOpen = this.notificationDrawerOpen || this.apiDrawerOpen;
const drawerClasses = { const drawerClasses = {
"pf-m-expanded": drawerOpen, "pf-m-expanded": drawerOpen,
"pf-m-collapsed": !drawerOpen, "pf-m-collapsed": !drawerOpen,
@ -136,11 +197,18 @@ export class AdminInterface extends AuthenticatedInterface {
return html` <ak-locale-context> return html` <ak-locale-context>
<div class="pf-c-page"> <div class="pf-c-page">
<ak-enterprise-status interface="admin"></ak-enterprise-status> <ak-page-navbar>
<ak-version-banner></ak-version-banner> <ak-version-banner></ak-version-banner>
<ak-admin-sidebar <ak-enterprise-status interface="admin"></ak-enterprise-status>
class="pf-c-page__sidebar ${classMap(sidebarClasses)}" </ak-page-navbar>
></ak-admin-sidebar>
<ak-sidebar class="${classMap(sidebarClasses)}">
${renderSidebarItems(AdminSidebarEntries)}
${this.licenseSummary?.status !== LicenseSummaryStatusEnum.Unlicensed
? renderSidebarItems(AdminSidebarEnterpriseEntries)
: nothing}
</ak-sidebar>
<div class="pf-c-page__drawer"> <div class="pf-c-page__drawer">
<div class="pf-c-drawer ${classMap(drawerClasses)}"> <div class="pf-c-drawer ${classMap(drawerClasses)}">
<div class="pf-c-drawer__main"> <div class="pf-c-drawer__main">

View File

@ -1,132 +1,77 @@
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import { WithVersion } from "@goauthentik/elements/Interface/versionProvider";
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route"; import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route";
import { getRootStyle } from "@goauthentik/elements/utils/getRootStyle";
import { spread } from "@open-wc/lit-helpers"; import { spread } from "@open-wc/lit-helpers";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { TemplateResult, html, nothing } from "lit"; import { TemplateResult, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { repeat } from "lit/directives/repeat.js";
import { map } from "lit/directives/map.js";
import { UiThemeEnum } from "@goauthentik/api"; // The second attribute type is of string[] to help with the 'activeWhen' control, which was
import type { SessionUser, UserSelf } from "@goauthentik/api"; // commonplace and singular enough to merit its own handler.
type SidebarEntry = [
@customElement("ak-admin-sidebar")
export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement)) {
@property({ type: Boolean, reflect: true })
open = true;
@state()
impersonation: UserSelf["username"] | null = null;
constructor() {
super();
me().then((user: SessionUser) => {
this.impersonation = user.original ? user.user.username : null;
});
this.toggleOpen = this.toggleOpen.bind(this);
this.checkWidth = this.checkWidth.bind(this);
}
// This has to be a bound method so the event listener can be removed on disconnection as
// needed.
toggleOpen() {
this.open = !this.open;
}
checkWidth() {
// This works just fine, but it assumes that the `--ak-sidebar--minimum-auto-width` is in
// REMs. If that changes, this code will have to be adjusted as well.
const minWidth =
parseFloat(getRootStyle("--ak-sidebar--minimum-auto-width")) *
parseFloat(getRootStyle("font-size"));
this.open = window.innerWidth >= minWidth;
}
connectedCallback() {
super.connectedCallback();
window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.toggleOpen);
window.addEventListener("resize", this.checkWidth);
// After connecting to the DOM, we can now perform this check to see if the sidebar should
// be open by default.
this.checkWidth();
}
// The symmetry (☟, ☝) here is critical in that you want to start adding these handlers after
// connection, and removing them before disconnection.
disconnectedCallback() {
window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.toggleOpen);
window.removeEventListener("resize", this.checkWidth);
super.disconnectedCallback();
}
render() {
return html`
<ak-sidebar
class="pf-c-page__sidebar ${this.open ? "pf-m-expanded" : "pf-m-collapsed"} ${this
.activeTheme === UiThemeEnum.Light
? "pf-m-light"
: ""}"
>
${this.renderSidebarItems()}
</ak-sidebar>
`;
}
updated() {
// This is permissible as`:host.classList` is not one of the properties Lit uses as a
// scheduling trigger. This sort of shenanigans can trigger an loop, in that it will trigger
// a browser reflow, which may trigger some other styling the application is monitoring,
// triggering a re-render which triggers a browser reflow, ad infinitum. But we've been
// living with that since jQuery, and it's both well-known and fortunately rare.
// eslint-disable-next-line wc/no-self-class
this.classList.remove("pf-m-expanded", "pf-m-collapsed");
// eslint-disable-next-line wc/no-self-class
this.classList.add(this.open ? "pf-m-expanded" : "pf-m-collapsed");
}
renderSidebarItems(): TemplateResult {
// The second attribute type is of string[] to help with the 'activeWhen' control, which was
// commonplace and singular enough to merit its own handler.
type SidebarEntry = [
path: string | null, path: string | null,
label: string, label: string,
attributes?: Record<string, any> | string[] | null, // eslint-disable-line attributes?: Record<string, any> | string[] | null, // eslint-disable-line
children?: SidebarEntry[], children?: SidebarEntry[],
]; ];
// prettier-ignore /**
const sidebarContent: SidebarEntry[] = [ * Recursively renders a sidebar entry.
*/
export function renderSidebarItem([
path,
label,
attributes,
children,
]: SidebarEntry): TemplateResult {
const properties = Array.isArray(attributes)
? { ".activeWhen": attributes }
: (attributes ?? {});
if (path) {
properties.path = path;
}
return html`<ak-sidebar-item ${spread(properties)}>
${label ? html`<span slot="label">${label}</span>` : nothing}
${children ? renderSidebarItems(children) : nothing}
</ak-sidebar-item>`;
}
/**
* Recursively renders a collection of sidebar entries.
*/
export function renderSidebarItems(entries: readonly SidebarEntry[]) {
return repeat(entries, ([path, label]) => path || label, renderSidebarItem);
}
// prettier-ignore
export const AdminSidebarEntries: readonly SidebarEntry[] = [
[null, msg("Dashboards"), { "?expanded": true }, [ [null, msg("Dashboards"), { "?expanded": true }, [
["/administration/overview", msg("Overview")], ["/administration/overview", msg("Overview")],
["/administration/dashboard/users", msg("User Statistics")], ["/administration/dashboard/users", msg("User Statistics")],
["/administration/system-tasks", msg("System Tasks")]]], ["/administration/system-tasks", msg("System Tasks")]]
],
[null, msg("Applications"), null, [ [null, msg("Applications"), null, [
["/core/applications", msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]], ["/core/applications", msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]],
["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`]], ["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`]],
["/outpost/outposts", msg("Outposts")]]], ["/outpost/outposts", msg("Outposts")]]
],
[null, msg("Events"), null, [ [null, msg("Events"), null, [
["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]], ["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]],
["/events/rules", msg("Notification Rules")], ["/events/rules", msg("Notification Rules")],
["/events/transports", msg("Notification Transports")]]], ["/events/transports", msg("Notification Transports")]]
],
[null, msg("Customization"), null, [ [null, msg("Customization"), null, [
["/policy/policies", msg("Policies")], ["/policy/policies", msg("Policies")],
["/core/property-mappings", msg("Property Mappings")], ["/core/property-mappings", msg("Property Mappings")],
["/blueprints/instances", msg("Blueprints")], ["/blueprints/instances", msg("Blueprints")],
["/policy/reputation", msg("Reputation scores")]]], ["/policy/reputation", msg("Reputation scores")]]
],
[null, msg("Flows and Stages"), null, [ [null, msg("Flows and Stages"), null, [
["/flow/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`]], ["/flow/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`]],
["/flow/stages", msg("Stages")], ["/flow/stages", msg("Stages")],
["/flow/stages/prompts", msg("Prompts")]]], ["/flow/stages/prompts", msg("Prompts")]]
],
[null, msg("Directory"), null, [ [null, msg("Directory"), null, [
["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]], ["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]],
["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]], ["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]],
@ -134,53 +79,19 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement
["/identity/initial-permissions", msg("Initial Permissions"), [`^/identity/initial-permissions/(?<id>${ID_REGEX})$`]], ["/identity/initial-permissions", msg("Initial Permissions"), [`^/identity/initial-permissions/(?<id>${ID_REGEX})$`]],
["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]], ["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]],
["/core/tokens", msg("Tokens and App passwords")], ["/core/tokens", msg("Tokens and App passwords")],
["/flow/stages/invitations", msg("Invitations")]]], ["/flow/stages/invitations", msg("Invitations")]]
],
[null, msg("System"), null, [ [null, msg("System"), null, [
["/core/brands", msg("Brands")], ["/core/brands", msg("Brands")],
["/crypto/certificates", msg("Certificates")], ["/crypto/certificates", msg("Certificates")],
["/outpost/integrations", msg("Outpost Integrations")], ["/outpost/integrations", msg("Outpost Integrations")],
["/admin/settings", msg("Settings")]]], ["/admin/settings", msg("Settings")]]
]; ],
];
// Typescript requires the type here to correctly type the recursive path // prettier-ignore
type SidebarRenderer = (_: SidebarEntry) => TemplateResult; export const AdminSidebarEnterpriseEntries: readonly SidebarEntry[] = [
[null, msg("Enterprise"), null, [
const renderOneSidebarItem: SidebarRenderer = ([path, label, attributes, children]) => { ["/enterprise/licenses", msg("Licenses"), null]
const properties = Array.isArray(attributes) ],
? { ".activeWhen": attributes } ]]
: (attributes ?? {});
if (path) {
properties.path = path;
}
return html`<ak-sidebar-item ${spread(properties)}>
${label ? html`<span slot="label">${label}</span>` : nothing}
${map(children, renderOneSidebarItem)}
</ak-sidebar-item>`;
};
// prettier-ignore
return html`
${map(sidebarContent, renderOneSidebarItem)}
${this.renderEnterpriseMenu()}
`;
}
renderEnterpriseMenu() {
return this.can(CapabilitiesEnum.IsEnterprise)
? html`
<ak-sidebar-item>
<span slot="label">${msg("Enterprise")}</span>
<ak-sidebar-item path="/enterprise/licenses">
<span slot="label">${msg("Licenses")}</span>
</ak-sidebar-item>
</ak-sidebar-item>
`
: nothing;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-admin-sidebar": AkAdminSidebar;
}
}

View File

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

View File

@ -83,13 +83,10 @@ export class AdminSettingsPage extends AKElement {
} }
render() { render() {
if (!this.settings) { if (!this.settings) return nothing;
return nothing;
}
return html` return html`
<ak-page-header icon="fa fa-cog" header="" description=""> <ak-page-header icon="fa fa-cog" header="${msg("System settings")}"> </ak-page-header>
<span slot="header"> ${msg("System settings")} </span>
</ak-page-header>
<section class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter"> <section class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter">
<div class="pf-c-card"> <div class="pf-c-card">
<div class="pf-c-card__body"> <div class="pf-c-card__body">

View File

@ -89,12 +89,7 @@ export class RoleObjectPermissionForm extends ModelForm<RoleAssignData, number>
> >
</ak-search-select> </ak-search-select>
</ak-form-element-horizontal> </ak-form-element-horizontal>
${this.modelPermissions?.results ${this.modelPermissions?.results.map((perm) => {
.filter((perm) => {
const [_app, model] = this.model?.split(".") || "";
return perm.codename !== `add_${model}`;
})
.map((perm) => {
return html` <ak-form-element-horizontal name="permissions.${perm.codename}"> return html` <ak-form-element-horizontal name="permissions.${perm.codename}">
<label class="pf-c-switch"> <label class="pf-c-switch">
<input class="pf-c-switch__input" type="checkbox" /> <input class="pf-c-switch__input" type="checkbox" />

View File

@ -45,7 +45,7 @@ export class RoleAssignedObjectPermissionTable extends Table<RoleAssignedObjectP
ordering: "codename", ordering: "codename",
}); });
modelPermissions.results = modelPermissions.results.filter((value) => { modelPermissions.results = modelPermissions.results.filter((value) => {
return value.codename !== `add_${this.model?.split(".")[1]}`; return !value.codename.startsWith("add_");
}); });
this.modelPermissions = modelPermissions; this.modelPermissions = modelPermissions;
return perms; return perms;

View File

@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger"; export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress"; export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current"; export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2025.4.0"; export const VERSION = "2025.2.4";
export const TITLE_DEFAULT = "authentik"; export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";"; export const ROUTE_SEPARATOR = ";";

View File

@ -1,26 +1,110 @@
import type { Config as DOMPurifyConfig } from "dompurify"; import type { Config as DOMPurifyConfig } from "dompurify";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { trustedTypes } from "trusted-types";
import { render } from "@lit-labs/ssr"; import { render } from "lit";
import { collectResult } from "@lit-labs/ssr/lib/render-result.js";
import { TemplateResult, html } from "lit";
import { unsafeHTML } from "lit/directives/unsafe-html.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { until } from "lit/directives/until.js";
/**
* Trusted types policy that escapes HTML content in place.
*
* @see {@linkcode SanitizedTrustPolicy} to strip HTML content.
*
* @returns {TrustedHTML} All HTML content, escaped.
*/
export const EscapeTrustPolicy = trustedTypes.createPolicy("authentik-escape", {
createHTML: (untrustedHTML: string) => {
return DOMPurify.sanitize(untrustedHTML, {
RETURN_TRUSTED_TYPE: false,
});
},
});
/**
* Trusted types policy, stripping all HTML content.
*
* @returns {TrustedHTML} Text content only, all HTML tags stripped.
*/
export const SanitizedTrustPolicy = trustedTypes.createPolicy("authentik-sanitize", {
createHTML: (untrustedHTML: string) => {
return DOMPurify.sanitize(untrustedHTML, {
RETURN_TRUSTED_TYPE: false,
ALLOWED_TAGS: ["#text"],
});
},
});
/**
* Trusted types policy, allowing a minimal set of _safe_ HTML tags supplied by
* a trusted source, such as the brand API.
*/
export const BrandedHTMLPolicy = trustedTypes.createPolicy("authentik-restrict", {
createHTML: (untrustedHTML: string) => {
return DOMPurify.sanitize(untrustedHTML, {
RETURN_TRUSTED_TYPE: false,
FORBID_TAGS: [
"script",
"style",
"iframe",
"link",
"object",
"embed",
"applet",
"meta",
"base",
"form",
"input",
"textarea",
"select",
"button",
],
FORBID_ATTR: [
"onerror",
"onclick",
"onload",
"onmouseover",
"onmouseout",
"onmouseup",
"onmousedown",
"onfocus",
"onblur",
"onsubmit",
],
});
},
});
export type AuthentikTrustPolicy =
| typeof EscapeTrustPolicy
| typeof SanitizedTrustPolicy
| typeof BrandedHTMLPolicy;
/**
* Sanitize an untrusted HTML string using a trusted types policy.
*/
export function sanitizeHTML(trustPolicy: AuthentikTrustPolicy, untrustedHTML: string) {
return unsafeHTML(trustPolicy.createHTML(untrustedHTML).toString());
}
/**
* DOMPurify configuration for strict sanitization.
*
* This configuration only allows text nodes and disallows all HTML tags.
*/
export const DOM_PURIFY_STRICT = { export const DOM_PURIFY_STRICT = {
ALLOWED_TAGS: ["#text"], ALLOWED_TAGS: ["#text"],
} as const satisfies DOMPurifyConfig; } as const satisfies DOMPurifyConfig;
export async function renderStatic(input: TemplateResult): Promise<string> { /**
return await collectResult(render(input)); * Render untrusted HTML to a string without escaping it.
} *
* @returns {string} The rendered HTML string.
*/
export function renderStaticHTMLUnsafe(untrustedHTML: unknown): string {
const container = document.createElement("html");
render(untrustedHTML, container);
export function purify(input: TemplateResult): TemplateResult { const result = container.innerHTML;
return html`${until(
(async () => { return result;
const rendered = await renderStatic(input);
const purified = DOMPurify.sanitize(rendered);
return html`${unsafeHTML(purified)}`;
})(),
)}`;
} }

View File

@ -17,6 +17,13 @@
/* Minimum width after which the sidebar becomes automatic */ /* Minimum width after which the sidebar becomes automatic */
--ak-sidebar--minimum-auto-width: 80rem; --ak-sidebar--minimum-auto-width: 80rem;
/**
* The height of the navbar and branded sidebar.
* @todo This shouldn't be necessary. The sidebar can instead use a grid layout
* ensuring they share the same height.
*/
--ak-navbar--height: 7rem;
} }
@supports selector(::-webkit-scrollbar) { @supports selector(::-webkit-scrollbar) {

View File

@ -0,0 +1,220 @@
/**
* @file Stylesheet utilities.
*/
import { CSSResult, CSSResultOrNative, ReactiveElement, css } from "lit";
/**
* Elements containing adoptable stylesheets.
*/
export type StyleSheetParent = Pick<DocumentOrShadowRoot, "adoptedStyleSheets">;
/**
* Type-predicate to determine if a given object has adoptable stylesheets.
*/
export function isAdoptableStyleSheetParent(input: unknown): input is StyleSheetParent {
// Sanity check - Does the input have the right shape?
if (!input || typeof input !== "object") return false;
if (!("adoptedStyleSheets" in input) || !input.adoptedStyleSheets) return false;
if (typeof input.adoptedStyleSheets !== "object") return false;
// We avoid `Array.isArray` because the adopted stylesheets property
// is defined as a proxied array.
// All we care about is that it's shaped like an array.
if (!("length" in input.adoptedStyleSheets)) return false;
if (typeof input.adoptedStyleSheets.length !== "number") return false;
// Finally is the array mutable?
return "push" in input.adoptedStyleSheets;
}
/**
* Assert that the given input can adopt stylesheets.
*/
export function assertAdoptableStyleSheetParent<T>(
input: T,
): asserts input is T & StyleSheetParent {
if (isAdoptableStyleSheetParent(input)) return;
console.debug("Given input missing `adoptedStyleSheets`", input);
throw new TypeError("Assertion failed: `adoptedStyleSheets` missing in given input");
}
export function resolveStyleSheetParent<T extends HTMLElement | DocumentFragment | Document>(
renderRoot: T,
) {
const styleRoot = "ShadyDOM" in window ? document : renderRoot;
assertAdoptableStyleSheetParent(styleRoot);
return styleRoot;
}
export type StyleSheetInit = string | CSSResult | CSSStyleSheet;
/**
* Given a source of CSS, create a `CSSStyleSheet`.
*
* @throw {@linkcode TypeError} if the input cannot be converted to a `CSSStyleSheet`
*
* @remarks
*
* Storybook's `build` does not currently have a coherent way of importing
* CSS-as-text into CSSStyleSheet.
*
* It works well when Storybook is running in `dev`, but in `build` it fails.
* Storied components will have to map their textual CSS imports.
*/
export function createStyleSheet(input: string): CSSResult {
const inputTemplate = [input] as unknown as TemplateStringsArray;
const result = css(inputTemplate, []);
return result;
}
/**
* Given a source of CSS, create a `CSSStyleSheet`.
*
* @see {@linkcode createStyleSheet}
*/
export function normalizeCSSSource(css: string): CSSStyleSheet;
export function normalizeCSSSource(styleSheet: CSSStyleSheet): CSSStyleSheet;
export function normalizeCSSSource(cssResult: CSSResult): CSSResult;
export function normalizeCSSSource(input: StyleSheetInit): CSSResultOrNative;
export function normalizeCSSSource(input: StyleSheetInit): CSSResultOrNative {
if (typeof input === "string") return createStyleSheet(input);
return input;
}
export function createStyleSheetUnsafe(input: StyleSheetInit): CSSStyleSheet {
const result = normalizeCSSSource(input);
if (result instanceof CSSStyleSheet) return result;
if (!result.styleSheet) {
console.debug(
"authentik/common/stylesheets: CSSResult missing styleSheet, returning empty",
{ result, input },
);
throw new TypeError("Expected a CSSStyleSheet");
}
return result.styleSheet;
}
/**
* Append stylesheet(s) to the given roots.
*/
export function appendStyleSheet(
insertions: CSSStyleSheet | Iterable<CSSStyleSheet>,
...styleParents: StyleSheetParent[]
): void {
insertions = Array.isArray(insertions) ? insertions : [insertions];
for (const nextStyleSheet of insertions) {
for (const styleParent of styleParents) {
if (styleParent.adoptedStyleSheets.includes(nextStyleSheet)) return;
styleParent.adoptedStyleSheets = [...styleParent.adoptedStyleSheets, nextStyleSheet];
}
}
}
/**
* Remove a stylesheet from the given roots, matching by referential equality.
*/
export function removeStyleSheet(
currentStyleSheet: CSSStyleSheet,
...styleParents: StyleSheetParent[]
): void {
for (const styleParent of styleParents) {
const nextAdoptedStyleSheets = styleParent.adoptedStyleSheets.filter(
(styleSheet) => styleSheet !== currentStyleSheet,
);
if (nextAdoptedStyleSheets.length === styleParent.adoptedStyleSheets.length) return;
styleParent.adoptedStyleSheets = nextAdoptedStyleSheets;
}
}
/**
* Serialize a stylesheet to a string.
*
* This is useful for debugging or inspecting the contents of a stylesheet.
*/
export function serializeStyleSheet(stylesheet: CSSStyleSheet): string {
return Array.from(stylesheet.cssRules || [], (rule) => rule.cssText || "").join("\n");
}
/**
* Inspect the adopted stylesheets of a given style parent, serializing them to strings.
*/
export function inspectStyleSheets(styleParent: StyleSheetParent): string[] {
return styleParent.adoptedStyleSheets.map((styleSheet) => serializeStyleSheet(styleSheet));
}
interface InspectedStyleSheetEntry {
tagName: string;
element: ReactiveElement;
styles: string[];
children?: InspectedStyleSheetEntry[];
}
/**
* Recursively inspect the adopted stylesheets of a given style parent, serializing them to strings.
*/
export function inspectStyleSheetTree(element: ReactiveElement): InspectedStyleSheetEntry {
const styleParent = resolveStyleSheetParent(element.renderRoot);
const styles = inspectStyleSheets(styleParent);
const tagName = element.tagName.toLowerCase();
const treewalker = document.createTreeWalker(element.renderRoot, NodeFilter.SHOW_ELEMENT, {
acceptNode(node) {
if (node instanceof ReactiveElement) {
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_SKIP;
},
});
const children: InspectedStyleSheetEntry[] = [];
let currentNode: Node | null = treewalker.nextNode();
while (currentNode) {
const childElement = currentNode as ReactiveElement;
if (!isAdoptableStyleSheetParent(childElement.renderRoot)) {
currentNode = treewalker.nextNode();
continue;
}
const childStyles = inspectStyleSheets(childElement.renderRoot);
children.push({
tagName: childElement.tagName.toLowerCase(),
element: childElement,
styles: childStyles,
});
currentNode = treewalker.nextNode();
}
return {
tagName,
element,
styles,
children,
};
}
if (process.env.NODE_ENV === "development") {
Object.assign(window, {
inspectStyleSheetTree,
serializeStyleSheet,
inspectStyleSheets,
});
}

200
web/src/common/theme.ts Normal file
View File

@ -0,0 +1,200 @@
/**
* @file Theme utilities.
*/
import { UIConfig } from "@goauthentik/common/ui/config";
import { Config, CurrentBrand, UiThemeEnum } from "@goauthentik/api";
//#region Scheme Types
/**
* Valid CSS color scheme values.
*
* @link {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme | MDN}
*
* @category CSS
*/
export type CSSColorSchemeValue = "dark" | "light" | "auto";
/**
* A CSS color scheme value that can be preferred by the user, i.e. not `"auto"`.
*
* @category CSS
*/
export type ResolvedCSSColorSchemeValue = Exclude<CSSColorSchemeValue, "auto">;
//#endregion
//#region UI Theme Types
/**
* A UI color scheme value that can be preferred by the user.
*
* i.e. not an lack of preference or unknown value.
*
* @category CSS
*/
export type ResolvedUITheme = typeof UiThemeEnum.Light | typeof UiThemeEnum.Dark;
/**
* A mapping of theme values to their respective inversion.
*
* @category CSS
*/
export const UIThemeInversion = {
dark: "light",
light: "dark",
} as const satisfies Record<ResolvedUITheme, ResolvedUITheme>;
/**
* Either a valid CSS color scheme value, or a theme preference.
*/
export type UIThemeHint = CSSColorSchemeValue | UiThemeEnum;
//#endregion
//#region Scheme Functions
/**
* Creates an event target for the given color scheme.
*
* @param colorScheme The color scheme to target.
* @returns A {@linkcode MediaQueryList} that can be used to listen for changes to the color scheme.
*
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList | MDN}
*
* @category CSS
*/
export function createColorSchemeTarget(colorScheme: ResolvedCSSColorSchemeValue): MediaQueryList {
return window.matchMedia(`(prefers-color-scheme: ${colorScheme})`);
}
/**
* Formats the given input into a valid CSS color scheme value.
*
* If the input is not provided, it defaults to "auto".
*
* @category CSS
*/
export function formatColorScheme(theme: ResolvedUITheme): ResolvedCSSColorSchemeValue;
export function formatColorScheme(
colorScheme: ResolvedCSSColorSchemeValue,
): ResolvedCSSColorSchemeValue;
export function formatColorScheme(hint?: UIThemeHint): CSSColorSchemeValue;
export function formatColorScheme(hint?: UIThemeHint): CSSColorSchemeValue {
if (!hint) return "auto";
switch (hint) {
case "dark":
case UiThemeEnum.Dark:
return "dark";
case "light":
case UiThemeEnum.Light:
return "light";
case "auto":
case UiThemeEnum.Automatic:
return "auto";
default:
console.warn(`Unknown color scheme hint: ${hint}. Defaulting to "auto".`);
return "auto";
}
}
//#endregion
//#region Theme Functions
/**
* Resolve the current UI theme based on the user's preference or the provided color scheme.
*
* @param hint The color scheme hint to use.
*
* @category CSS
*/
export function resolveUITheme(
hint?: UIThemeHint,
defaultUITheme: ResolvedUITheme = UiThemeEnum.Light,
): ResolvedUITheme {
const colorScheme = formatColorScheme(hint);
if (colorScheme !== "auto") return colorScheme;
// Given that we don't know the user's preference,
// we can determine the theme based on whether the default theme is
// currently being overridden.
const colorSchemeInversion = formatColorScheme(UIThemeInversion[defaultUITheme]);
const mediaQueryList = createColorSchemeTarget(colorSchemeInversion);
return mediaQueryList.matches ? colorSchemeInversion : defaultUITheme;
}
/**
* Effect listener invoked when the color scheme changes.
*/
export type UIThemeListener = (currentUITheme: ResolvedUITheme) => void;
/**
* Create an effect that runs
*
* @returns A cleanup function that removes the effect.
*/
export function createUIThemeEffect(
effect: UIThemeListener,
listenerOptions?: AddEventListenerOptions,
): () => void {
const colorSchemeTarget = resolveUITheme();
const invertedColorSchemeTarget = UIThemeInversion[colorSchemeTarget];
let previousUITheme: ResolvedUITheme | undefined;
// First, wrap the effect to ensure we can abort it.
const changeListener = (event: MediaQueryListEvent) => {
if (listenerOptions?.signal?.aborted) return;
const currentUITheme = event.matches ? colorSchemeTarget : invertedColorSchemeTarget;
if (previousUITheme === currentUITheme) return;
previousUITheme = currentUITheme;
effect(currentUITheme);
};
const mediaQueryList = createColorSchemeTarget(colorSchemeTarget);
// Trigger the effect immediately.
effect(colorSchemeTarget);
// Listen for changes to the color scheme...
mediaQueryList.addEventListener("change", changeListener, listenerOptions);
// Finally, allow the caller to remove the effect.
const cleanup = () => {
mediaQueryList.removeEventListener("change", changeListener);
};
return cleanup;
}
//#endregion
//#region Theme Element
/**
* An element that can be themed.
*/
export interface ThemedElement extends HTMLElement {
brand?: CurrentBrand;
uiConfig?: UIConfig;
config?: Config;
activeTheme: ResolvedUITheme;
}
export function rootInterface<T extends ThemedElement = ThemedElement>(): T | null {
const element = document.body.querySelector<T>("[data-ak-interface-root]");
return element;
}
//#endregion

View File

@ -95,7 +95,7 @@ export class NavigationButtons extends AKElement {
); );
}; };
return html`<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-lg"> return html`<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-xl">
<button class="pf-c-button pf-m-plain" type="button" @click=${onClick}> <button class="pf-c-button pf-m-plain" type="button" @click=${onClick}>
<pf-tooltip position="top" content=${msg("Open API drawer")}> <pf-tooltip position="top" content=${msg("Open API drawer")}>
<i class="fas fa-code" aria-hidden="true"></i> <i class="fas fa-code" aria-hidden="true"></i>
@ -116,7 +116,7 @@ export class NavigationButtons extends AKElement {
); );
}; };
return html`<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-lg"> return html`<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-xl">
<button <button
class="pf-c-button pf-m-plain" class="pf-c-button pf-m-plain"
type="button" type="button"
@ -156,9 +156,7 @@ export class NavigationButtons extends AKElement {
} }
renderImpersonation() { renderImpersonation() {
if (!this.me?.original) { if (!this.me?.original) return nothing;
return nothing;
}
const onClick = async () => { const onClick = async () => {
await new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve(); await new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve();
@ -175,6 +173,14 @@ export class NavigationButtons extends AKElement {
</div>`; </div>`;
} }
renderAvatar() {
return html`<img
class="pf-c-page__header-tools-item pf-c-avatar pf-m-hidden pf-m-visible-on-xl"
src=${ifDefined(this.me?.user.avatar)}
alt="${msg("Avatar image")}"
/>`;
}
get userDisplayName() { get userDisplayName() {
return match<UserDisplay | undefined, string | undefined>(this.uiConfig?.navbar.userDisplay) return match<UserDisplay | undefined, string | undefined>(this.uiConfig?.navbar.userDisplay)
.with(UserDisplay.username, () => this.me?.user.username) .with(UserDisplay.username, () => this.me?.user.username)
@ -206,17 +212,13 @@ export class NavigationButtons extends AKElement {
</div> </div>
${this.renderImpersonation()} ${this.renderImpersonation()}
${this.userDisplayName != "" ${this.userDisplayName != ""
? html`<div class="pf-c-page__header-tools-group"> ? html`<div class="pf-c-page__header-tools-group pf-m-hidden">
<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-md"> <div class="pf-c-page__header-tools-item pf-m-visible-on-2xl">
${this.userDisplayName} ${this.userDisplayName}
</div> </div>
</div>` </div>`
: nothing} : nothing}
<img ${this.renderAvatar()}
class="pf-c-avatar"
src=${ifDefined(this.me?.user.avatar)}
alt="${msg("Avatar image")}"
/>
</div>`; </div>`;
} }
} }

View File

@ -1,165 +1,127 @@
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { UIConfig } from "@goauthentik/common/ui/config"; import {
import { adaptCSS } from "@goauthentik/common/utils"; StyleSheetInit,
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet"; StyleSheetParent,
appendStyleSheet,
createStyleSheetUnsafe,
removeStyleSheet,
resolveStyleSheetParent,
} from "@goauthentik/common/stylesheets";
import { ResolvedUITheme, createUIThemeEffect, resolveUITheme } from "@goauthentik/common/theme";
import { type ThemedElement } from "@goauthentik/common/theme";
import { localized } from "@lit/localize"; import { localized } from "@lit/localize";
import { LitElement, ReactiveElement } from "lit"; import { CSSResultGroup, CSSResultOrNative, LitElement } from "lit";
import { property } from "lit/decorators.js";
import AKGlobal from "@goauthentik/common/styles/authentik.css"; import AKGlobal from "@goauthentik/common/styles/authentik.css";
import OneDark from "@goauthentik/common/styles/one-dark.css"; import OneDark from "@goauthentik/common/styles/one-dark.css";
import ThemeDark from "@goauthentik/common/styles/theme-dark.css"; import ThemeDark from "@goauthentik/common/styles/theme-dark.css";
import { Config, CurrentBrand, UiThemeEnum } from "@goauthentik/api"; import { CurrentBrand, UiThemeEnum } from "@goauthentik/api";
type AkInterface = HTMLElement & { // Re-export the theme helpers
getTheme: () => Promise<UiThemeEnum>; export { rootInterface } from "@goauthentik/common/theme";
brand?: CurrentBrand;
uiConfig?: UIConfig;
config?: Config;
get activeTheme(): UiThemeEnum | undefined;
};
export const rootInterface = <T extends AkInterface>(): T | undefined => export interface AKElementInit {
(document.body.querySelector("[data-ak-interface-root]") as T) ?? undefined; brand?: Partial<CurrentBrand>;
styleParents?: StyleSheetParent[];
export const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)"; }
// Ensure themes are converted to a static instance of CSS Stylesheet, otherwise the
// when changing themes we might not remove the correct css stylesheet instance.
const _darkTheme = ensureCSSStyleSheet(ThemeDark);
@localized() @localized()
export class AKElement extends LitElement { export class AKElement extends LitElement implements ThemedElement {
_mediaMatcher?: MediaQueryList;
_mediaMatcherHandler?: (ev?: MediaQueryListEvent) => void;
_activeTheme?: UiThemeEnum;
get activeTheme(): UiThemeEnum | undefined {
return this._activeTheme;
}
constructor() {
super();
}
setInitialStyles(root: DocumentOrShadowRoot) {
const styleRoot: DocumentOrShadowRoot = (
"ShadyDOM" in window ? document : root
) as DocumentOrShadowRoot;
styleRoot.adoptedStyleSheets = adaptCSS([
...styleRoot.adoptedStyleSheets,
ensureCSSStyleSheet(AKGlobal),
ensureCSSStyleSheet(OneDark),
]);
this._initTheme(styleRoot);
this._initCustomCSS(styleRoot);
}
protected createRenderRoot() {
this.fixElementStyles();
const root = super.createRenderRoot();
this.setInitialStyles(root as unknown as DocumentOrShadowRoot);
return root;
}
async getTheme(): Promise<UiThemeEnum> {
return rootInterface()?.getTheme() || UiThemeEnum.Automatic;
}
fixElementStyles() {
// Ensure all style sheets being passed are really style sheets.
(this.constructor as typeof ReactiveElement).elementStyles = (
this.constructor as typeof ReactiveElement
).elementStyles.map(ensureCSSStyleSheet);
}
async _initTheme(root: DocumentOrShadowRoot): Promise<void> {
// Early activate theme based on media query to prevent light flash
// when dark is preferred
this._applyTheme(root, globalAK().brand.uiTheme);
this._applyTheme(root, await this.getTheme());
}
async _initCustomCSS(root: DocumentOrShadowRoot): Promise<void> {
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 {
if (!theme) {
theme = UiThemeEnum.Automatic;
}
if (theme === UiThemeEnum.Automatic) {
// Create a media matcher to automatically switch the theme depending on
// prefers-color-scheme
if (!this._mediaMatcher) {
this._mediaMatcher = window.matchMedia(QUERY_MEDIA_COLOR_LIGHT);
this._mediaMatcherHandler = (ev?: MediaQueryListEvent) => {
const theme =
ev?.matches || this._mediaMatcher?.matches
? UiThemeEnum.Light
: UiThemeEnum.Dark;
this._activateTheme(theme, root);
};
this._mediaMatcherHandler(undefined);
this._mediaMatcher.addEventListener("change", this._mediaMatcherHandler);
}
return;
} else if (this._mediaMatcher && this._mediaMatcherHandler) {
// Theme isn't automatic and we have a matcher configured, remove the matcher
// to prevent changes
this._mediaMatcher.removeEventListener("change", this._mediaMatcherHandler);
this._mediaMatcher = undefined;
}
this._activateTheme(theme, root);
}
static themeToStylesheet(theme?: UiThemeEnum): CSSStyleSheet | undefined {
if (theme === UiThemeEnum.Dark) {
return _darkTheme;
}
return undefined;
}
/** /**
* Directly activate a given theme, accepts multiple document/ShadowDOMs to apply the stylesheet * The resolved theme of the current element.
* to. The stylesheets are applied to each DOM in order. Does nothing if the given theme is already active. *
* @remarks
*
* Unlike the browser's current color scheme, this is a value that can be
* resolved to a specific theme, i.e. dark or light.
*/ */
_activateTheme(theme: UiThemeEnum, ...roots: DocumentOrShadowRoot[]) { @property({
if (theme === this._activeTheme) { attribute: "theme",
return; type: String,
reflect: true,
})
public activeTheme: ResolvedUITheme;
protected static finalizeStyles(styles?: CSSResultGroup): CSSResultOrNative[] {
// Ensure all style sheets being passed are really style sheets.
const baseStyles: StyleSheetInit[] = [AKGlobal, OneDark];
if (!styles) return baseStyles.map(createStyleSheetUnsafe);
if (Array.isArray(styles)) {
return [
//---
...(styles as unknown as CSSResultOrNative[]),
...baseStyles,
].flatMap(createStyleSheetUnsafe);
} }
// Make sure we only get to this callback once we've picked a concise theme choice return [styles, ...baseStyles].map(createStyleSheetUnsafe);
this.dispatchEvent( }
new CustomEvent(EVENT_THEME_CHANGE, {
bubbles: true, constructor(init?: AKElementInit) {
composed: true, super();
detail: theme,
}), const config = globalAK();
const { brand = config.brand, styleParents = [] } = init || {};
this.activeTheme = resolveUITheme(brand?.uiTheme);
this.#styleParents = styleParents;
this.#customCSSStyleSheet = brand?.brandingCustomCss
? createStyleSheetUnsafe(brand.brandingCustomCss)
: null;
}
#styleParents: StyleSheetParent[] = [];
#customCSSStyleSheet: CSSStyleSheet | null;
#darkThemeStyleSheet: CSSStyleSheet | null = null;
#themeAbortController: AbortController | null = null;
public disconnectedCallback(): void {
super.disconnectedCallback();
this.#themeAbortController?.abort();
}
protected createRenderRoot(): HTMLElement | DocumentFragment {
const renderRoot = super.createRenderRoot();
const styleRoot = resolveStyleSheetParent(renderRoot);
const styleParents = Array.from(
new Set<StyleSheetParent>([styleRoot, ...this.#styleParents]),
); );
this.setAttribute("theme", theme);
const stylesheet = AKElement.themeToStylesheet(theme); if (this.#customCSSStyleSheet) {
const oldStylesheet = AKElement.themeToStylesheet(this._activeTheme); console.debug(`authentik/element[${this.tagName.toLowerCase()}]: Adding custom CSS`);
roots.forEach((root) => {
if (stylesheet) { styleRoot.adoptedStyleSheets = [
root.adoptedStyleSheets = [ ...styleRoot.adoptedStyleSheets,
...root.adoptedStyleSheets, this.#customCSSStyleSheet,
ensureCSSStyleSheet(stylesheet),
]; ];
} }
if (oldStylesheet) {
root.adoptedStyleSheets = root.adoptedStyleSheets.filter( this.#themeAbortController = new AbortController();
(v) => v !== oldStylesheet,
); createUIThemeEffect(
(currentUITheme) => {
if (currentUITheme === UiThemeEnum.Dark) {
this.#darkThemeStyleSheet ||= createStyleSheetUnsafe(ThemeDark);
appendStyleSheet(this.#darkThemeStyleSheet, ...styleParents);
} else if (this.#darkThemeStyleSheet) {
removeStyleSheet(this.#darkThemeStyleSheet, ...styleParents);
this.#darkThemeStyleSheet = null;
} }
}); this.activeTheme = currentUITheme;
this._activeTheme = theme; },
this.requestUpdate(); {
signal: this.#themeAbortController.signal,
},
);
return renderRoot;
} }
} }

View File

@ -1,5 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { ThemedElement } from "@goauthentik/common/theme";
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts"; import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js"; import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
@ -9,14 +10,12 @@ import type { ReactiveController } from "lit";
import type { CurrentBrand } from "@goauthentik/api"; import type { CurrentBrand } from "@goauthentik/api";
import { CoreApi } from "@goauthentik/api"; import { CoreApi } from "@goauthentik/api";
import type { AkInterface } from "./Interface";
export class BrandContextController implements ReactiveController { export class BrandContextController implements ReactiveController {
host!: ReactiveElementHost<AkInterface>; host!: ReactiveElementHost<ThemedElement>;
context!: ContextProvider<{ __context__: CurrentBrand | undefined }>; context!: ContextProvider<{ __context__: CurrentBrand | undefined }>;
constructor(host: ReactiveElementHost<AkInterface>) { constructor(host: ReactiveElementHost<ThemedElement>) {
this.host = host; this.host = host;
this.context = new ContextProvider(this.host, { this.context = new ContextProvider(this.host, {
context: authentikBrandContext, context: authentikBrandContext,

View File

@ -1,6 +1,7 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { ThemedElement } from "@goauthentik/common/theme";
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts"; import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js"; import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
@ -10,14 +11,12 @@ import type { ReactiveController } from "lit";
import type { Config } from "@goauthentik/api"; import type { Config } from "@goauthentik/api";
import { RootApi } from "@goauthentik/api"; import { RootApi } from "@goauthentik/api";
import type { AkInterface } from "./Interface";
export class ConfigContextController implements ReactiveController { export class ConfigContextController implements ReactiveController {
host!: ReactiveElementHost<AkInterface>; host!: ReactiveElementHost<ThemedElement>;
context!: ContextProvider<{ __context__: Config | undefined }>; context!: ContextProvider<{ __context__: Config | undefined }>;
constructor(host: ReactiveElementHost<AkInterface>) { constructor(host: ReactiveElementHost<ThemedElement>) {
this.host = host; this.host = host;
this.context = new ContextProvider(this.host, { this.context = new ContextProvider(this.host, {
context: authentikConfigContext, context: authentikConfigContext,

View File

@ -1,107 +1,85 @@
import { UIConfig, uiConfig } from "@goauthentik/common/ui/config"; import {
appendStyleSheet,
createStyleSheetUnsafe,
resolveStyleSheetParent,
} from "@goauthentik/common/stylesheets";
import { ThemedElement } from "@goauthentik/common/theme";
import { UIConfig } from "@goauthentik/common/ui/config";
import { AKElement, AKElementInit } from "@goauthentik/elements/Base";
import { VersionContextController } from "@goauthentik/elements/Interface/VersionContextController"; import { VersionContextController } from "@goauthentik/elements/Interface/VersionContextController";
import { ModalOrchestrationController } from "@goauthentik/elements/controllers/ModalOrchestrationController.js"; import { ModalOrchestrationController } from "@goauthentik/elements/controllers/ModalOrchestrationController.js";
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
import { state } from "lit/decorators.js"; import { state } from "lit/decorators.js";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import type { Config, CurrentBrand, LicenseSummary, Version } from "@goauthentik/api"; import type { Config, CurrentBrand, LicenseSummary, Version } from "@goauthentik/api";
import { UiThemeEnum } from "@goauthentik/api";
import { AKElement, rootInterface } from "../Base";
import { BrandContextController } from "./BrandContextController"; import { BrandContextController } from "./BrandContextController";
import { ConfigContextController } from "./ConfigContextController"; import { ConfigContextController } from "./ConfigContextController";
import { EnterpriseContextController } from "./EnterpriseContextController"; import { EnterpriseContextController } from "./EnterpriseContextController";
export type AkInterface = HTMLElement & {
getTheme: () => Promise<UiThemeEnum>;
brand?: CurrentBrand;
uiConfig?: UIConfig;
config?: Config;
};
const brandContext = Symbol("brandContext"); const brandContext = Symbol("brandContext");
const configContext = Symbol("configContext"); const configContext = Symbol("configContext");
const modalController = Symbol("modalController"); const modalController = Symbol("modalController");
const versionContext = Symbol("versionContext"); const versionContext = Symbol("versionContext");
export class Interface extends AKElement implements AkInterface { export abstract class Interface extends AKElement implements ThemedElement {
[brandContext]!: BrandContextController; protected static readonly PFBaseStyleSheet = createStyleSheetUnsafe(PFBase);
[configContext]!: ConfigContextController; [brandContext]: BrandContextController;
[modalController]!: ModalOrchestrationController; [configContext]: ConfigContextController;
[modalController]: ModalOrchestrationController;
@state() @state()
uiConfig?: UIConfig; public config?: Config;
@state() @state()
config?: Config; public brand?: CurrentBrand;
@state() constructor({ styleParents = [], ...init }: AKElementInit = {}) {
brand?: CurrentBrand; const styleParent = resolveStyleSheetParent(document);
constructor() { super({
super(); ...init,
document.adoptedStyleSheets = [...document.adoptedStyleSheets, ensureCSSStyleSheet(PFBase)]; styleParents: [styleParent, ...styleParents],
this._initContexts(); });
this.dataset.akInterfaceRoot = "true";
} this.dataset.akInterfaceRoot = this.tagName.toLowerCase();
appendStyleSheet(Interface.PFBaseStyleSheet, styleParent);
_initContexts() {
this[brandContext] = new BrandContextController(this); this[brandContext] = new BrandContextController(this);
this[configContext] = new ConfigContextController(this); this[configContext] = new ConfigContextController(this);
this[modalController] = new ModalOrchestrationController(this); this[modalController] = new ModalOrchestrationController(this);
} }
_activateTheme(theme: UiThemeEnum, ...roots: DocumentOrShadowRoot[]): void {
if (theme === this._activeTheme) {
return;
}
console.debug(
`authentik/interface[${rootInterface()?.tagName.toLowerCase()}]: Enabling theme ${theme}`,
);
// Special case for root interfaces, as they need to modify the global document CSS too
// Instead of calling ._activateTheme() twice, we insert the root document in the call
// since multiple calls to ._activateTheme() would not do anything after the first call
// as the theme is already enabled.
roots.unshift(document as unknown as DocumentOrShadowRoot);
super._activateTheme(theme, ...roots);
}
async getTheme(): Promise<UiThemeEnum> {
if (!this.uiConfig) {
this.uiConfig = await uiConfig();
}
return this.uiConfig.theme?.base || UiThemeEnum.Automatic;
}
} }
export type AkAuthenticatedInterface = AkInterface & { export interface AkAuthenticatedInterface extends ThemedElement {
licenseSummary?: LicenseSummary; licenseSummary?: LicenseSummary;
version?: Version; version?: Version;
}; }
const enterpriseContext = Symbol("enterpriseContext"); const enterpriseContext = Symbol("enterpriseContext");
export class AuthenticatedInterface extends Interface { export class AuthenticatedInterface extends Interface implements AkAuthenticatedInterface {
[enterpriseContext]!: EnterpriseContextController; [enterpriseContext]!: EnterpriseContextController;
[versionContext]!: VersionContextController; [versionContext]!: VersionContextController;
@state() @state()
licenseSummary?: LicenseSummary; public uiConfig?: UIConfig;
@state() @state()
version?: Version; public licenseSummary?: LicenseSummary;
constructor() { @state()
super(); public version?: Version;
}
constructor(init?: AKElementInit) {
super(init);
_initContexts(): void {
super._initContexts();
this[enterpriseContext] = new EnterpriseContextController(this); this[enterpriseContext] = new EnterpriseContextController(this);
this[versionContext] = new VersionContextController(this); this[versionContext] = new VersionContextController(this);
} }

View File

@ -5,20 +5,23 @@ import {
} from "@goauthentik/common/constants"; } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { currentInterface } from "@goauthentik/common/sentry"; import { currentInterface } from "@goauthentik/common/sentry";
import { UIConfig, UserDisplay, uiConfig } from "@goauthentik/common/ui/config"; import { UIConfig, UserDisplay, getConfigForUser } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import "@goauthentik/components/ak-nav-buttons"; import "@goauthentik/components/ak-nav-buttons";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
import { themeImage } from "@goauthentik/elements/utils/images";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html, nothing } from "lit"; import { CSSResult, LitElement, TemplateResult, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css"; import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css"; import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css"; import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css";
import PFNotificationBadge from "@patternfly/patternfly/components/NotificationBadge/notification-badge.css"; import PFNotificationBadge from "@patternfly/patternfly/components/NotificationBadge/notification-badge.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css"; import PFPage from "@patternfly/patternfly/components/Page/page.css";
@ -26,34 +29,52 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { SessionUser } from "@goauthentik/api"; import { SessionUser } from "@goauthentik/api";
@customElement("ak-page-header") //#region Page Navbar
export class PageHeader extends WithBrandConfig(AKElement) {
@property()
icon?: string;
@property({ type: Boolean }) export interface PageNavbarDetails {
iconImage = false; header?: string;
@property()
header = "";
@property()
description?: string; description?: string;
icon?: string;
iconImage?: boolean;
}
@property({ type: Boolean }) /**
hasIcon = true; * A global navbar component at the top of the page.
*
* Internally, this component listens for the `ak-page-header` event, which is
* dispatched by the `ak-page-header` component.
*/
@customElement("ak-page-navbar")
export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavbarDetails {
//#region Static Properties
@state() private static elementRef: AKPageNavbar | null = null;
me?: SessionUser;
@state() static readonly setNavbarDetails = (detail: Partial<PageNavbarDetails>): void => {
uiConfig!: UIConfig; const { elementRef } = AKPageNavbar;
if (!elementRef) {
console.debug(
`ak-page-header: Could not find ak-page-navbar, skipping event dispatch.`,
);
return;
}
const { header, description, icon, iconImage } = detail;
elementRef.header = header;
elementRef.description = description;
elementRef.icon = icon;
elementRef.iconImage = iconImage || false;
elementRef.hasIcon = !!icon;
};
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
PFBase, PFBase,
PFButton, PFButton,
PFPage, PFPage,
PFDrawer,
PFNotificationBadge, PFNotificationBadge,
PFContent, PFContent,
PFAvatar, PFAvatar,
@ -63,127 +84,313 @@ export class PageHeader extends WithBrandConfig(AKElement) {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: var(--pf-global--ZIndex--lg); z-index: var(--pf-global--ZIndex--lg);
--pf-c-page__header-tools--MarginRight: 0;
--ak-brand-logo-height: var(--pf-global--FontSize--4xl, 2.25rem);
--ak-brand-background-color: var(
--pf-c-page__sidebar--m-light--BackgroundColor
);
} }
.bar {
:host([theme="dark"]) {
--ak-brand-background-color: var(--pf-c-page__sidebar--BackgroundColor);
--pf-c-page__sidebar--BackgroundColor: var(--ak-dark-background-light);
color: var(--ak-dark-foreground);
}
navbar {
border-bottom: var(--pf-global--BorderWidth--sm); border-bottom: var(--pf-global--BorderWidth--sm);
border-bottom-style: solid; border-bottom-style: solid;
border-bottom-color: var(--pf-global--BorderColor--100); border-bottom-color: var(--pf-global--BorderColor--100);
background-color: var(--pf-c-page--BackgroundColor);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
min-height: 114px; min-height: 6rem;
max-height: 114px;
background-color: var(--pf-c-page--BackgroundColor); display: grid;
row-gap: var(--pf-global--spacer--sm);
column-gap: var(--pf-global--spacer--sm);
grid-template-columns: [brand] auto [toggle] auto [primary] 1fr [secondary] auto;
grid-template-rows: auto auto;
grid-template-areas:
"brand toggle primary secondary"
"brand toggle description secondary";
@media (max-width: 768px) {
row-gap: var(--pf-global--spacer--xs);
align-items: center;
grid-template-areas:
"toggle primary secondary"
"toggle description description";
justify-content: space-between;
width: 100%;
} }
.pf-c-page__main-section.pf-m-light {
background-color: transparent;
} }
.pf-c-page__main-section {
flex-grow: 1; .items {
flex-shrink: 1; display: block;
&.primary {
grid-column: primary;
grid-row: primary / description;
align-content: center;
padding-block: var(--pf-global--spacer--md);
@media (min-width: 426px) {
&.block-sibling {
padding-block-end: 0;
grid-row: primary;
}
}
@media (max-width: 768px) {
padding-block: var(--pf-global--spacer--sm);
}
.accent-icon {
height: 1em;
width: 1em;
@media (max-width: 768px) {
display: none;
}
}
}
&.page-description {
grid-area: description;
padding-block-end: var(--pf-global--spacer--md);
@media (max-width: 425px) {
display: none;
}
@media (min-width: 769px) {
text-wrap: balance;
}
}
&.secondary {
grid-area: secondary;
flex: 0 0 auto;
justify-self: end;
padding-block: var(--pf-global--spacer--sm);
padding-inline-end: var(--pf-global--spacer--sm);
@media (min-width: 769px) {
align-content: center;
padding-block: var(--pf-global--spacer--md);
padding-inline-end: var(--pf-global--spacer--xl);
}
}
}
.brand {
grid-area: brand;
background-color: var(--ak-brand-background-color);
height: 100%;
width: var(--pf-c-page__sidebar--Width);
align-items: center;
padding-inline: var(--pf-global--spacer--sm);
display: flex; display: flex;
flex-direction: column;
justify-content: center; justify-content: center;
&.pf-m-collapsed {
display: none;
} }
img.pf-icon {
max-height: 24px; @media (max-width: 1199px) {
display: none;
} }
}
.sidebar-trigger {
grid-area: toggle;
height: 100%;
}
.logo {
flex: 0 0 auto;
height: var(--ak-brand-logo-height);
& img {
height: 100%;
}
}
.sidebar-trigger, .sidebar-trigger,
.notification-trigger { .notification-trigger {
font-size: 24px; font-size: 1.5rem;
} }
.notification-trigger.has-notifications { .notification-trigger.has-notifications {
color: var(--pf-global--active-color--100); color: var(--pf-global--active-color--100);
} }
.page-title {
display: flex;
gap: var(--pf-global--spacer--xs);
}
h1 { h1 {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center !important; align-items: center !important;
} }
.pf-c-page__header-tools {
flex-shrink: 0;
}
.pf-c-page__header-tools-group {
height: 100%;
}
:host([theme="dark"]) .pf-c-page__header-tools {
color: var(--ak-dark-foreground) !important;
}
`, `,
]; ];
} }
constructor() { //#endregion
super();
window.addEventListener(EVENT_WS_MESSAGE, () => {
this.firstUpdated();
});
}
async firstUpdated() { //#region Properties
this.me = await me();
this.uiConfig = await uiConfig();
this.uiConfig.navbar.userDisplay = UserDisplay.none;
}
setTitle(header?: string) { @property({ type: String })
icon?: string;
@property({ type: Boolean })
iconImage = false;
@property({ type: String })
header?: string;
@property({ type: String })
description?: string;
@property({ type: Boolean })
hasIcon = true;
@property({ type: Boolean })
open = true;
@state()
session?: SessionUser;
@state()
uiConfig!: UIConfig;
//#endregion
//#region Private Methods
#setTitle(header?: string) {
const currentIf = currentInterface(); const currentIf = currentInterface();
let title = this.brand?.brandingTitle || TITLE_DEFAULT; let title = this.brand?.brandingTitle || TITLE_DEFAULT;
if (currentIf === "admin") { if (currentIf === "admin") {
title = `${msg("Admin")} - ${title}`; title = `${msg("Admin")} - ${title}`;
} }
// Prepend the header to the title // Prepend the header to the title
if (header !== undefined && header !== "") { if (header) {
title = `${header} - ${title}`; title = `${header} - ${title}`;
} }
document.title = title; document.title = title;
} }
willUpdate() { #toggleSidebar() {
// Always update title, even if there's no header value set, this.open = !this.open;
// as in that case we still need to return to the generic title
this.setTitle(this.header);
}
renderIcon() {
if (this.icon) {
if (this.iconImage && !this.icon.startsWith("fa://")) {
return html`<img class="pf-icon" src="${this.icon}" alt="page icon" />`;
}
const icon = this.icon.replaceAll("fa://", "fa ");
return html`<i class=${icon}></i>`;
}
return nothing;
}
render(): TemplateResult {
return html`<div class="bar">
<button
class="sidebar-trigger pf-c-button pf-m-plain"
@click=${() => {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent(EVENT_SIDEBAR_TOGGLE, { new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
bubbles: true, bubbles: true,
composed: true, composed: true,
}), }),
); );
}} }
//#endregion
//#region Lifecycle
public connectedCallback(): void {
super.connectedCallback();
AKPageNavbar.elementRef = this;
window.addEventListener(EVENT_WS_MESSAGE, () => {
this.firstUpdated();
});
}
public disconnectedCallback(): void {
super.disconnectedCallback();
AKPageNavbar.elementRef = null;
}
public async firstUpdated() {
this.session = await me();
this.uiConfig = getConfigForUser(this.session.user);
this.uiConfig.navbar.userDisplay = UserDisplay.none;
}
willUpdate() {
// Always update title, even if there's no header value set,
// as in that case we still need to return to the generic title
this.#setTitle(this.header);
}
//#endregion
//#region Render
renderIcon() {
if (this.icon) {
if (this.iconImage && !this.icon.startsWith("fa://")) {
return html`<img class="accent-icon pf-icon" src="${this.icon}" alt="page icon" />`;
}
const icon = this.icon.replaceAll("fa://", "fa ");
return html`<i class="accent-icon ${icon}"></i>`;
}
return nothing;
}
render(): TemplateResult {
return html`<navbar aria-label="Main" class="navbar">
<aside class="brand ${this.open ? "" : "pf-m-collapsed"}">
<a href="#/">
<div class="logo">
<img
src=${themeImage(
this.brand?.brandingLogo ?? DefaultBrand.brandingLogo,
)}
alt="${msg("authentik Logo")}"
loading="lazy"
/>
</div>
</a>
</aside>
<button
class="sidebar-trigger pf-c-button pf-m-plain"
@click=${this.#toggleSidebar}
aria-label=${msg("Toggle sidebar")}
aria-expanded=${this.open ? "true" : "false"}
> >
<i class="fas fa-bars"></i> <i class="fas fa-bars"></i>
</button> </button>
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content"> <section
<h1> class="items primary pf-c-content ${this.description ? "block-sibling" : ""}"
>
<h1 class="page-title">
${this.hasIcon ${this.hasIcon
? html`<slot name="icon">${this.renderIcon()}</slot>&nbsp;` ? html`<slot name="icon">${this.renderIcon()}</slot>`
: nothing} : nothing}
<slot name="header">${this.header}</slot> ${this.header}
</h1> </h1>
${this.description ? html`<p>${this.description}</p>` : html``}
</div>
</section> </section>
<div class="pf-c-page__header-tools"> ${this.description
? html`<section class="items page-description pf-c-content">
<p>${this.description}</p>
</section>`
: nothing}
<section class="items secondary">
<div class="pf-c-page__header-tools-group"> <div class="pf-c-page__header-tools-group">
<ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.me}> <ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.session}>
<a <a
class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md" class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md"
href="${globalAK().api.base}if/user/" href="${globalAK().api.base}if/user/"
@ -193,13 +400,76 @@ export class PageHeader extends WithBrandConfig(AKElement) {
</a> </a>
</ak-nav-buttons> </ak-nav-buttons>
</div> </div>
</div> </section>
</div>`; </navbar>
<slot></slot>`;
}
//#endregion
}
//#endregion
//#region Page Header
/**
* A page header component, used to display the page title and description.
*
* Internally, this component dispatches the `ak-page-header` event, which is
* listened to by the `ak-page-navbar` component.
*
* @singleton
*/
@customElement("ak-page-header")
export class AKPageHeader extends LitElement implements PageNavbarDetails {
@property({ type: String })
header?: string;
@property({ type: String })
description?: string;
@property({ type: String })
icon?: string;
@property({ type: Boolean })
iconImage = false;
static get styles(): CSSResult[] {
return [
css`
:host {
display: none;
}
`,
];
}
connectedCallback(): void {
super.connectedCallback();
AKPageNavbar.setNavbarDetails({
header: this.header,
description: this.description,
icon: this.icon,
iconImage: this.iconImage,
});
}
updated(): void {
AKPageNavbar.setNavbarDetails({
header: this.header,
description: this.description,
icon: this.icon,
iconImage: this.iconImage,
});
} }
} }
//#endregion
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ak-page-header": PageHeader; "ak-page-header": AKPageHeader;
"ak-page-navbar": AKPageNavbar;
} }
} }

View File

@ -85,6 +85,7 @@ export abstract class AKChart<T> extends AKElement {
.container { .container {
height: 100%; height: 100%;
width: 100%; width: 100%;
aspect-ratio: 1 / 1;
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -22,6 +22,7 @@ export class Sidebar extends AKElement {
css` css`
:host { :host {
z-index: 100; z-index: 100;
--pf-c-page__sidebar--Transition: 0 !important;
} }
.pf-c-nav__link.pf-m-current::after, .pf-c-nav__link.pf-m-current::after,
.pf-c-nav__link.pf-m-current:hover::after, .pf-c-nav__link.pf-m-current:hover::after,
@ -35,10 +36,7 @@ export class Sidebar extends AKElement {
.pf-c-nav__section + .pf-c-nav__section { .pf-c-nav__section + .pf-c-nav__section {
--pf-c-nav__section--section--MarginTop: var(--pf-global--spacer--sm); --pf-c-nav__section--section--MarginTop: var(--pf-global--spacer--sm);
} }
.pf-c-nav__list .sidebar-brand {
max-height: 82px;
margin-bottom: -0.5rem;
}
nav { nav {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -70,7 +68,6 @@ export class Sidebar extends AKElement {
class="pf-c-nav ${this.activeTheme === UiThemeEnum.Light ? "pf-m-light" : ""}" class="pf-c-nav ${this.activeTheme === UiThemeEnum.Light ? "pf-m-light" : ""}"
aria-label=${msg("Global")} aria-label=${msg("Global")}
> >
<ak-sidebar-brand></ak-sidebar-brand>
<ul class="pf-c-nav__list"> <ul class="pf-c-nav__list">
<slot></slot> <slot></slot>
</ul> </ul>

View File

@ -1,17 +1,3 @@
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
import { AKElement } from "@goauthentik/elements/Base";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import { themeImage } from "@goauthentik/elements/utils/images";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGlobal from "@patternfly/patternfly/patternfly-base.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { CurrentBrand, UiThemeEnum } from "@goauthentik/api"; import { CurrentBrand, UiThemeEnum } from "@goauthentik/api";
// If the viewport is wider than MIN_WIDTH, the sidebar // If the viewport is wider than MIN_WIDTH, the sidebar
@ -28,79 +14,3 @@ export const DefaultBrand: CurrentBrand = {
matchedDomain: "", matchedDomain: "",
defaultLocale: "", defaultLocale: "",
}; };
@customElement("ak-sidebar-brand")
export class SidebarBrand extends WithBrandConfig(AKElement) {
static get styles(): CSSResult[] {
return [
PFBase,
PFGlobal,
PFPage,
PFButton,
css`
:host {
display: flex;
flex-direction: row;
align-items: center;
height: 114px;
min-height: 114px;
border-bottom: var(--pf-global--BorderWidth--sm);
border-bottom-style: solid;
border-bottom-color: var(--pf-global--BorderColor--100);
}
.pf-c-brand img {
padding: 0 0.5rem;
height: 42px;
}
button.pf-c-button.sidebar-trigger {
background-color: transparent;
border-radius: 0px;
height: 100%;
color: var(--ak-dark-foreground);
}
`,
];
}
constructor() {
super();
window.addEventListener("resize", () => {
this.requestUpdate();
});
}
render(): TemplateResult {
return html` ${window.innerWidth <= MIN_WIDTH
? html`
<button
class="sidebar-trigger pf-c-button"
@click=${() => {
this.dispatchEvent(
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
bubbles: true,
composed: true,
}),
);
}}
>
<i class="fas fa-bars"></i>
</button>
`
: html``}
<a href="#/" class="pf-c-page__header-brand-link">
<div class="pf-c-brand ak-brand">
<img
src=${themeImage(this.brand?.brandingLogo ?? DefaultBrand.brandingLogo)}
alt="${msg("authentik Logo")}"
loading="lazy"
/>
</div>
</a>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-sidebar-brand": SidebarBrand;
}
}

View File

@ -1,19 +1,20 @@
import {
appendStyleSheet,
assertAdoptableStyleSheetParent,
} from "@goauthentik/common/stylesheets.js";
import { TemplateResult, render as litRender } from "lit"; import { TemplateResult, render as litRender } from "lit";
import AKGlobal from "@goauthentik/common/styles/authentik.css"; import AKGlobal from "@goauthentik/common/styles/authentik.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { ensureCSSStyleSheet } from "../utils/ensureCSSStyleSheet.js";
// A special version of render that ensures our style sheets will always be available // A special version of render that ensures our style sheets will always be available
// to all elements under test. Ensures they look right during testing, and that any // to all elements under test. Ensures they look right during testing, and that any
// CSS-based checks for visibility will return correct values. // CSS-based checks for visibility will return correct values.
export const render = (body: TemplateResult) => { export const render = (body: TemplateResult) => {
document.adoptedStyleSheets = [ assertAdoptableStyleSheetParent(document);
...document.adoptedStyleSheets,
ensureCSSStyleSheet(PFBase), appendStyleSheet([PFBase, AKGlobal], document);
ensureCSSStyleSheet(AKGlobal),
];
return litRender(body, document.body); return litRender(body, document.body);
}; };

View File

@ -1,9 +1,14 @@
import { AKElement } from "@goauthentik/elements/Base";
import { type LitElement, type ReactiveControllerHost, type TemplateResult, nothing } from "lit"; import { type LitElement, type ReactiveControllerHost, type TemplateResult, nothing } from "lit";
import "lit"; import "lit";
export type ReactiveElementHost<T = AKElement> = Partial<ReactiveControllerHost> & T; /**
* A custom element which may be used as a host for a ReactiveController.
*
* @remarks
*
* This type is derived from an internal type in Lit.
*/
export type ReactiveElementHost<T> = Partial<ReactiveControllerHost & T> & HTMLElement;
export type AbstractLitElementConstructor = abstract new (...args: never[]) => LitElement; export type AbstractLitElementConstructor = abstract new (...args: never[]) => LitElement;

View File

@ -1,35 +0,0 @@
import { CSSResult, unsafeCSS } from "lit";
const supportsAdoptingStyleSheets: boolean =
window.ShadowRoot &&
(window.ShadyCSS === undefined || window.ShadyCSS.nativeShadow) &&
"adoptedStyleSheets" in Document.prototype &&
"replace" in CSSStyleSheet.prototype;
function stringToStylesheet(css: string) {
if (supportsAdoptingStyleSheets) {
const sheet = unsafeCSS(css).styleSheet;
if (sheet === undefined) {
throw new Error(
`CSS processing error: undefined stylesheet from string. Source: ${css}`,
);
}
return sheet;
}
const sheet = new CSSStyleSheet();
sheet.replaceSync(css);
return sheet;
}
function cssResultToStylesheet(css: CSSResult) {
const sheet = css.styleSheet;
return sheet ? sheet : stringToStylesheet(css.toString());
}
export const ensureCSSStyleSheet = (css: string | CSSStyleSheet | CSSResult): CSSStyleSheet =>
css instanceof CSSResult
? cssResultToStylesheet(css)
: typeof css === "string"
? stringToStylesheet(css)
: css;

View File

@ -0,0 +1,55 @@
/**
* @file IFrame Utilities
*/
interface IFrameLoadResult {
contentWindow: Window;
contentDocument: Document;
}
export function pluckIFrameContent(iframe: HTMLIFrameElement) {
const contentWindow = iframe.contentWindow;
const contentDocument = iframe.contentDocument;
if (!contentWindow) {
throw new Error("Iframe contentWindow is not accessible");
}
if (!contentDocument) {
throw new Error("Iframe contentDocument is not accessible");
}
return {
contentWindow,
contentDocument,
};
}
export function resolveIFrameContent(iframe: HTMLIFrameElement): Promise<IFrameLoadResult> {
if (iframe.contentDocument?.readyState === "complete") {
return Promise.resolve(pluckIFrameContent(iframe));
}
return new Promise((resolve) => {
iframe.addEventListener("load", () => resolve(pluckIFrameContent(iframe)), { once: true });
});
}
/**
* Creates a minimal HTML wrapper for an iframe.
*
* @deprecated Use the `contentDocument.body` directly instead.
*/
export function createIFrameHTMLWrapper(bodyContent: string): string {
const html = String.raw;
return html`<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body style="display:flex;flex-direction:row;justify-content:center;">
${bodyContent}
</body>
</html>`;
}

View File

@ -1,13 +1,8 @@
import { QUERY_MEDIA_COLOR_LIGHT, rootInterface } from "@goauthentik/elements/Base"; import { resolveUITheme } from "@goauthentik/common/theme";
import { rootInterface } from "@goauthentik/elements/Base";
import { UiThemeEnum } from "@goauthentik/api";
export function themeImage(rawPath: string) { export function themeImage(rawPath: string) {
let enabledTheme = rootInterface()?.activeTheme; const enabledTheme = rootInterface()?.activeTheme || resolveUITheme();
if (!enabledTheme || enabledTheme === UiThemeEnum.Automatic) {
enabledTheme = window.matchMedia(QUERY_MEDIA_COLOR_LIGHT).matches
? UiThemeEnum.Light
: UiThemeEnum.Dark;
}
return rawPath.replaceAll("%(theme)s", enabledTheme); return rawPath.replaceAll("%(theme)s", enabledTheme);
} }

View File

@ -46,7 +46,6 @@ import {
FlowsApi, FlowsApi,
ResponseError, ResponseError,
ShellChallenge, ShellChallenge,
UiThemeEnum,
} from "@goauthentik/api"; } from "@goauthentik/api";
@customElement("ak-flow-executor") @customElement("ak-flow-executor")
@ -200,10 +199,6 @@ export class FlowExecutor extends Interface implements StageHost {
}); });
} }
async getTheme(): Promise<UiThemeEnum> {
return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic;
}
async submit( async submit(
payload?: FlowChallengeResponseRequest, payload?: FlowChallengeResponseRequest,
options?: SubmitOptions, options?: SubmitOptions,

View File

@ -1,4 +1,4 @@
import { purify } from "@goauthentik/common/purify"; import { BrandedHTMLPolicy, sanitizeHTML } from "@goauthentik/common/purify";
import { AKElement } from "@goauthentik/elements/Base.js"; import { AKElement } from "@goauthentik/elements/Base.js";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
@ -21,8 +21,6 @@ const styles = css`
} }
`; `;
const poweredBy: FooterLink = { name: msg("Powered by authentik"), href: null };
@customElement("ak-brand-links") @customElement("ak-brand-links")
export class BrandLinks extends AKElement { export class BrandLinks extends AKElement {
static get styles() { static get styles() {
@ -33,13 +31,21 @@ export class BrandLinks extends AKElement {
links: FooterLink[] = []; links: FooterLink[] = [];
render() { render() {
const links = [...(this.links ?? []), poweredBy]; const links = [...(this.links ?? [])];
return html` <ul class="pf-c-list pf-m-inline"> return html` <ul class="pf-c-list pf-m-inline">
${map(links, (link) => ${map(links, (link) => {
link.href const children = sanitizeHTML(BrandedHTMLPolicy, link.name);
? purify(html`<li><a href="${link.href}">${link.name}</a></li>`)
: html`<li><span>${link.name}</span></li>`, if (link.href) {
)} return html`<li><a href="${link.href}">${children}</a></li>`;
}
return html`<li>
<span> ${children} </span>
</li>`;
})}
<li><span>${msg("Powered by authentik")}</span></li>
</ul>`; </ul>`;
} }
} }

View File

@ -1,15 +1,16 @@
///<reference types="@hcaptcha/types"/> /// <reference types="@hcaptcha/types"/>
import { renderStatic } from "@goauthentik/common/purify"; /// <reference types="turnstile-types"/>
import { renderStaticHTMLUnsafe } from "@goauthentik/common/purify";
import "@goauthentik/elements/EmptyState"; import "@goauthentik/elements/EmptyState";
import { akEmptyState } from "@goauthentik/elements/EmptyState"; import { akEmptyState } from "@goauthentik/elements/EmptyState";
import { bound } from "@goauthentik/elements/decorators/bound"; import { bound } from "@goauthentik/elements/decorators/bound";
import "@goauthentik/elements/forms/FormElement"; import "@goauthentik/elements/forms/FormElement";
import { createIFrameHTMLWrapper } from "@goauthentik/elements/utils/iframe";
import { ListenerController } from "@goauthentik/elements/utils/listenerController.js"; import { ListenerController } from "@goauthentik/elements/utils/listenerController.js";
import { randomId } from "@goauthentik/elements/utils/randomId"; import { randomId } from "@goauthentik/elements/utils/randomId";
import "@goauthentik/flow/FormStatic"; import "@goauthentik/flow/FormStatic";
import { BaseStage } from "@goauthentik/flow/stages/base"; import { BaseStage } from "@goauthentik/flow/stages/base";
import { P, match } from "ts-pattern"; import { P, match } from "ts-pattern";
import type * as _ from "turnstile-types";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit"; import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
@ -56,18 +57,14 @@ type CaptchaHandler = {
// a resize. Because the Captcha is itself in an iframe, the reported height is often off by some // a resize. Because the Captcha is itself in an iframe, the reported height is often off by some
// margin, so adding 2rem of height to our container adds padding and prevents scroll bars or hidden // margin, so adding 2rem of height to our container adds padding and prevents scroll bars or hidden
// rendering. // rendering.
function iframeTemplate(children: TemplateResult, challengeURL: string): TemplateResult {
const iframeTemplate = (captchaElement: TemplateResult, challengeUrl: string) => return html` ${children}
html`<!doctype html>
<head>
<html>
<body style="display:flex;flex-direction:row;justify-content:center;">
${captchaElement}
<script> <script>
new ResizeObserver((entries) => { new ResizeObserver((entries) => {
const height = const height =
document.body.offsetHeight + document.body.offsetHeight +
parseFloat(getComputedStyle(document.body).fontSize) * 2; parseFloat(getComputedStyle(document.body).fontSize) * 2;
window.parent.postMessage({ window.parent.postMessage({
message: "resize", message: "resize",
source: "goauthentik.io", source: "goauthentik.io",
@ -76,20 +73,20 @@ const iframeTemplate = (captchaElement: TemplateResult, challengeUrl: string) =>
}); });
}).observe(document.querySelector(".ak-captcha-container")); }).observe(document.querySelector(".ak-captcha-container"));
</script> </script>
<script src=${challengeUrl}></script>
<script src=${challengeURL}></script>
<script> <script>
function callback(token) { function callback(token) {
window.parent.postMessage({ window.parent.postMessage({
message: "captcha", message: "captcha",
source: "goauthentik.io", source: "goauthentik.io",
context: "flow-executor", context: "flow-executor",
token: token, token,
}); });
} }
</script> </script>`;
</body> }
</html>
</head>`;
@customElement("ak-stage-captcha") @customElement("ak-stage-captcha")
export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> { export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
@ -305,11 +302,25 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
} }
async renderFrame(captchaElement: TemplateResult) { async renderFrame(captchaElement: TemplateResult) {
this.captchaFrame.contentWindow?.document.open(); const { contentDocument } = this.captchaFrame || {};
this.captchaFrame.contentWindow?.document.write(
await renderStatic(iframeTemplate(captchaElement, this.challenge.jsUrl)), if (!contentDocument) {
console.debug(
"authentik/stages/captcha: unable to render captcha frame, no contentDocument",
); );
this.captchaFrame.contentWindow?.document.close();
return;
}
contentDocument.open();
contentDocument.write(
createIFrameHTMLWrapper(
renderStaticHTMLUnsafe(iframeTemplate(captchaElement, this.challenge.jsUrl)),
),
);
contentDocument.close();
} }
renderBody() { renderBody() {

View File

@ -3,7 +3,6 @@ import "rapidoc";
import { CSRFHeaderName } from "@goauthentik/common/api/config"; import { CSRFHeaderName } from "@goauthentik/common/api/config";
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants"; import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { first, getCookie } from "@goauthentik/common/utils"; import { first, getCookie } from "@goauthentik/common/utils";
import { Interface } from "@goauthentik/elements/Interface"; import { Interface } from "@goauthentik/elements/Interface";
import "@goauthentik/elements/ak-locale-context"; import "@goauthentik/elements/ak-locale-context";
@ -62,10 +61,6 @@ export class APIBrowser extends Interface {
); );
} }
async getTheme(): Promise<UiThemeEnum> {
return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic;
}
render(): TemplateResult { render(): TemplateResult {
return html` return html`
<ak-locale-context> <ak-locale-context>

View File

@ -1,4 +1,3 @@
import { globalAK } from "@goauthentik/common/global";
import { Interface } from "@goauthentik/elements/Interface"; import { Interface } from "@goauthentik/elements/Interface";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
@ -10,8 +9,6 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.css"; import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { UiThemeEnum } from "@goauthentik/api";
@customElement("ak-loading") @customElement("ak-loading")
export class Loading extends Interface { export class Loading extends Interface {
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
@ -28,7 +25,7 @@ export class Loading extends Interface {
]; ];
} }
_initContexts(): void { registerContexts(): void {
// Stub function to avoid making API requests for things we don't need. The `Interface` base class loads // Stub function to avoid making API requests for things we don't need. The `Interface` base class loads
// a bunch of data that is used globally by various things, however this is an interface that is shown // a bunch of data that is used globally by various things, however this is an interface that is shown
// very briefly and we don't need any of that data. // very briefly and we don't need any of that data.
@ -38,10 +35,6 @@ export class Loading extends Interface {
// Stub function to avoid fetching custom CSS. // Stub function to avoid fetching custom CSS.
} }
async getTheme(): Promise<UiThemeEnum> {
return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic;
}
render(): TemplateResult { render(): TemplateResult {
return html` <section return html` <section
class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl" class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl"

View File

@ -1,18 +1,9 @@
import { FlowExecutor } from "@goauthentik/flow/FlowExecutor"; import { FlowExecutor } from "@goauthentik/flow/FlowExecutor";
import { customElement, property } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import { UiThemeEnum } from "@goauthentik/api";
@customElement("ak-storybook-interface-flow") @customElement("ak-storybook-interface-flow")
export class StoryFlowInterface extends FlowExecutor { export class StoryFlowInterface extends FlowExecutor {}
@property()
storyTheme: UiThemeEnum = UiThemeEnum.Dark;
async getTheme(): Promise<UiThemeEnum> {
return this.storyTheme;
}
}
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@ -1,18 +1,9 @@
import { Interface } from "@goauthentik/elements/Interface"; import { Interface } from "@goauthentik/elements/Interface";
import { customElement, property } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import { UiThemeEnum } from "@goauthentik/api";
@customElement("ak-storybook-interface") @customElement("ak-storybook-interface")
export class StoryInterface extends Interface { export class StoryInterface extends Interface {}
@property()
storyTheme: UiThemeEnum = UiThemeEnum.Dark;
async getTheme(): Promise<UiThemeEnum> {
return this.storyTheme;
}
}
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@ -6,7 +6,7 @@ import {
} from "@goauthentik/common/constants"; } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { configureSentry } from "@goauthentik/common/sentry"; import { configureSentry } from "@goauthentik/common/sentry";
import { UIConfig } from "@goauthentik/common/ui/config"; import { UIConfig, getConfigForUser } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { WebsocketClient } from "@goauthentik/common/ws"; import { WebsocketClient } from "@goauthentik/common/ws";
import "@goauthentik/components/ak-nav-buttons"; import "@goauthentik/components/ak-nav-buttons";
@ -292,6 +292,7 @@ export class UserInterface extends AuthenticatedInterface {
async connectedCallback() { async connectedCallback() {
super.connectedCallback(); super.connectedCallback();
window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, this.toggleNotificationDrawer); window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, this.toggleNotificationDrawer);
window.addEventListener(EVENT_API_DRAWER_TOGGLE, this.toggleApiDrawer); window.addEventListener(EVENT_API_DRAWER_TOGGLE, this.toggleApiDrawer);
window.addEventListener(EVENT_WS_MESSAGE, this.fetchConfigurationDetails); window.addEventListener(EVENT_WS_MESSAGE, this.fetchConfigurationDetails);
@ -301,6 +302,7 @@ export class UserInterface extends AuthenticatedInterface {
window.removeEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, this.toggleNotificationDrawer); window.removeEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, this.toggleNotificationDrawer);
window.removeEventListener(EVENT_API_DRAWER_TOGGLE, this.toggleApiDrawer); window.removeEventListener(EVENT_API_DRAWER_TOGGLE, this.toggleApiDrawer);
window.removeEventListener(EVENT_WS_MESSAGE, this.fetchConfigurationDetails); window.removeEventListener(EVENT_WS_MESSAGE, this.fetchConfigurationDetails);
super.disconnectedCallback(); super.disconnectedCallback();
} }
@ -319,8 +321,10 @@ export class UserInterface extends AuthenticatedInterface {
} }
fetchConfigurationDetails() { fetchConfigurationDetails() {
me().then((me: SessionUser) => { me().then((session: SessionUser) => {
this.me = me; this.me = session;
this.uiConfig = getConfigForUser(session.user);
new EventsApi(DEFAULT_CONFIG) new EventsApi(DEFAULT_CONFIG)
.eventsNotificationsList({ .eventsNotificationsList({
seen: false, seen: false,
@ -334,12 +338,16 @@ export class UserInterface extends AuthenticatedInterface {
}); });
} }
get isFullyConfigured() { render() {
return Boolean(this.uiConfig && this.me); if (!this.me) {
console.debug(`authentik/user/UserInterface: waiting for user session to be available`);
return nothing;
} }
render() { if (!this.uiConfig) {
if (!this.isFullyConfigured) { console.debug(`authentik/user/UserInterface: waiting for UI config to be available`);
return nothing; return nothing;
} }

Some files were not shown because too many files have changed in this diff Show More