Merge branch 'dev' into merge/pretzel-lukas-cleanup-nosquash

This commit is contained in:
luukas
2022-05-20 18:07:12 +03:00
53 changed files with 3658 additions and 268 deletions

6
.gitignore vendored
View File

@ -18,8 +18,10 @@ env.bak/
venv.bak/ venv.bak/
.idea/ .idea/
servers/ /servers/
backups/ /backups/
/docker/servers/
/docker/backups/
session.lock session.lock
.header .header
default.json default.json

View File

@ -62,6 +62,14 @@ class CraftyPermsController:
@staticmethod @staticmethod
def add_server_creation(user_id): 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) return PermissionsCrafty.add_server_creation(user_id)
@staticmethod @staticmethod

View File

@ -1,7 +1,8 @@
import logging import logging
import typing as t
from app.classes.models.roles import HelperRoles from app.classes.models.roles import HelperRoles
from app.classes.models.server_permissions import PermissionsServers from app.classes.models.server_permissions import PermissionsServers, RoleServers
from app.classes.shared.helpers import Helpers from app.classes.shared.helpers import Helpers
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -16,6 +17,10 @@ class RolesController:
def get_all_roles(): def get_all_roles():
return HelperRoles.get_all_roles() return HelperRoles.get_all_roles()
@staticmethod
def get_all_role_ids():
return HelperRoles.get_all_role_ids()
@staticmethod @staticmethod
def get_roleid_by_name(role_name): def get_roleid_by_name(role_name):
return HelperRoles.get_roleid_by_name(role_name) return HelperRoles.get_roleid_by_name(role_name)
@ -36,8 +41,12 @@ class RolesController:
if key == "role_id": if key == "role_id":
continue continue
elif key == "servers": elif key == "servers":
added_servers = role_data["servers"].difference(base_data["servers"]) added_servers = set(role_data["servers"]).difference(
removed_servers = base_data["servers"].difference(role_data["servers"]) set(base_data["servers"])
)
removed_servers = set(base_data["servers"]).difference(
set(role_data["servers"])
)
elif base_data[key] != role_data[key]: elif base_data[key] != role_data[key]:
up_data[key] = role_data[key] up_data[key] = role_data[key]
up_data["last_update"] = Helpers.get_time_as_string() up_data["last_update"] = Helpers.get_time_as_string()
@ -58,6 +67,95 @@ class RolesController:
def add_role(role_name): def add_role(role_name):
return HelperRoles.add_role(role_name) return HelperRoles.add_role(role_name)
class RoleServerJsonType(t.TypedDict):
server_id: t.Union[str, int]
permissions: str
@staticmethod
def get_server_ids_and_perms_from_role(
role_id: t.Union[str, int]
) -> t.List[RoleServerJsonType]:
# FIXME: somehow retrieve only the server ids, not the whole servers
return [
{
"server_id": role_servers.server_id.server_id,
"permissions": role_servers.permissions,
}
for role_servers in (
RoleServers.select(
RoleServers.server_id, RoleServers.permissions
).where(RoleServers.role_id == role_id)
)
]
@staticmethod
def add_role_advanced(
name: str,
servers: t.Iterable[RoleServerJsonType],
) -> int:
"""Add a role with a name and a list of servers
Args:
name (str): The new role's name
servers (t.List[RoleServerJsonType]): The new role's servers
Returns:
int: The new role's ID
"""
role_id: t.Final[int] = HelperRoles.add_role(name)
for server in servers:
PermissionsServers.get_or_create(
role_id, server["server_id"], server["permissions"]
)
return role_id
@staticmethod
def update_role_advanced(
role_id: t.Union[str, int],
role_name: t.Optional[str],
servers: t.Optional[t.Iterable[RoleServerJsonType]],
) -> None:
"""Update a role with a name and a list of servers
Args:
role_id (t.Union[str, int]): The ID of the role to be modified
role_name (t.Optional[str]): An optional new name for the role
servers (t.Optional[t.Iterable[RoleServerJsonType]]): An optional list of servers for the role
""" # pylint: disable=line-too-long
logger.debug(f"updating role {role_id} with advanced options")
if servers is not None:
base_data = RolesController.get_role_with_servers(role_id)
server_ids = {server["server_id"] for server in servers}
server_permissions_map = {
server["server_id"]: server["permissions"] for server in servers
}
added_servers = server_ids.difference(set(base_data["servers"]))
removed_servers = set(base_data["servers"]).difference(server_ids)
same_servers = server_ids.intersection(set(base_data["servers"]))
logger.debug(
f"role: {role_id} +server:{added_servers} -server{removed_servers}"
)
for server_id in added_servers:
PermissionsServers.get_or_create(
role_id, server_id, server_permissions_map[server_id]
)
if len(removed_servers) != 0:
PermissionsServers.delete_roles_permissions(role_id, removed_servers)
for server_id in same_servers:
PermissionsServers.update_role_permission(
role_id, server_id, server_permissions_map[server_id]
)
if role_name is not None:
up_data = {
"role_name": role_name,
"last_update": Helpers.get_time_as_string(),
}
# TODO: do the last_update on the db side
HelperRoles.update_role(role_id, up_data)
def remove_role(self, role_id): def remove_role(self, role_id):
role_data = RolesController.get_role_with_servers(role_id) role_data = RolesController.get_role_with_servers(role_id)
PermissionsServers.delete_roles_permissions(role_id, role_data["servers"]) PermissionsServers.delete_roles_permissions(role_id, role_data["servers"])
@ -73,9 +171,8 @@ class RolesController:
role = HelperRoles.get_role(role_id) role = HelperRoles.get_role(role_id)
if role: if role:
servers_query = PermissionsServers.get_servers_from_role(role_id) server_ids = PermissionsServers.get_server_ids_from_role(role_id)
servers = {s.server_id_id for s in servers_query} role["servers"] = server_ids
role["servers"] = servers
# logger.debug("role: ({}) {}".format(role_id, role)) # logger.debug("role: ({}) {}".format(role_id, role))
return role return role
else: else:

View File

@ -1,6 +1,7 @@
import os import os
import logging import logging
import json import json
import typing as t
from app.classes.controllers.roles_controller import RolesController from app.classes.controllers.roles_controller import RolesController
from app.classes.models.servers import HelperServers from app.classes.models.servers import HelperServers
@ -34,9 +35,31 @@ class ServersController:
server_log_file: str, server_log_file: str,
server_stop: str, server_stop: str,
server_type: str, server_type: str,
server_port=25565, server_port: int = 25565,
): server_host: str = "127.0.0.1",
return self.servers_helper.create_server( ) -> int:
"""Create a server in the database
Args:
name: The name of the server
server_uuid: This is the UUID of the server
server_dir: The directory where the server is located
backup_path: The path to the backup folder
server_command: The command to start the server
server_file: The name of the server file
server_log_file: The path to the server log file
server_stop: This is the command to stop the server
server_type: This is the type of server you're creating.
server_port: The port the server will be monitored on, defaults to 25565
server_host: The host the server will be monitored on, defaults to 127.0.0.1
Returns:
int: The new server's id
Raises:
PeeweeException: If the server already exists
"""
return HelperServers.create_server(
name, name,
server_uuid, server_uuid,
server_dir, server_dir,
@ -47,6 +70,7 @@ class ServersController:
server_stop, server_stop,
server_type, server_type,
server_port, server_port,
server_host,
) )
@staticmethod @staticmethod
@ -92,7 +116,7 @@ class ServersController:
@staticmethod @staticmethod
def get_authorized_servers(user_id): def get_authorized_servers(user_id):
server_data = [] server_data: t.List[t.Dict[str, t.Any]] = []
user_roles = HelperUsers.user_role_query(user_id) user_roles = HelperUsers.user_role_query(user_id)
for user in user_roles: for user in user_roles:
role_servers = PermissionsServers.get_role_servers_from_role_id( role_servers = PermissionsServers.get_role_servers_from_role_id(
@ -103,6 +127,20 @@ class ServersController:
return server_data return server_data
@staticmethod
def get_authorized_users(server_id: str):
user_ids: t.Set[int] = set()
roles_list = PermissionsServers.get_roles_from_server(server_id)
for role in roles_list:
role_users = HelperUsers.get_users_from_role(role.role_id)
for user_role in role_users:
user_ids.add(user_role.user_id)
for user_id in HelperUsers.get_super_user_list():
user_ids.add(user_id)
return user_ids
@staticmethod @staticmethod
def get_all_servers_stats(): def get_all_servers_stats():
return HelperServerStats.get_all_servers_stats() return HelperServerStats.get_all_servers_stats()
@ -110,7 +148,9 @@ class ServersController:
@staticmethod @staticmethod
def get_authorized_servers_stats_api_key(api_key: ApiKeys): def get_authorized_servers_stats_api_key(api_key: ApiKeys):
server_data = [] server_data = []
authorized_servers = ServersController.get_authorized_servers(api_key.user_id) authorized_servers = ServersController.get_authorized_servers(
api_key.user_id # TODO: API key authorized servers?
)
for server in authorized_servers: for server in authorized_servers:
latest = HelperServerStats.get_latest_server_stats(server.get("server_id")) latest = HelperServerStats.get_latest_server_stats(server.get("server_id"))

View File

@ -1,5 +1,5 @@
import logging import logging
from typing import Optional import typing as t
from app.classes.models.users import HelperUsers from app.classes.models.users import HelperUsers
from app.classes.models.crafty_permissions import ( from app.classes.models.crafty_permissions import (
@ -16,6 +16,74 @@ class UsersController:
self.users_helper = users_helper self.users_helper = users_helper
self.authentication = authentication self.authentication = authentication
_permissions_props = {
"name": {
"type": "string",
"enum": [
permission.name
for permission in PermissionsCrafty.get_permissions_list()
],
},
"quantity": {"type": "number", "minimum": 0},
"enabled": {"type": "boolean"},
}
self.user_jsonschema_props: t.Final = {
"username": {
"type": "string",
"maxLength": 20,
"minLength": 4,
"pattern": "^[a-z0-9_]+$",
"examples": ["admin"],
"title": "Username",
},
"password": {
"type": "string",
"maxLength": 20,
"minLength": 4,
"examples": ["crafty"],
"title": "Password",
},
"email": {
"type": "string",
"format": "email",
"examples": ["default@example.com"],
"title": "E-Mail",
},
"enabled": {
"type": "boolean",
"examples": [True],
"title": "Enabled",
},
"lang": {
"type": "string",
"maxLength": 10,
"minLength": 2,
"examples": ["en"],
"title": "Language",
},
"superuser": {
"type": "boolean",
"examples": [False],
"title": "Superuser",
},
"permissions": {
"type": "array",
"items": {
"type": "object",
"properties": _permissions_props,
"required": ["name", "quantity", "enabled"],
},
},
"roles": {
"type": "array",
"items": {
"type": "string",
"minLength": 1,
},
},
"hints": {"type": "boolean"},
}
# ********************************************************************************** # **********************************************************************************
# Users Methods # Users Methods
# ********************************************************************************** # **********************************************************************************
@ -23,6 +91,10 @@ class UsersController:
def get_all_users(): def get_all_users():
return HelperUsers.get_all_users() return HelperUsers.get_all_users()
@staticmethod
def get_all_user_ids() -> t.List[int]:
return HelperUsers.get_all_user_ids()
@staticmethod @staticmethod
def get_id_by_name(username): def get_id_by_name(username):
return HelperUsers.get_user_id_by_name(username) return HelperUsers.get_user_id_by_name(username)
@ -64,32 +136,38 @@ class UsersController:
if key == "user_id": if key == "user_id":
continue continue
elif key == "roles": elif key == "roles":
added_roles = user_data["roles"].difference(base_data["roles"]) added_roles = set(user_data["roles"]).difference(
removed_roles = base_data["roles"].difference(user_data["roles"]) set(base_data["roles"])
)
removed_roles = set(base_data["roles"]).difference(
set(user_data["roles"])
)
elif key == "password": elif key == "password":
if user_data["password"] is not None and user_data["password"] != "": if user_data["password"] is not None and user_data["password"] != "":
up_data["password"] = self.helper.encode_pass(user_data["password"]) up_data["password"] = self.helper.encode_pass(user_data["password"])
elif key == "lang":
up_data["lang"] = user_data["lang"]
elif key == "hints":
up_data["hints"] = user_data["hints"]
elif base_data[key] != user_data[key]: elif base_data[key] != user_data[key]:
up_data[key] = user_data[key] up_data[key] = user_data[key]
up_data["last_update"] = self.helper.get_time_as_string() up_data["last_update"] = self.helper.get_time_as_string()
up_data["lang"] = user_data["lang"]
up_data["hints"] = user_data["hints"]
logger.debug(f"user: {user_data} +role:{added_roles} -role:{removed_roles}") logger.debug(f"user: {user_data} +role:{added_roles} -role:{removed_roles}")
for role in added_roles: for role in added_roles:
HelperUsers.get_or_create(user_id=user_id, role_id=role) HelperUsers.get_or_create(user_id=user_id, role_id=role)
permissions_mask = user_crafty_data.get("permissions_mask", "000") permissions_mask = user_crafty_data.get("permissions_mask", "000")
if "server_quantity" in user_crafty_data: if "server_quantity" in user_crafty_data:
limit_server_creation = user_crafty_data["server_quantity"][ limit_server_creation = user_crafty_data["server_quantity"].get(
EnumPermissionsCrafty.SERVER_CREATION.name EnumPermissionsCrafty.SERVER_CREATION.name, 0
] )
limit_user_creation = user_crafty_data["server_quantity"][ limit_user_creation = user_crafty_data["server_quantity"].get(
EnumPermissionsCrafty.USER_CONFIG.name EnumPermissionsCrafty.USER_CONFIG.name, 0
] )
limit_role_creation = user_crafty_data["server_quantity"][ limit_role_creation = user_crafty_data["server_quantity"].get(
EnumPermissionsCrafty.ROLES_CONFIG.name EnumPermissionsCrafty.ROLES_CONFIG.name, 0
] )
else: else:
limit_server_creation = 0 limit_server_creation = 0
limit_user_creation = 0 limit_user_creation = 0
@ -107,6 +185,15 @@ class UsersController:
self.users_helper.update_user(user_id, up_data) self.users_helper.update_user(user_id, up_data)
def raw_update_user(self, user_id: int, up_data: t.Optional[t.Dict[str, t.Any]]):
"""Directly passes the data to the model helper.
Args:
user_id (int): The id of the user to update.
up_data (t.Optional[t.Dict[str, t.Any]]): Update data.
"""
self.users_helper.update_user(user_id, up_data)
def add_user( def add_user(
self, self,
username, username,
@ -159,7 +246,7 @@ class UsersController:
return token_data["user_id"] return token_data["user_id"]
def get_user_by_api_token(self, token: str): def get_user_by_api_token(self, token: str):
_, _, user = self.authentication.check(token) _, _, user = self.authentication.check_err(token)
return user return user
def get_api_key_by_token(self, token: str): def get_api_key_by_token(self, token: str):
@ -205,8 +292,8 @@ class UsersController:
name: str, name: str,
user_id: str, user_id: str,
superuser: bool = False, superuser: bool = False,
server_permissions_mask: Optional[str] = None, server_permissions_mask: t.Optional[str] = None,
crafty_permissions_mask: Optional[str] = None, crafty_permissions_mask: t.Optional[str] = None,
): ):
return self.users_helper.add_user_api_key( return self.users_helper.add_user_api_key(
name, user_id, superuser, server_permissions_mask, crafty_permissions_mask name, user_id, superuser, server_permissions_mask, crafty_permissions_mask

View File

@ -22,17 +22,10 @@ class ServerJars:
try: try:
response = requests.get(full_url, timeout=2) response = requests.get(full_url, timeout=2)
response.raise_for_status()
if response.status_code not in [200, 201]:
return {}
except Exception as e:
logger.error(f"Unable to connect to serverjar.com api due to error: {e}")
return {}
try:
api_data = json.loads(response.content) api_data = json.loads(response.content)
except Exception as e: except Exception as e:
logger.error(f"Unable to parse serverjar.com api result due to error: {e}") logger.error(f"Unable to load {full_url} api due to error: {e}")
return {} return {}
api_result = api_data.get("status") api_result = api_data.get("status")

View File

@ -1,4 +1,5 @@
import logging import logging
import typing
from enum import Enum from enum import Enum
from peewee import ( from peewee import (
ForeignKeyField, ForeignKeyField,
@ -168,7 +169,12 @@ class PermissionsCrafty:
) )
@staticmethod @staticmethod
def add_server_creation(user_id): 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.update(created_server=UserCrafty.created_server + 1).where(
UserCrafty.user_id == user_id UserCrafty.user_id == user_id
).execute() ).execute()

View File

@ -180,7 +180,12 @@ class HelpersManagement:
server_users = PermissionsServers.get_server_user_list(server_id) server_users = PermissionsServers.get_server_user_list(server_id)
for user in server_users: for user in server_users:
self.helper.websocket_helper.broadcast_user(user, "notification", audit_msg) try:
self.helper.websocket_helper.broadcast_user(
user, "notification", audit_msg
)
except Exception as e:
logger.error(f"Error broadcasting to user {user} - {e}")
AuditLog.insert( AuditLog.insert(
{ {
@ -191,7 +196,7 @@ class HelpersManagement:
AuditLog.source_ip: source_ip, AuditLog.source_ip: source_ip,
} }
).execute() ).execute()
# deletes records when they're more than 100 # deletes records when there's more than 300
ordered = AuditLog.select().order_by(+AuditLog.created) ordered = AuditLog.select().order_by(+AuditLog.created)
for item in ordered: for item in ordered:
if not self.helper.get_setting("max_audit_entries"): if not self.helper.get_setting("max_audit_entries"):
@ -213,7 +218,7 @@ class HelpersManagement:
AuditLog.source_ip: source_ip, AuditLog.source_ip: source_ip,
} }
).execute() ).execute()
# deletes records when they're more than 100 # deletes records when there's more than 300
ordered = AuditLog.select().order_by(+AuditLog.created) ordered = AuditLog.select().order_by(+AuditLog.created)
for item in ordered: for item in ordered:
# configurable through app/config/config.json # configurable through app/config/config.json
@ -399,7 +404,7 @@ class HelpersManagement:
return dir_list return dir_list
def add_excluded_backup_dir(self, server_id: int, dir_to_add: str): def add_excluded_backup_dir(self, server_id: int, dir_to_add: str):
dir_list = self.get_excluded_backup_dirs() dir_list = self.get_excluded_backup_dirs(server_id)
if dir_to_add not in dir_list: if dir_to_add not in dir_list:
dir_list.append(dir_to_add) dir_list.append(dir_to_add)
excluded_dirs = ",".join(dir_list) excluded_dirs = ",".join(dir_list)
@ -411,7 +416,7 @@ class HelpersManagement:
) )
def del_excluded_backup_dir(self, server_id: int, dir_to_del: str): def del_excluded_backup_dir(self, server_id: int, dir_to_del: str):
dir_list = self.get_excluded_backup_dirs() dir_list = self.get_excluded_backup_dirs(server_id)
if dir_to_del in dir_list: if dir_to_del in dir_list:
dir_list.remove(dir_to_del) dir_list.remove(dir_to_del)
excluded_dirs = ",".join(dir_list) excluded_dirs = ",".join(dir_list)

