From bb4602745e86d81ea74be12ed1530502b4c1114c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Simonyi=20Gerg=C5=91?=
Date: Tue, 15 Oct 2024 11:35:18 +0200
Subject: [PATCH] clean up recovery process by admin
---
authentik/core/api/users.py | 129 ++++++++++---------
authentik/flows/models.py | 9 ++
authentik/lib/utils/time.py | 2 +-
authentik/stages/email/stage.py | 33 +++--
schema.yml | 48 ++-----
web/src/admin/groups/RelatedUserList.ts | 74 ++---------
web/src/admin/users/UserListPage.ts | 52 ++------
web/src/admin/users/UserRecoveryLinkForm.ts | 104 +++++++++++++++
web/src/admin/users/UserResetEmailForm.ts | 70 ----------
web/src/admin/users/UserViewPage.ts | 20 +--
website/integrations/services/slack/index.md | 2 +-
11 files changed, 248 insertions(+), 295 deletions(-)
create mode 100644 web/src/admin/users/UserRecoveryLinkForm.ts
delete mode 100644 web/src/admin/users/UserResetEmailForm.ts
diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py
index a1bef44dc5..a78890d5a9 100644
--- a/authentik/core/api/users.py
+++ b/authentik/core/api/users.py
@@ -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(
diff --git a/authentik/flows/models.py b/authentik/flows/models.py
index d4d0369556..a5915eeecb 100644
--- a/authentik/flows/models.py
+++ b/authentik/flows/models.py
@@ -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"""
diff --git a/authentik/lib/utils/time.py b/authentik/lib/utils/time.py
index 4e7338cdb4..3f6d3b86bf 100644
--- a/authentik/lib/utils/time.py
+++ b/authentik/lib/utils/time.py
@@ -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(";"):
diff --git a/authentik/stages/email/stage.py b/authentik/stages/email/stage.py
index 062dae62ef..944fcf03d1 100644
--- a/authentik/stages/email/stage.py
+++ b/authentik/stages/email/stage.py
@@ -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:
diff --git a/schema.yml b/schema.yml
index 9896329197..7cf3ed877a 100644
--- a/schema.yml
+++ b/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
diff --git a/web/src/admin/groups/RelatedUserList.ts b/web/src/admin/groups/RelatedUserList.ts
index ed133ede3d..edc56aa045 100644
--- a/web/src/admin/groups/RelatedUserList.ts
+++ b/web/src/admin/groups/RelatedUserList.ts
@@ -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")}
- ${this.brand?.flowRecovery
+ ${this.brand.flowRecovery
? html`
- {
- 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")}
-
+ ${renderRecoveryLinkRequest(item)}
${item.email
- ? html`
-
- ${msg("Send link")}
-
-
- ${msg("Send recovery link to user")}
-
-
-
-
- `
+ ? renderRecoveryEmailRequest(item)
: html`${msg(
"Recovery link cannot be emailed, user has no email address saved.",
@@ -363,7 +309,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
`
: html`
${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.",
)}
`}
diff --git a/web/src/admin/users/UserListPage.ts b/web/src/admin/users/UserListPage.ts
index 64ee851ead..9da9eee8db 100644
--- a/web/src/admin/users/UserListPage.ts
+++ b/web/src/admin/users/UserListPage.ts
@@ -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`
+ ${msg("Create link")}
+ ${msg("Create recovery link")}
+
+
+ `;
export const renderRecoveryEmailRequest = (user: User) =>
html` ${msg("Send link")} ${msg("Send recovery link to user")}
-
+
+
@@ -362,12 +343,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
${this.brand.flowRecovery
? html`
- requestRecoveryLink(item)}
- >
- ${msg("Create recovery link")}
-
+ ${renderRecoveryLinkRequest(item)}
${item.email
? renderRecoveryEmailRequest(item)
: html` {
+ @property({ attribute: false })
+ user!: User;
+
+ @property({ type: Boolean })
+ withEmailStage = false;
+
+ async send(data: CoreUsersRecoveryLinkCreateRequest): Promise {
+ 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`
+
+ => {
+ 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;
+ }}
+ >
+
+
+ `;
+ }
+
+ renderForm(): TemplateResult {
+ return html`
+ ${this.renderEmailStageInput()}
+
+ ${msg("Duration for generated token")}
+
`}
+ >
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ak-user-recovery-link-form": UserRecoveryLinkForm;
+ }
+}
diff --git a/web/src/admin/users/UserResetEmailForm.ts b/web/src/admin/users/UserResetEmailForm.ts
deleted file mode 100644
index a684fdbd05..0000000000
--- a/web/src/admin/users/UserResetEmailForm.ts
+++ /dev/null
@@ -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 {
- @property({ attribute: false })
- user!: User;
-
- getSuccessMessage(): string {
- return msg("Successfully sent email.");
- }
-
- async send(data: CoreUsersRecoveryEmailCreateRequest): Promise {
- data.id = this.user.pk;
- return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryEmailCreate(data);
- }
-
- renderForm(): TemplateResult {
- return html`
- => {
- 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;
- }}
- >
-
- `;
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- "ak-user-reset-email-form": UserResetEmailForm;
- }
-}
diff --git a/web/src/admin/users/UserViewPage.ts b/web/src/admin/users/UserViewPage.ts
index 02890c0c65..380b8c318e 100644
--- a/web/src/admin/users/UserViewPage.ts
+++ b/web/src/admin/users/UserViewPage.ts
@@ -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) {
- requestRecoveryLink(user)}
- >
-
- ${msg("Create Recovery Link")}
-
-
+ ${renderRecoveryLinkRequest(user)}
${user.email ? renderRecoveryEmailRequest(user) : nothing}
`;
}
diff --git a/website/integrations/services/slack/index.md b/website/integrations/services/slack/index.md
index a974dc0bbd..a4993305ae 100644
--- a/website/integrations/services/slack/index.md
+++ b/website/integrations/services/slack/index.md
@@ -15,7 +15,7 @@ sidebar_label: Slack
The following placeholder will be used:
-- You can use slack.company> or my-workspace.slack.com as the FQDN of your Slack instance.
+- You can use slack.company or my-workspace.slack.com as the FQDN of your Slack instance.
- You can use authentik.company as the FQDN of the authentik installation.
:::note