Skip to content

Commit 294b429

Browse files
authored
Add dynamic CSS support (dani-garcia#4940)
* Add dynamic CSS support Together with dani-garcia/bw_web_builds#180 this PR will add support for dynamic CSS changes. For example, we could hide the register link if signups are not allowed. In the future show or hide the SSO button depending on if it is enabled or not. There also is a special `user.vaultwarden.scss` file so that users can add custom CSS without the need to modify the default (static) changes. This will prevent future changes from not being applied and still have the custom user changes to be added. Also added a special redirect when someone goes directly to `/index.html` as that might cause issues with loading other scripts and files. Signed-off-by: BlackDex <black.dex@gmail.com> * Add versions and fallback to built-in - Add both Vaultwarden and web-vault versions to the css_options. - Fallback to the inner templates if rendering or compiling the scss fails. This ensures the basics are always working even if someone breaks the templates. Signed-off-by: BlackDex <black.dex@gmail.com> * Fix fallback code to actually work The fallback now works by using an alternative `reg!` macro. This adds an extra template register which prefixes the template with `fallback_`. Signed-off-by: BlackDex <black.dex@gmail.com> * Updated the wiki link in the user template --------- Signed-off-by: BlackDex <black.dex@gmail.com>
1 parent 37c14c3 commit 294b429

File tree

6 files changed

+266
-4
lines changed

6 files changed

+266
-4
lines changed

Cargo.lock

Lines changed: 43 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ argon2 = "0.5.3"
163163
# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN
164164
rpassword = "7.3.1"
165165

166+
# Loading a dynamic CSS Stylesheet
167+
grass_compiler = { version = "0.13.4", default-features = false }
168+
166169
# Strip debuginfo from the release builds
167170
# The symbols are the provide better panic traces
168171
# Also enable fat LTO and use 1 codegen unit for optimizations

src/api/web.rs

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1+
use once_cell::sync::Lazy;
12
use std::path::{Path, PathBuf};
23

3-
use rocket::{fs::NamedFile, http::ContentType, response::content::RawHtml as Html, serde::json::Json, Catcher, Route};
4+
use rocket::{
5+
fs::NamedFile,
6+
http::ContentType,
7+
response::{content::RawCss as Css, content::RawHtml as Html, Redirect},
8+
serde::json::Json,
9+
Catcher, Route,
10+
};
411
use serde_json::Value;
512

613
use crate::{
714
api::{core::now, ApiResult, EmptyResult},
815
auth::decode_file_download,
916
error::Error,
10-
util::{Cached, SafeString},
17+
util::{get_web_vault_version, Cached, SafeString},
1118
CONFIG,
1219
};
1320

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

2229
#[cfg(debug_assertions)]
@@ -45,11 +52,101 @@ fn not_found() -> ApiResult<Html<String>> {
4552
Ok(Html(text))
4653
}
4754

55+
#[get("/css/vaultwarden.css")]
56+
fn vaultwarden_css() -> Cached<Css<String>> {
57+
// Configure the web-vault version as an integer so it can be used as a comparison smaller or greater then.
58+
// The default is based upon the version since this feature is added.
59+
static WEB_VAULT_VERSION: Lazy<u32> = Lazy::new(|| {
60+
let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap();
61+
let vault_version = get_web_vault_version();
62+
63+
let (major, minor, patch) = match re.captures(&vault_version) {
64+
Some(c) if c.len() == 4 => (
65+
c.get(1).unwrap().as_str().parse().unwrap(),
66+
c.get(2).unwrap().as_str().parse().unwrap(),
67+
c.get(3).unwrap().as_str().parse().unwrap(),
68+
),
69+
_ => (2024, 6, 2),
70+
};
71+
format!("{major}{minor:02}{patch:02}").parse::<u32>().unwrap()
72+
});
73+
74+
// Configure the Vaultwarden version as an integer so it can be used as a comparison smaller or greater then.
75+
// The default is based upon the version since this feature is added.
76+
static VW_VERSION: Lazy<u32> = Lazy::new(|| {
77+
let re = regex::Regex::new(r"(\d{1})\.(\d{1,2})\.(\d{1,2})").unwrap();
78+
let vw_version = crate::VERSION.unwrap_or("1.32.1");
79+
80+
let (major, minor, patch) = match re.captures(vw_version) {
81+
Some(c) if c.len() == 4 => (
82+
c.get(1).unwrap().as_str().parse().unwrap(),
83+
c.get(2).unwrap().as_str().parse().unwrap(),
84+
c.get(3).unwrap().as_str().parse().unwrap(),
85+
),
86+
_ => (1, 32, 1),
87+
};
88+
format!("{major}{minor:02}{patch:02}").parse::<u32>().unwrap()
89+
});
90+
91+
let css_options = json!({
92+
"web_vault_version": *WEB_VAULT_VERSION,
93+
"vw_version": *VW_VERSION,
94+
"signup_disabled": !CONFIG.signups_allowed() && CONFIG.signups_domains_whitelist().is_empty(),
95+
"mail_enabled": CONFIG.mail_enabled(),
96+
"yubico_enabled": CONFIG._enable_yubico() && (CONFIG.yubico_client_id().is_some() == CONFIG.yubico_secret_key().is_some()),
97+
"emergency_access_allowed": CONFIG.emergency_access_allowed(),
98+
"sends_allowed": CONFIG.sends_allowed(),
99+
"load_user_scss": true,
100+
});
101+
102+
let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) {
103+
Ok(t) => t,
104+
Err(e) => {
105+
// Something went wrong loading the template. Use the fallback
106+
warn!("Loading scss/vaultwarden.scss.hbs or scss/user.vaultwarden.scss.hbs failed. {e}");
107+
CONFIG
108+
.render_fallback_template("scss/vaultwarden.scss", &css_options)
109+
.expect("Fallback scss/vaultwarden.scss.hbs to render")
110+
}
111+
};
112+
113+
let css = match grass_compiler::from_string(
114+
scss,
115+
&grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed),
116+
) {
117+
Ok(css) => css,
118+
Err(e) => {
119+
// Something went wrong compiling the scss. Use the fallback
120+
warn!("Compiling the Vaultwarden SCSS styles failed. {e}");
121+
let mut css_options = css_options;
122+
css_options["load_user_scss"] = json!(false);
123+
let scss = CONFIG
124+
.render_fallback_template("scss/vaultwarden.scss", &css_options)
125+
.expect("Fallback scss/vaultwarden.scss.hbs to render");
126+
grass_compiler::from_string(
127+
scss,
128+
&grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed),
129+
)
130+
.expect("SCSS to compile")
131+
}
132+
};
133+
134+
// Cache for one day should be enough and not too much
135+
Cached::ttl(Css(css), 86_400, false)
136+
}
137+
48138
#[get("/")]
49139
async fn web_index() -> Cached<Option<NamedFile>> {
50140
Cached::short(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join("index.html")).await.ok(), false)
51141
}
52142

