Merge branch 'dev' into translation/update-FR

This commit is contained in:
Zedifus 2023-10-16 20:49:22 +01:00
commit ec8928c821
8 changed files with 271 additions and 95 deletions

View File

@ -18,6 +18,7 @@
- Fix bug where a reaction loop could be created, but would be cut short by an error when the loop occurred ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/636))
- Use controller on update user call ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/640))
- Move `imports` to `import/upload` in bind mount to better serve users on unraid with limited vdisk storage ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/642))
- Fix bug where everytime a page was loaded user settings would be reset #286 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/643))
### Refactor
- Consolidate remaining frontend functions into API V2, and remove ajax internal API ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/585))
- Replace bleach with nh3 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/628))
@ -33,6 +34,8 @@
- Bump orjson to 3.9.7 for python 3.12 support ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/638))
- Bump all Crafty required python dependancies, maintaining minimum 3.9 support ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/639))
- Better optimize and refactor docker launcher sh ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/642))
- Improve pop-up notifications with Toasts ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/641))
- Move username and password settings to buttons on panel config ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/643))
### Lang
TBD
<br><br>

View File

@ -214,14 +214,14 @@ class UsersController:
limit_server_creation = 0
limit_user_creation = 0
limit_role_creation = 0
PermissionsCrafty.add_or_update_user(
user_id,
permissions_mask,
limit_server_creation,
limit_user_creation,
limit_role_creation,
)
if user_crafty_data:
PermissionsCrafty.add_or_update_user(
user_id,
permissions_mask,
limit_server_creation,
limit_user_creation,
limit_role_creation,
)
self.users_helper.delete_user_roles(user_id, removed_roles)

View File

