Compare commits
	
		
			1 Commits
		
	
	
		
			version/20
			...
			admin/add-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| bb4602745e | 
@ -1,11 +1,12 @@
 | 
				
			|||||||
"""User API Views"""
 | 
					"""User API Views"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from datetime import timedelta
 | 
					from datetime import datetime, timedelta
 | 
				
			||||||
 | 
					from hashlib import sha256
 | 
				
			||||||
from json import loads
 | 
					from json import loads
 | 
				
			||||||
from typing import Any
 | 
					from typing import Any
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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 AnonymousUser, Permission
 | 
				
			||||||
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
 | 
				
			||||||
from django.db.models.functions import ExtractHour
 | 
					from django.db.models.functions import ExtractHour
 | 
				
			||||||
@ -84,6 +85,7 @@ from authentik.flows.models import FlowToken
 | 
				
			|||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
 | 
					from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
 | 
				
			||||||
from authentik.flows.views.executor import QS_KEY_TOKEN
 | 
					from authentik.flows.views.executor import QS_KEY_TOKEN
 | 
				
			||||||
from authentik.lib.avatars import get_avatar
 | 
					from authentik.lib.avatars import get_avatar
 | 
				
			||||||
 | 
					from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
 | 
				
			||||||
from authentik.rbac.decorators import permission_required
 | 
					from authentik.rbac.decorators import permission_required
 | 
				
			||||||
from authentik.rbac.models import get_permission_choices
 | 
					from authentik.rbac.models import get_permission_choices
 | 
				
			||||||
from authentik.stages.email.models import EmailStage
 | 
					from authentik.stages.email.models import EmailStage
 | 
				
			||||||
@ -446,15 +448,19 @@ class UserViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
    def list(self, request, *args, **kwargs):
 | 
					    def list(self, request, *args, **kwargs):
 | 
				
			||||||
        return super().list(request, *args, **kwargs)
 | 
					        return super().list(request, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _create_recovery_link(self) -> tuple[str, Token]:
 | 
					    def _create_recovery_link(self, expires: datetime) -> tuple[str, Token]:
 | 
				
			||||||
        """Create a recovery link (when the current brand has a recovery flow set),
 | 
					        """Create a recovery link (when the current brand has a recovery flow set),
 | 
				
			||||||
        that can either be shown to an admin or sent to the user directly"""
 | 
					        that can either be shown to an admin or sent to the user directly"""
 | 
				
			||||||
        brand: Brand = self.request._request.brand
 | 
					        brand: Brand = self.request._request.brand
 | 
				
			||||||
        # Check that there is a recovery flow, if not return an error
 | 
					        # Check that there is a recovery flow, if not return an error
 | 
				
			||||||
        flow = brand.flow_recovery
 | 
					        flow = brand.flow_recovery
 | 
				
			||||||
        if not flow:
 | 
					        if not flow:
 | 
				
			||||||
            raise ValidationError({"non_field_errors": "No recovery flow set."})
 | 
					            raise ValidationError(
 | 
				
			||||||
 | 
					                {"non_field_errors": [_("Recovery flow is not set for this brand.")]}
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        # Mimic an unauthenticated user navigating the recovery flow
 | 
				
			||||||
        user: User = self.get_object()
 | 
					        user: User = self.get_object()
 | 
				
			||||||
 | 
					        self.request._request.user = AnonymousUser()
 | 
				
			||||||
        planner = FlowPlanner(flow)
 | 
					        planner = FlowPlanner(flow)
 | 
				
			||||||
        planner.allow_empty_flows = True
 | 
					        planner.allow_empty_flows = True
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
@ -466,16 +472,16 @@ class UserViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
        except FlowNonApplicableException:
 | 
					        except FlowNonApplicableException:
 | 
				
			||||||
            raise ValidationError(
 | 
					            raise ValidationError(
 | 
				
			||||||
                {"non_field_errors": "Recovery flow not applicable to user"}
 | 
					                {"non_field_errors": [_("Recovery flow is not applicable to this user.")]}
 | 
				
			||||||
            ) from None
 | 
					            ) from None
 | 
				
			||||||
        token, __ = FlowToken.objects.update_or_create(
 | 
					        token = FlowToken.objects.create(
 | 
				
			||||||
            identifier=f"{user.uid}-password-reset",
 | 
					            identifier=f"{user.uid}-password-reset-{sha256(str(datetime.now()).encode('UTF-8')).hexdigest()[:8]}",
 | 
				
			||||||
            defaults={
 | 
					            user=user,
 | 
				
			||||||
                "user": user,
 | 
					            flow=flow,
 | 
				
			||||||
                "flow": flow,
 | 
					            _plan=FlowToken.pickle(plan),
 | 
				
			||||||
                "_plan": FlowToken.pickle(plan),
 | 
					            expires=expires,
 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        querystring = urlencode({QS_KEY_TOKEN: token.key})
 | 
					        querystring = urlencode({QS_KEY_TOKEN: token.key})
 | 
				
			||||||
        link = self.request.build_absolute_uri(
 | 
					        link = self.request.build_absolute_uri(
 | 
				
			||||||
            reverse_lazy("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
 | 
					            reverse_lazy("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
 | 
				
			||||||
@ -610,47 +616,53 @@ class UserViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @permission_required("authentik_core.reset_user_password")
 | 
					    @permission_required("authentik_core.reset_user_password")
 | 
				
			||||||
    @extend_schema(
 | 
					    @extend_schema(
 | 
				
			||||||
 | 
					        parameters=[
 | 
				
			||||||
 | 
					            OpenApiParameter(
 | 
				
			||||||
 | 
					                name="email_stage",
 | 
				
			||||||
 | 
					                location=OpenApiParameter.QUERY,
 | 
				
			||||||
 | 
					                type=OpenApiTypes.STR,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            OpenApiParameter(
 | 
				
			||||||
 | 
					                name="token_duration",
 | 
				
			||||||
 | 
					                location=OpenApiParameter.QUERY,
 | 
				
			||||||
 | 
					                type=OpenApiTypes.STR,
 | 
				
			||||||
 | 
					                required=True,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
        responses={
 | 
					        responses={
 | 
				
			||||||
            "200": LinkSerializer(many=False),
 | 
					            "200": LinkSerializer(many=False),
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        request=None,
 | 
					        request=None,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    @action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
 | 
					    @action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
 | 
				
			||||||
    def recovery(self, request: Request, pk: int) -> Response:
 | 
					    def recovery_link(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"""
 | 
				
			||||||
        link, _ = self._create_recovery_link()
 | 
					        token_duration = request.query_params.get("token_duration", "")
 | 
				
			||||||
        return Response({"link": link})
 | 
					        timedelta_string_validator(token_duration)
 | 
				
			||||||
 | 
					        expires = now() + timedelta_from_string(token_duration)
 | 
				
			||||||
 | 
					        link, token = self._create_recovery_link(expires)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @permission_required("authentik_core.reset_user_password")
 | 
					        if email_stage := request.query_params.get("email_stage"):
 | 
				
			||||||
    @extend_schema(
 | 
					 | 
				
			||||||
        parameters=[
 | 
					 | 
				
			||||||
            OpenApiParameter(
 | 
					 | 
				
			||||||
                name="email_stage",
 | 
					 | 
				
			||||||
                location=OpenApiParameter.QUERY,
 | 
					 | 
				
			||||||
                type=OpenApiTypes.STR,
 | 
					 | 
				
			||||||
                required=True,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
        responses={
 | 
					 | 
				
			||||||
            "204": OpenApiResponse(description="Successfully sent recover email"),
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        request=None,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    @action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
 | 
					 | 
				
			||||||
    def recovery_email(self, request: Request, pk: int) -> Response:
 | 
					 | 
				
			||||||
        """Create a temporary link that a user can use to recover their accounts"""
 | 
					 | 
				
			||||||
            for_user: User = self.get_object()
 | 
					            for_user: User = self.get_object()
 | 
				
			||||||
            if for_user.email == "":
 | 
					            if for_user.email == "":
 | 
				
			||||||
                LOGGER.debug("User doesn't have an email address")
 | 
					                LOGGER.debug("User doesn't have an email address")
 | 
				
			||||||
            raise ValidationError({"non_field_errors": "User does not have an email address set."})
 | 
					                raise ValidationError(
 | 
				
			||||||
        link, token = self._create_recovery_link()
 | 
					                    {"non_field_errors": [_("User does not have an email address set.")]}
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Lookup the email stage to assure the current user can access it
 | 
					            # Lookup the email stage to assure the current user can access it
 | 
				
			||||||
            stages = get_objects_for_user(
 | 
					            stages = get_objects_for_user(
 | 
				
			||||||
                request.user, "authentik_stages_email.view_emailstage"
 | 
					                request.user, "authentik_stages_email.view_emailstage"
 | 
				
			||||||
        ).filter(pk=request.query_params.get("email_stage"))
 | 
					            ).filter(pk=email_stage)
 | 
				
			||||||
            if not stages.exists():
 | 
					            if not stages.exists():
 | 
				
			||||||
            LOGGER.debug("Email stage does not exist/user has no permissions")
 | 
					                if stages := EmailStage.objects.filter(pk=email_stage).exists():
 | 
				
			||||||
            raise ValidationError({"non_field_errors": "Email stage does not exist."})
 | 
					                    raise ValidationError(
 | 
				
			||||||
 | 
					                        {"non_field_errors": [_("User has no permissions to this Email stage.")]}
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    raise ValidationError(
 | 
				
			||||||
 | 
					                        {"non_field_errors": [_("The given Email stage does not exist.")]}
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
            email_stage: EmailStage = stages.first()
 | 
					            email_stage: EmailStage = stages.first()
 | 
				
			||||||
            message = TemplateEmailMessage(
 | 
					            message = TemplateEmailMessage(
 | 
				
			||||||
                subject=_(email_stage.subject),
 | 
					                subject=_(email_stage.subject),
 | 
				
			||||||
@ -664,7 +676,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            send_mails(email_stage, message)
 | 
					            send_mails(email_stage, message)
 | 
				
			||||||
        return Response(status=204)
 | 
					
 | 
				
			||||||
 | 
					        return Response({"link": link})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @permission_required("authentik_core.impersonate")
 | 
					    @permission_required("authentik_core.impersonate")
 | 
				
			||||||
    @extend_schema(
 | 
					    @extend_schema(
 | 
				
			||||||
 | 
				
			|||||||
@ -36,6 +36,15 @@ class FlowAuthenticationRequirement(models.TextChoices):
 | 
				
			|||||||
    REQUIRE_REDIRECT = "require_redirect"
 | 
					    REQUIRE_REDIRECT = "require_redirect"
 | 
				
			||||||
    REQUIRE_OUTPOST = "require_outpost"
 | 
					    REQUIRE_OUTPOST = "require_outpost"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def possibly_unauthenticated(self) -> bool:
 | 
				
			||||||
 | 
					        """Check if unauthenticated users can run this flow. Flows like this may require additional
 | 
				
			||||||
 | 
					        hardening."""
 | 
				
			||||||
 | 
					        return self in [
 | 
				
			||||||
 | 
					            FlowAuthenticationRequirement.NONE,
 | 
				
			||||||
 | 
					            FlowAuthenticationRequirement.REQUIRE_UNAUTHENTICATED,
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NotConfiguredAction(models.TextChoices):
 | 
					class NotConfiguredAction(models.TextChoices):
 | 
				
			||||||
    """Decides how the FlowExecutor should proceed when a stage isn't configured"""
 | 
					    """Decides how the FlowExecutor should proceed when a stage isn't configured"""
 | 
				
			||||||
 | 
				
			|||||||
@ -31,7 +31,7 @@ def timedelta_string_validator(value: str):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def timedelta_from_string(expr: str) -> datetime.timedelta:
 | 
					def timedelta_from_string(expr: str) -> datetime.timedelta:
 | 
				
			||||||
    """Convert a string with the format of 'hours=1;minute=3;seconds=5' to a
 | 
					    """Convert a string with the format of 'hours=1;minutes=3;seconds=5' to a
 | 
				
			||||||
    `datetime.timedelta` Object with hours = 1, minutes = 3, seconds = 5"""
 | 
					    `datetime.timedelta` Object with hours = 1, minutes = 3, seconds = 5"""
 | 
				
			||||||
    kwargs = {}
 | 
					    kwargs = {}
 | 
				
			||||||
    for duration_pair in expr.split(";"):
 | 
					    for duration_pair in expr.split(";"):
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,7 @@ from rest_framework.serializers import ValidationError
 | 
				
			|||||||
from authentik.events.models import Event, EventAction
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
from authentik.flows.challenge import Challenge, ChallengeResponse
 | 
					from authentik.flows.challenge import Challenge, ChallengeResponse
 | 
				
			||||||
from authentik.flows.exceptions import StageInvalidException
 | 
					from authentik.flows.exceptions import StageInvalidException
 | 
				
			||||||
from authentik.flows.models import FlowDesignation, FlowToken
 | 
					from authentik.flows.models import FlowAuthenticationRequirement, FlowToken
 | 
				
			||||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
 | 
					from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
 | 
				
			||||||
from authentik.flows.stage import ChallengeStageView
 | 
					from authentik.flows.stage import ChallengeStageView
 | 
				
			||||||
from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY
 | 
					from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY
 | 
				
			||||||
@ -97,14 +97,27 @@ class EmailStageView(ChallengeStageView):
 | 
				
			|||||||
        """Helper function that sends the actual email. Implies that you've
 | 
					        """Helper function that sends the actual email. Implies that you've
 | 
				
			||||||
        already checked that there is a pending user."""
 | 
					        already checked that there is a pending user."""
 | 
				
			||||||
        pending_user = self.get_pending_user()
 | 
					        pending_user = self.get_pending_user()
 | 
				
			||||||
        if not pending_user.pk and self.executor.flow.designation == FlowDesignation.RECOVERY:
 | 
					        email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, pending_user.email)
 | 
				
			||||||
            # Pending user does not have a primary key, and we're in a recovery flow,
 | 
					        if FlowAuthenticationRequirement(
 | 
				
			||||||
            # which means the user entered an invalid identifier, so we pretend to send the
 | 
					            self.executor.flow.authentication
 | 
				
			||||||
            # email, to not disclose if the user exists
 | 
					        ).possibly_unauthenticated:
 | 
				
			||||||
 | 
					            # In possibly unauthenticated flows, do not disclose whether user or their email exists
 | 
				
			||||||
 | 
					            # to prevent enumeration attacks
 | 
				
			||||||
 | 
					            if not pending_user.pk:
 | 
				
			||||||
 | 
					                self.logger.debug(
 | 
				
			||||||
 | 
					                    "User object does not exist. Email not sent.", pending_user=pending_user
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
        email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, None)
 | 
					 | 
				
			||||||
            if not email:
 | 
					            if not email:
 | 
				
			||||||
            email = pending_user.email
 | 
					                self.logger.debug(
 | 
				
			||||||
 | 
					                    "No recipient email address could be determined. Email not sent.",
 | 
				
			||||||
 | 
					                    pending_user=pending_user,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                return
 | 
				
			||||||
 | 
					        if not email:
 | 
				
			||||||
 | 
					            raise StageInvalidException(
 | 
				
			||||||
 | 
					                "No recipient email address could be determined. Email not sent."
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
        current_stage: EmailStage = self.executor.current_stage
 | 
					        current_stage: EmailStage = self.executor.current_stage
 | 
				
			||||||
        token = self.get_token()
 | 
					        token = self.get_token()
 | 
				
			||||||
        # Send mail to user
 | 
					        # Send mail to user
 | 
				
			||||||
@ -133,7 +146,9 @@ class EmailStageView(ChallengeStageView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 | 
					    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 | 
				
			||||||
        # Check if the user came back from the email link to verify
 | 
					        # Check if the user came back from the email link to verify
 | 
				
			||||||
        restore_token: FlowToken = self.executor.plan.context.get(PLAN_CONTEXT_IS_RESTORED, None)
 | 
					        restore_token: FlowToken | None = self.executor.plan.context.get(
 | 
				
			||||||
 | 
					            PLAN_CONTEXT_IS_RESTORED, None
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        user = self.get_pending_user()
 | 
					        user = self.get_pending_user()
 | 
				
			||||||
        if restore_token:
 | 
					        if restore_token:
 | 
				
			||||||
            if restore_token.user != user:
 | 
					            if restore_token.user != user:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										48
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								schema.yml
									
									
									
									
									
								
							@ -6095,17 +6095,26 @@ paths:
 | 
				
			|||||||
              schema:
 | 
					              schema:
 | 
				
			||||||
                $ref: '#/components/schemas/GenericError'
 | 
					                $ref: '#/components/schemas/GenericError'
 | 
				
			||||||
          description: ''
 | 
					          description: ''
 | 
				
			||||||
  /core/users/{id}/recovery/:
 | 
					  /core/users/{id}/recovery_link/:
 | 
				
			||||||
    post:
 | 
					    post:
 | 
				
			||||||
      operationId: core_users_recovery_create
 | 
					      operationId: core_users_recovery_link_create
 | 
				
			||||||
      description: Create a temporary link that a user can use to recover their accounts
 | 
					      description: Create a temporary link that a user can use to recover their accounts
 | 
				
			||||||
      parameters:
 | 
					      parameters:
 | 
				
			||||||
 | 
					      - in: query
 | 
				
			||||||
 | 
					        name: email_stage
 | 
				
			||||||
 | 
					        schema:
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
      - in: path
 | 
					      - in: path
 | 
				
			||||||
        name: id
 | 
					        name: id
 | 
				
			||||||
        schema:
 | 
					        schema:
 | 
				
			||||||
          type: integer
 | 
					          type: integer
 | 
				
			||||||
        description: A unique integer value identifying this User.
 | 
					        description: A unique integer value identifying this User.
 | 
				
			||||||
        required: true
 | 
					        required: true
 | 
				
			||||||
 | 
					      - in: query
 | 
				
			||||||
 | 
					        name: token_duration
 | 
				
			||||||
 | 
					        schema:
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					        required: true
 | 
				
			||||||
      tags:
 | 
					      tags:
 | 
				
			||||||
      - core
 | 
					      - core
 | 
				
			||||||
      security:
 | 
					      security:
 | 
				
			||||||
@ -6129,41 +6138,6 @@ paths:
 | 
				
			|||||||
              schema:
 | 
					              schema:
 | 
				
			||||||
                $ref: '#/components/schemas/GenericError'
 | 
					                $ref: '#/components/schemas/GenericError'
 | 
				
			||||||
          description: ''
 | 
					          description: ''
 | 
				
			||||||
  /core/users/{id}/recovery_email/:
 | 
					 | 
				
			||||||
    post:
 | 
					 | 
				
			||||||
      operationId: core_users_recovery_email_create
 | 
					 | 
				
			||||||
      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: []
 | 
					 | 
				
			||||||
      responses:
 | 
					 | 
				
			||||||
        '204':
 | 
					 | 
				
			||||||
          description: Successfully sent recover email
 | 
					 | 
				
			||||||
        '400':
 | 
					 | 
				
			||||||
          content:
 | 
					 | 
				
			||||||
            application/json:
 | 
					 | 
				
			||||||
              schema:
 | 
					 | 
				
			||||||
                $ref: '#/components/schemas/ValidationError'
 | 
					 | 
				
			||||||
          description: ''
 | 
					 | 
				
			||||||
        '403':
 | 
					 | 
				
			||||||
          content:
 | 
					 | 
				
			||||||
            application/json:
 | 
					 | 
				
			||||||
              schema:
 | 
					 | 
				
			||||||
                $ref: '#/components/schemas/GenericError'
 | 
					 | 
				
			||||||
          description: ''
 | 
					 | 
				
			||||||
  /core/users/{id}/set_password/:
 | 
					  /core/users/{id}/set_password/:
 | 
				
			||||||
    post:
 | 
					    post:
 | 
				
			||||||
      operationId: core_users_set_password_create
 | 
					      operationId: core_users_set_password_create
 | 
				
			||||||
 | 
				
			|||||||
@ -2,11 +2,14 @@ import "@goauthentik/admin/users/ServiceAccountForm";
 | 
				
			|||||||
import "@goauthentik/admin/users/UserActiveForm";
 | 
					import "@goauthentik/admin/users/UserActiveForm";
 | 
				
			||||||
import "@goauthentik/admin/users/UserForm";
 | 
					import "@goauthentik/admin/users/UserForm";
 | 
				
			||||||
import "@goauthentik/admin/users/UserImpersonateForm";
 | 
					import "@goauthentik/admin/users/UserImpersonateForm";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    renderRecoveryEmailRequest,
 | 
				
			||||||
 | 
					    renderRecoveryLinkRequest,
 | 
				
			||||||
 | 
					} from "@goauthentik/admin/users/UserListPage";
 | 
				
			||||||
import "@goauthentik/admin/users/UserPasswordForm";
 | 
					import "@goauthentik/admin/users/UserPasswordForm";
 | 
				
			||||||
import "@goauthentik/admin/users/UserResetEmailForm";
 | 
					import "@goauthentik/admin/users/UserRecoveryLinkForm";
 | 
				
			||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
					import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
				
			||||||
import { PFSize } from "@goauthentik/common/enums.js";
 | 
					import { PFSize } from "@goauthentik/common/enums.js";
 | 
				
			||||||
import { MessageLevel } from "@goauthentik/common/messages";
 | 
					 | 
				
			||||||
import { me } from "@goauthentik/common/users";
 | 
					import { me } from "@goauthentik/common/users";
 | 
				
			||||||
import { getRelativeTime } from "@goauthentik/common/utils";
 | 
					import { getRelativeTime } from "@goauthentik/common/utils";
 | 
				
			||||||
import "@goauthentik/components/ak-status-label";
 | 
					import "@goauthentik/components/ak-status-label";
 | 
				
			||||||
@ -21,7 +24,6 @@ import "@goauthentik/elements/forms/DeleteBulkForm";
 | 
				
			|||||||
import { Form } from "@goauthentik/elements/forms/Form";
 | 
					import { Form } from "@goauthentik/elements/forms/Form";
 | 
				
			||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
					import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
				
			||||||
import "@goauthentik/elements/forms/ModalForm";
 | 
					import "@goauthentik/elements/forms/ModalForm";
 | 
				
			||||||
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
 | 
					 | 
				
			||||||
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
 | 
					import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
 | 
				
			||||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
 | 
					import { PaginatedResponse } from "@goauthentik/elements/table/Table";
 | 
				
			||||||
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
 | 
					import { Table, TableColumn } from "@goauthentik/elements/table/Table";
 | 
				
			||||||
@ -37,14 +39,7 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
 | 
				
			|||||||
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
 | 
					import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
 | 
				
			||||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
 | 
					import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import { CoreApi, CoreUsersListTypeEnum, Group, SessionUser, User } from "@goauthentik/api";
 | 
				
			||||||
    CoreApi,
 | 
					 | 
				
			||||||
    CoreUsersListTypeEnum,
 | 
					 | 
				
			||||||
    Group,
 | 
					 | 
				
			||||||
    ResponseError,
 | 
					 | 
				
			||||||
    SessionUser,
 | 
					 | 
				
			||||||
    User,
 | 
					 | 
				
			||||||
} from "@goauthentik/api";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ak-user-related-add")
 | 
					@customElement("ak-user-related-add")
 | 
				
			||||||
export class RelatedUserAdd extends Form<{ users: number[] }> {
 | 
					export class RelatedUserAdd extends Form<{ users: number[] }> {
 | 
				
			||||||
@ -301,60 +296,11 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
 | 
				
			|||||||
                                            ${msg("Set password")}
 | 
					                                            ${msg("Set password")}
 | 
				
			||||||
                                        </button>
 | 
					                                        </button>
 | 
				
			||||||
                                    </ak-forms-modal>
 | 
					                                    </ak-forms-modal>
 | 
				
			||||||
                                    ${this.brand?.flowRecovery
 | 
					                                    ${this.brand.flowRecovery
 | 
				
			||||||
                                        ? html`
 | 
					                                        ? html`
 | 
				
			||||||
                                              <ak-action-button
 | 
					                                              ${renderRecoveryLinkRequest(item)}
 | 
				
			||||||
                                                  class="pf-m-secondary"
 | 
					 | 
				
			||||||
                                                  .apiRequest=${() => {
 | 
					 | 
				
			||||||
                                                      return new CoreApi(DEFAULT_CONFIG)
 | 
					 | 
				
			||||||
                                                          .coreUsersRecoveryCreate({
 | 
					 | 
				
			||||||
                                                              id: item.pk,
 | 
					 | 
				
			||||||
                                                          })
 | 
					 | 
				
			||||||
                                                          .then((rec) => {
 | 
					 | 
				
			||||||
                                                              showMessage({
 | 
					 | 
				
			||||||
                                                                  level: MessageLevel.success,
 | 
					 | 
				
			||||||
                                                                  message: msg(
 | 
					 | 
				
			||||||
                                                                      "Successfully generated recovery link",
 | 
					 | 
				
			||||||
                                                                  ),
 | 
					 | 
				
			||||||
                                                                  description: rec.link,
 | 
					 | 
				
			||||||
                                                              });
 | 
					 | 
				
			||||||
                                                          })
 | 
					 | 
				
			||||||
                                                          .catch((ex: ResponseError) => {
 | 
					 | 
				
			||||||
                                                              ex.response.json().then(() => {
 | 
					 | 
				
			||||||
                                                                  showMessage({
 | 
					 | 
				
			||||||
                                                                      level: MessageLevel.error,
 | 
					 | 
				
			||||||
                                                                      message: msg(
 | 
					 | 
				
			||||||
                                                                          "No recovery flow is configured.",
 | 
					 | 
				
			||||||
                                                                      ),
 | 
					 | 
				
			||||||
                                                                  });
 | 
					 | 
				
			||||||
                                                              });
 | 
					 | 
				
			||||||
                                                          });
 | 
					 | 
				
			||||||
                                                  }}
 | 
					 | 
				
			||||||
                                              >
 | 
					 | 
				
			||||||
                                                  ${msg("Copy recovery link")}
 | 
					 | 
				
			||||||
                                              </ak-action-button>
 | 
					 | 
				
			||||||
                                              ${item.email
 | 
					                                              ${item.email
 | 
				
			||||||
                                                  ? html`<ak-forms-modal
 | 
					                                                  ? renderRecoveryEmailRequest(item)
 | 
				
			||||||
                                                        .closeAfterSuccessfulSubmit=${false}
 | 
					 | 
				
			||||||
                                                    >
 | 
					 | 
				
			||||||
                                                        <span slot="submit">
 | 
					 | 
				
			||||||
                                                            ${msg("Send link")}
 | 
					 | 
				
			||||||
                                                        </span>
 | 
					 | 
				
			||||||
                                                        <span slot="header">
 | 
					 | 
				
			||||||
                                                            ${msg("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"
 | 
					 | 
				
			||||||
                                                        >
 | 
					 | 
				
			||||||
                                                            ${msg("Email recovery link")}
 | 
					 | 
				
			||||||
                                                        </button>
 | 
					 | 
				
			||||||
                                                    </ak-forms-modal>`
 | 
					 | 
				
			||||||
                                                  : html`<span
 | 
					                                                  : html`<span
 | 
				
			||||||
                                                        >${msg(
 | 
					                                                        >${msg(
 | 
				
			||||||
                                                            "Recovery link cannot be emailed, user has no email address saved.",
 | 
					                                                            "Recovery link cannot be emailed, user has no email address saved.",
 | 
				
			||||||
@ -363,7 +309,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
 | 
				
			|||||||
                                          `
 | 
					                                          `
 | 
				
			||||||
                                        : html` <p>
 | 
					                                        : html` <p>
 | 
				
			||||||
                                              ${msg(
 | 
					                                              ${msg(
 | 
				
			||||||
                                                  "To let a user directly reset a their password, configure a recovery flow on the currently active brand.",
 | 
					                                                  "To let a user directly reset their password, configure a recovery flow on the currently active brand.",
 | 
				
			||||||
                                              )}
 | 
					                                              )}
 | 
				
			||||||
                                          </p>`}
 | 
					                                          </p>`}
 | 
				
			||||||
                                </div>
 | 
					                                </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -4,11 +4,10 @@ import "@goauthentik/admin/users/UserActiveForm";
 | 
				
			|||||||
import "@goauthentik/admin/users/UserForm";
 | 
					import "@goauthentik/admin/users/UserForm";
 | 
				
			||||||
import "@goauthentik/admin/users/UserImpersonateForm";
 | 
					import "@goauthentik/admin/users/UserImpersonateForm";
 | 
				
			||||||
import "@goauthentik/admin/users/UserPasswordForm";
 | 
					import "@goauthentik/admin/users/UserPasswordForm";
 | 
				
			||||||
import "@goauthentik/admin/users/UserResetEmailForm";
 | 
					import "@goauthentik/admin/users/UserRecoveryLinkForm";
 | 
				
			||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
					import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
				
			||||||
import { PFSize } from "@goauthentik/common/enums.js";
 | 
					import { PFSize } from "@goauthentik/common/enums.js";
 | 
				
			||||||
import { userTypeToLabel } from "@goauthentik/common/labels";
 | 
					import { userTypeToLabel } from "@goauthentik/common/labels";
 | 
				
			||||||
import { MessageLevel } from "@goauthentik/common/messages";
 | 
					 | 
				
			||||||
import { DefaultUIConfig, uiConfig } from "@goauthentik/common/ui/config";
 | 
					import { DefaultUIConfig, uiConfig } from "@goauthentik/common/ui/config";
 | 
				
			||||||
import { me } from "@goauthentik/common/users";
 | 
					import { me } from "@goauthentik/common/users";
 | 
				
			||||||
import { getRelativeTime } from "@goauthentik/common/utils";
 | 
					import { getRelativeTime } from "@goauthentik/common/utils";
 | 
				
			||||||
@ -23,12 +22,10 @@ import "@goauthentik/elements/TreeView";
 | 
				
			|||||||
import "@goauthentik/elements/buttons/ActionButton";
 | 
					import "@goauthentik/elements/buttons/ActionButton";
 | 
				
			||||||
import "@goauthentik/elements/forms/DeleteBulkForm";
 | 
					import "@goauthentik/elements/forms/DeleteBulkForm";
 | 
				
			||||||
import "@goauthentik/elements/forms/ModalForm";
 | 
					import "@goauthentik/elements/forms/ModalForm";
 | 
				
			||||||
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
 | 
					 | 
				
			||||||
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
 | 
					import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
 | 
				
			||||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
 | 
					import { PaginatedResponse } from "@goauthentik/elements/table/Table";
 | 
				
			||||||
import { TableColumn } from "@goauthentik/elements/table/Table";
 | 
					import { TableColumn } from "@goauthentik/elements/table/Table";
 | 
				
			||||||
import { TablePage } from "@goauthentik/elements/table/TablePage";
 | 
					import { TablePage } from "@goauthentik/elements/table/TablePage";
 | 
				
			||||||
import { writeToClipboard } from "@goauthentik/elements/utils/writeToClipboard";
 | 
					 | 
				
			||||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
 | 
					import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { msg, str } from "@lit/localize";
 | 
					import { msg, str } from "@lit/localize";
 | 
				
			||||||
@ -39,40 +36,24 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
 | 
				
			|||||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
 | 
					import PFCard from "@patternfly/patternfly/components/Card/card.css";
 | 
				
			||||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
 | 
					import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { CoreApi, ResponseError, SessionUser, User, UserPath } from "@goauthentik/api";
 | 
					import { CoreApi, SessionUser, User, UserPath } from "@goauthentik/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const requestRecoveryLink = (user: User) =>
 | 
					export const renderRecoveryLinkRequest = (user: User) =>
 | 
				
			||||||
    new CoreApi(DEFAULT_CONFIG)
 | 
					    html`<ak-forms-modal .closeAfterSuccessfulSubmit=${false} id="ak-link-recovery-request">
 | 
				
			||||||
        .coreUsersRecoveryCreate({
 | 
					        <span slot="submit"> ${msg("Create link")} </span>
 | 
				
			||||||
            id: user.pk,
 | 
					        <span slot="header"> ${msg("Create recovery link")} </span>
 | 
				
			||||||
        })
 | 
					        <ak-user-recovery-link-form slot="form" .user=${user}> </ak-user-recovery-link-form>
 | 
				
			||||||
        .then((rec) =>
 | 
					        <button slot="trigger" class="pf-c-button pf-m-secondary">
 | 
				
			||||||
            writeToClipboard(rec.link).then((wroteToClipboard) =>
 | 
					            ${msg("Create recovery link")}
 | 
				
			||||||
                showMessage({
 | 
					        </button>
 | 
				
			||||||
                    level: MessageLevel.success,
 | 
					    </ak-forms-modal>`;
 | 
				
			||||||
                    message: rec.link,
 | 
					 | 
				
			||||||
                    description: wroteToClipboard
 | 
					 | 
				
			||||||
                        ? msg("A copy of this recovery link has been placed in your clipboard")
 | 
					 | 
				
			||||||
                        : "",
 | 
					 | 
				
			||||||
                }),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .catch((ex: ResponseError) =>
 | 
					 | 
				
			||||||
            ex.response.json().then(() =>
 | 
					 | 
				
			||||||
                showMessage({
 | 
					 | 
				
			||||||
                    level: MessageLevel.error,
 | 
					 | 
				
			||||||
                    message: msg(
 | 
					 | 
				
			||||||
                        "The current brand must have a recovery flow configured to use a recovery link",
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                }),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const renderRecoveryEmailRequest = (user: User) =>
 | 
					export const renderRecoveryEmailRequest = (user: User) =>
 | 
				
			||||||
    html`<ak-forms-modal .closeAfterSuccessfulSubmit=${false} id="ak-email-recovery-request">
 | 
					    html`<ak-forms-modal .closeAfterSuccessfulSubmit=${false} id="ak-email-recovery-request">
 | 
				
			||||||
        <span slot="submit"> ${msg("Send link")} </span>
 | 
					        <span slot="submit"> ${msg("Send link")} </span>
 | 
				
			||||||
        <span slot="header"> ${msg("Send recovery link to user")} </span>
 | 
					        <span slot="header"> ${msg("Send recovery link to user")} </span>
 | 
				
			||||||
        <ak-user-reset-email-form slot="form" .user=${user}> </ak-user-reset-email-form>
 | 
					        <ak-user-recovery-link-form slot="form" .user=${user} .withEmailStage=${true}>
 | 
				
			||||||
 | 
					        </ak-user-recovery-link-form>
 | 
				
			||||||
        <button slot="trigger" class="pf-c-button pf-m-secondary">
 | 
					        <button slot="trigger" class="pf-c-button pf-m-secondary">
 | 
				
			||||||
            ${msg("Email recovery link")}
 | 
					            ${msg("Email recovery link")}
 | 
				
			||||||
        </button>
 | 
					        </button>
 | 
				
			||||||
@ -362,12 +343,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
 | 
				
			|||||||
                                    </ak-forms-modal>
 | 
					                                    </ak-forms-modal>
 | 
				
			||||||
                                    ${this.brand.flowRecovery
 | 
					                                    ${this.brand.flowRecovery
 | 
				
			||||||
                                        ? html`
 | 
					                                        ? html`
 | 
				
			||||||
                                              <ak-action-button
 | 
					                                              ${renderRecoveryLinkRequest(item)}
 | 
				
			||||||
                                                  class="pf-m-secondary"
 | 
					 | 
				
			||||||
                                                  .apiRequest=${() => requestRecoveryLink(item)}
 | 
					 | 
				
			||||||
                                              >
 | 
					 | 
				
			||||||
                                                  ${msg("Create recovery link")}
 | 
					 | 
				
			||||||
                                              </ak-action-button>
 | 
					 | 
				
			||||||
                                              ${item.email
 | 
					                                              ${item.email
 | 
				
			||||||
                                                  ? renderRecoveryEmailRequest(item)
 | 
					                                                  ? renderRecoveryEmailRequest(item)
 | 
				
			||||||
                                                  : html`<span
 | 
					                                                  : html`<span
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										104
									
								
								web/src/admin/users/UserRecoveryLinkForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								web/src/admin/users/UserRecoveryLinkForm.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,104 @@
 | 
				
			|||||||
 | 
					import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
				
			||||||
 | 
					import { groupBy } from "@goauthentik/common/utils";
 | 
				
			||||||
 | 
					import "@goauthentik/components/ak-text-input";
 | 
				
			||||||
 | 
					import { Form } from "@goauthentik/elements/forms/Form";
 | 
				
			||||||
 | 
					import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
				
			||||||
 | 
					import "@goauthentik/elements/forms/SearchSelect";
 | 
				
			||||||
 | 
					import { writeToClipboard } from "@goauthentik/elements/utils/writeToClipboard";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { msg } from "@lit/localize";
 | 
				
			||||||
 | 
					import { TemplateResult, html } from "lit";
 | 
				
			||||||
 | 
					import { customElement, property } from "lit/decorators.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    CoreApi,
 | 
				
			||||||
 | 
					    CoreUsersRecoveryLinkCreateRequest,
 | 
				
			||||||
 | 
					    Link,
 | 
				
			||||||
 | 
					    Stage,
 | 
				
			||||||
 | 
					    StagesAllListRequest,
 | 
				
			||||||
 | 
					    StagesApi,
 | 
				
			||||||
 | 
					    User,
 | 
				
			||||||
 | 
					} from "@goauthentik/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@customElement("ak-user-recovery-link-form")
 | 
				
			||||||
 | 
					export class UserRecoveryLinkForm extends Form<CoreUsersRecoveryLinkCreateRequest> {
 | 
				
			||||||
 | 
					    @property({ attribute: false })
 | 
				
			||||||
 | 
					    user!: User;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property({ type: Boolean })
 | 
				
			||||||
 | 
					    withEmailStage = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async send(data: CoreUsersRecoveryLinkCreateRequest): Promise<Link> {
 | 
				
			||||||
 | 
					        data.id = this.user.pk;
 | 
				
			||||||
 | 
					        const response = await new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryLinkCreate(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.withEmailStage) {
 | 
				
			||||||
 | 
					            this.successMessage = msg("Successfully sent email.");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            const wroteToClipboard = await writeToClipboard(response.link);
 | 
				
			||||||
 | 
					            if (wroteToClipboard) {
 | 
				
			||||||
 | 
					                this.successMessage = msg(
 | 
				
			||||||
 | 
					                    `A copy of this recovery link has been placed in your clipboard: ${response.link}`,
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                this.successMessage = msg(
 | 
				
			||||||
 | 
					                    `authentik does not have access to your clipboard, please copy the recovery link manually: ${response.link}`,
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return response;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    renderEmailStageInput(): TemplateResult {
 | 
				
			||||||
 | 
					        if (!this.withEmailStage) return html``;
 | 
				
			||||||
 | 
					        return html`
 | 
				
			||||||
 | 
					            <ak-form-element-horizontal name="emailStage" label=${msg("Email stage")} required>
 | 
				
			||||||
 | 
					                <ak-search-select
 | 
				
			||||||
 | 
					                    .fetchObjects=${async (query?: string): Promise<Stage[]> => {
 | 
				
			||||||
 | 
					                        const args: StagesAllListRequest = {
 | 
				
			||||||
 | 
					                            ordering: "name",
 | 
				
			||||||
 | 
					                        };
 | 
				
			||||||
 | 
					                        if (query !== undefined) {
 | 
				
			||||||
 | 
					                            args.search = query;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args);
 | 
				
			||||||
 | 
					                        return stages.results;
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                    .groupBy=${(items: Stage[]) => {
 | 
				
			||||||
 | 
					                        return groupBy(items, (stage) => stage.verboseNamePlural);
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                    .renderElement=${(stage: Stage): string => {
 | 
				
			||||||
 | 
					                        return stage.name;
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                    .value=${(stage: Stage | undefined): string | undefined => {
 | 
				
			||||||
 | 
					                        return stage?.pk;
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                </ak-search-select>
 | 
				
			||||||
 | 
					            </ak-form-element-horizontal>
 | 
				
			||||||
 | 
					        `;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    renderForm(): TemplateResult {
 | 
				
			||||||
 | 
					        return html`
 | 
				
			||||||
 | 
					            ${this.renderEmailStageInput()}
 | 
				
			||||||
 | 
					            <ak-text-input
 | 
				
			||||||
 | 
					                name="tokenDuration"
 | 
				
			||||||
 | 
					                label=${msg("Token duration")}
 | 
				
			||||||
 | 
					                required
 | 
				
			||||||
 | 
					                value="days=1"
 | 
				
			||||||
 | 
					                .bighelp=${html`<p class="pf-c-form__helper-text">
 | 
				
			||||||
 | 
					                    ${msg("Duration for generated token")}
 | 
				
			||||||
 | 
					                </p>`}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					            </ak-text-input>
 | 
				
			||||||
 | 
					        `;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare global {
 | 
				
			||||||
 | 
					    interface HTMLElementTagNameMap {
 | 
				
			||||||
 | 
					        "ak-user-recovery-link-form": UserRecoveryLinkForm;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,70 +0,0 @@
 | 
				
			|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
					 | 
				
			||||||
import { groupBy } from "@goauthentik/common/utils";
 | 
					 | 
				
			||||||
import { Form } from "@goauthentik/elements/forms/Form";
 | 
					 | 
				
			||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
					 | 
				
			||||||
import "@goauthentik/elements/forms/SearchSelect";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { msg } from "@lit/localize";
 | 
					 | 
				
			||||||
import { TemplateResult, html } from "lit";
 | 
					 | 
				
			||||||
import { customElement, property } from "lit/decorators.js";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
    CoreApi,
 | 
					 | 
				
			||||||
    CoreUsersRecoveryEmailCreateRequest,
 | 
					 | 
				
			||||||
    Stage,
 | 
					 | 
				
			||||||
    StagesAllListRequest,
 | 
					 | 
				
			||||||
    StagesApi,
 | 
					 | 
				
			||||||
    User,
 | 
					 | 
				
			||||||
} from "@goauthentik/api";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@customElement("ak-user-reset-email-form")
 | 
					 | 
				
			||||||
export class UserResetEmailForm extends Form<CoreUsersRecoveryEmailCreateRequest> {
 | 
					 | 
				
			||||||
    @property({ attribute: false })
 | 
					 | 
				
			||||||
    user!: User;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    getSuccessMessage(): string {
 | 
					 | 
				
			||||||
        return msg("Successfully sent email.");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async send(data: CoreUsersRecoveryEmailCreateRequest): Promise<void> {
 | 
					 | 
				
			||||||
        data.id = this.user.pk;
 | 
					 | 
				
			||||||
        return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryEmailCreate(data);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    renderForm(): TemplateResult {
 | 
					 | 
				
			||||||
        return html`<ak-form-element-horizontal
 | 
					 | 
				
			||||||
            label=${msg("Email stage")}
 | 
					 | 
				
			||||||
            ?required=${true}
 | 
					 | 
				
			||||||
            name="emailStage"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
            <ak-search-select
 | 
					 | 
				
			||||||
                .fetchObjects=${async (query?: string): Promise<Stage[]> => {
 | 
					 | 
				
			||||||
                    const args: StagesAllListRequest = {
 | 
					 | 
				
			||||||
                        ordering: "name",
 | 
					 | 
				
			||||||
                    };
 | 
					 | 
				
			||||||
                    if (query !== undefined) {
 | 
					 | 
				
			||||||
                        args.search = query;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args);
 | 
					 | 
				
			||||||
                    return stages.results;
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                .groupBy=${(items: Stage[]) => {
 | 
					 | 
				
			||||||
                    return groupBy(items, (stage) => stage.verboseNamePlural);
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                .renderElement=${(stage: Stage): string => {
 | 
					 | 
				
			||||||
                    return stage.name;
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                .value=${(stage: Stage | undefined): string | undefined => {
 | 
					 | 
				
			||||||
                    return stage?.pk;
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
            </ak-search-select>
 | 
					 | 
				
			||||||
        </ak-form-element-horizontal>`;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
declare global {
 | 
					 | 
				
			||||||
    interface HTMLElementTagNameMap {
 | 
					 | 
				
			||||||
        "ak-user-reset-email-form": UserResetEmailForm;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -8,7 +8,7 @@ import "@goauthentik/admin/users/UserForm";
 | 
				
			|||||||
import "@goauthentik/admin/users/UserImpersonateForm";
 | 
					import "@goauthentik/admin/users/UserImpersonateForm";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    renderRecoveryEmailRequest,
 | 
					    renderRecoveryEmailRequest,
 | 
				
			||||||
    requestRecoveryLink,
 | 
					    renderRecoveryLinkRequest,
 | 
				
			||||||
} from "@goauthentik/admin/users/UserListPage";
 | 
					} from "@goauthentik/admin/users/UserListPage";
 | 
				
			||||||
import "@goauthentik/admin/users/UserPasswordForm";
 | 
					import "@goauthentik/admin/users/UserPasswordForm";
 | 
				
			||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
					import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
				
			||||||
@ -110,11 +110,8 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
 | 
				
			|||||||
                .ak-button-collection > * {
 | 
					                .ak-button-collection > * {
 | 
				
			||||||
                    flex: 1 0 100%;
 | 
					                    flex: 1 0 100%;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                #reset-password-button {
 | 
					 | 
				
			||||||
                    margin-right: 0;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                #ak-email-recovery-request,
 | 
					                #ak-link-recovery-request .pf-c-button,
 | 
				
			||||||
                #update-password-request .pf-c-button,
 | 
					                #update-password-request .pf-c-button,
 | 
				
			||||||
                #ak-email-recovery-request .pf-c-button {
 | 
					                #ak-email-recovery-request .pf-c-button {
 | 
				
			||||||
                    margin: 0;
 | 
					                    margin: 0;
 | 
				
			||||||
@ -248,18 +245,7 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
 | 
				
			|||||||
                    </pf-tooltip>
 | 
					                    </pf-tooltip>
 | 
				
			||||||
                </button>
 | 
					                </button>
 | 
				
			||||||
            </ak-forms-modal>
 | 
					            </ak-forms-modal>
 | 
				
			||||||
            <ak-action-button
 | 
					            ${renderRecoveryLinkRequest(user)}
 | 
				
			||||||
                id="reset-password-button"
 | 
					 | 
				
			||||||
                class="pf-m-secondary pf-m-block"
 | 
					 | 
				
			||||||
                .apiRequest=${() => requestRecoveryLink(user)}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
                <pf-tooltip
 | 
					 | 
				
			||||||
                    position="top"
 | 
					 | 
				
			||||||
                    content=${msg("Create a link for this user to reset their password")}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                    ${msg("Create Recovery Link")}
 | 
					 | 
				
			||||||
                </pf-tooltip>
 | 
					 | 
				
			||||||
            </ak-action-button>
 | 
					 | 
				
			||||||
            ${user.email ? renderRecoveryEmailRequest(user) : nothing}
 | 
					            ${user.email ? renderRecoveryEmailRequest(user) : nothing}
 | 
				
			||||||
        </div> `;
 | 
					        </div> `;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@ sidebar_label: Slack
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
The following placeholder will be used:
 | 
					The following placeholder will be used:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- You can use <kbd>slack.<em>company</em>></kbd> or <kbd><em>my-workspace</em>.slack.com</kbd> as the FQDN of your Slack instance.
 | 
					- You can use <kbd>slack.<em>company</em></kbd> or <kbd><em>my-workspace</em>.slack.com</kbd> as the FQDN of your Slack instance.
 | 
				
			||||||
- You can use <kbd>authentik.company</kbd> as the FQDN of the authentik installation.
 | 
					- You can use <kbd>authentik.company</kbd> as the FQDN of the authentik installation.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
:::note
 | 
					:::note
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user