143+
// Make sure that `/index.html` redirect to actual domain path.
144+
// If not, this might cause issues with the web-vault
145+
#[get("/index.html")]
146+
fn web_index_direct() -> Redirect {
147+
Redirect::to(format!("{}/", CONFIG.domain_path()))
148+
}
149+
53150
#[head("/")]
54151
fn web_index_head() -> EmptyResult {
55152
// Add an explicit HEAD route to prevent uptime monitoring services from

src/config.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1269,11 +1269,16 @@ impl Config {
12691269
let hb = load_templates(CONFIG.templates_folder());
12701270
hb.render(name, data).map_err(Into::into)
12711271
} else {
1272-
let hb = &CONFIG.inner.read().unwrap().templates;
1272+
let hb = &self.inner.read().unwrap().templates;
12731273
hb.render(name, data).map_err(Into::into)
12741274
}
12751275
}
12761276

1277+
pub fn render_fallback_template<T: serde::ser::Serialize>(&self, name: &str, data: &T) -> Result<String, Error> {
1278+
let hb = &self.inner.read().unwrap().templates;
1279+
hb.render(&format!("fallback_{name}"), data).map_err(Into::into)
1280+
}
1281+
12771282
pub fn set_rocket_shutdown_handle(&self, handle: rocket::Shutdown) {
12781283
self.inner.write().unwrap().rocket_shutdown_handle = Some(handle);
12791284
}
@@ -1312,6 +1317,11 @@ where
13121317
reg!($name);
13131318
reg!(concat!($name, $ext));
13141319
}};
1320+
(@withfallback $name:expr) => {{
1321+
let template = include_str!(concat!("static/templates/", $name, ".hbs"));
1322+
hb.register_template_string($name, template).unwrap();
1323+
hb.register_template_string(concat!("fallback_", $name), template).unwrap();
1324+
}};
13151325
}
13161326

