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 === "") {
|
||||
return;
|
||||
}
|
||||
new CSSStyleSheet().replace(css).then((sheet) => {
|
||||
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
|
||||
});
|
||||
});
|
||||
const brand = globalAK().brand;
|
||||
if (!brand) {
|
||||
return;
|
||||
}
|
||||
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