mirror of
https://github.com/goauthentik/authentik
synced 2026-05-06 15:12:13 +02:00
Compare commits
85 Commits
pr/16059
...
developer-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c11f407470 | ||
|
|
b7c6b961a1 | ||
|
|
e6adb72695 | ||
|
|
9cbdcd2cad | ||
|
|
197f4c5585 | ||
|
|
80e9865c6a | ||
|
|
c08df26c65 | ||
|
|
332a53ceff | ||
|
|
4919772d68 | ||
|
|
a978b4b60e | ||
|
|
17bd1f1574 | ||
|
|
0b4be1fdda | ||
|
|
e305c98eb8 | ||
|
|
35bd1d9907 | ||
|
|
3150885889 | ||
|
|
5fd96518d3 | ||
|
|
287647beea | ||
|
|
2c1a0ca0fc | ||
|
|
da47095ebc | ||
|
|
2ea95ba189 | ||
|
|
b277828b21 | ||
|
|
8765c92fc4 | ||
|
|
536688f23b | ||
|
|
7861f5a40e | ||
|
|
e7b43b72ab | ||
|
|
2bf9a9d4fe | ||
|
|
f6af8f3b9d | ||
|
|
c9a4eff3a8 | ||
|
|
b893305e5f | ||
|
|
b3a5cc8320 | ||
|
|
94d7a989a1 | ||
|
|
359fa5d5df | ||
|
|
11c9015a49 | ||
|
|
f135990c6b | ||
|
|
6f63a3eb15 | ||
|
|
2209fcea2a | ||
|
|
e5efb50a37 | ||
|
|
bbc02dc065 | ||
|
|
f3f81951c6 | ||
|
|
739eff66e0 | ||
|
|
48de61a926 | ||
|
|
032031f2cf | ||
|
|
4e44209af1 | ||
|
|
289555abcd | ||
|
|
943c456555 | ||
|
|
a79b914d39 | ||
|
|
7a8816abd1 | ||
|
|
93e448c3fd | ||
|
|
109c869f97 | ||
|
|
8029fdad7b | ||
|
|
d2aac457ef | ||
|
|
70ce5ccceb | ||
|
|
173c334478 | ||
|
|
6e321097a1 | ||
|
|
f3bf8097b8 | ||
|
|
b869433e4d | ||
|
|
5aef86c3d1 | ||
|
|
970ac44ff8 | ||
|
|
9145d55e6c | ||
|
|
1c36b361b2 | ||
|
|
d55e23cdb8 | ||
|
|
52673e4223 | ||
|
|
5cbcbf8d2c | ||
|
|
f29a4c1876 | ||
|
|
38fb5cd712 | ||
|
|
5b2aad586f | ||
|
|
2dd1c7b1ab | ||
|
|
57c24e5c1c | ||
|
|
76d9b3479e | ||
|
|
e9f946cdf2 | ||
|
|
167452f1ed | ||
|
|
dbfdb37e83 | ||
|
|
efdbf7aeed | ||
|
|
8e9e4de80f | ||
|
|
a63c5b1846 | ||
|
|
80b84fa8a8 | ||
|
|
4ce9795491 | ||
|
|
e50cf1c150 | ||
|
|
4178717386 | ||
|
|
20d068f767 | ||
|
|
5b7a42e6d6 | ||
|
|
1398561142 | ||
|
|
55657e149b | ||
|
|
d5d7140631 | ||
|
|
17ff12f68f |
@@ -33,17 +33,12 @@ packages/prettier-config @goauthentik/frontend
|
||||
packages/tsconfig @goauthentik/frontend
|
||||
# Web
|
||||
web/ @goauthentik/frontend
|
||||
tests/wdio/ @goauthentik/frontend
|
||||
# Locale
|
||||
locale/ @goauthentik/backend @goauthentik/frontend
|
||||
web/xliff/ @goauthentik/backend @goauthentik/frontend
|
||||
# Docs & Website
|
||||
docs/ @goauthentik/docs
|
||||
# TODO Remove after moving website to docs
|
||||
# Docs
|
||||
website/ @goauthentik/docs
|
||||
CODE_OF_CONDUCT.md @goauthentik/docs
|
||||
# Security
|
||||
SECURITY.md @goauthentik/security @goauthentik/docs
|
||||
# TODO Remove after moving website to docs
|
||||
website/security/ @goauthentik/security @goauthentik/docs
|
||||
docs/security/ @goauthentik/security @goauthentik/docs
|
||||
|
||||
@@ -76,9 +76,9 @@ 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.10 AS uv
|
||||
FROM ghcr.io/astral-sh/uv:0.8.13 AS uv
|
||||
# Stage 5: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.6-slim-bookworm-fips AS python-base
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.7-slim-bookworm-fips AS python-base
|
||||
|
||||
ENV VENV_PATH="/ak-root/.venv" \
|
||||
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
||||
|
||||
25
SECURITY.md
25
SECURITY.md
@@ -20,12 +20,33 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
|
||||
|
||||
| Version | Supported |
|
||||
| --------- | --------- |
|
||||
| 2025.4.x | ✅ |
|
||||
| 2025.6.x | ✅ |
|
||||
| 2025.8.x | ✅ |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a vulnerability, send an email to [security@goauthentik.io](mailto:security@goauthentik.io). Be sure to include relevant information like which version you've found the issue in, instructions on how to reproduce the issue, and anything else that might make it easier for us to find the issue.
|
||||
If you discover a potential vulnerability, please report it responsibly through one of the following channels:
|
||||
|
||||
- **Email**: [security@goauthentik.io](mailto:security@goauthentik.io)
|
||||
- **GitHub**: Submit a private security advisory via our [repository’s advisory portal](https://github.com/goauthentik/authentik/security/advisories/new)
|
||||
|
||||
When submitting a report, please include as much detail as possible, such as:
|
||||
|
||||
- **Affected version(s)**: The version of authentik where the issue was identified.
|
||||
- **Steps to reproduce**: A clear description or proof of concept to help us verify the issue.
|
||||
- **Impact assessment**: How the vulnerability could be exploited and its potential effect.
|
||||
- **Additional information**: Logs, configuration details (if relevant), or any suggested mitigations.
|
||||
|
||||
We kindly ask that you do not disclose the vulnerability publicly until we have confirmed and addressed the issue.
|
||||
|
||||
Our team will:
|
||||
|
||||
- Acknowledge receipt of your report as quickly as possible.
|
||||
- Keep you updated on the investigation and resolution progress.
|
||||
|
||||
## Researcher Recognition
|
||||
|
||||
We value contributions from the security community. For each valid report, we will publish a dedicated entry on our Security Advisory page that optionally includes the reporter’s name (or preferred alias). Please note that while we do not currently offer monetary bounties, we are committed to giving researchers appropriate credit for their efforts in keeping authentik secure.
|
||||
|
||||
## Severity levels
|
||||
|
||||
|
||||
@@ -63,28 +63,6 @@ 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,7 +4,6 @@ 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
|
||||
@@ -21,9 +20,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()), match_length=Length("domain"))
|
||||
Brand.objects.annotate(host_domain=V(request.get_host()))
|
||||
.filter(Q(host_domain__iendswith=F("domain")) | _q_default)
|
||||
.order_by("-match_length", "default")
|
||||
.order_by("default")
|
||||
)
|
||||
brands = list(db_brands.all())
|
||||
if len(brands) < 1:
|
||||
|
||||
@@ -21,8 +21,6 @@ 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"""
|
||||
@@ -52,15 +50,6 @@ 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,6 +23,7 @@ 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):
|
||||
@@ -30,6 +31,18 @@ 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
|
||||
@@ -52,6 +65,8 @@ class NotificationTransportSerializer(ModelSerializer):
|
||||
"webhook_url",
|
||||
"webhook_mapping_body",
|
||||
"webhook_mapping_headers",
|
||||
"email_subject_prefix",
|
||||
"email_template",
|
||||
"send_once",
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# 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,6 +41,7 @@ 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
|
||||
@@ -295,6 +296,15 @@ 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(
|
||||
@@ -319,12 +329,6 @@ 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"""
|
||||
@@ -462,7 +466,6 @@ class NotificationTransport(TasksModel, SerializerModel):
|
||||
notification=notification,
|
||||
)
|
||||
return None
|
||||
subject_prefix = "authentik Notification: "
|
||||
context = {
|
||||
"key_value": {
|
||||
"user_email": notification.user.email,
|
||||
@@ -490,10 +493,10 @@ class NotificationTransport(TasksModel, SerializerModel):
|
||||
"from": self.name,
|
||||
}
|
||||
mail = TemplateEmailMessage(
|
||||
subject=subject_prefix + context["title"],
|
||||
subject=self.email_subject_prefix + context["title"],
|
||||
to=[(notification.user.name, notification.user.email)],
|
||||
language=notification.user.locale(),
|
||||
template_name="email/event_notification.html",
|
||||
template_name=self.email_template,
|
||||
template_context=context,
|
||||
)
|
||||
send_mail.send_with_options(args=(mail.__dict__,), rel_obj=self)
|
||||
|
||||
@@ -5,10 +5,12 @@ 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,
|
||||
@@ -18,6 +20,7 @@ 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):
|
||||
@@ -138,3 +141,76 @@ 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"]])
|
||||
|
||||
@@ -154,6 +154,7 @@ worker:
|
||||
consumer_listen_timeout: "seconds=30"
|
||||
task_max_retries: 20
|
||||
task_default_time_limit: "minutes=10"
|
||||
lock_purge_interval: "minutes=1"
|
||||
task_purge_interval: "days=1"
|
||||
task_expiration: "days=30"
|
||||
scheduler_interval: "seconds=60"
|
||||
|
||||
@@ -76,6 +76,7 @@ class OutpostConfig:
|
||||
kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict)
|
||||
kubernetes_ingress_secret_name: str = field(default="authentik-outpost-tls")
|
||||
kubernetes_ingress_class_name: str | None = field(default=None)
|
||||
kubernetes_ingress_path_type: str | None = field(default=None)
|
||||
kubernetes_httproute_annotations: dict[str, str] = field(default_factory=dict)
|
||||
kubernetes_httproute_parent_refs: list[dict[str, str]] = field(default_factory=list)
|
||||
kubernetes_service_type: str = field(default="ClusterIP")
|
||||
@@ -151,7 +152,7 @@ class OutpostServiceConnection(ScheduledModel, models.Model):
|
||||
|
||||
state = cache.get(self.state_key, None)
|
||||
if not state:
|
||||
outpost_service_connection_monitor.send_with_options(args=(self.pk), rel_obj=self)
|
||||
outpost_service_connection_monitor.send_with_options(args=(self.pk,), rel_obj=self)
|
||||
return OutpostServiceConnectionState("", False)
|
||||
return state
|
||||
|
||||
|
||||
@@ -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_lowercase:
|
||||
if self.amount_uppercase > 0 and len(RE_UPPER.findall(password)) < self.amount_uppercase:
|
||||
LOGGER.debug("password failed", check="static", reason="amount_uppercase")
|
||||
return PolicyResult(False, self.error_message)
|
||||
if self.amount_symbols > 0:
|
||||
|
||||
@@ -11,7 +11,8 @@ def migrate_sessions(apps, schema_editor, model):
|
||||
AuthenticatedSession = apps.get_model("authentik_core", "AuthenticatedSession")
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
for obj in Model.objects.using(db_alias).all():
|
||||
objs = list(Model.objects.using(db_alias).select_related("old_session").all())
|
||||
for obj in objs:
|
||||
if not obj.old_session:
|
||||
continue
|
||||
obj.session = (
|
||||
|
||||
@@ -23,7 +23,12 @@ def user_session_deleted_oauth_backchannel_logout_and_tokens_removal(
|
||||
|
||||
backchannel_logout_notification_dispatch.send(
|
||||
revocations=[
|
||||
(token.provider_id, token.id_token.iss, token.session.user.uid)
|
||||
(
|
||||
token.provider_id,
|
||||
token.id_token.iss,
|
||||
token.id_token.sub,
|
||||
instance.session.session_key,
|
||||
)
|
||||
for token in access_tokens
|
||||
],
|
||||
)
|
||||
|
||||
@@ -14,13 +14,19 @@ LOGGER = get_logger()
|
||||
|
||||
|
||||
@actor(description=_("Send a back-channel logout request to the registered client"))
|
||||
def send_backchannel_logout_request(provider_pk: int, iss: str, sub: str = None) -> bool:
|
||||
def send_backchannel_logout_request(
|
||||
provider_pk: int,
|
||||
iss: str,
|
||||
sub: str | None = None,
|
||||
session_key: str | None = None,
|
||||
) -> bool:
|
||||
"""Send a back-channel logout request to the registered client
|
||||
|
||||
Args:
|
||||
provider_pk: The OAuth2 provider's primary key
|
||||
iss: The issuer URL for the logout token
|
||||
sub: The subject identifier to include in the logout token
|
||||
session_key: The authentik session key to hash and include in the logout token
|
||||
|
||||
Returns:
|
||||
bool: True if the request was sent successfully, False otherwise
|
||||
@@ -33,11 +39,10 @@ def send_backchannel_logout_request(provider_pk: int, iss: str, sub: str = None)
|
||||
return
|
||||
|
||||
# Generate the logout token
|
||||
logout_token = create_logout_token(iss, provider, None, sub)
|
||||
logout_token = create_logout_token(provider, iss, sub, session_key)
|
||||
|
||||
# Get the back-channel logout URI from the provider's dedicated backchannel_logout_uri field
|
||||
# Back-channel logout requires explicit configuration - no fallback to redirect URIs
|
||||
|
||||
backchannel_logout_uri = provider.backchannel_logout_uri
|
||||
if not backchannel_logout_uri:
|
||||
self.info("No back-channel logout URI found for provider")
|
||||
@@ -60,9 +65,9 @@ def send_backchannel_logout_request(provider_pk: int, iss: str, sub: str = None)
|
||||
def backchannel_logout_notification_dispatch(revocations: list, **kwargs):
|
||||
"""Handle backchannel logout notifications dispatched via signal"""
|
||||
for revocation in revocations:
|
||||
provider_pk, iss, sub = revocation
|
||||
provider_pk, iss, sub, session_key = revocation
|
||||
provider = OAuth2Provider.objects.filter(pk=provider_pk).first()
|
||||
send_backchannel_logout_request.send_with_options(
|
||||
args=(provider_pk, iss, sub),
|
||||
args=(provider_pk, iss, sub, session_key),
|
||||
rel_obj=provider,
|
||||
)
|
||||
|
||||
@@ -217,17 +217,17 @@ class HttpResponseRedirectScheme(HttpResponseRedirect):
|
||||
|
||||
|
||||
def create_logout_token(
|
||||
iss: str,
|
||||
provider: OAuth2Provider,
|
||||
session_key: str | None = None,
|
||||
iss: str,
|
||||
sub: str | None = None,
|
||||
session_key: str | None = None,
|
||||
) -> str:
|
||||
"""Create a logout token for Back-Channel Logout
|
||||
|
||||
As per https://openid.net/specs/openid-connect-backchannel-1_0.html
|
||||
"""
|
||||
|
||||
LOGGER.debug("Creating logout token", provider=provider, session_key=session_key, sub=sub)
|
||||
LOGGER.debug("Creating logout token", provider=provider, sub=sub)
|
||||
|
||||
# Create the logout token payload
|
||||
payload = {
|
||||
|
||||
@@ -127,6 +127,9 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
|
||||
and self.controller.outpost.config.kubernetes_ingress_secret_name
|
||||
):
|
||||
tls_hosts.append(external_host_name.hostname)
|
||||
path_type = "Prefix"
|
||||
if self.controller.outpost.config.kubernetes_ingress_path_type:
|
||||
path_type = self.controller.outpost.config.kubernetes_ingress_path_type
|
||||
if proxy_provider.mode in [
|
||||
ProxyMode.FORWARD_SINGLE,
|
||||
ProxyMode.FORWARD_DOMAIN,
|
||||
@@ -143,7 +146,7 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
|
||||
),
|
||||
),
|
||||
path="/outpost.goauthentik.io",
|
||||
path_type="Prefix",
|
||||
path_type=path_type,
|
||||
)
|
||||
]
|
||||
),
|
||||
@@ -161,7 +164,7 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
|
||||
),
|
||||
),
|
||||
path="/",
|
||||
path_type="Prefix",
|
||||
path_type=path_type,
|
||||
)
|
||||
]
|
||||
),
|
||||
|
||||
69
authentik/rbac/middleware.py
Normal file
69
authentik/rbac/middleware.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""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,9 +5,12 @@ 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"""
|
||||
@@ -71,4 +74,10 @@ 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)
|
||||
|
||||
@@ -4,7 +4,6 @@ import importlib
|
||||
from collections import OrderedDict
|
||||
from hashlib import sha512
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
|
||||
import orjson
|
||||
from sentry_sdk import set_tag
|
||||
@@ -266,6 +265,7 @@ MIDDLEWARE = [
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"authentik.core.middleware.ImpersonateMiddleware",
|
||||
"authentik.rbac.middleware.InitialPermissionsMiddleware",
|
||||
]
|
||||
MIDDLEWARE_LAST = [
|
||||
"django_prometheus.middleware.PrometheusAfterMiddleware",
|
||||
@@ -368,6 +368,9 @@ DRAMATIQ = {
|
||||
"broker_class": "authentik.tasks.broker.Broker",
|
||||
"channel_prefix": "authentik",
|
||||
"task_model": "authentik.tasks.models.Task",
|
||||
"lock_purge_interval": timedelta_from_string(
|
||||
CONFIG.get("worker.lock_purge_interval")
|
||||
).total_seconds(),
|
||||
"task_purge_interval": timedelta_from_string(
|
||||
CONFIG.get("worker.task_purge_interval")
|
||||
).total_seconds(),
|
||||
@@ -424,7 +427,6 @@ DRAMATIQ = {
|
||||
(
|
||||
"authentik.tasks.middleware.MetricsMiddleware",
|
||||
{
|
||||
"multiproc_dir": str(Path(gettempdir()) / "authentik_prometheus_tmp"),
|
||||
"prefix": "authentik",
|
||||
},
|
||||
),
|
||||
|
||||
@@ -198,7 +198,10 @@ class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet):
|
||||
return {"error": "", "count": created}
|
||||
except RuntimeError as exc:
|
||||
LOGGER.warning("failed to get users from duo", exc=exc)
|
||||
return {"error": str(exc), "count": created}
|
||||
return {
|
||||
"error": "An internal error occurred while importing devices.",
|
||||
"count": created,
|
||||
}
|
||||
|
||||
|
||||
class DuoDeviceSerializer(ModelSerializer):
|
||||
|
||||
@@ -168,6 +168,8 @@ class AuthenticatorDuoStageTests(FlowTestCase):
|
||||
client_secret=generate_id(),
|
||||
api_hostname=generate_id(),
|
||||
)
|
||||
|
||||
# Test missing admin credentials
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_api:authenticatorduostage-import-devices-automatic",
|
||||
@@ -178,6 +180,31 @@ class AuthenticatorDuoStageTests(FlowTestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
# Test internal error handling
|
||||
stage.admin_integration_key = generate_id()
|
||||
stage.admin_secret_key = generate_id()
|
||||
stage.save()
|
||||
with patch(
|
||||
"duo_client.admin.Admin.get_users_iterator",
|
||||
MagicMock(side_effect=RuntimeError("Duo API error")),
|
||||
):
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_api:authenticatorduostage-import-devices-automatic",
|
||||
kwargs={
|
||||
"pk": str(stage.pk),
|
||||
},
|
||||
),
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
response.content,
|
||||
{
|
||||
"error": "An internal error occurred while importing devices.",
|
||||
"count": 0,
|
||||
},
|
||||
)
|
||||
|
||||
def test_api_import_automatic(self):
|
||||
"""test `import_devices_automatic`"""
|
||||
self.client.force_login(self.user)
|
||||
|
||||
@@ -15,18 +15,10 @@ 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
@@ -35,7 +35,12 @@ class Command(TenantCommand):
|
||||
template_context={},
|
||||
)
|
||||
try:
|
||||
send_mail(message.__dict__, stage.pk)
|
||||
if not stage.use_global_settings:
|
||||
message.from_email = stage.from_address
|
||||
|
||||
send_mail.send(message.__dict__, stage.pk).get_result(block=True)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f"Test email sent to {options['to']}"))
|
||||
finally:
|
||||
if delete_stage:
|
||||
stage.delete()
|
||||
|
||||
@@ -32,6 +32,14 @@ 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():
|
||||
|
||||
66
authentik/stages/email/tests/test_management_commands.py
Normal file
66
authentik/stages/email/tests/test_management_commands.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""Test email management commands"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.core import mail
|
||||
from django.core.mail.backends.locmem import EmailBackend
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.stages.email.models import EmailStage
|
||||
|
||||
|
||||
class TestEmailManagementCommands(TestCase):
|
||||
"""Test email management commands"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = create_test_admin_user()
|
||||
|
||||
def test_test_email_command_with_stage(self):
|
||||
"""Test test_email command with specified stage"""
|
||||
EmailStage.objects.create(
|
||||
name="test-stage",
|
||||
from_address="test@authentik.local",
|
||||
host="localhost",
|
||||
port=25,
|
||||
)
|
||||
|
||||
with patch("authentik.stages.email.models.EmailStage.backend_class", EmailBackend):
|
||||
call_command("test_email", "test@example.com", stage="test-stage")
|
||||
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].subject, "authentik Test-Email")
|
||||
self.assertEqual(mail.outbox[0].to, ["test@example.com"])
|
||||
|
||||
def test_test_email_command_with_global_settings(self):
|
||||
"""Test test_email command with global settings"""
|
||||
# Mock the backend to use Django's locmem backend
|
||||
with patch("authentik.stages.email.models.EmailStage.backend_class", EmailBackend):
|
||||
call_command("test_email", "test@example.com")
|
||||
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].subject, "authentik Test-Email")
|
||||
self.assertEqual(mail.outbox[0].to, ["test@example.com"])
|
||||
|
||||
def test_test_email_command_invalid_stage(self):
|
||||
"""Test test_email command with invalid stage"""
|
||||
call_command("test_email", "test@example.com", stage="nonexistent")
|
||||
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
||||
def test_test_email_command_with_custom_from(self):
|
||||
"""Test test_email command respects custom from address"""
|
||||
EmailStage.objects.create(
|
||||
name="test-stage",
|
||||
from_address="custom@authentik.local",
|
||||
host="localhost",
|
||||
port=25,
|
||||
)
|
||||
|
||||
with patch("authentik.stages.email.models.EmailStage.backend_class", EmailBackend):
|
||||
call_command("test_email", "test@example.com", stage="test-stage")
|
||||
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].from_email, "custom@authentik.local")
|
||||
self.assertEqual(mail.outbox[0].to, ["test@example.com"])
|
||||
@@ -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), 3)
|
||||
self.assertEqual(len(choices), 5)
|
||||
unlink(file)
|
||||
unlink(file2)
|
||||
|
||||
|
||||
@@ -100,10 +100,15 @@ class MessagesMiddleware(Middleware):
|
||||
TaskStatus.ERROR,
|
||||
exception,
|
||||
)
|
||||
event_kwargs = {
|
||||
"actor": task.actor_name,
|
||||
}
|
||||
if task.rel_obj:
|
||||
event_kwargs["rel_obj"] = task.rel_obj
|
||||
Event.new(
|
||||
EventAction.SYSTEM_TASK_EXCEPTION,
|
||||
message=f"Task {task.actor_name} encountered an error",
|
||||
actor=task.actor_name,
|
||||
**event_kwargs,
|
||||
).with_exception(exception).save()
|
||||
|
||||
def after_skip_message(self, broker: Broker, message: Message):
|
||||
@@ -151,7 +156,6 @@ class DescriptionMiddleware(Middleware):
|
||||
|
||||
|
||||
class _healthcheck_handler(BaseHTTPRequestHandler):
|
||||
|
||||
def log_request(self, code="-", size="-"):
|
||||
HEALTHCHECK_LOGGER.info(
|
||||
self.path,
|
||||
|
||||
0
authentik/tenants/management/commands/__init__.py
Normal file
0
authentik/tenants/management/commands/__init__.py
Normal file
10
authentik/tenants/management/commands/createsuperuser.py
Normal file
10
authentik/tenants/management/commands/createsuperuser.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "ak createsuperuser should not be used. Instead, use ak create_admin_group"
|
||||
|
||||
def handle(self, *args, **options): # noqa: ANN001, D401
|
||||
raise RuntimeError(
|
||||
"ak createsuperuser should not be used. Instead, use ak create_admin_group"
|
||||
)
|
||||
@@ -6753,6 +6753,15 @@
|
||||
"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",
|
||||
|
||||
@@ -5,7 +5,7 @@ metadata:
|
||||
blueprints.goauthentik.io/system-bootstrap: "true"
|
||||
blueprints.goauthentik.io/system: "true"
|
||||
blueprints.goauthentik.io/description: |
|
||||
This blueprint configures the default admin user and group, and configures them for the [Automated install](https://goauthentik.io/docs/installation/automated-install).
|
||||
This blueprint configures the default admin user and group, and configures them for the [Automated install](https://docs.goauthentik.io/docs/install-config/automated-install?utm_source=bootstrap_blueprint).
|
||||
context:
|
||||
username: akadmin
|
||||
group_name: authentik Admins
|
||||
|
||||
4
go.mod
4
go.mod
@@ -27,9 +27,9 @@ require (
|
||||
github.com/sethvargo/go-envconfig v1.3.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/stretchr/testify v1.11.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2025100.1
|
||||
goauthentik.io/api/v3 v3.2025100.2
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.16.0
|
||||
|
||||
8
go.sum
8
go.sum
@@ -169,8 +169,8 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/wwt/guac v1.3.2 h1:sH6OFGa/1tBs7ieWBVlZe7t6F5JAOWBry/tqQL/Vup4=
|
||||
github.com/wwt/guac v1.3.2/go.mod h1:eKm+NrnK7A88l4UBEcYNpZQGMpZRryYKoz4D/0/n1C0=
|
||||
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
|
||||
@@ -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.1 h1:xOMnQ2j1MtrYJlO+8bHJ8MdFPyymqTZcLQ+5PTspdqA=
|
||||
goauthentik.io/api/v3 v3.2025100.1/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
|
||||
goauthentik.io/api/v3 v3.2025100.2 h1:OF8qEpn6PzZFlB16RzL51RSIyFOY234gAWfd8/kjzhc=
|
||||
goauthentik.io/api/v3 v3.2025100.2/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=
|
||||
|
||||
91
internal/utils/web/http_compress.go
Normal file
91
internal/utils/web/http_compress.go
Normal file
@@ -0,0 +1,91 @@
|
||||
// https://github.com/gorilla/handlers/issues/259#issuecomment-2671695039
|
||||
package web
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
)
|
||||
|
||||
// compressHandler is an HTTP handler that adds the Content-Encoding header
|
||||
// back to responses when removed by the http.FileServer.
|
||||
//
|
||||
// handlers.CompressHandler(newCompressHandler(http.FileServer(...)))
|
||||
type compressHandler struct {
|
||||
// handler is an HTTP handler, usually an http.FileServer.
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
var _ http.Handler = &compressHandler{}
|
||||
|
||||
func NewCompressHandler(handler http.Handler) http.Handler {
|
||||
h := &compressHandler{
|
||||
handler: handler,
|
||||
}
|
||||
return handlers.CompressHandler(h)
|
||||
}
|
||||
|
||||
func (h *compressHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// The wrapped response writer saves the incoming content encoding so
|
||||
// it can be restored when writing the response headers.
|
||||
cw := &compressedResponseWriter{
|
||||
encoding: w.Header().Get("Content-Encoding"),
|
||||
fixed: false,
|
||||
responseWriter: w,
|
||||
}
|
||||
h.handler.ServeHTTP(cw, r)
|
||||
}
|
||||
|
||||
// compressedResponseWriter is an http.ResponseWriter that ensures that a
|
||||
// previously-set Content-Encoding header is in place before writing the
|
||||
// response.
|
||||
type compressedResponseWriter struct {
|
||||
encoding string
|
||||
fixed bool
|
||||
responseWriter http.ResponseWriter
|
||||
}
|
||||
|
||||
var _ http.ResponseWriter = &compressedResponseWriter{}
|
||||
|
||||
func (w *compressedResponseWriter) Header() http.Header {
|
||||
return w.responseWriter.Header()
|
||||
}
|
||||
|
||||
func (w *compressedResponseWriter) fixContentEncoding() {
|
||||
if w.fixed {
|
||||
return
|
||||
}
|
||||
w.fixed = true
|
||||
// The Go 1.23 http.FileServer() removes headers like Content-Encoding
|
||||
// from error responses. This breaks gzip and deflate encoding.
|
||||
// https://github.com/gorilla/handlers/issues/259
|
||||
// https://github.com/golang/go/issues/66343
|
||||
if w.encoding == "gzip" || w.encoding == "deflate" {
|
||||
if w.Header().Get("Content-Encoding") == "" {
|
||||
w.Header().Set("Content-Encoding", w.encoding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *compressedResponseWriter) Write(data []byte) (int, error) {
|
||||
w.fixContentEncoding()
|
||||
return w.responseWriter.Write(data)
|
||||
}
|
||||
|
||||
func (w *compressedResponseWriter) WriteHeader(statusCode int) {
|
||||
w.fixContentEncoding()
|
||||
w.responseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func (w *compressedResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if hj, ok := w.responseWriter.(http.Hijacker); ok {
|
||||
return hj.Hijack()
|
||||
}
|
||||
return nil, nil, http.ErrNotSupported
|
||||
}
|
||||
|
||||
// Ensure our compressedResponseWriter implements the necessary interfaces.
|
||||
var _ http.ResponseWriter = &compressedResponseWriter{}
|
||||
var _ http.Hijacker = &compressedResponseWriter{}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"goauthentik.io/internal/config"
|
||||
"goauthentik.io/internal/utils/sentry"
|
||||
"goauthentik.io/internal/utils/web"
|
||||
staticWeb "goauthentik.io/web"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -88,19 +90,81 @@ func (ws *WebServer) configureProxy() {
|
||||
}
|
||||
|
||||
func (ws *WebServer) proxyErrorHandler(rw http.ResponseWriter, req *http.Request, err error) {
|
||||
if !errors.Is(err, ErrAuthentikStarting) {
|
||||
ws.log.WithError(err).Warning("failed to proxy to backend")
|
||||
accept := req.Header.Get("Accept")
|
||||
|
||||
header := rw.Header()
|
||||
|
||||
if errors.Is(err, ErrAuthentikStarting) {
|
||||
header.Set("Retry-After", "5")
|
||||
|
||||
if strings.Contains(accept, "application/json") {
|
||||
header.Set("Content-Type", "application/json")
|
||||
|
||||
err = json.NewEncoder(rw).Encode(map[string]string{
|
||||
"error": "authentik starting",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Warning("failed to write error message")
|
||||
return
|
||||
}
|
||||
} else if strings.Contains(accept, "text/html") {
|
||||
header.Set("Content-Type", "text/html")
|
||||
rw.WriteHeader(http.StatusServiceUnavailable)
|
||||
|
||||
loadingSplashFile, err := staticWeb.StaticDir.Open("standalone/loading/startup.html")
|
||||
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Warning("failed to open startup splash screen")
|
||||
return
|
||||
}
|
||||
|
||||
loadingSplashHTML, err := io.ReadAll(loadingSplashFile)
|
||||
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Warning("failed to read startup splash screen")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = rw.Write(loadingSplashHTML)
|
||||
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Warning("failed to write startup splash screen")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
header.Set("Content-Type", "text/plain")
|
||||
rw.WriteHeader(http.StatusServiceUnavailable)
|
||||
|
||||
// Fallback to just a status message
|
||||
_, err = rw.Write([]byte("authentik starting"))
|
||||
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Warning("failed to write initializing HTML")
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
|
||||
ws.log.WithError(err).Warning("failed to proxy to backend")
|
||||
|
||||
em := fmt.Sprintf("failed to connect to authentik backend: %v", err)
|
||||
// return json if the client asks for json
|
||||
if req.Header.Get("Accept") == "application/json" {
|
||||
|
||||
if strings.Contains(accept, "application/json") {
|
||||
header.Set("Content-Type", "application/json")
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
|
||||
err = json.NewEncoder(rw).Encode(map[string]string{
|
||||
"error": em,
|
||||
})
|
||||
} else {
|
||||
header.Set("Content-Type", "text/plain")
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
|
||||
_, err = rw.Write([]byte(em))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Warning("failed to write error message")
|
||||
}
|
||||
|
||||
@@ -17,9 +17,7 @@ func (ws *WebServer) configureStatic() {
|
||||
// Setup routers
|
||||
staticRouter := ws.loggingRouter.NewRoute().Subrouter()
|
||||
staticRouter.Use(ws.staticHeaderMiddleware)
|
||||
indexLessRouter := staticRouter.NewRoute().Subrouter()
|
||||
// Specifically disable index
|
||||
indexLessRouter.Use(web.DisableIndex)
|
||||
staticRouter.Use(web.DisableIndex)
|
||||
|
||||
distFs := http.FileServer(http.Dir("./web/dist"))
|
||||
|
||||
@@ -31,18 +29,18 @@ func (ws *WebServer) configureStatic() {
|
||||
return h
|
||||
}
|
||||
|
||||
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/static/dist/").Handler(pathStripper(
|
||||
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/static/dist/").Handler(pathStripper(
|
||||
distFs,
|
||||
"static/dist/",
|
||||
config.Get().Web.Path,
|
||||
))
|
||||
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/static/authentik/").Handler(pathStripper(
|
||||
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/static/authentik/").Handler(pathStripper(
|
||||
http.FileServer(http.Dir("./web/authentik")),
|
||||
"static/authentik/",
|
||||
config.Get().Web.Path,
|
||||
))
|
||||
|
||||
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/flow/{flow_slug}/assets").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/flow/{flow_slug}/assets").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
pathStripper(
|
||||
@@ -51,9 +49,9 @@ func (ws *WebServer) configureStatic() {
|
||||
config.Get().Web.Path,
|
||||
).ServeHTTP(rw, r)
|
||||
})
|
||||
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/admin/assets").Handler(http.StripPrefix(fmt.Sprintf("%sif/admin", config.Get().Web.Path), distFs))
|
||||
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/user/assets").Handler(http.StripPrefix(fmt.Sprintf("%sif/user", config.Get().Web.Path), distFs))
|
||||
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/rac/{app_slug}/assets").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/admin/assets").Handler(http.StripPrefix(fmt.Sprintf("%sif/admin", config.Get().Web.Path), distFs))
|
||||
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/user/assets").Handler(http.StripPrefix(fmt.Sprintf("%sif/user", config.Get().Web.Path), distFs))
|
||||
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/rac/{app_slug}/assets").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
pathStripper(
|
||||
@@ -66,7 +64,7 @@ func (ws *WebServer) configureStatic() {
|
||||
// Media files, if backend is file
|
||||
if config.Get().Storage.Media.Backend == "file" {
|
||||
fsMedia := http.FileServer(http.Dir(config.Get().Storage.Media.File.Path))
|
||||
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/media/").Handler(pathStripper(
|
||||
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/media/").Handler(pathStripper(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
|
||||
fsMedia.ServeHTTP(w, r)
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/pires/go-proxyproto"
|
||||
@@ -60,7 +59,7 @@ func NewWebServer() *WebServer {
|
||||
l := log.WithField("logger", "authentik.router")
|
||||
mainHandler := mux.NewRouter()
|
||||
mainHandler.Use(web.ProxyHeaders())
|
||||
mainHandler.Use(handlers.CompressHandler)
|
||||
mainHandler.Use(web.NewCompressHandler)
|
||||
loggingHandler := mainHandler.NewRoute().Subrouter()
|
||||
loggingHandler.Use(web.NewLoggingHandler(l, nil))
|
||||
|
||||
|
||||
@@ -68,6 +68,11 @@ function prepare_debug {
|
||||
chown authentik:authentik /unittest.xml
|
||||
}
|
||||
|
||||
if [[ -z "${PROMETHEUS_MULTIPROC_DIR}" ]]; then
|
||||
export PROMETHEUS_MULTIPROC_DIR="${TMPDIR:-/tmp}"
|
||||
fi
|
||||
mkdir -p "${PROMETHEUS_MULTIPROC_DIR}"
|
||||
|
||||
if [[ "$(python -m authentik.lib.config debugger 2>/dev/null)" == "True" ]]; then
|
||||
prepare_debug
|
||||
fi
|
||||
|
||||
8
lifecycle/aws/package-lock.json
generated
8
lifecycle/aws/package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1025.0",
|
||||
"aws-cdk": "^2.1026.0",
|
||||
"cross-env": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -24,9 +24,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aws-cdk": {
|
||||
"version": "2.1025.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1025.0.tgz",
|
||||
"integrity": "sha512-qKYM+RG5+U/UbGpjTt8ZaxBEfKJMPdOmtPtFNidsIGlrdIWSIFdNcFYi13zo33FkMk6ZFA6yBnjfDry3fNR+hQ==",
|
||||
"version": "2.1026.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1026.0.tgz",
|
||||
"integrity": "sha512-JdXR20s9gMHY3niweK5/D9tILLG8u2FOyJjWgSaNZGJ+pq9u0sBFxufXPO4VxJzDitGFOIW5VvQThXP+Y2VrVA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1025.0",
|
||||
"aws-cdk": "^2.1026.0",
|
||||
"cross-env": "^10.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,15 +33,12 @@ wait_for_db()
|
||||
_tmp = Path(gettempdir())
|
||||
worker_class = "lifecycle.worker.DjangoUvicornWorker"
|
||||
worker_tmp_dir = str(_tmp.joinpath("authentik_gunicorn_tmp"))
|
||||
prometheus_tmp_dir = str(_tmp.joinpath("authentik_prometheus_tmp"))
|
||||
|
||||
os.makedirs(worker_tmp_dir, exist_ok=True)
|
||||
os.makedirs(prometheus_tmp_dir, exist_ok=True)
|
||||
|
||||
bind = f"unix://{str(_tmp.joinpath('authentik-core.sock'))}"
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
|
||||
os.environ.setdefault("PROMETHEUS_MULTIPROC_DIR", prometheus_tmp_dir)
|
||||
|
||||
preload_app = True
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-08-11 00:12+0000\n"
|
||||
"POT-Creation-Date: 2025-08-18 00:11+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,6 +790,12 @@ 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-"
|
||||
@@ -802,12 +808,6 @@ 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,10 +2905,6 @@ 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 ""
|
||||
@@ -3269,6 +3265,14 @@ 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 "
|
||||
|
||||
@@ -237,6 +237,9 @@ class _PostgresConsumer(Consumer):
|
||||
# Override because dramatiq doesn't allow us setting this manually
|
||||
self.timeout = Conf().worker["consumer_listen_timeout"]
|
||||
|
||||
self.lock_purge_interval = timezone.timedelta(seconds=Conf().lock_purge_interval)
|
||||
self.lock_purge_last_run = timezone.now()
|
||||
|
||||
self.task_purge_interval = timezone.timedelta(seconds=Conf().task_purge_interval)
|
||||
self.task_purge_last_run = timezone.now() - self.task_purge_interval
|
||||
|
||||
@@ -378,6 +381,8 @@ class _PostgresConsumer(Consumer):
|
||||
# Force creation of listen connection
|
||||
_ = self.listen_connection
|
||||
|
||||
self._purge_locks()
|
||||
|
||||
processing = len(self.in_processing)
|
||||
if processing >= self.prefetch:
|
||||
# Wait and don't consume the message, other worker will be faster
|
||||
@@ -415,24 +420,26 @@ class _PostgresConsumer(Consumer):
|
||||
)
|
||||
|
||||
# No message to process
|
||||
self._purge_locks()
|
||||
self._auto_purge()
|
||||
self._scheduler()
|
||||
|
||||
return None
|
||||
|
||||
def _purge_locks(self):
|
||||
if timezone.now() - self.lock_purge_last_run < self.lock_purge_interval:
|
||||
return
|
||||
while True:
|
||||
try:
|
||||
message_id = self.unlock_queue.get(block=False)
|
||||
except Empty:
|
||||
return
|
||||
break
|
||||
self.logger.debug("Unlocking message", message_id=message_id)
|
||||
with self.connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT pg_advisory_unlock(%s)", (self._get_message_lock_id(message_id),)
|
||||
)
|
||||
self.unlock_queue.task_done()
|
||||
self.lock_purge_last_run = timezone.now()
|
||||
|
||||
def _auto_purge(self):
|
||||
if timezone.now() - self.task_purge_last_run < self.task_purge_interval:
|
||||
@@ -444,6 +451,7 @@ class _PostgresConsumer(Consumer):
|
||||
result_expiry__lte=timezone.now(),
|
||||
).delete()
|
||||
self.logger.info("Purged messages in all queues", count=count)
|
||||
self.task_purge_last_run = timezone.now()
|
||||
|
||||
def _scheduler(self):
|
||||
if not self.scheduler:
|
||||
@@ -451,6 +459,7 @@ class _PostgresConsumer(Consumer):
|
||||
if timezone.now() - self.scheduler_last_run < self.scheduler_interval:
|
||||
return
|
||||
self.scheduler.run()
|
||||
self.schedule_last_run = timezone.now()
|
||||
|
||||
@raise_connection_error
|
||||
def close(self):
|
||||
@@ -465,4 +474,7 @@ class _PostgresConsumer(Consumer):
|
||||
if self._listen_connection is not None:
|
||||
conn = self._listen_connection
|
||||
self._listen_connection = None
|
||||
conn.close()
|
||||
try:
|
||||
conn.close()
|
||||
except DatabaseError:
|
||||
pass
|
||||
|
||||
@@ -56,6 +56,10 @@ class Conf:
|
||||
def task_model(self) -> str:
|
||||
return self.conf["task_model"]
|
||||
|
||||
@property
|
||||
def lock_purge_interval(self) -> int:
|
||||
return self.conf.get("lock_purge_interval", 60)
|
||||
|
||||
@property
|
||||
def task_purge_interval(self) -> int:
|
||||
# 24 hours
|
||||
|
||||
@@ -26,7 +26,7 @@ class HTTPServer(BaseHTTPServer):
|
||||
self.socket.close()
|
||||
|
||||
host, port = self.server_address[:2]
|
||||
if host == "0.0.0.0": # nosec
|
||||
if host == "0.0.0.0" and socket.has_dualstack_ipv6(): # nosec
|
||||
host = "::" # nosec
|
||||
|
||||
# Strip IPv6 brackets
|
||||
@@ -36,7 +36,9 @@ class HTTPServer(BaseHTTPServer):
|
||||
self.server_address = (host, port)
|
||||
|
||||
self.address_family = (
|
||||
socket.AF_INET6 if isinstance(ip_address(host), IPv6Address) else socket.AF_INET
|
||||
socket.AF_INET6
|
||||
if socket.has_dualstack_ipv6() and isinstance(ip_address(host), IPv6Address)
|
||||
else socket.AF_INET
|
||||
)
|
||||
|
||||
self.socket = socket.create_server(
|
||||
@@ -141,7 +143,6 @@ class MetricsMiddleware(Middleware):
|
||||
def __init__(
|
||||
self,
|
||||
prefix: str,
|
||||
multiproc_dir: str,
|
||||
labels: list[str] | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
@@ -151,9 +152,6 @@ class MetricsMiddleware(Middleware):
|
||||
self.delayed_messages = set()
|
||||
self.message_start_times = {}
|
||||
|
||||
os.makedirs(multiproc_dir, exist_ok=True)
|
||||
os.environ.setdefault("PROMETHEUS_MULTIPROC_DIR", multiproc_dir)
|
||||
|
||||
@property
|
||||
def forks(self):
|
||||
from django_dramatiq_postgres.forks import worker_metrics
|
||||
|
||||
15
packages/docusaurus-config/package-lock.json
generated
15
packages/docusaurus-config/package-lock.json
generated
@@ -10,8 +10,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"deepmerge-ts": "^7.1.5",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react-dom": ">=18"
|
||||
"prism-react-renderer": "^2.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/theme-common": "^3.8.1",
|
||||
@@ -35,7 +34,8 @@
|
||||
"@docusaurus/theme-common": "^3.8.1",
|
||||
"@docusaurus/theme-search-algolia": "^3.8.1",
|
||||
"@docusaurus/types": "^3.8.0",
|
||||
"react": ">=18"
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@docusaurus/theme-search-algolia": {
|
||||
@@ -43,6 +43,9 @@
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4643,9 +4646,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
||||
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
|
||||
"version": "19.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
|
||||
"integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -904,9 +904,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
|
||||
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
|
||||
"version": "24.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
|
||||
"integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
122
packages/eslint-config/package-lock.json
generated
122
packages/eslint-config/package-lock.json
generated
@@ -502,17 +502,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz",
|
||||
"integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz",
|
||||
"integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.39.1",
|
||||
"@typescript-eslint/type-utils": "8.39.1",
|
||||
"@typescript-eslint/utils": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1",
|
||||
"@typescript-eslint/scope-manager": "8.40.0",
|
||||
"@typescript-eslint/type-utils": "8.40.0",
|
||||
"@typescript-eslint/utils": "8.40.0",
|
||||
"@typescript-eslint/visitor-keys": "8.40.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -526,7 +526,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.39.1",
|
||||
"@typescript-eslint/parser": "^8.40.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
@@ -542,16 +542,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz",
|
||||
"integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz",
|
||||
"integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.39.1",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/typescript-estree": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1",
|
||||
"@typescript-eslint/scope-manager": "8.40.0",
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/typescript-estree": "8.40.0",
|
||||
"@typescript-eslint/visitor-keys": "8.40.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -567,14 +567,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz",
|
||||
"integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz",
|
||||
"integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.39.1",
|
||||
"@typescript-eslint/types": "^8.39.1",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.40.0",
|
||||
"@typescript-eslint/types": "^8.40.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -589,14 +589,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz",
|
||||
"integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz",
|
||||
"integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1"
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/visitor-keys": "8.40.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -607,9 +607,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz",
|
||||
"integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz",
|
||||
"integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -624,15 +624,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz",
|
||||
"integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz",
|
||||
"integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/typescript-estree": "8.39.1",
|
||||
"@typescript-eslint/utils": "8.39.1",
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/typescript-estree": "8.40.0",
|
||||
"@typescript-eslint/utils": "8.40.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
@@ -649,9 +649,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz",
|
||||
"integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz",
|
||||
"integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -663,16 +663,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz",
|
||||
"integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz",
|
||||
"integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.39.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.39.1",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1",
|
||||
"@typescript-eslint/project-service": "8.40.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.40.0",
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/visitor-keys": "8.40.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -731,16 +731,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz",
|
||||
"integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz",
|
||||
"integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.39.1",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/typescript-estree": "8.39.1"
|
||||
"@typescript-eslint/scope-manager": "8.40.0",
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/typescript-estree": "8.40.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -755,13 +755,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz",
|
||||
"integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz",
|
||||
"integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4709,16 +4709,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.1.tgz",
|
||||
"integrity": "sha512-GDUv6/NDYngUlNvwaHM1RamYftxf782IyEDbdj3SeaIHHv8fNQVRC++fITT7kUJV/5rIA/tkoRSSskt6osEfqg==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.40.0.tgz",
|
||||
"integrity": "sha512-Xvd2l+ZmFDPEt4oj1QEXzA4A2uUK6opvKu3eGN9aGjB8au02lIVcLyi375w94hHyejTOmzIU77L8ol2sRg9n7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.39.1",
|
||||
"@typescript-eslint/parser": "8.39.1",
|
||||
"@typescript-eslint/typescript-estree": "8.39.1",
|
||||
"@typescript-eslint/utils": "8.39.1"
|
||||
"@typescript-eslint/eslint-plugin": "8.40.0",
|
||||
"@typescript-eslint/parser": "8.40.0",
|
||||
"@typescript-eslint/typescript-estree": "8.40.0",
|
||||
"@typescript-eslint/utils": "8.40.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
||||
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.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
|
||||
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
|
||||
"version": "24.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
|
||||
"integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -6,7 +6,7 @@ authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
|
||||
requires-python = "==3.13.*"
|
||||
dependencies = [
|
||||
"argon2-cffi==25.1.0",
|
||||
"channels==4.3.0",
|
||||
"channels==4.3.1",
|
||||
"channels-redis==4.3.0",
|
||||
"cryptography==45.0.5",
|
||||
"dacite==1.9.2",
|
||||
@@ -79,7 +79,7 @@ dev = [
|
||||
"aws-cdk-lib==2.188.0",
|
||||
"bandit==1.8.3",
|
||||
"black==25.1.0",
|
||||
"channels[daphne]==4.3.0",
|
||||
"channels[daphne]==4.3.1",
|
||||
"codespell==2.4.1",
|
||||
"colorama==0.4.6",
|
||||
"constructs==10.4.2",
|
||||
|
||||
14
schema.yml
14
schema.yml
@@ -49345,6 +49345,10 @@ 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
|
||||
@@ -49384,6 +49388,11 @@ 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
|
||||
@@ -54481,6 +54490,11 @@ 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
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
services:
|
||||
chrome:
|
||||
platform: linux/x86_64
|
||||
image: docker.io/selenium/standalone-chrome:138.0
|
||||
image: docker.io/selenium/standalone-chrome:139.0
|
||||
volumes:
|
||||
- /dev/shm:/dev/shm
|
||||
network_mode: host
|
||||
restart: always
|
||||
mailpit:
|
||||
image: docker.io/axllent/mailpit:v1.27.4
|
||||
image: docker.io/axllent/mailpit:v1.27.6
|
||||
ports:
|
||||
- 1025:1025
|
||||
- 8025:8025
|
||||
|
||||
@@ -10,6 +10,7 @@ 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,
|
||||
@@ -88,6 +89,7 @@ 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)
|
||||
|
||||
12
uv.lock
generated
12
uv.lock
generated
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
revision = 2
|
||||
requires-python = "==3.13.*"
|
||||
|
||||
[manifest]
|
||||
@@ -260,7 +260,7 @@ dev = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "argon2-cffi", specifier = "==25.1.0" },
|
||||
{ name = "channels", specifier = "==4.3.0" },
|
||||
{ name = "channels", specifier = "==4.3.1" },
|
||||
{ name = "channels-redis", specifier = "==4.3.0" },
|
||||
{ name = "cryptography", specifier = "==45.0.5" },
|
||||
{ name = "dacite", specifier = "==1.9.2" },
|
||||
@@ -333,7 +333,7 @@ dev = [
|
||||
{ name = "aws-cdk-lib", specifier = "==2.188.0" },
|
||||
{ name = "bandit", specifier = "==1.8.3" },
|
||||
{ name = "black", specifier = "==25.1.0" },
|
||||
{ name = "channels", extras = ["daphne"], specifier = "==4.3.0" },
|
||||
{ name = "channels", extras = ["daphne"], specifier = "==4.3.1" },
|
||||
{ name = "codespell", specifier = "==2.4.1" },
|
||||
{ name = "colorama", specifier = "==0.4.6" },
|
||||
{ name = "constructs", specifier = "==10.4.2" },
|
||||
@@ -652,15 +652,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "channels"
|
||||
version = "4.3.0"
|
||||
version = "4.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/04/6768c7a887f9c593c4d49f99130c8aec4ea06e750bc17c306b689f6caf3b/channels-4.3.0.tar.gz", hash = "sha256:7db32c61dcd88eada1647e6c6f6ad2eb724b75d4852eeff26ad1c51ccd1a37f7", size = 26816, upload-time = "2025-07-28T13:52:50.334Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/12/a0/46450fcf9e56af18a6b0440ba49db6635419bb7bc84142c35f4143b1a66c/channels-4.3.1.tar.gz", hash = "sha256:97413ffd674542db08e16a9ef09cd86ec0113e5f8125fbd33cf0854adcf27cdb", size = 26896, upload-time = "2025-08-01T13:25:19.952Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/59/0866202ee593e1b0dab0b472ebb8169e1b2b7886ad3008d193da2bbe10cb/channels-4.3.0-py3-none-any.whl", hash = "sha256:0497f3affb95e621b37d6bae1b6a5d9e8e1e1221007a2566f280091cf30ffcce", size = 31238, upload-time = "2025-07-28T13:52:49.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/1c/eae1c2a8c195760376e7f65d0bdcc3e966695d29cfbe5c54841ce5c71408/channels-4.3.1-py3-none-any.whl", hash = "sha256:b091d4b26f91d807de3e84aead7ba785314f27eaf5bac31dd51b1c956b883859", size = 31286, upload-time = "2025-08-01T13:25:18.845Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
||||
436
web/package-lock.json
generated
436
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-1755033451",
|
||||
"@goauthentik/api": "^2025.10.0-rc1-1755254677",
|
||||
"@goauthentik/core": "^1.0.0",
|
||||
"@goauthentik/esbuild-plugin-live-reload": "^1.1.0",
|
||||
"@goauthentik/eslint-config": "^1.0.5",
|
||||
@@ -40,9 +40,9 @@
|
||||
"@open-wc/lit-helpers": "^0.7.0",
|
||||
"@openlayers-elements/core": "^0.4.0",
|
||||
"@openlayers-elements/maps": "^0.4.0",
|
||||
"@patternfly/elements": "^4.1.0",
|
||||
"@patternfly/elements": "^4.2.0",
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@sentry/browser": "^10.3.0",
|
||||
"@sentry/browser": "^10.5.0",
|
||||
"@spotlightjs/spotlight": "^3.0.2",
|
||||
"@storybook/addon-docs": "^9.1.2",
|
||||
"@storybook/addon-links": "^9.1.2",
|
||||
@@ -50,9 +50,9 @@
|
||||
"@storybook/web-components-vite": "^9.1.2",
|
||||
"@types/codemirror": "^5.60.16",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/guacamole-common-js": "^1.5.3",
|
||||
"@types/guacamole-common-js": "^1.5.4",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^24.2.1",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||
@@ -64,7 +64,7 @@
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"construct-style-sheets-polyfill": "^3.1.0",
|
||||
"core-js": "^3.45.0",
|
||||
"core-js": "^3.45.1",
|
||||
"country-flag-icons": "^1.5.19",
|
||||
"date-fns": "^4.1.0",
|
||||
"deepmerge-ts": "^7.1.5",
|
||||
@@ -85,7 +85,7 @@
|
||||
"lit": "^3.3.1",
|
||||
"lit-analyzer": "^2.0.3",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mermaid": "^11.9.0",
|
||||
"mermaid": "^11.10.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.6.2",
|
||||
"pseudolocale": "^2.1.0",
|
||||
@@ -106,7 +106,7 @@
|
||||
"ts-pattern": "^5.8.0",
|
||||
"turnstile-types": "^1.2.3",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.39.1",
|
||||
"typescript-eslint": "^8.40.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"webcomponent-qr-code": "^1.3.0",
|
||||
"wireit": "^0.14.12",
|
||||
@@ -119,14 +119,14 @@
|
||||
"@esbuild/darwin-arm64": "^0.25.4",
|
||||
"@esbuild/linux-arm64": "^0.25.4",
|
||||
"@esbuild/linux-x64": "^0.25.4",
|
||||
"@rollup/rollup-darwin-arm64": "^4.46.2",
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.46.2",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.46.2",
|
||||
"@rollup/rollup-darwin-arm64": "^4.46.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.46.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.46.3",
|
||||
"@wdio/browser-runner": "^9.19.1",
|
||||
"@wdio/cli": "^9.19.1",
|
||||
"@wdio/spec-reporter": "^9.19.1",
|
||||
"@web/test-runner": "^0.20.2",
|
||||
"chromedriver": "^139.0.0"
|
||||
"chromedriver": "^139.0.1"
|
||||
}
|
||||
},
|
||||
"../packages/core": {
|
||||
@@ -1509,9 +1509,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@goauthentik/api": {
|
||||
"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=="
|
||||
"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=="
|
||||
},
|
||||
"node_modules/@goauthentik/core": {
|
||||
"resolved": "packages/core",
|
||||
@@ -4042,16 +4042,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@patternfly/elements": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/elements/-/elements-4.1.0.tgz",
|
||||
"integrity": "sha512-5gUcB0Kwe7hvraCXayyj9Ut3+F0d9baLkZ74IEGWSyEvAynd7kXn68kmP8URzURL1YcES1g4gKPRs9rhVdkQiQ==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/elements/-/elements-4.2.0.tgz",
|
||||
"integrity": "sha512-GXVpEfiQzfZwEprUJ9QhSdRyIXDJRm1LT0r88+zlXCGGFDLzMdOlI3+krxQJlv1b+v3VPph4HY/VaGZT8Uxo+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lit/context": "^1.1.5",
|
||||
"@patternfly/icons": "^1.0.3",
|
||||
"@patternfly/pfe-core": "^5.0.0",
|
||||
"lit": "^3.2.1",
|
||||
"tslib": "^2.6.3"
|
||||
"@patternfly/pfe-core": "^5.0.2",
|
||||
"lit": "^3.3.0",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@patternfly/icons": {
|
||||
@@ -4065,14 +4065,14 @@
|
||||
"integrity": "sha512-io0huj+LCP5FgDZJDaLv1snxktTYs8iCFz/W1VDRneYoebNHLmGfQdF7Yn8bS6PF7qmN6oJKEBlq3AjmmE8vdA=="
|
||||
},
|
||||
"node_modules/@patternfly/pfe-core": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/pfe-core/-/pfe-core-5.0.1.tgz",
|
||||
"integrity": "sha512-oNctdm9XUEOrwTIMobcPUmYPt9toShCewPOcyLcdGraseuy518ZWkiHpFSEAsmuf7OjhWRD2gDWVaV/3TflK4w==",
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/pfe-core/-/pfe-core-5.0.3.tgz",
|
||||
"integrity": "sha512-tKc9YfbXD5kzUr6ssa5gKicKgWf+CEax3auvv0yv5jJ42RFMhDRVO0YWalPi5iBl70XHNentpMmdHioC00TJAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@lit/context": "^1.1.3",
|
||||
"lit": "^3.2.1"
|
||||
"@lit/context": "^1.1.5",
|
||||
"lit": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@petamoriken/float16": {
|
||||
@@ -4309,9 +4309,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz",
|
||||
"integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.3.tgz",
|
||||
"integrity": "sha512-UmTdvXnLlqQNOCJnyksjPs1G4GqXNGW1LrzCe8+8QoaLhhDeTXYBgJ3k6x61WIhlHX2U+VzEJ55TtIjR/HTySA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -4322,9 +4322,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz",
|
||||
"integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.3.tgz",
|
||||
"integrity": "sha512-8NoxqLpXm7VyeI0ocidh335D6OKT0UJ6fHdnIxf3+6oOerZZc+O7r+UhvROji6OspyPm+rrIdb1gTXtVIqn+Sg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4335,9 +4335,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz",
|
||||
"integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.3.tgz",
|
||||
"integrity": "sha512-csnNavqZVs1+7/hUKtgjMECsNG2cdB8F7XBHP6FfQjqhjF8rzMzb3SLyy/1BG7YSfQ+bG75Ph7DyedbUqwq1rA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4348,9 +4348,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz",
|
||||
"integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.3.tgz",
|
||||
"integrity": "sha512-r2MXNjbuYabSIX5yQqnT8SGSQ26XQc8fmp6UhlYJd95PZJkQD1u82fWP7HqvGUf33IsOC6qsiV+vcuD4SDP6iw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4361,9 +4361,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz",
|
||||
"integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.3.tgz",
|
||||
"integrity": "sha512-uluObTmgPJDuJh9xqxyr7MV61Imq+0IvVsAlWyvxAaBSNzCcmZlhfYcRhCdMaCsy46ccZa7vtDDripgs9Jkqsw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4374,9 +4374,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz",
|
||||
"integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.3.tgz",
|
||||
"integrity": "sha512-AVJXEq9RVHQnejdbFvh1eWEoobohUYN3nqJIPI4mNTMpsyYN01VvcAClxflyk2HIxvLpRcRggpX1m9hkXkpC/A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4387,9 +4387,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz",
|
||||
"integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.3.tgz",
|
||||
"integrity": "sha512-byyflM+huiwHlKi7VHLAYTKr67X199+V+mt1iRgJenAI594vcmGGddWlu6eHujmcdl6TqSNnvqaXJqZdnEWRGA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -4400,9 +4400,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz",
|
||||
"integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.3.tgz",
|
||||
"integrity": "sha512-aLm3NMIjr4Y9LklrH5cu7yybBqoVCdr4Nvnm8WB7PKCn34fMCGypVNpGK0JQWdPAzR/FnoEoFtlRqZbBBLhVoQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -4413,9 +4413,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz",
|
||||
"integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.3.tgz",
|
||||
"integrity": "sha512-VtilE6eznJRDIoFOzaagQodUksTEfLIsvXymS+UdJiSXrPW7Ai+WG4uapAc3F7Hgs791TwdGh4xyOzbuzIZrnw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4426,9 +4426,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz",
|
||||
"integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.3.tgz",
|
||||
"integrity": "sha512-dG3JuS6+cRAL0GQ925Vppafi0qwZnkHdPeuZIxIPXqkCLP02l7ka+OCyBoDEv8S+nKHxfjvjW4OZ7hTdHkx8/w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4439,9 +4439,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz",
|
||||
"integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.3.tgz",
|
||||
"integrity": "sha512-iU8DxnxEKJptf8Vcx4XvAUdpkZfaz0KWfRrnIRrOndL0SvzEte+MTM7nDH4A2Now4FvTZ01yFAgj6TX/mZl8hQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -4452,9 +4452,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz",
|
||||
"integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.3.tgz",
|
||||
"integrity": "sha512-VrQZp9tkk0yozJoQvQcqlWiqaPnLM6uY1qPYXvukKePb0fqaiQtOdMJSxNFUZFsGw5oA5vvVokjHrx8a9Qsz2A==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -4465,9 +4465,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz",
|
||||
"integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.3.tgz",
|
||||
"integrity": "sha512-uf2eucWSUb+M7b0poZ/08LsbcRgaDYL8NCGjUeFMwCWFwOuFcZ8D9ayPl25P3pl+D2FH45EbHdfyUesQ2Lt9wA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -4478,9 +4478,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz",
|
||||
"integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.3.tgz",
|
||||
"integrity": "sha512-7tnUcDvN8DHm/9ra+/nF7lLzYHDeODKKKrh6JmZejbh1FnCNZS8zMkZY5J4sEipy2OW1d1Ncc4gNHUd0DLqkSg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -4491,9 +4491,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz",
|
||||
"integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.3.tgz",
|
||||
"integrity": "sha512-MUpAOallJim8CsJK+4Lc9tQzlfPbHxWDrGXZm2z6biaadNpvh3a5ewcdat478W+tXDoUiHwErX/dOql7ETcLqg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -4504,9 +4504,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz",
|
||||
"integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.3.tgz",
|
||||
"integrity": "sha512-F42IgZI4JicE2vM2PWCe0N5mR5vR0gIdORPqhGQ32/u1S1v3kLtbZ0C/mi9FFk7C5T0PgdeyWEPajPjaUpyoKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4517,9 +4517,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz",
|
||||
"integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.3.tgz",
|
||||
"integrity": "sha512-oLc+JrwwvbimJUInzx56Q3ujL3Kkhxehg7O1gWAYzm8hImCd5ld1F2Gry5YDjR21MNb5WCKhC9hXgU7rRlyegQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4530,9 +4530,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz",
|
||||
"integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.3.tgz",
|
||||
"integrity": "sha512-lOrQ+BVRstruD1fkWg9yjmumhowR0oLAAzavB7yFSaGltY8klttmZtCLvOXCmGE9mLIn8IBV/IFrQOWz5xbFPg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4543,9 +4543,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz",
|
||||
"integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.3.tgz",
|
||||
"integrity": "sha512-vvrVKPRS4GduGR7VMH8EylCBqsDcw6U+/0nPDuIjXQRbHJc6xOBj+frx8ksfZAh6+Fptw5wHrN7etlMmQnPQVg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -4556,9 +4556,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz",
|
||||
"integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.3.tgz",
|
||||
"integrity": "sha512-fi3cPxCnu3ZeM3EwKZPgXbWoGzm2XHgB/WShKI81uj8wG0+laobmqy5wbgEwzstlbLu4MyO8C19FyhhWseYKNQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4587,75 +4587,75 @@
|
||||
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="
|
||||
},
|
||||
"node_modules/@sentry-internal/browser-utils": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.3.0.tgz",
|
||||
"integrity": "sha512-jKBoNMmxMgojzcpIsUqVk6XL6YiW0i8jtNdD9UdBKd8ExFpVkXhPuMdWB9f/5mVNK/9BnfI74eTiEVHZEkeZ6Q==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "10.3.0"
|
||||
"@sentry/core": "10.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/feedback": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.3.0.tgz",
|
||||
"integrity": "sha512-HGvBoUwbj164I/66vrtUjHICuqwcY5RIGAAutD+H+EwhUROpFuzaIe9utIalhyU9CrTN/vFs4UYPWmeOpqg2lQ==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.5.0.tgz",
|
||||
"integrity": "sha512-x79P4VZwUxb1EGZb9OQ5EEgrDWFCUlrbzHBwV/oocQA5Ss1SFz5u6cP5Ak7yJtILiJtdGzAyAoQOy4GKD13D4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "10.3.0"
|
||||
"@sentry/core": "10.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.3.0.tgz",
|
||||
"integrity": "sha512-SVF7mMDW++LaeaONyxFUQ2Na3aMv6vyhv9V5Yb6yHWgPXI8NCW83mJ/MidHDD3yI0bccgTJUEmB4S0vBioafzg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.5.0.tgz",
|
||||
"integrity": "sha512-Dp4coE/nPzhFrYH3iVrpVKmhNJ1m/jGXMEDBCNg3wJZRszI41Hrj0jCAM0Y2S3Q4IxYOmFYaFbGtVpAznRyOHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/browser-utils": "10.3.0",
|
||||
"@sentry/core": "10.3.0"
|
||||
"@sentry-internal/browser-utils": "10.5.0",
|
||||
"@sentry/core": "10.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay-canvas": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.3.0.tgz",
|
||||
"integrity": "sha512-JGE1YmWb5LYhnaEgaYVMKj03FCQsuvALF2RXJx+Qe8pPwWtEWWBXMFEIt714mv4mO3YQxZnnxQhxFRuSJqXQfQ==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/replay": "10.3.0",
|
||||
"@sentry/core": "10.3.0"
|
||||
"@sentry-internal/replay": "10.5.0",
|
||||
"@sentry/core": "10.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/browser": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.3.0.tgz",
|
||||
"integrity": "sha512-n0jROCST6XJhU7okSn04uRGFK4FjJZNjVR8nDSi/A6gU7VxVAs3iva5SUykXGFQKSVaXVE8kKjS6BtKfllulQA==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.5.0.tgz",
|
||||
"integrity": "sha512-o5pEJeZ/iZ7Fmaz2sIirThfnmSVNiP5ZYhacvcDi0qc288TmBbikCX3fXxq3xiSkhXfe1o5QIbNyovzfutyuVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@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"
|
||||
"@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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.3.0.tgz",
|
||||
"integrity": "sha512-FEFCqiGkzJrm6TNJvhyjhc4rpC1Kmo/abYOACRd6MLvm8GBz41eFFKxsNxGZAUA3Fk1tR2mPfXIHOJzS0ulVww==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.5.0.tgz",
|
||||
"integrity": "sha512-jTJ8NhZSKB2yj3QTVRXfCCngQzAOLThQUxCl9A7Mv+XF10tP7xbH/88MVQ5WiOr2IzcmrB9r2nmUe36BnMlLjA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -6482,9 +6482,9 @@
|
||||
"integrity": "sha512-fFxMtjAvXXMYTzDFK5NpcVB7WHnrHVLl00QzEGpuFxSAC789io6M+vjcn+g5FTEamIJtJr/IHkCDsqvJxeWDyw=="
|
||||
},
|
||||
"node_modules/@types/guacamole-common-js": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/guacamole-common-js/-/guacamole-common-js-1.5.3.tgz",
|
||||
"integrity": "sha512-PDW2kRwwIgzw0ys82X65g13+OHRPW4Ek/919vIoacWGEUU8jGGfULmH+6TuufLDGMO0cqXR03nxer8ceRDmy3g==",
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/guacamole-common-js/-/guacamole-common-js-1.5.4.tgz",
|
||||
"integrity": "sha512-RswPKwgqSgbbfvL8fHD6J2nmXhpQuejiuPrzfUkejBMUFRW5hYUEMaK4ySyDTM/Q4hWUe3F7KLxBvaI7LtqYXQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
@@ -6633,9 +6633,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
|
||||
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
|
||||
"version": "24.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
|
||||
"integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.10.0"
|
||||
@@ -6838,16 +6838,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz",
|
||||
"integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz",
|
||||
"integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.39.1",
|
||||
"@typescript-eslint/type-utils": "8.39.1",
|
||||
"@typescript-eslint/utils": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1",
|
||||
"@typescript-eslint/scope-manager": "8.40.0",
|
||||
"@typescript-eslint/type-utils": "8.40.0",
|
||||
"@typescript-eslint/utils": "8.40.0",
|
||||
"@typescript-eslint/visitor-keys": "8.40.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -6861,7 +6861,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.39.1",
|
||||
"@typescript-eslint/parser": "^8.40.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
@@ -6876,15 +6876,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz",
|
||||
"integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz",
|
||||
"integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.39.1",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/typescript-estree": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1",
|
||||
"@typescript-eslint/scope-manager": "8.40.0",
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/typescript-estree": "8.40.0",
|
||||
"@typescript-eslint/visitor-keys": "8.40.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -6900,13 +6900,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz",
|
||||
"integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz",
|
||||
"integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.39.1",
|
||||
"@typescript-eslint/types": "^8.39.1",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.40.0",
|
||||
"@typescript-eslint/types": "^8.40.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -6921,13 +6921,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz",
|
||||
"integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz",
|
||||
"integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1"
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/visitor-keys": "8.40.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -6938,9 +6938,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz",
|
||||
"integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz",
|
||||
"integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -6954,14 +6954,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz",
|
||||
"integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz",
|
||||
"integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/typescript-estree": "8.39.1",
|
||||
"@typescript-eslint/utils": "8.39.1",
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/typescript-estree": "8.40.0",
|
||||
"@typescript-eslint/utils": "8.40.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
@@ -6978,9 +6978,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz",
|
||||
"integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz",
|
||||
"integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -6991,15 +6991,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz",
|
||||
"integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz",
|
||||
"integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.39.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.39.1",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1",
|
||||
"@typescript-eslint/project-service": "8.40.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.40.0",
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/visitor-keys": "8.40.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -7019,15 +7019,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz",
|
||||
"integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz",
|
||||
"integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.39.1",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/typescript-estree": "8.39.1"
|
||||
"@typescript-eslint/scope-manager": "8.40.0",
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/typescript-estree": "8.40.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -7042,12 +7042,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz",
|
||||
"integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz",
|
||||
"integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -10711,9 +10711,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chromedriver": {
|
||||
"version": "139.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-139.0.0.tgz",
|
||||
"integrity": "sha512-4hzJhx2B2WbGP1160KaDkAIWMHmEBabldes1SrU75JwrzviYevopcVCxQw9B9Ykx+500ij06fv2retL5mK7DbA==",
|
||||
"version": "139.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-139.0.1.tgz",
|
||||
"integrity": "sha512-K16mpBWhVMY/85k+1pf2ZuCOCDNJxSfr/OuIh7YbWoVIT+baNlyB6OvVh2WQw+MYQK2fg7CS0rMUE8GvMY6oCA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
@@ -11147,9 +11147,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.45.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.0.tgz",
|
||||
"integrity": "sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA==",
|
||||
"version": "3.45.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz",
|
||||
"integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -19109,9 +19109,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mermaid": {
|
||||
"version": "11.9.0",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.9.0.tgz",
|
||||
"integrity": "sha512-YdPXn9slEwO0omQfQIsW6vS84weVQftIyyTGAZCwM//MGhPzL1+l6vO6bkf0wnP4tHigH1alZ5Ooy3HXI2gOag==",
|
||||
"version": "11.10.0",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.10.0.tgz",
|
||||
"integrity": "sha512-oQsFzPBy9xlpnGxUqLbVY8pvknLlsNIJ0NWwi8SUJjhbP1IT0E0o1lfhU4iYV3ubpy+xkzkaOyDUQMn06vQElQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.0.4",
|
||||
@@ -23326,9 +23326,9 @@
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz",
|
||||
"integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==",
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.3.tgz",
|
||||
"integrity": "sha512-RZn2XTjXb8t5g13f5YclGoilU/kwT696DIkY3sywjdZidNSi3+vseaQov7D7BZXVJCPv3pDWUN69C78GGbXsKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
@@ -23341,26 +23341,26 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.46.2",
|
||||
"@rollup/rollup-android-arm64": "4.46.2",
|
||||
"@rollup/rollup-darwin-arm64": "4.46.2",
|
||||
"@rollup/rollup-darwin-x64": "4.46.2",
|
||||
"@rollup/rollup-freebsd-arm64": "4.46.2",
|
||||
"@rollup/rollup-freebsd-x64": "4.46.2",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.46.2",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.46.2",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.46.2",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.46.2",
|
||||
"@rollup/rollup-linux-loongarch64-gnu": "4.46.2",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.46.2",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.46.2",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.46.2",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.46.2",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.46.2",
|
||||
"@rollup/rollup-linux-x64-musl": "4.46.2",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.46.2",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.46.2",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.46.2",
|
||||
"@rollup/rollup-android-arm-eabi": "4.46.3",
|
||||
"@rollup/rollup-android-arm64": "4.46.3",
|
||||
"@rollup/rollup-darwin-arm64": "4.46.3",
|
||||
"@rollup/rollup-darwin-x64": "4.46.3",
|
||||
"@rollup/rollup-freebsd-arm64": "4.46.3",
|
||||
"@rollup/rollup-freebsd-x64": "4.46.3",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.46.3",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.46.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.46.3",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.46.3",
|
||||
"@rollup/rollup-linux-loongarch64-gnu": "4.46.3",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.46.3",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.46.3",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.46.3",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.46.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.46.3",
|
||||
"@rollup/rollup-linux-x64-musl": "4.46.3",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.46.3",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.46.3",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.46.3",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@@ -25708,15 +25708,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.1.tgz",
|
||||
"integrity": "sha512-GDUv6/NDYngUlNvwaHM1RamYftxf782IyEDbdj3SeaIHHv8fNQVRC++fITT7kUJV/5rIA/tkoRSSskt6osEfqg==",
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.40.0.tgz",
|
||||
"integrity": "sha512-Xvd2l+ZmFDPEt4oj1QEXzA4A2uUK6opvKu3eGN9aGjB8au02lIVcLyi375w94hHyejTOmzIU77L8ol2sRg9n7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.39.1",
|
||||
"@typescript-eslint/parser": "8.39.1",
|
||||
"@typescript-eslint/typescript-estree": "8.39.1",
|
||||
"@typescript-eslint/utils": "8.39.1"
|
||||
"@typescript-eslint/eslint-plugin": "8.40.0",
|
||||
"@typescript-eslint/parser": "8.40.0",
|
||||
"@typescript-eslint/typescript-estree": "8.40.0",
|
||||
"@typescript-eslint/utils": "8.40.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -27835,7 +27835,7 @@
|
||||
"dependencies": {
|
||||
"@goauthentik/prettier-config": "^3.1.0",
|
||||
"@goauthentik/tsconfig": "^1.0.4",
|
||||
"@types/node": "^24.2.1",
|
||||
"@types/node": "^24.3.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
@@ -27915,7 +27915,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@goauthentik/api": "^2024.6.0-1719577139",
|
||||
"@goauthentik/api": "^2025.10.0-rc1-1755254677",
|
||||
"@goauthentik/core": "^1.0.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.6",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
@@ -27928,7 +27928,7 @@
|
||||
"formdata-polyfill": "^4.0.10",
|
||||
"jquery": "^3.7.1",
|
||||
"prettier": "^3.5.3",
|
||||
"rollup": "^4.46.2",
|
||||
"rollup": "^4.46.3",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"weakmap-polyfill": "^2.0.4"
|
||||
},
|
||||
|
||||
@@ -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-1755033451",
|
||||
"@goauthentik/api": "^2025.10.0-rc1-1755254677",
|
||||
"@goauthentik/core": "^1.0.0",
|
||||
"@goauthentik/esbuild-plugin-live-reload": "^1.1.0",
|
||||
"@goauthentik/eslint-config": "^1.0.5",
|
||||
@@ -111,9 +111,9 @@
|
||||
"@open-wc/lit-helpers": "^0.7.0",
|
||||
"@openlayers-elements/core": "^0.4.0",
|
||||
"@openlayers-elements/maps": "^0.4.0",
|
||||
"@patternfly/elements": "^4.1.0",
|
||||
"@patternfly/elements": "^4.2.0",
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@sentry/browser": "^10.3.0",
|
||||
"@sentry/browser": "^10.5.0",
|
||||
"@spotlightjs/spotlight": "^3.0.2",
|
||||
"@storybook/addon-docs": "^9.1.2",
|
||||
"@storybook/addon-links": "^9.1.2",
|
||||
@@ -121,9 +121,9 @@
|
||||
"@storybook/web-components-vite": "^9.1.2",
|
||||
"@types/codemirror": "^5.60.16",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/guacamole-common-js": "^1.5.3",
|
||||
"@types/guacamole-common-js": "^1.5.4",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^24.2.1",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||
@@ -135,7 +135,7 @@
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"construct-style-sheets-polyfill": "^3.1.0",
|
||||
"core-js": "^3.45.0",
|
||||
"core-js": "^3.45.1",
|
||||
"country-flag-icons": "^1.5.19",
|
||||
"date-fns": "^4.1.0",
|
||||
"deepmerge-ts": "^7.1.5",
|
||||
@@ -156,7 +156,7 @@
|
||||
"lit": "^3.3.1",
|
||||
"lit-analyzer": "^2.0.3",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mermaid": "^11.9.0",
|
||||
"mermaid": "^11.10.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.6.2",
|
||||
"pseudolocale": "^2.1.0",
|
||||
@@ -177,7 +177,7 @@
|
||||
"ts-pattern": "^5.8.0",
|
||||
"turnstile-types": "^1.2.3",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.39.1",
|
||||
"typescript-eslint": "^8.40.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"webcomponent-qr-code": "^1.3.0",
|
||||
"wireit": "^0.14.12",
|
||||
@@ -187,14 +187,14 @@
|
||||
"@esbuild/darwin-arm64": "^0.25.4",
|
||||
"@esbuild/linux-arm64": "^0.25.4",
|
||||
"@esbuild/linux-x64": "^0.25.4",
|
||||
"@rollup/rollup-darwin-arm64": "^4.46.2",
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.46.2",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.46.2",
|
||||
"@rollup/rollup-darwin-arm64": "^4.46.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.46.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.46.3",
|
||||
"@wdio/browser-runner": "^9.19.1",
|
||||
"@wdio/cli": "^9.19.1",
|
||||
"@wdio/spec-reporter": "^9.19.1",
|
||||
"@web/test-runner": "^0.20.2",
|
||||
"chromedriver": "^139.0.0"
|
||||
"chromedriver": "^139.0.1"
|
||||
},
|
||||
"wireit": {
|
||||
"build": {
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"dependencies": {
|
||||
"@goauthentik/prettier-config": "^3.1.0",
|
||||
"@goauthentik/tsconfig": "^1.0.4",
|
||||
"@types/node": "^24.2.1",
|
||||
"@types/node": "^24.3.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"watch": "rollup -w -c rollup.config.mjs --bundleConfigAsCjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@goauthentik/api": "^2024.6.0-1719577139",
|
||||
"@goauthentik/api": "^2025.10.0-rc1-1755254677",
|
||||
"@goauthentik/core": "^1.0.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.6",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
@@ -23,7 +23,7 @@
|
||||
"formdata-polyfill": "^4.0.10",
|
||||
"jquery": "^3.7.1",
|
||||
"prettier": "^3.5.3",
|
||||
"rollup": "^4.46.2",
|
||||
"rollup": "^4.46.3",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"weakmap-polyfill": "^2.0.4"
|
||||
},
|
||||
|
||||
@@ -48,6 +48,11 @@ const BASE_ESBUILD_OPTIONS = {
|
||||
plugins: [
|
||||
copy({
|
||||
assets: [
|
||||
{
|
||||
from: path.join(path.dirname(EntryPoint.StandaloneLoading.in), "startup", "**"),
|
||||
to: path.dirname(EntryPoint.StandaloneLoading.out),
|
||||
},
|
||||
|
||||
{
|
||||
from: path.join(patternflyPath, "patternfly.min.css"),
|
||||
to: ".",
|
||||
|
||||
@@ -66,7 +66,7 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
||||
quickActions: QuickAction[] = [
|
||||
[msg("Create a new application"), paramURL("/core/applications", { createWizard: true })],
|
||||
[msg("Check the logs"), paramURL("/events/log")],
|
||||
[msg("Explore integrations"), "https://goauthentik.io/integrations/", true],
|
||||
[msg("Explore integrations"), "https://integrations.goauthentik.io/", true],
|
||||
[msg("Manage users"), paramURL("/identity/users")],
|
||||
[
|
||||
msg("Check the release notes"),
|
||||
|
||||
@@ -5,6 +5,10 @@ import { groupBy } from "#common/utils";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { IDGenerator } from "#packages/core/id";
|
||||
|
||||
import { Provider, ProvidersAllListRequest, ProvidersApi } from "@goauthentik/api";
|
||||
|
||||
import { html, nothing } from "lit";
|
||||
@@ -38,11 +42,13 @@ export class AkProviderInput extends AKElement {
|
||||
return this;
|
||||
}
|
||||
|
||||
//#region Properties
|
||||
|
||||
@property({ type: String })
|
||||
name!: string;
|
||||
|
||||
@property({ type: String })
|
||||
label = "";
|
||||
label?: string;
|
||||
|
||||
@property({ type: Number })
|
||||
value?: number;
|
||||
@@ -60,14 +66,26 @@ export class AkProviderInput extends AKElement {
|
||||
super();
|
||||
this.selected = this.selected.bind(this);
|
||||
}
|
||||
/**
|
||||
* A unique ID to associate with the input and label.
|
||||
* @property
|
||||
*/
|
||||
@property({ type: String, reflect: false })
|
||||
public fieldID?: string = IDGenerator.elementID().toString();
|
||||
|
||||
selected(item: Provider) {
|
||||
return this.value !== undefined && this.value === item.pk;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
render() {
|
||||
return html` <ak-form-element-horizontal label=${this.label} name=${this.name}>
|
||||
return html` <ak-form-element-horizontal name=${this.name}>
|
||||
<div slot="label" class="pf-c-form__group-label">
|
||||
${AKLabel({ htmlFor: this.fieldID, required: this.required }, this.label)}
|
||||
</div>
|
||||
|
||||
<ak-search-select
|
||||
.fieldID=${this.fieldID}
|
||||
.selected=${this.selected}
|
||||
.fetchObjects=${fetch}
|
||||
.renderElement=${renderElement}
|
||||
|
||||
@@ -44,6 +44,7 @@ export class BrandForm extends ModelForm<Brand, string> {
|
||||
}
|
||||
|
||||
async send(data: Brand): Promise<Brand> {
|
||||
data.attributes ??= {};
|
||||
if (this.instance?.brandUuid) {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreBrandsUpdate({
|
||||
brandUuid: this.instance.brandUuid,
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
NotificationWebhookMapping,
|
||||
PropertymappingsApi,
|
||||
PropertymappingsNotificationListRequest,
|
||||
StagesApi,
|
||||
TypeCreate,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
@@ -33,10 +35,18 @@ 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.")
|
||||
@@ -56,18 +66,28 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
|
||||
}
|
||||
|
||||
onModeChange(mode: string | undefined): void {
|
||||
if (
|
||||
mode === NotificationTransportModeEnum.Webhook ||
|
||||
mode === NotificationTransportModeEnum.WebhookSlack
|
||||
) {
|
||||
this.showWebhook = true;
|
||||
} else {
|
||||
this.showWebhook = false;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
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)}"
|
||||
@@ -75,6 +95,26 @@ 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 }>) => {
|
||||
@@ -109,7 +149,7 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
|
||||
value="${this.instance?.webhookUrl || ""}"
|
||||
input-hint="code"
|
||||
?hidden=${!this.showWebhook}
|
||||
required
|
||||
?required=${this.showWebhook}
|
||||
>
|
||||
</ak-hidden-text-input>
|
||||
<ak-form-element-horizontal
|
||||
@@ -178,26 +218,39 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
|
||||
>
|
||||
</ak-search-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>`;
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ export class GroupForm extends ModelForm<Group, string> {
|
||||
}
|
||||
|
||||
async send(data: Group): Promise<Group> {
|
||||
data.attributes ??= {};
|
||||
if (this.instance?.pk) {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreGroupsPartialUpdate({
|
||||
groupUuid: this.instance.pk,
|
||||
@@ -145,7 +146,7 @@ export class GroupForm extends ModelForm<Group, string> {
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Attributes")} required name="attributes">
|
||||
<ak-form-element-horizontal label=${msg("Attributes")} name="attributes">
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.YAML}
|
||||
value="${YAML.stringify(this.instance?.attributes ?? {})}"
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { APIError } from "#common/errors/network";
|
||||
import { MessageLevel } from "#common/messages";
|
||||
|
||||
import { ModelForm } from "#elements/forms/ModelForm";
|
||||
import { APIMessage } from "#elements/messages/Message";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
|
||||
@@ -8,4 +12,14 @@ export abstract class BaseProviderForm<T> extends ModelForm<T, number> {
|
||||
? msg("Successfully updated provider.")
|
||||
: msg("Successfully created provider.");
|
||||
}
|
||||
|
||||
protected override formatAPIErrorMessage(error: APIError): APIMessage {
|
||||
return {
|
||||
level: MessageLevel.error,
|
||||
...super.formatAPIErrorMessage(error),
|
||||
message: this.instance
|
||||
? msg("An error occurred while updating the provider.")
|
||||
: msg("An error occurred while creating the provider."),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ export function renderForm(
|
||||
.redirectURI=${redirectURI}
|
||||
name="oauth2-redirect-uri"
|
||||
style="width: 100%"
|
||||
inputID="redirect-uri-${idx}"
|
||||
input-id="redirect-uri-${idx}"
|
||||
></ak-provider-oauth2-redirect-uri>`;
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -71,10 +71,10 @@ export class SAMLProviderViewPage extends AKElement {
|
||||
metadata?: SAMLMetadata;
|
||||
|
||||
@state()
|
||||
signer?: CertificateKeyPair;
|
||||
signer: CertificateKeyPair | null = null;
|
||||
|
||||
@state()
|
||||
verifier?: CertificateKeyPair;
|
||||
verifier: CertificateKeyPair | null = null;
|
||||
|
||||
@state()
|
||||
previewUser?: User;
|
||||
@@ -97,7 +97,7 @@ export class SAMLProviderViewPage extends AKElement {
|
||||
super();
|
||||
this.addEventListener(EVENT_REFRESH, () => {
|
||||
if (!this.provider?.pk) return;
|
||||
this.providerID = this.provider?.pk;
|
||||
this.fetchProvider(this.provider.pk);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -117,20 +117,32 @@ export class SAMLProviderViewPage extends AKElement {
|
||||
}
|
||||
|
||||
fetchSigningCertificate(kpUuid: string) {
|
||||
this.fetchCertificate(kpUuid).then((kp) => (this.signer = kp));
|
||||
this.fetchCertificate(kpUuid).then((kp) => {
|
||||
this.signer = kp;
|
||||
this.requestUpdate("signer");
|
||||
});
|
||||
}
|
||||
|
||||
fetchVerificationCertificate(kpUuid: string) {
|
||||
this.fetchCertificate(kpUuid).then((kp) => (this.verifier = kp));
|
||||
this.fetchCertificate(kpUuid).then((kp) => {
|
||||
this.verifier = kp;
|
||||
this.requestUpdate("verifier");
|
||||
});
|
||||
}
|
||||
|
||||
fetchProvider(id: number) {
|
||||
new ProvidersApi(DEFAULT_CONFIG).providersSamlRetrieve({ id }).then((prov) => {
|
||||
this.provider = prov;
|
||||
if (this.provider.signingKp) {
|
||||
// Clear existing signing certificate if the provider has none
|
||||
if (!this.provider.signingKp) {
|
||||
this.signer = null;
|
||||
} else {
|
||||
this.fetchSigningCertificate(this.provider.signingKp);
|
||||
}
|
||||
if (this.provider.verificationKp) {
|
||||
// Clear existing verification certificate if the provider has none
|
||||
if (!this.provider.verificationKp) {
|
||||
this.verifier = null;
|
||||
} else {
|
||||
this.fetchVerificationCertificate(this.provider.verificationKp);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -530,9 +530,8 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-group label=${msg("Advanced settings")}>
|
||||
<div class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Policy engine mode")}
|
||||
required
|
||||
|
||||
@@ -414,9 +414,8 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-group label=${msg("Advanced settings")}>
|
||||
<div class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Policy engine mode")}
|
||||
required
|
||||
|
||||
@@ -574,9 +574,8 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-group label=${msg("Advanced settings")}>
|
||||
<div class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Policy engine mode")}
|
||||
required
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
FlowsInstancesListDesignationEnum,
|
||||
FlowsInstancesListRequest,
|
||||
StagesApi,
|
||||
TypeCreate,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
@@ -33,6 +34,12 @@ 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;
|
||||
|
||||
@@ -262,6 +269,28 @@ 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>`;
|
||||
}
|
||||
|
||||
@@ -180,11 +180,7 @@ export class UserForm extends ModelForm<User, number> {
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Attributes")}
|
||||
?required=${false}
|
||||
name="attributes"
|
||||
>
|
||||
<ak-form-element-horizontal label=${msg("Attributes")} name="attributes">
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.YAML}
|
||||
value="${YAML.stringify(
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
import "#components/ak-text-input";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { globalAK } from "#common/global";
|
||||
import { MessageLevel } from "#common/messages";
|
||||
|
||||
import { Form } from "#elements/forms/Form";
|
||||
import { APIMessage } from "#elements/messages/Message";
|
||||
|
||||
import { CoreApi, ImpersonationRequest } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-user-impersonate-form")
|
||||
export class UserImpersonateForm extends Form<ImpersonationRequest> {
|
||||
@property({ type: Number })
|
||||
instancePk?: number;
|
||||
public instancePk?: number;
|
||||
|
||||
protected override formatAPISuccessMessage(): APIMessage | null {
|
||||
return {
|
||||
level: MessageLevel.success,
|
||||
message: msg(str`Impersonating user...`),
|
||||
description: msg("This may take a few seconds."),
|
||||
};
|
||||
}
|
||||
|
||||
async send(data: ImpersonationRequest): Promise<void> {
|
||||
return new CoreApi(DEFAULT_CONFIG)
|
||||
@@ -23,7 +32,7 @@ export class UserImpersonateForm extends Form<ImpersonationRequest> {
|
||||
impersonationRequest: data,
|
||||
})
|
||||
.then(() => {
|
||||
window.location.href = globalAK().api.base;
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,7 +40,12 @@ export class UserImpersonateForm extends Form<ImpersonationRequest> {
|
||||
return html`<ak-text-input
|
||||
name="reason"
|
||||
label=${msg("Reason")}
|
||||
help=${msg("Reason for impersonating the user")}
|
||||
autocomplete="off"
|
||||
placeholder=${msg("Reason for impersonating the user")}
|
||||
help=${msg(
|
||||
"A brief explanation of why you are impersonating the user. This will be included in audit logs.",
|
||||
)}
|
||||
required
|
||||
></ak-text-input>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
ValidationErrorFromJSON,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { sentenceCase } from "change-case";
|
||||
|
||||
//#region HTTP
|
||||
|
||||
/**
|
||||
@@ -233,3 +235,25 @@ export async function parseAPIResponseError<T extends APIError = APIError>(
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Validation errors
|
||||
|
||||
/**
|
||||
* Pluck a field error from a validation error.
|
||||
*
|
||||
* This is used to create a fallback error message when the API returns
|
||||
* a validation error that isn't associated with field within the form.
|
||||
*
|
||||
* We can still show the error message, to at least give the user some feedback.
|
||||
*/
|
||||
export function pluckFallbackFieldErrors(parsedError: APIError): string[] {
|
||||
for (const [fieldName, fieldErrors] of Object.entries(parsedError)) {
|
||||
if (Array.isArray(fieldErrors)) {
|
||||
return [`${sentenceCase(fieldName)}: ${fieldErrors.join(", ")}`];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -20,6 +20,22 @@ 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.
|
||||
*
|
||||
@@ -105,7 +121,9 @@ export function renderStaticHTMLUnsafe(untrustedHTML: unknown): string {
|
||||
|
||||
render(untrustedHTML, container);
|
||||
|
||||
const result = container.innerHTML;
|
||||
|
||||
const result = container.innerHTML
|
||||
// Remove all comments as they can interfere with the styles.
|
||||
.replaceAll("<!---->", "")
|
||||
.replaceAll(/<!--\?lit\$\d+\$-->/g, "");
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import { SlottedTemplateResult } from "../elements/types";
|
||||
import { AKElement, type AKElementProps } from "#elements/Base";
|
||||
|
||||
import { ErrorProp } from "#components/ak-field-errors";
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { IDGenerator } from "@goauthentik/core/id";
|
||||
|
||||
import { html, nothing, TemplateResult } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
export interface HorizontalLightComponentProps<T> extends AKElementProps {
|
||||
name: string;
|
||||
@@ -40,6 +40,8 @@ export abstract class HorizontalLightComponent<T>
|
||||
return this;
|
||||
}
|
||||
|
||||
//#region Properties
|
||||
|
||||
/**
|
||||
* The name attribute for the form element
|
||||
* @property
|
||||
@@ -61,7 +63,7 @@ export abstract class HorizontalLightComponent<T>
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
required = false;
|
||||
public required?: boolean;
|
||||
|
||||
/**
|
||||
* Help text to display below the form element. Optional
|
||||
@@ -96,10 +98,9 @@ export abstract class HorizontalLightComponent<T>
|
||||
* @property
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
errorMessages: string[] = [];
|
||||
public errorMessages?: ErrorProp[];
|
||||
|
||||
/**
|
||||
* @attribute
|
||||
* @property
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
@@ -114,11 +115,21 @@ export abstract class HorizontalLightComponent<T>
|
||||
@property({ type: String, attribute: "input-hint" })
|
||||
inputHint?: string;
|
||||
|
||||
protected renderControl() {
|
||||
throw new Error("Must be implemented in a subclass");
|
||||
}
|
||||
/**
|
||||
* A unique ID to associate with the input and label.
|
||||
* @property
|
||||
*/
|
||||
@property({ type: String, reflect: false })
|
||||
public fieldID?: string = IDGenerator.elementID().toString();
|
||||
|
||||
protected fieldID = IDGenerator.elementID().toString();
|
||||
//#endregion
|
||||
|
||||
//#region Rendering
|
||||
|
||||
/**
|
||||
* Render the control element, e.g. an input, textarea, select, etc.
|
||||
*/
|
||||
protected abstract renderControl(): SlottedTemplateResult;
|
||||
|
||||
protected renderHelp(): SlottedTemplateResult | SlottedTemplateResult[] {
|
||||
const bigHelp: SlottedTemplateResult[] = Array.isArray(this.bighelp)
|
||||
@@ -133,14 +144,19 @@ export abstract class HorizontalLightComponent<T>
|
||||
|
||||
render() {
|
||||
return html`<ak-form-element-horizontal
|
||||
fieldID=${this.fieldID}
|
||||
label=${ifDefined(this.label)}
|
||||
.fieldID=${this.fieldID}
|
||||
?required=${this.required}
|
||||
?hidden=${this.hidden}
|
||||
name=${this.name}
|
||||
.errorMessages=${this.errorMessages}
|
||||
>
|
||||
<div slot="label" class="pf-c-form__group-label">
|
||||
${AKLabel({ htmlFor: this.fieldID, required: this.required }, this.label)}
|
||||
</div>
|
||||
|
||||
${this.renderControl()} ${this.renderHelp()}
|
||||
</ak-form-element-horizontal> `;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import "#elements/buttons/Dropdown";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { StripHTMLTrustPolicy } from "#common/purify";
|
||||
import { rootInterface } from "#common/theme";
|
||||
|
||||
import { FormAssociated, FormAssociatedElement } from "#elements/forms/form-associated-element";
|
||||
import { PaginatedResponse } from "#elements/table/Table";
|
||||
|
||||
import DjangoQL, { Introspections } from "@mrmarble/djangoql-completion";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators.js";
|
||||
import { css, CSSResult, html, LitElement, nothing, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property } 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";
|
||||
@@ -25,40 +29,26 @@ export class QL extends DjangoQL {
|
||||
textareaResize() {}
|
||||
}
|
||||
|
||||
@customElement("ak-search-ql")
|
||||
export class QLSearch extends AKElement {
|
||||
@property()
|
||||
value?: string;
|
||||
/**
|
||||
* 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;
|
||||
|
||||
@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);
|
||||
if (delta < 0) {
|
||||
return (length + delta) % length;
|
||||
}
|
||||
|
||||
static styles: CSSResult[] = [
|
||||
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[] = [
|
||||
PFBase,
|
||||
PFFormControl,
|
||||
PFSearchInput,
|
||||
@@ -66,207 +56,409 @@ export class QLSearch extends AKElement {
|
||||
::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ql.pf-c-form-control {
|
||||
font-family: monospace;
|
||||
--input-height: 2.25em;
|
||||
|
||||
height: var(--input-height);
|
||||
min-height: var(--input-height);
|
||||
max-height: calc(var(--input-height) * 6);
|
||||
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);
|
||||
}
|
||||
: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;
|
||||
|
||||
: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;
|
||||
}
|
||||
}
|
||||
|
||||
.pf-c-search-input__menu {
|
||||
position: fixed;
|
||||
min-width: 0;
|
||||
overflow-y: auto;
|
||||
max-height: 50vh;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
firstUpdated() {
|
||||
if (!this.searchElement) {
|
||||
//#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) {
|
||||
return;
|
||||
}
|
||||
this.ql = new QL({
|
||||
|
||||
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({
|
||||
completionEnabled: true,
|
||||
introspections: {
|
||||
current_model: "",
|
||||
models: {},
|
||||
},
|
||||
selector: this.searchElement,
|
||||
selector: textarea,
|
||||
autoResize: false,
|
||||
});
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) {
|
||||
|
||||
const canvas = new OffscreenCanvas(300, 150);
|
||||
this.#ctx = canvas.getContext("2d");
|
||||
|
||||
if (!this.#ctx) {
|
||||
console.error("authentik/ql: failed to get canvas context");
|
||||
return;
|
||||
}
|
||||
context.font = window.getComputedStyle(this.searchElement).font;
|
||||
this.canvas = context;
|
||||
}
|
||||
|
||||
refreshCompletions() {
|
||||
this.value = this.searchElement?.value;
|
||||
if (!this.ql) {
|
||||
return;
|
||||
}
|
||||
this.ql.generateSuggestions();
|
||||
if (this.ql.suggestions.length < 1 || this.ql.loading) {
|
||||
this.menuOpen = false;
|
||||
return;
|
||||
}
|
||||
this.menuOpen = true;
|
||||
this.updateDropdownPosition();
|
||||
this.requestUpdate();
|
||||
}
|
||||
this.#ctx.font = window.getComputedStyle(textarea).font;
|
||||
|
||||
updateDropdownPosition() {
|
||||
if (!this.searchElement) {
|
||||
return;
|
||||
}
|
||||
const bcr = this.getBoundingClientRect();
|
||||
// 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;
|
||||
const metrics = this.#ctx?.measureText("m");
|
||||
this.#letterWidth = Math.ceil(metrics?.width || 0) + 1;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Completions
|
||||
|
||||
#refreshCompletions = () => {
|
||||
if (this.anchor) {
|
||||
this.value = this.anchor.value;
|
||||
}
|
||||
|
||||
if (!this.#ql) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#ql.generateSuggestions();
|
||||
|
||||
if (this.#ql.suggestions.length < 1 || this.#ql.loading) {
|
||||
this.open = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
|
||||
this.requestUpdate();
|
||||
|
||||
requestAnimationFrame(this.#updateDropdownPosition);
|
||||
};
|
||||
|
||||
#updateDropdownPosition = () => {
|
||||
const anchor = this.anchorRef.value;
|
||||
const menu = this.#menuRef.value;
|
||||
|
||||
if (!anchor || !menu) return;
|
||||
|
||||
const bcr = this.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(anchor);
|
||||
|
||||
// Mostly static variables for padding, font line-height and how many
|
||||
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 lineHeight = parseInt(style.lineHeight, 10);
|
||||
const paddingTop = parseInt(style.paddingTop, 10);
|
||||
const paddingLeft = parseInt(style.paddingLeft, 10);
|
||||
const paddingRight = parseInt(style.paddingRight, 10);
|
||||
|
||||
const actualInnerWidth = bcr.width - paddingLeft - paddingRight;
|
||||
|
||||
let relX = 0;
|
||||
let relY = 1;
|
||||
let letterIndex = 0;
|
||||
|
||||
this.searchElement.value.split(" ").some((word, idx) => {
|
||||
for (const word of anchor.value.split(" ")) {
|
||||
letterIndex += word.length;
|
||||
const newRelX = relX + word.length * letterWidth;
|
||||
const newRelX = relX + word.length * this.#letterWidth;
|
||||
|
||||
if (newRelX > actualInnerWidth) {
|
||||
relY += 1;
|
||||
if (letterIndex > this.searchElement!.selectionStart) {
|
||||
|
||||
if (letterIndex > anchor.selectionStart) {
|
||||
relX =
|
||||
letterWidth * word.length -
|
||||
(letterIndex - this.searchElement!.selectionStart) * letterWidth;
|
||||
return true;
|
||||
this.#letterWidth * word.length -
|
||||
(letterIndex - anchor.selectionStart) * this.#letterWidth;
|
||||
|
||||
break;
|
||||
}
|
||||
relX = word.length * letterWidth;
|
||||
|
||||
relX = word.length * this.#letterWidth;
|
||||
} else {
|
||||
relX = newRelX + 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.cursorX = bcr.x + paddingLeft + relX;
|
||||
this.cursorY = bcr.y + paddingTop + relY * lineHeight;
|
||||
}
|
||||
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);
|
||||
|
||||
onKeyDown(ev: KeyboardEvent) {
|
||||
this.updateDropdownPosition();
|
||||
if (ev.key === "Enter" && ev.metaKey && this.onSearch && this.searchElement) {
|
||||
this.onSearch(this.searchElement?.value);
|
||||
return;
|
||||
}
|
||||
if (!this.menuOpen) return;
|
||||
switch (ev.key) {
|
||||
|
||||
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) {
|
||||
case "ArrowUp":
|
||||
if (this.ql?.suggestions.length) {
|
||||
if (this.selected === undefined) {
|
||||
this.selected = this.ql?.suggestions.length - 1;
|
||||
} else if (this.selected === 0) {
|
||||
this.selected = undefined;
|
||||
if (suggestionsLength) {
|
||||
if (this.selectionIndex === -1) {
|
||||
this.selectionIndex = suggestionsLength - 1;
|
||||
} else {
|
||||
this.selected -= 1;
|
||||
this.selectionIndex = torusIndex(
|
||||
suggestionsLength,
|
||||
this.selectionIndex - 1,
|
||||
);
|
||||
}
|
||||
this.refreshCompletions();
|
||||
ev.preventDefault();
|
||||
|
||||
this.#refreshCompletions();
|
||||
event.preventDefault();
|
||||
}
|
||||
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;
|
||||
|
||||
return;
|
||||
|
||||
case "Tab":
|
||||
if (this.selected) {
|
||||
this.ql?.selectCompletion(this.selected);
|
||||
ev.preventDefault();
|
||||
if (this.selectionIndex) {
|
||||
this.#ql?.selectCompletion(this.selectionIndex);
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
|
||||
return;
|
||||
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.selected !== undefined) {
|
||||
this.ql?.selectCompletion(this.selected);
|
||||
if (this.selectionIndex !== -1) {
|
||||
this.#ql?.selectCompletion(this.selectionIndex);
|
||||
this.selectionIndex = 0;
|
||||
}
|
||||
ev.preventDefault();
|
||||
break;
|
||||
case "Escape":
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
renderMenu() {
|
||||
if (!this.menuOpen || !this.ql) {
|
||||
event.preventDefault();
|
||||
|
||||
return;
|
||||
case "Escape":
|
||||
this.open = false;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
#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) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<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) => {
|
||||
<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)}`;
|
||||
|
||||
return html`<li
|
||||
class="pf-c-search-input__menu-list-item ${this.selected === idx
|
||||
role="option"
|
||||
id="suggestion-${idx}"
|
||||
aria-selected=${this.selectionIndex === idx ? "true" : "false"}
|
||||
class="pf-c-search-input__menu-list-item ${this.selectionIndex === 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"
|
||||
>${suggestion.text}</span
|
||||
<span class="pf-c-search-input__menu-item-text pf-m-monospace">
|
||||
${suggestion.text}</span
|
||||
>
|
||||
</button>
|
||||
</li>`;
|
||||
@@ -276,25 +468,33 @@ export class QLSearch extends AKElement {
|
||||
`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
public override 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
|
||||
class="pf-c-form-control ql"
|
||||
${ref(this.anchorRef)}
|
||||
class="pf-c-form-control pf-m-monospace ql"
|
||||
name="search"
|
||||
autocomplete="off"
|
||||
aria-controls="ql-suggestions"
|
||||
?required=${this.required}
|
||||
placeholder=${msg("Search...")}
|
||||
spellcheck="false"
|
||||
@input=${(ev: InputEvent) => this.refreshCompletions()}
|
||||
@keydown=${this.onKeyDown}
|
||||
@input=${this.#refreshCompletions}
|
||||
@focus=${this.#focusListener}
|
||||
@blur=${this.#blurListener}
|
||||
@keydown=${this.#keydownListener}
|
||||
>
|
||||
${ifDefined(this.value)}</textarea
|
||||
${ifDefined(this.#value)}</textarea
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
${this.renderMenu()}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -125,6 +125,7 @@ export class AkSlugInput extends HorizontalLightComponent<string> {
|
||||
|
||||
public override renderControl() {
|
||||
return html`<input
|
||||
id=${ifDefined(this.fieldID)}
|
||||
@input=${(ev: Event) => this.handleTouch(ev)}
|
||||
type="text"
|
||||
value=${ifDefined(this.value)}
|
||||
|
||||
@@ -17,6 +17,7 @@ export class AkTextareaInput extends HorizontalLightComponent<string> {
|
||||
// Prevent the leading spaces added by Prettier's whitespace algo
|
||||
// prettier-ignore
|
||||
return html`<textarea
|
||||
id=${ifDefined(this.fieldID)}
|
||||
@input=${setValue}
|
||||
class="pf-c-form-control"
|
||||
?required=${this.required}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { html } from "lit";
|
||||
const ACTIONS: QuickAction[] = [
|
||||
["Create a new application", "/core/applications"],
|
||||
["Check the logs", "/events/log"],
|
||||
["Explore integrations", "https://goauthentik.io/integrations/", true],
|
||||
["Explore integrations", "https://integrations.goauthentik.io/", true],
|
||||
["Manage users", "/identity/users"],
|
||||
["Check the release notes", "https://goauthentik.io/docs/releases/", true],
|
||||
];
|
||||
|
||||
@@ -11,7 +11,7 @@ import { html } from "lit";
|
||||
const ACTIONS: QuickAction[] = [
|
||||
["Create a new application", "/core/applications"],
|
||||
["Check the logs", "/events/log"],
|
||||
["Explore integrations", "https://goauthentik.io/integrations/", true],
|
||||
["Explore integrations", "https://integrations.goauthentik.io/", true],
|
||||
["Manage users", "/identity/users"],
|
||||
["Check the release notes", "https://goauthentik.io/docs/releases/", true],
|
||||
];
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
import { parseAPIResponseError, pluckErrorDetail } from "#common/errors/network";
|
||||
import {
|
||||
APIError,
|
||||
parseAPIResponseError,
|
||||
pluckErrorDetail,
|
||||
pluckFallbackFieldErrors,
|
||||
} from "#common/errors/network";
|
||||
import { MessageLevel } from "#common/messages";
|
||||
import { dateToUTC } from "#common/temporal";
|
||||
|
||||
@@ -8,17 +13,18 @@ import { AKElement } from "#elements/Base";
|
||||
import { reportValidityDeep } from "#elements/forms/FormGroup";
|
||||
import { PreventFormSubmit } from "#elements/forms/helpers";
|
||||
import { HorizontalFormElement } from "#elements/forms/HorizontalFormElement";
|
||||
import { APIMessage } from "#elements/messages/Message";
|
||||
import { showMessage } from "#elements/messages/MessageContainer";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
import { createFileMap, isNamedElement, NamedElement } from "#elements/utils/inputs";
|
||||
|
||||
import { ErrorProp } from "#components/ak-field-errors";
|
||||
|
||||
import { instanceOfValidationError } from "@goauthentik/api";
|
||||
import { instanceOfValidationError, ValidationError } from "@goauthentik/api";
|
||||
|
||||
import { snakeCase } from "change-case";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
@@ -143,6 +149,41 @@ export function serializeForm<T = Record<string, unknown>>(elements: Iterable<AK
|
||||
return json as unknown as T;
|
||||
}
|
||||
|
||||
//#region Validation Reporting
|
||||
|
||||
/**
|
||||
* Assign all input-related errors to their respective elements.
|
||||
*/
|
||||
function reportInvalidFields(
|
||||
parsedError: ValidationError,
|
||||
elements: Iterable<HorizontalFormElement>,
|
||||
): HorizontalFormElement[] {
|
||||
const invalidFields: HorizontalFormElement[] = [];
|
||||
|
||||
for (const element of elements) {
|
||||
element.requestUpdate();
|
||||
|
||||
const elementName = element.name;
|
||||
|
||||
if (!elementName) continue;
|
||||
|
||||
const snakeProperty = snakeCase(elementName);
|
||||
const errorMessages: ErrorProp[] = parsedError[snakeProperty] ?? [];
|
||||
|
||||
element.errorMessages = errorMessages;
|
||||
|
||||
if (Array.isArray(errorMessages) && errorMessages.length) {
|
||||
invalidFields.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
return invalidFields;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Form
|
||||
|
||||
/**
|
||||
* Form
|
||||
*
|
||||
@@ -180,8 +221,8 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
|
||||
//#region Properties
|
||||
|
||||
@property()
|
||||
public successMessage = "";
|
||||
@property({ type: String })
|
||||
public successMessage?: string;
|
||||
|
||||
@property({ type: String })
|
||||
public autocomplete?: AutoFill;
|
||||
@@ -226,11 +267,38 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
|
||||
/**
|
||||
* An overridable method for returning a success message after a successful submission.
|
||||
*
|
||||
* @deprecated Use `formatAPISuccessMessage` instead.
|
||||
*/
|
||||
protected getSuccessMessage(): string {
|
||||
protected getSuccessMessage(): string | undefined {
|
||||
return this.successMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* An overridable method for returning a formatted message after a successful submission.
|
||||
*/
|
||||
protected formatAPISuccessMessage(response: unknown): APIMessage | null {
|
||||
const message = this.getSuccessMessage();
|
||||
|
||||
if (!message) return null;
|
||||
|
||||
return {
|
||||
level: MessageLevel.success,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* An overridable method for returning a formatted error message after a failed submission.
|
||||
*/
|
||||
protected formatAPIErrorMessage(error: APIError): APIMessage | null {
|
||||
return {
|
||||
message: msg("There was an error submitting the form."),
|
||||
description: pluckErrorDetail(error, pluckFallbackFieldErrors(error)[0]),
|
||||
level: MessageLevel.error,
|
||||
};
|
||||
}
|
||||
|
||||
//#region Public methods
|
||||
|
||||
public reset(): void {
|
||||
@@ -294,10 +362,7 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
|
||||
return this.send(data)
|
||||
.then((response) => {
|
||||
showMessage({
|
||||
level: MessageLevel.success,
|
||||
message: this.getSuccessMessage(),
|
||||
});
|
||||
showMessage(this.formatAPISuccessMessage(response));
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
@@ -314,81 +379,32 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
}
|
||||
|
||||
const parsedError = await parseAPIResponseError(error);
|
||||
let errorMessage = pluckErrorDetail(error);
|
||||
let focused = false;
|
||||
|
||||
//#region Validation errors
|
||||
|
||||
if (instanceOfValidationError(parsedError)) {
|
||||
// assign all input-related errors to their elements
|
||||
const elements =
|
||||
this.shadowRoot?.querySelectorAll<HorizontalFormElement>(
|
||||
"ak-form-element-horizontal",
|
||||
) || [];
|
||||
const invalidFields = reportInvalidFields(
|
||||
parsedError,
|
||||
this.renderRoot.querySelectorAll("ak-form-element-horizontal"),
|
||||
);
|
||||
|
||||
for (const element of elements) {
|
||||
element.requestUpdate();
|
||||
const focusTarget = Iterator.from(invalidFields)
|
||||
.map(({ focusTarget }) => focusTarget)
|
||||
.find(Boolean);
|
||||
|
||||
const elementName = element.name;
|
||||
|
||||
if (!elementName) continue;
|
||||
|
||||
const snakeProperty = snakeCase(elementName);
|
||||
const errorMessages: ErrorProp[] = parsedError[snakeProperty] ?? [];
|
||||
|
||||
element.errorMessages = errorMessages;
|
||||
const { controlledElement } = element;
|
||||
|
||||
if (!focused && Array.isArray(errorMessages) && errorMessages.length) {
|
||||
if (
|
||||
controlledElement?.checkVisibility() &&
|
||||
controlledElement instanceof HTMLElement
|
||||
) {
|
||||
focused = true;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
return controlledElement.focus?.();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedError.nonFieldErrors) {
|
||||
if (focusTarget) {
|
||||
requestAnimationFrame(() => focusTarget.focus());
|
||||
} else if (Array.isArray(parsedError.nonFieldErrors)) {
|
||||
this.nonFieldErrors = parsedError.nonFieldErrors;
|
||||
} else if (!focused) {
|
||||
// It's possible that the API has returned a field error that we're
|
||||
// not aware of. We can still show the error message, to at least
|
||||
// give the user some feedback.
|
||||
for (const [fieldName, fieldErrors] of Object.entries(parsedError)) {
|
||||
if (Array.isArray(fieldErrors)) {
|
||||
this.nonFieldErrors = [
|
||||
msg(str`${fieldName}: ${fieldErrors.join(", ")}`),
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.nonFieldErrors = pluckFallbackFieldErrors(parsedError);
|
||||
|
||||
console.error(
|
||||
"authentik/forms: API rejected the form submission due to an invalid field that doesn't appear to be in the form. This is likely a bug in authentik.",
|
||||
parsedError,
|
||||
);
|
||||
}
|
||||
|
||||
errorMessage = msg("Invalid update request.");
|
||||
|
||||
// Only change the message when we have `detail`.
|
||||
// Everything else is handled in the form.
|
||||
if ("detail" in parsedError) {
|
||||
errorMessage = parsedError.detail;
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
showMessage({
|
||||
message: errorMessage,
|
||||
level: MessageLevel.error,
|
||||
});
|
||||
showMessage(this.formatAPIErrorMessage(parsedError), true);
|
||||
|
||||
// Rethrow the error so the form doesn't close.
|
||||
throw error;
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { AKElement } from "#elements/Base";
|
||||
|
||||
import { ErrorDetail } from "@goauthentik/api";
|
||||
|
||||
import { CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
/**
|
||||
* This is used in two places outside of Flow, and in both cases is used primarily to
|
||||
* display content, not take input. It displays the TOTP QR code, and the static
|
||||
* recovery tokens. But it's used a lot in Flow.
|
||||
*/
|
||||
|
||||
@customElement("ak-form-element")
|
||||
export class FormElement extends AKElement {
|
||||
static styles: CSSResult[] = [PFBase, PFForm, PFFormControl];
|
||||
|
||||
@property()
|
||||
label?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
required = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
set errors(value: ErrorDetail[] | undefined) {
|
||||
this._errors = value;
|
||||
const hasError = (value || []).length > 0;
|
||||
this.querySelectorAll("input").forEach((input) => {
|
||||
input.setAttribute("aria-invalid", hasError.toString());
|
||||
});
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
_errors?: ErrorDetail[];
|
||||
|
||||
updated(): void {
|
||||
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach((input) => {
|
||||
input.focus();
|
||||
});
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<div class="pf-c-form__group">
|
||||
<label class="pf-c-form__label">
|
||||
<span class="pf-c-form__label-text">${this.label}</span>
|
||||
${this.required
|
||||
? html`<span class="pf-c-form__label-required" aria-hidden="true">*</span>`
|
||||
: html``}
|
||||
</label>
|
||||
<slot></slot>
|
||||
${(this._errors || []).map((error) => {
|
||||
return html`<p class="pf-c-form__helper-text pf-m-error">
|
||||
<span class="pf-c-form__helper-text-icon">
|
||||
<i class="fas fa-exclamation-circle" aria-hidden="true"></i> </span
|
||||
>${error.string}
|
||||
</p>`;
|
||||
})}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-form-element": FormElement;
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,20 @@ export class HorizontalFormElement extends AKElement {
|
||||
|
||||
//#endregion
|
||||
|
||||
public controlledElement: NamedElement | AkControlElement | null = null;
|
||||
#controlledElement: AkControlElement | NamedElement | null = null;
|
||||
|
||||
/**
|
||||
* The element that should be focused when the form is submitted.
|
||||
*/
|
||||
public get focusTarget(): AkControlElement | NamedElement<HTMLElement> | null {
|
||||
if (!(this.#controlledElement instanceof HTMLElement)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.#controlledElement.checkVisibility()) return null;
|
||||
|
||||
return this.#controlledElement;
|
||||
}
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
@@ -79,8 +92,8 @@ export class HorizontalFormElement extends AKElement {
|
||||
}
|
||||
|
||||
public override updated(changedProperties: PropertyValues<this>): void {
|
||||
if (changedProperties.has("errorMessages") && this.controlledElement) {
|
||||
this.controlledElement.setAttribute(
|
||||
if (changedProperties.has("errorMessages") && this.#controlledElement) {
|
||||
this.#controlledElement.setAttribute(
|
||||
"aria-invalid",
|
||||
this.errorMessages?.length ? "true" : "false",
|
||||
);
|
||||
@@ -99,12 +112,13 @@ export class HorizontalFormElement extends AKElement {
|
||||
for (const element of this.querySelectorAll("*")) {
|
||||
// Is this element capable of being named?
|
||||
if (!isControlElement(element) && !isNameableElement(element)) continue;
|
||||
// And does the element already match the name?
|
||||
if (element.getAttribute("name") === this.name) continue;
|
||||
|
||||
element.setAttribute("name", this.name);
|
||||
this.#controlledElement = element;
|
||||
|
||||
if (element.getAttribute("name") !== this.name) {
|
||||
element.setAttribute("name", this.name);
|
||||
}
|
||||
|
||||
this.controlledElement = element;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,18 @@ export class SearchSelectBase<T> extends AkControlElement<string> implements ISe
|
||||
|
||||
// Used to inform the form of the name of the object
|
||||
@property()
|
||||
name?: string;
|
||||
public name?: string;
|
||||
|
||||
/**
|
||||
* A unique ID to associate with the input and label.
|
||||
* @property
|
||||
*/
|
||||
@property({ type: String, reflect: false })
|
||||
public fieldID?: string;
|
||||
|
||||
// Used to inform the form of the input label.
|
||||
@property()
|
||||
public label?: string;
|
||||
|
||||
// The textual placeholder for the search's <input> object, if currently empty. Used as the
|
||||
// native <input> object's `placeholder` field.
|
||||
@@ -255,6 +266,7 @@ export class SearchSelectBase<T> extends AkControlElement<string> implements ISe
|
||||
|
||||
return html`<ak-search-select-view
|
||||
managed
|
||||
.fieldID=${this.fieldID}
|
||||
.options=${options}
|
||||
value=${ifDefined(value)}
|
||||
?blankable=${this.blankable}
|
||||
|
||||
216
web/src/elements/forms/form-associated-element.ts
Normal file
216
web/src/elements/forms/form-associated-element.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
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
Normal file
41
web/src/elements/forms/types.d.ts
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
@@ -27,7 +27,11 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
*
|
||||
* @todo Consider making this a static method on singleton {@linkcode MessageContainer}
|
||||
*/
|
||||
export function showMessage(message: APIMessage, unique = false): void {
|
||||
export function showMessage(message: APIMessage | null, unique = false): void {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.querySelector<MessageContainer>("ak-message-container");
|
||||
|
||||
if (!container) {
|
||||
@@ -35,7 +39,10 @@ export function showMessage(message: APIMessage, unique = false): void {
|
||||
}
|
||||
|
||||
if (!message.message.trim()) {
|
||||
message.message = msg("Error");
|
||||
console.warn("authentik/messages: `showMessage` received an empty message", message);
|
||||
|
||||
message.message = msg("An unknown error occurred");
|
||||
message.description ??= msg("Please check the browser console for more details.");
|
||||
}
|
||||
|
||||
container.addMessage(message, unique);
|
||||
|
||||
@@ -124,8 +124,8 @@ export abstract class Table<T extends object>
|
||||
@property({ type: String })
|
||||
public order?: string;
|
||||
|
||||
@property({ type: String })
|
||||
public search: string = "";
|
||||
@property({ type: String, attribute: false })
|
||||
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" : ""}"
|
||||
value=${ifDefined(this.search)}
|
||||
.defaultValue=${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,6 +8,7 @@ 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";
|
||||
@@ -16,17 +17,23 @@ import PFToolbar from "@patternfly/patternfly/components/Toolbar/toolbar.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
@customElement("ak-table-search")
|
||||
export class TableSearch extends WithLicenseSummary(AKElement) {
|
||||
@property()
|
||||
public value?: string;
|
||||
export class TableSearchForm extends WithLicenseSummary(AKElement) {
|
||||
@property({ type: String, reflect: false })
|
||||
public defaultValue?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
@property({ type: String })
|
||||
public label = msg("Table Search");
|
||||
|
||||
@property({ type: String })
|
||||
public placeholder = msg("Search...");
|
||||
|
||||
@property({ attribute: false })
|
||||
public supportsQL: boolean = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public apiResponse?: PaginatedResponse<unknown>;
|
||||
|
||||
@property()
|
||||
@property({ attribute: false })
|
||||
public onSearch?: (value: string) => void;
|
||||
|
||||
static styles: CSSResult[] = [
|
||||
@@ -45,25 +52,26 @@ export class TableSearch extends WithLicenseSummary(AKElement) {
|
||||
`,
|
||||
];
|
||||
|
||||
public reset = () => {
|
||||
if (!this.onSearch) return;
|
||||
this.value = "";
|
||||
this.onSearch("");
|
||||
#formRef = createRef<HTMLFormElement>();
|
||||
|
||||
public reset = (): void => {
|
||||
this.#formRef.value?.reset();
|
||||
|
||||
this.onSearch?.("");
|
||||
};
|
||||
|
||||
#submitListener = (event: SubmitEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!this.onSearch) return;
|
||||
const form = this.#formRef.value;
|
||||
|
||||
if (!form || !this.onSearch) return;
|
||||
|
||||
form.reportValidity();
|
||||
|
||||
const form = event.target as HTMLFormElement;
|
||||
const data = new FormData(form);
|
||||
|
||||
const value = data.get("search")?.toString().trim();
|
||||
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
const value = data.get("search")?.toString() ?? "";
|
||||
|
||||
this.onSearch(value);
|
||||
};
|
||||
@@ -71,27 +79,31 @@ export class TableSearch extends WithLicenseSummary(AKElement) {
|
||||
renderInput(): TemplateResult {
|
||||
if (this.supportsQL && this.hasEnterpriseLicense) {
|
||||
return html`<ak-search-ql
|
||||
.apiResponse=${this.apiResponse}
|
||||
.value=${this.value}
|
||||
.onSearch=${(value: string) => {
|
||||
if (!this.onSearch) return;
|
||||
this.onSearch(value);
|
||||
}}
|
||||
aria-label=${ifDefined(this.label)}
|
||||
name="search"
|
||||
required
|
||||
placeholder=${ifDefined(this.placeholder)}
|
||||
value=${ifDefined(this.defaultValue)}
|
||||
.apiResponse=${this.apiResponse}
|
||||
></ak-search-ql>`;
|
||||
}
|
||||
|
||||
return html`<input
|
||||
class="pf-c-form-control"
|
||||
aria-label=${ifDefined(this.label)}
|
||||
name="search"
|
||||
type="search"
|
||||
placeholder=${msg("Search...")}
|
||||
value="${ifDefined(this.value)}"
|
||||
required
|
||||
placeholder=${ifDefined(this.placeholder)}
|
||||
value=${ifDefined(this.defaultValue)}
|
||||
class="pf-c-form-control"
|
||||
/>`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<form class="pf-c-input-group" method="get" @submit=${this.#submitListener}>
|
||||
return html`<form
|
||||
${ref(this.#formRef)}
|
||||
class="pf-c-input-group"
|
||||
@submit=${this.#submitListener}
|
||||
>
|
||||
${this.renderInput()}
|
||||
<button
|
||||
aria-label=${msg("Clear search")}
|
||||
@@ -110,6 +122,6 @@ export class TableSearch extends WithLicenseSummary(AKElement) {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-table-search": TableSearch;
|
||||
"ak-table-search": TableSearchForm;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export function isNameableElement(element: Element): element is NamedElement {
|
||||
return false;
|
||||
}
|
||||
|
||||
return NameableElements.has(element.tagName);
|
||||
return NameableElements.has(element.tagName) || element.getAttribute("name") !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,6 @@ import { AKElement } from "#elements/Base";
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, CSSResult, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
|
||||
|
||||
@@ -18,24 +17,44 @@ export class FormStatic extends AKElement {
|
||||
static styles: CSSResult[] = [
|
||||
PFAvatar,
|
||||
css`
|
||||
/* Form with user */
|
||||
.form-control-static {
|
||||
margin-top: var(--pf-global--spacer--sm);
|
||||
margin-block-start: var(--pf-global--spacer--sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.form-control-static .avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.form-control-static img {
|
||||
margin-right: var(--pf-global--spacer--xs);
|
||||
}
|
||||
.form-control-static a {
|
||||
padding-top: var(--pf-global--spacer--xs);
|
||||
padding-bottom: var(--pf-global--spacer--xs);
|
||||
line-height: var(--pf-global--spacer--xl);
|
||||
gap: var(--pf-global--spacer--sm);
|
||||
|
||||
.pf-c-avatar {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.primary-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1 auto;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.username {
|
||||
flex: 1 1 auto;
|
||||
text-align: left;
|
||||
max-width: 20rem;
|
||||
text-overflow: ellipsis;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
display: box;
|
||||
display: -webkit-box;
|
||||
line-clamp: 3;
|
||||
-webkit-line-clamp: 3;
|
||||
box-orient: vertical;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.links {
|
||||
flex: 0 0 auto;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -44,17 +63,22 @@ export class FormStatic extends AKElement {
|
||||
if (!this.user) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="form-control-static">
|
||||
<div class="avatar">
|
||||
<img
|
||||
class="pf-c-avatar"
|
||||
src="${ifDefined(this.userAvatar)}"
|
||||
alt="${msg("User's avatar")}"
|
||||
/>
|
||||
${this.user}
|
||||
<div class="primary-content">
|
||||
${this.userAvatar
|
||||
? html`<img
|
||||
class="pf-c-avatar"
|
||||
src=${this.userAvatar}
|
||||
alt=${msg("User's avatar")}
|
||||
/>`
|
||||
: nothing}
|
||||
<div class="username" aria-label=${msg("Username")}>${this.user}</div>
|
||||
</div>
|
||||
<div class="links">
|
||||
<slot name="link"></slot>
|
||||
</div>
|
||||
<slot name="link"></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -33,13 +33,10 @@ export class FlowCard extends AKElement {
|
||||
PFLogin,
|
||||
PFTitle,
|
||||
css`
|
||||
slot[name="footer"],
|
||||
slot[name="footer-band"] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
flex-basis: 100%;
|
||||
.pf-c-login__main-footer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
slot[name="footer-band"] {
|
||||
text-align: center;
|
||||
background-color: var(--pf-c-login__main-footer-band--BackgroundColor);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import "#elements/forms/FormElement";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { bound } from "#elements/decorators/bound";
|
||||
import { isActiveElement } from "#elements/utils/focus";
|
||||
|
||||
import { AKFormErrors, ErrorProp } from "#components/ak-field-errors";
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
@@ -11,6 +12,7 @@ import { classMap } from "lit/directives/class-map.js";
|
||||
import { createRef, ref, Ref } from "lit/directives/ref.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
@@ -39,7 +41,7 @@ const Visibility = {
|
||||
|
||||
@customElement("ak-flow-input-password")
|
||||
export class InputPassword extends AKElement {
|
||||
static styles = [PFBase, PFInputGroup, PFFormControl, PFButton];
|
||||
static styles = [PFBase, PFForm, PFInputGroup, PFFormControl, PFButton];
|
||||
|
||||
//#region Properties
|
||||
|
||||
@@ -49,7 +51,7 @@ export class InputPassword extends AKElement {
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String, attribute: "input-id" })
|
||||
inputId = "ak-stage-password-input";
|
||||
public inputID = "ak-stage-password-input";
|
||||
|
||||
/**
|
||||
* The name of the input field.
|
||||
@@ -86,8 +88,8 @@ export class InputPassword extends AKElement {
|
||||
/**
|
||||
* The errors for the input field.
|
||||
*/
|
||||
@property({ type: Object })
|
||||
errors: Record<string, string> = {};
|
||||
@property({ attribute: false })
|
||||
public errors?: ErrorProp[];
|
||||
|
||||
/**
|
||||
* Whether to allow the user to toggle the visibility of the password.
|
||||
@@ -306,37 +308,32 @@ export class InputPassword extends AKElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` <ak-form-element
|
||||
label="${this.label}"
|
||||
required
|
||||
class="pf-c-form__group"
|
||||
.errors=${this.errors}
|
||||
>
|
||||
<div class="pf-c-form__group-control">
|
||||
<div class="pf-c-input-group">
|
||||
<input
|
||||
type=${this.passwordVisible ? "text" : "password"}
|
||||
id=${this.inputId}
|
||||
name=${this.name}
|
||||
placeholder=${this.placeholder}
|
||||
autocomplete="current-password"
|
||||
class="${classMap({
|
||||
"pf-c-form-control": true,
|
||||
"pf-m-icon": true,
|
||||
"pf-m-caps-lock": this.capsLock,
|
||||
})}"
|
||||
required
|
||||
aria-invalid=${this.errors?.length ? "true" : "false"}
|
||||
value=${this.initialValue}
|
||||
${ref(this.inputRef)}
|
||||
/>
|
||||
return html` ${AKLabel({ required: true, htmlFor: this.inputID }, this.label)}
|
||||
<div class="pf-c-form__group">
|
||||
<div class="pf-c-form__group-control">
|
||||
<div class="pf-c-input-group">
|
||||
<input
|
||||
type=${this.passwordVisible ? "text" : "password"}
|
||||
id=${this.inputID}
|
||||
name=${this.name}
|
||||
placeholder=${this.placeholder}
|
||||
autocomplete="current-password"
|
||||
class="${classMap({
|
||||
"pf-c-form-control": true,
|
||||
"pf-m-icon": true,
|
||||
"pf-m-caps-lock": this.capsLock,
|
||||
})}"
|
||||
required
|
||||
aria-invalid=${this.errors?.length ? "true" : "false"}
|
||||
value=${this.initialValue}
|
||||
${ref(this.inputRef)}
|
||||
/>
|
||||
|
||||
${this.renderVisibilityToggle()}
|
||||
${this.renderVisibilityToggle()}
|
||||
</div>
|
||||
${AKFormErrors({ errors: this.errors })} ${this.renderHelperText()}
|
||||
</div>
|
||||
|
||||
${this.renderHelperText()}
|
||||
</div>
|
||||
</ak-form-element>`;
|
||||
</div>`;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import "#elements/forms/FormElement";
|
||||
import "#flow/FormStatic";
|
||||
import "#flow/components/ak-flow-card";
|
||||
|
||||
import { AKFormErrors } from "#components/ak-field-errors";
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
|
||||
import {
|
||||
@@ -16,6 +18,7 @@ import { customElement } from "lit/decorators.js";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
@@ -25,15 +28,24 @@ export class OAuth2DeviceCode extends BaseStage<
|
||||
OAuthDeviceCodeChallenge,
|
||||
OAuthDeviceCodeChallengeResponseRequest
|
||||
> {
|
||||
static styles: CSSResult[] = [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton];
|
||||
static styles: CSSResult[] = [
|
||||
PFBase,
|
||||
PFLogin,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFTitle,
|
||||
PFButton,
|
||||
PFInputGroup,
|
||||
];
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<ak-flow-card .challenge=${this.challenge}>
|
||||
<form
|
||||
class="pf-c-form"
|
||||
@submit=${this.submitForm}
|
||||
>
|
||||
<form class="pf-c-form" @submit=${this.submitForm}>
|
||||
<div class="pf-c-form__group">
|
||||
${AKLabel({ required: true, htmlFor: "device-code-input" }, msg("Device Code"))}
|
||||
|
||||
<input
|
||||
id="device-code-input"
|
||||
type="text"
|
||||
name="code"
|
||||
inputmode="numeric"
|
||||
@@ -45,7 +57,8 @@ export class OAuth2DeviceCode extends BaseStage<
|
||||
value=""
|
||||
required
|
||||
/>
|
||||
</ak-form-element>
|
||||
${AKFormErrors({ errors: this.challenge.responseErrors?.code })}
|
||||
</div>
|
||||
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import "#elements/forms/FormElement";
|
||||
import "#flow/FormStatic";
|
||||
import "#flow/components/ak-flow-card";
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import "#elements/forms/FormElement";
|
||||
import "#flow/FormStatic";
|
||||
import "#flow/components/ak-flow-card";
|
||||
|
||||
import { AKFormErrors } from "#components/ak-field-errors";
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
|
||||
import {
|
||||
@@ -18,6 +20,7 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
@@ -33,6 +36,7 @@ export class AuthenticatorEmailStage extends BaseStage<
|
||||
PFLogin,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFInputGroup,
|
||||
PFTitle,
|
||||
PFButton,
|
||||
];
|
||||
@@ -51,13 +55,13 @@ export class AuthenticatorEmailStage extends BaseStage<
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
<ak-form-element
|
||||
label="${msg("Configure your email")}"
|
||||
required
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {}).email}
|
||||
>
|
||||
<div class="pf-c-form__group">
|
||||
${AKLabel(
|
||||
{ required: true, htmlFor: "email-input" },
|
||||
msg("Configure your email"),
|
||||
)}
|
||||
<input
|
||||
id="email-input"
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="${msg("Please enter your email address.")}"
|
||||
@@ -66,7 +70,8 @@ export class AuthenticatorEmailStage extends BaseStage<
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element>
|
||||
${AKFormErrors({ errors: this.challenge.responseErrors?.email })}
|
||||
</div>
|
||||
${this.renderNonFieldErrors()}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
@@ -93,13 +98,10 @@ export class AuthenticatorEmailStage extends BaseStage<
|
||||
A verification token has been sent to your configured email address
|
||||
${ifDefined(this.challenge.email)}
|
||||
<form class="pf-c-form" @submit=${this.submitForm}>
|
||||
<ak-form-element
|
||||
label="${msg("Code")}"
|
||||
required
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {}).code}
|
||||
>
|
||||
<div class="pf-c-form__group">
|
||||
${AKLabel({ required: true, htmlFor: "code-input" }, msg("Code"))}
|
||||
<input
|
||||
id="code-input"
|
||||
type="text"
|
||||
name="code"
|
||||
inputmode="numeric"
|
||||
@@ -110,7 +112,8 @@ export class AuthenticatorEmailStage extends BaseStage<
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element>
|
||||
${AKFormErrors({ errors: this.challenge.responseErrors?.code })}
|
||||
</div>
|
||||
${this.renderNonFieldErrors()}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user