Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dynamic CSS support #4940

Merged
merged 4 commits into from
Nov 11, 2024
Merged
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
43 changes: 43 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ argon2 = "0.5.3"
# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN
rpassword = "7.3.1"

# Loading a dynamic CSS Stylesheet
grass_compiler = { version = "0.13.4", default-features = false }

# Strip debuginfo from the release builds
# The symbols are the provide better panic traces
# Also enable fat LTO and use 1 codegen unit for optimizations
Expand Down
103 changes: 100 additions & 3 deletions src/api/web.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
use once_cell::sync::Lazy;
use std::path::{Path, PathBuf};

use rocket::{fs::NamedFile, http::ContentType, response::content::RawHtml as Html, serde::json::Json, Catcher, Route};
use rocket::{
fs::NamedFile,
http::ContentType,
response::{content::RawCss as Css, content::RawHtml as Html, Redirect},
serde::json::Json,
Catcher, Route,
};
use serde_json::Value;

use crate::{
api::{core::now, ApiResult, EmptyResult},
auth::decode_file_download,
error::Error,
util::{Cached, SafeString},
util::{get_web_vault_version, Cached, SafeString},
CONFIG,
};

Expand All @@ -16,7 +23,7 @@ pub fn routes() -> Vec<Route> {
// crate::utils::LOGGED_ROUTES to make sure they appear in the log
let mut routes = routes![attachments, alive, alive_head, static_files];
if CONFIG.web_vault_enabled() {
routes.append(&mut routes![web_index, web_index_head, app_id, web_files]);
routes.append(&mut routes![web_index, web_index_direct, web_index_head, app_id, web_files, vaultwarden_css]);
}

#[cfg(debug_assertions)]
Expand Down Expand Up @@ -45,11 +52,101 @@ fn not_found() -> ApiResult<Html<String>> {
Ok(Html(text))
}

#[get("/css/vaultwarden.css")]
fn vaultwarden_css() -> Cached<Css<String>> {
// Configure the web-vault version as an integer so it can be used as a comparison smaller or greater then.
// The default is based upon the version since this feature is added.
static WEB_VAULT_VERSION: Lazy<u32> = Lazy::new(|| {
let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap();
let vault_version = get_web_vault_version();

let (major, minor, patch) = match re.captures(&vault_version) {
Some(c) if c.len() == 4 => (
c.get(1).unwrap().as_str().parse().unwrap(),
c.get(2).unwrap().as_str().parse().unwrap(),
c.get(3).unwrap().as_str().parse().unwrap(),
),
_ => (2024, 6, 2),
};
format!("{major}{minor:02}{patch:02}").parse::<u32>().unwrap()
});

// Configure the Vaultwarden version as an integer so it can be used as a comparison smaller or greater then.
// The default is based upon the version since this feature is added.
static VW_VERSION: Lazy<u32> = Lazy::new(|| {
let re = regex::Regex::new(r"(\d{1})\.(\d{1,2})\.(\d{1,2})").unwrap();
let vw_version = crate::VERSION.unwrap_or("1.32.1");

let (major, minor, patch) = match re.captures(vw_version) {
Some(c) if c.len() == 4 => (
c.get(1).unwrap().as_str().parse().unwrap(),
c.get(2).unwrap().as_str().parse().unwrap(),
c.get(3).unwrap().as_str().parse().unwrap(),
),
_ => (1, 32, 1),
};
format!("{major}{minor:02}{patch:02}").parse::<u32>().unwrap()
});

let css_options = json!({
"web_vault_version": *WEB_VAULT_VERSION,
"vw_version": *VW_VERSION,
"signup_disabled": !CONFIG.signups_allowed() && CONFIG.signups_domains_whitelist().is_empty(),
"mail_enabled": CONFIG.mail_enabled(),
"yubico_enabled": CONFIG._enable_yubico() && (CONFIG.yubico_client_id().is_some() == CONFIG.yubico_secret_key().is_some()),
"emergency_access_allowed": CONFIG.emergency_access_allowed(),
"sends_allowed": CONFIG.sends_allowed(),
"load_user_scss": true,
});

let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) {
BlackDex marked this conversation as resolved.
Show resolved Hide resolved
Ok(t) => t,
Err(e) => {
// Something went wrong loading the template. Use the fallback
warn!("Loading scss/vaultwarden.scss.hbs or scss/user.vaultwarden.scss.hbs failed. {e}");
BlackDex marked this conversation as resolved.
Show resolved Hide resolved
CONFIG
.render_fallback_template("scss/vaultwarden.scss", &css_options)
.expect("Fallback scss/vaultwarden.scss.hbs to render")
}
};

let css = match grass_compiler::from_string(
scss,
&grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed),
) {
Ok(css) => css,
Err(e) => {
// Something went wrong compiling the scss. Use the fallback
warn!("Compiling the Vaultwarden SCSS styles failed. {e}");
BlackDex marked this conversation as resolved.
Show resolved Hide resolved
let mut css_options = css_options;
css_options["load_user_scss"] = json!(false);
let scss = CONFIG
.render_fallback_template("scss/vaultwarden.scss", &css_options)
.expect("Fallback scss/vaultwarden.scss.hbs to render");
grass_compiler::from_string(
scss,
&grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed),
)
.expect("SCSS to compile")
}
};

