vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

commit 4556f668de64dc45176d8bd6f55a92c6acbb6ea8
parent da8225a3bdca659d061a696ffacac909791482dd
Author: Daniel García <dani-garcia@users.noreply.github.com>
Date:   Tue, 28 Feb 2023 23:43:01 +0100

Merge pull request #3288 from BlackDex/admin-interface-updates

Some Admin Interface updates
Diffstat:
Msrc/api/admin.rs | 64+++++++++++++++++++++++++++++++++++++++++++++++++---------------
Msrc/db/models/collection.rs | 11+++++++++++
Msrc/db/models/event.rs | 11+++++++++++
Msrc/db/models/group.rs | 11+++++++++++
Msrc/static/scripts/admin.css | 6+++++-
Msrc/static/scripts/admin_diagnostics.js | 32+++++++++++++++++++++++---------
Msrc/static/scripts/admin_organizations.js | 2+-
Msrc/static/scripts/admin_users.js | 2+-
Msrc/static/scripts/datatables.css | 29++++++++++++++++++++---------
Msrc/static/scripts/datatables.js | 64++++++++++++++++++++++++++++++++++++++++++++++------------------
Msrc/static/templates/admin/diagnostics.hbs | 9+++++++--
Msrc/static/templates/admin/organizations.hbs | 10++++++++--
Msrc/static/templates/admin/users.hbs | 12++++++------
13 files changed, 199 insertions(+), 64 deletions(-)

