mirror of
https://gitlab.com/crafty-controller/crafty-4.git
synced 2024-08-30 18:23:09 +00:00
Merge branch 'merge/api-v2' into 'dev'
New API v2 PR See merge request crafty-controller/crafty-4!254
This commit is contained in:
commit
1e96e7a6c9
6
.gitignore
vendored
6
.gitignore
vendored
@ -18,8 +18,10 @@ env.bak/
|
||||
venv.bak/
|
||||
|
||||
.idea/
|
||||
servers/
|
||||
backups/
|
||||
/servers/
|
||||
/backups/
|
||||
/docker/servers/
|
||||
/docker/backups/
|
||||
session.lock
|
||||
.header
|
||||
default.json
|
||||
|
@ -62,6 +62,14 @@ class CraftyPermsController:
|
||||
|
||||
@staticmethod
|
||||
def add_server_creation(user_id):
|
||||
"""Increase the "Server Creation" counter for this user
|
||||
|
||||
Args:
|
||||
user_id (int): The modifiable user's ID
|
||||
|
||||
Returns:
|
||||
int: The new count of servers created by this user
|
||||
"""
|
||||
return PermissionsCrafty.add_server_creation(user_id)
|
||||
|
||||
@staticmethod
|
||||
|
@ -1,7 +1,8 @@
|
||||
import logging
|
||||
import typing as t
|
||||
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -16,6 +17,10 @@ class RolesController:
|
||||
def get_all_roles():
|
||||
return HelperRoles.get_all_roles()
|
||||
|
||||
@staticmethod
|
||||
def get_all_role_ids():
|
||||
return HelperRoles.get_all_role_ids()
|
||||
|
||||
@staticmethod
|
||||
def get_roleid_by_name(role_name):
|
||||
return HelperRoles.get_roleid_by_name(role_name)
|
||||
@ -36,8 +41,12 @@ class RolesController:
|
||||
if key == "role_id":
|
||||
continue
|
||||
elif key == "servers":
|
||||
added_servers = role_data["servers"].difference(base_data["servers"])
|
||||
removed_servers = base_data["servers"].difference(role_data["servers"])
|
||||
added_servers = set(role_data["servers"]).difference(
|
||||
set(base_data["servers"])
|
||||
)
|
||||
removed_servers = set(base_data["servers"]).difference(
|
||||
set(role_data["servers"])
|
||||
)
|
||||
elif base_data[key] != role_data[key]:
|
||||
up_data[key] = role_data[key]
|
||||
up_data["last_update"] = Helpers.get_time_as_string()
|
||||
@ -58,6 +67,95 @@ class RolesController:
|
||||
def 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):
|
||||
role_data = RolesController.get_role_with_servers(role_id)
|
||||
PermissionsServers.delete_roles_permissions(role_id, role_data["servers"])
|
||||
@ -73,12 +171,8 @@ class RolesController:
|
||||
role = HelperRoles.get_role(role_id)
|
||||
|
||||
if role:
|
||||
servers_query = PermissionsServers.get_servers_from_role(role_id)
|
||||
# TODO: this query needs to be narrower
|
||||
servers = set()
|
||||
for s in servers_query:
|
||||
servers.add(s.server_id.server_id)
|
||||
role["servers"] = servers
|
||||
server_ids = PermissionsServers.get_server_ids_from_role(role_id)
|
||||
role["servers"] = list(server_ids)
|
||||
# logger.debug("role: ({}) {}".format(role_id, role))
|
||||
return role
|
||||
else:
|
||||
|
@ -1,6 +1,7 @@
|
||||
import os
|
||||
import logging
|
||||
import json
|
||||
import typing as t
|
||||
|
||||
from app.classes.controllers.roles_controller import RolesController
|
||||
from app.classes.models.servers import HelperServers
|
||||
@ -34,9 +35,31 @@ class ServersController:
|
||||
server_log_file: str,
|
||||
server_stop: str,
|
||||
server_type: str,
|
||||
server_port=25565,
|
||||
):
|
||||
return self.servers_helper.create_server(
|
||||
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 HelperServers.create_server(
|
||||
name,
|
||||
server_uuid,
|
||||
server_dir,
|
||||
@ -47,6 +70,7 @@ class ServersController:
|
||||
server_stop,
|
||||
server_type,
|
||||
server_port,
|
||||
server_host,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@ -92,7 +116,7 @@ class ServersController:
|
||||
|
||||
@staticmethod
|
||||
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)
|
||||
for user in user_roles:
|
||||
role_servers = PermissionsServers.get_role_servers_from_role_id(
|
||||
@ -103,6 +127,20 @@ class ServersController:
|
||||
|
||||
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
|
||||
def get_all_servers_stats():
|
||||
return HelperServerStats.get_all_servers_stats()
|
||||
@ -111,7 +149,7 @@ class ServersController:
|
||||
def get_authorized_servers_stats_api_key(api_key: ApiKeys):
|
||||
server_data = []
|
||||
authorized_servers = ServersController.get_authorized_servers(
|
||||
api_key.user.user_id
|
||||
api_key.user.user_id # TODO: API key authorized servers?
|
||||
)
|
||||
|
||||
for server in authorized_servers:
|
||||
|
@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
import typing as t
|
||||
|
||||
from app.classes.models.users import HelperUsers
|
||||
from app.classes.models.crafty_permissions import (
|
||||
@ -16,6 +16,74 @@ class UsersController:
|
||||
self.users_helper = users_helper
|
||||
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
|
||||
# **********************************************************************************
|
||||
@ -23,6 +91,10 @@ class UsersController:
|
||||
def get_all_users():
|
||||
return HelperUsers.get_all_users()
|
||||
|
||||
@staticmethod
|
||||
def get_all_user_ids() -> t.List[int]:
|
||||
return HelperUsers.get_all_user_ids()
|
||||
|
||||
@staticmethod
|
||||
def get_id_by_name(username):
|
||||
return HelperUsers.get_user_id_by_name(username)
|
||||
@ -64,8 +136,12 @@ class UsersController:
|
||||
if key == "user_id":
|
||||
continue
|
||||
elif key == "roles":
|
||||
added_roles = user_data["roles"].difference(base_data["roles"])
|
||||
removed_roles = base_data["roles"].difference(user_data["roles"])
|
||||
added_roles = set(user_data["roles"]).difference(
|
||||
set(base_data["roles"])
|
||||
)
|
||||
removed_roles = set(base_data["roles"]).difference(
|
||||
set(user_data["roles"])
|
||||
)
|
||||
elif key == "password":
|
||||
if user_data["password"] is not None and user_data["password"] != "":
|
||||
up_data["password"] = self.helper.encode_pass(user_data["password"])
|
||||
@ -82,16 +158,16 @@ class UsersController:
|
||||
permissions_mask = user_crafty_data.get("permissions_mask", "000")
|
||||
|
||||
if "server_quantity" in user_crafty_data:
|
||||
limit_server_creation = user_crafty_data["server_quantity"][
|
||||
EnumPermissionsCrafty.SERVER_CREATION.name
|
||||
]
|
||||
limit_server_creation = user_crafty_data["server_quantity"].get(
|
||||
EnumPermissionsCrafty.SERVER_CREATION.name, 0
|
||||
)
|
||||
|
||||
limit_user_creation = user_crafty_data["server_quantity"][
|
||||
EnumPermissionsCrafty.USER_CONFIG.name
|
||||
]
|
||||
limit_role_creation = user_crafty_data["server_quantity"][
|
||||
EnumPermissionsCrafty.ROLES_CONFIG.name
|
||||
]
|
||||
limit_user_creation = user_crafty_data["server_quantity"].get(
|
||||
EnumPermissionsCrafty.USER_CONFIG.name, 0
|
||||
)
|
||||
limit_role_creation = user_crafty_data["server_quantity"].get(
|
||||
EnumPermissionsCrafty.ROLES_CONFIG.name, 0
|
||||
)
|
||||
else:
|
||||
limit_server_creation = 0
|
||||
limit_user_creation = 0
|
||||
@ -109,6 +185,15 @@ class UsersController:
|
||||
|
||||
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(
|
||||
self,
|
||||
username,
|
||||
@ -161,7 +246,7 @@ class UsersController:
|
||||
return token_data["user_id"]
|
||||
|
||||
def get_user_by_api_token(self, token: str):
|
||||
_, _, user = self.authentication.check(token)
|
||||
_, _, user = self.authentication.check_err(token)
|
||||
return user
|
||||
|
||||
def get_api_key_by_token(self, token: str):
|
||||
@ -207,8 +292,8 @@ class UsersController:
|
||||
name: str,
|
||||
user_id: str,
|
||||
superuser: bool = False,
|
||||
server_permissions_mask: Optional[str] = None,
|
||||
crafty_permissions_mask: Optional[str] = None,
|
||||
server_permissions_mask: t.Optional[str] = None,
|
||||
crafty_permissions_mask: t.Optional[str] = None,
|
||||
):
|
||||
return self.users_helper.add_user_api_key(
|
||||
name, user_id, superuser, server_permissions_mask, crafty_permissions_mask
|
||||
|
@ -22,17 +22,10 @@ class ServerJars:
|
||||
|
||||
try:
|
||||
response = requests.get(full_url, timeout=2)
|
||||
|
||||
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:
|
||||
response.raise_for_status()
|
||||
api_data = json.loads(response.content)
|
||||
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 {}
|
||||
|
||||
api_result = api_data.get("status")
|
||||
|
@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import typing
|
||||
from enum import Enum
|
||||
from peewee import (
|
||||
ForeignKeyField,
|
||||
@ -45,21 +46,24 @@ class PermissionsCrafty:
|
||||
# **********************************************************************************
|
||||
@staticmethod
|
||||
def get_permissions_list():
|
||||
permissions_list = []
|
||||
permissions_list: typing.List[EnumPermissionsCrafty] = []
|
||||
for member in EnumPermissionsCrafty.__members__.items():
|
||||
permissions_list.append(member[1])
|
||||
return permissions_list
|
||||
|
||||
@staticmethod
|
||||
def get_permissions(permissions_mask):
|
||||
permissions_list = []
|
||||
permissions_list: typing.List[EnumPermissionsCrafty] = []
|
||||
for member in EnumPermissionsCrafty.__members__.items():
|
||||
if PermissionsCrafty.has_permission(permissions_mask, member[1]):
|
||||
permissions_list.append(member[1])
|
||||
return permissions_list
|
||||
|
||||
@staticmethod
|
||||
def has_permission(permission_mask, permission_tested: EnumPermissionsCrafty):
|
||||
def has_permission(
|
||||
permission_mask: typing.Mapping[int, str],
|
||||
permission_tested: EnumPermissionsCrafty,
|
||||
):
|
||||
result = False
|
||||
if permission_mask[permission_tested.value] == "1":
|
||||
result = True
|
||||
@ -188,6 +192,14 @@ class PermissionsCrafty:
|
||||
|
||||
@staticmethod
|
||||
def add_server_creation(user_id):
|
||||
"""Increase the "Server Creation" counter for this user
|
||||
|
||||
Args:
|
||||
user_id (int): The modifiable user's ID
|
||||
|
||||
Returns:
|
||||
int: The new count of servers created by this user
|
||||
"""
|
||||
user_crafty = PermissionsCrafty.get_user_crafty(user_id)
|
||||
user_crafty.created_server += 1
|
||||
UserCrafty.save(user_crafty)
|
||||
|
@ -180,7 +180,12 @@ class HelpersManagement:
|
||||
|
||||
server_users = PermissionsServers.get_server_user_list(server_id)
|
||||
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(
|
||||
{
|
||||
@ -191,7 +196,7 @@ class HelpersManagement:
|
||||
AuditLog.source_ip: source_ip,
|
||||
}
|
||||
).execute()
|
||||
# deletes records when they're more than 100
|
||||
# deletes records when there's more than 300
|
||||
ordered = AuditLog.select().order_by(+AuditLog.created)
|
||||
for item in ordered:
|
||||
if not self.helper.get_setting("max_audit_entries"):
|
||||
@ -213,7 +218,7 @@ class HelpersManagement:
|
||||
AuditLog.source_ip: source_ip,
|
||||
}
|
||||
).execute()
|
||||
# deletes records when they're more than 100
|
||||
# deletes records when there's more than 300
|
||||
ordered = AuditLog.select().order_by(+AuditLog.created)
|
||||
for item in ordered:
|
||||
# configurable through app/config/config.json
|
||||
@ -400,7 +405,7 @@ class HelpersManagement:
|
||||
return dir_list
|
||||
|
||||
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:
|
||||
dir_list.append(dir_to_add)
|
||||
excluded_dirs = ",".join(dir_list)
|
||||
@ -412,7 +417,7 @@ class HelpersManagement:
|
||||
)
|
||||
|
||||
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:
|
||||
dir_list.remove(dir_to_del)
|
||||
excluded_dirs = ",".join(dir_list)
|
||||
|
@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import datetime
|
||||
import typing as t
|
||||
from peewee import (
|
||||
CharField,
|
||||
DoesNotExist,
|
||||
@ -35,8 +36,11 @@ class HelperRoles:
|
||||
|
||||
@staticmethod
|
||||
def get_all_roles():
|
||||
query = Roles.select()
|
||||
return query
|
||||
return Roles.select()
|
||||
|
||||
@staticmethod
|
||||
def get_all_role_ids() -> t.List[int]:
|
||||
return [role.role_id for role in Roles.select(Roles.role_id).execute()]
|
||||
|
||||
@staticmethod
|
||||
def get_roleid_by_name(role_name):
|
||||
@ -49,6 +53,24 @@ class HelperRoles:
|
||||
def get_role(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
|
||||
def add_role(role_name):
|
||||
role_id = Roles.insert(
|
||||
@ -64,12 +86,10 @@ class HelperRoles:
|
||||
return Roles.update(up_data).where(Roles.role_id == role_id).execute()
|
||||
|
||||
def remove_role(self, role_id):
|
||||
with self.database.atomic():
|
||||
role = Roles.get(Roles.role_id == role_id)
|
||||
return role.delete_instance()
|
||||
return Roles.delete().where(Roles.role_id == role_id).execute()
|
||||
|
||||
@staticmethod
|
||||
def role_id_exists(role_id):
|
||||
def role_id_exists(role_id) -> bool:
|
||||
if not HelperRoles.get_role(role_id):
|
||||
return False
|
||||
return True
|
||||
|
@ -1,6 +1,6 @@
|
||||
import logging
|
||||
import typing as t
|
||||
from enum import Enum
|
||||
import logging
|
||||
from peewee import (
|
||||
ForeignKeyField,
|
||||
CharField,
|
||||
@ -52,14 +52,14 @@ class PermissionsServers:
|
||||
|
||||
@staticmethod
|
||||
def get_permissions_list():
|
||||
permissions_list = []
|
||||
permissions_list: t.List[EnumPermissionsServer] = []
|
||||
for member in EnumPermissionsServer.__members__.items():
|
||||
permissions_list.append(member[1])
|
||||
return permissions_list
|
||||
|
||||
@staticmethod
|
||||
def get_permissions(permissions_mask):
|
||||
permissions_list = []
|
||||
permissions_list: t.List[EnumPermissionsServer] = []
|
||||
for member in EnumPermissionsServer.__members__.items():
|
||||
if PermissionsServers.has_permission(permissions_mask, member[1]):
|
||||
permissions_list.append(member[1])
|
||||
@ -96,17 +96,29 @@ class PermissionsServers:
|
||||
# Role_Servers Methods
|
||||
# **********************************************************************************
|
||||
@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)
|
||||
|
||||
@staticmethod
|
||||
def get_servers_from_role(role_id):
|
||||
def get_servers_from_role(role_id: t.Union[str, int]):
|
||||
return (
|
||||
RoleServers.select()
|
||||
.join(Servers, JOIN.INNER)
|
||||
.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
|
||||
def get_roles_from_server(server_id):
|
||||
return (
|
||||
@ -179,9 +191,9 @@ class PermissionsServers:
|
||||
RoleServers.save(role_server)
|
||||
|
||||
@staticmethod
|
||||
def delete_roles_permissions(role_id, removed_servers=None):
|
||||
if removed_servers is None:
|
||||
removed_servers = {}
|
||||
def delete_roles_permissions(
|
||||
role_id: t.Union[str, int], removed_servers: t.Sequence[t.Union[str, int]]
|
||||
):
|
||||
return (
|
||||
RoleServers.delete()
|
||||
.where(RoleServers.role_id == role_id)
|
||||
|
@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import datetime
|
||||
import typing as t
|
||||
from peewee import (
|
||||
CharField,
|
||||
AutoField,
|
||||
@ -7,6 +8,7 @@ from peewee import (
|
||||
BooleanField,
|
||||
IntegerField,
|
||||
)
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from app.classes.shared.main_models import DatabaseShortcuts
|
||||
from app.classes.models.base_model import BaseModel
|
||||
@ -61,8 +63,30 @@ class HelperServers:
|
||||
server_log_file: str,
|
||||
server_stop: 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(
|
||||
{
|
||||
Servers.server_name: name,
|
||||
@ -75,6 +99,7 @@ class HelperServers:
|
||||
Servers.crash_detection: False,
|
||||
Servers.log_path: server_log_file,
|
||||
Servers.server_port: server_port,
|
||||
Servers.server_ip: server_host,
|
||||
Servers.stop_command: server_stop,
|
||||
Servers.backup_path: backup_path,
|
||||
Servers.type: server_type,
|
||||
@ -106,6 +131,24 @@ class HelperServers:
|
||||
except IndexError:
|
||||
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
|
||||
# **********************************************************************************
|
||||
@ -114,6 +157,10 @@ class HelperServers:
|
||||
query = Servers.select()
|
||||
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
|
||||
def get_server_friendly_name(server_id):
|
||||
server_data = HelperServers.get_server_data_by_id(server_id)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import logging
|
||||
import datetime
|
||||
from typing import Optional, Union
|
||||
import typing as t
|
||||
|
||||
from peewee import (
|
||||
ForeignKeyField,
|
||||
@ -45,6 +45,15 @@ class Users(BaseModel):
|
||||
table_name = "users"
|
||||
|
||||
|
||||
PUBLIC_USER_ATTRS: t.Final = [
|
||||
"user_id",
|
||||
"created",
|
||||
"username",
|
||||
"enabled",
|
||||
"superuser",
|
||||
"lang", # maybe remove?
|
||||
]
|
||||
|
||||
# **********************************************************************************
|
||||
# API Keys Class
|
||||
# **********************************************************************************
|
||||
@ -90,6 +99,15 @@ class HelperUsers:
|
||||
query = Users.select().where(Users.username != "system")
|
||||
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
|
||||
def get_user_lang_by_id(user_id):
|
||||
return Users.get(Users.user_id == user_id).lang
|
||||
@ -134,6 +152,24 @@ class HelperUsers:
|
||||
# logger.debug("user: ({}) {}".format(user_id, {}))
|
||||
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
|
||||
def check_system_user(user_id):
|
||||
try:
|
||||
@ -153,7 +189,7 @@ class HelperUsers:
|
||||
self,
|
||||
username: str,
|
||||
password: str = None,
|
||||
email: Optional[str] = None,
|
||||
email: t.Optional[str] = None,
|
||||
enabled: bool = True,
|
||||
superuser: bool = False,
|
||||
) -> str:
|
||||
@ -177,7 +213,7 @@ class HelperUsers:
|
||||
def add_rawpass_user(
|
||||
username: str,
|
||||
password: str = None,
|
||||
email: Optional[str] = None,
|
||||
email: t.Optional[str] = None,
|
||||
enabled: bool = True,
|
||||
superuser: bool = False,
|
||||
) -> str:
|
||||
@ -212,7 +248,7 @@ class HelperUsers:
|
||||
|
||||
@staticmethod
|
||||
def get_super_user_list():
|
||||
final_users = []
|
||||
final_users: t.List[int] = []
|
||||
super_users = Users.select().where(
|
||||
Users.superuser == True # pylint: disable=singleton-comparison
|
||||
)
|
||||
@ -224,8 +260,7 @@ class HelperUsers:
|
||||
def remove_user(self, user_id):
|
||||
with self.database.atomic():
|
||||
UserRoles.delete().where(UserRoles.user_id == user_id).execute()
|
||||
user = Users.get(Users.user_id == user_id)
|
||||
return user.delete_instance()
|
||||
return Users.delete().where(Users.user_id == user_id).execute()
|
||||
|
||||
@staticmethod
|
||||
def set_support_path(user_id, support_path):
|
||||
@ -271,11 +306,10 @@ class HelperUsers:
|
||||
|
||||
@staticmethod
|
||||
def get_user_roles_names(user_id):
|
||||
roles_list = []
|
||||
roles = UserRoles.select().where(UserRoles.user_id == user_id)
|
||||
for r in roles:
|
||||
roles_list.append(HelperRoles.get_role(r.role_id)["role_name"])
|
||||
return roles_list
|
||||
roles = UserRoles.select(UserRoles.role_id).where(UserRoles.user_id == user_id)
|
||||
return [
|
||||
HelperRoles.get_role_column(role.role_id, "role_name") for role in roles
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def add_role_to_user(user_id, role_id):
|
||||
@ -284,7 +318,7 @@ class HelperUsers:
|
||||
).execute()
|
||||
|
||||
@staticmethod
|
||||
def add_user_roles(user: Union[dict, Users]):
|
||||
def add_user_roles(user: t.Union[dict, Users]):
|
||||
if isinstance(user, dict):
|
||||
user_id = user["user_id"]
|
||||
else:
|
||||
@ -329,6 +363,10 @@ class HelperUsers:
|
||||
def remove_roles_from_role_id(role_id):
|
||||
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
|
||||
# **********************************************************************************
|
||||
@ -346,8 +384,8 @@ class HelperUsers:
|
||||
name: str,
|
||||
user_id: str,
|
||||
superuser: bool = False,
|
||||
server_permissions_mask: Optional[str] = None,
|
||||
crafty_permissions_mask: Optional[str] = None,
|
||||
server_permissions_mask: t.Optional[str] = None,
|
||||
crafty_permissions_mask: t.Optional[str] = None,
|
||||
):
|
||||
return ApiKeys.insert(
|
||||
{
|
||||
|
@ -34,7 +34,7 @@ class Authentication:
|
||||
|
||||
def check_no_iat(self, token) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
return jwt.decode(token, self.secret, algorithms=["HS256"])
|
||||
return jwt.decode(str(token), self.secret, algorithms=["HS256"])
|
||||
except PyJWTError as error:
|
||||
logger.debug("Error while checking JWT token: ", exc_info=error)
|
||||
return None
|
||||
@ -44,7 +44,7 @@ class Authentication:
|
||||
token,
|
||||
) -> Optional[Tuple[Optional[ApiKeys], Dict[str, Any], Dict[str, Any]]]:
|
||||
try:
|
||||
data = jwt.decode(token, self.secret, algorithms=["HS256"])
|
||||
data = jwt.decode(str(token), self.secret, algorithms=["HS256"])
|
||||
except PyJWTError as error:
|
||||
logger.debug("Error while checking JWT token: ", exc_info=error)
|
||||
return None
|
||||
@ -65,5 +65,17 @@ class Authentication:
|
||||
else:
|
||||
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:
|
||||
return self.check(token) is not None
|
||||
|
@ -72,7 +72,7 @@ class Helpers:
|
||||
installer.do_install()
|
||||
|
||||
@staticmethod
|
||||
def float_to_string(gbs: int):
|
||||
def float_to_string(gbs: float):
|
||||
s = str(float(gbs) * 1000).rstrip("0").rstrip(".")
|
||||
return s
|
||||
|
||||
@ -232,7 +232,7 @@ class Helpers:
|
||||
return default_return
|
||||
|
||||
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:
|
||||
logger.critical(
|
||||
@ -270,18 +270,17 @@ class Helpers:
|
||||
|
||||
@staticmethod
|
||||
def get_announcements():
|
||||
response = requests.get("https://craftycontrol.com/notify.json", timeout=2)
|
||||
data = (
|
||||
'[{"id":"1","date":"Unknown",'
|
||||
'"title":"Error getting Announcements",'
|
||||
'"desc":"Error getting Announcements","link":""}]'
|
||||
)
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
try:
|
||||
data = json.loads(response.content)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load json content with error: {e}")
|
||||
try:
|
||||
response = requests.get("https://craftycontrol.com/notify.json", timeout=2)
|
||||
data = json.loads(response.content)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch notifications with error: {e}")
|
||||
|
||||
return data
|
||||
|
||||
@ -1001,10 +1000,11 @@ class Helpers:
|
||||
return text
|
||||
|
||||
@staticmethod
|
||||
def get_lang_page(text):
|
||||
lang = text.split("_")[0]
|
||||
region = text.split("_")[1]
|
||||
def get_lang_page(text) -> str:
|
||||
splitted = text.split("_")
|
||||
if len(splitted) != 2:
|
||||
return "en"
|
||||
lang, region = splitted
|
||||
if region == "EN":
|
||||
return "en"
|
||||
else:
|
||||
return lang + "-" + region
|
||||
return lang + "-" + region
|
||||
|
@ -5,7 +5,7 @@ import shutil
|
||||
import time
|
||||
import logging
|
||||
import tempfile
|
||||
from typing import Union
|
||||
import typing as t
|
||||
from peewee import DoesNotExist
|
||||
|
||||
# TZLocal is set as a hidden import on win pipeline
|
||||
@ -276,7 +276,7 @@ class Controller:
|
||||
except:
|
||||
return {"percent": 0, "total_files": 0}
|
||||
|
||||
def get_server_obj(self, server_id: Union[str, int]) -> Union[bool, Server]:
|
||||
def get_server_obj(self, server_id: t.Union[str, int]) -> t.Union[bool, Server]:
|
||||
for server in self.servers_list:
|
||||
if str(server["server_id"]) == str(server_id):
|
||||
return server["server_obj"]
|
||||
@ -297,6 +297,10 @@ class Controller:
|
||||
servers = HelperServers.get_all_defined_servers()
|
||||
return servers
|
||||
|
||||
@staticmethod
|
||||
def get_all_server_ids() -> t.List[int]:
|
||||
return HelperServers.get_all_server_ids()
|
||||
|
||||
def list_running_servers(self):
|
||||
running_servers = []
|
||||
|
||||
@ -337,6 +341,177 @@ class Controller:
|
||||
svr_obj = self.get_server_obj(server_id)
|
||||
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(
|
||||
self,
|
||||
server: str,
|
||||
@ -759,6 +934,7 @@ class Controller:
|
||||
server_stop: str,
|
||||
server_port: int,
|
||||
server_type: str,
|
||||
server_host: str = "127.0.0.1",
|
||||
):
|
||||
# put data in the db
|
||||
new_id = self.servers.create_server(
|
||||
@ -772,6 +948,7 @@ class Controller:
|
||||
server_stop,
|
||||
server_type,
|
||||
server_port,
|
||||
server_host,
|
||||
)
|
||||
|
||||
if not Helpers.check_file_exists(
|
||||
@ -788,7 +965,6 @@ class Controller:
|
||||
"The server is managed by Crafty Controller.\n "
|
||||
"Leave this directory/files alone please"
|
||||
)
|
||||
file.close()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unable to create required server files due to :{e}")
|
||||
|
@ -81,7 +81,7 @@ class Migrator(object):
|
||||
database = database.obj
|
||||
self.database: SqliteDatabase = database
|
||||
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)
|
||||
|
||||
def run(self):
|
||||
|
@ -12,6 +12,7 @@ from apscheduler.triggers.cron import CronTrigger
|
||||
from app.classes.models.management import HelpersManagement
|
||||
from app.classes.models.users import HelperUsers
|
||||
from app.classes.shared.console import Console
|
||||
from app.classes.shared.main_controller import Controller
|
||||
from app.classes.web.tornado_handler import Webserver
|
||||
|
||||
logger = logging.getLogger("apscheduler")
|
||||
@ -32,6 +33,8 @@ scheduler_intervals = {
|
||||
|
||||
|
||||
class TasksManager:
|
||||
controller: Controller
|
||||
|
||||
def __init__(self, helper, controller):
|
||||
self.helper = helper
|
||||
self.controller = controller
|
||||
@ -101,6 +104,17 @@ class TasksManager:
|
||||
elif command == "restart_server":
|
||||
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":
|
||||
svr.backup_server()
|
||||
|
||||
|
23
app/classes/web/base_api_handler.py
Normal file
23
app/classes/web/base_api_handler.py
Normal 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]]]
|
||||
# }}}
|
@ -1,18 +1,50 @@
|
||||
import logging
|
||||
from typing import Union, List, Optional, Tuple, Dict, Any
|
||||
import re
|
||||
import typing as t
|
||||
import orjson
|
||||
import bleach
|
||||
import tornado.web
|
||||
|
||||
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
|
||||
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__)
|
||||
|
||||
bearer_pattern = re.compile(r"^Bearer ", flags=re.IGNORECASE)
|
||||
|
||||
|
||||
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)}
|
||||
redactables = ("pass", "api")
|
||||
|
||||
helper: Helpers
|
||||
controller: Controller
|
||||
translator: Translation
|
||||
|
||||
# noinspection PyAttributeOutsideInit
|
||||
def initialize(
|
||||
self, helper=None, controller=None, tasks_manager=None, translator=None
|
||||
@ -30,11 +62,25 @@ class BaseHandler(tornado.web.RequestHandler):
|
||||
)
|
||||
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(
|
||||
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"))
|
||||
|
||||
def autobleach(self, name, text):
|
||||
@ -53,15 +99,15 @@ class BaseHandler(tornado.web.RequestHandler):
|
||||
def get_argument(
|
||||
self,
|
||||
name: str,
|
||||
default: Union[
|
||||
default: t.Union[
|
||||
None, str, tornado.web._ArgDefaultMarker
|
||||
] = tornado.web._ARG_DEFAULT,
|
||||
strip: bool = True,
|
||||
) -> Optional[str]:
|
||||
) -> t.Optional[str]:
|
||||
arg = self._get_argument(name, default, self.request.arguments, strip)
|
||||
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):
|
||||
raise AssertionError
|
||||
args = self._get_arguments(name, self.request.arguments, strip)
|
||||
@ -69,3 +115,127 @@ class BaseHandler(tornado.web.RequestHandler):
|
||||
for arg in args:
|
||||
args_ret += self.autobleach(name, arg)
|
||||
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
|
||||
|
@ -2,7 +2,7 @@
|
||||
import time
|
||||
import datetime
|
||||
import os
|
||||
from typing import Dict, Any, Tuple
|
||||
import typing as t
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
@ -31,16 +31,16 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PanelHandler(BaseHandler):
|
||||
def get_user_roles(self) -> Dict[str, list]:
|
||||
def get_user_roles(self) -> t.Dict[str, list]:
|
||||
user_roles = {}
|
||||
for user in self.controller.users.get_all_users():
|
||||
user_roles_list = self.controller.users.get_user_roles_names(user.user_id)
|
||||
for user_id in self.controller.users.get_all_user_ids():
|
||||
user_roles_list = self.controller.users.get_user_roles_names(user_id)
|
||||
# user_servers =
|
||||
# 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
|
||||
|
||||
def get_role_servers(self) -> set:
|
||||
def get_role_servers(self) -> t.Set[int]:
|
||||
servers = set()
|
||||
for server in self.controller.list_defined_servers():
|
||||
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))
|
||||
return servers
|
||||
|
||||
def get_perms_quantity(self) -> Tuple[str, dict]:
|
||||
def get_perms_quantity(self) -> t.Tuple[str, dict]:
|
||||
permissions_mask: str = "000"
|
||||
server_quantity: dict = {}
|
||||
for (
|
||||
@ -101,6 +101,16 @@ class PanelHandler(BaseHandler):
|
||||
)
|
||||
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:
|
||||
roles = set()
|
||||
for role in self.controller.roles.get_all_roles():
|
||||
@ -260,7 +270,7 @@ class PanelHandler(BaseHandler):
|
||||
user_order.remove(server_id)
|
||||
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
|
||||
"update_available": False,
|
||||
"serverTZ": get_localzone(),
|
||||
@ -302,6 +312,8 @@ class PanelHandler(BaseHandler):
|
||||
else None,
|
||||
"superuser": superuser,
|
||||
}
|
||||
|
||||
# http://en.gravatar.com/site/implement/images/#rating
|
||||
if self.helper.get_setting("allow_nsfw_profile_pictures"):
|
||||
rating = "x"
|
||||
else:
|
||||
|
157
app/classes/web/routes/api/api_handlers.py
Normal file
157
app/classes/web/routes/api/api_handlers.py
Normal 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,
|
||||
),
|
||||
]
|
21
app/classes/web/routes/api/auth/invalidate_tokens.py
Normal file
21
app/classes/web/routes/api/auth/invalidate_tokens.py
Normal 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"})
|
104
app/classes/web/routes/api/auth/login.py
Normal file
104
app/classes/web/routes/api/auth/login.py
Normal 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"},
|
||||
)
|
2
app/classes/web/routes/api/auth/register.py
Normal file
2
app/classes/web/routes/api/auth/register.py
Normal file
@ -0,0 +1,2 @@
|
||||
# nothing here yet
|
||||
# sometime implement configurable self service account creation?
|
17
app/classes/web/routes/api/index_handler.py
Normal file
17
app/classes/web/routes/api/index_handler.py
Normal 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}",
|
||||
},
|
||||
},
|
||||
)
|
107
app/classes/web/routes/api/jsonschema.py
Normal file
107
app/classes/web/routes/api/jsonschema.py
Normal 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}"
|
||||
),
|
||||
},
|
||||
)
|
18
app/classes/web/routes/api/not_found.py
Normal file
18
app/classes/web/routes/api/not_found.py
Normal 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]]]
|
131
app/classes/web/routes/api/roles/index.py
Normal file
131
app/classes/web/routes/api/roles/index.py
Normal 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}},
|
||||
)
|
143
app/classes/web/routes/api/roles/role/index.py
Normal file
143
app/classes/web/routes/api/roles/role/index.py
Normal 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"},
|
||||
)
|
32
app/classes/web/routes/api/roles/role/servers.py
Normal file
32
app/classes/web/routes/api/roles/role/servers.py
Normal 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),
|
||||
},
|
||||
)
|
36
app/classes/web/routes/api/roles/role/users.py
Normal file
36
app/classes/web/routes/api/roles/role/users.py
Normal 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})
|
713
app/classes/web/routes/api/servers/index.py
Normal file
713
app/classes/web/routes/api/servers/index.py
Normal 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,
|
||||
},
|
||||
},
|
||||
)
|
98
app/classes/web/routes/api/servers/server/action.py
Normal file
98
app/classes/web/routes/api/servers/server/action.py
Normal 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)}},
|
||||
)
|
168
app/classes/web/routes/api/servers/server/index.py
Normal file
168
app/classes/web/routes/api/servers/server/index.py
Normal 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"},
|
||||
)
|
73
app/classes/web/routes/api/servers/server/logs.py
Normal file
73
app/classes/web/routes/api/servers/server/logs.py
Normal 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})
|
23
app/classes/web/routes/api/servers/server/public.py
Normal file
23
app/classes/web/routes/api/servers/server/public.py
Normal 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"]
|
||||
},
|
||||
},
|
||||
)
|
28
app/classes/web/routes/api/servers/server/stats.py
Normal file
28
app/classes/web/routes/api/servers/server/stats.py
Normal 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]
|
||||
),
|
||||
},
|
||||
)
|
31
app/classes/web/routes/api/servers/server/users.py
Normal file
31
app/classes/web/routes/api/servers/server/users.py
Normal 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)),
|
||||
},
|
||||
)
|
164
app/classes/web/routes/api/users/index.py
Normal file
164
app/classes/web/routes/api/users/index.py
Normal 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)}},
|
||||
)
|
241
app/classes/web/routes/api/users/user/index.py
Normal file
241
app/classes/web/routes/api/users/user/index.py
Normal 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"})
|
49
app/classes/web/routes/api/users/user/pfp.py
Normal file
49
app/classes/web/routes/api/users/user/pfp.py
Normal 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})
|
37
app/classes/web/routes/api/users/user/public.py
Normal file
37
app/classes/web/routes/api/users/user/public.py
Normal 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},
|
||||
)
|
@ -13,10 +13,12 @@ import tornado.httpserver
|
||||
|
||||
from app.classes.shared.console import Console
|
||||
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.public_handler import PublicHandler
|
||||
from app.classes.web.panel_handler import PanelHandler
|
||||
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.ajax_handler import AjaxHandler
|
||||
from app.classes.web.api_handler import (
|
||||
@ -42,6 +44,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Webserver:
|
||||
controller: Controller
|
||||
helper: Helpers
|
||||
|
||||
def __init__(self, helper, controller, tasks_manager):
|
||||
self.ioloop = None
|
||||
self.http_server = None
|
||||
@ -150,7 +155,7 @@ class Webserver:
|
||||
(r"/ws", SocketHandler, handler_args),
|
||||
(r"/upload", UploadHandler, handler_args),
|
||||
(r"/status", StatusHandler, handler_args),
|
||||
# API Routes
|
||||
# API Routes V1
|
||||
(r"/api/v1/stats/servers", ServersStats, handler_args),
|
||||
(r"/api/v1/stats/node", NodeStats, 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/users/create_user", CreateUser, handler_args),
|
||||
(r"/api/v1/users/delete_user", DeleteUser, handler_args),
|
||||
# API Routes V2
|
||||
*api_handlers(handler_args),
|
||||
]
|
||||
|
||||
app = tornado.web.Application(
|
||||
|
@ -1,26 +1,27 @@
|
||||
{
|
||||
"http_port": 8000,
|
||||
"https_port": 8443,
|
||||
"language": "en_EN",
|
||||
"cookie_expire": 30,
|
||||
"cookie_secret": "random",
|
||||
"apikey_secret": "random",
|
||||
"show_errors": true,
|
||||
"history_max_age": 7,
|
||||
"stats_update_frequency": 30,
|
||||
"delete_default_json": false,
|
||||
"show_contribute_link": true,
|
||||
"virtual_terminal_lines": 70,
|
||||
"max_log_lines": 700,
|
||||
"max_audit_entries": 300,
|
||||
"disabled_language_files": [
|
||||
"lol_EN.json",
|
||||
""
|
||||
],
|
||||
"stream_size_GB": 1,
|
||||
"keywords": [
|
||||
"help",
|
||||
"chunk"
|
||||
],
|
||||
"allow_nsfw_profile_pictures": false
|
||||
}
|
||||
"http_port": 8000,
|
||||
"https_port": 8443,
|
||||
"language": "en_EN",
|
||||
"cookie_expire": 30,
|
||||
"cookie_secret": "random",
|
||||
"apikey_secret": "random",
|
||||
"show_errors": true,
|
||||
"history_max_age": 7,
|
||||
"stats_update_frequency": 30,
|
||||
"delete_default_json": false,
|
||||
"show_contribute_link": true,
|
||||
"virtual_terminal_lines": 70,
|
||||
"max_log_lines": 700,
|
||||
"max_audit_entries": 300,
|
||||
"disabled_language_files": [
|
||||
"lol_EN.json",
|
||||
""
|
||||
],
|
||||
"stream_size_GB": 1,
|
||||
"keywords": [
|
||||
"help",
|
||||
"chunk"
|
||||
],
|
||||
"allow_nsfw_profile_pictures": false,
|
||||
"enable_user_self_delete": false
|
||||
}
|
||||
|
@ -874,4 +874,4 @@
|
||||
|
||||
</script>
|
||||
|
||||
{% end %}
|
||||
{% end %}
|
||||
|
@ -17,3 +17,5 @@ requests==2.26
|
||||
termcolor==1.1
|
||||
tornado==6.0
|
||||
tzlocal==4.0
|
||||
jsonschema==4.4.0
|
||||
orjson==3.6.7
|
||||
|
Loading…
Reference in New Issue
Block a user