brands: migrate custom CSS to brands (#13172)

* brands: migrate custom CSS to brands

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

* fix missing default

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

* fix tests

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

* simpler migration

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

* add css to brand form

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

* fix

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-19 22:52:38 +00:00
committed by GitHub
parent 70b1f05a84
commit f37e1ca642
16 changed files with 94 additions and 54 deletions

View File

@ -49,6 +49,7 @@ class BrandSerializer(ModelSerializer):
"branding_title", "branding_title",
"branding_logo", "branding_logo",
"branding_favicon", "branding_favicon",
"branding_custom_css",
"flow_authentication", "flow_authentication",
"flow_invalidation", "flow_invalidation",
"flow_recovery", "flow_recovery",
@ -86,6 +87,7 @@ class CurrentBrandSerializer(PassiveSerializer):
branding_title = CharField() branding_title = CharField()
branding_logo = CharField(source="branding_logo_url") branding_logo = CharField(source="branding_logo_url")
branding_favicon = CharField(source="branding_favicon_url") branding_favicon = CharField(source="branding_favicon_url")
branding_custom_css = CharField()
ui_footer_links = ListField( ui_footer_links = ListField(
child=FooterLinkSerializer(), child=FooterLinkSerializer(),
read_only=True, read_only=True,

View File

@ -0,0 +1,35 @@
# Generated by Django 5.0.12 on 2025-02-22 01:51
from pathlib import Path
from django.db import migrations, models
from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migrate_custom_css(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Brand = apps.get_model("authentik_brands", "brand")
db_alias = schema_editor.connection.alias
path = Path("/web/dist/custom.css")
if not path.exists():
return
with path.read_text() as css:
Brand.objects.using(db_alias).update(branding_custom_css=css)
class Migration(migrations.Migration):
dependencies = [
("authentik_brands", "0007_brand_default_application"),
]
operations = [
migrations.AddField(
model_name="brand",
name="branding_custom_css",
field=models.TextField(blank=True, default=""),
),
migrations.RunPython(migrate_custom_css),
]

View File

@ -33,6 +33,7 @@ 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)
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"

View File

@ -24,6 +24,7 @@ class TestBrands(APITestCase):
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg", "branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_favicon": "/static/dist/assets/icons/icon.png", "branding_favicon": "/static/dist/assets/icons/icon.png",
"branding_title": "authentik", "branding_title": "authentik",
"branding_custom_css": "",
"matched_domain": brand.domain, "matched_domain": brand.domain,
"ui_footer_links": [], "ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC, "ui_theme": Themes.AUTOMATIC,
@ -43,6 +44,7 @@ class TestBrands(APITestCase):
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg", "branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_favicon": "/static/dist/assets/icons/icon.png", "branding_favicon": "/static/dist/assets/icons/icon.png",
"branding_title": "custom", "branding_title": "custom",
"branding_custom_css": "",
"matched_domain": "bar.baz", "matched_domain": "bar.baz",
"ui_footer_links": [], "ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC, "ui_theme": Themes.AUTOMATIC,
@ -59,6 +61,7 @@ class TestBrands(APITestCase):
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg", "branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_favicon": "/static/dist/assets/icons/icon.png", "branding_favicon": "/static/dist/assets/icons/icon.png",
"branding_title": "authentik", "branding_title": "authentik",
"branding_custom_css": "",
"matched_domain": "fallback", "matched_domain": "fallback",
"ui_footer_links": [], "ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC, "ui_theme": Themes.AUTOMATIC,

View File

@ -16,7 +16,7 @@
{% block head_before %} {% block head_before %}
{% endblock %} {% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject> <style>{{ brand.branding_custom_css }}</style>
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script> <script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script> <script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
{% block head %} {% block head %}

View File

@ -13016,6 +13016,10 @@
"minLength": 1, "minLength": 1,
"title": "Branding favicon" "title": "Branding favicon"
}, },
"branding_custom_css": {
"type": "string",
"title": "Branding custom css"
},
"flow_authentication": { "flow_authentication": {
"type": "string", "type": "string",
"format": "uuid", "format": "uuid",

View File

@ -8,7 +8,6 @@
<link rel="shortcut icon" type="image/png" href="/outpost.goauthentik.io/static/dist/assets/icons/icon.png"> <link rel="shortcut icon" type="image/png" href="/outpost.goauthentik.io/static/dist/assets/icons/icon.png">
<link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/patternfly.min.css"> <link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/patternfly.min.css">
<link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/authentik.css"> <link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/authentik.css">
<link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/custom.css">
<link rel="prefetch" href="/outpost.goauthentik.io/static/dist/assets/images/flow_background.jpg" /> <link rel="prefetch" href="/outpost.goauthentik.io/static/dist/assets/images/flow_background.jpg" />
<style> <style>
.pf-c-background-image::before { .pf-c-background-image::before {

View File

@ -41145,6 +41145,8 @@ components:
type: string type: string
branding_favicon: branding_favicon:
type: string type: string
branding_custom_css:
type: string
flow_authentication: flow_authentication:
type: string type: string
format: uuid format: uuid
@ -41204,6 +41206,8 @@ components:
branding_favicon: branding_favicon:
type: string type: string
minLength: 1 minLength: 1
branding_custom_css:
type: string
flow_authentication: flow_authentication:
type: string type: string
format: uuid format: uuid
@ -42096,6 +42100,8 @@ components:
type: string type: string
branding_favicon: branding_favicon:
type: string type: string
branding_custom_css:
type: string
ui_footer_links: ui_footer_links:
type: array type: array
items: items:
@ -42122,6 +42128,7 @@ components:
type: string type: string
readOnly: true readOnly: true
required: required:
- branding_custom_css
- branding_favicon - branding_favicon
- branding_logo - branding_logo
- branding_title - branding_title
@ -50125,6 +50132,8 @@ components:
branding_favicon: branding_favicon:
type: string type: string
minLength: 1 minLength: 1
branding_custom_css:
type: string
flow_authentication: flow_authentication:
type: string type: string
format: uuid format: uuid

8
web/package-lock.json generated
View File

@ -14,6 +14,7 @@
"./packages/*" "./packages/*"
], ],
"dependencies": { "dependencies": {
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9", "@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-python": "^6.1.6", "@codemirror/lang-python": "^6.1.6",
@ -1039,9 +1040,10 @@
} }
}, },
"node_modules/@codemirror/lang-css": { "node_modules/@codemirror/lang-css": {
"version": "6.3.0", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-CyR4rUNG9OYcXDZwMPvJdtb6PHbBDKUc/6Na2BIwZ6dKab1JQqKa4di+RNRY9Myn7JB81vayKwJeQ7jEdmNVDA==", "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",

View File

@ -2,6 +2,7 @@
"name": "@goauthentik/web", "name": "@goauthentik/web",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9", "@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-python": "^6.1.6", "@codemirror/lang-python": "^6.1.6",
@ -182,7 +183,6 @@
"./dist/enterprise/**", "./dist/enterprise/**",
"./dist/poly-*.js", "./dist/poly-*.js",
"./dist/poly-*.js.map", "./dist/poly-*.js.map",
"./dist/custom.css",
"./dist/theme-dark.css", "./dist/theme-dark.css",
"./dist/one-dark.css", "./dist/one-dark.css",
"./dist/patternfly.min.css" "./dist/patternfly.min.css"

View File

@ -52,7 +52,6 @@ const definitions = Object.fromEntries(
const assetsFileMappings = [ const assetsFileMappings = [
["node_modules/@patternfly/patternfly/patternfly.min.css", "."], ["node_modules/@patternfly/patternfly/patternfly.min.css", "."],
["node_modules/@patternfly/patternfly/assets/**", ".", "node_modules/@patternfly/patternfly/"], ["node_modules/@patternfly/patternfly/assets/**", ".", "node_modules/@patternfly/patternfly/"],
["src/custom.css", "."],
["src/common/styles/**", "."], ["src/common/styles/**", "."],
["src/assets/images/**", "./assets/images"], ["src/assets/images/**", "./assets/images"],
["./icons/*", "./assets/icons"], ["./icons/*", "./assets/icons"],

View File

@ -51,11 +51,7 @@ export class BrandForm extends ModelForm<Brand, string> {
} }
renderForm(): TemplateResult { renderForm(): TemplateResult {
return html` <ak-form-element-horizontal return html` <ak-form-element-horizontal label=${msg("Domain")} required name="domain">
label=${msg("Domain")}
?required=${true}
name="domain"
>
<input <input
type="text" type="text"
value="${first(this.instance?.domain, window.location.host)}" value="${first(this.instance?.domain, window.location.host)}"
@ -90,14 +86,10 @@ export class BrandForm extends ModelForm<Brand, string> {
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group .expanded=${true}> <ak-form-group>
<span slot="header"> ${msg("Branding settings")} </span> <span slot="header"> ${msg("Branding settings")} </span>
<div slot="body" class="pf-c-form"> <div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal label=${msg("Title")} required name="brandingTitle">
label=${msg("Title")}
?required=${true}
name="brandingTitle"
>
<input <input
type="text" type="text"
value="${first( value="${first(
@ -111,11 +103,7 @@ export class BrandForm extends ModelForm<Brand, string> {
${msg("Branding shown in page title and several other places.")} ${msg("Branding shown in page title and several other places.")}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal label=${msg("Logo")} required name="brandingLogo">
label=${msg("Logo")}
?required=${true}
name="brandingLogo"
>
<input <input
type="text" type="text"
value="${first(this.instance?.brandingLogo, DefaultBrand.brandingLogo)}" value="${first(this.instance?.brandingLogo, DefaultBrand.brandingLogo)}"
@ -130,7 +118,7 @@ export class BrandForm extends ModelForm<Brand, string> {
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Favicon")} label=${msg("Favicon")}
?required=${true} required
name="brandingFavicon" name="brandingFavicon"
> >
<input <input
@ -148,6 +136,23 @@ 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("Custom CSS")}
required
name="brandingCustomCss"
>
<ak-codemirror
mode=${CodeMirrorMode.CSS}
value="${first(
this.instance?.brandingCustomCss,
DefaultBrand.brandingCustomCss,
)}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">
${msg("Custom CSS to apply to pages when this brand is active.")}
</p>
</ak-form-element-horizontal>
</div> </div>
</ak-form-group> </ak-form-group>

View File

@ -1 +0,0 @@
/* User customisable */

View File

@ -24,26 +24,6 @@ type AkInterface = HTMLElement & {
export const rootInterface = <T extends AkInterface>(): T | undefined => export const rootInterface = <T extends AkInterface>(): T | undefined =>
(document.body.querySelector("[data-ak-interface-root]") as T) ?? undefined; (document.body.querySelector("[data-ak-interface-root]") as T) ?? undefined;
let css: Promise<string[]> | undefined;
function fetchCustomCSS(): Promise<string[]> {
if (!css) {
css = Promise.all(
Array.of(...document.head.querySelectorAll<HTMLLinkElement>("link[data-inject]")).map(
(link) => {
return fetch(link.href)
.then((res) => {
return res.text();
})
.finally(() => {
return "";
});
},
),
);
}
return css;
}
export const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)"; export const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)";
// Ensure themes are converted to a static instance of CSS Stylesheet, otherwise the // Ensure themes are converted to a static instance of CSS Stylesheet, otherwise the
@ -103,15 +83,12 @@ export class AKElement extends LitElement {
} }
async _initCustomCSS(root: DocumentOrShadowRoot): Promise<void> { async _initCustomCSS(root: DocumentOrShadowRoot): Promise<void> {
const sheets = await fetchCustomCSS(); const brand = globalAK().brand;
sheets.map((css) => { if (!brand) {
if (css === "") { return;
return; }
} const sheet = await new CSSStyleSheet().replace(brand.brandingCustomCss);
new CSSStyleSheet().replace(css).then((sheet) => { root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
});
});
} }
_applyTheme(root: DocumentOrShadowRoot, theme?: UiThemeEnum): void { _applyTheme(root: DocumentOrShadowRoot, theme?: UiThemeEnum): void {

View File

@ -1,4 +1,5 @@
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
import { css as cssLang } from "@codemirror/lang-css";
import { html as htmlLang } from "@codemirror/lang-html"; import { html as htmlLang } from "@codemirror/lang-html";
import { javascript } from "@codemirror/lang-javascript"; import { javascript } from "@codemirror/lang-javascript";
import { python } from "@codemirror/lang-python"; import { python } from "@codemirror/lang-python";
@ -27,6 +28,7 @@ export enum CodeMirrorMode {
XML = "xml", XML = "xml",
JavaScript = "javascript", JavaScript = "javascript",
HTML = "html", HTML = "html",
CSS = "css",
Python = "python", Python = "python",
YAML = "yaml", YAML = "yaml",
} }
@ -147,6 +149,8 @@ export class CodeMirrorTextarea<T> extends AKElement {
return htmlLang(); return htmlLang();
case CodeMirrorMode.Python: case CodeMirrorMode.Python:
return python(); return python();
case CodeMirrorMode.CSS:
return cssLang();
case CodeMirrorMode.YAML: case CodeMirrorMode.YAML:
return new LanguageSupport(StreamLanguage.define(yamlMode.yaml)); return new LanguageSupport(StreamLanguage.define(yamlMode.yaml));
} }

View File

@ -22,6 +22,7 @@ export const DefaultBrand: CurrentBrand = {
brandingLogo: "/static/dist/assets/icons/icon_left_brand.svg", brandingLogo: "/static/dist/assets/icons/icon_left_brand.svg",
brandingFavicon: "/static/dist/assets/icons/icon.png", brandingFavicon: "/static/dist/assets/icons/icon.png",
brandingTitle: "authentik", brandingTitle: "authentik",
brandingCustomCss: "",
uiFooterLinks: [], uiFooterLinks: [],
uiTheme: UiThemeEnum.Automatic, uiTheme: UiThemeEnum.Automatic,
matchedDomain: "", matchedDomain: "",