core: add API to directly send recovery link to user
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		| @ -1,13 +1,16 @@ | |||||||
| """User API Views""" | """User API Views""" | ||||||
| from json import loads | from json import loads | ||||||
|  | from typing import Optional | ||||||
|  |  | ||||||
| from django.db.models.query import QuerySet | from django.db.models.query import QuerySet | ||||||
| from django.urls import reverse_lazy | from django.urls import reverse_lazy | ||||||
| from django.utils.http import urlencode | from django.utils.http import urlencode | ||||||
|  | from django.utils.translation import gettext as _ | ||||||
| from django_filters.filters import BooleanFilter, CharFilter | from django_filters.filters import BooleanFilter, CharFilter | ||||||
| from django_filters.filterset import FilterSet | from django_filters.filterset import FilterSet | ||||||
| from drf_spectacular.utils import extend_schema, extend_schema_field | from drf_spectacular.types import OpenApiTypes | ||||||
| from guardian.utils import get_anonymous_user | from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_field | ||||||
|  | from guardian.shortcuts import get_anonymous_user, get_objects_for_user | ||||||
| from rest_framework.decorators import action | from rest_framework.decorators import action | ||||||
| from rest_framework.fields import CharField, JSONField, SerializerMethodField | from rest_framework.fields import CharField, JSONField, SerializerMethodField | ||||||
| from rest_framework.permissions import IsAuthenticated | from rest_framework.permissions import IsAuthenticated | ||||||
| @ -17,10 +20,12 @@ from rest_framework.serializers import ( | |||||||
|     BooleanField, |     BooleanField, | ||||||
|     ListSerializer, |     ListSerializer, | ||||||
|     ModelSerializer, |     ModelSerializer, | ||||||
|  |     Serializer, | ||||||
|     ValidationError, |     ValidationError, | ||||||
| ) | ) | ||||||
| from rest_framework.viewsets import ModelViewSet | from rest_framework.viewsets import ModelViewSet | ||||||
| from rest_framework_guardian.filters import ObjectPermissionsFilter | from rest_framework_guardian.filters import ObjectPermissionsFilter | ||||||
|  | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h | from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h | ||||||
| from authentik.api.decorators import permission_required | from authentik.api.decorators import permission_required | ||||||
| @ -30,8 +35,13 @@ from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict | |||||||
| from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER | from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER | ||||||
| from authentik.core.models import Token, TokenIntents, User | from authentik.core.models import Token, TokenIntents, User | ||||||
| from authentik.events.models import EventAction | from authentik.events.models import EventAction | ||||||
|  | from authentik.stages.email.models import EmailStage | ||||||
|  | from authentik.stages.email.tasks import send_mails | ||||||
|  | from authentik.stages.email.utils import TemplateEmailMessage | ||||||
| from authentik.tenants.models import Tenant | from authentik.tenants.models import Tenant | ||||||
|  |  | ||||||
|  | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
| class UserSerializer(ModelSerializer): | class UserSerializer(ModelSerializer): | ||||||
|     """User Serializer""" |     """User Serializer""" | ||||||
| @ -171,6 +181,28 @@ class UserViewSet(UsedByMixin, ModelViewSet): | |||||||
|     def get_queryset(self):  # pragma: no cover |     def get_queryset(self):  # pragma: no cover | ||||||
|         return User.objects.all().exclude(pk=get_anonymous_user().pk) |         return User.objects.all().exclude(pk=get_anonymous_user().pk) | ||||||
|  |  | ||||||
|  |     def _create_recovery_link(self) -> tuple[Optional[str], Optional[Token]]: | ||||||
|  |         """Create a recovery link (when the current tenant has a recovery flow set), | ||||||
|  |         that can either be shown to an admin or sent to the user directly""" | ||||||
|  |         tenant: Tenant = self.request._request.tenant | ||||||
|  |         # Check that there is a recovery flow, if not return an error | ||||||
|  |         flow = tenant.flow_recovery | ||||||
|  |         if not flow: | ||||||
|  |             LOGGER.debug("No recovery flow set") | ||||||
|  |             return None, None | ||||||
|  |         user: User = self.get_object() | ||||||
|  |         token, __ = Token.objects.get_or_create( | ||||||
|  |             identifier=f"{user.uid}-password-reset", | ||||||
|  |             user=user, | ||||||
|  |             intent=TokenIntents.INTENT_RECOVERY, | ||||||
|  |         ) | ||||||
|  |         querystring = urlencode({"token": token.key}) | ||||||
|  |         link = self.request.build_absolute_uri( | ||||||
|  |             reverse_lazy("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) | ||||||
|  |             + f"?{querystring}" | ||||||
|  |         ) | ||||||
|  |         return link, token | ||||||
|  |  | ||||||
|     @extend_schema(responses={200: SessionUserSerializer(many=False)}) |     @extend_schema(responses={200: SessionUserSerializer(many=False)}) | ||||||
|     @action(detail=False, pagination_class=None, filter_backends=[]) |     @action(detail=False, pagination_class=None, filter_backends=[]) | ||||||
|     # pylint: disable=invalid-name |     # pylint: disable=invalid-name | ||||||
| @ -226,24 +258,60 @@ class UserViewSet(UsedByMixin, ModelViewSet): | |||||||
|     # pylint: disable=invalid-name, unused-argument |     # pylint: disable=invalid-name, unused-argument | ||||||
|     def recovery(self, request: Request, pk: int) -> Response: |     def recovery(self, request: Request, pk: int) -> Response: | ||||||
|         """Create a temporary link that a user can use to recover their accounts""" |         """Create a temporary link that a user can use to recover their accounts""" | ||||||
|         tenant: Tenant = request._request.tenant |         link, _ = self._create_recovery_link() | ||||||
|         # Check that there is a recovery flow, if not return an error |         if not link: | ||||||
|         flow = tenant.flow_recovery |             LOGGER.debug("Couldn't create token") | ||||||
|         if not flow: |  | ||||||
|             return Response({"link": ""}, status=404) |             return Response({"link": ""}, status=404) | ||||||
|         user: User = self.get_object() |  | ||||||
|         token, __ = Token.objects.get_or_create( |  | ||||||
|             identifier=f"{user.uid}-password-reset", |  | ||||||
|             user=user, |  | ||||||
|             intent=TokenIntents.INTENT_RECOVERY, |  | ||||||
|         ) |  | ||||||
|         querystring = urlencode({"token": token.key}) |  | ||||||
|         link = request.build_absolute_uri( |  | ||||||
|             reverse_lazy("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) |  | ||||||
|             + f"?{querystring}" |  | ||||||
|         ) |  | ||||||
|         return Response({"link": link}) |         return Response({"link": link}) | ||||||
|  |  | ||||||
|  |     @permission_required("authentik_core.reset_user_password") | ||||||
|  |     @extend_schema( | ||||||
|  |         parameters=[ | ||||||
|  |             OpenApiParameter( | ||||||
|  |                 name="email_stage", | ||||||
|  |                 location=OpenApiParameter.QUERY, | ||||||
|  |                 type=OpenApiTypes.STR, | ||||||
|  |                 required=True, | ||||||
|  |             ) | ||||||
|  |         ], | ||||||
|  |         responses={ | ||||||
|  |             "204": Serializer(), | ||||||
|  |             "404": Serializer(), | ||||||
|  |         }, | ||||||
|  |     ) | ||||||
|  |     @action(detail=True, pagination_class=None, filter_backends=[]) | ||||||
|  |     # pylint: disable=invalid-name, unused-argument | ||||||
|  |     def recovery_email(self, request: Request, pk: int) -> Response: | ||||||
|  |         """Create a temporary link that a user can use to recover their accounts""" | ||||||
|  |         for_user = self.get_object() | ||||||
|  |         if for_user.email == "": | ||||||
|  |             LOGGER.debug("User doesn't have an email address") | ||||||
|  |             return Response(status=404) | ||||||
|  |         link, token = self._create_recovery_link() | ||||||
|  |         if not link: | ||||||
|  |             LOGGER.debug("Couldn't create token") | ||||||
|  |             return Response(status=404) | ||||||
|  |         # Lookup the email stage to assure the current user can access it | ||||||
|  |         stages = get_objects_for_user( | ||||||
|  |             request.user, "authentik_stages_email.view_emailstage" | ||||||
|  |         ).filter(pk=request.query_params.get("email_stage")) | ||||||
|  |         if not stages.exists(): | ||||||
|  |             LOGGER.debug("Email stage does not exist/user has no permissions") | ||||||
|  |             return Response(status=404) | ||||||
|  |         email_stage: EmailStage = stages.first() | ||||||
|  |         message = TemplateEmailMessage( | ||||||
|  |             subject=_(email_stage.subject), | ||||||
|  |             template_name=email_stage.template, | ||||||
|  |             to=[for_user.email], | ||||||
|  |             template_context={ | ||||||
|  |                 "url": link, | ||||||
|  |                 "user": for_user, | ||||||
|  |                 "expires": token.expires, | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         send_mails(email_stage, message) | ||||||
|  |         return Response(status=204) | ||||||
|  |  | ||||||
|     def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet: |     def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet: | ||||||
|         """Custom filter_queryset method which ignores guardian, but still supports sorting""" |         """Custom filter_queryset method which ignores guardian, but still supports sorting""" | ||||||
|         for backend in list(self.filter_backends): |         for backend in list(self.filter_backends): | ||||||
|  | |||||||
| @ -3,6 +3,9 @@ from django.urls.base import reverse | |||||||
| from rest_framework.test import APITestCase | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
|  | from authentik.flows.models import Flow, FlowDesignation | ||||||
|  | from authentik.stages.email.models import EmailStage | ||||||
|  | from authentik.tenants.models import Tenant | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestUsersAPI(APITestCase): | class TestUsersAPI(APITestCase): | ||||||
| @ -27,3 +30,78 @@ class TestUsersAPI(APITestCase): | |||||||
|             reverse("authentik_api:user-metrics", kwargs={"pk": self.user.pk}) |             reverse("authentik_api:user-metrics", kwargs={"pk": self.user.pk}) | ||||||
|         ) |         ) | ||||||
|         self.assertEqual(response.status_code, 403) |         self.assertEqual(response.status_code, 403) | ||||||
|  |  | ||||||
|  |     def test_recovery_no_flow(self): | ||||||
|  |         """Test user recovery link (no recovery flow set)""" | ||||||
|  |         self.client.force_login(self.admin) | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}) | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, 404) | ||||||
|  |  | ||||||
|  |     def test_recovery(self): | ||||||
|  |         """Test user recovery link (no recovery flow set)""" | ||||||
|  |         flow = Flow.objects.create( | ||||||
|  |             name="test", title="test", slug="test", designation=FlowDesignation.RECOVERY | ||||||
|  |         ) | ||||||
|  |         tenant: Tenant = Tenant.objects.first() | ||||||
|  |         tenant.flow_recovery = flow | ||||||
|  |         tenant.save() | ||||||
|  |         self.client.force_login(self.admin) | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}) | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|  |     def test_recovery_email_no_flow(self): | ||||||
|  |         """Test user recovery link (no recovery flow set)""" | ||||||
|  |         self.client.force_login(self.admin) | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk}) | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, 404) | ||||||
|  |         self.user.email = "foo@bar.baz" | ||||||
|  |         self.user.save() | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk}) | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, 404) | ||||||
|  |  | ||||||
|  |     def test_recovery_email_no_stage(self): | ||||||
|  |         """Test user recovery link (no email stage)""" | ||||||
|  |         self.user.email = "foo@bar.baz" | ||||||
|  |         self.user.save() | ||||||
|  |         flow = Flow.objects.create( | ||||||
|  |             name="test", title="test", slug="test", designation=FlowDesignation.RECOVERY | ||||||
|  |         ) | ||||||
|  |         tenant: Tenant = Tenant.objects.first() | ||||||
|  |         tenant.flow_recovery = flow | ||||||
|  |         tenant.save() | ||||||
|  |         self.client.force_login(self.admin) | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk}) | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, 404) | ||||||
|  |  | ||||||
|  |     def test_recovery_email(self): | ||||||
|  |         """Test user recovery link""" | ||||||
|  |         self.user.email = "foo@bar.baz" | ||||||
|  |         self.user.save() | ||||||
|  |         flow = Flow.objects.create( | ||||||
|  |             name="test", title="test", slug="test", designation=FlowDesignation.RECOVERY | ||||||
|  |         ) | ||||||
|  |         tenant: Tenant = Tenant.objects.first() | ||||||
|  |         tenant.flow_recovery = flow | ||||||
|  |         tenant.save() | ||||||
|  |  | ||||||
|  |         stage = EmailStage.objects.create(name="email") | ||||||
|  |  | ||||||
|  |         self.client.force_login(self.admin) | ||||||
|  |         response = self.client.get( | ||||||
|  |             reverse( | ||||||
|  |                 "authentik_api:user-recovery-email", | ||||||
|  |                 kwargs={"pk": self.user.pk}, | ||||||
|  |             ) | ||||||
|  |             + f"?email_stage={stage.pk}" | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, 204) | ||||||
|  | |||||||
							
								
								
									
										30
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								schema.yml
									
									
									
									
									
								
							| @ -3185,6 +3185,36 @@ paths: | |||||||
