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:
		| @ -49,6 +49,7 @@ class BrandSerializer(ModelSerializer): | ||||
|             "branding_title", | ||||
|             "branding_logo", | ||||
|             "branding_favicon", | ||||
|             "branding_custom_css", | ||||
|             "flow_authentication", | ||||
|             "flow_invalidation", | ||||
|             "flow_recovery", | ||||
| @ -86,6 +87,7 @@ class CurrentBrandSerializer(PassiveSerializer): | ||||
|     branding_title = CharField() | ||||
|     branding_logo = CharField(source="branding_logo_url") | ||||
|     branding_favicon = CharField(source="branding_favicon_url") | ||||
|     branding_custom_css = CharField() | ||||
|     ui_footer_links = ListField( | ||||
|         child=FooterLinkSerializer(), | ||||
|         read_only=True, | ||||
|  | ||||
| @ -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), | ||||
|     ] | ||||
| @ -33,6 +33,7 @@ class Brand(SerializerModel): | ||||
|  | ||||
|     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_custom_css = models.TextField(default="", blank=True) | ||||
|  | ||||
|     flow_authentication = models.ForeignKey( | ||||
|         Flow, null=True, on_delete=models.SET_NULL, related_name="brand_authentication" | ||||
|  | ||||
| @ -24,6 +24,7 @@ class TestBrands(APITestCase): | ||||
|                 "branding_logo": "/static/dist/assets/icons/icon_left_brand.svg", | ||||
|                 "branding_favicon": "/static/dist/assets/icons/icon.png", | ||||
|                 "branding_title": "authentik", | ||||
|                 "branding_custom_css": "", | ||||
|                 "matched_domain": brand.domain, | ||||
|                 "ui_footer_links": [], | ||||
|                 "ui_theme": Themes.AUTOMATIC, | ||||
| @ -43,6 +44,7 @@ class TestBrands(APITestCase): | ||||
|                 "branding_logo": "/static/dist/assets/icons/icon_left_brand.svg", | ||||
|                 "branding_favicon": "/static/dist/assets/icons/icon.png", | ||||
|                 "branding_title": "custom", | ||||
|                 "branding_custom_css": "", | ||||
|                 "matched_domain": "bar.baz", | ||||
|                 "ui_footer_links": [], | ||||
|                 "ui_theme": Themes.AUTOMATIC, | ||||
| @ -59,6 +61,7 @@ class TestBrands(APITestCase): | ||||
|                 "branding_logo": "/static/dist/assets/icons/icon_left_brand.svg", | ||||
|                 "branding_favicon": "/static/dist/assets/icons/icon.png", | ||||
|                 "branding_title": "authentik", | ||||
|                 "branding_custom_css": "", | ||||
|                 "matched_domain": "fallback", | ||||
|                 "ui_footer_links": [], | ||||
|                 "ui_theme": Themes.AUTOMATIC, | ||||
|  | ||||
| @ -16,7 +16,7 @@ | ||||
|         {% block head_before %} | ||||
|         {% endblock %} | ||||
|         <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/standalone/loading/index-%v.js' %}" type="module"></script> | ||||
|         {% block head %} | ||||
|  | ||||
| @ -13016,6 +13016,10 @@ | ||||
|                     "minLength": 1, | ||||
|                     "title": "Branding favicon" | ||||
|                 }, | ||||
|                 "branding_custom_css": { | ||||
|                     "type": "string", | ||||
|                     "title": "Branding custom css" | ||||
|                 }, | ||||
|                 "flow_authentication": { | ||||
|                     "type": "string", | ||||
|                     "format": "uuid", | ||||
|  | ||||
| @ -8,7 +8,6 @@ | ||||
|         <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/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" /> | ||||
|         <style> | ||||
|             .pf-c-background-image::before { | ||||
|  | ||||
| @ -41145,6 +41145,8 @@ components: | ||||
|           type: string | ||||
|         branding_favicon: | ||||
|           type: string | ||||
|         branding_custom_css: | ||||
|           type: string | ||||
|         flow_authentication: | ||||
|           type: string | ||||
|           format: uuid | ||||
| @ -41204,6 +41206,8 @@ components: | ||||
|         branding_favicon: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|         branding_custom_css: | ||||
|           type: string | ||||
|         flow_authentication: | ||||
|           type: string | ||||
|           format: uuid | ||||
| @ -42096,6 +42100,8 @@ components: | ||||
|           type: string | ||||
|         branding_favicon: | ||||
|           type: string | ||||
|         branding_custom_css: | ||||
|           type: string | ||||
|         ui_footer_links: | ||||
|           type: array | ||||
|           items: | ||||
| @ -42122,6 +42128,7 @@ components: | ||||
|           type: string | ||||
|           readOnly: true | ||||
|       required: | ||||
|       - branding_custom_css | ||||
|       - branding_favicon | ||||
|       - branding_logo | ||||
|       - branding_title | ||||
| @ -50125,6 +50132,8 @@ components: | ||||
|         branding_favicon: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|         branding_custom_css: | ||||
|           type: string | ||||
|         flow_authentication: | ||||
|           type: string | ||||
|           format: uuid | ||||
|  | ||||
							
								
								
									
										8
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -14,6 +14,7 @@ | ||||
|                 "./packages/*" | ||||
|             ], | ||||
|             "dependencies": { | ||||
|                 "@codemirror/lang-css": "^6.3.1", | ||||
|                 "@codemirror/lang-html": "^6.4.9", | ||||
|                 "@codemirror/lang-javascript": "^6.2.2", | ||||
|                 "@codemirror/lang-python": "^6.1.6", | ||||
| @ -1039,9 +1040,10 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@codemirror/lang-css": { | ||||
|             "version": "6.3.0", | ||||
|             "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.0.tgz", | ||||
|             "integrity": "sha512-CyR4rUNG9OYcXDZwMPvJdtb6PHbBDKUc/6Na2BIwZ6dKab1JQqKa4di+RNRY9Myn7JB81vayKwJeQ7jEdmNVDA==", | ||||
|             "version": "6.3.1", | ||||
|             "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", | ||||
|             "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@codemirror/autocomplete": "^6.0.0", | ||||
|                 "@codemirror/language": "^6.0.0", | ||||
|  | ||||
| @ -2,6 +2,7 @@ | ||||
|     "name": "@goauthentik/web", | ||||
|     "version": "0.0.0", | ||||
|     "dependencies": { | ||||
|         "@codemirror/lang-css": "^6.3.1", | ||||
|         "@codemirror/lang-html": "^6.4.9", | ||||
|         "@codemirror/lang-javascript": "^6.2.2", | ||||
|         "@codemirror/lang-python": "^6.1.6", | ||||
| @ -182,7 +183,6 @@ | ||||
|                 "./dist/enterprise/**", | ||||
|                 "./dist/poly-*.js", | ||||
|                 "./dist/poly-*.js.map", | ||||
|                 "./dist/custom.css", | ||||
|                 "./dist/theme-dark.css", | ||||
|                 "./dist/one-dark.css", | ||||
|                 "./dist/patternfly.min.css" | ||||
|  | ||||
| @ -52,7 +52,6 @@ const definitions = Object.fromEntries( | ||||
| const assetsFileMappings = [ | ||||
|     ["node_modules/@patternfly/patternfly/patternfly.min.css", "."], | ||||
|     ["node_modules/@patternfly/patternfly/assets/**", ".", "node_modules/@patternfly/patternfly/"], | ||||
|     ["src/custom.css", "."], | ||||
|     ["src/common/styles/**", "."], | ||||
|     ["src/assets/images/**", "./assets/images"], | ||||
|     ["./icons/*", "./assets/icons"], | ||||
|  | ||||
| @ -51,11 +51,7 @@ export class BrandForm extends ModelForm<Brand, string> { | ||||
|     } | ||||
|  | ||||
|     renderForm(): TemplateResult { | ||||
|         return html` <ak-form-element-horizontal | ||||
|                 label=${msg("Domain")} | ||||
|                 ?required=${true} | ||||
|                 name="domain" | ||||
|             > | ||||
|         return html` <ak-form-element-horizontal label=${msg("Domain")} required name="domain"> | ||||
|                 <input | ||||
|                     type="text" | ||||
|                     value="${first(this.instance?.domain, window.location.host)}" | ||||
| @ -90,14 +86,10 @@ export class BrandForm extends ModelForm<Brand, string> { | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|  | ||||
|             <ak-form-group .expanded=${true}> | ||||
|             <ak-form-group> | ||||
|                 <span slot="header"> ${msg("Branding settings")} </span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${msg("Title")} | ||||
|                         ?required=${true} | ||||
|                         name="brandingTitle" | ||||
|                     > | ||||
|                     <ak-form-element-horizontal label=${msg("Title")} required name="brandingTitle"> | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${first( | ||||
| @ -111,11 +103,7 @@ export class BrandForm extends ModelForm<Brand, string> { | ||||
|                             ${msg("Branding shown in page title and several other places.")} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${msg("Logo")} | ||||
|                         ?required=${true} | ||||
|                         name="brandingLogo" | ||||
|                     > | ||||
|                     <ak-form-element-horizontal label=${msg("Logo")} required name="brandingLogo"> | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             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 | ||||
|                         label=${msg("Favicon")} | ||||
|                         ?required=${true} | ||||
|                         required | ||||
|                         name="brandingFavicon" | ||||
|                     > | ||||
|                         <input | ||||
| @ -148,6 +136,23 @@ export class BrandForm extends ModelForm<Brand, string> { | ||||
|                             ${msg("Icon shown in the browser tab.")} | ||||
|                         </p> | ||||
|                     </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> | ||||
|             </ak-form-group> | ||||
|  | ||||
|  | ||||
| @ -1 +0,0 @@ | ||||
| /* User customisable */ | ||||
| @ -24,26 +24,6 @@ type AkInterface = HTMLElement & { | ||||
| export const rootInterface = <T extends AkInterface>(): 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)"; | ||||
|  | ||||
| // 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> { | ||||
|         const sheets = await fetchCustomCSS(); | ||||
|         sheets.map((css) => { | ||||
|             if (css === "") { | ||||
|         const brand = globalAK().brand; | ||||
|         if (!brand) { | ||||
|             return; | ||||
|         } | ||||
|             new CSSStyleSheet().replace(css).then((sheet) => { | ||||
|         const sheet = await new CSSStyleSheet().replace(brand.brandingCustomCss); | ||||
|         root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet]; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     _applyTheme(root: DocumentOrShadowRoot, theme?: UiThemeEnum): void { | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; | ||||
| import { css as cssLang } from "@codemirror/lang-css"; | ||||
| import { html as htmlLang } from "@codemirror/lang-html"; | ||||
| import { javascript } from "@codemirror/lang-javascript"; | ||||
| import { python } from "@codemirror/lang-python"; | ||||
| @ -27,6 +28,7 @@ export enum CodeMirrorMode { | ||||
|     XML = "xml", | ||||
|     JavaScript = "javascript", | ||||
|     HTML = "html", | ||||
|     CSS = "css", | ||||
|     Python = "python", | ||||
|     YAML = "yaml", | ||||
| } | ||||
| @ -147,6 +149,8 @@ export class CodeMirrorTextarea<T> extends AKElement { | ||||
|                 return htmlLang(); | ||||
|             case CodeMirrorMode.Python: | ||||
|                 return python(); | ||||
|             case CodeMirrorMode.CSS: | ||||
|                 return cssLang(); | ||||
|             case CodeMirrorMode.YAML: | ||||
|                 return new LanguageSupport(StreamLanguage.define(yamlMode.yaml)); | ||||
|         } | ||||
|  | ||||
| @ -22,6 +22,7 @@ export const DefaultBrand: CurrentBrand = { | ||||
|     brandingLogo: "/static/dist/assets/icons/icon_left_brand.svg", | ||||
|     brandingFavicon: "/static/dist/assets/icons/icon.png", | ||||
|     brandingTitle: "authentik", | ||||
|     brandingCustomCss: "", | ||||
|     uiFooterLinks: [], | ||||
|     uiTheme: UiThemeEnum.Automatic, | ||||
|     matchedDomain: "", | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens L.
					Jens L.