mirror of
				https://github.com/dani-garcia/vaultwarden.git
				synced 2025-10-31 10:18:19 +02:00 
			
		
		
		
	Update admin interface (#4737)
- Updated datatables - Set Cookie Secure flag if the connection is https - Prevent possible XSS via Organization Name Converted all `innerHTML` and `innerText` to the Safe Sink version `textContent` - Removed `jsesc` function as handlebars escapes all these chars already and more by default
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							035f694d2f
						
					
				
				
					commit
					54bfcb8bc3
				
			| @@ -18,7 +18,7 @@ use crate::{ | |||||||
|         core::{log_event, two_factor}, |         core::{log_event, two_factor}, | ||||||
|         unregister_push_device, ApiResult, EmptyResult, JsonResult, Notify, |         unregister_push_device, ApiResult, EmptyResult, JsonResult, Notify, | ||||||
|     }, |     }, | ||||||
|     auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp}, |     auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp, Secure}, | ||||||
|     config::ConfigBuilder, |     config::ConfigBuilder, | ||||||
|     db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType}, |     db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType}, | ||||||
|     error::{Error, MapResult}, |     error::{Error, MapResult}, | ||||||
| @@ -169,7 +169,12 @@ struct LoginForm { | |||||||
| } | } | ||||||
|  |  | ||||||
| #[post("/", data = "<data>")] | #[post("/", data = "<data>")] | ||||||
| fn post_admin_login(data: Form<LoginForm>, cookies: &CookieJar<'_>, ip: ClientIp) -> Result<Redirect, AdminResponse> { | fn post_admin_login( | ||||||
|  |     data: Form<LoginForm>, | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     ip: ClientIp, | ||||||
|  |     secure: Secure, | ||||||
|  | ) -> Result<Redirect, AdminResponse> { | ||||||
|     let data = data.into_inner(); |     let data = data.into_inner(); | ||||||
|     let redirect = data.redirect; |     let redirect = data.redirect; | ||||||
|  |  | ||||||
| @@ -193,7 +198,8 @@ fn post_admin_login(data: Form<LoginForm>, cookies: &CookieJar<'_>, ip: ClientIp | |||||||
|             .path(admin_path()) |             .path(admin_path()) | ||||||
|             .max_age(rocket::time::Duration::minutes(CONFIG.admin_session_lifetime())) |             .max_age(rocket::time::Duration::minutes(CONFIG.admin_session_lifetime())) | ||||||
|             .same_site(SameSite::Strict) |             .same_site(SameSite::Strict) | ||||||
|             .http_only(true); |             .http_only(true) | ||||||
|  |             .secure(secure.https); | ||||||
|  |  | ||||||
|         cookies.add(cookie); |         cookies.add(cookie); | ||||||
|         if let Some(redirect) = redirect { |         if let Some(redirect) = redirect { | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								src/auth.rs
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								src/auth.rs
									
									
									
									
									
								
							| @@ -379,8 +379,6 @@ impl<'r> FromRequest<'r> for Host { | |||||||
|             referer.to_string() |             referer.to_string() | ||||||
|         } else { |         } else { | ||||||
|             // Try to guess from the headers |             // Try to guess from the headers | ||||||
|             use std::env; |  | ||||||
|  |  | ||||||
|             let protocol = if let Some(proto) = headers.get_one("X-Forwarded-Proto") { |             let protocol = if let Some(proto) = headers.get_one("X-Forwarded-Proto") { | ||||||
|                 proto |                 proto | ||||||
|             } else if env::var("ROCKET_TLS").is_ok() { |             } else if env::var("ROCKET_TLS").is_ok() { | ||||||
| @@ -806,6 +804,7 @@ impl<'r> FromRequest<'r> for OwnerHeaders { | |||||||
| // Client IP address detection | // Client IP address detection | ||||||
| // | // | ||||||
| use std::{ | use std::{ | ||||||
|  |     env, | ||||||
|     fs::File, |     fs::File, | ||||||
|     io::{Read, Write}, |     io::{Read, Write}, | ||||||
|     net::IpAddr, |     net::IpAddr, | ||||||
| @@ -842,6 +841,35 @@ impl<'r> FromRequest<'r> for ClientIp { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | pub struct Secure { | ||||||
|  |     pub https: bool, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[rocket::async_trait] | ||||||
|  | impl<'r> FromRequest<'r> for Secure { | ||||||
|  |     type Error = (); | ||||||
|  |  | ||||||
|  |     async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> { | ||||||
|  |         let headers = request.headers(); | ||||||
|  |  | ||||||
|  |         // Try to guess from the headers | ||||||
|  |         let protocol = match headers.get_one("X-Forwarded-Proto") { | ||||||
|  |             Some(proto) => proto, | ||||||
|  |             None => { | ||||||
|  |                 if env::var("ROCKET_TLS").is_ok() { | ||||||
|  |                     "https" | ||||||
|  |                 } else { | ||||||
|  |                     "http" | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         Outcome::Success(Secure { | ||||||
|  |             https: protocol == "https", | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| pub struct WsAccessTokenHeader { | pub struct WsAccessTokenHeader { | ||||||
|     pub access_token: Option<String>, |     pub access_token: Option<String>, | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1277,7 +1277,6 @@ where | |||||||
|     hb.set_strict_mode(true); |     hb.set_strict_mode(true); | ||||||
|     // Register helpers |     // Register helpers | ||||||
|     hb.register_helper("case", Box::new(case_helper)); |     hb.register_helper("case", Box::new(case_helper)); | ||||||
|     hb.register_helper("jsesc", Box::new(js_escape_helper)); |  | ||||||
|     hb.register_helper("to_json", Box::new(to_json)); |     hb.register_helper("to_json", Box::new(to_json)); | ||||||
|  |  | ||||||
|     macro_rules! reg { |     macro_rules! reg { | ||||||
| @@ -1365,32 +1364,6 @@ fn case_helper<'reg, 'rc>( | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| fn js_escape_helper<'reg, 'rc>( |  | ||||||
|     h: &Helper<'rc>, |  | ||||||
|     _r: &'reg Handlebars<'_>, |  | ||||||
|     _ctx: &'rc Context, |  | ||||||
|     _rc: &mut RenderContext<'reg, 'rc>, |  | ||||||
|     out: &mut dyn Output, |  | ||||||
| ) -> HelperResult { |  | ||||||
|     let param = |  | ||||||
|         h.param(0).ok_or_else(|| RenderErrorReason::Other(String::from("Param not found for helper \"jsesc\"")))?; |  | ||||||
|  |  | ||||||
|     let no_quote = h.param(1).is_some(); |  | ||||||
|  |  | ||||||
|     let value = param |  | ||||||
|         .value() |  | ||||||
|         .as_str() |  | ||||||
|         .ok_or_else(|| RenderErrorReason::Other(String::from("Param for helper \"jsesc\" is not a String")))?; |  | ||||||
|  |  | ||||||
|     let mut escaped_value = value.replace('\\', "").replace('\'', "\\x22").replace('\"', "\\x27"); |  | ||||||
|     if !no_quote { |  | ||||||
|         escaped_value = format!(""{escaped_value}""); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     out.write(&escaped_value)?; |  | ||||||
|     Ok(()) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fn to_json<'reg, 'rc>( | fn to_json<'reg, 'rc>( | ||||||
|     h: &Helper<'rc>, |     h: &Helper<'rc>, | ||||||
|     _r: &'reg Handlebars<'_>, |     _r: &'reg Handlebars<'_>, | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								src/static/scripts/admin.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								src/static/scripts/admin.js
									
									
									
									
										vendored
									
									
								
							| @@ -98,7 +98,7 @@ const showActiveTheme = (theme, focus = false) => { | |||||||
|     const themeSwitcherText = document.querySelector("#bd-theme-text"); |     const themeSwitcherText = document.querySelector("#bd-theme-text"); | ||||||
|     const activeThemeIcon = document.querySelector(".theme-icon-active use"); |     const activeThemeIcon = document.querySelector(".theme-icon-active use"); | ||||||
|     const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`); |     const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`); | ||||||
|     const svgOfActiveBtn = btnToActive.querySelector("span use").innerText; |     const svgOfActiveBtn = btnToActive.querySelector("span use").textContent; | ||||||
|  |  | ||||||
|     document.querySelectorAll("[data-bs-theme-value]").forEach(element => { |     document.querySelectorAll("[data-bs-theme-value]").forEach(element => { | ||||||
|         element.classList.remove("active"); |         element.classList.remove("active"); | ||||||
| @@ -107,7 +107,7 @@ const showActiveTheme = (theme, focus = false) => { | |||||||
|  |  | ||||||
|     btnToActive.classList.add("active"); |     btnToActive.classList.add("active"); | ||||||
|     btnToActive.setAttribute("aria-pressed", "true"); |     btnToActive.setAttribute("aria-pressed", "true"); | ||||||
|     activeThemeIcon.innerText = svgOfActiveBtn; |     activeThemeIcon.textContent = svgOfActiveBtn; | ||||||
|     const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`; |     const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`; | ||||||
|     themeSwitcher.setAttribute("aria-label", themeSwitcherLabel); |     themeSwitcher.setAttribute("aria-label", themeSwitcherLabel); | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								src/static/scripts/admin_diagnostics.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								src/static/scripts/admin_diagnostics.js
									
									
									
									
										vendored
									
									
								
							| @@ -117,7 +117,7 @@ async function generateSupportString(event, dj) { | |||||||
|     supportString += `\n**Environment settings which are overridden:** ${dj.overrides}\n`; |     supportString += `\n**Environment settings which are overridden:** ${dj.overrides}\n`; | ||||||
|     supportString += "\n\n```json\n" + JSON.stringify(configJson, undefined, 2) + "\n```\n</details>\n"; |     supportString += "\n\n```json\n" + JSON.stringify(configJson, undefined, 2) + "\n```\n</details>\n"; | ||||||
|  |  | ||||||
|     document.getElementById("support-string").innerText = supportString; |     document.getElementById("support-string").textContent = supportString; | ||||||
|     document.getElementById("support-string").classList.remove("d-none"); |     document.getElementById("support-string").classList.remove("d-none"); | ||||||
|     document.getElementById("copy-support").classList.remove("d-none"); |     document.getElementById("copy-support").classList.remove("d-none"); | ||||||
| } | } | ||||||
| @@ -126,7 +126,7 @@ function copyToClipboard(event) { | |||||||
|     event.preventDefault(); |     event.preventDefault(); | ||||||
|     event.stopPropagation(); |     event.stopPropagation(); | ||||||
|  |  | ||||||
|     const supportStr = document.getElementById("support-string").innerText; |     const supportStr = document.getElementById("support-string").textContent; | ||||||
|     const tmpCopyEl = document.createElement("textarea"); |     const tmpCopyEl = document.createElement("textarea"); | ||||||
|  |  | ||||||
|     tmpCopyEl.setAttribute("id", "copy-support-string"); |     tmpCopyEl.setAttribute("id", "copy-support-string"); | ||||||
| @@ -201,7 +201,7 @@ function checkDns(dns_resolved) { | |||||||
|  |  | ||||||
| function init(dj) { | function init(dj) { | ||||||
|     // Time check |     // Time check | ||||||
|     document.getElementById("time-browser-string").innerText = browserUTC; |     document.getElementById("time-browser-string").textContent = browserUTC; | ||||||
|  |  | ||||||
|     // Check if we were able to fetch a valid NTP Time |     // Check if we were able to fetch a valid NTP Time | ||||||
|     // If so, compare both browser and server with NTP |     // If so, compare both browser and server with NTP | ||||||
| @@ -217,7 +217,7 @@ function init(dj) { | |||||||
|  |  | ||||||
|     // Domain check |     // Domain check | ||||||
|     const browserURL = location.href.toLowerCase(); |     const browserURL = location.href.toLowerCase(); | ||||||
|     document.getElementById("domain-browser-string").innerText = browserURL; |     document.getElementById("domain-browser-string").textContent = browserURL; | ||||||
|     checkDomain(browserURL, dj.admin_url.toLowerCase()); |     checkDomain(browserURL, dj.admin_url.toLowerCase()); | ||||||
|  |  | ||||||
|     // Version check |     // Version check | ||||||
| @@ -229,7 +229,7 @@ function init(dj) { | |||||||
|  |  | ||||||
| // onLoad events | // onLoad events | ||||||
| document.addEventListener("DOMContentLoaded", (event) => { | document.addEventListener("DOMContentLoaded", (event) => { | ||||||
|     const diag_json = JSON.parse(document.getElementById("diagnostics_json").innerText); |     const diag_json = JSON.parse(document.getElementById("diagnostics_json").textContent); | ||||||
|     init(diag_json); |     init(diag_json); | ||||||
|  |  | ||||||
|     const btnGenSupport = document.getElementById("gen-support"); |     const btnGenSupport = document.getElementById("gen-support"); | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								src/static/scripts/admin_settings.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/static/scripts/admin_settings.js
									
									
									
									
										vendored
									
									
								
							| @@ -122,7 +122,7 @@ function submitTestEmailOnEnter() { | |||||||
| function colorRiskSettings() { | function colorRiskSettings() { | ||||||
|     const risk_items = document.getElementsByClassName("col-form-label"); |     const risk_items = document.getElementsByClassName("col-form-label"); | ||||||
|     Array.from(risk_items).forEach((el) => { |     Array.from(risk_items).forEach((el) => { | ||||||
|         if (el.innerText.toLowerCase().includes("risks") ) { |         if (el.textContent.toLowerCase().includes("risks") ) { | ||||||
|             el.parentElement.className += " alert-danger"; |             el.parentElement.className += " alert-danger"; | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								src/static/scripts/admin_users.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								src/static/scripts/admin_users.js
									
									
									
									
										vendored
									
									
								
							| @@ -198,7 +198,8 @@ userOrgTypeDialog.addEventListener("show.bs.modal", function(event) { | |||||||
|     const orgName = event.relatedTarget.dataset.vwOrgName; |     const orgName = event.relatedTarget.dataset.vwOrgName; | ||||||
|     const orgUuid = event.relatedTarget.dataset.vwOrgUuid; |     const orgUuid = event.relatedTarget.dataset.vwOrgUuid; | ||||||
|  |  | ||||||
|     document.getElementById("userOrgTypeDialogTitle").innerHTML = `<b>Update User Type:</b><br><b>Organization:</b> ${orgName}<br><b>User:</b> ${userEmail}`; |     document.getElementById("userOrgTypeDialogOrgName").textContent = orgName; | ||||||
|  |     document.getElementById("userOrgTypeDialogUserEmail").textContent = userEmail; | ||||||
|     document.getElementById("userOrgTypeUserUuid").value = userUuid; |     document.getElementById("userOrgTypeUserUuid").value = userUuid; | ||||||
|     document.getElementById("userOrgTypeOrgUuid").value = orgUuid; |     document.getElementById("userOrgTypeOrgUuid").value = orgUuid; | ||||||
|     document.getElementById(`userOrgType${userOrgTypeName}`).checked = true; |     document.getElementById(`userOrgType${userOrgTypeName}`).checked = true; | ||||||
| @@ -206,7 +207,8 @@ userOrgTypeDialog.addEventListener("show.bs.modal", function(event) { | |||||||
|  |  | ||||||
| // Prevent accidental submission of the form with valid elements after the modal has been hidden. | // Prevent accidental submission of the form with valid elements after the modal has been hidden. | ||||||
| userOrgTypeDialog.addEventListener("hide.bs.modal", function() { | userOrgTypeDialog.addEventListener("hide.bs.modal", function() { | ||||||
|     document.getElementById("userOrgTypeDialogTitle").innerHTML = ""; |     document.getElementById("userOrgTypeDialogOrgName").textContent = ""; | ||||||
|  |     document.getElementById("userOrgTypeDialogUserEmail").textContent = ""; | ||||||
|     document.getElementById("userOrgTypeUserUuid").value = ""; |     document.getElementById("userOrgTypeUserUuid").value = ""; | ||||||
|     document.getElementById("userOrgTypeOrgUuid").value = ""; |     document.getElementById("userOrgTypeOrgUuid").value = ""; | ||||||
| }, false); | }, false); | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								src/static/scripts/datatables.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								src/static/scripts/datatables.css
									
									
									
									
										vendored
									
									
								
							| @@ -4,10 +4,10 @@ | |||||||
|  * |  * | ||||||
|  * To rebuild or modify this file with the latest versions of the included |  * To rebuild or modify this file with the latest versions of the included | ||||||
|  * software please visit: |  * software please visit: | ||||||
|  *   https://datatables.net/download/#bs5/dt-2.0.7 |  *   https://datatables.net/download/#bs5/dt-2.0.8 | ||||||
|  * |  * | ||||||
|  * Included libraries: |  * Included libraries: | ||||||
|  *   DataTables 2.0.7 |  *   DataTables 2.0.8 | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
| @charset "UTF-8"; | @charset "UTF-8"; | ||||||
|   | |||||||
							
								
								
									
										53
									
								
								src/static/scripts/datatables.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										53
									
								
								src/static/scripts/datatables.js
									
									
									
									
										vendored
									
									
								
							| @@ -4,20 +4,20 @@ | |||||||
|  * |  * | ||||||
|  * To rebuild or modify this file with the latest versions of the included |  * To rebuild or modify this file with the latest versions of the included | ||||||
|  * software please visit: |  * software please visit: | ||||||
|  *   https://datatables.net/download/#bs5/dt-2.0.7 |  *   https://datatables.net/download/#bs5/dt-2.0.8 | ||||||
|  * |  * | ||||||
|  * Included libraries: |  * Included libraries: | ||||||
|  *   DataTables 2.0.7 |  *   DataTables 2.0.8 | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
| /*! DataTables 2.0.7 | /*! DataTables 2.0.8 | ||||||
|  * © SpryMedia Ltd - datatables.net/license |  * © SpryMedia Ltd - datatables.net/license | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @summary     DataTables |  * @summary     DataTables | ||||||
|  * @description Paginate, search and order HTML tables |  * @description Paginate, search and order HTML tables | ||||||
|  * @version     2.0.7 |  * @version     2.0.8 | ||||||
|  * @author      SpryMedia Ltd |  * @author      SpryMedia Ltd | ||||||
|  * @contact     www.datatables.net |  * @contact     www.datatables.net | ||||||
|  * @copyright   SpryMedia Ltd. |  * @copyright   SpryMedia Ltd. | ||||||
| @@ -563,7 +563,7 @@ | |||||||
| 		 * | 		 * | ||||||
| 		 *  @type string | 		 *  @type string | ||||||
| 		 */ | 		 */ | ||||||
| 		builder: "bs5/dt-2.0.7", | 		builder: "bs5/dt-2.0.8", | ||||||
| 	 | 	 | ||||||
| 	 | 	 | ||||||
| 		/** | 		/** | ||||||
| @@ -7572,6 +7572,16 @@ | |||||||
| 			order  = opts.order,   // applied, current, index (original - compatibility with 1.9) | 			order  = opts.order,   // applied, current, index (original - compatibility with 1.9) | ||||||
| 			page   = opts.page;    // all, current | 			page   = opts.page;    // all, current | ||||||
| 	 | 	 | ||||||
|  | 		if ( _fnDataSource( settings ) == 'ssp' ) { | ||||||
|  | 			// In server-side processing mode, most options are irrelevant since | ||||||
|  | 			// rows not shown don't exist and the index order is the applied order | ||||||
|  | 			// Removed is a special case - for consistency just return an empty | ||||||
|  | 			// array | ||||||
|  | 			return search === 'removed' ? | ||||||
|  | 				[] : | ||||||
|  | 				_range( 0, displayMaster.length ); | ||||||
|  | 		} | ||||||
|  | 	 | ||||||
| 		if ( page == 'current' ) { | 		if ( page == 'current' ) { | ||||||
| 			// Current page implies that order=current and filter=applied, since it is | 			// Current page implies that order=current and filter=applied, since it is | ||||||
| 			// fairly senseless otherwise, regardless of what order and search actually | 			// fairly senseless otherwise, regardless of what order and search actually | ||||||
| @@ -8243,7 +8253,7 @@ | |||||||
| 	_api_register( _child_obj+'.isShown()', function () { | 	_api_register( _child_obj+'.isShown()', function () { | ||||||
| 		var ctx = this.context; | 		var ctx = this.context; | ||||||
| 	 | 	 | ||||||
| 		if ( ctx.length && this.length ) { | 		if ( ctx.length && this.length && ctx[0].aoData[ this[0] ] ) { | ||||||
| 			// _detailsShown as false or undefined will fall through to return false | 			// _detailsShown as false or undefined will fall through to return false | ||||||
| 			return ctx[0].aoData[ this[0] ]._detailsShow || false; | 			return ctx[0].aoData[ this[0] ]._detailsShow || false; | ||||||
| 		} | 		} | ||||||
| @@ -8266,7 +8276,7 @@ | |||||||
| 	// can be an array of these items, comma separated list, or an array of comma | 	// can be an array of these items, comma separated list, or an array of comma | ||||||
| 	// separated lists | 	// separated lists | ||||||
| 	 | 	 | ||||||
| 	var __re_column_selector = /^([^:]+):(name|title|visIdx|visible)$/; | 	var __re_column_selector = /^([^:]+)?:(name|title|visIdx|visible)$/; | ||||||
| 	 | 	 | ||||||
| 	 | 	 | ||||||
| 	// r1 and r2 are redundant - but it means that the parameters match for the | 	// r1 and r2 are redundant - but it means that the parameters match for the | ||||||
| @@ -8338,17 +8348,24 @@ | |||||||
| 				switch( match[2] ) { | 				switch( match[2] ) { | ||||||
| 					case 'visIdx': | 					case 'visIdx': | ||||||
| 					case 'visible': | 					case 'visible': | ||||||
| 						var idx = parseInt( match[1], 10 ); | 						if (match[1]) { | ||||||
| 						// Visible index given, convert to column index | 							var idx = parseInt( match[1], 10 ); | ||||||
| 						if ( idx < 0 ) { | 							// Visible index given, convert to column index | ||||||
| 							// Counting from the right | 							if ( idx < 0 ) { | ||||||
| 							var visColumns = columns.map( function (col,i) { | 								// Counting from the right | ||||||
| 								return col.bVisible ? i : null; | 								var visColumns = columns.map( function (col,i) { | ||||||
| 							} ); | 									return col.bVisible ? i : null; | ||||||
| 							return [ visColumns[ visColumns.length + idx ] ]; | 								} ); | ||||||
|  | 								return [ visColumns[ visColumns.length + idx ] ]; | ||||||
|  | 							} | ||||||
|  | 							// Counting from the left | ||||||
|  | 							return [ _fnVisibleToColumnIndex( settings, idx ) ]; | ||||||
| 						} | 						} | ||||||
| 						// Counting from the left | 						 | ||||||
| 						return [ _fnVisibleToColumnIndex( settings, idx ) ]; | 						// `:visible` on its own | ||||||
|  | 						return columns.map( function (col, i) { | ||||||
|  | 							return col.bVisible ? i : null; | ||||||
|  | 						} ); | ||||||
| 	 | 	 | ||||||
| 					case 'name': | 					case 'name': | ||||||
| 						// match by name. `names` is column index complete and in order | 						// match by name. `names` is column index complete and in order | ||||||
| @@ -9623,7 +9640,7 @@ | |||||||
| 	 *  @type string | 	 *  @type string | ||||||
| 	 *  @default Version number | 	 *  @default Version number | ||||||
| 	 */ | 	 */ | ||||||
| 	DataTable.version = "2.0.7"; | 	DataTable.version = "2.0.8"; | ||||||
| 	 | 	 | ||||||
| 	/** | 	/** | ||||||
| 	 * Private data store, containing all of the settings objects that are | 	 * Private data store, containing all of the settings objects that are | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ | |||||||
|                             <span class="d-block"><strong>Events:</strong> {{event_count}}</span> |                             <span class="d-block"><strong>Events:</strong> {{event_count}}</span> | ||||||
|                         </td> |                         </td> | ||||||
|                         <td class="text-end px-0 small"> |                         <td class="text-end px-0 small"> | ||||||
|                             <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-organization data-vw-org-uuid="{{jsesc id no_quote}}" data-vw-org-name="{{jsesc name no_quote}}" data-vw-billing-email="{{jsesc billingEmail no_quote}}">Delete Organization</button><br> |                             <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-organization data-vw-org-uuid="{{id}}" data-vw-org-name="{{name}}" data-vw-billing-email="{{billingEmail}}">Delete Organization</button><br> | ||||||
|                         </td> |                         </td> | ||||||
|                     </tr> |                     </tr> | ||||||
|                     {{/each}} |                     {{/each}} | ||||||
|   | |||||||
| @@ -54,14 +54,14 @@ | |||||||
|                             {{/if}} |                             {{/if}} | ||||||
|                         </td> |                         </td> | ||||||
|                         <td> |                         <td> | ||||||
|                             <div class="overflow-auto vw-org-cell" data-vw-user-email="{{jsesc email no_quote}}" data-vw-user-uuid="{{jsesc id no_quote}}"> |                             <div class="overflow-auto vw-org-cell" data-vw-user-email="{{email}}" data-vw-user-uuid="{{id}}"> | ||||||
|                             {{#each organizations}} |                             {{#each organizations}} | ||||||
|                             <button class="badge" data-bs-toggle="modal" data-bs-target="#userOrgTypeDialog" data-vw-org-type="{{type}}" data-vw-org-uuid="{{jsesc id no_quote}}" data-vw-org-name="{{jsesc name no_quote}}">{{name}}</button> |                             <button class="badge" data-bs-toggle="modal" data-bs-target="#userOrgTypeDialog" data-vw-org-type="{{type}}" data-vw-org-uuid="{{id}}" data-vw-org-name="{{name}}">{{name}}</button> | ||||||
|                             {{/each}} |                             {{/each}} | ||||||
|                             </div> |                             </div> | ||||||
|                         </td> |                         </td> | ||||||
|                         <td class="text-end px-0 small"> |                         <td class="text-end px-0 small"> | ||||||
|                             <span data-vw-user-uuid="{{jsesc id no_quote}}" data-vw-user-email="{{jsesc email no_quote}}"> |                             <span data-vw-user-uuid="{{id}}" data-vw-user-email="{{email}}"> | ||||||
|                                 {{#if twoFactorEnabled}} |                                 {{#if twoFactorEnabled}} | ||||||
|                                 <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-remove2fa>Remove all 2FA</button><br> |                                 <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-remove2fa>Remove all 2FA</button><br> | ||||||
|                                 {{/if}} |                                 {{/if}} | ||||||
| @@ -109,7 +109,9 @@ | |||||||
|         <div class="modal-dialog modal-dialog-centered modal-sm"> |         <div class="modal-dialog modal-dialog-centered modal-sm"> | ||||||
|             <div class="modal-content"> |             <div class="modal-content"> | ||||||
|                 <div class="modal-header"> |                 <div class="modal-header"> | ||||||
|                     <h6 class="modal-title" id="userOrgTypeDialogTitle"></h6> |                     <h6 class="modal-title"> | ||||||
|  |                         <b>Update User Type:</b><br><b>Organization:</b> <span id="userOrgTypeDialogOrgName"></span><br><b>User:</b> <span id="userOrgTypeDialogUserEmail"></span> | ||||||
|  |                     </h6> | ||||||
|                     <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |                     <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | ||||||
|                 </div> |                 </div> | ||||||
|                 <form class="form" id="userOrgTypeForm"> |                 <form class="form" id="userOrgTypeForm"> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user