mirror of
https://github.com/goauthentik/authentik
synced 2026-05-07 15:42:48 +02:00
Compare commits
5 Commits
root/move-
...
pr/16059
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42afef2bb2 | ||
|
|
b3ba6f9134 | ||
|
|
22971942c1 | ||
|
|
907d311339 | ||
|
|
9890ea8275 |
36
Dockerfile
36
Dockerfile
@@ -76,12 +76,12 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
||||
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||
|
||||
# Stage 4: Download uv
|
||||
FROM ghcr.io/astral-sh/uv:0.8.11 AS uv
|
||||
FROM ghcr.io/astral-sh/uv:0.8.10 AS uv
|
||||
# Stage 5: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.6-slim-bookworm-fips AS python-base
|
||||
|
||||
ENV VENV_PATH="/ak-root/.venv" \
|
||||
PATH="/ak-root/lifecycle:/ak-root/venv/bin:$PATH" \
|
||||
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
||||
UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy \
|
||||
UV_NATIVE_TLS=1 \
|
||||
@@ -145,6 +145,8 @@ LABEL org.opencontainers.image.authors="Authentik Security Inc." \
|
||||
org.opencontainers.image.vendor="Authentik Security Inc." \
|
||||
org.opencontainers.image.version=${VERSION}
|
||||
|
||||
WORKDIR /
|
||||
|
||||
# We cannot cache this layer otherwise we'll end up with a bigger image
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
@@ -155,26 +157,28 @@ RUN apt-get update && \
|
||||
pip3 install --no-cache-dir --upgrade pip && \
|
||||
apt-get clean && \
|
||||
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \
|
||||
adduser --system --no-create-home --uid 1000 --group --home /ak-root authentik && \
|
||||
adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \
|
||||
mkdir -p /certs /media /blueprints && \
|
||||
mkdir -p /ak-root/authentik/.ssh && \
|
||||
chown authentik:authentik /certs /media /ak-root/authentik/.ssh /ak-root
|
||||
mkdir -p /authentik/.ssh && \
|
||||
mkdir -p /ak-root && \
|
||||
chown authentik:authentik /certs /media /authentik/.ssh /ak-root
|
||||
|
||||
COPY ./authentik/ /ak-root/authentik
|
||||
COPY ./pyproject.toml /ak-root/
|
||||
COPY ./uv.lock /ak-root/
|
||||
COPY ./schemas /ak-root/schemas
|
||||
COPY ./locale /ak-root/locale
|
||||
COPY ./tests /ak-root/tests
|
||||
COPY ./manage.py /ak-root/
|
||||
COPY ./authentik/ /authentik
|
||||
COPY ./pyproject.toml /
|
||||
COPY ./uv.lock /
|
||||
COPY ./schemas /schemas
|
||||
COPY ./locale /locale
|
||||
COPY ./tests /tests
|
||||
COPY ./manage.py /
|
||||
COPY ./blueprints /blueprints
|
||||
COPY ./lifecycle/ /ak-root/lifecycle
|
||||
COPY ./lifecycle/ /lifecycle
|
||||
COPY ./authentik/sources/kerberos/krb5.conf /etc/krb5.conf
|
||||
COPY --from=go-builder /go/authentik /bin/authentik
|
||||
COPY ./packages/ /ak-root/packages
|
||||
RUN ln -s /ak-root/packages /packages
|
||||
COPY --from=python-deps /ak-root/.venv /ak-root/.venv
|
||||
COPY --from=node-builder /work/web/dist/ /ak-root/web/dist/
|
||||
COPY --from=node-builder /work/web/authentik/ /ak-root/web/authentik/
|
||||
COPY --from=node-builder /work/web/dist/ /web/dist/
|
||||
COPY --from=node-builder /work/web/authentik/ /web/authentik/
|
||||
COPY --from=geoip /usr/share/GeoIP /geoip
|
||||
|
||||
USER 1000
|
||||
@@ -186,6 +190,4 @@ ENV TMPDIR=/dev/shm/ \
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "ak", "healthcheck" ]
|
||||
|
||||
WORKDIR /ak-root
|
||||
|
||||
ENTRYPOINT [ "dumb-init", "--", "ak" ]
|
||||
|
||||
@@ -63,6 +63,28 @@ class TestBrands(APITestCase):
|
||||
},
|
||||
)
|
||||
|
||||
def test_brand_subdomain_same_suffix(self):
|
||||
"""Test Current brand API"""
|
||||
Brand.objects.all().delete()
|
||||
Brand.objects.create(domain="bar.baz", branding_title="custom")
|
||||
Brand.objects.create(domain="foo.bar.baz", branding_title="custom")
|
||||
self.assertJSONEqual(
|
||||
self.client.get(
|
||||
reverse("authentik_api:brand-current"), HTTP_HOST="foo.bar.baz"
|
||||
).content.decode(),
|
||||
{
|
||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||
"branding_title": "custom",
|
||||
"branding_custom_css": "",
|
||||
"matched_domain": "foo.bar.baz",
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
)
|
||||
|
||||
def test_fallback(self):
|
||||
"""Test fallback brand"""
|
||||
Brand.objects.all().delete()
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Any
|
||||
|
||||
from django.db.models import F, Q
|
||||
from django.db.models import Value as V
|
||||
from django.db.models.functions import Length
|
||||
from django.http.request import HttpRequest
|
||||
from django.utils.html import _json_script_escapes
|
||||
from django.utils.safestring import mark_safe
|
||||
@@ -20,9 +21,9 @@ DEFAULT_BRAND = Brand(domain="fallback")
|
||||
def get_brand_for_request(request: HttpRequest) -> Brand:
|
||||
"""Get brand object for current request"""
|
||||
db_brands = (
|
||||
Brand.objects.annotate(host_domain=V(request.get_host()))
|
||||
Brand.objects.annotate(host_domain=V(request.get_host()), match_length=Length("domain"))
|
||||
.filter(Q(host_domain__iendswith=F("domain")) | _q_default)
|
||||
.order_by("default")
|
||||
.order_by("-match_length", "default")
|
||||
)
|
||||
brands = list(db_brands.all())
|
||||
if len(brands) < 1:
|
||||
|
||||
@@ -21,6 +21,8 @@ from rest_framework.serializers import (
|
||||
raise_errors_on_nested_writes,
|
||||
)
|
||||
|
||||
from authentik.rbac.permissions import assign_initial_permissions
|
||||
|
||||
|
||||
def is_dict(value: Any):
|
||||
"""Ensure a value is a dictionary, useful for JSONFields"""
|
||||
@@ -50,6 +52,15 @@ class ModelSerializer(BaseModelSerializer):
|
||||
serializer_field_mapping = BaseModelSerializer.serializer_field_mapping.copy()
|
||||
serializer_field_mapping[models.JSONField] = JSONDictField
|
||||
|
||||
def create(self, validated_data):
|
||||
instance = super().create(validated_data)
|
||||
|
||||
request = self.context.get("request")
|
||||
if request and hasattr(request, "user") and not request.user.is_anonymous:
|
||||
assign_initial_permissions(request.user, instance)
|
||||
|
||||
return instance
|
||||
|
||||
def update(self, instance: Model, validated_data):
|
||||
raise_errors_on_nested_writes("update", self, validated_data)
|
||||
info = model_meta.get_field_info(instance)
|
||||
|
||||
@@ -23,7 +23,6 @@ from authentik.events.models import (
|
||||
)
|
||||
from authentik.events.utils import get_user
|
||||
from authentik.rbac.decorators import permission_required
|
||||
from authentik.stages.email.models import get_template_choices
|
||||
|
||||
|
||||
class NotificationTransportSerializer(ModelSerializer):
|
||||
@@ -31,18 +30,6 @@ class NotificationTransportSerializer(ModelSerializer):
|
||||
|
||||
mode_verbose = SerializerMethodField()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["email_template"].choices = get_template_choices()
|
||||
|
||||
def validate_email_template(self, value: str) -> str:
|
||||
"""Check validity of email template"""
|
||||
choices = get_template_choices()
|
||||
for path, _ in choices:
|
||||
if path == value:
|
||||
return value
|
||||
raise ValidationError(f"Invalid template '{value}' specified.")
|
||||
|
||||
def get_mode_verbose(self, instance: NotificationTransport) -> str:
|
||||
"""Return selected mode with a UI Label"""
|
||||
return TransportMode(instance.mode).label
|
||||
@@ -65,8 +52,6 @@ class NotificationTransportSerializer(ModelSerializer):
|
||||
"webhook_url",
|
||||
"webhook_mapping_body",
|
||||
"webhook_mapping_headers",
|
||||
"email_subject_prefix",
|
||||
"email_template",
|
||||
"send_once",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 5.1.11 on 2025-08-14 13:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_events", "0011_alter_systemtask_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="notificationtransport",
|
||||
name="email_subject_prefix",
|
||||
field=models.TextField(blank=True, default="authentik Notification: "),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="notificationtransport",
|
||||
name="email_template",
|
||||
field=models.TextField(default="email/event_notification.html"),
|
||||
),
|
||||
]
|
||||
@@ -41,7 +41,6 @@ from authentik.lib.utils.http import get_http_session
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
from authentik.root.middleware import ClientIPMiddleware
|
||||
from authentik.stages.email.models import EmailTemplates
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
from authentik.tasks.models import TasksModel
|
||||
from authentik.tenants.models import Tenant
|
||||
@@ -296,15 +295,6 @@ class NotificationTransport(TasksModel, SerializerModel):
|
||||
|
||||
name = models.TextField(unique=True)
|
||||
mode = models.TextField(choices=TransportMode.choices, default=TransportMode.LOCAL)
|
||||
send_once = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_(
|
||||
"Only send notification once, for example when sending a webhook into a chat channel."
|
||||
),
|
||||
)
|
||||
|
||||
email_subject_prefix = models.TextField(default="authentik Notification: ", blank=True)
|
||||
email_template = models.TextField(default=EmailTemplates.EVENT_NOTIFICATION)
|
||||
|
||||
webhook_url = models.TextField(blank=True, validators=[DomainlessURLValidator()])
|
||||
webhook_mapping_body = models.ForeignKey(
|
||||
@@ -329,6 +319,12 @@ class NotificationTransport(TasksModel, SerializerModel):
|
||||
"Mapping should return a dictionary of key-value pairs"
|
||||
),
|
||||
)
|
||||
send_once = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_(
|
||||
"Only send notification once, for example when sending a webhook into a chat channel."
|
||||
),
|
||||
)
|
||||
|
||||
def send(self, notification: "Notification") -> list[str]:
|
||||
"""Send notification to user, called from async task"""
|
||||
@@ -466,6 +462,7 @@ class NotificationTransport(TasksModel, SerializerModel):
|
||||
notification=notification,
|
||||
)
|
||||
return None
|
||||
subject_prefix = "authentik Notification: "
|
||||
context = {
|
||||
"key_value": {
|
||||
"user_email": notification.user.email,
|
||||
@@ -493,10 +490,10 @@ class NotificationTransport(TasksModel, SerializerModel):
|
||||
"from": self.name,
|
||||
}
|
||||
mail = TemplateEmailMessage(
|
||||
subject=self.email_subject_prefix + context["title"],
|
||||
subject=subject_prefix + context["title"],
|
||||
to=[(notification.user.name, notification.user.email)],
|
||||
language=notification.user.locale(),
|
||||
template_name=self.email_template,
|
||||
template_name="email/event_notification.html",
|
||||
template_context=context,
|
||||
)
|
||||
send_mail.send_with_options(args=(mail.__dict__,), rel_obj=self)
|
||||
|
||||
@@ -5,12 +5,10 @@ from unittest.mock import PropertyMock, patch
|
||||
from django.core import mail
|
||||
from django.core.mail.backends.locmem import EmailBackend
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from requests_mock import Mocker
|
||||
|
||||
from authentik import authentik_full_version
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.events.api.notification_transports import NotificationTransportSerializer
|
||||
from authentik.events.models import (
|
||||
Event,
|
||||
Notification,
|
||||
@@ -20,7 +18,6 @@ from authentik.events.models import (
|
||||
TransportMode,
|
||||
)
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.stages.email.models import get_template_choices
|
||||
|
||||
|
||||
class TestEventTransports(TestCase):
|
||||
@@ -141,76 +138,3 @@ class TestEventTransports(TestCase):
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].subject, "authentik Notification: custom_foo")
|
||||
self.assertIn(self.notification.body, mail.outbox[0].alternatives[0][0])
|
||||
|
||||
def test_transport_email_custom_template(self):
|
||||
"""Test email transport with custom template"""
|
||||
transport: NotificationTransport = NotificationTransport.objects.create(
|
||||
name=generate_id(),
|
||||
mode=TransportMode.EMAIL,
|
||||
email_template="email/event_notification.html",
|
||||
)
|
||||
with patch(
|
||||
"authentik.stages.email.models.EmailStage.backend_class",
|
||||
PropertyMock(return_value=EmailBackend),
|
||||
):
|
||||
transport.send(self.notification)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertIn(self.notification.body, mail.outbox[0].alternatives[0][0])
|
||||
|
||||
def test_transport_email_custom_subject_prefix(self):
|
||||
"""Test email transport with custom subject prefix"""
|
||||
transport: NotificationTransport = NotificationTransport.objects.create(
|
||||
name=generate_id(),
|
||||
mode=TransportMode.EMAIL,
|
||||
email_subject_prefix="[CUSTOM] ",
|
||||
)
|
||||
with patch(
|
||||
"authentik.stages.email.models.EmailStage.backend_class",
|
||||
PropertyMock(return_value=EmailBackend),
|
||||
):
|
||||
transport.send(self.notification)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].subject, "[CUSTOM] custom_foo")
|
||||
|
||||
def test_transport_email_validation(self):
|
||||
"""Test email transport template validation"""
|
||||
|
||||
# Test valid template
|
||||
serializer = NotificationTransportSerializer(
|
||||
data={
|
||||
"name": generate_id(),
|
||||
"mode": TransportMode.EMAIL,
|
||||
"email_template": "email/event_notification.html",
|
||||
}
|
||||
)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
|
||||
# Test invalid template - should fail due to choices validation
|
||||
serializer = NotificationTransportSerializer(
|
||||
data={
|
||||
"name": generate_id(),
|
||||
"mode": TransportMode.EMAIL,
|
||||
"email_template": "invalid/template.html",
|
||||
}
|
||||
)
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertIn("email_template", serializer.errors)
|
||||
|
||||
def test_templates_api_endpoint(self):
|
||||
"""Test templates API endpoint returns valid templates"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("authentik_api:emailstage-templates"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data = response.json()
|
||||
self.assertIsInstance(data, list)
|
||||
|
||||
# Check that we have at least the default templates
|
||||
template_names = [item["name"] for item in data]
|
||||
self.assertIn("email/event_notification.html", template_names)
|
||||
|
||||
# Verify all templates are valid choices
|
||||
valid_choices = dict(get_template_choices())
|
||||
for template in data:
|
||||
self.assertIn(template["name"], valid_choices)
|
||||
self.assertEqual(template["description"], valid_choices[template["name"]])
|
||||
|
||||
@@ -103,7 +103,7 @@ class PasswordPolicy(Policy):
|
||||
if self.amount_lowercase > 0 and len(RE_LOWER.findall(password)) < self.amount_lowercase:
|
||||
LOGGER.debug("password failed", check="static", reason="amount_lowercase")
|
||||
return PolicyResult(False, self.error_message)
|
||||
if self.amount_uppercase > 0 and len(RE_UPPER.findall(password)) < self.amount_uppercase:
|
||||
if self.amount_uppercase > 0 and len(RE_UPPER.findall(password)) < self.amount_lowercase:
|
||||
LOGGER.debug("password failed", check="static", reason="amount_uppercase")
|
||||
return PolicyResult(False, self.error_message)
|
||||
if self.amount_symbols > 0:
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
"""InitialPermissions middleware"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from contextvars import ContextVar
|
||||
from functools import partial
|
||||
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.rbac.permissions import assign_initial_permissions
|
||||
|
||||
_CTX_REQUEST = ContextVar[HttpRequest | None]("authentik_initial_permissions_request", default=None)
|
||||
|
||||
|
||||
class InitialPermissionsMiddleware:
|
||||
"""Register a handler for duration of request-response that assigns InitialPermissions"""
|
||||
|
||||
get_response: Callable[[HttpRequest], HttpResponse]
|
||||
|
||||
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
|
||||
self.get_response = get_response
|
||||
|
||||
def get_uid(self, request_id: str) -> str:
|
||||
return f"InitialPermissionMiddleware-{request_id}"
|
||||
|
||||
def connect(self, request: HttpRequest):
|
||||
if not hasattr(request, "request_id"):
|
||||
return
|
||||
post_save.connect(
|
||||
partial(self.post_save_handler, request=request),
|
||||
dispatch_uid=self.get_uid(request.request_id),
|
||||
weak=False,
|
||||
)
|
||||
|
||||
def disconnect(self, request: HttpRequest):
|
||||
if not hasattr(request, "request_id"):
|
||||
return
|
||||
post_save.disconnect(dispatch_uid=self.get_uid(request.request_id))
|
||||
|
||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||
_CTX_REQUEST.set(request)
|
||||
self.connect(request)
|
||||
|
||||
response = self.get_response(request)
|
||||
|
||||
self.disconnect(request)
|
||||
_CTX_REQUEST.set(None)
|
||||
return response
|
||||
|
||||
def process_exception(self, request: HttpRequest, exception: Exception):
|
||||
self.disconnect(request)
|
||||
|
||||
def post_save_handler(
|
||||
self,
|
||||
request: HttpRequest,
|
||||
instance: Model,
|
||||
created: bool,
|
||||
**_,
|
||||
):
|
||||
if not created:
|
||||
return
|
||||
if request.request_id != _CTX_REQUEST.get().request_id:
|
||||
return
|
||||
user: User = request.user
|
||||
if not user or user.is_anonymous:
|
||||
return
|
||||
assign_initial_permissions(user, instance)
|
||||
@@ -5,12 +5,9 @@ from django.db.models import Model
|
||||
from guardian.shortcuts import assign_perm
|
||||
from rest_framework.permissions import BasePermission, DjangoObjectPermissions
|
||||
from rest_framework.request import Request
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.rbac.models import InitialPermissions, InitialPermissionsMode
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class ObjectPermissions(DjangoObjectPermissions):
|
||||
"""RBAC Permissions"""
|
||||
@@ -74,10 +71,4 @@ def assign_initial_permissions(user, instance: Model):
|
||||
if initial_permissions.mode == InitialPermissionsMode.USER
|
||||
else initial_permissions.role.group
|
||||
)
|
||||
LOGGER.debug(
|
||||
"Adding initial permission",
|
||||
initial_permission=permission,
|
||||
subject=assign_to,
|
||||
object=instance,
|
||||
)
|
||||
assign_perm(permission, assign_to, instance)
|
||||
|
||||
@@ -266,7 +266,6 @@ MIDDLEWARE = [
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"authentik.core.middleware.ImpersonateMiddleware",
|
||||
"authentik.rbac.middleware.InitialPermissionsMiddleware",
|
||||
]
|
||||
MIDDLEWARE_LAST = [
|
||||
"django_prometheus.middleware.PrometheusAfterMiddleware",
|
||||
|
||||
@@ -15,10 +15,18 @@ from authentik.lib.config import CONFIG
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
from authentik.stages.authenticator.models import SideChannelDevice
|
||||
from authentik.stages.email.models import EmailTemplates
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
|
||||
|
||||
class EmailTemplates(models.TextChoices):
|
||||
"""Templates used for rendering the Email"""
|
||||
|
||||
EMAIL_OTP = (
|
||||
"email/email_otp.html",
|
||||
_("Email OTP"),
|
||||
) # nosec
|
||||
|
||||
|
||||
class AuthenticatorEmailStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
"""Use Email-based authentication instead of authenticator-based."""
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -32,14 +32,6 @@ class EmailTemplates(models.TextChoices):
|
||||
"email/account_confirmation.html",
|
||||
_("Account Confirmation"),
|
||||
)
|
||||
EMAIL_OTP = (
|
||||
"email/email_otp.html",
|
||||
_("Email OTP"),
|
||||
) # nosec
|
||||
EVENT_NOTIFICATION = (
|
||||
"email/event_notification.html",
|
||||
_("Event Notification"),
|
||||
)
|
||||
|
||||
|
||||
def get_template_choices():
|
||||
|
||||
@@ -54,7 +54,7 @@ class TestEmailStageTemplates(FlowTestCase):
|
||||
chmod(file2, 0o000) # Remove all permissions so we can't read the file
|
||||
choices = get_template_choices()
|
||||
self.assertEqual(choices[-1][0], Path(file).name)
|
||||
self.assertEqual(len(choices), 5)
|
||||
self.assertEqual(len(choices), 3)
|
||||
unlink(file)
|
||||
unlink(file2)
|
||||
|
||||
|
||||
@@ -6753,15 +6753,6 @@
|
||||
"title": "Webhook mapping headers",
|
||||
"description": "Configure additional headers to be sent. Mapping should return a dictionary of key-value pairs"
|
||||
},
|
||||
"email_subject_prefix": {
|
||||
"type": "string",
|
||||
"title": "Email subject prefix"
|
||||
},
|
||||
"email_template": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Email template"
|
||||
},
|
||||
"send_once": {
|
||||
"type": "boolean",
|
||||
"title": "Send once",
|
||||
|
||||
2
go.mod
2
go.mod
@@ -29,7 +29,7 @@ require (
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2025100.2
|
||||
goauthentik.io/api/v3 v3.2025100.1
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.16.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -185,8 +185,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
goauthentik.io/api/v3 v3.2025100.2 h1:OF8qEpn6PzZFlB16RzL51RSIyFOY234gAWfd8/kjzhc=
|
||||
goauthentik.io/api/v3 v3.2025100.2/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
|
||||
goauthentik.io/api/v3 v3.2025100.1 h1:xOMnQ2j1MtrYJlO+8bHJ8MdFPyymqTZcLQ+5PTspdqA=
|
||||
goauthentik.io/api/v3 v3.2025100.1/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
|
||||
23
lifecycle/ak
23
lifecycle/ak
@@ -68,33 +68,10 @@ function prepare_debug {
|
||||
chown authentik:authentik /unittest.xml
|
||||
}
|
||||
|
||||
function migrate_container_change_root_dir {
|
||||
# With authentik 2025.10 we're moving the root directory of the authentik app
|
||||
# into /ak-root, mainly to not clutter the root filesystem of the container
|
||||
# and to make it possible to use devcontainers in the future.
|
||||
# In most installs this migration isn't required as no files are mounted into
|
||||
# these directories, however it is used if scripts are overwritten from the outside
|
||||
# or more commonly the flow background image is overwritten in `/web`
|
||||
# Check if we're in a container
|
||||
if [ ! -d /ak-root ]; then
|
||||
return
|
||||
fi
|
||||
if [ -d /authentik ]; then
|
||||
log "Legacy /authentik folder exist, migrating files"
|
||||
cp -rp /authentik/* /ak-root/authentik
|
||||
fi
|
||||
if [ ! -d /web ]; then
|
||||
log "Legacy /web folder exist, migrating files"
|
||||
cp -rp /web/* /ak-root/web
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "$(python -m authentik.lib.config debugger 2>/dev/null)" == "True" ]]; then
|
||||
prepare_debug
|
||||
fi
|
||||
|
||||
migrate_container_change_root_dir
|
||||
|
||||
if [[ "$1" == "server" ]]; then
|
||||
set_mode "server"
|
||||
run_authentik
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-08-18 00:11+0000\n"
|
||||
"POT-Creation-Date: 2025-08-11 00:12+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -790,12 +790,6 @@ msgstr ""
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"Only send notification once, for example when sending a webhook into a chat "
|
||||
"channel."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"Customize the body of the request. Mapping should return data that is JSON-"
|
||||
@@ -808,6 +802,12 @@ msgid ""
|
||||
"of key-value pairs"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"Only send notification once, for example when sending a webhook into a chat "
|
||||
"channel."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid "Severity"
|
||||
msgstr ""
|
||||
@@ -2905,6 +2905,10 @@ msgstr ""
|
||||
msgid "Duo Devices"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
msgid "Email OTP"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
#: authentik/stages/email/models.py
|
||||
msgid ""
|
||||
@@ -3265,14 +3269,6 @@ msgstr ""
|
||||
msgid "Account Confirmation"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Email OTP"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Event Notification"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid ""
|
||||
"The time window used to count recent account recovery attempts. If the "
|
||||
|
||||
@@ -904,9 +904,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
|
||||
"integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
|
||||
"version": "24.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
|
||||
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
6
packages/prettier-config/package-lock.json
generated
6
packages/prettier-config/package-lock.json
generated
@@ -385,9 +385,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
|
||||
"integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
|
||||
"version": "24.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
|
||||
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
14
schema.yml
14
schema.yml
@@ -49345,10 +49345,6 @@ components:
|
||||
nullable: true
|
||||
description: Configure additional headers to be sent. Mapping should return
|
||||
a dictionary of key-value pairs
|
||||
email_subject_prefix:
|
||||
type: string
|
||||
email_template:
|
||||
type: string
|
||||
send_once:
|
||||
type: boolean
|
||||
description: Only send notification once, for example when sending a webhook
|
||||
@@ -49388,11 +49384,6 @@ components:
|
||||
nullable: true
|
||||
description: Configure additional headers to be sent. Mapping should return
|
||||
a dictionary of key-value pairs
|
||||
email_subject_prefix:
|
||||
type: string
|
||||
email_template:
|
||||
type: string
|
||||
minLength: 1
|
||||
send_once:
|
||||
type: boolean
|
||||
description: Only send notification once, for example when sending a webhook
|
||||
@@ -54490,11 +54481,6 @@ components:
|
||||
nullable: true
|
||||
description: Configure additional headers to be sent. Mapping should return
|
||||
a dictionary of key-value pairs
|
||||
email_subject_prefix:
|
||||
type: string
|
||||
email_template:
|
||||
type: string
|
||||
minLength: 1
|
||||
send_once:
|
||||
type: boolean
|
||||
description: Only send notification once, for example when sending a webhook
|
||||
|
||||
@@ -10,7 +10,6 @@ from docker.types.healthcheck import Healthcheck
|
||||
|
||||
from authentik.core.tests.utils import create_test_flow
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.outposts.models import (
|
||||
DockerServiceConnection,
|
||||
Outpost,
|
||||
@@ -89,7 +88,6 @@ class TestProxyDocker(DockerTestCase, ChannelsLiveServerTestCase):
|
||||
pass
|
||||
|
||||
@pytest.mark.timeout(120)
|
||||
@CONFIG.patch("outposts.container_image_base", "ghcr.io/goauthentik/dev-proxy:gh-main")
|
||||
def test_docker_controller(self):
|
||||
"""test that deployment requires update"""
|
||||
controller = DockerController(self.outpost, self.service_connection)
|
||||
|
||||
88
web/package-lock.json
generated
88
web/package-lock.json
generated
@@ -23,7 +23,7 @@
|
||||
"@floating-ui/dom": "^1.7.3",
|
||||
"@formatjs/intl-listformat": "^7.7.11",
|
||||
"@fortawesome/fontawesome-free": "^7.0.0",
|
||||
"@goauthentik/api": "^2025.10.0-rc1-1755254677",
|
||||
"@goauthentik/api": "^2025.10.0-rc1-1755033451",
|
||||
"@goauthentik/core": "^1.0.0",
|
||||
"@goauthentik/esbuild-plugin-live-reload": "^1.1.0",
|
||||
"@goauthentik/eslint-config": "^1.0.5",
|
||||
@@ -42,7 +42,7 @@
|
||||
"@openlayers-elements/maps": "^0.4.0",
|
||||
"@patternfly/elements": "^4.1.0",
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@sentry/browser": "^10.5.0",
|
||||
"@sentry/browser": "^10.3.0",
|
||||
"@spotlightjs/spotlight": "^3.0.2",
|
||||
"@storybook/addon-docs": "^9.1.2",
|
||||
"@storybook/addon-links": "^9.1.2",
|
||||
@@ -52,7 +52,7 @@
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/guacamole-common-js": "^1.5.3",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/node": "^24.2.1",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||
@@ -126,7 +126,7 @@
|
||||
"@wdio/cli": "^9.19.1",
|
||||
"@wdio/spec-reporter": "^9.19.1",
|
||||
"@web/test-runner": "^0.20.2",
|
||||
"chromedriver": "^139.0.1"
|
||||
"chromedriver": "^139.0.0"
|
||||
}
|
||||
},
|
||||
"../packages/core": {
|
||||
@@ -1509,9 +1509,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@goauthentik/api": {
|
||||
"version": "2025.10.0-rc1-1755254677",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.10.0-rc1-1755254677.tgz",
|
||||
"integrity": "sha512-hq+xGPtwaeptEDn92Y40Yb4e7yL2KVvuqy2kWAZLPtr/r9ML82vzNYCfW6bFNPnopDRizjOBIzlD3gNP/2rs8Q=="
|
||||
"version": "2025.10.0-rc1-1755033451",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.10.0-rc1-1755033451.tgz",
|
||||
"integrity": "sha512-Opp94OCa15N8WG69NonHsHqsSeewIvqmgRasxWgCVtABzqv+LWPrFB3ChzW3W+W3to7DrgpsS/cBABYNY8W/eA=="
|
||||
},
|
||||
"node_modules/@goauthentik/core": {
|
||||
"resolved": "packages/core",
|
||||
@@ -4587,75 +4587,75 @@
|
||||
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="
|
||||
},
|
||||
"node_modules/@sentry-internal/browser-utils": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.5.0.tgz",
|
||||
"integrity": "sha512-4KIJdEj/8Ip9yqJleVSFe68r/U5bn5o/lYUwnFNEnDNxmpUbOlr7x3DXYuRFi1sfoMUxK9K1DrjnBkR7YYF00g==",
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.3.0.tgz",
|
||||
"integrity": "sha512-jKBoNMmxMgojzcpIsUqVk6XL6YiW0i8jtNdD9UdBKd8ExFpVkXhPuMdWB9f/5mVNK/9BnfI74eTiEVHZEkeZ6Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "10.5.0"
|
||||
"@sentry/core": "10.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/feedback": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.5.0.tgz",
|
||||
"integrity": "sha512-x79P4VZwUxb1EGZb9OQ5EEgrDWFCUlrbzHBwV/oocQA5Ss1SFz5u6cP5Ak7yJtILiJtdGzAyAoQOy4GKD13D4Q==",
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.3.0.tgz",
|
||||
"integrity": "sha512-HGvBoUwbj164I/66vrtUjHICuqwcY5RIGAAutD+H+EwhUROpFuzaIe9utIalhyU9CrTN/vFs4UYPWmeOpqg2lQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "10.5.0"
|
||||
"@sentry/core": "10.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.5.0.tgz",
|
||||
"integrity": "sha512-Dp4coE/nPzhFrYH3iVrpVKmhNJ1m/jGXMEDBCNg3wJZRszI41Hrj0jCAM0Y2S3Q4IxYOmFYaFbGtVpAznRyOHg==",
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.3.0.tgz",
|
||||
"integrity": "sha512-SVF7mMDW++LaeaONyxFUQ2Na3aMv6vyhv9V5Yb6yHWgPXI8NCW83mJ/MidHDD3yI0bccgTJUEmB4S0vBioafzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/browser-utils": "10.5.0",
|
||||
"@sentry/core": "10.5.0"
|
||||
"@sentry-internal/browser-utils": "10.3.0",
|
||||
"@sentry/core": "10.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay-canvas": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.5.0.tgz",
|
||||
"integrity": "sha512-5nrRKd5swefd9+sFXFZ/NeL3bz/VxBls3ubAQ3afak15FikkSyHq3oKRKpMOtDsiYKXE3Bc0y3rF5A+y3OXjIA==",
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.3.0.tgz",
|
||||
"integrity": "sha512-JGE1YmWb5LYhnaEgaYVMKj03FCQsuvALF2RXJx+Qe8pPwWtEWWBXMFEIt714mv4mO3YQxZnnxQhxFRuSJqXQfQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/replay": "10.5.0",
|
||||
"@sentry/core": "10.5.0"
|
||||
"@sentry-internal/replay": "10.3.0",
|
||||
"@sentry/core": "10.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/browser": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.5.0.tgz",
|
||||
"integrity": "sha512-o5pEJeZ/iZ7Fmaz2sIirThfnmSVNiP5ZYhacvcDi0qc288TmBbikCX3fXxq3xiSkhXfe1o5QIbNyovzfutyuVw==",
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.3.0.tgz",
|
||||
"integrity": "sha512-n0jROCST6XJhU7okSn04uRGFK4FjJZNjVR8nDSi/A6gU7VxVAs3iva5SUykXGFQKSVaXVE8kKjS6BtKfllulQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/browser-utils": "10.5.0",
|
||||
"@sentry-internal/feedback": "10.5.0",
|
||||
"@sentry-internal/replay": "10.5.0",
|
||||
"@sentry-internal/replay-canvas": "10.5.0",
|
||||
"@sentry/core": "10.5.0"
|
||||
"@sentry-internal/browser-utils": "10.3.0",
|
||||
"@sentry-internal/feedback": "10.3.0",
|
||||
"@sentry-internal/replay": "10.3.0",
|
||||
"@sentry-internal/replay-canvas": "10.3.0",
|
||||
"@sentry/core": "10.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.5.0.tgz",
|
||||
"integrity": "sha512-jTJ8NhZSKB2yj3QTVRXfCCngQzAOLThQUxCl9A7Mv+XF10tP7xbH/88MVQ5WiOr2IzcmrB9r2nmUe36BnMlLjA==",
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.3.0.tgz",
|
||||
"integrity": "sha512-FEFCqiGkzJrm6TNJvhyjhc4rpC1Kmo/abYOACRd6MLvm8GBz41eFFKxsNxGZAUA3Fk1tR2mPfXIHOJzS0ulVww==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -6633,9 +6633,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
|
||||
"integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
|
||||
"version": "24.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
|
||||
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.10.0"
|
||||
@@ -10711,9 +10711,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chromedriver": {
|
||||
"version": "139.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-139.0.1.tgz",
|
||||
"integrity": "sha512-K16mpBWhVMY/85k+1pf2ZuCOCDNJxSfr/OuIh7YbWoVIT+baNlyB6OvVh2WQw+MYQK2fg7CS0rMUE8GvMY6oCA==",
|
||||
"version": "139.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-139.0.0.tgz",
|
||||
"integrity": "sha512-4hzJhx2B2WbGP1160KaDkAIWMHmEBabldes1SrU75JwrzviYevopcVCxQw9B9Ykx+500ij06fv2retL5mK7DbA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
@@ -27835,7 +27835,7 @@
|
||||
"dependencies": {
|
||||
"@goauthentik/prettier-config": "^3.1.0",
|
||||
"@goauthentik/tsconfig": "^1.0.4",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/node": "^24.2.1",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
@@ -27915,7 +27915,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@goauthentik/api": "^2025.10.0-rc1-1755254677",
|
||||
"@goauthentik/api": "^2024.6.0-1719577139",
|
||||
"@goauthentik/core": "^1.0.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.6",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
"@floating-ui/dom": "^1.7.3",
|
||||
"@formatjs/intl-listformat": "^7.7.11",
|
||||
"@fortawesome/fontawesome-free": "^7.0.0",
|
||||
"@goauthentik/api": "^2025.10.0-rc1-1755254677",
|
||||
"@goauthentik/api": "^2025.10.0-rc1-1755033451",
|
||||
"@goauthentik/core": "^1.0.0",
|
||||
"@goauthentik/esbuild-plugin-live-reload": "^1.1.0",
|
||||
"@goauthentik/eslint-config": "^1.0.5",
|
||||
@@ -113,7 +113,7 @@
|
||||
"@openlayers-elements/maps": "^0.4.0",
|
||||
"@patternfly/elements": "^4.1.0",
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@sentry/browser": "^10.5.0",
|
||||
"@sentry/browser": "^10.3.0",
|
||||
"@spotlightjs/spotlight": "^3.0.2",
|
||||
"@storybook/addon-docs": "^9.1.2",
|
||||
"@storybook/addon-links": "^9.1.2",
|
||||
@@ -123,7 +123,7 @@
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/guacamole-common-js": "^1.5.3",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/node": "^24.2.1",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||
@@ -194,7 +194,7 @@
|
||||
"@wdio/cli": "^9.19.1",
|
||||
"@wdio/spec-reporter": "^9.19.1",
|
||||
"@web/test-runner": "^0.20.2",
|
||||
"chromedriver": "^139.0.1"
|
||||
"chromedriver": "^139.0.0"
|
||||
},
|
||||
"wireit": {
|
||||
"build": {
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"dependencies": {
|
||||
"@goauthentik/prettier-config": "^3.1.0",
|
||||
"@goauthentik/tsconfig": "^1.0.4",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/node": "^24.2.1",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"watch": "rollup -w -c rollup.config.mjs --bundleConfigAsCjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@goauthentik/api": "^2025.10.0-rc1-1755254677",
|
||||
"@goauthentik/api": "^2024.6.0-1719577139",
|
||||
"@goauthentik/core": "^1.0.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.6",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
NotificationWebhookMapping,
|
||||
PropertymappingsApi,
|
||||
PropertymappingsNotificationListRequest,
|
||||
StagesApi,
|
||||
TypeCreate,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
@@ -35,18 +33,10 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
|
||||
return transport;
|
||||
});
|
||||
}
|
||||
async load(): Promise<void> {
|
||||
this.templates = await new StagesApi(DEFAULT_CONFIG).stagesEmailTemplatesList();
|
||||
}
|
||||
|
||||
templates?: TypeCreate[];
|
||||
|
||||
@property({ type: Boolean })
|
||||
showWebhook = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
showEmail = false;
|
||||
|
||||
getSuccessMessage(): string {
|
||||
return this.instance
|
||||
? msg("Successfully updated transport.")
|
||||
@@ -66,28 +56,18 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
|
||||
}
|
||||
|
||||
onModeChange(mode: string | undefined): void {
|
||||
// Reset all flags
|
||||
this.showWebhook = false;
|
||||
this.showEmail = false;
|
||||
|
||||
switch (mode) {
|
||||
case NotificationTransportModeEnum.Webhook:
|
||||
case NotificationTransportModeEnum.WebhookSlack:
|
||||
this.showWebhook = true;
|
||||
break;
|
||||
case NotificationTransportModeEnum.Email:
|
||||
this.showEmail = true;
|
||||
break;
|
||||
case NotificationTransportModeEnum.Local:
|
||||
default:
|
||||
// Both flags remain false
|
||||
break;
|
||||
if (
|
||||
mode === NotificationTransportModeEnum.Webhook ||
|
||||
mode === NotificationTransportModeEnum.WebhookSlack
|
||||
) {
|
||||
this.showWebhook = true;
|
||||
} else {
|
||||
this.showWebhook = false;
|
||||
}
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`
|
||||
<ak-form-element-horizontal label=${msg("Name")} required name="name">
|
||||
return html` <ak-form-element-horizontal label=${msg("Name")} required name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.name)}"
|
||||
@@ -95,26 +75,6 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="sendOnce">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${this.instance?.sendOnce ?? false}
|
||||
/>
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pf-c-switch__label">${msg("Send once")}</span>
|
||||
</label>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Only send notification once, for example when sending a webhook into a chat channel.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Mode")} required name="mode">
|
||||
<ak-radio
|
||||
@change=${(ev: CustomEvent<{ value: NotificationTransportModeEnum }>) => {
|
||||
@@ -149,7 +109,7 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
|
||||
value="${this.instance?.webhookUrl || ""}"
|
||||
input-hint="code"
|
||||
?hidden=${!this.showWebhook}
|
||||
?required=${this.showWebhook}
|
||||
required
|
||||
>
|
||||
</ak-hidden-text-input>
|
||||
<ak-form-element-horizontal
|
||||
@@ -218,39 +178,26 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
|
||||
>
|
||||
</ak-search-select>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
?hidden=${!this.showEmail}
|
||||
?required=${this.showEmail}
|
||||
label=${msg("Email Subject Prefix")}
|
||||
name="emailSubjectPrefix"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${this.instance?.emailSubjectPrefix || "authentik Notification: "}"
|
||||
class="pf-c-form-control"
|
||||
?hidden=${!this.showEmail}
|
||||
?required=${this.showEmail}
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
?hidden=${!this.showEmail}
|
||||
?required=${this.showEmail}
|
||||
label=${msg("Email Template")}
|
||||
name="emailTemplate"
|
||||
>
|
||||
<select name="users" class="pf-c-form-control">
|
||||
${this.templates?.map((template) => {
|
||||
const selected =
|
||||
this.instance?.emailTemplate === template.name ||
|
||||
(!this.instance?.emailTemplate &&
|
||||
template.name === "email/event_notification.html");
|
||||
return html`<option value=${ifDefined(template.name)} ?selected=${selected}>
|
||||
${template.description}
|
||||
</option>`;
|
||||
})}
|
||||
</select>
|
||||
</ak-form-element-horizontal>
|
||||
`;
|
||||
<ak-form-element-horizontal name="sendOnce">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${this.instance?.sendOnce ?? false}
|
||||
/>
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pf-c-switch__label">${msg("Send once")}</span>
|
||||
</label>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Only send notification once, for example when sending a webhook into a chat channel.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
FlowsInstancesListDesignationEnum,
|
||||
FlowsInstancesListRequest,
|
||||
StagesApi,
|
||||
TypeCreate,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
@@ -34,12 +33,6 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
|
||||
return stage;
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
this.templates = await new StagesApi(DEFAULT_CONFIG).stagesEmailTemplatesList();
|
||||
}
|
||||
|
||||
templates?: TypeCreate[];
|
||||
|
||||
@property({ type: Boolean })
|
||||
showConnectionSettings = false;
|
||||
|
||||
@@ -269,28 +262,6 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Template")} name="template">
|
||||
<select
|
||||
class="pf-c-form-control"
|
||||
?disabled=${!this.templates || this.templates.length === 0}
|
||||
>
|
||||
${this.templates && this.templates.length > 0
|
||||
? this.templates.map((template: TypeCreate) => {
|
||||
return html`<option
|
||||
value="${template.name}"
|
||||
?selected=${this.instance?.template === template.name ||
|
||||
(!this.instance?.template &&
|
||||
template.name === "email/email_otp.html")}
|
||||
>
|
||||
${template.description}
|
||||
</option>`;
|
||||
})
|
||||
: html`<option value="">${msg("Loading templates...")}</option>`}
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Template used for the verification email.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>`;
|
||||
}
|
||||
|
||||
@@ -20,22 +20,6 @@ export const EscapeTrustPolicy = trustedTypes.createPolicy("authentik-escape", {
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Trusted types policy that removes all HTML content.
|
||||
*
|
||||
*
|
||||
* @returns {TrustedHTML} All remaining text content.
|
||||
*/
|
||||
export const StripHTMLTrustPolicy = trustedTypes.createPolicy("authentik-strip-html", {
|
||||
createHTML: (untrustedHTML: string) => {
|
||||
return DOMPurify.sanitize(untrustedHTML, {
|
||||
RETURN_TRUSTED_TYPE: false,
|
||||
ALLOWED_TAGS: [],
|
||||
ALLOWED_ATTR: [],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Trusted types policy, stripping all HTML content.
|
||||
*
|
||||
@@ -121,9 +105,7 @@ export function renderStaticHTMLUnsafe(untrustedHTML: unknown): string {
|
||||
|
||||
render(untrustedHTML, container);
|
||||
|
||||
const result = container.innerHTML
|
||||
// Remove all comments as they can interfere with the styles.
|
||||
.replaceAll("<!---->", "")
|
||||
.replaceAll(/<!--\?lit\$\d+\$-->/g, "");
|
||||
const result = container.innerHTML;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import "#elements/buttons/Dropdown";
|
||||
|
||||
import { StripHTMLTrustPolicy } from "#common/purify";
|
||||
import { rootInterface } from "#common/theme";
|
||||
|
||||
import { FormAssociated, FormAssociatedElement } from "#elements/forms/form-associated-element";
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { PaginatedResponse } from "#elements/table/Table";
|
||||
|
||||
import DjangoQL, { Introspections } from "@mrmarble/djangoql-completion";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, CSSResult, html, LitElement, nothing, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { createRef, ref, Ref } from "lit/directives/ref.js";
|
||||
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFSearchInput from "@patternfly/patternfly/components/SearchInput/search-input.css";
|
||||
@@ -29,26 +25,40 @@ export class QL extends DjangoQL {
|
||||
textareaResize() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array or length, return logical index of the element at the given delta.
|
||||
* This is effectively a modulo loop, allowing for positive and negative deltas.
|
||||
*/
|
||||
function torusIndex(lengthLike: number | ArrayLike<number>, delta: number): number {
|
||||
const length = typeof lengthLike === "number" ? lengthLike : lengthLike.length;
|
||||
@customElement("ak-search-ql")
|
||||
export class QLSearch extends AKElement {
|
||||
@property()
|
||||
value?: string;
|
||||
|
||||
if (delta < 0) {
|
||||
return (length + delta) % length;
|
||||
@query("[name=search]")
|
||||
searchElement?: HTMLTextAreaElement;
|
||||
|
||||
@state()
|
||||
menuOpen = false;
|
||||
|
||||
@property()
|
||||
onSearch?: (value: string) => void;
|
||||
|
||||
@state()
|
||||
selected?: number;
|
||||
|
||||
@state()
|
||||
cursorX: number = 0;
|
||||
|
||||
@state()
|
||||
cursorY: number = 0;
|
||||
|
||||
ql?: QL;
|
||||
canvas?: CanvasRenderingContext2D;
|
||||
|
||||
set apiResponse(value: PaginatedResponse<unknown> | undefined) {
|
||||
if (!value || !value.autocomplete || !this.ql) {
|
||||
return;
|
||||
}
|
||||
this.ql.loadIntrospections(value.autocomplete as unknown as Introspections);
|
||||
}
|
||||
|
||||
return ((delta % length) + length) % length;
|
||||
}
|
||||
|
||||
@customElement("ak-search-ql")
|
||||
export class QLSearch extends FormAssociatedElement<string> implements FormAssociated {
|
||||
declare anchorRef: Ref<HTMLTextAreaElement>;
|
||||
declare anchor: HTMLTextAreaElement | null;
|
||||
|
||||
public static styles: CSSResult[] = [
|
||||
static styles: CSSResult[] = [
|
||||
PFBase,
|
||||
PFFormControl,
|
||||
PFSearchInput,
|
||||
@@ -56,409 +66,207 @@ export class QLSearch extends FormAssociatedElement<string> implements FormAssoc
|
||||
::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ql.pf-c-form-control {
|
||||
--input-height: 2.25em;
|
||||
|
||||
height: var(--input-height);
|
||||
min-height: var(--input-height);
|
||||
max-height: calc(var(--input-height) * 6);
|
||||
font-family: monospace;
|
||||
resize: vertical;
|
||||
height: 2.25em;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: var(--pf-c-search-input__menu-item--hover--BackgroundColor);
|
||||
}
|
||||
|
||||
:host([theme="dark"]) {
|
||||
.pf-c-search-input__menu {
|
||||
--pf-c-search-input__menu--BackgroundColor: var(--ak-dark-background-light-ish);
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-c-search-input__menu-item {
|
||||
--pf-c-search-input__menu-item--Color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
||||
.pf-c-search-input__menu-item:hover {
|
||||
--pf-c-search-input__menu-item--BackgroundColor: var(
|
||||
--ak-dark-background-lighter
|
||||
);
|
||||
}
|
||||
|
||||
.pf-c-search-input__menu-list-item.selected {
|
||||
--pf-c-search-input__menu-item--hover--BackgroundColor: var(
|
||||
--ak-dark-background-light
|
||||
);
|
||||
}
|
||||
|
||||
.pf-c-search-input__text::before {
|
||||
border: 0;
|
||||
}
|
||||
:host([theme="dark"]) .pf-c-search-input__menu {
|
||||
--pf-c-search-input__menu--BackgroundColor: var(--ak-dark-background-light-ish);
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
:host([theme="dark"]) .pf-c-search-input__menu-item {
|
||||
--pf-c-search-input__menu-item--Color: var(--ak-dark-foreground);
|
||||
}
|
||||
:host([theme="dark"]) .pf-c-search-input__menu-item:hover {
|
||||
--pf-c-search-input__menu-item--BackgroundColor: var(--ak-dark-background-lighter);
|
||||
}
|
||||
:host([theme="dark"]) .pf-c-search-input__menu-list-item.selected {
|
||||
--pf-c-search-input__menu-item--hover--BackgroundColor: var(
|
||||
--ak-dark-background-light
|
||||
);
|
||||
}
|
||||
:host([theme="dark"]) .pf-c-search-input__text::before {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.pf-c-search-input__menu {
|
||||
position: fixed;
|
||||
min-width: 0;
|
||||
overflow-y: auto;
|
||||
max-height: 50vh;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
//#region Properties
|
||||
|
||||
@property({ type: Boolean })
|
||||
public open = false;
|
||||
|
||||
@property({ type: Number, attribute: false })
|
||||
public selectionIndex = -1;
|
||||
|
||||
#value = "";
|
||||
|
||||
@property({ type: String })
|
||||
public get value(): string {
|
||||
return this.#value;
|
||||
}
|
||||
|
||||
public set value(value: unknown) {
|
||||
const parsed = typeof value === "string" ? value : "";
|
||||
|
||||
this.setFormValue(parsed.trim(), parsed);
|
||||
this.#value = parsed;
|
||||
|
||||
if (this.anchor) {
|
||||
this.anchor.value = this.#value;
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region State
|
||||
|
||||
#menuRef = createRef<HTMLDivElement>();
|
||||
|
||||
#ql: QL | null = null;
|
||||
#ctx: OffscreenCanvasRenderingContext2D | null = null;
|
||||
#letterWidth = -1;
|
||||
#scrollContainer: HTMLElement | null = null;
|
||||
|
||||
public set apiResponse(value: PaginatedResponse<unknown> | undefined) {
|
||||
if (!value?.autocomplete || !this.#ql) {
|
||||
firstUpdated() {
|
||||
if (!this.searchElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#ql.loadIntrospections(value.autocomplete as unknown as Introspections);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
public override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.internals.ariaAutoComplete = "list";
|
||||
this.internals.role = "combobox";
|
||||
this.internals.ariaHasPopup = "listbox";
|
||||
|
||||
this.#scrollContainer =
|
||||
rootInterface<LitElement>().renderRoot.querySelector("#main-content");
|
||||
|
||||
this.#scrollContainer?.addEventListener("scroll", this.#updateDropdownPosition, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
this.tabIndex = 0;
|
||||
}
|
||||
|
||||
public override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
this.#scrollContainer?.removeEventListener("scroll", this.#updateDropdownPosition);
|
||||
}
|
||||
|
||||
public formStateRestoreCallback(state: string) {
|
||||
this.value = state;
|
||||
}
|
||||
|
||||
public formResetCallback() {
|
||||
this.value = "";
|
||||
}
|
||||
|
||||
public toJSON() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public override updated(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("open")) {
|
||||
this.internals.ariaExpanded = this.open ? "true" : "false";
|
||||
}
|
||||
|
||||
if (changedProperties.has("selectionIndex")) {
|
||||
const id = `suggestion-${this.selectionIndex}`;
|
||||
|
||||
this.setAttribute("aria-activedescendant", this.selectionIndex === -1 ? "" : id);
|
||||
|
||||
this.renderRoot.querySelector(`#${id}`)?.scrollIntoView({
|
||||
behavior: "auto",
|
||||
block: "nearest",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public override firstUpdated() {
|
||||
const textarea = this.anchorRef.value;
|
||||
|
||||
if (!textarea) return;
|
||||
|
||||
this.#ql = new QL({
|
||||
this.ql = new QL({
|
||||
completionEnabled: true,
|
||||
introspections: {
|
||||
current_model: "",
|
||||
models: {},
|
||||
},
|
||||
selector: textarea,
|
||||
selector: this.searchElement,
|
||||
autoResize: false,
|
||||
});
|
||||
|
||||
const canvas = new OffscreenCanvas(300, 150);
|
||||
this.#ctx = canvas.getContext("2d");
|
||||
|
||||
if (!this.#ctx) {
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) {
|
||||
console.error("authentik/ql: failed to get canvas context");
|
||||
return;
|
||||
}
|
||||
|
||||
this.#ctx.font = window.getComputedStyle(textarea).font;
|
||||
|
||||
// We need the width of a letter to measure x; we use a monospaced font but still
|
||||
// check the length for `m` as its the widest ASCII char
|
||||
const metrics = this.#ctx?.measureText("m");
|
||||
this.#letterWidth = Math.ceil(metrics?.width || 0) + 1;
|
||||
context.font = window.getComputedStyle(this.searchElement).font;
|
||||
this.canvas = context;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Completions
|
||||
|
||||
#refreshCompletions = () => {
|
||||
if (this.anchor) {
|
||||
this.value = this.anchor.value;
|
||||
}
|
||||
|
||||
if (!this.#ql) {
|
||||
refreshCompletions() {
|
||||
this.value = this.searchElement?.value;
|
||||
if (!this.ql) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#ql.generateSuggestions();
|
||||
|
||||
if (this.#ql.suggestions.length < 1 || this.#ql.loading) {
|
||||
this.open = false;
|
||||
this.ql.generateSuggestions();
|
||||
if (this.ql.suggestions.length < 1 || this.ql.loading) {
|
||||
this.menuOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
|
||||
this.menuOpen = true;
|
||||
this.updateDropdownPosition();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
requestAnimationFrame(this.#updateDropdownPosition);
|
||||
};
|
||||
|
||||
#updateDropdownPosition = () => {
|
||||
const anchor = this.anchorRef.value;
|
||||
const menu = this.#menuRef.value;
|
||||
|
||||
if (!anchor || !menu) return;
|
||||
|
||||
updateDropdownPosition() {
|
||||
if (!this.searchElement) {
|
||||
return;
|
||||
}
|
||||
const bcr = this.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(anchor);
|
||||
// We need the width of a letter to measure x; we use a monospaced font but still
|
||||
// check the length for `m` as its the widest ASCII char
|
||||
const metrics = this.canvas?.measureText("m");
|
||||
const letterWidth = Math.ceil(metrics?.width || 0) + 1;
|
||||
|
||||
// Mostly static variables for padding, font line-height and how many
|
||||
const lineHeight = parseInt(style.lineHeight, 10);
|
||||
const paddingTop = parseInt(style.paddingTop, 10);
|
||||
const paddingLeft = parseInt(style.paddingLeft, 10);
|
||||
const paddingRight = parseInt(style.paddingRight, 10);
|
||||
|
||||
const lineHeight = parseInt(window.getComputedStyle(this.searchElement).lineHeight, 10);
|
||||
const paddingTop = parseInt(window.getComputedStyle(this.searchElement).paddingTop, 10);
|
||||
const paddingLeft = parseInt(window.getComputedStyle(this.searchElement).paddingLeft, 10);
|
||||
const paddingRight = parseInt(window.getComputedStyle(this.searchElement).paddingRight, 10);
|
||||
const actualInnerWidth = bcr.width - paddingLeft - paddingRight;
|
||||
|
||||
let relX = 0;
|
||||
let relY = 1;
|
||||
let letterIndex = 0;
|
||||
|
||||
for (const word of anchor.value.split(" ")) {
|
||||
this.searchElement.value.split(" ").some((word, idx) => {
|
||||
letterIndex += word.length;
|
||||
const newRelX = relX + word.length * this.#letterWidth;
|
||||
|
||||
const newRelX = relX + word.length * letterWidth;
|
||||
if (newRelX > actualInnerWidth) {
|
||||
relY += 1;
|
||||
|
||||
if (letterIndex > anchor.selectionStart) {
|
||||
if (letterIndex > this.searchElement!.selectionStart) {
|
||||
relX =
|
||||
this.#letterWidth * word.length -
|
||||
(letterIndex - anchor.selectionStart) * this.#letterWidth;
|
||||
|
||||
break;
|
||||
letterWidth * word.length -
|
||||
(letterIndex - this.searchElement!.selectionStart) * letterWidth;
|
||||
return true;
|
||||
}
|
||||
|
||||
relX = word.length * this.#letterWidth;
|
||||
relX = word.length * letterWidth;
|
||||
} else {
|
||||
relX = newRelX + 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const x = bcr.x + paddingLeft + relX;
|
||||
const y = bcr.y + paddingTop + relY * lineHeight;
|
||||
|
||||
Object.assign(menu.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
} satisfies Partial<CSSStyleDeclaration>);
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Event Listeners
|
||||
|
||||
#keydownListener = (event: KeyboardEvent) => {
|
||||
this.#updateDropdownPosition();
|
||||
|
||||
const suggestionsLength = this.#ql?.suggestions.length;
|
||||
|
||||
if (event.key === "Enter" && !this.open && this.form) {
|
||||
const submitEvent = new SubmitEvent("submit", {
|
||||
submitter: this,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
this.form.dispatchEvent(submitEvent);
|
||||
this.cursorX = bcr.x + paddingLeft + relX;
|
||||
this.cursorY = bcr.y + paddingTop + relY * lineHeight;
|
||||
}
|
||||
|
||||
onKeyDown(ev: KeyboardEvent) {
|
||||
this.updateDropdownPosition();
|
||||
if (ev.key === "Enter" && ev.metaKey && this.onSearch && this.searchElement) {
|
||||
this.onSearch(this.searchElement?.value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.open && suggestionsLength) {
|
||||
if (this.selectionIndex === -1) {
|
||||
this.selectionIndex = 0;
|
||||
} else {
|
||||
this.selectionIndex = torusIndex(suggestionsLength, this.selectionIndex + 1);
|
||||
}
|
||||
|
||||
this.#refreshCompletions();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectionIndex = 0;
|
||||
this.#refreshCompletions();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.open) return;
|
||||
|
||||
switch (event.key) {
|
||||
if (!this.menuOpen) return;
|
||||
switch (ev.key) {
|
||||
case "ArrowUp":
|
||||
if (suggestionsLength) {
|
||||
if (this.selectionIndex === -1) {
|
||||
this.selectionIndex = suggestionsLength - 1;
|
||||
if (this.ql?.suggestions.length) {
|
||||
if (this.selected === undefined) {
|
||||
this.selected = this.ql?.suggestions.length - 1;
|
||||
} else if (this.selected === 0) {
|
||||
this.selected = undefined;
|
||||
} else {
|
||||
this.selectionIndex = torusIndex(
|
||||
suggestionsLength,
|
||||
this.selectionIndex - 1,
|
||||
);
|
||||
this.selected -= 1;
|
||||
}
|
||||
|
||||
this.#refreshCompletions();
|
||||
event.preventDefault();
|
||||
this.refreshCompletions();
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
break;
|
||||
case "ArrowDown":
|
||||
if (this.ql?.suggestions.length) {
|
||||
if (this.selected === undefined) {
|
||||
this.selected = 0;
|
||||
} else if (this.selected < this.ql?.suggestions.length - 1) {
|
||||
this.selected += 1;
|
||||
} else {
|
||||
this.selected = undefined;
|
||||
}
|
||||
this.refreshCompletions();
|
||||
ev.preventDefault();
|
||||
}
|
||||
break;
|
||||
case "Tab":
|
||||
if (this.selectionIndex) {
|
||||
this.#ql?.selectCompletion(this.selectionIndex);
|
||||
event.preventDefault();
|
||||
if (this.selected) {
|
||||
this.ql?.selectCompletion(this.selected);
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
return;
|
||||
break;
|
||||
case "Enter":
|
||||
// Technically this is a textarea, due to automatic multi-line feature,
|
||||
// but other than that it should look and behave like a normal input.
|
||||
// So expected behavior when pressing Enter is to submit the form,
|
||||
// not to add a new line.
|
||||
if (this.selectionIndex !== -1) {
|
||||
this.#ql?.selectCompletion(this.selectionIndex);
|
||||
this.selectionIndex = 0;
|
||||
if (this.selected !== undefined) {
|
||||
this.ql?.selectCompletion(this.selected);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
return;
|
||||
ev.preventDefault();
|
||||
break;
|
||||
case "Escape":
|
||||
this.open = false;
|
||||
return;
|
||||
this.menuOpen = false;
|
||||
break;
|
||||
case "Shift": // Shift
|
||||
case "Control": // Ctrl
|
||||
case "Alt": // Alt
|
||||
case "Meta": // Windows Key or Cmd on Mac
|
||||
// Control keys shouldn't trigger completion popup
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#blurListener = ({ relatedTarget }: FocusEvent) => {
|
||||
if (relatedTarget instanceof Node && this.renderRoot.contains(relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
};
|
||||
|
||||
#focusListener = () => {
|
||||
this.selectionIndex = this.selectionIndex === -1 ? 0 : this.selectionIndex;
|
||||
|
||||
this.#refreshCompletions();
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Render
|
||||
|
||||
protected renderMenu() {
|
||||
if (!this.open || !this.#ql) {
|
||||
renderMenu() {
|
||||
if (!this.menuOpen || !this.ql) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div ${ref(this.#menuRef)} class="pf-c-search-input__menu">
|
||||
<ul
|
||||
class="pf-c-search-input__menu-list"
|
||||
role="listbox"
|
||||
id="ql-suggestions"
|
||||
aria-label=${msg("Query suggestions")}
|
||||
>
|
||||
${this.#ql.suggestions.map((suggestion, idx) => {
|
||||
// Cast to string to sooth Lit Analyzer's primitive type rule.
|
||||
const label = `${StripHTMLTrustPolicy.createHTML(suggestion.suggestionText)}`;
|
||||
|
||||
<div
|
||||
class="pf-c-search-input__menu"
|
||||
style="left: ${this.cursorX}px; top: ${this.cursorY}px;"
|
||||
>
|
||||
<ul class="pf-c-search-input__menu-list">
|
||||
${this.ql.suggestions.map((suggestion, idx) => {
|
||||
return html`<li
|
||||
role="option"
|
||||
id="suggestion-${idx}"
|
||||
aria-selected=${this.selectionIndex === idx ? "true" : "false"}
|
||||
class="pf-c-search-input__menu-list-item ${this.selectionIndex === idx
|
||||
class="pf-c-search-input__menu-list-item ${this.selected === idx
|
||||
? "selected"
|
||||
: ""}"
|
||||
>
|
||||
<button
|
||||
class="pf-c-search-input__menu-item"
|
||||
type="button"
|
||||
aria-label=${label}
|
||||
@click=${() => {
|
||||
this.#ql?.selectCompletion(idx);
|
||||
this.#refreshCompletions();
|
||||
this.ql?.selectCompletion(idx);
|
||||
this.refreshCompletions();
|
||||
}}
|
||||
>
|
||||
<span class="pf-c-search-input__menu-item-text pf-m-monospace">
|
||||
${suggestion.text}</span
|
||||
<span class="pf-c-search-input__menu-item-text"
|
||||
>${suggestion.text}</span
|
||||
>
|
||||
</button>
|
||||
</li>`;
|
||||
@@ -468,33 +276,25 @@ export class QLSearch extends FormAssociatedElement<string> implements FormAssoc
|
||||
`;
|
||||
}
|
||||
|
||||
public override render(): TemplateResult {
|
||||
render(): TemplateResult {
|
||||
return html`<div class="pf-c-search-input">
|
||||
<div class="pf-c-search-input__bar">
|
||||
<span class="pf-c-search-input__text">
|
||||
<textarea
|
||||
${ref(this.anchorRef)}
|
||||
class="pf-c-form-control pf-m-monospace ql"
|
||||
class="pf-c-form-control ql"
|
||||
name="search"
|
||||
autocomplete="off"
|
||||
aria-controls="ql-suggestions"
|
||||
?required=${this.required}
|
||||
placeholder=${msg("Search...")}
|
||||
spellcheck="false"
|
||||
@input=${this.#refreshCompletions}
|
||||
@focus=${this.#focusListener}
|
||||
@blur=${this.#blurListener}
|
||||
@keydown=${this.#keydownListener}
|
||||
@input=${(ev: InputEvent) => this.refreshCompletions()}
|
||||
@keydown=${this.onKeyDown}
|
||||
>
|
||||
${ifDefined(this.#value)}</textarea
|
||||
${ifDefined(this.value)}</textarea
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
${this.renderMenu()}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
import { AKElement } from "#elements/Base";
|
||||
|
||||
import { Jsonifiable } from "type-fest";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { LitElement } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
import { createRef, Ref } from "lit/directives/ref.js";
|
||||
|
||||
/**
|
||||
* A subset of form associated {@linkcode ElementInternals} properties.
|
||||
*
|
||||
* @see {@linkcode FormAssociatedElement} for usage.
|
||||
*/
|
||||
export interface FormAssociated
|
||||
extends Pick<
|
||||
ElementInternals,
|
||||
| "form"
|
||||
| "validity"
|
||||
| "validationMessage"
|
||||
| "willValidate"
|
||||
| "labels"
|
||||
| "checkValidity"
|
||||
| "reportValidity"
|
||||
> {
|
||||
/**
|
||||
* The name of the input, provided to the form.
|
||||
*/
|
||||
readonly name: string | null;
|
||||
|
||||
/**
|
||||
* The type of the input, provided to the form.
|
||||
*/
|
||||
readonly type: string;
|
||||
|
||||
/**
|
||||
* Whether or not the input is required.
|
||||
*/
|
||||
required?: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the input is read-only.
|
||||
*/
|
||||
readonly?: boolean;
|
||||
|
||||
/**
|
||||
* A JSON representation of the value.
|
||||
*/
|
||||
toJSON(): Jsonifiable;
|
||||
}
|
||||
|
||||
export type FormValue = File | string | FormData | null;
|
||||
|
||||
/**
|
||||
* A base element which provides reactive properties and methods for interacting with a parent form.
|
||||
*
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals | MDN}
|
||||
*/
|
||||
export abstract class FormAssociatedElement<
|
||||
V extends FormValue = string,
|
||||
T extends Jsonifiable = V extends string ? V : Jsonifiable,
|
||||
S extends FormValue = V,
|
||||
>
|
||||
extends AKElement
|
||||
implements FormAssociated
|
||||
{
|
||||
static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true };
|
||||
|
||||
public static readonly formAssociated = true;
|
||||
|
||||
/**
|
||||
* The internals of the element.
|
||||
*
|
||||
* @protected
|
||||
* @see {@linkcode FormAssociated}
|
||||
*/
|
||||
protected internals = this.attachInternals();
|
||||
|
||||
//#region Reactive Properties
|
||||
|
||||
@property({ type: Boolean })
|
||||
public get required() {
|
||||
return this.internals.ariaRequired === "true";
|
||||
}
|
||||
|
||||
public set required(value: boolean) {
|
||||
this.internals.ariaRequired = value ? "true" : "false";
|
||||
}
|
||||
|
||||
@property({ type: Boolean, attribute: "readonly" })
|
||||
public get readOnly() {
|
||||
return this.internals.ariaReadOnly === "true";
|
||||
}
|
||||
|
||||
public set readOnly(value: boolean) {
|
||||
this.internals.ariaReadOnly = value ? "true" : "false";
|
||||
}
|
||||
|
||||
@property({ type: Boolean })
|
||||
public get disabled() {
|
||||
return this.internals.ariaDisabled === "true";
|
||||
}
|
||||
|
||||
public set disabled(value: boolean) {
|
||||
this.internals.ariaDisabled = value ? "true" : "false";
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Aliased Properties
|
||||
|
||||
public get form(): HTMLFormElement | null {
|
||||
return this.internals.form;
|
||||
}
|
||||
|
||||
public get name() {
|
||||
return this.getAttribute("name");
|
||||
}
|
||||
|
||||
public get type() {
|
||||
return this.localName;
|
||||
}
|
||||
|
||||
public get validity() {
|
||||
return this.internals.validity;
|
||||
}
|
||||
|
||||
public get validationMessage() {
|
||||
return this.internals.validationMessage;
|
||||
}
|
||||
|
||||
public get willValidate() {
|
||||
return this.internals.willValidate;
|
||||
}
|
||||
|
||||
public get labels() {
|
||||
return this.internals.labels;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Values
|
||||
|
||||
/**
|
||||
* A reference to an element that is focusable when validation fails.
|
||||
*/
|
||||
protected anchorRef: Ref<HTMLElement>;
|
||||
|
||||
/**
|
||||
* The element that is focusable when validation fails.
|
||||
*/
|
||||
declare protected anchor: HTMLElement | null;
|
||||
|
||||
/**
|
||||
* Set the value of the form.
|
||||
*
|
||||
* @param value The value visible to the form during submission.
|
||||
* @param state The value as provided by the user.
|
||||
*/
|
||||
protected setFormValue(value: V, state?: S) {
|
||||
this.internals.setFormValue(value, state);
|
||||
|
||||
if (this.required) {
|
||||
if (value) {
|
||||
this.internals.setValidity({});
|
||||
} else {
|
||||
this.internals.setValidity(
|
||||
{
|
||||
valueMissing: true,
|
||||
},
|
||||
msg("This field is required."),
|
||||
this.anchorRef.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract toJSON(): T;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Validation
|
||||
|
||||
public checkValidity = this.internals.checkValidity.bind(this.internals);
|
||||
public reportValidity = this.internals.reportValidity.bind(this.internals);
|
||||
|
||||
//#endregion
|
||||
|
||||
/**
|
||||
* Set the validity state of the form.
|
||||
*
|
||||
* @param flags The validity state flags.
|
||||
* @param message The validation message.
|
||||
* @param element The element to set the validity state on.
|
||||
*/
|
||||
protected setValidity(flags: ValidityStateFlags = {}, message?: string, element?: HTMLElement) {
|
||||
this.internals.setValidity(flags, message, element ?? this.anchorRef.value);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
this.anchorRef = createRef<HTMLElement>();
|
||||
|
||||
// We define the getter here to allow the base type to be extended,
|
||||
// letting the subclasses define a more accurate HTMLElement type.
|
||||
Object.defineProperty(this, "anchor", {
|
||||
get() {
|
||||
return this.anchorRef.value || null;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
41
web/src/elements/forms/types.d.ts
vendored
41
web/src/elements/forms/types.d.ts
vendored
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* @file Type definitions for form-associated elements.
|
||||
*
|
||||
* While these types are part of the HTML standard, they're not yet defined
|
||||
* in the TypeScript standard library, so we define them here.
|
||||
*
|
||||
* @expires 2026-01-01
|
||||
*/
|
||||
|
||||
/**
|
||||
* Callbacks for form-associated elements.
|
||||
*/
|
||||
interface HTMLElement {
|
||||
/**
|
||||
* A callback invoked when the browser autofilling sets a value.
|
||||
*/
|
||||
formStateRestoreCallback?(state: FormValue, mode: "autocomplete"): void;
|
||||
/**
|
||||
* A callback invoked when the browser restores a value from a previous session.
|
||||
*/
|
||||
formStateRestoreCallback?(state: FormValue, mode: "restore"): void;
|
||||
/**
|
||||
* A callback invoked when the browser restores a value from a previous session.
|
||||
*/
|
||||
formStateRestoreCallback?(state: FormValue, mode: "restore" | "autocomplete"): void;
|
||||
|
||||
/**
|
||||
* A callback that is invoked when the form is reset.
|
||||
*/
|
||||
formResetCallback?(): void;
|
||||
|
||||
/**
|
||||
* A callback that is invoked when the element's disabled state changes.
|
||||
*/
|
||||
formDisabledCallback?(disabled: boolean): void;
|
||||
|
||||
/**
|
||||
* A callback that is invoked when the element is associated with a form.
|
||||
*/
|
||||
formAssociatedCallback?(form: HTMLFormElement): void;
|
||||
}
|
||||
@@ -124,8 +124,8 @@ export abstract class Table<T extends object>
|
||||
@property({ type: String })
|
||||
public order?: string;
|
||||
|
||||
@property({ type: String, attribute: false })
|
||||
public search?: string;
|
||||
@property({ type: String })
|
||||
public search: string = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
public checkbox = false;
|
||||
@@ -547,11 +547,11 @@ export abstract class Table<T extends object>
|
||||
return html`<div class="pf-c-toolbar__group pf-m-search-filter ${isQL ? "ql" : ""}">
|
||||
<ak-table-search
|
||||
class="pf-c-toolbar__item pf-m-search-filter ${isQL ? "ql" : ""}"
|
||||
.defaultValue=${this.search}
|
||||
value=${ifDefined(this.search)}
|
||||
label=${ifDefined(this.searchLabel)}
|
||||
placeholder=${ifDefined(this.searchPlaceholder)}
|
||||
.onSearch=${this.#searchListener}
|
||||
.supportsQL=${this.supportsQL}
|
||||
?supportsQL=${this.supportsQL}
|
||||
.apiResponse=${this.data}
|
||||
>
|
||||
</ak-table-search>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { msg } from "@lit/localize";
|
||||
import { css, CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { createRef, ref } from "lit/directives/ref.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
@@ -17,23 +16,17 @@ import PFToolbar from "@patternfly/patternfly/components/Toolbar/toolbar.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
@customElement("ak-table-search")
|
||||
export class TableSearchForm extends WithLicenseSummary(AKElement) {
|
||||
@property({ type: String, reflect: false })
|
||||
public defaultValue?: string;
|
||||
export class TableSearch extends WithLicenseSummary(AKElement) {
|
||||
@property()
|
||||
public value?: string;
|
||||
|
||||
@property({ type: String })
|
||||
public label = msg("Table Search");
|
||||
|
||||
@property({ type: String })
|
||||
public placeholder = msg("Search...");
|
||||
|
||||
@property({ attribute: false })
|
||||
@property({ type: Boolean })
|
||||
public supportsQL: boolean = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public apiResponse?: PaginatedResponse<unknown>;
|
||||
|
||||
@property({ attribute: false })
|
||||
@property()
|
||||
public onSearch?: (value: string) => void;
|
||||
|
||||
static styles: CSSResult[] = [
|
||||
@@ -52,26 +45,25 @@ export class TableSearchForm extends WithLicenseSummary(AKElement) {
|
||||
`,
|
||||
];
|
||||
|
||||
#formRef = createRef<HTMLFormElement>();
|
||||
|
||||
public reset = (): void => {
|
||||
this.#formRef.value?.reset();
|
||||
|
||||
this.onSearch?.("");
|
||||
public reset = () => {
|
||||
if (!this.onSearch) return;
|
||||
this.value = "";
|
||||
this.onSearch("");
|
||||
};
|
||||
|
||||
#submitListener = (event: SubmitEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const form = this.#formRef.value;
|
||||
|
||||
if (!form || !this.onSearch) return;
|
||||
|
||||
form.reportValidity();
|
||||
if (!this.onSearch) return;
|
||||
|
||||
const form = event.target as HTMLFormElement;
|
||||
const data = new FormData(form);
|
||||
|
||||
const value = data.get("search")?.toString() ?? "";
|
||||
const value = data.get("search")?.toString().trim();
|
||||
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onSearch(value);
|
||||
};
|
||||
@@ -79,31 +71,27 @@ export class TableSearchForm extends WithLicenseSummary(AKElement) {
|
||||
renderInput(): TemplateResult {
|
||||
if (this.supportsQL && this.hasEnterpriseLicense) {
|
||||
return html`<ak-search-ql
|
||||
aria-label=${ifDefined(this.label)}
|
||||
name="search"
|
||||
required
|
||||
placeholder=${ifDefined(this.placeholder)}
|
||||
value=${ifDefined(this.defaultValue)}
|
||||
.apiResponse=${this.apiResponse}
|
||||
.value=${this.value}
|
||||
.onSearch=${(value: string) => {
|
||||
if (!this.onSearch) return;
|
||||
this.onSearch(value);
|
||||
}}
|
||||
name="search"
|
||||
></ak-search-ql>`;
|
||||
}
|
||||
|
||||
return html`<input
|
||||
aria-label=${ifDefined(this.label)}
|
||||
name="search"
|
||||
required
|
||||
placeholder=${ifDefined(this.placeholder)}
|
||||
value=${ifDefined(this.defaultValue)}
|
||||
class="pf-c-form-control"
|
||||
name="search"
|
||||
type="search"
|
||||
placeholder=${msg("Search...")}
|
||||
value="${ifDefined(this.value)}"
|
||||
/>`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<form
|
||||
${ref(this.#formRef)}
|
||||
class="pf-c-input-group"
|
||||
@submit=${this.#submitListener}
|
||||
>
|
||||
return html`<form class="pf-c-input-group" method="get" @submit=${this.#submitListener}>
|
||||
${this.renderInput()}
|
||||
<button
|
||||
aria-label=${msg("Clear search")}
|
||||
@@ -122,6 +110,6 @@ export class TableSearchForm extends WithLicenseSummary(AKElement) {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-table-search": TableSearchForm;
|
||||
"ak-table-search": TableSearch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ListenerController } from "#elements/utils/listenerController";
|
||||
import { randomId } from "#elements/utils/randomId";
|
||||
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
import { CaptchaHandler, CaptchaProvider, iframeTemplate } from "#flow/stages/captcha/shared";
|
||||
import { CaptchaHandler, iframeTemplate } from "#flow/stages/captcha/shared";
|
||||
|
||||
import { CaptchaChallenge, CaptchaChallengeResponseRequest } from "@goauthentik/api";
|
||||
|
||||
@@ -109,7 +109,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
//#region State
|
||||
|
||||
@state()
|
||||
protected activeHandler: CaptchaProvider | null = null;
|
||||
protected activeHandler: CaptchaHandler | null = null;
|
||||
|
||||
@state()
|
||||
protected error: string | null = null;
|
||||
@@ -265,12 +265,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
|
||||
//#endregion
|
||||
|
||||
/**
|
||||
* Mapping of captcha provider names to their respective JS API global.
|
||||
*
|
||||
* Note that this is a `Map` to ensure the preferred order of discovering provider globals.
|
||||
*/
|
||||
#handlers = new Map<CaptchaProvider, CaptchaHandler>([
|
||||
#handlers = new Map<string, CaptchaHandler>([
|
||||
[
|
||||
"grecaptcha",
|
||||
{
|
||||
@@ -420,7 +415,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Resizing
|
||||
//#region Listeners
|
||||
|
||||
#loadListener = () => {
|
||||
const iframe = this.#iframeRef.value;
|
||||
@@ -428,73 +423,17 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
|
||||
if (!iframe || !contentDocument) return;
|
||||
|
||||
let synchronizeHeight: () => void;
|
||||
const resizeListener: ResizeObserverCallback = () => {
|
||||
if (!this.#iframeRef) return;
|
||||
|
||||
if (this.activeHandler === CaptchaProvider.reCAPTCHA) {
|
||||
// reCAPTCHA's use of nested iframes prevents their internal resize observer from
|
||||
// reporting the correct height back to our iframe, so we have to do it ourselves.
|
||||
const target = contentDocument.getElementById("ak-container");
|
||||
|
||||
synchronizeHeight = () => {
|
||||
if (!this.#iframeRef) return;
|
||||
if (!target) return;
|
||||
|
||||
const target = contentDocument.getElementById("ak-container");
|
||||
this.iframeHeight = Math.round(target.clientHeight);
|
||||
};
|
||||
|
||||
if (!target) return;
|
||||
|
||||
const innerIFrame = contentDocument.querySelector<HTMLIFrameElement>(
|
||||
'iframe[style~="height:"]',
|
||||
);
|
||||
|
||||
const innerBottom = innerIFrame?.getBoundingClientRect().bottom ?? 0;
|
||||
|
||||
const actualHeight = Math.max(innerBottom, target.clientHeight);
|
||||
|
||||
this.iframeHeight = Math.round(actualHeight * 1.1);
|
||||
|
||||
if (innerIFrame?.parentElement) {
|
||||
innerIFrame.parentElement.style.height = `${actualHeight}px`;
|
||||
}
|
||||
};
|
||||
|
||||
// We watch for any newly inserted iframes, as they may alter the height
|
||||
// of the parent iframe...
|
||||
const mutationObserver = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== "childList") continue;
|
||||
|
||||
for (const node of mutation.addedNodes as NodeListOf<HTMLElement>) {
|
||||
if (node.tagName !== "IFRAME") continue;
|
||||
|
||||
// And then resize the iframe to match the new size.
|
||||
//
|
||||
// This doesn't fix the issue entirely since the challenge frame
|
||||
// doesn't yet know the correct height, but at least the user can
|
||||
// try to load the challenge again with the correct height.
|
||||
|
||||
resizeObserver.observe(node as HTMLIFrameElement);
|
||||
|
||||
requestAnimationFrame(synchronizeHeight);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mutationObserver.observe(contentDocument.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
} else {
|
||||
synchronizeHeight = () => {
|
||||
if (!this.#iframeRef) return;
|
||||
|
||||
const target = contentDocument.getElementById("ak-container");
|
||||
|
||||
if (!target) return;
|
||||
|
||||
this.iframeHeight = Math.round(target.clientHeight);
|
||||
};
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(synchronizeHeight);
|
||||
const resizeObserver = new ResizeObserver(resizeListener);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
resizeObserver.observe(contentDocument.body);
|
||||
@@ -503,26 +442,22 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
});
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Loading
|
||||
|
||||
#scriptLoadListener = async (): Promise<void> => {
|
||||
console.debug("authentik/stages/captcha: script loaded");
|
||||
|
||||
this.error = null;
|
||||
this.#iframeLoaded = false;
|
||||
|
||||
for (const name of this.#handlers.keys()) {
|
||||
for (const [name, handler] of this.#handlers) {
|
||||
if (!Object.hasOwn(window, name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.#run(name);
|
||||
await this.#run(handler);
|
||||
console.debug(`authentik/stages/captcha[${name}]: handler succeeded`);
|
||||
|
||||
this.activeHandler = name;
|
||||
this.activeHandler = handler;
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
@@ -534,9 +469,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
}
|
||||
};
|
||||
|
||||
async #run(captchaProvider: CaptchaProvider) {
|
||||
const handler = this.#handlers.get(captchaProvider)!;
|
||||
|
||||
async #run(handler: CaptchaHandler) {
|
||||
if (this.challenge.interactive) {
|
||||
const iframe = this.#iframeRef.value;
|
||||
|
||||
@@ -545,44 +478,18 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
return;
|
||||
}
|
||||
|
||||
const { contentDocument } = iframe;
|
||||
|
||||
if (!contentDocument) {
|
||||
console.debug(
|
||||
`authentik/stages/captcha: No iframe content window found, skipping.`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug(`authentik/stages/captcha: Rendering interactive.`);
|
||||
|
||||
const captchaElement = handler.interactive();
|
||||
const template = iframeTemplate(captchaElement, {
|
||||
challengeURL: this.challenge.jsUrl,
|
||||
theme: this.activeTheme,
|
||||
});
|
||||
const template = iframeTemplate(captchaElement, this.challenge.jsUrl);
|
||||
|
||||
if (captchaProvider === CaptchaProvider.reCAPTCHA) {
|
||||
// reCAPTCHA's domain verification can't seem to penetrate the true origin
|
||||
// of the page when loaded from a blob URL, likely due to their double-nested
|
||||
// iframe structure.
|
||||
// We fallback to the deprecated `document.write` to get around this.
|
||||
this.#iframeSource = "about:blank";
|
||||
contentDocument.open();
|
||||
contentDocument.write(template);
|
||||
contentDocument.close();
|
||||
URL.revokeObjectURL(this.#iframeSource);
|
||||
|
||||
// this.#loadListener();
|
||||
} else {
|
||||
URL.revokeObjectURL(this.#iframeSource);
|
||||
const url = URL.createObjectURL(new Blob([template], { type: "text/html" }));
|
||||
|
||||
const url = URL.createObjectURL(new Blob([template], { type: "text/html" }));
|
||||
this.#iframeSource = url;
|
||||
|
||||
this.#iframeSource = url;
|
||||
|
||||
iframe.src = url;
|
||||
}
|
||||
iframe.src = url;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
import type { ResolvedUITheme } from "#common/theme";
|
||||
|
||||
import { createDocumentTemplate } from "#elements/utils/iframe";
|
||||
|
||||
import { html, TemplateResult } from "lit";
|
||||
|
||||
/**
|
||||
* Mapping of captcha provider names to their respective JS API global.
|
||||
*/
|
||||
export const CaptchaProvider = {
|
||||
reCAPTCHA: "grecaptcha",
|
||||
hCaptcha: "hcaptcha",
|
||||
Turnstile: "turnstile",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
export type CaptchaProvider = (typeof CaptchaProvider)[keyof typeof CaptchaProvider];
|
||||
|
||||
export interface CaptchaHandler {
|
||||
interactive(): TemplateResult;
|
||||
execute(): Promise<void>;
|
||||
@@ -22,29 +9,6 @@ export interface CaptchaHandler {
|
||||
refresh(): Promise<void>;
|
||||
}
|
||||
|
||||
const ThemeColor = {
|
||||
dark: "#18191a",
|
||||
light: "#ffffff",
|
||||
} as const satisfies Record<ResolvedUITheme, string>;
|
||||
|
||||
export function themeMeta(theme: ResolvedUITheme) {
|
||||
switch (theme) {
|
||||
case "dark":
|
||||
return html`
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<meta name="theme-color" content=${ThemeColor.dark} />
|
||||
`;
|
||||
case "light":
|
||||
return html` <meta name="color-scheme" content="light" />
|
||||
<meta name="theme-color" content=${ThemeColor.light} />`;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IFrameTemplateInit {
|
||||
challengeURL: string;
|
||||
theme: ResolvedUITheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* A container iframe for a hosted Captcha, with an event emitter to monitor
|
||||
* when the Captcha forces a resize.
|
||||
@@ -53,17 +17,10 @@ export interface IFrameTemplateInit {
|
||||
* margin, adding 2rem of height to our container adds padding and prevents scrollbars
|
||||
* or hidden rendering.
|
||||
*/
|
||||
export function iframeTemplate(
|
||||
children: TemplateResult,
|
||||
{ challengeURL, theme }: IFrameTemplateInit,
|
||||
) {
|
||||
export function iframeTemplate(children: TemplateResult, challengeURL: string): string {
|
||||
return createDocumentTemplate({
|
||||
head: html`
|
||||
<meta charset="UTF-8" />
|
||||
head: html`<meta charset="UTF-8" />
|
||||
|
||||
${themeMeta(theme)}
|
||||
`,
|
||||
body: html`
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
@@ -86,11 +43,6 @@ export function iframeTemplate(
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
background: ${ThemeColor[theme]};
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -106,9 +58,8 @@ export function iframeTemplate(
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
${children}
|
||||
<script onload="loadListener()" src="${challengeURL}"></script>
|
||||
`,
|
||||
</style>`,
|
||||
body: html`${children}
|
||||
<script onload="loadListener()" src="${challengeURL}"></script> `,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10037,24 +10037,6 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
|
||||
</trans-unit>
|
||||
<trans-unit id="s669b18c6d2d9c95b">
|
||||
<source>None</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2de7890b100c4f4c">
|
||||
<source>Flags</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf0b34d32a602aee8">
|
||||
<source>Modify flags to opt into new authentik behaviours early.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s503217f10ebdc63e">
|
||||
<source>Loading templates...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6891d230751dd7bf">
|
||||
<source>Template used for the verification email.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf1fc82d7ae3f4d81">
|
||||
<source>Email Subject Prefix</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -7917,24 +7917,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s669b18c6d2d9c95b">
|
||||
<source>None</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2de7890b100c4f4c">
|
||||
<source>Flags</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf0b34d32a602aee8">
|
||||
<source>Modify flags to opt into new authentik behaviours early.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s503217f10ebdc63e">
|
||||
<source>Loading templates...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6891d230751dd7bf">
|
||||
<source>Template used for the verification email.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf1fc82d7ae3f4d81">
|
||||
<source>Email Subject Prefix</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -10085,24 +10085,6 @@ El valor de este campo se compara con el atributo de pertenencia del usuario.</t
|
||||
</trans-unit>
|
||||
<trans-unit id="s669b18c6d2d9c95b">
|
||||
<source>None</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2de7890b100c4f4c">
|
||||
<source>Flags</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf0b34d32a602aee8">
|
||||
<source>Modify flags to opt into new authentik behaviours early.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s503217f10ebdc63e">
|
||||
<source>Loading templates...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6891d230751dd7bf">
|
||||
<source>Template used for the verification email.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf1fc82d7ae3f4d81">
|
||||
<source>Email Subject Prefix</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -10108,24 +10108,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
<trans-unit id="s669b18c6d2d9c95b">
|
||||
<source>None</source>
|
||||
<target>None</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2de7890b100c4f4c">
|
||||
<source>Flags</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf0b34d32a602aee8">
|
||||
<source>Modify flags to opt into new authentik behaviours early.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s503217f10ebdc63e">
|
||||
<source>Loading templates...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6891d230751dd7bf">
|
||||
<source>Template used for the verification email.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf1fc82d7ae3f4d81">
|
||||
<source>Email Subject Prefix</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -10041,24 +10041,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s669b18c6d2d9c95b">
|
||||
<source>None</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2de7890b100c4f4c">
|
||||
<source>Flags</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf0b34d32a602aee8">
|
||||
<source>Modify flags to opt into new authentik behaviours early.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s503217f10ebdc63e">
|
||||
<source>Loading templates...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6891d230751dd7bf">
|
||||
<source>Template used for the verification email.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf1fc82d7ae3f4d81">
|
||||
<source>Email Subject Prefix</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -9363,24 +9363,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s669b18c6d2d9c95b">
|
||||
<source>None</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2de7890b100c4f4c">
|
||||
<source>Flags</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf0b34d32a602aee8">
|
||||
<source>Modify flags to opt into new authentik behaviours early.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s503217f10ebdc63e">
|
||||
<source>Loading templates...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6891d230751dd7bf">
|
||||
<source>Template used for the verification email.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf1fc82d7ae3f4d81">
|
||||
<source>Email Subject Prefix</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -9269,24 +9269,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
</trans-unit>
|
||||
<trans-unit id="s669b18c6d2d9c95b">
|
||||
<source>None</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2de7890b100c4f4c">
|
||||
<source>Flags</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf0b34d32a602aee8">
|
||||
<source>Modify flags to opt into new authentik behaviours early.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s503217f10ebdc63e">
|
||||
<source>Loading templates...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6891d230751dd7bf">
|
||||
<source>Template used for the verification email.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf1fc82d7ae3f4d81">
|
||||
<source>Email Subject Prefix</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -9684,24 +9684,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
|
||||
</trans-unit>
|
||||
<trans-unit id="s669b18c6d2d9c95b">
|
||||
<source>None</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2de7890b100c4f4c">
|
||||
<source>Flags</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf0b34d32a602aee8">
|
||||
<source>Modify flags to opt into new authentik behaviours early.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s503217f10ebdc63e">
|
||||
<source>Loading templates...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6891d230751dd7bf">
|
||||
<source>Template used for the verification email.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf1fc82d7ae3f4d81">
|
||||
<source>Email Subject Prefix</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -9693,22 +9693,4 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s669b18c6d2d9c95b">
|
||||
<source>None</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2de7890b100c4f4c">
|
||||
<source>Flags</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf0b34d32a602aee8">
|
||||
<source>Modify flags to opt into new authentik behaviours early.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s503217f10ebdc63e">
|
||||
<source>Loading templates...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6891d230751dd7bf">
|
||||
<source>Template used for the verification email.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf1fc82d7ae3f4d81">
|
||||
<source>Email Subject Prefix</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
</body></file></xliff>
|
||||
|
||||
@@ -9775,24 +9775,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s669b18c6d2d9c95b">
|
||||
<source>None</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2de7890b100c4f4c">
|
||||
<source>Flags</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf0b34d32a602aee8">
|
||||
<source>Modify flags to opt into new authentik behaviours early.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s503217f10ebdc63e">
|
||||
<source>Loading templates...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6891d230751dd7bf">
|
||||
<source>Template used for the verification email.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf1fc82d7ae3f4d81">
|
||||
<source>Email Subject Prefix</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -9748,24 +9748,6 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
|
||||
</trans-unit>
|
||||
<trans-unit id="s669b18c6d2d9c95b">
|
||||
<source>None</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2de7890b100c4f4c">
|
||||
<source>Flags</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf0b34d32a602aee8">
|
||||
<source>Modify flags to opt into new authentik behaviours early.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s503217f10ebdc63e">
|
||||
<source>Loading templates...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6891d230751dd7bf">
|
||||
<source>Template used for the verification email.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf1fc82d7ae3f4d81">
|
||||
<source>Email Subject Prefix</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -6544,24 +6544,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s669b18c6d2d9c95b">
|
||||
<source>None</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2de7890b100c4f4c">
|
||||
<source>Flags</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf0b34d32a602aee8">
|
||||
<source>Modify flags to opt into new authentik behaviours early.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s503217f10ebdc63e">
|
||||
<source>Loading templates...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6891d230751dd7bf">
|
||||
<source>Template used for the verification email.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf1fc82d7ae3f4d81">
|
||||
<source>Email Subject Prefix</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -10042,24 +10042,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s669b18c6d2d9c95b">
|
||||
<source>None</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2de7890b100c4f4c">
|
||||
<source>Flags</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf0b34d32a602aee8">
|
||||
<source>Modify flags to opt into new authentik behaviours early.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s503217f10ebdc63e">
|
||||
<source>Loading templates...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6891d230751dd7bf">
|
||||
<source>Template used for the verification email.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf1fc82d7ae3f4d81">
|
||||
<source>Email Subject Prefix</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -7623,24 +7623,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s669b18c6d2d9c95b">
|
||||
<source>None</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2de7890b100c4f4c">
|
||||
<source>Flags</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf0b34d32a602aee8">
|
||||
<source>Modify flags to opt into new authentik behaviours early.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s503217f10ebdc63e">
|
||||
<source>Loading templates...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6891d230751dd7bf">
|
||||
<source>Template used for the verification email.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf1fc82d7ae3f4d81">
|
||||
<source>Email Subject Prefix</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -9343,24 +9343,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s669b18c6d2d9c95b">
|
||||
<source>None</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2de7890b100c4f4c">
|
||||
<source>Flags</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf0b34d32a602aee8">
|
||||
<source>Modify flags to opt into new authentik behaviours early.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s503217f10ebdc63e">
|
||||
<source>Loading templates...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6891d230751dd7bf">
|
||||
<source>Template used for the verification email.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf1fc82d7ae3f4d81">
|
||||
<source>Email Subject Prefix</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -6,4 +6,3 @@ coverage
|
||||
node_modules
|
||||
help
|
||||
static
|
||||
scripts/docsmg/target/
|
||||
|
||||
@@ -34,7 +34,7 @@ tautulli_password: password
|
||||
|
||||
Add all Tautulli users to the Group. You should also create a Group Membership Policy to limit access to the application.
|
||||
|
||||
## Tautulli configuration
|
||||
## Tautulli Setup
|
||||
|
||||
- Internal host
|
||||
|
||||
@@ -46,11 +46,11 @@ Add all Tautulli users to the Group. You should also create a Group Membership P
|
||||
|
||||
Set this to the external URL you will be accessing Tautulli from.
|
||||
|
||||
Basic authentication settings have been removed from the UI and are now available in the `config.ini` file. For basic auth to work, do the following:
|
||||
Basic authentication settings have been removed from the UI and are now available in config.ini. For basic auth to work, do the following:
|
||||
|
||||
1. Close Tautulli.
|
||||
1. shut down Tautulli
|
||||
|
||||
2. Set the following in the config file:
|
||||
2. Set/Change the following in the config file:
|
||||
|
||||
```yaml
|
||||
http_basic_auth = 1
|
||||
@@ -59,11 +59,11 @@ http_hashed_password = 1
|
||||
http_password = `<enter your password>`
|
||||
```
|
||||
|
||||
3. Save the changes and then restart Tautulli.
|
||||
3. Save the changes and then restart Tautulli
|
||||
|
||||
4. Afterwards, you need to deploy an Outpost in front of Tautulli, as described [here](https://docs.goauthentik.io/docs/add-secure-apps/outposts/).
|
||||
Note: You can use the embedded outpost and simply add Tatulli to the list of applications to use.
|
||||
4. Afterwards, you need to deploy an Outpost in front of Tautulli, as described [here](https://docs.goauthentik.io/docs/add-secure-apps/outposts/)
|
||||
Note: You can use the embedded outpost and simply add Tatulli to the list of applications to use
|
||||
|
||||
## Configuration verification
|
||||
|
||||
To confirm that authentik is properly configured with Tautulli, log out and log back in via authentik (you can use private browsing mode to validate) and navigate to Tautulli. You should bypass the login prompt if setup correctly.
|
||||
To confirm that authentik is properly configured with `Tautulli`, log out and log back in via authentik (you can use private browsing mode to validate) and navigate to Tautulli. You should bypass the login prompt if setup correctly.
|
||||
|
||||
@@ -6,7 +6,7 @@ support_level: community
|
||||
|
||||
## What is Headscale
|
||||
|
||||
> Headscale is an open source alternative to the Tailscale coordination server and can be self-hosted for a single tailnet. Headscale is a re-implemented version of the Tailscale coordination server, developed independently and completely separate from Tailscale, with its own independent community of users and developers.
|
||||
> [Headscale](https://github.com/juanfont/headscale) is an open source alternative to the Tailscale coordination server and can be self-hosted for a single tailnet. Headscale is a re-implemented version of the Tailscale coordination server, developed independently and completely separate from Tailscale, with its own independent community of users and developers.
|
||||
>
|
||||
> -- https://headscale.net
|
||||
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
title: Integrate with Vaultwarden
|
||||
sidebar_label: Vaultwarden
|
||||
support_level: community
|
||||
---
|
||||
|
||||
## What is Vaultwarden
|
||||
|
||||
> Vaultwarden is an alternative server implementation of the Bitwarden Client API, written in Rust and compatible with official Bitwarden clients, perfect for self-hosted deployment where running the official resource-heavy service might not be ideal.
|
||||
>
|
||||
> -- https://github.com/dani-garcia/vaultwarden
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders are used in this guide:
|
||||
|
||||
- `vaultwarden.company` is the FQDN of the Vaultwarden installation.
|
||||
- `authentik.company` is the FQDN of the authentik installation.
|
||||
|
||||
:::note
|
||||
This documentation lists only the settings that you need to change from their default values. Be aware that any changes other than those explicitly mentioned in this guide could cause issues accessing your application.
|
||||
:::
|
||||
|
||||
## authentik configuration
|
||||
|
||||
To support the integration of Vaultwarden with authentik, you need to create an application/provider pair in authentik.
|
||||
|
||||
### Create an application and provider in authentik
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Applications** and click **Create with Provider** to create an application and provider pair. (Alternatively you can first create a provider separately, then create the application and connect it with the provider.)
|
||||
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings.
|
||||
- **Choose a Provider type**: select **OAuth2/OpenID Connect** as the provider type.
|
||||
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
|
||||
- Note the **Client ID**,**Client Secret**, and **slug** values because they will be required later.
|
||||
- Set a `Strict` redirect URI to `https://vaultwarden.company/identity/connect/oidc-signin`.
|
||||
- Select any available signing key.
|
||||
- Under **Advanced protocol settings**:
|
||||
- Set **Access token validity** to more than 5 minutes.
|
||||
- Ensure the `offline_access` scope mapping is available by adding `authentik default OAuth Mapping: OpenID 'offline_access'` to the selected scopes.
|
||||
- **Configure Bindings** _(optional)_: you can create a [binding](/docs/add-secure-apps/flows-stages/bindings/) (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page.
|
||||
|
||||
3. Click **Submit** to save the new application and provider.
|
||||
|
||||
## Vaultwarden configuration
|
||||
|
||||
To configure authentik with Vaultwarden, you must add the following environment variables to your Vaultwarden deployment:
|
||||
|
||||
```yaml
|
||||
SSO_ENABLED=true
|
||||
SSO_AUTHORITY=https://authentik.company/application/o/<application_slug>
|
||||
SSO_CLIENT_ID=<client_id>
|
||||
SSO_CLIENT_SECRET=<client_secret>
|
||||
SSO_SCOPES="openid email profile offline_access"
|
||||
SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION=false
|
||||
SSO_CLIENT_CACHE_EXPIRATION=0
|
||||
SSO_ONLY=false # Set to true to disable email+master password login and require SSO
|
||||
SSO_SIGNUPS_MATCH_EMAIL=true # Match first SSO login to existing account by email
|
||||
```
|
||||
|
||||
Then restart Vaultwarden to apply the changes.
|
||||
|
||||
## References
|
||||
|
||||
- [Vaultwarden Wiki - SSO using OpenID Connect](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect)
|
||||
|
||||
## Configuration verification
|
||||
|
||||
To verify the integration of authentik with Vaultwarden, log out of Vaultwarden, then on the login page enter a verified email and click **Use single sign-on**.
|
||||
8
website/package-lock.json
generated
8
website/package-lock.json
generated
@@ -19,7 +19,7 @@
|
||||
"@goauthentik/eslint-config": "^1.0.5",
|
||||
"@goauthentik/prettier-config": "^3.1.0",
|
||||
"@goauthentik/tsconfig": "^1.0.4",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/node": "^24.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.39.1",
|
||||
"@typescript-eslint/parser": "^8.39.1",
|
||||
"eslint": "^9.33.0",
|
||||
@@ -7310,9 +7310,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
|
||||
"integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
|
||||
"version": "24.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
|
||||
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.10.0"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@goauthentik/eslint-config": "^1.0.5",
|
||||
"@goauthentik/prettier-config": "^3.1.0",
|
||||
"@goauthentik/tsconfig": "^1.0.4",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/node": "^24.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.39.1",
|
||||
"@typescript-eslint/parser": "^8.39.1",
|
||||
"eslint": "^9.33.0",
|
||||
|
||||
Reference in New Issue
Block a user