diff --git a/src/api/admin.rs b/src/api/admin.rs @@ -300,8 +300,9 @@ fn logout(cookies: &CookieJar<'_>) -> Redirect { #[get("/users")] async fn get_users_json(_token: AdminToken, mut conn: DbConn) -> Json<Value> { - let mut users_json = Vec::new(); - for u in User::get_all(&mut conn).await { + let users = User::get_all(&mut conn).await; + let mut users_json = Vec::with_capacity(users.len()); + for u in users { let mut usr = u.to_json(&mut conn).await; usr["UserEnabled"] = json!(u.enabled); usr["CreatedAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT)); @@ -313,8 +314,9 @@ async fn get_users_json(_token: AdminToken, mut conn: DbConn) -> Json<Value> { #[get("/users/overview")] async fn users_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<String>> { - let mut users_json = Vec::new(); - for u in User::get_all(&mut conn).await { + let users = User::get_all(&mut conn).await; + let mut users_json = Vec::with_capacity(users.len()); + for u in users { let mut usr = u.to_json(&mut conn).await; usr["cipher_count"] = json!(Cipher::count_owned_by_user(&u.uuid, &mut conn).await); usr["attachment_count"] = json!(Attachment::count_by_user(&u.uuid, &mut conn).await); @@ -490,11 +492,15 @@ async fn update_revision_users(_token: AdminToken, mut conn: DbConn) -> EmptyRes #[get("/organizations/overview")] async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<String>> { - let mut organizations_json = Vec::new(); - for o in Organization::get_all(&mut conn).await { + let organizations = Organization::get_all(&mut conn).await; + let mut organizations_json = Vec::with_capacity(organizations.len()); + for o in organizations { let mut org = o.to_json(); org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &mut conn).await); org["cipher_count"] = json!(Cipher::count_by_org(&o.uuid, &mut conn).await); + org["collection_count"] = json!(Collection::count_by_org(&o.uuid, &mut conn).await); + org["group_count"] = json!(Group::count_by_org(&o.uuid, &mut conn).await); + org["event_count"] = json!(Event::count_by_org(&o.uuid, &mut conn).await); org["attachment_count"] = json!(Attachment::count_by_org(&o.uuid, &mut conn).await); org["attachment_size"] = json!(get_display_size(Attachment::size_by_org(&o.uuid, &mut conn).await as i32)); organizations_json.push(org); @@ -525,10 +531,20 @@ struct GitCommit { sha: String, } -async fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> { - let github_api = get_reqwest_client(); +#[derive(Deserialize)] +struct TimeApi { + year: u16, + month: u8, + day: u8, + hour: u8, + minute: u8, + seconds: u8, +} + +async fn get_json_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> { + let json_api = get_reqwest_client(); - Ok(github_api.get(url).send().await?.error_for_status()?.json::<T>().await?) + Ok(json_api.get(url).send().await?.error_for_status()?.json::<T>().await?) } async fn has_http_access() -> bool { @@ -548,14 +564,13 @@ async fn get_release_info(has_http_access: bool, running_within_docker: bool) -> // If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway. if has_http_access { ( - match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/vaultwarden/releases/latest") + match get_json_api::<GitRelease>("https://api.github.com/repos/dani-garcia/vaultwarden/releases/latest") .await { Ok(r) => r.tag_name, _ => "-".to_string(), }, - match get_github_api::<GitCommit>("https://api.github.com/repos/dani-garcia/vaultwarden/commits/main").await - { + match get_json_api::<GitCommit>("https://api.github.com/repos/dani-garcia/vaultwarden/commits/main").await { Ok(mut c) => { c.sha.truncate(8); c.sha @@ -567,7 +582,7 @@ async fn get_release_info(has_http_access: bool, running_within_docker: bool) -> if running_within_docker { "-".to_string() } else { - match get_github_api::<GitRelease>( + match get_json_api::<GitRelease>( "https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest", ) .await @@ -582,6 +597,24 @@ async fn get_release_info(has_http_access: bool, running_within_docker: bool) -> } } +async fn get_ntp_time(has_http_access: bool) -> String { + if has_http_access { + if let Ok(ntp_time) = get_json_api::<TimeApi>("https://www.timeapi.io/api/Time/current/zone?timeZone=UTC").await + { + return format!( + "{year}-{month:02}-{day:02} {hour:02}:{minute:02}:{seconds:02} UTC", + year = ntp_time.year, + month = ntp_time.month, + day = ntp_time.day, + hour = ntp_time.hour, + minute = ntp_time.minute, + seconds = ntp_time.seconds + ); + } + } + String::from("Unable to fetch NTP time.") +} + #[get("/diagnostics")] async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) -> ApiResult<Html<String>> { use chrono::prelude::*; @@ -610,7 +643,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) // Check if we are able to resolve DNS entries let dns_resolved = match ("github.com", 0).to_socket_addrs().map(|mut i| i.next()) { Ok(Some(a)) => a.ip().to_string(), - _ => "Could not resolve domain name.".to_string(), + _ => "Unable to resolve domain name.".to_string(), }; let (latest_release, latest_commit, latest_web_build) = @@ -644,7 +677,8 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) "host_arch": std::env::consts::ARCH, "host_os": std::env::consts::OS, "server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(), - "server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the date/time check as the last item to minimize the difference + "server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the server date/time check as late as possible to minimize the time difference + "ntp_time": get_ntp_time(has_http_access).await, // Run the ntp check as late as possible to minimize the time difference }); let text = AdminTemplateData::new("admin/diagnostics", diagnostics_json).render()?; diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs @@ -234,6 +234,17 @@ impl Collection { }} } + pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 { + db_run! { conn: { + collections::table + .filter(collections::org_uuid.eq(org_uuid)) + .count() + .first::<i64>(conn) + .ok() + .unwrap_or(0) + }} + } + pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> { db_run! { conn: { collections::table diff --git a/src/db/models/event.rs b/src/db/models/event.rs @@ -263,6 +263,17 @@ impl Event { }} } + pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 { + db_run! { conn: { + event::table + .filter(event::org_uuid.eq(org_uuid)) + .count() + .first::<i64>(conn) + .ok() + .unwrap_or(0) + }} + } + pub async fn find_by_org_and_user_org( org_uuid: &str, user_org_uuid: &str, diff --git a/src/db/models/group.rs b/src/db/models/group.rs @@ -168,6 +168,17 @@ impl Group { }} } + pub async fn count_by_org(organizations_uuid: &str, conn: &mut DbConn) -> i64 { + db_run! { conn: { + groups::table + .filter(groups::organizations_uuid.eq(organizations_uuid)) + .count() + .first::<i64>(conn) + .ok() + .unwrap_or(0) + }} + } + pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> { db_run! { conn: { groups::table diff --git a/src/static/scripts/admin.css b/src/static/scripts/admin.css @@ -25,10 +25,14 @@ img { min-width: 85px; max-width: 85px; } -#users-table .vw-items, #orgs-table .vw-items, #orgs-table .vw-users { +#users-table .vw-ciphers, #orgs-table .vw-users, #orgs-table .vw-ciphers { min-width: 35px; max-width: 40px; } +#orgs-table .vw-misc { + min-width: 65px; + max-width: 80px; +} #users-table .vw-attachments, #orgs-table .vw-attachments { min-width: 100px; max-width: 130px; diff --git a/src/static/scripts/admin_diagnostics.js b/src/static/scripts/admin_diagnostics.js @@ -4,6 +4,7 @@ var dnsCheck = false; var timeCheck = false; +var ntpTimeCheck = false; var domainCheck = false; var httpsCheck = false; @@ -90,7 +91,8 @@ async function generateSupportString(event, dj) { supportString += `* Internet access: ${dj.has_http_access}\n`; supportString += `* Internet access via a proxy: ${dj.uses_proxy}\n`; supportString += `* DNS Check: ${dnsCheck}\n`; - supportString += `* Time Check: ${timeCheck}\n`; + supportString += `* Browser/Server Time Check: ${timeCheck}\n`; + supportString += `* Server/NTP Time Check: ${ntpTimeCheck}\n`; supportString += `* Domain Configuration Check: ${domainCheck}\n`; supportString += `* HTTPS Check: ${httpsCheck}\n`; supportString += `* Database type: ${dj.db_type}\n`; @@ -136,16 +138,17 @@ function copyToClipboard(event) { new BSN.Toast("#toastClipboardCopy").show(); } -function checkTimeDrift(browserUTC, serverUTC) { +function checkTimeDrift(utcTimeA, utcTimeB, statusPrefix) { const timeDrift = ( - Date.parse(serverUTC.replace(" ", "T").replace(" UTC", "")) - - Date.parse(browserUTC.replace(" ", "T").replace(" UTC", "")) + Date.parse(utcTimeA.replace(" ", "T").replace(" UTC", "")) - + Date.parse(utcTimeB.replace(" ", "T").replace(" UTC", "")) ) / 1000; - if (timeDrift > 20 || timeDrift < -20) { - document.getElementById("time-warning").classList.remove("d-none"); + if (timeDrift > 15 || timeDrift < -15) { + document.getElementById(`${statusPrefix}-warning`).classList.remove("d-none"); + return false; } else { - document.getElementById("time-success").classList.remove("d-none"); - timeCheck = true; + document.getElementById(`${statusPrefix}-success`).classList.remove("d-none"); + return true; } } @@ -195,7 +198,18 @@ function checkDns(dns_resolved) { function init(dj) { // Time check document.getElementById("time-browser-string").innerText = browserUTC; - checkTimeDrift(browserUTC, dj.server_time); + + // Check if we were able to fetch a valid NTP Time + // If so, compare both browser and server with NTP + // Else, compare browser and server. + if (dj.ntp_time.indexOf("UTC") !== -1) { + timeCheck = checkTimeDrift(dj.server_time, browserUTC, "time"); + checkTimeDrift(dj.ntp_time, browserUTC, "ntp-browser"); + ntpTimeCheck = checkTimeDrift(dj.ntp_time, dj.server_time, "ntp-server"); + } else { + timeCheck = checkTimeDrift(dj.server_time, browserUTC, "time"); + ntpTimeCheck = "n/a"; + } // Domain check const browserURL = location.href.toLowerCase(); diff --git a/src/static/scripts/admin_organizations.js b/src/static/scripts/admin_organizations.js @@ -54,7 +54,7 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => { ], "pageLength": -1, // Default show all "columnDefs": [{ - "targets": 4, + "targets": [4,5], "searchable": false, "orderable": false }] diff --git a/src/static/scripts/admin_users.js b/src/static/scripts/admin_users.js @@ -244,7 +244,7 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => { [-1, 2, 5, 10, 25, 50], ["All", 2, 5, 10, 25, 50] ], - "pageLength": 2, // Default show all + "pageLength": -1, // Default show all "columnDefs": [{ "targets": [1, 2], "type": "date-iso" diff --git a/src/static/scripts/datatables.css b/src/static/scripts/datatables.css @@ -4,13 +4,19 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-1.13.1 + * https://datatables.net/download/#bs5/dt-1.13.2 * * Included libraries: - * DataTables 1.13.1 + * DataTables 1.13.2 */ @charset "UTF-8"; +:root { + --dt-row-selected: 13, 110, 253; + --dt-row-selected-text: 255, 255, 255; + --dt-row-selected-link: 9, 10, 11; +} + table.dataTable td.dt-control { text-align: center; cursor: pointer; @@ -126,7 +132,7 @@ div.dataTables_processing > div:last-child > div { width: 13px; height: 13px; border-radius: 50%; - background: rgba(13, 110, 253, 0.9); + background: 13 110 253; animation-timing-function: cubic-bezier(0, 1, 1, 0); } div.dataTables_processing > div:last-child > div:nth-child(1) { @@ -284,23 +290,28 @@ table.dataTable > tbody > tr { background-color: transparent; } table.dataTable > tbody > tr.selected > * { - box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.9); - color: white; + box-shadow: inset 0 0 0 9999px rgb(13, 110, 253); + box-shadow: inset 0 0 0 9999px rgb(var(--dt-row-selected)); + color: rgb(255, 255, 255); + color: rgb(var(--dt-row-selected-text)); } table.dataTable > tbody > tr.selected a { - color: #090a0b; + color: rgb(9, 10, 11); + color: rgb(var(--dt-row-selected-link)); } table.dataTable.table-striped > tbody > tr.odd > * { box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.05); } table.dataTable.table-striped > tbody > tr.odd.selected > * { box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.95); + box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.95); } table.dataTable.table-hover > tbody > tr:hover > * { box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.075); } table.dataTable.table-hover > tbody > tr.selected:hover > * { box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.975); + box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.975); } div.dataTables_wrapper div.dataTables_length label { @@ -374,9 +385,9 @@ div.dataTables_scrollFoot > .dataTables_scrollFootInner > table { @media screen and (max-width: 767px) { div.dataTables_wrapper div.dataTables_length, -div.dataTables_wrapper div.dataTables_filter, -div.dataTables_wrapper div.dataTables_info, -div.dataTables_wrapper div.dataTables_paginate { + div.dataTables_wrapper div.dataTables_filter, + div.dataTables_wrapper div.dataTables_info, + div.dataTables_wrapper div.dataTables_paginate { text-align: center; } div.dataTables_wrapper div.dataTables_paginate ul.pagination { diff --git a/src/static/scripts/datatables.js b/src/static/scripts/datatables.js @@ -4,20 +4,20 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-1.13.1 + * https://datatables.net/download/#bs5/dt-1.13.2 * * Included libraries: - * DataTables 1.13.1 + * DataTables 1.13.2 */ -/*! DataTables 1.13.1 - * ©2008-2022 SpryMedia Ltd - datatables.net/license +/*! DataTables 1.13.2 + * ©2008-2023 SpryMedia Ltd - datatables.net/license */ /** * @summary DataTables * @description Paginate, search and order HTML tables - * @version 1.13.1 + * @version 1.13.2 * @author SpryMedia Ltd * @contact www.datatables.net * @copyright SpryMedia Ltd. @@ -1382,7 +1382,12 @@ var _isNumber = function ( d, decimalPoint, formatted ) { - var strType = typeof d === 'string'; + let type = typeof d; + var strType = type === 'string'; + + if ( type === 'number' || type === 'bigint') { + return true; + } // If empty return immediately so there must be a number if it is a // formatted string (this stops the string "k", or "kr", etc being detected @@ -6789,8 +6794,15 @@ if ( eventName !== null ) { var e = $.Event( eventName+'.dt' ); + var table = $(settings.nTable); - $(settings.nTable).trigger( e, args ); + table.trigger( e, args ); + + // If not yet attached to the document, trigger the event + // on the body directly to sort of simulate the bubble + if (table.parents('body').length === 0) { + $('body').trigger( e, args ); + } ret.push( e.result ); } @@ -7256,7 +7268,7 @@ pluck: function ( prop ) { - let fn = DataTable.util.get(prop); + var fn = DataTable.util.get(prop); return this.map( function ( el ) { return fn(el); @@ -8353,10 +8365,9 @@ $(document).on('plugin-init.dt', function (e, context) { var api = new _Api( context ); - - const namespace = 'on-plugin-init'; - const stateSaveParamsEvent = `stateSaveParams.${namespace}`; - const destroyEvent = `destroy.${namespace}`; + var namespace = 'on-plugin-init'; + var stateSaveParamsEvent = 'stateSaveParams.' + namespace; + var destroyEvent = 'destroy. ' + namespace; api.on( stateSaveParamsEvent, function ( e, settings, d ) { // This could be more compact with the API, but it is a lot faster as a simple @@ -8375,7 +8386,7 @@ }); api.on( destroyEvent, function () { - api.off(`${stateSaveParamsEvent} ${destroyEvent}`); + api.off(stateSaveParamsEvent + ' ' + destroyEvent); }); var loaded = api.state.loaded(); @@ -9697,7 +9708,7 @@ * @type string * @default Version number */ - DataTable.version = "1.13.1"; + DataTable.version = "1.13.2"; /** * Private data store, containing all of the settings objects that are @@ -14121,7 +14132,7 @@ * * @type string */ - build:"bs5/dt-1.13.1", + build:"bs5/dt-1.13.2", /** @@ -14830,10 +14841,17 @@ } if ( btnDisplay !== null ) { - node = $('<a>', { + var tag = settings.oInit.pagingTag || 'a'; + var disabled = btnClass.indexOf(disabledClass) !== -1; + + + node = $('<'+tag+'>', { 'class': classes.sPageButton+' '+btnClass, 'aria-controls': settings.sTableId, + 'aria-disabled': disabled ? 'true' : null, 'aria-label': aria[ button ], + 'aria-role': 'link', + 'aria-current': btnClass === classes.sPageButtonActive ? 'page' : null, 'data-dt-idx': button, 'tabindex': tabIndex, 'id': idx === 0 && typeof button === 'string' ? @@ -14965,6 +14983,12 @@ if ( d !== 0 && (!d || d === '-') ) { return -Infinity; } + + let type = typeof d; + + if (type === 'number' || type === 'bigint') { + return d; + } // If a decimal place other than `.` is used, it needs to be given to the // function so we can detect it and replace with a `.` which is the only @@ -15647,7 +15671,6 @@ require('datatables.net')(root, $); } - return factory( $, root, root.document ); }; } @@ -15755,6 +15778,8 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu } if ( btnDisplay ) { + var disabled = btnClass.indexOf('disabled') !== -1; + node = $('<li>', { 'class': classes.sPageButton+' '+btnClass, 'id': idx === 0 && typeof button === 'string' ? @@ -15762,9 +15787,12 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu null } ) .append( $('<a>', { - 'href': '#', + 'href': disabled ? null : '#', 'aria-controls': settings.sTableId, + 'aria-disabled': disabled ? 'true' : null, 'aria-label': aria[ button ], + 'aria-role': 'link', + 'aria-current': btnClass === 'active' ? 'page' : null, 'data-dt-idx': button, 'tabindex': settings.iTabIndex, 'class': 'page-link' diff --git a/src/static/templates/admin/diagnostics.hbs b/src/static/templates/admin/diagnostics.hbs @@ -144,10 +144,15 @@ <span><b>Server:</b> {{page_data.server_time_local}}</span> </dd> <dt class="col-sm-5">Date & Time (UTC) - <span class="badge bg-success d-none" id="time-success" title="Server and browser times are within 20 seconds of each other.">Ok</span> - <span class="badge bg-danger d-none" id="time-warning" title="Server and browser times are more than 20 seconds apart.">Error</span> + <span class="badge bg-success d-none" id="time-success" title="Server and browser times are within 15 seconds of each other.">Server/Browser Ok</span> + <span class="badge bg-danger d-none" id="time-warning" title="Server and browser times are more than 15 seconds apart.">Server/Browser Error</span> + <span class="badge bg-success d-none" id="ntp-server-success" title="Server and NTP times are within 15 seconds of each other.">Server NTP Ok</span> + <span class="badge bg-danger d-none" id="ntp-server-warning" title="Server and NTP times are more than 15 seconds apart.">Server NTP Error</span> + <span class="badge bg-success d-none" id="ntp-browser-success" title="Browser and NTP times are within 15 seconds of each other.">Browser NTP Ok</span> + <span class="badge bg-danger d-none" id="ntp-browser-warning" title="Browser and NTP times are more than 15 seconds apart.">Browser NTP Error</span> </dt> <dd class="col-sm-7"> + <span id="ntp-time" class="d-block"><b>NTP:</b> <span id="ntp-server-string">{{page_data.ntp_time}}</span></span> <span id="time-server" class="d-block"><b>Server:</b> <span id="time-server-string">{{page_data.server_time}}</span></span> <span id="time-browser" class="d-block"><b>Browser:</b> <span id="time-browser-string"></span></span> </dd> diff --git a/src/static/templates/admin/organizations.hbs b/src/static/templates/admin/organizations.hbs @@ -7,8 +7,9 @@ <tr> <th class="vw-org-details">Organization</th> <th class="vw-users">Users</th> - <th class="vw-items">Items</th> + <th class="vw-ciphers">Ciphers</th> <th class="vw-attachments">Attachments</th> + <th class="vw-misc">Misc</th> <th class="vw-actions">Actions</th> </tr> </thead> @@ -37,8 +38,13 @@ <span class="d-block"><strong>Size:</strong> {{attachment_size}}</span> {{/if}} </td> + <td> + <span class="d-block"><strong>Collections:</strong> {{collection_count}}</span> + <span class="d-block"><strong>Groups:</strong> {{group_count}}</span> + <span class="d-block"><strong>Events:</strong> {{event_count}}</span> + </td> <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> + <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> </td> </tr> {{/each}} diff --git a/src/static/templates/admin/users.hbs b/src/static/templates/admin/users.hbs @@ -8,7 +8,7 @@ <th class="vw-account-details">User</th> <th class="vw-created-at">Created at</th> <th class="vw-last-active">Last Active</th> - <th class="vw-items">Items</th> + <th class="vw-ciphers">Ciphers</th> <th class="vw-attachments">Attachments</th> <th class="vw-organizations">Organizations</th> <th class="vw-actions">Actions</th> @@ -63,14 +63,14 @@ <td class="text-end px-0 small"> <span data-vw-user-uuid="{{jsesc Id no_quote}}" data-vw-user-email="{{jsesc Email no_quote}}"> {{#if TwoFactorEnabled}} - <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-remove2fa>Remove all 2FA</button> + <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-remove2fa>Remove all 2FA</button><br> {{/if}} - <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-deauth-user>Deauthorize sessions</button> - <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-user>Delete User</button> + <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-deauth-user>Deauthorize sessions</button><br> + <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-user>Delete User</button><br> {{#if user_enabled}} - <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-disable-user>Disable User</button> + <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-disable-user>Disable User</button><br> {{else}} - <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-enable-user>Enable User</button> + <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-enable-user>Enable User</button><br> {{/if}} </span> </td>