View File

@ -1,5 +1,6 @@
import logging import logging
import datetime import datetime
import typing as t
from peewee import ( from peewee import (
CharField, CharField,
DoesNotExist, DoesNotExist,
@ -35,8 +36,11 @@ class HelperRoles:
@staticmethod @staticmethod
def get_all_roles(): def get_all_roles():
query = Roles.select() return Roles.select()
return query
@staticmethod
def get_all_role_ids() -> t.List[int]:
return [role.role_id for role in Roles.select(Roles.role_id).execute()]
@staticmethod @staticmethod
def get_roleid_by_name(role_name): def get_roleid_by_name(role_name):
@ -49,6 +53,24 @@ class HelperRoles:
def get_role(role_id): def get_role(role_id):
return model_to_dict(Roles.get(Roles.role_id == role_id)) return model_to_dict(Roles.get(Roles.role_id == role_id))
@staticmethod
def get_role_columns(
role_id: t.Union[str, int], column_names: t.List[str]
) -> t.List[t.Any]:
columns = [getattr(Roles, column) for column in column_names]
return model_to_dict(
Roles.select(*columns).where(Roles.role_id == role_id).get(),
only=columns,
)
@staticmethod
def get_role_column(role_id: t.Union[str, int], column_name: str) -> t.Any:
column = getattr(Roles, column_name)
return model_to_dict(
Roles.select(column).where(Roles.role_id == role_id).get(),
only=[column],
)[column_name]
@staticmethod @staticmethod
def add_role(role_name): def add_role(role_name):
role_id = Roles.insert( role_id = Roles.insert(
@ -67,5 +89,5 @@ class HelperRoles:
return Roles.delete().where(Roles.role_id == role_id).execute() return Roles.delete().where(Roles.role_id == role_id).execute()
@staticmethod @staticmethod
def role_id_exists(role_id): def role_id_exists(role_id) -> bool:
return Roles.select().where(Roles.role_id == role_id).count() != 0 return Roles.select().where(Roles.role_id == role_id).count() != 0

View File

@ -1,6 +1,6 @@
import logging
import typing as t import typing as t
from enum import Enum from enum import Enum
import logging
from peewee import ( from peewee import (
ForeignKeyField, ForeignKeyField,
CharField, CharField,
@ -93,17 +93,29 @@ class PermissionsServers:
# Role_Servers Methods # Role_Servers Methods
# ********************************************************************************** # **********************************************************************************
@staticmethod @staticmethod
def get_role_servers_from_role_id(roleid): def get_role_servers_from_role_id(roleid: t.Union[str, int]):
return RoleServers.select().where(RoleServers.role_id == roleid) return RoleServers.select().where(RoleServers.role_id == roleid)
@staticmethod @staticmethod
def get_servers_from_role(role_id): def get_servers_from_role(role_id: t.Union[str, int]):
return ( return (
RoleServers.select() RoleServers.select()
.join(Servers, JOIN.INNER) .join(Servers, JOIN.INNER)
.where(RoleServers.role_id == role_id) .where(RoleServers.role_id == role_id)
) )
@staticmethod
def get_server_ids_from_role(role_id: t.Union[str, int]) -> t.List[int]:
# FIXME: somehow retrieve only the server ids, not the whole servers
return [
role_servers.server_id.server_id
for role_servers in (
RoleServers.select(RoleServers.server_id).where(
RoleServers.role_id == role_id
)
)
]
@staticmethod @staticmethod
def get_roles_from_server(server_id): def get_roles_from_server(server_id):
return ( return (
@ -171,9 +183,9 @@ class PermissionsServers:
).execute() ).execute()
@staticmethod @staticmethod
def delete_roles_permissions(role_id, removed_servers=None): def delete_roles_permissions(
if removed_servers is None: role_id: t.Union[str, int], removed_servers: t.Sequence[t.Union[str, int]]
removed_servers = {} ):
return ( return (
RoleServers.delete() RoleServers.delete()
.where(RoleServers.role_id == role_id) .where(RoleServers.role_id == role_id)

View File

@ -1,5 +1,6 @@
import logging import logging
import datetime import datetime
import typing as t
from peewee import ( from peewee import (
CharField, CharField,
AutoField, AutoField,
@ -7,6 +8,7 @@ from peewee import (
BooleanField, BooleanField,
IntegerField, IntegerField,
) )
from playhouse.shortcuts import model_to_dict
from app.classes.shared.main_models import DatabaseShortcuts from app.classes.shared.main_models import DatabaseShortcuts
from app.classes.models.base_model import BaseModel from app.classes.models.base_model import BaseModel
@ -61,8 +63,30 @@ class HelperServers:
server_log_file: str, server_log_file: str,
server_stop: str, server_stop: str,
server_type: str, server_type: str,
server_port=25565, server_port: int = 25565,
): server_host: str = "127.0.0.1",
) -> int:
"""Create a server in the database
Args:
name: The name of the server
server_uuid: This is the UUID of the server
server_dir: The directory where the server is located
backup_path: The path to the backup folder
server_command: The command to start the server
server_file: The name of the server file
server_log_file: The path to the server log file
server_stop: This is the command to stop the server
server_type: This is the type of server you're creating.
server_port: The port the server will be monitored on, defaults to 25565
server_host: The host the server will be monitored on, defaults to 127.0.0.1
Returns:
int: The new server's id
Raises:
PeeweeException: If the server already exists
"""
return Servers.insert( return Servers.insert(
{ {
Servers.server_name: name, Servers.server_name: name,
@ -75,6 +99,7 @@ class HelperServers:
Servers.crash_detection: False, Servers.crash_detection: False,
Servers.log_path: server_log_file, Servers.log_path: server_log_file,
Servers.server_port: server_port, Servers.server_port: server_port,
Servers.server_ip: server_host,
Servers.stop_command: server_stop, Servers.stop_command: server_stop,
Servers.backup_path: backup_path, Servers.backup_path: backup_path,
Servers.type: server_type, Servers.type: server_type,
@ -105,6 +130,24 @@ class HelperServers:
except IndexError: except IndexError:
return {} return {}
@staticmethod
def get_server_columns(
server_id: t.Union[str, int], column_names: t.List[str]
) -> t.List[t.Any]:
columns = [getattr(Servers, column) for column in column_names]
return model_to_dict(
Servers.select(*columns).where(Servers.server_id == server_id).get(),
only=columns,
)
@staticmethod
def get_server_column(server_id: t.Union[str, int], column_name: str) -> t.Any:
column = getattr(Servers, column_name)
return model_to_dict(
Servers.select(column).where(Servers.server_id == server_id).get(),
only=[column],
)[column_name]
# ********************************************************************************** # **********************************************************************************
# Servers Methods # Servers Methods
# ********************************************************************************** # **********************************************************************************
@ -113,6 +156,10 @@ class HelperServers:
query = Servers.select() query = Servers.select()
return DatabaseShortcuts.return_rows(query) return DatabaseShortcuts.return_rows(query)
@staticmethod
def get_all_server_ids() -> t.List[int]:
return [server.server_id for server in Servers.select(Servers.server_id)]
@staticmethod @staticmethod
def get_server_friendly_name(server_id): def get_server_friendly_name(server_id):
server_data = HelperServers.get_server_data_by_id(server_id) server_data = HelperServers.get_server_data_by_id(server_id)

View File

@ -1,6 +1,6 @@
import logging import logging
import datetime import datetime
from typing import Optional, Union import typing as t
from peewee import ( from peewee import (
ForeignKeyField, ForeignKeyField,
@ -45,6 +45,15 @@ class Users(BaseModel):
table_name = "users" table_name = "users"
PUBLIC_USER_ATTRS: t.Final = [
"user_id",
"created",
"username",
"enabled",
"superuser",
"lang", # maybe remove?
]
# ********************************************************************************** # **********************************************************************************
# API Keys Class # API Keys Class
# ********************************************************************************** # **********************************************************************************
@ -90,6 +99,15 @@ class HelperUsers:
query = Users.select().where(Users.username != "system") query = Users.select().where(Users.username != "system")
return query return query
@staticmethod
def get_all_user_ids() -> t.List[int]:
return [
user.user_id
for user in Users.select(Users.user_id)
.where(Users.username != "system")
.execute()
]
@staticmethod @staticmethod
def get_user_lang_by_id(user_id): def get_user_lang_by_id(user_id):
return Users.get(Users.user_id == user_id).lang return Users.get(Users.user_id == user_id).lang
@ -134,6 +152,24 @@ class HelperUsers:
# logger.debug("user: ({}) {}".format(user_id, {})) # logger.debug("user: ({}) {}".format(user_id, {}))
return {} return {}
@staticmethod
def get_user_columns(
user_id: t.Union[str, int], column_names: t.List[str]
) -> t.List[t.Any]:
columns = [getattr(Users, column) for column in column_names]
return model_to_dict(
Users.select(*columns).where(Users.user_id == user_id).get(),
only=columns,
)
@staticmethod
def get_user_column(user_id: t.Union[str, int], column_name: str) -> t.Any:
column = getattr(Users, column_name)
return model_to_dict(
Users.select(column).where(Users.user_id == user_id).get(),
only=[column],
)[column_name]
@staticmethod @staticmethod
def check_system_user(user_id): def check_system_user(user_id):
try: try:
@ -153,7 +189,7 @@ class HelperUsers:
self, self,
username: str, username: str,
password: str = None, password: str = None,
email: Optional[str] = None, email: t.Optional[str] = None,
enabled: bool = True, enabled: bool = True,
superuser: bool = False, superuser: bool = False,
) -> str: ) -> str:
@ -177,7 +213,7 @@ class HelperUsers:
def add_rawpass_user( def add_rawpass_user(
username: str, username: str,
password: str = None, password: str = None,
email: Optional[str] = None, email: t.Optional[str] = None,
enabled: bool = True, enabled: bool = True,
superuser: bool = False, superuser: bool = False,
) -> str: ) -> str:
@ -212,7 +248,7 @@ class HelperUsers:
@staticmethod @staticmethod
def get_super_user_list(): def get_super_user_list():
final_users = [] final_users: t.List[int] = []
super_users = Users.select().where( super_users = Users.select().where(
Users.superuser == True # pylint: disable=singleton-comparison Users.superuser == True # pylint: disable=singleton-comparison
) )
@ -224,7 +260,7 @@ class HelperUsers:
def remove_user(self, user_id): def remove_user(self, user_id):
with self.database.atomic(): with self.database.atomic():
UserRoles.delete().where(UserRoles.user_id == user_id).execute() UserRoles.delete().where(UserRoles.user_id == user_id).execute()
Users.delete().where(Users.user_id == user_id).execute() return Users.delete().where(Users.user_id == user_id).execute()
@staticmethod @staticmethod
def set_support_path(user_id, support_path): def set_support_path(user_id, support_path):
@ -268,11 +304,10 @@ class HelperUsers:
@staticmethod @staticmethod
def get_user_roles_names(user_id): def get_user_roles_names(user_id):
roles_list = [] roles = UserRoles.select(UserRoles.role_id).where(UserRoles.user_id == user_id)
roles = UserRoles.select().where(UserRoles.user_id == user_id) return [
for r in roles: HelperRoles.get_role_column(role.role_id, "role_name") for role in roles
roles_list.append(HelperRoles.get_role(r.role_id)["role_name"]) ]
return roles_list
@staticmethod @staticmethod
def add_role_to_user(user_id, role_id): def add_role_to_user(user_id, role_id):
@ -281,7 +316,7 @@ class HelperUsers:
).execute() ).execute()
@staticmethod @staticmethod
def add_user_roles(user: Union[dict, Users]): def add_user_roles(user: t.Union[dict, Users]):
if isinstance(user, dict): if isinstance(user, dict):
user_id = user["user_id"] user_id = user["user_id"]
else: else:
@ -323,6 +358,10 @@ class HelperUsers:
def remove_roles_from_role_id(role_id): def remove_roles_from_role_id(role_id):
UserRoles.delete().where(UserRoles.role_id == role_id).execute() UserRoles.delete().where(UserRoles.role_id == role_id).execute()
@staticmethod
def get_users_from_role(role_id):
UserRoles.select().where(UserRoles.role_id == role_id).execute()
# ********************************************************************************** # **********************************************************************************
# ApiKeys Methods # ApiKeys Methods
# ********************************************************************************** # **********************************************************************************
@ -340,8 +379,8 @@ class HelperUsers:
name: str, name: str,
user_id: str, user_id: str,
superuser: bool = False, superuser: bool = False,
server_permissions_mask: Optional[str] = None, server_permissions_mask: t.Optional[str] = None,
crafty_permissions_mask: Optional[str] = None, crafty_permissions_mask: t.Optional[str] = None,
): ):
return ApiKeys.insert( return ApiKeys.insert(
{ {

View File

@ -34,7 +34,7 @@ class Authentication:
def check_no_iat(self, token) -> Optional[Dict[str, Any]]: def check_no_iat(self, token) -> Optional[Dict[str, Any]]:
try: try:
return jwt.decode(token, self.secret, algorithms=["HS256"]) return jwt.decode(str(token), self.secret, algorithms=["HS256"])
except PyJWTError as error: except PyJWTError as error:
logger.debug("Error while checking JWT token: ", exc_info=error) logger.debug("Error while checking JWT token: ", exc_info=error)
return None return None
@ -44,7 +44,7 @@ class Authentication:
token, token,
) -> Optional[Tuple[Optional[ApiKeys], Dict[str, Any], Dict[str, Any]]]: ) -> Optional[Tuple[Optional[ApiKeys], Dict[str, Any], Dict[str, Any]]]:
try: try:
data = jwt.decode(token, self.secret, algorithms=["HS256"]) data = jwt.decode(str(token), self.secret, algorithms=["HS256"])
except PyJWTError as error: except PyJWTError as error:
logger.debug("Error while checking JWT token: ", exc_info=error) logger.debug("Error while checking JWT token: ", exc_info=error)
return None return None
@ -65,5 +65,17 @@ class Authentication:
else: else:
return None return None
def check_err(
self,
token,
) -> Tuple[Optional[ApiKeys], Dict[str, Any], Dict[str, Any]]:
# Without this function there would be runtime exceptions like the following:
# "None" object is not iterable
output = self.check(token)
if output is None:
raise Exception("Invalid token")
return output
def check_bool(self, token) -> bool: def check_bool(self, token) -> bool:
return self.check(token) is not None return self.check(token) is not None

View File

@ -3,6 +3,7 @@ import cmd
import time import time
import threading import threading
import logging import logging
import getpass
from app.classes.shared.console import Console from app.classes.shared.console import Console
from app.classes.shared.import3 import Import3 from app.classes.shared.import3 import Import3
@ -11,11 +12,13 @@ logger = logging.getLogger(__name__)
class MainPrompt(cmd.Cmd): class MainPrompt(cmd.Cmd):
def __init__(self, helper, tasks_manager, migration_manager): def __init__(self, helper, tasks_manager, migration_manager, main_controller):
super().__init__() super().__init__()
self.helper = helper self.helper = helper
self.tasks_manager = tasks_manager self.tasks_manager = tasks_manager
self.migration_manager = migration_manager self.migration_manager = migration_manager
self.controller = main_controller
# overrides the default Prompt # overrides the default Prompt
self.prompt = f"Crafty Controller v{self.helper.get_version_string()} > " self.prompt = f"Crafty Controller v{self.helper.get_version_string()} > "
@ -49,6 +52,37 @@ class MainPrompt(cmd.Cmd):
else: else:
Console.info("Unknown migration command") Console.info("Unknown migration command")
def do_set_passwd(self, line):
try:
username = str(line).lower()
# If no user is found it returns None
user_id = self.controller.users.get_id_by_name(username)
if not username:
Console.error("You must enter a username. Ex: `set_passwd admin'")
return False
if not user_id:
Console.error(f"No user found by the name of {username}")
return False
except:
Console.error(f"User: {line} Not Found")
return False
new_pass = getpass.getpass(prompt=f"NEW password for: {username} > ")
new_pass_conf = getpass.getpass(prompt="Re-enter your password: > ")
if new_pass != new_pass_conf:
Console.error("Passwords do not match. Please try again.")
return False
if len(new_pass) > 512:
Console.warning("Passwords must be greater than 6char long and under 512")
return False
if len(new_pass) < 6:
Console.warning("Passwords must be greater than 6char long and under 512")
return False
self.controller.users.update_user(user_id, {"password": new_pass})
@staticmethod @staticmethod
def do_threads(_line): def do_threads(_line):
for thread in threading.enumerate(): for thread in threading.enumerate():

View File

@ -72,7 +72,7 @@ class Helpers:
installer.do_install() installer.do_install()
@staticmethod @staticmethod
def float_to_string(gbs: int): def float_to_string(gbs: float):
s = str(float(gbs) * 1000).rstrip("0").rstrip(".") s = str(float(gbs) * 1000).rstrip("0").rstrip(".")
return s return s
@ -232,7 +232,7 @@ class Helpers:
return default_return return default_return
with open(self.settings_file, "w", encoding="utf-8") as f: with open(self.settings_file, "w", encoding="utf-8") as f:
json.dump(data, f, indent=1) json.dump(data, f, indent=2)
except Exception as e: except Exception as e:
logger.critical( logger.critical(
@ -270,18 +270,17 @@ class Helpers:
@staticmethod @staticmethod
def get_announcements(): def get_announcements():
response = requests.get("https://craftycontrol.com/notify.json", timeout=2)
data = ( data = (
'[{"id":"1","date":"Unknown",' '[{"id":"1","date":"Unknown",'
'"title":"Error getting Announcements",' '"title":"Error getting Announcements",'
'"desc":"Error getting Announcements","link":""}]' '"desc":"Error getting Announcements","link":""}]'
) )
if response.status_code in [200, 201]:
try: try:
response = requests.get("https://craftycontrol.com/notify.json", timeout=2)
data = json.loads(response.content) data = json.loads(response.content)
except Exception as e: except Exception as e:
logger.error(f"Failed to load json content with error: {e}") logger.error(f"Failed to fetch notifications with error: {e}")
return data return data
@ -1001,10 +1000,11 @@ class Helpers:
return text return text
@staticmethod @staticmethod
def get_lang_page(text): def get_lang_page(text) -> str:
lang = text.split("_")[0] splitted = text.split("_")
region = text.split("_")[1] if len(splitted) != 2:
return "en"
lang, region = splitted
if region == "EN": if region == "EN":
return "en" return "en"
else:
return lang + "-" + region return lang + "-" + region

View File

@ -5,7 +5,7 @@ import shutil
import time import time
import logging import logging
import tempfile import tempfile
from typing import Optional, Union import typing as t
from peewee import DoesNotExist from peewee import DoesNotExist
# TZLocal is set as a hidden import on win pipeline # TZLocal is set as a hidden import on win pipeline
@ -276,7 +276,7 @@ class Controller:
except: except:
return {"percent": 0, "total_files": 0} return {"percent": 0, "total_files": 0}
def get_server_obj(self, server_id: Union[str, int]) -> Server: def get_server_obj(self, server_id: t.Union[str, int]) -> Server:
for server in self.servers_list: for server in self.servers_list:
if str(server["server_id"]) == str(server_id): if str(server["server_id"]) == str(server_id):
return server["server_obj"] return server["server_obj"]
@ -284,7 +284,9 @@ class Controller:
logger.warning(f"Unable to find server object for server id {server_id}") logger.warning(f"Unable to find server object for server id {server_id}")
raise Exception(f"Unable to find server object for server id {server_id}") raise Exception(f"Unable to find server object for server id {server_id}")
def get_server_obj_optional(self, server_id: Union[str, int]) -> Optional[Server]: def get_server_obj_optional(
self, server_id: t.Union[str, int]
) -> t.Optional[Server]:
for server in self.servers_list: for server in self.servers_list:
if str(server["server_id"]) == str(server_id): if str(server["server_id"]) == str(server_id):
return server["server_obj"] return server["server_obj"]
@ -305,6 +307,10 @@ class Controller:
servers = HelperServers.get_all_defined_servers() servers = HelperServers.get_all_defined_servers()
return servers return servers
@staticmethod
def get_all_server_ids() -> t.List[int]:
return HelperServers.get_all_server_ids()
def list_running_servers(self): def list_running_servers(self):
running_servers = [] running_servers = []
@ -345,6 +351,177 @@ class Controller:
svr_obj = self.get_server_obj(server_id) svr_obj = self.get_server_obj(server_id)
svr_obj.stop_threaded_server() svr_obj.stop_threaded_server()
def create_api_server(self, data: dict):
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)
if Helpers.is_os_windows():
new_server_path = Helpers.wtol_path(new_server_path)
backup_path = Helpers.wtol_path(backup_path)
new_server_path.replace(" ", "^ ")
backup_path.replace(" ", "^ ")
Helpers.ensure_dir_exists(new_server_path)
Helpers.ensure_dir_exists(backup_path)
def _copy_import_dir_files(existing_server_path):
existing_server_path = Helpers.get_os_understandable_path(
existing_server_path
)
try:
FileHelpers.copy_dir(existing_server_path, new_server_path, True)
except shutil.Error as ex:
logger.error(f"Server import failed with error: {ex}")
def _create_server_properties_if_needed(port, empty=False):
properties_file = os.path.join(new_server_path, "server.properties")
has_properties = os.path.exists(properties_file)
if not has_properties:
logger.info(
f"No server.properties found on import."
f"Creating one with port selection of {port}"
)
with open(
properties_file,
"w",
encoding="utf-8",
) as file:
file.write(
"# generated by Crafty Controller"
+ ("" if empty else f"\nserver-port={port}")
)
root_create_data = data[data["create_type"] + "_create_data"]
create_data = root_create_data[root_create_data["create_type"] + "_create_data"]
if data["create_type"] == "minecraft_java":
if root_create_data["create_type"] == "download_jar":
server_file = f"{create_data['type']}-{create_data['version']}.jar"
full_jar_path = os.path.join(new_server_path, server_file)
# Create an EULA file
with open(
os.path.join(new_server_path, "eula.txt"), "w", encoding="utf-8"
) as file:
file.write(
"eula=" + ("true" if create_data["agree_to_eula"] else "false")
)
elif root_create_data["create_type"] == "import_server":
_copy_import_dir_files(create_data["existing_server_path"])
full_jar_path = os.path.join(new_server_path, create_data["jarfile"])
elif root_create_data["create_type"] == "import_zip":
# TODO: Copy files from the zip file to the new server directory
full_jar_path = os.path.join(new_server_path, create_data["jarfile"])
raise Exception("Not yet implemented")
_create_server_properties_if_needed(create_data["server_properties_port"])
min_mem = create_data["mem_min"]
max_mem = create_data["mem_max"]
def _gibs_to_mibs(gibs: float) -> str:
return str(int(gibs * 1024))
def _wrap_jar_if_windows():
return (
f'"{full_jar_path}"' if Helpers.is_os_windows() else full_jar_path
)
server_command = (
f"java -Xms{_gibs_to_mibs(min_mem)}M "
f"-Xmx{_gibs_to_mibs(max_mem)}M "
f"-jar {_wrap_jar_if_windows()} nogui"
)
elif data["create_type"] == "minecraft_bedrock":
if root_create_data["create_type"] == "import_server":
existing_server_path = Helpers.get_os_understandable_path(
create_data["existing_server_path"]
)
try:
FileHelpers.copy_dir(existing_server_path, new_server_path, True)
except shutil.Error as ex:
logger.error(f"Server import failed with error: {ex}")
elif root_create_data["create_type"] == "import_zip":
# TODO: Copy files from the zip file to the new server directory
raise Exception("Not yet implemented")
_create_server_properties_if_needed(0, True)
server_command = create_data["command"]
server_file = ""
elif data["create_type"] == "custom":
# TODO: working_directory, executable_update
if root_create_data["create_type"] == "raw_exec":
pass
elif root_create_data["create_type"] == "import_server":
existing_server_path = Helpers.get_os_understandable_path(
create_data["existing_server_path"]
)
try:
FileHelpers.copy_dir(existing_server_path, new_server_path, True)
except shutil.Error as ex:
logger.error(f"Server import failed with error: {ex}")
elif root_create_data["create_type"] == "import_zip":
# TODO: Copy files from the zip file to the new server directory
raise Exception("Not yet implemented")
_create_server_properties_if_needed(0, True)
server_command = create_data["command"]
server_file = root_create_data["executable_update"].get("file", "")
stop_command = data.get("stop_command", "")
if stop_command == "":
# TODO: different default stop commands for server creation types
stop_command = "stop"
log_location = data.get("log_location", "")
if log_location == "":
# TODO: different default log locations for server creation types
log_location = "/logs/latest.log"
if data["monitoring_type"] == "minecraft_java":
monitoring_port = data["minecraft_java_monitoring_data"]["port"]
monitoring_host = data["minecraft_java_monitoring_data"]["host"]
monitoring_type = "minecraft-java"
elif data["monitoring_type"] == "minecraft_bedrock":
monitoring_port = data["minecraft_bedrock_monitoring_data"]["port"]
monitoring_host = data["minecraft_bedrock_monitoring_data"]["host"]
monitoring_type = "minecraft-bedrock"
elif data["monitoring_type"] == "none":
# TODO: this needs to be NUKED..
# There shouldn't be anything set if there are nothing to monitor
monitoring_port = 25565
monitoring_host = "127.0.0.1"
monitoring_type = "minecraft-java"
new_server_id = self.register_server(
name=data["name"],
server_uuid=server_fs_uuid,
server_dir=new_server_path,
backup_path=backup_path,
server_command=server_command,
server_file=server_file,
server_log_file=log_location,
server_stop=stop_command,
server_port=monitoring_port,
server_host=monitoring_host,
server_type=monitoring_type,
)
if (
data["create_type"] == "minecraft_java"
and root_create_data["create_type"] == "download_jar"
):
self.server_jars.download_jar(
create_data["type"],
create_data["version"],
full_jar_path,
new_server_id,
)
return new_server_id, server_fs_uuid
def create_jar_server( def create_jar_server(
self, self,
server: str, server: str,
@ -767,6 +944,7 @@ class Controller:
server_stop: str, server_stop: str,
server_port: int, server_port: int,
server_type: str, server_type: str,
server_host: str = "127.0.0.1",
): ):
# put data in the db # put data in the db
new_id = self.servers.create_server( new_id = self.servers.create_server(
@ -780,6 +958,7 @@ class Controller:
server_stop, server_stop,
server_type, server_type,
server_port, server_port,
server_host,
) )
if not Helpers.check_file_exists( if not Helpers.check_file_exists(
@ -796,7 +975,6 @@ class Controller:
"The server is managed by Crafty Controller.\n " "The server is managed by Crafty Controller.\n "
"Leave this directory/files alone please" "Leave this directory/files alone please"
) )
file.close()
except Exception as e: except Exception as e:
logger.error(f"Unable to create required server files due to :{e}") logger.error(f"Unable to create required server files due to :{e}")

View File

@ -81,7 +81,7 @@ class Migrator(object):
database = database.obj database = database.obj
self.database: SqliteDatabase = database self.database: SqliteDatabase = database
self.table_dict: t.Dict[str, peewee.Model] = {} self.table_dict: t.Dict[str, peewee.Model] = {}
self.operations: t.List[t.Union[Operation, callable]] = [] self.operations: t.List[t.Union[Operation, t.Callable]] = []
self.migrator = SqliteMigrator(database) self.migrator = SqliteMigrator(database)
def run(self): def run(self):

View File

@ -12,6 +12,7 @@ from apscheduler.triggers.cron import CronTrigger
from app.classes.models.management import HelpersManagement from app.classes.models.management import HelpersManagement
from app.classes.models.users import HelperUsers from app.classes.models.users import HelperUsers
from app.classes.shared.console import Console from app.classes.shared.console import Console
from app.classes.shared.main_controller import Controller
from app.classes.web.tornado_handler import Webserver from app.classes.web.tornado_handler import Webserver
logger = logging.getLogger("apscheduler") logger = logging.getLogger("apscheduler")
@ -32,6 +33,8 @@ scheduler_intervals = {
class TasksManager: class TasksManager:
controller: Controller
def __init__(self, helper, controller): def __init__(self, helper, controller):
self.helper = helper self.helper = helper
self.controller = controller self.controller = controller
@ -102,6 +105,17 @@ class TasksManager:
elif command == "restart_server": elif command == "restart_server":
svr.restart_threaded_server(user_id) svr.restart_threaded_server(user_id)
elif command == "kill_server":
try:
svr.kill()
time.sleep(5)
svr.cleanup_server_object()
svr.record_server_stats()
except Exception as e:
logger.error(
f"Could not find PID for requested termsig. Full error: {e}"
)
elif command == "backup_server": elif command == "backup_server":
svr.backup_server() svr.backup_server()

View File

@ -0,0 +1,23 @@
from typing import Awaitable, Callable, Optional
from app.classes.web.base_handler import BaseHandler
class BaseApiHandler(BaseHandler):
# {{{ Disable XSRF protection on API routes
def check_xsrf_cookie(self) -> None:
pass
# }}}
# {{{ 405 Method Not Allowed as JSON
def _unimplemented_method(self, *_args: str, **_kwargs: str) -> None:
self.finish_json(405, {"status": "error", "error": "METHOD_NOT_ALLOWED"})
head = _unimplemented_method # type: Callable[..., Optional[Awaitable[None]]]
get = _unimplemented_method # type: Callable[..., Optional[Awaitable[None]]]
post = _unimplemented_method # type: Callable[..., Optional[Awaitable[None]]]
delete = _unimplemented_method # type: Callable[..., Optional[Awaitable[None]]]
patch = _unimplemented_method # type: Callable[..., Optional[Awaitable[None]]]
put = _unimplemented_method # type: Callable[..., Optional[Awaitable[None]]]
options = _unimplemented_method # type: Callable[..., Optional[Awaitable[None]]]
# }}}

View File

@ -1,18 +1,50 @@
import logging import logging
from typing import Union, List, Optional, Tuple, Dict, Any import re
import typing as t
import orjson
import bleach import bleach
import tornado.web import tornado.web
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.models.users import ApiKeys from app.classes.models.users import ApiKeys
from app.classes.shared.helpers import Helpers
from app.classes.shared.main_controller import Controller
from app.classes.shared.translation import Translation
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
bearer_pattern = re.compile(r"^Bearer ", flags=re.IGNORECASE)
class BaseHandler(tornado.web.RequestHandler): class BaseHandler(tornado.web.RequestHandler):
def set_default_headers(self) -> None:
"""
Fix CORS
"""
self.set_header("Access-Control-Allow-Origin", "*")
self.set_header(
"Access-Control-Allow-Headers",
"Content-Type, x-requested-with, Authorization",
)
self.set_header(
"Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS"
)
def options(self, *_, **__):
"""
Fix CORS
"""
# no body
self.set_status(204)
self.finish()
nobleach = {bool, type(None)} nobleach = {bool, type(None)}
redactables = ("pass", "api") redactables = ("pass", "api")
helper: Helpers
controller: Controller
translator: Translation
# noinspection PyAttributeOutsideInit # noinspection PyAttributeOutsideInit
def initialize( def initialize(
self, helper=None, controller=None, tasks_manager=None, translator=None self, helper=None, controller=None, tasks_manager=None, translator=None
@ -30,11 +62,25 @@ class BaseHandler(tornado.web.RequestHandler):
) )
return remote_ip return remote_ip
current_user: Optional[Tuple[Optional[ApiKeys], Dict[str, Any], Dict[str, Any]]] current_user: t.Tuple[t.Optional[ApiKeys], t.Dict[str, t.Any], t.Dict[str, t.Any]]
"""
A variable that contains the current user's data. Please see
Please only use this with routes using the `@tornado.web.authenticated` decorator.
"""
def get_current_user( def get_current_user(
self, self,
) -> Optional[Tuple[Optional[ApiKeys], Dict[str, Any], Dict[str, Any]]]: ) -> t.Optional[
t.Tuple[t.Optional[ApiKeys], t.Dict[str, t.Any], t.Dict[str, t.Any]]
]:
"""
Get the token's API key, the token's payload and user data.
Returns:
t.Optional[ApiKeys]: The API key of the token.
t.Dict[str, t.Any]: The token's payload.
t.Dict[str, t.Any]: The user's data from the database.
"""
return self.controller.authentication.check(self.get_cookie("token")) return self.controller.authentication.check(self.get_cookie("token"))
def autobleach(self, name, text): def autobleach(self, name, text):
@ -53,15 +99,15 @@ class BaseHandler(tornado.web.RequestHandler):
def get_argument( def get_argument(
self, self,
name: str, name: str,
default: Union[ default: t.Union[
None, str, tornado.web._ArgDefaultMarker None, str, tornado.web._ArgDefaultMarker
] = tornado.web._ARG_DEFAULT, ] = tornado.web._ARG_DEFAULT,
strip: bool = True, strip: bool = True,
) -> Optional[str]: ) -> t.Optional[str]:
arg = self._get_argument(name, default, self.request.arguments, strip) arg = self._get_argument(name, default, self.request.arguments, strip)
return self.autobleach(name, arg) return self.autobleach(name, arg)
def get_arguments(self, name: str, strip: bool = True) -> List[str]: def get_arguments(self, name: str, strip: bool = True) -> t.List[str]:
if not isinstance(strip, bool): if not isinstance(strip, bool):
raise AssertionError raise AssertionError
args = self._get_arguments(name, self.request.arguments, strip) args = self._get_arguments(name, self.request.arguments, strip)
@ -69,3 +115,127 @@ class BaseHandler(tornado.web.RequestHandler):
for arg in args: for arg in args:
args_ret += self.autobleach(name, arg) args_ret += self.autobleach(name, arg)
return args_ret return args_ret
def access_denied(self, user: t.Optional[str], reason: t.Optional[str]):
ip = self.get_remote_ip()
route = self.request.path
if user is not None:
user_data = f"User {user} from IP {ip}"
else:
user_data = f"An unknown user from IP {ip}"
if reason:
ending = f"to the API route {route} because {reason}"
else:
ending = f"to the API route {route}"
logger.info(f"{user_data} was denied access {ending}")
self.finish_json(
403,
{
"status": "error",
"error": "ACCESS_DENIED",
"info": "You were denied access to the requested resource",
},
)
def _auth_get_api_token(self) -> t.Optional[str]:
"""Get an API token from the request
The API token is searched in the following order:
1. The `token` query parameter
2. The `Authorization` header
3. The `token` cookie
Returns:
t.Optional[str]: The API token or None if no token was found.
"""
logger.debug("Searching for specified token")
api_token = self.get_query_argument("token", None)
if api_token is None and self.request.headers.get("Authorization"):
api_token = bearer_pattern.sub(
"", self.request.headers.get("Authorization")
)
elif api_token is None:
api_token = self.get_cookie("token")
return api_token
def authenticate_user(
self,
) -> t.Optional[
t.Tuple[
t.List,
t.List[EnumPermissionsCrafty],
t.List[str],
bool,
t.Dict[str, t.Any],
]
]:
try:
api_key, _token_data, user = self.controller.authentication.check_err(
self._auth_get_api_token()
)
superuser = user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
exec_user_role = set()
if superuser:
authorized_servers = self.controller.list_defined_servers()
exec_user_role.add("Super User")
exec_user_crafty_permissions = (
self.controller.crafty_perms.list_defined_crafty_permissions()
)
else:
if api_key is not None:
exec_user_crafty_permissions = (
self.controller.crafty_perms.get_api_key_permissions_list(
api_key
)
)
else:
exec_user_crafty_permissions = (
self.controller.crafty_perms.get_crafty_permissions_list(
user["user_id"]
)
)
logger.debug(user["roles"])
for r in user["roles"]:
role = self.controller.roles.get_role(r)
exec_user_role.add(role["role_name"])
authorized_servers = self.controller.servers.get_authorized_servers(
user["user_id"] # TODO: API key authorized servers?
)
logger.debug("Checking results")
if user:
return (
authorized_servers,
exec_user_crafty_permissions,
exec_user_role,
superuser,
user,
)
else:
logging.debug("Auth unsuccessful")
self.access_denied(None, "the user provided an invalid token")
return None
except Exception as auth_exception:
logger.debug(
"An error occured while authenticating an API user:",
exc_info=auth_exception,
)
self.finish_json(
403,
{
"status": "error",
"error": "ACCESS_DENIED",
"info": "An error occured while authenticating the user",
},
)
return None
def finish_json(self, status: int, data: t.Dict[str, t.Any]):
self.set_status(status)
self.set_header("Content-Type", "application/json")
self.finish(orjson.dumps(data)) # pylint: disable=no-member

View File

@ -2,7 +2,7 @@
import time import time
import datetime import datetime
import os import os
from typing import Dict, Any, Tuple import typing as t
import json import json
import logging import logging
import threading import threading
@ -31,16 +31,16 @@ logger = logging.getLogger(__name__)
class PanelHandler(BaseHandler): class PanelHandler(BaseHandler):
def get_user_roles(self) -> Dict[str, list]: def get_user_roles(self) -> t.Dict[str, list]:
user_roles = {} user_roles = {}
for user in self.controller.users.get_all_users(): for user_id in self.controller.users.get_all_user_ids():
user_roles_list = self.controller.users.get_user_roles_names(user.user_id) user_roles_list = self.controller.users.get_user_roles_names(user_id)
# user_servers = # user_servers =
# self.controller.servers.get_authorized_servers(user.user_id) # self.controller.servers.get_authorized_servers(user.user_id)
user_roles[user.user_id] = user_roles_list user_roles[user_id] = user_roles_list
return user_roles return user_roles
def get_role_servers(self) -> set: def get_role_servers(self) -> t.Set[int]:
servers = set() servers = set()
for server in self.controller.list_defined_servers(): for server in self.controller.list_defined_servers():
argument = self.get_argument(f"server_{server['server_id']}_access", "0") argument = self.get_argument(f"server_{server['server_id']}_access", "0")
@ -60,7 +60,7 @@ class PanelHandler(BaseHandler):
servers.add((server["server_id"], permission_mask)) servers.add((server["server_id"], permission_mask))
return servers return servers
def get_perms_quantity(self) -> Tuple[str, dict]: def get_perms_quantity(self) -> t.Tuple[str, dict]:
permissions_mask: str = "000" permissions_mask: str = "000"
server_quantity: dict = {} server_quantity: dict = {}
for ( for (
@ -101,6 +101,16 @@ class PanelHandler(BaseHandler):
) )
return permissions_mask return permissions_mask
def get_perms_server(self) -> str:
permissions_mask: str = "00000000"
for permission in self.controller.server_perms.list_defined_permissions():
argument = self.get_argument(f"permission_{permission.name}", None)
if argument is not None:
permissions_mask = self.controller.server_perms.set_permission(
permissions_mask, permission, 1 if argument == "1" else 0
)
return permissions_mask
def get_user_role_memberships(self) -> set: def get_user_role_memberships(self) -> set:
roles = set() roles = set()
for role in self.controller.roles.get_all_roles(): for role in self.controller.roles.get_all_roles():
@ -260,7 +270,7 @@ class PanelHandler(BaseHandler):
user_order.remove(server_id) user_order.remove(server_id)
defined_servers = page_servers defined_servers = page_servers
page_data: Dict[str, Any] = { page_data: t.Dict[str, t.Any] = {
# todo: make this actually pull and compare version data # todo: make this actually pull and compare version data
"update_available": False, "update_available": False,
"serverTZ": get_localzone(), "serverTZ": get_localzone(),
@ -302,6 +312,8 @@ class PanelHandler(BaseHandler):
else None, else None,
"superuser": superuser, "superuser": superuser,
} }
# http://en.gravatar.com/site/implement/images/#rating
if self.helper.get_setting("allow_nsfw_profile_pictures"): if self.helper.get_setting("allow_nsfw_profile_pictures"):
rating = "x" rating = "x"
else: else:

View File

@ -0,0 +1,157 @@
from app.classes.web.routes.api.index_handler import ApiIndexHandler
from app.classes.web.routes.api.jsonschema import (
ApiJsonSchemaHandler,
ApiJsonSchemaListHandler,
)
from app.classes.web.routes.api.not_found import ApiNotFoundHandler
from app.classes.web.routes.api.auth.invalidate_tokens import (
ApiAuthInvalidateTokensHandler,
)
from app.classes.web.routes.api.auth.login import ApiAuthLoginHandler
from app.classes.web.routes.api.roles.index import ApiRolesIndexHandler
from app.classes.web.routes.api.roles.role.index import ApiRolesRoleIndexHandler
from app.classes.web.routes.api.roles.role.servers import ApiRolesRoleServersHandler
from app.classes.web.routes.api.roles.role.users import ApiRolesRoleUsersHandler
from app.classes.web.routes.api.servers.index import ApiServersIndexHandler
from app.classes.web.routes.api.servers.server.action import (
ApiServersServerActionHandler,
)
from app.classes.web.routes.api.servers.server.index import ApiServersServerIndexHandler
from app.classes.web.routes.api.servers.server.logs import ApiServersServerLogsHandler
from app.classes.web.routes.api.servers.server.public import (
ApiServersServerPublicHandler,
)
from app.classes.web.routes.api.servers.server.stats import ApiServersServerStatsHandler
from app.classes.web.routes.api.servers.server.users import ApiServersServerUsersHandler
from app.classes.web.routes.api.users.index import ApiUsersIndexHandler
from app.classes.web.routes.api.users.user.index import ApiUsersUserIndexHandler
from app.classes.web.routes.api.users.user.pfp import ApiUsersUserPfpHandler
from app.classes.web.routes.api.users.user.public import ApiUsersUserPublicHandler
def api_handlers(handler_args):
return [
# Auth routes
(
r"/api/v2/auth/login/?",
ApiAuthLoginHandler,
handler_args,
),
(
r"/api/v2/auth/invalidate_tokens/?",
ApiAuthInvalidateTokensHandler,
handler_args,
),
# User routes
(
r"/api/v2/users/?",
ApiUsersIndexHandler,
handler_args,
),
(
r"/api/v2/users/([0-9]+)/?",
ApiUsersUserIndexHandler,
handler_args,
),
(
r"/api/v2/users/(@me)/?",
ApiUsersUserIndexHandler,
handler_args,
),
(
r"/api/v2/users/([0-9]+)/pfp/?",
ApiUsersUserPfpHandler,
handler_args,
),
(
r"/api/v2/users/(@me)/pfp/?",
ApiUsersUserPfpHandler,
handler_args,
),
(
r"/api/v2/users/([0-9]+)/public/?",
ApiUsersUserPublicHandler,
handler_args,
),
(
r"/api/v2/users/(@me)/public/?",
ApiUsersUserPublicHandler,
handler_args,
),
# Server routes
(
r"/api/v2/servers/?",
ApiServersIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/?",
ApiServersServerIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/stats/?",
ApiServersServerStatsHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/action/([a-z_]+)/?",
ApiServersServerActionHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/logs/?",
ApiServersServerLogsHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/users/?",
ApiServersServerUsersHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/public/?",
ApiServersServerPublicHandler,
handler_args,
),
(
r"/api/v2/roles/?",
ApiRolesIndexHandler,
handler_args,
),
(
r"/api/v2/roles/([0-9]+)/?",
ApiRolesRoleIndexHandler,
handler_args,
),
(
r"/api/v2/roles/([0-9]+)/servers/?",
ApiRolesRoleServersHandler,
handler_args,
),
(
r"/api/v2/roles/([0-9]+)/users/?",
ApiRolesRoleUsersHandler,
handler_args,
),
(
r"/api/v2/jsonschema/?",
ApiJsonSchemaListHandler,
handler_args,
),
(
r"/api/v2/jsonschema/([a-z0-9_]+)/?",
ApiJsonSchemaHandler,
handler_args,
),
(
r"/api/v2/?",
ApiIndexHandler,
handler_args,
),
(
r"/api/v2/(.*)",
ApiNotFoundHandler,
handler_args,
),
]

View File

@ -0,0 +1,21 @@
import datetime
import logging
from app.classes.shared.console import Console
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
class ApiAuthInvalidateTokensHandler(BaseApiHandler):
def post(self):
auth_data = self.authenticate_user()
if not auth_data:
return
# TODO: Invalidate tokens
Console.info("invalidate_tokens")
self.controller.users.raw_update_user(
auth_data[4]["user_id"], {"valid_tokens_from": datetime.datetime.now()}
)
self.finish_json(200, {"status": "ok"})

View File

@ -0,0 +1,104 @@
import logging
import json
from jsonschema import validate
from jsonschema.exceptions import ValidationError
from app.classes.models.users import Users
from app.classes.shared.helpers import Helpers
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
login_schema = {
"type": "object",
"properties": {
"username": {
"type": "string",
"maxLength": 20,
"minLength": 4,
"pattern": "^[a-z0-9_]+$",
},
"password": {"type": "string", "maxLength": 20, "minLength": 4},
},
"required": ["username", "password"],
"additionalProperties": False,
}
class ApiAuthLoginHandler(BaseApiHandler):
def post(self):
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, login_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
username = data["username"]
password = data["password"]
# pylint: disable=no-member
user_data = Users.get_or_none(Users.username == username)
if user_data is None:
return self.finish_json(
401,
{"status": "error", "error": "INCORRECT_CREDENTIALS", "token": None},
)
if not user_data.enabled:
self.finish_json(
403, {"status": "error", "error": "ACCOUNT_DISABLED", "token": None}
)
return
login_result = self.helper.verify_pass(password, user_data.password)
# Valid Login
if login_result:
logger.info(f"User: {user_data} Logged in from IP: {self.get_remote_ip()}")
# record this login
query = Users.select().where(Users.username == username.lower()).get()
query.last_ip = self.get_remote_ip()
query.last_login = Helpers.get_time_as_string()
query.save()
# log this login
self.controller.management.add_to_audit_log(
user_data.user_id, "logged in via the API", 0, self.get_remote_ip()
)
self.finish_json(
200,
{
"status": "ok",
"data": {
"token": self.controller.authentication.generate(
user_data.user_id
),
"user_id": str(user_data.user_id),
},
},
)
else:
# log this failed login attempt
self.controller.management.add_to_audit_log(
user_data.user_id, "Tried to log in", 0, self.get_remote_ip()
)
self.finish_json(
401,
{"status": "error", "error": "INCORRECT_CREDENTIALS"},
)

View File

@ -0,0 +1,2 @@
# nothing here yet
# sometime implement configurable self service account creation?

View File

@ -0,0 +1,17 @@
from app.classes.web.base_api_handler import BaseApiHandler
WIKI_API_LINK = "https://wiki.craftycontrol.com/en/4/docs/API V2"
class ApiIndexHandler(BaseApiHandler):
def get(self):
self.finish_json(
200,
{
"status": "ok",
"data": {
"version": self.controller.helper.get_version_string(),
"message": f"Please see the API documentation at {WIKI_API_LINK}",
},
},
)

View File

@ -0,0 +1,107 @@
import typing as t
from app.classes.web.base_api_handler import BaseApiHandler
from app.classes.web.routes.api.auth.login import login_schema
from app.classes.web.routes.api.roles.role.index import modify_role_schema
from app.classes.web.routes.api.roles.index import create_role_schema
from app.classes.web.routes.api.servers.server.index import server_patch_schema
from app.classes.web.routes.api.servers.index import new_server_schema
SCHEMA_LIST: t.Final = [
"login",
"modify_role",
"create_role",
"server_patch",
"new_server",
"user_patch",
"new_user",
]
class ApiJsonSchemaListHandler(BaseApiHandler):
def get(self):
self.finish_json(
200,
{"status": "ok", "data": SCHEMA_LIST},
)
class ApiJsonSchemaHandler(BaseApiHandler):
def get(self, schema_name: str):
if schema_name == "login":
self.finish_json(
200,
{"status": "ok", "data": login_schema},
)
elif schema_name == "modify_role":
self.finish_json(
200,
{"status": "ok", "data": modify_role_schema},
)
elif schema_name == "create_role":
self.finish_json(
200,
{"status": "ok", "data": create_role_schema},
)
elif schema_name == "server_patch":
self.finish_json(200, server_patch_schema)
elif schema_name == "new_server":
self.finish_json(
200,
new_server_schema,
)
elif schema_name == "user_patch":
self.finish_json(
200,
{
"status": "ok",
"data": {
"type": "object",
"properties": {
**self.controller.users.user_jsonschema_props,
},
"anyOf": [
# Require at least one property
{"required": [name]}
for name in [
"username",
"password",
"email",
"enabled",
"lang",
"superuser",
"permissions",
"roles",
"hints",
]
],
"additionalProperties": False,
},
},
)
elif schema_name == "new_user":
self.finish_json(
200,
{
"status": "ok",
"data": {
"type": "object",
"properties": {
**self.controller.users.user_jsonschema_props,
},
"required": ["username", "password"],
"additionalProperties": False,
},
},
)
else:
self.finish_json(
404,
{
"status": "error",
"error": "UNKNOWN_JSON_SCHEMA",
"info": (
f"Unknown JSON schema: {schema_name}."
f" Here's a list of all the valid schema names: {SCHEMA_LIST}"
),
},
)

View File

@ -0,0 +1,18 @@
from typing import Awaitable, Callable, Optional
from app.classes.web.base_api_handler import BaseApiHandler
class ApiNotFoundHandler(BaseApiHandler):
def _not_found(self, page: str) -> None:
self.finish_json(
404,
{"status": "error", "error": "API_HANDLER_NOT_FOUND", "page": page},
)
head = _not_found # type: Callable[..., Optional[Awaitable[None]]]
get = _not_found # type: Callable[..., Optional[Awaitable[None]]]
post = _not_found # type: Callable[..., Optional[Awaitable[None]]]
delete = _not_found # type: Callable[..., Optional[Awaitable[None]]]
patch = _not_found # type: Callable[..., Optional[Awaitable[None]]]
put = _not_found # type: Callable[..., Optional[Awaitable[None]]]
options = _not_found # type: Callable[..., Optional[Awaitable[None]]]

View File

@ -0,0 +1,131 @@
import typing as t
from jsonschema import ValidationError, validate
import orjson
from playhouse.shortcuts import model_to_dict
from app.classes.web.base_api_handler import BaseApiHandler
create_role_schema = {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
},
"servers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"server_id": {
"type": "integer",
"minimum": 1,
},
"permissions": {
"type": "string",
"pattern": "^[01]{8}$", # 8 bits, see EnumPermissionsServer
},
},
"required": ["server_id", "permissions"],
},
},
},
"required": ["name"],
"additionalProperties": False,
}
class ApiRolesIndexHandler(BaseApiHandler):
def get(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
superuser,
_,
) = auth_data
# GET /api/v2/roles?ids=true
get_only_ids = self.get_query_argument("ids", None) == "true"
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.finish_json(
200,
{
"status": "ok",
"data": self.controller.roles.get_all_role_ids()
if get_only_ids
else [model_to_dict(r) for r in self.controller.roles.get_all_roles()],
},
)
def post(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
superuser,
user,
) = auth_data
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = orjson.loads(self.request.body) # pylint: disable=no-member
except orjson.decoder.JSONDecodeError as e: # pylint: disable=no-member
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, create_role_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
role_name = data["name"]
# Get the servers
servers_dict = {server["server_id"]: server for server in data["servers"]}
server_ids = (
(
{server["server_id"] for server in data["servers"]}
& set(self.controller.get_all_server_ids())
) # Only allow existing servers
if "servers" in data
else set()
)
servers: t.List[dict] = [servers_dict[server_id] for server_id in server_ids]
if self.controller.roles.get_roleid_by_name(role_name) is not None:
return self.finish_json(
400, {"status": "error", "error": "ROLE_NAME_ALREADY_EXISTS"}
)
role_id = self.controller.roles.add_role_advanced(role_name, servers)
self.controller.management.add_to_audit_log(
user["user_id"],
f"created role {role_name} (RID:{role_id})",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.finish_json(
200,
{"status": "ok", "data": {"role_id": role_id}},
)

View File

@ -0,0 +1,143 @@
from jsonschema import ValidationError, validate
import orjson
from peewee import DoesNotExist
from app.classes.web.base_api_handler import BaseApiHandler
modify_role_schema = {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
},
"servers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"server_id": {
"type": "integer",
"minimum": 1,
},
"permissions": {
"type": "string",
"pattern": "^[01]{8}$", # 8 bits, see EnumPermissionsServer
},
},
"required": ["server_id", "permissions"],
},
},
},
"anyOf": [
{"required": ["name"]},
{"required": ["servers"]},
],
"additionalProperties": False,
}
class ApiRolesRoleIndexHandler(BaseApiHandler):
def get(self, role_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
superuser,
_,
) = auth_data
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
self.finish_json(
200,
{"status": "ok", "data": self.controller.roles.get_role(role_id)},
)
except DoesNotExist:
self.finish_json(404, {"status": "error", "error": "ROLE_NOT_FOUND"})
def delete(self, role_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
superuser,
user,
) = auth_data
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.controller.roles.remove_role(role_id)
self.finish_json(
200,
{"status": "ok", "data": role_id},
)
self.controller.management.add_to_audit_log(
user["user_id"],
f"deleted role with ID {role_id}",
server_id=0,
source_ip=self.get_remote_ip(),
)
def patch(self, role_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
superuser,
user,
) = auth_data
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = orjson.loads(self.request.body) # pylint: disable=no-member
except orjson.decoder.JSONDecodeError as e: # pylint: disable=no-member
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, modify_role_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
try:
self.controller.roles.update_role_advanced(
role_id, data.get("role_name", None), data.get("servers", None)
)
except DoesNotExist:
return self.finish_json(404, {"status": "error", "error": "ROLE_NOT_FOUND"})
self.controller.management.add_to_audit_log(
user["user_id"],
f"modified role with ID {role_id}",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.finish_json(
200,
{"status": "ok"},
)

View File

@ -0,0 +1,32 @@
from app.classes.models.server_permissions import PermissionsServers
from app.classes.web.base_api_handler import BaseApiHandler
class ApiRolesRoleServersHandler(BaseApiHandler):
def get(self, role_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
superuser,
_,
) = auth_data
# GET /api/v2/roles/role/servers?ids=true
get_only_ids = self.get_query_argument("ids", None) == "true"
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.finish_json(
200,
{
"status": "ok",
"data": PermissionsServers.get_server_ids_from_role(role_id)
if get_only_ids
else self.controller.roles.get_server_ids_and_perms_from_role(role_id),
},
)

View File

@ -0,0 +1,36 @@
from app.classes.web.base_api_handler import BaseApiHandler
class ApiRolesRoleUsersHandler(BaseApiHandler):
def get(self, role_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
superuser,
_,
) = auth_data
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
all_user_ids = self.controller.users.get_all_user_ids()
user_roles = {}
for user_id in all_user_ids:
user_roles_list = self.controller.users.get_user_roles_names(user_id)
user_roles[user_id] = user_roles_list
role = self.controller.roles.get_role(role_id)
user_ids = []
for user_id in all_user_ids:
for role_user in user_roles[user_id]:
if role_user == role["role_name"]:
user_ids.append(user_id)
self.finish_json(200, {"status": "ok", "data": user_ids})

View File

@ -0,0 +1,713 @@
import logging
from jsonschema import ValidationError, validate
import orjson
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
new_server_schema = {
"definitions": {},
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Root",
"type": "object",
"required": [
"name",
"monitoring_type",
"create_type",
],
"examples": [
{
"name": "My Server",
"monitoring_type": "minecraft_java",
"minecraft_java_monitoring_data": {"host": "127.0.0.1", "port": 25565},
"create_type": "minecraft_java",
"minecraft_java_create_data": {
"create_type": "download_jar",
"download_jar_create_data": {
"type": "Paper",
"version": "1.18.2",
"mem_min": 1,
"mem_max": 2,
"server_properties_port": 25565,
},
},
}
],
"properties": {
"name": {
"title": "Name",
"type": "string",
"examples": ["My Server"],
"minLength": 2,
},
"stop_command": {
"title": "Stop command",
"description": '"" means the default for the server creation type.',
"type": "string",
"default": "",
"examples": ["stop", "end"],
},
"log_location": {
"title": "Log file",
"description": '"" means the default for the server creation type.',
"type": "string",
"default": "",
"examples": ["./logs/latest.log", "./proxy.log.0"],
},
"crashdetection": {
"title": "Crash detection",
"type": "boolean",
"default": False,
},
"autostart": {
"title": "Autostart",
"description": "If true, the server will be started"
+ " automatically when Crafty is launched.",
"type": "boolean",
"default": False,
},
"autostart_delay": {
"title": "Autostart delay",
"description": "Delay in seconds before autostarting. (If enabled)",
"type": "number",
"default": 10,
"minimum": 0,
},
"monitoring_type": {
"title": "Server monitoring type",
"type": "string",
"default": "minecraft_java",
"enum": ["minecraft_java", "minecraft_bedrock", "none"],
# TODO: SteamCMD, RakNet, etc.
},
"minecraft_java_monitoring_data": {
"title": "Minecraft Java monitoring data",
"type": "object",
"required": ["host", "port"],
"properties": {
"host": {
"title": "Host",
"type": "string",
"default": "127.0.0.1",
"examples": ["127.0.0.1"],
"minLength": 1,
},
"port": {
"title": "Port",
"type": "integer",
"examples": [25565],
"default": 25565,
"minimum": 0,
},
},
},
"minecraft_bedrock_monitoring_data": {
"title": "Minecraft Bedrock monitoring data",
"type": "object",
"required": ["host", "port"],
"properties": {
"host": {
"title": "Host",
"type": "string",
"default": "127.0.0.1",
"examples": ["127.0.0.1"],
"minLength": 1,
},
"port": {
"title": "Port",
"type": "integer",
"examples": [19132],
"default": 19132,
"minimum": 0,
},
},
},
"create_type": {
# This is only used for creation, this is not saved in the db
"title": "Server creation type",
"type": "string",
"default": "minecraft_java",
"enum": ["minecraft_java", "minecraft_bedrock", "custom"],
},
"minecraft_java_create_data": {
"title": "Java creation data",
"type": "object",
"required": ["create_type"],
"properties": {
"create_type": {
"title": "Creation type",
"type": "string",
"default": "download_jar",
"enum": ["download_jar", "import_server", "import_zip"],
},
"download_jar_create_data": {
"title": "JAR download data",
"type": "object",
"required": [
"type",
"version",
"mem_min",
"mem_max",
"server_properties_port",
"agree_to_eula",
],
"properties": {
"type": {
"title": "Server JAR Type",
"type": "string",
"examples": ["Paper"],
"minLength": 1,
},
"version": {
"title": "Server JAR Version",
"type": "string",
"examples": ["1.18.2"],
"minLength": 1,
},
"mem_min": {
"title": "Minimum JVM memory (in GiBs)",
"type": "number",
"examples": [1],
"default": 1,
"exclusiveMinimum": 0,
},
"mem_max": {
"title": "Maximum JVM memory (in GiBs)",
"type": "number",
"examples": [2],
"default": 2,
"exclusiveMinimum": 0,
},
"server_properties_port": {
"title": "Port",
"type": "integer",
"examples": [25565],
"default": 25565,
"minimum": 0,
},
"agree_to_eula": {
"title": "Agree to the EULA",
"type": "boolean",
"default": False,
},
},
},
"import_server_create_data": {
"title": "Import server data",
"type": "object",
"required": [
"existing_server_path",
"jarfile",
"mem_min",
"mem_max",
"server_properties_port",
"agree_to_eula",
],
"properties": {
"existing_server_path": {
"title": "Server path",
"description": "Absolute path to the old server",
"type": "string",
"examples": ["/var/opt/server"],
"minLength": 1,
},
"jarfile": {
"title": "JAR file",
"description": "The JAR file relative to the previous path",
"type": "string",
"examples": ["paper.jar", "jars/vanilla-1.12.jar"],
"minLength": 1,
},
"mem_min": {
"title": "Minimum JVM memory (in GiBs)",
"type": "number",
"examples": [1],
"default": 1,
"exclusiveMinimum": 0,
},
"mem_max": {
"title": "Maximum JVM memory (in GiBs)",
"type": "number",
"examples": [2],
"default": 2,
"exclusiveMinimum": 0,
},
"server_properties_port": {
"title": "Port",
"type": "integer",
"examples": [25565],
"default": 25565,
"minimum": 0,
},
"agree_to_eula": {
"title": "Agree to the EULA",
"type": "boolean",
"default": False,
},
},
},
"import_zip_create_data": {
"title": "Import ZIP server data",
"type": "object",
"required": [
"zip_path",
"zip_root",
"jarfile",
"mem_min",
"mem_max",
"server_properties_port",
"agree_to_eula",
],
"properties": {
"zip_path": {
"title": "ZIP path",
"description": "Absolute path to the ZIP archive",
"type": "string",
"examples": ["/var/opt/server.zip"],
"minLength": 1,
},
"zip_root": {
"title": "Server root directory",
"description": "The server root in the ZIP archive",
"type": "string",
"examples": ["/", "/paper-server/", "server-1"],
"minLength": 1,
},
"jarfile": {
"title": "JAR file",
"description": "The JAR relative to the configured root",
"type": "string",
"examples": ["paper.jar", "jars/vanilla-1.12.jar"],
"minLength": 1,
},
"mem_min": {
"title": "Minimum JVM memory (in GiBs)",
"type": "number",
"examples": [1],
"default": 1,
"exclusiveMinimum": 0,
},
"mem_max": {
"title": "Maximum JVM memory (in GiBs)",
"type": "number",
"examples": [2],
"default": 2,
"exclusiveMinimum": 0,
},
"server_properties_port": {
"title": "Port",
"type": "integer",
"examples": [25565],
"default": 25565,
"minimum": 0,
},
"agree_to_eula": {
"title": "Agree to the EULA",
"type": "boolean",
"default": False,
},
},
},
},
"allOf": [
{
"$comment": "If..then section",
"allOf": [
{
"if": {
"properties": {"create_type": {"const": "download_jar"}}
},
"then": {"required": ["download_jar_create_data"]},
},
{
"if": {
"properties": {"create_type": {"const": "import_exec"}}
},
"then": {"required": ["import_server_create_data"]},
},
{
"if": {
"properties": {"create_type": {"const": "import_zip"}}
},
"then": {"required": ["import_zip_create_data"]},
},
],
},
{
"title": "Only one creation data",
"oneOf": [
{"required": ["download_jar_create_data"]},
{"required": ["import_server_create_data"]},
{"required": ["import_zip_create_data"]},
],
},
],
},
"minecraft_bedrock_create_data": {
"title": "Minecraft Bedrock creation data",
"type": "object",
"required": ["create_type"],
"properties": {
"create_type": {
"title": "Creation type",
"type": "string",
"default": "import_server",
"enum": ["import_server", "import_zip"],
},
"import_server_create_data": {
"title": "Import server data",
"type": "object",
"required": ["existing_server_path", "command"],
"properties": {
"existing_server_path": {
"title": "Server path",
"description": "Absolute path to the old server",
"type": "string",
"examples": ["/var/opt/server"],
"minLength": 1,
},
"command": {
"title": "Command",
"type": "string",
"default": "echo foo bar baz",
"examples": ["LD_LIBRARY_PATH=. ./bedrock_server"],
"minLength": 1,
},
},
},
"import_zip_create_data": {
"title": "Import ZIP server data",
"type": "object",
"required": ["zip_path", "zip_root", "command"],
"properties": {
"zip_path": {
"title": "ZIP path",
"description": "Absolute path to the ZIP archive",
"type": "string",
"examples": ["/var/opt/server.zip"],
"minLength": 1,
},
"zip_root": {
"title": "Server root directory",
"description": "The server root in the ZIP archive",
"type": "string",
"examples": ["/", "/paper-server/", "server-1"],
"minLength": 1,
},
"command": {
"title": "Command",
"type": "string",
"default": "echo foo bar baz",
"examples": ["LD_LIBRARY_PATH=. ./bedrock_server"],
"minLength": 1,
},
},
},
},
"allOf": [
{
"$comment": "If..then section",
"allOf": [
{
"if": {
"properties": {"create_type": {"const": "import_exec"}}
},
"then": {"required": ["import_server_create_data"]},
},
{
"if": {
"properties": {"create_type": {"const": "import_zip"}}
},
"then": {"required": ["import_zip_create_data"]},
},
],
},
{
"title": "Only one creation data",
"oneOf": [
{"required": ["import_server_create_data"]},
{"required": ["import_zip_create_data"]},
],
},
],
},
"custom_create_data": {
"title": "Custom creation data",
"type": "object",
"required": [
"working_directory",
"executable_update",
"create_type",
],
"properties": {
"working_directory": {
"title": "Working directory",
"description": '"" means the default',
"type": "string",
"default": "",
"examples": ["/mnt/mydrive/server-configs/", "./subdirectory", ""],
},
"executable_update": {
"title": "Executable Updation",
"description": "Also configurable later on and for other servers",
"type": "object",
"required": ["enabled", "file", "url"],
"properties": {
"enabled": {
"title": "Enabled",
"type": "boolean",
"default": False,
},
"file": {
"title": "Executable to update",
"type": "string",
"default": "",
"examples": ["./paper.jar"],
},
"url": {
"title": "URL to download the executable from",
"type": "string",
"default": "",
},
},
},
"create_type": {
"title": "Creation type",
"type": "string",
"default": "raw_exec",
"enum": ["raw_exec", "import_server", "import_zip"],
},
"raw_exec_create_data": {
"title": "Raw execution command create data",
"type": "object",
"required": ["command"],
"properties": {
"command": {
"title": "Command",
"type": "string",
"default": "echo foo bar baz",
"examples": ["caddy start"],
"minLength": 1,
}
},
},
"import_server_create_data": {
"title": "Import server data",
"type": "object",
"required": ["existing_server_path", "command"],
"properties": {
"existing_server_path": {
"title": "Server path",
"description": "Absolute path to the old server",
"type": "string",
"examples": ["/var/opt/server"],
"minLength": 1,
},
"command": {
"title": "Command",
"type": "string",
"default": "echo foo bar baz",
"examples": ["caddy start"],
"minLength": 1,
},
},
},
"import_zip_create_data": {
"title": "Import ZIP server data",
"type": "object",
"required": ["zip_path", "zip_root", "command"],
"properties": {
"zip_path": {
"title": "ZIP path",
"description": "Absolute path to the ZIP archive",
"type": "string",
"examples": ["/var/opt/server.zip"],
"minLength": 1,
},
"zip_root": {
"title": "Server root directory",
"description": "The server root in the ZIP archive",
"type": "string",
"examples": ["/", "/paper-server/", "server-1"],
"minLength": 1,
},
"command": {
"title": "Command",
"type": "string",
"default": "echo foo bar baz",
"examples": ["caddy start"],
"minLength": 1,
},
},
},
},
"allOf": [
{
"$comment": "If..then section",
"allOf": [
{
"if": {
"properties": {"create_type": {"const": "raw_exec"}}
},
"then": {"required": ["raw_exec_create_data"]},
},
{
"if": {
"properties": {
"create_type": {"const": "import_server"}
}
},
"then": {"required": ["import_server_create_data"]},
},
{
"if": {
"properties": {"create_type": {"const": "import_zip"}}
},
"then": {"required": ["import_zip_create_data"]},
},
],
},
{
"title": "Only one creation data",
"oneOf": [
{"required": ["raw_exec_create_data"]},
{"required": ["import_server_create_data"]},
{"required": ["import_zip_create_data"]},
],
},
],
},
},
"allOf": [
{
"$comment": "If..then section",
"allOf": [
# start require creation data
{
"if": {"properties": {"create_type": {"const": "minecraft_java"}}},
"then": {"required": ["minecraft_java_create_data"]},
},
{
"if": {
"properties": {"create_type": {"const": "minecraft_bedrock"}}
},
"then": {"required": ["minecraft_bedrock_create_data"]},
},
{
"if": {"properties": {"create_type": {"const": "custom"}}},
"then": {"required": ["custom_create_data"]},
},
# end require creation data
# start require monitoring data
{
"if": {
"properties": {"monitoring_type": {"const": "minecraft_java"}}
},
"then": {"required": ["minecraft_java_monitoring_data"]},
},
{
"if": {
"properties": {
"monitoring_type": {"const": "minecraft_bedrock"}
}
},
"then": {"required": ["minecraft_bedrock_monitoring_data"]},
},
# end require monitoring data
],
},
{
"title": "Only one creation data",
"oneOf": [
{"required": ["minecraft_java_create_data"]},
{"required": ["minecraft_bedrock_create_data"]},
{"required": ["custom_create_data"]},
],
},
{
"title": "Only one monitoring data",
"oneOf": [
{"required": ["minecraft_java_monitoring_data"]},
{"required": ["minecraft_bedrock_monitoring_data"]},
{"properties": {"monitoring_type": {"const": "none"}}},
],
},
],
}
class ApiServersIndexHandler(BaseApiHandler):
def get(self):
auth_data = self.authenticate_user()
if not auth_data:
return
# TODO: limit some columns for specific permissions
self.finish_json(200, {"status": "ok", "data": auth_data[0]})
def post(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
exec_user_crafty_permissions,
_,
_superuser,
user,
) = auth_data
if EnumPermissionsCrafty.SERVER_CREATION not in exec_user_crafty_permissions:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = orjson.loads(self.request.body) # pylint: disable=no-member
except orjson.decoder.JSONDecodeError as e: # pylint: disable=no-member
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, new_server_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
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"])
self.controller.stats.record_stats()
self.controller.management.add_to_audit_log(
user["user_id"],
(
f"created server {data['name']}"
f" (ID: {new_server_id})"
f" (UUID: {new_server_uuid})"
),
server_id=new_server_id,
source_ip=self.get_remote_ip(),
)
self.finish_json(
201,
{
"status": "ok",
"data": {
"new_server_id": str(new_server_id),
"new_server_uuid": new_server_uuid,
},
},
)

View File

@ -0,0 +1,98 @@
import logging
import os
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.models.servers import Servers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.helpers import Helpers
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
class ApiServersServerActionHandler(BaseApiHandler):
def post(self, server_id: str, action: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.COMMANDS
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Commands permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if action == "clone_server":
return self._clone_server(server_id, auth_data[4]["user_id"])
self.controller.management.send_command(
auth_data[4]["user_id"], server_id, self.get_remote_ip(), action
)
self.finish_json(
200,
{"status": "ok"},
)
def _clone_server(self, server_id, user_id):
def is_name_used(name):
return Servers.select().where(Servers.server_name == name).count() != 0
server_data = self.controller.servers.get_server_data_by_id(server_id)
server_uuid = server_data.get("server_uuid")
new_server_name = server_data.get("server_name") + " (Copy)"
name_counter = 1
while is_name_used(new_server_name):
name_counter += 1
new_server_name = server_data.get("server_name") + f" (Copy {name_counter})"
new_server_uuid = Helpers.create_uuid()
while os.path.exists(os.path.join(self.helper.servers_dir, new_server_uuid)):
new_server_uuid = Helpers.create_uuid()
new_server_path = os.path.join(self.helper.servers_dir, new_server_uuid)
self.controller.management.add_to_audit_log(
user_id,
f"is cloning server {server_id} named {server_data.get('server_name')}",
server_id,
self.get_remote_ip(),
)
# copy the old server
FileHelpers.copy_dir(server_data.get("path"), new_server_path)
# TODO get old server DB data to individual variables
new_server_command = str(server_data.get("execution_command")).replace(
server_uuid, new_server_uuid
)
new_server_log_file = str(
self.helper.get_os_understandable_path(server_data.get("log_path"))
).replace(server_uuid, new_server_uuid)
new_server_id = self.controller.servers.create_server(
new_server_name,
new_server_uuid,
new_server_path,
"",
new_server_command,
server_data.get("executable"),
new_server_log_file,
server_data.get("stop_command"),
server_data.get("type"),
server_data.get("server_port"),
)
self.controller.init_all_servers()
self.finish_json(
200,
{"status": "ok", "data": {"new_server_id": str(new_server_id)}},
)

View File

@ -0,0 +1,168 @@
import logging
import json
from jsonschema import validate
from jsonschema.exceptions import ValidationError
from playhouse.shortcuts import model_to_dict
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
# TODO: modify monitoring
server_patch_schema = {
"type": "object",
"properties": {
"server_name": {"type": "string", "minLength": 1},
"path": {"type": "string", "minLength": 1},
"backup_path": {"type": "string"},
"executable": {"type": "string"},
"log_path": {"type": "string", "minLength": 1},
"execution_command": {"type": "string", "minLength": 1},
"auto_start": {"type": "boolean"},
"auto_start_delay": {"type": "integer"},
"crash_detection": {"type": "boolean"},
"stop_command": {"type": "string"},
"executable_update_url": {"type": "string", "minLength": 1},
"server_ip": {"type": "string", "minLength": 1},
"server_port": {"type": "integer"},
"logs_delete_after": {"type": "integer"},
"type": {"type": "string", "minLength": 1},
},
"anyOf": [
# Require at least one property
{"required": [name]}
for name in [
"server_name",
"path",
"backup_path",
"executable",
"log_path",
"execution_command",
"auto_start",
"auto_start_delay",
"crash_detection",
"stop_command",
"executable_update_url",
"server_ip",
"server_port",
"logs_delete_after",
"type",
]
],
"additionalProperties": False,
}
class ApiServersServerIndexHandler(BaseApiHandler):
def get(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
server_obj = self.controller.servers.get_server_obj(server_id)
server = model_to_dict(server_obj)
# TODO: limit some columns for specific permissions?
self.finish_json(200, {"status": "ok", "data": server})
def patch(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, server_patch_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.CONFIG
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Config permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
server_obj = self.controller.servers.get_server_obj(server_id)
for key in data:
# If we don't validate the input there could be security issues
setattr(self, key, data[key])
self.controller.servers.update_server(server_obj)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"modified the server with ID {server_id}",
server_id,
self.get_remote_ip(),
)
return self.finish_json(200, {"status": "ok"})
def delete(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
# DELETE /api/v2/servers/server?files=true
remove_files = self.get_query_argument("files", None) == "true"
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.CONFIG
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Config permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
logger.info(
(
"Removing server and all associated files for server: "
if remove_files
else "Removing server from panel for server: "
)
+ self.controller.servers.get_server_friendly_name(server_id)
)
self.tasks_manager.remove_all_server_tasks(server_id)
self.controller.remove_server(server_id, remove_files)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"deleted the server {server_id}",
server_id,
self.get_remote_ip(),
)
self.finish_json(
200,
{"status": "ok"},
)

View File

@ -0,0 +1,73 @@
import html
import logging
import re
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.shared.server import ServerOutBuf
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
class ApiServersServerLogsHandler(BaseApiHandler):
def get(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
# GET /api/v2/servers/server/logs?file=true
read_log_file = self.get_query_argument("file", None) == "true"
# GET /api/v2/servers/server/logs?colors=true
colored_output = self.get_query_argument("colors", None) == "true"
# GET /api/v2/servers/server/logs?raw=true
disable_ansi_strip = self.get_query_argument("raw", None) == "true"
# GET /api/v2/servers/server/logs?html=true
use_html = self.get_query_argument("html", None) == "true"
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.LOGS
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Logs permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
server_data = self.controller.servers.get_server_data_by_id(server_id)
if read_log_file:
log_lines = self.helper.get_setting("max_log_lines")
raw_lines = self.helper.tail_file(
self.helper.get_os_understandable_path(server_data["log_path"]),
log_lines,
)
else:
raw_lines = ServerOutBuf.lines.get(server_id, [])
lines = []
for line in raw_lines:
try:
if not disable_ansi_strip:
line = re.sub(
"(\033\\[(0;)?[0-9]*[A-z]?(;[0-9])?m?)|(> )", "", line
)
line = re.sub("[A-z]{2}\b\b", "", line)
line = html.escape(line)
if colored_output:
line = self.helper.log_colors(line)
lines.append(line)
except Exception as e:
logger.warning(f"Skipping Log Line due to error: {e}")
if use_html:
for line in lines:
self.write(f"{line}<br />")
else:
self.finish_json(200, {"status": "ok", "data": lines})

View File

@ -0,0 +1,23 @@
import logging
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
class ApiServersServerPublicHandler(BaseApiHandler):
def get(self, server_id):
auth_data = self.authenticate_user()
if not auth_data:
return
server_obj = self.controller.servers.get_server_obj(server_id)
self.finish_json(
200,
{
"status": "ok",
"data": {
key: getattr(server_obj, key)
for key in ["server_id", "created", "server_name", "type"]
},
},
)

View File

@ -0,0 +1,28 @@
import logging
from playhouse.shortcuts import model_to_dict
from app.classes.models.server_stats import HelperServerStats
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
class ApiServersServerStatsHandler(BaseApiHandler):
def get(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.finish_json(
200,
{
"status": "ok",
"data": model_to_dict(
HelperServerStats.get_latest_server_stats(server_id)[0]
),
},
)

View File

@ -0,0 +1,31 @@
import logging
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
class ApiServersServerUsersHandler(BaseApiHandler):
def get(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if EnumPermissionsCrafty.USER_CONFIG not in auth_data[1]:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if EnumPermissionsCrafty.ROLES_CONFIG not in auth_data[1]:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.finish_json(
200,
{
"status": "ok",
"data": list(self.controller.servers.get_authorized_users(server_id)),
},
)

View File

@ -0,0 +1,164 @@
import logging
import json
from jsonschema import validate
from jsonschema.exceptions import ValidationError
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.models.roles import Roles, HelperRoles
from app.classes.models.users import PUBLIC_USER_ATTRS
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
class ApiUsersIndexHandler(BaseApiHandler):
def get(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
exec_user_crafty_permissions,
_,
_,
user,
) = auth_data
# GET /api/v2/users?ids=true
get_only_ids = self.get_query_argument("ids", None) == "true"
if EnumPermissionsCrafty.USER_CONFIG in exec_user_crafty_permissions:
if get_only_ids:
data = self.controller.users.get_all_user_ids()
else:
data = [
{key: getattr(user_res, key) for key in PUBLIC_USER_ATTRS}
for user_res in self.controller.users.get_all_users().execute()
]
else:
if get_only_ids:
data = [user["user_id"]]
else:
user_res = self.controller.users.get_user_by_id(user["user_id"])
user_res["roles"] = list(
map(HelperRoles.get_role, user_res.get("roles", set()))
)
data = [{key: user_res[key] for key in PUBLIC_USER_ATTRS}]
self.finish_json(
200,
{
"status": "ok",
"data": data,
},
)
def post(self):
new_user_schema = {
"type": "object",
"properties": {
**self.controller.users.user_jsonschema_props,
},
"required": ["username", "password"],
"additionalProperties": False,
}
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
exec_user_crafty_permissions,
_,
superuser,
user,
) = auth_data
if EnumPermissionsCrafty.USER_CONFIG not in exec_user_crafty_permissions:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, new_user_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
username = data["username"]
password = data["password"]
email = data.get("email", "default@example.com")
enabled = data.get("enabled", True)
lang = data.get("lang", self.helper.get_setting("language"))
superuser = data.get("superuser", False)
permissions = data.get("permissions", None)
roles = data.get("roles", None)
hints = data.get("hints", True)
if username.lower() in ["system", ""]:
return self.finish_json(
400, {"status": "error", "error": "INVALID_USERNAME"}
)
if self.controller.users.get_id_by_name(username) is not None:
return self.finish_json(400, {"status": "error", "error": "USER_EXISTS"})
if roles is None:
roles = set()
else:
role_ids = [str(role_id) for role_id in Roles.select(Roles.role_id)]
roles = {role for role in roles if str(role) in role_ids}
permissions_mask = "0" * len(EnumPermissionsCrafty.__members__.items())
server_quantity = {
perm.name: 0
for perm in self.controller.crafty_perms.list_defined_crafty_permissions()
}
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)
# TODO: do this in the most efficient way
user_id = self.controller.users.add_user(
username,
password,
email,
enabled,
superuser,
)
self.controller.users.update_user(
user_id,
{"roles": roles, "lang": lang, "hints": hints},
{
"permissions_mask": permissions_mask,
"server_quantity": server_quantity,
},
)
self.controller.management.add_to_audit_log(
user["user_id"],
f"added user {username} (UID:{user_id}) with roles {roles}",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.finish_json(
201,
{"status": "ok", "data": {"user_id": str(user_id)}},
)

View File

@ -0,0 +1,241 @@
import json
import logging
from jsonschema import ValidationError, validate
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
logger = logging.getLogger(__name__)
class ApiUsersUserIndexHandler(BaseApiHandler):
def get(self, user_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
exec_user_crafty_permissions,
_,
_,
user,
) = auth_data
if user_id in ["@me", user["user_id"]]:
user_id = user["user_id"]
res_user = user
elif EnumPermissionsCrafty.USER_CONFIG not in exec_user_crafty_permissions:
return self.finish_json(
400,
{
"status": "error",
"error": "NOT_AUTHORIZED",
},
)
else:
# has User_Config permission and isn't viewing self
res_user = self.controller.users.get_user_by_id(user_id)
if not res_user:
return self.finish_json(
404,
{
"status": "error",
"error": "USER_NOT_FOUND",
},
)
# Remove password and valid_tokens_from from the response
# as those should never be sent out to the client.
res_user.pop("password", None)
res_user.pop("valid_tokens_from", None)
res_user["roles"] = list(
map(HelperRoles.get_role, res_user.get("roles", set()))
)
self.finish_json(
200,
{"status": "ok", "data": res_user},
)
def delete(self, user_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
exec_user_crafty_permissions,
_,
_,
user,
) = auth_data
if (user_id in ["@me", user["user_id"]]) and self.helper.get_setting(
"allow_self_delete", False
):
user_id = user["user_id"]
self.controller.users.remove_user(user_id)
elif EnumPermissionsCrafty.USER_CONFIG not in exec_user_crafty_permissions:
return self.finish_json(
400,
{
"status": "error",
"error": "NOT_AUTHORIZED",
},
)
else:
# has User_Config permission
self.controller.users.remove_user(user_id)
self.controller.management.add_to_audit_log(
user["user_id"],
f"deleted the user {user_id}",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.finish_json(
200,
{"status": "ok"},
)
def patch(self, user_id: str):
user_patch_schema = {
"type": "object",
"properties": {
**self.controller.users.user_jsonschema_props,
},
"anyOf": [
# Require at least one property
{"required": [name]}
for name in [
"username",
"password",
"email",
"enabled",
"lang",
"superuser",
"permissions",
"roles",
"hints",
]
],
"additionalProperties": False,
}
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
exec_user_crafty_permissions,
_,
superuser,
user,
) = auth_data
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, user_patch_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if user_id == "@me":
user_id = user["user_id"]
if (
EnumPermissionsCrafty.USER_CONFIG not in exec_user_crafty_permissions
and str(user["user_id"]) != str(user_id)
):
# If doesn't have perm can't edit other users
return self.finish_json(
400,
{
"status": "error",
"error": "NOT_AUTHORIZED",
},
)
if data.get("username", None) is not None:
if data["username"].lower() in ["system", ""]:
return self.finish_json(
400, {"status": "error", "error": "INVALID_USERNAME"}
)
if self.controller.users.get_id_by_name(data["username"]) is not None:
return self.finish_json(
400, {"status": "error", "error": "USER_EXISTS"}
)
if data.get("superuser", None) is not None:
if str(user["user_id"]) == str(user_id):
# Checks if user is trying to change super user status of self.
# We don't want that.
return self.finish_json(
400, {"status": "error", "error": "INVALID_SUPERUSER_MODIFY"}
)
if not superuser:
# The user is not superuser so they can't change the superuser status
data.pop("superuser")
if data.get("permissions", None) is not None:
if str(user["user_id"]) == str(user_id):
# Checks if user is trying to change permissions of self.
# We don't want that.
return self.finish_json(
400, {"status": "error", "error": "INVALID_PERMISSIONS_MODIFY"}
)
if EnumPermissionsCrafty.USER_CONFIG not in exec_user_crafty_permissions:
# Checks if user is trying to change permissions of someone
# else without User Config permission. We don't want that.
return self.finish_json(
400, {"status": "error", "error": "INVALID_PERMISSIONS_MODIFY"}
)
if data.get("roles", None) is not None:
if str(user["user_id"]) == str(user_id):
# Checks if user is trying to change roles of self.
# We don't want that.
return self.finish_json(
400, {"status": "error", "error": "INVALID_ROLES_MODIFY"}
)
if EnumPermissionsCrafty.USER_CONFIG not in exec_user_crafty_permissions:
# Checks if user is trying to change roles of someone
# else without User Config permission. We don't want that.
return self.finish_json(
400, {"status": "error", "error": "INVALID_ROLES_MODIFY"}
)
# TODO: make this more efficient
# TODO: add permissions and roles because I forgot
user_obj = HelperUsers.get_user_model(user_id)
self.controller.management.add_to_audit_log(
user["user_id"],
(
f"edited user {user_obj.username} (UID: {user_id})"
f"with roles {user_obj.roles}"
),
server_id=0,
source_ip=self.get_remote_ip(),
)
for key in data:
# If we don't validate the input there could be security issues
setattr(user_obj, key, data[key])
user_obj.save()
return self.finish_json(200, {"status": "ok"})

View File

@ -0,0 +1,49 @@
import logging
import libgravatar
import requests
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
class ApiUsersUserPfpHandler(BaseApiHandler):
def get(self, user_id):
auth_data = self.authenticate_user()
if not auth_data:
return
if user_id == "@me":
user = auth_data[4]
else:
user = self.controller.users.get_user_by_id(user_id)
logger.debug(
f'User {auth_data[4]["user_id"]} is fetching the pfp for user {user_id}'
)
# http://en.gravatar.com/site/implement/images/#rating
if self.helper.get_setting("allow_nsfw_profile_pictures"):
rating = "x"
else:
rating = "g"
# Get grvatar hash for profile pictures
if user["email"] != "default@example.com" or "":
gravatar = libgravatar.Gravatar(libgravatar.sanitize_email(user["email"]))
url = gravatar.get_image(
size=80,
default="404",
force_default=False,
rating=rating,
filetype_extension=False,
use_ssl=True,
)
try:
requests.head(url).raise_for_status()
except requests.HTTPError as e:
logger.debug("Gravatar profile picture not found", exc_info=e)
else:
self.finish_json(200, {"status": "ok", "data": url})
return
self.finish_json(200, {"status": "ok", "data": None})

View File

@ -0,0 +1,37 @@
import logging
from app.classes.models.roles import HelperRoles
from app.classes.models.users import PUBLIC_USER_ATTRS
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
class ApiUsersUserPublicHandler(BaseApiHandler):
def get(self, user_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
_,
user,
) = auth_data
if user_id == "@me":
user_id = user["user_id"]
public_user = user
else:
public_user = self.controller.users.get_user_by_id(user_id)
public_user = {key: public_user.get(key) for key in PUBLIC_USER_ATTRS}
public_user["roles"] = list(
map(HelperRoles.get_role, public_user.get("roles", set()))
)
self.finish_json(
200,
{"status": "ok", "data": public_user},
)

View File

@ -13,10 +13,12 @@ import tornado.httpserver
from app.classes.shared.console import Console from app.classes.shared.console import Console
from app.classes.shared.helpers import Helpers from app.classes.shared.helpers import Helpers
from app.classes.shared.main_controller import Controller
from app.classes.web.file_handler import FileHandler from app.classes.web.file_handler import FileHandler
from app.classes.web.public_handler import PublicHandler from app.classes.web.public_handler import PublicHandler
from app.classes.web.panel_handler import PanelHandler from app.classes.web.panel_handler import PanelHandler
from app.classes.web.default_handler import DefaultHandler from app.classes.web.default_handler import DefaultHandler
from app.classes.web.routes.api.api_handlers import api_handlers
from app.classes.web.server_handler import ServerHandler from app.classes.web.server_handler import ServerHandler
from app.classes.web.ajax_handler import AjaxHandler from app.classes.web.ajax_handler import AjaxHandler
from app.classes.web.api_handler import ( from app.classes.web.api_handler import (
@ -42,6 +44,9 @@ logger = logging.getLogger(__name__)
class Webserver: class Webserver:
controller: Controller
helper: Helpers
def __init__(self, helper, controller, tasks_manager): def __init__(self, helper, controller, tasks_manager):
self.ioloop = None self.ioloop = None
self.http_server = None self.http_server = None
@ -150,7 +155,7 @@ class Webserver:
(r"/ws", SocketHandler, handler_args), (r"/ws", SocketHandler, handler_args),
(r"/upload", UploadHandler, handler_args), (r"/upload", UploadHandler, handler_args),
(r"/status", StatusHandler, handler_args), (r"/status", StatusHandler, handler_args),
# API Routes # API Routes V1
(r"/api/v1/stats/servers", ServersStats, handler_args), (r"/api/v1/stats/servers", ServersStats, handler_args),
(r"/api/v1/stats/node", NodeStats, handler_args), (r"/api/v1/stats/node", NodeStats, handler_args),
(r"/api/v1/server/send_command", SendCommand, handler_args), (r"/api/v1/server/send_command", SendCommand, handler_args),
@ -161,6 +166,8 @@ class Webserver:
(r"/api/v1/list_servers", ListServers, handler_args), (r"/api/v1/list_servers", ListServers, handler_args),
(r"/api/v1/users/create_user", CreateUser, handler_args), (r"/api/v1/users/create_user", CreateUser, handler_args),
(r"/api/v1/users/delete_user", DeleteUser, handler_args), (r"/api/v1/users/delete_user", DeleteUser, handler_args),
# API Routes V2
*api_handlers(handler_args),
] ]
app = tornado.web.Application( app = tornado.web.Application(

View File

@ -22,5 +22,6 @@
"help", "help",
"chunk" "chunk"
], ],
"allow_nsfw_profile_pictures": false "allow_nsfw_profile_pictures": false,
"enable_user_self_delete": false
} }

View File

@ -115,7 +115,8 @@
{% end %} {% end %}
{% if len(data['servers']) > 0 %} {% if len(data['servers']) > 0 %}
<table id="servers_table" class="table table-hover"> <!-- View for Large screen -->
<table id="servers_table" class="table table-hover d-none d-sm-table">
<thead> <thead>
<tr class="rounded" id="first" draggable="false"> <tr class="rounded" id="first" draggable="false">
<th draggable="false">{{ translate('dashboard', 'server', data['lang']) }}</th> <th draggable="false">{{ translate('dashboard', 'server', data['lang']) }}</th>
@ -140,18 +141,18 @@
<td draggable="false" id="controls{{server['server_data']['server_id']}}" class="actions_serverlist"> <td draggable="false" id="controls{{server['server_data']['server_id']}}" class="actions_serverlist">
{% if server['user_command_permission'] %} {% if server['user_command_permission'] %}
{% if server['stats']['running'] %} {% if server['stats']['running'] %}
<a data-id="{{server['server_data']['server_id']}}" class="stop_button" data-toggle="tooltip" <a data-id="{{server['server_data']['server_id']}}" class="stop_button"
title="{{ translate('dashboard', 'stop' , data['lang']) }}"> data-toggle="tooltip" title="{{ translate('dashboard', 'stop' , data['lang']) }}">
<i class="fas fa-stop"></i> <i class="fas fa-stop"></i>
</a> &nbsp; </a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="restart_button" data-toggle="tooltip" <a data-id="{{server['server_data']['server_id']}}" class="restart_button"
title="{{ translate('dashboard', 'restart' , data['lang']) }}"> data-toggle="tooltip" title="{{ translate('dashboard', 'restart' , data['lang']) }}">
<i class="fas fa-sync"></i> <i class="fas fa-sync"></i>
</a> &nbsp; </a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="kill_button" data-toggle="tooltip" <a data-id="{{server['server_data']['server_id']}}" class="kill_button"
title="{{ translate('dashboard', 'kill' , data['lang']) }}"> data-toggle="tooltip" title="{{ translate('dashboard', 'kill' , data['lang']) }}">
<i class="fas fa-skull"></i> <i class="fas fa-skull"></i>
</a> &nbsp; </a> &nbsp;
@ -165,20 +166,19 @@
translate('dashboard', 'delay-explained' , data['lang'])}}">{{ translate('dashboard', 'starting', translate('dashboard', 'delay-explained' , data['lang'])}}">{{ translate('dashboard', 'starting',
data['lang']) }}</i></a> data['lang']) }}</i></a>
{% elif server['stats']['downloading']%} {% elif server['stats']['downloading']%}
<a data-id="{{server['server_data']['server_id']}}" class=""><i class="fa fa-spinner fa-spin"></i> <a data-id="{{server['server_data']['server_id']}}" class=""><i class="fa fa-spinner fa-spin"></i> {{ translate('serverTerm', 'downloading',
{{ translate('serverTerm', 'downloading',
data['lang']) }}</a> data['lang']) }}</a>
{% else %} {% else %}
<a data-id="{{server['server_data']['server_id']}}" class="play_button" data-toggle="tooltip" <a data-id="{{server['server_data']['server_id']}}" class="play_button"
title="{{ translate('dashboard', 'start' , data['lang']) }}"> data-toggle="tooltip" title="{{ translate('dashboard', 'start' , data['lang']) }}">
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
</a> &nbsp; </a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="clone_button" data-toggle="tooltip" <a data-id="{{server['server_data']['server_id']}}" class="clone_button"
title="{{ translate('dashboard', 'clone' , data['lang']) }}"> data-toggle="tooltip" title="{{ translate('dashboard', 'clone' , data['lang']) }}">
<i class="fas fa-clone"></i> <i class="fas fa-clone"></i>
</a> &nbsp; </a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="kill_button" data-toggle="tooltip" <a data-id="{{server['server_data']['server_id']}}" class="kill_button"
title="{{ translate('dashboard', 'kill' , data['lang']) }}"> data-toggle="tooltip" title="{{ translate('dashboard', 'kill' , data['lang']) }}">
<i class="fas fa-skull"></i> <i class="fas fa-skull"></i>
</a> &nbsp; </a> &nbsp;
{% end %} {% end %}
@ -249,8 +249,7 @@
<span class="text-success"><i class="fas fa-signal"></i> {{ translate('dashboard', 'online', <span class="text-success"><i class="fas fa-signal"></i> {{ translate('dashboard', 'online',
data['lang']) }}</span> data['lang']) }}</span>
{% elif server['stats']['crashed'] %} {% elif server['stats']['crashed'] %}
<span class="text-danger"><i class="fas fa-exclamation-triangle"></i> {{ translate('dashboard', <span class="text-danger"><i class="fas fa-exclamation-triangle"></i> {{ translate('dashboard', 'crashed',
'crashed',
data['lang']) }}</span> data['lang']) }}</span>
{% else %} {% else %}
<span class="text-warning"><i class="fas fa-ban"></i> {{ translate('dashboard', 'offline', <span class="text-warning"><i class="fas fa-ban"></i> {{ translate('dashboard', 'offline',
@ -261,7 +260,164 @@
data-players="{{ server['stats']['online']}}" data-max="{{ server['stats']['max'] }}"></span> data-players="{{ server['stats']['online']}}" data-max="{{ server['stats']['max'] }}"></span>
</tr> </tr>
{% end %} {% end %}
</tbody>
</table>
<!-- View for Small screen -->
<table id="servers_table" class="table table-hover d-table d-sm-none">
<thead>
<tr class="rounded" id="first" draggable="false">
<th scope="col" draggable="false">{{ translate('dashboard', 'server', data['lang']) }}</th>
<th scope="col" draggable="false">{{ translate('dashboard', 'actions', data['lang']) }}</th>
<th scope="col" draggable="false">{{ translate('dashboard', 'status', data['lang']) }}</th>
<th scope="col" draggable="false"></th>
</tr>
</thead>
<tbody>
{% for server in data['servers'] %}
<tr id="{{server['server_data']['server_id']}}" draggable="false">
<td scope="row"><i class="fas fa-server"></i>
<a draggable="false" href="/panel/server_detail?id={{server['server_data']['server_id']}}">
{{ server['server_data']['server_name'] }}
</a>
</td>
<td draggable="false" id="controls{{server['server_data']['server_id']}}" class="actions_serverlist">
{% if server['user_command_permission'] %}
{% if server['stats']['running'] %}
<a data-id="{{server['server_data']['server_id']}}" class="stop_button"
data-toggle="tooltip" title="{{ translate('dashboard', 'stop' , data['lang']) }}">
<i class="fas fa-stop"></i>
</a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="restart_button"
data-toggle="tooltip" title="{{ translate('dashboard', 'restart' , data['lang']) }}">
<i class="fas fa-sync"></i>
</a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="kill_button"
data-toggle="tooltip" title="{{ translate('dashboard', 'kill' , data['lang']) }}">
<i class="fas fa-skull"></i>
</a> &nbsp;
{% elif server['stats']['updating']%}
<!-- WHAT HAPPENED HERE -->
<a data-id="{{server['server_data']['server_id']}}" class="">{{ translate('serverTerm', 'updating', data['lang']) }}</i></a>
{% elif server['stats']['waiting_start']%}
<!-- WHAT HAPPENED HERE -->
<a data-id="{{server['server_data']['server_id']}}" class="" title="{{
translate('dashboard', 'delay-explained' , data['lang'])}}">{{ translate('dashboard', 'starting', data['lang']) }}</i></a>
{% elif server['stats']['downloading']%}
<a data-id="{{server['server_data']['server_id']}}" class=""><i class="fa fa-spinner fa-spin"></i> {{ translate('serverTerm', 'downloading', data['lang']) }}</a>
{% else %}
<a data-id="{{server['server_data']['server_id']}}" class="play_button"
data-toggle="tooltip" title="{{ translate('dashboard', 'start' , data['lang']) }}">
<i class="fas fa-play"></i>
</a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="clone_button"
data-toggle="tooltip" title="{{ translate('dashboard', 'clone' , data['lang']) }}">
<i class="fas fa-clone"></i>
</a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="kill_button"
data-toggle="tooltip" title="{{ translate('dashboard', 'kill' , data['lang']) }}">
<i class="fas fa-skull"></i>
</a> &nbsp;
{% end %}
{% end %}
</td>
<td draggable="false" id="m_server_running_status_{{server['server_data']['server_id']}}">
{% if server['stats']['running'] %}
<span class="text-success"><i class="fas fa-signal"></i> {{ translate('dashboard', 'online',
data['lang']) }}</span>
{% elif server['stats']['crashed'] %}
<span class="text-danger"><i class="fas fa-exclamation-triangle"></i> {{ translate('dashboard', 'crashed',
data['lang']) }}</span>
{% else %}
<span class="text-warning"><i class="fas fa-ban"></i> {{ translate('dashboard', 'offline',
data['lang']) }}</span>
{% end %}
</td>
<td>
<span data-toggle="collapse" data-target="#details_{{server['server_data']['server_id']}}" aria-expanded="false" aria-controls="details_{{server['server_data']['server_id']}}"><i class="fas fa-chevron-down"></i></span>
</td>
</tr>
<tr id="details_{{server['server_data']['server_id']}}" class="collapse" draggable="false">
<td colspan="4">
<div class="collapse" id="details_{{server['server_data']['server_id']}}">
<div class="row">
<div class="col-6">
<h6>{{ translate('dashboard', 'cpuUsage', data['lang']) }}</h6>
<div id="m_server_cpu_{{server['server_data']['server_id']}}">
<div class="progress mb-1" data-toggle="tooltip" data-placement="top"
title="{{server['stats']['cpu']}}">
<div class="progress-bar
{% if server['stats']['cpu'] <= 33 %}
bg-success
{% elif 34 <= server['stats']['cpu'] <= 66 %}
bg-warning
{% else %}
bg-danger
{% end %}
" role="progressbar" style="width: {{server['stats']['cpu']}}%" aria-valuenow="0"
aria-valuemin="0" aria-valuemax="100"></div>
</div>
{{server['stats']['cpu']}}%
</div>
</div>
<div class="col-6">
<h6>{{ translate('dashboard', 'memUsage', data['lang']) }}</h6>
<div draggable="false" id="m_server_mem_{{server['server_data']['server_id']}}">
<div class="progress mb-1" data-toggle="tooltip" data-placement="top"
title="{{server['stats']['mem']}}">
<div class="progress-bar
{% if server['stats']['mem_percent'] <= 33 %}
bg-success
{% elif 34 <= server['stats']['mem_percent'] <= 66 %}
bg-warning
{% else %}
bg-danger
{% end %}
" role="progressbar" style="width: {{server['stats']['mem_percent']}}%" aria-valuenow="0"
aria-valuemin="0" aria-valuemax="100"></div>
</div>
{{server['stats']['mem_percent']}}% -
{% if server['stats']['mem'] == 0 %}
0 MB
{% else %}
{{server['stats']['mem']}}
{% end %}
</div>
</div>
</div>
<br />
<div class="row">
<div class="col-6">
<h6>{{ translate('dashboard', 'size', data['lang']) }}</h6>
<div draggable="false" id="m_server_world_{{server['server_data']['server_id']}}">
{{ server['stats']['world_size'] }}
</div>
</div>
<div class="col-6" style="width: auto;">
<h6>{{ translate('dashboard', 'players', data['lang']) }}</h6>
<div draggable="false" id="m_server_desc_{{server['server_data']['server_id']}}">
{% if server['stats']['int_ping_results'] %}
{{ server['stats']['online'] }} / {{ server['stats']['max'] }} {{ translate('dashboard', 'max',
data['lang']) }} <br />
{% if server['stats']['desc'] != 'False' %}
<div id="desc_id" style="overflow-wrap: break-word !important; max-width: 85px !important; overflow: scroll;">{{ server['stats']['desc'] }}</div> <br />
{% end %}
{% if server['stats']['version'] != 'False' %}
{{ server['stats']['version'] }}
{% end %}
{% end %}
</div>
</div>
</div>
</div>
</td>
</tr>
{% end %}
</tbody> </tbody>
</table> </table>
{% end %} {% end %}
@ -373,6 +529,7 @@
} }
function update_one_server_status(server) { function update_one_server_status(server) {
/* Mobile view update */
server_cpu = document.getElementById('server_cpu_' + server.id); server_cpu = document.getElementById('server_cpu_' + server.id);
server_mem = document.getElementById('server_mem_' + server.id); server_mem = document.getElementById('server_mem_' + server.id);
server_world = document.getElementById('server_world_' + server.id); server_world = document.getElementById('server_world_' + server.id);
@ -381,6 +538,13 @@
server_players = document.getElementById('server_players_' + server.id); server_players = document.getElementById('server_players_' + server.id);
total_players = document.getElementById('total_players'); total_players = document.getElementById('total_players');
/* Mobile view update */
m_server_cpu = document.getElementById('m_server_cpu_' + server.id);
m_server_mem = document.getElementById('m_server_mem_' + server.id);
m_server_world = document.getElementById('m_server_world_' + server.id);
m_server_desc = document.getElementById('m_server_desc_' + server.id);
m_server_online_status = document.getElementById('m_server_running_status_' + server.id);
console.log("Received Data : " + server.id + ": " + server); console.log("Received Data : " + server.id + ": " + server);
/* TODO Update each element */ /* TODO Update each element */
@ -393,6 +557,7 @@
} }
server_cpu.innerHTML = `<div class="progress mb-1" data-toggle="tooltip" data-placement="top" title="` + server.cpu + `"><div class="progress-bar ` + cpu_status + `" role="progressbar" style="width: ` + server.cpu + `%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div></div>` + server.cpu + `%`; server_cpu.innerHTML = `<div class="progress mb-1" data-toggle="tooltip" data-placement="top" title="` + server.cpu + `"><div class="progress-bar ` + cpu_status + `" role="progressbar" style="width: ` + server.cpu + `%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div></div>` + server.cpu + `%`;
m_server_cpu.innerHTML = `<div class="progress mb-1" data-toggle="tooltip" data-placement="top" title="` + server.cpu + `"><div class="progress-bar ` + cpu_status + `" role="progressbar" style="width: ` + server.cpu + `%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div></div>` + server.cpu + `%`;
/* Update Memory */ /* Update Memory */
@ -410,15 +575,18 @@
} }
server_mem.innerHTML = `<div class="progress mb-1" data-toggle="tooltip" data-placement="top" title="` + server_mem + `"><div class="progress-bar ` + mem_status + `" role="progressbar" style="width: ` + server.mem_percent + `%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div></div>` + server.mem_percent + `% - ` + total_mem; server_mem.innerHTML = `<div class="progress mb-1" data-toggle="tooltip" data-placement="top" title="` + server_mem + `"><div class="progress-bar ` + mem_status + `" role="progressbar" style="width: ` + server.mem_percent + `%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div></div>` + server.mem_percent + `% - ` + total_mem;
m_server_mem.innerHTML = `<div class="progress mb-1" data-toggle="tooltip" data-placement="top" title="` + server_mem + `"><div class="progress-bar ` + mem_status + `" role="progressbar" style="width: ` + server.mem_percent + `%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div></div>` + server.mem_percent + `% - ` + total_mem;
/* Update World Infos */ /* Update World Infos */
server_world.innerHTML = server.world_size server_world.innerHTML = server.world_size
m_server_world.innerHTML = server.world_size
/* Update Server Infos */ /* Update Server Infos */
if (server.int_ping_results) { if (server.int_ping_results) {
/* Update Players */ /* Update Players */
if (server.players) { if (server.players) {
server_desc.innerHTML = server.online + ` / ` + server.max + ` {{ translate('dashboard', 'max', data['lang']) }}<br />` server_desc.innerHTML = server.online + ` / ` + server.max + ` {{ translate('dashboard', 'max', data['lang']) }}<br />`
m_server_desc.innerHTML = server.online + ` / ` + server.max + ` {{ translate('dashboard', 'max', data['lang']) }}<br />`
server_players.setAttribute('data-players', server.online); server_players.setAttribute('data-players', server.online);
server_players.setAttribute('data-max', server.max); server_players.setAttribute('data-max', server.max);
@ -439,6 +607,7 @@
server_infos = ""; server_infos = "";
m_server_infos = "";
server_infos = server.online + " / " + server.max + " {{ translate('dashboard', 'max', data['lang']) }}<br />" server_infos = server.online + " / " + server.max + " {{ translate('dashboard', 'max', data['lang']) }}<br />"
} }
@ -446,14 +615,17 @@
let motd = ""; let motd = "";
if (server.desc) { if (server.desc) {
motd = `<span id="input_motd_` + server.id + `" class="input_motd">` + server.desc + `</span>`; motd = `<span id="input_motd_` + server.id + `" class="input_motd">` + server.desc + `</span>`;
m_server_infos = server_infos + '<div id="desc_id" style="word-wrap: break-word; overflow: auto;">' + motd + '</div>' + "<br />";
server_infos = server_infos + '<div id="desc_id" style="word-wrap: break-word; max-width: 85px !important; overflow: auto;">' + motd + '</div>' + "<br />"; server_infos = server_infos + '<div id="desc_id" style="word-wrap: break-word; max-width: 85px !important; overflow: auto;">' + motd + '</div>' + "<br />";
} }
/* Version */ /* Version */
if (server.version) { if (server.version) {
server_infos = server_infos + server.version server_infos = server_infos + server.version
m_server_infos = m_server_infos + server.version
} }
server_desc.innerHTML = server_infos; server_desc.innerHTML = server_infos;
m_server_desc.innerHTML = m_server_infos;
} }
/* Update Online Status */ /* Update Online Status */

