Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.dev
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
SIZE_LIMIT=10MiB
VERBOSITY=debug
THEME_CUSTOM_CSS_FILE=/app/custom.css
# THEME_SVG=https://mway.io/images/logo/mway_logo_black.svg
# THEME_SVG=https://mway.digital/favicon.svg
# THEME_SVG=https://bluerange.io/img/assets/favicon.svg
# THEME_IMAGE=https://mway.io/images/logo/BlueRange_Logo_black.svg
# THEME_IMAGE=https://repo.mwaysolutions.com/uploads/-/system/appearance/header_logo/1/mway_logo_white_outline_small.png
THEME_IMAGE=/app/logo.jpeg
# THEME_IMAGE="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI1LjMuMSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkViZW5lXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCA2NjkuNCAxMTcuNCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNjY5LjQgMTE3LjQ7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7Y2xpcC1wYXRoOnVybCgjU1ZHSURfMl8pO30KPC9zdHlsZT4KPGc+Cgk8ZGVmcz4KCQk8cmVjdCBpZD0iU1ZHSURfMV8iIHg9IjEuMyIgeT0iMy42IiB3aWR0aD0iNjY2LjEiIGhlaWdodD0iMTExLjgiLz4KCTwvZGVmcz4KCTxjbGlwUGF0aCBpZD0iU1ZHSURfMl8iPgoJCTx1c2UgeGxpbms6aHJlZj0iI1NWR0lEXzFfIiAgc3R5bGU9Im92ZXJmbG93OnZpc2libGU7Ii8+Cgk8L2NsaXBQYXRoPgoJPHBhdGggY2xhc3M9InN0MCIgZD0iTTEyMi4yLDQ4LjdjMCwxOS4xLTE1LjcsMzQuNi0zNS4yLDM0LjZjLTMuOSwwLTcuNi0wLjYtMTEuMS0xLjhjLTMuMS0xLTYtMi40LTguNi00LjIKCQljLTIuNiwxLjgtNS41LDMuMi04LjYsNC4yYy0zLjUsMS4xLTcuMiwxLjgtMTEuMSwxLjhjLTE5LjQsMC0zNS4yLTE1LjUtMzUuMi0zNC42YzAtMTkuMSwxNS43LTM0LjYsMzUuMi0zNC42CgkJYzMuOSwwLDcuNiwwLjYsMTEuMSwxLjhjMy4xLDEsNiwyLjQsOC42LDQuMmMyLjYtMS44LDUuNS0zLjIsOC42LTQuMmMzLjUtMS4xLDcuMi0xLjgsMTEuMS0xLjhDMTA2LjUsMTQuMSwxMjIuMiwyOS42LDEyMi4yLDQ4LjcKCQkgTTEzMy40LDQ4LjdjMC0yNC45LTIwLjUtNDUuMS00NS43LTQ1LjFjLTcuMSwwLTE0LDEuNi0yMC4zLDQuN0M2MSw1LjIsNTQuMSwzLjYsNDcsMy42QzIxLjgsMy42LDEuMywyMy44LDEuMyw0OC43CgkJYzAsMjQuOSwyMC41LDQ1LjEsNDUuNyw0NS4xYzcuMSwwLDE0LTEuNiwyMC4zLTQuN2M2LjQsMy4xLDEzLjIsNC43LDIwLjMsNC43QzExMi45LDkzLjksMTMzLjQsNzMuNiwxMzMuNCw0OC43Ii8+Cgk8cGF0aCBjbGFzcz0ic3QwIiBkPSJNODcuMSw3My40YzEzLjcsMCwyNC44LTExLDI0LjgtMjQuNWMwLTEzLjUtMTEuMS0yNC41LTI0LjgtMjQuNWMtMSwwLTIsMC4xLTMuMSwwLjJjLTMuMSwwLjQtNiwxLjMtOC43LDIuOAoJCWMtMy4xLDEuNy01LjksNC4xLTgsNi45Yy0zLjEsNC4xLTQuOCw5LjEtNC45LDE0LjNsMCwwLjNsMCwwLjNjLTAuMSw1LjEtMyw5LjctNy42LDEyLjFjLTIsMS4xLTQuMiwxLjYtNi41LDEuNgoJCWMtMC41LDAtMS4xLDAtMS43LTAuMUMzOS4zLDYyLDM0LDU2LDM0LDQ4LjljMC03LjEsNS40LTEzLDEyLjUtMTMuOWMwLjYtMC4xLDEuMi0wLjEsMS43LTAuMWMyLjMsMCw0LjUsMC41LDYuNSwxLjZsMC41LDAuMgoJCWwwLjItMC41YzEuMi0yLjksMi44LTUuNiw0LjctOC4xbDAuNC0wLjVMNjAsMjcuNGMtMi43LTEuNC01LjYtMi40LTguNy0yLjhjLTEuMS0wLjEtMi4yLTAuMi0zLjEtMC4yYy0xMy43LDAtMjQuOCwxMS0yNC44LDI0LjUKCQljMCwxMy41LDExLjEsMjQuNSwyNC44LDI0LjVjMC45LDAsMS45LTAuMSwzLjEtMC4yYzMuMS0wLjQsNi0xLjMsOC43LTIuOGMzLjEtMS43LDUuOS00LjEsOC02LjljMy4xLTQuMSw0LjgtOS4xLDQuOS0xNC4zbDAtMC4zCgkJbDAtMC4zYzAuMS01LjEsMy05LjcsNy42LTEyLjFjMi0xLjEsNC4yLTEuNiw2LjUtMS42YzAuNSwwLDEuMSwwLDEuNywwLjFjNy4xLDAuOSwxMi41LDYuOCwxMi41LDEzLjljMCw3LjEtNS40LDEzLTEyLjUsMTMuOQoJCWMtMC43LDAuMS0xLjIsMC4xLTEuNywwLjFjLTIuMywwLTQuNS0wLjUtNi41LTEuNmwtMC41LTAuMmwtMC4yLDAuNWMtMS4yLDIuOS0yLjgsNS42LTQuNyw4LjFsLTAuNCwwLjVsMC41LDAuMwoJCWMyLjcsMS40LDUuNiwyLjQsOC43LDIuOEM4NS4yLDczLjMsODYuMSw3My40LDg3LjEsNzMuNCIvPgoJPHBhdGggY2xhc3M9InN0MCIgZD0iTTQ4LjEsNTQuMmMtMi45LDAtNS4zLTIuNC01LjMtNS4zYzAtMi45LDIuNC01LjMsNS4zLTUuM2MyLjksMCw1LjMsMi40LDUuMyw1LjNDNTMuNCw1MS44LDUxLDU0LjIsNDguMSw1NC4yIgoJCS8+Cgk8cGF0aCBjbGFzcz0ic3QwIiBkPSJNNTk4LjIsNTYuN2MwLDgtNi41LDE0LjYtMTQuNSwxNC42Yy04LDAtMTQuNS02LjUtMTQuNS0xNC42YzAtOCw2LjUtMTQuNiwxNC41LTE0LjYKCQlDNTkxLjgsNDIuMSw1OTguMiw0OC43LDU5OC4yLDU2LjcgTTYxMC4xLDM1LjdsLTkuNS03LjJsLTQuNSw2LjRjLTMuNi0yLjEtNy44LTMuMy0xMi4zLTMuM2MtMTMuOCwwLTI0LjksMTEuMy0yNC45LDI1LjEKCQljMCwxMy43LDExLDI0LjksMjQuNiwyNS4xYzAsMCwwLDAsMCwwYzcuMywwLDE1LjIsMi44LDE1LjIsMTEuMmMwLDguNS02LjYsMTIuMS0xNSwxMi4xYy04LjQsMC0xNS41LTUuMi0xNS41LTEzLjIKCQljMCwwLTEwLjEsMC0xMC4xLDBjMCwxNC4xLDEwLjksMjMuNCwyNS41LDIzLjRjMTQuNiwwLDI1LjItNy44LDI1LjItMjIuMmMwLTYuNi0yLjItMTEuNy0xMC40LTE2LjFjNi4zLTQuNiwxMC40LTEyLDEwLjQtMjAuNAoJCWMwLTUuMS0xLjUtOS45LTQuMi0xMy45TDYxMC4xLDM1Ljd6IE00NjMuMyw0MS44Yy05LDAtMTYuMyw3LjMtMTYuMywxNi4zYzAsOSw3LjMsMTYuMywxNi4zLDE2LjNjOSwwLDE2LjMtNy4zLDE2LjMtMTYuMwoJCUM0NzkuNiw0OS4xLDQ3Mi4zLDQxLjgsNDYzLjMsNDEuOCBNNDgxLjQsODMuMmwtMS4xLTQuOGMtNC42LDMuOS0xMC42LDYuMi0xNy4xLDYuMmMtMTQuNiwwLTI2LjUtMTEuOS0yNi41LTI2LjUKCQljMC0xNC42LDExLjktMjYuNSwyNi41LTI2LjVjNi41LDAsMTIuNCwyLjMsMTcsNi4ydi0wLjFsMS4xLTQuOGg5LjR2NTAuMkg0ODEuNHogTTIzMS42LDgzLjJoMTAuNXYtNjloLTEwLjVWODMuMnogTTI1My42LDMzdjMwLjYKCQljMCwxMy41LDkuOCwyMSwxOS45LDIxYzUuNCwwLDEzLjItMS45LDE3LjUtNi4ybDAuNyw0LjhoOS41VjMzLjFoLTEwLjVWNjNjMCw2LjUtOC4zLDEyLTE0LjksMTJjLTYuOCwwLTExLjYtNS0xMS42LTExLjVWMzNIMjUzLjYKCQl6IE01NDkuOCw4My4yVjUyLjZjMC0xMy41LTkuOC0yMS0xOS45LTIxYy01LjQsMC0xMy4yLDIuMi0xNy41LDYuNGwtMC43LTVoLTkuNXY1MC4yaDEwLjVWNTMuMmMwLTYuNSw4LjMtMTIsMTQuOS0xMgoJCWM2LjgsMCwxMS42LDUsMTEuNiwxMS41djMwLjVINTQ5Ljh6IE00MTkuNCwzNi43YzAtNC45LTMuMi0xMS44LTEwLjQtMTEuOGgtMjMuN3YyMS41SDQwOUM0MTYuMyw0Ni40LDQxOS40LDQxLjYsNDE5LjQsMzYuNwoJCSBNNDMzLjksODMuM2gtMTMuN2wtMTcuNy0yNi41aC0xNy4ydjI2LjVoLTExLjVWMTQuMWM5LjcsMCwyNS41LDAuMSwzNS4xLDAuMWMxNC4zLDAuMSwyMS45LDExLjcsMjEuOSwyMi4yCgkJYzAsOC4zLTMuOCwxNi44LTE1LjQsMTkuMWwxOC40LDI3VjgzLjN6IE0zNTEuNCw1M2gtMjljMi4xLTYuNSw3LjMtMTEuMiwxNC41LTExLjJTMzQ5LjIsNDYuNSwzNTEuNCw1MyBNMzYyLjQsNjIuMnYtNC4xCgkJYzAtMTQuNi0xMC45LTI2LjUtMjUuNS0yNi41cy0yNS41LDExLjktMjUuNSwyNi41czExLjksMjYuNSwyNi41LDI2LjVjOC4xLDAsMTUuNi0zLjIsMjAuNC04LjZsLTcuNi02LjljLTMsMy4zLTcuNiw1LjItMTIuOSw1LjIKCQljLTcuNSwwLTE0LjUtNS40LTE1LjUtMTIuM0gzNjIuNHogTTY0MS45LDQxLjhjLTcuMiwwLTEyLjMsNC43LTE0LjUsMTEuMmgyOUM2NTQuMyw0Ni41LDY0OS4yLDQxLjgsNjQxLjksNDEuOCBNNjI3LjQsNjIuMgoJCWMxLDYuOCw4LDEyLjMsMTUuNSwxMi4zYzUuMywwLDkuOS0xLjksMTIuOS01LjJsNy42LDYuOWMtNC44LDUuNC0xMi4zLDguNi0yMC40LDguNmMtMTQuNiwwLTI2LjUtMTEuOS0yNi41LTI2LjUKCQlzMTAuOS0yNi41LDI1LjUtMjYuNWMxNC42LDAsMjUuNSwxMS45LDI1LjUsMjYuNXY0LjFINjI3LjR6IE0yMTAuNyw2Mi45YzAsNS41LTMuMiw5LjQtMTAuNiw5LjRoLTI0LjZWNTIuOGgyMy42CgkJQzIwOCw1Mi44LDIxMC43LDU3LjYsMjEwLjcsNjIuOSBNMjA5LDMzLjdjMCwzLjMtMS44LDkuNS0xMC4zLDkuNWgtMjMuM1YyNWgyMy4zQzIwNy41LDI1LDIwOSwzMSwyMDksMzMuNyBNMjE5LjYsMzQuMQoJCWMwLTExLjctOC40LTE5LjktMTkuNS0xOS45aC0zNS4ydjY5aDM1LjJjMTEuOCwwLDIxLjItOC4zLDIxLjItMjEuMmMwLTMuNy0yLjQtMTEuOS05LjEtMTRDMjE3LjUsNDUuNSwyMTkuNiwzOC42LDIxOS42LDM0LjEiLz4KPC9nPgo8L3N2Zz4K"
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ of the notes even if it tried to.
| `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable |
| `THEME_NEW_NOTE_NOTICE` | `true` | Show the message about how notes are stored in the memory and may be evicted after creating a new note. Defaults to `true`. |
| `THEME_HOME_LINK` | `true` | Show the `/home` link in the footer. Defaults to `true`. |
| `THEME_CUSTOM_CSS_FILE` | `""` | Path to a CSS file inside the container. When set, the file is served at `/custom.css` and loaded by the frontend, allowing full visual customisation via CSS variables or selectors. Mount the file as a volume: `-v ./my-theme.css:/custom.css:ro` and set `THEME_CUSTOM_CSS_FILE=/custom.css`. |
| `IMPRINT_URL` | `""` | Custom url for an Imprint hosted somewhere else. Must be publicly reachable. Takes precedence above `IMPRINT_HTML`. |
| `IMPRINT_HTML` | `""` | Alternative to `IMPRINT_URL`, this can be used to specify the HTML code to show on `/imprint`. Only `IMPRINT_HTML` or `IMPRINT_URL` should be specified, not both. |

Expand Down
6 changes: 5 additions & 1 deletion docker-compose.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,8 @@ services:
interval: 1m
timeout: 3s
retries: 2
start_period: 5s
start_period: 5s

volumes:
- './custom.css:/app/custom.css'
- './logo.jpeg:/app/logo.jpeg'
4 changes: 3 additions & 1 deletion packages/backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ lazy_static = "1"
ring = "0.17"
bs62 = "0.1"
byte-unit = "4"
dotenv = "0.15"
dotenv = "0.15"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
base64 = "0.22"
8 changes: 7 additions & 1 deletion packages/backend/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use byte_unit::Byte;
use std::sync::OnceLock;

pub static THEME_RESOLVED_SVG: OnceLock<String> = OnceLock::new();
pub static THEME_RESOLVED_IMAGE: OnceLock<String> = OnceLock::new();

// Internal
lazy_static! {
Expand Down Expand Up @@ -82,4 +86,6 @@ lazy_static! {
.unwrap_or("true".to_string())
.parse()
.unwrap();
}
pub static ref THEME_CUSTOM_CSS_FILE: String = std::env::var("THEME_CUSTOM_CSS_FILE")
.unwrap_or("".to_string());
}
95 changes: 89 additions & 6 deletions packages/backend/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
use std::{collections::HashMap, sync::Arc};
use base64::Engine;
use reqwest;

use axum::{
Router, ServiceExt,
extract::{DefaultBodyLimit, Request},
Expand All @@ -20,15 +24,90 @@ mod note;
mod status;
mod store;

fn is_svg_content(s: &str) -> bool {
let t = s.trim_start();
t.starts_with("<svg") || (t.starts_with("<?xml") && t.contains("<svg"))
}

fn mime_from_path(path: &str) -> &'static str {
let lower = path.to_lowercase();
if lower.ends_with(".png") { "image/png" }
else if lower.ends_with(".jpg") || lower.ends_with(".jpeg") { "image/jpeg" }
else if lower.ends_with(".gif") { "image/gif" }
else if lower.ends_with(".webp") { "image/webp" }
else if lower.ends_with(".ico") { "image/x-icon" }
else if lower.ends_with(".bmp") { "image/bmp" }
else { "application/octet-stream" }
}

async fn resolve_theme_image(value: &str) {
if value.is_empty() {
return;
}

if let Some(rest) = value.strip_prefix("data:") {
// Base64 or raw data URL: data:[mediatype][;base64],<data>
let (header, data) = rest.split_once(',')
.unwrap_or_else(|| panic!("Invalid data URL format in THEME_IMAGE"));
if header.contains("svg") {
let svg = if header.contains(";base64") {
let bytes = base64::engine::general_purpose::STANDARD.decode(data)
.unwrap_or_else(|e| panic!("Invalid base64 in THEME_IMAGE: {e}"));
String::from_utf8(bytes)
.unwrap_or_else(|e| panic!("Invalid UTF-8 in THEME_IMAGE SVG data: {e}"))
} else {
data.to_string()
};
config::THEME_RESOLVED_SVG.set(svg).unwrap();
} else {
config::THEME_RESOLVED_IMAGE.set(value.to_string()).unwrap();
}
} else if value.starts_with("http://") || value.starts_with("https://") {
let response = reqwest::get(value)
.await
.unwrap_or_else(|e| panic!("Failed to fetch THEME_IMAGE from {value}: {e}"));
let is_svg_ct = response.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.map(|ct| ct.contains("svg"))
.unwrap_or(false);
let content = response.text()
.await
.unwrap_or_else(|e| panic!("Failed to read THEME_IMAGE response: {e}"));
if is_svg_ct || is_svg_content(&content) {
config::THEME_RESOLVED_SVG.set(content).unwrap();
} else {
config::THEME_RESOLVED_IMAGE.set(value.to_string()).unwrap();
}
} else {
// Local file path
let bytes = std::fs::read(value)
.unwrap_or_else(|e| panic!("Cannot read THEME_IMAGE file {value}: {e}"));
let is_svg = value.to_lowercase().ends_with(".svg")
|| std::str::from_utf8(&bytes).map(is_svg_content).unwrap_or(false);
if is_svg {
let content = String::from_utf8(bytes)
.unwrap_or_else(|e| panic!("Invalid UTF-8 in THEME_IMAGE SVG file: {e}"));
config::THEME_RESOLVED_SVG.set(content).unwrap();
} else {
let mime = mime_from_path(value);
let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes);
config::THEME_RESOLVED_IMAGE.set(format!("data:{mime};base64,{encoded}")).unwrap();
}
}
}

#[tokio::main]
async fn main() {
dotenv().ok();

if !store::can_reach_cache() {
println!("cannot reach cache");
panic!("cannot reach cache");
println!("cannot reach redis");
panic!("cannot reach redis");
}


resolve_theme_image(config::THEME_IMAGE.as_str()).await;

let notes_routes = Router::new()
.route("/", post(note::create))
.route("/{id}", delete(note::view))
Expand All @@ -44,9 +123,13 @@ async fn main() {
let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
let serve_dir =
ServeDir::new(config::FRONTEND_PATH.to_string()).not_found_service(ServeFile::new(index));
let app = Router::new()
let mut app = Router::new()
.nest("/api", api_routes)
.merge(health_routes)
.merge(health_routes);
if !config::THEME_CUSTOM_CSS_FILE.is_empty() {
app = app.route_service("/custom.css", ServeFile::new(config::THEME_CUSTOM_CSS_FILE.as_str()));
}
let app = app
.fallback_service(serve_dir)
.layer(DefaultBodyLimit::max(*config::LIMIT))
.layer(
Expand All @@ -66,4 +149,4 @@ async fn main() {
axum::serve(listener, ServiceExt::<Request>::into_make_service(app))
.await
.unwrap();
}
}
6 changes: 5 additions & 1 deletion packages/backend/src/status/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ pub struct Status {
pub imprint_html: String,
// Theme
pub theme_image: String,
pub theme_svg: String,
pub theme_text: String,
pub theme_page_title: String,
pub theme_favicon: String,
pub theme_new_note_notice: bool,
pub theme_home_link: bool,
pub theme_custom_css: bool,
}

pub async fn get_status() -> (StatusCode, Json<Status>) {
Expand All @@ -35,7 +37,9 @@ pub async fn get_status() -> (StatusCode, Json<Status>) {
imprint_html: config::IMPRINT_HTML.to_string(),
theme_new_note_notice: *config::THEME_NEW_NOTE_NOTICE,
theme_home_link: *config::THEME_HOME_LINK,
theme_image: config::THEME_IMAGE.to_string(),
theme_custom_css: !config::THEME_CUSTOM_CSS_FILE.is_empty(),
theme_image: config::THEME_RESOLVED_IMAGE.get().cloned().unwrap_or_default(),
theme_svg: config::THEME_RESOLVED_SVG.get().cloned().unwrap_or_default(),
theme_text: config::THEME_TEXT.to_string(),
theme_page_title: config::THEME_PAGE_TITLE.to_string(),
theme_favicon: config::THEME_FAVICON.to_string(),
Expand Down
Empty file added packages/cli/src/shared/api.ts
Empty file.
2 changes: 2 additions & 0 deletions packages/frontend/src/lib/views/Header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
<a onclick={reset} href="/">
{#if $status === null}
<!-- waiting for status to load to avoid flashing default logo -->
{:else if $status.theme_svg}
{@html $status.theme_svg}
{:else if $status.theme_image}
<img alt="logo" src={$status.theme_image} />
{:else}
Expand Down
3 changes: 3 additions & 0 deletions packages/frontend/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
<svelte:head>
<title>{$status?.theme_page_title || 'cryptgeon'}</title>
<link rel="icon" href={$status?.theme_favicon || '/favicon.png'} />
{#if $status?.theme_custom_css}
<link rel="stylesheet" href="/custom.css" />
{/if}
</svelte:head>

{#await waitLocale() then _}
Expand Down
4 changes: 3 additions & 1 deletion packages/shared/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ export type Status = {
imprint_url: string;
imprint_html: string;
theme_image: string;
theme_svg: string
theme_text: string;
theme_page_title: string;
theme_favicon: string;
theme_new_note_notice: boolean;
theme_home_link: boolean;
};
theme_custom_css: boolean
};