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:
@ -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",
|
||||||
|
|||||||
@ -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"),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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
|
||||||
|
|||||||
@ -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": "",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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"):
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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")
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
12
schema.yml
12
schema.yml
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user