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:
parent
8a50279142
commit
30d708dd1f
|
@ -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