diff --git a/CHANGELOG.md b/CHANGELOG.md
index ba8d0a20..08219366 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,19 @@
# Changelog
+## --- [4.0.11] - 2022/08/28
+### New features
+- Add server import status indicators ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/433))
+- Users can now be assigned as manager of other users/roles ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/434))
+- Add variable shutdown timeouts ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/435))
+- Add server metrics graph ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/436))
+### Bug fixes
+- Fix creation quota not refilling after server delete ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/434))
+- Add missing bedrock dependency (libcurl.so.4) ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/437))
+### Tweaks
+- Make imports threaded ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/433))
+- Add 'Created By' Field to servers ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/434))
+- Add Zip comments to support archives ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/438))
+
+
## --- [4.0.10] - 2022/08/14
### Bug fixes
- Fix reaction tasks not firing ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/423))
diff --git a/Dockerfile b/Dockerfile
index c1bae3a6..4bd83bba 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -15,6 +15,7 @@ RUN apt-get update \
&& apt-get -y --no-install-recommends install \
sudo \
gcc \
+ libcurl4 \
python3 \
python3-dev \
python3-pip \
diff --git a/README.md b/README.md
index 1cd41069..25dc3ed7 100644
--- a/README.md
+++ b/README.md
@@ -2,11 +2,11 @@
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Supported Python Versions](https://shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20-blue)](https://www.python.org)
-[![Version(temp-hardcoded)](https://img.shields.io/badge/release-v4.0.10--beta-orange)](https://gitlab.com/crafty-controller/crafty-4/-/releases)
+[![Version(temp-hardcoded)](https://img.shields.io/badge/release-v4.0.11--beta-orange)](https://gitlab.com/crafty-controller/crafty-4/-/releases)
[![Code Quality(temp-hardcoded)](https://img.shields.io/badge/code%20quality-10-brightgreen)](https://gitlab.com/crafty-controller/crafty-4)
[![Build Status](https://gitlab.com/crafty-controller/crafty-4/badges/master/pipeline.svg)](https://gitlab.com/crafty-controller/crafty-4/-/commits/master)
-# Crafty Controller 4.0.10-beta
+# Crafty Controller 4.0.11-beta
> Python based Control Panel for your Minecraft Server
## What is Crafty Controller?
diff --git a/app/classes/controllers/crafty_perms_controller.py b/app/classes/controllers/crafty_perms_controller.py
index 9c79c33a..111e3971 100644
--- a/app/classes/controllers/crafty_perms_controller.py
+++ b/app/classes/controllers/crafty_perms_controller.py
@@ -60,26 +60,6 @@ class CraftyPermsController:
permissions_list = PermissionsCrafty.get_permissions(permissions_mask)
return permissions_list
- @staticmethod
- def add_server_creation(user_id):
- """Increase the "Server Creation" counter for this user
-
- Args:
- user_id (int): The modifiable user's ID
-
- Returns:
- int: The new count of servers created by this user
- """
- return PermissionsCrafty.add_server_creation(user_id)
-
- @staticmethod
- def add_user_creation(user_id):
- return PermissionsCrafty.add_user_creation(user_id)
-
- @staticmethod
- def add_role_creation(user_id):
- return PermissionsCrafty.add_role_creation(user_id)
-
@staticmethod
def get_api_key_permissions_list(key: ApiKeys):
return PermissionsCrafty.get_api_key_permissions_list(key)
diff --git a/app/classes/controllers/roles_controller.py b/app/classes/controllers/roles_controller.py
index 5e7925a3..ab5dcd5a 100644
--- a/app/classes/controllers/roles_controller.py
+++ b/app/classes/controllers/roles_controller.py
@@ -64,8 +64,8 @@ class RolesController:
HelperRoles.update_role(role_id, up_data)
@staticmethod
- def add_role(role_name):
- return HelperRoles.add_role(role_name)
+ def add_role(role_name, manager):
+ return HelperRoles.add_role(role_name, manager)
class RoleServerJsonType(t.TypedDict):
server_id: t.Union[str, int]
@@ -92,6 +92,7 @@ class RolesController:
def add_role_advanced(
name: str,
servers: t.Iterable[RoleServerJsonType],
+ manager: int,
) -> int:
"""Add a role with a name and a list of servers
@@ -102,7 +103,7 @@ class RolesController:
Returns:
int: The new role's ID
"""
- role_id: t.Final[int] = HelperRoles.add_role(name)
+ role_id: t.Final[int] = HelperRoles.add_role(name, manager)
for server in servers:
PermissionsServers.get_or_create(
role_id, server["server_id"], server["permissions"]
@@ -114,6 +115,7 @@ class RolesController:
role_id: t.Union[str, int],
role_name: t.Optional[str],
servers: t.Optional[t.Iterable[RoleServerJsonType]],
+ manager: int,
) -> None:
"""Update a role with a name and a list of servers
@@ -152,6 +154,7 @@ class RolesController:
up_data = {
"role_name": role_name,
"last_update": Helpers.get_time_as_string(),
+ "manager": manager,
}
# TODO: do the last_update on the db side
HelperRoles.update_role(role_id, up_data)
diff --git a/app/classes/controllers/servers_controller.py b/app/classes/controllers/servers_controller.py
index 650a16b0..a0948769 100644
--- a/app/classes/controllers/servers_controller.py
+++ b/app/classes/controllers/servers_controller.py
@@ -4,6 +4,7 @@ import time
import json
import pathlib
import typing as t
+import datetime
from app.classes.controllers.roles_controller import RolesController
from app.classes.shared.file_helpers import FileHelpers
@@ -51,6 +52,7 @@ class ServersController(metaclass=Singleton):
server_log_file: str,
server_stop: str,
server_type: str,
+ created_by: int,
server_port: int = 25565,
server_host: str = "127.0.0.1",
) -> int:
@@ -85,6 +87,7 @@ class ServersController(metaclass=Singleton):
server_log_file,
server_stop,
server_type,
+ created_by,
server_port,
server_host,
)
@@ -102,25 +105,32 @@ class ServersController(metaclass=Singleton):
server_instance.update_server_instance()
return ret
+ def get_history_stats(self, server_id):
+ max_age = self.helper.get_setting("history_max_age")
+ now = datetime.datetime.now()
+ minimum_to_exist = now - datetime.timedelta(days=max_age)
+ srv = ServersController().get_server_instance_by_id(server_id)
+ return srv.stats_helper.get_history_stats(server_id, minimum_to_exist)
+
@staticmethod
def update_unloaded_server(server_obj):
ret = HelperServers.update_server(server_obj)
return ret
@staticmethod
- def set_download(server_id):
+ def set_import(server_id):
srv = ServersController().get_server_instance_by_id(server_id)
- return srv.stats_helper.set_download()
+ return srv.stats_helper.set_import()
@staticmethod
- def finish_download(server_id):
+ def finish_import(server_id):
srv = ServersController().get_server_instance_by_id(server_id)
- return srv.stats_helper.finish_download()
+ return srv.stats_helper.finish_import()
@staticmethod
- def get_download_status(server_id):
+ def get_import_status(server_id):
server = ServersController().get_server_instance_by_id(server_id)
- return server.stats_helper.get_download_status()
+ return server.stats_helper.get_import_status()
def remove_server(self, server_id):
roles_list = PermissionsServers.get_roles_from_server(server_id)
diff --git a/app/classes/controllers/users_controller.py b/app/classes/controllers/users_controller.py
index c3c90b2f..13b8fb4f 100644
--- a/app/classes/controllers/users_controller.py
+++ b/app/classes/controllers/users_controller.py
@@ -1,7 +1,9 @@
import logging
import typing as t
+from app.classes.models.servers import HelperServers
from app.classes.models.users import HelperUsers
+from app.classes.models.roles import HelperRoles
from app.classes.models.crafty_permissions import (
PermissionsCrafty,
EnumPermissionsCrafty,
@@ -132,6 +134,18 @@ class UsersController:
def set_support_path(user_id, support_path):
HelperUsers.set_support_path(user_id, support_path)
+ @staticmethod
+ def get_managed_users(exec_user_id):
+ return HelperUsers.get_managed_users(exec_user_id)
+
+ @staticmethod
+ def get_managed_roles(exec_user_id):
+ return HelperUsers.get_managed_roles(exec_user_id)
+
+ @staticmethod
+ def get_created_servers(exec_user_id):
+ return HelperServers.get_total_owned_servers(exec_user_id)
+
def update_user(self, user_id: str, user_data=None, user_crafty_data=None):
if user_crafty_data is None:
user_crafty_data = {}
@@ -206,6 +220,7 @@ class UsersController:
def add_user(
self,
username,
+ manager,
password,
email="default@example.com",
enabled: bool = True,
@@ -213,6 +228,7 @@ class UsersController:
):
return self.users_helper.add_user(
username,
+ manager,
password=password,
email=email,
enabled=enabled,
@@ -236,6 +252,10 @@ class UsersController:
)
def remove_user(self, user_id):
+ for user in self.get_managed_users(user_id):
+ self.update_user(user.user_id, {"manager": None})
+ for role in HelperUsers.get_managed_roles(user_id):
+ HelperRoles.update_role(role.role_id, {"manager": None})
return self.users_helper.remove_user(user_id)
@staticmethod
diff --git a/app/classes/minecraft/serverjars.py b/app/classes/minecraft/serverjars.py
index a5eb11ba..1ecfc0f1 100644
--- a/app/classes/minecraft/serverjars.py
+++ b/app/classes/minecraft/serverjars.py
@@ -175,7 +175,7 @@ class ServerJars:
# we submit a db update for it's stats.
while True:
try:
- ServersController.set_download(server_id)
+ ServersController.set_import(server_id)
for user in server_users:
self.helper.websocket_helper.broadcast_user(
user, "send_start_reload", {}
@@ -190,7 +190,7 @@ class ServerJars:
try:
with open(path, "wb") as output:
shutil.copyfileobj(r.raw, output)
- ServersController.finish_download(server_id)
+ ServersController.finish_import(server_id)
for user in server_users:
self.helper.websocket_helper.broadcast_user(
@@ -203,7 +203,7 @@ class ServerJars:
return True
except Exception as e:
logger.error(f"Unable to save jar to {path} due to error:{e}")
- ServersController.finish_download(server_id)
+ ServersController.finish_import(server_id)
server_users = PermissionsServers.get_server_user_list(server_id)
for user in server_users:
self.helper.websocket_helper.broadcast_user(
diff --git a/app/classes/models/crafty_permissions.py b/app/classes/models/crafty_permissions.py
index 9b99bfb0..22383408 100644
--- a/app/classes/models/crafty_permissions.py
+++ b/app/classes/models/crafty_permissions.py
@@ -9,6 +9,7 @@ from peewee import (
)
from app.classes.models.base_model import BaseModel
+from app.classes.models.servers import HelperServers
from app.classes.models.users import Users, ApiKeys, HelperUsers
from app.classes.shared.permission_helper import PermissionHelper
@@ -23,9 +24,6 @@ class UserCrafty(BaseModel):
limit_server_creation = IntegerField(default=-1)
limit_user_creation = IntegerField(default=0)
limit_role_creation = IntegerField(default=0)
- created_server = IntegerField(default=0)
- created_user = IntegerField(default=0)
- created_role = IntegerField(default=0)
class Meta:
table_name = "user_crafty"
@@ -107,9 +105,6 @@ class PermissionsCrafty:
UserCrafty.limit_server_creation: 0,
UserCrafty.limit_user_creation: 0,
UserCrafty.limit_role_creation: 0,
- UserCrafty.created_server: 0,
- UserCrafty.created_user: 0,
- UserCrafty.created_role: 0,
}
).execute()
user_crafty = PermissionsCrafty.get_user_crafty(user_id)
@@ -159,11 +154,16 @@ class PermissionsCrafty:
@staticmethod
def get_created_quantity_list(user_id):
- user_crafty = PermissionsCrafty.get_user_crafty(user_id)
quantity_list = {
- EnumPermissionsCrafty.SERVER_CREATION.name: user_crafty.created_server,
- EnumPermissionsCrafty.USER_CONFIG.name: user_crafty.created_user,
- EnumPermissionsCrafty.ROLES_CONFIG.name: user_crafty.created_role,
+ EnumPermissionsCrafty.SERVER_CREATION.name: HelperServers.get_total_owned_servers( # pylint: disable=line-too-long
+ user_id
+ ),
+ EnumPermissionsCrafty.USER_CONFIG.name: HelperUsers.get_managed_users(
+ user_id
+ ).count(),
+ EnumPermissionsCrafty.ROLES_CONFIG.name: HelperUsers.get_managed_roles(
+ user_id
+ ).count(),
}
return quantity_list
@@ -183,31 +183,6 @@ class PermissionsCrafty:
or limit_list[permission.name] == -1
)
- @staticmethod
- def add_server_creation(user_id: int):
- """Increase the "Server Creation" counter for this user
-
- Args:
- user_id (int): The modifiable user's ID
- """
- UserCrafty.update(created_server=UserCrafty.created_server + 1).where(
- UserCrafty.user_id == user_id
- ).execute()
-
- @staticmethod
- def add_user_creation(user_id):
- user_crafty = PermissionsCrafty.get_user_crafty(user_id)
- user_crafty.created_user += 1
- UserCrafty.save(user_crafty)
- return user_crafty.created_user
-
- @staticmethod
- def add_role_creation(user_id):
- user_crafty = PermissionsCrafty.get_user_crafty(user_id)
- user_crafty.created_role += 1
- UserCrafty.save(user_crafty)
- return user_crafty.created_role
-
@staticmethod
def get_api_key_permissions_list(key: ApiKeys):
user = HelperUsers.get_user(key.user_id)
diff --git a/app/classes/models/roles.py b/app/classes/models/roles.py
index 4d61e051..541f67e8 100644
--- a/app/classes/models/roles.py
+++ b/app/classes/models/roles.py
@@ -6,6 +6,7 @@ from peewee import (
DoesNotExist,
AutoField,
DateTimeField,
+ IntegerField,
)
from playhouse.shortcuts import model_to_dict
@@ -22,6 +23,7 @@ class Roles(BaseModel):
created = DateTimeField(default=datetime.datetime.now)
last_update = DateTimeField(default=datetime.datetime.now)
role_name = CharField(default="", unique=True, index=True)
+ manager = IntegerField(null=True)
class Meta:
table_name = "roles"
@@ -71,11 +73,12 @@ class HelperRoles:
)
@staticmethod
- def add_role(role_name):
+ def add_role(role_name, manager):
role_id = Roles.insert(
{
Roles.role_name: role_name.lower(),
Roles.created: Helpers.get_time_as_string(),
+ Roles.manager: manager,
}
).execute()
return role_id
diff --git a/app/classes/models/server_stats.py b/app/classes/models/server_stats.py
index e3c943e3..6e589ffc 100644
--- a/app/classes/models/server_stats.py
+++ b/app/classes/models/server_stats.py
@@ -53,7 +53,7 @@ class ServerStats(Model):
waiting_start = BooleanField(default=False)
first_run = BooleanField(default=True)
crashed = BooleanField(default=False)
- downloading = BooleanField(default=False)
+ importing = BooleanField(default=False)
class Meta:
table_name = "server_stats"
@@ -137,6 +137,14 @@ class HelperServerStats:
)
return server_data
+ def get_history_stats(self, server_id, max_age):
+ return (
+ ServerStats.select()
+ .where(ServerStats.created > max_age)
+ .where(ServerStats.server_id == server_id)
+ .execute(self.database)
+ )
+
def insert_server_stats(self, server_stats):
server_id = server_stats.get("id", 0)
@@ -207,26 +215,26 @@ class HelperServerStats:
ServerStats.server_id == self.server_id
).execute(self.database)
- def set_download(self):
+ def set_import(self):
# self.select_database(self.server_id)
- ServerStats.update(downloading=True).where(
+ ServerStats.update(importing=True).where(
ServerStats.server_id == self.server_id
).execute(self.database)
- def finish_download(self):
+ def finish_import(self):
# self.select_database(self.server_id)
- ServerStats.update(downloading=False).where(
+ ServerStats.update(importing=False).where(
ServerStats.server_id == self.server_id
).execute(self.database)
- def get_download_status(self):
+ def get_import_status(self):
# self.select_database(self.server_id)
- download_status = (
+ import_status = (
ServerStats.select()
.where(ServerStats.server_id == self.server_id)
.get(self.database)
)
- return download_status.downloading
+ return import_status.importing
def server_crash_reset(self):
if self.server_id is None:
@@ -249,7 +257,6 @@ class HelperServerStats:
def set_update(self, value):
if self.server_id is None:
return
-
# self.select_database(self.server_id)
try:
# Checks if server even exists
diff --git a/app/classes/models/servers.py b/app/classes/models/servers.py
index 71ca4851..69d05866 100644
--- a/app/classes/models/servers.py
+++ b/app/classes/models/servers.py
@@ -38,6 +38,8 @@ class Servers(BaseModel):
logs_delete_after = IntegerField(default=0)
type = CharField(default="minecraft-java")
show_status = BooleanField(default=1)
+ created_by = IntegerField(default=-100)
+ shutdown_timeout = IntegerField(default=60)
class Meta:
table_name = "servers"
@@ -64,6 +66,7 @@ class HelperServers:
server_log_file: str,
server_stop: str,
server_type: str,
+ created_by: int,
server_port: int = 25565,
server_host: str = "127.0.0.1",
) -> int:
@@ -105,6 +108,7 @@ class HelperServers:
Servers.stop_command: server_stop,
Servers.backup_path: backup_path,
Servers.type: server_type,
+ Servers.created_by: created_by,
}
).execute()
@@ -112,6 +116,10 @@ class HelperServers:
def get_server_obj(server_id):
return Servers.get_by_id(server_id)
+ @staticmethod
+ def get_total_owned_servers(user_id):
+ return Servers.select().where(Servers.created_by == user_id).count()
+
@staticmethod
def get_server_type_by_id(server_id):
server_type = Servers.select().where(Servers.server_id == server_id).get()
diff --git a/app/classes/models/users.py b/app/classes/models/users.py
index ac204e3c..0d8e596b 100644
--- a/app/classes/models/users.py
+++ b/app/classes/models/users.py
@@ -6,6 +6,7 @@ from peewee import (
ForeignKeyField,
CharField,
AutoField,
+ IntegerField,
DateTimeField,
BooleanField,
CompositeKey,
@@ -40,6 +41,7 @@ class Users(BaseModel):
server_order = CharField(default="")
preparing = BooleanField(default=False)
hints = BooleanField(default=True)
+ manager = IntegerField(default=None, null=True)
class Meta:
table_name = "users"
@@ -138,6 +140,16 @@ class HelperUsers:
user_query = Users.select().where(Users.user_id == user_id)
return user_query
+ @staticmethod
+ def get_managed_users(exec_user_id):
+ user_query = Users.select().where(Users.manager == exec_user_id)
+ return user_query
+
+ @staticmethod
+ def get_managed_roles(exec_user_id):
+ roles_query = Roles.select().where(Roles.manager == exec_user_id)
+ return roles_query
+
@staticmethod
def get_user(user_id):
if user_id == 0:
@@ -192,6 +204,7 @@ class HelperUsers:
def add_user(
self,
username: str,
+ manager: str,
password: str = None,
email: t.Optional[str] = None,
enabled: bool = True,
@@ -209,6 +222,7 @@ class HelperUsers:
Users.enabled: enabled,
Users.superuser: superuser,
Users.created: Helpers.get_time_as_string(),
+ Users.manager: manager,
}
).execute()
return user_id
@@ -229,6 +243,7 @@ class HelperUsers:
Users.enabled: enabled,
Users.superuser: superuser,
Users.created: Helpers.get_time_as_string(),
+ Users.manager: None,
}
).execute()
return user_id
diff --git a/app/classes/shared/file_helpers.py b/app/classes/shared/file_helpers.py
index 5cd38bbf..04ec3305 100644
--- a/app/classes/shared/file_helpers.py
+++ b/app/classes/shared/file_helpers.py
@@ -27,10 +27,15 @@ class FileHelpers:
FileHelpers.del_dirs(sub)
else:
# Delete file if it is a file:
- sub.unlink()
-
- # This removes the top-level folder:
- path.rmdir()
+ try:
+ sub.unlink()
+ except:
+ logger.error(f"Unable to delete file {sub}")
+ try:
+ # This removes the top-level folder:
+ path.rmdir()
+ except:
+ logger.error("Unable to remove top level")
return True
@staticmethod
@@ -65,10 +70,13 @@ class FileHelpers:
FileHelpers.del_file(src_path)
@staticmethod
- def make_archive(path_to_destination, path_to_zip):
+ def make_archive(path_to_destination, path_to_zip, comment=""):
# create a ZipFile object
path_to_destination += ".zip"
with ZipFile(path_to_destination, "w") as zip_file:
+ zip_file.comment = bytes(
+ comment, "utf-8"
+ ) # comments over 65535 bytes will be truncated
for root, _dirs, files in os.walk(path_to_zip, topdown=True):
ziproot = path_to_zip
for file in files:
@@ -93,10 +101,13 @@ class FileHelpers:
return True
@staticmethod
- def make_compressed_archive(path_to_destination, path_to_zip):
+ def make_compressed_archive(path_to_destination, path_to_zip, comment=""):
# create a ZipFile object
path_to_destination += ".zip"
with ZipFile(path_to_destination, "w", ZIP_DEFLATED) as zip_file:
+ zip_file.comment = bytes(
+ comment, "utf-8"
+ ) # comments over 65535 bytes will be truncated
for root, _dirs, files in os.walk(path_to_zip, topdown=True):
ziproot = path_to_zip
for file in files:
@@ -122,7 +133,7 @@ class FileHelpers:
return True
def make_compressed_backup(
- self, path_to_destination, path_to_zip, excluded_dirs, server_id
+ self, path_to_destination, path_to_zip, excluded_dirs, server_id, comment=""
):
# create a ZipFile object
path_to_destination += ".zip"
@@ -140,6 +151,9 @@ class FileHelpers:
results,
)
with ZipFile(path_to_destination, "w", ZIP_DEFLATED) as zip_file:
+ zip_file.comment = bytes(
+ comment, "utf-8"
+ ) # comments over 65535 bytes will be truncated
for root, dirs, files in os.walk(path_to_zip, topdown=True):
for l_dir in dirs:
if str(os.path.join(root, l_dir)).replace("\\", "/") in ex_replace:
@@ -184,7 +198,9 @@ class FileHelpers:
return True
- def make_backup(self, path_to_destination, path_to_zip, excluded_dirs, server_id):
+ def make_backup(
+ self, path_to_destination, path_to_zip, excluded_dirs, server_id, comment=""
+ ):
# create a ZipFile object
path_to_destination += ".zip"
ex_replace = [p.replace("\\", "/") for p in excluded_dirs]
@@ -201,6 +217,9 @@ class FileHelpers:
results,
)
with ZipFile(path_to_destination, "w") as zip_file:
+ zip_file.comment = bytes(
+ comment, "utf-8"
+ ) # comments over 65535 bytes will be truncated
for root, dirs, files in os.walk(path_to_zip, topdown=True):
for l_dir in dirs:
if str(os.path.join(root, l_dir)).replace("\\", "/") in ex_replace:
diff --git a/app/classes/shared/import_helper.py b/app/classes/shared/import_helper.py
new file mode 100644
index 00000000..769ebc3a
--- /dev/null
+++ b/app/classes/shared/import_helper.py
@@ -0,0 +1,216 @@
+import os
+import time
+import shutil
+import logging
+import threading
+
+from app.classes.controllers.server_perms_controller import PermissionsServers
+from app.classes.controllers.servers_controller import ServersController
+from app.classes.shared.helpers import Helpers
+from app.classes.shared.file_helpers import FileHelpers
+
+logger = logging.getLogger(__name__)
+
+
+class ImportHelpers:
+ allowed_quotes = ['"', "'", "`"]
+
+ def __init__(self, helper, file_helper):
+ self.file_helper: FileHelpers = file_helper
+ self.helper: Helpers = helper
+
+ def import_jar_server(self, server_path, new_server_dir, port, new_id):
+ import_thread = threading.Thread(
+ target=self.import_threaded_jar_server,
+ daemon=True,
+ args=(server_path, new_server_dir, port, new_id),
+ name=f"{new_id}_import",
+ )
+ import_thread.start()
+
+ def import_threaded_jar_server(self, server_path, new_server_dir, port, new_id):
+ for item in os.listdir(server_path):
+ if not item == "db_stats":
+ try:
+ if os.path.isdir(os.path.join(server_path, item)):
+ FileHelpers.copy_dir(
+ os.path.join(server_path, item),
+ os.path.join(new_server_dir, item),
+ )
+ else:
+ FileHelpers.copy_file(
+ os.path.join(server_path, item),
+ os.path.join(new_server_dir, item),
+ )
+ except shutil.Error as ex:
+ logger.error(f"Server import failed with error: {ex}")
+
+ has_properties = False
+ for item in os.listdir(new_server_dir):
+ if str(item) == "server.properties":
+ has_properties = True
+ if not has_properties:
+ logger.info(
+ f"No server.properties found on zip file import. "
+ f"Creating one with port selection of {str(port)}"
+ )
+ with open(
+ os.path.join(new_server_dir, "server.properties"), "w", encoding="utf-8"
+ ) as file:
+ file.write(f"server-port={port}")
+ file.close()
+ time.sleep(5)
+ ServersController.finish_import(new_id)
+ server_users = PermissionsServers.get_server_user_list(new_id)
+ for user in server_users:
+ self.helper.websocket_helper.broadcast_user(user, "send_start_reload", {})
+
+ def import_java_zip_server(self, temp_dir, new_server_dir, port, new_id):
+ import_thread = threading.Thread(
+ target=self.import_threaded_java_zip_server,
+ daemon=True,
+ args=(temp_dir, new_server_dir, port, new_id),
+ name=f"{new_id}_java_zip_import",
+ )
+ import_thread.start()
+
+ def import_threaded_java_zip_server(self, temp_dir, new_server_dir, port, new_id):
+ has_properties = False
+ # extracts archive to temp directory
+ for item in os.listdir(temp_dir):
+ if str(item) == "server.properties":
+ has_properties = True
+ try:
+ if not os.path.isdir(os.path.join(temp_dir, item)):
+ FileHelpers.move_file(
+ os.path.join(temp_dir, item), os.path.join(new_server_dir, item)
+ )
+ else:
+ if item != "db_stats":
+ FileHelpers.move_dir(
+ os.path.join(temp_dir, item),
+ os.path.join(new_server_dir, item),
+ )
+ except Exception as ex:
+ logger.error(f"ERROR IN ZIP IMPORT: {ex}")
+ if not has_properties:
+ logger.info(
+ f"No server.properties found on zip file import. "
+ f"Creating one with port selection of {str(port)}"
+ )
+ with open(
+ os.path.join(new_server_dir, "server.properties"), "w", encoding="utf-8"
+ ) as file:
+ file.write(f"server-port={port}")
+ file.close()
+
+ server_users = PermissionsServers.get_server_user_list(new_id)
+ ServersController.finish_import(new_id)
+ for user in server_users:
+ self.helper.websocket_helper.broadcast_user(user, "send_start_reload", {})
+ # deletes temp dir
+ FileHelpers.del_dirs(temp_dir)
+
+ def import_bedrock_server(
+ self, server_path, new_server_dir, port, full_jar_path, new_id
+ ):
+ import_thread = threading.Thread(
+ target=self.import_threaded_bedrock_server,
+ daemon=True,
+ args=(server_path, new_server_dir, port, full_jar_path, new_id),
+ name=f"{new_id}_bedrock_import",
+ )
+ import_thread.start()
+
+ def import_threaded_bedrock_server(
+ self, server_path, new_server_dir, port, full_jar_path, new_id
+ ):
+ for item in os.listdir(server_path):
+ if not item == "db_stats":
+ try:
+ if os.path.isdir(os.path.join(server_path, item)):
+ FileHelpers.copy_dir(
+ os.path.join(server_path, item),
+ os.path.join(new_server_dir, item),
+ )
+ else:
+ FileHelpers.copy_file(
+ os.path.join(server_path, item),
+ os.path.join(new_server_dir, item),
+ )
+ except shutil.Error as ex:
+ logger.error(f"Server import failed with error: {ex}")
+
+ has_properties = False
+ for item in os.listdir(new_server_dir):
+ if str(item) == "server.properties":
+ has_properties = True
+ if not has_properties:
+ logger.info(
+ f"No server.properties found on zip file import. "
+ f"Creating one with port selection of {str(port)}"
+ )
+ with open(
+ os.path.join(new_server_dir, "server.properties"), "w", encoding="utf-8"
+ ) as file:
+ file.write(f"server-port={port}")
+ file.close()
+ if os.name != "nt":
+ if Helpers.check_file_exists(full_jar_path):
+ os.chmod(full_jar_path, 0o2760)
+ ServersController.finish_import(new_id)
+ server_users = PermissionsServers.get_server_user_list(new_id)
+ for user in server_users:
+ self.helper.websocket_helper.broadcast_user(user, "send_start_reload", {})
+
+ def import_bedrock_zip_server(
+ self, temp_dir, new_server_dir, full_jar_path, port, new_id
+ ):
+ import_thread = threading.Thread(
+ target=self.import_threaded_bedrock_zip_server,
+ daemon=True,
+ args=(temp_dir, new_server_dir, full_jar_path, port, new_id),
+ name=f"{new_id}_bedrock_import",
+ )
+ import_thread.start()
+
+ def import_threaded_bedrock_zip_server(
+ self, temp_dir, new_server_dir, full_jar_path, port, new_id
+ ):
+ has_properties = False
+ # extracts archive to temp directory
+ for item in os.listdir(temp_dir):
+ if str(item) == "server.properties":
+ has_properties = True
+ try:
+ if not os.path.isdir(os.path.join(temp_dir, item)):
+ FileHelpers.move_file(
+ os.path.join(temp_dir, item), os.path.join(new_server_dir, item)
+ )
+ else:
+ if item != "db_stats":
+ FileHelpers.move_dir(
+ os.path.join(temp_dir, item),
+ os.path.join(new_server_dir, item),
+ )
+ except Exception as ex:
+ logger.error(f"ERROR IN ZIP IMPORT: {ex}")
+ if not has_properties:
+ logger.info(
+ f"No server.properties found on zip file import. "
+ f"Creating one with port selection of {str(port)}"
+ )
+ with open(
+ os.path.join(new_server_dir, "server.properties"), "w", encoding="utf-8"
+ ) as file:
+ file.write(f"server-port={port}")
+ file.close()
+ ServersController.finish_import(new_id)
+ server_users = PermissionsServers.get_server_user_list(new_id)
+ for user in server_users:
+ self.helper.websocket_helper.broadcast_user(user, "send_start_reload", {})
+ if os.name != "nt":
+ if Helpers.check_file_exists(full_jar_path):
+ os.chmod(full_jar_path, 0o2760)
+ # deletes temp dir
+ FileHelpers.del_dirs(temp_dir)
diff --git a/app/classes/shared/main_controller.py b/app/classes/shared/main_controller.py
index bca24fe8..cfda4c8d 100644
--- a/app/classes/shared/main_controller.py
+++ b/app/classes/shared/main_controller.py
@@ -1,6 +1,7 @@
import os
import pathlib
from pathlib import Path
+from datetime import datetime
import platform
import shutil
import time
@@ -28,15 +29,17 @@ from app.classes.shared.authentication import Authentication
from app.classes.shared.console import Console
from app.classes.shared.helpers import Helpers
from app.classes.shared.file_helpers import FileHelpers
+from app.classes.shared.import_helper import ImportHelpers
from app.classes.minecraft.serverjars import ServerJars
logger = logging.getLogger(__name__)
class Controller:
- def __init__(self, database, helper, file_helper):
+ def __init__(self, database, helper, file_helper, import_helper):
self.helper: Helpers = helper
self.file_helper: FileHelpers = file_helper
+ self.import_helper: ImportHelpers = import_helper
self.server_jars: ServerJars = ServerJars(helper)
self.users_helper: HelperUsers = HelperUsers(database, self.helper)
self.roles_helper: HelperRoles = HelperRoles(database)
@@ -178,18 +181,20 @@ class Controller:
)
# Make version file .txt when we download it for support
# Most people have a default editor for .txt also more mobile friendly...
- FileHelpers.copy_file(
- os.path.join(self.project_root, "app", "config", "version.json"),
- os.path.join(temp_dir, "crafty_sys_info.txt"),
+ sys_info_string = (
+ f"Crafty v{self.helper.get_version_string()} Support Logs\n"
+ f"\n"
+ f"OS Info:- \n"
+ f"OS: {str(platform.system())}\n"
+ f"Version: {str(platform.release())}"
+ f"\n \n"
+ f"Log archive created on: {datetime.now()}"
)
with open(
os.path.join(temp_dir, "crafty_sys_info.txt"), "a", encoding="utf-8"
) as f:
- f.write("\n")
- f.write("OS Info:\n")
- f.write("OS: " + str(platform.system()) + "\n")
- f.write("Version: " + str(platform.release()))
- FileHelpers.make_compressed_archive(temp_zip_storage, temp_dir)
+ f.write(sys_info_string)
+ FileHelpers.make_compressed_archive(temp_zip_storage, temp_dir, sys_info_string)
if len(self.helper.websocket_helper.clients) > 0:
self.helper.websocket_helper.broadcast_user(
exec_user["user_id"],
@@ -244,7 +249,7 @@ class Controller:
except:
return {"percent": 0, "total_files": 0}
- def create_api_server(self, data: dict):
+ def create_api_server(self, data: dict, user_id):
server_fs_uuid = Helpers.create_uuid()
new_server_path = os.path.join(self.helper.servers_dir, server_fs_uuid)
backup_path = os.path.join(self.helper.backup_path, server_fs_uuid)
@@ -307,7 +312,9 @@ class Controller:
# TODO: Copy files from the zip file to the new server directory
server_file = create_data["jarfile"]
raise Exception("Not yet implemented")
- _create_server_properties_if_needed(create_data["server_properties_port"])
+ _create_server_properties_if_needed(
+ create_data["server_properties_port"],
+ )
min_mem = create_data["mem_min"]
max_mem = create_data["mem_max"]
@@ -403,6 +410,7 @@ class Controller:
server_log_file=log_location,
server_stop=stop_command,
server_port=monitoring_port,
+ created_by=user_id,
server_host=monitoring_host,
server_type=monitoring_type,
)
@@ -429,6 +437,7 @@ class Controller:
min_mem: int,
max_mem: int,
port: int,
+ user_id: int,
):
server_id = Helpers.create_uuid()
server_dir = os.path.join(self.helper.servers_dir, server_id)
@@ -489,6 +498,7 @@ class Controller:
server_log_file,
server_stop,
port,
+ user_id,
server_type="minecraft-java",
)
@@ -524,6 +534,7 @@ class Controller:
min_mem: int,
max_mem: int,
port: int,
+ user_id: int,
):
server_id = Helpers.create_uuid()
new_server_dir = os.path.join(self.helper.servers_dir, server_id)
@@ -537,25 +548,6 @@ class Controller:
Helpers.ensure_dir_exists(new_server_dir)
Helpers.ensure_dir_exists(backup_path)
server_path = Helpers.get_os_understandable_path(server_path)
- try:
- FileHelpers.copy_dir(server_path, new_server_dir, True)
- except shutil.Error as ex:
- logger.error(f"Server import failed with error: {ex}")
-
- has_properties = False
- for item in os.listdir(new_server_dir):
- if str(item) == "server.properties":
- has_properties = True
- if not has_properties:
- logger.info(
- f"No server.properties found on zip file import. "
- f"Creating one with port selection of {str(port)}"
- )
- with open(
- os.path.join(new_server_dir, "server.properties"), "w", encoding="utf-8"
- ) as file:
- file.write(f"server-port={port}")
- file.close()
full_jar_path = os.path.join(new_server_dir, server_jar)
@@ -584,8 +576,11 @@ class Controller:
server_log_file,
server_stop,
port,
+ user_id,
server_type="minecraft-java",
)
+ ServersController.set_import(new_id)
+ self.import_helper.import_jar_server(server_path, new_server_dir, port, new_id)
return new_id
def import_zip_server(
@@ -596,6 +591,7 @@ class Controller:
min_mem: int,
max_mem: int,
port: int,
+ user_id: int,
):
server_id = Helpers.create_uuid()
new_server_dir = os.path.join(self.helper.servers_dir, server_id)
@@ -609,32 +605,6 @@ class Controller:
temp_dir = Helpers.get_os_understandable_path(zip_path)
Helpers.ensure_dir_exists(new_server_dir)
Helpers.ensure_dir_exists(backup_path)
- has_properties = False
- # extracts archive to temp directory
- for item in os.listdir(temp_dir):
- if str(item) == "server.properties":
- has_properties = True
- try:
- if not os.path.isdir(os.path.join(temp_dir, item)):
- FileHelpers.move_file(
- os.path.join(temp_dir, item), os.path.join(new_server_dir, item)
- )
- else:
- FileHelpers.move_dir(
- os.path.join(temp_dir, item), os.path.join(new_server_dir, item)
- )
- except Exception as ex:
- logger.error(f"ERROR IN ZIP IMPORT: {ex}")
- if not has_properties:
- logger.info(
- f"No server.properties found on zip file import. "
- f"Creating one with port selection of {str(port)}"
- )
- with open(
- os.path.join(new_server_dir, "server.properties"), "w", encoding="utf-8"
- ) as file:
- file.write(f"server-port={port}")
- file.close()
full_jar_path = os.path.join(new_server_dir, server_jar)
@@ -664,8 +634,13 @@ class Controller:
server_log_file,
server_stop,
port,
+ user_id,
server_type="minecraft-java",
)
+ ServersController.set_import(new_id)
+ self.import_helper.import_java_zip_server(
+ temp_dir, new_server_dir, port, new_id
+ )
return new_id
# **********************************************************************************
@@ -673,7 +648,12 @@ class Controller:
# **********************************************************************************
def import_bedrock_server(
- self, server_name: str, server_path: str, server_exe: str, port: int
+ self,
+ server_name: str,
+ server_path: str,
+ server_exe: str,
+ port: int,
+ user_id: int,
):
server_id = Helpers.create_uuid()
new_server_dir = os.path.join(self.helper.servers_dir, server_id)
@@ -687,25 +667,6 @@ class Controller:
Helpers.ensure_dir_exists(new_server_dir)
Helpers.ensure_dir_exists(backup_path)
server_path = Helpers.get_os_understandable_path(server_path)
- try:
- FileHelpers.copy_dir(server_path, new_server_dir, True)
- except shutil.Error as ex:
- logger.error(f"Server import failed with error: {ex}")
-
- has_properties = False
- for item in os.listdir(new_server_dir):
- if str(item) == "server.properties":
- has_properties = True
- if not has_properties:
- logger.info(
- f"No server.properties found on zip file import. "
- f"Creating one with port selection of {str(port)}"
- )
- with open(
- os.path.join(new_server_dir, "server.properties"), "w", encoding="utf-8"
- ) as file:
- file.write(f"server-port={port}")
- file.close()
full_jar_path = os.path.join(new_server_dir, server_exe)
@@ -727,15 +688,22 @@ class Controller:
server_log_file,
server_stop,
port,
+ user_id,
server_type="minecraft-bedrock",
)
- if os.name != "nt":
- if Helpers.check_file_exists(full_jar_path):
- os.chmod(full_jar_path, 0o2760)
+ ServersController.set_import(new_id)
+ self.import_helper.import_bedrock_server(
+ server_path, new_server_dir, port, full_jar_path, new_id
+ )
return new_id
def import_bedrock_zip_server(
- self, server_name: str, zip_path: str, server_exe: str, port: int
+ self,
+ server_name: str,
+ zip_path: str,
+ server_exe: str,
+ port: int,
+ user_id: int,
):
server_id = Helpers.create_uuid()
new_server_dir = os.path.join(self.helper.servers_dir, server_id)
@@ -749,32 +717,6 @@ class Controller:
temp_dir = Helpers.get_os_understandable_path(zip_path)
Helpers.ensure_dir_exists(new_server_dir)
Helpers.ensure_dir_exists(backup_path)
- has_properties = False
- # extracts archive to temp directory
- for item in os.listdir(temp_dir):
- if str(item) == "server.properties":
- has_properties = True
- try:
- if not os.path.isdir(os.path.join(temp_dir, item)):
- FileHelpers.move_file(
- os.path.join(temp_dir, item), os.path.join(new_server_dir, item)
- )
- else:
- FileHelpers.move_dir(
- os.path.join(temp_dir, item), os.path.join(new_server_dir, item)
- )
- except Exception as ex:
- logger.error(f"ERROR IN ZIP IMPORT: {ex}")
- if not has_properties:
- logger.info(
- f"No server.properties found on zip file import. "
- f"Creating one with port selection of {str(port)}"
- )
- with open(
- os.path.join(new_server_dir, "server.properties"), "w", encoding="utf-8"
- ) as file:
- file.write(f"server-port={port}")
- file.close()
full_jar_path = os.path.join(new_server_dir, server_exe)
@@ -796,8 +738,12 @@ class Controller:
server_log_file,
server_stop,
port,
+ user_id,
server_type="minecraft-bedrock",
)
+ self.import_helper.import_bedrock_zip_server(
+ temp_dir, new_server_dir, full_jar_path, port, new_id
+ )
if os.name != "nt":
if Helpers.check_file_exists(full_jar_path):
os.chmod(full_jar_path, 0o2760)
@@ -838,6 +784,7 @@ class Controller:
server_log_file: str,
server_stop: str,
server_port: int,
+ created_by: int,
server_type: str,
server_host: str = "127.0.0.1",
):
@@ -852,6 +799,7 @@ class Controller:
server_log_file,
server_stop,
server_type,
+ created_by,
server_port,
server_host,
)
diff --git a/app/classes/shared/main_models.py b/app/classes/shared/main_models.py
index 73d1d484..7c43a131 100644
--- a/app/classes/shared/main_models.py
+++ b/app/classes/shared/main_models.py
@@ -26,6 +26,7 @@ class DatabaseBuilder:
password=password,
email="default@example.com",
superuser=True,
+ manager=None,
)
def is_fresh_install(self):
diff --git a/app/classes/shared/server.py b/app/classes/shared/server.py
index 499a0080..0b2c5b96 100644
--- a/app/classes/shared/server.py
+++ b/app/classes/shared/server.py
@@ -293,7 +293,7 @@ class ServerInstance:
else:
user_lang = HelperUsers.get_user_lang_by_id(user_id)
- if self.stats_helper.get_download_status():
+ if self.stats_helper.get_import_status():
if user_id:
self.helper.websocket_helper.broadcast_user(
user_id,
@@ -612,21 +612,25 @@ class ServerInstance:
# caching the name and pid number
server_name = self.name
server_pid = self.process.pid
+ self.shutdown_timeout = self.settings["shutdown_timeout"]
while running:
i += 1
- logstr = (
- f"Server {server_name} is still running "
- f"- waiting 2s to see if it stops ({int(60-(i*2))} "
- f"seconds until force close)"
- )
- logger.info(logstr)
- Console.info(logstr)
+ ttk = int(self.shutdown_timeout - (i * 2))
+ if i <= self.shutdown_timeout / 2:
+ logstr = (
+ f"Server {server_name} is still running "
+ "- waiting 2s to see if it stops"
+ f"({ttk} "
+ f"seconds until force close)"
+ )
+ logger.info(logstr)
+ Console.info(logstr)
running = self.check_running()
time.sleep(2)
# if we haven't closed in 60 seconds, let's just slam down on the PID
- if i >= 30:
+ if i >= round(self.shutdown_timeout / 2, 0):
logger.info(
f"Server {server_name} is still running - Forcing the process down"
)
@@ -1223,6 +1227,7 @@ class ServerInstance:
"version": raw_ping_result.get("version"),
"icon": raw_ping_result.get("icon"),
"crashed": self.is_crashed,
+ "created": datetime.datetime.now().strftime("%Y/%m/%d, %H:%M:%S"),
},
)
total_players += int(raw_ping_result.get("online"))
diff --git a/app/classes/web/ajax_handler.py b/app/classes/web/ajax_handler.py
index 9aa41d28..bd8f9424 100644
--- a/app/classes/web/ajax_handler.py
+++ b/app/classes/web/ajax_handler.py
@@ -394,6 +394,7 @@ class AjaxHandler(BaseHandler):
"1",
"2",
server_data["server_port"],
+ server_data["created_by"],
)
new_server_id = new_server
new_server = self.controller.servers.get_server_data(new_server)
@@ -416,6 +417,7 @@ class AjaxHandler(BaseHandler):
temp_dir,
server_data["executable"],
server_data["server_port"],
+ server_data["created_by"],
)
new_server_id = new_server
new_server = self.controller.servers.get_server_data(new_server)
diff --git a/app/classes/web/api_handler.py b/app/classes/web/api_handler.py
index 43af4ae8..34b09ee8 100644
--- a/app/classes/web/api_handler.py
+++ b/app/classes/web/api_handler.py
@@ -340,10 +340,11 @@ class CreateUser(ApiHandler):
new_username = self.get_argument("username").lower()
new_pass = self.get_argument("password")
+ manager = int(user_obj["user_id"])
if new_username:
self.controller.users.add_user(
- new_username, new_pass, "default@example.com", True, False
+ new_username, manager, new_pass, "default@example.com", True, False
)
self.return_response(
diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py
index a5e56029..a8bac6e2 100644
--- a/app/classes/web/panel_handler.py
+++ b/app/classes/web/panel_handler.py
@@ -453,8 +453,8 @@ class PanelHandler(BaseHandler):
for server in un_used_servers[:]:
if flag == 0:
server["stats"][
- "downloading"
- ] = self.controller.servers.get_download_status(
+ "importing"
+ ] = self.controller.servers.get_import_status(
str(server["stats"]["server_id"]["server_id"])
)
server["stats"]["crashed"] = self.controller.servers.is_crashed(
@@ -524,6 +524,7 @@ class PanelHandler(BaseHandler):
"files",
"admin_controls",
"schedules",
+ "metrics",
]
if not self.failed_server:
server = self.controller.servers.get_server_instance_by_id(server_id)
@@ -571,11 +572,11 @@ class PanelHandler(BaseHandler):
"started": "False",
}
if not self.failed_server:
- page_data["downloading"] = self.controller.servers.get_download_status(
+ page_data["importing"] = self.controller.servers.get_import_status(
server_id
)
else:
- page_data["downloading"] = False
+ page_data["importing"] = False
page_data["server_id"] = server_id
try:
page_data["waiting_start"] = self.controller.servers.get_waiting_start(
@@ -754,6 +755,11 @@ class PanelHandler(BaseHandler):
page_data["backup_list"] = []
page_data["backup_path"] = Helpers.wtol_path(server_info["backup_path"])
+ if subpage == "metrics":
+ page_data["history_stats"] = self.controller.servers.get_history_stats(
+ server_id
+ )
+
def get_banned_players_html():
banned_players = self.controller.servers.get_banned_players(server_id)
if banned_players is None:
@@ -860,6 +866,18 @@ class PanelHandler(BaseHandler):
page_data["users"] = self.controller.users.get_all_users()
page_data["roles"] = self.controller.roles.get_all_roles()
page_data["auth-servers"][user.user_id] = super_auth_servers
+ page_data["managed_users"] = []
+ else:
+ page_data["managed_users"] = self.controller.users.get_managed_users(
+ exec_user["user_id"]
+ )
+ page_data["assigned_roles"] = []
+ for item in page_data["roles"]:
+ page_data["assigned_roles"].append(item.role_id)
+
+ page_data["managed_roles"] = self.controller.users.get_managed_roles(
+ exec_user["user_id"]
+ )
template = "panel/panel_config.html"
@@ -885,7 +903,7 @@ class PanelHandler(BaseHandler):
)
return
- page_data["roles_all"] = self.controller.roles.get_all_roles()
+ page_data["roles"] = self.controller.roles.get_all_roles()
page_data["servers"] = []
page_data["servers_all"] = self.controller.servers.get_all_defined_servers()
page_data["role-servers"] = []
@@ -904,8 +922,16 @@ class PanelHandler(BaseHandler):
)
if superuser:
page_data["super-disabled"] = ""
+ page_data["users"] = self.controller.users.get_all_users()
else:
page_data["super-disabled"] = "disabled"
+
+ page_data["exec_user"] = exec_user["user_id"]
+
+ page_data["manager"] = {
+ "user_id": -100,
+ "username": "None",
+ }
for file in sorted(
os.listdir(os.path.join(self.helper.root_dir, "app", "translations"))
):
@@ -1074,9 +1100,21 @@ class PanelHandler(BaseHandler):
page_data["user"] = self.controller.users.get_user_by_id(user_id)
page_data["servers"] = set()
page_data["role-servers"] = page_role_servers
- page_data["roles_all"] = self.controller.roles.get_all_roles()
+ page_data["roles"] = self.controller.roles.get_all_roles()
+ page_data["exec_user"] = exec_user["user_id"]
page_data["servers_all"] = self.controller.servers.get_all_defined_servers()
page_data["superuser"] = superuser
+ if page_data["user"]["manager"] is not None:
+ page_data["manager"] = self.controller.users.get_user_by_id(
+ page_data["user"]["manager"]
+ )
+ else:
+ page_data["manager"] = {
+ "user_id": -100,
+ "username": "None",
+ }
+ if exec_user["superuser"]:
+ page_data["users"] = self.controller.users.get_all_users()
page_data[
"permissions_all"
] = self.controller.crafty_perms.list_defined_crafty_permissions()
@@ -1115,6 +1153,17 @@ class PanelHandler(BaseHandler):
"/panel/error?error=Unauthorized access: not a user editor"
)
return
+ if (
+ (
+ self.controller.users.get_user_by_id(user_id)["manager"]
+ != exec_user["user_id"]
+ )
+ and not exec_user["superuser"]
+ and str(exec_user["user_id"]) != str(user_id)
+ ):
+ self.redirect(
+ "/panel/error?error=Unauthorized access: you cannot edit this user"
+ )
page_data["servers"] = []
page_data["role-servers"] = []
@@ -1212,6 +1261,11 @@ class PanelHandler(BaseHandler):
defined_servers = self.controller.servers.get_authorized_servers(
exec_user["user_id"]
)
+
+ page_data["role_manager"] = {
+ "user_id": -100,
+ "username": "None",
+ }
page_servers = []
for server in defined_servers:
if server not in page_servers:
@@ -1229,6 +1283,7 @@ class PanelHandler(BaseHandler):
user_roles = self.get_user_roles()
page_data["new_role"] = False
role_id = self.get_argument("id", None)
+ role = self.controller.roles.get_role(role_id)
page_data["role"] = self.controller.roles.get_role_with_servers(role_id)
if exec_user["superuser"]:
defined_servers = self.controller.servers.list_defined_servers()
@@ -1252,7 +1307,21 @@ class PanelHandler(BaseHandler):
page_data["user-roles"] = user_roles
page_data["users"] = self.controller.users.get_all_users()
- if EnumPermissionsCrafty.ROLES_CONFIG not in exec_user_crafty_permissions:
+ if page_data["role"]["manager"] is not None:
+ page_data["role_manager"] = self.controller.users.get_user_by_id(
+ page_data["role"]["manager"]
+ )
+ else:
+ page_data["role_manager"] = {
+ "user_id": -100,
+ "username": "None",
+ }
+
+ if (
+ EnumPermissionsCrafty.ROLES_CONFIG not in exec_user_crafty_permissions
+ or exec_user["user_id"] != role["manager"]
+ and not exec_user["superuser"]
+ ):
self.redirect(
"/panel/error?error=Unauthorized access: not a role editor"
)
@@ -1266,8 +1335,15 @@ class PanelHandler(BaseHandler):
elif page == "remove_role":
role_id = bleach.clean(self.get_argument("id", None))
- if not superuser:
- self.redirect("/panel/error?error=Unauthorized access: not superuser")
+ if (
+ not superuser
+ and self.controller.roles.get_role(role_id)["manager"]
+ != exec_user["user_id"]
+ ):
+ self.redirect(
+ "/panel/error?error=Unauthorized access: not superuser not"
+ " role manager"
+ )
return
if role_id is None:
self.redirect("/panel/error?error=Invalid Role ID")
@@ -1418,6 +1494,7 @@ class PanelHandler(BaseHandler):
return
server_name = self.get_argument("server_name", None)
server_obj = self.controller.servers.get_server_obj(server_id)
+ shutdown_timeout = self.get_argument("shutdown_timeout", 60)
if superuser:
server_path = self.get_argument("server_path", None)
if Helpers.is_os_windows():
@@ -1498,6 +1575,7 @@ class PanelHandler(BaseHandler):
)
server_obj.server_name = server_name
+ server_obj.shutdown_timeout = shutdown_timeout
if superuser:
if Helpers.validate_traversal(
self.helper.get_servers_root_dir(), server_path
@@ -1930,6 +2008,7 @@ class PanelHandler(BaseHandler):
"system user is not editable"
)
user_id = bleach.clean(self.get_argument("id", None))
+ user = self.controller.users.get_user_by_id(user_id)
username = bleach.clean(self.get_argument("username", None).lower())
if (
username != self.controller.users.get_user_by_id(user_id)["username"]
@@ -1962,7 +2041,19 @@ class PanelHandler(BaseHandler):
else:
superuser = 0
- if not exec_user["superuser"]:
+ if exec_user["superuser"]:
+ manager = self.get_argument("manager")
+ if manager == "":
+ manager = None
+ else:
+ manager = int(manager)
+ else:
+ manager = user["manager"]
+
+ if (
+ not exec_user["superuser"]
+ and int(exec_user["user_id"]) != user["manager"]
+ ):
if username is None or username == "":
self.redirect("/panel/error?error=Invalid username")
return
@@ -2013,6 +2104,7 @@ class PanelHandler(BaseHandler):
user_data = {
"username": username,
+ "manager": manager,
"password": password0,
"email": email,
"enabled": enabled,
@@ -2029,13 +2121,13 @@ class PanelHandler(BaseHandler):
user_id, user_data=user_data, user_crafty_data=user_crafty_data
)
- self.controller.management.add_to_audit_log(
- exec_user["user_id"],
- f"Edited user {username} (UID:{user_id}) with roles {roles} "
- f"and permissions {permissions_mask}",
- server_id=0,
- source_ip=self.get_remote_ip(),
- )
+ self.controller.management.add_to_audit_log(
+ exec_user["user_id"],
+ f"Edited user {username} (UID:{user_id}) with roles {roles} "
+ f"and permissions {permissions_mask}",
+ server_id=0,
+ source_ip=self.get_remote_ip(),
+ )
self.redirect("/panel/panel_config")
elif page == "edit_user_apikeys":
@@ -2158,6 +2250,15 @@ class PanelHandler(BaseHandler):
if username is None or username == "":
self.redirect("/panel/error?error=Invalid username")
return
+
+ if exec_user["superuser"]:
+ manager = self.get_argument("manager")
+ if manager == "":
+ manager = None
+ else:
+ manager = int(manager)
+ else:
+ manager = int(exec_user["user_id"])
# does this user id exist?
if self.controller.users.get_id_by_name(username) is not None:
self.redirect("/panel/error?error=User exists")
@@ -2172,6 +2273,7 @@ class PanelHandler(BaseHandler):
user_id = self.controller.users.add_user(
username,
+ manager=manager,
password=password0,
email=email,
enabled=enabled,
@@ -2198,14 +2300,19 @@ class PanelHandler(BaseHandler):
server_id=0,
source_ip=self.get_remote_ip(),
)
- self.controller.crafty_perms.add_user_creation(exec_user["user_id"])
self.redirect("/panel/panel_config")
elif page == "edit_role":
role_id = bleach.clean(self.get_argument("id", None))
role_name = bleach.clean(self.get_argument("role_name", None))
- if EnumPermissionsCrafty.ROLES_CONFIG not in exec_user_crafty_permissions:
+ role = self.controller.roles.get_role(role_id)
+
+ if (
+ EnumPermissionsCrafty.ROLES_CONFIG not in exec_user_crafty_permissions
+ and exec_user["user_id"] != role["manager"]
+ and not exec_user["superuser"]
+ ):
self.redirect(
"/panel/error?error=Unauthorized access: not a role editor"
)
@@ -2221,9 +2328,18 @@ class PanelHandler(BaseHandler):
self.redirect("/panel/error?error=Invalid Role ID")
return
+ if exec_user["superuser"]:
+ manager = self.get_argument("manager", None)
+ if manager == "":
+ manager = None
+ else:
+ manager = role["manager"]
+
servers = self.get_role_servers()
- self.controller.roles.update_role_advanced(role_id, role_name, servers)
+ self.controller.roles.update_role_advanced(
+ role_id, role_name, servers, manager
+ )
self.controller.management.add_to_audit_log(
exec_user["user_id"],
@@ -2235,6 +2351,12 @@ class PanelHandler(BaseHandler):
elif page == "add_role":
role_name = bleach.clean(self.get_argument("role_name", None))
+ if exec_user["superuser"]:
+ manager = self.get_argument("manager", None)
+ if manager == "":
+ manager = None
+ else:
+ manager = exec_user["user_id"]
if EnumPermissionsCrafty.ROLES_CONFIG not in exec_user_crafty_permissions:
self.redirect(
@@ -2259,7 +2381,9 @@ class PanelHandler(BaseHandler):
servers = self.get_role_servers()
- role_id = self.controller.roles.add_role_advanced(role_name, servers)
+ role_id = self.controller.roles.add_role_advanced(
+ role_name, servers, manager
+ )
self.controller.management.add_to_audit_log(
exec_user["user_id"],
@@ -2267,7 +2391,6 @@ class PanelHandler(BaseHandler):
server_id=0,
source_ip=self.get_remote_ip(),
)
- self.controller.crafty_perms.add_role_creation(exec_user["user_id"])
self.redirect("/panel/panel_config")
else:
diff --git a/app/classes/web/routes/api/roles/index.py b/app/classes/web/routes/api/roles/index.py
index 2ca1baf3..150bff0c 100644
--- a/app/classes/web/routes/api/roles/index.py
+++ b/app/classes/web/routes/api/roles/index.py
@@ -116,7 +116,9 @@ class ApiRolesIndexHandler(BaseApiHandler):
400, {"status": "error", "error": "ROLE_NAME_ALREADY_EXISTS"}
)
- role_id = self.controller.roles.add_role_advanced(role_name, servers)
+ role_id = self.controller.roles.add_role_advanced(
+ role_name, servers, user["user_id"]
+ )
self.controller.management.add_to_audit_log(
user["user_id"],
diff --git a/app/classes/web/routes/api/servers/index.py b/app/classes/web/routes/api/servers/index.py
index 7db12f45..b94b4c01 100644
--- a/app/classes/web/routes/api/servers/index.py
+++ b/app/classes/web/routes/api/servers/index.py
@@ -665,10 +665,9 @@ class ApiServersIndexHandler(BaseApiHandler):
},
)
- new_server_id, new_server_uuid = self.controller.create_api_server(data)
-
- # Increase the server creation counter
- self.controller.crafty_perms.add_server_creation(user["user_id"])
+ new_server_id, new_server_uuid = self.controller.create_api_server(
+ data, user["user_id"]
+ )
self.controller.servers.stats.record_stats()
diff --git a/app/classes/web/routes/api/servers/server/action.py b/app/classes/web/routes/api/servers/server/action.py
index cf9163b9..e5b3ae23 100644
--- a/app/classes/web/routes/api/servers/server/action.py
+++ b/app/classes/web/routes/api/servers/server/action.py
@@ -84,6 +84,7 @@ class ApiServersServerActionHandler(BaseApiHandler):
new_server_log_file,
server_data.get("stop_command"),
server_data.get("type"),
+ user_id,
server_data.get("server_port"),
)
diff --git a/app/classes/web/routes/api/users/index.py b/app/classes/web/routes/api/users/index.py
index 3e4cfdab..6f46740e 100644
--- a/app/classes/web/routes/api/users/index.py
+++ b/app/classes/web/routes/api/users/index.py
@@ -96,6 +96,7 @@ class ApiUsersIndexHandler(BaseApiHandler):
username = data["username"]
username = str(username).lower()
+ manager = int(user["user_id"])
password = data["password"]
email = data.get("email", "default@example.com")
enabled = data.get("enabled", True)
@@ -149,6 +150,7 @@ class ApiUsersIndexHandler(BaseApiHandler):
# TODO: do this in the most efficient way
user_id = self.controller.users.add_user(
username,
+ manager,
password,
email,
enabled,
diff --git a/app/classes/web/server_handler.py b/app/classes/web/server_handler.py
index df4ba684..c13198ed 100644
--- a/app/classes/web/server_handler.py
+++ b/app/classes/web/server_handler.py
@@ -1,6 +1,7 @@
import json
import logging
import os
+import time
import tornado.web
import tornado.escape
import bleach
@@ -224,6 +225,23 @@ class ServerHandler(BaseHandler):
if server_id is not None:
if command == "clone_server":
+ if (
+ not superuser
+ and not self.controller.crafty_perms.can_create_server(
+ exec_user["user_id"]
+ )
+ ):
+ time.sleep(3)
+ self.helper.websocket_helper.broadcast_user(
+ exec_user["user_id"],
+ "send_start_error",
+ {
+ "error": ""
+ " Not a server creator or server limit reached."
+ },
+ )
+ return
def is_name_used(name):
for server in self.controller.servers.get_all_defined_servers():
@@ -231,6 +249,7 @@ class ServerHandler(BaseHandler):
return True
return
+ template = "/panel/dashboard"
server_data = self.controller.servers.get_server_data_by_id(
server_id
)
@@ -265,6 +284,7 @@ class ServerHandler(BaseHandler):
backup_path = os.path.join(self.helper.backup_path, new_server_uuid)
server_port = server_data.get("server_port")
server_type = server_data.get("type")
+ created_by = exec_user["user_id"]
new_server_id = self.controller.servers.create_server(
new_server_name,
@@ -276,6 +296,7 @@ class ServerHandler(BaseHandler):
new_server_log_file,
stop_command,
server_type,
+ created_by,
server_port,
)
if not exec_user["superuser"]:
@@ -283,7 +304,8 @@ class ServerHandler(BaseHandler):
new_server_id
).get("server_uuid")
role_id = self.controller.roles.add_role(
- f"Creator of Server with uuid={new_server_uuid}"
+ f"Creator of Server with uuid={new_server_uuid}",
+ exec_user["user_id"],
)
self.controller.server_perms.add_role_server(
new_server_id, role_id, "11111111"
@@ -291,9 +313,6 @@ class ServerHandler(BaseHandler):
self.controller.users.add_role_to_user(
exec_user["user_id"], role_id
)
- self.controller.crafty_perms.add_server_creation(
- exec_user["user_id"]
- )
self.controller.servers.init_all_servers()
@@ -353,6 +372,7 @@ class ServerHandler(BaseHandler):
min_mem,
max_mem,
port,
+ exec_user["user_id"],
)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
@@ -369,7 +389,13 @@ class ServerHandler(BaseHandler):
return
new_server_id = self.controller.import_zip_server(
- server_name, zip_path, import_server_jar, min_mem, max_mem, port
+ server_name,
+ zip_path,
+ import_server_jar,
+ min_mem,
+ max_mem,
+ port,
+ exec_user["user_id"],
)
if new_server_id == "false":
self.redirect(
@@ -385,8 +411,6 @@ class ServerHandler(BaseHandler):
new_server_id,
self.get_remote_ip(),
)
- # deletes temp dir
- FileHelpers.del_dirs(zip_path)
else:
if len(server_parts) != 3:
self.redirect("/panel/error?error=Invalid server data")
@@ -402,6 +426,7 @@ class ServerHandler(BaseHandler):
min_mem,
max_mem,
port,
+ exec_user["user_id"],
)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
@@ -420,7 +445,8 @@ class ServerHandler(BaseHandler):
new_server_id
).get("server_uuid")
role_id = self.controller.roles.add_role(
- f"Creator of Server with uuid={new_server_uuid}"
+ f"Creator of Server with uuid={new_server_uuid}",
+ exec_user["user_id"],
)
self.controller.server_perms.add_role_server(
new_server_id, role_id, "11111111"
@@ -428,9 +454,6 @@ class ServerHandler(BaseHandler):
self.controller.users.add_role_to_user(
exec_user["user_id"], role_id
)
- self.controller.crafty_perms.add_server_creation(
- exec_user["user_id"]
- )
else:
for role in captured_roles:
@@ -483,7 +506,11 @@ class ServerHandler(BaseHandler):
return
new_server_id = self.controller.import_bedrock_server(
- server_name, import_server_path, import_server_exe, port
+ server_name,
+ import_server_path,
+ import_server_exe,
+ port,
+ exec_user["user_id"],
)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
@@ -500,7 +527,11 @@ class ServerHandler(BaseHandler):
return
new_server_id = self.controller.import_bedrock_zip_server(
- server_name, zip_path, import_server_exe, port
+ server_name,
+ zip_path,
+ import_server_exe,
+ port,
+ exec_user["user_id"],
)
if new_server_id == "false":
self.redirect(
@@ -516,8 +547,6 @@ class ServerHandler(BaseHandler):
new_server_id,
self.get_remote_ip(),
)
- # deletes temp dir
- FileHelpers.del_dirs(zip_path)
else:
if len(server_parts) != 2:
self.redirect("/panel/error?error=Invalid server data")
@@ -526,7 +555,13 @@ class ServerHandler(BaseHandler):
# TODO: add server type check here and call the correct server
# add functions if not a jar
new_server_id = self.controller.create_jar_server(
- server_type, server_version, server_name, min_mem, max_mem, port
+ server_type,
+ server_version,
+ server_name,
+ min_mem,
+ max_mem,
+ port,
+ exec_user["user_id"],
)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
@@ -545,7 +580,8 @@ class ServerHandler(BaseHandler):
new_server_id
).get("server_uuid")
role_id = self.controller.roles.add_role(
- f"Creator of Server with uuid={new_server_uuid}"
+ f"Creator of Server with uuid={new_server_uuid}",
+ exec_user["user_id"],
)
self.controller.server_perms.add_role_server(
new_server_id, role_id, "11111111"
@@ -553,9 +589,6 @@ class ServerHandler(BaseHandler):
self.controller.users.add_role_to_user(
exec_user["user_id"], role_id
)
- self.controller.crafty_perms.add_server_creation(
- exec_user["user_id"]
- )
else:
for role in captured_roles:
diff --git a/app/config/version.json b/app/config/version.json
index a10889cc..f795fa3e 100644
--- a/app/config/version.json
+++ b/app/config/version.json
@@ -1,6 +1,6 @@
{
"major": 4,
"minor": 0,
- "sub": 10,
+ "sub": 11,
"meta": "beta"
}
diff --git a/app/frontend/templates/base.html b/app/frontend/templates/base.html
index 46b6e622..1ab50da8 100755
--- a/app/frontend/templates/base.html
+++ b/app/frontend/templates/base.html
@@ -40,6 +40,16 @@
+
+
+
+
diff --git a/app/frontend/templates/panel/dashboard.html b/app/frontend/templates/panel/dashboard.html
index ec5a21db..23ebf2c7 100644
--- a/app/frontend/templates/panel/dashboard.html
+++ b/app/frontend/templates/panel/dashboard.html
@@ -193,9 +193,9 @@
{{ translate('dashboard', 'starting',
data['lang']) }}
- {% elif server['stats']['downloading']%}
+ {% elif server['stats']['importing']%}
- {{ translate('serverTerm', 'downloading',
+ {{ translate('serverTerm', 'importing',
data['lang']) }}
{% else %}
- {% elif server['stats']['downloading']%}
+ {% elif server['stats']['importing']%}