View File

@ -0,0 +1,31 @@
<div class="col-sm-12 mt-4 mb-4">
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-expanded="false">
Server Controls
</button>
<div class="dropdown-menu col-md-12" aria-labelledby="dropdownMenuButton">
{% if data['permissions']['Terminal'] in data['user_permissions'] %}
<a class="dropdown-item {% if data['active_link'] == 'term' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=term" role="tab" aria-selected="false"><i class="fas fa-file-signature"></i> {{ translate('serverDetails', 'terminal', data['lang']) }}</a>
{% end %}
<!--Bedrock servers don't have logs so we'll only show it if we know it's not a bedrock server.-->
{% if data['permissions']['Logs'] in data['user_permissions'] and data['server_data']['type'] != 'minecraft-bedrock'%}
<a class="dropdown-item {% if data['active_link'] == 'logs' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=logs" role="tab" aria-selected="false"><i class="fas fa-file-signature"></i> {{ translate('serverDetails', 'logs', data['lang']) }}</a>
{% end %}
{% if data['permissions']['Schedule'] in data['user_permissions'] %}
<a class="dropdown-item {% if data['active_link'] == 'schedules' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=schedules" role="tab" aria-selected="false"><i class="fas fa-clock"></i> {{ translate('serverDetails', 'schedule', data['lang']) }}</a>
{% end %}
{% if data['permissions']['Backup'] in data['user_permissions'] %}
<a class="dropdown-item {% if data['active_link'] == 'backup' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=backup" role="tab" aria-selected="false"><i class="fas fa-save"></i> {{ translate('serverDetails', 'backup', data['lang']) }}</a>
{% end %}
{% if data['permissions']['Files'] in data['user_permissions'] %}
<a class="dropdown-item {% if data['active_link'] == 'files' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=files" role="tab" aria-selected="false"><i class="fas fa-folder-tree"></i> {{ translate('serverDetails', 'files', data['lang']) }}</a>
{% end %}
{% if data['permissions']['Config'] in data['user_permissions'] %}
<a class="dropdown-item {% if data['active_link'] == 'config' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=config" role="tab" aria-selected="true"><i class="fas fa-cogs"></i> {{ translate('serverDetails', 'config', data['lang']) }}</a>
{% end %}
{% if data['permissions']['Players'] in data['user_permissions'] and data['server_data']['type'] != 'minecraft-bedrock' %}
<a class="dropdown-item {% if data['active_link'] == 'admin_controls' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=admin_controls" role="tab" aria-selected="true"><i class="fas fa-users"></i> {{ translate('serverDetails', 'playerControls', data['lang']) }}</a>
{% end %}
</div>
</div>
</div>