|           $ref: '#/components/schemas/ValidationError' |           $ref: '#/components/schemas/ValidationError' | ||||||
|         '403': |         '403': | ||||||
|           $ref: '#/components/schemas/GenericError' |           $ref: '#/components/schemas/GenericError' | ||||||
|  |   /api/v2beta/core/users/{id}/recovery_email/: | ||||||
|  |     get: | ||||||
|  |       operationId: core_users_recovery_email_retrieve | ||||||
|  |       description: Create a temporary link that a user can use to recover their accounts | ||||||
|  |       parameters: | ||||||
|  |       - in: query | ||||||
|  |         name: email_stage | ||||||
|  |         schema: | ||||||
|  |           type: string | ||||||
|  |         required: true | ||||||
|  |       - in: path | ||||||
|  |         name: id | ||||||
|  |         schema: | ||||||
|  |           type: integer | ||||||
|  |         description: A unique integer value identifying this User. | ||||||
|  |         required: true | ||||||
|  |       tags: | ||||||
|  |       - core | ||||||
|  |       security: | ||||||
|  |       - authentik: [] | ||||||
|  |       - cookieAuth: [] | ||||||
|  |       responses: | ||||||
|  |         '204': | ||||||
|  |           description: No response body | ||||||
|  |         '404': | ||||||
|  |           description: No response body | ||||||
|  |         '400': | ||||||
|  |           $ref: '#/components/schemas/ValidationError' | ||||||
|  |         '403': | ||||||
|  |           $ref: '#/components/schemas/GenericError' | ||||||
|   /api/v2beta/core/users/{id}/used_by/: |   /api/v2beta/core/users/{id}/used_by/: | ||||||
|     get: |     get: | ||||||
|       operationId: core_users_used_by_list |       operationId: core_users_used_by_list | ||||||
|  | |||||||
| @ -122,8 +122,13 @@ msgstr "Actions" | |||||||
| msgid "Actions over the last 24 hours" | msgid "Actions over the last 24 hours" | ||||||
| msgstr "Actions over the last 24 hours" | msgstr "Actions over the last 24 hours" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Activate" | ||||||
|  | msgstr "Activate" | ||||||
|  |  | ||||||
| #: src/pages/groups/MemberSelectModal.ts | #: src/pages/groups/MemberSelectModal.ts | ||||||
| #: src/pages/users/UserListPage.ts | #: src/pages/users/UserListPage.ts | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
| #: src/pages/users/UserViewPage.ts | #: src/pages/users/UserViewPage.ts | ||||||
| msgid "Active" | msgid "Active" | ||||||
| msgstr "Active" | msgstr "Active" | ||||||
| @ -560,6 +565,10 @@ msgstr "Certificates" | |||||||
| msgid "Change password" | msgid "Change password" | ||||||
| msgstr "Change password" | msgstr "Change password" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Change status" | ||||||
|  | msgstr "Change status" | ||||||
|  |  | ||||||
| #: src/pages/user-settings/settings/UserSettingsPassword.ts | #: src/pages/user-settings/settings/UserSettingsPassword.ts | ||||||
| msgid "Change your password" | msgid "Change your password" | ||||||
| msgstr "Change your password" | msgstr "Change your password" | ||||||
| @ -862,6 +871,10 @@ msgstr "Copy Key" | |||||||
| msgid "Copy download URL" | msgid "Copy download URL" | ||||||
| msgstr "Copy download URL" | msgstr "Copy download URL" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Copy recovery link" | ||||||
|  | msgstr "Copy recovery link" | ||||||
|  |  | ||||||
| #: src/pages/applications/ApplicationForm.ts | #: src/pages/applications/ApplicationForm.ts | ||||||
| #: src/pages/applications/ApplicationListPage.ts | #: src/pages/applications/ApplicationListPage.ts | ||||||
| #: src/pages/applications/ApplicationListPage.ts | #: src/pages/applications/ApplicationListPage.ts | ||||||
| @ -1042,6 +1055,10 @@ msgstr "Date" | |||||||
| msgid "Date Time" | msgid "Date Time" | ||||||
| msgstr "Date Time" | msgstr "Date Time" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Deactivate" | ||||||
|  | msgstr "Deactivate" | ||||||
|  |  | ||||||
| #: src/pages/flows/FlowForm.ts | #: src/pages/flows/FlowForm.ts | ||||||
| msgid "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik." | msgid "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik." | ||||||
| msgstr "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik." | msgstr "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik." | ||||||
| @ -1200,10 +1217,10 @@ msgstr "Digest algorithm" | |||||||
| msgid "Digits" | msgid "Digits" | ||||||
| msgstr "Digits" | msgstr "Digits" | ||||||
|  |  | ||||||
| #: src/pages/users/UserListPage.ts | #:  | ||||||
| #: src/pages/users/UserListPage.ts | #:  | ||||||
| msgid "Disable" | #~ msgid "Disable" | ||||||
| msgstr "Disable" | #~ msgstr "Disable" | ||||||
|  |  | ||||||
| #: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts | #: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts | ||||||
| msgid "Disable Duo authenticator" | msgid "Disable Duo authenticator" | ||||||
| @ -1286,7 +1303,6 @@ msgstr "Each provider has a different issuer, based on the application slug." | |||||||
| #: src/pages/sources/oauth/OAuthSourceViewPage.ts | #: src/pages/sources/oauth/OAuthSourceViewPage.ts | ||||||
| #: src/pages/sources/plex/PlexSourceViewPage.ts | #: src/pages/sources/plex/PlexSourceViewPage.ts | ||||||
| #: src/pages/sources/saml/SAMLSourceViewPage.ts | #: src/pages/sources/saml/SAMLSourceViewPage.ts | ||||||
| #: src/pages/users/UserListPage.ts |  | ||||||
| #: src/pages/users/UserViewPage.ts | #: src/pages/users/UserViewPage.ts | ||||||
| msgid "Edit" | msgid "Edit" | ||||||
| msgstr "Edit" | msgstr "Edit" | ||||||
| @ -1333,18 +1349,26 @@ msgstr "Email address" | |||||||
| msgid "Email info:" | msgid "Email info:" | ||||||
| msgstr "Email info:" | msgstr "Email info:" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Email recovery link" | ||||||
|  | msgstr "Email recovery link" | ||||||
|  |  | ||||||
| #: src/pages/events/utils.ts | #: src/pages/events/utils.ts | ||||||
| msgid "Email sent" | msgid "Email sent" | ||||||
| msgstr "Email sent" | msgstr "Email sent" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserResetEmailForm.ts | ||||||
|  | msgid "Email stage" | ||||||
|  | msgstr "Email stage" | ||||||
|  |  | ||||||
| #: src/pages/stages/prompt/PromptForm.ts | #: src/pages/stages/prompt/PromptForm.ts | ||||||
| msgid "Email: Text field with Email type." | msgid "Email: Text field with Email type." | ||||||
| msgstr "Email: Text field with Email type." | msgstr "Email: Text field with Email type." | ||||||
|  |  | ||||||
| #: src/pages/users/UserListPage.ts | #:  | ||||||
| #: src/pages/users/UserListPage.ts | #:  | ||||||
| msgid "Enable" | #~ msgid "Enable" | ||||||
| msgstr "Enable" | #~ msgstr "Enable" | ||||||
|  |  | ||||||
| #: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts | #: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts | ||||||
| msgid "Enable Duo authenticator" | msgid "Enable Duo authenticator" | ||||||
| @ -1942,6 +1966,10 @@ msgstr "Import certificates of external providers or create certificates to sign | |||||||
| msgid "In case you can't access any other method." | msgid "In case you can't access any other method." | ||||||
| msgstr "In case you can't access any other method." | msgstr "In case you can't access any other method." | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Inactive" | ||||||
|  | msgstr "Inactive" | ||||||
|  |  | ||||||
| #: src/pages/providers/oauth2/OAuth2ProviderForm.ts | #: src/pages/providers/oauth2/OAuth2ProviderForm.ts | ||||||
| msgid "Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint." | msgid "Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint." | ||||||
| msgstr "Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint." | msgstr "Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint." | ||||||
| @ -2210,6 +2238,7 @@ msgstr "Loading" | |||||||
| #: src/pages/tenants/TenantForm.ts | #: src/pages/tenants/TenantForm.ts | ||||||
| #: src/pages/tenants/TenantForm.ts | #: src/pages/tenants/TenantForm.ts | ||||||
| #: src/pages/tenants/TenantForm.ts | #: src/pages/tenants/TenantForm.ts | ||||||
|  | #: src/pages/users/UserResetEmailForm.ts | ||||||
| msgid "Loading..." | msgid "Loading..." | ||||||
| msgstr "Loading..." | msgstr "Loading..." | ||||||
|  |  | ||||||
| @ -3066,6 +3095,7 @@ msgid "Receive a push notification on your phone to prove your identity." | |||||||
| msgstr "Receive a push notification on your phone to prove your identity." | msgstr "Receive a push notification on your phone to prove your identity." | ||||||
|  |  | ||||||
| #: src/pages/flows/FlowForm.ts | #: src/pages/flows/FlowForm.ts | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
| msgid "Recovery" | msgid "Recovery" | ||||||
| msgstr "Recovery" | msgstr "Recovery" | ||||||
|  |  | ||||||
| @ -3082,6 +3112,10 @@ msgstr "Recovery flow. If left empty, the first applicable flow sorted by the sl | |||||||
| msgid "Recovery keys" | msgid "Recovery keys" | ||||||
| msgstr "Recovery keys" | msgstr "Recovery keys" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Recovery link cannot be emailed, user has no email address saved." | ||||||
|  | msgstr "Recovery link cannot be emailed, user has no email address saved." | ||||||
|  |  | ||||||
| #: src/pages/providers/saml/SAMLProviderForm.ts | #: src/pages/providers/saml/SAMLProviderForm.ts | ||||||
| msgid "Redirect" | msgid "Redirect" | ||||||
| msgstr "Redirect" | msgstr "Redirect" | ||||||
| @ -3114,6 +3148,10 @@ msgstr "Register device" | |||||||
| msgid "Regular expressions for which authentication is not required. Each new line is interpreted as a new Regular Expression." | msgid "Regular expressions for which authentication is not required. Each new line is interpreted as a new Regular Expression." | ||||||
| msgstr "Regular expressions for which authentication is not required. Each new line is interpreted as a new Regular Expression." | msgstr "Regular expressions for which authentication is not required. Each new line is interpreted as a new Regular Expression." | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Regular user" | ||||||
|  | msgstr "Regular user" | ||||||
|  |  | ||||||
| #: src/pages/applications/ApplicationViewPage.ts | #: src/pages/applications/ApplicationViewPage.ts | ||||||
| #: src/pages/flows/FlowViewPage.ts | #: src/pages/flows/FlowViewPage.ts | ||||||
| msgid "Related" | msgid "Related" | ||||||
| @ -3161,7 +3199,6 @@ msgstr "Required" | |||||||
| msgid "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only." | msgid "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only." | ||||||
| msgstr "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only." | msgstr "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only." | ||||||
|  |  | ||||||
| #: src/pages/users/UserListPage.ts |  | ||||||
| #: src/pages/users/UserViewPage.ts | #: src/pages/users/UserViewPage.ts | ||||||
| msgid "Reset Password" | msgid "Reset Password" | ||||||
| msgstr "Reset Password" | msgstr "Reset Password" | ||||||
| @ -3362,6 +3399,10 @@ msgstr "Selection of backends to test the password against." | |||||||
| msgid "Send Email again." | msgid "Send Email again." | ||||||
| msgstr "Send Email again." | msgstr "Send Email again." | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Send link" | ||||||
|  | msgstr "Send link" | ||||||
|  |  | ||||||
| #: src/pages/events/RuleListPage.ts | #: src/pages/events/RuleListPage.ts | ||||||
| msgid "Send notifications whenever a specific Event is created and matched by policies." | msgid "Send notifications whenever a specific Event is created and matched by policies." | ||||||
| msgstr "Send notifications whenever a specific Event is created and matched by policies." | msgstr "Send notifications whenever a specific Event is created and matched by policies." | ||||||
| @ -3370,6 +3411,10 @@ msgstr "Send notifications whenever a specific Event is created and matched by p | |||||||
| msgid "Send once" | msgid "Send once" | ||||||
| msgstr "Send once" | msgstr "Send once" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Send recovery link to user" | ||||||
|  | msgstr "Send recovery link to user" | ||||||
|  |  | ||||||
| #: src/pages/events/RuleListPage.ts | #: src/pages/events/RuleListPage.ts | ||||||
| msgid "Sent to group" | msgid "Sent to group" | ||||||
| msgstr "Sent to group" | msgstr "Sent to group" | ||||||
| @ -3793,6 +3838,10 @@ msgstr "Successfully imported flow." | |||||||
| msgid "Successfully imported provider." | msgid "Successfully imported provider." | ||||||
| msgstr "Successfully imported provider." | msgstr "Successfully imported provider." | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserResetEmailForm.ts | ||||||
|  | msgid "Successfully sent email." | ||||||
|  | msgstr "Successfully sent email." | ||||||
|  |  | ||||||
| #: src/pages/applications/ApplicationCheckAccessForm.ts | #: src/pages/applications/ApplicationCheckAccessForm.ts | ||||||
| #: src/pages/policies/PolicyTestForm.ts | #: src/pages/policies/PolicyTestForm.ts | ||||||
| #: src/pages/property-mappings/PropertyMappingTestForm.ts | #: src/pages/property-mappings/PropertyMappingTestForm.ts | ||||||
| @ -3925,6 +3974,7 @@ msgstr "Successfully updated user." | |||||||
| msgid "Successfully updated {0} {1}" | msgid "Successfully updated {0} {1}" | ||||||
| msgstr "Successfully updated {0} {1}" | msgstr "Successfully updated {0} {1}" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
| #: src/pages/users/UserViewPage.ts | #: src/pages/users/UserViewPage.ts | ||||||
| msgid "Superuser" | msgid "Superuser" | ||||||
| msgstr "Superuser" | msgstr "Superuser" | ||||||
| @ -4569,6 +4619,10 @@ msgstr "User object filter" | |||||||
| msgid "User password writeback" | msgid "User password writeback" | ||||||
| msgstr "User password writeback" | msgstr "User password writeback" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "User status" | ||||||
|  | msgstr "User status" | ||||||
|  |  | ||||||
| #: src/pages/events/utils.ts | #: src/pages/events/utils.ts | ||||||
| msgid "User was written to" | msgid "User was written to" | ||||||
| msgstr "User was written to" | msgstr "User was written to" | ||||||
|  | |||||||
| @ -122,8 +122,13 @@ msgstr "" | |||||||
| msgid "Actions over the last 24 hours" | msgid "Actions over the last 24 hours" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Activate" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/groups/MemberSelectModal.ts | #: src/pages/groups/MemberSelectModal.ts | ||||||
| #: src/pages/users/UserListPage.ts | #: src/pages/users/UserListPage.ts | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
| #: src/pages/users/UserViewPage.ts | #: src/pages/users/UserViewPage.ts | ||||||
| msgid "Active" | msgid "Active" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -556,6 +561,10 @@ msgstr "" | |||||||
| msgid "Change password" | msgid "Change password" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Change status" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/user-settings/settings/UserSettingsPassword.ts | #: src/pages/user-settings/settings/UserSettingsPassword.ts | ||||||
| msgid "Change your password" | msgid "Change your password" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -856,6 +865,10 @@ msgstr "" | |||||||
| msgid "Copy download URL" | msgid "Copy download URL" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Copy recovery link" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/applications/ApplicationForm.ts | #: src/pages/applications/ApplicationForm.ts | ||||||
| #: src/pages/applications/ApplicationListPage.ts | #: src/pages/applications/ApplicationListPage.ts | ||||||
| #: src/pages/applications/ApplicationListPage.ts | #: src/pages/applications/ApplicationListPage.ts | ||||||
| @ -1036,6 +1049,10 @@ msgstr "" | |||||||
| msgid "Date Time" | msgid "Date Time" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Deactivate" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/flows/FlowForm.ts | #: src/pages/flows/FlowForm.ts | ||||||
| msgid "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik." | msgid "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -1192,10 +1209,10 @@ msgstr "" | |||||||
| msgid "Digits" | msgid "Digits" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/users/UserListPage.ts | #:  | ||||||
| #: src/pages/users/UserListPage.ts | #:  | ||||||
| msgid "Disable" | #~ msgid "Disable" | ||||||
| msgstr "" | #~ msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts | #: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts | ||||||
| msgid "Disable Duo authenticator" | msgid "Disable Duo authenticator" | ||||||
| @ -1278,7 +1295,6 @@ msgstr "" | |||||||
| #: src/pages/sources/oauth/OAuthSourceViewPage.ts | #: src/pages/sources/oauth/OAuthSourceViewPage.ts | ||||||
| #: src/pages/sources/plex/PlexSourceViewPage.ts | #: src/pages/sources/plex/PlexSourceViewPage.ts | ||||||
| #: src/pages/sources/saml/SAMLSourceViewPage.ts | #: src/pages/sources/saml/SAMLSourceViewPage.ts | ||||||
| #: src/pages/users/UserListPage.ts |  | ||||||
| #: src/pages/users/UserViewPage.ts | #: src/pages/users/UserViewPage.ts | ||||||
| msgid "Edit" | msgid "Edit" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -1325,18 +1341,26 @@ msgstr "" | |||||||
| msgid "Email info:" | msgid "Email info:" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Email recovery link" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/events/utils.ts | #: src/pages/events/utils.ts | ||||||
| msgid "Email sent" | msgid "Email sent" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserResetEmailForm.ts | ||||||
|  | msgid "Email stage" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/stages/prompt/PromptForm.ts | #: src/pages/stages/prompt/PromptForm.ts | ||||||
| msgid "Email: Text field with Email type." | msgid "Email: Text field with Email type." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/users/UserListPage.ts | #:  | ||||||
| #: src/pages/users/UserListPage.ts | #:  | ||||||
| msgid "Enable" | #~ msgid "Enable" | ||||||
| msgstr "" | #~ msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts | #: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts | ||||||
| msgid "Enable Duo authenticator" | msgid "Enable Duo authenticator" | ||||||
| @ -1934,6 +1958,10 @@ msgstr "" | |||||||
| msgid "In case you can't access any other method." | msgid "In case you can't access any other method." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Inactive" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/providers/oauth2/OAuth2ProviderForm.ts | #: src/pages/providers/oauth2/OAuth2ProviderForm.ts | ||||||
| msgid "Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint." | msgid "Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -2202,6 +2230,7 @@ msgstr "" | |||||||
| #: src/pages/tenants/TenantForm.ts | #: src/pages/tenants/TenantForm.ts | ||||||
| #: src/pages/tenants/TenantForm.ts | #: src/pages/tenants/TenantForm.ts | ||||||
| #: src/pages/tenants/TenantForm.ts | #: src/pages/tenants/TenantForm.ts | ||||||
|  | #: src/pages/users/UserResetEmailForm.ts | ||||||
| msgid "Loading..." | msgid "Loading..." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| @ -3058,6 +3087,7 @@ msgid "Receive a push notification on your phone to prove your identity." | |||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/flows/FlowForm.ts | #: src/pages/flows/FlowForm.ts | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
| msgid "Recovery" | msgid "Recovery" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| @ -3074,6 +3104,10 @@ msgstr "" | |||||||
| msgid "Recovery keys" | msgid "Recovery keys" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Recovery link cannot be emailed, user has no email address saved." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/providers/saml/SAMLProviderForm.ts | #: src/pages/providers/saml/SAMLProviderForm.ts | ||||||
| msgid "Redirect" | msgid "Redirect" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -3106,6 +3140,10 @@ msgstr "" | |||||||
| msgid "Regular expressions for which authentication is not required. Each new line is interpreted as a new Regular Expression." | msgid "Regular expressions for which authentication is not required. Each new line is interpreted as a new Regular Expression." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Regular user" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/applications/ApplicationViewPage.ts | #: src/pages/applications/ApplicationViewPage.ts | ||||||
| #: src/pages/flows/FlowViewPage.ts | #: src/pages/flows/FlowViewPage.ts | ||||||
| msgid "Related" | msgid "Related" | ||||||
| @ -3153,7 +3191,6 @@ msgstr "" | |||||||
| msgid "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only." | msgid "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/users/UserListPage.ts |  | ||||||
| #: src/pages/users/UserViewPage.ts | #: src/pages/users/UserViewPage.ts | ||||||
| msgid "Reset Password" | msgid "Reset Password" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -3354,6 +3391,10 @@ msgstr "" | |||||||
| msgid "Send Email again." | msgid "Send Email again." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Send link" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/events/RuleListPage.ts | #: src/pages/events/RuleListPage.ts | ||||||
| msgid "Send notifications whenever a specific Event is created and matched by policies." | msgid "Send notifications whenever a specific Event is created and matched by policies." | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -3362,6 +3403,10 @@ msgstr "" | |||||||
| msgid "Send once" | msgid "Send once" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "Send recovery link to user" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/events/RuleListPage.ts | #: src/pages/events/RuleListPage.ts | ||||||
| msgid "Sent to group" | msgid "Sent to group" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -3785,6 +3830,10 @@ msgstr "" | |||||||
| msgid "Successfully imported provider." | msgid "Successfully imported provider." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserResetEmailForm.ts | ||||||
|  | msgid "Successfully sent email." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/applications/ApplicationCheckAccessForm.ts | #: src/pages/applications/ApplicationCheckAccessForm.ts | ||||||
| #: src/pages/policies/PolicyTestForm.ts | #: src/pages/policies/PolicyTestForm.ts | ||||||
| #: src/pages/property-mappings/PropertyMappingTestForm.ts | #: src/pages/property-mappings/PropertyMappingTestForm.ts | ||||||
| @ -3917,6 +3966,7 @@ msgstr "" | |||||||
| msgid "Successfully updated {0} {1}" | msgid "Successfully updated {0} {1}" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
| #: src/pages/users/UserViewPage.ts | #: src/pages/users/UserViewPage.ts | ||||||
| msgid "Superuser" | msgid "Superuser" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -4554,6 +4604,10 @@ msgstr "" | |||||||
| msgid "User password writeback" | msgid "User password writeback" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/users/UserListPage.ts | ||||||
|  | msgid "User status" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/events/utils.ts | #: src/pages/events/utils.ts | ||||||
| msgid "User was written to" | msgid "User was written to" | ||||||
| msgstr "" | msgstr "" | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ export class OutpostForm extends ModelForm<Outpost, string> { | |||||||
|     type: OutpostTypeEnum = OutpostTypeEnum.Proxy; |     type: OutpostTypeEnum = OutpostTypeEnum.Proxy; | ||||||
|  |  | ||||||
|     @property({ type: Boolean }) |     @property({ type: Boolean }) | ||||||
|     embedded: boolean = false; |     embedded = false; | ||||||
|  |  | ||||||
|     loadInstance(pk: string): Promise<Outpost> { |     loadInstance(pk: string): Promise<Outpost> { | ||||||
|         return new OutpostsApi(DEFAULT_CONFIG) |         return new OutpostsApi(DEFAULT_CONFIG) | ||||||
|  | |||||||
| @ -1,10 +1,10 @@ | |||||||
| import { t } from "@lingui/macro"; | import { t } from "@lingui/macro"; | ||||||
| import { customElement, html, property, TemplateResult } from "lit-element"; | import { CSSResult, customElement, html, property, TemplateResult } from "lit-element"; | ||||||
| import { AKResponse } from "../../api/Client"; | import { AKResponse } from "../../api/Client"; | ||||||
| import { TablePage } from "../../elements/table/TablePage"; | import { TablePage } from "../../elements/table/TablePage"; | ||||||
|  | import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; | ||||||
|  |  | ||||||
| import "../../elements/forms/ModalForm"; | import "../../elements/forms/ModalForm"; | ||||||
| import "../../elements/buttons/Dropdown"; |  | ||||||
| import "../../elements/buttons/ActionButton"; | import "../../elements/buttons/ActionButton"; | ||||||
| import { TableColumn } from "../../elements/table/Table"; | import { TableColumn } from "../../elements/table/Table"; | ||||||
| import { PAGE_SIZE } from "../../constants"; | import { PAGE_SIZE } from "../../constants"; | ||||||
| @ -13,6 +13,7 @@ import { DEFAULT_CONFIG, tenant } from "../../api/Config"; | |||||||
| import "../../elements/forms/DeleteForm"; | import "../../elements/forms/DeleteForm"; | ||||||
| import "./UserActiveForm"; | import "./UserActiveForm"; | ||||||
| import "./UserForm"; | import "./UserForm"; | ||||||
|  | import "./UserResetEmailForm"; | ||||||
| import { showMessage } from "../../elements/messages/MessageContainer"; | import { showMessage } from "../../elements/messages/MessageContainer"; | ||||||
| import { MessageLevel } from "../../elements/messages/Message"; | import { MessageLevel } from "../../elements/messages/Message"; | ||||||
| import { first } from "../../utils"; | import { first } from "../../utils"; | ||||||
| @ -20,6 +21,9 @@ import { until } from "lit-html/directives/until"; | |||||||
|  |  | ||||||
| @customElement("ak-user-list") | @customElement("ak-user-list") | ||||||
| export class UserListPage extends TablePage<User> { | export class UserListPage extends TablePage<User> { | ||||||
|  |     expandable = true; | ||||||
|  |     checkbox = true; | ||||||
|  |  | ||||||
|     searchEnabled(): boolean { |     searchEnabled(): boolean { | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| @ -39,6 +43,10 @@ export class UserListPage extends TablePage<User> { | |||||||
|     @property({ type: Boolean }) |     @property({ type: Boolean }) | ||||||
|     hideServiceAccounts = true; |     hideServiceAccounts = true; | ||||||
|  |  | ||||||
|  |     static get styles(): CSSResult[] { | ||||||
|  |         return super.styles.concat(PFDescriptionList); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     apiEndpoint(page: number): Promise<AKResponse<User>> { |     apiEndpoint(page: number): Promise<AKResponse<User>> { | ||||||
|         return new CoreApi(DEFAULT_CONFIG).coreUsersList({ |         return new CoreApi(DEFAULT_CONFIG).coreUsersList({ | ||||||
|             ordering: this.order, |             ordering: this.order, | ||||||
| @ -62,6 +70,29 @@ export class UserListPage extends TablePage<User> { | |||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     renderToolbarSelected(): TemplateResult { | ||||||
|  |         const disabled = this.selectedElements.length !== 1; | ||||||
|  |         const item = this.selectedElements[0]; | ||||||
|  |         return html` <ak-forms-delete | ||||||
|  |             .obj=${item} | ||||||
|  |             objectLabel=${t`User`} | ||||||
|  |             .usedBy=${() => { | ||||||
|  |                 return new CoreApi(DEFAULT_CONFIG).coreUsersUsedByList({ | ||||||
|  |                     id: item.pk, | ||||||
|  |                 }); | ||||||
|  |             }} | ||||||
|  |             .delete=${() => { | ||||||
|  |                 return new CoreApi(DEFAULT_CONFIG).coreUsersDestroy({ | ||||||
|  |                     id: item.pk, | ||||||
|  |                 }); | ||||||
|  |             }} | ||||||
|  |         > | ||||||
|  |             <button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger"> | ||||||
|  |                 ${t`Delete`} | ||||||
|  |             </button> | ||||||
|  |         </ak-forms-delete>`; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     row(item: User): TemplateResult[] { |     row(item: User): TemplateResult[] { | ||||||
|         return [ |         return [ | ||||||
|             html`<a href="#/identity/users/${item.pk}"> |             html`<a href="#/identity/users/${item.pk}"> | ||||||
| @ -74,100 +105,135 @@ export class UserListPage extends TablePage<User> { | |||||||
|                     <span slot="submit"> ${t`Update`} </span> |                     <span slot="submit"> ${t`Update`} </span> | ||||||
|                     <span slot="header"> ${t`Update User`} </span> |                     <span slot="header"> ${t`Update User`} </span> | ||||||
|                     <ak-user-form slot="form" .instancePk=${item.pk}> </ak-user-form> |                     <ak-user-form slot="form" .instancePk=${item.pk}> </ak-user-form> | ||||||
|                     <button slot="trigger" class="pf-m-secondary pf-c-button">${t`Edit`}</button> |                     <button slot="trigger" class="pf-c-button pf-m-plain"> | ||||||
|                 </ak-forms-modal> |                         <i class="fas fa-edit"></i> | ||||||
|                 <ak-dropdown class="pf-c-dropdown"> |  | ||||||
|                     <button class="pf-m-primary pf-c-dropdown__toggle" type="button"> |  | ||||||
|                         <span class="pf-c-dropdown__toggle-text" |  | ||||||
|                             >${item.isActive ? t`Disable` : t`Enable`}</span |  | ||||||
|                         > |  | ||||||
|                         <i |  | ||||||
|                             class="fas fa-caret-down pf-c-dropdown__toggle-icon" |  | ||||||
|                             aria-hidden="true" |  | ||||||
|                         ></i> |  | ||||||
|                     </button> |                     </button> | ||||||
|                     <ul class="pf-c-dropdown__menu" hidden> |                 </ak-forms-modal> | ||||||
|                         <li> |  | ||||||
|                             <ak-user-active-form |  | ||||||
|                                 .obj=${item} |  | ||||||
|                                 objectLabel=${t`User`} |  | ||||||
|                                 .delete=${() => { |  | ||||||
|                                     return new CoreApi(DEFAULT_CONFIG).coreUsersPartialUpdate({ |  | ||||||
|                                         id: item.pk || 0, |  | ||||||
|                                         patchedUserRequest: { |  | ||||||
|                                             username: item.username, |  | ||||||
|                                             name: item.name, |  | ||||||
|                                             isActive: !item.isActive, |  | ||||||
|                                         }, |  | ||||||
|                                     }); |  | ||||||
|                                 }} |  | ||||||
|                             > |  | ||||||
|                                 <button slot="trigger" class="pf-c-dropdown__menu-item"> |  | ||||||
|                                     ${item.isActive ? t`Disable` : t`Enable`} |  | ||||||
|                                 </button> |  | ||||||
|                             </ak-user-active-form> |  | ||||||
|                         </li> |  | ||||||
|                         <li class="pf-c-divider" role="separator"></li> |  | ||||||
|                         <li> |  | ||||||
|                             <ak-forms-delete |  | ||||||
|                                 .obj=${item} |  | ||||||
|                                 objectLabel=${t`User`} |  | ||||||
|                                 .usedBy=${() => { |  | ||||||
|                                     return new CoreApi(DEFAULT_CONFIG).coreUsersUsedByList({ |  | ||||||
|                                         id: item.pk, |  | ||||||
|                                     }); |  | ||||||
|                                 }} |  | ||||||
|                                 .delete=${() => { |  | ||||||
|                                     return new CoreApi(DEFAULT_CONFIG).coreUsersDestroy({ |  | ||||||
|                                         id: item.pk, |  | ||||||
|                                     }); |  | ||||||
|                                 }} |  | ||||||
|                             > |  | ||||||
|                                 <button slot="trigger" class="pf-c-dropdown__menu-item"> |  | ||||||
|                                     ${t`Delete`} |  | ||||||
|                                 </button> |  | ||||||
|                             </ak-forms-delete> |  | ||||||
|                         </li> |  | ||||||
|                     </ul> |  | ||||||
|                 </ak-dropdown> |  | ||||||
|                 ${until( |  | ||||||
|                     tenant().then((te) => { |  | ||||||
|                         if (te.flowRecovery) { |  | ||||||
|                             return html` <ak-action-button |  | ||||||
|                                 .apiRequest=${() => { |  | ||||||
|                                     return new CoreApi(DEFAULT_CONFIG) |  | ||||||
|                                         .coreUsersRecoveryRetrieve({ |  | ||||||
|                                             id: item.pk || 0, |  | ||||||
|                                         }) |  | ||||||
|                                         .then((rec) => { |  | ||||||
|                                             showMessage({ |  | ||||||
|                                                 level: MessageLevel.success, |  | ||||||
|                                                 message: t`Successfully generated recovery link`, |  | ||||||
|                                                 description: rec.link, |  | ||||||
|                                             }); |  | ||||||
|                                         }) |  | ||||||
|                                         .catch((ex: Response) => { |  | ||||||
|                                             ex.json().then(() => { |  | ||||||
|                                                 showMessage({ |  | ||||||
|                                                     level: MessageLevel.error, |  | ||||||
|                                                     message: t`No recovery flow is configured.`, |  | ||||||
|                                                 }); |  | ||||||
|                                             }); |  | ||||||
|                                         }); |  | ||||||
|                                 }} |  | ||||||
|                             > |  | ||||||
|                                 ${t`Reset Password`} |  | ||||||
|                             </ak-action-button>`; |  | ||||||
|                         } |  | ||||||
|                         return html``; |  | ||||||
|                     }), |  | ||||||
|                 )} |  | ||||||
|                 <a class="pf-c-button pf-m-tertiary" href="${`/-/impersonation/${item.pk}/`}"> |                 <a class="pf-c-button pf-m-tertiary" href="${`/-/impersonation/${item.pk}/`}"> | ||||||
|                     ${t`Impersonate`} |                     ${t`Impersonate`} | ||||||
|                 </a>`, |                 </a>`, | ||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     renderExpanded(item: User): TemplateResult { | ||||||
|  |         return html`<td role="cell" colspan="3"> | ||||||
|  |                 <div class="pf-c-table__expandable-row-content"> | ||||||
|  |                     <dl class="pf-c-description-list pf-m-horizontal"> | ||||||
|  |                         <div class="pf-c-description-list__group"> | ||||||
|  |                             <dt class="pf-c-description-list__term"> | ||||||
|  |                                 <span class="pf-c-description-list__text">${t`User status`}</span> | ||||||
|  |                             </dt> | ||||||
|  |                             <dd class="pf-c-description-list__description"> | ||||||
|  |                                 <div class="pf-c-description-list__text"> | ||||||
|  |                                     ${item.isActive ? t`Active` : t`Inactive`} | ||||||
|  |                                 </div> | ||||||
|  |                                 <div class="pf-c-description-list__text"> | ||||||
|  |                                     ${item.isSuperuser ? t`Superuser` : t`Regular user`} | ||||||
|  |                                 </div> | ||||||
|  |                             </dd> | ||||||
|  |                         </div> | ||||||
|  |                         <div class="pf-c-description-list__group"> | ||||||
|  |                             <dt class="pf-c-description-list__term"> | ||||||
|  |                                 <span class="pf-c-description-list__text">${t`Change status`}</span> | ||||||
|  |                             </dt> | ||||||
|  |                             <dd class="pf-c-description-list__description"> | ||||||
|  |                                 <div class="pf-c-description-list__text"> | ||||||
|  |                                     <ak-user-active-form | ||||||
|  |                                         .obj=${item} | ||||||
|  |                                         objectLabel=${t`User`} | ||||||
|  |                                         .delete=${() => { | ||||||
|  |                                             return new CoreApi( | ||||||
|  |                                                 DEFAULT_CONFIG, | ||||||
|  |                                             ).coreUsersPartialUpdate({ | ||||||
|  |                                                 id: item.pk || 0, | ||||||
|  |                                                 patchedUserRequest: { | ||||||
|  |                                                     username: item.username, | ||||||
|  |                                                     name: item.name, | ||||||
|  |                                                     isActive: !item.isActive, | ||||||
|  |                                                 }, | ||||||
|  |                                             }); | ||||||
|  |                                         }} | ||||||
|  |                                     > | ||||||
|  |                                         <button slot="trigger" class="pf-c-button pf-m-warning"> | ||||||
|  |                                             ${item.isActive ? t`Deactivate` : t`Activate`} | ||||||
|  |                                         </button> | ||||||
|  |                                     </ak-user-active-form> | ||||||
|  |                                 </div> | ||||||
|  |                             </dd> | ||||||
|  |                         </div> | ||||||
|  |                         ${until( | ||||||
|  |                             tenant().then((te) => { | ||||||
|  |                                 if (!te.flowRecovery) { | ||||||
|  |                                     return html``; | ||||||
|  |                                 } | ||||||
|  |                                 return html`<div class="pf-c-description-list__group"> | ||||||
|  |                                     <dt class="pf-c-description-list__term"> | ||||||
|  |                                         <span class="pf-c-description-list__text" | ||||||
|  |                                             >${t`Recovery`}</span | ||||||
|  |                                         > | ||||||
|  |                                     </dt> | ||||||
|  |                                     <dd class="pf-c-description-list__description"> | ||||||
|  |                                         <div class="pf-c-description-list__text"> | ||||||
|  |                                             <ak-action-button | ||||||
|  |                                                 .apiRequest=${() => { | ||||||
|  |                                                     return new CoreApi(DEFAULT_CONFIG) | ||||||
|  |                                                         .coreUsersRecoveryRetrieve({ | ||||||
|  |                                                             id: item.pk || 0, | ||||||
|  |                                                         }) | ||||||
|  |                                                         .then((rec) => { | ||||||
|  |                                                             showMessage({ | ||||||
|  |                                                                 level: MessageLevel.success, | ||||||
|  |                                                                 message: t`Successfully generated recovery link`, | ||||||
|  |                                                                 description: rec.link, | ||||||
|  |                                                             }); | ||||||
|  |                                                         }) | ||||||
|  |                                                         .catch((ex: Response) => { | ||||||
|  |                                                             ex.json().then(() => { | ||||||
|  |                                                                 showMessage({ | ||||||
|  |                                                                     level: MessageLevel.error, | ||||||
|  |                                                                     message: t`No recovery flow is configured.`, | ||||||
|  |                                                                 }); | ||||||
|  |                                                             }); | ||||||
|  |                                                         }); | ||||||
|  |                                                 }} | ||||||
|  |                                             > | ||||||
|  |                                                 ${t`Copy recovery link`} | ||||||
|  |                                             </ak-action-button> | ||||||
|  |                                             ${item.email | ||||||
|  |                                                 ? html`<ak-forms-modal | ||||||
|  |                                                       .closeAfterSuccessfulSubmit=${false} | ||||||
|  |                                                   > | ||||||
|  |                                                       <span slot="submit"> ${t`Send link`} </span> | ||||||
|  |                                                       <span slot="header"> | ||||||
|  |                                                           ${t`Send recovery link to user`} | ||||||
|  |                                                       </span> | ||||||
|  |                                                       <ak-user-reset-email-form | ||||||
|  |                                                           slot="form" | ||||||
|  |                                                           .user=${item} | ||||||
|  |                                                       > | ||||||
|  |                                                       </ak-user-reset-email-form> | ||||||
|  |                                                       <button | ||||||
|  |                                                           slot="trigger" | ||||||
|  |                                                           class="pf-c-button pf-m-secondary" | ||||||
|  |                                                       > | ||||||
|  |                                                           ${t`Email recovery link`} | ||||||
|  |                                                       </button> | ||||||
|  |                                                   </ak-forms-modal>` | ||||||
|  |                                                 : html`<span | ||||||
|  |                                                       >${t`Recovery link cannot be emailed, user has no email address saved.`}</span | ||||||
|  |                                                   >`} | ||||||
|  |                                         </div> | ||||||
|  |                                     </dd> | ||||||
|  |                                 </div>`; | ||||||
|  |                             }), | ||||||
|  |                         )} | ||||||
|  |                     </dl> | ||||||
|  |                 </div> | ||||||
|  |             </td> | ||||||
|  |             <td></td> | ||||||
|  |             <td></td>`; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     renderToolbar(): TemplateResult { |     renderToolbar(): TemplateResult { | ||||||
|         return html` |         return html` | ||||||
|             <ak-forms-modal> |             <ak-forms-modal> | ||||||
|  | |||||||
							
								
								
									
										44
									
								
								web/src/pages/users/UserResetEmailForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								web/src/pages/users/UserResetEmailForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | |||||||
|  | import { CoreApi, CoreUsersRecoveryEmailRetrieveRequest, StagesApi, User } from "authentik-api"; | ||||||
|  | import { t } from "@lingui/macro"; | ||||||
|  | import { customElement, property } from "lit-element"; | ||||||
|  | import { html, TemplateResult } from "lit-html"; | ||||||
|  | import { DEFAULT_CONFIG } from "../../api/Config"; | ||||||
|  | import { Form } from "../../elements/forms/Form"; | ||||||
|  | import { until } from "lit-html/directives/until"; | ||||||
|  | import "../../elements/forms/HorizontalFormElement"; | ||||||
|  |  | ||||||
|  | @customElement("ak-user-reset-email-form") | ||||||
|  | export class UserResetEmailForm extends Form<CoreUsersRecoveryEmailRetrieveRequest> { | ||||||
|  |     @property({ attribute: false }) | ||||||
|  |     user!: User; | ||||||
|  |  | ||||||
|  |     getSuccessMessage(): string { | ||||||
|  |         return t`Successfully sent email.`; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     send = (data: CoreUsersRecoveryEmailRetrieveRequest): Promise<void> => { | ||||||
|  |         data.id = this.user.pk; | ||||||
|  |         return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryEmailRetrieve(data); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     renderForm(): TemplateResult { | ||||||
|  |         return html`<form class="pf-c-form pf-m-horizontal"> | ||||||
|  |             <ak-form-element-horizontal label=${t`Email stage`} ?required=${true} name="emailStage"> | ||||||
|  |                 <select class="pf-c-form-control"> | ||||||
|  |                     ${until( | ||||||
|  |                         new StagesApi(DEFAULT_CONFIG) | ||||||
|  |                             .stagesEmailList({ | ||||||
|  |                                 ordering: "name", | ||||||
|  |                             }) | ||||||
|  |                             .then((stages) => { | ||||||
|  |                                 return stages.results.map((stage) => { | ||||||
|  |                                     return html`<option value=${stage.pk}>${stage.name}</option>`; | ||||||
|  |                                 }); | ||||||
|  |                             }), | ||||||
|  |                         html`<option>${t`Loading...`}</option>`, | ||||||
|  |                     )} | ||||||
|  |                 </select> | ||||||
|  |             </ak-form-element-horizontal> | ||||||
|  |         </form>`; | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer