Compare commits
	
		
			1 Commits
		
	
	
		
			policies-n
			...
			admin/add-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| bb4602745e | 
| @ -1,11 +1,12 @@ | ||||
| """User API Views""" | ||||
|  | ||||
| from datetime import timedelta | ||||
| from datetime import datetime, timedelta | ||||
| from hashlib import sha256 | ||||
| from json import loads | ||||
| from typing import Any | ||||
|  | ||||
| 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.core.cache import cache | ||||
| 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.views.executor import QS_KEY_TOKEN | ||||
| 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.models import get_permission_choices | ||||
| from authentik.stages.email.models import EmailStage | ||||
| @ -446,15 +448,19 @@ class UserViewSet(UsedByMixin, ModelViewSet): | ||||
|     def list(self, 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), | ||||
|         that can either be shown to an admin or sent to the user directly""" | ||||
|         brand: Brand = self.request._request.brand | ||||
|         # Check that there is a recovery flow, if not return an error | ||||
|         flow = brand.flow_recovery | ||||
|         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() | ||||
|         self.request._request.user = AnonymousUser() | ||||
|         planner = FlowPlanner(flow) | ||||
|         planner.allow_empty_flows = True | ||||
|         try: | ||||
| @ -466,16 +472,16 @@ class UserViewSet(UsedByMixin, ModelViewSet): | ||||
|             ) | ||||
|         except FlowNonApplicableException: | ||||
|             raise ValidationError( | ||||
|                 {"non_field_errors": "Recovery flow not applicable to user"} | ||||
|                 {"non_field_errors": [_("Recovery flow is not applicable to this user.")]} | ||||
|             ) from None | ||||
|         token, __ = FlowToken.objects.update_or_create( | ||||
|             identifier=f"{user.uid}-password-reset", | ||||
|             defaults={ | ||||
|                 "user": user, | ||||
|                 "flow": flow, | ||||
|                 "_plan": FlowToken.pickle(plan), | ||||
|             }, | ||||
|         token = FlowToken.objects.create( | ||||
|             identifier=f"{user.uid}-password-reset-{sha256(str(datetime.now()).encode('UTF-8')).hexdigest()[:8]}", | ||||
|             user=user, | ||||
|             flow=flow, | ||||
|             _plan=FlowToken.pickle(plan), | ||||
|             expires=expires, | ||||
|         ) | ||||
|  | ||||
|         querystring = urlencode({QS_KEY_TOKEN: token.key}) | ||||
|         link = self.request.build_absolute_uri( | ||||
|             reverse_lazy("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) | ||||
| @ -610,61 +616,68 @@ class UserViewSet(UsedByMixin, ModelViewSet): | ||||
|  | ||||
|     @permission_required("authentik_core.reset_user_password") | ||||
|     @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={ | ||||
|             "200": LinkSerializer(many=False), | ||||
|         }, | ||||
|         request=None, | ||||
|     ) | ||||
|     @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""" | ||||
|         link, _ = self._create_recovery_link() | ||||
|         return Response({"link": link}) | ||||
|         token_duration = request.query_params.get("token_duration", "") | ||||
|         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") | ||||
|     @extend_schema( | ||||
|         parameters=[ | ||||
|             OpenApiParameter( | ||||
|                 name="email_stage", | ||||
|                 location=OpenApiParameter.QUERY, | ||||
|                 type=OpenApiTypes.STR, | ||||
|                 required=True, | ||||
|         if email_stage := request.query_params.get("email_stage"): | ||||
|             for_user: User = self.get_object() | ||||
|             if for_user.email == "": | ||||
|                 LOGGER.debug("User doesn't have an email address") | ||||
|                 raise ValidationError( | ||||
|                     {"non_field_errors": [_("User does not have an email address set.")]} | ||||
|                 ) | ||||
|  | ||||
|             # 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=email_stage) | ||||
|             if not stages.exists(): | ||||
|                 if stages := EmailStage.objects.filter(pk=email_stage).exists(): | ||||
|                     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() | ||||
|             message = TemplateEmailMessage( | ||||
|                 subject=_(email_stage.subject), | ||||
|                 to=[(for_user.name, for_user.email)], | ||||
|                 template_name=email_stage.template, | ||||
|                 language=for_user.locale(request), | ||||
|                 template_context={ | ||||
|                     "url": link, | ||||
|                     "user": for_user, | ||||
|                     "expires": token.expires, | ||||
|                 }, | ||||
|             ) | ||||
|         ], | ||||
|         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() | ||||
|         if for_user.email == "": | ||||
|             LOGGER.debug("User doesn't have an email address") | ||||
|             raise ValidationError({"non_field_errors": "User does not have an email address set."}) | ||||
|         link, token = self._create_recovery_link() | ||||
|         # 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") | ||||
|             raise ValidationError({"non_field_errors": "Email stage does not exist."}) | ||||
|         email_stage: EmailStage = stages.first() | ||||
|         message = TemplateEmailMessage( | ||||
|             subject=_(email_stage.subject), | ||||
|             to=[(for_user.name, for_user.email)], | ||||
|             template_name=email_stage.template, | ||||
|             language=for_user.locale(request), | ||||
|             template_context={ | ||||
|                 "url": link, | ||||
|                 "user": for_user, | ||||
|                 "expires": token.expires, | ||||
|             }, | ||||
|         ) | ||||
|         send_mails(email_stage, message) | ||||
|         return Response(status=204) | ||||
|             send_mails(email_stage, message) | ||||
|  | ||||
|         return Response({"link": link}) | ||||
|  | ||||
|     @permission_required("authentik_core.impersonate") | ||||
|     @extend_schema( | ||||
|  | ||||
| @ -36,6 +36,15 @@ class FlowAuthenticationRequirement(models.TextChoices): | ||||
|     REQUIRE_REDIRECT = "require_redirect" | ||||
|     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): | ||||
|     """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: | ||||
|     """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""" | ||||
|     kwargs = {} | ||||
|     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.flows.challenge import Challenge, ChallengeResponse | ||||
| 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.stage import ChallengeStageView | ||||
| 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 | ||||
|         already checked that there is a pending user.""" | ||||
|         pending_user = self.get_pending_user() | ||||
|         if not pending_user.pk and self.executor.flow.designation == FlowDesignation.RECOVERY: | ||||
|             # Pending user does not have a primary key, and we're in a recovery flow, | ||||
|             # which means the user entered an invalid identifier, so we pretend to send the | ||||
|             # email, to not disclose if the user exists | ||||
|             return | ||||
|         email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, None) | ||||
|         email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, pending_user.email) | ||||
|         if FlowAuthenticationRequirement( | ||||
|             self.executor.flow.authentication | ||||
|         ).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 | ||||
|             if not email: | ||||
|                 self.logger.debug( | ||||
|                     "No recipient email address could be determined. Email not sent.", | ||||
|                     pending_user=pending_user, | ||||
|                 ) | ||||
|                 return | ||||
|         if not email: | ||||
|             email = pending_user.email | ||||
|             raise StageInvalidException( | ||||
|                 "No recipient email address could be determined. Email not sent." | ||||
|             ) | ||||
|         current_stage: EmailStage = self.executor.current_stage | ||||
|         token = self.get_token() | ||||
|         # Send mail to user | ||||
| @ -133,7 +146,9 @@ class EmailStageView(ChallengeStageView): | ||||
|  | ||||
|     def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: | ||||
|         # 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() | ||||
|         if restore_token: | ||||
|             if restore_token.user != user: | ||||
|  | ||||
							
								
								
									
										48
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								schema.yml
									
									
									
									
									
								
							| @ -6095,17 +6095,26 @@ paths: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           description: '' | ||||
