Compare commits
	
		
			72 Commits
		
	
	
		
			web/legibi
			...
			version-20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6da73037ce | |||
| 8e84fe6efd | |||
| 74eab55c61 | |||
| 06137fc633 | |||
| 63ec664532 | |||
| 4e4516f9a2 | |||
| 748a8e560f | |||
| d6c35787b0 | |||
| cc214a0eb7 | |||
| 0c9fd5f056 | |||
| 92a1f7e01a | |||
| 1a727b9ea0 | |||
| 28cc75af29 | |||
| 0ad245f7f6 | |||
| b10957e5df | |||
| 3adf79c493 | |||
| f478593826 | |||
| edf4de7271 | |||
| db43869e25 | |||
| 8a668af5f6 | |||
| eef233fd11 | |||
| 833b350c42 | |||
| b388265d98 | |||
| faefd9776d | |||
| a5ee159189 | |||
| 35c739ee84 | |||
| e9764333ea | |||
| 22af17be2c | |||
| 679bf17d6f | |||
| cbfa51fb31 | |||
| 5f8c21cc88 | |||
| 69b3d1722b | |||
| fa4ce1d629 | |||
| e4a392834f | |||
| 31fe0e5923 | |||
| 8b619635ea | |||
| 1f1db523c0 | |||
| bbc23e1d77 | |||
| c30b7ee3e9 | |||
| 2ba79627bc | |||
| 198cbe1d9d | |||
| db6da159d5 | |||
| 9862e32078 | |||
| a7714e2892 | |||
| 073e1d241b | |||
| 5c5cc1c7da | |||
| 3dccce1095 | |||
| 78f997fbee | |||
| ed83c2b0b1 | |||
| af780deb27 | |||
| a4be38567f | |||
| 39aafbb34a | |||
| 07eb5fe533 | |||
| 301a89dd92 | |||
| cd6d0a47f3 | |||
| 8a23eaef1e | |||
| 8f285fbcc5 | |||
| 5d391424f7 | |||
| 2de11f8a69 | |||
| b2dcf94aba | |||
| adb532fc5d | |||
| 5d3b35d1ba | |||
| 433a94d9ee | |||
| f28d622d10 | |||
| 50a68c22c5 | |||
| 13c99c8546 | |||
| 7243add30f | |||
| 6611a64a62 | |||
| 5262f61483 | |||
| 9dcbb4af9e | |||
| 0665bfac58 | |||
| 790e0c4d80 | 
| @ -1,5 +1,5 @@ | |||||||
| [bumpversion] | [bumpversion] | ||||||
| current_version = 2025.2.0 | current_version = 2025.2.4 | ||||||
| tag = True | tag = True | ||||||
| commit = True | commit = True | ||||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? | parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							| @ -28,11 +28,7 @@ Output of docker-compose logs or kubectl logs respectively | |||||||
|  |  | ||||||
| **Version and Deployment (please complete the following information):** | **Version and Deployment (please complete the following information):** | ||||||
|  |  | ||||||
| <!-- | -   authentik version: [e.g. 2021.8.5] | ||||||
| Notice: authentik supports installation via Docker, Kubernetes, and AWS CloudFormation only. Support is not available for other methods. For detailed installation and configuration instructions, please refer to the official documentation at https://docs.goauthentik.io/docs/install-config/. |  | ||||||
| --> |  | ||||||
|  |  | ||||||
| -   authentik version: [e.g. 2025.2.0] |  | ||||||
| -   Deployment: [e.g. docker-compose, helm] | -   Deployment: [e.g. docker-compose, helm] | ||||||
|  |  | ||||||
| **Additional context** | **Additional context** | ||||||
|  | |||||||
							
								
								
									
										7
									
								
								.github/ISSUE_TEMPLATE/question.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/ISSUE_TEMPLATE/question.md
									
									
									
									
										vendored
									
									
								
							| @ -20,12 +20,7 @@ Output of docker-compose logs or kubectl logs respectively | |||||||
|  |  | ||||||
| **Version and Deployment (please complete the following information):** | **Version and Deployment (please complete the following information):** | ||||||
|  |  | ||||||
| <!-- | -   authentik version: [e.g. 2021.8.5] | ||||||
| Notice: authentik supports installation via Docker, Kubernetes, and AWS CloudFormation only. Support is not available for other methods. For detailed installation and configuration instructions, please refer to the official documentation at https://docs.goauthentik.io/docs/install-config/. |  | ||||||
| --> |  | ||||||
|  |  | ||||||
|  |  | ||||||
| -   authentik version: [e.g. 2025.2.0] |  | ||||||
| -   Deployment: [e.g. docker-compose, helm] | -   Deployment: [e.g. docker-compose, helm] | ||||||
|  |  | ||||||
| **Additional context** | **Additional context** | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								.github/actions/setup/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/actions/setup/action.yml
									
									
									
									
										vendored
									
									
								
							| @ -35,7 +35,7 @@ runs: | |||||||
