mirror of
https://gitlab.com/crafty-controller/crafty-4.git
synced 2025-12-05 01:10:15 +00:00
Merge branch 'dev' into 'master'
v4.5.5 See merge request crafty-controller/crafty-4!905
This commit is contained in:
@@ -44,8 +44,7 @@ docker-build:
|
||||
mkdir -p ~/.docker/cli-plugins
|
||||
mv docker-buildx ~/.docker/cli-plugins/docker-buildx
|
||||
docker version
|
||||
- docker run --rm --privileged aptman/qus -- -r
|
||||
- docker run --rm --privileged aptman/qus -s -- -p aarch64 x86_64
|
||||
- docker run --rm --privileged tonistiigi/binfmt --install arm64,amd64
|
||||
- echo $CI_JOB_TOKEN | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
|
||||
- echo $DOCKERHUB_TOKEN | docker login -u "$DOCKERHUB_USER" --password-stdin $DOCKERHUB_REGISTRY
|
||||
- docker context create tls-environment-$CI_JOB_ID
|
||||
@@ -83,7 +82,7 @@ docker-build:
|
||||
--build-arg "CRAFTY_VER=${VERSION}" \
|
||||
--provenance false \
|
||||
$TARGS \
|
||||
--platform linux/arm64/v8,linux/amd64 \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--push .
|
||||
else
|
||||
echo "Using cache for build."
|
||||
@@ -95,7 +94,7 @@ docker-build:
|
||||
--build-arg "CRAFTY_VER=${VERSION}" \
|
||||
--provenance false \
|
||||
$TARGS \
|
||||
--platform linux/arm64/v8,linux/amd64 \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--push .
|
||||
fi
|
||||
|
||||
@@ -104,5 +103,5 @@ docker-build:
|
||||
- docker context rm tls-environment-$CI_JOB_ID || true
|
||||
- echo "Please review multi-arch manifests are present:"
|
||||
- if [ "$ENVIRONMENT_NAME" = "development" ]; then docker buildx imagetools inspect "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"; fi
|
||||
- if [ "$ENVIRONMENT_NAME" = "production" ] && [ -n "$VERSION" ]; then docker buildx imagetools inspect "$CI_REGISTRY_IMAGE:$VERSION"; fi
|
||||
- if [ "$ENVIRONMENT_NAME" = "production" ]; then docker buildx imagetools inspect "$CI_REGISTRY_IMAGE:$VERSION"; fi
|
||||
- if [ "$ENVIRONMENT_NAME" = "nightly" ]; then docker buildx imagetools inspect "$CI_REGISTRY_IMAGE:nightly"; fi
|
||||
|
||||
17
CHANGELOG.md
17
CHANGELOG.md
@@ -1,4 +1,21 @@
|
||||
# Changelog
|
||||
## --- [4.5.5] - 2025/10/14
|
||||
### Bug fixes
|
||||
- Fix MFA login failure when the totp `dict`'s attempted codes list changes size while being processed ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/899))
|
||||
- Resolve additional json being appended to downloaded files ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/902))
|
||||
- Fix certain users not showing up following a change made in `4.5.0` ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/901))
|
||||
- Fix password dialogue closing with no indicator the password did not change when the passwords did not match ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/901))
|
||||
- Fix empty 'Reason' when banning user from crafty UI ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/904))
|
||||
### Tweaks
|
||||
- Remove triple option for validation and use sole robust error ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/901))
|
||||
- Allow numeric passwords ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/901))
|
||||
- Enable use of `<enter>` key for password form submission ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/901))
|
||||
- Add extended debug logging to help troubleshoot MFA issues ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/898))
|
||||
- Change upload progress bar to monitor chunk processing ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/896))
|
||||
### Lang
|
||||
- Consolidate "passLength" & "numbericPassword" to single "passProp" translation for validators ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/901))
|
||||
<br><br>
|
||||
|
||||
## --- [4.5.4] - 2025/09/15
|
||||
### Bug fixes
|
||||
- Refactor upload chunk removal to ensure file operations are asynchronous ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/894))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[](https://craftycontrol.com)
|
||||
# Crafty Controller 4.5.4
|
||||
# Crafty Controller 4.5.5
|
||||
> Python based Control Panel for your Minecraft Server
|
||||
|
||||
## What is Crafty Controller?
|
||||
|
||||
@@ -62,11 +62,14 @@ class TOTPController:
|
||||
_type_: _description_
|
||||
"""
|
||||
user = HelperUsers.get_by_id(user_id)
|
||||
logger.debug("Validating TOTP entry for user %s", user.username)
|
||||
authenticated = False
|
||||
# Iterate through just in case a user has multiple 2FA methods
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
logger.debug("TOTP sequence: current time is %s", now)
|
||||
|
||||
# Check to see if someone is trying to reuse a key in the 60 second window
|
||||
logger.debug("TOTP sequence: checking for reused code.")
|
||||
if str(user_id) in self.used_totp_codes:
|
||||
if str(totp_code) in self.used_totp_codes[
|
||||
str(user_id)
|
||||
@@ -78,14 +81,17 @@ class TOTPController:
|
||||
)
|
||||
return authenticated
|
||||
else:
|
||||
logger.debug("TOTP sequence: No previous code dict found. Creating one")
|
||||
self.used_totp_codes[str(user_id)] = {} # Init empty dict if not in there
|
||||
|
||||
logger.debug("TOTP sequence: reused code check passed. Validating user code")
|
||||
# Store OTP as used for 60 seconds
|
||||
self.used_totp_codes[str(user_id)][str(totp_code)] = now
|
||||
|
||||
self.clear_stale_entries()
|
||||
|
||||
for totp in user.totp_user:
|
||||
logger.debug("TOTP sequence: Starting TOTP factory initialization")
|
||||
totp_factory = pyotp.TOTP(totp.totp_secret)
|
||||
if totp_factory.verify(
|
||||
totp_code,
|
||||
@@ -94,15 +100,20 @@ class TOTPController:
|
||||
):
|
||||
logger.info("Successfully verified user MFA %s", user_id)
|
||||
authenticated = True
|
||||
if not authenticated:
|
||||
logger.error("TOTP sequence: Code validation failed. Code is not valid.")
|
||||
return authenticated
|
||||
|
||||
def clear_stale_entries(self):
|
||||
"""clears out totp codes older than 1 minute when one is sent"""
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
# Clean up expired entries reclaim some memory
|
||||
for key, totp_dict in self.used_totp_codes.items():
|
||||
for item, timestamp in totp_dict.items():
|
||||
logger.debug("Checking for used codes entries older than 1 minute")
|
||||
for key, totp_dict in list(self.used_totp_codes.items()):
|
||||
# Iterate over copy of dict (list) to prevent size change during iteration
|
||||
for item, timestamp in list(totp_dict.items()):
|
||||
if now - timestamp > timedelta(seconds=60):
|
||||
logger.debug("Found saved code older than one minute. Deleting...")
|
||||
# needs to ref the self var to remove expired entries
|
||||
del self.used_totp_codes[ # pylint: disable=unnecessary-dict-index-lookup
|
||||
key
|
||||
|
||||
@@ -62,14 +62,10 @@ class UsersController:
|
||||
"password": {
|
||||
"type": "string",
|
||||
"minLength": self.helper.minimum_password_length,
|
||||
"pattern": "(?=.*[^0-9])",
|
||||
"pattern": "^.{8,}$",
|
||||
"examples": ["crafty"],
|
||||
"title": "Password",
|
||||
"error": {
|
||||
"minLength": "passLength",
|
||||
"type": "numbericPassword",
|
||||
"pattern": "numbericPassword",
|
||||
},
|
||||
"error": "passProp",
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
|
||||
@@ -716,7 +716,7 @@ class PanelHandler(BaseHandler):
|
||||
html += f"""
|
||||
<li class="playerItem banned">
|
||||
<h3>{player['name']}</h3>
|
||||
<span>Banned by {player['source']} for reason: {player['reason']}</span>
|
||||
<span>Banned by {player.get('source', '')} for reason: {player.get('reason', 'None')}</span>
|
||||
<button onclick="send_command_to_server('pardon {player['name']}')" type="button" class="btn btn-danger">Unban</button>
|
||||
</li>
|
||||
"""
|
||||
|
||||
@@ -6,6 +6,7 @@ from PIL import Image
|
||||
from app.classes.models.server_permissions import EnumPermissionsServer
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
from app.classes.web.websocket_handler import WebSocketManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
IMAGE_MIME_TYPES = [
|
||||
@@ -295,6 +296,11 @@ class ApiFilesUploadHandler(BaseApiHandler):
|
||||
if len(received_chunks) == total_chunks:
|
||||
async with await anyio.open_file(file_path, "wb") as outfile:
|
||||
for i in range(total_chunks):
|
||||
WebSocketManager().broadcast_user(
|
||||
auth_data[4]["user_id"],
|
||||
"upload_process",
|
||||
{"cur_file": i, "total_files": total_chunks, "type": u_type},
|
||||
)
|
||||
chunk_file = os.path.join(self.temp_dir, f"{self.filename}.part{i}")
|
||||
async with await anyio.open_file(chunk_file, "rb") as infile:
|
||||
await outfile.write(await infile.read())
|
||||
|
||||
@@ -934,4 +934,4 @@ class ApiServersServerFileDownload(BaseApiHandler):
|
||||
if directory_download:
|
||||
os.remove(download_path)
|
||||
|
||||
return self.finish_json(200, {"status": "ok"})
|
||||
return None
|
||||
|
||||
@@ -101,9 +101,14 @@ class ApiUsersIndexHandler(BaseApiHandler):
|
||||
offending_key = ""
|
||||
if why.schema.get("fill", None):
|
||||
offending_key = why.path[0] if why.path else None
|
||||
schema_error = why.schema.get("error", "additionalProperties")
|
||||
# We need to get the type of this for additional password property errors
|
||||
translate_key = schema_error
|
||||
if isinstance(schema_error, dict):
|
||||
translate_key = schema_error[why.validator]
|
||||
err = f"""{offending_key} {self.translator.translate(
|
||||
"validators",
|
||||
why.schema.get("error", "additionalProperties")[why.validator],
|
||||
translate_key,
|
||||
self.controller.users.get_user_lang_by_id(auth_data[4]["user_id"]),
|
||||
)} {why.schema.get("enum", "")}"""
|
||||
return self.finish_json(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"major": 4,
|
||||
"minor": 5,
|
||||
"sub": 4
|
||||
"sub": 5
|
||||
}
|
||||
|
||||
@@ -223,4 +223,16 @@ function uuidv4() {
|
||||
v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (webSocket) {
|
||||
webSocket.on('upload_process', function (data) {
|
||||
if (data.total_files === data.cur_file) {
|
||||
updateProgressBar(100, data.type, data.cur_file)
|
||||
} else {
|
||||
let progress = Math.round((data.cur_file / data.total_files) * 100, 1);
|
||||
updateProgressBar(progress, data.type, data.cur_file)
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -24,40 +24,58 @@ $(document).on("submit", ".bootbox form", function (e) {
|
||||
$(".edit_password").on("click", async function () {
|
||||
const token = getCookie("_xsrf");
|
||||
let user_id = $(this).data('id');
|
||||
bootbox.confirm(`<form class="form" id='infos' action=''>\
|
||||
bootbox.dialog({
|
||||
message: `
|
||||
<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>\
|
||||
<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' />\
|
||||
<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) {
|
||||
let 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 {
|
||||
</form>
|
||||
`,
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: "Cancel",
|
||||
className: "btn-secondary"
|
||||
},
|
||||
confirm: {
|
||||
label: "OK",
|
||||
className: "btn-primary",
|
||||
callback: function () {
|
||||
let password = validateForm();
|
||||
if (!password) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bootbox.alert({
|
||||
title: responseData.status,
|
||||
message: responseData.error_data
|
||||
});
|
||||
(async () => {
|
||||
password = password.toString();
|
||||
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);
|
||||
bootbox.hideAll();
|
||||
} else {
|
||||
bootbox.hideAll();
|
||||
bootbox.alert({
|
||||
title: responseData.status,
|
||||
message: responseData.error_data
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
$(".edit_user").on("click", function () {
|
||||
|
||||
@@ -410,10 +410,14 @@ data['lang']) }}{% end %}
|
||||
}
|
||||
}
|
||||
function replacer(key, value) {
|
||||
if (typeof value == "boolean" || key === "email" || key === "permissions" || key === "roles" || key === "password") {
|
||||
if (typeof value == "boolean" || key === "email" || key === "permissions" || key === "roles") {
|
||||
console.log(key)
|
||||
return value
|
||||
} else {
|
||||
} else if (key === "password"){
|
||||
value = value.toString()
|
||||
return value
|
||||
}
|
||||
else {
|
||||
console.log(key, value)
|
||||
return (isNaN(value) ? value : +value);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<tr id="playerItem-{{ player }}" class="playerItem--">
|
||||
<td><strong> {{ player['name'] }}</strong></td>
|
||||
<td>Banned on {{ player['banned_on'] }}</td>
|
||||
<td>Banned by : {{ player['source'] }} <br />Reason : {{ player['reason'] }}</td>
|
||||
<td>Banned by : {{ player.get('source', '') }} <br />Reason : {{ player.get('reason', 'None') }}</td>
|
||||
<td class="buttons">
|
||||
<button onclick="send_command_to_server(`pardon {{ player['name'] }}`)" type="button" class="btn btn-danger">Unban</button>
|
||||
</td>
|
||||
|
||||
@@ -182,13 +182,13 @@
|
||||
let formDataObject = Object.fromEntries(formData.entries());
|
||||
let body = {
|
||||
"username": formDataObject.username,
|
||||
"password": formDataObject.password,
|
||||
"password": (formDataObject.password).toString(),
|
||||
}
|
||||
if (formDataObject.totp != "") {
|
||||
let key = $("#2fa-type").val();
|
||||
body = {
|
||||
"username": formDataObject.username,
|
||||
"password": formDataObject.password,
|
||||
"password": (formDataObject.password).toString(),
|
||||
[key]: formDataObject.totp,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -763,8 +763,7 @@
|
||||
"filesPageLen": "length must be greater than 1 for property",
|
||||
"insufficientPerms": "Permission Error: Missing permissions for this resource",
|
||||
"mfaName": "Input must be of type string and a minimum of 3 characters for property",
|
||||
"passLength": "Password Too Short. Minimum Length: 8",
|
||||
"numbericPassword": "Numeric Password. Needs at least 1 alphabetic character",
|
||||
"passProp": "Password must be a string with a min length of 8 characters.",
|
||||
"roleManager": "Role manager must be of type integer (manager ID) or None",
|
||||
"roleName": "Role name must be a string that is greater than 1 character. It must not include any of the following symbols: [ ] , ",
|
||||
"roleServerId": "Server ID property must be a string with a minimum length of 1",
|
||||
|
||||
@@ -120,8 +120,8 @@
|
||||
"datatables": {
|
||||
"i18n": {
|
||||
"aria": {
|
||||
"sortAscending": ": activer pour trier les colonnes dans l'ordre croissant",
|
||||
"sortDescending": ": activer pour trier les colonnes dans l'ordre décroissant"
|
||||
"sortAscending": ": activer pour trier les colonnes dans l'ordre croissant",
|
||||
"sortDescending": ": activer pour trier les colonnes dans l'ordre décroissant"
|
||||
},
|
||||
"buttons": {
|
||||
"collection": "Collection <span class='ui-button-icon-primary ui-icon ui-icon-triangle-1-s'/>",
|
||||
|
||||
@@ -3,7 +3,7 @@ sonar.organization=crafty-controller
|
||||
|
||||
# This is the name and version displayed in the SonarCloud UI.
|
||||
sonar.projectName=Crafty 4
|
||||
sonar.projectVersion=4.5.4
|
||||
sonar.projectVersion=4.5.5
|
||||
sonar.python.version=3.9, 3.10, 3.11
|
||||
sonar.exclusions=app/migrations/**, app/frontend/static/assets/vendors/**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user