diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index 697a4c19e..475c9666c 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -470,7 +470,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): # pylint: disable=invalid-name, unused-argument def recovery_email(self, request: Request, pk: int) -> Response: """Create a temporary link that a user can use to recover their accounts""" - for_user = self.get_object() + for_user: User = self.get_object() if for_user.email == "": LOGGER.debug("User doesn't have an email address") return Response(status=404) @@ -488,8 +488,9 @@ class UserViewSet(UsedByMixin, ModelViewSet): email_stage: EmailStage = stages.first() message = TemplateEmailMessage( subject=_(email_stage.subject), - template_name=email_stage.template, to=[for_user.email], + template_name=email_stage.template, + language=for_user.locale(request), template_context={ "url": link, "user": for_user, diff --git a/authentik/core/middleware.py b/authentik/core/middleware.py index ccdf09325..2cda0b0c3 100644 --- a/authentik/core/middleware.py +++ b/authentik/core/middleware.py @@ -4,6 +4,7 @@ from typing import Callable, Optional from uuid import uuid4 from django.http import HttpRequest, HttpResponse +from django.utils.translation import activate from sentry_sdk.api import set_tag from structlog.contextvars import STRUCTLOG_KEY_PREFIX @@ -29,6 +30,10 @@ class ImpersonateMiddleware: def __call__(self, request: HttpRequest) -> HttpResponse: # No permission checks are done here, they need to be checked before # SESSION_KEY_IMPERSONATE_USER is set. + if request.user.is_authenticated: + locale = request.user.locale(request) + if locale != "": + activate(locale) if SESSION_KEY_IMPERSONATE_USER in request.session: request.user = request.session[SESSION_KEY_IMPERSONATE_USER] diff --git a/authentik/core/models.py b/authentik/core/models.py index 4e747abc6..0df7e998f 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -220,6 +220,17 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser): """Generate a globally unique UID, based on the user ID and the hashed secret key""" return sha256(f"{self.id}-{settings.SECRET_KEY}".encode("ascii")).hexdigest() + def locale(self, request: Optional[HttpRequest] = None) -> str: + """Get the locale the user has configured""" + try: + return self.attributes.get("settings", {}).get("locale", "") + # pylint: disable=broad-except + except Exception as exc: + LOGGER.warning("Failed to get default locale", exc=exc) + if request: + return request.tenant.locale + return "" + @property def avatar(self) -> str: """Get avatar, depending on authentik.avatar setting""" diff --git a/authentik/events/models.py b/authentik/events/models.py index f497afaed..54d4a132c 100644 --- a/authentik/events/models.py +++ b/authentik/events/models.py @@ -445,8 +445,9 @@ class NotificationTransport(SerializerModel): subject += notification.body[:75] mail = TemplateEmailMessage( subject=subject, - template_name="email/generic.html", to=[notification.user.email], + language=notification.user.locale(), + template_name="email/generic.html", template_context={ "title": subject, "body": notification.body, diff --git a/authentik/stages/email/management/commands/test_email.py b/authentik/stages/email/management/commands/test_email.py index c168e7278..d067708a9 100644 --- a/authentik/stages/email/management/commands/test_email.py +++ b/authentik/stages/email/management/commands/test_email.py @@ -28,8 +28,8 @@ class Command(BaseCommand): delete_stage = True message = TemplateEmailMessage( subject="authentik Test-Email", - template_name="email/setup.html", to=[options["to"]], + template_name="email/setup.html", template_context={}, ) try: diff --git a/authentik/stages/email/stage.py b/authentik/stages/email/stage.py index 8d35916b4..f931dedc9 100644 --- a/authentik/stages/email/stage.py +++ b/authentik/stages/email/stage.py @@ -11,6 +11,7 @@ from django.utils.translation import gettext as _ from rest_framework.fields import CharField from rest_framework.serializers import ValidationError +from authentik.core.models import User from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes from authentik.flows.models import FlowToken from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER @@ -81,7 +82,7 @@ class EmailStageView(ChallengeStageView): def send_email(self): """Helper function that sends the actual email. Implies that you've already checked that there is a pending user.""" - pending_user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] + pending_user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, None) if not email: email = pending_user.email @@ -90,8 +91,9 @@ class EmailStageView(ChallengeStageView): # Send mail to user message = TemplateEmailMessage( subject=_(current_stage.subject), - template_name=current_stage.template, to=[email], + language=pending_user.locale(self.request), + template_name=current_stage.template, template_context={ "url": self.get_full_url(**{QS_KEY_TOKEN: token.key}), "user": pending_user, diff --git a/authentik/stages/email/utils.py b/authentik/stages/email/utils.py index 139b6af82..13650ec4b 100644 --- a/authentik/stages/email/utils.py +++ b/authentik/stages/email/utils.py @@ -1,13 +1,15 @@ """email utils""" from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string +from django.utils import translation class TemplateEmailMessage(EmailMultiAlternatives): """Wrapper around EmailMultiAlternatives with integrated template rendering""" - def __init__(self, template_name=None, template_context=None, **kwargs): - html_content = render_to_string(template_name, template_context) + def __init__(self, template_name=None, template_context=None, language="", **kwargs): + with translation.override(language): + html_content = render_to_string(template_name, template_context) super().__init__(**kwargs) self.content_subtype = "html" self.attach_alternative(html_content, "text/html") diff --git a/authentik/tenants/middleware.py b/authentik/tenants/middleware.py index 01a54a87e..300a8f512 100644 --- a/authentik/tenants/middleware.py +++ b/authentik/tenants/middleware.py @@ -3,6 +3,7 @@ from typing import Callable from django.http.request import HttpRequest from django.http.response import HttpResponse +from django.utils.translation import activate from sentry_sdk.api import set_tag from authentik.tenants.utils import get_tenant_for_request @@ -22,4 +23,7 @@ class TenantMiddleware: setattr(request, "tenant", tenant) set_tag("authentik.tenant_uuid", tenant.tenant_uuid.hex) set_tag("authentik.tenant_domain", tenant.domain) + locale = tenant.default_locale + if locale != "": + activate(locale) return self.get_response(request)