brands: add option to set global default flow background (#13079)

* brands: add option to set global default flow background

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* test

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L.
2025-03-20 01:07:05 +00:00
committed by GitHub
parent e4a8c05d25
commit 27856ec301
12 changed files with 99 additions and 10 deletions

View File

@ -50,6 +50,7 @@ class BrandSerializer(ModelSerializer):
"branding_logo", "branding_logo",
"branding_favicon", "branding_favicon",
"branding_custom_css", "branding_custom_css",
"branding_default_flow_background",
"flow_authentication", "flow_authentication",
"flow_invalidation", "flow_invalidation",
"flow_recovery", "flow_recovery",
@ -127,6 +128,7 @@ class BrandViewSet(UsedByMixin, ModelViewSet):
"branding_title", "branding_title",
"branding_logo", "branding_logo",
"branding_favicon", "branding_favicon",
"branding_default_flow_background",
"flow_authentication", "flow_authentication",
"flow_invalidation", "flow_invalidation",
"flow_recovery", "flow_recovery",

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.13 on 2025-03-19 22:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_brands", "0008_brand_branding_custom_css"),
]
operations = [
migrations.AddField(
model_name="brand",
name="branding_default_flow_background",
field=models.TextField(default="/static/dist/assets/images/flow_background.jpg"),
),
]

View File

@ -34,6 +34,9 @@ class Brand(SerializerModel):
branding_logo = models.TextField(default="/static/dist/assets/icons/icon_left_brand.svg") branding_logo = models.TextField(default="/static/dist/assets/icons/icon_left_brand.svg")
branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png") branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png")
branding_custom_css = models.TextField(default="", blank=True) branding_custom_css = models.TextField(default="", blank=True)
branding_default_flow_background = models.TextField(
default="/static/dist/assets/images/flow_background.jpg"
)
flow_authentication = models.ForeignKey( flow_authentication = models.ForeignKey(
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_authentication" Flow, null=True, on_delete=models.SET_NULL, related_name="brand_authentication"
@ -85,6 +88,12 @@ class Brand(SerializerModel):
return CONFIG.get("web.path", "/")[:-1] + self.branding_favicon return CONFIG.get("web.path", "/")[:-1] + self.branding_favicon
return self.branding_favicon return self.branding_favicon
def branding_default_flow_background_url(self) -> str:
"""Get branding_default_flow_background with the correct prefix"""
if self.branding_default_flow_background.startswith("/static"):
return CONFIG.get("web.path", "/")[:-1] + self.branding_default_flow_background
return self.branding_default_flow_background
@property @property
def serializer(self) -> Serializer: def serializer(self) -> Serializer:
from authentik.brands.api import BrandSerializer from authentik.brands.api import BrandSerializer

View File

@ -124,3 +124,27 @@ class TestBrands(APITestCase):
"subject": None, "subject": None,
}, },
) )
def test_branding_url(self):
"""Test branding attributes return correct values"""
brand = create_test_brand()
brand.branding_default_flow_background = "https://goauthentik.io/img/icon.png"
brand.branding_favicon = "https://goauthentik.io/img/icon.png"
brand.branding_logo = "https://goauthentik.io/img/icon.png"
brand.save()
self.assertEqual(
brand.branding_default_flow_background_url(), "https://goauthentik.io/img/icon.png"
)
self.assertJSONEqual(
self.client.get(reverse("authentik_api:brand-current")).content.decode(),
{
"branding_logo": "https://goauthentik.io/img/icon.png",
"branding_favicon": "https://goauthentik.io/img/icon.png",
"branding_title": "authentik",
"branding_custom_css": "",
"matched_domain": brand.domain,
"ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC,
"default_locale": "",
},
)

View File

@ -4,7 +4,7 @@
{% load i18n %} {% load i18n %}
{% block head_before %} {% block head_before %}
<link rel="prefetch" href="{% static 'dist/assets/images/flow_background.jpg' %}" /> <link rel="prefetch" href="{{ request.brand.branding_default_flow_background_url }}" />
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)"> <link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
{% include "base/header_js.html" %} {% include "base/header_js.html" %}
@ -13,7 +13,7 @@
{% block head %} {% block head %}
<style> <style>
:root { :root {
--ak-flow-background: url("{% static 'dist/assets/images/flow_background.jpg' %}"); --ak-flow-background: url("{{ request.brand.branding_default_flow_background_url }}");
--pf-c-background-image--BackgroundImage: var(--ak-flow-background); --pf-c-background-image--BackgroundImage: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background); --pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background); --pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background);

View File