13171327
// First register default templates here
@@ -1355,6 +1365,9 @@ where
13551365

13561366
reg!("404");
13571367

1368+
reg!(@withfallback "scss/vaultwarden.scss");
1369+
reg!("scss/user.vaultwarden.scss");
1370+
13581371
// And then load user templates to overwrite the defaults
13591372
// Use .hbs extension for the files
13601373
// Templates get registered with their relative name
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/* See the wiki for examples and details: https://github.com/dani-garcia/vaultwarden/wiki/Customize-Vaultwarden-CSS */
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**** START Static Vaultwarden changes ****/
2+
/* This combines all selectors extending it into one */
3+
%vw-hide {
4+
display: none !important;
5+
}
6+
7+
/* This allows searching for the combined style in the browsers dev-tools (look into the head tag) */
8+
.vw-hide,
9+
head {
10+
@extend %vw-hide;
11+
}
12+
13+
/* Hide the Subscription Page tab */
14+
bit-nav-item[route="settings/subscription"] {
15+
@extend %vw-hide;
16+
}
17+
18+
/* Hide any link pointing to Free Bitwarden Families */
19+
a[href$="/settings/sponsored-families"] {
20+
@extend %vw-hide;
21+
}
22+
23+
/* Hide the `Enterprise Single Sign-On` button on the login page */
24+
a[routerlink="/sso"] {
25+
@extend %vw-hide;
26+
}
27+
28+
/* Hide Two-Factor menu in Organization settings */
29+
bit-nav-item[route="settings/two-factor"],
30+
a[href$="/settings/two-factor"] {
31+
@extend %vw-hide;
32+
}
33+
34+
/* Hide Business Owned checkbox */
35+
app-org-info > form:nth-child(1) > div:nth-child(3) {
36+
@extend %vw-hide;
37+
}
38+
39+
/* Hide the `This account is owned by a business` checkbox and label */
40+
#ownedBusiness,
41+
label[for^="ownedBusiness"] {
42+
@extend %vw-hide;
43+
}
44+
45+
/* Hide the radio button and label for the `Custom` org user type */
46+
#userTypeCustom,
47+
label[for^="userTypeCustom"] {
48+
@extend %vw-hide;
49+
}
50+
51+
/* Hide Business Name */
52+
app-org-account form div bit-form-field.tw-block:nth-child(3) {
53+
@extend %vw-hide;
54+
}
55+
56+
/* Hide organization plans */
57+
app-organization-plans > form > bit-section:nth-child(2) {
58+
@extend %vw-hide;
59+
}
60+
61+
/* Hide Device Verification form at the Two Step Login screen */
62+
app-security > app-two-factor-setup > form {
63+
@extend %vw-hide;
64+
}
65+
/**** END Static Vaultwarden Changes ****/
66+
/**** START Dynamic Vaultwarden Changes ****/
67+
{{#if signup_disabled}}
68+
/* Hide the register link on the login screen */
69+
app-frontend-layout > app-login > form > div > div > div > p {
70+
@extend %vw-hide;
71+
}
72+
{{/if}}
73+
74+
/* Hide `Email` 2FA if mail is not enabled */
75+
{{#unless mail_enabled}}
76+
app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(5) {
77+
@extend %vw-hide;
78+
}
79+
{{/unless}}
80+
81+
/* Hide `YubiKey OTP security key` 2FA if it is not enabled */
82+
{{#unless yubico_enabled}}
83+
app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(2) {
84+
@extend %vw-hide;
85+
}
86+
{{/unless}}
87+
88+
/* Hide Emergency Access if not allowed */
89+
{{#unless emergency_access_allowed}}
90+
bit-nav-item[route="settings/emergency-access"] {
91+
@extend %vw-hide;
92+
}
93+
{{/unless}}
94+
95+
/* Hide Sends if not allowed */
96+
{{#unless sends_allowed}}
97+
bit-nav-item[route="sends"] {
98+
@extend %vw-hide;
99+
}
100+
{{/unless}}
101+
/**** End Dynamic Vaultwarden Changes ****/
102+
/**** Include a special user stylesheet for custom changes ****/
103+
{{#if load_user_scss}}
104+
{{> scss/user.vaultwarden.scss }}
105+
{{/if}}

0 commit comments

Comments
 (0)