// Cache for one day should be enough and not too much
Cached::ttl(Css(css), 86_400, false)
}

#[get("/")]
async fn web_index() -> Cached<Option<NamedFile>> {
Cached::short(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join("index.html")).await.ok(), false)
}

// Make sure that `/index.html` redirect to actual domain path.
// If not, this might cause issues with the web-vault
#[get("/index.html")]
fn web_index_direct() -> Redirect {
Redirect::to(format!("{}/", CONFIG.domain_path()))
}

#[head("/")]
fn web_index_head() -> EmptyResult {
// Add an explicit HEAD route to prevent uptime monitoring services from
Expand Down
15 changes: 14 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1269,11 +1269,16 @@ impl Config {
let hb = load_templates(CONFIG.templates_folder());
hb.render(name, data).map_err(Into::into)
} else {
let hb = &CONFIG.inner.read().unwrap().templates;
let hb = &self.inner.read().unwrap().templates;
hb.render(name, data).map_err(Into::into)
}
}

pub fn render_fallback_template<T: serde::ser::Serialize>(&self, name: &str, data: &T) -> Result<String, Error> {
let hb = &self.inner.read().unwrap().templates;
hb.render(&format!("fallback_{name}"), data).map_err(Into::into)
}

pub fn set_rocket_shutdown_handle(&self, handle: rocket::Shutdown) {
self.inner.write().unwrap().rocket_shutdown_handle = Some(handle);
}
Expand Down Expand Up @@ -1312,6 +1317,11 @@ where
reg!($name);
reg!(concat!($name, $ext));
}};
(@withfallback $name:expr) => {{
let template = include_str!(concat!("static/templates/", $name, ".hbs"));
hb.register_template_string($name, template).unwrap();
hb.register_template_string(concat!("fallback_", $name), template).unwrap();
}};
}

// First register default templates here
Expand Down Expand Up @@ -1355,6 +1365,9 @@ where

reg!("404");

reg!(@withfallback "scss/vaultwarden.scss");
reg!("scss/user.vaultwarden.scss");

// And then load user templates to overwrite the defaults
// Use .hbs extension for the files
// Templates get registered with their relative name
Expand Down
1 change: 1 addition & 0 deletions src/static/templates/scss/user.vaultwarden.scss.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/* See the wiki for examples and details: https://github.com/dani-garcia/vaultwarden/wiki/Customize-Vaultwarden-CSS */
105 changes: 105 additions & 0 deletions src/static/templates/scss/vaultwarden.scss.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**** START Static Vaultwarden changes ****/
/* This combines all selectors extending it into one */
%vw-hide {
display: none !important;
}

/* This allows searching for the combined style in the browsers dev-tools (look into the head tag) */
.vw-hide,
head {
@extend %vw-hide;
}

/* Hide the Subscription Page tab */
bit-nav-item[route="settings/subscription"] {
@extend %vw-hide;
}

/* Hide any link pointing to Free Bitwarden Families */
a[href$="/settings/sponsored-families"] {
@extend %vw-hide;
}

/* Hide the `Enterprise Single Sign-On` button on the login page */
a[routerlink="/sso"] {
@extend %vw-hide;
}

/* Hide Two-Factor menu in Organization settings */
bit-nav-item[route="settings/two-factor"],
a[href$="/settings/two-factor"] {
@extend %vw-hide;
}

/* Hide Business Owned checkbox */
app-org-info > form:nth-child(1) > div:nth-child(3) {
@extend %vw-hide;
}

/* Hide the `This account is owned by a business` checkbox and label */
#ownedBusiness,
label[for^="ownedBusiness"] {
@extend %vw-hide;
}

/* Hide the radio button and label for the `Custom` org user type */
#userTypeCustom,
label[for^="userTypeCustom"] {
@extend %vw-hide;
}

/* Hide Business Name */
app-org-account form div bit-form-field.tw-block:nth-child(3) {
@extend %vw-hide;
}

/* Hide organization plans */
app-organization-plans > form > bit-section:nth-child(2) {
@extend %vw-hide;
}

/* Hide Device Verification form at the Two Step Login screen */
app-security > app-two-factor-setup > form {
@extend %vw-hide;
}
/**** END Static Vaultwarden Changes ****/
/**** START Dynamic Vaultwarden Changes ****/
{{#if signup_disabled}}
/* Hide the register link on the login screen */
app-frontend-layout > app-login > form > div > div > div > p {
@extend %vw-hide;
}
{{/if}}

/* Hide `Email` 2FA if mail is not enabled */
{{#unless mail_enabled}}
app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(5) {
@extend %vw-hide;
}
{{/unless}}

/* Hide `YubiKey OTP security key` 2FA if it is not enabled */
{{#unless yubico_enabled}}
app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(2) {
@extend %vw-hide;
}
{{/unless}}

/* Hide Emergency Access if not allowed */
{{#unless emergency_access_allowed}}
bit-nav-item[route="settings/emergency-access"] {
@extend %vw-hide;
}
{{/unless}}

/* Hide Sends if not allowed */
{{#unless sends_allowed}}
bit-nav-item[route="sends"] {
@extend %vw-hide;
}
{{/unless}}
/**** End Dynamic Vaultwarden Changes ****/
/**** Include a special user stylesheet for custom changes ****/
{{#if load_user_scss}}
{{> scss/user.vaultwarden.scss }}
{{/if}}
Loading