@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
from uuid import uuid4 from uuid import uuid4
from django.db import models from django.db import models
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from rest_framework.serializers import BaseSerializer from rest_framework.serializers import BaseSerializer
@ -178,14 +179,11 @@ class Flow(SerializerModel, PolicyBindingModel):
help_text=_("Required level of authentication and authorization to access a flow."), help_text=_("Required level of authentication and authorization to access a flow."),
) )
@property def background_url(self, request: HttpRequest) -> str:
def background_url(self) -> str:
"""Get the URL to the background image. If the name is /static or starts with http """Get the URL to the background image. If the name is /static or starts with http
it is returned as-is""" it is returned as-is"""
if not self.background: if not self.background:
return ( return request.brand.branding_default_flow_background_url()
CONFIG.get("web.path", "/")[:-1] + "/static/dist/assets/images/flow_background.jpg"
)
if self.background.name.startswith("http"): if self.background.name.startswith("http"):
return self.background.name return self.background.name
if self.background.name.startswith("/static"): if self.background.name.startswith("/static"):

View File

@ -184,7 +184,7 @@ class ChallengeStageView(StageView):
flow_info = ContextualFlowInfo( flow_info = ContextualFlowInfo(
data={ data={
"title": self.format_title(), "title": self.format_title(),
"background": self.executor.flow.background_url, "background": self.executor.flow.background_url(self.request),
"cancel_url": reverse("authentik_flows:cancel"), "cancel_url": reverse("authentik_flows:cancel"),
"layout": self.executor.flow.layout, "layout": self.executor.flow.layout,
} }

View File

@ -27,7 +27,6 @@ class FlowTestCase(APITestCase):
self.assertIsNotNone(raw_response["component"]) self.assertIsNotNone(raw_response["component"])
if flow: if flow:
self.assertIn("flow_info", raw_response) self.assertIn("flow_info", raw_response)
self.assertEqual(raw_response["flow_info"]["background"], flow.background_url)
self.assertEqual( self.assertEqual(
raw_response["flow_info"]["cancel_url"], reverse("authentik_flows:cancel") raw_response["flow_info"]["cancel_url"], reverse("authentik_flows:cancel")
) )

View File

@ -49,7 +49,7 @@ class TestFlowInspector(APITestCase):
"captcha_stage": None, "captcha_stage": None,
"component": "ak-stage-identification", "component": "ak-stage-identification",
"flow_info": { "flow_info": {
"background": flow.background_url, "background": "/static/dist/assets/images/flow_background.jpg",
"cancel_url": reverse("authentik_flows:cancel"), "cancel_url": reverse("authentik_flows:cancel"),
"title": flow.title, "title": flow.title,
"layout": "stacked", "layout": "stacked",

View File

@ -13020,6 +13020,11 @@
"type": "string", "type": "string",
"title": "Branding custom css" "title": "Branding custom css"
}, },
"branding_default_flow_background": {
"type": "string",
"minLength": 1,
"title": "Branding default flow background"
},
"flow_authentication": { "flow_authentication": {
"type": "string", "type": "string",
"format": "uuid", "format": "uuid",

View File

@ -4447,6 +4447,10 @@ paths:
schema: schema:
type: string type: string
format: uuid format: uuid
- in: query
name: branding_default_flow_background
schema:
type: string
- in: query - in: query
name: branding_favicon name: branding_favicon
schema: schema:
@ -41147,6 +41151,8 @@ components:
type: string type: string
branding_custom_css: branding_custom_css:
type: string type: string
branding_default_flow_background:
type: string
flow_authentication: flow_authentication:
type: string type: string
format: uuid format: uuid
@ -41208,6 +41214,9 @@ components:
minLength: 1 minLength: 1
branding_custom_css: branding_custom_css:
type: string type: string
branding_default_flow_background:
type: string
minLength: 1
flow_authentication: flow_authentication:
type: string type: string
format: uuid format: uuid
@ -50134,6 +50143,9 @@ components:
minLength: 1 minLength: 1
branding_custom_css: branding_custom_css:
type: string type: string
branding_default_flow_background:
type: string
minLength: 1
flow_authentication: flow_authentication:
type: string type: string
format: uuid format: uuid

View File

@ -136,6 +136,28 @@ export class BrandForm extends ModelForm<Brand, string> {
${msg("Icon shown in the browser tab.")} ${msg("Icon shown in the browser tab.")}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Default flow background")}
?required=${true}
name="brandingDefaultFlowBackground"
>
<input
type="text"
value="${first(
this.instance?.brandingDefaultFlowBackground,
"/static/dist/assets/images/flow_background.jpg",
)}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"Default background used during flow execution. Can be overridden per flow.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Custom CSS")} label=${msg("Custom CSS")}
required required