|       run: | |       run: | | ||||||
|         export PSQL_TAG=${{ inputs.postgresql_version }} |         export PSQL_TAG=${{ inputs.postgresql_version }} | ||||||
|         docker compose -f .github/actions/setup/docker-compose.yml up -d |         docker compose -f .github/actions/setup/docker-compose.yml up -d | ||||||
|         poetry sync |         poetry install --sync | ||||||
|         cd web && npm ci |         cd web && npm ci | ||||||
|     - name: Generate config |     - name: Generate config | ||||||
|       shell: poetry run python {0} |       shell: poetry run python {0} | ||||||
|  | |||||||
| @ -1,13 +1,9 @@ | |||||||
| --- | --- | ||||||
| name: authentik-translate-extract-compile | name: authentik-backend-translate-extract-compile | ||||||
| on: | on: | ||||||
|   schedule: |   schedule: | ||||||
|     - cron: "0 0 * * *" # every day at midnight |     - cron: "0 0 * * *" # every day at midnight | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
|   pull_request: |  | ||||||
|     branches: |  | ||||||
|       - main |  | ||||||
|       - version-* |  | ||||||
|  |  | ||||||
| env: | env: | ||||||
|   POSTGRES_DB: authentik |   POSTGRES_DB: authentik | ||||||
| @ -36,7 +32,6 @@ jobs: | |||||||
|           poetry run ak compilemessages |           poetry run ak compilemessages | ||||||
|           make web-check-compile |           make web-check-compile | ||||||
|       - name: Create Pull Request |       - name: Create Pull Request | ||||||
|         if: ${{ github.event_name != 'pull_request' }} |  | ||||||
|         uses: peter-evans/create-pull-request@v7 |         uses: peter-evans/create-pull-request@v7 | ||||||
|         with: |         with: | ||||||
|           token: ${{ steps.generate_token.outputs.token }} |           token: ${{ steps.generate_token.outputs.token }} | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
|  |  | ||||||
| from os import environ | from os import environ | ||||||
|  |  | ||||||
| __version__ = "2025.2.0" | __version__ = "2025.2.4" | ||||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -59,7 +59,7 @@ class SystemInfoSerializer(PassiveSerializer): | |||||||
|             if not isinstance(value, str): |             if not isinstance(value, str): | ||||||
|                 continue |                 continue | ||||||
|             actual_value = value |             actual_value = value | ||||||
|             if raw_session in actual_value: |             if raw_session is not None and raw_session in actual_value: | ||||||
|                 actual_value = actual_value.replace( |                 actual_value = actual_value.replace( | ||||||
|                     raw_session, SafeExceptionReporterFilter.cleansed_substitute |                     raw_session, SafeExceptionReporterFilter.cleansed_substitute | ||||||
|                 ) |                 ) | ||||||
|  | |||||||
| @ -1,13 +1,14 @@ | |||||||
| """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.cache import KEY_PREFIX | from django.contrib.sessions.backends.base import SessionBase | ||||||
| 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 | ||||||
| @ -91,6 +92,7 @@ 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): | ||||||
| @ -373,7 +375,7 @@ class UsersFilter(FilterSet): | |||||||
|         method="filter_attributes", |         method="filter_attributes", | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser") |     is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser") | ||||||
|     uuid = UUIDFilter(field_name="uuid") |     uuid = UUIDFilter(field_name="uuid") | ||||||
|  |  | ||||||
|     path = CharFilter(field_name="path") |     path = CharFilter(field_name="path") | ||||||
| @ -391,6 +393,11 @@ 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: | ||||||
| @ -769,7 +776,8 @@ 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) | ||||||
|             cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids) |             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 | ||||||
|  | |||||||
| @ -1,7 +1,10 @@ | |||||||
| """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.cache import KEY_PREFIX | from django.contrib.sessions.backends.base import SessionBase | ||||||
| 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 | ||||||
| @ -25,6 +28,7 @@ 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) | ||||||
| @ -60,8 +64,7 @@ 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""" | ||||||
|     cache_key = f"{KEY_PREFIX}{instance.session_key}" |     SessionStore(instance.session_key).delete() | ||||||
|     cache.delete(cache_key) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @receiver(pre_save) | @receiver(pre_save) | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ | |||||||
|         build: "{{ build }}", |         build: "{{ build }}", | ||||||
|         api: { |         api: { | ||||||
|             base: "{{ base_url }}", |             base: "{{ base_url }}", | ||||||
|  |             relBase: "{{ base_url_rel }}", | ||||||
|         }, |         }, | ||||||
|     }; |     }; | ||||||
|     window.addEventListener("DOMContentLoaded", function () { |     window.addEventListener("DOMContentLoaded", function () { | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| """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 | ||||||
| @ -15,7 +16,11 @@ from authentik.core.models import ( | |||||||
|     User, |     User, | ||||||
|     UserTypes, |     UserTypes, | ||||||
| ) | ) | ||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow | from authentik.core.tests.utils import ( | ||||||
|  |     create_test_admin_user, | ||||||
|  |     create_test_brand, | ||||||
|  |     create_test_flow, | ||||||
|  | ) | ||||||
| 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 | ||||||
| @ -41,6 +46,32 @@ 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""" | ||||||
|  |         self.client.force_login(self.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"], self.admin.username) | ||||||
|  |         # Test non-superuser | ||||||
|  |         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"], self.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) | ||||||
|  | |||||||
| @ -55,7 +55,7 @@ class RedirectToAppLaunch(View): | |||||||
|             ) |             ) | ||||||
|         except FlowNonApplicableException: |         except FlowNonApplicableException: | ||||||
|             raise Http404 from None |             raise Http404 from None | ||||||
|         plan.insert_stage(in_memory_stage(RedirectToAppStage)) |         plan.append_stage(in_memory_stage(RedirectToAppStage)) | ||||||
|         return plan.to_redirect(request, flow) |         return plan.to_redirect(request, flow) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -53,6 +53,7 @@ class InterfaceView(TemplateView): | |||||||
|         kwargs["build"] = get_build_hash() |         kwargs["build"] = get_build_hash() | ||||||
|         kwargs["url_kwargs"] = self.kwargs |         kwargs["url_kwargs"] = self.kwargs | ||||||
|         kwargs["base_url"] = self.request.build_absolute_uri(CONFIG.get("web.path", "/")) |         kwargs["base_url"] = self.request.build_absolute_uri(CONFIG.get("web.path", "/")) | ||||||
|  |         kwargs["base_url_rel"] = CONFIG.get("web.path", "/") | ||||||
|         return super().get_context_data(**kwargs) |         return super().get_context_data(**kwargs) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -89,9 +89,9 @@ class SourceStageFinal(StageView): | |||||||
|     This stage uses the override flow token to resume execution of the initial flow the |     This stage uses the override flow token to resume execution of the initial flow the | ||||||
|     source stage is bound to.""" |     source stage is bound to.""" | ||||||
|  |  | ||||||
|     def dispatch(self): |     def dispatch(self, *args, **kwargs): | ||||||
|         token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN) |         token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN) | ||||||
|         self._logger.info("Replacing source flow with overridden flow", flow=token.flow.slug) |         self.logger.info("Replacing source flow with overridden flow", flow=token.flow.slug) | ||||||
|         plan = token.plan |         plan = token.plan | ||||||
|         plan.context[PLAN_CONTEXT_IS_RESTORED] = token |         plan.context[PLAN_CONTEXT_IS_RESTORED] = token | ||||||
|         response = plan.to_redirect(self.request, token.flow) |         response = plan.to_redirect(self.request, token.flow) | ||||||
|  | |||||||
| @ -4,7 +4,8 @@ from django.urls import reverse | |||||||
|  |  | ||||||
| from authentik.core.tests.utils import create_test_flow, create_test_user | from authentik.core.tests.utils import create_test_flow, create_test_user | ||||||
| from authentik.enterprise.stages.source.models import SourceStage | from authentik.enterprise.stages.source.models import SourceStage | ||||||
| from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken | from authentik.enterprise.stages.source.stage import SourceStageFinal | ||||||
|  | from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken, in_memory_stage | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, FlowPlan | from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, FlowPlan | ||||||
| from authentik.flows.tests import FlowTestCase | from authentik.flows.tests import FlowTestCase | ||||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| @ -87,6 +88,7 @@ class TestSourceStage(FlowTestCase): | |||||||
|         self.assertIsNotNone(flow_token) |         self.assertIsNotNone(flow_token) | ||||||
|         session = self.client.session |         session = self.client.session | ||||||
|         plan: FlowPlan = session[SESSION_KEY_PLAN] |         plan: FlowPlan = session[SESSION_KEY_PLAN] | ||||||
|  |         plan.insert_stage(in_memory_stage(SourceStageFinal), index=0) | ||||||
|         plan.context[PLAN_CONTEXT_IS_RESTORED] = flow_token |         plan.context[PLAN_CONTEXT_IS_RESTORED] = flow_token | ||||||
|         session[SESSION_KEY_PLAN] = plan |         session[SESSION_KEY_PLAN] = plan | ||||||
|         session.save() |         session.save() | ||||||
| @ -96,4 +98,6 @@ class TestSourceStage(FlowTestCase): | |||||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), follow=True |             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), follow=True | ||||||
|         ) |         ) | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|         self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) |         self.assertStageRedirects( | ||||||
|  |             response, reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) | ||||||
|  |         ) | ||||||
|  | |||||||
| @ -76,10 +76,10 @@ class FlowPlan: | |||||||
|         self.bindings.append(binding) |         self.bindings.append(binding) | ||||||
|         self.markers.append(marker or StageMarker()) |         self.markers.append(marker or StageMarker()) | ||||||
|  |  | ||||||
|     def insert_stage(self, stage: Stage, marker: StageMarker | None = None): |     def insert_stage(self, stage: Stage, marker: StageMarker | None = None, index=1): | ||||||
|         """Insert stage into plan, as immediate next stage""" |         """Insert stage into plan, as immediate next stage""" | ||||||
|         self.bindings.insert(1, FlowStageBinding(stage=stage, order=0)) |         self.bindings.insert(index, FlowStageBinding(stage=stage, order=0)) | ||||||
|         self.markers.insert(1, marker or StageMarker()) |         self.markers.insert(index, marker or StageMarker()) | ||||||
|  |  | ||||||
|     def redirect(self, destination: str): |     def redirect(self, destination: str): | ||||||
|         """Insert a redirect stage as next stage""" |         """Insert a redirect stage as next stage""" | ||||||
|  | |||||||
| @ -282,16 +282,14 @@ class ConfigLoader: | |||||||
|  |  | ||||||
|     def get_optional_int(self, path: str, default=None) -> int | None: |     def get_optional_int(self, path: str, default=None) -> int | None: | ||||||
|         """Wrapper for get that converts value into int or None if set""" |         """Wrapper for get that converts value into int or None if set""" | ||||||
|         value = self.get(path, default) |         value = self.get(path, UNSET) | ||||||
|         if value is UNSET: |         if value is UNSET: | ||||||
|             return default |             return default | ||||||
|         try: |         try: | ||||||
|             return int(value) |             return int(value) | ||||||
|         except (ValueError, TypeError) as exc: |         except (ValueError, TypeError) as exc: | ||||||
|             if value is None or (isinstance(value, str) and value.lower() == "null"): |             if value is None or (isinstance(value, str) and value.lower() == "null"): | ||||||
|                 return default |                 return None | ||||||
|             if value is UNSET: |  | ||||||
|                 return default |  | ||||||
|             self.log("warning", "Failed to parse config as int", path=path, exc=str(exc)) |             self.log("warning", "Failed to parse config as int", path=path, exc=str(exc)) | ||||||
|             return default |             return default | ||||||
|  |  | ||||||
| @ -372,9 +370,9 @@ def django_db_config(config: ConfigLoader | None = None) -> dict: | |||||||
|                 "sslcert": config.get("postgresql.sslcert"), |                 "sslcert": config.get("postgresql.sslcert"), | ||||||
|                 "sslkey": config.get("postgresql.sslkey"), |                 "sslkey": config.get("postgresql.sslkey"), | ||||||
|             }, |             }, | ||||||
|             "CONN_MAX_AGE": CONFIG.get_optional_int("postgresql.conn_max_age", 0), |             "CONN_MAX_AGE": config.get_optional_int("postgresql.conn_max_age", 0), | ||||||
|             "CONN_HEALTH_CHECKS": CONFIG.get_bool("postgresql.conn_health_checks", False), |             "CONN_HEALTH_CHECKS": config.get_bool("postgresql.conn_health_checks", False), | ||||||
|             "DISABLE_SERVER_SIDE_CURSORS": CONFIG.get_bool( |             "DISABLE_SERVER_SIDE_CURSORS": config.get_bool( | ||||||
|                 "postgresql.disable_server_side_cursors", False |                 "postgresql.disable_server_side_cursors", False | ||||||
|             ), |             ), | ||||||
|             "TEST": { |             "TEST": { | ||||||
| @ -383,8 +381,8 @@ def django_db_config(config: ConfigLoader | None = None) -> dict: | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     conn_max_age = CONFIG.get_optional_int("postgresql.conn_max_age", UNSET) |     conn_max_age = config.get_optional_int("postgresql.conn_max_age", UNSET) | ||||||
|     disable_server_side_cursors = CONFIG.get_bool("postgresql.disable_server_side_cursors", UNSET) |     disable_server_side_cursors = config.get_bool("postgresql.disable_server_side_cursors", UNSET) | ||||||
|     if config.get_bool("postgresql.use_pgpool", False): |     if config.get_bool("postgresql.use_pgpool", False): | ||||||
|         db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True |         db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True | ||||||
|         if disable_server_side_cursors is not UNSET: |         if disable_server_side_cursors is not UNSET: | ||||||
|  | |||||||
| @ -64,8 +64,6 @@ debugger: false | |||||||
| log_level: info | log_level: info | ||||||
|  |  | ||||||
| session_storage: cache | session_storage: cache | ||||||
| sessions: |  | ||||||
|   unauthenticated_age: days=1 |  | ||||||
|  |  | ||||||
| error_reporting: | error_reporting: | ||||||
|   enabled: false |   enabled: false | ||||||
|  | |||||||
| @ -158,6 +158,18 @@ class TestConfig(TestCase): | |||||||
|             test_obj = Test() |             test_obj = Test() | ||||||
|             dumps(test_obj, indent=4, cls=AttrEncoder) |             dumps(test_obj, indent=4, cls=AttrEncoder) | ||||||
|  |  | ||||||
|  |     def test_get_optional_int(self): | ||||||
|  |         config = ConfigLoader() | ||||||
|  |         self.assertEqual(config.get_optional_int("foo", 21), 21) | ||||||
|  |         self.assertEqual(config.get_optional_int("foo"), None) | ||||||
|  |         config.set("foo", "21") | ||||||
|  |         self.assertEqual(config.get_optional_int("foo"), 21) | ||||||
|  |         self.assertEqual(config.get_optional_int("foo", 0), 21) | ||||||
|  |         self.assertEqual(config.get_optional_int("foo", "null"), 21) | ||||||
|  |         config.set("foo", "null") | ||||||
|  |         self.assertEqual(config.get_optional_int("foo"), None) | ||||||
|  |         self.assertEqual(config.get_optional_int("foo", 21), None) | ||||||
|  |  | ||||||
|     @mock.patch.dict(environ, check_deprecations_env_vars) |     @mock.patch.dict(environ, check_deprecations_env_vars) | ||||||
|     def test_check_deprecations(self): |     def test_check_deprecations(self): | ||||||
|         """Test config key re-write for deprecated env vars""" |         """Test config key re-write for deprecated env vars""" | ||||||
| @ -221,6 +233,16 @@ class TestConfig(TestCase): | |||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     def test_db_conn_max_age(self): | ||||||
|  |         """Test DB conn_max_age Config""" | ||||||
|  |         config = ConfigLoader() | ||||||
|  |         config.set("postgresql.conn_max_age", "null") | ||||||
|  |         conf = django_db_config(config) | ||||||
|  |         self.assertEqual( | ||||||
|  |             conf["default"]["CONN_MAX_AGE"], | ||||||
|  |             None, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def test_db_read_replicas(self): |     def test_db_read_replicas(self): | ||||||
|         """Test read replicas""" |         """Test read replicas""" | ||||||
|         config = ConfigLoader() |         config = ConfigLoader() | ||||||
|  | |||||||
| @ -71,7 +71,7 @@ class CodeValidatorView(PolicyAccessView): | |||||||
|         except FlowNonApplicableException: |         except FlowNonApplicableException: | ||||||
|             LOGGER.warning("Flow not applicable to user") |             LOGGER.warning("Flow not applicable to user") | ||||||
|             return None |             return None | ||||||
|         plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage)) |         plan.append_stage(in_memory_stage(OAuthDeviceCodeFinishStage)) | ||||||
|         return plan.to_redirect(self.request, self.token.provider.authorization_flow) |         return plan.to_redirect(self.request, self.token.provider.authorization_flow) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -34,5 +34,5 @@ class EndSessionView(PolicyAccessView): | |||||||
|                 PLAN_CONTEXT_APPLICATION: self.application, |                 PLAN_CONTEXT_APPLICATION: self.application, | ||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
|         plan.insert_stage(in_memory_stage(SessionEndStage)) |         plan.append_stage(in_memory_stage(SessionEndStage)) | ||||||
|         return plan.to_redirect(self.request, self.flow) |         return plan.to_redirect(self.request, self.flow) | ||||||
|  | |||||||
| @ -36,17 +36,17 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]): | |||||||
|     def reconciler_name() -> str: |     def reconciler_name() -> str: | ||||||
|         return "ingress" |         return "ingress" | ||||||
|  |  | ||||||
|     def _check_annotations(self, reference: V1Ingress): |     def _check_annotations(self, current: V1Ingress, reference: V1Ingress): | ||||||
|         """Check that all annotations *we* set are correct""" |         """Check that all annotations *we* set are correct""" | ||||||
|         for key, value in self.get_ingress_annotations().items(): |         for key, value in reference.metadata.annotations.items(): | ||||||
|             if key not in reference.metadata.annotations: |             if key not in current.metadata.annotations: | ||||||
|                 raise NeedsUpdate() |                 raise NeedsUpdate() | ||||||
|             if reference.metadata.annotations[key] != value: |             if current.metadata.annotations[key] != value: | ||||||
|                 raise NeedsUpdate() |                 raise NeedsUpdate() | ||||||
|  |  | ||||||
|     def reconcile(self, current: V1Ingress, reference: V1Ingress): |     def reconcile(self, current: V1Ingress, reference: V1Ingress): | ||||||
|         super().reconcile(current, reference) |         super().reconcile(current, reference) | ||||||
|         self._check_annotations(reference) |         self._check_annotations(current, reference) | ||||||
|         # Create a list of all expected host and tls hosts |         # Create a list of all expected host and tls hosts | ||||||
|         expected_hosts = [] |         expected_hosts = [] | ||||||
|         expected_hosts_tls = [] |         expected_hosts_tls = [] | ||||||
|  | |||||||
| @ -1,9 +1,9 @@ | |||||||
| """RAC app config""" | """RAC app config""" | ||||||
|  |  | ||||||
| from django.apps import AppConfig | from authentik.blueprints.apps import ManagedAppConfig | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikProviderRAC(AppConfig): | class AuthentikProviderRAC(ManagedAppConfig): | ||||||
|     """authentik rac app config""" |     """authentik rac app config""" | ||||||
|  |  | ||||||
|     name = "authentik.providers.rac" |     name = "authentik.providers.rac" | ||||||
|  | |||||||
| @ -4,8 +4,7 @@ from asgiref.sync import async_to_sync | |||||||
| from channels.layers import get_channel_layer | from channels.layers import get_channel_layer | ||||||
| from django.contrib.auth.signals import user_logged_out | from django.contrib.auth.signals import user_logged_out | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from django.db.models import Model | from django.db.models.signals import post_delete, post_save, pre_delete | ||||||
| from django.db.models.signals import post_save, pre_delete |  | ||||||
| from django.dispatch import receiver | from django.dispatch import receiver | ||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
|  |  | ||||||
| @ -46,12 +45,8 @@ def pre_delete_connection_token_disconnect(sender, instance: ConnectionToken, ** | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @receiver(post_save, sender=Endpoint) | @receiver([post_save, post_delete], sender=Endpoint) | ||||||
| def post_save_endpoint(sender: type[Model], instance, created: bool, **_): | def post_save_post_delete_endpoint(**_): | ||||||
|     """Clear user's endpoint cache upon endpoint creation""" |     """Clear user's endpoint cache upon endpoint creation or deletion""" | ||||||
|     if not created:  # pragma: no cover |  | ||||||
|         return |  | ||||||
|  |  | ||||||
|     # Delete user endpoint cache |  | ||||||
|     keys = cache.keys(user_endpoint_cache_key("*")) |     keys = cache.keys(user_endpoint_cache_key("*")) | ||||||
|     cache.delete_many(keys) |     cache.delete_many(keys) | ||||||
|  | |||||||
| @ -46,7 +46,7 @@ class RACStartView(PolicyAccessView): | |||||||
|             ) |             ) | ||||||
|         except FlowNonApplicableException: |         except FlowNonApplicableException: | ||||||
|             raise Http404 from None |             raise Http404 from None | ||||||
|         plan.insert_stage( |         plan.append_stage( | ||||||
|             in_memory_stage( |             in_memory_stage( | ||||||
|                 RACFinalStage, |                 RACFinalStage, | ||||||
|                 application=self.application, |                 application=self.application, | ||||||
|  | |||||||
| @ -61,7 +61,7 @@ class SAMLSLOView(PolicyAccessView): | |||||||
|                 PLAN_CONTEXT_APPLICATION: self.application, |                 PLAN_CONTEXT_APPLICATION: self.application, | ||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
|         plan.insert_stage(in_memory_stage(SessionEndStage)) |         plan.append_stage(in_memory_stage(SessionEndStage)) | ||||||
|         return plan.to_redirect(self.request, self.flow) |         return plan.to_redirect(self.request, self.flow) | ||||||
|  |  | ||||||
|     def post(self, request: HttpRequest, application_slug: str) -> HttpResponse: |     def post(self, request: HttpRequest, application_slug: str) -> HttpResponse: | ||||||
|  | |||||||
| @ -243,9 +243,10 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]): | |||||||
|             if user.value not in users_should: |             if user.value not in users_should: | ||||||
|                 users_to_remove.append(user.value) |                 users_to_remove.append(user.value) | ||||||
|         # Check users that should be in the group and add them |         # Check users that should be in the group and add them | ||||||
|         for user in users_should: |         if current_group.members is not None: | ||||||
|             if len([x for x in current_group.members if x.value == user]) < 1: |             for user in users_should: | ||||||
|                 users_to_add.append(user) |                 if len([x for x in current_group.members if x.value == user]) < 1: | ||||||
|  |                     users_to_add.append(user) | ||||||
|         # Only send request if we need to make changes |         # Only send request if we need to make changes | ||||||
|         if len(users_to_add) < 1 and len(users_to_remove) < 1: |         if len(users_to_add) < 1 and len(users_to_remove) < 1: | ||||||
|             return |             return | ||||||
|  | |||||||
| @ -1,10 +1,12 @@ | |||||||
| """User client""" | """User client""" | ||||||
|  |  | ||||||
|  | from django.db import transaction | ||||||
|  | from django.utils.http import urlencode | ||||||
| from pydantic import ValidationError | from pydantic import ValidationError | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.lib.sync.mapper import PropertyMappingManager | from authentik.lib.sync.mapper import PropertyMappingManager | ||||||
| from authentik.lib.sync.outgoing.exceptions import StopSync | from authentik.lib.sync.outgoing.exceptions import ObjectExistsSyncException, StopSync | ||||||
| from authentik.policies.utils import delete_none_values | from authentik.policies.utils import delete_none_values | ||||||
| from authentik.providers.scim.clients.base import SCIMClient | from authentik.providers.scim.clients.base import SCIMClient | ||||||
| from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA | from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA | ||||||
| @ -55,18 +57,35 @@ class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]): | |||||||
|     def create(self, user: User): |     def create(self, user: User): | ||||||
|         """Create user from scratch and create a connection object""" |         """Create user from scratch and create a connection object""" | ||||||
|         scim_user = self.to_schema(user, None) |         scim_user = self.to_schema(user, None) | ||||||
|         response = self._request( |         with transaction.atomic(): | ||||||
|             "POST", |             try: | ||||||
|             "/Users", |                 response = self._request( | ||||||
|             json=scim_user.model_dump( |                     "POST", | ||||||
|                 mode="json", |                     "/Users", | ||||||
|                 exclude_unset=True, |                     json=scim_user.model_dump( | ||||||
|             ), |                         mode="json", | ||||||
|         ) |                         exclude_unset=True, | ||||||
|         scim_id = response.get("id") |                     ), | ||||||
|         if not scim_id or scim_id == "": |                 ) | ||||||
|             raise StopSync("SCIM Response with missing or invalid `id`") |             except ObjectExistsSyncException as exc: | ||||||
|         return SCIMProviderUser.objects.create(provider=self.provider, user=user, scim_id=scim_id) |                 if not self._config.filter.supported: | ||||||
|  |                     raise exc | ||||||
|  |                 users = self._request( | ||||||
|  |                     "GET", f"/Users?{urlencode({'filter': f'userName eq {scim_user.userName}'})}" | ||||||
|  |                 ) | ||||||
|  |                 users_res = users.get("Resources", []) | ||||||
|  |                 if len(users_res) < 1: | ||||||
|  |                     raise exc | ||||||
|  |                 return SCIMProviderUser.objects.create( | ||||||
|  |                     provider=self.provider, user=user, scim_id=users_res[0]["id"] | ||||||
|  |                 ) | ||||||
|  |             else: | ||||||
|  |                 scim_id = response.get("id") | ||||||
|  |                 if not scim_id or scim_id == "": | ||||||
|  |                     raise StopSync("SCIM Response with missing or invalid `id`") | ||||||
|  |                 return SCIMProviderUser.objects.create( | ||||||
|  |                     provider=self.provider, user=user, scim_id=scim_id | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|     def update(self, user: User, connection: SCIMProviderUser): |     def update(self, user: User, connection: SCIMProviderUser): | ||||||
|         """Update existing user""" |         """Update existing user""" | ||||||
|  | |||||||
| @ -16,7 +16,6 @@ from authentik.lib.config import CONFIG, django_db_config, redis_url | |||||||
| from authentik.lib.logging import get_logger_config, structlog_configure | from authentik.lib.logging import get_logger_config, structlog_configure | ||||||
| from authentik.lib.sentry import sentry_init | from authentik.lib.sentry import sentry_init | ||||||
| from authentik.lib.utils.reflection import get_env | from authentik.lib.utils.reflection import get_env | ||||||
| from authentik.lib.utils.time import timedelta_from_string |  | ||||||
| from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP | from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP | ||||||
|  |  | ||||||
| BASE_DIR = Path(__file__).absolute().parent.parent.parent | BASE_DIR = Path(__file__).absolute().parent.parent.parent | ||||||
| @ -243,9 +242,6 @@ SESSION_CACHE_ALIAS = "default" | |||||||
| # Configured via custom SessionMiddleware | # Configured via custom SessionMiddleware | ||||||
| # SESSION_COOKIE_SAMESITE = "None" | # SESSION_COOKIE_SAMESITE = "None" | ||||||
| # SESSION_COOKIE_SECURE = True | # SESSION_COOKIE_SECURE = True | ||||||
| SESSION_COOKIE_AGE = timedelta_from_string( |  | ||||||
|     CONFIG.get("sessions.unauthenticated_age", "days=1") |  | ||||||
| ).total_seconds() |  | ||||||
| SESSION_EXPIRE_AT_BROWSER_CLOSE = True | SESSION_EXPIRE_AT_BROWSER_CLOSE = True | ||||||
|  |  | ||||||
| MESSAGE_STORAGE = "authentik.root.messages.storage.ChannelsStorage" | MESSAGE_STORAGE = "authentik.root.messages.storage.ChannelsStorage" | ||||||
|  | |||||||
| @ -68,8 +68,6 @@ class OAuth2Client(BaseOAuthClient): | |||||||
|             error_desc = self.get_request_arg("error_description", None) |             error_desc = self.get_request_arg("error_description", None) | ||||||
|             return {"error": error_desc or error or _("No token received.")} |             return {"error": error_desc or error or _("No token received.")} | ||||||
|         args = { |         args = { | ||||||
|             "client_id": self.get_client_id(), |  | ||||||
|             "client_secret": self.get_client_secret(), |  | ||||||
|             "redirect_uri": callback, |             "redirect_uri": callback, | ||||||
|             "code": code, |             "code": code, | ||||||
|             "grant_type": "authorization_code", |             "grant_type": "authorization_code", | ||||||
|  | |||||||
| @ -28,7 +28,7 @@ def update_well_known_jwks(self: SystemTask): | |||||||
|             LOGGER.warning("Failed to update well_known", source=source, exc=exc, text=text) |             LOGGER.warning("Failed to update well_known", source=source, exc=exc, text=text) | ||||||
|             messages.append(f"Failed to update OIDC configuration for {source.slug}") |             messages.append(f"Failed to update OIDC configuration for {source.slug}") | ||||||
|             continue |             continue | ||||||
|         config = well_known_config.json() |         config: dict = well_known_config.json() | ||||||
|         try: |         try: | ||||||
|             dirty = False |             dirty = False | ||||||
|             source_attr_key = ( |             source_attr_key = ( | ||||||
| @ -40,7 +40,9 @@ def update_well_known_jwks(self: SystemTask): | |||||||
|             for source_attr, config_key in source_attr_key: |             for source_attr, config_key in source_attr_key: | ||||||
|                 # Check if we're actually changing anything to only |                 # Check if we're actually changing anything to only | ||||||
|                 # save when something has changed |                 # save when something has changed | ||||||
|                 if getattr(source, source_attr, "") != config[config_key]: |                 if config_key not in config: | ||||||
|  |                     continue | ||||||
|  |                 if getattr(source, source_attr, "") != config.get(config_key, ""): | ||||||
|                     dirty = True |                     dirty = True | ||||||
|                 setattr(source, source_attr, config[config_key]) |                 setattr(source, source_attr, config[config_key]) | ||||||
|         except (IndexError, KeyError) as exc: |         except (IndexError, KeyError) as exc: | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _ | |||||||
| from django.views import View | from django.views import View | ||||||
| from rest_framework.serializers import BaseSerializer | from rest_framework.serializers import BaseSerializer | ||||||
|  |  | ||||||
|  | from authentik.core.types import UserSettingSerializer | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| from authentik.flows.exceptions import StageInvalidException | from authentik.flows.exceptions import StageInvalidException | ||||||
| from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage | from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage | ||||||
| @ -71,6 +72,14 @@ class AuthenticatorEmailStage(ConfigurableStage, FriendlyNamedStage, Stage): | |||||||
|     def component(self) -> str: |     def component(self) -> str: | ||||||
|         return "ak-stage-authenticator-email-form" |         return "ak-stage-authenticator-email-form" | ||||||
|  |  | ||||||
|  |     def ui_user_settings(self) -> UserSettingSerializer | None: | ||||||
|  |         return UserSettingSerializer( | ||||||
|  |             data={ | ||||||
|  |                 "title": self.friendly_name or str(self._meta.verbose_name), | ||||||
|  |                 "component": "ak-user-settings-authenticator-email", | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def backend_class(self) -> type[BaseEmailBackend]: |     def backend_class(self) -> type[BaseEmailBackend]: | ||||||
|         """Get the email backend class to use""" |         """Get the email backend class to use""" | ||||||
|  | |||||||
| @ -300,9 +300,11 @@ class TestAuthenticatorEmailStage(FlowTestCase): | |||||||
|             ) |             ) | ||||||
|             self.assertEqual(response.status_code, 200) |             self.assertEqual(response.status_code, 200) | ||||||
|             self.assertTrue(device.confirmed) |             self.assertTrue(device.confirmed) | ||||||
|             # Session key should be removed after device is saved |             # Get a fresh session to check if the key was removed | ||||||
|             device.save() |             session = self.client.session | ||||||
|             self.assertNotIn(SESSION_KEY_EMAIL_DEVICE, self.client.session) |             session.save() | ||||||
|  |             session.load() | ||||||
|  |             self.assertNotIn(SESSION_KEY_EMAIL_DEVICE, session) | ||||||
|  |  | ||||||
|     def test_model_properties_and_methods(self): |     def test_model_properties_and_methods(self): | ||||||
|         """Test model properties""" |         """Test model properties""" | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ from structlog.stdlib import get_logger | |||||||
|  |  | ||||||
| from authentik.events.models import Event, EventAction, TaskStatus | from authentik.events.models import Event, EventAction, TaskStatus | ||||||
| from authentik.events.system_tasks import SystemTask | from authentik.events.system_tasks import SystemTask | ||||||
|  | from authentik.lib.utils.reflection import class_to_path, path_to_class | ||||||
| from authentik.root.celery import CELERY_APP | from authentik.root.celery import CELERY_APP | ||||||
| from authentik.stages.authenticator_email.models import AuthenticatorEmailStage | from authentik.stages.authenticator_email.models import AuthenticatorEmailStage | ||||||
| from authentik.stages.email.models import EmailStage | from authentik.stages.email.models import EmailStage | ||||||
| @ -32,9 +33,10 @@ def send_mails( | |||||||
|         Celery group promise for the email sending tasks |         Celery group promise for the email sending tasks | ||||||
|     """ |     """ | ||||||
|     tasks = [] |     tasks = [] | ||||||
|     stage_class = stage.__class__ |     # Use the class path instead of the class itself for serialization | ||||||
|  |     stage_class_path = class_to_path(stage.__class__) | ||||||
|     for message in messages: |     for message in messages: | ||||||
|         tasks.append(send_mail.s(message.__dict__, stage_class, str(stage.pk))) |         tasks.append(send_mail.s(message.__dict__, stage_class_path, str(stage.pk))) | ||||||
|     lazy_group = group(*tasks) |     lazy_group = group(*tasks) | ||||||
|     promise = lazy_group() |     promise = lazy_group() | ||||||
|     return promise |     return promise | ||||||
| @ -61,7 +63,7 @@ def get_email_body(email: EmailMultiAlternatives) -> str: | |||||||
| def send_mail( | def send_mail( | ||||||
|     self: SystemTask, |     self: SystemTask, | ||||||
|     message: dict[Any, Any], |     message: dict[Any, Any], | ||||||
|     stage_class: EmailStage | AuthenticatorEmailStage = EmailStage, |     stage_class_path: str | None = None, | ||||||
|     email_stage_pk: str | None = None, |     email_stage_pk: str | None = None, | ||||||
| ): | ): | ||||||
|     """Send Email for Email Stage. Retries are scheduled automatically.""" |     """Send Email for Email Stage. Retries are scheduled automatically.""" | ||||||
| @ -69,9 +71,10 @@ def send_mail( | |||||||
|     message_id = make_msgid(domain=DNS_NAME) |     message_id = make_msgid(domain=DNS_NAME) | ||||||
|     self.set_uid(slugify(message_id.replace(".", "_").replace("@", "_"))) |     self.set_uid(slugify(message_id.replace(".", "_").replace("@", "_"))) | ||||||
|     try: |     try: | ||||||
|         if not email_stage_pk: |         if not stage_class_path or not email_stage_pk: | ||||||
|             stage: EmailStage | AuthenticatorEmailStage = stage_class(use_global_settings=True) |             stage = EmailStage(use_global_settings=True) | ||||||
|         else: |         else: | ||||||
|  |             stage_class = path_to_class(stage_class_path) | ||||||
|             stages = stage_class.objects.filter(pk=email_stage_pk) |             stages = stage_class.objects.filter(pk=email_stage_pk) | ||||||
|             if not stages.exists(): |             if not stages.exists(): | ||||||
|                 self.set_status( |                 self.set_status( | ||||||
| @ -101,6 +104,13 @@ def send_mail( | |||||||
|         # can't be converted to json) |         # can't be converted to json) | ||||||
|         message_object.attach(logo_data()) |         message_object.attach(logo_data()) | ||||||
|  |  | ||||||
|  |         if ( | ||||||
|  |             message_object.to | ||||||
|  |             and isinstance(message_object.to[0], str) | ||||||
|  |             and "=?utf-8?" in message_object.to[0] | ||||||
|  |         ): | ||||||
|  |             message_object.to = [message_object.to[0].split("<")[-1].replace(">", "")] | ||||||
|  |  | ||||||
|         LOGGER.debug("Sending mail", to=message_object.to) |         LOGGER.debug("Sending mail", to=message_object.to) | ||||||
|         backend.send_messages([message_object]) |         backend.send_messages([message_object]) | ||||||
|         Event.new( |         Event.new( | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ from django.core.mail.backends.locmem import EmailBackend | |||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow | from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| from authentik.flows.markers import StageMarker | from authentik.flows.markers import StageMarker | ||||||
| from authentik.flows.models import FlowDesignation, FlowStageBinding | from authentik.flows.models import FlowDesignation, FlowStageBinding | ||||||
| @ -67,6 +67,67 @@ class TestEmailStageSending(FlowTestCase): | |||||||
|             self.assertEqual(event.context["to_email"], [f"{self.user.name} <{self.user.email}>"]) |             self.assertEqual(event.context["to_email"], [f"{self.user.name} <{self.user.email}>"]) | ||||||
|             self.assertEqual(event.context["from_email"], "system@authentik.local") |             self.assertEqual(event.context["from_email"], "system@authentik.local") | ||||||
|  |  | ||||||
|  |     def test_newlines_long_name(self): | ||||||
|  |         """Test with pending user""" | ||||||
|  |         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) | ||||||
|  |         long_user = create_test_user() | ||||||
|  |         long_user.name = "Test User\r\n Many Words\r\n" | ||||||
|  |         long_user.save() | ||||||
|  |         plan.context[PLAN_CONTEXT_PENDING_USER] = long_user | ||||||
|  |         session = self.client.session | ||||||
|  |         session[SESSION_KEY_PLAN] = plan | ||||||
|  |         session.save() | ||||||
|  |         Event.objects.filter(action=EventAction.EMAIL_SENT).delete() | ||||||
|  |  | ||||||
|  |         url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) | ||||||
|  |         with patch( | ||||||
|  |             "authentik.stages.email.models.EmailStage.backend_class", | ||||||
|  |             PropertyMock(return_value=EmailBackend), | ||||||
|  |         ): | ||||||
|  |             response = self.client.post(url) | ||||||
|  |             self.assertEqual(response.status_code, 200) | ||||||
|  |             self.assertStageResponse( | ||||||
|  |                 response, | ||||||
|  |                 self.flow, | ||||||
|  |                 response_errors={ | ||||||
|  |                     "non_field_errors": [{"string": "email-sent", "code": "email-sent"}] | ||||||
|  |                 }, | ||||||
|  |             ) | ||||||
|  |             self.assertEqual(len(mail.outbox), 1) | ||||||
|  |             self.assertEqual(mail.outbox[0].subject, "authentik") | ||||||
|  |             self.assertEqual(mail.outbox[0].to, [f"Test User   Many Words   <{long_user.email}>"]) | ||||||
|  |  | ||||||
|  |     def test_utf8_name(self): | ||||||
|  |         """Test with pending user""" | ||||||
|  |         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) | ||||||
|  |         utf8_user = create_test_user() | ||||||
|  |         utf8_user.name = "Cirilo ЉМНЊ el cirilico И̂ӢЙӤ " | ||||||
|  |         utf8_user.email = "cyrillic@authentik.local" | ||||||
|  |         utf8_user.save() | ||||||
|  |         plan.context[PLAN_CONTEXT_PENDING_USER] = utf8_user | ||||||
|  |         session = self.client.session | ||||||
|  |         session[SESSION_KEY_PLAN] = plan | ||||||
|  |         session.save() | ||||||
|  |         Event.objects.filter(action=EventAction.EMAIL_SENT).delete() | ||||||
|  |  | ||||||
|  |         url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) | ||||||
|  |         with patch( | ||||||
|  |             "authentik.stages.email.models.EmailStage.backend_class", | ||||||
|  |             PropertyMock(return_value=EmailBackend), | ||||||
|  |         ): | ||||||
|  |             response = self.client.post(url) | ||||||
|  |             self.assertEqual(response.status_code, 200) | ||||||
|  |             self.assertStageResponse( | ||||||
|  |                 response, | ||||||
|  |                 self.flow, | ||||||
|  |                 response_errors={ | ||||||
|  |                     "non_field_errors": [{"string": "email-sent", "code": "email-sent"}] | ||||||
|  |                 }, | ||||||
|  |             ) | ||||||
|  |             self.assertEqual(len(mail.outbox), 1) | ||||||
|  |             self.assertEqual(mail.outbox[0].subject, "authentik") | ||||||
|  |             self.assertEqual(mail.outbox[0].to, [f"{utf8_user.email}"]) | ||||||
|  |  | ||||||
|     def test_pending_fake_user(self): |     def test_pending_fake_user(self): | ||||||
|         """Test with pending (fake) user""" |         """Test with pending (fake) user""" | ||||||
|         self.flow.designation = FlowDesignation.RECOVERY |         self.flow.designation = FlowDesignation.RECOVERY | ||||||
|  | |||||||
							
								
								
									
										58
									
								
								authentik/stages/email/tests/test_tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								authentik/stages/email/tests/test_tasks.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | |||||||
|  | """Test email stage tasks""" | ||||||
|  |  | ||||||
|  | from unittest.mock import patch | ||||||
|  |  | ||||||
|  | from django.core.mail import EmailMultiAlternatives | ||||||
|  | from django.test import TestCase | ||||||
|  |  | ||||||
|  | from authentik.core.tests.utils import create_test_admin_user | ||||||
|  | from authentik.lib.utils.reflection import class_to_path | ||||||
|  | from authentik.stages.authenticator_email.models import AuthenticatorEmailStage | ||||||
|  | from authentik.stages.email.models import EmailStage | ||||||
|  | from authentik.stages.email.tasks import get_email_body, send_mails | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestEmailTasks(TestCase): | ||||||
|  |     """Test email stage tasks""" | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         self.user = create_test_admin_user() | ||||||
|  |         self.stage = EmailStage.objects.create( | ||||||
|  |             name="test-email", | ||||||
|  |             use_global_settings=True, | ||||||
|  |         ) | ||||||
|  |         self.auth_stage = AuthenticatorEmailStage.objects.create( | ||||||
|  |             name="test-auth-email", | ||||||
|  |             use_global_settings=True, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def test_get_email_body_html(self): | ||||||
|  |         """Test get_email_body with HTML alternative""" | ||||||
|  |         message = EmailMultiAlternatives() | ||||||
|  |         message.body = "plain text" | ||||||
|  |         message.attach_alternative("<p>html content</p>", "text/html") | ||||||
|  |         self.assertEqual(get_email_body(message), "<p>html content</p>") | ||||||
|  |  | ||||||
|  |     def test_get_email_body_plain(self): | ||||||
|  |         """Test get_email_body with plain text only""" | ||||||
|  |         message = EmailMultiAlternatives() | ||||||
|  |         message.body = "plain text" | ||||||
|  |         self.assertEqual(get_email_body(message), "plain text") | ||||||
|  |  | ||||||
|  |     def test_send_mails_email_stage(self): | ||||||
|  |         """Test send_mails with EmailStage""" | ||||||
|  |         message = EmailMultiAlternatives() | ||||||
|  |         with patch("authentik.stages.email.tasks.send_mail") as mock_send: | ||||||
|  |             send_mails(self.stage, message) | ||||||
|  |             mock_send.s.assert_called_once_with( | ||||||
|  |                 message.__dict__, class_to_path(EmailStage), str(self.stage.pk) | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |     def test_send_mails_authenticator_stage(self): | ||||||
|  |         """Test send_mails with AuthenticatorEmailStage""" | ||||||
|  |         message = EmailMultiAlternatives() | ||||||
|  |         with patch("authentik.stages.email.tasks.send_mail") as mock_send: | ||||||
|  |             send_mails(self.auth_stage, message) | ||||||
|  |             mock_send.s.assert_called_once_with( | ||||||
|  |                 message.__dict__, class_to_path(AuthenticatorEmailStage), str(self.auth_stage.pk) | ||||||
|  |             ) | ||||||
| @ -32,7 +32,14 @@ class TemplateEmailMessage(EmailMultiAlternatives): | |||||||
|         sanitized_to = [] |         sanitized_to = [] | ||||||
|         # Ensure that all recipients are valid |         # Ensure that all recipients are valid | ||||||
|         for recipient_name, recipient_email in to: |         for recipient_name, recipient_email in to: | ||||||
|             sanitized_to.append(sanitize_address((recipient_name, recipient_email), "utf-8")) |             # Remove any newline characters from name and email before sanitizing | ||||||
|  |             clean_name = ( | ||||||
|  |                 recipient_name.replace("\n", " ").replace("\r", " ") if recipient_name else "" | ||||||
|  |             ) | ||||||
|  |             clean_email = ( | ||||||
|  |                 recipient_email.replace("\n", "").replace("\r", "") if recipient_email else "" | ||||||
|  |             ) | ||||||
|  |             sanitized_to.append(sanitize_address((clean_name, clean_email), "utf-8")) | ||||||
|         super().__init__(to=sanitized_to, **kwargs) |         super().__init__(to=sanitized_to, **kwargs) | ||||||
|         if not template_name: |         if not template_name: | ||||||
|             return |             return | ||||||
|  | |||||||
| @ -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.0 Blueprint schema", |     "title": "authentik 2025.2.4 Blueprint schema", | ||||||
|     "required": [ |     "required": [ | ||||||
|         "version", |         "version", | ||||||
|         "entries" |         "entries" | ||||||
|  | |||||||
| @ -31,7 +31,7 @@ services: | |||||||
|     volumes: |     volumes: | ||||||
|       - redis:/data |       - redis:/data | ||||||
|   server: |   server: | ||||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.0} |     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.4} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: server |     command: server | ||||||
|     environment: |     environment: | ||||||
| @ -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.0} |     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.4} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: worker |     command: worker | ||||||
|     environment: |     environment: | ||||||
|  | |||||||
							
								
								
									
										24
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								go.mod
									
									
									
									
									
								
							| @ -1,8 +1,8 @@ | |||||||
| module goauthentik.io | module goauthentik.io | ||||||
|  |  | ||||||
| go 1.23.0 | go 1.23 | ||||||
|  |  | ||||||
| toolchain go1.24.0 | toolchain go1.23.0 | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	beryju.io/ldap v0.1.0 | 	beryju.io/ldap v0.1.0 | ||||||
| @ -22,16 +22,16 @@ require ( | |||||||
| 	github.com/mitchellh/mapstructure v1.5.0 | 	github.com/mitchellh/mapstructure v1.5.0 | ||||||
| 	github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 | 	github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 | ||||||
| 	github.com/pires/go-proxyproto v0.8.0 | 	github.com/pires/go-proxyproto v0.8.0 | ||||||
| 	github.com/prometheus/client_golang v1.21.0 | 	github.com/prometheus/client_golang v1.20.5 | ||||||
| 	github.com/redis/go-redis/v9 v9.7.1 | 	github.com/redis/go-redis/v9 v9.7.0 | ||||||
| 	github.com/sethvargo/go-envconfig v1.1.1 | 	github.com/sethvargo/go-envconfig v1.1.1 | ||||||
| 	github.com/sirupsen/logrus v1.9.3 | 	github.com/sirupsen/logrus v1.9.3 | ||||||
| 	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.2025020.1 | 	goauthentik.io/api/v3 v3.2024123.6 | ||||||
| 	golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab | 	golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab | ||||||
| 	golang.org/x/oauth2 v0.27.0 | 	golang.org/x/oauth2 v0.26.0 | ||||||
| 	golang.org/x/sync v0.11.0 | 	golang.org/x/sync v0.11.0 | ||||||
| 	gopkg.in/yaml.v2 v2.4.0 | 	gopkg.in/yaml.v2 v2.4.0 | ||||||
| 	layeh.com/radius v0.0.0-20210819152912-ad72663a72ab | 	layeh.com/radius v0.0.0-20210819152912-ad72663a72ab | ||||||
| @ -48,7 +48,7 @@ require ( | |||||||
| 	github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect | 	github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect | ||||||
| 	github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27 // indirect | 	github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27 // indirect | ||||||
| 	github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a // indirect | 	github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a // indirect | ||||||
| 	github.com/go-jose/go-jose/v4 v4.0.5 // indirect | 	github.com/go-jose/go-jose/v4 v4.0.2 // indirect | ||||||
| 	github.com/go-logr/logr v1.4.1 // indirect | 	github.com/go-logr/logr v1.4.1 // indirect | ||||||
| 	github.com/go-logr/stdr v1.2.2 // indirect | 	github.com/go-logr/stdr v1.2.2 // indirect | ||||||
| 	github.com/go-openapi/analysis v0.23.0 // indirect | 	github.com/go-openapi/analysis v0.23.0 // indirect | ||||||
| @ -62,23 +62,23 @@ require ( | |||||||
| 	github.com/go-openapi/validate v0.24.0 // indirect | 	github.com/go-openapi/validate v0.24.0 // indirect | ||||||
| 	github.com/inconshreveable/mousetrap v1.1.0 // indirect | 	github.com/inconshreveable/mousetrap v1.1.0 // indirect | ||||||
| 	github.com/josharian/intern v1.0.0 // indirect | 	github.com/josharian/intern v1.0.0 // indirect | ||||||
| 	github.com/klauspost/compress v1.17.11 // indirect | 	github.com/klauspost/compress v1.17.9 // indirect | ||||||
| 	github.com/mailru/easyjson v0.7.7 // indirect | 	github.com/mailru/easyjson v0.7.7 // indirect | ||||||
| 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect | 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect | ||||||
| 	github.com/oklog/ulid v1.3.1 // indirect | 	github.com/oklog/ulid v1.3.1 // indirect | ||||||
| 	github.com/opentracing/opentracing-go v1.2.0 // indirect | 	github.com/opentracing/opentracing-go v1.2.0 // indirect | ||||||
| 	github.com/pmezard/go-difflib v1.0.0 // indirect | 	github.com/pmezard/go-difflib v1.0.0 // indirect | ||||||
| 	github.com/prometheus/client_model v0.6.1 // indirect | 	github.com/prometheus/client_model v0.6.1 // indirect | ||||||
| 	github.com/prometheus/common v0.62.0 // indirect | 	github.com/prometheus/common v0.55.0 // indirect | ||||||
| 	github.com/prometheus/procfs v0.15.1 // indirect | 	github.com/prometheus/procfs v0.15.1 // indirect | ||||||
| 	github.com/spf13/pflag v1.0.6 // indirect | 	github.com/spf13/pflag v1.0.6 // indirect | ||||||
| 	go.mongodb.org/mongo-driver v1.14.0 // indirect | 	go.mongodb.org/mongo-driver v1.14.0 // indirect | ||||||
| 	go.opentelemetry.io/otel v1.24.0 // indirect | 	go.opentelemetry.io/otel v1.24.0 // indirect | ||||||
| 	go.opentelemetry.io/otel/metric v1.24.0 // indirect | 	go.opentelemetry.io/otel/metric v1.24.0 // indirect | ||||||
| 	go.opentelemetry.io/otel/trace v1.24.0 // indirect | 	go.opentelemetry.io/otel/trace v1.24.0 // indirect | ||||||
| 	golang.org/x/crypto v0.32.0 // indirect | 	golang.org/x/crypto v0.31.0 // indirect | ||||||
| 	golang.org/x/sys v0.29.0 // indirect | 	golang.org/x/sys v0.28.0 // indirect | ||||||
| 	golang.org/x/text v0.21.0 // indirect | 	golang.org/x/text v0.21.0 // indirect | ||||||
| 	google.golang.org/protobuf v1.36.1 // indirect | 	google.golang.org/protobuf v1.34.2 // indirect | ||||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||||
| ) | ) | ||||||
|  | |||||||
							
								
								
									
										38
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								go.sum
									
									
									
									
									
								
							| @ -84,8 +84,8 @@ github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27 h1:O6yi4xa9b2D | |||||||
| github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27/go.mod h1:AYvN8omj7nKLmbcXS2dyABYU6JB1Lz1bHmkkq1kf4I4= | github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27/go.mod h1:AYvN8omj7nKLmbcXS2dyABYU6JB1Lz1bHmkkq1kf4I4= | ||||||
| github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a h1:v6zMvHuY9yue4+QkG/HQ/W67wvtQmWJ4SDo9aK/GIno= | github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a h1:v6zMvHuY9yue4+QkG/HQ/W67wvtQmWJ4SDo9aK/GIno= | ||||||
| github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= | github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= | ||||||
| github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= | github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= | ||||||
| github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= | github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= | ||||||
| github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= | github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= | ||||||
| github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= | github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= | ||||||
| github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= | ||||||
| @ -207,8 +207,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF | |||||||
| github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= | ||||||
| github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= | ||||||
| github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= | ||||||
| github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= | github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= | ||||||
| github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= | github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= | ||||||
| github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= | ||||||
| github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | ||||||
| github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= | ||||||
| @ -239,17 +239,17 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | |||||||
| github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||||
| github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= | github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= | ||||||
| github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= | github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= | ||||||
| github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | ||||||
| github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= | ||||||
| github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= | ||||||
| github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= | github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= | ||||||
| github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= | github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= | ||||||
| github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= | ||||||
| github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= | ||||||
| github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc= | github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= | ||||||
| github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= | github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= | ||||||
| github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= | ||||||
| github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= | ||||||
| github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= | ||||||
| @ -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.2025020.1 h1:7922W4XiGif7lUCl2qlaeQJ3wSx1wDDDpXx8ryx0Hv0= | goauthentik.io/api/v3 v3.2024123.6 h1:AGOCa7Fc/9eONCPEW4sEhTiyEBvxN57Lfqz1zm6Gy98= | ||||||
| goauthentik.io/api/v3 v3.2025020.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= | goauthentik.io/api/v3 v3.2024123.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= | ||||||
| @ -312,9 +312,8 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 | |||||||
| golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= | ||||||
| golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= | ||||||
| golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= | ||||||
|  | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= | ||||||
| golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= | ||||||
| golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= |  | ||||||
| golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= |  | ||||||
| golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | ||||||
| golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | ||||||
| golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= | ||||||
| @ -394,8 +393,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr | |||||||
| golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | ||||||
| golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | ||||||
| golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= | ||||||
| golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= | golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= | ||||||
| golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= | golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= | ||||||
| golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| @ -448,9 +447,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |||||||
| golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||||
| golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||||
|  | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= | ||||||
| golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||||
| golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= |  | ||||||
| golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= |  | ||||||
| golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= | ||||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||||
| golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||||
| @ -597,8 +595,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 | |||||||
| google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= | ||||||
| google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= | ||||||
| google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= | ||||||
| google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= | ||||||
| google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= | ||||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
| gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||||||
|  | |||||||
| @ -29,4 +29,4 @@ func UserAgent() string { | |||||||
| 	return fmt.Sprintf("authentik@%s", FullVersion()) | 	return fmt.Sprintf("authentik@%s", FullVersion()) | ||||||
| } | } | ||||||
|  |  | ||||||
| const VERSION = "2025.2.0" | const VERSION = "2025.2.4" | ||||||
|  | |||||||
| @ -35,13 +35,19 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]]( | |||||||
| 	req PaginatorRequest[Treq, Tres], | 	req PaginatorRequest[Treq, Tres], | ||||||
| 	opts PaginatorOptions, | 	opts PaginatorOptions, | ||||||
| ) ([]Tobj, error) { | ) ([]Tobj, error) { | ||||||
|  | 	if opts.Logger == nil { | ||||||
|  | 		opts.Logger = log.NewEntry(log.StandardLogger()) | ||||||
|  | 	} | ||||||
| 	var bfreq, cfreq interface{} | 	var bfreq, cfreq interface{} | ||||||
| 	fetchOffset := func(page int32) (Tres, error) { | 	fetchOffset := func(page int32) (Tres, error) { | ||||||
| 		bfreq = req.Page(page) | 		bfreq = req.Page(page) | ||||||
| 		cfreq = bfreq.(PaginatorRequest[Treq, Tres]).PageSize(int32(opts.PageSize)) | 		cfreq = bfreq.(PaginatorRequest[Treq, Tres]).PageSize(int32(opts.PageSize)) | ||||||
| 		res, _, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute() | 		res, hres, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			opts.Logger.WithError(err).WithField("page", page).Warning("failed to fetch page") | 			opts.Logger.WithError(err).WithField("page", page).Warning("failed to fetch page") | ||||||
|  | 			if hres != nil && hres.StatusCode >= 400 && hres.StatusCode < 500 { | ||||||
|  | 				return res, err | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 		return res, err | 		return res, err | ||||||
| 	} | 	} | ||||||
| @ -51,6 +57,9 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]]( | |||||||
| 	for { | 	for { | ||||||
| 		apiObjects, err := fetchOffset(page) | 		apiObjects, err := fetchOffset(page) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | 			if page == 1 { | ||||||
|  | 				return objects, err | ||||||
|  | 			} | ||||||
| 			errs = append(errs, err) | 			errs = append(errs, err) | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -1,5 +1,64 @@ | |||||||
| package ak | package ak | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"net/http" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"goauthentik.io/api/v3" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type fakeAPIType struct{} | ||||||
|  |  | ||||||
|  | type fakeAPIResponse struct { | ||||||
|  | 	results    []fakeAPIType | ||||||
|  | 	pagination api.Pagination | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (fapi *fakeAPIResponse) GetResults() []fakeAPIType     { return fapi.results } | ||||||
|  | func (fapi *fakeAPIResponse) GetPagination() api.Pagination { return fapi.pagination } | ||||||
|  |  | ||||||
|  | type fakeAPIRequest struct { | ||||||
|  | 	res  *fakeAPIResponse | ||||||
|  | 	http *http.Response | ||||||
|  | 	err  error | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (fapi *fakeAPIRequest) Page(page int32) *fakeAPIRequest     { return fapi } | ||||||
|  | func (fapi *fakeAPIRequest) PageSize(size int32) *fakeAPIRequest { return fapi } | ||||||
|  | func (fapi *fakeAPIRequest) Execute() (*fakeAPIResponse, *http.Response, error) { | ||||||
|  | 	return fapi.res, fapi.http, fapi.err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Test_Simple(t *testing.T) { | ||||||
|  | 	req := &fakeAPIRequest{ | ||||||
|  | 		res: &fakeAPIResponse{ | ||||||
|  | 			results: []fakeAPIType{ | ||||||
|  | 				{}, | ||||||
|  | 			}, | ||||||
|  | 			pagination: api.Pagination{ | ||||||
|  | 				TotalPages: 1, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	res, err := Paginator(req, PaginatorOptions{}) | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	assert.Len(t, res, 1) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Test_BadRequest(t *testing.T) { | ||||||
|  | 	req := &fakeAPIRequest{ | ||||||
|  | 		http: &http.Response{ | ||||||
|  | 			StatusCode: 400, | ||||||
|  | 		}, | ||||||
|  | 		err: errors.New("foo"), | ||||||
|  | 	} | ||||||
|  | 	res, err := Paginator(req, PaginatorOptions{}) | ||||||
|  | 	assert.Error(t, err) | ||||||
|  | 	assert.Equal(t, []fakeAPIType{}, res) | ||||||
|  | } | ||||||
|  |  | ||||||
| // func Test_PaginatorCompile(t *testing.T) { | // func Test_PaginatorCompile(t *testing.T) { | ||||||
| // 	req := api.ApiCoreUsersListRequest{} | // 	req := api.ApiCoreUsersListRequest{} | ||||||
| // 	Paginator(req, PaginatorOptions{ | // 	Paginator(req, PaginatorOptions{ | ||||||
|  | |||||||
| @ -82,7 +82,8 @@ if [[ "$1" == "server" ]]; then | |||||||
|     run_authentik |     run_authentik | ||||||
| elif [[ "$1" == "worker" ]]; then | elif [[ "$1" == "worker" ]]; then | ||||||
|     set_mode "worker" |     set_mode "worker" | ||||||
|     check_if_root "python -m manage worker" |     shift | ||||||
|  |     check_if_root "python -m manage worker $@" | ||||||
| elif [[ "$1" == "worker-status" ]]; then | elif [[ "$1" == "worker-status" ]]; then | ||||||
|     wait_for_db |     wait_for_db | ||||||
|     celery -A authentik.root.celery flower \ |     celery -A authentik.root.celery flower \ | ||||||
|  | |||||||
							
								
								
									
										8
									
								
								lifecycle/aws/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								lifecycle/aws/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -9,7 +9,7 @@ | |||||||
|             "version": "0.0.0", |             "version": "0.0.0", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "devDependencies": { |             "devDependencies": { | ||||||
|                 "aws-cdk": "^2.1000.3", |                 "aws-cdk": "^2.179.0", | ||||||
|                 "cross-env": "^7.0.3" |                 "cross-env": "^7.0.3" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
| @ -17,9 +17,9 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/aws-cdk": { |         "node_modules/aws-cdk": { | ||||||
|             "version": "2.1000.3", |             "version": "2.179.0", | ||||||
|             "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1000.3.tgz", |             "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.179.0.tgz", | ||||||
|             "integrity": "sha512-y0sU603gGWpVTwqDw9MKVHg3e1t49Mvve6t3YDOvjeKY195Vu6dgHlHjW4h8n1vX04r49NKfpoApG60V8sMbdw==", |             "integrity": "sha512-aA2+8S2g4UBQHkUEt0mYd16VLt/ucR+QfyUJi34LDKRAhOCNDjPCZ4z9z/JEDyuni0BdzsYA55pnpDN9tMULpA==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "Apache-2.0", |             "license": "Apache-2.0", | ||||||
|             "bin": { |             "bin": { | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ | |||||||
|         "node": ">=20" |         "node": ">=20" | ||||||
|     }, |     }, | ||||||
|     "devDependencies": { |     "devDependencies": { | ||||||
|         "aws-cdk": "^2.1000.3", |         "aws-cdk": "^2.179.0", | ||||||
|         "cross-env": "^7.0.3" |         "cross-env": "^7.0.3" | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -26,7 +26,7 @@ Parameters: | |||||||
|     Description: authentik Docker image |     Description: authentik Docker image | ||||||
|   AuthentikVersion: |   AuthentikVersion: | ||||||
|     Type: String |     Type: String | ||||||
|     Default: 2025.2.0 |     Default: 2025.2.4 | ||||||
|     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-02-25 00:11+0000\n" | "POT-Creation-Date: 2025-02-14 14:49+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" | ||||||
| @ -109,10 +109,6 @@ msgstr "" | |||||||
| msgid "Extra description not available" | msgid "Extra description not available" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/core/api/groups.py |  | ||||||
| msgid "Cannot set group as parent of itself." |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/core/api/providers.py | #: authentik/core/api/providers.py | ||||||
| msgid "" | msgid "" | ||||||
| "When not set all providers are returned. When set to true, only backchannel " | "When not set all providers are returned. When set to true, only backchannel " | ||||||
| @ -156,14 +152,6 @@ msgstr "" | |||||||
| msgid "Remove user from group" | msgid "Remove user from group" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/core/models.py |  | ||||||
| msgid "Enable superuser status" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/core/models.py |  | ||||||
| msgid "Disable superuser status" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/core/models.py | #: authentik/core/models.py | ||||||
| msgid "User's display name." | msgid "User's display name." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -512,6 +500,57 @@ msgstr "" | |||||||
| msgid "Microsoft Entra Provider Mappings" | msgid "Microsoft Entra Provider Mappings" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/enterprise/providers/rac/models.py | ||||||
|  | #: authentik/stages/user_login/models.py | ||||||
|  | msgid "" | ||||||
|  | "Determines how long a session lasts. Default of 0 means that the sessions " | ||||||
|  | "lasts until the browser is closed. (Format: hours=-1;minutes=-2;seconds=-3)" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/enterprise/providers/rac/models.py | ||||||
|  | msgid "When set to true, connection tokens will be deleted upon disconnect." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/enterprise/providers/rac/models.py | ||||||
|  | msgid "RAC Provider" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/enterprise/providers/rac/models.py | ||||||
|  | msgid "RAC Providers" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/enterprise/providers/rac/models.py | ||||||
|  | msgid "RAC Endpoint" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/enterprise/providers/rac/models.py | ||||||
|  | msgid "RAC Endpoints" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/enterprise/providers/rac/models.py | ||||||
|  | msgid "RAC Provider Property Mapping" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/enterprise/providers/rac/models.py | ||||||
|  | msgid "RAC Provider Property Mappings" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/enterprise/providers/rac/models.py | ||||||
|  | msgid "RAC Connection token" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/enterprise/providers/rac/models.py | ||||||
|  | msgid "RAC Connection tokens" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/enterprise/providers/rac/views.py | ||||||
|  | msgid "Maximum connection limit reached." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/enterprise/providers/rac/views.py | ||||||
|  | msgid "(You are already connected in another tab/window)" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/enterprise/providers/ssf/models.py | #: authentik/enterprise/providers/ssf/models.py | ||||||
| #: authentik/providers/oauth2/models.py | #: authentik/providers/oauth2/models.py | ||||||
| msgid "Signing Key" | msgid "Signing Key" | ||||||
| @ -612,7 +651,7 @@ msgstr "" | |||||||
| msgid "Slack Webhook (Slack/Discord)" | msgid "Slack Webhook (Slack/Discord)" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/events/models.py authentik/stages/authenticator_validate/models.py | #: authentik/events/models.py | ||||||
| msgid "Email" | msgid "Email" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| @ -1066,14 +1105,6 @@ msgstr "" | |||||||
| msgid "Client IP is not in an allowed country." | msgid "Client IP is not in an allowed country." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/policies/geoip/models.py |  | ||||||
| msgid "Distance from previous authentication is larger than threshold." |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/policies/geoip/models.py |  | ||||||
| msgid "Distance is further than possible." |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/policies/geoip/models.py | #: authentik/policies/geoip/models.py | ||||||
| msgid "GeoIP Policy" | msgid "GeoIP Policy" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -1612,56 +1643,6 @@ msgstr "" | |||||||
| msgid "Proxy Providers" | msgid "Proxy Providers" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/providers/rac/models.py authentik/stages/user_login/models.py |  | ||||||
| msgid "" |  | ||||||
| "Determines how long a session lasts. Default of 0 means that the sessions " |  | ||||||
| "lasts until the browser is closed. (Format: hours=-1;minutes=-2;seconds=-3)" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/providers/rac/models.py |  | ||||||
| msgid "When set to true, connection tokens will be deleted upon disconnect." |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/providers/rac/models.py |  | ||||||
| msgid "RAC Provider" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/providers/rac/models.py |  | ||||||
| msgid "RAC Providers" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/providers/rac/models.py |  | ||||||
| msgid "RAC Endpoint" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/providers/rac/models.py |  | ||||||
| msgid "RAC Endpoints" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/providers/rac/models.py |  | ||||||
| msgid "RAC Provider Property Mapping" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/providers/rac/models.py |  | ||||||
| msgid "RAC Provider Property Mappings" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/providers/rac/models.py |  | ||||||
| msgid "RAC Connection token" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/providers/rac/models.py |  | ||||||
| msgid "RAC Connection tokens" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/providers/rac/views.py |  | ||||||
| msgid "Maximum connection limit reached." |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/providers/rac/views.py |  | ||||||
| msgid "(You are already connected in another tab/window)" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/providers/radius/models.py | #: authentik/providers/radius/models.py | ||||||
| msgid "Shared secret between clients and server to hash packets." | msgid "Shared secret between clients and server to hash packets." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2505,98 +2486,6 @@ msgstr "" | |||||||
| msgid "Duo Devices" | msgid "Duo Devices" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/models.py |  | ||||||
| msgid "Email OTP" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/models.py |  | ||||||
| #: authentik/stages/email/models.py |  | ||||||
| msgid "" |  | ||||||
| "When enabled, global Email connection settings will be used and connection " |  | ||||||
| "settings below will be ignored." |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/models.py |  | ||||||
| msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)." |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/models.py |  | ||||||
| msgid "Email Authenticator Setup Stage" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/models.py |  | ||||||
| msgid "Email Authenticator Setup Stages" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/models.py |  | ||||||
| #: authentik/stages/authenticator_email/stage.py |  | ||||||
| #: authentik/stages/email/stage.py |  | ||||||
| msgid "Exception occurred while rendering E-mail template" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/models.py |  | ||||||
| msgid "Email Device" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/models.py |  | ||||||
| msgid "Email Devices" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/stage.py |  | ||||||
| #: authentik/stages/authenticator_sms/stage.py |  | ||||||
| #: authentik/stages/authenticator_totp/stage.py |  | ||||||
| msgid "Code does not match" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/stage.py |  | ||||||
| msgid "Invalid email" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/templates/email/email_otp.html |  | ||||||
| #: authentik/stages/email/templates/email/password_reset.html |  | ||||||
| #, python-format |  | ||||||
| msgid "" |  | ||||||
| "\n" |  | ||||||
| "      Hi %(username)s,\n" |  | ||||||
| "      " |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/templates/email/email_otp.html |  | ||||||
| msgid "" |  | ||||||
| "\n" |  | ||||||
| "          Email MFA code.\n" |  | ||||||
| "          " |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/templates/email/email_otp.html |  | ||||||
| #, python-format |  | ||||||
| msgid "" |  | ||||||
| "\n" |  | ||||||
| "    If you did not request this code, please ignore this email. The code " |  | ||||||
| "above is valid for %(expires)s.\n" |  | ||||||
| "    " |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/templates/email/email_otp.txt |  | ||||||
| #: authentik/stages/email/templates/email/password_reset.txt |  | ||||||
| #, python-format |  | ||||||
| msgid "Hi %(username)s," |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/templates/email/email_otp.txt |  | ||||||
| msgid "" |  | ||||||
| "\n" |  | ||||||
| "Email MFA code\n" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_email/templates/email/email_otp.txt |  | ||||||
| #, python-format |  | ||||||
| msgid "" |  | ||||||
| "\n" |  | ||||||
| "If you did not request this code, please ignore this email. The code above " |  | ||||||
| "is valid for %(expires)s.\n" |  | ||||||
| msgstr "" |  | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_sms/models.py | #: authentik/stages/authenticator_sms/models.py | ||||||
| msgid "" | msgid "" | ||||||
| "When enabled, the Phone number is only used during enrollment to verify the " | "When enabled, the Phone number is only used during enrollment to verify the " | ||||||
| @ -2629,6 +2518,11 @@ msgstr "" | |||||||
| msgid "SMS Devices" | msgid "SMS Devices" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/stages/authenticator_sms/stage.py | ||||||
|  | #: authentik/stages/authenticator_totp/stage.py | ||||||
|  | msgid "Code does not match" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/authenticator_sms/stage.py | #: authentik/stages/authenticator_sms/stage.py | ||||||
| msgid "Invalid phone number" | msgid "Invalid phone number" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2851,6 +2745,12 @@ msgstr "" | |||||||
| msgid "Account Confirmation" | msgid "Account Confirmation" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/stages/email/models.py | ||||||
|  | msgid "" | ||||||
|  | "When enabled, global Email connection settings will be used and connection " | ||||||
|  | "settings below will be ignored." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/email/models.py | #: authentik/stages/email/models.py | ||||||
| msgid "Activate users upon completion of stage." | msgid "Activate users upon completion of stage." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2867,6 +2767,10 @@ msgstr "" | |||||||
| msgid "Email Stages" | msgid "Email Stages" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/stages/email/stage.py | ||||||
|  | msgid "Exception occurred while rendering E-mail template" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/email/stage.py | #: authentik/stages/email/stage.py | ||||||
| msgid "Successfully verified Email." | msgid "Successfully verified Email." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2941,6 +2845,14 @@ msgid "" | |||||||
| "This email was sent from the notification transport %(name)s.\n" | "This email was sent from the notification transport %(name)s.\n" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/stages/email/templates/email/password_reset.html | ||||||
|  | #, python-format | ||||||
|  | msgid "" | ||||||
|  | "\n" | ||||||
|  | "      Hi %(username)s,\n" | ||||||
|  | "      " | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/email/templates/email/password_reset.html | #: authentik/stages/email/templates/email/password_reset.html | ||||||
| msgid "" | msgid "" | ||||||
| "\n" | "\n" | ||||||
| @ -2958,6 +2870,11 @@ msgid "" | |||||||
| "    " | "    " | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/stages/email/templates/email/password_reset.txt | ||||||
|  | #, python-format | ||||||
|  | msgid "Hi %(username)s," | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/stages/email/templates/email/password_reset.txt | #: authentik/stages/email/templates/email/password_reset.txt | ||||||
| msgid "" | msgid "" | ||||||
| "\n" | "\n" | ||||||
|  | |||||||
										
											Binary file not shown.
										
									
								
							| @ -1,5 +1,5 @@ | |||||||
| { | { | ||||||
|     "name": "@goauthentik/authentik", |     "name": "@goauthentik/authentik", | ||||||
|     "version": "2025.2.0", |     "version": "2025.2.4", | ||||||
|     "private": true |     "private": true | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										634
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										634
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,6 +1,6 @@ | |||||||
| [tool.poetry] | [tool.poetry] | ||||||
| name = "authentik" | name = "authentik" | ||||||
| version = "2025.2.0" | version = "2025.2.4" | ||||||
| description = "" | description = "" | ||||||
| authors = ["authentik Team <hello@goauthentik.io>"] | authors = ["authentik Team <hello@goauthentik.io>"] | ||||||
|  |  | ||||||
| @ -91,7 +91,7 @@ cryptography = "*" | |||||||
| dacite = "*" | dacite = "*" | ||||||
| deepmerge = "*" | deepmerge = "*" | ||||||
| defusedxml = "*" | defusedxml = "*" | ||||||
| django = "*" | django = "5.0.14" | ||||||
| django-countries = "*" | django-countries = "*" | ||||||
| django-cte = "*" | django-cte = "*" | ||||||
| django-filter = "*" | django-filter = "*" | ||||||
| @ -123,7 +123,7 @@ kubernetes = "*" | |||||||
| ldap3 = "*" | ldap3 = "*" | ||||||
| lxml = "*" | lxml = "*" | ||||||
| msgraph-sdk = "*" | msgraph-sdk = "*" | ||||||
| opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf", extras = ["reggie"] } | opencontainers = { git = "https://github.com/BeryJu/oci-python", rev = "c791b19056769cd67957322806809ab70f5bead8", extras = ["reggie"] } | ||||||
| packaging = "*" | packaging = "*" | ||||||
| paramiko = "*" | paramiko = "*" | ||||||
| psycopg = { extras = ["c"], version = "*" } | psycopg = { extras = ["c"], version = "*" } | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| openapi: 3.0.3 | openapi: 3.0.3 | ||||||
| info: | info: | ||||||
|   title: authentik |   title: authentik | ||||||
|   version: 2025.2.0 |   version: 2025.2.4 | ||||||
|   description: Making authentication simple. |   description: Making authentication simple. | ||||||
|   contact: |   contact: | ||||||
|     email: hello@goauthentik.io |     email: hello@goauthentik.io | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ This package provides a generated API Client for [authentik](https://goauthentik | |||||||
|  |  | ||||||
| ### Building | ### Building | ||||||
|  |  | ||||||
| See https://docs.goauthentik.io/docs/developer-docs/api/making-schema-changes#building-the-web-client | See https://docs.goauthentik.io/docs/developer-docs/making-schema-changes | ||||||
|  |  | ||||||
| ### Consuming | ### Consuming | ||||||
|  |  | ||||||
|  | |||||||
| @ -88,11 +88,7 @@ const baseArgs = { | |||||||
|     treeShaking: true, |     treeShaking: true, | ||||||
|     external: ["*.woff", "*.woff2"], |     external: ["*.woff", "*.woff2"], | ||||||
|     tsconfig: "./tsconfig.json", |     tsconfig: "./tsconfig.json", | ||||||
|     loader: { |     loader: { ".css": "text", ".md": "text" }, | ||||||
|         ".css": "text", |  | ||||||
|         ".md": "text", |  | ||||||
|         ".mdx": "text", |  | ||||||
|     }, |  | ||||||
|     define: definitions, |     define: definitions, | ||||||
|     format: "esm", |     format: "esm", | ||||||
|     logOverride: { |     logOverride: { | ||||||
|  | |||||||
							
								
								
									
										8
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -23,7 +23,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.0-1740418530", |                 "@goauthentik/api": "^2024.12.3-1739965710", | ||||||
|                 "@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", | ||||||
| @ -1814,9 +1814,9 @@ | |||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@goauthentik/api": { |         "node_modules/@goauthentik/api": { | ||||||
|             "version": "2025.2.0-1740418530", |             "version": "2024.12.3-1739965710", | ||||||
|             "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.2.0-1740418530.tgz", |             "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.12.3-1739965710.tgz", | ||||||
|             "integrity": "sha512-vFoIzmEuQ7sbWxIEFP7l7OwEMt8M9TqvxScyv0liQSgGd/xanc2W/R+JuOdhq9ePrCfXa1YcmuZtT41HZXFP6g==" |             "integrity": "sha512-16zoQWeJhAFSwttvqLRoXoQA43tMW1ZXDEihW6r8rtWtlxqPh7n36RtcWYraYiLcjmJskI90zdgz6k1kmY5AXw==" | ||||||
|         }, |         }, | ||||||
|         "node_modules/@goauthentik/web": { |         "node_modules/@goauthentik/web": { | ||||||
|             "resolved": "", |             "resolved": "", | ||||||
|  | |||||||
| @ -11,7 +11,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.0-1740418530", |         "@goauthentik/api": "^2024.12.3-1739965710", | ||||||
|         "@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", | ||||||
|  | |||||||
| @ -94,7 +94,7 @@ export class ApplicationEntitlementsPage extends Table<ApplicationEntitlement> { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderExpanded(item: ApplicationEntitlement): TemplateResult { |     renderExpanded(item: ApplicationEntitlement): TemplateResult { | ||||||
|         return html` <td></td> |         return html`<td></td> | ||||||
|             <td role="cell" colspan="4"> |             <td role="cell" colspan="4"> | ||||||
|                 <div class="pf-c-table__expandable-row-content"> |                 <div class="pf-c-table__expandable-row-content"> | ||||||
|                     <div class="pf-c-content"> |                     <div class="pf-c-content"> | ||||||
|  | |||||||
| @ -58,7 +58,7 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep { | |||||||
|     get bindingsAsColumns() { |     get bindingsAsColumns() { | ||||||
|         return this.wizard.bindings.map((binding, index) => { |         return this.wizard.bindings.map((binding, index) => { | ||||||
|             const { order, enabled, timeout } = binding; |             const { order, enabled, timeout } = binding; | ||||||
|             const isSet = P.string.minLength(1); |             const isSet = P.union(P.string.minLength(1), P.number); | ||||||
|             const policy = match(binding) |             const policy = match(binding) | ||||||
|                 .with({ policy: isSet }, (v) => msg(str`Policy ${v.policyObj?.name}`)) |                 .with({ policy: isSet }, (v) => msg(str`Policy ${v.policyObj?.name}`)) | ||||||
|                 .with({ group: isSet }, (v) => msg(str`Group ${v.groupObj?.name}`)) |                 .with({ group: isSet }, (v) => msg(str`Group ${v.groupObj?.name}`)) | ||||||
|  | |||||||
| @ -21,12 +21,22 @@ export class RelatedApplicationButton extends AKElement { | |||||||
|     @property({ attribute: false }) |     @property({ attribute: false }) | ||||||
|     provider?: Provider; |     provider?: Provider; | ||||||
|  |  | ||||||
|  |     @property() | ||||||
|  |     mode: "primary" | "backchannel" = "primary"; | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
|         if (this.provider?.assignedApplicationSlug) { |         if (this.mode === "primary" && this.provider?.assignedApplicationSlug) { | ||||||
|             return html`<a href="#/core/applications/${this.provider.assignedApplicationSlug}"> |             return html`<a href="#/core/applications/${this.provider.assignedApplicationSlug}"> | ||||||
|                 ${this.provider.assignedApplicationName} |                 ${this.provider.assignedApplicationName} | ||||||
|             </a>`; |             </a>`; | ||||||
|         } |         } | ||||||
|  |         if (this.mode === "backchannel" && this.provider?.assignedBackchannelApplicationSlug) { | ||||||
|  |             return html`<a | ||||||
|  |                 href="#/core/applications/${this.provider.assignedBackchannelApplicationSlug}" | ||||||
|  |             > | ||||||
|  |                 ${this.provider.assignedBackchannelApplicationName} | ||||||
|  |             </a>`; | ||||||
|  |         } | ||||||
|         return html`<ak-forms-modal> |         return html`<ak-forms-modal> | ||||||
|             <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> | ||||||
|  | |||||||
| @ -7,10 +7,10 @@ import { EVENT_REFRESH } from "@goauthentik/common/constants"; | |||||||
| 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/Markdown"; | import "@goauthentik/elements/Markdown"; | ||||||
| import "@goauthentik/elements/SyncStatusCard"; |  | ||||||
| import "@goauthentik/elements/Tabs"; | import "@goauthentik/elements/Tabs"; | ||||||
| import "@goauthentik/elements/buttons/ActionButton"; | import "@goauthentik/elements/buttons/ActionButton"; | ||||||
| import "@goauthentik/elements/buttons/ModalButton"; | import "@goauthentik/elements/buttons/ModalButton"; | ||||||
|  | import "@goauthentik/elements/sync/SyncStatusCard"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { CSSResult, PropertyValues, TemplateResult, html } from "lit"; | import { CSSResult, PropertyValues, TemplateResult, html } from "lit"; | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | |||||||
| import { EVENT_REFRESH } from "@goauthentik/common/constants"; | import { EVENT_REFRESH } from "@goauthentik/common/constants"; | ||||||
| import renderDescriptionList from "@goauthentik/components/DescriptionList"; | import renderDescriptionList from "@goauthentik/components/DescriptionList"; | ||||||
| import "@goauthentik/components/events/ObjectChangelog"; | import "@goauthentik/components/events/ObjectChangelog"; | ||||||
| import MDProviderOAuth2 from "@goauthentik/docs/add-secure-apps/providers/oauth2/index.mdx"; | import MDProviderOAuth2 from "@goauthentik/docs/add-secure-apps/providers/oauth2/index.md"; | ||||||
| import { AKElement } from "@goauthentik/elements/Base"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
| import "@goauthentik/elements/CodeMirror"; | import "@goauthentik/elements/CodeMirror"; | ||||||
| import "@goauthentik/elements/EmptyState"; | import "@goauthentik/elements/EmptyState"; | ||||||
|  | |||||||
| @ -13,7 +13,7 @@ import MDNginxStandalone from "@goauthentik/docs/add-secure-apps/providers/proxy | |||||||
| import MDTraefikCompose from "@goauthentik/docs/add-secure-apps/providers/proxy/_traefik_compose.md"; | import MDTraefikCompose from "@goauthentik/docs/add-secure-apps/providers/proxy/_traefik_compose.md"; | ||||||
| import MDTraefikIngress from "@goauthentik/docs/add-secure-apps/providers/proxy/_traefik_ingress.md"; | import MDTraefikIngress from "@goauthentik/docs/add-secure-apps/providers/proxy/_traefik_ingress.md"; | ||||||
| import MDTraefikStandalone from "@goauthentik/docs/add-secure-apps/providers/proxy/_traefik_standalone.md"; | import MDTraefikStandalone from "@goauthentik/docs/add-secure-apps/providers/proxy/_traefik_standalone.md"; | ||||||
| import MDHeaderAuthentication from "@goauthentik/docs/add-secure-apps/providers/proxy/header_authentication.mdx"; | import MDHeaderAuthentication from "@goauthentik/docs/add-secure-apps/providers/proxy/header_authentication.md"; | ||||||
| import { AKElement } from "@goauthentik/elements/Base"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
| import "@goauthentik/elements/CodeMirror"; | import "@goauthentik/elements/CodeMirror"; | ||||||
| import "@goauthentik/elements/Markdown"; | import "@goauthentik/elements/Markdown"; | ||||||
| @ -118,7 +118,7 @@ export class ProxyProviderViewPage extends AKElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderConfig(): TemplateResult { |     renderConfig(): TemplateResult { | ||||||
|         const servers = [ |         const serves = [ | ||||||
|             { |             { | ||||||
|                 label: msg("Nginx (Ingress)"), |                 label: msg("Nginx (Ingress)"), | ||||||
|                 md: MDNginxIngress, |                 md: MDNginxIngress, | ||||||
| @ -184,7 +184,7 @@ export class ProxyProviderViewPage extends AKElement { | |||||||
|             }, |             }, | ||||||
|         ]; |         ]; | ||||||
|         return html`<ak-tabs pageIdentifier="proxy-setup"> |         return html`<ak-tabs pageIdentifier="proxy-setup"> | ||||||
|             ${servers.map((server) => { |             ${serves.map((server) => { | ||||||
|                 return html`<section |                 return html`<section | ||||||
|                     slot="page-${convertToSlug(server.label)}" |                     slot="page-${convertToSlug(server.label)}" | ||||||
|                     data-tab-title="${server.label}" |                     data-tab-title="${server.label}" | ||||||
|  | |||||||
| @ -9,10 +9,10 @@ import "@goauthentik/components/events/ObjectChangelog"; | |||||||
| import MDSCIMProvider from "@goauthentik/docs/add-secure-apps/providers/scim/index.md"; | import MDSCIMProvider from "@goauthentik/docs/add-secure-apps/providers/scim/index.md"; | ||||||
| import { AKElement } from "@goauthentik/elements/Base"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
| import "@goauthentik/elements/Markdown"; | import "@goauthentik/elements/Markdown"; | ||||||
| import "@goauthentik/elements/SyncStatusCard"; |  | ||||||
| import "@goauthentik/elements/Tabs"; | import "@goauthentik/elements/Tabs"; | ||||||
| import "@goauthentik/elements/buttons/ActionButton"; | import "@goauthentik/elements/buttons/ActionButton"; | ||||||
| import "@goauthentik/elements/buttons/ModalButton"; | import "@goauthentik/elements/buttons/ModalButton"; | ||||||
|  | import "@goauthentik/elements/sync/SyncStatusCard"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { CSSResult, PropertyValues, TemplateResult, html } from "lit"; | import { CSSResult, PropertyValues, TemplateResult, html } from "lit"; | ||||||
| @ -173,6 +173,7 @@ export class SCIMProviderViewPage extends AKElement { | |||||||
|                                     <dd class="pf-c-description-list__description"> |                                     <dd class="pf-c-description-list__description"> | ||||||
|                                         <div class="pf-c-description-list__text"> |                                         <div class="pf-c-description-list__text"> | ||||||
|                                             <ak-provider-related-application |                                             <ak-provider-related-application | ||||||
|  |                                                 mode="backchannel" | ||||||
|                                                 .provider=${this.provider} |                                                 .provider=${this.provider} | ||||||
|                                             ></ak-provider-related-application> |                                             ></ak-provider-related-application> | ||||||
|                                         </div> |                                         </div> | ||||||
|  | |||||||
| @ -8,11 +8,11 @@ import MDSourceKerberosBrowser from "@goauthentik/docs/users-sources/sources/pro | |||||||
| import { AKElement } from "@goauthentik/elements/Base"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
| import "@goauthentik/elements/CodeMirror"; | import "@goauthentik/elements/CodeMirror"; | ||||||
| import "@goauthentik/elements/Markdown"; | import "@goauthentik/elements/Markdown"; | ||||||
| import "@goauthentik/elements/SyncStatusCard"; |  | ||||||
| import "@goauthentik/elements/Tabs"; | import "@goauthentik/elements/Tabs"; | ||||||
| import "@goauthentik/elements/buttons/ActionButton"; | import "@goauthentik/elements/buttons/ActionButton"; | ||||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | import "@goauthentik/elements/buttons/SpinnerButton"; | ||||||
| import "@goauthentik/elements/forms/ModalForm"; | import "@goauthentik/elements/forms/ModalForm"; | ||||||
|  | import "@goauthentik/elements/sync/SyncStatusCard"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { CSSResult, TemplateResult, html } from "lit"; | import { CSSResult, TemplateResult, html } from "lit"; | ||||||
|  | |||||||
| @ -6,11 +6,11 @@ import { EVENT_REFRESH } from "@goauthentik/common/constants"; | |||||||
| 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/CodeMirror"; | import "@goauthentik/elements/CodeMirror"; | ||||||
| import "@goauthentik/elements/SyncStatusCard"; |  | ||||||
| import "@goauthentik/elements/Tabs"; | import "@goauthentik/elements/Tabs"; | ||||||
| import "@goauthentik/elements/buttons/ActionButton"; | import "@goauthentik/elements/buttons/ActionButton"; | ||||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | import "@goauthentik/elements/buttons/SpinnerButton"; | ||||||
| import "@goauthentik/elements/forms/ModalForm"; | import "@goauthentik/elements/forms/ModalForm"; | ||||||
|  | import "@goauthentik/elements/sync/SyncStatusCard"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { CSSResult, TemplateResult, html } from "lit"; | import { CSSResult, TemplateResult, html } from "lit"; | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success"; | |||||||
| export const ERROR_CLASS = "pf-m-danger"; | export const ERROR_CLASS = "pf-m-danger"; | ||||||
| export const PROGRESS_CLASS = "pf-m-in-progress"; | export const PROGRESS_CLASS = "pf-m-in-progress"; | ||||||
| export const CURRENT_CLASS = "pf-m-current"; | export const CURRENT_CLASS = "pf-m-current"; | ||||||
| export const VERSION = "2025.2.0"; | export const VERSION = "2025.2.4"; | ||||||
| export const TITLE_DEFAULT = "authentik"; | export const TITLE_DEFAULT = "authentik"; | ||||||
| export const ROUTE_SEPARATOR = ";"; | export const ROUTE_SEPARATOR = ";"; | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| import { AkControlElement } from "@goauthentik/elements/AkControlElement.js"; | import { AkControlElement } from "@goauthentik/elements/AkControlElement.js"; | ||||||
| import { debounce } from "@goauthentik/elements/utils/debounce"; | import { debounce } from "@goauthentik/elements/utils/debounce"; | ||||||
|  | import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { PropertyValues, html } from "lit"; | import { PropertyValues, html } from "lit"; | ||||||
| @ -11,11 +12,6 @@ import type { Pagination } from "@goauthentik/api"; | |||||||
|  |  | ||||||
| import "./ak-dual-select"; | import "./ak-dual-select"; | ||||||
| import { AkDualSelect } from "./ak-dual-select"; | import { AkDualSelect } from "./ak-dual-select"; | ||||||
| import { |  | ||||||
|     DualSelectChangeEvent, |  | ||||||
|     DualSelectPaginatorNavEvent, |  | ||||||
|     DualSelectSearchEvent, |  | ||||||
| } from "./events"; |  | ||||||
| import type { DataProvider, DualSelectPair } from "./types"; | import type { DataProvider, DualSelectPair } from "./types"; | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @ -30,7 +26,7 @@ import type { DataProvider, DualSelectPair } from "./types"; | |||||||
|  */ |  */ | ||||||
|  |  | ||||||
| @customElement("ak-dual-select-provider") | @customElement("ak-dual-select-provider") | ||||||
| export class AkDualSelectProvider extends AkControlElement { | export class AkDualSelectProvider extends CustomListenerElement(AkControlElement) { | ||||||
|     /** A function that takes a page and returns the DualSelectPair[] collection with which to update |     /** A function that takes a page and returns the DualSelectPair[] collection with which to update | ||||||
|      * the "Available" pane. |      * the "Available" pane. | ||||||
|      * |      * | ||||||
| @ -90,9 +86,9 @@ export class AkDualSelectProvider extends AkControlElement { | |||||||
|         this.onNav = this.onNav.bind(this); |         this.onNav = this.onNav.bind(this); | ||||||
|         this.onChange = this.onChange.bind(this); |         this.onChange = this.onChange.bind(this); | ||||||
|         this.onSearch = this.onSearch.bind(this); |         this.onSearch = this.onSearch.bind(this); | ||||||
|         this.addEventListener(DualSelectPaginatorNavEvent.eventName, this.onNav); |         this.addCustomListener("ak-pagination-nav-to", this.onNav); | ||||||
|         this.addEventListener(DualSelectSearchEvent.eventName, this.onSearch); |         this.addCustomListener("ak-dual-select-change", this.onChange); | ||||||
|         this.addEventListener(DualSelectChangeEvent.eventName, this.onChange); |         this.addCustomListener("ak-dual-select-search", this.onSearch); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     willUpdate(changedProperties: PropertyValues<this>) { |     willUpdate(changedProperties: PropertyValues<this>) { | ||||||
| @ -126,16 +122,26 @@ export class AkDualSelectProvider extends AkControlElement { | |||||||
|         this.isLoading = false; |         this.isLoading = false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     onNav(event: DualSelectPaginatorNavEvent) { |     onNav(event: Event) { | ||||||
|         this.fetch(event.page); |         if (!(event instanceof CustomEvent)) { | ||||||
|  |             throw new Error(`Expecting a CustomEvent for navigation, received ${event} instead`); | ||||||
|  |         } | ||||||
|  |         this.fetch(event.detail); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     onChange(event: DualSelectChangeEvent) { |     onChange(event: Event) { | ||||||
|         this.selected = this.internalSelected = event.selected; |         if (!(event instanceof CustomEvent)) { | ||||||
|  |             throw new Error(`Expecting a CustomEvent for change, received ${event} instead`); | ||||||
|  |         } | ||||||
|  |         this.internalSelected = event.detail.value; | ||||||
|  |         this.selected = this.internalSelected; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     onSearch(event: DualSelectSearchEvent) { |     onSearch(event: Event) { | ||||||
|         this.doSearch(event.search); |         if (!(event instanceof CustomEvent)) { | ||||||
|  |             throw new Error(`Expecting a CustomEvent for change, received ${event} instead`); | ||||||
|  |         } | ||||||
|  |         this.doSearch(event.detail); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     doSearch(search: string) { |     doSearch(search: string) { | ||||||
|  | |||||||
| @ -1,5 +1,8 @@ | |||||||
| import { AKElement } from "@goauthentik/elements/Base"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
| import { match } from "ts-pattern"; | import { | ||||||
|  |     CustomEmitterElement, | ||||||
|  |     CustomListenerElement, | ||||||
|  | } from "@goauthentik/elements/utils/eventEmitter"; | ||||||
|  |  | ||||||
| import { msg, str } from "@lit/localize"; | import { msg, str } from "@lit/localize"; | ||||||
| import { PropertyValues, html, nothing } from "lit"; | import { PropertyValues, html, nothing } from "lit"; | ||||||
| @ -20,13 +23,15 @@ import { AkDualSelectSelectedPane } from "./components/ak-dual-select-selected-p | |||||||
| import "./components/ak-pagination"; | import "./components/ak-pagination"; | ||||||
| import "./components/ak-search-bar"; | import "./components/ak-search-bar"; | ||||||
| import { | import { | ||||||
|     DualSelectChangeEvent, |     EVENT_ADD_ALL, | ||||||
|     DualSelectMoveRequestEvent, |     EVENT_ADD_ONE, | ||||||
|     DualSelectPanelSearchEvent, |     EVENT_ADD_SELECTED, | ||||||
|     DualSelectSearchEvent, |     EVENT_DELETE_ALL, | ||||||
|     DualSelectUpdateEvent, |     EVENT_REMOVE_ALL, | ||||||
| } from "./events"; |     EVENT_REMOVE_ONE, | ||||||
| import type { BasePagination, DualSelectPair } from "./types"; |     EVENT_REMOVE_SELECTED, | ||||||
|  | } from "./constants"; | ||||||
|  | import type { BasePagination, DualSelectPair, SearchbarEvent } from "./types"; | ||||||
|  |  | ||||||
| function alphaSort([_k1, v1, s1]: DualSelectPair, [_k2, v2, s2]: DualSelectPair) { | function alphaSort([_k1, v1, s1]: DualSelectPair, [_k2, v2, s2]: DualSelectPair) { | ||||||
|     const [l, r] = [s1 !== undefined ? s1 : v1, s2 !== undefined ? s2 : v2]; |     const [l, r] = [s1 !== undefined ? s1 : v1, s2 !== undefined ? s2 : v2]; | ||||||
| @ -55,7 +60,7 @@ const keyfinder = | |||||||
|         k === key; |         k === key; | ||||||
|  |  | ||||||
| @customElement("ak-dual-select") | @customElement("ak-dual-select") | ||||||
| export class AkDualSelect extends AKElement { | export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKElement)) { | ||||||
|     static get styles() { |     static get styles() { | ||||||
|         return styles; |         return styles; | ||||||
|     } |     } | ||||||
| @ -91,9 +96,21 @@ export class AkDualSelect extends AKElement { | |||||||
|         super(); |         super(); | ||||||
|         this.handleMove = this.handleMove.bind(this); |         this.handleMove = this.handleMove.bind(this); | ||||||
|         this.handleSearch = this.handleSearch.bind(this); |         this.handleSearch = this.handleSearch.bind(this); | ||||||
|         this.addEventListener(DualSelectMoveRequestEvent.eventName, this.handleMove); |         [ | ||||||
|         this.addEventListener(DualSelectUpdateEvent.eventName, () => this.requestUpdate()); |             EVENT_ADD_ALL, | ||||||
|         this.addEventListener(DualSelectPanelSearchEvent.eventName, this.handleSearch); |             EVENT_ADD_SELECTED, | ||||||
|  |             EVENT_DELETE_ALL, | ||||||
|  |             EVENT_REMOVE_ALL, | ||||||
|  |             EVENT_REMOVE_SELECTED, | ||||||
|  |             EVENT_ADD_ONE, | ||||||
|  |             EVENT_REMOVE_ONE, | ||||||
|  |         ].forEach((eventName: string) => { | ||||||
|  |             this.addCustomListener(eventName, (event: Event) => this.handleMove(eventName, event)); | ||||||
|  |         }); | ||||||
|  |         this.addCustomListener("ak-dual-select-move", () => { | ||||||
|  |             this.requestUpdate(); | ||||||
|  |         }); | ||||||
|  |         this.addCustomListener("ak-search", this.handleSearch); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     willUpdate(changedProperties: PropertyValues<this>) { |     willUpdate(changedProperties: PropertyValues<this>) { | ||||||
| @ -106,17 +123,47 @@ export class AkDualSelect extends AKElement { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     handleMove(event: DualSelectMoveRequestEvent) { |     handleMove(eventName: string, event: Event) { | ||||||
|         match(event.move) |         if (!(event instanceof CustomEvent)) { | ||||||
|             .with("add-all", () => this.addAllVisible()) |             throw new Error(`Expected move event here, got ${eventName}`); | ||||||
|             .with("add-one", () => this.addOne(event.key)) |         } | ||||||
|             .with("add-selected", () => this.addSelected()) |  | ||||||
|             .with("delete-all", () => this.removeAll()) |         switch (eventName) { | ||||||
|             .with("remove-all", () => this.removeAllVisible()) |             case EVENT_ADD_SELECTED: { | ||||||
|             .with("remove-one", () => this.removeOne(event.key)) |                 this.addSelected(); | ||||||
|             .with("remove-selected", () => this.removeSelected()) |                 break; | ||||||
|             .exhaustive(); |             } | ||||||
|         this.dispatchEvent(new DualSelectChangeEvent(this.value)); |             case EVENT_REMOVE_SELECTED: { | ||||||
|  |                 this.removeSelected(); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             case EVENT_ADD_ALL: { | ||||||
|  |                 this.addAllVisible(); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             case EVENT_REMOVE_ALL: { | ||||||
|  |                 this.removeAllVisible(); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             case EVENT_DELETE_ALL: { | ||||||
|  |                 this.removeAll(); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             case EVENT_ADD_ONE: { | ||||||
|  |                 this.addOne(event.detail); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             case EVENT_REMOVE_ONE: { | ||||||
|  |                 this.removeOne(event.detail); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             default: | ||||||
|  |                 throw new Error( | ||||||
|  |                     `AkDualSelect.handleMove received unknown event type: ${eventName}`, | ||||||
|  |                 ); | ||||||
|  |         } | ||||||
|  |         this.dispatchCustomEvent("ak-dual-select-change", { value: this.value }); | ||||||
|         event.stopPropagation(); |         event.stopPropagation(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @ -135,10 +182,7 @@ export class AkDualSelect extends AKElement { | |||||||
|         this.availablePane.value!.clearMove(); |         this.availablePane.value!.clearMove(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     addOne(key?: string) { |     addOne(key: string) { | ||||||
|         if (!key) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         const requested = this.options.find(keyfinder(key)); |         const requested = this.options.find(keyfinder(key)); | ||||||
|         if (requested && !this.selected.find(keyfinder(requested[0]))) { |         if (requested && !this.selected.find(keyfinder(requested[0]))) { | ||||||
|             this.selected = [...this.selected, requested]; |             this.selected = [...this.selected, requested]; | ||||||
| @ -163,10 +207,7 @@ export class AkDualSelect extends AKElement { | |||||||
|         this.selectedPane.value!.clearMove(); |         this.selectedPane.value!.clearMove(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     removeOne(key?: string) { |     removeOne(key: string) { | ||||||
|         if (!key) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         this.selected = this.selected.filter(([k]) => k !== key); |         this.selected = this.selected.filter(([k]) => k !== key); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @ -182,18 +223,18 @@ export class AkDualSelect extends AKElement { | |||||||
|         this.selectedPane.value!.clearMove(); |         this.selectedPane.value!.clearMove(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     handleSearch(event: DualSelectPanelSearchEvent) { |     handleSearch(event: SearchbarEvent) { | ||||||
|         switch (event.source) { |         switch (event.detail.source) { | ||||||
|             case "ak-dual-list-available-search": |             case "ak-dual-list-available-search": | ||||||
|                 return this.handleAvailableSearch(event.filterOn); |                 return this.handleAvailableSearch(event.detail.value); | ||||||
|             case "ak-dual-list-selected-search": |             case "ak-dual-list-selected-search": | ||||||
|                 return this.handleSelectedSearch(event.filterOn); |                 return this.handleSelectedSearch(event.detail.value); | ||||||
|         } |         } | ||||||
|         event.stopPropagation(); |         event.stopPropagation(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     handleAvailableSearch(value: string) { |     handleAvailableSearch(value: string) { | ||||||
|         this.dispatchEvent(new DualSelectSearchEvent(value)); |         this.dispatchCustomEvent("ak-dual-select-search", value); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     handleSelectedSearch(value: string) { |     handleSelectedSearch(value: string) { | ||||||
|  | |||||||
| @ -1,19 +1,26 @@ | |||||||
| import { bound } from "@goauthentik/elements/decorators/bound"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
|  | import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; | ||||||
|  |  | ||||||
| import { html, nothing } from "lit"; | import { html, nothing } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators.js"; | import { customElement, property, state } from "lit/decorators.js"; | ||||||
| import { classMap } from "lit/directives/class-map.js"; | import { classMap } from "lit/directives/class-map.js"; | ||||||
| import { map } from "lit/directives/map.js"; | import { map } from "lit/directives/map.js"; | ||||||
|  |  | ||||||
| import { availablePaneStyles } from "./styles.css"; | import { availablePaneStyles, listStyles } from "./styles.css"; | ||||||
|  | import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||||
|  | import PFDualListSelector from "@patternfly/patternfly/components/DualListSelector/dual-list-selector.css"; | ||||||
|  | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  |  | ||||||
| import { | import { EVENT_ADD_ONE } from "../constants"; | ||||||
|     DualSelectMoveAvailableEvent, |  | ||||||
|     DualSelectMoveRequestEvent, |  | ||||||
|     DualSelectUpdateEvent, |  | ||||||
| } from "../events"; |  | ||||||
| import type { DualSelectPair } from "../types"; | import type { DualSelectPair } from "../types"; | ||||||
| import { AkDualSelectAbstractPane } from "./ak-dual-select-pane"; |  | ||||||
|  | const styles = [PFBase, PFButton, PFDualListSelector, listStyles, availablePaneStyles]; | ||||||
|  |  | ||||||
|  | const hostAttributes = [ | ||||||
|  |     ["aria-labelledby", "dual-list-selector-available-pane-status"], | ||||||
|  |     ["aria-multiselectable", "true"], | ||||||
|  |     ["role", "listbox"], | ||||||
|  | ]; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @element ak-dual-select-available-panel |  * @element ak-dual-select-available-panel | ||||||
| @ -33,9 +40,9 @@ import { AkDualSelectAbstractPane } from "./ak-dual-select-pane"; | |||||||
|  * |  * | ||||||
|  */ |  */ | ||||||
| @customElement("ak-dual-select-available-pane") | @customElement("ak-dual-select-available-pane") | ||||||
| export class AkDualSelectAvailablePane extends AkDualSelectAbstractPane { | export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) { | ||||||
|     static get styles() { |     static get styles() { | ||||||
|         return [...AkDualSelectAbstractPane.styles, availablePaneStyles]; |         return styles; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /* The array of key/value pairs this pane is currently showing */ |     /* The array of key/value pairs this pane is currently showing */ | ||||||
| @ -49,31 +56,68 @@ export class AkDualSelectAvailablePane extends AkDualSelectAbstractPane { | |||||||
|     @property({ type: Object }) |     @property({ type: Object }) | ||||||
|     readonly selected: Set<string> = new Set(); |     readonly selected: Set<string> = new Set(); | ||||||
|  |  | ||||||
|     @bound |     /* This is the only mutator for this object. It collects the list of objects the user has | ||||||
|  |      * clicked on *in this pane*. It is explicitly marked as "public" to emphasize that the parent | ||||||
|  |      * orchestrator for the dual-select widget can and will access it to get the list of keys to be | ||||||
|  |      * moved (removed) if the user so requests. | ||||||
|  |      * | ||||||
|  |      */ | ||||||
|  |     @state() | ||||||
|  |     public toMove: Set<string> = new Set(); | ||||||
|  |  | ||||||
|  |     constructor() { | ||||||
|  |         super(); | ||||||
|  |         this.onClick = this.onClick.bind(this); | ||||||
|  |         this.onMove = this.onMove.bind(this); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     connectedCallback() { | ||||||
|  |         super.connectedCallback(); | ||||||
|  |         hostAttributes.forEach(([attr, value]) => { | ||||||
|  |             if (!this.hasAttribute(attr)) { | ||||||
|  |                 this.setAttribute(attr, value); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     clearMove() { | ||||||
|  |         this.toMove = new Set(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     onClick(key: string) { |     onClick(key: string) { | ||||||
|         if (this.selected.has(key)) { |         if (this.selected.has(key)) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         this.move(key); |         if (this.toMove.has(key)) { | ||||||
|         this.dispatchEvent(new DualSelectMoveAvailableEvent(this.moveable.sort())); |             this.toMove.delete(key); | ||||||
|         this.dispatchEvent(new DualSelectUpdateEvent()); |         } else { | ||||||
|  |             this.toMove.add(key); | ||||||
|  |         } | ||||||
|  |         this.dispatchCustomEvent( | ||||||
|  |             "ak-dual-select-available-move-changed", | ||||||
|  |             Array.from(this.toMove.values()).sort(), | ||||||
|  |         ); | ||||||
|  |         this.dispatchCustomEvent("ak-dual-select-move"); | ||||||
|         // Necessary because updating a map won't trigger a state change |         // Necessary because updating a map won't trigger a state change | ||||||
|         this.requestUpdate(); |         this.requestUpdate(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @bound |  | ||||||
|     onMove(key: string) { |     onMove(key: string) { | ||||||
|         this.toMove.delete(key); |         this.toMove.delete(key); | ||||||
|         this.dispatchEvent(new DualSelectMoveRequestEvent("add-one", key)); |         this.dispatchCustomEvent(EVENT_ADD_ONE, key); | ||||||
|         this.requestUpdate(); |         this.requestUpdate(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     get moveable() { | ||||||
|  |         return Array.from(this.toMove.values()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     // DO NOT use `Array.map()` instead of Lit's `map()` function. Lit's `map()` is object-aware and |     // DO NOT use `Array.map()` instead of Lit's `map()` function. Lit's `map()` is object-aware and | ||||||
|     // will not re-arrange or reconstruct the list automatically if the actual sources do not |     // will not re-arrange or reconstruct the list automatically if the actual sources do not | ||||||
|     // change; this allows the available pane to illustrate selected items with the checkmark |     // change; this allows the available pane to illustrate selected items with the checkmark | ||||||
|     // without causing the list to scroll back up to the top. |     // without causing the list to scroll back up to the top. | ||||||
|  |  | ||||||
|     override render() { |     render() { | ||||||
|         return html` |         return html` | ||||||
|             <div class="pf-c-dual-list-selector__menu"> |             <div class="pf-c-dual-list-selector__menu"> | ||||||
|                 <ul class="pf-c-dual-list-selector__list"> |                 <ul class="pf-c-dual-list-selector__list"> | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| import { AKElement } from "@goauthentik/elements/Base"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
|  | import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { css, html, nothing } from "lit"; | import { css, html, nothing } from "lit"; | ||||||
| @ -7,7 +8,13 @@ import { customElement, property } from "lit/decorators.js"; | |||||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  |  | ||||||
| import { DualSelectMoveRequestEvent, type MoveEventType } from "../events"; | import { | ||||||
|  |     EVENT_ADD_ALL, | ||||||
|  |     EVENT_ADD_SELECTED, | ||||||
|  |     EVENT_DELETE_ALL, | ||||||
|  |     EVENT_REMOVE_ALL, | ||||||
|  |     EVENT_REMOVE_SELECTED, | ||||||
|  | } from "../constants"; | ||||||
|  |  | ||||||
| const styles = [ | const styles = [ | ||||||
|     PFBase, |     PFBase, | ||||||
| @ -40,7 +47,7 @@ const styles = [ | |||||||
|  */ |  */ | ||||||
|  |  | ||||||
| @customElement("ak-dual-select-controls") | @customElement("ak-dual-select-controls") | ||||||
| export class AkDualSelectControls extends AKElement { | export class AkDualSelectControls extends CustomEmitterElement(AKElement) { | ||||||
|     static get styles() { |     static get styles() { | ||||||
|         return styles; |         return styles; | ||||||
|     } |     } | ||||||
| @ -89,11 +96,11 @@ export class AkDualSelectControls extends AKElement { | |||||||
|         this.onClick = this.onClick.bind(this); |         this.onClick = this.onClick.bind(this); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     onClick(eventName: MoveEventType) { |     onClick(eventName: string) { | ||||||
|         this.dispatchEvent(new DualSelectMoveRequestEvent(eventName)); |         this.dispatchCustomEvent(eventName); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderButton(label: string, event: MoveEventType, active: boolean, direction: string) { |     renderButton(label: string, event: string, active: boolean, direction: string) { | ||||||
|         return html` |         return html` | ||||||
|             <div class="pf-c-dual-list-selector__controls-item"> |             <div class="pf-c-dual-list-selector__controls-item"> | ||||||
|                 <button |                 <button | ||||||
| @ -114,18 +121,23 @@ export class AkDualSelectControls extends AKElement { | |||||||
|     render() { |     render() { | ||||||
|         return html` |         return html` | ||||||
|             <div class="ak-dual-list-selector__controls"> |             <div class="ak-dual-list-selector__controls"> | ||||||
|                 ${this.renderButton(msg("Add"), "add-selected", this.addActive, "fa-angle-right")} |                 ${this.renderButton( | ||||||
|  |                     msg("Add"), | ||||||
|  |                     EVENT_ADD_SELECTED, | ||||||
|  |                     this.addActive, | ||||||
|  |                     "fa-angle-right", | ||||||
|  |                 )} | ||||||
|                 ${this.selectAll |                 ${this.selectAll | ||||||
|                     ? html` |                     ? html` | ||||||
|                           ${this.renderButton( |                           ${this.renderButton( | ||||||
|                               msg("Add All Available"), |                               msg("Add All Available"), | ||||||
|                               "add-all", |                               EVENT_ADD_ALL, | ||||||
|                               this.addAllActive, |                               this.addAllActive, | ||||||
|                               "fa-angle-double-right", |                               "fa-angle-double-right", | ||||||
|                           )} |                           )} | ||||||
|                           ${this.renderButton( |                           ${this.renderButton( | ||||||
|                               msg("Remove All Available"), |                               msg("Remove All Available"), | ||||||
|                               "remove-all", |                               EVENT_REMOVE_ALL, | ||||||
|                               this.removeAllActive, |                               this.removeAllActive, | ||||||
|                               "fa-angle-double-left", |                               "fa-angle-double-left", | ||||||
|                           )} |                           )} | ||||||
| @ -133,14 +145,14 @@ export class AkDualSelectControls extends AKElement { | |||||||
|                     : nothing} |                     : nothing} | ||||||
|                 ${this.renderButton( |                 ${this.renderButton( | ||||||
|                     msg("Remove"), |                     msg("Remove"), | ||||||
|                     "remove-selected", |                     EVENT_REMOVE_SELECTED, | ||||||
|                     this.removeActive, |                     this.removeActive, | ||||||
|                     "fa-angle-left", |                     "fa-angle-left", | ||||||
|                 )} |                 )} | ||||||
|                 ${this.deleteAll |                 ${this.deleteAll | ||||||
|                     ? html`${this.renderButton( |                     ? html`${this.renderButton( | ||||||
|                           msg("Remove All"), |                           msg("Remove All"), | ||||||
|                           "delete-all", |                           EVENT_DELETE_ALL, | ||||||
|                           this.enableDeleteAll, |                           this.enableDeleteAll, | ||||||
|                           "fa-times", |                           "fa-times", | ||||||
|                       )}` |                       )}` | ||||||
|  | |||||||
| @ -1,75 +0,0 @@ | |||||||
| import { AKElement } from "@goauthentik/elements/Base"; |  | ||||||
|  |  | ||||||
| import { TemplateResult } from "lit"; |  | ||||||
| import { state } from "lit/decorators.js"; |  | ||||||
|  |  | ||||||
| import { listStyles } from "./styles.css"; |  | ||||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; |  | ||||||
| import PFDualListSelector from "@patternfly/patternfly/components/DualListSelector/dual-list-selector.css"; |  | ||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; |  | ||||||
|  |  | ||||||
| const styles = [PFBase, PFButton, PFDualListSelector, listStyles]; |  | ||||||
|  |  | ||||||
| const hostAttributes = [ |  | ||||||
|     ["aria-labelledby", "dual-list-selector-selected-pane-status"], |  | ||||||
|     ["aria-multiselectable", "true"], |  | ||||||
|     ["role", "listbox"], |  | ||||||
| ]; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @element ak-dual-select-panel |  | ||||||
|  * |  | ||||||
|  * The "selected options" or "right" pane in a dual-list multi-select.  It receives from its parent |  | ||||||
|  * a list of the selected options, and maintains an internal list of objects selected to move. |  | ||||||
|  * |  | ||||||
|  * @fires ak-dual-select-selected-move-changed - When the list of "to move" entries changed. |  | ||||||
|  * Includes the current `toMove` content. |  | ||||||
|  * |  | ||||||
|  * @fires ak-dual-select-remove-one - Double-click with the element clicked on. |  | ||||||
|  * |  | ||||||
|  * It is not expected that the `ak-dual-select-selected-move-changed` will be used; instead, the |  | ||||||
|  * attribute will be read by the parent when a control is clicked. |  | ||||||
|  * |  | ||||||
|  */ |  | ||||||
| export abstract class AkDualSelectAbstractPane extends AKElement { |  | ||||||
|     static get styles() { |  | ||||||
|         return styles; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /* |  | ||||||
|      * This is the only mutator for this object. It collects the list of objects the user has |  | ||||||
|      * clicked on *in this pane*. It is explicitly marked as "public" to emphasize that the parent |  | ||||||
|      * orchestrator for the dual-select widget can and will access it to get the list of keys to be |  | ||||||
|      * moved (removed) if the user so requests. |  | ||||||
|      * |  | ||||||
|      */ |  | ||||||
|     @state() |  | ||||||
|     public toMove: Set<string> = new Set(); |  | ||||||
|  |  | ||||||
|     connectedCallback() { |  | ||||||
|         super.connectedCallback(); |  | ||||||
|         hostAttributes.forEach(([attr, value]) => { |  | ||||||
|             if (!this.hasAttribute(attr)) { |  | ||||||
|                 this.setAttribute(attr, value); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     clearMove() { |  | ||||||
|         this.toMove = new Set(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     move(key: string) { |  | ||||||
|         if (this.toMove.has(key)) { |  | ||||||
|             this.toMove.delete(key); |  | ||||||
|         } else { |  | ||||||
|             this.toMove.add(key); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     get moveable() { |  | ||||||
|         return Array.from(this.toMove.values()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     abstract render(): TemplateResult; |  | ||||||
| } |  | ||||||
| @ -1,19 +1,26 @@ | |||||||
| import { bound } from "@goauthentik/elements/decorators/bound"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
|  | import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; | ||||||
|  |  | ||||||
| import { html } from "lit"; | import { html } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators.js"; | import { customElement, property, state } from "lit/decorators.js"; | ||||||
| import { classMap } from "lit/directives/class-map.js"; | import { classMap } from "lit/directives/class-map.js"; | ||||||
| import { map } from "lit/directives/map.js"; | import { map } from "lit/directives/map.js"; | ||||||
|  |  | ||||||
| import { selectedPaneStyles } from "./styles.css"; | import { listStyles, selectedPaneStyles } from "./styles.css"; | ||||||
|  | import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||||
|  | import PFDualListSelector from "@patternfly/patternfly/components/DualListSelector/dual-list-selector.css"; | ||||||
|  | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  |  | ||||||
| import { | import { EVENT_REMOVE_ONE } from "../constants"; | ||||||
|     DualSelectMoveRequestEvent, |  | ||||||
|     DualSelectMoveSelectedEvent, |  | ||||||
|     DualSelectUpdateEvent, |  | ||||||
| } from "../events"; |  | ||||||
| import type { DualSelectPair } from "../types"; | import type { DualSelectPair } from "../types"; | ||||||
| import { AkDualSelectAbstractPane } from "./ak-dual-select-pane"; |  | ||||||
|  | const styles = [PFBase, PFButton, PFDualListSelector, listStyles, selectedPaneStyles]; | ||||||
|  |  | ||||||
|  | const hostAttributes = [ | ||||||
|  |     ["aria-labelledby", "dual-list-selector-selected-pane-status"], | ||||||
|  |     ["aria-multiselectable", "true"], | ||||||
|  |     ["role", "listbox"], | ||||||
|  | ]; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @element ak-dual-select-available-panel |  * @element ak-dual-select-available-panel | ||||||
| @ -31,32 +38,70 @@ import { AkDualSelectAbstractPane } from "./ak-dual-select-pane"; | |||||||
|  * |  * | ||||||
|  */ |  */ | ||||||
| @customElement("ak-dual-select-selected-pane") | @customElement("ak-dual-select-selected-pane") | ||||||
| export class AkDualSelectSelectedPane extends AkDualSelectAbstractPane { | export class AkDualSelectSelectedPane extends CustomEmitterElement(AKElement) { | ||||||
|     static get styles() { |     static get styles() { | ||||||
|         return [...AkDualSelectAbstractPane.styles, selectedPaneStyles]; |         return styles; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /* The array of key/value pairs that are in the selected list.  ALL of them. */ |     /* The array of key/value pairs that are in the selected list.  ALL of them. */ | ||||||
|     @property({ type: Array }) |     @property({ type: Array }) | ||||||
|     readonly selected: DualSelectPair[] = []; |     readonly selected: DualSelectPair[] = []; | ||||||
|  |  | ||||||
|     @bound |     /* | ||||||
|  |      * This is the only mutator for this object. It collects the list of objects the user has | ||||||
|  |      * clicked on *in this pane*. It is explicitly marked as "public" to emphasize that the parent | ||||||
|  |      * orchestrator for the dual-select widget can and will access it to get the list of keys to be | ||||||
|  |      * moved (removed) if the user so requests. | ||||||
|  |      * | ||||||
|  |      */ | ||||||
|  |     @state() | ||||||
|  |     public toMove: Set<string> = new Set(); | ||||||
|  |  | ||||||
|  |     constructor() { | ||||||
|  |         super(); | ||||||
|  |         this.onClick = this.onClick.bind(this); | ||||||
|  |         this.onMove = this.onMove.bind(this); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     connectedCallback() { | ||||||
|  |         super.connectedCallback(); | ||||||
|  |         hostAttributes.forEach(([attr, value]) => { | ||||||
|  |             if (!this.hasAttribute(attr)) { | ||||||
|  |                 this.setAttribute(attr, value); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     clearMove() { | ||||||
|  |         this.toMove = new Set(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     onClick(key: string) { |     onClick(key: string) { | ||||||
|         this.move(key); |         if (this.toMove.has(key)) { | ||||||
|         this.dispatchEvent(new DualSelectMoveSelectedEvent(this.moveable.sort())); |             this.toMove.delete(key); | ||||||
|         this.dispatchEvent(new DualSelectUpdateEvent()); |         } else { | ||||||
|  |             this.toMove.add(key); | ||||||
|  |         } | ||||||
|  |         this.dispatchCustomEvent( | ||||||
|  |             "ak-dual-select-selected-move-changed", | ||||||
|  |             Array.from(this.toMove.values()).sort(), | ||||||
|  |         ); | ||||||
|  |         this.dispatchCustomEvent("ak-dual-select-move"); | ||||||
|         // Necessary because updating a map won't trigger a state change |         // Necessary because updating a map won't trigger a state change | ||||||
|         this.requestUpdate(); |         this.requestUpdate(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @bound |  | ||||||
|     onMove(key: string) { |     onMove(key: string) { | ||||||
|         this.toMove.delete(key); |         this.toMove.delete(key); | ||||||
|         this.dispatchEvent(new DualSelectMoveRequestEvent("remove-one", key)); |         this.dispatchCustomEvent(EVENT_REMOVE_ONE, key); | ||||||
|         this.requestUpdate(); |         this.requestUpdate(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override render() { |     get moveable() { | ||||||
|  |         return Array.from(this.toMove.values()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     render() { | ||||||
|         return html` |         return html` | ||||||
|             <div class="pf-c-dual-list-selector__menu"> |             <div class="pf-c-dual-list-selector__menu"> | ||||||
|                 <ul class="pf-c-dual-list-selector__list"> |                 <ul class="pf-c-dual-list-selector__list"> | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| import { AKElement } from "@goauthentik/elements/Base"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
|  | import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; | ||||||
|  |  | ||||||
| import { msg, str } from "@lit/localize"; | import { msg, str } from "@lit/localize"; | ||||||
| import { css, html, nothing } from "lit"; | import { css, html, nothing } from "lit"; | ||||||
| @ -8,7 +9,6 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css"; | |||||||
| import PFPagination from "@patternfly/patternfly/components/Pagination/pagination.css"; | import PFPagination from "@patternfly/patternfly/components/Pagination/pagination.css"; | ||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  |  | ||||||
| import { DualSelectPaginatorNavEvent } from "../events"; |  | ||||||
| import type { BasePagination } from "../types"; | import type { BasePagination } from "../types"; | ||||||
|  |  | ||||||
| const styles = [ | const styles = [ | ||||||
| @ -27,7 +27,7 @@ const styles = [ | |||||||
| ]; | ]; | ||||||
|  |  | ||||||
| @customElement("ak-pagination") | @customElement("ak-pagination") | ||||||
| export class AkPagination extends AKElement { | export class AkPagination extends CustomEmitterElement(AKElement) { | ||||||
|     static get styles() { |     static get styles() { | ||||||
|         return styles; |         return styles; | ||||||
|     } |     } | ||||||
| @ -41,7 +41,7 @@ export class AkPagination extends AKElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     onClick(nav: number | undefined) { |     onClick(nav: number | undefined) { | ||||||
|         this.dispatchEvent(new DualSelectPaginatorNavEvent(nav ?? 0)); |         this.dispatchCustomEvent("ak-pagination-nav-to", nav ?? 0); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     render() { |     render() { | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| import { AKElement } from "@goauthentik/elements/Base"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
|  | import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; | ||||||
|  |  | ||||||
| import { html } from "lit"; | import { html } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators.js"; | import { customElement, property } from "lit/decorators.js"; | ||||||
| @ -8,12 +9,12 @@ import type { Ref } from "lit/directives/ref.js"; | |||||||
| import { globalVariables, searchStyles } from "./search.css"; | import { globalVariables, searchStyles } from "./search.css"; | ||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  |  | ||||||
| import { DualSelectPanelSearchEvent } from "../events"; | import type { SearchbarEvent } from "../types"; | ||||||
|  |  | ||||||
| const styles = [PFBase, globalVariables, searchStyles]; | const styles = [PFBase, globalVariables, searchStyles]; | ||||||
|  |  | ||||||
| @customElement("ak-search-bar") | @customElement("ak-search-bar") | ||||||
| export class AkSearchbar extends AKElement { | export class AkSearchbar extends CustomEmitterElement(AKElement) { | ||||||
|     static get styles() { |     static get styles() { | ||||||
|         return styles; |         return styles; | ||||||
|     } |     } | ||||||
| @ -39,7 +40,10 @@ export class AkSearchbar extends AKElement { | |||||||
|         if (this.input.value) { |         if (this.input.value) { | ||||||
|             this.value = this.input.value.value; |             this.value = this.input.value.value; | ||||||
|         } |         } | ||||||
|         this.dispatchEvent(new DualSelectPanelSearchEvent(this.name, this.value)); |         this.dispatchCustomEvent<SearchbarEvent>("ak-search", { | ||||||
|  |             source: this.name, | ||||||
|  |             value: this.value, | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     render() { |     render() { | ||||||
|  | |||||||
| @ -1,112 +0,0 @@ | |||||||
| import { DualSelectPair } from "./types"; |  | ||||||
|  |  | ||||||
| // Handled by the Server layer provider |  | ||||||
|  |  | ||||||
| // Request to provide a different page of the paginated results in the "available" panel. |  | ||||||
| export class DualSelectPaginatorNavEvent extends Event { |  | ||||||
|     static readonly eventName = "ak-dual-select-paginator-nav"; |  | ||||||
|     constructor(public page: number = 0) { |  | ||||||
|         super(DualSelectPaginatorNavEvent.eventName, { bubbles: true, composed: true }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Request to provide a filtered collection for the "available" panel via a search string |  | ||||||
| export class DualSelectSearchEvent extends Event { |  | ||||||
|     static readonly eventName = "ak-dual-select-search"; |  | ||||||
|     constructor(public search: string) { |  | ||||||
|         super(DualSelectSearchEvent.eventName, { bubbles: true, composed: true }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Request to update the "selected" list in the provider |  | ||||||
| export class DualSelectChangeEvent extends Event { |  | ||||||
|     static readonly eventName = "ak-dual-select-change"; |  | ||||||
|     constructor(public selected: DualSelectPair[]) { |  | ||||||
|         super(DualSelectChangeEvent.eventName, { bubbles: true, composed: true }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Paginator and specific item events |  | ||||||
|  |  | ||||||
| export const moveEvents = [ |  | ||||||
|     "add-all", |  | ||||||
|     "add-one", |  | ||||||
|     "add-selected", |  | ||||||
|     "delete-all", |  | ||||||
|     "remove-all", |  | ||||||
|     "remove-one", |  | ||||||
|     "remove-selected", |  | ||||||
| ] as const; |  | ||||||
|  |  | ||||||
| export type MoveEventType = (typeof moveEvents)[number]; |  | ||||||
|  |  | ||||||
| // Request to add or remove all, some, or just one item from the "selected" panel |  | ||||||
| export class DualSelectMoveRequestEvent extends Event { |  | ||||||
|     static readonly eventName = "ak-dual-select-request-move"; |  | ||||||
|     constructor( |  | ||||||
|         public move: MoveEventType, |  | ||||||
|         public key?: string, |  | ||||||
|     ) { |  | ||||||
|         super(DualSelectMoveRequestEvent.eventName, { bubbles: true, composed: true }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Update events |  | ||||||
|  |  | ||||||
| // Request to update the viewset |  | ||||||
| export class DualSelectUpdateEvent extends Event { |  | ||||||
|     static readonly eventName = "ak-dual-select-update"; |  | ||||||
|     constructor() { |  | ||||||
|         super(DualSelectUpdateEvent.eventName, { bubbles: true, composed: true }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| interface DualSelectMoveChangedEvent { |  | ||||||
|     keys: string[]; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Request to update the list of "marked for move" items in the "available" panel |  | ||||||
| export class DualSelectMoveAvailableEvent extends Event implements DualSelectMoveChangedEvent { |  | ||||||
|     static readonly eventName = "ak-dual-select-move-available"; |  | ||||||
|     constructor(public keys: string[]) { |  | ||||||
|         super(DualSelectMoveAvailableEvent.eventName, { bubbles: true, composed: true }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Request to update the list of "marked for move" items in the "selected" panel |  | ||||||
| export class DualSelectMoveSelectedEvent extends Event implements DualSelectMoveChangedEvent { |  | ||||||
|     static readonly eventName = "ak-dual-select-move-selected"; |  | ||||||
|     constructor(public keys: string[]) { |  | ||||||
|         super(DualSelectMoveSelectedEvent.eventName, { bubbles: true, composed: true }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Request to update either panel with a Filter |  | ||||||
| export class DualSelectPanelSearchEvent extends Event { |  | ||||||
|     static readonly eventName = "ak-dual-select-panel-search"; |  | ||||||
|     constructor( |  | ||||||
|         public source: string, |  | ||||||
|         public filterOn: string, |  | ||||||
|     ) { |  | ||||||
|         super(DualSelectPanelSearchEvent.eventName, { bubbles: true, composed: true }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|     interface HTMLElementEventMap { |  | ||||||
|         [DualSelectUpdateEvent.eventName]: DualSelectUpdateEvent; |  | ||||||
|         [DualSelectMoveAvailableEvent.eventName]: DualSelectMoveAvailableEvent; |  | ||||||
|         [DualSelectMoveSelectedEvent.eventName]: DualSelectMoveSelectedEvent; |  | ||||||
|         [DualSelectMoveRequestEvent.eventName]: DualSelectMoveRequestEvent; |  | ||||||
|         [DualSelectPaginatorNavEvent.eventName]: DualSelectPaginatorNavEvent; |  | ||||||
|         [DualSelectSearchEvent.eventName]: DualSelectSearchEvent; |  | ||||||
|         [DualSelectChangeEvent.eventName]: DualSelectChangeEvent; |  | ||||||
|         [DualSelectPanelSearchEvent.eventName]: DualSelectPanelSearchEvent; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     interface WindowEventMap { |  | ||||||
|         [DualSelectMoveRequestEvent.eventName]: DualSelectMoveRequestEvent; |  | ||||||
|         [DualSelectPaginatorNavEvent.eventName]: DualSelectPaginatorNavEvent; |  | ||||||
|         [DualSelectMoveSelectedEvent.eventName]: DualSelectMoveSelectedEvent; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -6,7 +6,6 @@ import { TemplateResult, html } from "lit"; | |||||||
|  |  | ||||||
| import "../components/ak-dual-select-available-pane"; | import "../components/ak-dual-select-available-pane"; | ||||||
| import { AkDualSelectAvailablePane } from "../components/ak-dual-select-available-pane"; | import { AkDualSelectAvailablePane } from "../components/ak-dual-select-available-pane"; | ||||||
| import { DualSelectMoveSelectedEvent } from "../events"; |  | ||||||
| import "./sb-host-provider"; | import "./sb-host-provider"; | ||||||
|  |  | ||||||
| const metadata: Meta<AkDualSelectAvailablePane> = { | const metadata: Meta<AkDualSelectAvailablePane> = { | ||||||
| @ -54,15 +53,15 @@ const container = (testItem: TemplateResult) => | |||||||
|     </div>`; |     </div>`; | ||||||
|  |  | ||||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||
| const handleMoveChanged = (result: DualSelectMoveSelectedEvent) => { | const handleMoveChanged = (result: any) => { | ||||||
|     const target = document.querySelector("#action-button-message-pad"); |     const target = document.querySelector("#action-button-message-pad"); | ||||||
|     target!.innerHTML = ""; |     target!.innerHTML = ""; | ||||||
|     result.keys.forEach((key: string) => { |     result.detail.forEach((key: string) => { | ||||||
|         target!.append(new DOMParser().parseFromString(`<li>${key}</li>`, "text/xml").firstChild!); |         target!.append(new DOMParser().parseFromString(`<li>${key}</li>`, "text/xml").firstChild!); | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| window.addEventListener(DualSelectMoveSelectedEvent.eventName, handleMoveChanged); | window.addEventListener("ak-dual-select-available-move-changed", handleMoveChanged); | ||||||
|  |  | ||||||
| type Story = StoryObj; | type Story = StoryObj; | ||||||
|  |  | ||||||
|  | |||||||
| @ -5,7 +5,6 @@ import { TemplateResult, html } from "lit"; | |||||||
|  |  | ||||||
| import "../components/ak-dual-select-controls"; | import "../components/ak-dual-select-controls"; | ||||||
| import { AkDualSelectControls } from "../components/ak-dual-select-controls"; | import { AkDualSelectControls } from "../components/ak-dual-select-controls"; | ||||||
| import { DualSelectMoveRequestEvent } from "../events"; |  | ||||||
|  |  | ||||||
| const metadata: Meta<AkDualSelectControls> = { | const metadata: Meta<AkDualSelectControls> = { | ||||||
|     title: "Elements / Dual Select / Control Panel", |     title: "Elements / Dual Select / Control Panel", | ||||||
| @ -60,9 +59,10 @@ const displayMessage = (result: any) => { | |||||||
|     target!.appendChild(doc.firstChild!); |     target!.appendChild(doc.firstChild!); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| window.addEventListener(DualSelectMoveRequestEvent.eventName, (ev: DualSelectMoveRequestEvent) => | window.addEventListener("ak-dual-select-add", () => displayMessage("add")); | ||||||
|     displayMessage(ev.move.toString()), | window.addEventListener("ak-dual-select-remove", () => displayMessage("remove")); | ||||||
| ); | window.addEventListener("ak-dual-select-add-all", () => displayMessage("add all")); | ||||||
|  | window.addEventListener("ak-dual-select-remove-all", () => displayMessage("remove all")); | ||||||
|  |  | ||||||
| type Story = StoryObj; | type Story = StoryObj; | ||||||
|  |  | ||||||
|  | |||||||
| @ -9,7 +9,6 @@ import { Pagination } from "@goauthentik/api"; | |||||||
|  |  | ||||||
| import "../ak-dual-select"; | import "../ak-dual-select"; | ||||||
| import { AkDualSelect } from "../ak-dual-select"; | import { AkDualSelect } from "../ak-dual-select"; | ||||||
| import { DualSelectPaginatorNavEvent } from "../events"; |  | ||||||
| import type { DualSelectPair } from "../types"; | import type { DualSelectPair } from "../types"; | ||||||
|  |  | ||||||
| const goodForYouRaw = ` | const goodForYouRaw = ` | ||||||
| @ -84,11 +83,11 @@ export class AkSbFruity extends LitElement { | |||||||
|             totalPages: Math.ceil(this.options.length / this.pageLength), |             totalPages: Math.ceil(this.options.length / this.pageLength), | ||||||
|         }; |         }; | ||||||
|         this.onNavigation = this.onNavigation.bind(this); |         this.onNavigation = this.onNavigation.bind(this); | ||||||
|         this.addEventListener(DualSelectPaginatorNavEvent.eventName, this.onNavigation); |         this.addEventListener("ak-pagination-nav-to", this.onNavigation); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     onNavigation(evt: DualSelectPaginatorNavEvent) { |     onNavigation(evt: Event) { | ||||||
|         const current = evt.page; |         const current: number = (evt as CustomEvent).detail; | ||||||
|         const index = current - 1; |         const index = current - 1; | ||||||
|         if (index * this.pageLength > this.options.length) { |         if (index * this.pageLength > this.options.length) { | ||||||
|             console.warn( |             console.warn( | ||||||
|  | |||||||
| @ -6,7 +6,6 @@ import { TemplateResult, html } from "lit"; | |||||||
|  |  | ||||||
| import "../components/ak-dual-select-selected-pane"; | import "../components/ak-dual-select-selected-pane"; | ||||||
| import { AkDualSelectSelectedPane } from "../components/ak-dual-select-selected-pane"; | import { AkDualSelectSelectedPane } from "../components/ak-dual-select-selected-pane"; | ||||||
| import { DualSelectMoveSelectedEvent } from "../events"; |  | ||||||
| import "./sb-host-provider"; | import "./sb-host-provider"; | ||||||
|  |  | ||||||
| const metadata: Meta<AkDualSelectSelectedPane> = { | const metadata: Meta<AkDualSelectSelectedPane> = { | ||||||
| @ -51,15 +50,15 @@ const container = (testItem: TemplateResult) => | |||||||
|     </div>`; |     </div>`; | ||||||
|  |  | ||||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||
| const handleMoveChanged = (result: DualSelectMoveSelectedEvent) => { | const handleMoveChanged = (result: any) => { | ||||||
|     const target = document.querySelector("#action-button-message-pad"); |     const target = document.querySelector("#action-button-message-pad"); | ||||||
|     target!.innerHTML = ""; |     target!.innerHTML = ""; | ||||||
|     result.keys.forEach((key: string) => { |     result.detail.forEach((key: string) => { | ||||||
|         target!.append(new DOMParser().parseFromString(`<li>${key}</li>`, "text/xml").firstChild!); |         target!.append(new DOMParser().parseFromString(`<li>${key}</li>`, "text/xml").firstChild!); | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| window.addEventListener(DualSelectMoveSelectedEvent.eventName, handleMoveChanged); | window.addEventListener("ak-dual-select-selected-move-changed", handleMoveChanged); | ||||||
|  |  | ||||||
| type Story = StoryObj; | type Story = StoryObj; | ||||||
|  |  | ||||||
|  | |||||||
| @ -5,7 +5,6 @@ import { TemplateResult, html } from "lit"; | |||||||
|  |  | ||||||
| import "../components/ak-pagination"; | import "../components/ak-pagination"; | ||||||
| import { AkPagination } from "../components/ak-pagination"; | import { AkPagination } from "../components/ak-pagination"; | ||||||
| import { DualSelectPaginatorNavEvent } from "../events"; |  | ||||||
|  |  | ||||||
| const metadata: Meta<AkPagination> = { | const metadata: Meta<AkPagination> = { | ||||||
|     title: "Elements / Dual Select / Pagination Control", |     title: "Elements / Dual Select / Pagination Control", | ||||||
| @ -44,18 +43,18 @@ const container = (testItem: TemplateResult) => | |||||||
|     </div>`; |     </div>`; | ||||||
|  |  | ||||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||
| const handleMoveChanged = (result: DualSelectPaginatorNavEvent) => { | const handleMoveChanged = (result: any) => { | ||||||
|     console.debug(result); |     console.debug(result); | ||||||
|     const target = document.querySelector("#action-button-message-pad"); |     const target = document.querySelector("#action-button-message-pad"); | ||||||
|     target!.append( |     target!.append( | ||||||
|         new DOMParser().parseFromString( |         new DOMParser().parseFromString( | ||||||
|             `<li>Request to move to page ${result.page}</li>`, |             `<li>Request to move to page ${result.detail}</li>`, | ||||||
|             "text/xml", |             "text/xml", | ||||||
|         ).firstChild!, |         ).firstChild!, | ||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| window.addEventListener(DualSelectPaginatorNavEvent.eventName, handleMoveChanged); | window.addEventListener("ak-pagination-nav-to", handleMoveChanged); | ||||||
|  |  | ||||||
| type Story = StoryObj; | type Story = StoryObj; | ||||||
|  |  | ||||||
|  | |||||||
| @ -29,3 +29,10 @@ export type DataProvision = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| export type DataProvider = (page: number, search?: string) => Promise<DataProvision>; | export type DataProvider = (page: number, search?: string) => Promise<DataProvision>; | ||||||
|  |  | ||||||
|  | export interface SearchbarEvent extends CustomEvent { | ||||||
|  |     detail: { | ||||||
|  |         source: string; | ||||||
|  |         value: string; | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | |||||||
							
								
								
									
										157
									
								
								web/src/elements/sync/SyncStatusCard.stories.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								web/src/elements/sync/SyncStatusCard.stories.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,157 @@ | |||||||
|  | import type { Meta, StoryObj } from "@storybook/web-components"; | ||||||
|  |  | ||||||
|  | import { html } from "lit"; | ||||||
|  |  | ||||||
|  | import { LogLevelEnum, SyncStatus, SystemTaskStatusEnum } from "@goauthentik/api"; | ||||||
|  |  | ||||||
|  | import "./SyncStatusCard"; | ||||||
|  |  | ||||||
|  | const metadata: Meta<SyncStatus> = { | ||||||
|  |     title: "Elements/<ak-sync-status-card>", | ||||||
|  |     component: "ak-sync-status-card", | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default metadata; | ||||||
|  |  | ||||||
|  | export const Running: StoryObj = { | ||||||
|  |     args: { | ||||||
|  |         status: { | ||||||
|  |             isRunning: true, | ||||||
|  |             tasks: [], | ||||||
|  |         } as SyncStatus, | ||||||
|  |     }, | ||||||
|  |     // @ts-ignore | ||||||
|  |     render: ({ status }: SyncStatus) => { | ||||||
|  |         return html` <div style="background-color: #f0f0f0; padding: 1rem;"> | ||||||
|  |             <ak-sync-status-card | ||||||
|  |                 .fetch=${async () => { | ||||||
|  |                     return status; | ||||||
|  |                 }} | ||||||
|  |             ></ak-sync-status-card> | ||||||
|  |         </div>`; | ||||||
|  |     }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const SingleTask: StoryObj = { | ||||||
|  |     args: { | ||||||
|  |         status: { | ||||||
|  |             isRunning: false, | ||||||
|  |             tasks: [ | ||||||
|  |                 { | ||||||
|  |                     uuid: "9ff42169-8249-4b67-ae3d-e455d822de2b", | ||||||
|  |                     name: "Single task", | ||||||
|  |                     fullName: "foo:bar:baz", | ||||||
|  |                     status: SystemTaskStatusEnum.Successful, | ||||||
|  |                     messages: [ | ||||||
|  |                         { | ||||||
|  |                             logger: "foo", | ||||||
|  |                             event: "bar", | ||||||
|  |                             attributes: { | ||||||
|  |                                 foo: "bar", | ||||||
|  |                             }, | ||||||
|  |                             timestamp: new Date(), | ||||||
|  |                             logLevel: LogLevelEnum.Info, | ||||||
|  |                         }, | ||||||
|  |                     ], | ||||||
|  |                     description: "foo", | ||||||
|  |                     startTimestamp: new Date(), | ||||||
|  |                     finishTimestamp: new Date(), | ||||||
|  |                     duration: 0, | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|  |         } as SyncStatus, | ||||||
|  |     }, | ||||||
|  |     // @ts-ignore | ||||||
|  |     render: ({ status }: SyncStatus) => { | ||||||
|  |         return html` <div style="background-color: #f0f0f0; padding: 1rem;"> | ||||||
|  |             <ak-sync-status-card | ||||||
|  |                 .fetch=${async () => { | ||||||
|  |                     return status; | ||||||
|  |                 }} | ||||||
|  |             ></ak-sync-status-card> | ||||||
|  |         </div>`; | ||||||
|  |     }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const MultipleTasks: StoryObj = { | ||||||
|  |     args: { | ||||||
|  |         status: { | ||||||
|  |             isRunning: false, | ||||||
|  |             tasks: [ | ||||||
|  |                 { | ||||||
|  |                     uuid: "9ff42169-8249-4b67-ae3d-e455d822de2b", | ||||||
|  |                     name: "Single task", | ||||||
|  |                     fullName: "foo:bar:baz", | ||||||
|  |                     status: SystemTaskStatusEnum.Successful, | ||||||
|  |                     messages: [ | ||||||
|  |                         { | ||||||
|  |                             logger: "foo", | ||||||
|  |                             event: "bar", | ||||||
|  |                             attributes: { | ||||||
|  |                                 foo: "bar", | ||||||
|  |                             }, | ||||||
|  |                             timestamp: new Date(), | ||||||
|  |                             logLevel: LogLevelEnum.Info, | ||||||
|  |                         }, | ||||||
|  |                     ], | ||||||
|  |                     description: "foo", | ||||||
|  |                     startTimestamp: new Date(), | ||||||
|  |                     finishTimestamp: new Date(), | ||||||
|  |                     duration: 0, | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     uuid: "9ff42169-8249-4b67-ae3d-e455d822de2b", | ||||||
|  |                     name: "Single task", | ||||||
|  |                     fullName: "foo:bar:baz", | ||||||
|  |                     status: SystemTaskStatusEnum.Successful, | ||||||
|  |                     messages: [ | ||||||
|  |                         { | ||||||
|  |                             logger: "foo", | ||||||
|  |                             event: "bar", | ||||||
|  |                             attributes: { | ||||||
|  |                                 foo: "bar", | ||||||
|  |                             }, | ||||||
|  |                             timestamp: new Date(), | ||||||
|  |                             logLevel: LogLevelEnum.Info, | ||||||
|  |                         }, | ||||||
|  |                     ], | ||||||
|  |                     description: "foo", | ||||||
|  |                     startTimestamp: new Date(), | ||||||
|  |                     finishTimestamp: new Date(), | ||||||
|  |                     duration: 0, | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     uuid: "9ff42169-8249-4b67-ae3d-e455d822de2b", | ||||||
|  |                     name: "Single task", | ||||||
|  |                     fullName: "foo:bar:baz", | ||||||
|  |                     status: SystemTaskStatusEnum.Successful, | ||||||
|  |                     messages: [ | ||||||
|  |                         { | ||||||
|  |                             logger: "foo", | ||||||
|  |                             event: "bar", | ||||||
|  |                             attributes: { | ||||||
|  |                                 foo: "bar", | ||||||
|  |                             }, | ||||||
|  |                             timestamp: new Date(), | ||||||
|  |                             logLevel: LogLevelEnum.Info, | ||||||
|  |                         }, | ||||||
|  |                     ], | ||||||
|  |                     description: "foo", | ||||||
|  |                     startTimestamp: new Date(), | ||||||
|  |                     finishTimestamp: new Date(), | ||||||
|  |                     duration: 0, | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|  |         } as SyncStatus, | ||||||
|  |     }, | ||||||
|  |     // @ts-ignore | ||||||
|  |     render: ({ status }: SyncStatus) => { | ||||||
|  |         return html` <div style="background-color: #f0f0f0; padding: 1rem;"> | ||||||
|  |             <ak-sync-status-card | ||||||
|  |                 .fetch=${async () => { | ||||||
|  |                     return status; | ||||||
|  |                 }} | ||||||
|  |             ></ak-sync-status-card> | ||||||
|  |         </div>`; | ||||||
|  |     }, | ||||||
|  | }; | ||||||
| @ -3,17 +3,92 @@ import { getRelativeTime } from "@goauthentik/common/utils"; | |||||||
| import "@goauthentik/components/ak-status-label"; | import "@goauthentik/components/ak-status-label"; | ||||||
| import { AKElement } from "@goauthentik/elements/Base"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
| import "@goauthentik/elements/EmptyState"; | import "@goauthentik/elements/EmptyState"; | ||||||
|  | import "@goauthentik/elements/buttons/ActionButton"; | ||||||
| import "@goauthentik/elements/events/LogViewer"; | import "@goauthentik/elements/events/LogViewer"; | ||||||
|  | import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table"; | ||||||
| 
 | 
 | ||||||
| import { msg, str } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { CSSResult, TemplateResult, html, nothing } from "lit"; | import { CSSResult, TemplateResult, css, html } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators.js"; | import { customElement, property, state } from "lit/decorators.js"; | ||||||
| 
 | 
 | ||||||
| import PFCard from "@patternfly/patternfly/components/Card/card.css"; | import PFCard from "@patternfly/patternfly/components/Card/card.css"; | ||||||
|  | import PFTable from "@patternfly/patternfly/components/Table/table.css"; | ||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
| 
 | 
 | ||||||
| import { SyncStatus, SystemTask, SystemTaskStatusEnum } from "@goauthentik/api"; | import { SyncStatus, SystemTask, SystemTaskStatusEnum } from "@goauthentik/api"; | ||||||
| 
 | 
 | ||||||
|  | @customElement("ak-sync-status-table") | ||||||
|  | export class SyncStatusTable extends Table<SystemTask> { | ||||||
|  |     @property({ attribute: false }) | ||||||
|  |     tasks: SystemTask[] = []; | ||||||
|  | 
 | ||||||
|  |     expandable = true; | ||||||
|  | 
 | ||||||
|  |     static get styles() { | ||||||
|  |         return super.styles.concat(css` | ||||||
|  |             code:not(:last-of-type)::after { | ||||||
|  |                 content: "-"; | ||||||
|  |                 margin: 0 0.25rem; | ||||||
|  |             } | ||||||
|  |         `);
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async apiEndpoint(): Promise<PaginatedResponse<SystemTask>> { | ||||||
|  |         return { | ||||||
|  |             pagination: { | ||||||
|  |                 next: 0, | ||||||
|  |                 previous: 0, | ||||||
|  |                 count: this.tasks.length, | ||||||
|  |                 current: 1, | ||||||
|  |                 totalPages: 1, | ||||||
|  |                 startIndex: 0, | ||||||
|  |                 endIndex: this.tasks.length, | ||||||
|  |             }, | ||||||
|  |             results: this.tasks, | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     columns(): TableColumn[] { | ||||||
|  |         return [ | ||||||
|  |             new TableColumn(msg("Task")), | ||||||
|  |             new TableColumn(msg("Status")), | ||||||
|  |             new TableColumn(msg("Finished")), | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     row(item: SystemTask): TemplateResult[] { | ||||||
|  |         const nameParts = item.fullName.split(":"); | ||||||
|  |         nameParts.shift(); | ||||||
|  |         return [ | ||||||
|  |             html`<div>${item.name}</div>
 | ||||||
|  |                 <small>${nameParts.map((part) => html`<code>${part}</code>`)}</small>`,
 | ||||||
|  |             html`<ak-status-label
 | ||||||
|  |                 ?good=${item.status === SystemTaskStatusEnum.Successful} | ||||||
|  |                 good-label=${msg("Finished successfully")} | ||||||
|  |                 bad-label=${msg("Finished with errors")} | ||||||
|  |             ></ak-status-label>`, | ||||||
|  |             html`<div>${getRelativeTime(item.finishTimestamp)}</div>
 | ||||||
|  |                 <small>${item.finishTimestamp.toLocaleString()}</small>`,
 | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     renderExpanded(item: SystemTask): TemplateResult { | ||||||
|  |         return html`<td role="cell" colspan="4">
 | ||||||
|  |             <div class="pf-c-table__expandable-row-content"> | ||||||
|  |                 <ak-log-viewer .logs=${item?.messages}></ak-log-viewer> | ||||||
|  |             </div> | ||||||
|  |         </td>`;
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     renderToolbarContainer() { | ||||||
|  |         return html``; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     renderTablePagination() { | ||||||
|  |         return html``; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| @customElement("ak-sync-status-card") | @customElement("ak-sync-status-card") | ||||||
| export class SyncStatusCard extends AKElement { | export class SyncStatusCard extends AKElement { | ||||||
|     @state() |     @state() | ||||||
| @ -29,7 +104,7 @@ export class SyncStatusCard extends AKElement { | |||||||
|     triggerSync!: () => Promise<unknown>; |     triggerSync!: () => Promise<unknown>; | ||||||
| 
 | 
 | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
|         return [PFBase, PFCard]; |         return [PFBase, PFCard, PFTable]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     firstUpdated() { |     firstUpdated() { | ||||||
| @ -40,25 +115,6 @@ export class SyncStatusCard extends AKElement { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     renderSyncTask(task: SystemTask): TemplateResult { |  | ||||||
|         return html`<li>
 |  | ||||||
|             ${(this.syncState?.tasks || []).length > 1 ? html`<span>${task.name}</span>` : nothing} |  | ||||||
|             <span |  | ||||||
|                 ><ak-status-label |  | ||||||
|                     ?good=${task.status === SystemTaskStatusEnum.Successful} |  | ||||||
|                     good-label=${msg("Finished successfully")} |  | ||||||
|                     bad-label=${msg("Finished with errors")} |  | ||||||
|                 ></ak-status-label |  | ||||||
|             ></span> |  | ||||||
|             <span |  | ||||||
|                 >${msg( |  | ||||||
|                     str`Finished ${getRelativeTime(task.finishTimestamp)} (${task.finishTimestamp.toLocaleString()})`, |  | ||||||
|                 )}</span |  | ||||||
|             > |  | ||||||
|             <ak-log-viewer .logs=${task?.messages}></ak-log-viewer> |  | ||||||
|         </li> `;
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     renderSyncStatus(): TemplateResult { |     renderSyncStatus(): TemplateResult { | ||||||
|         if (this.loading) { |         if (this.loading) { | ||||||
|             return html`<ak-empty-state ?loading=${true}></ak-empty-state>`; |             return html`<ak-empty-state ?loading=${true}></ak-empty-state>`; | ||||||
| @ -72,13 +128,7 @@ export class SyncStatusCard extends AKElement { | |||||||
|         if (this.syncState.tasks.length < 1) { |         if (this.syncState.tasks.length < 1) { | ||||||
|             return html`${msg("Not synced yet.")}`; |             return html`${msg("Not synced yet.")}`; | ||||||
|         } |         } | ||||||
|         return html` |         return html`<ak-sync-status-table .tasks=${this.syncState.tasks}></ak-sync-status-table>`; | ||||||
|             <ul class="pf-c-list"> |  | ||||||
|                 ${this.syncState.tasks.map((task) => { |  | ||||||
|                     return this.renderSyncTask(task); |  | ||||||
|                 })} |  | ||||||
|             </ul> |  | ||||||
|         `;
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
| @ -120,6 +170,7 @@ export class SyncStatusCard extends AKElement { | |||||||
| 
 | 
 | ||||||
| declare global { | declare global { | ||||||
|     interface HTMLElementTagNameMap { |     interface HTMLElementTagNameMap { | ||||||
|  |         "ak-sync-status-table": SyncStatusTable; | ||||||
|         "ak-sync-status-card": SyncStatusCard; |         "ak-sync-status-card": SyncStatusCard; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @ -5,6 +5,7 @@ import { | |||||||
|     TITLE_DEFAULT, |     TITLE_DEFAULT, | ||||||
| } from "@goauthentik/common/constants"; | } from "@goauthentik/common/constants"; | ||||||
| import { globalAK } from "@goauthentik/common/global"; | import { globalAK } from "@goauthentik/common/global"; | ||||||
|  | import { purify } from "@goauthentik/common/purify"; | ||||||
| import { configureSentry } from "@goauthentik/common/sentry"; | import { configureSentry } from "@goauthentik/common/sentry"; | ||||||
| import { first } from "@goauthentik/common/utils"; | import { first } from "@goauthentik/common/utils"; | ||||||
| import { WebsocketClient } from "@goauthentik/common/ws"; | import { WebsocketClient } from "@goauthentik/common/ws"; | ||||||
| @ -13,7 +14,6 @@ import "@goauthentik/elements/LoadingOverlay"; | |||||||
| import "@goauthentik/elements/ak-locale-context"; | import "@goauthentik/elements/ak-locale-context"; | ||||||
| import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand"; | import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand"; | ||||||
| import { themeImage } from "@goauthentik/elements/utils/images"; | import { themeImage } from "@goauthentik/elements/utils/images"; | ||||||
| import "@goauthentik/flow/components/ak-brand-footer"; |  | ||||||
| import "@goauthentik/flow/sources/apple/AppleLoginInit"; | import "@goauthentik/flow/sources/apple/AppleLoginInit"; | ||||||
| import "@goauthentik/flow/sources/plex/PlexLoginInit"; | import "@goauthentik/flow/sources/plex/PlexLoginInit"; | ||||||
| import "@goauthentik/flow/stages/FlowErrorStage"; | import "@goauthentik/flow/stages/FlowErrorStage"; | ||||||
| @ -537,10 +537,27 @@ export class FlowExecutor extends Interface implements StageHost { | |||||||
|                                             </div> |                                             </div> | ||||||
|                                             ${until(this.renderChallenge())} |                                             ${until(this.renderChallenge())} | ||||||
|                                         </div> |                                         </div> | ||||||
|                                         <ak-brand-links |                                         <footer class="pf-c-login__footer"> | ||||||
|                                             class="pf-c-login__footer" |                                             <ul class="pf-c-list pf-m-inline"> | ||||||
|                                             .links=${this.brand?.uiFooterLinks ?? []} |                                                 ${this.brand?.uiFooterLinks?.map((link) => { | ||||||
|                                         ></ak-brand-links> |                                                     if (link.href) { | ||||||
|  |                                                         return html`${purify( | ||||||
|  |                                                             html`<li> | ||||||
|  |                                                                 <a href="${link.href}" | ||||||
|  |                                                                     >${link.name}</a | ||||||
|  |                                                                 > | ||||||
|  |                                                             </li>`, | ||||||
|  |                                                         )}`; | ||||||
|  |                                                     } | ||||||
|  |                                                     return html`<li> | ||||||
|  |                                                         <span>${link.name}</span> | ||||||
|  |                                                     </li>`; | ||||||
|  |                                                 })} | ||||||
|  |                                                 <li> | ||||||
|  |                                                     <span>${msg("Powered by authentik")}</span> | ||||||
|  |                                                 </li> | ||||||
|  |                                             </ul> | ||||||
|  |                                         </footer> | ||||||
|                                     </div> |                                     </div> | ||||||
|                                 </div> |                                 </div> | ||||||
|                             </div> |                             </div> | ||||||
|  | |||||||
| @ -1,51 +0,0 @@ | |||||||
| import { purify } from "@goauthentik/common/purify"; |  | ||||||
| import { AKElement } from "@goauthentik/elements/Base.js"; |  | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; |  | ||||||
| import { css, html } from "lit"; |  | ||||||
| import { customElement, property } from "lit/decorators.js"; |  | ||||||
| import { map } from "lit/directives/map.js"; |  | ||||||
|  |  | ||||||
| import PFList from "@patternfly/patternfly/components/List/list.css"; |  | ||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; |  | ||||||
|  |  | ||||||
| import { FooterLink } from "@goauthentik/api"; |  | ||||||
|  |  | ||||||
| const styles = css` |  | ||||||
|     .pf-c-list a { |  | ||||||
|         color: unset; |  | ||||||
|     } |  | ||||||
|     ul.pf-c-list.pf-m-inline { |  | ||||||
|         justify-content: center; |  | ||||||
|         padding: calc(var(--pf-global--spacer--xs) / 2) 0px; |  | ||||||
|     } |  | ||||||
| `; |  | ||||||
|  |  | ||||||
| const poweredBy: FooterLink = { name: msg("Powered by authentik"), href: null }; |  | ||||||
|  |  | ||||||
| @customElement("ak-brand-links") |  | ||||||
| export class BrandLinks extends AKElement { |  | ||||||
|     static get styles() { |  | ||||||
|         return [PFBase, PFList, styles]; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @property({ type: Array, attribute: false }) |  | ||||||
|     links: FooterLink[] = []; |  | ||||||
|  |  | ||||||
|     render() { |  | ||||||
|         const links = [...(this.links ?? []), poweredBy]; |  | ||||||
|         return html` <ul class="pf-c-list pf-m-inline"> |  | ||||||
|             ${map(links, (link) => |  | ||||||
|                 link.href |  | ||||||
|                     ? purify(html`<li><a href="${link.href}">${link.name}</a></li>`) |  | ||||||
|                     : html`<li><span>${link.name}</span></li>`, |  | ||||||
|             )} |  | ||||||
|         </ul>`; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|     interface HTMLElementTagNameMap { |  | ||||||
|         "ak-brand-links": BrandLinks; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -70,52 +70,57 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage< | |||||||
|             return html`<ak-empty-state loading> </ak-empty-state>`; |             return html`<ak-empty-state loading> </ak-empty-state>`; | ||||||
|         } |         } | ||||||
|         return html`<div class="pf-c-login__main-body"> |         return html`<div class="pf-c-login__main-body"> | ||||||
|             <form |                 <form | ||||||
|                 class="pf-c-form" |                     class="pf-c-form" | ||||||
|                 @submit=${(e: Event) => { |                     @submit=${(e: Event) => { | ||||||
|                     this.submitForm(e); |                         this.submitForm(e); | ||||||
|                 }} |                     }} | ||||||
|             > |  | ||||||
|                 ${this.renderUserInfo()} |  | ||||||
|                 <div class="icon-description"> |  | ||||||
|                     <i class="fa ${this.deviceIcon()}" aria-hidden="true"></i> |  | ||||||
|                     <p>${this.deviceMessage()}</p> |  | ||||||
|                 </div> |  | ||||||
|                 <ak-form-element |  | ||||||
|                     label="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static |  | ||||||
|                         ? msg("Static token") |  | ||||||
|                         : msg("Authentication code")}" |  | ||||||
|                     required |  | ||||||
|                     class="pf-c-form__group" |  | ||||||
|                     .errors=${(this.challenge?.responseErrors || {})["code"]} |  | ||||||
|                 > |                 > | ||||||
|                     <!-- @ts-ignore --> |                     ${this.renderUserInfo()} | ||||||
|                     <input |                     <div class="icon-description"> | ||||||
|                         type="text" |                         <i class="fa ${this.deviceIcon()}" aria-hidden="true"></i> | ||||||
|                         name="code" |                         <p>${this.deviceMessage()}</p> | ||||||
|                         inputmode="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static |                     </div> | ||||||
|                             ? "text" |                     <ak-form-element | ||||||
|                             : "numeric"}" |                         label="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static | ||||||
|                         pattern="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static |                             ? msg("Static token") | ||||||
|                             ? "[0-9a-zA-Z]*" |                             : msg("Authentication code")}" | ||||||
|                             : "[0-9]*"}" |  | ||||||
|                         placeholder="${msg("Please enter your code")}" |  | ||||||
|                         autofocus="" |  | ||||||
|                         autocomplete="one-time-code" |  | ||||||
|                         class="pf-c-form-control" |  | ||||||
|                         value="${PasswordManagerPrefill.totp || ""}" |  | ||||||
|                         required |                         required | ||||||
|                     /> |                         class="pf-c-form__group" | ||||||
|                 </ak-form-element> |                         .errors=${(this.challenge?.responseErrors || {})["code"]} | ||||||
|  |                     > | ||||||
|  |                         <!-- @ts-ignore --> | ||||||
|  |                         <input | ||||||
|  |                             type="text" | ||||||
|  |                             name="code" | ||||||
|  |                             inputmode="${this.deviceChallenge?.deviceClass === | ||||||
|  |                             DeviceClassesEnum.Static | ||||||
|  |                                 ? "text" | ||||||
|  |                                 : "numeric"}" | ||||||
|  |                             pattern="${this.deviceChallenge?.deviceClass === | ||||||
|  |                             DeviceClassesEnum.Static | ||||||
|  |                                 ? "[0-9a-zA-Z]*" | ||||||
|  |                                 : "[0-9]*"}" | ||||||
|  |                             placeholder="${msg("Please enter your code")}" | ||||||
|  |                             autofocus="" | ||||||
|  |                             autocomplete="one-time-code" | ||||||
|  |                             class="pf-c-form-control" | ||||||
|  |                             value="${PasswordManagerPrefill.totp || ""}" | ||||||
|  |                             required | ||||||
|  |                         /> | ||||||
|  |                     </ak-form-element> | ||||||
|  |  | ||||||
|                 <div class="pf-c-form__group pf-m-action"> |                     <div class="pf-c-form__group pf-m-action"> | ||||||
|                     <button type="submit" class="pf-c-button pf-m-primary pf-m-block"> |                         <button type="submit" class="pf-c-button pf-m-primary pf-m-block"> | ||||||
|                         ${msg("Continue")} |                             ${msg("Continue")} | ||||||
|                     </button> |                         </button> | ||||||
|                     ${this.renderReturnToDevicePicker()} |                         ${this.renderReturnToDevicePicker()} | ||||||
|                 </div> |                     </div> | ||||||
|             </form> |                 </form> | ||||||
|         </div>`; |             </div> | ||||||
|  |             <footer class="pf-c-login__main-footer"> | ||||||
|  |                 <ul class="pf-c-login__main-footer-links"></ul> | ||||||
|  |             </footer>`; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -72,7 +72,9 @@ export class BaseStage< | |||||||
|         } |         } | ||||||
|         return this.host?.submit(object as unknown as Tout).then((successful) => { |         return this.host?.submit(object as unknown as Tout).then((successful) => { | ||||||
|             if (successful) { |             if (successful) { | ||||||
|                 this.cleanup(); |                 this.onSubmitSuccess(); | ||||||
|  |             } else { | ||||||
|  |                 this.onSubmitFailure(); | ||||||
|             } |             } | ||||||
|             return successful; |             return successful; | ||||||
|         }); |         }); | ||||||
| @ -124,7 +126,11 @@ export class BaseStage< | |||||||
|         `; |         `; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     cleanup(): void { |     onSubmitSuccess(): void { | ||||||
|  |         // Method that can be overridden by stages | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     onSubmitFailure(): void { | ||||||
|         // Method that can be overridden by stages |         // Method that can be overridden by stages | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ import { randomId } from "@goauthentik/elements/utils/randomId"; | |||||||
| import "@goauthentik/flow/FormStatic"; | import "@goauthentik/flow/FormStatic"; | ||||||
| import { BaseStage } from "@goauthentik/flow/stages/base"; | import { BaseStage } from "@goauthentik/flow/stages/base"; | ||||||
| import { P, match } from "ts-pattern"; | import { P, match } from "ts-pattern"; | ||||||
| import type { TurnstileObject } from "turnstile-types"; | import type * as _ from "turnstile-types"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit"; | import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit"; | ||||||
| @ -24,10 +24,6 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css"; | |||||||
|  |  | ||||||
| import { CaptchaChallenge, CaptchaChallengeResponseRequest } from "@goauthentik/api"; | import { CaptchaChallenge, CaptchaChallengeResponseRequest } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| interface TurnstileWindow extends Window { |  | ||||||
|     turnstile: TurnstileObject; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type TokenHandler = (token: string) => void; | type TokenHandler = (token: string) => void; | ||||||
|  |  | ||||||
| type Dims = { height: number }; | type Dims = { height: number }; | ||||||
| @ -52,6 +48,8 @@ type CaptchaHandler = { | |||||||
|     name: string; |     name: string; | ||||||
|     interactive: () => Promise<unknown>; |     interactive: () => Promise<unknown>; | ||||||
|     execute: () => Promise<unknown>; |     execute: () => Promise<unknown>; | ||||||
|  |     refreshInteractive: () => Promise<unknown>; | ||||||
|  |     refresh: () => Promise<unknown>; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| // A container iframe for a hosted Captcha, with an event emitter to monitor when the Captcha forces | // A container iframe for a hosted Captcha, with an event emitter to monitor when the Captcha forces | ||||||
| @ -119,6 +117,12 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe | |||||||
|         this.host.submit({ component: "ak-stage-captcha", token }); |         this.host.submit({ component: "ak-stage-captcha", token }); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  |     @property({ attribute: false }) | ||||||
|  |     refreshedAt = new Date(); | ||||||
|  |  | ||||||
|  |     @state() | ||||||
|  |     activeHandler?: CaptchaHandler = undefined; | ||||||
|  |  | ||||||
|     @state() |     @state() | ||||||
|     error?: string; |     error?: string; | ||||||
|  |  | ||||||
| @ -127,16 +131,22 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe | |||||||
|             name: "grecaptcha", |             name: "grecaptcha", | ||||||
|             interactive: this.renderGReCaptchaFrame, |             interactive: this.renderGReCaptchaFrame, | ||||||
|             execute: this.executeGReCaptcha, |             execute: this.executeGReCaptcha, | ||||||
|  |             refreshInteractive: this.refreshGReCaptchaFrame, | ||||||
|  |             refresh: this.refreshGReCaptcha, | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             name: "hcaptcha", |             name: "hcaptcha", | ||||||
|             interactive: this.renderHCaptchaFrame, |             interactive: this.renderHCaptchaFrame, | ||||||
|             execute: this.executeHCaptcha, |             execute: this.executeHCaptcha, | ||||||
|  |             refreshInteractive: this.refreshHCaptchaFrame, | ||||||
|  |             refresh: this.refreshHCaptcha, | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             name: "turnstile", |             name: "turnstile", | ||||||
|             interactive: this.renderTurnstileFrame, |             interactive: this.renderTurnstileFrame, | ||||||
|             execute: this.executeTurnstile, |             execute: this.executeTurnstile, | ||||||
|  |             refreshInteractive: this.refreshTurnstileFrame, | ||||||
|  |             refresh: this.refreshTurnstile, | ||||||
|         }, |         }, | ||||||
|     ]; |     ]; | ||||||
|  |  | ||||||
| @ -230,6 +240,15 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async refreshGReCaptchaFrame() { | ||||||
|  |         (this.captchaFrame.contentWindow as typeof window)?.grecaptcha.reset(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async refreshGReCaptcha() { | ||||||
|  |         window.grecaptcha.reset(); | ||||||
|  |         window.grecaptcha.execute(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     async renderHCaptchaFrame() { |     async renderHCaptchaFrame() { | ||||||
|         this.renderFrame( |         this.renderFrame( | ||||||
|             html`<div |             html`<div | ||||||
| @ -251,6 +270,15 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe | |||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async refreshHCaptchaFrame() { | ||||||
|  |         (this.captchaFrame.contentWindow as typeof window)?.hcaptcha.reset(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async refreshHCaptcha() { | ||||||
|  |         window.hcaptcha.reset(); | ||||||
|  |         window.hcaptcha.execute(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     async renderTurnstileFrame() { |     async renderTurnstileFrame() { | ||||||
|         this.renderFrame( |         this.renderFrame( | ||||||
|             html`<div |             html`<div | ||||||
| @ -262,13 +290,18 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async executeTurnstile() { |     async executeTurnstile() { | ||||||
|         return (window as unknown as TurnstileWindow).turnstile.render( |         return window.turnstile.render(this.captchaDocumentContainer, { | ||||||
|             this.captchaDocumentContainer, |             sitekey: this.challenge.siteKey, | ||||||
|             { |             callback: this.onTokenChange, | ||||||
|                 sitekey: this.challenge.siteKey, |         }); | ||||||
|                 callback: this.onTokenChange, |     } | ||||||
|             }, |  | ||||||
|         ); |     async refreshTurnstileFrame() { | ||||||
|  |         (this.captchaFrame.contentWindow as typeof window)?.turnstile.reset(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async refreshTurnstile() { | ||||||
|  |         window.turnstile.reset(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async renderFrame(captchaElement: TemplateResult) { |     async renderFrame(captchaElement: TemplateResult) { | ||||||
| @ -336,16 +369,19 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe | |||||||
|             const handlers = this.handlers.filter(({ name }) => Object.hasOwn(window, name)); |             const handlers = this.handlers.filter(({ name }) => Object.hasOwn(window, name)); | ||||||
|             let lastError = undefined; |             let lastError = undefined; | ||||||
|             let found = false; |             let found = false; | ||||||
|             for (const { name, interactive, execute } of handlers) { |             for (const handler of handlers) { | ||||||
|                 console.debug(`authentik/stages/captcha: trying handler ${name}`); |                 console.debug(`authentik/stages/captcha: trying handler ${handler.name}`); | ||||||
|                 try { |                 try { | ||||||
|                     const runner = this.challenge.interactive ? interactive : execute; |                     const runner = this.challenge.interactive | ||||||
|  |                         ? handler.interactive | ||||||
|  |                         : handler.execute; | ||||||
|                     await runner.apply(this); |                     await runner.apply(this); | ||||||
|                     console.debug(`authentik/stages/captcha[${name}]: handler succeeded`); |                     console.debug(`authentik/stages/captcha[${handler.name}]: handler succeeded`); | ||||||
|                     found = true; |                     found = true; | ||||||
|  |                     this.activeHandler = handler; | ||||||
|                     break; |                     break; | ||||||
|                 } catch (exc) { |                 } catch (exc) { | ||||||
|                     console.debug(`authentik/stages/captcha[${name}]: handler failed`); |                     console.debug(`authentik/stages/captcha[${handler.name}]: handler failed`); | ||||||
|                     console.debug(exc); |                     console.debug(exc); | ||||||
|                     lastError = exc; |                     lastError = exc; | ||||||
|                 } |                 } | ||||||
| @ -370,6 +406,19 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe | |||||||
|             document.body.appendChild(this.captchaDocumentContainer); |             document.body.appendChild(this.captchaDocumentContainer); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     updated(changedProperties: PropertyValues<this>) { | ||||||
|  |         if (!changedProperties.has("refreshedAt") || !this.challenge) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         console.debug("authentik/stages/captcha: refresh triggered"); | ||||||
|  |         if (this.challenge.interactive) { | ||||||
|  |             this.activeHandler?.refreshInteractive.apply(this); | ||||||
|  |         } else { | ||||||
|  |             this.activeHandler?.refresh.apply(this); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|  | |||||||
| @ -49,6 +49,8 @@ export class IdentificationStage extends BaseStage< | |||||||
|  |  | ||||||
|     @state() |     @state() | ||||||
|     captchaToken = ""; |     captchaToken = ""; | ||||||
|  |     @state() | ||||||
|  |     captchaRefreshedAt = new Date(); | ||||||
|  |  | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
|         return [ |         return [ | ||||||
| @ -179,12 +181,16 @@ export class IdentificationStage extends BaseStage< | |||||||
|         this.form.appendChild(totp); |         this.form.appendChild(totp); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     cleanup(): void { |     onSubmitSuccess(): void { | ||||||
|         if (this.form) { |         if (this.form) { | ||||||
|             this.form.remove(); |             this.form.remove(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     onSubmitFailure(): void { | ||||||
|  |         this.captchaRefreshedAt = new Date(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     renderSource(source: LoginSource): TemplateResult { |     renderSource(source: LoginSource): TemplateResult { | ||||||
|         const icon = renderSourceIcon(source.name, source.iconUrl); |         const icon = renderSourceIcon(source.name, source.iconUrl); | ||||||
|         return html`<li class="pf-c-login__main-footer-links-item"> |         return html`<li class="pf-c-login__main-footer-links-item"> | ||||||
| @ -287,6 +293,7 @@ export class IdentificationStage extends BaseStage< | |||||||
|                           .onTokenChange=${(token: string) => { |                           .onTokenChange=${(token: string) => { | ||||||
|                               this.captchaToken = token; |                               this.captchaToken = token; | ||||||
|                           }} |                           }} | ||||||
|  |                           .refreshedAt=${this.captchaRefreshedAt} | ||||||
|                           embedded |                           embedded | ||||||
|                       ></ak-stage-captcha> |                       ></ak-stage-captcha> | ||||||
|                   ` |                   ` | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								web/src/global.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								web/src/global.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -6,12 +6,6 @@ declare module "*.md" { | |||||||
|     const filename: string; |     const filename: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| declare module "*.mdx" { |  | ||||||
|     const html: string; |  | ||||||
|     const metadata: { [key: string]: string }; |  | ||||||
|     const filename: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare namespace Intl { | declare namespace Intl { | ||||||
|     class ListFormat { |     class ListFormat { | ||||||
|         constructor(locale: string, args: { [key: string]: string }); |         constructor(locale: string, args: { [key: string]: string }); | ||||||
|  | |||||||
| @ -165,13 +165,21 @@ class UserInterfacePresentation extends AKElement { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         return html`<a |         return html`<a | ||||||
|             class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md" |                 class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md" | ||||||
|             href="${globalAK().api.base}if/admin/" |                 href="${globalAK().api.base}if/admin/" | ||||||
|             slot="extra" |                 slot="extra" | ||||||
|         > |             > | ||||||
|             ${msg("Admin interface")} |                 ${msg("Admin interface")} | ||||||
|         </a>`; |             </a> | ||||||
|  |             <a | ||||||
|  |                 class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none-on-md pf-u-display-block" | ||||||
|  |                 href="${globalAK().api.base}if/admin/" | ||||||
|  |                 slot="extra" | ||||||
|  |             > | ||||||
|  |                 ${msg("Admin")} | ||||||
|  |             </a>`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     render() { |     render() { | ||||||
|         // The `!` in the field definitions above only re-assure typescript and eslint that the |         // The `!` in the field definitions above only re-assure typescript and eslint that the | ||||||
|         // values *should* be available, not that they *are*. Thus this contract check; it asserts |         // values *should* be available, not that they *are*. Thus this contract check; it asserts | ||||||
|  | |||||||
| @ -59,6 +59,10 @@ export class UserSettingsPage extends AKElement { | |||||||
|                 :host([theme="dark"]) .pf-c-page__main-section { |                 :host([theme="dark"]) .pf-c-page__main-section { | ||||||
|                     --pf-c-page__main-section--BackgroundColor: transparent; |                     --pf-c-page__main-section--BackgroundColor: transparent; | ||||||
|                 } |                 } | ||||||
|  |                 .pf-c-page__main { | ||||||
|  |                     min-height: 100vh; | ||||||
|  |                     overflow-y: auto; | ||||||
|  |                 } | ||||||
|                 @media screen and (min-width: 1200px) { |                 @media screen and (min-width: 1200px) { | ||||||
|                     :host { |                     :host { | ||||||
|                         width: 90rem; |                         width: 90rem; | ||||||
|  | |||||||
| @ -8600,7 +8600,7 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
|   <source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source> |   <source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source> | ||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s66f572bec2bde9c4"> | <trans-unit id="s66f572bec2bde9c4"> | ||||||
|   <source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source> |   <source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source> | ||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s58bec0ecd4f3ccd4"> | <trans-unit id="s58bec0ecd4f3ccd4"> | ||||||
|   <source>Strict</source> |   <source>Strict</source> | ||||||
| @ -8926,96 +8926,6 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s47b7ce63a543564c"> | <trans-unit id="s47b7ce63a543564c"> | ||||||
|   <source>Fewer details</source> |   <source>Fewer details</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s140111d464591e6b"> |  | ||||||
|   <source>Create a new application and configure a provider for it.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s5e0c81c05565bf42"> |  | ||||||
|   <source>Using this form will only create an Application. In order to authenticate with the application, you will have to manually pair it with a Provider.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s035bfd9c5f97e4d3"> |  | ||||||
|   <source>Distance settings</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s207e6f8a8b3515fd"> |  | ||||||
|   <source>Check historical distance of logins</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s8158f4b3e5c869be"> |  | ||||||
|   <source>When this option enabled, the GeoIP data of the policy request is compared to the specified number of historical logins.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sb8b7450c8515894c"> |  | ||||||
|   <source>Maximum distance</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s40cdbaa532bc9899"> |  | ||||||
|   <source>Maximum distance a login attempt is allowed from in kilometers.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="seef852b5c0f8a529"> |  | ||||||
|   <source>Distance tolerance</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sce567ced300aeb8a"> |  | ||||||
|   <source>Tolerance in checking for distances in kilometers.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s9ea9cdabd74f8f97"> |  | ||||||
|   <source>Historical Login Count</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s27aec4c2de1ae777"> |  | ||||||
|   <source>Amount of previous login events to check against.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s48611ce6e85874dc"> |  | ||||||
|   <source>Check impossible travel</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s8cf926e8311f8065"> |  | ||||||
|   <source>When this option enabled, the GeoIP data of the policy request is compared to the specified number of historical logins and if the travel would have been possible in the amount of time since the previous event.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sa963d05af436770b"> |  | ||||||
|   <source>Impossible travel tolerance</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s5760cd97ca42a238"> |  | ||||||
|   <source>Static rule settings</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s8fec035fa1737294"> |  | ||||||
|   <source>Create with Provider</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sca2487321ec12bd6"> |  | ||||||
|   <source>Email address the verification email will be sent from.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s24a8fdfc73e8137f"> |  | ||||||
|   <source>Stage used to configure an email-based authenticator.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sea0da186a814a212"> |  | ||||||
|   <source>Use global connection settings</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s7754fa56a4439de4"> |  | ||||||
|   <source>When enabled, global email connection settings will be used and connection settings below will be ignored.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s7e2bcca51126ec9c"> |  | ||||||
|   <source>Subject of the verification email.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc12c90b1da0f3a47"> |  | ||||||
|   <source>Token expiration</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc264a82f9c710f14"> |  | ||||||
|   <source>Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s15986693bfc99fb7"> |  | ||||||
|   <source>Email-based Authenticators</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s6bb30c61df4cf486"> |  | ||||||
|   <source>Caps Lock is enabled.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s3f8a07912545e72e"> |  | ||||||
|   <source>Configure your email</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="scedf77e8b75cad5a"> |  | ||||||
|   <source>Please enter your email address.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s7cdd62c100b6b17b"> |  | ||||||
|   <source>Please enter the code you received via email</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s1d64dba9bb8b284d"> |  | ||||||
|   <source>A code has been sent to you via email<x id="0" equiv-text="${email ? ` ${email}` : ""}"/></source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s833cfe815918c143"> |  | ||||||
|   <source>Tokens sent via email.</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -7128,7 +7128,7 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
|   <source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source> |   <source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source> | ||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s66f572bec2bde9c4"> | <trans-unit id="s66f572bec2bde9c4"> | ||||||
|   <source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source> |   <source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source> | ||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s58bec0ecd4f3ccd4"> | <trans-unit id="s58bec0ecd4f3ccd4"> | ||||||
|   <source>Strict</source> |   <source>Strict</source> | ||||||
| @ -7453,96 +7453,6 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s47b7ce63a543564c"> | <trans-unit id="s47b7ce63a543564c"> | ||||||
|   <source>Fewer details</source> |   <source>Fewer details</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s140111d464591e6b"> |  | ||||||
|   <source>Create a new application and configure a provider for it.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s5e0c81c05565bf42"> |  | ||||||
|   <source>Using this form will only create an Application. In order to authenticate with the application, you will have to manually pair it with a Provider.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s035bfd9c5f97e4d3"> |  | ||||||
|   <source>Distance settings</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s207e6f8a8b3515fd"> |  | ||||||
|   <source>Check historical distance of logins</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s8158f4b3e5c869be"> |  | ||||||
|   <source>When this option enabled, the GeoIP data of the policy request is compared to the specified number of historical logins.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sb8b7450c8515894c"> |  | ||||||
|   <source>Maximum distance</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s40cdbaa532bc9899"> |  | ||||||
|   <source>Maximum distance a login attempt is allowed from in kilometers.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="seef852b5c0f8a529"> |  | ||||||
|   <source>Distance tolerance</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sce567ced300aeb8a"> |  | ||||||
|   <source>Tolerance in checking for distances in kilometers.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s9ea9cdabd74f8f97"> |  | ||||||
|   <source>Historical Login Count</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s27aec4c2de1ae777"> |  | ||||||
|   <source>Amount of previous login events to check against.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s48611ce6e85874dc"> |  | ||||||
|   <source>Check impossible travel</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s8cf926e8311f8065"> |  | ||||||
|   <source>When this option enabled, the GeoIP data of the policy request is compared to the specified number of historical logins and if the travel would have been possible in the amount of time since the previous event.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sa963d05af436770b"> |  | ||||||
|   <source>Impossible travel tolerance</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s5760cd97ca42a238"> |  | ||||||
|   <source>Static rule settings</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s8fec035fa1737294"> |  | ||||||
|   <source>Create with Provider</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sca2487321ec12bd6"> |  | ||||||
|   <source>Email address the verification email will be sent from.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s24a8fdfc73e8137f"> |  | ||||||
|   <source>Stage used to configure an email-based authenticator.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sea0da186a814a212"> |  | ||||||
|   <source>Use global connection settings</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s7754fa56a4439de4"> |  | ||||||
|   <source>When enabled, global email connection settings will be used and connection settings below will be ignored.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s7e2bcca51126ec9c"> |  | ||||||
|   <source>Subject of the verification email.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc12c90b1da0f3a47"> |  | ||||||
|   <source>Token expiration</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc264a82f9c710f14"> |  | ||||||
|   <source>Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s15986693bfc99fb7"> |  | ||||||
|   <source>Email-based Authenticators</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s6bb30c61df4cf486"> |  | ||||||
|   <source>Caps Lock is enabled.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s3f8a07912545e72e"> |  | ||||||
|   <source>Configure your email</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="scedf77e8b75cad5a"> |  | ||||||
|   <source>Please enter your email address.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s7cdd62c100b6b17b"> |  | ||||||
|   <source>Please enter the code you received via email</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s1d64dba9bb8b284d"> |  | ||||||
|   <source>A code has been sent to you via email<x id="0" equiv-text="${email ? ` ${email}` : ""}"/></source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s833cfe815918c143"> |  | ||||||
|   <source>Tokens sent via email.</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -8688,7 +8688,7 @@ Las vinculaciones a grupos o usuarios se comparan con el usuario del evento.</ta | |||||||
|   <source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source> |   <source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source> | ||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s66f572bec2bde9c4"> | <trans-unit id="s66f572bec2bde9c4"> | ||||||
|   <source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source> |   <source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source> | ||||||
|   <target>Aplicaciones externas que utilizan <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> como proveedor de identidad a través de protocolos como OAuth2 y SAML. Aquí se muestran todas las aplicaciones, incluso aquellas a las que no puede acceder.</target> |   <target>Aplicaciones externas que utilizan <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> como proveedor de identidad a través de protocolos como OAuth2 y SAML. Aquí se muestran todas las aplicaciones, incluso aquellas a las que no puede acceder.</target> | ||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s58bec0ecd4f3ccd4"> | <trans-unit id="s58bec0ecd4f3ccd4"> | ||||||
| @ -9019,96 +9019,6 @@ Las vinculaciones a grupos o usuarios se comparan con el usuario del evento.</ta | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s47b7ce63a543564c"> | <trans-unit id="s47b7ce63a543564c"> | ||||||
|   <source>Fewer details</source> |   <source>Fewer details</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s140111d464591e6b"> |  | ||||||
|   <source>Create a new application and configure a provider for it.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s5e0c81c05565bf42"> |  | ||||||
|   <source>Using this form will only create an Application. In order to authenticate with the application, you will have to manually pair it with a Provider.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s035bfd9c5f97e4d3"> |  | ||||||
|   <source>Distance settings</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s207e6f8a8b3515fd"> |  | ||||||
|   <source>Check historical distance of logins</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s8158f4b3e5c869be"> |  | ||||||
|   <source>When this option enabled, the GeoIP data of the policy request is compared to the specified number of historical logins.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sb8b7450c8515894c"> |  | ||||||
|   <source>Maximum distance</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s40cdbaa532bc9899"> |  | ||||||
|   <source>Maximum distance a login attempt is allowed from in kilometers.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="seef852b5c0f8a529"> |  | ||||||
|   <source>Distance tolerance</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sce567ced300aeb8a"> |  | ||||||
|   <source>Tolerance in checking for distances in kilometers.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s9ea9cdabd74f8f97"> |  | ||||||
|   <source>Historical Login Count</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s27aec4c2de1ae777"> |  | ||||||
|   <source>Amount of previous login events to check against.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s48611ce6e85874dc"> |  | ||||||
|   <source>Check impossible travel</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s8cf926e8311f8065"> |  | ||||||
|   <source>When this option enabled, the GeoIP data of the policy request is compared to the specified number of historical logins and if the travel would have been possible in the amount of time since the previous event.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sa963d05af436770b"> |  | ||||||
|   <source>Impossible travel tolerance</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s5760cd97ca42a238"> |  | ||||||
|   <source>Static rule settings</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s8fec035fa1737294"> |  | ||||||
|   <source>Create with Provider</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sca2487321ec12bd6"> |  | ||||||
|   <source>Email address the verification email will be sent from.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s24a8fdfc73e8137f"> |  | ||||||
|   <source>Stage used to configure an email-based authenticator.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sea0da186a814a212"> |  | ||||||
|   <source>Use global connection settings</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s7754fa56a4439de4"> |  | ||||||
|   <source>When enabled, global email connection settings will be used and connection settings below will be ignored.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s7e2bcca51126ec9c"> |  | ||||||
|   <source>Subject of the verification email.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc12c90b1da0f3a47"> |  | ||||||
|   <source>Token expiration</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc264a82f9c710f14"> |  | ||||||
|   <source>Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s15986693bfc99fb7"> |  | ||||||
|   <source>Email-based Authenticators</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s6bb30c61df4cf486"> |  | ||||||
|   <source>Caps Lock is enabled.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s3f8a07912545e72e"> |  | ||||||
|   <source>Configure your email</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="scedf77e8b75cad5a"> |  | ||||||
|   <source>Please enter your email address.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s7cdd62c100b6b17b"> |  | ||||||
|   <source>Please enter the code you received via email</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s1d64dba9bb8b284d"> |  | ||||||
|   <source>A code has been sent to you via email<x id="0" equiv-text="${email ? ` ${email}` : ""}"/></source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s833cfe815918c143"> |  | ||||||
|   <source>Tokens sent via email.</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -9046,7 +9046,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti | |||||||
|   <target>Cette option configure les liens affichés en bas de page sur l’exécuteur de flux. L'URL est limitée à des addresses web et courriel. Si le nom est laissé vide, l'URL sera affichée.</target> |   <target>Cette option configure les liens affichés en bas de page sur l’exécuteur de flux. L'URL est limitée à des addresses web et courriel. Si le nom est laissé vide, l'URL sera affichée.</target> | ||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s66f572bec2bde9c4"> | <trans-unit id="s66f572bec2bde9c4"> | ||||||
|   <source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source> |   <source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source> | ||||||
|   <target>Applications externes qui utilisent <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> comme fournisseur d'identité en utilisant des protocoles comme OAuth2 et SAML. Toutes les applications sont affichées ici, même celles auxquelles vous n'avez pas accès.</target> |   <target>Applications externes qui utilisent <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> comme fournisseur d'identité en utilisant des protocoles comme OAuth2 et SAML. Toutes les applications sont affichées ici, même celles auxquelles vous n'avez pas accès.</target> | ||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s58bec0ecd4f3ccd4"> | <trans-unit id="s58bec0ecd4f3ccd4"> | ||||||
| @ -9482,96 +9482,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti | |||||||
| <trans-unit id="s47b7ce63a543564c"> | <trans-unit id="s47b7ce63a543564c"> | ||||||
|   <source>Fewer details</source> |   <source>Fewer details</source> | ||||||
|   <target>Moins de détails</target> |   <target>Moins de détails</target> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s140111d464591e6b"> |  | ||||||
|   <source>Create a new application and configure a provider for it.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s5e0c81c05565bf42"> |  | ||||||
|   <source>Using this form will only create an Application. In order to authenticate with the application, you will have to manually pair it with a Provider.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s035bfd9c5f97e4d3"> |  | ||||||
|   <source>Distance settings</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s207e6f8a8b3515fd"> |  | ||||||
|   <source>Check historical distance of logins</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s8158f4b3e5c869be"> |  | ||||||
|   <source>When this option enabled, the GeoIP data of the policy request is compared to the specified number of historical logins.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sb8b7450c8515894c"> |  | ||||||
|   <source>Maximum distance</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s40cdbaa532bc9899"> |  | ||||||
|   <source>Maximum distance a login attempt is allowed from in kilometers.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="seef852b5c0f8a529"> |  | ||||||
|   <source>Distance tolerance</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sce567ced300aeb8a"> |  | ||||||
|   <source>Tolerance in checking for distances in kilometers.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s9ea9cdabd74f8f97"> |  | ||||||
|   <source>Historical Login Count</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s27aec4c2de1ae777"> |  | ||||||
|   <source>Amount of previous login events to check against.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s48611ce6e85874dc"> |  | ||||||
|   <source>Check impossible travel</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s8cf926e8311f8065"> |  | ||||||
|   <source>When this option enabled, the GeoIP data of the policy request is compared to the specified number of historical logins and if the travel would have been possible in the amount of time since the previous event.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sa963d05af436770b"> |  | ||||||
|   <source>Impossible travel tolerance</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s5760cd97ca42a238"> |  | ||||||
|   <source>Static rule settings</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s8fec035fa1737294"> |  | ||||||
|   <source>Create with Provider</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sca2487321ec12bd6"> |  | ||||||
|   <source>Email address the verification email will be sent from.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s24a8fdfc73e8137f"> |  | ||||||
|   <source>Stage used to configure an email-based authenticator.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sea0da186a814a212"> |  | ||||||
|   <source>Use global connection settings</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s7754fa56a4439de4"> |  | ||||||
|   <source>When enabled, global email connection settings will be used and connection settings below will be ignored.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s7e2bcca51126ec9c"> |  | ||||||
|   <source>Subject of the verification email.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc12c90b1da0f3a47"> |  | ||||||
|   <source>Token expiration</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc264a82f9c710f14"> |  | ||||||
|   <source>Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s15986693bfc99fb7"> |  | ||||||
|   <source>Email-based Authenticators</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s6bb30c61df4cf486"> |  | ||||||
|   <source>Caps Lock is enabled.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s3f8a07912545e72e"> |  | ||||||
|   <source>Configure your email</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="scedf77e8b75cad5a"> |  | ||||||
|   <source>Please enter your email address.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s7cdd62c100b6b17b"> |  | ||||||
|   <source>Please enter the code you received via email</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s1d64dba9bb8b284d"> |  | ||||||
|   <source>A code has been sent to you via email<x id="0" equiv-text="${email ? ` ${email}` : ""}"/></source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s833cfe815918c143"> |  | ||||||
|   <source>Tokens sent via email.</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -9015,7 +9015,7 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
|   <target>Questo opzione configura il link in basso nel flusso delle pagine di esecuzione. L'URL e' limitato a web e indirizzo mail-Se il nome viene lasciato vuoto, verra' visualizzato l'URL</target> |   <target>Questo opzione configura il link in basso nel flusso delle pagine di esecuzione. L'URL e' limitato a web e indirizzo mail-Se il nome viene lasciato vuoto, verra' visualizzato l'URL</target> | ||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s66f572bec2bde9c4"> | <trans-unit id="s66f572bec2bde9c4"> | ||||||
|   <source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source> |   <source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source> | ||||||
|   <target>Applicazioni esterne che usano <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> come identity provider tramite protocolli come OAuth2 e SAML. Sono mostrate tutte le applicazioni, anche quelle alle quali non hai accesso.</target> |   <target>Applicazioni esterne che usano <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> come identity provider tramite protocolli come OAuth2 e SAML. Sono mostrate tutte le applicazioni, anche quelle alle quali non hai accesso.</target> | ||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s58bec0ecd4f3ccd4"> | <trans-unit id="s58bec0ecd4f3ccd4"> | ||||||
| @ -9370,96 +9370,6 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s47b7ce63a543564c"> | <trans-unit id="s47b7ce63a543564c"> | ||||||
|   <source>Fewer details</source> |   <source>Fewer details</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s140111d464591e6b"> |  | ||||||
|   <source>Create a new application and configure a provider for it.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s5e0c81c05565bf42"> |  | ||||||
|   <source>Using this form will only create an Application. In order to authenticate with the application, you will have to manually pair it with a Provider.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s035bfd9c5f97e4d3"> |  | ||||||
|   <source>Distance settings</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s207e6f8a8b3515fd"> |  | ||||||
|   <source>Check historical distance of logins</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s8158f4b3e5c869be"> |  | ||||||
|   <source>When this option enabled, the GeoIP data of the policy request is compared to the specified number of historical logins.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sb8b7450c8515894c"> |  | ||||||
|   <source>Maximum distance</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s40cdbaa532bc9899"> |  | ||||||
|   <source>Maximum distance a login attempt is allowed from in kilometers.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="seef852b5c0f8a529"> |  | ||||||
|   <source>Distance tolerance</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sce567ced300aeb8a"> |  | ||||||
|   <source>Tolerance in checking for distances in kilometers.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s9ea9cdabd74f8f97"> |  | ||||||
|   <source>Historical Login Count</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s27aec4c2de1ae777"> |  | ||||||
|   <source>Amount of previous login events to check against.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s48611ce6e85874dc"> |  | ||||||
|   <source>Check impossible travel</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s8cf926e8311f8065"> |  | ||||||
|   <source>When this option enabled, the GeoIP data of the policy request is compared to the specified number of historical logins and if the travel would have been possible in the amount of time since the previous event.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sa963d05af436770b"> |  | ||||||
|   <source>Impossible travel tolerance</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s5760cd97ca42a238"> |  | ||||||
|   <source>Static rule settings</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s8fec035fa1737294"> |  | ||||||
|   <source>Create with Provider</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sca2487321ec12bd6"> |  | ||||||
|   <source>Email address the verification email will be sent from.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s24a8fdfc73e8137f"> |  | ||||||
|   <source>Stage used to configure an email-based authenticator.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sea0da186a814a212"> |  | ||||||
|   <source>Use global connection settings</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s7754fa56a4439de4"> |  | ||||||
|   <source>When enabled, global email connection settings will be used and connection settings below will be ignored.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s7e2bcca51126ec9c"> |  | ||||||
|   <source>Subject of the verification email.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc12c90b1da0f3a47"> |  | ||||||
|   <source>Token expiration</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc264a82f9c710f14"> |  | ||||||
|   <source>Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s15986693bfc99fb7"> |  | ||||||
|   <source>Email-based Authenticators</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s6bb30c61df4cf486"> |  | ||||||
|   <source>Caps Lock is enabled.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s3f8a07912545e72e"> |  | ||||||
|   <source>Configure your email</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="scedf77e8b75cad5a"> |  | ||||||
|   <source>Please enter your email address.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s7cdd62c100b6b17b"> |  | ||||||
|   <source>Please enter the code you received via email</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s1d64dba9bb8b284d"> |  | ||||||
|   <source>A code has been sent to you via email<x id="0" equiv-text="${email ? ` ${email}` : ""}"/></source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s833cfe815918c143"> |  | ||||||
|   <source>Tokens sent via email.</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -8600,7 +8600,7 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
|   <source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source> |   <source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source> | ||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s66f572bec2bde9c4"> | <trans-unit id="s66f572bec2bde9c4"> | ||||||
|   <source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source> |   <source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source> | ||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s58bec0ecd4f3ccd4"> | <trans-unit id="s58bec0ecd4f3ccd4"> | ||||||
|   <source>Strict</source> |   <source>Strict</source> | ||||||
| @ -8926,96 +8926,6 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s47b7ce63a543564c"> | <trans-unit id="s47b7ce63a543564c"> | ||||||
|   <source>Fewer details</source> |   <source>Fewer details</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s140111d464591e6b"> |  | ||||||
|   <source>Create a new application and configure a provider for it.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s5e0c81c05565bf42"> |  | ||||||
|   <source>Using this form will only create an Application. In order to authenticate with the application, you will have to manually pair it with a Provider.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s035bfd9c5f97e4d3"> |  | ||||||
|   <source>Distance settings</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s207e6f8a8b3515fd"> |  | ||||||
|   <source>Check historical distance of logins</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s8158f4b3e5c869be"> |  | ||||||
|   <source>When this option enabled, the GeoIP data of the policy request is compared to the specified number of historical logins.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sb8b7450c8515894c"> |  | ||||||
|   <source>Maximum distance</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s40cdbaa532bc9899"> |  | ||||||
|   <source>Maximum distance a login attempt is allowed from in kilometers.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="seef852b5c0f8a529"> |  | ||||||
|   <source>Distance tolerance</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sce567ced300aeb8a"> |  | ||||||
|   <source>Tolerance in checking for distances in kilometers.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s9ea9cdabd74f8f97"> |  | ||||||
|   <source>Historical Login Count</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s27aec4c2de1ae777"> |  | ||||||
|   <source>Amount of previous login events to check against.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s48611ce6e85874dc"> |  | ||||||
|   <source>Check impossible travel</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s8cf926e8311f8065"> |  | ||||||
|   <source>When this option enabled, the GeoIP data of the policy request is compared to the specified number of historical logins and if the travel would have been possible in the amount of time since the previous event.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sa963d05af436770b"> |  | ||||||
|   <source>Impossible travel tolerance</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s5760cd97ca42a238"> |  | ||||||
|   <source>Static rule settings</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s8fec035fa1737294"> |  | ||||||
|   <source>Create with Provider</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sca2487321ec12bd6"> |  | ||||||
|   <source>Email address the verification email will be sent from.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s24a8fdfc73e8137f"> |  | ||||||
|   <source>Stage used to configure an email-based authenticator.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sea0da186a814a212"> |  | ||||||
|   <source>Use global connection settings</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s7754fa56a4439de4"> |  | ||||||
|   <source>When enabled, global email connection settings will be used and connection settings below will be ignored.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s7e2bcca51126ec9c"> |  | ||||||
|   <source>Subject of the verification email.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc12c90b1da0f3a47"> |  | ||||||
|   <source>Token expiration</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc264a82f9c710f14"> |  | ||||||
|   <source>Time the token sent is valid (Format: hours=3,minutes=17,seconds=300).</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s15986693bfc99fb7"> |  | ||||||
|   <source>Email-based Authenticators</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s6bb30c61df4cf486"> |  | ||||||
|   <source>Caps Lock is enabled.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s3f8a07912545e72e"> |  | ||||||
|   <source>Configure your email</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="scedf77e8b75cad5a"> |  | ||||||
|   <source>Please enter your email address.</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s7cdd62c100b6b17b"> |  | ||||||
|   <source>Please enter the code you received via email</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s1d64dba9bb8b284d"> |  | ||||||
|   <source>A code has been sent to you via email<x id="0" equiv-text="${email ? ` ${email}` : ""}"/></source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s833cfe815918c143"> |  | ||||||
|   <source>Tokens sent via email.</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	![gcp-cherry-pick-bot[bot]](/assets/img/avatar_default.png)