@ -4,10 +4,7 @@ import typing as t
from jsonschema import ValidationError, validate
from app.classes.controllers.users_controller import UsersController
from app.classes.models.crafty_permissions import (
EnumPermissionsCrafty,
PermissionsCrafty,
)
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.models.roles import HelperRoles
from app.classes.models.users import HelperUsers
from app.classes.web.base_api_handler import BaseApiHandler
@ -247,31 +244,25 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
or data["manager"] == 0
):
data["manager"] = None
crafty_perms = None
if "permissions" in data:
permissions: t.List[UsersController.ApiPermissionDict] = data.pop(
"permissions"
)
permissions_mask = "0" * len(EnumPermissionsCrafty)
limit_server_creation = 0
limit_user_creation = 0
limit_role_creation = 0
for permission in permissions:
permissions_mask = self.controller.crafty_perms.set_permission(
permissions_mask,
EnumPermissionsCrafty.__members__[permission["name"]],
"1" if permission["enabled"] else "0",
)
PermissionsCrafty.add_or_update_user(
user_id,
permissions_mask,
limit_server_creation,
limit_user_creation,
limit_role_creation,
)
if permissions is not None:
server_quantity = {}
permissions_mask = list(permissions_mask)
for permission in permissions:
server_quantity[permission["name"]] = permission["quantity"]
permissions_mask[
EnumPermissionsCrafty[permission["name"]].value
] = ("1" if permission["enabled"] else "0")
permissions_mask = "".join(permissions_mask)
crafty_perms = {
"permissions_mask": permissions_mask,
"server_quantity": server_quantity,
}
# TODO: make this more efficient
if len(data) != 0:
for key in data:
@ -280,7 +271,11 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
if key == "password":
value = self.helper.encode_pass(value)
setattr(user_obj, key, value)
self.controller.users.update_user(auth_data[4]["user_id"], data)
self.controller.users.update_user(
user_id,
data,
crafty_perms,
)
self.controller.management.add_to_audit_log(
user["user_id"],

View File

@ -124,19 +124,32 @@
.notification {
position: relative;
box-sizing: border-box;
padding: 0.5rem;
padding-left: 0.7rem;
width: 180px;
margin-left: 10px;
margin-right: 10px;
margin-right: 1rem;
background: var(--card-banner-bg);
-webkit-transition: right 0.75s, opacity 0.75s, top 0.75s;
-moz-transition: right 0.75s, opacity 0.75s, top 0.75s;
-o-transition: right 0.75s, opacity 0.75s, top 0.75s;
transition: right 0.75s, opacity 0.75s, top 0.75s;
right: -6rem;
right: -20rem;
opacity: 0.1;
margin-bottom: 1rem;
z-index: 999;
top: 0px;
}
.toast-header {
background-color: var(--card-banner-bg);
color: var(--base-text);
}
.toast-body {
background-color: var(--dropdown-bg);
color: var(--base-text);
}
.notification img {
max-height: 20px;
}
.notification strong {
line-height: 20px;
}
.notification.active {
@ -150,27 +163,17 @@
top: -2rem;
}
.notification p {
margin: 0px;
width: calc(160.8px - 16px);
z-index: inherit;
}
.notification span {
position: absolute;
right: 0.5rem;
top: 0.46rem;
cursor: pointer;
font-weight: bold;
line-height: 20px;
font-size: 22px;
font-size: 15px;
user-select: none;
z-index: inherit;
cursor: pointer;
}
</style>
<div class="notifications"></div>
<div class="notifications"></div>
<script src="/static/assets/vendors/js/vendor.bundle.base.js"></script>
<script src="/static/assets/js/shared/off-canvas.js"></script>
<script src="/static/assets/js/shared/hoverable-collapse.js"></script>
@ -502,30 +505,55 @@
function notify(message) {
console.log(`notify(${message})`);
var paragraphEl = document.createElement('p');
var closeEl = document.createElement('span');
var intId = getRandomInt(0, 100);
var toastDiv = document.createElement('div');
toastDiv.setAttribute("id", "toast_" + intId);
toastDiv.setAttribute("class", "notification toast");
toastDiv.setAttribute("role", "alert");
toastDiv.setAttribute("aria-lived", "assertive");
toastDiv.setAttribute("aria-atomic", "true");
toastDiv.setAttribute("data-delay", "3000");
toastDiv.setAttribute("data-animation", "true");
toastDiv.setAttribute("data-autohide", "false");
paragraphEl.textContent = message;
var toastHeaderDiv = document.createElement('div');
toastHeaderDiv.setAttribute("class", "toast-header");
closeEl.innerHTML = '&times;';
closeEl.addEventListener('click', function () { closeNotification(this) });
var toastHeaderImg = document.createElement('img');
toastHeaderImg.setAttribute("src", "/static/assets/images/logo_small.svg");
toastHeaderImg.setAttribute("class", "mr-auto");
toastHeaderImg.setAttribute("alt", "logo");
toastHeaderDiv.appendChild(toastHeaderImg);
var parentEl = document.createElement('div');
parentEl.appendChild(paragraphEl);
parentEl.appendChild(closeEl);
var toastHeaderTitle = document.createElement('strong');
toastHeaderTitle.setAttribute("class", "mr-auto");
toastHeaderTitle.innerHTML = " Crafty Controller ";
toastHeaderDiv.appendChild(toastHeaderTitle);
parentEl.classList.add('notification');
var toastHeaderCloseSpan = document.createElement('span');
toastHeaderCloseSpan.setAttribute("class", "fa-solid fa-xmark");
toastHeaderCloseSpan.setAttribute("aria-hidden", "true");
toastHeaderCloseSpan.addEventListener('click', function () { closeNotification(this.parentElement) });
toastHeaderDiv.appendChild(toastHeaderCloseSpan);
document.querySelector('.notifications').appendChild(parentEl);
var toastBodyDiv = document.createElement('div');
toastBodyDiv.setAttribute("class", "toast-body");
toastBodyDiv.textContent = message;
toastDiv.appendChild(toastHeaderDiv);
toastDiv.appendChild(toastBodyDiv);
document.querySelector('.notifications').appendChild(toastDiv);
$('#toast_' + intId).toast('show');
setTimeout(function () {
parentEl.classList.add('active');
toastDiv.classList.add('active');
}, 200);
setTimeout(function (element) {
closeNotification(element);
}, 7500, closeEl);
closeNotification(element.parentElement);
}, 7500, toastHeaderCloseSpan);
`
<div class="notification">

View File

@ -992,23 +992,28 @@
});
$(document).ready(function () {
function sendOrder(id_string) {
async function sendOrder(id_string) {
const token = getCookie("_xsrf")
$.ajax({
type: "PATCH",
headers: { 'X-XSRFToken': token },
url: `/api/v2/users/@me`,
data: JSON.stringify({
let res = await fetch(`/api/v2/users/@me`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({
server_order: id_string,
}),
success: function (data) {
console.log("got response:");
console.log(data);
},
});
let responseData = await res.json();
if (responseData.status === "ok") {
return
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
// Inits the sortable
$("table#servers_table tbody")
.sortable({

View File

@ -79,7 +79,7 @@
<tbody>
{% for user in data['users'] %}
<tr>
<td><i class="fas fa-user"></i> {{ user.username }}</td>
<td><i class="fas fa-user"></i><span id="user_{{user.user_id}}">{{ user.username }}</span></td>
<td>
{% if user.enabled %}
<span class="text-success">
@ -106,7 +106,10 @@
{% end %}
</ul>
</td>
<td><a href="/panel/edit_user?id={{user.user_id}}"><i class="fas fa-pencil-alt"></i></a></td>
<td><span data-translate="{{translate('userConfig', 'userName', data['lang'])}}" data-toggle="tooltip" title="{{ translate('userConfig', 'userName', data['lang'])}}" id="username_{{user.user_id}}" class="edit_user clickable" data-name="{{user.username}}" data-id="{{user.user_id}}"><i class="fa-solid fa-user"></i></span>
&nbsp;&nbsp;<span data-translate1="{{translate('userConfig', 'password', data['lang'])}}" data-translate2="{{translate('userConfig', 'repeat', data['lang'])}}" data-toggle="tooltip" title="{{ translate('userConfig', 'password', data['lang'])}}" class="edit_password clickable" data-id="{{user.user_id}}"><i class="fa-solid fa-lock"></i></span>
&nbsp;&nbsp;<a data-toggle="tooltip" title="{{ translate('userConfig', 'pageTitle', data['lang'])}}" href="/panel/edit_user?id={{user.user_id}}"><i class="fas fa-pencil-alt"></i></a>
</td>
</tr>
{% end %}
{% for user in data['managed_users'] %}
@ -138,7 +141,10 @@
{% end %}
</ul>
</td>
<td><a href="/panel/edit_user?id={{user.user_id}}"><i class="fas fa-pencil-alt"></i></a></td>
<td><span data-translate="{{translate('userConfig', 'userName', data['lang'])}}" data-toggle="tooltip" title="{{ translate('userConfig', 'userName', data['lang'])}}" id="username_{{user.user_id}}" class="edit_user clickable" data-name="{{user.username}}" data-id="{{user.user_id}}"><i class="fa-solid fa-user"></i></span>
&nbsp;&nbsp;<span data-translate1="{{translate('userConfig', 'password', data['lang'])}}" data-translate2="{{translate('userConfig', 'repeat', data['lang'])}}" data-toggle="tooltip" title="{{ translate('userConfig', 'password', data['lang'])}}" class="edit_password clickable" data-id="{{user.user_id}}"><i class="fa-solid fa-lock"></i></span>
&nbsp;&nbsp;<a data-toggle="tooltip" title="{{ translate('userConfig', 'pageTitle', data['lang'])}}" href="/panel/edit_user?id={{user.user_id}}"><i class="fas fa-pencil-alt"></i></a>
</td>
</tr>
{% end %}
</tbody>
@ -274,6 +280,12 @@
</div>
<style>
.clickable {
color: #007bff;
}
.clickable:hover {
cursor: pointer;
}
.custom-picker {
border: 1px solid var(--outline);
}
@ -312,6 +324,99 @@
{% block js %}
<script>
function validateForm() {
let password0 = document.getElementById("password0").value;
let password1 = document.getElementById("password1").value;
if (password0 != password1) {
$('.passwords-match').popover('show');
$('.popover-body').click(function () {
$('.passwords-match').popover("hide");
});
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
$("#password0").css("outline", "1px solid red");
$("#password1").css("outline", "1px solid red");
return false;
} else {
return password1;
}
}
$(".edit_password").on("click", async function(){
const token = getCookie("_xsrf");
let user_id = $(this).data('id');
bootbox.confirm(`<form class="form" id='infos' action=''>\
<div class="form-group">
<label for="new_password">${$(this).data("translate1")}</label>
<input class="form-control" type='password' id="password0" name='new_password' /></br>\
</div>
<div class="form-group">
<label for="confirm_password">${$(this).data("translate2")}</label>
<input class="form-control" type='password' id="password1" name='confirm_password' />\
</div>
</form>`, async function(result) {
if(result){
password = validateForm();
if (!password){
return;
}
let res = await fetch(`/api/v2/users/${user_id}`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({"password": password}),
});
let responseData = await res.json();
if (responseData.status === "ok") {
console.log(responseData.data)
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
});
});
$(document).on("submit", ".bootbox form", function(e) {
e.preventDefault();
$(".bootbox .btn-primary").click();
});
$(".edit_user").on("click", function(){
const token = getCookie("_xsrf");
let username = $(this).data('name');
let user_id = $(this).data('id');
bootbox.confirm(`<form class="form" id='infos' action=''>\
<div class="form-group">
<label for="username">${$(this).data("translate")}</label>
<input class="form-control" type='text' name='username' id="username_field" value=${username} /><br/>\
</div>
</form>`, async function(result) {
if(result){
let new_username = $("#username_field").val();
let res = await fetch(`/api/v2/users/${user_id}`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({"username": new_username}),
});
let responseData = await res.json();
if (responseData.status === "ok") {
$(`#user_${user_id}`).html(` ${new_username}`)
$(`#username_${user_id}`).data('name', new_username);
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
});
});
if (webSocket) {
webSocket.on('move_status', function (message) {
if (message === "done") {

View File

@ -71,6 +71,7 @@ data['lang']) }}{% end %}
data['lang']) }}</h4>
</div>
<div class="card-body">
{% if data['new_user'] %}
<div class="form-group">
<label class="form-label" for="username">{{ translate('userConfig', 'userName', data['lang'])
}}<small class="text-muted ml-1"> - {{ translate('userConfig', 'userNameDesc', data['lang'])
@ -98,6 +99,15 @@ data['lang']) }}{% end %}
data-content="{{ translate('panelConfig', 'match', data['lang']) }}" ,
data-placement="right"></span>
</div>
{% else %}
<div class="form-group">
<label class="form-label" for="username">{{ translate('userConfig', 'userName', data['lang'])
}}<small class="text-muted ml-1"> - {{ translate('userConfig', 'userNameDesc', data['lang'])
}}</small> </label>
<input type="text" class="form-control" name="username" id="username" autocomplete="off"
data-lpignore="true" value="{{ data['user']['username'] }}" placeholder="User Name" disabled>
</div>
{% end %}
<div class="form-group">
<label class="form-label" for="email">{{ translate('userConfig', 'gravEmail', data['lang'])
}}<small class="text-muted ml-1"> - {{ translate('userConfig', 'gravDesc', data['lang'])
@ -388,18 +398,43 @@ data['lang']) }}{% end %}
return (isNaN(value) ? value : +value);
}
}
const userId = new URLSearchParams(document.location.search).get('id')
$("#user_form").on("submit", async function (e) {
const userId = new URLSearchParams(document.location.search).get('id');
console.log(userId)
e.preventDefault();
let password = validateForm();
if (!password){
return;
let password = null;
if(!userId){
password = validateForm();
if (!password){
return;
}
}
const token = getCookie("_xsrf")
let userRes = await fetch(`/api/v2/users/@me`, {
method: "GET",
headers: {
'X-XSRFToken': token
},
});
let userData = await userRes.json();
let superuser = null;
if (userData.status === "ok") {
superuser = userData.data["superuser"];
edit_id = userData.data["user_id"];
} else {
bootbox.alert({
title: userData.error,
message: userData.error
});
}
let userForm = document.getElementById("user_form");
let disabled_flag = false;
let roles = $('.role_check').map(function() {
let roles = null;
if (superuser || userId != edit_id){
roles = $('.role_check').map(function() {
if ($(this).attr("disabled")){
disabled_flag = true;
}
@ -407,7 +442,6 @@ data['lang']) }}{% end %}
return $(this).val();
}
}).get();
let avail_permissions = $('.perm-name').map(function() {
return $(this).data("perm");
}).get();
@ -416,20 +450,26 @@ data['lang']) }}{% end %}
for(i=0; i < avail_permissions.length; i++){
permissions.push({"name": avail_permissions[i], "quantity": $(`#quantity_${avail_permissions[i]}`).val(), "enabled": $(`#permission_${avail_permissions[i]}`).is(':checked')})
}
console.log(permissions);
}
let formData = new FormData(userForm);
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
if(userId){
delete formDataObject.username
}
if (superuser || userId != edit_id){
if (!disabled_flag){
formDataObject.roles = roles;
}
if ($("#permissions").length){
formDataObject.permissions = permissions;
}
if(typeof password === "string"){
if(!userId){
if(typeof password === "string"){
formDataObject.password = password;
}
}
}
formDataObject.enabled = $("#enabled").is(":checked");
if ($("#superuser").is(":enabled")){
formDataObject.superuser = $("#superuser").is(":checked");

View File

@ -615,7 +615,7 @@
"pageTitleNew": "Create User",
"password": "New Password",
"permName": "Permission Name",
"repeat": "Repeat Password",
"repeat": "Confirm Password",
"roleName": "Role Name",
"selectManager": "Select Manager for User",
"super": "Super User",