Compare commits
1 Commits
revert-rev
...
safari-cra
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e473c16fa |
@ -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*))?
|
||||||
|
|||||||
13
.github/workflows/ci-main.yml
vendored
13
.github/workflows/ci-main.yml
vendored
@ -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
|
||||||
|
|||||||
8
.github/workflows/packages-npm-publish.yml
vendored
8
.github/workflows/packages-npm-publish.yml
vendored
@ -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:
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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(
|
||||||
_(
|
_(
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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,
|
||||||
# }
|
}
|
||||||
# },
|
},
|
||||||
# )
|
)
|
||||||
|
|||||||
@ -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/"
|
|
||||||
|
|||||||
@ -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 %}
|
|
||||||
@ -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())
|
|
||||||
@ -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),
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
@ -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"
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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"]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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.
@ -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.
@ -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.
@ -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.
@ -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 ""
|
||||||
|
|||||||
@ -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 ""
|
||||||
|
|||||||
@ -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."
|
||||||
|
|||||||
@ -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 ""
|
||||||
|
|||||||
@ -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.
@ -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.
@ -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 ""
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@goauthentik/authentik",
|
"name": "@goauthentik/authentik",
|
||||||
"version": "2025.4.0",
|
"version": "2025.2.4",
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"]
|
||||||
|
|||||||
@ -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.*"
|
||||||
|
|||||||
@ -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"]
|
||||||
|
|||||||
@ -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"]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
|
||||||
)
|
|
||||||
|
|||||||
@ -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
14
uv.lock
generated
@ -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
12198
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 = ";";
|
||||||
|
|
||||||
|
|||||||
@ -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)}`;
|
|
||||||
})(),
|
|
||||||
)}`;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
220
web/src/common/stylesheets.ts
Normal file
220
web/src/common/stylesheets.ts
Normal 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
200
web/src/common/theme.ts
Normal 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
|
||||||
@ -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>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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> `
|
? 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
55
web/src/elements/utils/iframe.ts
Normal file
55
web/src/elements/utils/iframe.ts
Normal 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>`;
|
||||||
|
}
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
Reference in New Issue
Block a user