|   /core/users/{id}/recovery/: | ||||
|   /core/users/{id}/recovery_link/: | ||||
|     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 | ||||
|       parameters: | ||||
|       - in: query | ||||
|         name: email_stage | ||||
|         schema: | ||||
|           type: string | ||||
|       - in: path | ||||
|         name: id | ||||
|         schema: | ||||
|           type: integer | ||||
|         description: A unique integer value identifying this User. | ||||
|         required: true | ||||
|       - in: query | ||||
|         name: token_duration | ||||
|         schema: | ||||
|           type: string | ||||
|         required: true | ||||
|       tags: | ||||
|       - core | ||||
|       security: | ||||
| @ -6129,41 +6138,6 @@ paths: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           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/: | ||||
|     post: | ||||
|       operationId: core_users_set_password_create | ||||
|  | ||||
| @ -2,11 +2,14 @@ import "@goauthentik/admin/users/ServiceAccountForm"; | ||||
| import "@goauthentik/admin/users/UserActiveForm"; | ||||
| import "@goauthentik/admin/users/UserForm"; | ||||
| import "@goauthentik/admin/users/UserImpersonateForm"; | ||||
| import { | ||||
|     renderRecoveryEmailRequest, | ||||
|     renderRecoveryLinkRequest, | ||||
| } from "@goauthentik/admin/users/UserListPage"; | ||||
| import "@goauthentik/admin/users/UserPasswordForm"; | ||||
| import "@goauthentik/admin/users/UserResetEmailForm"; | ||||
| import "@goauthentik/admin/users/UserRecoveryLinkForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { PFSize } from "@goauthentik/common/enums.js"; | ||||
| import { MessageLevel } from "@goauthentik/common/messages"; | ||||
| import { me } from "@goauthentik/common/users"; | ||||
| import { getRelativeTime } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/components/ak-status-label"; | ||||
| @ -21,7 +24,6 @@ import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| import { Form } from "@goauthentik/elements/forms/Form"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import { showMessage } from "@goauthentik/elements/messages/MessageContainer"; | ||||
| import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch"; | ||||
| import { PaginatedResponse } 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 PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; | ||||
|  | ||||
| import { | ||||
|     CoreApi, | ||||
|     CoreUsersListTypeEnum, | ||||
|     Group, | ||||
|     ResponseError, | ||||
|     SessionUser, | ||||
|     User, | ||||
| } from "@goauthentik/api"; | ||||
| import { CoreApi, CoreUsersListTypeEnum, Group, SessionUser, User } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-user-related-add") | ||||
| export class RelatedUserAdd extends Form<{ users: number[] }> { | ||||
| @ -301,60 +296,11 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl | ||||
|                                             ${msg("Set password")} | ||||
|                                         </button> | ||||
|                                     </ak-forms-modal> | ||||
|                                     ${this.brand?.flowRecovery | ||||
|                                     ${this.brand.flowRecovery | ||||
|                                         ? html` | ||||
|                                               <ak-action-button | ||||
|                                                   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> | ||||
|                                               ${renderRecoveryLinkRequest(item)} | ||||
|                                               ${item.email | ||||
|                                                   ? html`<ak-forms-modal | ||||
|                                                         .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>` | ||||
|                                                   ? renderRecoveryEmailRequest(item) | ||||
|                                                   : html`<span | ||||
|                                                         >${msg( | ||||
|                                                             "Recovery link cannot be emailed, user has no email address saved.", | ||||
| @ -363,7 +309,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl | ||||
|                                           ` | ||||
|                                         : html` <p> | ||||
|                                               ${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>`} | ||||
|                                 </div> | ||||
|  | ||||
| @ -4,11 +4,10 @@ import "@goauthentik/admin/users/UserActiveForm"; | ||||
| import "@goauthentik/admin/users/UserForm"; | ||||
| import "@goauthentik/admin/users/UserImpersonateForm"; | ||||
| import "@goauthentik/admin/users/UserPasswordForm"; | ||||
| import "@goauthentik/admin/users/UserResetEmailForm"; | ||||
| import "@goauthentik/admin/users/UserRecoveryLinkForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { PFSize } from "@goauthentik/common/enums.js"; | ||||
| import { userTypeToLabel } from "@goauthentik/common/labels"; | ||||
| import { MessageLevel } from "@goauthentik/common/messages"; | ||||
| import { DefaultUIConfig, uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import { me } from "@goauthentik/common/users"; | ||||
| import { getRelativeTime } from "@goauthentik/common/utils"; | ||||
| @ -23,12 +22,10 @@ import "@goauthentik/elements/TreeView"; | ||||
| import "@goauthentik/elements/buttons/ActionButton"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| import "@goauthentik/elements/forms/ModalForm"; | ||||
| import { showMessage } from "@goauthentik/elements/messages/MessageContainer"; | ||||
| import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { TableColumn } from "@goauthentik/elements/table/Table"; | ||||
| import { TablePage } from "@goauthentik/elements/table/TablePage"; | ||||
| import { writeToClipboard } from "@goauthentik/elements/utils/writeToClipboard"; | ||||
| import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; | ||||
|  | ||||
| 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 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) => | ||||
|     new CoreApi(DEFAULT_CONFIG) | ||||
|         .coreUsersRecoveryCreate({ | ||||
|             id: user.pk, | ||||
|         }) | ||||
|         .then((rec) => | ||||
|             writeToClipboard(rec.link).then((wroteToClipboard) => | ||||
|                 showMessage({ | ||||
|                     level: MessageLevel.success, | ||||
|                     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 renderRecoveryLinkRequest = (user: User) => | ||||
|     html`<ak-forms-modal .closeAfterSuccessfulSubmit=${false} id="ak-link-recovery-request"> | ||||
|         <span slot="submit"> ${msg("Create link")} </span> | ||||
|         <span slot="header"> ${msg("Create recovery link")} </span> | ||||
|         <ak-user-recovery-link-form slot="form" .user=${user}> </ak-user-recovery-link-form> | ||||
|         <button slot="trigger" class="pf-c-button pf-m-secondary"> | ||||
|             ${msg("Create recovery link")} | ||||
|         </button> | ||||
|     </ak-forms-modal>`; | ||||
|  | ||||
| export const renderRecoveryEmailRequest = (user: User) => | ||||
|     html`<ak-forms-modal .closeAfterSuccessfulSubmit=${false} id="ak-email-recovery-request"> | ||||
|         <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=${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"> | ||||
|             ${msg("Email recovery link")} | ||||
|         </button> | ||||
| @ -362,12 +343,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa | ||||
|                                     </ak-forms-modal> | ||||
|                                     ${this.brand.flowRecovery | ||||
|                                         ? html` | ||||
|                                               <ak-action-button | ||||
|                                                   class="pf-m-secondary" | ||||
|                                                   .apiRequest=${() => requestRecoveryLink(item)} | ||||
|                                               > | ||||
|                                                   ${msg("Create recovery link")} | ||||
|                                               </ak-action-button> | ||||
|                                               ${renderRecoveryLinkRequest(item)} | ||||
|                                               ${item.email | ||||
|                                                   ? renderRecoveryEmailRequest(item) | ||||
|                                                   : 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 { | ||||
|     renderRecoveryEmailRequest, | ||||
|     requestRecoveryLink, | ||||
|     renderRecoveryLinkRequest, | ||||
| } from "@goauthentik/admin/users/UserListPage"; | ||||
| import "@goauthentik/admin/users/UserPasswordForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| @ -110,11 +110,8 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) { | ||||
|                 .ak-button-collection > * { | ||||
|                     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, | ||||
|                 #ak-email-recovery-request .pf-c-button { | ||||
|                     margin: 0; | ||||
| @ -248,18 +245,7 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) { | ||||
|                     </pf-tooltip> | ||||
|                 </button> | ||||
|             </ak-forms-modal> | ||||
|             <ak-action-button | ||||
|                 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> | ||||
|             ${renderRecoveryLinkRequest(user)} | ||||
|             ${user.email ? renderRecoveryEmailRequest(user) : nothing} | ||||
|         </div> `; | ||||
|     } | ||||
|  | ||||
| @ -15,7 +15,7 @@ sidebar_label: Slack | ||||
|  | ||||
| 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. | ||||
|  | ||||
| :::note | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	