Compare commits
4 Commits
router-tid
...
fix/issue_
| Author | SHA1 | Date | |
|---|---|---|---|
| 13c8cbf03a | |||
| 1776981f29 | |||
| 5a4df95011 | |||
| f2927e5725 |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2025.2.3
|
current_version = 2025.2.2
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
||||||
@ -17,8 +17,6 @@ optional_value = final
|
|||||||
|
|
||||||
[bumpversion:file:pyproject.toml]
|
[bumpversion:file:pyproject.toml]
|
||||||
|
|
||||||
[bumpversion:file:uv.lock]
|
|
||||||
|
|
||||||
[bumpversion:file:package.json]
|
[bumpversion:file:package.json]
|
||||||
|
|
||||||
[bumpversion:file:docker-compose.yml]
|
[bumpversion:file:docker-compose.yml]
|
||||||
|
|||||||
@ -43,7 +43,7 @@ COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Stage 3: Build go proxy
|
# Stage 3: Build go proxy
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder
|
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS go-builder
|
||||||
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
@ -76,7 +76,7 @@ COPY ./go.sum /go/src/goauthentik.io/go.sum
|
|||||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||||
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
||||||
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
|
||||||
go build -o /go/authentik ./cmd/server
|
go build -o /go/authentik ./cmd/server
|
||||||
|
|
||||||
# Stage 4: MaxMind GeoIP
|
# Stage 4: MaxMind GeoIP
|
||||||
@ -94,9 +94,9 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
|||||||
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||||
|
|
||||||
# Stage 5: Download uv
|
# Stage 5: Download uv
|
||||||
FROM ghcr.io/astral-sh/uv:0.6.11 AS uv
|
FROM ghcr.io/astral-sh/uv:0.6.10 AS uv
|
||||||
# Stage 6: Base python image
|
# Stage 6: Base python image
|
||||||
FROM ghcr.io/goauthentik/fips-python:3.12.9-slim-bookworm-fips AS python-base
|
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-base
|
||||||
|
|
||||||
ENV VENV_PATH="/ak-root/.venv" \
|
ENV VENV_PATH="/ak-root/.venv" \
|
||||||
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
__version__ = "2025.2.3"
|
__version__ = "2025.2.2"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -46,7 +46,7 @@ LOGGER = get_logger()
|
|||||||
|
|
||||||
def user_app_cache_key(user_pk: str, page_number: int | None = None) -> str:
|
def user_app_cache_key(user_pk: str, page_number: int | None = None) -> str:
|
||||||
"""Cache key where application list for user is saved"""
|
"""Cache key where application list for user is saved"""
|
||||||
key = f"{CACHE_PREFIX}app_access/{user_pk}"
|
key = f"{CACHE_PREFIX}/app_access/{user_pk}"
|
||||||
if page_number:
|
if page_number:
|
||||||
key += f"/{page_number}"
|
key += f"/{page_number}"
|
||||||
return key
|
return key
|
||||||
|
|||||||
@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django_filters.filters import BooleanFilter
|
from django_filters.filters import BooleanFilter
|
||||||
from django_filters.filterset import FilterSet
|
from django_filters.filterset import FilterSet
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.fields import ReadOnlyField, SerializerMethodField
|
from rest_framework.fields import SerializerMethodField
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
from authentik.core.api.object_types import TypesMixin
|
from authentik.core.api.object_types import TypesMixin
|
||||||
@ -18,10 +18,10 @@ from authentik.core.models import Provider
|
|||||||
class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||||
"""Provider Serializer"""
|
"""Provider Serializer"""
|
||||||
|
|
||||||
assigned_application_slug = ReadOnlyField(source="application.slug")
|
assigned_application_slug = SerializerMethodField()
|
||||||
assigned_application_name = ReadOnlyField(source="application.name")
|
assigned_application_name = SerializerMethodField()
|
||||||
assigned_backchannel_application_slug = ReadOnlyField(source="backchannel_application.slug")
|
assigned_backchannel_application_slug = SerializerMethodField()
|
||||||
assigned_backchannel_application_name = ReadOnlyField(source="backchannel_application.name")
|
assigned_backchannel_application_name = SerializerMethodField()
|
||||||
|
|
||||||
component = SerializerMethodField()
|
component = SerializerMethodField()
|
||||||
|
|
||||||
@ -31,6 +31,38 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
return ""
|
return ""
|
||||||
return obj.component
|
return obj.component
|
||||||
|
|
||||||
|
def get_assigned_application_slug(self, obj: Provider) -> str:
|
||||||
|
"""Get application slug, return empty string if no application exists"""
|
||||||
|
try:
|
||||||
|
return obj.application.slug
|
||||||
|
except Provider.application.RelatedObjectDoesNotExist:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_assigned_application_name(self, obj: Provider) -> str:
|
||||||
|
"""Get application name, return empty string if no application exists"""
|
||||||
|
try:
|
||||||
|
return obj.application.name
|
||||||
|
except Provider.application.RelatedObjectDoesNotExist:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_assigned_backchannel_application_slug(self, obj: Provider) -> str:
|
||||||
|
"""Get backchannel application slug.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
|
"""
|
||||||
|
if not obj.backchannel_application:
|
||||||
|
return ""
|
||||||
|
return obj.backchannel_application.slug or ""
|
||||||
|
|
||||||
|
def get_assigned_backchannel_application_name(self, obj: Provider) -> str:
|
||||||
|
"""Get backchannel application name.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
|
"""
|
||||||
|
if not obj.backchannel_application:
|
||||||
|
return ""
|
||||||
|
return obj.backchannel_application.name or ""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Provider
|
model = Provider
|
||||||
fields = [
|
fields = [
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
"""User API Views"""
|
"""User API Views"""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from importlib import import_module
|
|
||||||
from json import loads
|
from json import loads
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth import update_session_auth_hash
|
from django.contrib.auth import update_session_auth_hash
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.contrib.sessions.backends.base import SessionBase
|
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||||
|
from django.core.cache import cache
|
||||||
from django.db.models.functions import ExtractHour
|
from django.db.models.functions import ExtractHour
|
||||||
from django.db.transaction import atomic
|
from django.db.transaction import atomic
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
@ -92,7 +91,6 @@ from authentik.stages.email.tasks import send_mails
|
|||||||
from authentik.stages.email.utils import TemplateEmailMessage
|
from authentik.stages.email.utils import TemplateEmailMessage
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
|
|
||||||
|
|
||||||
|
|
||||||
class UserGroupSerializer(ModelSerializer):
|
class UserGroupSerializer(ModelSerializer):
|
||||||
@ -375,7 +373,7 @@ class UsersFilter(FilterSet):
|
|||||||
method="filter_attributes",
|
method="filter_attributes",
|
||||||
)
|
)
|
||||||
|
|
||||||
is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser")
|
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
|
||||||
uuid = UUIDFilter(field_name="uuid")
|
uuid = UUIDFilter(field_name="uuid")
|
||||||
|
|
||||||
path = CharFilter(field_name="path")
|
path = CharFilter(field_name="path")
|
||||||
@ -393,11 +391,6 @@ class UsersFilter(FilterSet):
|
|||||||
queryset=Group.objects.all().order_by("name"),
|
queryset=Group.objects.all().order_by("name"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def filter_is_superuser(self, queryset, name, value):
|
|
||||||
if value:
|
|
||||||
return queryset.filter(ak_groups__is_superuser=True).distinct()
|
|
||||||
return queryset.exclude(ak_groups__is_superuser=True).distinct()
|
|
||||||
|
|
||||||
def filter_attributes(self, queryset, name, value):
|
def filter_attributes(self, queryset, name, value):
|
||||||
"""Filter attributes by query args"""
|
"""Filter attributes by query args"""
|
||||||
try:
|
try:
|
||||||
@ -776,8 +769,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
if not instance.is_active:
|
if not instance.is_active:
|
||||||
sessions = AuthenticatedSession.objects.filter(user=instance)
|
sessions = AuthenticatedSession.objects.filter(user=instance)
|
||||||
session_ids = sessions.values_list("session_key", flat=True)
|
session_ids = sessions.values_list("session_key", flat=True)
|
||||||
for session in session_ids:
|
cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids)
|
||||||
SessionStore(session).delete()
|
|
||||||
sessions.delete()
|
sessions.delete()
|
||||||
LOGGER.debug("Deleted user's sessions", user=instance.username)
|
LOGGER.debug("Deleted user's sessions", user=instance.username)
|
||||||
return response
|
return response
|
||||||
|
|||||||
@ -761,17 +761,11 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
|||||||
@property
|
@property
|
||||||
def component(self) -> str:
|
def component(self) -> str:
|
||||||
"""Return component used to edit this object"""
|
"""Return component used to edit this object"""
|
||||||
if self.managed == self.MANAGED_INBUILT:
|
|
||||||
return ""
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def property_mapping_type(self) -> "type[PropertyMapping]":
|
def property_mapping_type(self) -> "type[PropertyMapping]":
|
||||||
"""Return property mapping type used by this object"""
|
"""Return property mapping type used by this object"""
|
||||||
if self.managed == self.MANAGED_INBUILT:
|
|
||||||
from authentik.core.models import PropertyMapping
|
|
||||||
|
|
||||||
return PropertyMapping
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def ui_login_button(self, request: HttpRequest) -> UILoginButton | None:
|
def ui_login_button(self, request: HttpRequest) -> UILoginButton | None:
|
||||||
@ -786,14 +780,10 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
|||||||
|
|
||||||
def get_base_user_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
def get_base_user_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
||||||
"""Get base properties for a user to build final properties upon."""
|
"""Get base properties for a user to build final properties upon."""
|
||||||
if self.managed == self.MANAGED_INBUILT:
|
|
||||||
return {}
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_base_group_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
def get_base_group_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
||||||
"""Get base properties for a group to build final properties upon."""
|
"""Get base properties for a group to build final properties upon."""
|
||||||
if self.managed == self.MANAGED_INBUILT:
|
|
||||||
return {}
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
"""authentik core signals"""
|
"""authentik core signals"""
|
||||||
|
|
||||||
from importlib import import_module
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||||
from django.contrib.sessions.backends.base import SessionBase
|
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.signals import Signal
|
from django.core.signals import Signal
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
@ -28,7 +25,6 @@ password_changed = Signal()
|
|||||||
login_failed = Signal()
|
login_failed = Signal()
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Application)
|
@receiver(post_save, sender=Application)
|
||||||
@ -64,7 +60,8 @@ def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
|
|||||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
@receiver(pre_delete, sender=AuthenticatedSession)
|
||||||
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
||||||
"""Delete session when authenticated session is deleted"""
|
"""Delete session when authenticated session is deleted"""
|
||||||
SessionStore(instance.session_key).delete()
|
cache_key = f"{KEY_PREFIX}{instance.session_key}"
|
||||||
|
cache.delete(cache_key)
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_save)
|
@receiver(pre_save)
|
||||||
|
|||||||
@ -36,7 +36,6 @@ from authentik.flows.planner import (
|
|||||||
)
|
)
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import StageView
|
||||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
|
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
|
||||||
from authentik.lib.utils.urls import is_url_absolute
|
|
||||||
from authentik.lib.views import bad_request_message
|
from authentik.lib.views import bad_request_message
|
||||||
from authentik.policies.denied import AccessDeniedResponse
|
from authentik.policies.denied import AccessDeniedResponse
|
||||||
from authentik.policies.utils import delete_none_values
|
from authentik.policies.utils import delete_none_values
|
||||||
@ -210,8 +209,6 @@ class SourceFlowManager:
|
|||||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||||
NEXT_ARG_NAME, "authentik_core:if-user"
|
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||||
)
|
)
|
||||||
if not is_url_absolute(final_redirect):
|
|
||||||
final_redirect = "authentik_core:if-user"
|
|
||||||
flow_context.update(
|
flow_context.update(
|
||||||
{
|
{
|
||||||
# Since we authenticate the user by their token, they have no backend set
|
# Since we authenticate the user by their token, they have no backend set
|
||||||
|
|||||||
@ -133,6 +133,8 @@ class TestApplicationsAPI(APITestCase):
|
|||||||
"provider_obj": {
|
"provider_obj": {
|
||||||
"assigned_application_name": "allowed",
|
"assigned_application_name": "allowed",
|
||||||
"assigned_application_slug": "allowed",
|
"assigned_application_slug": "allowed",
|
||||||
|
"assigned_backchannel_application_name": "",
|
||||||
|
"assigned_backchannel_application_slug": "",
|
||||||
"authentication_flow": None,
|
"authentication_flow": None,
|
||||||
"invalidation_flow": None,
|
"invalidation_flow": None,
|
||||||
"authorization_flow": str(self.provider.authorization_flow.pk),
|
"authorization_flow": str(self.provider.authorization_flow.pk),
|
||||||
@ -186,6 +188,8 @@ class TestApplicationsAPI(APITestCase):
|
|||||||
"provider_obj": {
|
"provider_obj": {
|
||||||
"assigned_application_name": "allowed",
|
"assigned_application_name": "allowed",
|
||||||
"assigned_application_slug": "allowed",
|
"assigned_application_slug": "allowed",
|
||||||
|
"assigned_backchannel_application_name": "",
|
||||||
|
"assigned_backchannel_application_slug": "",
|
||||||
"authentication_flow": None,
|
"authentication_flow": None,
|
||||||
"invalidation_flow": None,
|
"invalidation_flow": None,
|
||||||
"authorization_flow": str(self.provider.authorization_flow.pk),
|
"authorization_flow": str(self.provider.authorization_flow.pk),
|
||||||
|
|||||||
@ -3,7 +3,8 @@
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import PropertyMapping
|
from authentik.core.api.providers import ProviderSerializer
|
||||||
|
from authentik.core.models import Application, PropertyMapping, Provider
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
|
|
||||||
|
|
||||||
@ -24,3 +25,51 @@ class TestProvidersAPI(APITestCase):
|
|||||||
reverse("authentik_api:provider-types"),
|
reverse("authentik_api:provider-types"),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_provider_serializer_without_application(self):
|
||||||
|
"""Test that Provider serializer handles missing application gracefully"""
|
||||||
|
# Create a provider without an application
|
||||||
|
provider = Provider.objects.create(name="test-provider")
|
||||||
|
|
||||||
|
serializer = ProviderSerializer(instance=provider)
|
||||||
|
serialized_data = serializer.data
|
||||||
|
|
||||||
|
# Check that fields return empty strings when no application exists
|
||||||
|
self.assertEqual(serialized_data["assigned_application_slug"], "")
|
||||||
|
self.assertEqual(serialized_data["assigned_application_name"], "")
|
||||||
|
self.assertEqual(serialized_data["assigned_backchannel_application_slug"], "")
|
||||||
|
self.assertEqual(serialized_data["assigned_backchannel_application_name"], "")
|
||||||
|
|
||||||
|
def test_provider_serializer_with_application(self):
|
||||||
|
"""Test that Provider serializer correctly includes application data"""
|
||||||
|
# Create an application
|
||||||
|
app = Application.objects.create(name="Test App", slug="test-app")
|
||||||
|
|
||||||
|
# Create a provider with an application
|
||||||
|
provider = Provider.objects.create(name="test-provider-with-app")
|
||||||
|
app.provider = provider
|
||||||
|
app.save()
|
||||||
|
|
||||||
|
serializer = ProviderSerializer(instance=provider)
|
||||||
|
serialized_data = serializer.data
|
||||||
|
|
||||||
|
# Check that fields return correct values when application exists
|
||||||
|
self.assertEqual(serialized_data["assigned_application_slug"], "test-app")
|
||||||
|
self.assertEqual(serialized_data["assigned_application_name"], "Test App")
|
||||||
|
self.assertEqual(serialized_data["assigned_backchannel_application_slug"], "")
|
||||||
|
self.assertEqual(serialized_data["assigned_backchannel_application_name"], "")
|
||||||
|
|
||||||
|
def test_provider_api_response(self):
|
||||||
|
"""Test that the API response includes empty strings for missing applications"""
|
||||||
|
# Create a provider without an application
|
||||||
|
provider = Provider.objects.create(name="test-provider-api")
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("authentik_api:provider-detail", kwargs={"pk": provider.pk}),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.data["assigned_application_slug"], "")
|
||||||
|
self.assertEqual(response.data["assigned_application_name"], "")
|
||||||
|
self.assertEqual(response.data["assigned_backchannel_application_slug"], "")
|
||||||
|
self.assertEqual(response.data["assigned_backchannel_application_name"], "")
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
from django.apps import apps
|
|
||||||
from django.urls import reverse
|
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
|
||||||
|
|
||||||
|
|
||||||
class TestSourceAPI(APITestCase):
|
|
||||||
def setUp(self) -> None:
|
|
||||||
self.user = create_test_admin_user()
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
|
|
||||||
def test_builtin_source_used_by(self):
|
|
||||||
"""Test Providers's types endpoint"""
|
|
||||||
apps.get_app_config("authentik_core").source_inbuilt()
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("authentik_api:source-used-by", kwargs={"slug": "authentik-built-in"}),
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
"""Test Users API"""
|
"""Test Users API"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from json import loads
|
|
||||||
|
|
||||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
@ -16,12 +15,7 @@ from authentik.core.models import (
|
|||||||
User,
|
User,
|
||||||
UserTypes,
|
UserTypes,
|
||||||
)
|
)
|
||||||
from authentik.core.tests.utils import (
|
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
|
||||||
create_test_admin_user,
|
|
||||||
create_test_brand,
|
|
||||||
create_test_flow,
|
|
||||||
create_test_user,
|
|
||||||
)
|
|
||||||
from authentik.flows.models import FlowDesignation
|
from authentik.flows.models import FlowDesignation
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
from authentik.stages.email.models import EmailStage
|
from authentik.stages.email.models import EmailStage
|
||||||
@ -32,7 +26,7 @@ class TestUsersAPI(APITestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.admin = create_test_admin_user()
|
self.admin = create_test_admin_user()
|
||||||
self.user = create_test_user()
|
self.user = User.objects.create(username="test-user")
|
||||||
|
|
||||||
def test_filter_type(self):
|
def test_filter_type(self):
|
||||||
"""Test API filtering by type"""
|
"""Test API filtering by type"""
|
||||||
@ -47,35 +41,6 @@ class TestUsersAPI(APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_filter_is_superuser(self):
|
|
||||||
"""Test API filtering by superuser status"""
|
|
||||||
User.objects.all().delete()
|
|
||||||
admin = create_test_admin_user()
|
|
||||||
self.client.force_login(admin)
|
|
||||||
# Test superuser
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("authentik_api:user-list"),
|
|
||||||
data={
|
|
||||||
"is_superuser": True,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
body = loads(response.content)
|
|
||||||
self.assertEqual(len(body["results"]), 1)
|
|
||||||
self.assertEqual(body["results"][0]["username"], admin.username)
|
|
||||||
# Test non-superuser
|
|
||||||
user = create_test_user()
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("authentik_api:user-list"),
|
|
||||||
data={
|
|
||||||
"is_superuser": False,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
body = loads(response.content)
|
|
||||||
self.assertEqual(len(body["results"]), 1, body)
|
|
||||||
self.assertEqual(body["results"][0]["username"], user.username)
|
|
||||||
|
|
||||||
def test_list_with_groups(self):
|
def test_list_with_groups(self):
|
||||||
"""Test listing with groups"""
|
"""Test listing with groups"""
|
||||||
self.client.force_login(self.admin)
|
self.client.force_login(self.admin)
|
||||||
@ -134,8 +99,6 @@ class TestUsersAPI(APITestCase):
|
|||||||
def test_recovery_email_no_flow(self):
|
def test_recovery_email_no_flow(self):
|
||||||
"""Test user recovery link (no recovery flow set)"""
|
"""Test user recovery link (no recovery flow set)"""
|
||||||
self.client.force_login(self.admin)
|
self.client.force_login(self.admin)
|
||||||
self.user.email = ""
|
|
||||||
self.user.save()
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk})
|
reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk})
|
||||||
)
|
)
|
||||||
|
|||||||
@ -69,7 +69,6 @@ SESSION_KEY_APPLICATION_PRE = "authentik/flows/application_pre"
|
|||||||
SESSION_KEY_GET = "authentik/flows/get"
|
SESSION_KEY_GET = "authentik/flows/get"
|
||||||
SESSION_KEY_POST = "authentik/flows/post"
|
SESSION_KEY_POST = "authentik/flows/post"
|
||||||
SESSION_KEY_HISTORY = "authentik/flows/history"
|
SESSION_KEY_HISTORY = "authentik/flows/history"
|
||||||
SESSION_KEY_AUTH_STARTED = "authentik/flows/auth_started"
|
|
||||||
QS_KEY_TOKEN = "flow_token" # nosec
|
QS_KEY_TOKEN = "flow_token" # nosec
|
||||||
QS_QUERY = "query"
|
QS_QUERY = "query"
|
||||||
|
|
||||||
@ -454,7 +453,6 @@ class FlowExecutorView(APIView):
|
|||||||
SESSION_KEY_APPLICATION_PRE,
|
SESSION_KEY_APPLICATION_PRE,
|
||||||
SESSION_KEY_PLAN,
|
SESSION_KEY_PLAN,
|
||||||
SESSION_KEY_GET,
|
SESSION_KEY_GET,
|
||||||
SESSION_KEY_AUTH_STARTED,
|
|
||||||
# We might need the initial POST payloads for later requests
|
# We might need the initial POST payloads for later requests
|
||||||
# SESSION_KEY_POST,
|
# SESSION_KEY_POST,
|
||||||
# We don't delete the history on purpose, as a user might
|
# We don't delete the history on purpose, as a user might
|
||||||
|
|||||||
@ -6,22 +6,14 @@ from django.shortcuts import get_object_or_404
|
|||||||
from ua_parser.user_agent_parser import Parse
|
from ua_parser.user_agent_parser import Parse
|
||||||
|
|
||||||
from authentik.core.views.interface import InterfaceView
|
from authentik.core.views.interface import InterfaceView
|
||||||
from authentik.flows.models import Flow, FlowDesignation
|
from authentik.flows.models import Flow
|
||||||
from authentik.flows.views.executor import SESSION_KEY_AUTH_STARTED
|
|
||||||
|
|
||||||
|
|
||||||
class FlowInterfaceView(InterfaceView):
|
class FlowInterfaceView(InterfaceView):
|
||||||
"""Flow interface"""
|
"""Flow interface"""
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||||
flow = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
||||||
kwargs["flow"] = flow
|
|
||||||
if (
|
|
||||||
not self.request.user.is_authenticated
|
|
||||||
and flow.designation == FlowDesignation.AUTHENTICATION
|
|
||||||
):
|
|
||||||
self.request.session[SESSION_KEY_AUTH_STARTED] = True
|
|
||||||
self.request.session.save()
|
|
||||||
kwargs["inspector"] = "inspector" in self.request.GET
|
kwargs["inspector"] = "inspector" in self.request.GET
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|||||||
@ -18,15 +18,6 @@ class SerializerModel(models.Model):
|
|||||||
@property
|
@property
|
||||||
def serializer(self) -> type[BaseSerializer]:
|
def serializer(self) -> type[BaseSerializer]:
|
||||||
"""Get serializer for this model"""
|
"""Get serializer for this model"""
|
||||||
# Special handling for built-in source
|
|
||||||
if (
|
|
||||||
hasattr(self, "managed")
|
|
||||||
and hasattr(self, "MANAGED_INBUILT")
|
|
||||||
and self.managed == self.MANAGED_INBUILT
|
|
||||||
):
|
|
||||||
from authentik.core.api.sources import SourceSerializer
|
|
||||||
|
|
||||||
return SourceSerializer
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -35,4 +35,3 @@ class AuthentikPoliciesConfig(ManagedAppConfig):
|
|||||||
label = "authentik_policies"
|
label = "authentik_policies"
|
||||||
verbose_name = "authentik Policies"
|
verbose_name = "authentik Policies"
|
||||||
default = True
|
default = True
|
||||||
mountpoint = "policy/"
|
|
||||||
|
|||||||
@ -1,89 +0,0 @@
|
|||||||
{% extends 'login/base_full.html' %}
|
|
||||||
|
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
{{ block.super }}
|
|
||||||
<script>
|
|
||||||
let redirecting = false;
|
|
||||||
const checkAuth = async () => {
|
|
||||||
if (redirecting) return true;
|
|
||||||
const url = "{{ check_auth_url }}";
|
|
||||||
console.debug("authentik/policies/buffer: Checking authentication...");
|
|
||||||
try {
|
|
||||||
const result = await fetch(url, {
|
|
||||||
method: "HEAD",
|
|
||||||
});
|
|
||||||
if (result.status >= 400) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
console.debug("authentik/policies/buffer: Continuing");
|
|
||||||
redirecting = true;
|
|
||||||
if ("{{ auth_req_method }}" === "post") {
|
|
||||||
document.querySelector("form").submit();
|
|
||||||
} else {
|
|
||||||
window.location.assign("{{ continue_url|escapejs }}");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let timeout = 100;
|
|
||||||
let offset = 20;
|
|
||||||
let attempt = 0;
|
|
||||||
const main = async () => {
|
|
||||||
attempt += 1;
|
|
||||||
await checkAuth();
|
|
||||||
console.debug(`authentik/policies/buffer: Waiting ${timeout}ms...`);
|
|
||||||
setTimeout(main, timeout);
|
|
||||||
timeout += (offset * attempt);
|
|
||||||
if (timeout >= 2000) {
|
|
||||||
timeout = 2000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener("visibilitychange", async () => {
|
|
||||||
if (document.hidden) return;
|
|
||||||
console.debug("authentik/policies/buffer: Checking authentication on tab activate...");
|
|
||||||
await checkAuth();
|
|
||||||
});
|
|
||||||
main();
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block title %}
|
|
||||||
{% trans 'Waiting for authentication...' %} - {{ brand.branding_title }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block card_title %}
|
|
||||||
{% trans 'Waiting for authentication...' %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block card %}
|
|
||||||
<form class="pf-c-form" method="{{ auth_req_method }}" action="{{ continue_url }}">
|
|
||||||
{% if auth_req_method == "post" %}
|
|
||||||
{% for key, value in auth_req_body.items %}
|
|
||||||
<input type="hidden" name="{{ key }}" value="{{ value }}" />
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
<div class="pf-c-empty-state">
|
|
||||||
<div class="pf-c-empty-state__content">
|
|
||||||
<div class="pf-c-empty-state__icon">
|
|
||||||
<span class="pf-c-spinner pf-m-xl" role="progressbar">
|
|
||||||
<span class="pf-c-spinner__clipper"></span>
|
|
||||||
<span class="pf-c-spinner__lead-ball"></span>
|
|
||||||
<span class="pf-c-spinner__tail-ball"></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<h1 class="pf-c-title pf-m-lg">
|
|
||||||
{% trans "You're already authenticating in another tab. This page will refresh once authentication is completed." %}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pf-c-form__group pf-m-action">
|
|
||||||
<a href="{{ auth_req_url }}" class="pf-c-button pf-m-primary pf-m-block">
|
|
||||||
{% trans "Authenticate in this tab" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
from django.contrib.auth.models import AnonymousUser
|
|
||||||
from django.contrib.sessions.middleware import SessionMiddleware
|
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.test import RequestFactory, TestCase
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
from authentik.core.models import Application, Provider
|
|
||||||
from authentik.core.tests.utils import create_test_flow, create_test_user
|
|
||||||
from authentik.flows.models import FlowDesignation
|
|
||||||
from authentik.flows.planner import FlowPlan
|
|
||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
|
||||||
from authentik.lib.generators import generate_id
|
|
||||||
from authentik.lib.tests.utils import dummy_get_response
|
|
||||||
from authentik.policies.views import (
|
|
||||||
QS_BUFFER_ID,
|
|
||||||
SESSION_KEY_BUFFER,
|
|
||||||
BufferedPolicyAccessView,
|
|
||||||
BufferView,
|
|
||||||
PolicyAccessView,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestPolicyViews(TestCase):
|
|
||||||
"""Test PolicyAccessView"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.factory = RequestFactory()
|
|
||||||
self.user = create_test_user()
|
|
||||||
|
|
||||||
def test_pav(self):
|
|
||||||
"""Test simple policy access view"""
|
|
||||||
provider = Provider.objects.create(
|
|
||||||
name=generate_id(),
|
|
||||||
)
|
|
||||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
|
||||||
|
|
||||||
class TestView(PolicyAccessView):
|
|
||||||
def resolve_provider_application(self):
|
|
||||||
self.provider = provider
|
|
||||||
self.application = app
|
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
|
||||||
return HttpResponse("foo")
|
|
||||||
|
|
||||||
req = self.factory.get("/")
|
|
||||||
req.user = self.user
|
|
||||||
res = TestView.as_view()(req)
|
|
||||||
self.assertEqual(res.status_code, 200)
|
|
||||||
self.assertEqual(res.content, b"foo")
|
|
||||||
|
|
||||||
def test_pav_buffer(self):
|
|
||||||
"""Test simple policy access view"""
|
|
||||||
provider = Provider.objects.create(
|
|
||||||
name=generate_id(),
|
|
||||||
)
|
|
||||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
|
||||||
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
|
||||||
|
|
||||||
class TestView(BufferedPolicyAccessView):
|
|
||||||
def resolve_provider_application(self):
|
|
||||||
self.provider = provider
|
|
||||||
self.application = app
|
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
|
||||||
return HttpResponse("foo")
|
|
||||||
|
|
||||||
req = self.factory.get("/")
|
|
||||||
req.user = AnonymousUser()
|
|
||||||
middleware = SessionMiddleware(dummy_get_response)
|
|
||||||
middleware.process_request(req)
|
|
||||||
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
|
|
||||||
req.session.save()
|
|
||||||
res = TestView.as_view()(req)
|
|
||||||
self.assertEqual(res.status_code, 302)
|
|
||||||
self.assertTrue(res.url.startswith(reverse("authentik_policies:buffer")))
|
|
||||||
|
|
||||||
def test_pav_buffer_skip(self):
|
|
||||||
"""Test simple policy access view (skip buffer)"""
|
|
||||||
provider = Provider.objects.create(
|
|
||||||
name=generate_id(),
|
|
||||||
)
|
|
||||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
|
||||||
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
|
||||||
|
|
||||||
class TestView(BufferedPolicyAccessView):
|
|
||||||
def resolve_provider_application(self):
|
|
||||||
self.provider = provider
|
|
||||||
self.application = app
|
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
|
||||||
return HttpResponse("foo")
|
|
||||||
|
|
||||||
req = self.factory.get("/?skip_buffer=true")
|
|
||||||
req.user = AnonymousUser()
|
|
||||||
middleware = SessionMiddleware(dummy_get_response)
|
|
||||||
middleware.process_request(req)
|
|
||||||
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
|
|
||||||
req.session.save()
|
|
||||||
res = TestView.as_view()(req)
|
|
||||||
self.assertEqual(res.status_code, 302)
|
|
||||||
self.assertTrue(res.url.startswith(reverse("authentik_flows:default-authentication")))
|
|
||||||
|
|
||||||
def test_buffer(self):
|
|
||||||
"""Test buffer view"""
|
|
||||||
uid = generate_id()
|
|
||||||
req = self.factory.get(f"/?{QS_BUFFER_ID}={uid}")
|
|
||||||
req.user = AnonymousUser()
|
|
||||||
middleware = SessionMiddleware(dummy_get_response)
|
|
||||||
middleware.process_request(req)
|
|
||||||
ts = generate_id()
|
|
||||||
req.session[SESSION_KEY_BUFFER % uid] = {
|
|
||||||
"method": "get",
|
|
||||||
"body": {},
|
|
||||||
"url": f"/{ts}",
|
|
||||||
}
|
|
||||||
req.session.save()
|
|
||||||
|
|
||||||
res = BufferView.as_view()(req)
|
|
||||||
self.assertEqual(res.status_code, 200)
|
|
||||||
self.assertIn(ts, res.render().content.decode())
|
|
||||||
@ -1,14 +1,7 @@
|
|||||||
"""API URLs"""
|
"""API URLs"""
|
||||||
|
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from authentik.policies.api.bindings import PolicyBindingViewSet
|
from authentik.policies.api.bindings import PolicyBindingViewSet
|
||||||
from authentik.policies.api.policies import PolicyViewSet
|
from authentik.policies.api.policies import PolicyViewSet
|
||||||
from authentik.policies.views import BufferView
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path("buffer", BufferView.as_view(), name="buffer"),
|
|
||||||
]
|
|
||||||
|
|
||||||
api_urlpatterns = [
|
api_urlpatterns = [
|
||||||
("policies/all", PolicyViewSet),
|
("policies/all", PolicyViewSet),
|
||||||
|
|||||||
@ -1,37 +1,23 @@
|
|||||||
"""authentik access helper classes"""
|
"""authentik access helper classes"""
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import AccessMixin
|
from django.contrib.auth.mixins import AccessMixin
|
||||||
from django.contrib.auth.views import redirect_to_login
|
from django.contrib.auth.views import redirect_to_login
|
||||||
from django.http import HttpRequest, HttpResponse, QueryDict
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import redirect
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.http import urlencode
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic.base import TemplateView, View
|
from django.views.generic.base import View
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import Application, Provider, User
|
from authentik.core.models import Application, Provider, User
|
||||||
from authentik.flows.models import Flow, FlowDesignation
|
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_POST
|
||||||
from authentik.flows.planner import FlowPlan
|
|
||||||
from authentik.flows.views.executor import (
|
|
||||||
SESSION_KEY_APPLICATION_PRE,
|
|
||||||
SESSION_KEY_AUTH_STARTED,
|
|
||||||
SESSION_KEY_PLAN,
|
|
||||||
SESSION_KEY_POST,
|
|
||||||
)
|
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
from authentik.lib.sentry import SentryIgnoredException
|
||||||
from authentik.policies.denied import AccessDeniedResponse
|
from authentik.policies.denied import AccessDeniedResponse
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
QS_BUFFER_ID = "af_bf_id"
|
|
||||||
QS_SKIP_BUFFER = "skip_buffer"
|
|
||||||
SESSION_KEY_BUFFER = "authentik/policies/pav_buffer/%s"
|
|
||||||
|
|
||||||
|
|
||||||
class RequestValidationError(SentryIgnoredException):
|
class RequestValidationError(SentryIgnoredException):
|
||||||
@ -139,65 +125,3 @@ class PolicyAccessView(AccessMixin, View):
|
|||||||
for message in result.messages:
|
for message in result.messages:
|
||||||
messages.error(self.request, _(message))
|
messages.error(self.request, _(message))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def url_with_qs(url: str, **kwargs):
|
|
||||||
"""Update/set querystring of `url` with the parameters in `kwargs`. Original query string
|
|
||||||
parameters are retained"""
|
|
||||||
if "?" not in url:
|
|
||||||
return url + f"?{urlencode(kwargs)}"
|
|
||||||
url, _, qs = url.partition("?")
|
|
||||||
qs = QueryDict(qs, mutable=True)
|
|
||||||
qs.update(kwargs)
|
|
||||||
return url + f"?{urlencode(qs.items())}"
|
|
||||||
|
|
||||||
|
|
||||||
class BufferView(TemplateView):
|
|
||||||
"""Buffer view"""
|
|
||||||
|
|
||||||
template_name = "policies/buffer.html"
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
buf_id = self.request.GET.get(QS_BUFFER_ID)
|
|
||||||
buffer: dict = self.request.session.get(SESSION_KEY_BUFFER % buf_id)
|
|
||||||
kwargs["auth_req_method"] = buffer["method"]
|
|
||||||
kwargs["auth_req_body"] = buffer["body"]
|
|
||||||
kwargs["auth_req_url"] = url_with_qs(buffer["url"], **{QS_SKIP_BUFFER: True})
|
|
||||||
kwargs["check_auth_url"] = reverse("authentik_api:user-me")
|
|
||||||
kwargs["continue_url"] = url_with_qs(buffer["url"], **{QS_BUFFER_ID: buf_id})
|
|
||||||
return super().get_context_data(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class BufferedPolicyAccessView(PolicyAccessView):
|
|
||||||
"""PolicyAccessView which buffers access requests in case the user is not logged in"""
|
|
||||||
|
|
||||||
def handle_no_permission(self):
|
|
||||||
plan: FlowPlan | None = self.request.session.get(SESSION_KEY_PLAN)
|
|
||||||
authenticating = self.request.session.get(SESSION_KEY_AUTH_STARTED)
|
|
||||||
if plan:
|
|
||||||
flow = Flow.objects.filter(pk=plan.flow_pk).first()
|
|
||||||
if not flow or flow.designation != FlowDesignation.AUTHENTICATION:
|
|
||||||
LOGGER.debug("Not buffering request, no flow or flow not for authentication")
|
|
||||||
return super().handle_no_permission()
|
|
||||||
if not plan and authenticating is None:
|
|
||||||
LOGGER.debug("Not buffering request, no flow plan active")
|
|
||||||
return super().handle_no_permission()
|
|
||||||
if self.request.GET.get(QS_SKIP_BUFFER):
|
|
||||||
LOGGER.debug("Not buffering request, explicit skip")
|
|
||||||
return super().handle_no_permission()
|
|
||||||
buffer_id = str(uuid4())
|
|
||||||
LOGGER.debug("Buffering access request", bf_id=buffer_id)
|
|
||||||
self.request.session[SESSION_KEY_BUFFER % buffer_id] = {
|
|
||||||
"body": self.request.POST,
|
|
||||||
"url": self.request.build_absolute_uri(self.request.get_full_path()),
|
|
||||||
"method": self.request.method.lower(),
|
|
||||||
}
|
|
||||||
return redirect(
|
|
||||||
url_with_qs(reverse("authentik_policies:buffer"), **{QS_BUFFER_ID: buffer_id})
|
|
||||||
)
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
response = super().dispatch(request, *args, **kwargs)
|
|
||||||
if QS_BUFFER_ID in self.request.GET:
|
|
||||||
self.request.session.pop(SESSION_KEY_BUFFER % self.request.GET[QS_BUFFER_ID], None)
|
|
||||||
return response
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@ from authentik.flows.stage import StageView
|
|||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
from authentik.lib.views import bad_request_message
|
from authentik.lib.views import bad_request_message
|
||||||
from authentik.policies.types import PolicyRequest
|
from authentik.policies.types import PolicyRequest
|
||||||
from authentik.policies.views import BufferedPolicyAccessView, RequestValidationError
|
from authentik.policies.views import PolicyAccessView, RequestValidationError
|
||||||
from authentik.providers.oauth2.constants import (
|
from authentik.providers.oauth2.constants import (
|
||||||
PKCE_METHOD_PLAIN,
|
PKCE_METHOD_PLAIN,
|
||||||
PKCE_METHOD_S256,
|
PKCE_METHOD_S256,
|
||||||
@ -328,7 +328,7 @@ class OAuthAuthorizationParams:
|
|||||||
return code
|
return code
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationFlowInitView(BufferedPolicyAccessView):
|
class AuthorizationFlowInitView(PolicyAccessView):
|
||||||
"""OAuth2 Flow initializer, checks access to application and starts flow"""
|
"""OAuth2 Flow initializer, checks access to application and starts flow"""
|
||||||
|
|
||||||
params: OAuthAuthorizationParams
|
params: OAuthAuthorizationParams
|
||||||
|
|||||||
@ -74,6 +74,8 @@ class TestEndpointsAPI(APITestCase):
|
|||||||
"component": "ak-provider-rac-form",
|
"component": "ak-provider-rac-form",
|
||||||
"assigned_application_slug": self.app.slug,
|
"assigned_application_slug": self.app.slug,
|
||||||
"assigned_application_name": self.app.name,
|
"assigned_application_name": self.app.name,
|
||||||
|
"assigned_backchannel_application_slug": "",
|
||||||
|
"assigned_backchannel_application_name": "",
|
||||||
"verbose_name": "RAC Provider",
|
"verbose_name": "RAC Provider",
|
||||||
"verbose_name_plural": "RAC Providers",
|
"verbose_name_plural": "RAC Providers",
|
||||||
"meta_model_name": "authentik_providers_rac.racprovider",
|
"meta_model_name": "authentik_providers_rac.racprovider",
|
||||||
@ -124,6 +126,8 @@ class TestEndpointsAPI(APITestCase):
|
|||||||
"component": "ak-provider-rac-form",
|
"component": "ak-provider-rac-form",
|
||||||
"assigned_application_slug": self.app.slug,
|
"assigned_application_slug": self.app.slug,
|
||||||
"assigned_application_name": self.app.name,
|
"assigned_application_name": self.app.name,
|
||||||
|
"assigned_backchannel_application_slug": "",
|
||||||
|
"assigned_backchannel_application_name": "",
|
||||||
"connection_expiry": "hours=8",
|
"connection_expiry": "hours=8",
|
||||||
"delete_token_on_disconnect": False,
|
"delete_token_on_disconnect": False,
|
||||||
"verbose_name": "RAC Provider",
|
"verbose_name": "RAC Provider",
|
||||||
@ -153,6 +157,8 @@ class TestEndpointsAPI(APITestCase):
|
|||||||
"component": "ak-provider-rac-form",
|
"component": "ak-provider-rac-form",
|
||||||
"assigned_application_slug": self.app.slug,
|
"assigned_application_slug": self.app.slug,
|
||||||
"assigned_application_name": self.app.name,
|
"assigned_application_name": self.app.name,
|
||||||
|
"assigned_backchannel_application_slug": "",
|
||||||
|
"assigned_backchannel_application_name": "",
|
||||||
"connection_expiry": "hours=8",
|
"connection_expiry": "hours=8",
|
||||||
"delete_token_on_disconnect": False,
|
"delete_token_on_disconnect": False,
|
||||||
"verbose_name": "RAC Provider",
|
"verbose_name": "RAC Provider",
|
||||||
|
|||||||
@ -18,11 +18,11 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
|
|||||||
from authentik.flows.stage import RedirectStage
|
from authentik.flows.stage import RedirectStage
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
from authentik.policies.views import BufferedPolicyAccessView
|
from authentik.policies.views import PolicyAccessView
|
||||||
from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider
|
from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider
|
||||||
|
|
||||||
|
|
||||||
class RACStartView(BufferedPolicyAccessView):
|
class RACStartView(PolicyAccessView):
|
||||||
"""Start a RAC connection by checking access and creating a connection token"""
|
"""Start a RAC connection by checking access and creating a connection token"""
|
||||||
|
|
||||||
endpoint: Endpoint
|
endpoint: Endpoint
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
# Generated by Django 5.0.13 on 2025-03-31 13:50
|
|
||||||
|
|
||||||
import authentik.lib.models
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_providers_saml", "0017_samlprovider_authn_context_class_ref_mapping"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="samlprovider",
|
|
||||||
name="acs_url",
|
|
||||||
field=models.TextField(
|
|
||||||
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
|
|
||||||
verbose_name="ACS URL",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -10,7 +10,6 @@ from structlog.stdlib import get_logger
|
|||||||
from authentik.core.api.object_types import CreatableType
|
from authentik.core.api.object_types import CreatableType
|
||||||
from authentik.core.models import PropertyMapping, Provider
|
from authentik.core.models import PropertyMapping, Provider
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.lib.models import DomainlessURLValidator
|
|
||||||
from authentik.lib.utils.time import timedelta_string_validator
|
from authentik.lib.utils.time import timedelta_string_validator
|
||||||
from authentik.sources.saml.processors.constants import (
|
from authentik.sources.saml.processors.constants import (
|
||||||
DSA_SHA1,
|
DSA_SHA1,
|
||||||
@ -41,9 +40,7 @@ class SAMLBindings(models.TextChoices):
|
|||||||
class SAMLProvider(Provider):
|
class SAMLProvider(Provider):
|
||||||
"""SAML 2.0 Endpoint for applications which support SAML."""
|
"""SAML 2.0 Endpoint for applications which support SAML."""
|
||||||
|
|
||||||
acs_url = models.TextField(
|
acs_url = models.URLField(verbose_name=_("ACS URL"))
|
||||||
validators=[DomainlessURLValidator(schemes=("http", "https"))], verbose_name=_("ACS URL")
|
|
||||||
)
|
|
||||||
audience = models.TextField(
|
audience = models.TextField(
|
||||||
default="",
|
default="",
|
||||||
blank=True,
|
blank=True,
|
||||||
|
|||||||
@ -15,7 +15,7 @@ from authentik.flows.models import in_memory_stage
|
|||||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
|
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
|
||||||
from authentik.flows.views.executor import SESSION_KEY_POST
|
from authentik.flows.views.executor import SESSION_KEY_POST
|
||||||
from authentik.lib.views import bad_request_message
|
from authentik.lib.views import bad_request_message
|
||||||
from authentik.policies.views import BufferedPolicyAccessView
|
from authentik.policies.views import PolicyAccessView
|
||||||
from authentik.providers.saml.exceptions import CannotHandleAssertion
|
from authentik.providers.saml.exceptions import CannotHandleAssertion
|
||||||
from authentik.providers.saml.models import SAMLBindings, SAMLProvider
|
from authentik.providers.saml.models import SAMLBindings, SAMLProvider
|
||||||
from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser
|
from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser
|
||||||
@ -35,7 +35,7 @@ from authentik.stages.consent.stage import (
|
|||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
class SAMLSSOView(BufferedPolicyAccessView):
|
class SAMLSSOView(PolicyAccessView):
|
||||||
"""SAML SSO Base View, which plans a flow and injects our final stage.
|
"""SAML SSO Base View, which plans a flow and injects our final stage.
|
||||||
Calls get/post handler."""
|
Calls get/post handler."""
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ class SAMLSSOView(BufferedPolicyAccessView):
|
|||||||
|
|
||||||
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
||||||
"""GET and POST use the same handler, but we can't
|
"""GET and POST use the same handler, but we can't
|
||||||
override .dispatch easily because BufferedPolicyAccessView's dispatch"""
|
override .dispatch easily because PolicyAccessView's dispatch"""
|
||||||
return self.get(request, application_slug)
|
return self.get(request, application_slug)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,35 +0,0 @@
|
|||||||
# Generated by Django 5.0.13 on 2025-03-31 13:53
|
|
||||||
|
|
||||||
import authentik.lib.models
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_sources_saml", "0017_fix_x509subjectname"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="samlsource",
|
|
||||||
name="slo_url",
|
|
||||||
field=models.TextField(
|
|
||||||
blank=True,
|
|
||||||
default=None,
|
|
||||||
help_text="Optional URL if your IDP supports Single-Logout.",
|
|
||||||
null=True,
|
|
||||||
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
|
|
||||||
verbose_name="SLO URL",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="samlsource",
|
|
||||||
name="sso_url",
|
|
||||||
field=models.TextField(
|
|
||||||
help_text="URL that the initial Login request is sent to.",
|
|
||||||
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
|
|
||||||
verbose_name="SSO URL",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -20,7 +20,6 @@ from authentik.crypto.models import CertificateKeyPair
|
|||||||
from authentik.flows.challenge import RedirectChallenge
|
from authentik.flows.challenge import RedirectChallenge
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||||
from authentik.lib.models import DomainlessURLValidator
|
|
||||||
from authentik.lib.utils.time import timedelta_string_validator
|
from authentik.lib.utils.time import timedelta_string_validator
|
||||||
from authentik.sources.saml.processors.constants import (
|
from authentik.sources.saml.processors.constants import (
|
||||||
DSA_SHA1,
|
DSA_SHA1,
|
||||||
@ -92,13 +91,11 @@ class SAMLSource(Source):
|
|||||||
help_text=_("Also known as Entity ID. Defaults the Metadata URL."),
|
help_text=_("Also known as Entity ID. Defaults the Metadata URL."),
|
||||||
)
|
)
|
||||||
|
|
||||||
sso_url = models.TextField(
|
sso_url = models.URLField(
|
||||||
validators=[DomainlessURLValidator(schemes=("http", "https"))],
|
|
||||||
verbose_name=_("SSO URL"),
|
verbose_name=_("SSO URL"),
|
||||||
help_text=_("URL that the initial Login request is sent to."),
|
help_text=_("URL that the initial Login request is sent to."),
|
||||||
)
|
)
|
||||||
slo_url = models.TextField(
|
slo_url = models.URLField(
|
||||||
validators=[DomainlessURLValidator(schemes=("http", "https"))],
|
|
||||||
default=None,
|
default=None,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
|
|||||||
@ -33,7 +33,6 @@ from authentik.flows.planner import (
|
|||||||
)
|
)
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||||
from authentik.lib.utils.urls import is_url_absolute
|
|
||||||
from authentik.lib.views import bad_request_message
|
from authentik.lib.views import bad_request_message
|
||||||
from authentik.providers.saml.utils.encoding import nice64
|
from authentik.providers.saml.utils.encoding import nice64
|
||||||
from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat
|
from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat
|
||||||
@ -74,8 +73,6 @@ class InitiateView(View):
|
|||||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||||
NEXT_ARG_NAME, "authentik_core:if-user"
|
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||||
)
|
)
|
||||||
if not is_url_absolute(final_redirect):
|
|
||||||
final_redirect = "authentik_core:if-user"
|
|
||||||
kwargs.update(
|
kwargs.update(
|
||||||
{
|
{
|
||||||
PLAN_CONTEXT_SSO: True,
|
PLAN_CONTEXT_SSO: True,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -142,38 +142,35 @@ class IdentificationChallengeResponse(ChallengeResponse):
|
|||||||
raise ValidationError("Failed to authenticate.")
|
raise ValidationError("Failed to authenticate.")
|
||||||
self.pre_user = pre_user
|
self.pre_user = pre_user
|
||||||
|
|
||||||
|
# Password check
|
||||||
|
if current_stage.password_stage:
|
||||||
|
password = attrs.get("password", None)
|
||||||
|
if not password:
|
||||||
|
self.stage.logger.warning("Password not set for ident+auth attempt")
|
||||||
|
try:
|
||||||
|
with start_span(
|
||||||
|
op="authentik.stages.identification.authenticate",
|
||||||
|
name="User authenticate call (combo stage)",
|
||||||
|
):
|
||||||
|
user = authenticate(
|
||||||
|
self.stage.request,
|
||||||
|
current_stage.password_stage.backends,
|
||||||
|
current_stage,
|
||||||
|
username=self.pre_user.username,
|
||||||
|
password=password,
|
||||||
|
)
|
||||||
|
if not user:
|
||||||
|
raise ValidationError("Failed to authenticate.")
|
||||||
|
self.pre_user = user
|
||||||
|
except PermissionDenied as exc:
|
||||||
|
raise ValidationError(str(exc)) from exc
|
||||||
|
|
||||||
# Captcha check
|
# Captcha check
|
||||||
if captcha_stage := current_stage.captcha_stage:
|
if captcha_stage := current_stage.captcha_stage:
|
||||||
captcha_token = attrs.get("captcha_token", None)
|
captcha_token = attrs.get("captcha_token", None)
|
||||||
if not captcha_token:
|
if not captcha_token:
|
||||||
self.stage.logger.warning("Token not set for captcha attempt")
|
self.stage.logger.warning("Token not set for captcha attempt")
|
||||||
verify_captcha_token(captcha_stage, captcha_token, client_ip)
|
verify_captcha_token(captcha_stage, captcha_token, client_ip)
|
||||||
|
|
||||||
# Password check
|
|
||||||
if not current_stage.password_stage:
|
|
||||||
# No password stage select, don't validate the password
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
password = attrs.get("password", None)
|
|
||||||
if not password:
|
|
||||||
self.stage.logger.warning("Password not set for ident+auth attempt")
|
|
||||||
try:
|
|
||||||
with start_span(
|
|
||||||
op="authentik.stages.identification.authenticate",
|
|
||||||
name="User authenticate call (combo stage)",
|
|
||||||
):
|
|
||||||
user = authenticate(
|
|
||||||
self.stage.request,
|
|
||||||
current_stage.password_stage.backends,
|
|
||||||
current_stage,
|
|
||||||
username=self.pre_user.username,
|
|
||||||
password=password,
|
|
||||||
)
|
|
||||||
if not user:
|
|
||||||
raise ValidationError("Failed to authenticate.")
|
|
||||||
self.pre_user = user
|
|
||||||
except PermissionDenied as exc:
|
|
||||||
raise ValidationError(str(exc)) from exc
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"$schema": "http://json-schema.org/draft-07/schema",
|
"$schema": "http://json-schema.org/draft-07/schema",
|
||||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "authentik 2025.2.3 Blueprint schema",
|
"title": "authentik 2025.2.2 Blueprint schema",
|
||||||
"required": [
|
"required": [
|
||||||
"version",
|
"version",
|
||||||
"entries"
|
"entries"
|
||||||
@ -6423,6 +6423,8 @@
|
|||||||
},
|
},
|
||||||
"acs_url": {
|
"acs_url": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"maxLength": 200,
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"title": "ACS URL"
|
"title": "ACS URL"
|
||||||
},
|
},
|
||||||
@ -8731,6 +8733,8 @@
|
|||||||
},
|
},
|
||||||
"sso_url": {
|
"sso_url": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"maxLength": 200,
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"title": "SSO URL",
|
"title": "SSO URL",
|
||||||
"description": "URL that the initial Login request is sent to."
|
"description": "URL that the initial Login request is sent to."
|
||||||
@ -8740,6 +8744,8 @@
|
|||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
],
|
],
|
||||||
|
"format": "uri",
|
||||||
|
"maxLength": 200,
|
||||||
"title": "SLO URL",
|
"title": "SLO URL",
|
||||||
"description": "Optional URL if your IDP supports Single-Logout."
|
"description": "Optional URL if your IDP supports Single-Logout."
|
||||||
},
|
},
|
||||||
|
|||||||
@ -31,7 +31,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- redis:/data
|
- redis:/data
|
||||||
server:
|
server:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.3}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.2}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: server
|
command: server
|
||||||
environment:
|
environment:
|
||||||
@ -54,7 +54,7 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
worker:
|
worker:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.3}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.2}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: worker
|
command: worker
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
7
go.mod
7
go.mod
@ -1,6 +1,9 @@
|
|||||||
module goauthentik.io
|
module goauthentik.io
|
||||||
|
|
||||||
go 1.24.0
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
beryju.io/ldap v0.1.0
|
beryju.io/ldap v0.1.0
|
||||||
github.com/coreos/go-oidc/v3 v3.13.0
|
github.com/coreos/go-oidc/v3 v3.13.0
|
||||||
@ -26,7 +29,7 @@ require (
|
|||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.9.1
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/wwt/guac v1.3.2
|
github.com/wwt/guac v1.3.2
|
||||||
goauthentik.io/api/v3 v3.2025023.2
|
goauthentik.io/api/v3 v3.2025022.6
|
||||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||||
golang.org/x/oauth2 v0.28.0
|
golang.org/x/oauth2 v0.28.0
|
||||||
golang.org/x/sync v0.12.0
|
golang.org/x/sync v0.12.0
|
||||||
|
|||||||
4
go.sum
4
go.sum
@ -299,8 +299,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
|
|||||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
go.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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
goauthentik.io/api/v3 v3.2025023.2 h1:4XHlnykN5jQH78liQ4cp2Jf8eigvQImIJp+A+bsq1nA=
|
goauthentik.io/api/v3 v3.2025022.6 h1:M5M8Cd/1N7E8KLkvYYh7VdcdKz5nfzjKPFLK+YOtOVg=
|
||||||
goauthentik.io/api/v3 v3.2025023.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
goauthentik.io/api/v3 v3.2025022.6/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
|||||||
@ -29,4 +29,4 @@ func UserAgent() string {
|
|||||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||||
}
|
}
|
||||||
|
|
||||||
const VERSION = "2025.2.3"
|
const VERSION = "2025.2.2"
|
||||||
|
|||||||
5
internal/crypto/backend/fips_disabled.go
Normal file
5
internal/crypto/backend/fips_disabled.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
//go:build requirefips
|
||||||
|
|
||||||
|
package backend
|
||||||
|
|
||||||
|
var FipsEnabled = true
|
||||||
5
internal/crypto/backend/fips_enabled.go
Normal file
5
internal/crypto/backend/fips_enabled.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
//go:build !requirefips
|
||||||
|
|
||||||
|
package backend
|
||||||
|
|
||||||
|
var FipsEnabled = false
|
||||||
@ -2,7 +2,6 @@ package ak
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/fips140"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -204,7 +203,7 @@ func (a *APIController) getWebsocketPingArgs() map[string]interface{} {
|
|||||||
"golangVersion": runtime.Version(),
|
"golangVersion": runtime.Version(),
|
||||||
"opensslEnabled": cryptobackend.OpensslEnabled,
|
"opensslEnabled": cryptobackend.OpensslEnabled,
|
||||||
"opensslVersion": cryptobackend.OpensslVersion(),
|
"opensslVersion": cryptobackend.OpensslVersion(),
|
||||||
"fipsEnabled": fips140.Enabled(),
|
"fipsEnabled": cryptobackend.FipsEnabled,
|
||||||
}
|
}
|
||||||
hostname, err := os.Hostname()
|
hostname, err := os.Hostname()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
# Stage 1: Build
|
# Stage 1: Build
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder
|
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder
|
||||||
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
@ -27,7 +27,7 @@ COPY . .
|
|||||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||||
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
||||||
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
|
||||||
go build -o /go/ldap ./cmd/ldap
|
go build -o /go/ldap ./cmd/ldap
|
||||||
|
|
||||||
# Stage 2: Run
|
# Stage 2: Run
|
||||||
|
|||||||
@ -26,7 +26,7 @@ Parameters:
|
|||||||
Description: authentik Docker image
|
Description: authentik Docker image
|
||||||
AuthentikVersion:
|
AuthentikVersion:
|
||||||
Type: String
|
Type: String
|
||||||
Default: 2025.2.3
|
Default: 2025.2.2
|
||||||
Description: authentik Docker image tag
|
Description: authentik Docker image tag
|
||||||
AuthentikServerCPU:
|
AuthentikServerCPU:
|
||||||
Type: Number
|
Type: Number
|
||||||
|
|||||||
@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-03-31 00:10+0000\n"
|
"POT-Creation-Date: 2025-03-22 00:10+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@ -1220,20 +1220,6 @@ msgstr ""
|
|||||||
msgid "Reputation Scores"
|
msgid "Reputation Scores"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/buffer.html
|
|
||||||
msgid "Waiting for authentication..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/buffer.html
|
|
||||||
msgid ""
|
|
||||||
"You're already authenticating in another tab. This page will refresh once "
|
|
||||||
"authentication is completed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/buffer.html
|
|
||||||
msgid "Authenticate in this tab"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/denied.html
|
#: authentik/policies/templates/policies/denied.html
|
||||||
msgid "Permission denied"
|
msgid "Permission denied"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@ -10,8 +10,8 @@
|
|||||||
# Manuel Viens, 2023
|
# Manuel Viens, 2023
|
||||||
# Mordecai, 2023
|
# Mordecai, 2023
|
||||||
# nerdinator <florian.dupret@gmail.com>, 2024
|
# nerdinator <florian.dupret@gmail.com>, 2024
|
||||||
|
# Tina, 2024
|
||||||
# Charles Leclerc, 2025
|
# Charles Leclerc, 2025
|
||||||
# Tina, 2025
|
|
||||||
# Marc Schmitt, 2025
|
# Marc Schmitt, 2025
|
||||||
#
|
#
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
@ -19,7 +19,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-03-31 00:10+0000\n"
|
"POT-Creation-Date: 2025-03-22 00:10+0000\n"
|
||||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||||
"Last-Translator: Marc Schmitt, 2025\n"
|
"Last-Translator: Marc Schmitt, 2025\n"
|
||||||
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
|
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
|
||||||
@ -1347,22 +1347,6 @@ msgstr "Score de Réputation"
|
|||||||
msgid "Reputation Scores"
|
msgid "Reputation Scores"
|
||||||
msgstr "Scores de Réputation"
|
msgstr "Scores de Réputation"
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/buffer.html
|
|
||||||
msgid "Waiting for authentication..."
|
|
||||||
msgstr "En attente de l'authentification..."
|
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/buffer.html
|
|
||||||
msgid ""
|
|
||||||
"You're already authenticating in another tab. This page will refresh once "
|
|
||||||
"authentication is completed."
|
|
||||||
msgstr ""
|
|
||||||
"Vous êtes déjà en cours d'authentification dans un autre onglet. Cette page "
|
|
||||||
"se rafraîchira lorsque l'authentification sera terminée."
|
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/buffer.html
|
|
||||||
msgid "Authenticate in this tab"
|
|
||||||
msgstr "S'authentifier dans cet onglet"
|
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/denied.html
|
#: authentik/policies/templates/policies/denied.html
|
||||||
msgid "Permission denied"
|
msgid "Permission denied"
|
||||||
msgstr "Permission refusée"
|
msgstr "Permission refusée"
|
||||||
|
|||||||
@ -14,7 +14,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-03-31 00:10+0000\n"
|
"POT-Creation-Date: 2025-03-22 00:10+0000\n"
|
||||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||||
"Last-Translator: deluxghost, 2025\n"
|
"Last-Translator: deluxghost, 2025\n"
|
||||||
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
||||||
@ -1234,20 +1234,6 @@ msgstr "信誉分数"
|
|||||||
msgid "Reputation Scores"
|
msgid "Reputation Scores"
|
||||||
msgstr "信誉分数"
|
msgstr "信誉分数"
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/buffer.html
|
|
||||||
msgid "Waiting for authentication..."
|
|
||||||
msgstr "正在等待身份验证…"
|
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/buffer.html
|
|
||||||
msgid ""
|
|
||||||
"You're already authenticating in another tab. This page will refresh once "
|
|
||||||
"authentication is completed."
|
|
||||||
msgstr "您正在另一个标签页中验证身份。身份验证完成后,此页面会刷新。"
|
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/buffer.html
|
|
||||||
msgid "Authenticate in this tab"
|
|
||||||
msgstr "在此标签页中验证身份"
|
|
||||||
|
|
||||||
#: authentik/policies/templates/policies/denied.html
|
#: authentik/policies/templates/policies/denied.html
|
||||||
msgid "Permission denied"
|
msgid "Permission denied"
|
||||||
msgstr "权限被拒绝"
|
msgstr "权限被拒绝"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@goauthentik/authentik",
|
"name": "@goauthentik/authentik",
|
||||||
"version": "2025.2.3",
|
"version": "2025.2.2",
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ COPY web .
|
|||||||
RUN npm run build-proxy
|
RUN npm run build-proxy
|
||||||
|
|
||||||
# Stage 2: Build
|
# Stage 2: Build
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder
|
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder
|
||||||
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
@ -43,7 +43,7 @@ COPY . .
|
|||||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||||
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
||||||
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
|
||||||
go build -o /go/proxy ./cmd/proxy
|
go build -o /go/proxy ./cmd/proxy
|
||||||
|
|
||||||
# Stage 3: Run
|
# Stage 3: Run
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "authentik"
|
name = "authentik"
|
||||||
version = "2025.2.3"
|
version = "2025.2.2"
|
||||||
description = ""
|
description = ""
|
||||||
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
|
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
|
||||||
requires-python = "==3.12.*"
|
requires-python = "==3.12.*"
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
# Stage 1: Build
|
# Stage 1: Build
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder
|
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder
|
||||||
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
@ -27,7 +27,7 @@ COPY . .
|
|||||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||||
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
||||||
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
|
||||||
go build -o /go/rac ./cmd/rac
|
go build -o /go/rac ./cmd/rac
|
||||||
|
|
||||||
# Stage 2: Run
|
# Stage 2: Run
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
# Stage 1: Build
|
# Stage 1: Build
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder
|
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder
|
||||||
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
@ -27,7 +27,7 @@ COPY . .
|
|||||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||||
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
||||||
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
|
||||||
go build -o /go/radius ./cmd/radius
|
go build -o /go/radius ./cmd/radius
|
||||||
|
|
||||||
# Stage 2: Run
|
# Stage 2: Run
|
||||||
|
|||||||
169
schema.yml
169
schema.yml
@ -1,7 +1,7 @@
|
|||||||
openapi: 3.0.3
|
openapi: 3.0.3
|
||||||
info:
|
info:
|
||||||
title: authentik
|
title: authentik
|
||||||
version: 2025.2.3
|
version: 2025.2.2
|
||||||
description: Making authentication simple.
|
description: Making authentication simple.
|
||||||
contact:
|
contact:
|
||||||
email: hello@goauthentik.io
|
email: hello@goauthentik.io
|
||||||
@ -44141,11 +44141,17 @@ components:
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_slug:
|
assigned_backchannel_application_slug:
|
||||||
type: string
|
type: string
|
||||||
description: Internal application name, used in URLs.
|
description: |-
|
||||||
|
Get backchannel application slug.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_name:
|
assigned_backchannel_application_name:
|
||||||
type: string
|
type: string
|
||||||
description: Application's display Name.
|
description: |-
|
||||||
|
Get backchannel application name.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
verbose_name:
|
verbose_name:
|
||||||
type: string
|
type: string
|
||||||
@ -45675,19 +45681,27 @@ components:
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_application_slug:
|
assigned_application_slug:
|
||||||
type: string
|
type: string
|
||||||
description: Internal application name, used in URLs.
|
description: Get application slug, return empty string if no application
|
||||||
|
exists
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_application_name:
|
assigned_application_name:
|
||||||
type: string
|
type: string
|
||||||
description: Application's display Name.
|
description: Get application name, return empty string if no application
|
||||||
|
exists
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_slug:
|
assigned_backchannel_application_slug:
|
||||||
type: string
|
type: string
|
||||||
description: Internal application name, used in URLs.
|
description: |-
|
||||||
|
Get backchannel application slug.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_name:
|
assigned_backchannel_application_name:
|
||||||
type: string
|
type: string
|
||||||
description: Application's display Name.
|
description: |-
|
||||||
|
Get backchannel application name.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
verbose_name:
|
verbose_name:
|
||||||
type: string
|
type: string
|
||||||
@ -46395,11 +46409,17 @@ components:
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_slug:
|
assigned_backchannel_application_slug:
|
||||||
type: string
|
type: string
|
||||||
description: Internal application name, used in URLs.
|
description: |-
|
||||||
|
Get backchannel application slug.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_name:
|
assigned_backchannel_application_name:
|
||||||
type: string
|
type: string
|
||||||
description: Application's display Name.
|
description: |-
|
||||||
|
Get backchannel application name.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
verbose_name:
|
verbose_name:
|
||||||
type: string
|
type: string
|
||||||
@ -47022,19 +47042,27 @@ components:
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_application_slug:
|
assigned_application_slug:
|
||||||
type: string
|
type: string
|
||||||
description: Internal application name, used in URLs.
|
description: Get application slug, return empty string if no application
|
||||||
|
exists
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_application_name:
|
assigned_application_name:
|
||||||
type: string
|
type: string
|
||||||
description: Application's display Name.
|
description: Get application name, return empty string if no application
|
||||||
|
exists
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_slug:
|
assigned_backchannel_application_slug:
|
||||||
type: string
|
type: string
|
||||||
description: Internal application name, used in URLs.
|
description: |-
|
||||||
|
Get backchannel application slug.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_name:
|
assigned_backchannel_application_name:
|
||||||
type: string
|
type: string
|
||||||
description: Application's display Name.
|
description: |-
|
||||||
|
Get backchannel application name.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
verbose_name:
|
verbose_name:
|
||||||
type: string
|
type: string
|
||||||
@ -52245,8 +52273,9 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
acs_url:
|
acs_url:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
|
||||||
format: uri
|
format: uri
|
||||||
|
minLength: 1
|
||||||
|
maxLength: 200
|
||||||
audience:
|
audience:
|
||||||
type: string
|
type: string
|
||||||
description: Value of the audience restriction field of the assertion. When
|
description: Value of the audience restriction field of the assertion. When
|
||||||
@ -52403,14 +52432,16 @@ components:
|
|||||||
description: Also known as Entity ID. Defaults the Metadata URL.
|
description: Also known as Entity ID. Defaults the Metadata URL.
|
||||||
sso_url:
|
sso_url:
|
||||||
type: string
|
type: string
|
||||||
|
format: uri
|
||||||
minLength: 1
|
minLength: 1
|
||||||
description: URL that the initial Login request is sent to.
|
description: URL that the initial Login request is sent to.
|
||||||
format: uri
|
maxLength: 200
|
||||||
slo_url:
|
slo_url:
|
||||||
type: string
|
type: string
|
||||||
|
format: uri
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Optional URL if your IDP supports Single-Logout.
|
description: Optional URL if your IDP supports Single-Logout.
|
||||||
format: uri
|
maxLength: 200
|
||||||
allow_idp_initiated:
|
allow_idp_initiated:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Allows authentication flows initiated by the IdP. This can
|
description: Allows authentication flows initiated by the IdP. This can
|
||||||
@ -53845,19 +53876,27 @@ components:
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_application_slug:
|
assigned_application_slug:
|
||||||
type: string
|
type: string
|
||||||
description: Internal application name, used in URLs.
|
description: Get application slug, return empty string if no application
|
||||||
|
exists
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_application_name:
|
assigned_application_name:
|
||||||
type: string
|
type: string
|
||||||
description: Application's display Name.
|
description: Get application name, return empty string if no application
|
||||||
|
exists
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_slug:
|
assigned_backchannel_application_slug:
|
||||||
type: string
|
type: string
|
||||||
description: Internal application name, used in URLs.
|
description: |-
|
||||||
|
Get backchannel application slug.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_name:
|
assigned_backchannel_application_name:
|
||||||
type: string
|
type: string
|
||||||
description: Application's display Name.
|
description: |-
|
||||||
|
Get backchannel application name.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
verbose_name:
|
verbose_name:
|
||||||
type: string
|
type: string
|
||||||
@ -54086,19 +54125,27 @@ components:
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_application_slug:
|
assigned_application_slug:
|
||||||
type: string
|
type: string
|
||||||
description: Internal application name, used in URLs.
|
description: Get application slug, return empty string if no application
|
||||||
|
exists
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_application_name:
|
assigned_application_name:
|
||||||
type: string
|
type: string
|
||||||
description: Application's display Name.
|
description: Get application name, return empty string if no application
|
||||||
|
exists
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_slug:
|
assigned_backchannel_application_slug:
|
||||||
type: string
|
type: string
|
||||||
description: Internal application name, used in URLs.
|
description: |-
|
||||||
|
Get backchannel application slug.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_name:
|
assigned_backchannel_application_name:
|
||||||
type: string
|
type: string
|
||||||
description: Application's display Name.
|
description: |-
|
||||||
|
Get backchannel application name.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
verbose_name:
|
verbose_name:
|
||||||
type: string
|
type: string
|
||||||
@ -54405,19 +54452,27 @@ components:
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_application_slug:
|
assigned_application_slug:
|
||||||
type: string
|
type: string
|
||||||
description: Internal application name, used in URLs.
|
description: Get application slug, return empty string if no application
|
||||||
|
exists
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_application_name:
|
assigned_application_name:
|
||||||
type: string
|
type: string
|
||||||
description: Application's display Name.
|
description: Get application name, return empty string if no application
|
||||||
|
exists
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_slug:
|
assigned_backchannel_application_slug:
|
||||||
type: string
|
type: string
|
||||||
description: Internal application name, used in URLs.
|
description: |-
|
||||||
|
Get backchannel application slug.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_name:
|
assigned_backchannel_application_name:
|
||||||
type: string
|
type: string
|
||||||
description: Application's display Name.
|
description: |-
|
||||||
|
Get backchannel application name.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
verbose_name:
|
verbose_name:
|
||||||
type: string
|
type: string
|
||||||
@ -54570,19 +54625,27 @@ components:
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_application_slug:
|
assigned_application_slug:
|
||||||
type: string
|
type: string
|
||||||
description: Internal application name, used in URLs.
|
description: Get application slug, return empty string if no application
|
||||||
|
exists
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_application_name:
|
assigned_application_name:
|
||||||
type: string
|
type: string
|
||||||
description: Application's display Name.
|
description: Get application name, return empty string if no application
|
||||||
|
exists
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_slug:
|
assigned_backchannel_application_slug:
|
||||||
type: string
|
type: string
|
||||||
description: Internal application name, used in URLs.
|
description: |-
|
||||||
|
Get backchannel application slug.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_name:
|
assigned_backchannel_application_name:
|
||||||
type: string
|
type: string
|
||||||
description: Application's display Name.
|
description: |-
|
||||||
|
Get backchannel application name.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
verbose_name:
|
verbose_name:
|
||||||
type: string
|
type: string
|
||||||
@ -55182,19 +55245,27 @@ components:
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_application_slug:
|
assigned_application_slug:
|
||||||
type: string
|
type: string
|
||||||
description: Internal application name, used in URLs.
|
description: Get application slug, return empty string if no application
|
||||||
|
exists
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_application_name:
|
assigned_application_name:
|
||||||
type: string
|
type: string
|
||||||
description: Application's display Name.
|
description: Get application name, return empty string if no application
|
||||||
|
exists
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_slug:
|
assigned_backchannel_application_slug:
|
||||||
type: string
|
type: string
|
||||||
description: Internal application name, used in URLs.
|
description: |-
|
||||||
|
Get backchannel application slug.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_name:
|
assigned_backchannel_application_name:
|
||||||
type: string
|
type: string
|
||||||
description: Application's display Name.
|
description: |-
|
||||||
|
Get backchannel application name.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
verbose_name:
|
verbose_name:
|
||||||
type: string
|
type: string
|
||||||
@ -55211,6 +55282,7 @@ components:
|
|||||||
acs_url:
|
acs_url:
|
||||||
type: string
|
type: string
|
||||||
format: uri
|
format: uri
|
||||||
|
maxLength: 200
|
||||||
audience:
|
audience:
|
||||||
type: string
|
type: string
|
||||||
description: Value of the audience restriction field of the assertion. When
|
description: Value of the audience restriction field of the assertion. When
|
||||||
@ -55377,8 +55449,9 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
acs_url:
|
acs_url:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
|
||||||
format: uri
|
format: uri
|
||||||
|
minLength: 1
|
||||||
|
maxLength: 200
|
||||||
audience:
|
audience:
|
||||||
type: string
|
type: string
|
||||||
description: Value of the audience restriction field of the assertion. When
|
description: Value of the audience restriction field of the assertion. When
|
||||||
@ -55551,13 +55624,15 @@ components:
|
|||||||
description: Also known as Entity ID. Defaults the Metadata URL.
|
description: Also known as Entity ID. Defaults the Metadata URL.
|
||||||
sso_url:
|
sso_url:
|
||||||
type: string
|
type: string
|
||||||
description: URL that the initial Login request is sent to.
|
|
||||||
format: uri
|
format: uri
|
||||||
|
description: URL that the initial Login request is sent to.
|
||||||
|
maxLength: 200
|
||||||
slo_url:
|
slo_url:
|
||||||
type: string
|
type: string
|
||||||
|
format: uri
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Optional URL if your IDP supports Single-Logout.
|
description: Optional URL if your IDP supports Single-Logout.
|
||||||
format: uri
|
maxLength: 200
|
||||||
allow_idp_initiated:
|
allow_idp_initiated:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Allows authentication flows initiated by the IdP. This can
|
description: Allows authentication flows initiated by the IdP. This can
|
||||||
@ -55740,14 +55815,16 @@ components:
|
|||||||
description: Also known as Entity ID. Defaults the Metadata URL.
|
description: Also known as Entity ID. Defaults the Metadata URL.
|
||||||
sso_url:
|
sso_url:
|
||||||
type: string
|
type: string
|
||||||
|
format: uri
|
||||||
minLength: 1
|
minLength: 1
|
||||||
description: URL that the initial Login request is sent to.
|
description: URL that the initial Login request is sent to.
|
||||||
format: uri
|
maxLength: 200
|
||||||
slo_url:
|
slo_url:
|
||||||
type: string
|
type: string
|
||||||
|
format: uri
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Optional URL if your IDP supports Single-Logout.
|
description: Optional URL if your IDP supports Single-Logout.
|
||||||
format: uri
|
maxLength: 200
|
||||||
allow_idp_initiated:
|
allow_idp_initiated:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Allows authentication flows initiated by the IdP. This can
|
description: Allows authentication flows initiated by the IdP. This can
|
||||||
@ -55891,11 +55968,17 @@ components:
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_slug:
|
assigned_backchannel_application_slug:
|
||||||
type: string
|
type: string
|
||||||
description: Internal application name, used in URLs.
|
description: |-
|
||||||
|
Get backchannel application slug.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
assigned_backchannel_application_name:
|
assigned_backchannel_application_name:
|
||||||
type: string
|
type: string
|
||||||
description: Application's display Name.
|
description: |-
|
||||||
|
Get backchannel application name.
|
||||||
|
|
||||||
|
Returns an empty string if no backchannel application exists.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
verbose_name:
|
verbose_name:
|
||||||
type: string
|
type: string
|
||||||
|
|||||||
@ -410,77 +410,3 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||||
"Permission denied",
|
"Permission denied",
|
||||||
)
|
)
|
||||||
|
|
||||||
@retry()
|
|
||||||
@apply_blueprint(
|
|
||||||
"default/flow-default-authentication-flow.yaml",
|
|
||||||
"default/flow-default-invalidation-flow.yaml",
|
|
||||||
)
|
|
||||||
@apply_blueprint("default/flow-default-provider-authorization-implicit-consent.yaml")
|
|
||||||
@apply_blueprint("system/providers-oauth2.yaml")
|
|
||||||
@reconcile_app("authentik_crypto")
|
|
||||||
def test_authorization_consent_implied_parallel(self):
|
|
||||||
"""test OpenID Provider flow (default authorization flow with implied consent)"""
|
|
||||||
# Bootstrap all needed objects
|
|
||||||
authorization_flow = Flow.objects.get(
|
|
||||||
slug="default-provider-authorization-implicit-consent"
|
|
||||||
)
|
|
||||||
provider = OAuth2Provider.objects.create(
|
|
||||||
name=generate_id(),
|
|
||||||
client_type=ClientTypes.CONFIDENTIAL,
|
|
||||||
client_id=self.client_id,
|
|
||||||
client_secret=self.client_secret,
|
|
||||||
signing_key=create_test_cert(),
|
|
||||||
redirect_uris=[
|
|
||||||
RedirectURI(
|
|
||||||
RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth"
|
|
||||||
)
|
|
||||||
],
|
|
||||||
authorization_flow=authorization_flow,
|
|
||||||
)
|
|
||||||
provider.property_mappings.set(
|
|
||||||
ScopeMapping.objects.filter(
|
|
||||||
scope_name__in=[
|
|
||||||
SCOPE_OPENID,
|
|
||||||
SCOPE_OPENID_EMAIL,
|
|
||||||
SCOPE_OPENID_PROFILE,
|
|
||||||
SCOPE_OFFLINE_ACCESS,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Application.objects.create(
|
|
||||||
name=generate_id(),
|
|
||||||
slug=self.app_slug,
|
|
||||||
provider=provider,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.driver.get(self.live_server_url)
|
|
||||||
login_window = self.driver.current_window_handle
|
|
||||||
|
|
||||||
self.driver.switch_to.new_window("tab")
|
|
||||||
grafana_window = self.driver.current_window_handle
|
|
||||||
self.driver.get("http://localhost:3000")
|
|
||||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
|
||||||
|
|
||||||
self.driver.switch_to.window(login_window)
|
|
||||||
self.login()
|
|
||||||
|
|
||||||
self.driver.switch_to.window(grafana_window)
|
|
||||||
self.wait_for_url("http://localhost:3000/?orgId=1")
|
|
||||||
self.driver.get("http://localhost:3000/profile")
|
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
|
||||||
self.user.name,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute("value"),
|
|
||||||
self.user.name,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=email]").get_attribute("value"),
|
|
||||||
self.user.email,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=login]").get_attribute("value"),
|
|
||||||
self.user.email,
|
|
||||||
)
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@ from tests.e2e.utils import SeleniumTestCase, retry
|
|||||||
class TestProviderSAML(SeleniumTestCase):
|
class TestProviderSAML(SeleniumTestCase):
|
||||||
"""test SAML Provider flow"""
|
"""test SAML Provider flow"""
|
||||||
|
|
||||||
def setup_client(self, provider: SAMLProvider, force_post: bool = False, **kwargs):
|
def setup_client(self, provider: SAMLProvider, force_post: bool = False):
|
||||||
"""Setup client saml-sp container which we test SAML against"""
|
"""Setup client saml-sp container which we test SAML against"""
|
||||||
metadata_url = (
|
metadata_url = (
|
||||||
self.url(
|
self.url(
|
||||||
@ -40,7 +40,6 @@ class TestProviderSAML(SeleniumTestCase):
|
|||||||
"SP_ENTITY_ID": provider.issuer,
|
"SP_ENTITY_ID": provider.issuer,
|
||||||
"SP_SSO_BINDING": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
"SP_SSO_BINDING": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
||||||
"SP_METADATA_URL": metadata_url,
|
"SP_METADATA_URL": metadata_url,
|
||||||
**kwargs,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -112,74 +111,6 @@ class TestProviderSAML(SeleniumTestCase):
|
|||||||
[self.user.email],
|
[self.user.email],
|
||||||
)
|
)
|
||||||
|
|
||||||
@retry()
|
|
||||||
@apply_blueprint(
|
|
||||||
"default/flow-default-authentication-flow.yaml",
|
|
||||||
"default/flow-default-invalidation-flow.yaml",
|
|
||||||
)
|
|
||||||
@apply_blueprint(
|
|
||||||
"default/flow-default-provider-authorization-implicit-consent.yaml",
|
|
||||||
)
|
|
||||||
@apply_blueprint(
|
|
||||||
"system/providers-saml.yaml",
|
|
||||||
)
|
|
||||||
@reconcile_app("authentik_crypto")
|
|
||||||
def test_sp_initiated_implicit_post(self):
|
|
||||||
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
|
|
||||||
# Bootstrap all needed objects
|
|
||||||
authorization_flow = Flow.objects.get(
|
|
||||||
slug="default-provider-authorization-implicit-consent"
|
|
||||||
)
|
|
||||||
provider: SAMLProvider = SAMLProvider.objects.create(
|
|
||||||
name="saml-test",
|
|
||||||
acs_url="http://localhost:9009/saml/acs",
|
|
||||||
audience="authentik-e2e",
|
|
||||||
issuer="authentik-e2e",
|
|
||||||
sp_binding=SAMLBindings.POST,
|
|
||||||
authorization_flow=authorization_flow,
|
|
||||||
signing_kp=create_test_cert(),
|
|
||||||
)
|
|
||||||
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
|
||||||
provider.save()
|
|
||||||
Application.objects.create(
|
|
||||||
name="SAML",
|
|
||||||
slug="authentik-saml",
|
|
||||||
provider=provider,
|
|
||||||
)
|
|
||||||
self.setup_client(provider, True)
|
|
||||||
self.driver.get("http://localhost:9009")
|
|
||||||
self.login()
|
|
||||||
self.wait_for_url("http://localhost:9009/")
|
|
||||||
|
|
||||||
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],
|
|
||||||
[self.user.name],
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
body["attr"][
|
|
||||||
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
|
|
||||||
],
|
|
||||||
[self.user.username],
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/username"],
|
|
||||||
[self.user.username],
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/uid"],
|
|
||||||
[str(self.user.pk)],
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
|
|
||||||
[self.user.email],
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"],
|
|
||||||
[self.user.email],
|
|
||||||
)
|
|
||||||
|
|
||||||
@retry()
|
@retry()
|
||||||
@apply_blueprint(
|
@apply_blueprint(
|
||||||
"default/flow-default-authentication-flow.yaml",
|
"default/flow-default-authentication-flow.yaml",
|
||||||
@ -519,81 +450,3 @@ class TestProviderSAML(SeleniumTestCase):
|
|||||||
lambda driver: driver.current_url.startswith(should_url),
|
lambda driver: driver.current_url.startswith(should_url),
|
||||||
f"URL {self.driver.current_url} doesn't match expected URL {should_url}",
|
f"URL {self.driver.current_url} doesn't match expected URL {should_url}",
|
||||||
)
|
)
|
||||||
|
|
||||||
@retry()
|
|
||||||
@apply_blueprint(
|
|
||||||
"default/flow-default-authentication-flow.yaml",
|
|
||||||
"default/flow-default-invalidation-flow.yaml",
|
|
||||||
)
|
|
||||||
@apply_blueprint(
|
|
||||||
"default/flow-default-provider-authorization-implicit-consent.yaml",
|
|
||||||
)
|
|
||||||
@apply_blueprint(
|
|
||||||
"system/providers-saml.yaml",
|
|
||||||
)
|
|
||||||
@reconcile_app("authentik_crypto")
|
|
||||||
def test_sp_initiated_implicit_post_buffer(self):
|
|
||||||
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
|
|
||||||
# Bootstrap all needed objects
|
|
||||||
authorization_flow = Flow.objects.get(
|
|
||||||
slug="default-provider-authorization-implicit-consent"
|
|
||||||
)
|
|
||||||
provider: SAMLProvider = SAMLProvider.objects.create(
|
|
||||||
name="saml-test",
|
|
||||||
acs_url=f"http://{self.host}:9009/saml/acs",
|
|
||||||
audience="authentik-e2e",
|
|
||||||
issuer="authentik-e2e",
|
|
||||||
sp_binding=SAMLBindings.POST,
|
|
||||||
authorization_flow=authorization_flow,
|
|
||||||
signing_kp=create_test_cert(),
|
|
||||||
)
|
|
||||||
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
|
||||||
provider.save()
|
|
||||||
Application.objects.create(
|
|
||||||
name="SAML",
|
|
||||||
slug="authentik-saml",
|
|
||||||
provider=provider,
|
|
||||||
)
|
|
||||||
self.setup_client(provider, True, SP_ROOT_URL=f"http://{self.host}:9009")
|
|
||||||
|
|
||||||
self.driver.get(self.live_server_url)
|
|
||||||
login_window = self.driver.current_window_handle
|
|
||||||
self.driver.switch_to.new_window("tab")
|
|
||||||
client_window = self.driver.current_window_handle
|
|
||||||
# We need to access the SP on the same host as the IdP for SameSite cookies
|
|
||||||
self.driver.get(f"http://{self.host}:9009")
|
|
||||||
|
|
||||||
self.driver.switch_to.window(login_window)
|
|
||||||
self.login()
|
|
||||||
self.driver.switch_to.window(client_window)
|
|
||||||
|
|
||||||
self.wait_for_url(f"http://{self.host}:9009/")
|
|
||||||
|
|
||||||
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],
|
|
||||||
[self.user.name],
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
body["attr"][
|
|
||||||
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
|
|
||||||
],
|
|
||||||
[self.user.username],
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/username"],
|
|
||||||
[self.user.username],
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/uid"],
|
|
||||||
[str(self.user.pk)],
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
|
|
||||||
[self.user.email],
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"],
|
|
||||||
[self.user.email],
|
|
||||||
)
|
|
||||||
|
|||||||
2
uv.lock
generated
2
uv.lock
generated
@ -162,7 +162,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "authentik"
|
name = "authentik"
|
||||||
version = "2025.2.3"
|
version = "2025.2.2"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "argon2-cffi" },
|
{ name = "argon2-cffi" },
|
||||||
|
|||||||
114
web/package-lock.json
generated
114
web/package-lock.json
generated
@ -24,7 +24,7 @@
|
|||||||
"@floating-ui/dom": "^1.6.11",
|
"@floating-ui/dom": "^1.6.11",
|
||||||
"@formatjs/intl-listformat": "^7.5.7",
|
"@formatjs/intl-listformat": "^7.5.7",
|
||||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||||
"@goauthentik/api": "^2025.2.3-1743464496",
|
"@goauthentik/api": "^2025.2.2-1742585853",
|
||||||
"@lit-labs/ssr": "^3.2.2",
|
"@lit-labs/ssr": "^3.2.2",
|
||||||
"@lit/context": "^1.1.2",
|
"@lit/context": "^1.1.2",
|
||||||
"@lit/localize": "^0.12.2",
|
"@lit/localize": "^0.12.2",
|
||||||
@ -1835,9 +1835,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@goauthentik/api": {
|
"node_modules/@goauthentik/api": {
|
||||||
"version": "2025.2.3-1743464496",
|
"version": "2025.2.2-1742585853",
|
||||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.2.3-1743464496.tgz",
|
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.2.2-1742585853.tgz",
|
||||||
"integrity": "sha512-35+SqFNoBZ+WNpyG2Xv/VKYKIIxjwRmIbgX5WZSpc9IlJVv7yyckUYvLpU2F0hZVUMDnxAUE5bsiNn7K4EQslw=="
|
"integrity": "sha512-bg/816ljAuUixLxi8tZd3W7sEcHgG5aYl0IMkbTsFYOAuiOdl/5wqSWaVM8g8O9SQ9feP3v6xDLOGncMoJxh4g=="
|
||||||
},
|
},
|
||||||
"node_modules/@goauthentik/web": {
|
"node_modules/@goauthentik/web": {
|
||||||
"resolved": "",
|
"resolved": "",
|
||||||
@ -8815,80 +8815,50 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||||
},
|
},
|
||||||
"node_modules/bare-events": {
|
"node_modules/bare-events": {
|
||||||
"version": "2.5.4",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz",
|
||||||
"integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==",
|
"integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/bare-fs": {
|
"node_modules/bare-fs": {
|
||||||
"version": "4.0.2",
|
"version": "2.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz",
|
||||||
"integrity": "sha512-S5mmkMesiduMqnz51Bfh0Et9EX0aTCJxhsI4bvzFFLs8Z1AV8RDHadfY5CyLwdoLHgXbNBEN1gQcbEtGwuvixw==",
|
"integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bare-events": "^2.5.4",
|
"bare-events": "^2.0.0",
|
||||||
"bare-path": "^3.0.0",
|
"bare-path": "^2.0.0",
|
||||||
"bare-stream": "^2.6.4"
|
"bare-stream": "^2.0.0"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"bare": ">=1.16.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"bare-buffer": "*"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"bare-buffer": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bare-os": {
|
"node_modules/bare-os": {
|
||||||
"version": "3.6.1",
|
"version": "2.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.4.tgz",
|
||||||
"integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==",
|
"integrity": "sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"optional": true
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
|
||||||
"bare": ">=1.14.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/bare-path": {
|
"node_modules/bare-path": {
|
||||||
"version": "3.0.0",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz",
|
||||||
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
|
"integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bare-os": "^3.0.1"
|
"bare-os": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bare-stream": {
|
"node_modules/bare-stream": {
|
||||||
"version": "2.6.5",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.3.0.tgz",
|
||||||
"integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==",
|
"integrity": "sha512-pVRWciewGUeCyKEuRxwv06M079r+fRjAQjBEK2P6OYGrO43O+Z0LrPZZEjlc4mB6C2RpZ9AxJ1s7NLEtOHO6eA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"streamx": "^2.21.0"
|
"b4a": "^1.6.6",
|
||||||
},
|
"streamx": "^2.20.0"
|
||||||
"peerDependencies": {
|
|
||||||
"bare-buffer": "*",
|
|
||||||
"bare-events": "*"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"bare-buffer": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"bare-events": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/base64-arraybuffer": {
|
"node_modules/base64-arraybuffer": {
|
||||||
@ -20200,10 +20170,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prebuild-install/node_modules/tar-fs": {
|
"node_modules/prebuild-install/node_modules/tar-fs": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
|
||||||
"integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==",
|
"integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chownr": "^1.1.1",
|
"chownr": "^1.1.1",
|
||||||
@ -22785,13 +22754,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/streamx": {
|
"node_modules/streamx": {
|
||||||
"version": "2.22.0",
|
"version": "2.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz",
|
||||||
"integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==",
|
"integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-fifo": "^1.3.2",
|
"fast-fifo": "^1.3.2",
|
||||||
|
"queue-tick": "^1.0.1",
|
||||||
"text-decoder": "^1.1.0"
|
"text-decoder": "^1.1.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
@ -23246,18 +23215,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar-fs": {
|
"node_modules/tar-fs": {
|
||||||
"version": "3.0.8",
|
"version": "3.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz",
|
||||||
"integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==",
|
"integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pump": "^3.0.0",
|
"pump": "^3.0.0",
|
||||||
"tar-stream": "^3.1.5"
|
"tar-stream": "^3.1.5"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"bare-fs": "^4.0.1",
|
"bare-fs": "^2.1.1",
|
||||||
"bare-path": "^3.0.0"
|
"bare-path": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar-stream": {
|
"node_modules/tar-stream": {
|
||||||
@ -24792,9 +24760,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.4.16",
|
"version": "5.4.15",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.16.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz",
|
||||||
"integrity": "sha512-Y5gnfp4NemVfgOTDQAunSD4346fal44L9mszGGY/e+qxsRT5y1sMlS/8tiQ8AFAp+MFgYNSINdfEchJiPm41vQ==",
|
"integrity": "sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
"@floating-ui/dom": "^1.6.11",
|
"@floating-ui/dom": "^1.6.11",
|
||||||
"@formatjs/intl-listformat": "^7.5.7",
|
"@formatjs/intl-listformat": "^7.5.7",
|
||||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||||
"@goauthentik/api": "^2025.2.3-1743464496",
|
"@goauthentik/api": "^2025.2.2-1742585853",
|
||||||
"@lit-labs/ssr": "^3.2.2",
|
"@lit-labs/ssr": "^3.2.2",
|
||||||
"@lit/context": "^1.1.2",
|
"@lit/context": "^1.1.2",
|
||||||
"@lit/localize": "^0.12.2",
|
"@lit/localize": "^0.12.2",
|
||||||
|
|||||||
@ -16,8 +16,8 @@ import "@goauthentik/elements/messages/MessageContainer";
|
|||||||
import "@goauthentik/elements/messages/MessageContainer";
|
import "@goauthentik/elements/messages/MessageContainer";
|
||||||
import "@goauthentik/elements/notifications/APIDrawer";
|
import "@goauthentik/elements/notifications/APIDrawer";
|
||||||
import "@goauthentik/elements/notifications/NotificationDrawer";
|
import "@goauthentik/elements/notifications/NotificationDrawer";
|
||||||
|
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
|
||||||
import "@goauthentik/elements/router/RouterOutlet";
|
import "@goauthentik/elements/router/RouterOutlet";
|
||||||
import { getRouteParameter, patchRouteParams } from "@goauthentik/elements/router/utils";
|
|
||||||
import "@goauthentik/elements/sidebar/Sidebar";
|
import "@goauthentik/elements/sidebar/Sidebar";
|
||||||
import "@goauthentik/elements/sidebar/SidebarItem";
|
import "@goauthentik/elements/sidebar/SidebarItem";
|
||||||
|
|
||||||
@ -37,10 +37,10 @@ import "./AdminSidebar";
|
|||||||
@customElement("ak-interface-admin")
|
@customElement("ak-interface-admin")
|
||||||
export class AdminInterface extends AuthenticatedInterface {
|
export class AdminInterface extends AuthenticatedInterface {
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
notificationDrawerOpen = getRouteParameter("notificationDrawerOpen", false);
|
notificationDrawerOpen = getURLParam("notificationDrawerOpen", false);
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
apiDrawerOpen = getRouteParameter("apiDrawerOpen", false);
|
apiDrawerOpen = getURLParam("apiDrawerOpen", false);
|
||||||
|
|
||||||
ws: WebsocketClient;
|
ws: WebsocketClient;
|
||||||
|
|
||||||
@ -93,14 +93,14 @@ export class AdminInterface extends AuthenticatedInterface {
|
|||||||
|
|
||||||
window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => {
|
window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => {
|
||||||
this.notificationDrawerOpen = !this.notificationDrawerOpen;
|
this.notificationDrawerOpen = !this.notificationDrawerOpen;
|
||||||
patchRouteParams({
|
updateURLParams({
|
||||||
notificationDrawerOpen: this.notificationDrawerOpen,
|
notificationDrawerOpen: this.notificationDrawerOpen,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener(EVENT_API_DRAWER_TOGGLE, () => {
|
window.addEventListener(EVENT_API_DRAWER_TOGGLE, () => {
|
||||||
this.apiDrawerOpen = !this.apiDrawerOpen;
|
this.apiDrawerOpen = !this.apiDrawerOpen;
|
||||||
patchRouteParams({
|
updateURLParams({
|
||||||
apiDrawerOpen: this.apiDrawerOpen,
|
apiDrawerOpen: this.apiDrawerOpen,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -123,7 +123,7 @@ export class AdminInterface extends AuthenticatedInterface {
|
|||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) {
|
if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) {
|
||||||
const { ESBuildObserver } = await import("src/development/build-observer");
|
const { ESBuildObserver } = await import("@goauthentik/common/client");
|
||||||
|
|
||||||
new ESBuildObserver(process.env.WATCHER_URL);
|
new ESBuildObserver(process.env.WATCHER_URL);
|
||||||
}
|
}
|
||||||
@ -158,7 +158,7 @@ export class AdminInterface extends AuthenticatedInterface {
|
|||||||
class="pf-c-page__main"
|
class="pf-c-page__main"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
id="main-content"
|
id="main-content"
|
||||||
defaultURL="/administration/overview"
|
defaultUrl="/administration/overview"
|
||||||
.routes=${ROUTES}
|
.routes=${ROUTES}
|
||||||
>
|
>
|
||||||
</ak-router-outlet>
|
</ak-router-outlet>
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
WithCapabilitiesConfig,
|
WithCapabilitiesConfig,
|
||||||
} from "@goauthentik/elements/Interface/capabilitiesProvider";
|
} from "@goauthentik/elements/Interface/capabilitiesProvider";
|
||||||
import { WithVersion } from "@goauthentik/elements/Interface/versionProvider";
|
import { WithVersion } from "@goauthentik/elements/Interface/versionProvider";
|
||||||
import { ID_PATTERN, SLUG_PATTERN, UUID_PATTERN } from "@goauthentik/elements/router";
|
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route";
|
||||||
import { getRootStyle } from "@goauthentik/elements/utils/getRootStyle";
|
import { getRootStyle } from "@goauthentik/elements/utils/getRootStyle";
|
||||||
import { spread } from "@open-wc/lit-helpers";
|
import { spread } from "@open-wc/lit-helpers";
|
||||||
|
|
||||||
@ -95,127 +95,62 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderSidebarItems(): TemplateResult {
|
renderSidebarItems(): TemplateResult {
|
||||||
|
// The second attribute type is of string[] to help with the 'activeWhen' control, which was
|
||||||
|
// commonplace and singular enough to merit its own handler.
|
||||||
type SidebarEntry = [
|
type SidebarEntry = [
|
||||||
/**
|
path: string | null,
|
||||||
* The pathname to match against. If null, this is a parent item.
|
|
||||||
*/
|
|
||||||
pathname: string | null,
|
|
||||||
/**
|
|
||||||
* The label to display in the sidebar.
|
|
||||||
*/
|
|
||||||
label: string,
|
label: string,
|
||||||
/**
|
attributes?: Record<string, any> | string[] | null, // eslint-disable-line
|
||||||
* The attributes to apply to the sidebar item. This is a map of attribute name to value.
|
|
||||||
*
|
|
||||||
* The second attribute type is of string[] to help with the 'activeWhen' control,
|
|
||||||
* which was commonplace and singular enough to merit its own handler.
|
|
||||||
*/
|
|
||||||
attributes?: Record<string, unknown> | string[] | null,
|
|
||||||
/**
|
|
||||||
* The children of this sidebar item. This is a recursive structure.
|
|
||||||
*/
|
|
||||||
children?: SidebarEntry[],
|
children?: SidebarEntry[],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
const sidebarContent: SidebarEntry[] = [
|
const sidebarContent: SidebarEntry[] = [
|
||||||
// ---
|
[null, msg("Dashboards"), { "?expanded": true }, [
|
||||||
[
|
["/administration/overview", msg("Overview")],
|
||||||
null,
|
["/administration/dashboard/users", msg("User Statistics")],
|
||||||
msg("Dashboards"),
|
["/administration/system-tasks", msg("System Tasks")]]],
|
||||||
{ "?expanded": true },
|
[null, msg("Applications"), null, [
|
||||||
[
|
["/core/applications", msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]],
|
||||||
["/administration/overview", msg("Overview")],
|
["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`]],
|
||||||
["/administration/dashboard/users", msg("User Statistics")],
|
["/outpost/outposts", msg("Outposts")]]],
|
||||||
["/administration/system-tasks", msg("System Tasks")],
|
[null, msg("Events"), null, [
|
||||||
],
|
["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]],
|
||||||
],
|
["/events/rules", msg("Notification Rules")],
|
||||||
[
|
["/events/transports", msg("Notification Transports")]]],
|
||||||
null,
|
[null, msg("Customization"), null, [
|
||||||
msg("Applications"),
|
["/policy/policies", msg("Policies")],
|
||||||
null,
|
["/core/property-mappings", msg("Property Mappings")],
|
||||||
[
|
["/blueprints/instances", msg("Blueprints")],
|
||||||
[
|
["/policy/reputation", msg("Reputation scores")]]],
|
||||||
"/core/applications",
|
[null, msg("Flows and Stages"), null, [
|
||||||
msg("Applications"),
|
["/flow/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`]],
|
||||||
[`/core/applications/:slug(${SLUG_PATTERN})`],
|
["/flow/stages", msg("Stages")],
|
||||||
],
|
["/flow/stages/prompts", msg("Prompts")]]],
|
||||||
["/core/providers", msg("Providers"), [`/core/providers/:id(${ID_PATTERN})`]],
|
[null, msg("Directory"), null, [
|
||||||
["/outpost/outposts", msg("Outposts")],
|
["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]],
|
||||||
],
|
["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]],
|
||||||
],
|
["/identity/roles", msg("Roles"), [`^/identity/roles/(?<id>${UUID_REGEX})$`]],
|
||||||
[
|
["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]],
|
||||||
null,
|
["/core/tokens", msg("Tokens and App passwords")],
|
||||||
msg("Events"),
|
["/flow/stages/invitations", msg("Invitations")]]],
|
||||||
null,
|
[null, msg("System"), null, [
|
||||||
[
|
["/core/brands", msg("Brands")],
|
||||||
["/events/log", msg("Logs"), [`/events/log/:id(${UUID_PATTERN})`]],
|
["/crypto/certificates", msg("Certificates")],
|
||||||
["/events/rules", msg("Notification Rules")],
|
["/outpost/integrations", msg("Outpost Integrations")],
|
||||||
["/events/transports", msg("Notification Transports")],
|
["/admin/settings", msg("Settings")]]],
|
||||||
],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
null,
|
|
||||||
msg("Customization"),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
["/policy/policies", msg("Policies")],
|
|
||||||
["/core/property-mappings", msg("Property Mappings")],
|
|
||||||
["/blueprints/instances", msg("Blueprints")],
|
|
||||||
["/policy/reputation", msg("Reputation scores")],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
null,
|
|
||||||
msg("Flows and Stages"),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
["/flow/flows", msg("Flows"), [`/flow/flows/:slug(${SLUG_PATTERN})`]],
|
|
||||||
["/flow/stages", msg("Stages")],
|
|
||||||
["/flow/stages/prompts", msg("Prompts")],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
null,
|
|
||||||
msg("Directory"),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
["/identity/users", msg("Users"), [`/identity/users/:id(${ID_PATTERN})`]],
|
|
||||||
["/identity/groups", msg("Groups"), [`/identity/groups/:id(${UUID_PATTERN})`]],
|
|
||||||
["/identity/roles", msg("Roles"), [`/identity/roles/:id(${UUID_PATTERN})`]],
|
|
||||||
[
|
|
||||||
"/core/sources",
|
|
||||||
msg("Federation and Social login"),
|
|
||||||
[`/core/sources/:slug(${SLUG_PATTERN})`],
|
|
||||||
],
|
|
||||||
["/core/tokens", msg("Tokens and App passwords")],
|
|
||||||
["/flow/stages/invitations", msg("Invitations")],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
null,
|
|
||||||
msg("System"),
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
["/core/brands", msg("Brands")],
|
|
||||||
["/crypto/certificates", msg("Certificates")],
|
|
||||||
["/outpost/integrations", msg("Outpost Integrations")],
|
|
||||||
["/admin/settings", msg("Settings")],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Typescript requires the type here to correctly type the recursive path
|
// Typescript requires the type here to correctly type the recursive path
|
||||||
type SidebarRenderer = (_: SidebarEntry) => TemplateResult;
|
type SidebarRenderer = (_: SidebarEntry) => TemplateResult;
|
||||||
|
|
||||||
const renderOneSidebarItem: SidebarRenderer = ([pathname, label, attributes, children]) => {
|
const renderOneSidebarItem: SidebarRenderer = ([path, label, attributes, children]) => {
|
||||||
const properties = Array.isArray(attributes)
|
const properties = Array.isArray(attributes)
|
||||||
? { ".activeWhen": attributes }
|
? { ".activeWhen": attributes }
|
||||||
: (attributes ?? {});
|
: (attributes ?? {});
|
||||||
|
if (path) {
|
||||||
if (pathname) {
|
properties.path = path;
|
||||||
properties.pathname = pathname;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`<ak-sidebar-item ${spread(properties)}>
|
return html`<ak-sidebar-item ${spread(properties)}>
|
||||||
${label ? html`<span slot="label">${label}</span>` : nothing}
|
${label ? html`<span slot="label">${label}</span>` : nothing}
|
||||||
${map(children, renderOneSidebarItem)}
|
${map(children, renderOneSidebarItem)}
|
||||||
|
|||||||
@ -1,210 +1,155 @@
|
|||||||
import "@goauthentik/admin/admin-overview/AdminOverviewPage";
|
import "@goauthentik/admin/admin-overview/AdminOverviewPage";
|
||||||
import { Route } from "@goauthentik/elements/router/Route";
|
import { ID_REGEX, Route, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route";
|
||||||
import { ID_PATTERN, SLUG_PATTERN, UUID_PATTERN } from "@goauthentik/elements/router/constants";
|
|
||||||
|
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
|
||||||
interface IDParameters {
|
export const ROUTES: Route[] = [
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SlugParameters {
|
|
||||||
slug: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UUIDParameters {
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ROUTES = [
|
|
||||||
// Prevent infinite Shell loops
|
// Prevent infinite Shell loops
|
||||||
Route.redirect("^/$", "/administration/overview"),
|
new Route(new RegExp("^/$")).redirect("/administration/overview"),
|
||||||
Route.redirect("^#.*", "/administration/overview"),
|
new Route(new RegExp("^#.*")).redirect("/administration/overview"),
|
||||||
Route.redirect("^/library$", "/if/user/", true),
|
new Route(new RegExp("^/library$")).redirect("/if/user/", true),
|
||||||
// statically imported since this is the default route
|
// statically imported since this is the default route
|
||||||
new Route("/administration/overview", () => {
|
new Route(new RegExp("^/administration/overview$"), async () => {
|
||||||
return html`<ak-admin-overview></ak-admin-overview>`;
|
return html`<ak-admin-overview></ak-admin-overview>`;
|
||||||
}),
|
}),
|
||||||
new Route("/administration/dashboard/users", async () => {
|
new Route(new RegExp("^/administration/dashboard/users$"), async () => {
|
||||||
await import("@goauthentik/admin/admin-overview/DashboardUserPage");
|
await import("@goauthentik/admin/admin-overview/DashboardUserPage");
|
||||||
|
|
||||||
return html`<ak-admin-dashboard-users></ak-admin-dashboard-users>`;
|
return html`<ak-admin-dashboard-users></ak-admin-dashboard-users>`;
|
||||||
}),
|
}),
|
||||||
new Route("/administration/system-tasks", async () => {
|
new Route(new RegExp("^/administration/system-tasks$"), async () => {
|
||||||
await import("@goauthentik/admin/system-tasks/SystemTaskListPage");
|
await import("@goauthentik/admin/system-tasks/SystemTaskListPage");
|
||||||
|
|
||||||
return html`<ak-system-task-list></ak-system-task-list>`;
|
return html`<ak-system-task-list></ak-system-task-list>`;
|
||||||
}),
|
}),
|
||||||
new Route("/core/providers", async () => {
|
new Route(new RegExp("^/core/providers$"), async () => {
|
||||||
await import("@goauthentik/admin/providers/ProviderListPage");
|
await import("@goauthentik/admin/providers/ProviderListPage");
|
||||||
|
|
||||||
return html`<ak-provider-list></ak-provider-list>`;
|
return html`<ak-provider-list></ak-provider-list>`;
|
||||||
}),
|
}),
|
||||||
new Route<IDParameters>(
|
new Route(new RegExp(`^/core/providers/(?<id>${ID_REGEX})$`), async (args) => {
|
||||||
new URLPattern({
|
await import("@goauthentik/admin/providers/ProviderViewPage");
|
||||||
pathname: `/core/providers/:id(${ID_PATTERN})`,
|
return html`<ak-provider-view .providerID=${parseInt(args.id, 10)}></ak-provider-view>`;
|
||||||
}),
|
}),
|
||||||
async (params) => {
|
new Route(new RegExp("^/core/applications$"), async () => {
|
||||||
await import("@goauthentik/admin/providers/ProviderViewPage");
|
|
||||||
|
|
||||||
return html`<ak-provider-view
|
|
||||||
.providerID=${parseInt(params.id, 10)}
|
|
||||||
></ak-provider-view>`;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
new Route("/core/applications", async () => {
|
|
||||||
await import("@goauthentik/admin/applications/ApplicationListPage");
|
await import("@goauthentik/admin/applications/ApplicationListPage");
|
||||||
|
|
||||||
return html`<ak-application-list></ak-application-list>`;
|
return html`<ak-application-list></ak-application-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(`/core/applications/:slug(${SLUG_PATTERN})`, async ({ slug }) => {
|
new Route(new RegExp(`^/core/applications/(?<slug>${SLUG_REGEX})$`), async (args) => {
|
||||||
await import("@goauthentik/admin/applications/ApplicationViewPage");
|
await import("@goauthentik/admin/applications/ApplicationViewPage");
|
||||||
|
return html`<ak-application-view .applicationSlug=${args.slug}></ak-application-view>`;
|
||||||
return html`<ak-application-view .applicationSlug=${slug}></ak-application-view>`;
|
|
||||||
}),
|
}),
|
||||||
new Route("/core/sources", async () => {
|
new Route(new RegExp("^/core/sources$"), async () => {
|
||||||
await import("@goauthentik/admin/sources/SourceListPage");
|
await import("@goauthentik/admin/sources/SourceListPage");
|
||||||
|
|
||||||
return html`<ak-source-list></ak-source-list>`;
|
return html`<ak-source-list></ak-source-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(`/core/sources/:slug(${SLUG_PATTERN})`, async ({ slug }) => {
|
new Route(new RegExp(`^/core/sources/(?<slug>${SLUG_REGEX})$`), async (args) => {
|
||||||
await import("@goauthentik/admin/sources/SourceViewPage");
|
await import("@goauthentik/admin/sources/SourceViewPage");
|
||||||
|
return html`<ak-source-view .sourceSlug=${args.slug}></ak-source-view>`;
|
||||||
return html`<ak-source-view .sourceSlug=${slug}></ak-source-view>`;
|
|
||||||
}),
|
}),
|
||||||
new Route("/core/property-mappings", async () => {
|
new Route(new RegExp("^/core/property-mappings$"), async () => {
|
||||||
await import("@goauthentik/admin/property-mappings/PropertyMappingListPage");
|
await import("@goauthentik/admin/property-mappings/PropertyMappingListPage");
|
||||||
|
|
||||||
return html`<ak-property-mapping-list></ak-property-mapping-list>`;
|
return html`<ak-property-mapping-list></ak-property-mapping-list>`;
|
||||||
}),
|
}),
|
||||||
new Route("/core/tokens", async () => {
|
new Route(new RegExp("^/core/tokens$"), async () => {
|
||||||
await import("@goauthentik/admin/tokens/TokenListPage");
|
await import("@goauthentik/admin/tokens/TokenListPage");
|
||||||
|
|
||||||
return html`<ak-token-list></ak-token-list>`;
|
return html`<ak-token-list></ak-token-list>`;
|
||||||
}),
|
}),
|
||||||
new Route("/core/brands", async () => {
|
new Route(new RegExp("^/core/brands"), async () => {
|
||||||
await import("@goauthentik/admin/brands/BrandListPage");
|
await import("@goauthentik/admin/brands/BrandListPage");
|
||||||
|
|
||||||
return html`<ak-brand-list></ak-brand-list>`;
|
return html`<ak-brand-list></ak-brand-list>`;
|
||||||
}),
|
}),
|
||||||
new Route("/policy/policies", async () => {
|
new Route(new RegExp("^/policy/policies$"), async () => {
|
||||||
await import("@goauthentik/admin/policies/PolicyListPage");
|
await import("@goauthentik/admin/policies/PolicyListPage");
|
||||||
|
|
||||||
return html`<ak-policy-list></ak-policy-list>`;
|
return html`<ak-policy-list></ak-policy-list>`;
|
||||||
}),
|
}),
|
||||||
new Route("/policy/reputation", async () => {
|
new Route(new RegExp("^/policy/reputation$"), async () => {
|
||||||
await import("@goauthentik/admin/policies/reputation/ReputationListPage");
|
await import("@goauthentik/admin/policies/reputation/ReputationListPage");
|
||||||
|
|
||||||
return html`<ak-policy-reputation-list></ak-policy-reputation-list>`;
|
return html`<ak-policy-reputation-list></ak-policy-reputation-list>`;
|
||||||
}),
|
}),
|
||||||
new Route("/identity/groups", async () => {
|
new Route(new RegExp("^/identity/groups$"), async () => {
|
||||||
await import("@goauthentik/admin/groups/GroupListPage");
|
await import("@goauthentik/admin/groups/GroupListPage");
|
||||||
|
|
||||||
return html`<ak-group-list></ak-group-list>`;
|
return html`<ak-group-list></ak-group-list>`;
|
||||||
}),
|
}),
|
||||||
new Route<UUIDParameters>(`/identity/groups/:uuid(${UUID_PATTERN})`, async ({ uuid }) => {
|
new Route(new RegExp(`^/identity/groups/(?<uuid>${UUID_REGEX})$`), async (args) => {
|
||||||
await import("@goauthentik/admin/groups/GroupViewPage");
|
await import("@goauthentik/admin/groups/GroupViewPage");
|
||||||
|
return html`<ak-group-view .groupId=${args.uuid}></ak-group-view>`;
|
||||||
return html`<ak-group-view .groupId=${uuid}></ak-group-view>`;
|
|
||||||
}),
|
}),
|
||||||
new Route("/identity/users", async () => {
|
new Route(new RegExp("^/identity/users$"), async () => {
|
||||||
await import("@goauthentik/admin/users/UserListPage");
|
await import("@goauthentik/admin/users/UserListPage");
|
||||||
|
|
||||||
return html`<ak-user-list></ak-user-list>`;
|
return html`<ak-user-list></ak-user-list>`;
|
||||||
}),
|
}),
|
||||||
new Route<IDParameters>(`/identity/users/:id(${ID_PATTERN})`, async ({ id }) => {
|
new Route(new RegExp(`^/identity/users/(?<id>${ID_REGEX})$`), async (args) => {
|
||||||
await import("@goauthentik/admin/users/UserViewPage");
|
await import("@goauthentik/admin/users/UserViewPage");
|
||||||
|
return html`<ak-user-view .userId=${parseInt(args.id, 10)}></ak-user-view>`;
|
||||||
return html`<ak-user-view .userId=${parseInt(id, 10)}></ak-user-view>`;
|
|
||||||
}),
|
}),
|
||||||
new Route("/identity/roles", async () => {
|
new Route(new RegExp("^/identity/roles$"), async () => {
|
||||||
await import("@goauthentik/admin/roles/RoleListPage");
|
await import("@goauthentik/admin/roles/RoleListPage");
|
||||||
|
|
||||||
return html`<ak-role-list></ak-role-list>`;
|
return html`<ak-role-list></ak-role-list>`;
|
||||||
}),
|
}),
|
||||||
new Route<IDParameters>(`/identity/roles/:id(${UUID_PATTERN})`, async ({ id }) => {
|
new Route(new RegExp(`^/identity/roles/(?<id>${UUID_REGEX})$`), async (args) => {
|
||||||
await import("@goauthentik/admin/roles/RoleViewPage");
|
await import("@goauthentik/admin/roles/RoleViewPage");
|
||||||
|
return html`<ak-role-view roleId=${args.id}></ak-role-view>`;
|
||||||
return html`<ak-role-view roleId=${id}></ak-role-view>`;
|
|
||||||
}),
|
}),
|
||||||
new Route("/flow/stages/invitations", async () => {
|
new Route(new RegExp("^/flow/stages/invitations$"), async () => {
|
||||||
await import("@goauthentik/admin/stages/invitation/InvitationListPage");
|
await import("@goauthentik/admin/stages/invitation/InvitationListPage");
|
||||||
|
|
||||||
return html`<ak-stage-invitation-list></ak-stage-invitation-list>`;
|
return html`<ak-stage-invitation-list></ak-stage-invitation-list>`;
|
||||||
}),
|
}),
|
||||||
new Route("/flow/stages/prompts", async () => {
|
new Route(new RegExp("^/flow/stages/prompts$"), async () => {
|
||||||
await import("@goauthentik/admin/stages/prompt/PromptListPage");
|
await import("@goauthentik/admin/stages/prompt/PromptListPage");
|
||||||
|
|
||||||
return html`<ak-stage-prompt-list></ak-stage-prompt-list>`;
|
return html`<ak-stage-prompt-list></ak-stage-prompt-list>`;
|
||||||
}),
|
}),
|
||||||
new Route("/flow/stages", async () => {
|
new Route(new RegExp("^/flow/stages$"), async () => {
|
||||||
await import("@goauthentik/admin/stages/StageListPage");
|
await import("@goauthentik/admin/stages/StageListPage");
|
||||||
|
|
||||||
return html`<ak-stage-list></ak-stage-list>`;
|
return html`<ak-stage-list></ak-stage-list>`;
|
||||||
}),
|
}),
|
||||||
new Route("/flow/flows", async () => {
|
new Route(new RegExp("^/flow/flows$"), async () => {
|
||||||
await import("@goauthentik/admin/flows/FlowListPage");
|
await import("@goauthentik/admin/flows/FlowListPage");
|
||||||
|
|
||||||
return html`<ak-flow-list></ak-flow-list>`;
|
return html`<ak-flow-list></ak-flow-list>`;
|
||||||
}),
|
}),
|
||||||
new Route<SlugParameters>(`/flow/flows/:slug(${SLUG_PATTERN})`, async ({ slug }) => {
|
new Route(new RegExp(`^/flow/flows/(?<slug>${SLUG_REGEX})$`), async (args) => {
|
||||||
await import("@goauthentik/admin/flows/FlowViewPage");
|
await import("@goauthentik/admin/flows/FlowViewPage");
|
||||||
|
return html`<ak-flow-view .flowSlug=${args.slug}></ak-flow-view>`;
|
||||||
return html`<ak-flow-view .flowSlug=${slug}></ak-flow-view>`;
|
|
||||||
}),
|
}),
|
||||||
new Route("/events/log", async () => {
|
new Route(new RegExp("^/events/log$"), async () => {
|
||||||
await import("@goauthentik/admin/events/EventListPage");
|
await import("@goauthentik/admin/events/EventListPage");
|
||||||
|
|
||||||
return html`<ak-event-list></ak-event-list>`;
|
return html`<ak-event-list></ak-event-list>`;
|
||||||
}),
|
}),
|
||||||
new Route<IDParameters>(`/events/log/:id(${UUID_PATTERN})`, async ({ id }) => {
|
new Route(new RegExp(`^/events/log/(?<id>${UUID_REGEX})$`), async (args) => {
|
||||||
await import("@goauthentik/admin/events/EventViewPage");
|
await import("@goauthentik/admin/events/EventViewPage");
|
||||||
|
return html`<ak-event-view .eventID=${args.id}></ak-event-view>`;
|
||||||
return html`<ak-event-view .eventID=${id}></ak-event-view>`;
|
|
||||||
}),
|
}),
|
||||||
new Route("/events/transports", async () => {
|
new Route(new RegExp("^/events/transports$"), async () => {
|
||||||
await import("@goauthentik/admin/events/TransportListPage");
|
await import("@goauthentik/admin/events/TransportListPage");
|
||||||
|
|
||||||
return html`<ak-event-transport-list></ak-event-transport-list>`;
|
return html`<ak-event-transport-list></ak-event-transport-list>`;
|
||||||
}),
|
}),
|
||||||
new Route("/events/rules", async () => {
|
new Route(new RegExp("^/events/rules$"), async () => {
|
||||||
await import("@goauthentik/admin/events/RuleListPage");
|
await import("@goauthentik/admin/events/RuleListPage");
|
||||||
|
|
||||||
return html`<ak-event-rule-list></ak-event-rule-list>`;
|
return html`<ak-event-rule-list></ak-event-rule-list>`;
|
||||||
}),
|
}),
|
||||||
new Route("/outpost/outposts", async () => {
|
new Route(new RegExp("^/outpost/outposts$"), async () => {
|
||||||
await import("@goauthentik/admin/outposts/OutpostListPage");
|
await import("@goauthentik/admin/outposts/OutpostListPage");
|
||||||
|
|
||||||
return html`<ak-outpost-list></ak-outpost-list>`;
|
return html`<ak-outpost-list></ak-outpost-list>`;
|
||||||
}),
|
}),
|
||||||
new Route("/outpost/integrations", async () => {
|
new Route(new RegExp("^/outpost/integrations$"), async () => {
|
||||||
await import("@goauthentik/admin/outposts/ServiceConnectionListPage");
|
await import("@goauthentik/admin/outposts/ServiceConnectionListPage");
|
||||||
|
|
||||||
return html`<ak-outpost-service-connection-list></ak-outpost-service-connection-list>`;
|
return html`<ak-outpost-service-connection-list></ak-outpost-service-connection-list>`;
|
||||||
}),
|
}),
|
||||||
new Route("/crypto/certificates", async () => {
|
new Route(new RegExp("^/crypto/certificates$"), async () => {
|
||||||
await import("@goauthentik/admin/crypto/CertificateKeyPairListPage");
|
await import("@goauthentik/admin/crypto/CertificateKeyPairListPage");
|
||||||
|
|
||||||
return html`<ak-crypto-certificate-list></ak-crypto-certificate-list>`;
|
return html`<ak-crypto-certificate-list></ak-crypto-certificate-list>`;
|
||||||
}),
|
}),
|
||||||
new Route("/admin/settings", async () => {
|
new Route(new RegExp("^/admin/settings$"), async () => {
|
||||||
await import("@goauthentik/admin/admin-settings/AdminSettingsPage");
|
await import("@goauthentik/admin/admin-settings/AdminSettingsPage");
|
||||||
|
|
||||||
return html`<ak-admin-settings></ak-admin-settings>`;
|
return html`<ak-admin-settings></ak-admin-settings>`;
|
||||||
}),
|
}),
|
||||||
new Route("/blueprints/instances", async () => {
|
new Route(new RegExp("^/blueprints/instances$"), async () => {
|
||||||
await import("@goauthentik/admin/blueprints/BlueprintListPage");
|
await import("@goauthentik/admin/blueprints/BlueprintListPage");
|
||||||
|
|
||||||
return html`<ak-blueprint-list></ak-blueprint-list>`;
|
return html`<ak-blueprint-list></ak-blueprint-list>`;
|
||||||
}),
|
}),
|
||||||
new Route("/debug", async () => {
|
new Route(new RegExp("^/debug$"), async () => {
|
||||||
await import("@goauthentik/admin/DebugPage");
|
await import("@goauthentik/admin/DebugPage");
|
||||||
|
|
||||||
return html`<ak-admin-debug-page></ak-admin-debug-page>`;
|
return html`<ak-admin-debug-page></ak-admin-debug-page>`;
|
||||||
}),
|
}),
|
||||||
new Route("/enterprise/licenses", async () => {
|
new Route(new RegExp("^/enterprise/licenses$"), async () => {
|
||||||
await import("@goauthentik/admin/enterprise/EnterpriseLicenseListPage");
|
await import("@goauthentik/admin/enterprise/EnterpriseLicenseListPage");
|
||||||
|
|
||||||
return html`<ak-enterprise-license-list></ak-enterprise-license-list>`;
|
return html`<ak-enterprise-license-list></ak-enterprise-license-list>`;
|
||||||
}),
|
}),
|
||||||
] satisfies Route<never>[];
|
];
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import "@goauthentik/elements/PageHeader";
|
|||||||
import "@goauthentik/elements/cards/AggregatePromiseCard";
|
import "@goauthentik/elements/cards/AggregatePromiseCard";
|
||||||
import "@goauthentik/elements/cards/QuickActionsCard.js";
|
import "@goauthentik/elements/cards/QuickActionsCard.js";
|
||||||
import type { QuickAction } from "@goauthentik/elements/cards/QuickActionsCard.js";
|
import type { QuickAction } from "@goauthentik/elements/cards/QuickActionsCard.js";
|
||||||
import { formatRouteHash } from "@goauthentik/elements/router";
|
import { paramURL } from "@goauthentik/elements/router/RouterOutlet";
|
||||||
|
|
||||||
import { msg, str } from "@lit/localize";
|
import { msg, str } from "@lit/localize";
|
||||||
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
|
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
|
||||||
@ -79,13 +79,10 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
quickActions: QuickAction[] = [
|
quickActions: QuickAction[] = [
|
||||||
[
|
[msg("Create a new application"), paramURL("/core/applications", { createForm: true })],
|
||||||
msg("Create a new application"),
|
[msg("Check the logs"), paramURL("/events/log")],
|
||||||
formatRouteHash("/core/applications", { createForm: true }),
|
|
||||||
],
|
|
||||||
[msg("Check the logs"), formatRouteHash("/events/log")],
|
|
||||||
[msg("Explore integrations"), "https://goauthentik.io/integrations/", true],
|
[msg("Explore integrations"), "https://goauthentik.io/integrations/", true],
|
||||||
[msg("Manage users"), formatRouteHash("/identity/users")],
|
[msg("Manage users"), paramURL("/identity/users")],
|
||||||
[msg("Check the release notes"), `https://goauthentik.io/docs/releases/${RELEASE}`, true],
|
[msg("Check the release notes"), `https://goauthentik.io/docs/releases/${RELEASE}`, true],
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -198,13 +195,10 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
|||||||
const release = `${versionFamily()}#fixed-in-${VERSION.replaceAll(".", "")}`;
|
const release = `${versionFamily()}#fixed-in-${VERSION.replaceAll(".", "")}`;
|
||||||
|
|
||||||
const quickActions: [string, string][] = [
|
const quickActions: [string, string][] = [
|
||||||
[
|
[msg("Create a new application"), paramURL("/core/applications", { createForm: true })],
|
||||||
msg("Create a new application"),
|
[msg("Check the logs"), paramURL("/events/log")],
|
||||||
formatRouteHash("/core/applications", { createForm: true }),
|
|
||||||
],
|
|
||||||
[msg("Check the logs"), formatRouteHash("/events/log")],
|
|
||||||
[msg("Explore integrations"), "https://goauthentik.io/integrations/"],
|
[msg("Explore integrations"), "https://goauthentik.io/integrations/"],
|
||||||
[msg("Manage users"), formatRouteHash("/identity/users")],
|
[msg("Manage users"), paramURL("/identity/users")],
|
||||||
[msg("Check the release notes"), `https://goauthentik.io/docs/releases/${release}`],
|
[msg("Check the release notes"), `https://goauthentik.io/docs/releases/${release}`],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import "@goauthentik/elements/ak-mdx";
|
|||||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||||
import "@goauthentik/elements/forms/ModalForm";
|
import "@goauthentik/elements/forms/ModalForm";
|
||||||
import { getRouteParameter } from "@goauthentik/elements/router/utils";
|
import { getURLParam } from "@goauthentik/elements/router/RouteMatch";
|
||||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||||
import { TableColumn } from "@goauthentik/elements/table/Table";
|
import { TableColumn } from "@goauthentik/elements/table/Table";
|
||||||
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
||||||
@ -156,7 +156,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderObjectCreate(): TemplateResult {
|
renderObjectCreate(): TemplateResult {
|
||||||
return html` <ak-application-wizard .open=${getRouteParameter("createWizard", false)}>
|
return html` <ak-application-wizard .open=${getURLParam("createWizard", false)}>
|
||||||
<button
|
<button
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
class="pf-c-button pf-m-primary"
|
class="pf-c-button pf-m-primary"
|
||||||
@ -165,7 +165,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
|||||||
${msg("Create with Provider")}
|
${msg("Create with Provider")}
|
||||||
</button>
|
</button>
|
||||||
</ak-application-wizard>
|
</ak-application-wizard>
|
||||||
<ak-forms-modal .open=${getRouteParameter("createForm", false)}>
|
<ak-forms-modal .open=${getURLParam("createForm", false)}>
|
||||||
<span slot="submit"> ${msg("Create")} </span>
|
<span slot="submit"> ${msg("Create")} </span>
|
||||||
<span slot="header"> ${msg("Create Application")} </span>
|
<span slot="header"> ${msg("Create Application")} </span>
|
||||||
<ak-application-form slot="form"> </ak-application-form>
|
<ak-application-form slot="form"> </ak-application-form>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import "@goauthentik/components/ak-hint/ak-hint-body";
|
|||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import "@goauthentik/elements/Label";
|
import "@goauthentik/elements/Label";
|
||||||
import "@goauthentik/elements/buttons/ActionButton/ak-action-button";
|
import "@goauthentik/elements/buttons/ActionButton/ak-action-button";
|
||||||
import { getRouteParameter } from "@goauthentik/elements/router/utils";
|
import { getURLParam } from "@goauthentik/elements/router/RouteMatch";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { css, html } from "lit";
|
import { css, html } from "lit";
|
||||||
@ -110,7 +110,7 @@ export class AkApplicationWizardHint extends AKElement implements ShowHintContro
|
|||||||
the same time with our new Application Wizard.
|
the same time with our new Application Wizard.
|
||||||
<!-- <a href="(link to docs)">Learn more about the wizard here.</a> -->
|
<!-- <a href="(link to docs)">Learn more about the wizard here.</a> -->
|
||||||
</p>
|
</p>
|
||||||
<ak-application-wizard .open=${getRouteParameter("createWizard", false)}>
|
<ak-application-wizard .open=${getURLParam("createWizard", false)}>
|
||||||
<button
|
<button
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
class="pf-c-button pf-m-primary"
|
class="pf-c-button pf-m-primary"
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { policyOptions } from "@goauthentik/admin/applications/PolicyOptions.js";
|
import { policyOptions } from "@goauthentik/admin/applications/PolicyOptions.js";
|
||||||
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
|
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
|
||||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
|
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
|
||||||
|
import { isSlug } from "@goauthentik/common/utils.js";
|
||||||
import { camelToSnake } from "@goauthentik/common/utils.js";
|
import { camelToSnake } from "@goauthentik/common/utils.js";
|
||||||
import "@goauthentik/components/ak-radio-input";
|
import "@goauthentik/components/ak-radio-input";
|
||||||
import "@goauthentik/components/ak-slug-input";
|
import "@goauthentik/components/ak-slug-input";
|
||||||
@ -10,7 +11,6 @@ import { type NavigableButton, type WizardButton } from "@goauthentik/components
|
|||||||
import { type KeyUnknown } from "@goauthentik/elements/forms/Form";
|
import { type KeyUnknown } from "@goauthentik/elements/forms/Form";
|
||||||
import "@goauthentik/elements/forms/FormGroup";
|
import "@goauthentik/elements/forms/FormGroup";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import { isSlug } from "@goauthentik/elements/router";
|
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import "@goauthentik/admin/flows/FlowForm";
|
import "@goauthentik/admin/flows/FlowForm";
|
||||||
import "@goauthentik/admin/flows/FlowImportForm";
|
import "@goauthentik/admin/flows/FlowImportForm";
|
||||||
import { DesignationToLabel, formatFlowURL } from "@goauthentik/admin/flows/utils";
|
import { DesignationToLabel } from "@goauthentik/admin/flows/utils";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { groupBy } from "@goauthentik/common/utils";
|
import { groupBy } from "@goauthentik/common/utils";
|
||||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||||
import "@goauthentik/elements/forms/ConfirmationForm";
|
import "@goauthentik/elements/forms/ConfirmationForm";
|
||||||
@ -107,9 +107,10 @@ export class FlowListPage extends TablePage<Flow> {
|
|||||||
<button
|
<button
|
||||||
class="pf-c-button pf-m-plain"
|
class="pf-c-button pf-m-plain"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
const url = formatFlowURL(item);
|
const finalURL = `${window.location.origin}/if/flow/${item.slug}/${AndNext(
|
||||||
|
`${window.location.pathname}#${window.location.hash}`,
|
||||||
window.open(url, "_blank");
|
)}`;
|
||||||
|
window.open(finalURL, "_blank");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<pf-tooltip position="top" content=${msg("Execute")}>
|
<pf-tooltip position="top" content=${msg("Execute")}>
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import "@goauthentik/admin/flows/BoundStagesList";
|
import "@goauthentik/admin/flows/BoundStagesList";
|
||||||
import "@goauthentik/admin/flows/FlowDiagram";
|
import "@goauthentik/admin/flows/FlowDiagram";
|
||||||
import "@goauthentik/admin/flows/FlowForm";
|
import "@goauthentik/admin/flows/FlowForm";
|
||||||
import { DesignationToLabel, applyNextParam, formatFlowURL } from "@goauthentik/admin/flows/utils";
|
import { DesignationToLabel } from "@goauthentik/admin/flows/utils";
|
||||||
import "@goauthentik/admin/policies/BoundPoliciesList";
|
import "@goauthentik/admin/policies/BoundPoliciesList";
|
||||||
import "@goauthentik/admin/rbac/ObjectPermissionsPage";
|
import "@goauthentik/admin/rbac/ObjectPermissionsPage";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import "@goauthentik/components/events/ObjectChangelog";
|
import "@goauthentik/components/events/ObjectChangelog";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import "@goauthentik/elements/PageHeader";
|
import "@goauthentik/elements/PageHeader";
|
||||||
@ -151,9 +151,12 @@ export class FlowViewPage extends AKElement {
|
|||||||
<button
|
<button
|
||||||
class="pf-c-button pf-m-block pf-m-primary"
|
class="pf-c-button pf-m-block pf-m-primary"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
const url = formatFlowURL(this.flow);
|
const finalURL = `${
|
||||||
|
window.location.origin
|
||||||
window.open(url, "_blank");
|
}/if/flow/${this.flow.slug}/${AndNext(
|
||||||
|
`${window.location.pathname}#${window.location.hash}`,
|
||||||
|
)}`;
|
||||||
|
window.open(finalURL, "_blank");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
${msg("Normal")}
|
${msg("Normal")}
|
||||||
@ -165,16 +168,12 @@ export class FlowViewPage extends AKElement {
|
|||||||
.flowsInstancesExecuteRetrieve({
|
.flowsInstancesExecuteRetrieve({
|
||||||
slug: this.flow.slug,
|
slug: this.flow.slug,
|
||||||
})
|
})
|
||||||
.then(({ link }) => {
|
.then((link) => {
|
||||||
const finalURL = URL.canParse(link)
|
const finalURL = `${
|
||||||
? new URL(link)
|
link.link
|
||||||
: new URL(
|
}${AndNext(
|
||||||
link,
|
`${window.location.pathname}#${window.location.hash}`,
|
||||||
window.location.origin,
|
)}`;
|
||||||
);
|
|
||||||
|
|
||||||
applyNextParam(finalURL);
|
|
||||||
|
|
||||||
window.open(finalURL, "_blank");
|
window.open(finalURL, "_blank");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -43,51 +43,3 @@ export function LayoutToLabel(layout: FlowLayoutEnum): string {
|
|||||||
return msg("Unknown layout");
|
return msg("Unknown layout");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies the next URL as a query parameter to the given URL or URLSearchParams object.
|
|
||||||
*
|
|
||||||
* @todo deprecate this once hash routing is removed.
|
|
||||||
*/
|
|
||||||
export function applyNextParam(
|
|
||||||
target: URL | URLSearchParams,
|
|
||||||
destination: string | URL = window.location.pathname + "#" + window.location.hash,
|
|
||||||
): void {
|
|
||||||
const searchParams = target instanceof URL ? target.searchParams : target;
|
|
||||||
|
|
||||||
searchParams.set("next", destination.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a URLSearchParams object with the next URL as a query parameter.
|
|
||||||
*
|
|
||||||
* @todo deprecate this once hash routing is removed.
|
|
||||||
*/
|
|
||||||
export function createNextSearchParams(
|
|
||||||
destination: string | URL = window.location.pathname + "#" + window.location.hash,
|
|
||||||
): URLSearchParams {
|
|
||||||
const searchParams = new URLSearchParams();
|
|
||||||
|
|
||||||
applyNextParam(searchParams, destination);
|
|
||||||
|
|
||||||
return searchParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a URL to a flow, with the next URL as a query parameter.
|
|
||||||
*
|
|
||||||
* @param flow The flow to create the URL for.
|
|
||||||
* @param destination The next URL to redirect to after the flow is completed, `true` to use the current route.
|
|
||||||
*/
|
|
||||||
export function formatFlowURL(
|
|
||||||
flow: Flow,
|
|
||||||
destination: string | URL | null = window.location.pathname + "#" + window.location.hash,
|
|
||||||
): URL {
|
|
||||||
const url = new URL(`/if/flow/${flow.slug}/`, window.location.origin);
|
|
||||||
|
|
||||||
if (destination) {
|
|
||||||
applyNextParam(url, destination);
|
|
||||||
}
|
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import { Form } from "@goauthentik/elements/forms/Form";
|
|||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import "@goauthentik/elements/forms/ModalForm";
|
import "@goauthentik/elements/forms/ModalForm";
|
||||||
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
||||||
import { getRouteParameter, patchRouteParams } from "@goauthentik/elements/router/utils";
|
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
|
||||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||||
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
|
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
|
||||||
import { UserOption } from "@goauthentik/elements/user/utils";
|
import { UserOption } from "@goauthentik/elements/user/utils";
|
||||||
@ -127,7 +127,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
|||||||
order = "last_login";
|
order = "last_login";
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
hideServiceAccounts = getRouteParameter<boolean>("hideServiceAccounts", true);
|
hideServiceAccounts = getURLParam<boolean>("hideServiceAccounts", true);
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
me?: SessionUser;
|
me?: SessionUser;
|
||||||
@ -466,7 +466,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
|||||||
this.hideServiceAccounts = !this.hideServiceAccounts;
|
this.hideServiceAccounts = !this.hideServiceAccounts;
|
||||||
this.page = 1;
|
this.page = 1;
|
||||||
this.fetch();
|
this.fetch();
|
||||||
patchRouteParams({
|
updateURLParams({
|
||||||
hideServiceAccounts: this.hideServiceAccounts,
|
hideServiceAccounts: this.hideServiceAccounts,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
|||||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||||
import "@goauthentik/elements/forms/ModalForm";
|
import "@goauthentik/elements/forms/ModalForm";
|
||||||
import "@goauthentik/elements/forms/ProxyForm";
|
import "@goauthentik/elements/forms/ProxyForm";
|
||||||
import { getRouteParameter, patchRouteParams } from "@goauthentik/elements/router/utils";
|
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
|
||||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||||
import { TableColumn } from "@goauthentik/elements/table/Table";
|
import { TableColumn } from "@goauthentik/elements/table/Table";
|
||||||
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
||||||
@ -54,7 +54,7 @@ export class PropertyMappingListPage extends TablePage<PropertyMapping> {
|
|||||||
order = "name";
|
order = "name";
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
hideManaged = getRouteParameter<boolean>("hideManaged", true);
|
hideManaged = getURLParam<boolean>("hideManaged", true);
|
||||||
|
|
||||||
async apiEndpoint(): Promise<PaginatedResponse<PropertyMapping>> {
|
async apiEndpoint(): Promise<PaginatedResponse<PropertyMapping>> {
|
||||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsAllList({
|
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsAllList({
|
||||||
@ -148,7 +148,7 @@ export class PropertyMappingListPage extends TablePage<PropertyMapping> {
|
|||||||
this.hideManaged = !this.hideManaged;
|
this.hideManaged = !this.hideManaged;
|
||||||
this.page = 1;
|
this.page = 1;
|
||||||
this.fetch();
|
this.fetch();
|
||||||
patchRouteParams({
|
updateURLParams({
|
||||||
hideManaged: this.hideManaged,
|
hideManaged: this.hideManaged,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
|
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
|
||||||
|
|
||||||
import { PropertymappingsApi, ScopeMapping } from "@goauthentik/api";
|
import { PropertymappingsApi, ScopeMapping } from "@goauthentik/api";
|
||||||
|
|
||||||
@ -21,7 +22,11 @@ export async function propertyMappingsProvider(page = 1, search = "") {
|
|||||||
|
|
||||||
export function propertyMappingsSelector(instanceMappings?: string[]) {
|
export function propertyMappingsSelector(instanceMappings?: string[]) {
|
||||||
if (!instanceMappings) {
|
if (!instanceMappings) {
|
||||||
return async () => [];
|
return async (mappings: DualSelectPair<ScopeMapping>[]) =>
|
||||||
|
mappings.filter(
|
||||||
|
([_0, _1, _2, scope]: DualSelectPair<ScopeMapping>) =>
|
||||||
|
!(scope?.managed ?? "").startsWith("goauthentik.io/providers"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return async () => {
|
return async () => {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import "@goauthentik/admin/providers/proxy/ProxyProviderForm";
|
|||||||
import "@goauthentik/admin/rbac/ObjectPermissionsPage";
|
import "@goauthentik/admin/rbac/ObjectPermissionsPage";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||||
|
import { convertToSlug } from "@goauthentik/common/utils";
|
||||||
import "@goauthentik/components/ak-status-label";
|
import "@goauthentik/components/ak-status-label";
|
||||||
import "@goauthentik/components/events/ObjectChangelog";
|
import "@goauthentik/components/events/ObjectChangelog";
|
||||||
import MDCaddyStandalone from "@goauthentik/docs/add-secure-apps/providers/proxy/_caddy_standalone.md";
|
import MDCaddyStandalone from "@goauthentik/docs/add-secure-apps/providers/proxy/_caddy_standalone.md";
|
||||||
@ -20,8 +21,7 @@ import "@goauthentik/elements/ak-mdx";
|
|||||||
import type { Replacer } from "@goauthentik/elements/ak-mdx";
|
import type { Replacer } from "@goauthentik/elements/ak-mdx";
|
||||||
import "@goauthentik/elements/buttons/ModalButton";
|
import "@goauthentik/elements/buttons/ModalButton";
|
||||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||||
import { formatAsSlug } from "@goauthentik/elements/router";
|
import { getURLParam } from "@goauthentik/elements/router/RouteMatch";
|
||||||
import { getRouteParameter } from "@goauthentik/elements/router/utils";
|
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { CSSResult, PropertyValues, TemplateResult, css, html } from "lit";
|
import { CSSResult, PropertyValues, TemplateResult, css, html } from "lit";
|
||||||
@ -156,7 +156,7 @@ export class ProxyProviderViewPage extends AKElement {
|
|||||||
(input: string): string => {
|
(input: string): string => {
|
||||||
// The generated config is pretty unreliable currently so
|
// The generated config is pretty unreliable currently so
|
||||||
// put it behind a flag
|
// put it behind a flag
|
||||||
if (!getRouteParameter("generatedConfig", false)) {
|
if (!getURLParam("generatedConfig", false)) {
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
if (!this.provider) {
|
if (!this.provider) {
|
||||||
@ -183,7 +183,7 @@ export class ProxyProviderViewPage extends AKElement {
|
|||||||
return html`<ak-tabs pageIdentifier="proxy-setup">
|
return html`<ak-tabs pageIdentifier="proxy-setup">
|
||||||
${servers.map((server) => {
|
${servers.map((server) => {
|
||||||
return html`<section
|
return html`<section
|
||||||
slot="page-${formatAsSlug(server.label)}"
|
slot="page-${convertToSlug(server.label)}"
|
||||||
data-tab-title="${server.label}"
|
data-tab-title="${server.label}"
|
||||||
class="pf-c-page__main-section pf-m-no-padding-mobile ak-markdown-section"
|
class="pf-c-page__main-section pf-m-no-padding-mobile ak-markdown-section"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
|||||||
import { PFSize } from "@goauthentik/common/enums.js";
|
import { PFSize } from "@goauthentik/common/enums.js";
|
||||||
import { userTypeToLabel } from "@goauthentik/common/labels";
|
import { userTypeToLabel } from "@goauthentik/common/labels";
|
||||||
import { MessageLevel } from "@goauthentik/common/messages";
|
import { MessageLevel } from "@goauthentik/common/messages";
|
||||||
import { createUIConfig, uiConfig } from "@goauthentik/common/ui/config";
|
import { DefaultUIConfig, uiConfig } from "@goauthentik/common/ui/config";
|
||||||
import { me } from "@goauthentik/common/users";
|
import { me } from "@goauthentik/common/users";
|
||||||
import { getRelativeTime } from "@goauthentik/common/utils";
|
import { getRelativeTime } from "@goauthentik/common/utils";
|
||||||
import "@goauthentik/components/ak-status-label";
|
import "@goauthentik/components/ak-status-label";
|
||||||
@ -24,7 +24,7 @@ import "@goauthentik/elements/buttons/ActionButton";
|
|||||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||||
import "@goauthentik/elements/forms/ModalForm";
|
import "@goauthentik/elements/forms/ModalForm";
|
||||||
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
||||||
import { getRouteParameter, patchRouteParams } from "@goauthentik/elements/router/utils";
|
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
|
||||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||||
import { TableColumn } from "@goauthentik/elements/table/Table";
|
import { TableColumn } from "@goauthentik/elements/table/Table";
|
||||||
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
||||||
@ -117,7 +117,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
|
|||||||
activePath;
|
activePath;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
hideDeactivated = getRouteParameter<boolean>("hideDeactivated", false);
|
hideDeactivated = getURLParam<boolean>("hideDeactivated", false);
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
userPaths?: UserPath;
|
userPaths?: UserPath;
|
||||||
@ -131,10 +131,8 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
const defaultPath = new DefaultUIConfig().defaults.userPath;
|
||||||
const defaultPath = createUIConfig().defaults.userPath;
|
this.activePath = getURLParam<string>("path", defaultPath);
|
||||||
this.activePath = getRouteParameter("path", defaultPath);
|
|
||||||
|
|
||||||
uiConfig().then((c) => {
|
uiConfig().then((c) => {
|
||||||
if (c.defaults.userPath !== defaultPath) {
|
if (c.defaults.userPath !== defaultPath) {
|
||||||
this.activePath = c.defaults.userPath;
|
this.activePath = c.defaults.userPath;
|
||||||
@ -145,7 +143,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
|
|||||||
async apiEndpoint(): Promise<PaginatedResponse<User>> {
|
async apiEndpoint(): Promise<PaginatedResponse<User>> {
|
||||||
const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList({
|
const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList({
|
||||||
...(await this.defaultEndpointConfig()),
|
...(await this.defaultEndpointConfig()),
|
||||||
pathStartswith: getRouteParameter("path", ""),
|
pathStartswith: getURLParam("path", ""),
|
||||||
isActive: this.hideDeactivated ? true : undefined,
|
isActive: this.hideDeactivated ? true : undefined,
|
||||||
includeGroups: false,
|
includeGroups: false,
|
||||||
});
|
});
|
||||||
@ -227,7 +225,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
|
|||||||
this.hideDeactivated = !this.hideDeactivated;
|
this.hideDeactivated = !this.hideDeactivated;
|
||||||
this.page = 1;
|
this.page = 1;
|
||||||
this.fetch();
|
this.fetch();
|
||||||
patchRouteParams({
|
updateURLParams({
|
||||||
hideDeactivated: this.hideDeactivated,
|
hideDeactivated: this.hideDeactivated,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -79,4 +79,11 @@ export const DEFAULT_CONFIG = new Configuration({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// This is just a function so eslint doesn't complain about
|
||||||
|
// missing-whitespace-between-attributes or
|
||||||
|
// unexpected-character-in-attribute-name
|
||||||
|
export function AndNext(url: string): string {
|
||||||
|
return `?next=${encodeURIComponent(url)}`;
|
||||||
|
}
|
||||||
|
|
||||||
console.debug(`authentik(early): version ${VERSION}, apiBase ${DEFAULT_CONFIG.basePath}`);
|
console.debug(`authentik(early): version ${VERSION}, apiBase ${DEFAULT_CONFIG.basePath}`);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { EVENT_REQUEST_POST } from "@goauthentik/common/constants";
|
import { EVENT_REQUEST_POST } from "@goauthentik/common/constants";
|
||||||
import { getCookie } from "@goauthentik/common/http";
|
import { getCookie } from "@goauthentik/common/utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CurrentBrand,
|
CurrentBrand,
|
||||||
|
|||||||
@ -1,60 +1,170 @@
|
|||||||
/**
|
/**
|
||||||
* @file Client-side utilities.
|
* @file
|
||||||
|
* Client-side observer for ESBuild events.
|
||||||
*/
|
*/
|
||||||
import { TITLE_DEFAULT } from "@goauthentik/common/constants";
|
import type { Message as ESBuildMessage } from "esbuild";
|
||||||
import { isAdminRoute } from "@goauthentik/elements/router";
|
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
const logPrefix = "👷 [ESBuild]";
|
||||||
|
const log = console.debug.bind(console, logPrefix);
|
||||||
|
|
||||||
import type { CurrentBrand } from "@goauthentik/api";
|
type BuildEventListener<Data = unknown> = (event: MessageEvent<Data>) => void;
|
||||||
|
|
||||||
type BrandTitleLike = Partial<Pick<CurrentBrand, "brandingTitle">>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a title for the page.
|
* A client-side watcher for ESBuild.
|
||||||
*
|
*
|
||||||
* @param brand - The brand object to append to the title.
|
* Note that this should be conditionally imported in your code, so that
|
||||||
* @param segments - The segments to prepend to the title.
|
* ESBuild may tree-shake it out of production builds.
|
||||||
*/
|
|
||||||
export function formatPageTitle(
|
|
||||||
brand: BrandTitleLike | undefined,
|
|
||||||
...segments: Array<string | undefined>
|
|
||||||
): string;
|
|
||||||
/**
|
|
||||||
* Create a title for the page.
|
|
||||||
*
|
*
|
||||||
* @param segments - The segments to prepend to the title.
|
* ```ts
|
||||||
*/
|
* if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) {
|
||||||
export function formatPageTitle(...segments: Array<string | undefined>): string;
|
* const { ESBuildObserver } = await import("@goauthentik/common/client");
|
||||||
/**
|
|
||||||
* Create a title for the page.
|
|
||||||
*
|
*
|
||||||
* @param args - The segments to prepend to the title.
|
* new ESBuildObserver(process.env.WATCHER_URL);
|
||||||
* @param args - The brand object to append to the title.
|
* }
|
||||||
*/
|
* ```
|
||||||
export function formatPageTitle(
|
}
|
||||||
...args: [BrandTitleLike | string | undefined, ...Array<string | undefined>]
|
|
||||||
): string {
|
*/
|
||||||
const segments: string[] = [];
|
export class ESBuildObserver extends EventSource {
|
||||||
|
/**
|
||||||
if (isAdminRoute()) {
|
* Whether the watcher has a recent connection to the server.
|
||||||
segments.push(msg("Admin"));
|
*/
|
||||||
}
|
alive = true;
|
||||||
|
|
||||||
const [arg1, ...rest] = args;
|
/**
|
||||||
|
* The number of errors that have occurred since the watcher started.
|
||||||
if (typeof arg1 === "object") {
|
*/
|
||||||
const { brandingTitle = TITLE_DEFAULT } = arg1;
|
errorCount = 0;
|
||||||
segments.push(brandingTitle);
|
|
||||||
} else {
|
/**
|
||||||
segments.push(TITLE_DEFAULT);
|
* Whether a reload has been requested while offline.
|
||||||
}
|
*/
|
||||||
|
deferredReload = false;
|
||||||
for (const segment of rest) {
|
|
||||||
if (segment) {
|
/**
|
||||||
segments.push(segment);
|
* The last time a message was received from the server.
|
||||||
}
|
*/
|
||||||
}
|
lastUpdatedAt = Date.now();
|
||||||
|
|
||||||
return segments.join(" - ");
|
/**
|
||||||
|
* Whether the browser considers itself online.
|
||||||
|
*/
|
||||||
|
online = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the animation frame for the reload.
|
||||||
|
*/
|
||||||
|
#reloadFrameID = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The interval for the keep-alive check.
|
||||||
|
*/
|
||||||
|
#keepAliveInterval: ReturnType<typeof setInterval> | undefined;
|
||||||
|
|
||||||
|
#trackActivity = () => {
|
||||||
|
this.lastUpdatedAt = Date.now();
|
||||||
|
this.alive = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
#startListener: BuildEventListener = () => {
|
||||||
|
this.#trackActivity();
|
||||||
|
log("⏰ Build started...");
|
||||||
|
};
|
||||||
|
|
||||||
|
#internalErrorListener = () => {
|
||||||
|
this.errorCount += 1;
|
||||||
|
|
||||||
|
if (this.errorCount > 100) {
|
||||||
|
clearTimeout(this.#keepAliveInterval);
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
log("⛔️ Closing connection");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#errorListener: BuildEventListener<string> = (event) => {
|
||||||
|
this.#trackActivity();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.group(logPrefix, "⛔️⛔️⛔️ Build error...");
|
||||||
|
|
||||||
|
const esbuildErrorMessages: ESBuildMessage[] = JSON.parse(event.data);
|
||||||
|
|
||||||
|
for (const error of esbuildErrorMessages) {
|
||||||
|
console.warn(error.text);
|
||||||
|
|
||||||
|
if (error.location) {
|
||||||
|
console.debug(
|
||||||
|
`file://${error.location.file}:${error.location.line}:${error.location.column}`,
|
||||||
|
);
|
||||||
|
console.debug(error.location.lineText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.groupEnd();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endListener: BuildEventListener = () => {
|
||||||
|
cancelAnimationFrame(this.#reloadFrameID);
|
||||||
|
|
||||||
|
this.#trackActivity();
|
||||||
|
|
||||||
|
if (!this.online) {
|
||||||
|
log("🚫 Build finished while offline.");
|
||||||
|
this.deferredReload = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log("🛎️ Build completed! Reloading...");
|
||||||
|
|
||||||
|
// We use an animation frame to keep the reload from happening before the
|
||||||
|
// event loop has a chance to process the message.
|
||||||
|
this.#reloadFrameID = requestAnimationFrame(() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
#keepAliveListener: BuildEventListener = () => {
|
||||||
|
this.#trackActivity();
|
||||||
|
log("🏓 Keep-alive");
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(url: string | URL) {
|
||||||
|
super(url);
|
||||||
|
|
||||||
|
this.addEventListener("esbuild:start", this.#startListener);
|
||||||
|
this.addEventListener("esbuild:end", this.#endListener);
|
||||||
|
this.addEventListener("esbuild:error", this.#errorListener);
|
||||||
|
this.addEventListener("esbuild:keep-alive", this.#keepAliveListener);
|
||||||
|
|
||||||
|
this.addEventListener("error", this.#internalErrorListener);
|
||||||
|
|
||||||
|
window.addEventListener("offline", () => {
|
||||||
|
this.online = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("online", () => {
|
||||||
|
this.online = true;
|
||||||
|
|
||||||
|
if (!this.deferredReload) return;
|
||||||
|
|
||||||
|
log("🛎️ Reloading after offline build...");
|
||||||
|
this.deferredReload = false;
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
log("🛎️ Listening for build changes...");
|
||||||
|
|
||||||
|
this.#keepAliveInterval = setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (now - this.lastUpdatedAt < 10_000) return;
|
||||||
|
|
||||||
|
this.alive = false;
|
||||||
|
log("👋 Waiting for build to start...");
|
||||||
|
}, 15_000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,9 @@ export const SUCCESS_CLASS = "pf-m-success";
|
|||||||
export const ERROR_CLASS = "pf-m-danger";
|
export const ERROR_CLASS = "pf-m-danger";
|
||||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||||
export const CURRENT_CLASS = "pf-m-current";
|
export const CURRENT_CLASS = "pf-m-current";
|
||||||
export const VERSION = "2025.2.3";
|
export const VERSION = "2025.2.2";
|
||||||
export const TITLE_DEFAULT = "authentik";
|
export const TITLE_DEFAULT = "authentik";
|
||||||
|
export const ROUTE_SEPARATOR = ";";
|
||||||
|
|
||||||
export const EVENT_REFRESH = "ak-refresh";
|
export const EVENT_REFRESH = "ak-refresh";
|
||||||
export const EVENT_NOTIFICATION_DRAWER_TOGGLE = "ak-notification-toggle";
|
export const EVENT_NOTIFICATION_DRAWER_TOGGLE = "ak-notification-toggle";
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file HTTP utilities.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the value of a cookie by its name.
|
|
||||||
*
|
|
||||||
* @param cookieName - The name of the cookie to retrieve.
|
|
||||||
* @returns The value of the cookie, or an empty string if the cookie is not found.
|
|
||||||
*/
|
|
||||||
export function getCookie(cookieName: string): string {
|
|
||||||
if (!cookieName) return "";
|
|
||||||
if (typeof document === "undefined") return "";
|
|
||||||
if (typeof document.cookie !== "string") return "";
|
|
||||||
if (!document.cookie) return "";
|
|
||||||
|
|
||||||
const search = cookieName + "=";
|
|
||||||
// Split the cookie string into individual name=value pairs...
|
|
||||||
const keyValPairs = document.cookie.split(";").map((cookie) => cookie.trim());
|
|
||||||
|
|
||||||
for (const pair of keyValPairs) {
|
|
||||||
if (!pair.startsWith(search)) continue;
|
|
||||||
|
|
||||||
return decodeURIComponent(pair.substring(search.length));
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
@ -2,7 +2,6 @@ import { config } from "@goauthentik/common/api/config";
|
|||||||
import { VERSION } from "@goauthentik/common/constants";
|
import { VERSION } from "@goauthentik/common/constants";
|
||||||
import { SentryIgnoredError } from "@goauthentik/common/errors";
|
import { SentryIgnoredError } from "@goauthentik/common/errors";
|
||||||
import { me } from "@goauthentik/common/users";
|
import { me } from "@goauthentik/common/users";
|
||||||
import { readInterfaceRouteParam } from "@goauthentik/elements/router/utils";
|
|
||||||
import {
|
import {
|
||||||
ErrorEvent,
|
ErrorEvent,
|
||||||
EventHint,
|
EventHint,
|
||||||
@ -65,7 +64,7 @@ export async function configureSentry(canDoPpi = false): Promise<Config> {
|
|||||||
});
|
});
|
||||||
setTag(TAG_SENTRY_CAPABILITIES, cfg.capabilities.join(","));
|
setTag(TAG_SENTRY_CAPABILITIES, cfg.capabilities.join(","));
|
||||||
if (window.location.pathname.includes("if/")) {
|
if (window.location.pathname.includes("if/")) {
|
||||||
setTag(TAG_SENTRY_COMPONENT, `web/${readInterfaceRouteParam()}`);
|
setTag(TAG_SENTRY_COMPONENT, `web/${currentInterface()}`);
|
||||||
}
|
}
|
||||||
if (cfg.capabilities.includes(CapabilitiesEnum.CanDebug)) {
|
if (cfg.capabilities.includes(CapabilitiesEnum.CanDebug)) {
|
||||||
const Spotlight = await import("@spotlightjs/spotlight");
|
const Spotlight = await import("@spotlightjs/spotlight");
|
||||||
@ -83,3 +82,13 @@ export async function configureSentry(canDoPpi = false): Promise<Config> {
|
|||||||
}
|
}
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the interface name from URL
|
||||||
|
export function currentInterface(): string {
|
||||||
|
const pathMatches = window.location.pathname.match(/.+if\/(\w+)\//);
|
||||||
|
let currentInterface = "unknown";
|
||||||
|
if (pathMatches && pathMatches.length >= 2) {
|
||||||
|
currentInterface = pathMatches[1];
|
||||||
|
}
|
||||||
|
return currentInterface.toLowerCase();
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
import { currentInterface } from "@goauthentik/common/sentry";
|
||||||
import { me } from "@goauthentik/common/users";
|
import { me } from "@goauthentik/common/users";
|
||||||
import { isUserRoute } from "@goauthentik/elements/router";
|
|
||||||
|
|
||||||
import { UiThemeEnum } from "@goauthentik/api";
|
import { UiThemeEnum, UserSelf } from "@goauthentik/api";
|
||||||
|
|
||||||
export enum UserDisplay {
|
export enum UserDisplay {
|
||||||
username = "username",
|
username = "username",
|
||||||
@ -18,27 +18,15 @@ export enum LayoutType {
|
|||||||
|
|
||||||
export interface UIConfig {
|
export interface UIConfig {
|
||||||
enabledFeatures: {
|
enabledFeatures: {
|
||||||
/**
|
// API Request drawer in navbar
|
||||||
* Whether to show the API request drawer in the navbar.
|
|
||||||
*/
|
|
||||||
apiDrawer: boolean;
|
apiDrawer: boolean;
|
||||||
/**
|
// Notification drawer in navbar
|
||||||
* Whether to show the notification drawer in the navbar.
|
|
||||||
*/
|
|
||||||
notificationDrawer: boolean;
|
notificationDrawer: boolean;
|
||||||
/**
|
// Settings in user dropdown
|
||||||
* Whether to show the settings in the user dropdown.
|
|
||||||
*/
|
|
||||||
settings: boolean;
|
settings: boolean;
|
||||||
/**
|
// Application edit in library (only shown when user is superuser)
|
||||||
* Whether to show the application edit button in the library.
|
|
||||||
*
|
|
||||||
* This is only shown when the user is a superuser.
|
|
||||||
*/
|
|
||||||
applicationEdit: boolean;
|
applicationEdit: boolean;
|
||||||
/**
|
// Search bar
|
||||||
* Whether to show the search bar.
|
|
||||||
*/
|
|
||||||
search: boolean;
|
search: boolean;
|
||||||
};
|
};
|
||||||
navbar: {
|
navbar: {
|
||||||
@ -50,77 +38,68 @@ export interface UIConfig {
|
|||||||
cardBackground: string;
|
cardBackground: string;
|
||||||
};
|
};
|
||||||
pagination: {
|
pagination: {
|
||||||
/**
|
|
||||||
* Number of items to show per page in paginated lists.
|
|
||||||
*/
|
|
||||||
perPage: number;
|
perPage: number;
|
||||||
};
|
};
|
||||||
layout: {
|
layout: {
|
||||||
/**
|
|
||||||
* Layout type to use for the application.
|
|
||||||
*/
|
|
||||||
type: LayoutType;
|
type: LayoutType;
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* Locale to use for the application.
|
|
||||||
*/
|
|
||||||
locale: string;
|
locale: string;
|
||||||
/**
|
|
||||||
* Default values.
|
|
||||||
*/
|
|
||||||
defaults: {
|
defaults: {
|
||||||
/**
|
|
||||||
* Default path to use for user API calls.
|
|
||||||
*/
|
|
||||||
userPath: string;
|
userPath: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createUIConfig(overrides: Partial<UIConfig> = {}): UIConfig {
|
export class DefaultUIConfig implements UIConfig {
|
||||||
const uiConfig: UIConfig = {
|
enabledFeatures = {
|
||||||
enabledFeatures: {
|
apiDrawer: true,
|
||||||
// TODO: Is the intent that only user routes should have the API drawer disabled,
|
notificationDrawer: true,
|
||||||
// or only admin routes?
|
settings: true,
|
||||||
apiDrawer: !isUserRoute(),
|
applicationEdit: true,
|
||||||
notificationDrawer: true,
|
search: true,
|
||||||
settings: true,
|
};
|
||||||
applicationEdit: true,
|
layout = {
|
||||||
search: true,
|
type: LayoutType.row,
|
||||||
},
|
};
|
||||||
layout: {
|
navbar = {
|
||||||
type: LayoutType.row,
|
userDisplay: UserDisplay.username,
|
||||||
},
|
};
|
||||||
navbar: {
|
theme = {
|
||||||
userDisplay: UserDisplay.username,
|
base: UiThemeEnum.Automatic,
|
||||||
},
|
background: "",
|
||||||
theme: {
|
cardBackground: "",
|
||||||
base: UiThemeEnum.Automatic,
|
};
|
||||||
background: "",
|
pagination = {
|
||||||
cardBackground: "",
|
perPage: 20,
|
||||||
},
|
};
|
||||||
pagination: {
|
locale = "";
|
||||||
perPage: 20,
|
defaults = {
|
||||||
},
|
userPath: "users",
|
||||||
locale: "",
|
|
||||||
defaults: {
|
|
||||||
userPath: "users",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Should we deep merge the overrides instead of shallow?
|
constructor() {
|
||||||
Object.assign(uiConfig, overrides);
|
if (currentInterface() === "user") {
|
||||||
|
this.enabledFeatures.apiDrawer = false;
|
||||||
return uiConfig;
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cachedUIConfig: UIConfig | null = null;
|
let globalUiConfig: Promise<UIConfig>;
|
||||||
|
|
||||||
|
export function getConfigForUser(user: UserSelf): UIConfig {
|
||||||
|
const settings = user.settings;
|
||||||
|
let config = new DefaultUIConfig();
|
||||||
|
if (!settings) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
config = Object.assign(new DefaultUIConfig(), settings);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
export function uiConfig(): Promise<UIConfig> {
|
export function uiConfig(): Promise<UIConfig> {
|
||||||
if (cachedUIConfig) return Promise.resolve(cachedUIConfig);
|
if (!globalUiConfig) {
|
||||||
|
globalUiConfig = me().then((user) => {
|
||||||
return me().then((session) => {
|
return getConfigForUser(user.user);
|
||||||
cachedUIConfig = createUIConfig(session.user.settings);
|
});
|
||||||
|
}
|
||||||
return cachedUIConfig;
|
return globalUiConfig;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,35 @@ import { SentryIgnoredError } from "@goauthentik/common/errors";
|
|||||||
|
|
||||||
import { CSSResult, css } from "lit";
|
import { CSSResult, css } from "lit";
|
||||||
|
|
||||||
|
export function getCookie(name: string): string {
|
||||||
|
let cookieValue = "";
|
||||||
|
if (document.cookie && document.cookie !== "") {
|
||||||
|
const cookies = document.cookie.split(";");
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const cookie = cookies[i].trim();
|
||||||
|
// Does this cookie string begin with the name we want?
|
||||||
|
if (cookie.substring(0, name.length + 1) === name + "=") {
|
||||||
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertToSlug(text: string): string {
|
||||||
|
return text
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ /g, "-")
|
||||||
|
.replace(/[^\w-]+/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSlug(text: string): boolean {
|
||||||
|
const lowered = text.toLowerCase();
|
||||||
|
const forbidden = /([^\w-]|\s)/.test(lowered);
|
||||||
|
return lowered === text && !forbidden;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Truncate a string based on maximum word count
|
* Truncate a string based on maximum word count
|
||||||
*/
|
*/
|
||||||
@ -34,29 +63,17 @@ export function snakeToCamel(key: string) {
|
|||||||
|
|
||||||
export function groupBy<T>(objects: T[], callback: (obj: T) => string): Array<[string, T[]]> {
|
export function groupBy<T>(objects: T[], callback: (obj: T) => string): Array<[string, T[]]> {
|
||||||
const m = new Map<string, T[]>();
|
const m = new Map<string, T[]>();
|
||||||
|
|
||||||
objects.forEach((obj) => {
|
objects.forEach((obj) => {
|
||||||
const group = callback(obj);
|
const group = callback(obj);
|
||||||
if (!m.has(group)) {
|
if (!m.has(group)) {
|
||||||
m.set(group, []);
|
m.set(group, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tProviders = m.get(group) || [];
|
const tProviders = m.get(group) || [];
|
||||||
tProviders.push(obj);
|
tProviders.push(obj);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(m).sort();
|
return Array.from(m).sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the first non-null and non-undefined argument.
|
|
||||||
*
|
|
||||||
* @deprecated Use nullish coalescing operator (??) instead.
|
|
||||||
* @remarks
|
|
||||||
*
|
|
||||||
* This needs a deeper look. Some instances of this function use `new Date()`
|
|
||||||
* which may cause issues during rendering.
|
|
||||||
*/
|
|
||||||
export function first<T>(...args: Array<T | undefined | null>): T {
|
export function first<T>(...args: Array<T | undefined | null>): T {
|
||||||
for (let index = 0; index < args.length; index++) {
|
for (let index = 0; index < args.length; index++) {
|
||||||
const element = args[index];
|
const element = args[index];
|
||||||
@ -140,26 +157,23 @@ export function adaptCSS(sheet: AdaptableStylesheet | AdaptableStylesheet[]): Ad
|
|||||||
return Array.isArray(sheet) ? sheet.map(_adaptCSS) : _adaptCSS(sheet);
|
return Array.isArray(sheet) ? sheet.map(_adaptCSS) : _adaptCSS(sheet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _timeUnits = new Map<Intl.RelativeTimeFormatUnit, number>([
|
||||||
|
["year", 24 * 60 * 60 * 1000 * 365],
|
||||||
|
["month", (24 * 60 * 60 * 1000 * 365) / 12],
|
||||||
|
["day", 24 * 60 * 60 * 1000],
|
||||||
|
["hour", 60 * 60 * 1000],
|
||||||
|
["minute", 60 * 1000],
|
||||||
|
["second", 1000],
|
||||||
|
]);
|
||||||
|
|
||||||
export function getRelativeTime(d1: Date, d2: Date = new Date()): string {
|
export function getRelativeTime(d1: Date, d2: Date = new Date()): string {
|
||||||
const elapsed = d1.getTime() - d2.getTime();
|
|
||||||
const rtf = new Intl.RelativeTimeFormat("default", { numeric: "auto" });
|
const rtf = new Intl.RelativeTimeFormat("default", { numeric: "auto" });
|
||||||
|
const elapsed = d1.getTime() - d2.getTime();
|
||||||
|
|
||||||
const _timeUnits: [Intl.RelativeTimeFormatUnit, number][] = [
|
// "Math.abs" accounts for both "past" & "future" scenarios
|
||||||
["year", 1000 * 60 * 60 * 24 * 365],
|
|
||||||
["month", (24 * 60 * 60 * 1000 * 365) / 12],
|
|
||||||
["day", 1000 * 60 * 60 * 24],
|
|
||||||
["hour", 1000 * 60 * 60],
|
|
||||||
["minute", 1000 * 60],
|
|
||||||
["second", 1000],
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const [key, value] of _timeUnits) {
|
for (const [key, value] of _timeUnits) {
|
||||||
if (Math.abs(elapsed) > value || key === "second") {
|
if (Math.abs(elapsed) > value || key == "second") {
|
||||||
let rounded = Math.round(elapsed / value);
|
return rtf.format(Math.round(elapsed / value), key);
|
||||||
if (!isFinite(rounded)) {
|
|
||||||
rounded = 0;
|
|
||||||
}
|
|
||||||
return rtf.format(rounded, key);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rtf.format(Math.round(elapsed / 1000), "second");
|
return rtf.format(Math.round(elapsed / 1000), "second");
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { formatAsSlug } from "@goauthentik/elements/router";
|
import { convertToSlug } from "@goauthentik/common/utils";
|
||||||
|
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators.js";
|
import { customElement, property, query } from "lit/decorators.js";
|
||||||
@ -34,7 +34,7 @@ export class AkSlugInput extends HorizontalLightComponent<string> {
|
|||||||
// Do not stop propagation of this event; it must be sent up the tree so that a parent
|
// Do not stop propagation of this event; it must be sent up the tree so that a parent
|
||||||
// component, such as a custom forms manager, may receive it.
|
// component, such as a custom forms manager, may receive it.
|
||||||
handleTouch(ev: Event) {
|
handleTouch(ev: Event) {
|
||||||
this.input.value = formatAsSlug(this.input.value);
|
this.input.value = convertToSlug(this.input.value);
|
||||||
this.value = this.input.value;
|
this.value = this.input.value;
|
||||||
|
|
||||||
if (this.origin && this.origin.value === "" && this.input.value === "") {
|
if (this.origin && this.origin.value === "" && this.input.value === "") {
|
||||||
@ -67,7 +67,7 @@ export class AkSlugInput extends HorizontalLightComponent<string> {
|
|||||||
// "any event which adds or removes a character but leaves the rest of the slug looking like
|
// "any event which adds or removes a character but leaves the rest of the slug looking like
|
||||||
// the previous iteration, set it to the current iteration."
|
// the previous iteration, set it to the current iteration."
|
||||||
|
|
||||||
const newSlug = formatAsSlug(ev.target.value);
|
const newSlug = convertToSlug(ev.target.value);
|
||||||
const oldSlug = this.input.value;
|
const oldSlug = this.input.value;
|
||||||
const [shorter, longer] =
|
const [shorter, longer] =
|
||||||
newSlug.length < oldSlug.length ? [newSlug, oldSlug] : [oldSlug, newSlug];
|
newSlug.length < oldSlug.length ? [newSlug, oldSlug] : [oldSlug, newSlug];
|
||||||
|
|||||||
@ -1,169 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file
|
|
||||||
* Client-side observer for ESBuild events.
|
|
||||||
*/
|
|
||||||
import type { Message as ESBuildMessage } from "esbuild";
|
|
||||||
|
|
||||||
const logPrefix = "👷 [ESBuild]";
|
|
||||||
const log = console.debug.bind(console, logPrefix);
|
|
||||||
|
|
||||||
type BuildEventListener<Data = unknown> = (event: MessageEvent<Data>) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A client-side watcher for ESBuild.
|
|
||||||
*
|
|
||||||
* Note that this should be conditionally imported in your code, so that
|
|
||||||
* ESBuild may tree-shake it out of production builds.
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) {
|
|
||||||
* const { ESBuildObserver } = await import("@goauthentik/common/development/build-observer");
|
|
||||||
*
|
|
||||||
* new ESBuildObserver(process.env.WATCHER_URL);
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export class ESBuildObserver extends EventSource {
|
|
||||||
/**
|
|
||||||
* Whether the watcher has a recent connection to the server.
|
|
||||||
*/
|
|
||||||
alive = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of errors that have occurred since the watcher started.
|
|
||||||
*/
|
|
||||||
errorCount = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether a reload has been requested while offline.
|
|
||||||
*/
|
|
||||||
deferredReload = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The last time a message was received from the server.
|
|
||||||
*/
|
|
||||||
lastUpdatedAt = Date.now();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the browser considers itself online.
|
|
||||||
*/
|
|
||||||
online = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ID of the animation frame for the reload.
|
|
||||||
*/
|
|
||||||
#reloadFrameID = -1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The interval for the keep-alive check.
|
|
||||||
*/
|
|
||||||
#keepAliveInterval: ReturnType<typeof setInterval> | undefined;
|
|
||||||
|
|
||||||
#trackActivity = () => {
|
|
||||||
this.lastUpdatedAt = Date.now();
|
|
||||||
this.alive = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
#startListener: BuildEventListener = () => {
|
|
||||||
this.#trackActivity();
|
|
||||||
log("⏰ Build started...");
|
|
||||||
};
|
|
||||||
|
|
||||||
#internalErrorListener = () => {
|
|
||||||
this.errorCount += 1;
|
|
||||||
|
|
||||||
if (this.errorCount > 100) {
|
|
||||||
clearTimeout(this.#keepAliveInterval);
|
|
||||||
|
|
||||||
this.close();
|
|
||||||
log("⛔️ Closing connection");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#errorListener: BuildEventListener<string> = (event) => {
|
|
||||||
this.#trackActivity();
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.group(logPrefix, "⛔️⛔️⛔️ Build error...");
|
|
||||||
|
|
||||||
const esbuildErrorMessages: ESBuildMessage[] = JSON.parse(event.data);
|
|
||||||
|
|
||||||
for (const error of esbuildErrorMessages) {
|
|
||||||
console.warn(error.text);
|
|
||||||
|
|
||||||
if (error.location) {
|
|
||||||
console.debug(
|
|
||||||
`file://${error.location.file}:${error.location.line}:${error.location.column}`,
|
|
||||||
);
|
|
||||||
console.debug(error.location.lineText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.groupEnd();
|
|
||||||
};
|
|
||||||
|
|
||||||
#endListener: BuildEventListener = () => {
|
|
||||||
cancelAnimationFrame(this.#reloadFrameID);
|
|
||||||
|
|
||||||
this.#trackActivity();
|
|
||||||
|
|
||||||
if (!this.online) {
|
|
||||||
log("🚫 Build finished while offline.");
|
|
||||||
this.deferredReload = true;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log("🛎️ Build completed! Reloading...");
|
|
||||||
|
|
||||||
// We use an animation frame to keep the reload from happening before the
|
|
||||||
// event loop has a chance to process the message.
|
|
||||||
this.#reloadFrameID = requestAnimationFrame(() => {
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
#keepAliveListener: BuildEventListener = () => {
|
|
||||||
this.#trackActivity();
|
|
||||||
log("🏓 Keep-alive");
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(url: string | URL) {
|
|
||||||
super(url);
|
|
||||||
|
|
||||||
this.addEventListener("esbuild:start", this.#startListener);
|
|
||||||
this.addEventListener("esbuild:end", this.#endListener);
|
|
||||||
this.addEventListener("esbuild:error", this.#errorListener);
|
|
||||||
this.addEventListener("esbuild:keep-alive", this.#keepAliveListener);
|
|
||||||
|
|
||||||
this.addEventListener("error", this.#internalErrorListener);
|
|
||||||
|
|
||||||
window.addEventListener("offline", () => {
|
|
||||||
this.online = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("online", () => {
|
|
||||||
this.online = true;
|
|
||||||
|
|
||||||
if (!this.deferredReload) return;
|
|
||||||
|
|
||||||
log("🛎️ Reloading after offline build...");
|
|
||||||
this.deferredReload = false;
|
|
||||||
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
|
|
||||||
log("🛎️ Listening for build changes...");
|
|
||||||
|
|
||||||
this.#keepAliveInterval = setInterval(() => {
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
if (now - this.lastUpdatedAt < 10_000) return;
|
|
||||||
|
|
||||||
this.alive = false;
|
|
||||||
log("👋 Waiting for build to start...");
|
|
||||||
}, 15_000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +1,10 @@
|
|||||||
import { formatPageTitle } from "@goauthentik/common/client";
|
import {
|
||||||
import { EVENT_SIDEBAR_TOGGLE, EVENT_WS_MESSAGE } from "@goauthentik/common/constants";
|
EVENT_SIDEBAR_TOGGLE,
|
||||||
|
EVENT_WS_MESSAGE,
|
||||||
|
TITLE_DEFAULT,
|
||||||
|
} from "@goauthentik/common/constants";
|
||||||
import { globalAK } from "@goauthentik/common/global";
|
import { globalAK } from "@goauthentik/common/global";
|
||||||
|
import { currentInterface } from "@goauthentik/common/sentry";
|
||||||
import { UIConfig, UserDisplay, uiConfig } from "@goauthentik/common/ui/config";
|
import { UIConfig, UserDisplay, uiConfig } from "@goauthentik/common/ui/config";
|
||||||
import { me } from "@goauthentik/common/users";
|
import { me } from "@goauthentik/common/users";
|
||||||
import "@goauthentik/components/ak-nav-buttons";
|
import "@goauthentik/components/ak-nav-buttons";
|
||||||
@ -121,8 +125,17 @@ export class PageHeader extends WithBrandConfig(AKElement) {
|
|||||||
this.uiConfig.navbar.userDisplay = UserDisplay.none;
|
this.uiConfig.navbar.userDisplay = UserDisplay.none;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTitle(pageTitle?: string) {
|
setTitle(header?: string) {
|
||||||
document.title = formatPageTitle(this.brand, pageTitle);
|
const currentIf = currentInterface();
|
||||||
|
let title = this.brand?.brandingTitle || TITLE_DEFAULT;
|
||||||
|
if (currentIf === "admin") {
|
||||||
|
title = `${msg("Admin")} - ${title}`;
|
||||||
|
}
|
||||||
|
// Prepend the header to the title
|
||||||
|
if (header !== undefined && header !== "") {
|
||||||
|
title = `${header} - ${title}`;
|
||||||
|
}
|
||||||
|
document.title = title;
|
||||||
}
|
}
|
||||||
|
|
||||||
willUpdate() {
|
willUpdate() {
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { CURRENT_CLASS, EVENT_REFRESH } from "@goauthentik/common/constants";
|
import { CURRENT_CLASS, EVENT_REFRESH, ROUTE_SEPARATOR } from "@goauthentik/common/constants";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { ROUTE_SEPARATOR } from "@goauthentik/elements/router";
|
import { getURLParams, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
|
||||||
import { getRouteParams, patchRouteParams } from "@goauthentik/elements/router/utils";
|
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||||
@ -11,8 +10,6 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
|||||||
import PFTabs from "@patternfly/patternfly/components/Tabs/tabs.css";
|
import PFTabs from "@patternfly/patternfly/components/Tabs/tabs.css";
|
||||||
import PFGlobal from "@patternfly/patternfly/patternfly-base.css";
|
import PFGlobal from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
const SLOT_PREFIX = "page-";
|
|
||||||
|
|
||||||
@customElement("ak-tabs")
|
@customElement("ak-tabs")
|
||||||
export class Tabs extends AKElement {
|
export class Tabs extends AKElement {
|
||||||
@property()
|
@property()
|
||||||
@ -21,14 +18,6 @@ export class Tabs extends AKElement {
|
|||||||
@property()
|
@property()
|
||||||
currentPage?: string;
|
currentPage?: string;
|
||||||
|
|
||||||
get currentPageParamName(): string | null {
|
|
||||||
if (!this.currentPage) return null;
|
|
||||||
|
|
||||||
return this.currentPage.startsWith(SLOT_PREFIX)
|
|
||||||
? this.currentPage.slice(SLOT_PREFIX.length)
|
|
||||||
: this.currentPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
vertical = false;
|
vertical = false;
|
||||||
|
|
||||||
@ -79,30 +68,13 @@ export class Tabs extends AKElement {
|
|||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
onClick(slot?: string): void {
|
||||||
* Sync route params with the current page.
|
this.currentPage = slot;
|
||||||
*
|
const params: { [key: string]: string | undefined } = {};
|
||||||
* @todo This should be moved to a router component.
|
params[this.pageIdentifier] = slot;
|
||||||
*/
|
updateURLParams(params);
|
||||||
#syncRouteParams(): void {
|
|
||||||
const { currentPageParamName } = this;
|
|
||||||
|
|
||||||
if (!currentPageParamName) return;
|
|
||||||
|
|
||||||
patchRouteParams({
|
|
||||||
[this.pageIdentifier]: currentPageParamName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
activatePage(nextPage?: string): void {
|
|
||||||
this.currentPage = nextPage;
|
|
||||||
|
|
||||||
this.#syncRouteParams();
|
|
||||||
|
|
||||||
const page = this.querySelector(`[slot='${this.currentPage}']`);
|
const page = this.querySelector(`[slot='${this.currentPage}']`);
|
||||||
|
|
||||||
if (!page) return;
|
if (!page) return;
|
||||||
|
|
||||||
page.dispatchEvent(new CustomEvent(EVENT_REFRESH));
|
page.dispatchEvent(new CustomEvent(EVENT_REFRESH));
|
||||||
page.dispatchEvent(new CustomEvent("activate"));
|
page.dispatchEvent(new CustomEvent("activate"));
|
||||||
}
|
}
|
||||||
@ -110,7 +82,7 @@ export class Tabs extends AKElement {
|
|||||||
renderTab(page: Element): TemplateResult {
|
renderTab(page: Element): TemplateResult {
|
||||||
const slot = page.attributes.getNamedItem("slot")?.value;
|
const slot = page.attributes.getNamedItem("slot")?.value;
|
||||||
return html` <li class="pf-c-tabs__item ${slot === this.currentPage ? CURRENT_CLASS : ""}">
|
return html` <li class="pf-c-tabs__item ${slot === this.currentPage ? CURRENT_CLASS : ""}">
|
||||||
<button class="pf-c-tabs__link" @click=${() => this.activatePage(slot)}>
|
<button class="pf-c-tabs__link" @click=${() => this.onClick(slot)}>
|
||||||
<span class="pf-c-tabs__item-text"> ${page.getAttribute("data-tab-title")} </span>
|
<span class="pf-c-tabs__item-text"> ${page.getAttribute("data-tab-title")} </span>
|
||||||
</button>
|
</button>
|
||||||
</li>`;
|
</li>`;
|
||||||
@ -118,41 +90,24 @@ export class Tabs extends AKElement {
|
|||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
const pages = Array.from(this.querySelectorAll(":scope > [slot^='page-']"));
|
const pages = Array.from(this.querySelectorAll(":scope > [slot^='page-']"));
|
||||||
|
|
||||||
if (window.location.hash.includes(ROUTE_SEPARATOR)) {
|
if (window.location.hash.includes(ROUTE_SEPARATOR)) {
|
||||||
const params = getRouteParams();
|
const params = getURLParams();
|
||||||
|
|
||||||
const slotName = params[this.pageIdentifier];
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
slotName &&
|
this.pageIdentifier in params &&
|
||||||
typeof slotName === "string" &&
|
|
||||||
!this.currentPage &&
|
!this.currentPage &&
|
||||||
this.querySelector(`[slot='${slotName}']`) !== null
|
this.querySelector(`[slot='${params[this.pageIdentifier]}']`) !== null
|
||||||
) {
|
) {
|
||||||
console.debug(
|
// To update the URL to match with the current slot
|
||||||
`authentik/tabs (${this.pageIdentifier}): setting current page to`,
|
this.onClick(params[this.pageIdentifier] as string);
|
||||||
slotName,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.activatePage(slotName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.currentPage) {
|
if (!this.currentPage) {
|
||||||
if (pages.length < 1) {
|
if (pages.length < 1) {
|
||||||
return html`<h1>${msg("no tabs defined")}</h1>`;
|
return html`<h1>${msg("no tabs defined")}</h1>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wantedPage = pages[0].attributes.getNamedItem("slot")?.value;
|
const wantedPage = pages[0].attributes.getNamedItem("slot")?.value;
|
||||||
|
this.onClick(wantedPage);
|
||||||
console.debug(
|
|
||||||
`authentik/tabs (${this.pageIdentifier}): setting current page to`,
|
|
||||||
wantedPage,
|
|
||||||
);
|
|
||||||
this.activatePage(wantedPage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`<div class="pf-c-tabs ${this.vertical ? "pf-m-vertical pf-m-box" : ""}">
|
return html`<div class="pf-c-tabs ${this.vertical ? "pf-m-vertical pf-m-box" : ""}">
|
||||||
<ul class="pf-c-tabs__list">
|
<ul class="pf-c-tabs__list">
|
||||||
${pages.map((page) => this.renderTab(page))}
|
${pages.map((page) => this.renderTab(page))}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { setRouteParams } from "@goauthentik/elements/router/utils";
|
import { setURLParams } from "@goauthentik/elements/router/RouteMatch";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { CSSResult, TemplateResult, html } from "lit";
|
import { CSSResult, TemplateResult, html } from "lit";
|
||||||
@ -84,7 +84,7 @@ export class TreeViewNode extends AKElement {
|
|||||||
if (this.host) {
|
if (this.host) {
|
||||||
this.host.activeNode = this;
|
this.host.activeNode = this;
|
||||||
}
|
}
|
||||||
setRouteParams({ path: this.fullPath });
|
setURLParams({ path: this.fullPath });
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent(EVENT_REFRESH, {
|
new CustomEvent(EVENT_REFRESH, {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||||
import { parseAPIError } from "@goauthentik/common/errors";
|
import { parseAPIError } from "@goauthentik/common/errors";
|
||||||
import { MessageLevel } from "@goauthentik/common/messages";
|
import { MessageLevel } from "@goauthentik/common/messages";
|
||||||
import { dateToUTC } from "@goauthentik/common/utils";
|
import { camelToSnake, convertToSlug, dateToUTC } from "@goauthentik/common/utils";
|
||||||
import { camelToSnake } from "@goauthentik/common/utils";
|
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
|
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import { PreventFormSubmit } from "@goauthentik/elements/forms/helpers";
|
import { PreventFormSubmit } from "@goauthentik/elements/forms/helpers";
|
||||||
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
||||||
import { formatAsSlug } from "@goauthentik/elements/router/slugs";
|
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||||
@ -225,11 +223,11 @@ export abstract class Form<T> extends AKElement {
|
|||||||
// Only attach handler if the slug is already equal to the name
|
// Only attach handler if the slug is already equal to the name
|
||||||
// if not, they are probably completely different and shouldn't update
|
// if not, they are probably completely different and shouldn't update
|
||||||
// each other
|
// each other
|
||||||
if (formatAsSlug(input.value) !== slugField.value) {
|
if (convertToSlug(input.value) !== slugField.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
nameInput.addEventListener("input", () => {
|
nameInput.addEventListener("input", () => {
|
||||||
slugField.value = formatAsSlug(input.value);
|
slugField.value = convertToSlug(input.value);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
import { convertToSlug } from "@goauthentik/common/utils";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { FormGroup } from "@goauthentik/elements/forms/FormGroup";
|
import { FormGroup } from "@goauthentik/elements/forms/FormGroup";
|
||||||
import { formatAsSlug } from "@goauthentik/elements/router";
|
|
||||||
|
|
||||||
import { msg, str } from "@lit/localize";
|
import { msg, str } from "@lit/localize";
|
||||||
import { CSSResult, css } from "lit";
|
import { CSSResult, css } from "lit";
|
||||||
@ -123,7 +123,7 @@ export class HorizontalFormElement extends AKElement {
|
|||||||
if (this.name === "slug" || this.slugMode) {
|
if (this.name === "slug" || this.slugMode) {
|
||||||
this.querySelectorAll<HTMLInputElement>("input[type='text']").forEach((input) => {
|
this.querySelectorAll<HTMLInputElement>("input[type='text']").forEach((input) => {
|
||||||
input.addEventListener("keyup", () => {
|
input.addEventListener("keyup", () => {
|
||||||
input.value = formatAsSlug(input.value);
|
input.value = convertToSlug(input.value);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,60 +1,66 @@
|
|||||||
import "@goauthentik/elements/EmptyState";
|
import "@goauthentik/elements/EmptyState";
|
||||||
import { SlottedTemplateResult } from "@goauthentik/elements/types";
|
|
||||||
|
|
||||||
import { TemplateResult, html, nothing } from "lit";
|
import { TemplateResult, html } from "lit";
|
||||||
import { until } from "lit/directives/until.js";
|
import { until } from "lit/directives/until.js";
|
||||||
|
|
||||||
export type PrimitiveRouteParameter = string | number | boolean | null | undefined;
|
export const SLUG_REGEX = "[-a-zA-Z0-9_]+";
|
||||||
export type RouteParameterRecord = { [key: string]: PrimitiveRouteParameter };
|
export const ID_REGEX = "\\d+";
|
||||||
|
export const UUID_REGEX = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
||||||
|
|
||||||
export type RouteCallback<P = unknown> = (
|
export interface RouteArgs {
|
||||||
params: P,
|
[key: string]: string;
|
||||||
) => SlottedTemplateResult | Promise<SlottedTemplateResult>;
|
}
|
||||||
|
|
||||||
export type RouteInitTuple = [string | RegExp, RouteCallback | undefined];
|
export class Route {
|
||||||
|
url: RegExp;
|
||||||
|
|
||||||
export class Route<P = unknown> {
|
private element?: TemplateResult;
|
||||||
public readonly pattern: URLPattern;
|
private callback?: (args: RouteArgs) => Promise<TemplateResult>;
|
||||||
|
|
||||||
#callback: RouteCallback<P>;
|
constructor(url: RegExp, callback?: (args: RouteArgs) => Promise<TemplateResult>) {
|
||||||
|
this.url = url;
|
||||||
constructor(patternInit: URLPatternInit | string, callback: RouteCallback<P>) {
|
this.callback = callback;
|
||||||
this.pattern = new URLPattern(
|
|
||||||
typeof patternInit === "string"
|
|
||||||
? {
|
|
||||||
pathname: patternInit,
|
|
||||||
}
|
|
||||||
: patternInit,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.#callback = callback;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
redirect(to: string, raw = false): Route {
|
||||||
* Create a new redirect route.
|
this.callback = async () => {
|
||||||
*
|
|
||||||
* @param patternInit The pattern to match.
|
|
||||||
* @param to The URL to redirect to.
|
|
||||||
* @param raw Whether to use the raw URL or not.
|
|
||||||
*/
|
|
||||||
static redirect(patternInit: URLPatternInit | string, to: string, raw = false): Route<unknown> {
|
|
||||||
return new Route(patternInit, () => {
|
|
||||||
console.debug(`authentik/router: redirecting ${to}`);
|
console.debug(`authentik/router: redirecting ${to}`);
|
||||||
|
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
window.location.hash = `#${to}`;
|
window.location.hash = `#${to}`;
|
||||||
} else {
|
} else {
|
||||||
window.location.hash = to;
|
window.location.hash = to;
|
||||||
}
|
}
|
||||||
|
return html``;
|
||||||
return nothing;
|
};
|
||||||
});
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
render(params: P): TemplateResult {
|
then(render: (args: RouteArgs) => TemplateResult): Route {
|
||||||
return html`${until(
|
this.callback = async (args) => {
|
||||||
this.#callback(params),
|
return render(args);
|
||||||
html`<ak-empty-state ?loading=${true}></ak-empty-state>`,
|
};
|
||||||
)}`;
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
thenAsync(render: (args: RouteArgs) => Promise<TemplateResult>): Route {
|
||||||
|
this.callback = render;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(args: RouteArgs): TemplateResult {
|
||||||
|
if (this.callback) {
|
||||||
|
return html`${until(
|
||||||
|
this.callback(args),
|
||||||
|
html`<ak-empty-state ?loading=${true}></ak-empty-state>`,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
if (this.element) {
|
||||||
|
return this.element;
|
||||||
|
}
|
||||||
|
throw new Error("Route does not have callback or element");
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return `<Route url=${this.url} callback=${this.callback ? "true" : "false"}>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
66
web/src/elements/router/RouteMatch.ts
Normal file
66
web/src/elements/router/RouteMatch.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { ROUTE_SEPARATOR } from "@goauthentik/common/constants";
|
||||||
|
import { Route } from "@goauthentik/elements/router/Route";
|
||||||
|
|
||||||
|
import { TemplateResult } from "lit";
|
||||||
|
|
||||||
|
export class RouteMatch {
|
||||||
|
route: Route;
|
||||||
|
arguments: { [key: string]: string };
|
||||||
|
fullUrl?: string;
|
||||||
|
|
||||||
|
constructor(route: Route) {
|
||||||
|
this.route = route;
|
||||||
|
this.arguments = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): TemplateResult {
|
||||||
|
return this.route.render(this.arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return `<RouteMatch url=${this.fullUrl} route=${this.route} arguments=${JSON.stringify(
|
||||||
|
this.arguments,
|
||||||
|
)}>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getURLParam<T>(key: string, fallback: T): T {
|
||||||
|
const params = getURLParams();
|
||||||
|
if (key in params) {
|
||||||
|
return params[key] as T;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getURLParams(): { [key: string]: unknown } {
|
||||||
|
const params = {};
|
||||||
|
if (window.location.hash.includes(ROUTE_SEPARATOR)) {
|
||||||
|
const urlParts = window.location.hash.slice(1, Infinity).split(ROUTE_SEPARATOR, 2);
|
||||||
|
const rawParams = decodeURIComponent(urlParts[1]);
|
||||||
|
try {
|
||||||
|
return JSON.parse(rawParams);
|
||||||
|
} catch {
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setURLParams(params: { [key: string]: unknown }, replace = true): void {
|
||||||
|
const paramsString = JSON.stringify(params);
|
||||||
|
const currentUrl = window.location.hash.slice(1, Infinity).split(ROUTE_SEPARATOR)[0];
|
||||||
|
const newUrl = `#${currentUrl};${encodeURIComponent(paramsString)}`;
|
||||||
|
if (replace) {
|
||||||
|
history.replaceState(undefined, "", newUrl);
|
||||||
|
} else {
|
||||||
|
history.pushState(undefined, "", newUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateURLParams(params: { [key: string]: unknown }, replace = true): void {
|
||||||
|
const currentParams = getURLParams();
|
||||||
|
for (const key in params) {
|
||||||
|
currentParams[key] = params[key] as string;
|
||||||
|
}
|
||||||
|
setURLParams(currentParams, replace);
|
||||||
|
}
|
||||||
@ -11,7 +11,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
|||||||
@customElement("ak-router-404")
|
@customElement("ak-router-404")
|
||||||
export class Router404 extends AKElement {
|
export class Router404 extends AKElement {
|
||||||
@property()
|
@property()
|
||||||
pathname = "";
|
url = "";
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFEmptyState, PFTitle];
|
return [PFBase, PFEmptyState, PFTitle];
|
||||||
@ -23,7 +23,7 @@ export class Router404 extends AKElement {
|
|||||||
<i class="fas fa-question-circle pf-c-empty-state__icon" aria-hidden="true"></i>
|
<i class="fas fa-question-circle pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||||
<h1 class="pf-c-title pf-m-lg">${msg("Not found")}</h1>
|
<h1 class="pf-c-title pf-m-lg">${msg("Not found")}</h1>
|
||||||
<div class="pf-c-empty-state__body">
|
<div class="pf-c-empty-state__body">
|
||||||
${msg(str`The URL "${this.pathname}" was not found.`)}
|
${msg(str`The URL "${this.url}" was not found.`)}
|
||||||
</div>
|
</div>
|
||||||
<a href="#/" class="pf-c-button pf-m-primary" type="button"
|
<a href="#/" class="pf-c-button pf-m-primary" type="button"
|
||||||
>${msg("Return home")}</a
|
>${msg("Return home")}</a
|
||||||
|
|||||||
@ -1,47 +1,57 @@
|
|||||||
|
import { ROUTE_SEPARATOR } from "@goauthentik/common/constants";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { Route } from "@goauthentik/elements/router/Route";
|
import { Route } from "@goauthentik/elements/router/Route";
|
||||||
|
import { RouteMatch } from "@goauthentik/elements/router/RouteMatch";
|
||||||
import "@goauthentik/elements/router/Router404";
|
import "@goauthentik/elements/router/Router404";
|
||||||
import { matchRoute, pluckRoute } from "@goauthentik/elements/router/utils";
|
|
||||||
|
|
||||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
// Poliyfill for hashchange.newURL,
|
// Poliyfill for hashchange.newURL,
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onhashchange
|
// https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onhashchange
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
if (window.HashChangeEvent) return;
|
if (!window.HashChangeEvent)
|
||||||
|
(function () {
|
||||||
console.debug("authentik/router: polyfilling hashchange event");
|
let lastURL = document.URL;
|
||||||
|
window.addEventListener("hashchange", function (event) {
|
||||||
let lastURL = document.URL;
|
Object.defineProperty(event, "oldURL", {
|
||||||
|
enumerable: true,
|
||||||
window.addEventListener("hashchange", function (event) {
|
configurable: true,
|
||||||
Object.defineProperty(event, "oldURL", {
|
value: lastURL,
|
||||||
enumerable: true,
|
});
|
||||||
configurable: true,
|
Object.defineProperty(event, "newURL", {
|
||||||
value: lastURL,
|
enumerable: true,
|
||||||
});
|
configurable: true,
|
||||||
|
value: document.URL,
|
||||||
Object.defineProperty(event, "newURL", {
|
});
|
||||||
enumerable: true,
|
lastURL = document.URL;
|
||||||
configurable: true,
|
});
|
||||||
value: document.URL,
|
})();
|
||||||
});
|
|
||||||
|
|
||||||
lastURL = document.URL;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function paramURL(url: string, params?: { [key: string]: unknown }): string {
|
||||||
|
let finalUrl = "#";
|
||||||
|
finalUrl += url;
|
||||||
|
if (params) {
|
||||||
|
finalUrl += ";";
|
||||||
|
finalUrl += encodeURIComponent(JSON.stringify(params));
|
||||||
|
}
|
||||||
|
return finalUrl;
|
||||||
|
}
|
||||||
|
export function navigate(url: string, params?: { [key: string]: unknown }): void {
|
||||||
|
window.location.assign(paramURL(url, params));
|
||||||
|
}
|
||||||
|
|
||||||
@customElement("ak-router-outlet")
|
@customElement("ak-router-outlet")
|
||||||
export class RouterOutlet extends AKElement {
|
export class RouterOutlet extends AKElement {
|
||||||
@state()
|
@property({ attribute: false })
|
||||||
private currentPathname: string | null = null;
|
current?: RouteMatch;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
public defaultURL?: string;
|
defaultUrl?: string;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public routes: Route[] = [];
|
routes: Route[] = [];
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [
|
return [
|
||||||
@ -49,7 +59,6 @@ export class RouterOutlet extends AKElement {
|
|||||||
:host {
|
:host {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
*:first-child {
|
*:first-child {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@ -57,78 +66,56 @@ export class RouterOutlet extends AKElement {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback(): void {
|
constructor() {
|
||||||
super.connectedCallback();
|
super();
|
||||||
|
window.addEventListener("hashchange", (ev: HashChangeEvent) => this.navigate(ev));
|
||||||
window.addEventListener("hashchange", this.#refreshLocation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback(): void {
|
firstUpdated(): void {
|
||||||
super.disconnectedCallback();
|
this.navigate();
|
||||||
|
|
||||||
window.removeEventListener("hashchange", this.#refreshLocation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated(): void {
|
navigate(ev?: HashChangeEvent): void {
|
||||||
const currentPathname = pluckRoute(window.location).pathname;
|
let activeUrl = window.location.hash.slice(1, Infinity).split(ROUTE_SEPARATOR)[0];
|
||||||
|
if (ev) {
|
||||||
if (currentPathname) return;
|
// Check if we've actually changed paths
|
||||||
|
const oldPath = new URL(ev.oldURL).hash.slice(1, Infinity).split(ROUTE_SEPARATOR)[0];
|
||||||
console.debug("authentik/router: defaulted route to empty pathname");
|
if (oldPath === activeUrl) return;
|
||||||
|
}
|
||||||
this.#redirectToDefault();
|
if (activeUrl === "") {
|
||||||
}
|
activeUrl = this.defaultUrl || "/";
|
||||||
|
window.location.hash = `#${activeUrl}`;
|
||||||
#redirectToDefault(): void {
|
console.debug(`authentik/router: defaulted URL to ${window.location.hash}`);
|
||||||
const nextPathname = this.defaultURL || "/";
|
return;
|
||||||
|
}
|
||||||
window.location.hash = "#" + nextPathname;
|
let matchedRoute: RouteMatch | null = null;
|
||||||
}
|
this.routes.some((route) => {
|
||||||
|
const match = route.url.exec(activeUrl);
|
||||||
#refreshLocation = (event: HashChangeEvent): void => {
|
if (match !== null) {
|
||||||
console.debug("authentik/router: hashchange event", event);
|
matchedRoute = new RouteMatch(route);
|
||||||
const nextPathname = pluckRoute(event.newURL).pathname;
|
matchedRoute.arguments = match.groups || {};
|
||||||
const previousPathname = pluckRoute(event.oldURL).pathname;
|
matchedRoute.fullUrl = activeUrl;
|
||||||
|
console.debug("authentik/router: found match ", matchedRoute);
|
||||||
if (previousPathname === nextPathname) {
|
return true;
|
||||||
console.debug("authentik/router: hashchange event, but no change in path", event, {
|
}
|
||||||
currentPathname: nextPathname,
|
return false;
|
||||||
previousPathname,
|
});
|
||||||
|
if (!matchedRoute) {
|
||||||
|
console.debug(`authentik/router: route "${activeUrl}" not defined`);
|
||||||
|
const route = new Route(RegExp(""), async () => {
|
||||||
|
return html`<div class="pf-c-page__main">
|
||||||
|
<ak-router-404 url=${activeUrl}></ak-router-404>
|
||||||
|
</div>`;
|
||||||
});
|
});
|
||||||
|
matchedRoute = new RouteMatch(route);
|
||||||
return;
|
matchedRoute.arguments = route.url.exec(activeUrl)?.groups || {};
|
||||||
|
matchedRoute.fullUrl = activeUrl;
|
||||||
}
|
}
|
||||||
|
this.current = matchedRoute;
|
||||||
if (!nextPathname) {
|
}
|
||||||
console.debug(`authentik/router: defaulted route to ${nextPathname}`);
|
|
||||||
|
|
||||||
this.#redirectToDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentPathname = nextPathname;
|
|
||||||
};
|
|
||||||
|
|
||||||
render(): TemplateResult | undefined {
|
render(): TemplateResult | undefined {
|
||||||
let currentPathname = this.currentPathname;
|
return this.current?.render();
|
||||||
|
|
||||||
if (!currentPathname) {
|
|
||||||
currentPathname = pluckRoute(window.location).pathname;
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = matchRoute(currentPathname, this.routes);
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
return html`<div class="pf-c-page__main">
|
|
||||||
<ak-router-404 pathname=${currentPathname}></ak-router-404>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug("authentik/router: found match", match);
|
|
||||||
|
|
||||||
const { parameters, route } = match;
|
|
||||||
|
|
||||||
return route.render(parameters);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file Router constants.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Route separator, used to separate the path from the mock query string.
|
|
||||||
*/
|
|
||||||
export const ROUTE_SEPARATOR = "?";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Slug pattern, matching alphanumeric characters, underscores, and hyphens.
|
|
||||||
*/
|
|
||||||
export const SLUG_PATTERN = "[a-zA-Z0-9_\\-]+";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Numeric ID pattern, typically used for database IDs.
|
|
||||||
*/
|
|
||||||
export const ID_PATTERN = "\\d+";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UUID v4 pattern
|
|
||||||
*
|
|
||||||
* @todo Enforcing this format on the front-end may be a bit too strict.
|
|
||||||
* We may want to allow other UUID formats, or move this to a validation step.
|
|
||||||
*/
|
|
||||||
export const UUID_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
export * from "./Route.js";
|
|
||||||
export * from "./constants.js";
|
|
||||||
export * from "./Router404.js";
|
|
||||||
export * from "./RouterOutlet.js";
|
|
||||||
export * from "./utils.js";
|
|
||||||
export * from "./slugs.js";
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
/**
|
|
||||||
* Given a string, return a URL-friendly slug.
|
|
||||||
*/
|
|
||||||
export function formatAsSlug(text: string): string {
|
|
||||||
return text
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/ /g, "-")
|
|
||||||
.replace(/[^\w-]+/g, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard to check if a given string is a valid URL slug, i.e.
|
|
||||||
* only containing alphanumeric characters, dashes, and underscores.
|
|
||||||
*/
|
|
||||||
export function isSlug(input: unknown): input is string {
|
|
||||||
if (typeof input !== "string") return false;
|
|
||||||
if (!input) return false;
|
|
||||||
|
|
||||||
const lowered = input.toLowerCase();
|
|
||||||
if (input !== lowered) return false;
|
|
||||||
|
|
||||||
return /([^\w-]|\s)/.test(lowered);
|
|
||||||
}
|
|
||||||
@ -1,254 +0,0 @@
|
|||||||
import { ROUTE_SEPARATOR } from "@goauthentik/elements/router";
|
|
||||||
import type { Route, RouteParameterRecord } from "@goauthentik/elements/router/Route";
|
|
||||||
|
|
||||||
export interface RouteMatch<P extends RouteParameterRecord = RouteParameterRecord> {
|
|
||||||
readonly route: Route<P>;
|
|
||||||
readonly parameters: P;
|
|
||||||
readonly pathname: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Match a route against a pathname.
|
|
||||||
*/
|
|
||||||
export function matchRoute<P extends RouteParameterRecord>(
|
|
||||||
pathname: string,
|
|
||||||
routes: Route<P>[],
|
|
||||||
): RouteMatch<P> | null {
|
|
||||||
if (!pathname) return null;
|
|
||||||
|
|
||||||
for (const route of routes) {
|
|
||||||
const match = route.pattern.exec({ pathname });
|
|
||||||
|
|
||||||
if (!match) continue;
|
|
||||||
|
|
||||||
console.debug(
|
|
||||||
`authentik/router: matched route ${route.pattern} to ${pathname} with params`,
|
|
||||||
match.pathname.groups,
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
route: route as Route<P>,
|
|
||||||
parameters: match.pathname.groups as P,
|
|
||||||
pathname,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug(`authentik/router: no route matched ${pathname}`);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate to a route.
|
|
||||||
*
|
|
||||||
* @param {string} pathname The pathname of the route.
|
|
||||||
* @param {RouteParameterRecord} params The parameters to serialize.
|
|
||||||
*/
|
|
||||||
export function navigate(pathname: string, params?: RouteParameterRecord): void {
|
|
||||||
window.location.assign(formatRouteHash(pathname, params));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a route hash from a pathname and parameters.
|
|
||||||
*
|
|
||||||
* @param {string} pathname The pathname of the route.
|
|
||||||
* @param {RouteParameterRecord} params The parameters to serialize.
|
|
||||||
* @returns {string} The formatted route hash, starting with `#`.
|
|
||||||
* @see {@linkcode navigate} to navigate to a route.
|
|
||||||
*/
|
|
||||||
export function formatRouteHash(pathname: string, params?: RouteParameterRecord): string {
|
|
||||||
const routePrefix = "#" + pathname;
|
|
||||||
|
|
||||||
if (!params) return routePrefix;
|
|
||||||
|
|
||||||
const searchParams = new URLSearchParams();
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(params)) {
|
|
||||||
if (typeof value === "boolean" && value) {
|
|
||||||
searchParams.set(key, "true");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === "undefined" || value === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
for (const item of value) {
|
|
||||||
searchParams.append(key, item.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchParams.set(key, String(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
return [routePrefix, searchParams.toString()].join(ROUTE_SEPARATOR);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a route to an interface by name, optionally with parameters.
|
|
||||||
*/
|
|
||||||
export function formatInterfaceRoute(
|
|
||||||
interfaceName: RouteInterfaceName,
|
|
||||||
pathname?: string,
|
|
||||||
params?: RouteParameterRecord,
|
|
||||||
): string {
|
|
||||||
const prefix = `/if/${interfaceName}/`;
|
|
||||||
|
|
||||||
if (!pathname) return prefix;
|
|
||||||
|
|
||||||
return prefix + formatRouteHash(pathname, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SerializedRoute {
|
|
||||||
pathname: string;
|
|
||||||
serializedParameters?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pluckRoute(source: Pick<URL, "hash"> | string = window.location): SerializedRoute {
|
|
||||||
source = typeof source === "string" ? new URL(source) : source;
|
|
||||||
|
|
||||||
const [pathname, serializedParameters] = source.hash.slice(1).split(ROUTE_SEPARATOR, 2);
|
|
||||||
|
|
||||||
return {
|
|
||||||
pathname,
|
|
||||||
serializedParameters,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a parameter from the current route.
|
|
||||||
*
|
|
||||||
* @template T - The type of the parameter.
|
|
||||||
* @param {string} paramName - The name of the parameter to retrieve.
|
|
||||||
* @param {T} fallback - The fallback value to return if the parameter is not found.
|
|
||||||
*/
|
|
||||||
export function getRouteParameter<T>(paramName: string, fallback: T): T {
|
|
||||||
const params = getRouteParams();
|
|
||||||
|
|
||||||
if (Object.hasOwn(params, paramName)) {
|
|
||||||
return params[paramName] as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the route parameters from the URL.
|
|
||||||
*
|
|
||||||
* @template T - The type of the route parameters.
|
|
||||||
*/
|
|
||||||
export function getRouteParams<T = RouteParameterRecord>(): T {
|
|
||||||
const { serializedParameters } = pluckRoute();
|
|
||||||
|
|
||||||
if (!serializedParameters) return {} as T;
|
|
||||||
|
|
||||||
let searchParams: URLSearchParams;
|
|
||||||
|
|
||||||
try {
|
|
||||||
searchParams = new URLSearchParams(serializedParameters);
|
|
||||||
} catch (_error) {
|
|
||||||
console.warn("Failed to parse URL parameters", serializedParameters);
|
|
||||||
return {} as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
const decodedParameters: Record<string, unknown> = {};
|
|
||||||
for (const [key, value] of searchParams.entries()) {
|
|
||||||
if (value === "true" || value === "") {
|
|
||||||
decodedParameters[key] = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value === "false") {
|
|
||||||
decodedParameters[key] = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
decodedParameters[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return decodedParameters as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the route parameters in the URL.
|
|
||||||
*
|
|
||||||
* @param nextParams - The JSON-serializable parameters to set in the URL.
|
|
||||||
* @param replace - Whether to replace the current history entry or create a new one.
|
|
||||||
*/
|
|
||||||
export function setRouteParams(nextParams: RouteParameterRecord, replace = true): void {
|
|
||||||
const { pathname } = pluckRoute();
|
|
||||||
const nextHash = formatRouteHash(pathname, nextParams);
|
|
||||||
|
|
||||||
if (replace) {
|
|
||||||
history.replaceState(undefined, "", nextHash);
|
|
||||||
} else {
|
|
||||||
history.pushState(undefined, "", nextHash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Patch the route parameters in the URL, retaining existing parameters not specified in the input.
|
|
||||||
*
|
|
||||||
* @param patchedParams - The parameters to patch in the URL.
|
|
||||||
* @param replace - Whether to replace the current history entry or create a new one.
|
|
||||||
*
|
|
||||||
* @todo Most instances of this should be URL search params, not hash params.
|
|
||||||
*/
|
|
||||||
export function patchRouteParams(patchedParams: RouteParameterRecord, replace = true): void {
|
|
||||||
const currentParams = getRouteParams();
|
|
||||||
const nextParams = { ...currentParams, ...patchedParams };
|
|
||||||
|
|
||||||
setRouteParams(nextParams, replace);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard to check if a given input is parsable as a URL.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* isURLInput("https://example.com") // true
|
|
||||||
* isURLInput("invalid-url") // false
|
|
||||||
* isURLInput(new URL("https://example.com")) // true
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function isURLInput(input: unknown): input is string | URL {
|
|
||||||
if (typeof input !== "string" && !(input instanceof URL)) return false;
|
|
||||||
|
|
||||||
if (!input) return false;
|
|
||||||
|
|
||||||
return URL.canParse(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name identifier for the current interface.
|
|
||||||
*/
|
|
||||||
export type RouteInterfaceName = "user" | "admin" | "flow" | "unknown";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read the current interface route parameter from the URL.
|
|
||||||
*
|
|
||||||
* @param location - The location object to read the pathname from. Defaults to `window.location`.
|
|
||||||
* * @returns The name of the current interface, or "unknown" if not found.
|
|
||||||
*/
|
|
||||||
export function readInterfaceRouteParam(
|
|
||||||
location: Pick<URL, "pathname"> = window.location,
|
|
||||||
): RouteInterfaceName {
|
|
||||||
const [, currentInterface = "unknown"] = location.pathname.match(/.+if\/(\w+)\//) || [];
|
|
||||||
|
|
||||||
return currentInterface.toLowerCase() as RouteInterfaceName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Predicate to determine if the current route is for the admin interface.
|
|
||||||
*/
|
|
||||||
export function isAdminRoute(location: Pick<URL, "pathname"> = window.location): boolean {
|
|
||||||
return readInterfaceRouteParam(location) === "admin";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Predicate to determine if the current route is for the user interface.
|
|
||||||
*/
|
|
||||||
export function isUserRoute(location: Pick<URL, "pathname"> = window.location): boolean {
|
|
||||||
return readInterfaceRouteParam(location) === "user";
|
|
||||||
}
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
|
import { ROUTE_SEPARATOR } from "@goauthentik/common/constants";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { pluckRoute } from "@goauthentik/elements/router";
|
|
||||||
|
|
||||||
import { CSSResult, css } from "lit";
|
import { CSSResult, css } from "lit";
|
||||||
import { TemplateResult, html } from "lit";
|
import { TemplateResult, html } from "lit";
|
||||||
@ -69,9 +69,9 @@ export class SidebarItem extends AKElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
pathname?: string;
|
path?: string;
|
||||||
|
|
||||||
#activeMatchers: URLPattern[] = [];
|
activeMatchers: RegExp[] = [];
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
expanded = false;
|
expanded = false;
|
||||||
@ -94,57 +94,41 @@ export class SidebarItem extends AKElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
set activeWhen(nextPathnamePatterns: string[]) {
|
set activeWhen(regexp: string[]) {
|
||||||
for (const pathname of nextPathnamePatterns) {
|
regexp.forEach((r) => {
|
||||||
this.#activeMatchers.push(new URLPattern({ pathname }));
|
this.activeMatchers.push(new RegExp(r));
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated(): void {
|
firstUpdated(): void {
|
||||||
this.#hashListener();
|
this.onHashChange();
|
||||||
window.addEventListener("hashchange", this.#hashListener);
|
window.addEventListener("hashchange", () => this.onHashChange());
|
||||||
}
|
}
|
||||||
|
|
||||||
#hashListener = (): void => {
|
onHashChange(): void {
|
||||||
const currentPathname = pluckRoute(window.location).pathname;
|
const activePath = window.location.hash.slice(1, Infinity).split(ROUTE_SEPARATOR)[0];
|
||||||
|
|
||||||
this.childItems.forEach((item) => {
|
this.childItems.forEach((item) => {
|
||||||
this.expandParentRecursive(currentPathname, item);
|
this.expandParentRecursive(activePath, item);
|
||||||
});
|
});
|
||||||
|
this.isActive = this.matchesPath(activePath);
|
||||||
|
}
|
||||||
|
|
||||||
this.isActive = this.matchesPath(currentPathname);
|
private matchesPath(path: string): boolean {
|
||||||
};
|
if (!this.path) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private matchesPath(targetPathname: string): boolean {
|
const ourPath = this.path.split(";")[0];
|
||||||
if (!this.pathname) return false;
|
const pathIsWholePath = new RegExp(`^${ourPath}$`).test(path);
|
||||||
|
const pathIsAnActivePath = this.activeMatchers.some((v) => v.test(path));
|
||||||
const criteria = {
|
return pathIsWholePath || pathIsAnActivePath;
|
||||||
pathname: targetPathname,
|
|
||||||
};
|
|
||||||
|
|
||||||
const matchesWholePath = new URLPattern({
|
|
||||||
pathname: this.pathname,
|
|
||||||
}).test(criteria);
|
|
||||||
|
|
||||||
const activePath = this.#activeMatchers.some((v) => v.test(criteria));
|
|
||||||
|
|
||||||
return matchesWholePath || activePath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
expandParentRecursive(activePath: string, item: SidebarItem): void {
|
expandParentRecursive(activePath: string, item: SidebarItem): void {
|
||||||
if (item.matchesPath(activePath) && item.parent) {
|
if (item.matchesPath(activePath) && item.parent) {
|
||||||
item.parent.expanded = true;
|
item.parent.expanded = true;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
|
|
||||||
if (!item.childItems.length) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.scrollIntoView({
|
|
||||||
block: "nearest",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
item.childItems.forEach((i) => this.expandParentRecursive(activePath, i));
|
item.childItems.forEach((i) => this.expandParentRecursive(activePath, i));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,7 +191,7 @@ export class SidebarItem extends AKElement {
|
|||||||
renderWithPath() {
|
renderWithPath() {
|
||||||
return html`
|
return html`
|
||||||
<a
|
<a
|
||||||
href="${this.isAbsoluteLink ? "" : "#"}${this.pathname}"
|
href="${this.isAbsoluteLink ? "" : "#"}${this.path}"
|
||||||
class="pf-c-nav__link ${this.isActive ? "pf-m-current" : ""}"
|
class="pf-c-nav__link ${this.isActive ? "pf-m-current" : ""}"
|
||||||
>
|
>
|
||||||
<slot name="label"></slot>
|
<slot name="label"></slot>
|
||||||
@ -225,11 +209,11 @@ export class SidebarItem extends AKElement {
|
|||||||
|
|
||||||
renderInner() {
|
renderInner() {
|
||||||
if (this.childItems.length > 0) {
|
if (this.childItems.length > 0) {
|
||||||
return this.pathname ? this.renderWithPathAndChildren() : this.renderWithChildren();
|
return this.path ? this.renderWithPathAndChildren() : this.renderWithChildren();
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`<li class="pf-c-nav__item">
|
return html`<li class="pf-c-nav__item">
|
||||||
${this.pathname ? this.renderWithPath() : this.renderWithLabel()}
|
${this.path ? this.renderWithPath() : this.renderWithLabel()}
|
||||||
</li>`;
|
</li>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import "@goauthentik/elements/EmptyState";
|
|||||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||||
import "@goauthentik/elements/chips/Chip";
|
import "@goauthentik/elements/chips/Chip";
|
||||||
import "@goauthentik/elements/chips/ChipGroup";
|
import "@goauthentik/elements/chips/ChipGroup";
|
||||||
import { getRouteParameter, patchRouteParams } from "@goauthentik/elements/router/utils";
|
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
|
||||||
import "@goauthentik/elements/table/TablePagination";
|
import "@goauthentik/elements/table/TablePagination";
|
||||||
import "@goauthentik/elements/table/TableSearch";
|
import "@goauthentik/elements/table/TableSearch";
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ export abstract class Table<T> extends AKElement implements TableLike {
|
|||||||
data?: PaginatedResponse<T>;
|
data?: PaginatedResponse<T>;
|
||||||
|
|
||||||
@property({ type: Number })
|
@property({ type: Number })
|
||||||
page = getRouteParameter("tablePage", 1);
|
page = getURLParam("tablePage", 1);
|
||||||
|
|
||||||
/** @prop
|
/** @prop
|
||||||
*
|
*
|
||||||
@ -200,7 +200,7 @@ export abstract class Table<T> extends AKElement implements TableLike {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (this.searchEnabled()) {
|
if (this.searchEnabled()) {
|
||||||
this.search = getRouteParameter("search", "");
|
this.search = getURLParam("search", "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -441,7 +441,7 @@ export abstract class Table<T> extends AKElement implements TableLike {
|
|||||||
renderSearch(): TemplateResult {
|
renderSearch(): TemplateResult {
|
||||||
const runSearch = (value: string) => {
|
const runSearch = (value: string) => {
|
||||||
this.search = value;
|
this.search = value;
|
||||||
patchRouteParams({
|
updateURLParams({
|
||||||
search: value,
|
search: value,
|
||||||
});
|
});
|
||||||
this.fetch();
|
this.fetch();
|
||||||
@ -524,7 +524,7 @@ export abstract class Table<T> extends AKElement implements TableLike {
|
|||||||
/* A simple pagination display, shown at both the top and bottom of the page. */
|
/* A simple pagination display, shown at both the top and bottom of the page. */
|
||||||
renderTablePagination(): TemplateResult {
|
renderTablePagination(): TemplateResult {
|
||||||
const handler = (page: number) => {
|
const handler = (page: number) => {
|
||||||
patchRouteParams({ tablePage: page });
|
updateURLParams({ tablePage: page });
|
||||||
this.page = page;
|
this.page = page;
|
||||||
this.fetch();
|
this.fetch();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import "@goauthentik/elements/PageHeader";
|
import "@goauthentik/elements/PageHeader";
|
||||||
import { patchRouteParams } from "@goauthentik/elements/router/utils";
|
import { updateURLParams } from "@goauthentik/elements/router/RouteMatch";
|
||||||
import { Table } from "@goauthentik/elements/table/Table";
|
import { Table } from "@goauthentik/elements/table/Table";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
@ -60,7 +60,7 @@ export abstract class TablePage<T> extends Table<T> {
|
|||||||
this.search = "";
|
this.search = "";
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
this.fetch();
|
this.fetch();
|
||||||
patchRouteParams({
|
updateURLParams({
|
||||||
search: "",
|
search: "",
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import { applyNextParam } from "@goauthentik/admin/flows/utils";
|
import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
|
||||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||||
import { MessageLevel } from "@goauthentik/common/messages";
|
import { MessageLevel } from "@goauthentik/common/messages";
|
||||||
import "@goauthentik/elements/Spinner";
|
import "@goauthentik/elements/Spinner";
|
||||||
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
||||||
import { formatInterfaceRoute } from "@goauthentik/elements/router/utils";
|
|
||||||
import { BaseUserSettings } from "@goauthentik/elements/user/sources/BaseUserSettings";
|
import { BaseUserSettings } from "@goauthentik/elements/user/sources/BaseUserSettings";
|
||||||
|
|
||||||
import { msg, str } from "@lit/localize";
|
import { msg, str } from "@lit/localize";
|
||||||
@ -59,13 +57,12 @@ export class SourceSettingsOAuth extends BaseUserSettings {
|
|||||||
</button>`;
|
</button>`;
|
||||||
}
|
}
|
||||||
if (this.configureUrl) {
|
if (this.configureUrl) {
|
||||||
const target = new URL(this.configureUrl);
|
return html`<a
|
||||||
|
class="pf-c-button pf-m-primary"
|
||||||
const destination = formatInterfaceRoute("user", "settings", { page: "sources" });
|
href="${this.configureUrl}${AndNext(
|
||||||
|
`/if/user/#/settings;${JSON.stringify({ page: "page-sources" })}`,
|
||||||
applyNextParam(target, destination);
|
)}"
|
||||||
|
>
|
||||||
return html`<a class="pf-c-button pf-m-primary" href="${target}">
|
|
||||||
${msg("Connect")}
|
${msg("Connect")}
|
||||||
</a>`;
|
</a>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import { applyNextParam } from "@goauthentik/admin/flows/utils";
|
import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
|
||||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||||
import { MessageLevel } from "@goauthentik/common/messages";
|
import { MessageLevel } from "@goauthentik/common/messages";
|
||||||
import "@goauthentik/elements/Spinner";
|
import "@goauthentik/elements/Spinner";
|
||||||
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
||||||
import { formatInterfaceRoute } from "@goauthentik/elements/router/utils";
|
|
||||||
import { BaseUserSettings } from "@goauthentik/elements/user/sources/BaseUserSettings";
|
import { BaseUserSettings } from "@goauthentik/elements/user/sources/BaseUserSettings";
|
||||||
|
|
||||||
import { msg, str } from "@lit/localize";
|
import { msg, str } from "@lit/localize";
|
||||||
@ -59,13 +57,12 @@ export class SourceSettingsSAML extends BaseUserSettings {
|
|||||||
</button>`;
|
</button>`;
|
||||||
}
|
}
|
||||||
if (this.configureUrl) {
|
if (this.configureUrl) {
|
||||||
const target = new URL(this.configureUrl);
|
return html`<a
|
||||||
|
class="pf-c-button pf-m-primary"
|
||||||
const destination = formatInterfaceRoute("user", "settings", { page: "sources" });
|
href="${this.configureUrl}${AndNext(
|
||||||
|
`/if/user/#/settings;${JSON.stringify({ page: "page-sources" })}`,
|
||||||
applyNextParam(target, destination);
|
)}"
|
||||||
|
>
|
||||||
return html`<a class="pf-c-button pf-m-primary" href="${target}">
|
|
||||||
${msg("Connect")}
|
${msg("Connect")}
|
||||||
</a>`;
|
</a>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import "@goauthentik/flow/stages/password/PasswordStage";
|
|||||||
// end of stage import
|
// end of stage import
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) {
|
if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) {
|
||||||
const { ESBuildObserver } = await import("src/development/build-observer");
|
const { ESBuildObserver } = await import("@goauthentik/common/client");
|
||||||
|
|
||||||
new ESBuildObserver(process.env.WATCHER_URL);
|
new ESBuildObserver(process.env.WATCHER_URL);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user