core: explicitly enable locales (#3889)
* activate locales Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * set locale for email templates Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		@ -470,7 +470,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
    # pylint: disable=invalid-name, unused-argument
 | 
					    # pylint: disable=invalid-name, unused-argument
 | 
				
			||||||
    def recovery_email(self, request: Request, pk: int) -> Response:
 | 
					    def recovery_email(self, request: Request, pk: int) -> Response:
 | 
				
			||||||
        """Create a temporary link that a user can use to recover their accounts"""
 | 
					        """Create a temporary link that a user can use to recover their accounts"""
 | 
				
			||||||
        for_user = self.get_object()
 | 
					        for_user: User = self.get_object()
 | 
				
			||||||
        if for_user.email == "":
 | 
					        if for_user.email == "":
 | 
				
			||||||
            LOGGER.debug("User doesn't have an email address")
 | 
					            LOGGER.debug("User doesn't have an email address")
 | 
				
			||||||
            return Response(status=404)
 | 
					            return Response(status=404)
 | 
				
			||||||
@ -488,8 +488,9 @@ class UserViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
        email_stage: EmailStage = stages.first()
 | 
					        email_stage: EmailStage = stages.first()
 | 
				
			||||||
        message = TemplateEmailMessage(
 | 
					        message = TemplateEmailMessage(
 | 
				
			||||||
            subject=_(email_stage.subject),
 | 
					            subject=_(email_stage.subject),
 | 
				
			||||||
            template_name=email_stage.template,
 | 
					 | 
				
			||||||
            to=[for_user.email],
 | 
					            to=[for_user.email],
 | 
				
			||||||
 | 
					            template_name=email_stage.template,
 | 
				
			||||||
 | 
					            language=for_user.locale(request),
 | 
				
			||||||
            template_context={
 | 
					            template_context={
 | 
				
			||||||
                "url": link,
 | 
					                "url": link,
 | 
				
			||||||
                "user": for_user,
 | 
					                "user": for_user,
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ from typing import Callable, Optional
 | 
				
			|||||||
from uuid import uuid4
 | 
					from uuid import uuid4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.http import HttpRequest, HttpResponse
 | 
					from django.http import HttpRequest, HttpResponse
 | 
				
			||||||
 | 
					from django.utils.translation import activate
 | 
				
			||||||
from sentry_sdk.api import set_tag
 | 
					from sentry_sdk.api import set_tag
 | 
				
			||||||
from structlog.contextvars import STRUCTLOG_KEY_PREFIX
 | 
					from structlog.contextvars import STRUCTLOG_KEY_PREFIX
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -29,6 +30,10 @@ class ImpersonateMiddleware:
 | 
				
			|||||||
    def __call__(self, request: HttpRequest) -> HttpResponse:
 | 
					    def __call__(self, request: HttpRequest) -> HttpResponse:
 | 
				
			||||||
        # No permission checks are done here, they need to be checked before
 | 
					        # No permission checks are done here, they need to be checked before
 | 
				
			||||||
        # SESSION_KEY_IMPERSONATE_USER is set.
 | 
					        # 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:
 | 
					        if SESSION_KEY_IMPERSONATE_USER in request.session:
 | 
				
			||||||
            request.user = request.session[SESSION_KEY_IMPERSONATE_USER]
 | 
					            request.user = request.session[SESSION_KEY_IMPERSONATE_USER]
 | 
				
			||||||
 | 
				
			|||||||
@ -220,6 +220,17 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
 | 
				
			|||||||
        """Generate a globally unique UID, based on the user ID and the hashed secret key"""
 | 
					        """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()
 | 
					        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
 | 
					    @property
 | 
				
			||||||
    def avatar(self) -> str:
 | 
					    def avatar(self) -> str:
 | 
				
			||||||
        """Get avatar, depending on authentik.avatar setting"""
 | 
					        """Get avatar, depending on authentik.avatar setting"""
 | 
				
			||||||
 | 
				
			|||||||
@ -445,8 +445,9 @@ class NotificationTransport(SerializerModel):
 | 
				
			|||||||
            subject += notification.body[:75]
 | 
					            subject += notification.body[:75]
 | 
				
			||||||
        mail = TemplateEmailMessage(
 | 
					        mail = TemplateEmailMessage(
 | 
				
			||||||
            subject=subject,
 | 
					            subject=subject,
 | 
				
			||||||
            template_name="email/generic.html",
 | 
					 | 
				
			||||||
            to=[notification.user.email],
 | 
					            to=[notification.user.email],
 | 
				
			||||||
 | 
					            language=notification.user.locale(),
 | 
				
			||||||
 | 
					            template_name="email/generic.html",
 | 
				
			||||||
            template_context={
 | 
					            template_context={
 | 
				
			||||||
                "title": subject,
 | 
					                "title": subject,
 | 
				
			||||||
                "body": notification.body,
 | 
					                "body": notification.body,
 | 
				
			||||||
 | 
				
			|||||||
@ -28,8 +28,8 @@ class Command(BaseCommand):
 | 
				
			|||||||
            delete_stage = True
 | 
					            delete_stage = True
 | 
				
			||||||
        message = TemplateEmailMessage(
 | 
					        message = TemplateEmailMessage(
 | 
				
			||||||
            subject="authentik Test-Email",
 | 
					            subject="authentik Test-Email",
 | 
				
			||||||
            template_name="email/setup.html",
 | 
					 | 
				
			||||||
            to=[options["to"]],
 | 
					            to=[options["to"]],
 | 
				
			||||||
 | 
					            template_name="email/setup.html",
 | 
				
			||||||
            template_context={},
 | 
					            template_context={},
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
 | 
				
			|||||||
@ -11,6 +11,7 @@ from django.utils.translation import gettext as _
 | 
				
			|||||||
from rest_framework.fields import CharField
 | 
					from rest_framework.fields import CharField
 | 
				
			||||||
from rest_framework.serializers import ValidationError
 | 
					from rest_framework.serializers import ValidationError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.core.models import User
 | 
				
			||||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
 | 
					from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
 | 
				
			||||||
from authentik.flows.models import FlowToken
 | 
					from authentik.flows.models import FlowToken
 | 
				
			||||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
 | 
					from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
 | 
				
			||||||
@ -81,7 +82,7 @@ class EmailStageView(ChallengeStageView):
 | 
				
			|||||||
    def send_email(self):
 | 
					    def send_email(self):
 | 
				
			||||||
        """Helper function that sends the actual email. Implies that you've
 | 
					        """Helper function that sends the actual email. Implies that you've
 | 
				
			||||||
        already checked that there is a pending user."""
 | 
					        already checked that there is a pending user."""
 | 
				
			||||||
        pending_user = self.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)
 | 
					        email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, None)
 | 
				
			||||||
        if not email:
 | 
					        if not email:
 | 
				
			||||||
            email = pending_user.email
 | 
					            email = pending_user.email
 | 
				
			||||||
@ -90,8 +91,9 @@ class EmailStageView(ChallengeStageView):
 | 
				
			|||||||
        # Send mail to user
 | 
					        # Send mail to user
 | 
				
			||||||
        message = TemplateEmailMessage(
 | 
					        message = TemplateEmailMessage(
 | 
				
			||||||
            subject=_(current_stage.subject),
 | 
					            subject=_(current_stage.subject),
 | 
				
			||||||
            template_name=current_stage.template,
 | 
					 | 
				
			||||||
            to=[email],
 | 
					            to=[email],
 | 
				
			||||||
 | 
					            language=pending_user.locale(self.request),
 | 
				
			||||||
 | 
					            template_name=current_stage.template,
 | 
				
			||||||
            template_context={
 | 
					            template_context={
 | 
				
			||||||
                "url": self.get_full_url(**{QS_KEY_TOKEN: token.key}),
 | 
					                "url": self.get_full_url(**{QS_KEY_TOKEN: token.key}),
 | 
				
			||||||
                "user": pending_user,
 | 
					                "user": pending_user,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,13 +1,15 @@
 | 
				
			|||||||
"""email utils"""
 | 
					"""email utils"""
 | 
				
			||||||
from django.core.mail import EmailMultiAlternatives
 | 
					from django.core.mail import EmailMultiAlternatives
 | 
				
			||||||
from django.template.loader import render_to_string
 | 
					from django.template.loader import render_to_string
 | 
				
			||||||
 | 
					from django.utils import translation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TemplateEmailMessage(EmailMultiAlternatives):
 | 
					class TemplateEmailMessage(EmailMultiAlternatives):
 | 
				
			||||||
    """Wrapper around EmailMultiAlternatives with integrated template rendering"""
 | 
					    """Wrapper around EmailMultiAlternatives with integrated template rendering"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, template_name=None, template_context=None, **kwargs):
 | 
					    def __init__(self, template_name=None, template_context=None, language="", **kwargs):
 | 
				
			||||||
        html_content = render_to_string(template_name, template_context)
 | 
					        with translation.override(language):
 | 
				
			||||||
 | 
					            html_content = render_to_string(template_name, template_context)
 | 
				
			||||||
        super().__init__(**kwargs)
 | 
					        super().__init__(**kwargs)
 | 
				
			||||||
        self.content_subtype = "html"
 | 
					        self.content_subtype = "html"
 | 
				
			||||||
        self.attach_alternative(html_content, "text/html")
 | 
					        self.attach_alternative(html_content, "text/html")
 | 
				
			||||||
 | 
				
			|||||||
@ -3,6 +3,7 @@ from typing import Callable
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from django.http.request import HttpRequest
 | 
					from django.http.request import HttpRequest
 | 
				
			||||||
from django.http.response import HttpResponse
 | 
					from django.http.response import HttpResponse
 | 
				
			||||||
 | 
					from django.utils.translation import activate
 | 
				
			||||||
from sentry_sdk.api import set_tag
 | 
					from sentry_sdk.api import set_tag
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.tenants.utils import get_tenant_for_request
 | 
					from authentik.tenants.utils import get_tenant_for_request
 | 
				
			||||||
@ -22,4 +23,7 @@ class TenantMiddleware:
 | 
				
			|||||||
            setattr(request, "tenant", tenant)
 | 
					            setattr(request, "tenant", tenant)
 | 
				
			||||||
            set_tag("authentik.tenant_uuid", tenant.tenant_uuid.hex)
 | 
					            set_tag("authentik.tenant_uuid", tenant.tenant_uuid.hex)
 | 
				
			||||||
            set_tag("authentik.tenant_domain", tenant.domain)
 | 
					            set_tag("authentik.tenant_domain", tenant.domain)
 | 
				
			||||||
 | 
					            locale = tenant.default_locale
 | 
				
			||||||
 | 
					            if locale != "":
 | 
				
			||||||
 | 
					                activate(locale)
 | 
				
			||||||
        return self.get_response(request)
 | 
					        return self.get_response(request)
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user