Files
vaultwarden/src/static/templates/admin/users.hbs
BlackDex 5a05139efe Change the handling of login errors.
Previously FlashMessage was used to provide an error message during login.
This PR changes that flow to not use redirect for this, but renders the HTML and responds using the correct status code where needed. This should solve some issues which were reported in the past.

Thanks to @RealOrangeOne, for initiating this with a PR.

Fixes #2448
Fixes #2712
Closes #2715

Co-authored-by: Jake Howard <git@theorangeone.net>
2022-09-06 17:27:20 +02:00

294 lines
14 KiB
Handlebars

<main class="container-xl">
<div id="users-block" class="my-3 p-3 bg-white rounded shadow">
<h6 class="border-bottom pb-2 mb-3">Registered Users</h6>
<div class="table-responsive-xl small">
<table id="users-table" class="table table-sm table-striped table-hover">
<thead>
<tr>
<th>User</th>
<th style="width: 85px; min-width: 70px;">Created at</th>
<th style="width: 85px; min-width: 70px;">Last Active</th>
<th style="width: 35px; min-width: 35px;">Items</th>
<th>Attachments</th>
<th style="min-width: 120px;">Organizations</th>
<th style="width: 130px; min-width: 130px;">Actions</th>
</tr>
</thead>
<tbody>
{{#each page_data}}
<tr>
<td>
<img class="float-start me-2 rounded identicon" data-src="{{Email}}">
<div class="float-start">
<strong>{{Name}}</strong>
<span class="d-block">{{Email}}</span>
<span class="d-block">
{{#unless user_enabled}}
<span class="badge bg-danger me-2" title="User is disabled">Disabled</span>
{{/unless}}
{{#if TwoFactorEnabled}}
<span class="badge bg-success me-2" title="2FA is enabled">2FA</span>
{{/if}}
{{#case _Status 1}}
<span class="badge bg-warning me-2" title="User is invited">Invited</span>
{{/case}}
{{#if EmailVerified}}
<span class="badge bg-success me-2" title="Email has been verified">Verified</span>
{{/if}}
</span>
</div>
</td>
<td>
<span class="d-block">{{created_at}}</span>
</td>
<td>
<span class="d-block">{{last_active}}</span>
</td>
<td>
<span class="d-block">{{cipher_count}}</span>
</td>
<td>
<span class="d-block"><strong>Amount:</strong> {{attachment_count}}</span>
{{#if attachment_count}}
<span class="d-block"><strong>Size:</strong> {{attachment_size}}</span>
{{/if}}
</td>
<td>
<div class="overflow-auto" style="max-height: 120px;">
{{#each Organizations}}
<button class="badge" data-bs-toggle="modal" data-bs-target="#userOrgTypeDialog" data-orgtype="{{Type}}" data-orguuid="{{jsesc Id no_quote}}" data-orgname="{{jsesc Name no_quote}}" data-useremail="{{jsesc ../Email no_quote}}" data-useruuid="{{jsesc ../Id no_quote}}">{{Name}}</button>
{{/each}}
</div>
</td>
<td class="text-end px-0 small">
{{#if TwoFactorEnabled}}
<button type="button" class="btn btn-sm btn-link p-0 border-0" onclick='remove2fa({{jsesc Id}})'>Remove all 2FA</button>
{{/if}}
<button type="button" class="btn btn-sm btn-link p-0 border-0" onclick='deauthUser({{jsesc Id}})'>Deauthorize sessions</button>
<button type="button" class="btn btn-sm btn-link p-0 border-0" onclick='deleteUser({{jsesc Id}}, {{jsesc Email}})'>Delete User</button>
{{#if user_enabled}}
<button type="button" class="btn btn-sm btn-link p-0 border-0" onclick='disableUser({{jsesc Id}}, {{jsesc Email}})'>Disable User</button>
{{else}}
<button type="button" class="btn btn-sm btn-link p-0 border-0" onclick='enableUser({{jsesc Id}}, {{jsesc Email}})'>Enable User</button>
{{/if}}
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
<div class="mt-3">
<button type="button" class="btn btn-sm btn-danger" onclick="updateRevisions();"
title="Force all clients to fetch new data next time they connect. Useful after restoring a backup to remove any stale data.">
Force clients to resync
</button>
<button type="button" class="btn btn-sm btn-primary float-end" onclick="reload();">Reload users</button>
</div>
</div>
<div id="invite-form-block" class="align-items-center p-3 mb-3 text-white-50 bg-secondary rounded shadow">
<div>
<h6 class="mb-0 text-white">Invite User</h6>
<small>Email:</small>
<form class="form-inline input-group w-50" id="invite-form" onsubmit="inviteUser(); return false;">
<input type="email" class="form-control me-2" id="email-invite" placeholder="Enter email" required>
<button type="submit" class="btn btn-primary">Invite</button>
</form>
</div>
</div>
<div id="userOrgTypeDialog" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content">
<div class="modal-header">
<h6 class="modal-title" id="userOrgTypeDialogTitle"></h6>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form class="form" id="userOrgTypeForm" onsubmit="updateUserOrgType(); return false;">
<input type="hidden" name="user_uuid" id="userOrgTypeUserUuid" value="">
<input type="hidden" name="org_uuid" id="userOrgTypeOrgUuid" value="">
<div class="modal-body">
<div class="radio">
<label><input type="radio" value="2" class="form-radio-input" name="user_type" id="userOrgTypeUser">&nbsp;User</label>
</div>
<div class="radio">
<label><input type="radio" value="3" class="form-radio-input" name="user_type" id="userOrgTypeManager">&nbsp;Manager</label>
</div>
<div class="radio">
<label><input type="radio" value="1" class="form-radio-input" name="user_type" id="userOrgTypeAdmin">&nbsp;Admin</label>
</div>
<div class="radio">
<label><input type="radio" value="0" class="form-radio-input" name="user_type" id="userOrgTypeOwner">&nbsp;Owner</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-sm btn-primary">Change Role</button>
</div>
</form>
</div>
</div>
</div>
</main>
<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
<script src="{{urlpath}}/vw_static/jquery-3.6.1.slim.js"></script>
<script src="{{urlpath}}/vw_static/datatables.js"></script>
<script>
'use strict';
function deleteUser(id, mail) {
var input_mail = prompt("To delete user '" + mail + "', please type the email below")
if (input_mail != null) {
if (input_mail == mail) {
_post("{{urlpath}}/admin/users/" + id + "/delete",
"User deleted correctly",
"Error deleting user");
} else {
alert("Wrong email, please try again")
}
}
return false;
}
function remove2fa(id) {
_post("{{urlpath}}/admin/users/" + id + "/remove-2fa",
"2FA removed correctly",
"Error removing 2FA");
return false;
}
function deauthUser(id) {
_post("{{urlpath}}/admin/users/" + id + "/deauth",
"Sessions deauthorized correctly",
"Error deauthorizing sessions");
return false;
}
function disableUser(id, mail) {
var confirmed = confirm("Are you sure you want to disable user '" + mail + "'? This will also deauthorize their sessions.")
if (confirmed) {
_post("{{urlpath}}/admin/users/" + id + "/disable",
"User disabled successfully",
"Error disabling user");
}
return false;
}
function enableUser(id, mail) {
var confirmed = confirm("Are you sure you want to enable user '" + mail + "'?")
if (confirmed) {
_post("{{urlpath}}/admin/users/" + id + "/enable",
"User enabled successfully",
"Error enabling user");
}
return false;
}
function updateRevisions() {
_post("{{urlpath}}/admin/users/update_revision",
"Success, clients will sync next time they connect",
"Error forcing clients to sync");
return false;
}
function inviteUser() {
const inv = document.getElementById("email-invite");
const data = JSON.stringify({ "email": inv.value });
inv.value = "";
_post("{{urlpath}}/admin/invite/", "User invited correctly",
"Error inviting user", data);
return false;
}
let OrgTypes = {
"0": { "name": "Owner", "color": "orange" },
"1": { "name": "Admin", "color": "blueviolet" },
"2": { "name": "User", "color": "blue" },
"3": { "name": "Manager", "color": "green" },
};
(async () => {
for (let e of document.querySelectorAll("img.identicon")) {
e.src = await identicon(e.dataset.src);
}
})();
document.querySelectorAll("[data-orgtype]").forEach(function (e) {
let orgtype = OrgTypes[e.dataset.orgtype];
e.style.backgroundColor = orgtype.color;
e.title = orgtype.name;
});
// Special sort function to sort dates in ISO format
jQuery.extend( jQuery.fn.dataTableExt.oSort, {
"date-iso-pre": function ( a ) {
let x;
let sortDate = a.replace(/(<([^>]+)>)/gi, "").trim();
if ( sortDate !== '' ) {
let dtParts = sortDate.split(' ');
var timeParts = (undefined != dtParts[1]) ? dtParts[1].split(':') : ['00','00','00'];
var dateParts = dtParts[0].split('-');
x = (dateParts[0] + dateParts[1] + dateParts[2] + timeParts[0] + timeParts[1] + ((undefined != timeParts[2]) ? timeParts[2] : 0)) * 1;
if ( isNaN(x) ) {
x = 0;
}
} else {
x = Infinity;
}
return x;
},
"date-iso-asc": function ( a, b ) {
return a - b;
},
"date-iso-desc": function ( a, b ) {
return b - a;
}
});
document.addEventListener("DOMContentLoaded", function() {
$('#users-table').DataTable({
"responsive": true,
"lengthMenu": [ [-1, 5, 10, 25, 50], ["All", 5, 10, 25, 50] ],
"pageLength": -1, // Default show all
"columnDefs": [
{ "targets": [1,2], "type": "date-iso" },
{ "targets": 6, "searchable": false, "orderable": false }
]
});
});
var userOrgTypeDialog = document.getElementById('userOrgTypeDialog');
// Fill the form and title
userOrgTypeDialog.addEventListener('show.bs.modal', function(event){
let userOrgType = event.relatedTarget.getAttribute("data-orgtype");
let userOrgTypeName = OrgTypes[userOrgType]["name"];
let orgName = event.relatedTarget.getAttribute("data-orgname");
let userEmail = event.relatedTarget.getAttribute("data-useremail");
let orgUuid = event.relatedTarget.getAttribute("data-orguuid");
let userUuid = event.relatedTarget.getAttribute("data-useruuid");
document.getElementById("userOrgTypeDialogTitle").innerHTML = "<b>Update User Type:</b><br><b>Organization:</b> " + orgName + "<br><b>User:</b> " + userEmail;
document.getElementById("userOrgTypeUserUuid").value = userUuid;
document.getElementById("userOrgTypeOrgUuid").value = orgUuid;
document.getElementById("userOrgType"+userOrgTypeName).checked = true;
}, false);
// Prevent accidental submission of the form with valid elements after the modal has been hidden.
userOrgTypeDialog.addEventListener('hide.bs.modal', function(){
document.getElementById("userOrgTypeDialogTitle").innerHTML = '';
document.getElementById("userOrgTypeUserUuid").value = '';
document.getElementById("userOrgTypeOrgUuid").value = '';
}, false);
function updateUserOrgType() {
let orgForm = document.getElementById("userOrgTypeForm");
const data = JSON.stringify(Object.fromEntries(new FormData(orgForm).entries()));
_post("{{urlpath}}/admin/users/org_type",
"Updated organization type of the user successfully",
"Error updating organization type of the user", data);
return false;
}
</script>