View File

@ -14,8 +14,7 @@
<div class="col-12"> <div class="col-12">
<div class="page-header"> <div class="page-header">
<h4 class="page-title"> <h4 class="page-title">
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{ {{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{ data['server_stats']['server_id']['server_name'] }}
data['server_stats']['server_id']['server_name'] }}
<br /> <br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small> <small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
</h4> </h4>
@ -25,14 +24,19 @@
</div> </div>
<!-- Page Title Header Ends--> <!-- Page Title Header Ends-->
{% include "parts/details_stats.html" %} {% include "parts/details_stats.html %}
<div class="row"> <div class="row">
<div class="col-sm-12 grid-margin"> <div class="col-sm-12 grid-margin">
<div class="card"> <div class="card">
<div class="card-body pt-0"> <div class="card-body pt-0">
{% include "parts/server_controls_list.html" %} <span class="d-none d-sm-block">
{% include "parts/server_controls_list.html %}
</span>
<span class="d-block d-sm-none">
{% include "parts/m_server_controls_list.html %}
</span>
<div class="row"> <div class="row">
<div class="col-md-6 col-sm-12"> <div class="col-md-6 col-sm-12">
@ -73,14 +77,10 @@
<li class="playerItem"> <li class="playerItem">
<h3>{{ player }}</h3> <h3>{{ player }}</h3>
<div class="buttons"> <div class="buttons">
<button onclick="send_command_to_server('ban {{ player }}')" type="button" <button onclick="send_command_to_server('ban {{ player }}')" type="button" class="btn btn-danger">Ban</button>
class="btn btn-danger">Ban</button> <button onclick="send_command_to_server('kick {{ player }}')" type="button" class="btn btn-outline-danger">Kick</button>
<button onclick="send_command_to_server('kick {{ player }}')" type="button" <button onclick="send_command_to_server('op {{ player }}')" type="button" class="btn btn-warning">OP</button>
class="btn btn-outline-danger">Kick</button> <button onclick="send_command_to_server('deop {{ player }}')" type="button" class="btn btn-outline-warning">De-OP</button>
<button onclick="send_command_to_server('op {{ player }}')" type="button"
class="btn btn-warning">OP</button>
<button onclick="send_command_to_server('deop {{ player }}')" type="button"
class="btn btn-outline-warning">De-OP</button>
</div> </div>
</li> </li>
{% end %} {% end %}

View File

@ -14,8 +14,7 @@
<div class="col-12"> <div class="col-12">
<div class="page-header"> <div class="page-header">
<h4 class="page-title"> <h4 class="page-title">
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{ {{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{ data['server_stats']['server_id']['server_name'] }}
data['server_stats']['server_id']['server_name'] }}
<br /> <br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small> <small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
</h4> </h4>
@ -32,7 +31,13 @@
<div class="col-sm-12 grid-margin"> <div class="col-sm-12 grid-margin">
<div class="card"> <div class="card">
<div class="card-body pt-0"> <div class="card-body pt-0">
<span class="d-none d-sm-block">
{% include "parts/server_controls_list.html %} {% include "parts/server_controls_list.html %}
</span>
<span class="d-block d-sm-none">
{% include "parts/m_server_controls_list.html %}
</span>
<div class="row"> <div class="row">
<div class="col-md-6 col-sm-12"> <div class="col-md-6 col-sm-12">
@ -46,10 +51,7 @@
{% if data['backing_up'] %} {% if data['backing_up'] %}
<div class="progress" style="height: 15px;"> <div class="progress" style="height: 15px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar" <div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar" role="progressbar" style="width:{{data['backup_stats']['percent']}}%;" aria-valuenow="{{data['backup_stats']['percent']}}" aria-valuemin="0" aria-valuemax="100">{{ data['backup_stats']['percent'] }}%</div>
role="progressbar" style="width:{{data['backup_stats']['percent']}}%;"
aria-valuenow="{{data['backup_stats']['percent']}}" aria-valuemin="0" aria-valuemax="100">{{
data['backup_stats']['percent'] }}%</div>
</div> </div>
<p>Backing up <span id="total_files">{{data['backup_stats']['total_files']}}</span> Files</p> <p>Backing up <span id="total_files">{{data['backup_stats']['total_files']}}</span> Files</p>
{% end %} {% end %}
@ -57,63 +59,47 @@
<br> <br>
{% if not data['backing_up'] %} {% if not data['backing_up'] %}
<div id="backup_button" class="form-group"> <div id="backup_button" class="form-group">
<button class="btn btn-primary" id="backup_now_button">{{ translate('serverBackups', 'backupNow', <button class="btn btn-primary" id="backup_now_button">{{ translate('serverBackups', 'backupNow', data['lang']) }}</button>
data['lang']) }}</button>
</div> </div>
{% end %} {% end %}
<div class="form-group"> <div class="form-group">
{% if data['super_user'] %} {% if data['super_user'] %}
<label for="server_name">{{ translate('serverBackups', 'storageLocation', data['lang']) }} <small <label for="server_name">{{ translate('serverBackups', 'storageLocation', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverBackups', 'storageLocationDesc', data['lang']) }}</small> </label>
class="text-muted ml-1"> - {{ translate('serverBackups', 'storageLocationDesc', data['lang']) <input type="text" class="form-control" name="backup_path" id="backup_path" value="{{ data['server_stats']['server_id']['backup_path'] }}" placeholder="{{ translate('serverBackups', 'storageLocation', data['lang']) }}">
}}</small> </label>
<input type="text" class="form-control" name="backup_path" id="backup_path"
value="{{ data['server_stats']['server_id']['backup_path'] }}"
placeholder="{{ translate('serverBackups', 'storageLocation', data['lang']) }}">
{% end %} {% end %}
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="server_path">{{ translate('serverBackups', 'maxBackups', data['lang']) }} <small <label for="server_path">{{ translate('serverBackups', 'maxBackups', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverBackups', 'maxBackupsDesc', data['lang']) }}</small> </label>
class="text-muted ml-1"> - {{ translate('serverBackups', 'maxBackupsDesc', data['lang']) <input type="text" class="form-control" name="max_backups" id="max_backups" value="{{ data['backup_config']['max_backups'] }}" placeholder="{{ translate('serverBackups', 'maxBackups', data['lang']) }}">
}}</small> </label>
<input type="text" class="form-control" name="max_backups" id="max_backups"
value="{{ data['backup_config']['max_backups'] }}"
placeholder="{{ translate('serverBackups', 'maxBackups', data['lang']) }}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="compress" class="form-check-label ml-4 mb-4"></label> <label for="compress" class="form-check-label ml-4 mb-4"></label>
{% if data['backup_config']['compress'] %} {% if data['backup_config']['compress'] %}
<input type="checkbox" class="form-check-input" id="compress" name="compress" checked="" <input type="checkbox" class="form-check-input" id="compress" name="compress"
value="True">{{ translate('serverBackups', 'compress', data['lang']) }} checked="" value="True">{{ translate('serverBackups', 'compress', data['lang']) }}
{% else %} {% else %}
<input type="checkbox" class="form-check-input" id="compress" name="compress" value="True">{{ <input type="checkbox" class="form-check-input" id="compress" name="compress"
translate('serverBackups', 'compress', data['lang']) }} value="True">{{ translate('serverBackups', 'compress', data['lang']) }}
{% end %} {% end %}
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="server">{{ translate('serverBackups', 'exclusionsTitle', data['lang']) }} <small> - {{ <label for="server">{{ translate('serverBackups', 'exclusionsTitle', data['lang']) }} <small> - {{ translate('serverBackups', 'excludedChoose', data['lang']) }}</small></label>
translate('serverBackups', 'excludedChoose', data['lang']) }}</small></label>
<br> <br>
<button class="btn btn-primary mr-2" id="root_files_button" <button class="btn btn-primary mr-2" id="root_files_button" data-server_path="{{ data['server_stats']['server_id']['path']}}" type="button">{{ translate('serverBackups', 'clickExclude', data['lang']) }}</button>
data-server_path="{{ data['server_stats']['server_id']['path']}}" type="button">{{
translate('serverBackups', 'clickExclude', data['lang']) }}</button>
</div> </div>
<input type="number" class="form-control" name="changed" id="changed" value="0" <input type="number" class="form-control" name="changed" id="changed" value="0" style="visibility: hidden;"></input>
style="visibility: hidden;"></input> <div class="modal fade" id="dir_select" tabindex="-1" role="dialog" aria-labelledby="dir_select" aria-hidden="true">
<div class="modal fade" id="dir_select" tabindex="-1" role="dialog" aria-labelledby="dir_select"
aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">{{ translate('serverBackups', <h5 class="modal-title" id="exampleModalLongTitle">{{ translate('serverBackups', 'excludedChoose', data['lang']) }}</h5>
'excludedChoose', data['lang']) }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="tree-ctx-item" id="main-tree-div" data-path="" <div class="tree-ctx-item" id="main-tree-div" data-path="" style="overflow: scroll; max-height:75%;">
style="overflow: scroll; max-height:75%;">
<input type="checkbox" id="main-tree-input" name="root_path" value="" disabled> <input type="checkbox" id="main-tree-input" name="root_path" value="" disabled>
<span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path=""> <span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path="">
<i class="far fa-folder"></i> <i class="far fa-folder"></i>
@ -124,19 +110,15 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" id="modal-cancel" class="btn btn-secondary" data-dismiss="modal">{{ <button type="button" id="modal-cancel" class="btn btn-secondary" data-dismiss="modal">{{ translate('serverBackups', 'cancel', data['lang']) }}</button>
translate('serverBackups', 'cancel', data['lang']) }}</button> <button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary">{{ translate('serverWizard', 'save', data['lang']) }}</button>
<button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary">{{
translate('serverWizard', 'save', data['lang']) }}</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<button type="submit" class="btn btn-success mr-2">{{ translate('serverBackups', 'save', data['lang']) <button type="submit" class="btn btn-success mr-2">{{ translate('serverBackups', 'save', data['lang']) }}</button>
}}</button> <button type="reset" class="btn btn-light">{{ translate('serverBackups', 'cancel', data['lang']) }}</button>
<button type="reset" class="btn btn-light">{{ translate('serverBackups', 'cancel', data['lang'])
}}</button>
</form> </form>
</div> </div>
@ -156,15 +138,13 @@
{% for backup in data['backup_list'] %} {% for backup in data['backup_list'] %}
<tr> <tr>
<td> <td>
<a href="/panel/download_backup?file={{ backup['path'] }}&id={{ data['server_stats']['server_id']['server_id'] }}" <a href="/panel/download_backup?file={{ backup['path'] }}&id={{ data['server_stats']['server_id']['server_id'] }}" class="btn btn-primary">
class="btn btn-primary">
<i class="fas fa-download" aria-hidden="true"></i> <i class="fas fa-download" aria-hidden="true"></i>
{{ translate('serverBackups', 'download', data['lang']) }} {{ translate('serverBackups', 'download', data['lang']) }}
</a> </a>
<br> <br>
<br> <br>
<button data-file="{{ backup['path'] }}" data-backup_path="{{ data['backup_path'] }}" <button data-file="{{ backup['path'] }}" data-backup_path="{{ data['backup_path'] }}" class="btn btn-danger del_button">
class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i> <i class="fas fa-trash" aria-hidden="true"></i>
{{ translate('serverBackups', 'delete', data['lang']) }} {{ translate('serverBackups', 'delete', data['lang']) }}
</button> </button>
@ -188,8 +168,7 @@
<br> <br>
<br> <br>
<div class="card-header header-sm d-flex justify-content-between align-items-center"> <div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fas fa-server"></i> {{ translate('serverBackups', 'excludedBackups', <h4 class="card-title"><i class="fas fa-server"></i> {{ translate('serverBackups', 'excludedBackups', data['lang']) }} <small class="text-muted ml-1"></small> </h4>
data['lang']) }} <small class="text-muted ml-1"></small> </h4>
</div> </div>
<br> <br>
<ul> <ul>

View File

@ -14,8 +14,7 @@
<div class="col-12"> <div class="col-12">
<div class="page-header"> <div class="page-header">
<h4 class="page-title"> <h4 class="page-title">
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{ {{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{ data['server_stats']['server_id']['server_name'] }}
data['server_stats']['server_id']['server_name'] }}
<br /> <br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small> <small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
</h4> </h4>
@ -25,14 +24,20 @@
</div> </div>
<!-- Page Title Header Ends--> <!-- Page Title Header Ends-->
{% include "parts/details_stats.html" %} {% include "parts/details_stats.html %}
<div class="row"> <div class="row">
<div class="col-sm-12 grid-margin"> <div class="col-sm-12 grid-margin">
<div class="card"> <div class="card">
<div class="card-body pt-0"> <div class="card-body pt-0">
{% include "parts/server_controls_list.html" %}
<span class="d-none d-sm-block">
{% include "parts/server_controls_list.html %}
</span>
<span class="d-block d-sm-none">
{% include "parts/m_server_controls_list.html %}
</span>
<div class="row"> <div class="row">
<div class="col-md-12 col-sm-12" style="overflow-x:auto;"> <div class="col-md-12 col-sm-12" style="overflow-x:auto;">
@ -100,9 +105,7 @@
{% end %} {% end %}
</td> </td>
<td id="{{schedule.action}}" class="action"> <td id="{{schedule.action}}" class="action">
<button <button onclick="window.location.href='/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{schedule.schedule_id}}'" class="btn btn-info">
onclick="window.location.href='/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{schedule.schedule_id}}'"
class="btn btn-info">
<i class="fas fa-pencil-alt"></i> <i class="fas fa-pencil-alt"></i>
</button> </button>
<br> <br>
@ -116,8 +119,7 @@
</tbody> </tbody>
</table> </table>
<hr /> <hr />
<table class="table table-hover d-block d-lg-none" id="mini_schedule_table" width="100%" <table class="table table-hover d-block d-lg-none" id="mini_schedule_table" width="100%" style="table-layout:fixed;">
style="table-layout:fixed;">
<thead> <thead>
<tr class="rounded"> <tr class="rounded">
<th style="width: 25%; min-width: 50px;">Action</th> <th style="width: 25%; min-width: 50px;">Action</th>
@ -147,8 +149,7 @@
</td> </td>
</tr> </tr>
<!-- Modal --> <!-- Modal -->
<div class="modal fade" id="task_details_{{schedule.schedule_id}}" tabindex="-1" role="dialog" <div class="modal fade" id="task_details_{{schedule.schedule_id}}" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@ -187,8 +188,7 @@
<h4>Start Time</h4> <h4>Start Time</h4>
<p>{{schedule.start_time}}</p> <p>{{schedule.start_time}}</p>
</li> </li>
<li id="{{schedule.enabled}}" class="action" <li id="{{schedule.enabled}}" class="action" style="border-top: .1em solid gray; border-bottom: .1em solid gray">
style="border-top: .1em solid gray; border-bottom: .1em solid gray">
{% if schedule.enabled %} {% if schedule.enabled %}
<h4>Enabled</h4> <span class="text-success"> <h4>Enabled</h4> <span class="text-success">
<i class="fas fa-check-square"></i> Yes <i class="fas fa-check-square"></i> Yes
@ -202,9 +202,7 @@
</ul> </ul>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button <button onclick="window.location.href='/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{schedule.schedule_id}}'" class="btn btn-info">
onclick="window.location.href='/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{schedule.schedule_id}}'"
class="btn btn-info">
<i class="fas fa-pencil-alt"></i> Edit <i class="fas fa-pencil-alt"></i> Edit
</button> </button>
<button data-sch={{ schedule.schedule_id }} class="btn btn-danger del_button"> <button data-sch={{ schedule.schedule_id }} class="btn btn-danger del_button">

View File

@ -31,7 +31,13 @@
<div class="col-sm-12 grid-margin"> <div class="col-sm-12 grid-margin">
<div class="card"> <div class="card">
<div class="card-body pt-0"> <div class="card-body pt-0">
<span class="d-none d-sm-block">
{% include "parts/server_controls_list.html %} {% include "parts/server_controls_list.html %}
</span>
<span class="d-block d-sm-none">
{% include "parts/m_server_controls_list.html %}
</span>
<div class="col-md-12"> <div class="col-md-12">
<div class="input-group"> <div class="input-group">

View File

@ -195,7 +195,7 @@ if __name__ == "__main__":
if not controller.check_system_user(): if not controller.check_system_user():
controller.add_system_user() controller.add_system_user()
Crafty = MainPrompt(helper, tasks_manager, migration_manager) Crafty = MainPrompt(helper, tasks_manager, migration_manager, controller)
project_root = os.path.dirname(__file__) project_root = os.path.dirname(__file__)
controller.set_project_root(project_root) controller.set_project_root(project_root)

View File

@ -4,7 +4,7 @@ argon2-cffi==20.1
bleach==4.1 bleach==4.1
cached_property==1.5.2 cached_property==1.5.2
colorama==0.4 colorama==0.4
crontier==1.3.5 croniter==1.3.5
cryptography==3.4.8 cryptography==3.4.8
libgravatar==1.0.0 libgravatar==1.0.0
peewee==3.13 peewee==3.13
@ -17,3 +17,5 @@ requests==2.26
termcolor==1.1 termcolor==1.1
tornado==6.0 tornado==6.0
tzlocal==4.0 tzlocal==4.0
jsonschema==4.4.0
orjson==3.6.7