Compare commits
1 Commits
website/in
...
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