mirror of
				https://github.com/dani-garcia/vaultwarden.git
				synced 2025-10-31 02:08:20 +02:00 
			
		
		
		
	Implement new registration flow with email verification (#5215)
* Implement registration with required verified email * Optional name, emergency access, and signups_allowed * Implement org invite, remove unneeded invite accept * fix invitation logic for new registration flow (#5691) * fix invitation logic for new registration flow * clarify email_2fa_enforce_on_verified_invite --------- Co-authored-by: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com>
This commit is contained in:
		| @@ -70,18 +70,31 @@ pub fn routes() -> Vec<rocket::Route> { | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct RegisterData { | ||||
|     email: String, | ||||
|  | ||||
|     kdf: Option<i32>, | ||||
|     kdf_iterations: Option<i32>, | ||||
|     kdf_memory: Option<i32>, | ||||
|     kdf_parallelism: Option<i32>, | ||||
|  | ||||
|     #[serde(alias = "userSymmetricKey")] | ||||
|     key: String, | ||||
|     #[serde(alias = "userAsymmetricKeys")] | ||||
|     keys: Option<KeysData>, | ||||
|  | ||||
|     master_password_hash: String, | ||||
|     master_password_hint: Option<String>, | ||||
|  | ||||
|     name: Option<String>, | ||||
|     token: Option<String>, | ||||
|  | ||||
|     #[allow(dead_code)] | ||||
|     organization_user_id: Option<MembershipId>, | ||||
|  | ||||
|     // Used only from the register/finish endpoint | ||||
|     email_verification_token: Option<String>, | ||||
|     accept_emergency_access_id: Option<EmergencyAccessId>, | ||||
|     accept_emergency_access_invite_token: Option<String>, | ||||
|     #[serde(alias = "token")] | ||||
|     org_invite_token: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize)] | ||||
| @@ -124,13 +137,78 @@ async fn is_email_2fa_required(member_id: Option<MembershipId>, conn: &mut DbCon | ||||
|  | ||||
| #[post("/accounts/register", data = "<data>")] | ||||
| async fn register(data: Json<RegisterData>, conn: DbConn) -> JsonResult { | ||||
|     _register(data, conn).await | ||||
|     _register(data, false, conn).await | ||||
| } | ||||
|  | ||||
| pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult { | ||||
|     let data: RegisterData = data.into_inner(); | ||||
| pub async fn _register(data: Json<RegisterData>, email_verification: bool, mut conn: DbConn) -> JsonResult { | ||||
|     let mut data: RegisterData = data.into_inner(); | ||||
|     let email = data.email.to_lowercase(); | ||||
|  | ||||
|     let mut email_verified = false; | ||||
|  | ||||
|     let mut pending_emergency_access = None; | ||||
|  | ||||
|     // First, validate the provided verification tokens | ||||
|     if email_verification { | ||||
|         match ( | ||||
|             &data.email_verification_token, | ||||
|             &data.accept_emergency_access_id, | ||||
|             &data.accept_emergency_access_invite_token, | ||||
|             &data.organization_user_id, | ||||
|             &data.org_invite_token, | ||||
|         ) { | ||||
|             // Normal user registration, when email verification is required | ||||
|             (Some(email_verification_token), None, None, None, None) => { | ||||
|                 let claims = crate::auth::decode_register_verify(email_verification_token)?; | ||||
|                 if claims.sub != data.email { | ||||
|                     err!("Email verification token does not match email"); | ||||
|                 } | ||||
|  | ||||
|                 // During this call we don't get the name, so extract it from the claims | ||||
|                 if claims.name.is_some() { | ||||
|                     data.name = claims.name; | ||||
|                 } | ||||
|                 email_verified = claims.verified; | ||||
|             } | ||||
|             // Emergency access registration | ||||
|             (None, Some(accept_emergency_access_id), Some(accept_emergency_access_invite_token), None, None) => { | ||||
|                 if !CONFIG.emergency_access_allowed() { | ||||
|                     err!("Emergency access is not enabled.") | ||||
|                 } | ||||
|  | ||||
|                 let claims = crate::auth::decode_emergency_access_invite(accept_emergency_access_invite_token)?; | ||||
|  | ||||
|                 if claims.email != data.email { | ||||
|                     err!("Claim email does not match email") | ||||
|                 } | ||||
|                 if &claims.emer_id != accept_emergency_access_id { | ||||
|                     err!("Claim emer_id does not match accept_emergency_access_id") | ||||
|                 } | ||||
|  | ||||
|                 pending_emergency_access = Some((accept_emergency_access_id, claims)); | ||||
|                 email_verified = true; | ||||
|             } | ||||
|             // Org invite | ||||
|             (None, None, None, Some(organization_user_id), Some(org_invite_token)) => { | ||||
|                 let claims = decode_invite(org_invite_token)?; | ||||
|  | ||||
|                 if claims.email != data.email { | ||||
|                     err!("Claim email does not match email") | ||||
|                 } | ||||
|  | ||||
|                 if &claims.member_id != organization_user_id { | ||||
|                     err!("Claim org_user_id does not match organization_user_id") | ||||
|                 } | ||||
|  | ||||
|                 email_verified = true; | ||||
|             } | ||||
|  | ||||
|             _ => { | ||||
|                 err!("Registration is missing required parameters") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden) | ||||
|     // This also prevents issues with very long usernames causing to large JWT's. See #2419 | ||||
|     if let Some(ref name) = data.name { | ||||
| @@ -144,20 +222,17 @@ pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult | ||||
|     let password_hint = clean_password_hint(&data.master_password_hint); | ||||
|     enforce_password_hint_setting(&password_hint)?; | ||||
|  | ||||
|     let mut verified_by_invite = false; | ||||
|  | ||||
|     let mut user = match User::find_by_mail(&email, &mut conn).await { | ||||
|         Some(mut user) => { | ||||
|         Some(user) => { | ||||
|             if !user.password_hash.is_empty() { | ||||
|                 err!("Registration not allowed or user already exists") | ||||
|             } | ||||
|  | ||||
|             if let Some(token) = data.token { | ||||
|             if let Some(token) = data.org_invite_token { | ||||
|                 let claims = decode_invite(&token)?; | ||||
|                 if claims.email == email { | ||||
|                     // Verify the email address when signing up via a valid invite token | ||||
|                     verified_by_invite = true; | ||||
|                     user.verified_at = Some(Utc::now().naive_utc()); | ||||
|                     email_verified = true; | ||||
|                     user | ||||
|                 } else { | ||||
|                     err!("Registration email does not match invite email") | ||||
| @@ -181,7 +256,10 @@ pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult | ||||
|             // Order is important here; the invitation check must come first | ||||
|             // because the vaultwarden admin can invite anyone, regardless | ||||
|             // of other signup restrictions. | ||||
|             if Invitation::take(&email, &mut conn).await || CONFIG.is_signup_allowed(&email) { | ||||
|             if Invitation::take(&email, &mut conn).await | ||||
|                 || CONFIG.is_signup_allowed(&email) | ||||
|                 || pending_emergency_access.is_some() | ||||
|             { | ||||
|                 User::new(email.clone()) | ||||
|             } else { | ||||
|                 err!("Registration not allowed or user already exists") | ||||
| @@ -216,8 +294,12 @@ pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult | ||||
|         user.public_key = Some(keys.public_key); | ||||
|     } | ||||
|  | ||||
|     if email_verified { | ||||
|         user.verified_at = Some(Utc::now().naive_utc()); | ||||
|     } | ||||
|  | ||||
|     if CONFIG.mail_enabled() { | ||||
|         if CONFIG.signups_verify() && !verified_by_invite { | ||||
|         if CONFIG.signups_verify() && !email_verified { | ||||
|             if let Err(e) = mail::send_welcome_must_verify(&user.email, &user.uuid).await { | ||||
|                 error!("Error sending welcome email: {:#?}", e); | ||||
|             } | ||||
| @@ -226,7 +308,7 @@ pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult | ||||
|             error!("Error sending welcome email: {:#?}", e); | ||||
|         } | ||||
|  | ||||
|         if verified_by_invite && is_email_2fa_required(data.organization_user_id, &mut conn).await { | ||||
|         if email_verified && is_email_2fa_required(data.organization_user_id, &mut conn).await { | ||||
|             email::activate_email_2fa(&user, &mut conn).await.ok(); | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -205,6 +205,9 @@ fn config() -> Json<Value> { | ||||
|     feature_states.insert("key-rotation-improvements".to_string(), true); | ||||
|     feature_states.insert("flexible-collections-v-1".to_string(), false); | ||||
|  | ||||
|     feature_states.insert("email-verification".to_string(), true); | ||||
|     feature_states.insert("unauth-ui-refresh".to_string(), true); | ||||
|  | ||||
|     Json(json!({ | ||||
|         // Note: The clients use this version to handle backwards compatibility concerns | ||||
|         // This means they expect a version that closely matches the Bitwarden server version | ||||
|   | ||||
| @@ -24,7 +24,7 @@ use crate::{ | ||||
| }; | ||||
|  | ||||
| pub fn routes() -> Vec<Route> { | ||||
|     routes![login, prelogin, identity_register] | ||||
|     routes![login, prelogin, identity_register, register_verification_email, register_finish] | ||||
| } | ||||
|  | ||||
| #[post("/connect/token", data = "<data>")] | ||||
| @@ -714,7 +714,68 @@ async fn prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> { | ||||
|  | ||||
| #[post("/accounts/register", data = "<data>")] | ||||
| async fn identity_register(data: Json<RegisterData>, conn: DbConn) -> JsonResult { | ||||
|     _register(data, conn).await | ||||
|     _register(data, false, conn).await | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| struct RegisterVerificationData { | ||||
|     email: String, | ||||
|     name: Option<String>, | ||||
|     // receiveMarketingEmails: bool, | ||||
| } | ||||
|  | ||||
| #[derive(rocket::Responder)] | ||||
| enum RegisterVerificationResponse { | ||||
|     NoContent(()), | ||||
|     Token(Json<String>), | ||||
| } | ||||
|  | ||||
| #[post("/accounts/register/send-verification-email", data = "<data>")] | ||||
| async fn register_verification_email( | ||||
|     data: Json<RegisterVerificationData>, | ||||
|     mut conn: DbConn, | ||||
| ) -> ApiResult<RegisterVerificationResponse> { | ||||
|     let data = data.into_inner(); | ||||
|  | ||||
|     if !CONFIG.is_signup_allowed(&data.email) { | ||||
|         err!("Registration not allowed or user already exists") | ||||
|     } | ||||
|  | ||||
|     let should_send_mail = CONFIG.mail_enabled() && CONFIG.signups_verify(); | ||||
|  | ||||
|     if User::find_by_mail(&data.email, &mut conn).await.is_some() { | ||||
|         if should_send_mail { | ||||
|             // There is still a timing side channel here in that the code | ||||
|             // paths that send mail take noticeably longer than ones that | ||||
|             // don't. Add a randomized sleep to mitigate this somewhat. | ||||
|             use rand::{rngs::SmallRng, Rng, SeedableRng}; | ||||
|             let mut rng = SmallRng::from_os_rng(); | ||||
|             let delta: i32 = 100; | ||||
|             let sleep_ms = (1_000 + rng.random_range(-delta..=delta)) as u64; | ||||
|             tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await; | ||||
|         } | ||||
|         return Ok(RegisterVerificationResponse::NoContent(())); | ||||
|     } | ||||
|  | ||||
|     let token_claims = | ||||
|         crate::auth::generate_register_verify_claims(data.email.clone(), data.name.clone(), should_send_mail); | ||||
|     let token = crate::auth::encode_jwt(&token_claims); | ||||
|  | ||||
|     if should_send_mail { | ||||
|         mail::send_register_verify_email(&data.email, &token).await?; | ||||
|  | ||||
|         Ok(RegisterVerificationResponse::NoContent(())) | ||||
|     } else { | ||||
|         // If email verification is not required, return the token directly | ||||
|         // the clients will use this token to finish the registration | ||||
|         Ok(RegisterVerificationResponse::Token(Json(token))) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[post("/accounts/register/finish", data = "<data>")] | ||||
| async fn register_finish(data: Json<RegisterData>, conn: DbConn) -> JsonResult { | ||||
|     _register(data, true, conn).await | ||||
| } | ||||
|  | ||||
| // https://github.com/bitwarden/jslib/blob/master/common/src/models/request/tokenRequest.ts | ||||
|   | ||||
							
								
								
									
										32
									
								
								src/auth.rs
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								src/auth.rs
									
									
									
									
									
								
							| @@ -35,6 +35,7 @@ static JWT_ADMIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|admin", CONFIG. | ||||
| static JWT_SEND_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|send", CONFIG.domain_origin())); | ||||
| static JWT_ORG_API_KEY_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|api.organization", CONFIG.domain_origin())); | ||||
| static JWT_FILE_DOWNLOAD_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|file_download", CONFIG.domain_origin())); | ||||
| static JWT_REGISTER_VERIFY_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|register_verify", CONFIG.domain_origin())); | ||||
|  | ||||
| static PRIVATE_RSA_KEY: OnceCell<EncodingKey> = OnceCell::new(); | ||||
| static PUBLIC_RSA_KEY: OnceCell<DecodingKey> = OnceCell::new(); | ||||
| @@ -145,6 +146,10 @@ pub fn decode_file_download(token: &str) -> Result<FileDownloadClaims, Error> { | ||||
|     decode_jwt(token, JWT_FILE_DOWNLOAD_ISSUER.to_string()) | ||||
| } | ||||
|  | ||||
| pub fn decode_register_verify(token: &str) -> Result<RegisterVerifyClaims, Error> { | ||||
|     decode_jwt(token, JWT_REGISTER_VERIFY_ISSUER.to_string()) | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct LoginJwtClaims { | ||||
|     // Not before | ||||
| @@ -315,6 +320,33 @@ pub fn generate_file_download_claims(cipher_id: CipherId, file_id: AttachmentId) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct RegisterVerifyClaims { | ||||
|     // Not before | ||||
|     pub nbf: i64, | ||||
|     // Expiration time | ||||
|     pub exp: i64, | ||||
|     // Issuer | ||||
|     pub iss: String, | ||||
|     // Subject | ||||
|     pub sub: String, | ||||
|  | ||||
|     pub name: Option<String>, | ||||
|     pub verified: bool, | ||||
| } | ||||
|  | ||||
| pub fn generate_register_verify_claims(email: String, name: Option<String>, verified: bool) -> RegisterVerifyClaims { | ||||
|     let time_now = Utc::now(); | ||||
|     RegisterVerifyClaims { | ||||
|         nbf: time_now.timestamp(), | ||||
|         exp: (time_now + TimeDelta::try_minutes(30).unwrap()).timestamp(), | ||||
|         iss: JWT_REGISTER_VERIFY_ISSUER.to_string(), | ||||
|         sub: email, | ||||
|         name, | ||||
|         verified, | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct BasicJwtClaims { | ||||
|     // Not before | ||||
|   | ||||
| @@ -484,7 +484,8 @@ make_config! { | ||||
|         disable_icon_download:  bool,   true,   def,    false; | ||||
|         /// Allow new signups |> Controls whether new users can register. Users can be invited by the vaultwarden admin even if this is disabled | ||||
|         signups_allowed:        bool,   true,   def,    true; | ||||
|         /// Require email verification on signups. This will prevent logins from succeeding until the address has been verified | ||||
|         /// Require email verification on signups. On new client versions, this will require verification at signup time. On older clients, | ||||
|         /// this will prevent logins from succeeding until the address has been verified | ||||
|         signups_verify:         bool,   true,   def,    false; | ||||
|         /// If signups require email verification, automatically re-send verification email if it hasn't been sent for a while (in seconds) | ||||
|         signups_verify_resend_time: u64, true,  def,    3_600; | ||||
| @@ -734,7 +735,7 @@ make_config! { | ||||
|         email_expiration_time:  u64,    true,   def,      600; | ||||
|         /// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent | ||||
|         email_attempts_limit:   u64,    true,   def,      3; | ||||
|         /// Automatically enforce at login |> Setup email 2FA provider regardless of any organization policy | ||||
|         /// Setup email 2FA at signup |> Setup email 2FA provider on registration regardless of any organization policy | ||||
|         email_2fa_enforce_on_verified_invite: bool,   true,   def,      false; | ||||
|         /// Auto-enable 2FA (Know the risks!) |> Automatically setup email 2FA as fallback provider when needed | ||||
|         email_2fa_auto_fallback: bool,  true,   def,      false; | ||||
| @@ -1386,6 +1387,7 @@ where | ||||
|     reg!("email/protected_action", ".html"); | ||||
|     reg!("email/pw_hint_none", ".html"); | ||||
|     reg!("email/pw_hint_some", ".html"); | ||||
|     reg!("email/register_verify_email", ".html"); | ||||
|     reg!("email/send_2fa_removed_from_org", ".html"); | ||||
|     reg!("email/send_emergency_access_invite", ".html"); | ||||
|     reg!("email/send_org_invite", ".html"); | ||||
|   | ||||
							
								
								
									
										21
									
								
								src/mail.rs
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								src/mail.rs
									
									
									
									
									
								
							| @@ -201,6 +201,27 @@ pub async fn send_verify_email(address: &str, user_id: &UserId) -> EmptyResult { | ||||
|     send_email(address, &subject, body_html, body_text).await | ||||
| } | ||||
|  | ||||
| pub async fn send_register_verify_email(email: &str, token: &str) -> EmptyResult { | ||||
|     let mut query = url::Url::parse("https://query.builder").unwrap(); | ||||
|     query.query_pairs_mut().append_pair("email", email).append_pair("token", token); | ||||
|     let query_string = match query.query() { | ||||
|         None => err!("Failed to build verify URL query parameters"), | ||||
|         Some(query) => query, | ||||
|     }; | ||||
|  | ||||
|     let (subject, body_html, body_text) = get_text( | ||||
|         "email/register_verify_email", | ||||
|         json!({ | ||||
|             // `url.Url` would place the anchor `#` after the query parameters | ||||
|             "url": format!("{}/#/finish-signup/?{}", CONFIG.domain(), query_string), | ||||
|             "img_src": CONFIG._smtp_img_src(), | ||||
|             "email": email, | ||||
|         }), | ||||
|     )?; | ||||
|  | ||||
|     send_email(email, &subject, body_html, body_text).await | ||||
| } | ||||
|  | ||||
| pub async fn send_welcome(address: &str) -> EmptyResult { | ||||
|     let (subject, body_html, body_text) = get_text( | ||||
|         "email/welcome", | ||||
|   | ||||
							
								
								
									
										8
									
								
								src/static/templates/email/register_verify_email.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/static/templates/email/register_verify_email.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| Verify Your Email | ||||
| <!----------------> | ||||
| Verify this email address to finish creating your account by clicking the link below. | ||||
|  | ||||
| Verify Email Address Now: {{{url}}} | ||||
|  | ||||
| If you did not request to verify your account, you can safely ignore this email. | ||||
| {{> email/email_footer_text }} | ||||
							
								
								
									
										24
									
								
								src/static/templates/email/register_verify_email.html.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/static/templates/email/register_verify_email.html.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| Verify Your Email | ||||
| <!----------------> | ||||
| {{> email/email_header }} | ||||
| <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | ||||
|    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | ||||
|       <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center"> | ||||
|          Verify this email address to finish creating your account by clicking the link below. | ||||
|       </td> | ||||
|    </tr> | ||||
|    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | ||||
|       <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center"> | ||||
|          <a href="{{{url}}}" | ||||
|             clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | ||||
|          Verify Email Address Now | ||||
|          </a> | ||||
|       </td> | ||||
|    </tr> | ||||
|    <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | ||||
|       <td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center"> | ||||
|          If you did not request to verify your account, you can safely ignore this email. | ||||
|       </td> | ||||
|    </tr> | ||||
| </table> | ||||
| {{> email/email_footer }} | ||||
| @@ -93,12 +93,19 @@ bit-nav-logo bit-nav-item .bwi-shield { | ||||
| /**** END Static Vaultwarden Changes ****/ | ||||
| /**** START Dynamic Vaultwarden Changes ****/ | ||||
| {{#if signup_disabled}} | ||||
| /* From web vault 2025.1.2 and onwards, the signup button is hidden  | ||||
|   when signups are disabled as the web vault checks the /api/config endpoint. | ||||
|   Note that the clients tend to aggressively cache this endpoint, so it might | ||||
|   take a while for the change to take effect. To avoid the button appearing  | ||||
|   when it shouldn't, we'll keep this style in place for a couple of versions */ | ||||
| {{#if webver "<2025.3.0"}} | ||||
| /* Hide the register link on the login screen */ | ||||
| app-login form div + div + div + div + hr, | ||||
| app-login form div + div + div + div + hr + p { | ||||
|   @extend %vw-hide; | ||||
| } | ||||
| {{/if}} | ||||
| {{/if}} | ||||
|  | ||||
| {{#unless mail_enabled}} | ||||
| /* Hide `Email` 2FA if mail is not enabled */ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user