Merge branch 'feature/steamcmd' of gitlab.com:crafty-controller/crafty-4 into feature/steamcmd

This commit is contained in:
Zedifus 2023-12-07 17:05:17 +00:00
commit 7c3c19df5b
108 changed files with 7397 additions and 6265 deletions

View File

@ -3,6 +3,7 @@ docker/
.dockerignore
Dockerfile
docker-compose.yml
docker-compose.yml.example
# git & gitlab related
.git/
@ -17,6 +18,8 @@ docker-compose.yml
.venv
.vscode
crafty_commander.exe
CHANGELOG.md
CONTRIBUTING.md
DBCHANGES.md
docker-compose.yml.example
README.md
sonar-project.properties

View File

@ -28,7 +28,7 @@ docker-build-dev:
docker version
- docker run --rm --privileged aptman/qus -- -r
- docker run --rm --privileged aptman/qus -s -- -p aarch64 x86_64
- echo $CI_BUILD_TOKEN | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
- echo $CI_JOB_TOKEN | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
- echo $DOCKERHUB_TOKEN | docker login -u "$DOCKERHUB_USER" --password-stdin $DOCKERHUB_REGISTRY
script:
- |
@ -45,6 +45,7 @@ docker-build-dev:
--build-arg "BUILD_DATE=$(date +"%Y-%m-%dT%H:%M:%SZ")"
--build-arg "BUILD_REF=${CI_COMMIT_SHA}"
--build-arg "CRAFTY_VER=${VERSION}"
--provenance false
--tag "$CI_REGISTRY_IMAGE${tag}"
--tag "arcadiatechnology/crafty-4${tag}"
--platform linux/arm64/v8,linux/amd64
@ -84,7 +85,7 @@ docker-build-prod:
docker version
- docker run --rm --privileged aptman/qus -- -r
- docker run --rm --privileged aptman/qus -s -- -p aarch64 x86_64
- echo $CI_BUILD_TOKEN | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
- echo $CI_JOB_TOKEN | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
- echo $DOCKERHUB_TOKEN | docker login -u "$DOCKERHUB_USER" --password-stdin $DOCKERHUB_REGISTRY
script:
- |
@ -100,6 +101,7 @@ docker-build-prod:
--build-arg "BUILD_DATE=$(date +"%Y-%m-%dT%H:%M:%SZ")"
--build-arg "BUILD_REF=${CI_COMMIT_SHA}"
--build-arg "CRAFTY_VER=${VERSION}"
--provenance false
--tag "$CI_REGISTRY_IMAGE:$VERSION"
--tag "$CI_REGISTRY_IMAGE:latest"
--tag "arcadiatechnology/crafty-4:$VERSION"

View File

@ -57,3 +57,27 @@ pylint:
reports:
codequality: codeclimate.json
when: always
# SonarQube/SonarCloud - Code Climate & QA [https://www.sonarsource.com]
sonarcloud-check:
stage: lint
image:
name: sonarsource/sonar-scanner-cli:latest
entrypoint: [""]
tags:
- docker
rules:
- if: "$SONAR_TOKEN == null"
when: never
- if: "$CODE_QUALITY_DISABLED"
when: never
- if: "$CI_COMMIT_TAG || $CI_COMMIT_BRANCH"
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" # Defines the location of the analysis task cache
GIT_DEPTH: "0" # Tells git to fetch all the branches of the project, required by the analysis task
cache:
key: "${CI_JOB_NAME}"
paths:
- .sonar/cache
script:
- sonar-scanner

View File

@ -606,5 +606,5 @@ preferred-modules=
# Exceptions that will emit a warning when being caught. Defaults to
# "BaseException, Exception".
overgeneral-exceptions=BaseException,
Exception
overgeneral-exceptions=builtins.BaseException,
builtins.Exception

View File

@ -1,15 +1,80 @@
# Changelog
## --- [4.0.23] - 2023/TBD
## --- [4.2.0] - 2023/TBD
### New features
TBD
- Finish and Activate Arcadia notification backend ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/621))
### Bug fixes
TBD
- PWA: Removed the custom offline page in favour of browser default ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/607))
- Fix hidden servers appearing visible on public mobile status page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/612))
- Correctly handle if a server returns a string instead of json data on socket ping ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/614))
- Bump tornado to resolve #269 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/623))
- Bump crypto to resolve #267 & #268 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/622))
### Refactor
- Consolidate remaining frontend functions into API V2, and remove ajax internal API ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/585))
### Tweaks
TBD
- Polish/Enhance display for InApp Documentation ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/613))
- Add get_users command to Crafty's console ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/620))
### Lang
TBD
<br><br>
## --- [4.1.3] - 2023/07/18
### Bug fixes
- Include tzdata in Docker image ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/604))
- Fix text/formatting issue on server config page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/602))
- Bump required version of PyYAML to 6.0.1 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/609))
- Fix enable/disable schedule toggles on schedule list ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/606))
- Fix formatting on Creation page when server jars is unavailable ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/601))
### Refactor
- Replace "in_file" helper method ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/605))
### Tweaks
- Add public status link to login ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/608))
<br><br>
## --- [4.1.2] - 2023/06/18
### Bug fixes
- Fix upload root files being hidden ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/590))
- Send empty json for no banned/cached players ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/589))
- Bump Tornado from 6.0 to 6.3.2 in response to CVE-2023-28370 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/591))
- Fix bug where commands would show "command_server" when initially created ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/592))
- Add ID autofield to management CraftySettings class ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/599))
### Refactor
- Optimize player management page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/593))
### Tweaks
- Remove bedrock servers in serverjars options ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/595))
- Bump cryptography & pyOpenSSL ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/596))
- Bump requests ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/600))
### Lang
- Update es_ES & pl_PL lang, thank you `.lucyy_` & `terrariadlc` ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/597))
<br><br>
## --- [4.1.1] - 2023/05/23
### Bug fixes
- Fix task scheduling where a command was not sent to the DB ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/586))
### Tweaks
- Improve the UI on several areas of the Crafty Panel ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/547))
- Improve creation page errors / Server Jars Credit ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/587))
<br><br>
## --- [4.1.0] - 2023/05/15
### New features
- Mobile PWA App (beta) | Ability to add a Crafty icon to your mobile's home screen ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/576))
- [New Crafty Documentation release](https://docs.craftycontrol.com)
### Refactor
- Frontend Ajax Refactor | Start using API to send Remote Comms to Server ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/565))
- MKDocs Release | Replace wiki names with docs ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/583))
### Bug fixes
- Fix pipelines failing to build from gitlab pre-defined variable deprecation ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/582))
- Fix incompatible buildx provenance meta, causing digest issues on GL/DH container registries ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/582))
- Fix Auth'd servers in roles | Refine server ordering ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/574))
- Fix import loop detection ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/575))
- Fix Cargo errors on Ubuntu 23.04 installs ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/579))
- Fix project root error on first start ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/580))
### Tweaks
- Check for python version so we don't just fail out on unsupported python versions ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/577))
- Show warning for serverjars API connection issues ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/581))
- Retain pathing in execution command on backup restore ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/578))
<br><br>
## --- [4.0.22] - 2023/04/08
### Bug fixes
- Fix dashboard crash for users without disks or if crafty doesn't have permission to access mount point ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/571))

View File

@ -26,6 +26,7 @@ RUN apt-get update \
openjdk-11-jre-headless \
openjdk-17-jre-headless \
lib32stdc++6 \
tzdata \
&& apt-get autoremove \
&& apt-get clean
@ -68,7 +69,7 @@ LABEL \
org.opencontainers.image.title="Crafty Controller" \
org.opencontainers.image.description="A Game Server Control Panel / Launcher" \
org.opencontainers.image.url="https://craftycontrol.com/" \
org.opencontainers.image.documentation="https://wiki.craftycontrol.com/" \
org.opencontainers.image.documentation="https://docs.craftycontrol.com" \
org.opencontainers.image.source="https://gitlab.com/crafty-controller/crafty-4" \
org.opencontainers.image.vendor="Arcadia Technology, LLC." \
org.opencontainers.image.licenses="GPL-3.0"

View File

@ -1,5 +1,5 @@
[![Crafty Logo](app/frontend/static/assets/images/logo_long.svg)](https://craftycontrol.com)
# Crafty Controller 4.0.23
# Crafty Controller 4.2.0
> Python based Control Panel for your Minecraft Server
## What is Crafty Controller?
@ -9,7 +9,7 @@ a web interface for the server administrators to interact with their servers. Cr
is compatible with Docker, Linux, Windows 7, Windows 8 and Windows 10.
## Documentation
Documentation available on [wiki.craftycontrol.com](https://craftycontrol.com)
Documentation available on [Crafty Docs](https://docs.craftycontrol.com)
## Meta
Project Homepage - https://craftycontrol.com

View File

@ -79,8 +79,8 @@ class ManagementController:
# Audit_Log Methods
# **********************************************************************************
@staticmethod
def get_actity_log():
return HelpersManagement.get_actity_log()
def get_activity_log():
return HelpersManagement.get_activity_log()
def add_to_audit_log(self, user_id, log_msg, server_id=None, source_ip=None):
return self.management_helper.add_to_audit_log(

View File

@ -255,6 +255,7 @@ class ServersController(metaclass=Singleton):
@staticmethod
def get_authorized_servers(user_id):
server_ids = []
server_data: t.List[t.Dict[str, t.Any]] = []
user_roles = HelperUsers.user_role_query(user_id)
for user in user_roles:
@ -262,11 +263,13 @@ class ServersController(metaclass=Singleton):
user.role_id
)
for role in role_servers:
server_data.append(
ServersController().get_server_instance_by_id(
role.server_id.server_id
if role.server_id.server_id not in server_ids:
server_ids.append(role.server_id.server_id)
server_data.append(
ServersController().get_server_instance_by_id(
role.server_id.server_id
)
)
)
return server_data
@ -277,11 +280,10 @@ class ServersController(metaclass=Singleton):
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)
user_ids.add(user_role.user_id.user_id)
for user_id in HelperUsers.get_super_user_list():
user_ids.add(user_id)
return user_ids
def get_all_servers_stats(self):
@ -515,6 +517,25 @@ class ServersController(metaclass=Singleton):
# **********************************************************************************
# Servers Helpers Methods
# **********************************************************************************
@staticmethod
def get_cached_players(server_id):
srv = ServersController().get_server_instance_by_id(server_id)
stats = srv.stats_helper.get_server_stats()
server_path = stats["server_id"]["path"]
path = os.path.join(server_path, "usercache.json")
try:
with open(
Helpers.get_os_understandable_path(path), encoding="utf-8"
) as file:
content = file.read()
file.close()
except Exception as ex:
logger.error(ex)
return {}
return json.loads(content)
@staticmethod
def get_banned_players(server_id):
srv = ServersController().get_server_instance_by_id(server_id)
@ -529,8 +550,8 @@ class ServersController(metaclass=Singleton):
content = file.read()
file.close()
except Exception as ex:
print(ex)
return None
logger.error(ex)
return {}
return json.loads(content)

View File

@ -31,7 +31,7 @@ class UsersController:
for permission in PermissionsCrafty.get_permissions_list()
],
},
"quantity": {"type": "number", "minimum": 0},
"quantity": {"type": "number", "minimum": -1},
"enabled": {"type": "boolean"},
}
self.user_jsonschema_props: t.Final = {
@ -46,7 +46,7 @@ class UsersController:
"password": {
"type": "string",
"maxLength": 20,
"minLength": 4,
"minLength": 6,
"examples": ["crafty"],
"title": "Password",
},
@ -73,6 +73,8 @@ class UsersController:
"examples": [False],
"title": "Superuser",
},
"manager": {"type": ["integer", "null"]},
"theme": {"type": "string"},
"permissions": {
"type": "array",
"items": {
@ -84,11 +86,12 @@ class UsersController:
"roles": {
"type": "array",
"items": {
"type": "string",
"type": "integer",
"minLength": 1,
},
},
"hints": {"type": "boolean"},
"server_order": {"type": "string"},
}
# **********************************************************************************

View File

@ -16,6 +16,12 @@ logger = logging.getLogger(__name__)
class Server:
def __init__(self, data):
if isinstance(data, str):
logger.error(
"Failed to calculate stats. Expected object. "
f"Server returned string: {data}"
)
return
self.description = data.get("description")
# print(self.description)
if isinstance(self.description, dict):

View File

@ -106,19 +106,36 @@ class RaknetPing:
if sliced[3] != RaknetPing.magic:
raise ValueError(f"Incorrect magic received ({sliced[3]})")
ret["server_guid"] = sliced[2]
ret["server_string_raw"] = sliced[4]
server_info = sliced[4].split(";")
ret["server_edition"] = server_info[0]
ret["server_motd"] = (server_info[1], server_info[7])
ret["server_protocol_version"] = server_info[2]
ret["server_version_name"] = server_info[3]
ret["server_player_count"] = server_info[4]
ret["server_player_max"] = server_info[5]
ret["server_uuid"] = server_info[6]
ret["server_game_mode"] = server_info[8]
ret["server_game_mode_num"] = server_info[9]
ret["server_port_ipv4"] = server_info[10]
ret["server_port_ipv6"] = server_info[11]
try:
if len(sliced) >= 5:
ret["server_string_raw"] = sliced[4]
server_info = sliced[4].split(";")
ret["server_edition"] = server_info[0]
ret["server_motd"] = (server_info[1], server_info[7])
ret["server_protocol_version"] = server_info[2]
ret["server_version_name"] = server_info[3]
ret["server_player_count"] = server_info[4]
ret["server_player_max"] = server_info[5]
ret["server_uuid"] = server_info[6]
ret["server_game_mode"] = server_info[8]
ret["server_game_mode_num"] = server_info[9]
ret["server_port_ipv4"] = server_info[10]
ret["server_port_ipv6"] = server_info[11]
else:
ret["server_string_raw"] = ""
ret["server_edition"] = "Generic Raknet"
ret["server_motd"] = (None, None)
ret["server_protocol_version"] = None
ret["server_version_name"] = "Generic Raknet"
ret["server_player_count"] = -1
ret["server_player_max"] = -1
ret["server_uuid"] = None
ret["server_game_mode"] = None
ret["server_game_mode_num"] = 0
ret["server_port_ipv4"] = ""
ret["server_port_ipv6"] = ""
except:
raise ValueError(f"Received unexpected Raknet response: {sliced}")
return ret
raise ValueError(f"Incorrect packet type ({data[0]} detected")

View File

@ -153,6 +153,9 @@ class ServerJars:
def _get_server_type_list(self):
url = "/api/fetchTypes/"
response = self._get_api_result(url)
if "bedrock" in response.keys():
# remove pocketmine from options
del response["bedrock"]
return response
def download_jar(self, jar, server, version, path, server_id):

View File

@ -43,6 +43,7 @@ class AuditLog(BaseModel):
# Crafty Settings Class
# **********************************************************************************
class CraftySettings(BaseModel):
id = AutoField()
secret_api_key = CharField(default="")
cookie_secret = CharField(default="")
login_photo = CharField(default="login_1.jpg")
@ -144,7 +145,7 @@ class HelpersManagement:
# Audit_Log Methods
# **********************************************************************************
@staticmethod
def get_actity_log():
def get_activity_log():
query = AuditLog.select()
return DatabaseShortcuts.return_db_rows(query)

View File

@ -45,6 +45,7 @@ class Users(BaseModel):
manager = IntegerField(default=None, null=True)
pfp = CharField(default="/static/assets/images/faces-clipart/pic-3.png")
theme = CharField(default="default")
cleared_notifs = CharField(default="default")
class Meta:
table_name = "users"
@ -171,6 +172,7 @@ class HelperUsers:
"roles": [],
"servers": [],
"support_logs": "",
"cleared_notifs": "",
}
user = model_to_dict(Users.get(Users.user_id == user_id))
@ -386,7 +388,7 @@ class HelperUsers:
@staticmethod
def get_users_from_role(role_id):
UserRoles.select().where(UserRoles.role_id == role_id).execute()
return UserRoles.select().where(UserRoles.role_id == role_id).execute()
# **********************************************************************************
# ApiKeys Methods

View File

@ -92,6 +92,9 @@ class MainPrompt(cmd.Cmd):
self.controller.users.update_user(user_id, {"password": new_pass})
def do_get_users(self, _line):
Console.info(self.controller.users.get_all_usernames())
@staticmethod
def do_threads(_line):
for thread in threading.enumerate():

View File

@ -325,3 +325,12 @@ class FileHelpers:
else:
return "false"
return
def unzip_server(self, zip_path, user_id):
if Helpers.check_file_perms(zip_path):
temp_dir = tempfile.mkdtemp()
with zipfile.ZipFile(zip_path, "r") as zip_ref:
# extracts archive to temp directory
zip_ref.extractall(temp_dir)
if user_id:
return temp_dir

View File

@ -16,6 +16,7 @@ import zipfile
import pathlib
import ctypes
import shutil
import shlex
import subprocess
import itertools
from datetime import datetime
@ -148,6 +149,29 @@ class Helpers:
logger.error(f"Unable to resolve remote bedrock download url! \n{e}")
return False
def get_execution_java(self, value, execution_command):
if self.is_os_windows():
execution_list = shlex.split(execution_command, posix=False)
else:
execution_list = shlex.split(execution_command, posix=True)
if (
not any(value in path for path in self.find_java_installs())
and value != "java"
):
return
if value != "java":
if self.is_os_windows():
execution_list[0] = '"' + value + '/bin/java"'
else:
execution_list[0] = '"' + value + '"'
else:
execution_list[0] = "java"
execution_command = ""
for item in execution_list:
execution_command += item + " "
return execution_command
def detect_java(self):
if len(self.find_java_installs()) > 0:
return True
@ -302,6 +326,16 @@ class Helpers:
except Exception:
return False
@staticmethod
def check_address_status(address):
try:
response = requests.get(address, timeout=2)
return (
response.status_code // 100 == 2
) # Check if the status code starts with 2
except requests.RequestException:
return False
@staticmethod
def check_port(server_port):
try:
@ -474,9 +508,9 @@ class Helpers:
return mounts
def is_subdir(self, server_path, root_dir):
server_path = os.path.realpath(server_path)
root_dir = os.path.realpath(root_dir)
def is_subdir(self, child_path, parent_path):
server_path = os.path.realpath(child_path)
root_dir = os.path.realpath(parent_path)
if self.is_os_windows():
try:
@ -546,20 +580,16 @@ class Helpers:
return version_data
@staticmethod
def get_announcements():
data = (
'[{"id":"1","date":"Unknown",'
'"title":"Error getting Announcements",'
'"desc":"Error getting Announcements","link":""}]'
)
def get_announcements(self):
data = []
try:
response = requests.get("https://craftycontrol.com/notify.json", timeout=2)
response = requests.get("https://craftycontrol.com/notify", timeout=2)
data = json.loads(response.content)
except Exception as e:
logger.error(f"Failed to fetch notifications with error: {e}")
if self.update_available:
data.append(self.update_available)
return data
def get_version_string(self):
@ -1059,87 +1089,6 @@ class Helpers:
return data
def generate_tree(self, folder, output=""):
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
for item in file_list:
if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item)
elif str(item) != self.ignored_names:
unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold
)
for raw_filename in file_list:
filename = html.escape(raw_filename)
rel = os.path.join(folder, raw_filename)
dpath = os.path.join(folder, filename)
if os.path.isdir(rel):
if filename not in self.ignored_names:
output += f"""<li id="{dpath}li" class="tree-item"
data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}"
class="tree-caret tree-ctx-item tree-folder">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}"
data-name="{filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
{filename}
</span>
</div><li>
\n"""
else:
if filename not in self.ignored_names:
output += f"""<li id="{dpath}li"
class="d-block tree-ctx-item tree-file tree-item"
data-path="{dpath}"
data-name="{filename}"
onclick="clickOnFile(event)"><span style="margin-right: 6px;">
<i class="far fa-file"></i></span>{filename}</li>"""
return output
def generate_dir(self, folder, output=""):
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
for item in file_list:
if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item)
elif str(item) != self.ignored_names:
unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold
)
output += f"""<ul class="tree-nested d-block" id="{folder}ul">"""
for raw_filename in file_list:
filename = html.escape(raw_filename)
dpath = os.path.join(folder, filename)
rel = os.path.join(folder, raw_filename)
if os.path.isdir(rel):
if filename not in self.ignored_names:
output += f"""<li id="{dpath}li" class="tree-item"
data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}"
class="tree-caret tree-ctx-item tree-folder">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}"
data-name="{filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
{filename}
</span>
</div><li>"""
else:
if filename not in self.ignored_names:
output += f"""<li id="{dpath}li"
class="d-block tree-ctx-item tree-file tree-item"
data-path="{dpath}"
data-name="{filename}"
onclick="clickOnFile(event)"><span style="margin-right: 6px;">
<i class="far fa-file"></i></span>{filename}</li>"""
output += "</ul>\n"
return output
@staticmethod
def generate_zip_tree(folder, output=""):
file_list = os.listdir(folder)
@ -1183,23 +1132,6 @@ class Helpers:
</input></div><li>"""
return output
def unzip_server(self, zip_path, user_id):
if Helpers.check_file_perms(zip_path):
temp_dir = tempfile.mkdtemp()
with zipfile.ZipFile(zip_path, "r") as zip_ref:
# extracts archive to temp directory
zip_ref.extractall(temp_dir)
if user_id:
self.websocket_helper.broadcast_user(
user_id, "send_temp_path", {"path": temp_dir}
)
def backup_select(self, path, user_id):
if user_id:
self.websocket_helper.broadcast_user(
user_id, "send_temp_path", {"path": path}
)
@staticmethod
def unzip_backup_archive(backup_path, zip_name):
zip_path = os.path.join(backup_path, zip_name)
@ -1211,22 +1143,6 @@ class Helpers:
return temp_dir
return False
@staticmethod
def in_path(parent_path, child_path):
# Smooth out relative path names, note: if you are concerned about
# symbolic links, you should use os.path.realpath too
parent_path = os.path.abspath(parent_path)
child_path = os.path.abspath(child_path)
# Compare the common path of the parent and child path with the
# common path of just the parent path. Using the commonpath method
# on just the parent path will regularise the path name in the same way
# as the comparison that deals with both paths, removing any trailing
# path separator
return os.path.commonpath([parent_path]) == os.path.commonpath(
[parent_path, child_path]
)
@staticmethod
def download_file(executable_url, jar_path):
try:
@ -1261,3 +1177,24 @@ class Helpers:
if region == "EN":
return "en"
return lang + "-" + region
@staticmethod
def get_player_avatar(uuid_player):
mojang_response = requests.get(
f"https://sessionserver.mojang.com/session/minecraft/profile/{uuid_player}",
timeout=10,
)
if mojang_response.status_code == 200:
uuid_profile = mojang_response.json()
profile_properties = uuid_profile["properties"]
for prop in profile_properties:
if prop["name"] == "textures":
decoded_bytes = base64.b64decode(prop["value"])
decoded_str = decoded_bytes.decode("utf-8")
texture_json = json.loads(decoded_str)
skin_url = texture_json["textures"]["SKIN"]["url"]
skin_response = requests.get(skin_url, stream=True, timeout=10)
if skin_response.status_code == 200:
return base64.b64encode(skin_response.content)
else:
return

View File

@ -9,6 +9,8 @@ from app.classes.controllers.server_perms_controller import PermissionsServers
from app.classes.controllers.servers_controller import ServersController
from app.classes.shared.helpers import Helpers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.steamcmd.serverapps import SteamApps
from app.classes.steamcmd.steamcmd import SteamCMD
logger = logging.getLogger(__name__)
@ -19,6 +21,8 @@ class ImportHelpers:
def __init__(self, helper, file_helper):
self.file_helper: FileHelpers = file_helper
self.helper: Helpers = helper
self.steam_apps: SteamApps = SteamApps(helper)
self.steam: SteamCMD()
def import_jar_server(self, server_path, new_server_dir, port, new_id):
import_thread = threading.Thread(
@ -216,6 +220,34 @@ class ImportHelpers:
# deletes temp dir
FileHelpers.del_dirs(temp_dir)
def download_steam_server(self, app_id, server_id, server_dir, server_exe):
download_thread = threading.Thread(
target=self.create_steam_server,
daemon=True,
args=(app_id, server_id, server_dir, server_exe),
name=f"{server_id}_download",
)
download_thread.start()
def create_steam_server(self, app_id, server_id, server_dir, server_exe):
# TODO: what is the server exe called @zedifus
server_exe = "steamcmd.exe"
# Sets the steamCMD install directory for next install.
self.steam = SteamCMD(server_dir)
self.steam.install()
full_jar_path = os.path.join(server_dir, server_exe)
if Helpers.is_os_windows():
server_command = f'"{full_jar_path}"'
else:
server_command = f"./{server_exe}"
logger.debug("command: " + server_command)
ServersController.set_import(server_id)
self.steam.app_update(app_id, "./gamefiles")
ServersController.finish_import(server_id)
def download_bedrock_server(self, path, new_id):
download_thread = threading.Thread(
target=self.download_threaded_bedrock_server,

View File

@ -5,6 +5,7 @@ from datetime import datetime
import platform
import shutil
import time
import json
import logging
import threading
from peewee import DoesNotExist
@ -33,7 +34,6 @@ from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.import_helper import ImportHelpers
from app.classes.minecraft.serverjars import ServerJars
from app.classes.steamcmd.serverapps import SteamApps
from app.classes.steamcmd.steamcmd import SteamCMD
logger = logging.getLogger(__name__)
@ -45,7 +45,6 @@ class Controller:
self.import_helper: ImportHelpers = import_helper
self.server_jars: ServerJars = ServerJars(helper)
self.steam_apps: SteamApps = SteamApps(helper)
self.steam: SteamCMD()
self.users_helper: HelperUsers = HelperUsers(database, self.helper)
self.roles_helper: HelperRoles = HelperRoles(database)
self.servers_helper: HelperServers = HelperServers(database)
@ -88,6 +87,17 @@ class Controller:
def set_project_root(self, root_dir):
self.project_root = root_dir
def set_config_json(self, data):
current_config = self.helper.get_all_settings()
for key in current_config:
if key in data:
current_config[key] = data[key]
keys = list(current_config.keys())
keys.sort()
sorted_data = {i: current_config[i] for i in keys}
with open(self.helper.settings_file, "w", encoding="utf-8") as f:
json.dump(sorted_data, f, indent=4)
def package_support_logs(self, exec_user):
if exec_user["preparing"]:
return
@ -304,15 +314,6 @@ class Controller:
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)
@ -340,22 +341,25 @@ class Controller:
server_file = f"{create_data['type']}-{create_data['version']}.jar"
# 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")
)
if "agree_to_eula" in create_data:
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"])
server_file = create_data["jarfile"]
elif root_create_data["create_type"] == "import_zip":
# TODO: Copy files from the zip file to the new server directory
server_file = create_data["jarfile"]
raise NotImplementedError("Not yet implemented")
_create_server_properties_if_needed(
create_data["server_properties_port"],
)
# self.import_helper.import_java_zip_server()
if data["create_type"] == "minecraft_java":
_create_server_properties_if_needed(
create_data["server_properties_port"],
)
min_mem = create_data["mem_min"]
max_mem = create_data["mem_max"]
@ -368,30 +372,72 @@ class Controller:
def _wrap_jar_if_windows():
return f'"{server_file}"' if Helpers.is_os_windows() else server_file
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"
)
if root_create_data["create_type"] == "download_jar":
if Helpers.is_os_windows():
# Let's check for and setup for install server commands
if create_data["type"] == "forge":
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f'-jar "{server_file}" --installServer'
)
else:
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f'-jar "{server_file}" nogui'
)
else:
if create_data["type"] == "forge":
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f"-jar {server_file} --installServer"
)
else:
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f"-jar {server_file} nogui"
)
else:
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}")
if Helpers.is_os_windows():
server_command = (
f'"{os.path.join(new_server_path, create_data["executable"])}"'
)
else:
server_command = f"./{create_data['executable']}"
logger.debug("command: " + server_command)
server_file = create_data["executable"]
elif root_create_data["create_type"] == "import_zip":
# TODO: Copy files from the zip file to the new server directory
raise NotImplementedError("Not yet implemented")
else:
server_file = "bedrock_server"
if Helpers.is_os_windows():
# if this is windows we will override the linux bedrock server name.
server_file = "bedrock_server.exe"
full_jar_path = os.path.join(new_server_path, server_file)
if self.helper.is_os_windows():
server_command = f'"{full_jar_path}"'
else:
server_command = f"./{server_file}"
_create_server_properties_if_needed(0, True)
server_command = create_data["command"]
server_file = (
"./bedrock_server" # HACK: This is a hack to make the server start
)
server_command = create_data.get("command", server_command)
elif data["create_type"] == "custom":
# TODO: working_directory, executable_update
if root_create_data["create_type"] == "raw_exec":
@ -416,7 +462,13 @@ class Controller:
if server_file_new != "":
# HACK: Horrible hack to make the server start
server_file = server_file_new
elif data["create_type"] == "steam_cmd":
server_file = "steamcmd.exe"
full_jar_path = os.path.join(new_server_path, server_file)
if Helpers.is_os_windows():
server_command = f'"{full_jar_path}"'
else:
server_command = f"./{server_file}"
stop_command = data.get("stop_command", "")
if stop_command == "":
# TODO: different default stop commands for server creation types
@ -433,7 +485,11 @@ class Controller:
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"
monitoring_type = "raknet"
elif data["monitoring_type"] == "steam_cmd":
monitoring_port = data["steam_cmd_monitoring_data"]["port"]
monitoring_host = data["steam_cmd_monitoring_data"]["host"]
monitoring_type = "raknet"
elif data["monitoring_type"] == "none":
# TODO: this needs to be NUKED..
# There shouldn't be anything set if there is nothing to monitor
@ -455,131 +511,94 @@ class Controller:
server_host=monitoring_host,
server_type=monitoring_type,
)
if (
data["create_type"] == "minecraft_java"
and root_create_data["create_type"] == "download_jar"
):
# modded update urls from server jars will only update the installer
if create_data["category"] != "modded":
server_obj = self.servers.get_server_obj(new_server_id)
url = (
f"https://serverjars.com/api/fetchJar/{create_data['category']}"
f"/{create_data['type']}/{create_data['version']}"
if data["create_type"] == "minecraft_java":
if root_create_data["create_type"] == "download_jar":
# modded update urls from server jars will only update the installer
if create_data["category"] != "modded":
server_obj = self.servers.get_server_obj(new_server_id)
url = (
f"https://serverjars.com/api/fetchJar/{create_data['category']}"
f"/{create_data['type']}/{create_data['version']}"
)
server_obj.executable_update_url = url
self.servers.update_server(server_obj)
self.server_jars.download_jar(
create_data["category"],
create_data["type"],
create_data["version"],
full_jar_path,
new_server_id,
)
server_obj.executable_update_url = url
self.servers.update_server(server_obj)
self.server_jars.download_jar(
create_data["category"],
create_data["type"],
create_data["version"],
full_jar_path,
new_server_id,
)
elif root_create_data["create_type"] == "import_server":
ServersController.set_import(new_server_id)
self.import_helper.import_jar_server(
create_data["existing_server_path"],
new_server_path,
monitoring_port,
new_server_id,
)
elif root_create_data["create_type"] == "import_zip":
ServersController.set_import(new_server_id)
elif data["create_type"] == "minecraft_bedrock":
if root_create_data["create_type"] == "download_exe":
ServersController.set_import(new_server_id)
self.import_helper.download_bedrock_server(
new_server_path, new_server_id
)
elif root_create_data["create_type"] == "import_server":
ServersController.set_import(new_server_id)
full_exe_path = os.path.join(new_server_path, create_data["executable"])
self.import_helper.import_bedrock_server(
create_data["existing_server_path"],
new_server_path,
monitoring_port,
full_exe_path,
new_server_id,
)
elif root_create_data["create_type"] == "import_zip":
ServersController.set_import(new_server_id)
full_exe_path = os.path.join(new_server_path, create_data["executable"])
self.import_helper.import_bedrock_zip_server(
create_data["zip_path"],
new_server_path,
os.path.join(create_data["zip_root"], create_data["executable"]),
monitoring_port,
new_server_id,
)
elif data["create_type"] == "steam_cmd":
server_exe = "steamcmd.exe"
if root_create_data["create_type"] == "download_exe":
ServersController.set_import(new_server_id)
self.import_helper.download_steam_server(
create_data["app_id"],
new_server_id,
new_server_path,
server_exe,
)
exec_user = self.users.get_user_by_id(int(user_id))
captured_roles = data.get("roles", [])
# These lines create a new Role for the Server with full permissions
# and add the user to it if he's not a superuser
if len(captured_roles) == 0:
if not exec_user["superuser"]:
new_server_uuid = self.servers.get_server_data_by_id(new_server_id).get(
"server_uuid"
)
role_id = self.roles.add_role(
f"Creator of Server with uuid={new_server_uuid}",
exec_user["user_id"],
)
self.server_perms.add_role_server(new_server_id, role_id, "11111111")
self.users.add_role_to_user(exec_user["user_id"], role_id)
else:
for role in captured_roles:
role_id = role
self.server_perms.add_role_server(new_server_id, role_id, "11111111")
return new_server_id, server_fs_uuid
def create_jar_server(
self,
jar: str,
server: str,
version: str,
name: str,
min_mem: int,
max_mem: int,
port: int,
user_id: int,
):
server_id = Helpers.create_uuid()
server_dir = os.path.join(self.helper.servers_dir, server_id)
backup_path = os.path.join(self.helper.backup_path, server_id)
if Helpers.is_os_windows():
server_dir = Helpers.wtol_path(server_dir)
backup_path = Helpers.wtol_path(backup_path)
server_dir.replace(" ", "^ ")
backup_path.replace(" ", "^ ")
server_file = f"{server}-{version}.jar"
# make the dir - perhaps a UUID?
Helpers.ensure_dir_exists(server_dir)
Helpers.ensure_dir_exists(backup_path)
try:
# do a eula.txt
with open(
os.path.join(server_dir, "eula.txt"), "w", encoding="utf-8"
) as file:
file.write("eula=false")
file.close()
# setup server.properties with the port
with open(
os.path.join(server_dir, "server.properties"), "w", encoding="utf-8"
) as file:
file.write(f"server-port={port}")
file.close()
except Exception as e:
logger.error(f"Unable to create required server files due to :{e}")
return False
if Helpers.is_os_windows():
# Let's check for and setup for install server commands
if server == "forge":
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f'-jar "{server_file}" --installServer'
)
else:
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f'-jar "{server_file}" nogui'
)
else:
if server == "forge":
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f"-jar {server_file} --installServer"
)
else:
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f"-jar {server_file} nogui"
)
server_log_file = "./logs/latest.log"
server_stop = "stop"
new_id = self.register_server(
name,
server_id,
server_dir,
backup_path,
server_command,
server_file,
server_log_file,
server_stop,
port,
user_id,
server_type="minecraft-java",
)
# modded update urls from server jars will only update the installer
if jar != "modded":
server_obj = self.servers.get_server_obj(new_id)
url = f"https://serverjars.com/api/fetchJar/{jar}/{server}/{version}"
server_obj.executable_update_url = url
self.servers.update_server(server_obj)
# download the jar
self.server_jars.download_jar(
jar, server, version, os.path.join(server_dir, server_file), new_id
)
return new_id
@staticmethod
def verify_jar_server(server_path: str, server_jar: str):
server_path = Helpers.get_os_understandable_path(server_path)
@ -597,123 +616,6 @@ class Controller:
return False
return True
def import_jar_server(
self,
server_name: str,
server_path: str,
server_jar: str,
min_mem: int,
max_mem: int,
port: int,
user_id: int,
):
server_id = Helpers.create_uuid()
new_server_dir = os.path.join(self.helper.servers_dir, server_id)
backup_path = os.path.join(self.helper.backup_path, server_id)
if Helpers.is_os_windows():
new_server_dir = Helpers.wtol_path(new_server_dir)
backup_path = Helpers.wtol_path(backup_path)
new_server_dir.replace(" ", "^ ")
backup_path.replace(" ", "^ ")
Helpers.ensure_dir_exists(new_server_dir)
Helpers.ensure_dir_exists(backup_path)
server_path = Helpers.get_os_understandable_path(server_path)
full_jar_path = os.path.join(new_server_dir, server_jar)
if Helpers.is_os_windows():
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f'-jar "{full_jar_path}" nogui'
)
else:
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f"-jar {full_jar_path} nogui"
)
server_log_file = "./logs/latest.log"
server_stop = "stop"
new_id = self.register_server(
server_name,
server_id,
new_server_dir,
backup_path,
server_command,
server_jar,
server_log_file,
server_stop,
port,
user_id,
server_type="minecraft-java",
)
ServersController.set_import(new_id)
self.import_helper.import_jar_server(server_path, new_server_dir, port, new_id)
return new_id
def import_zip_server(
self,
server_name: str,
zip_path: str,
server_jar: str,
min_mem: int,
max_mem: int,
port: int,
user_id: int,
):
server_id = Helpers.create_uuid()
new_server_dir = os.path.join(self.helper.servers_dir, server_id)
backup_path = os.path.join(self.helper.backup_path, server_id)
if Helpers.is_os_windows():
new_server_dir = Helpers.wtol_path(new_server_dir)
backup_path = Helpers.wtol_path(backup_path)
new_server_dir.replace(" ", "^ ")
backup_path.replace(" ", "^ ")
temp_dir = Helpers.get_os_understandable_path(zip_path)
Helpers.ensure_dir_exists(new_server_dir)
Helpers.ensure_dir_exists(backup_path)
full_jar_path = os.path.join(new_server_dir, server_jar)
if Helpers.is_os_windows():
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f'-jar "{full_jar_path}" nogui'
)
else:
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f"-jar {full_jar_path} nogui"
)
logger.debug("command: " + server_command)
server_log_file = "./logs/latest.log"
server_stop = "stop"
new_id = self.register_server(
server_name,
server_id,
new_server_dir,
backup_path,
server_command,
server_jar,
server_log_file,
server_stop,
port,
user_id,
server_type="minecraft-java",
)
ServersController.set_import(new_id)
self.import_helper.import_java_zip_server(
temp_dir, new_server_dir, port, new_id
)
return new_id
# **********************************************************************************
# BEDROCK IMPORTS
# **********************************************************************************
@ -768,54 +670,6 @@ class Controller:
)
return new_id
def create_steam_server(self, app_id, server_name, user_id):
server_id = Helpers.create_uuid()
new_server_dir = os.path.join(self.helper.servers_dir, server_id)
backup_path = os.path.join(self.helper.backup_path, server_id)
# TODO: what is the server exe called @zedifus
server_exe = "steamcmd.exe"
if Helpers.is_os_windows():
new_server_dir = Helpers.wtol_path(new_server_dir)
backup_path = Helpers.wtol_path(backup_path)
new_server_dir.replace(" ", "^ ")
backup_path.replace(" ", "^ ")
Helpers.ensure_dir_exists(new_server_dir)
Helpers.ensure_dir_exists(backup_path)
# Sets the steamCMD install directory for next install.
self.steam = SteamCMD(new_server_dir)
self.steam.install()
full_jar_path = os.path.join(new_server_dir, server_exe)
if Helpers.is_os_windows():
server_command = f'"{full_jar_path}"'
else:
server_command = f"./{server_exe}"
logger.debug("command: " + server_command)
server_log_file = "bootstrap_log.txt"
server_stop = "stop"
new_id = self.register_server(
server_name,
server_id,
new_server_dir,
backup_path,
server_command,
server_exe,
server_log_file,
server_stop,
2456,
user_id,
server_type="steam",
app_id=app_id,
)
ServersController.set_import(new_id)
self.steam.app_update(app_id, "./gamefiles")
ServersController.finish_import(new_id)
return new_id
def create_bedrock_server(self, server_name, user_id):
server_id = Helpers.create_uuid()
new_server_dir = os.path.join(self.helper.servers_dir, server_id)
@ -1120,6 +974,8 @@ class Controller:
"the new directory."
},
)
self.helper.dir_migration = False
return
# set the cached serve dir
self.helper.servers_dir = new_server_path

View File

@ -11,6 +11,7 @@ import subprocess
import html
import urllib.request
import glob
import json
from zoneinfo import ZoneInfo
@ -132,6 +133,15 @@ class ServerInstance:
self.server_object = HelperServers.get_server_obj(self.server_id)
self.stats_helper = HelperServerStats(self.server_id)
self.last_backup_failed = False
try:
with open(
os.path.join(self.server_object.path, "db_stats", "players_cache.json"),
"r",
encoding="utf-8",
) as f:
self.player_cache = list(json.load(f).values())
except:
self.player_cache = []
try:
self.tz = get_localzone()
except ZoneInfoNotFoundError as e:
@ -460,7 +470,47 @@ class ServerInstance:
# STEAM SERVERS
# ***********************************************
# ***********************************************
elif HelperServers.get_server_type_by_id(self.server_id) == "steam":
elif HelperServers.get_server_type_by_id(self.server_id) == "raknet":
my_env = os.environ
env_mod = False
with open(
self.server_path + "/env.json",
) as env_file:
env_file_data = json.load(env_file)
for key, value in env_file_data.items():
if "path" in key.lower():
items_validated = []
for item in value["contents"]:
try:
p = Helpers.validate_traversal(self.server_path, item)
except ValueError:
logger.warning(
"Path traversal detected on server {self.server_id} for env {k} value {i}, skipping"
)
p = str(p).replace(":", "\:")
items_validated.append(p)
if my_env.get(key, None):
if value["mode"] == "append":
items_validated.insert(0, my_env[key])
elif value["mode"] == "prepend":
items_validated.append(my_env[key])
my_env[key] = ":".join(items_validated)
else:
items = value["contents"]
if value["mode"] == "append":
items.insert(0, my_env[key])
elif value["mode"] == "prepend":
items.append(my_env[key])
my_env[key] = ",".join(items)
env_mod = True
if env_mod:
logger.debug(
f"Launching process for server {self.server_id} with modified environment {my_env}"
)
else:
logger.debug(
f"Launching process for server {self.server_id} with un-modified environment"
)
try:
self.process = subprocess.Popen(
self.server_command,
@ -468,6 +518,7 @@ class ServerInstance:
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=my_env,
)
except Exception as ex:
logger.error(
@ -486,6 +537,9 @@ class ServerInstance:
return
else:
logger.debug(
f"Starting server {self.server_id} with unknown type {HelperServers.get_server_type_by_id(self.server_id)}"
)
try:
self.process = subprocess.Popen(
self.server_command,
@ -799,6 +853,7 @@ class ServerInstance:
)
if self.settings["stop_command"]:
self.send_command(self.settings["stop_command"])
self.write_player_cache()
else:
# windows will need to be handled separately for Ctrl+C
self.process.terminate()
@ -1247,6 +1302,40 @@ class ServerInstance:
)
update_thread.start()
def write_player_cache(self):
write_json = {}
for item in self.player_cache:
write_json[item["name"]] = item
with open(
os.path.join(self.server_path, "db_stats", "players_cache.json"),
"w",
encoding="utf-8",
) as f:
f.write(json.dumps(write_json, indent=4))
logger.info("Cache file refreshed")
def cache_players(self):
server_players = self.get_server_players()
for p in self.player_cache[:]:
if p["status"] == "Online" and p["name"] not in server_players:
p["status"] = "Offline"
p["last_seen"] = datetime.datetime.now().strftime("%d/%m/%Y %H:%M")
elif p["name"] in server_players:
self.player_cache.remove(p)
for player in server_players:
if player == "Anonymous Player":
# Skip Anonymous Player
continue
if player in self.player_cache:
self.player_cache.remove(player)
self.player_cache.append(
{
"name": player,
"status": "Online",
"last_seen": datetime.datetime.now().strftime("%d/%m/%Y %H:%M"),
}
)
def check_update(self):
return self.stats_helper.get_server_stats()["updating"]
@ -1433,6 +1522,12 @@ class ServerInstance:
minutes=self.helper.get_setting("dir_size_poll_freq_minutes"),
id=str(self.server_id) + "_dir_poll",
)
self.dir_scheduler.add_job(
self.cache_players,
"interval",
seconds=5,
id=str(self.server_id) + "_players_poll",
)
def calc_dir_size(self):
server_dt = HelperServers.get_server_data_by_id(self.server_id)
@ -1502,6 +1597,7 @@ class ServerInstance:
"created": datetime.datetime.now().strftime(
"%Y/%m/%d, %H:%M:%S"
),
"players_cache": self.player_cache,
},
)
total_players += int(raw_ping_result.get("online"))
@ -1543,7 +1639,10 @@ class ServerInstance:
server_name = server.get("server_name", f"ID#{server_id}")
logger.debug(f"Pinging server '{server}' on {internal_ip}:{server_port}")
if HelperServers.get_server_type_by_id(server_id) == "minecraft-bedrock":
if (
HelperServers.get_server_type_by_id(server_id) == "minecraft-bedrock"
or HelperServers.get_server_type_by_id(server_id) == "raknet"
):
int_mc_ping = ping_raknet(internal_ip, int(server_port))
else:
try:
@ -1557,10 +1656,10 @@ class ServerInstance:
# if we got a good ping return, let's parse it
if int_mc_ping:
int_data = True
if HelperServers.get_server_type_by_id(
server["server_id"]
) == "minecraft-bedrock" or HelperServers.get_server_type_by_id(
server["server_id"] == "steam"
if (
HelperServers.get_server_type_by_id(server["server_id"])
== "minecraft-bedrock"
or HelperServers.get_server_type_by_id(server["server_id"]) == "raknet"
):
ping_data = Stats.parse_server_raknet_ping(int_mc_ping)
else:
@ -1670,7 +1769,10 @@ class ServerInstance:
server_port = server_dt["server_port"]
logger.debug(f"Pinging server '{self.name}' on {internal_ip}:{server_port}")
if HelperServers.get_server_type_by_id(server_id) == "minecraft-bedrock":
if (
HelperServers.get_server_type_by_id(server_id) == "minecraft-bedrock"
or HelperServers.get_server_type_by_id(server_id) == "raknet"
):
int_mc_ping = ping_raknet(internal_ip, int(server_port))
else:
int_mc_ping = ping(internal_ip, int(server_port))

View File

@ -41,10 +41,10 @@ scheduler_intervals = {
class TasksManager:
controller: Controller
def __init__(self, helper, controller):
def __init__(self, helper, controller, file_helper):
self.helper: Helpers = helper
self.controller: Controller = controller
self.tornado: Webserver = Webserver(helper, controller, self)
self.tornado: Webserver = Webserver(helper, controller, self, file_helper)
try:
self.tz = get_localzone()
except ZoneInfoNotFoundError as e:
@ -421,6 +421,7 @@ class TasksManager:
)
for item in jobs:
logger.info(f"JOB: {item}")
return task.schedule_id
def remove_all_server_tasks(self, server_id):
schedules = HelpersManagement.get_schedules_by_server(server_id)
@ -450,7 +451,6 @@ class TasksManager:
# created task a child of itself.
if str(job_data.get("parent")) == str(sch_id):
job_data["parent"] = None
HelpersManagement.update_scheduled_task(sch_id, job_data)
if not (
@ -738,12 +738,21 @@ class TasksManager:
def check_for_updates(self):
logger.info("Checking for Crafty updates...")
self.helper.update_available = self.helper.check_remote_version()
remote = self.helper.update_available
if self.helper.update_available:
logger.info(f"Found new version {self.helper.update_available}")
else:
logger.info(
"No updates found! You are on the most up to date Crafty version."
)
if self.helper.update_available:
self.helper.update_available = {
"id": str(remote),
"title": f"{remote} Update Available",
"date": "",
"desc": "Release notes are available by clicking this notification.",
"link": "https://gitlab.com/crafty-controller/crafty-4/-/releases",
}
logger.info("Refreshing Gravatar PFPs...")
for user in HelperUsers.get_all_users():
if user.email:
@ -775,31 +784,37 @@ class TasksManager:
def check_for_old_logs(self):
# check for server logs first
self.controller.servers.check_for_old_logs()
# check for crafty logs now
logs_path = os.path.join(self.controller.project_root, "logs")
logs_delete_after = int(
self.helper.get_setting("crafty_logs_delete_after_days")
)
latest_log_files = [
"session.log",
"schedule.log",
"tornado-access.log",
"session.log",
"commander.log",
]
# we won't delete if delete logs after is set to 0
if logs_delete_after != 0:
log_files = list(
filter(
lambda val: val not in latest_log_files,
os.listdir(logs_path),
)
try:
# check for crafty logs now
logs_path = os.path.join(self.controller.project_root, "logs")
logs_delete_after = int(
self.helper.get_setting("crafty_logs_delete_after_days")
)
latest_log_files = [
"session.log",
"schedule.log",
"tornado-access.log",
"session.log",
"commander.log",
]
# we won't delete if delete logs after is set to 0
if logs_delete_after != 0:
log_files = list(
filter(
lambda val: val not in latest_log_files,
os.listdir(logs_path),
)
)
for log_file in log_files:
log_file_path = os.path.join(logs_path, log_file)
if Helpers.check_file_exists(
log_file_path
) and Helpers.is_file_older_than_x_days(
log_file_path, logs_delete_after
):
os.remove(log_file_path)
except:
logger.debug(
"Unable to find project root."
" If this issue persists please contact support."
)
for log_file in log_files:
log_file_path = os.path.join(logs_path, log_file)
if Helpers.check_file_exists(
log_file_path
) and Helpers.is_file_older_than_x_days(
log_file_path, logs_delete_after
):
os.remove(log_file_path)

View File

@ -1,769 +0,0 @@
import os
import html
import pathlib
import re
import logging
import time
import urllib.parse
import bleach
import tornado.web
import tornado.escape
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.shared.console import Console
from app.classes.shared.helpers import Helpers
from app.classes.shared.server import ServerOutBuf
from app.classes.web.base_handler import BaseHandler
logger = logging.getLogger(__name__)
class AjaxHandler(BaseHandler):
def render_page(self, template, page_data):
self.render(
template,
data=page_data,
translate=self.translator.translate,
)
@tornado.web.authenticated
def get(self, page):
_, _, exec_user = self.current_user
error = bleach.clean(self.get_argument("error", "WTF Error!"))
template = "panel/denied.html"
page_data = {"user_data": exec_user, "error": error}
if page == "error":
template = "public/error.html"
self.render_page(template, page_data)
elif page == "server_log":
server_id = self.get_argument("id", None)
full_log = self.get_argument("full", False)
if server_id is None:
logger.warning("Server ID not found in server_log ajax call")
self.redirect("/panel/error?error=Server ID Not Found")
return
server_id = bleach.clean(server_id)
server_data = self.controller.servers.get_server_data_by_id(server_id)
if not server_data:
logger.warning("Server Data not found in server_log ajax call")
self.redirect("/panel/error?error=Server ID Not Found")
return
if not server_data["log_path"]:
logger.warning(
f"Log path not found in server_log ajax call ({server_id})"
)
if full_log:
log_lines = self.helper.get_setting("max_log_lines")
data = Helpers.tail_file(
# If the log path is absolute it returns it as is
# If it is relative it joins the paths below like normal
pathlib.Path(server_data["path"], server_data["log_path"]),
log_lines,
)
else:
data = ServerOutBuf.lines.get(server_id, [])
for line in data:
try:
line = re.sub("(\033\\[(0;)?[0-9]*[A-z]?(;[0-9])?m?)", "", line)
line = re.sub("[A-z]{2}\b\b", "", line)
line = self.helper.log_colors(html.escape(line))
self.write(f"<span class='box'>{line}<br /></span>")
# self.write(d.encode("utf-8"))
except Exception as e:
logger.warning(f"Skipping Log Line due to error: {e}")
elif page == "announcements":
data = Helpers.get_announcements()
page_data["notify_data"] = data
self.render_page("ajax/notify.html", page_data)
elif page == "get_zip_tree":
path = self.get_argument("path", None)
self.write(
Helpers.get_os_understandable_path(path)
+ "\n"
+ Helpers.generate_zip_tree(path)
)
self.finish()
elif page == "get_zip_dir":
path = self.get_argument("path", None)
self.write(
Helpers.get_os_understandable_path(path)
+ "\n"
+ Helpers.generate_zip_dir(path)
)
self.finish()
elif page == "get_backup_tree":
server_id = self.get_argument("id", None)
folder = self.get_argument("path", None)
output = ""
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
for item in file_list:
if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item)
else:
unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold
)
output += f"""<ul class="tree-nested d-block" id="{folder}ul">"""
for raw_filename in file_list:
filename = html.escape(raw_filename)
rel = os.path.join(folder, raw_filename)
dpath = os.path.join(folder, filename)
if str(dpath) in self.controller.management.get_excluded_backup_dirs(
server_id
):
if os.path.isdir(rel):
output += f"""<li class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" class="checkBoxClass" name="root_path" value="{dpath}" checked>
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
<strong>{filename}</strong>
</span>
</input></div><li>
\n"""
else:
output += f"""<li
class="d-block tree-ctx-item tree-file"
data-path="{dpath}"
data-name="{filename}"
onclick=""><input type='checkbox' class="checkBoxClass" name='root_path' value="{dpath}" checked><span style="margin-right: 6px;">
<i class="far fa-file"></i></span></input>{filename}</li>"""
else:
if os.path.isdir(rel):
output += f"""<li class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" class="checkBoxClass" name="root_path" value="{dpath}">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
<strong>{filename}</strong>
</span>
</input></div><li>
\n"""
else:
output += f"""<li
class="d-block tree-ctx-item tree-file"
data-path="{dpath}"
data-name="{filename}"
onclick=""><input type='checkbox' class="checkBoxClass" name='root_path' value="{dpath}">
<span style="margin-right: 6px;"><i class="far fa-file">
</i></span></input>{filename}</li>"""
self.write(Helpers.get_os_understandable_path(folder) + "\n" + output)
self.finish()
elif page == "get_backup_dir":
server_id = self.get_argument("id", None)
folder = self.get_argument("path", None)
output = ""
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
for item in file_list:
if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item)
else:
unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold
)
output += f"""<ul class="tree-nested d-block" id="{folder}ul">"""
for raw_filename in file_list:
filename = html.escape(raw_filename)
rel = os.path.join(folder, raw_filename)
dpath = os.path.join(folder, filename)
if str(dpath) in self.controller.management.get_excluded_backup_dirs(
server_id
):
if os.path.isdir(rel):
output += f"""<li class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" name="root_path" value="{dpath}" checked>
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
<i class="far fa-folder"></i>
<i class="far fa-folder-open"></i>
<strong>{filename}</strong>
</span>
</input></div><li>"""
else:
output += f"""<li
class="tree-item tree-nested d-block tree-ctx-item tree-file"
data-path="{dpath}"
data-name="{filename}"
onclick=""><input type='checkbox' name='root_path' value='{dpath}' checked><span style="margin-right: 6px;">
<i class="far fa-file"></i></span></input>{filename}</li>"""
else:
if os.path.isdir(rel):
output += f"""<li class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" name="root_path" value="{dpath}">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
<i class="far fa-folder"></i>
<i class="far fa-folder-open"></i>
<strong>{filename}</strong>
</span>
</input></div><li>"""
else:
output += f"""<li
class="tree-item tree-nested d-block tree-ctx-item tree-file"
data-path="{dpath}"
data-name="{filename}"
onclick=""><input type='checkbox' name='root_path' value='{dpath}'>
<span style="margin-right: 6px;"><i class="far fa-file">
</i></span></input>{filename}</li>"""
self.write(Helpers.get_os_understandable_path(folder) + "\n" + output)
self.finish()
elif page == "get_dir":
server_id = self.get_argument("id", None)
path = self.get_argument("path", None)
if not self.check_server_id(server_id, "get_tree"):
return
server_id = bleach.clean(server_id)
if Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"], path
):
self.write(
Helpers.get_os_understandable_path(path)
+ "\n"
+ Helpers.generate_dir(path)
)
self.finish()
@tornado.web.authenticated
def post(self, page):
api_key, _, exec_user = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument("id", None)
permissions = {
"Commands": EnumPermissionsServer.COMMANDS,
"Terminal": EnumPermissionsServer.TERMINAL,
"Logs": EnumPermissionsServer.LOGS,
"Schedule": EnumPermissionsServer.SCHEDULE,
"Backup": EnumPermissionsServer.BACKUP,
"Files": EnumPermissionsServer.FILES,
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
if page == "send_command":
command = self.get_body_argument("command", default=None, strip=True)
server_id = self.get_argument("id", None)
if server_id is None:
logger.warning("Server ID not found in send_command ajax call")
Console.warning("Server ID not found in send_command ajax call")
srv_obj = self.controller.servers.get_server_instance_by_id(server_id)
if command == srv_obj.settings["stop_command"]:
logger.info(
"Stop command detected as terminal input - intercepting."
+ f"Starting Crafty's stop process for server with id: {server_id}"
)
self.controller.management.send_command(
exec_user["user_id"], server_id, self.get_remote_ip(), "stop_server"
)
command = None
elif command == "restart":
logger.info(
"Restart command detected as terminal input - intercepting."
+ f"Starting Crafty's stop process for server with id: {server_id}"
)
self.controller.management.send_command(
exec_user["user_id"],
server_id,
self.get_remote_ip(),
"restart_server",
)
command = None
if command:
if srv_obj.check_running():
srv_obj.send_command(command)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Sent command to "
f"{self.controller.servers.get_server_friendly_name(server_id)} "
f"terminal: {command}",
server_id,
self.get_remote_ip(),
)
elif page == "send_order":
self.controller.users.update_server_order(
exec_user["user_id"], bleach.clean(self.get_argument("order"))
)
return
elif page == "backup_now":
server_id = self.get_argument("id", None)
if server_id is None:
logger.error("Server ID is none. Canceling backup!")
return
server = self.controller.servers.get_server_instance_by_id(server_id)
self.controller.management.add_to_audit_log_raw(
self.controller.users.get_user_by_id(exec_user["user_id"])["username"],
exec_user["user_id"],
server_id,
f"Backup now executed for server {server_id} ",
source_ip=self.get_remote_ip(),
)
server.backup_server()
elif page == "select_photo":
if exec_user["superuser"]:
photo = urllib.parse.unquote(self.get_argument("photo", ""))
opacity = self.get_argument("opacity", 100)
self.controller.management.set_login_opacity(int(opacity))
if photo == "login_1.jpg":
self.controller.management.set_login_image("login_1.jpg")
self.controller.cached_login = f"{photo}"
else:
self.controller.management.set_login_image(f"custom/{photo}")
self.controller.cached_login = f"custom/{photo}"
return
elif page == "delete_photo":
if exec_user["superuser"]:
photo = urllib.parse.unquote(self.get_argument("photo", None))
if photo and photo != "login_1.jpg":
os.remove(
os.path.join(
self.controller.project_root,
f"app/frontend/static/assets/images/auth/custom/{photo}",
)
)
current = self.controller.cached_login
split = current.split("/")
if len(split) == 1:
current_photo = current
else:
current_photo = split[1]
if current_photo == photo:
self.controller.management.set_login_image("login_1.jpg")
self.controller.cached_login = "login_1.jpg"
return
elif page == "kill":
if not permissions["Commands"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Commands")
return
server_id = self.get_argument("id", None)
svr = self.controller.servers.get_server_instance_by_id(server_id)
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}"
)
return
elif page == "eula":
server_id = self.get_argument("id", None)
svr = self.controller.servers.get_server_instance_by_id(server_id)
svr.agree_eula(exec_user["user_id"])
elif page == "restore_backup":
if not permissions["Backup"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Backups")
return
server_id = bleach.clean(self.get_argument("id", None))
zip_name = bleach.clean(self.get_argument("zip_file", None))
svr_obj = self.controller.servers.get_server_obj(server_id)
server_data = self.controller.servers.get_server_data_by_id(server_id)
# import the server again based on zipfile
if server_data["type"] == "minecraft-java":
backup_path = svr_obj.backup_path
if Helpers.validate_traversal(backup_path, zip_name):
temp_dir = Helpers.unzip_backup_archive(backup_path, zip_name)
new_server = self.controller.import_zip_server(
svr_obj.server_name,
temp_dir,
server_data["executable"],
"1",
"2",
server_data["server_port"],
server_data["created_by"],
)
new_server_id = new_server
new_server = self.controller.servers.get_server_data(new_server)
self.controller.rename_backup_dir(
server_id, new_server_id, new_server["server_uuid"]
)
# preserve current schedules
for schedule in self.controller.management.get_schedules_by_server(
server_id
):
self.tasks_manager.update_job(
schedule.schedule_id, {"server_id": new_server_id}
)
# preserve execution command
new_server_obj = self.controller.servers.get_server_obj(
new_server_id
)
new_server_obj.execution_command = server_data["execution_command"]
self.controller.servers.update_server(new_server_obj)
# preserve backup config
backup_config = self.controller.management.get_backup_config(
server_id
)
excluded_dirs = []
server_obj = self.controller.servers.get_server_obj(server_id)
loop_backup_path = self.helper.wtol_path(server_obj.path)
for item in self.controller.management.get_excluded_backup_dirs(
server_id
):
item_path = self.helper.wtol_path(item)
bu_path = os.path.relpath(item_path, loop_backup_path)
bu_path = os.path.join(new_server_obj.path, bu_path)
excluded_dirs.append(bu_path)
self.controller.management.set_backup_config(
new_server_id,
new_server_obj.backup_path,
backup_config["max_backups"],
excluded_dirs,
backup_config["compress"],
backup_config["shutdown"],
)
# remove old server's tasks
try:
self.tasks_manager.remove_all_server_tasks(server_id)
except:
logger.info("No active tasks found for server")
self.controller.remove_server(server_id, True)
self.redirect("/panel/dashboard")
else:
backup_path = svr_obj.backup_path
if Helpers.validate_traversal(backup_path, zip_name):
temp_dir = Helpers.unzip_backup_archive(backup_path, zip_name)
new_server = self.controller.import_bedrock_zip_server(
svr_obj.server_name,
temp_dir,
server_data["executable"],
server_data["server_port"],
server_data["created_by"],
)
new_server_id = new_server
new_server = self.controller.servers.get_server_data(new_server)
self.controller.rename_backup_dir(
server_id, new_server_id, new_server["server_uuid"]
)
# preserve current schedules
for schedule in self.controller.management.get_schedules_by_server(
server_id
):
self.tasks_manager.update_job(
schedule.schedule_id, {"server_id": new_server_id}
)
# preserve execution command
new_server_obj = self.controller.servers.get_server_obj(
new_server_id
)
new_server_obj.execution_command = server_data["execution_command"]
self.controller.servers.update_server(new_server_obj)
# preserve backup config
backup_config = self.controller.management.get_backup_config(
server_id
)
excluded_dirs = []
server_obj = self.controller.servers.get_server_obj(server_id)
loop_backup_path = self.helper.wtol_path(server_obj.path)
for item in self.controller.management.get_excluded_backup_dirs(
server_id
):
item_path = self.helper.wtol_path(item)
bu_path = os.path.relpath(item_path, loop_backup_path)
bu_path = os.path.join(new_server_obj.path, bu_path)
excluded_dirs.append(bu_path)
self.controller.management.set_backup_config(
new_server_id,
new_server_obj.backup_path,
backup_config["max_backups"],
excluded_dirs,
backup_config["compress"],
backup_config["shutdown"],
)
try:
self.tasks_manager.remove_all_server_tasks(server_id)
except:
logger.info("No active tasks found for server")
self.controller.remove_server(server_id, True)
self.redirect("/panel/dashboard")
elif page == "unzip_server":
path = urllib.parse.unquote(self.get_argument("path", ""))
if not path:
path = os.path.join(
self.controller.project_root,
"imports",
urllib.parse.unquote(self.get_argument("file", "")),
)
if Helpers.check_file_exists(path):
self.helper.unzip_server(path, exec_user["user_id"])
else:
user_id = exec_user["user_id"]
if user_id:
time.sleep(5)
user_lang = self.controller.users.get_user_lang_by_id(user_id)
self.helper.websocket_helper.broadcast_user(
user_id,
"send_start_error",
{
"error": self.helper.translation.translate(
"error", "no-file", user_lang
)
},
)
return
elif page == "backup_select":
path = self.get_argument("path", None)
self.helper.backup_select(path, exec_user["user_id"])
return
elif page == "jar_cache":
if not superuser:
self.redirect("/panel/error?error=Not a super user")
return
self.controller.server_jars.manual_refresh_cache()
return
elif page == "update_server_dir":
if self.helper.dir_migration:
return
for server in self.controller.servers.get_all_servers_stats():
if server["stats"]["running"]:
self.helper.websocket_helper.broadcast_user(
exec_user["user_id"],
"send_start_error",
{
"error": "You must stop all servers before "
"starting a storage migration."
},
)
return
if not superuser:
self.redirect("/panel/error?error=Not a super user")
return
if self.helper.is_env_docker():
self.redirect(
"/panel/error?error=This feature is not"
" supported on docker environments"
)
return
new_dir = urllib.parse.unquote(self.get_argument("server_dir"))
self.controller.update_master_server_dir(new_dir, exec_user["user_id"])
return
@tornado.web.authenticated
def delete(self, page):
api_key, _, exec_user = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument("id", None)
permissions = {
"Commands": EnumPermissionsServer.COMMANDS,
"Terminal": EnumPermissionsServer.TERMINAL,
"Logs": EnumPermissionsServer.LOGS,
"Schedule": EnumPermissionsServer.SCHEDULE,
"Backup": EnumPermissionsServer.BACKUP,
"Files": EnumPermissionsServer.FILES,
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
if page == "del_task":
if not permissions["Schedule"] in user_perms:
self.redirect("/panel/error?error=Unauthorized access to Tasks")
else:
sch_id = self.get_argument("schedule_id", "-404")
self.tasks_manager.remove_job(sch_id)
if page == "del_backup":
if not permissions["Backup"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Backups")
return
file_path = Helpers.get_os_understandable_path(
self.get_body_argument("file_path", default=None, strip=True)
)
server_id = self.get_argument("id", None)
Console.warning(f"Delete {file_path} for server {server_id}")
if not self.check_server_id(server_id, "del_backup"):
return
server_id = bleach.clean(server_id)
server_info = self.controller.servers.get_server_data_by_id(server_id)
if not (
Helpers.in_path(
Helpers.get_os_understandable_path(server_info["path"]), file_path
)
or Helpers.in_path(
Helpers.get_os_understandable_path(server_info["backup_path"]),
file_path,
)
) or not Helpers.check_file_exists(os.path.abspath(file_path)):
logger.warning(f"Invalid path in del_backup ajax call ({file_path})")
Console.warning(f"Invalid path in del_backup ajax call ({file_path})")
return
# Delete the file
if Helpers.validate_traversal(
Helpers.get_os_understandable_path(server_info["backup_path"]),
file_path,
):
os.remove(file_path)
elif page == "delete_server":
if not permissions["Config"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Config")
return
server_id = self.get_argument("id", None)
logger.info(
f"Removing server from panel for server: "
f"{self.controller.servers.get_server_friendly_name(server_id)}"
)
server_data = self.controller.servers.get_server_data(server_id)
server_name = server_data["server_name"]
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Deleted server {server_id} named {server_name}",
server_id,
self.get_remote_ip(),
)
self.tasks_manager.remove_all_server_tasks(server_id)
self.controller.remove_server(server_id, False)
elif page == "delete_server_files":
if not permissions["Config"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Config")
return
server_id = self.get_argument("id", None)
logger.info(
f"Removing server and all associated files for server: "
f"{self.controller.servers.get_server_friendly_name(server_id)}"
)
server_data = self.controller.servers.get_server_data(server_id)
server_name = server_data["server_name"]
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Deleted server {server_id} named {server_name}",
server_id,
self.get_remote_ip(),
)
for server in self.controller.servers.failed_servers:
if server["server_id"] == int(server_id):
return
self.tasks_manager.remove_all_server_tasks(server_id)
self.controller.remove_server(server_id, True)
elif page == "delete_unloaded_server":
if not permissions["Config"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Config")
return
server_id = self.get_argument("id", None)
logger.info(
f"Removing server and all associated files for server: "
f"{self.controller.servers.get_server_friendly_name(server_id)}"
)
server_data = self.controller.servers.get_server_data_by_id(server_id)
server_name = server_data["server_name"]
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Deleted server {server_id} named {server_name}",
server_id,
self.get_remote_ip(),
)
self.tasks_manager.remove_all_server_tasks(server_id)
for item in self.controller.servers.failed_servers[:]:
if item["server_id"] == int(server_id):
self.controller.servers.failed_servers.remove(item)
self.controller.remove_unloaded_server(server_id)
def check_server_id(self, server_id, page_name):
if server_id is None:
logger.warning(
f"Server ID not defined in {page_name} ajax call ({server_id})"
)
Console.warning(
f"Server ID not defined in {page_name} ajax call ({server_id})"
)
return
server_id = bleach.clean(server_id)
# does this server id exist?
if not self.controller.servers.server_id_exists(server_id):
logger.warning(
f"Server ID not found in {page_name} ajax call ({server_id})"
)
Console.warning(
f"Server ID not found in {page_name} ajax call ({server_id})"
)
return
return True

View File

@ -8,6 +8,7 @@ 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.file_helpers import FileHelpers
from app.classes.shared.main_controller import Controller
from app.classes.shared.translation import Translation
from app.classes.models.management import DatabaseShortcuts
@ -24,15 +25,22 @@ class BaseHandler(tornado.web.RequestHandler):
helper: Helpers
controller: Controller
translator: Translation
file_helper: FileHelpers
# noinspection PyAttributeOutsideInit
def initialize(
self, helper=None, controller=None, tasks_manager=None, translator=None
self,
helper=None,
controller=None,
tasks_manager=None,
translator=None,
file_helper=None,
):
self.helper = helper
self.controller = controller
self.tasks_manager = tasks_manager
self.translator = translator
self.file_helper = file_helper
def set_default_headers(self) -> None:
"""

View File

@ -1,518 +0,0 @@
import os
import logging
import bleach
import tornado.web
import tornado.escape
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.shared.console import Console
from app.classes.shared.helpers import Helpers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.web.base_handler import BaseHandler
logger = logging.getLogger(__name__)
class FileHandler(BaseHandler):
def render_page(self, template, page_data):
self.render(
template,
data=page_data,
translate=self.translator.translate,
)
@tornado.web.authenticated
def get(self, page):
api_key, _, exec_user = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument("id", None)
permissions = {
"Commands": EnumPermissionsServer.COMMANDS,
"Terminal": EnumPermissionsServer.TERMINAL,
"Logs": EnumPermissionsServer.LOGS,
"Schedule": EnumPermissionsServer.SCHEDULE,
"Backup": EnumPermissionsServer.BACKUP,
"Files": EnumPermissionsServer.FILES,
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
if page == "get_file":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
file_path = Helpers.get_os_understandable_path(
self.get_argument("file_path", None)
)
if not self.check_server_id(server_id, "get_file"):
return
server_id = bleach.clean(server_id)
if not Helpers.in_path(
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
file_path,
) or not Helpers.check_file_exists(os.path.abspath(file_path)):
logger.warning(
f"Invalid path in get_file file file ajax call ({file_path})"
)
Console.warning(
f"Invalid path in get_file file file ajax call ({file_path})"
)
return
error = None
try:
with open(file_path, encoding="utf-8") as file:
file_contents = file.read()
except UnicodeDecodeError:
file_contents = ""
error = "UnicodeDecodeError"
self.write({"content": file_contents, "error": error})
self.finish()
elif page == "get_tree":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
path = self.get_argument("path", None)
if not self.check_server_id(server_id, "get_tree"):
return
server_id = bleach.clean(server_id)
if Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"], path
):
self.write(
Helpers.get_os_understandable_path(path)
+ "\n"
+ self.helper.generate_tree(path)
)
self.finish()
elif page == "get_dir":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
path = self.get_argument("path", None)
if not self.check_server_id(server_id, "get_tree"):
return
server_id = bleach.clean(server_id)
if Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"], path
):
self.write(
Helpers.get_os_understandable_path(path)
+ "\n"
+ self.helper.generate_dir(path)
)
self.finish()
@tornado.web.authenticated
def post(self, page):
api_key, _, exec_user = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument("id", None)
permissions = {
"Commands": EnumPermissionsServer.COMMANDS,
"Terminal": EnumPermissionsServer.TERMINAL,
"Logs": EnumPermissionsServer.LOGS,
"Schedule": EnumPermissionsServer.SCHEDULE,
"Backup": EnumPermissionsServer.BACKUP,
"Files": EnumPermissionsServer.FILES,
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
if page == "create_file":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
file_parent = Helpers.get_os_understandable_path(
self.get_body_argument("file_parent", default=None, strip=True)
)
file_name = self.get_body_argument("file_name", default=None, strip=True)
file_path = os.path.join(file_parent, file_name)
if not self.check_server_id(server_id, "create_file"):
return
server_id = bleach.clean(server_id)
if not Helpers.in_path(
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
file_path,
) or Helpers.check_file_exists(os.path.abspath(file_path)):
logger.warning(
f"Invalid path in create_file file ajax call ({file_path})"
)
Console.warning(
f"Invalid path in create_file file ajax call ({file_path})"
)
return
# Create the file by opening it
with open(file_path, "w", encoding="utf-8") as file_object:
file_object.close()
elif page == "create_dir":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
dir_parent = Helpers.get_os_understandable_path(
self.get_body_argument("dir_parent", default=None, strip=True)
)
dir_name = self.get_body_argument("dir_name", default=None, strip=True)
dir_path = os.path.join(dir_parent, dir_name)
if not self.check_server_id(server_id, "create_dir"):
return
server_id = bleach.clean(server_id)
if not Helpers.in_path(
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
dir_path,
) or Helpers.check_path_exists(os.path.abspath(dir_path)):
logger.warning(
f"Invalid path in create_dir file ajax call ({dir_path})"
)
Console.warning(
f"Invalid path in create_dir file ajax call ({dir_path})"
)
return
# Create the directory
os.mkdir(dir_path)
elif page == "unzip_file":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
path = Helpers.get_os_understandable_path(self.get_argument("path", None))
if Helpers.is_os_windows():
path = Helpers.wtol_path(path)
FileHelpers.unzip_file(path)
self.redirect(f"/panel/server_detail?id={server_id}&subpage=files")
return
@tornado.web.authenticated
def delete(self, page):
api_key, _, exec_user = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument("id", None)
permissions = {
"Commands": EnumPermissionsServer.COMMANDS,
"Terminal": EnumPermissionsServer.TERMINAL,
"Logs": EnumPermissionsServer.LOGS,
"Schedule": EnumPermissionsServer.SCHEDULE,
"Backup": EnumPermissionsServer.BACKUP,
"Files": EnumPermissionsServer.FILES,
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
if page == "del_file":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
file_path = Helpers.get_os_understandable_path(
self.get_body_argument("file_path", default=None, strip=True)
)
Console.warning(f"Delete {file_path} for server {server_id}")
if not self.check_server_id(server_id, "del_file"):
return
server_id = bleach.clean(server_id)
server_info = self.controller.servers.get_server_data_by_id(server_id)
if not (
Helpers.in_path(
Helpers.get_os_understandable_path(server_info["path"]), file_path
)
or Helpers.in_path(
Helpers.get_os_understandable_path(server_info["backup_path"]),
file_path,
)
) or not Helpers.check_file_exists(os.path.abspath(file_path)):
logger.warning(f"Invalid path in del_file file ajax call ({file_path})")
Console.warning(
f"Invalid path in del_file file ajax call ({file_path})"
)
return
# Delete the file
FileHelpers.del_file(file_path)
elif page == "del_dir":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
dir_path = Helpers.get_os_understandable_path(
self.get_body_argument("dir_path", default=None, strip=True)
)
Console.warning(f"Delete {dir_path} for server {server_id}")
if not self.check_server_id(server_id, "del_dir"):
return
server_id = bleach.clean(server_id)
server_info = self.controller.servers.get_server_data_by_id(server_id)
if not Helpers.in_path(
Helpers.get_os_understandable_path(server_info["path"]), dir_path
) or not Helpers.check_path_exists(os.path.abspath(dir_path)):
logger.warning(f"Invalid path in del_file file ajax call ({dir_path})")
Console.warning(f"Invalid path in del_file file ajax call ({dir_path})")
return
# Delete the directory
# os.rmdir(dir_path) # Would only remove empty directories
if Helpers.validate_traversal(
Helpers.get_os_understandable_path(server_info["path"]), dir_path
):
# Removes also when there are contents
FileHelpers.del_dirs(dir_path)
@tornado.web.authenticated
def put(self, page):
api_key, _, exec_user = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument("id", None)
permissions = {
"Commands": EnumPermissionsServer.COMMANDS,
"Terminal": EnumPermissionsServer.TERMINAL,
"Logs": EnumPermissionsServer.LOGS,
"Schedule": EnumPermissionsServer.SCHEDULE,
"Backup": EnumPermissionsServer.BACKUP,
"Files": EnumPermissionsServer.FILES,
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
if page == "save_file":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
file_contents = self.get_body_argument(
"file_contents", default=None, strip=True
)
file_path = Helpers.get_os_understandable_path(
self.get_body_argument("file_path", default=None, strip=True)
)
if not self.check_server_id(server_id, "save_file"):
return
server_id = bleach.clean(server_id)
if not Helpers.in_path(
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
file_path,
) or not Helpers.check_file_exists(os.path.abspath(file_path)):
logger.warning(
f"Invalid path in save_file file ajax call ({file_path})"
)
Console.warning(
f"Invalid path in save_file file ajax call ({file_path})"
)
return
# Open the file in write mode and store the content in file_object
with open(file_path, "w", encoding="utf-8") as file_object:
file_object.write(file_contents)
elif page == "rename_file":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
item_path = Helpers.get_os_understandable_path(
self.get_body_argument("item_path", default=None, strip=True)
)
new_item_name = self.get_body_argument(
"new_item_name", default=None, strip=True
)
if not self.check_server_id(server_id, "rename_file"):
return
server_id = bleach.clean(server_id)
if item_path is None or new_item_name is None:
logger.warning("Invalid path(s) in rename_file file ajax call")
Console.warning("Invalid path(s) in rename_file file ajax call")
return
if not Helpers.in_path(
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
item_path,
) or not Helpers.check_path_exists(os.path.abspath(item_path)):
logger.warning(
f"Invalid old name path in rename_file file ajax call ({server_id})"
)
Console.warning(
f"Invalid old name path in rename_file file ajax call ({server_id})"
)
return
new_item_path = os.path.join(os.path.split(item_path)[0], new_item_name)
if not Helpers.in_path(
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
new_item_path,
) or Helpers.check_path_exists(os.path.abspath(new_item_path)):
logger.warning(
f"Invalid new name path in rename_file file ajax call ({server_id})"
)
Console.warning(
f"Invalid new name path in rename_file file ajax call ({server_id})"
)
return
# RENAME
os.rename(item_path, new_item_path)
@tornado.web.authenticated
def patch(self, page):
api_key, _, exec_user = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument("id", None)
permissions = {
"Commands": EnumPermissionsServer.COMMANDS,
"Terminal": EnumPermissionsServer.TERMINAL,
"Logs": EnumPermissionsServer.LOGS,
"Schedule": EnumPermissionsServer.SCHEDULE,
"Backup": EnumPermissionsServer.BACKUP,
"Files": EnumPermissionsServer.FILES,
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
if page == "rename_file":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
item_path = Helpers.get_os_understandable_path(
self.get_body_argument("item_path", default=None, strip=True)
)
new_item_name = self.get_body_argument(
"new_item_name", default=None, strip=True
)
if not self.check_server_id(server_id, "rename_file"):
return
server_id = bleach.clean(server_id)
if item_path is None or new_item_name is None:
logger.warning("Invalid path(s) in rename_file file ajax call")
Console.warning("Invalid path(s) in rename_file file ajax call")
return
if not Helpers.in_path(
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
item_path,
) or not Helpers.check_path_exists(os.path.abspath(item_path)):
logger.warning(
f"Invalid old name path in rename_file file ajax call ({server_id})"
)
Console.warning(
f"Invalid old name path in rename_file file ajax call ({server_id})"
)
return
new_item_path = os.path.join(os.path.split(item_path)[0], new_item_name)
if not Helpers.in_path(
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
new_item_path,
) or Helpers.check_path_exists(os.path.abspath(new_item_path)):
logger.warning(
f"Invalid new name path in rename_file file ajax call ({server_id})"
)
Console.warning(
f"Invalid new name path in rename_file file ajax call ({server_id})"
)
return
# RENAME
os.rename(item_path, new_item_path)
def check_server_id(self, server_id, page_name):
if server_id is None:
logger.warning(
f"Server ID not defined in {page_name} file ajax call ({server_id})"
)
Console.warning(
f"Server ID not defined in {page_name} file ajax call ({server_id})"
)
return
server_id = bleach.clean(server_id)
# does this server id exist?
if not self.controller.servers.server_id_exists(server_id):
logger.warning(
f"Server ID not found in {page_name} file ajax call ({server_id})"
)
Console.warning(
f"Server ID not found in {page_name} file ajax call ({server_id})"
)
return
return True

File diff suppressed because it is too large Load Diff

View File

@ -50,12 +50,15 @@ class PublicHandler(BaseHandler):
if page == "login":
template = "public/login.html"
elif page == 404:
elif page == "404":
template = "public/404.html"
elif page == "error":
template = "public/error.html"
elif page == "offline":
template = "public/offline.html"
elif page == "logout":
self.clear_cookie("token")
# self.clear_cookie("user")

View File

@ -26,6 +26,17 @@ from app.classes.web.routes.api.servers.server.stdin import ApiServersServerStdi
from app.classes.web.routes.api.servers.server.tasks.index import (
ApiServersServerTasksIndexHandler,
)
from app.classes.web.routes.api.servers.server.backups.index import (
ApiServersServerBackupsIndexHandler,
)
from app.classes.web.routes.api.servers.server.backups.backup.index import (
ApiServersServerBackupsBackupIndexHandler,
)
from app.classes.web.routes.api.servers.server.files import (
ApiServersServerFilesIndexHandler,
ApiServersServerFilesCreateHandler,
ApiServersServerFilesZipHandler,
)
from app.classes.web.routes.api.servers.server.tasks.task.children import (
ApiServersServerTasksTaskChildrenHandler,
)
@ -38,8 +49,25 @@ from app.classes.web.routes.api.users.user.index import ApiUsersUserIndexHandler
from app.classes.web.routes.api.users.user.permissions import (
ApiUsersUserPermissionsHandler,
)
from app.classes.web.routes.api.users.user.api import ApiUsersUserKeyHandler
from app.classes.web.routes.api.users.user.pfp import ApiUsersUserPfpHandler
from app.classes.web.routes.api.users.user.public import ApiUsersUserPublicHandler
from app.classes.web.routes.api.crafty.announcements.index import (
ApiAnnounceIndexHandler,
)
from app.classes.web.routes.api.crafty.config.index import (
ApiCraftyConfigIndexHandler,
ApiCraftyCustomizeIndexHandler,
)
from app.classes.web.routes.api.crafty.config.server_dir import (
ApiCraftyConfigServerDirHandler,
)
from app.classes.web.routes.api.crafty.clogs.index import ApiCraftyLogIndexHandler
from app.classes.web.routes.api.crafty.imports.index import ApiImportFilesIndexHandler
from app.classes.web.routes.api.crafty.exe_cache import (
ApiCraftyJarCacheIndexHandler,
ApiCraftySteamCacheIndexHandler,
)
def api_handlers(handler_args):
@ -55,12 +83,52 @@ def api_handlers(handler_args):
ApiAuthInvalidateTokensHandler,
handler_args,
),
(
r"/api/v2/crafty/announcements/?",
ApiAnnounceIndexHandler,
handler_args,
),
(
r"/api/v2/crafty/config/?",
ApiCraftyConfigIndexHandler,
handler_args,
),
(
r"/api/v2/crafty/config/customize/?",
ApiCraftyCustomizeIndexHandler,
handler_args,
),
(
r"/api/v2/crafty/config/servers_dir/?",
ApiCraftyConfigServerDirHandler,
handler_args,
),
(
r"/api/v2/crafty/logs/([a-z0-9_]+)/?",
ApiCraftyLogIndexHandler,
handler_args,
),
(
r"/api/v2/import/file/unzip/?",
ApiImportFilesIndexHandler,
handler_args,
),
# User routes
(
r"/api/v2/users/?",
ApiUsersIndexHandler,
handler_args,
),
(
r"/api/v2/users/([0-9]+)/key/?",
ApiUsersUserKeyHandler,
handler_args,
),
(
r"/api/v2/users/([0-9]+)/key/([0-9]+)/?",
ApiUsersUserKeyHandler,
handler_args,
),
(
r"/api/v2/users/([0-9]+)/?",
ApiUsersUserIndexHandler,
@ -107,11 +175,46 @@ def api_handlers(handler_args):
ApiServersIndexHandler,
handler_args,
),
(
r"/api/v2/crafty/JarCache/?",
ApiCraftyJarCacheIndexHandler,
handler_args,
),
(
r"/api/v2/crafty/SteamCache/?",
ApiCraftySteamCacheIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/?",
ApiServersServerIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/backups/?",
ApiServersServerBackupsIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/backups/backup/?",
ApiServersServerBackupsBackupIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/files/?",
ApiServersServerFilesIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/files/create/?",
ApiServersServerFilesCreateHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/files/zip/?",
ApiServersServerFilesZipHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/tasks/?",
ApiServersServerTasksIndexHandler,

View File

@ -0,0 +1,110 @@
import logging
import json
from jsonschema import ValidationError, validate
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
notif_schema = {
"type": "object",
"properties": {
"id": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
class ApiAnnounceIndexHandler(BaseApiHandler):
def get(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_exec_user_crafty_permissions,
_,
_,
_user,
) = auth_data
data = self.helper.get_announcements()
cleared = str(
self.controller.users.get_user_by_id(auth_data[4]["user_id"])[
"cleared_notifs"
]
).split(",")
res = [d.get("id", None) for d in data]
# remove notifs that are no longer in Crafty.
for item in cleared[:]:
if item not in res:
cleared.remove(item)
updata = {"cleared_notifs": ",".join(cleared)}
self.controller.users.update_user(auth_data[4]["user_id"], updata)
if len(cleared) > 0:
for item in data[:]:
if item["id"] in cleared:
data.remove(item)
self.finish_json(
200,
{
"status": "ok",
"data": data,
},
)
def post(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_exec_user_crafty_permissions,
_,
_,
_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, notif_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
announcements = self.helper.get_announcements()
res = [d.get("id", None) for d in announcements]
cleared_notifs = str(
self.controller.users.get_user_by_id(auth_data[4]["user_id"])[
"cleared_notifs"
]
).split(",")
# remove notifs that are no longer in Crafty.
for item in cleared_notifs[:]:
if item not in res:
cleared_notifs.remove(item)
if str(data["id"]) in str(res):
cleared_notifs.append(data["id"])
else:
self.finish_json(200, {"status": "error", "error": "INVALID_DATA"})
return
updata = {"cleared_notifs": ",".join(cleared_notifs)}
self.controller.users.update_user(auth_data[4]["user_id"], updata)
self.finish_json(
200,
{
"status": "ok",
"data": {},
},
)

View File

@ -0,0 +1,34 @@
from app.classes.web.base_api_handler import BaseApiHandler
class ApiCraftyLogIndexHandler(BaseApiHandler):
def get(self, log_type: 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"})
log_types = ["audit", "session", "schedule"]
if log_type not in log_types:
raise NotImplementedError
if log_type == "audit":
return self.finish_json(
200,
{"status": "ok", "data": self.controller.management.get_activity_log()},
)
if log_type == "session":
raise NotImplementedError
if log_type == "schedule":
raise NotImplementedError

View File

@ -0,0 +1,312 @@
import os
import json
from jsonschema import ValidationError, validate
import orjson
from playhouse.shortcuts import model_to_dict
from app.classes.shared.file_helpers import FileHelpers
from app.classes.web.base_api_handler import BaseApiHandler
config_json_schema = {
"type": "object",
"properties": {
"http_port": {"type": "integer"},
"https_port": {"type": "integer"},
"language": {
"type": "string",
},
"cookie_expire": {"type": "integer"},
"show_errors": {"type": "boolean"},
"history_max_age": {"type": "integer"},
"stats_update_frequency_seconds": {"type": "integer"},
"delete_default_json": {"type": "boolean"},
"show_contribute_link": {"type": "boolean"},
"virtual_terminal_lines": {"type": "integer"},
"max_log_lines": {"type": "integer"},
"max_audit_entries": {"type": "integer"},
"disabled_language_files": {"type": "array"},
"stream_size_GB": {"type": "integer"},
"keywords": {"type": "array"},
"allow_nsfw_profile_pictures": {"type": "boolean"},
"enable_user_self_delete": {"type": "boolean"},
"reset_secrets_on_next_boot": {"type": "boolean"},
"monitored_mounts": {"type": "array"},
"dir_size_poll_freq_minutes": {"type": "integer"},
"crafty_logs_delete_after_days": {"type": "integer"},
},
"additionalProperties": False,
"minProperties": 1,
}
customize_json_schema = {
"type": "object",
"properties": {
"photo": {"type": "string"},
"opacity": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
photo_delete_schema = {
"type": "object",
"properties": {
"photo": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
DEFAULT_PHOTO = "login_1.jpg"
class ApiCraftyConfigIndexHandler(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 patch(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)
except orjson.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, config_json_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
self.controller.set_config_json(data)
self.controller.management.add_to_audit_log(
user["user_id"],
"edited config.json",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.finish_json(
200,
{"status": "ok"},
)
class ApiCraftyCustomizeIndexHandler(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 patch(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)
except orjson.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, customize_json_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if not self.helper.validate_traversal(
os.path.join(
self.controller.project_root,
"app/frontend/static/assets/images/auth/",
),
os.path.join(
self.controller.project_root,
f"app/frontend/static/assets/images/auth/{data['photo']}",
),
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": "TRIED TO REACH FILES THAT ARE"
" NOT SUPPOSED TO BE ACCESSIBLE",
},
)
self.controller.management.add_to_audit_log(
user["user_id"],
f"customized login photo: {data['photo']}/{data['opacity']}",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.controller.management.set_login_opacity(int(data["opacity"]))
if data["photo"] == DEFAULT_PHOTO:
self.controller.management.set_login_image(DEFAULT_PHOTO)
self.controller.cached_login = f"{data['photo']}"
else:
self.controller.management.set_login_image(f"custom/{data['photo']}")
self.controller.cached_login = f"custom/{data['photo']}"
self.finish_json(
200,
{
"status": "ok",
"data": {"photo": data["photo"], "opacity": data["opacity"]},
},
)
def delete(self):
auth_data = self.authenticate_user()
if not auth_data:
return
if not auth_data[4]["superuser"]:
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, photo_delete_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if not self.helper.validate_traversal(
os.path.join(
self.controller.project_root,
"app",
"frontend",
"/static/assets/images/auth/",
),
os.path.join(
self.controller.project_root,
"app",
"frontend",
"/static/assets/images/auth/",
data["photo"],
),
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": "TRIED TO REACH FILES THAT ARE"
" NOT SUPPOSED TO BE ACCESSIBLE",
},
)
if data["photo"] == DEFAULT_PHOTO:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID FILE",
"error_data": "CANNOT DELETE DEFAULT",
},
)
FileHelpers.del_file(
os.path.join(
self.controller.project_root,
f"app/frontend/static/assets/images/auth/custom/{data['photo']}",
)
)
current = self.controller.cached_login
split = current.split("/")
if len(split) == 1:
current_photo = current
else:
current_photo = split[1]
if current_photo == data["photo"]:
self.controller.management.set_login_image(DEFAULT_PHOTO)
self.controller.cached_login = DEFAULT_PHOTO
return self.finish_json(200, {"status": "ok"})

View File

@ -0,0 +1,115 @@
from jsonschema import ValidationError, validate
import orjson
from playhouse.shortcuts import model_to_dict
from app.classes.web.base_api_handler import BaseApiHandler
server_dir_schema = {
"type": "object",
"properties": {
"new_dir": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
class ApiCraftyConfigServerDirHandler(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 patch(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
_,
_,
) = auth_data
if not auth_data:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if not auth_data[4]["superuser"]:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if self.helper.is_env_docker():
raise NotImplementedError
try:
data = orjson.loads(self.request.body)
except orjson.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, server_dir_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if self.helper.dir_migration:
return self.finish_json(
400,
{
"status": "error",
"error": "IN PROGRESS",
"error_data": "Migration already in progress. Please be patient",
},
)
for server in self.controller.servers.get_all_servers_stats():
if server["stats"]["running"]:
return self.finish_json(
400,
{
"status": "error",
"error": "SERVER RUNNING",
},
)
new_dir = data["new_dir"]
self.controller.update_master_server_dir(new_dir, auth_data[4]["user_id"])
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"updated master servers dir to {new_dir}/servers",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.finish_json(
200,
{"status": "ok"},
)

View File

@ -0,0 +1,53 @@
from app.classes.web.base_api_handler import BaseApiHandler
class ApiCraftyJarCacheIndexHandler(BaseApiHandler):
def get(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
_,
_,
) = auth_data
if not auth_data[4]["superuser"]:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.controller.server_jars.manual_refresh_cache()
self.finish_json(
200,
{
"status": "ok",
"data": self.controller.server_jars.get_serverjar_data(),
},
)
class ApiCraftySteamCacheIndexHandler(BaseApiHandler):
def get(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
_,
_,
) = auth_data
if not auth_data[4]["superuser"]:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.controller.server_jars.refresh_cache()
self.finish_json(
200,
{
"status": "ok",
"data": self.controller.steam_apps.fetch_cache(),
},
)

View File

@ -0,0 +1,128 @@
import os
import logging
import json
import html
from jsonschema import validate
from jsonschema.exceptions import ValidationError
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.shared.helpers import Helpers
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
files_get_schema = {
"type": "object",
"properties": {
"page": {"type": "string", "minLength": 1},
"folder": {"type": "string"},
"upload": {"type": "boolean", "default": "False"},
"unzip": {"type": "boolean", "default": "True"},
},
"additionalProperties": False,
"minProperties": 1,
}
class ApiImportFilesIndexHandler(BaseApiHandler):
def post(self):
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsCrafty.SERVER_CREATION
not in self.controller.crafty_perms.get_crafty_permissions_list(
auth_data[4]["user_id"]
)
and not auth_data[4]["superuser"]
):
# if the user doesn't have Files or Backup permission, return an error
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, files_get_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
# TODO: limit some columns for specific permissions?
folder = data["folder"]
user_id = auth_data[4]["user_id"]
root_path = False
if data["unzip"]:
# This is awful. Once uploads go to return
# JSON we need to remove this and just send
# the path.
if data["upload"]:
folder = os.path.join(self.controller.project_root, "imports", folder)
if Helpers.check_file_exists(folder):
folder = self.file_helper.unzip_server(folder, user_id)
root_path = True
else:
if user_id:
user_lang = self.controller.users.get_user_lang_by_id(user_id)
self.helper.websocket_helper.broadcast_user(
user_id,
"send_start_error",
{
"error": self.helper.translation.translate(
"error", "no-file", user_lang
)
},
)
else:
if not self.helper.check_path_exists(folder) and user_id:
user_lang = self.controller.users.get_user_lang_by_id(user_id)
self.helper.websocket_helper.broadcast_user(
user_id,
"send_start_error",
{
"error": self.helper.translation.translate(
"error", "no-file", user_lang
)
},
)
return_json = {
"root_path": {
"path": folder,
"top": root_path,
}
}
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
for item in file_list:
if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item)
else:
unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold
)
for raw_filename in file_list:
filename = html.escape(raw_filename)
rel = os.path.join(folder, raw_filename)
dpath = os.path.join(folder, filename)
dpath = self.helper.wtol_path(dpath)
if os.path.isdir(rel):
return_json[filename] = {
"path": dpath,
"dir": True,
}
else:
return_json[filename] = {
"path": dpath,
"dir": False,
}
self.finish_json(200, {"status": "ok", "data": return_json})

View File

@ -28,9 +28,39 @@ create_role_schema = {
"required": ["server_id", "permissions"],
},
},
"manager": {"type": ["integer", "null"]},
},
"required": ["name"],
"additionalProperties": False,
"minProperties": 1,
}
basic_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"],
},
},
},
"additionalProperties": False,
"minProperties": 1,
}
@ -86,7 +116,10 @@ class ApiRolesIndexHandler(BaseApiHandler):
)
try:
validate(data, create_role_schema)
if auth_data[4]["superuser"]:
validate(data, create_role_schema)
else:
validate(data, basic_create_role_schema)
except ValidationError as e:
return self.finish_json(
400,
@ -98,6 +131,9 @@ class ApiRolesIndexHandler(BaseApiHandler):
)
role_name = data["name"]
manager = data.get("manager", None)
if manager == self.controller.users.get_id_by_name("SYSTEM") or manager == 0:
manager = None
# Get the servers
servers_dict = {server["server_id"]: server for server in data["servers"]}
@ -116,9 +152,7 @@ class ApiRolesIndexHandler(BaseApiHandler):
400, {"status": "error", "error": "ROLE_NAME_ALREADY_EXISTS"}
)
role_id = self.controller.roles.add_role_advanced(
role_name, servers, user["user_id"]
)
role_id = self.controller.roles.add_role_advanced(role_name, servers, manager)
self.controller.management.add_to_audit_log(
user["user_id"],

View File

@ -4,6 +4,36 @@ 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"],
},
},
"manager": {"type": ["integer", "null"]},
},
"additionalProperties": False,
"minProperties": 1,
}
basic_modify_role_schema = {
"type": "object",
"properties": {
"name": {
@ -109,7 +139,10 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
)
try:
validate(data, modify_role_schema)
if auth_data[4]["superuser"]:
validate(data, modify_role_schema)
else:
validate(data, basic_modify_role_schema)
except ValidationError as e:
return self.finish_json(
400,
@ -120,9 +153,18 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
},
)
manager = data.get(
"manager", self.controller.roles.get_role(role_id)["manager"]
)
if manager == self.controller.users.get_id_by_name("system") or manager == 0:
manager = None
try:
self.controller.roles.update_role_advanced(
role_id, data.get("role_name", None), data.get("servers", None)
role_id,
data.get("name", None),
data.get("servers", None),
manager,
)
except DoesNotExist:
return self.finish_json(404, {"status": "error", "error": "ROLE_NOT_FOUND"})

View File

@ -24,6 +24,7 @@ new_server_schema = {
"examples": ["My Server"],
"minLength": 2,
},
"roles": {"title": "Roles to add", "type": "array", "examples": [1, 2, 3]},
"stop_command": {
"title": "Stop command",
"description": '"" means the default for the server creation type.',
@ -61,7 +62,7 @@ new_server_schema = {
"title": "Server monitoring type",
"type": "string",
"default": "minecraft_java",
"enum": ["minecraft_java", "minecraft_bedrock", "none"],
"enum": ["minecraft_java", "minecraft_bedrock", "steam_cmd", "none"],
# TODO: SteamCMD, RakNet, etc.
},
"minecraft_java_monitoring_data": {
@ -111,7 +112,7 @@ new_server_schema = {
"title": "Server creation type",
"type": "string",
"default": "minecraft_java",
"enum": ["minecraft_java", "minecraft_bedrock", "custom"],
"enum": ["minecraft_java", "minecraft_bedrock", "steam_cmd", "custom"],
},
"minecraft_java_create_data": {
"title": "Java creation data",
@ -133,8 +134,13 @@ new_server_schema = {
"mem_min",
"mem_max",
"server_properties_port",
"agree_to_eula",
"category",
],
"category": {
"title": "Jar Category",
"type": "string",
"examples": ["modded", "vanilla"],
},
"properties": {
"type": {
"title": "Server JAR Type",
@ -185,7 +191,6 @@ new_server_schema = {
"mem_min",
"mem_max",
"server_properties_port",
"agree_to_eula",
],
"properties": {
"existing_server_path": {
@ -240,7 +245,6 @@ new_server_schema = {
"mem_min",
"mem_max",
"server_properties_port",
"agree_to_eula",
],
"properties": {
"zip_path": {
@ -336,12 +340,24 @@ new_server_schema = {
"title": "Creation type",
"type": "string",
"default": "import_server",
"enum": ["import_server", "import_zip"],
"enum": ["download_exe", "import_server", "import_zip"],
},
"download_exe_create_data": {
"title": "Import server data",
"type": "object",
"required": [],
"properties": {
"agree_to_eula": {
"title": "Agree to the EULA",
"type": "boolean",
"enum": [True],
},
},
},
"import_server_create_data": {
"title": "Import server data",
"type": "object",
"required": ["existing_server_path", "command"],
"required": ["existing_server_path", "executable"],
"properties": {
"existing_server_path": {
"title": "Server path",
@ -350,6 +366,14 @@ new_server_schema = {
"examples": ["/var/opt/server"],
"minLength": 1,
},
"executable": {
"title": "Executable File",
"description": "File Crafty should execute"
"on server launch",
"type": "string",
"examples": ["bedrock_server.exe"],
"minlength": 1,
},
"command": {
"title": "Command",
"type": "string",
@ -371,6 +395,14 @@ new_server_schema = {
"examples": ["/var/opt/server.zip"],
"minLength": 1,
},
"executable": {
"title": "Executable File",
"description": "File Crafty should execute"
"on server launch",
"type": "string",
"examples": ["bedrock_server.exe"],
"minlength": 1,
},
"zip_root": {
"title": "Server root directory",
"description": "The server root in the ZIP archive",
@ -394,7 +426,9 @@ new_server_schema = {
"allOf": [
{
"if": {
"properties": {"create_type": {"const": "import_exec"}}
"properties": {
"create_type": {"const": "import_server"}
}
},
"then": {"required": ["import_server_create_data"]},
},
@ -404,6 +438,16 @@ new_server_schema = {
},
"then": {"required": ["import_zip_create_data"]},
},
{
"if": {
"properties": {"create_type": {"const": "download_exe"}}
},
"then": {
"required": [
"download_exe_create_data",
]
},
},
],
},
{
@ -411,6 +455,59 @@ new_server_schema = {
"oneOf": [
{"required": ["import_server_create_data"]},
{"required": ["import_zip_create_data"]},
{"required": ["download_exe_create_data"]},
],
},
],
},
"steam_cmd_create_data": {
"title": "Minecraft Bedrock creation data",
"type": "object",
"required": ["create_type"],
"properties": {
"create_type": {
"title": "Creation type",
"type": "string",
"default": "download_exe",
"enum": ["download_exe"],
},
"download_exe_create_data": {
"title": "Import server data",
"type": "object",
"required": [],
"properties": {
"app_id": {
"title": "Steam Server App ID",
"type": "integer",
},
"agree_to_eula": {
"title": "Agree to the EULA",
"type": "boolean",
"enum": [True],
},
},
},
},
"allOf": [
{
"$comment": "If..then section",
"allOf": [
{
"if": {
"properties": {"create_type": {"const": "download_exe"}}
},
"then": {
"required": [
"download_exe_create_data",
]
},
},
],
},
{
"title": "Only one creation data",
"oneOf": [
{"required": ["download_exe_create_data"]},
],
},
],
@ -577,6 +674,10 @@ new_server_schema = {
},
"then": {"required": ["minecraft_bedrock_create_data"]},
},
{
"if": {"properties": {"create_type": {"const": "steam_cmd"}}},
"then": {"required": ["steam_cmd_create_data"]},
},
{
"if": {"properties": {"create_type": {"const": "custom"}}},
"then": {"required": ["custom_create_data"]},
@ -605,6 +706,7 @@ new_server_schema = {
"oneOf": [
{"required": ["minecraft_java_create_data"]},
{"required": ["minecraft_bedrock_create_data"]},
{"required": ["steam_cmd_create_data"]},
{"required": ["custom_create_data"]},
],
},
@ -613,6 +715,7 @@ new_server_schema = {
"oneOf": [
{"required": ["minecraft_java_monitoring_data"]},
{"required": ["minecraft_bedrock_monitoring_data"]},
{"required": ["steam_cmd_monitoring_data"]},
{"properties": {"monitoring_type": {"const": "none"}}},
],
},
@ -651,7 +754,7 @@ class ApiServersIndexHandler(BaseApiHandler):
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
print(data)
try:
validate(data, new_server_schema)
except ValidationError as e:

View File

@ -31,6 +31,8 @@ class ApiServersServerActionHandler(BaseApiHandler):
if action == "clone_server":
return self._clone_server(server_id, auth_data[4]["user_id"])
if action == "eula":
return self._agree_eula(server_id, auth_data[4]["user_id"])
self.controller.management.send_command(
auth_data[4]["user_id"], server_id, self.get_remote_ip(), action
@ -41,6 +43,11 @@ class ApiServersServerActionHandler(BaseApiHandler):
{"status": "ok"},
)
def _agree_eula(self, server_id, user):
svr = self.controller.servers.get_server_instance_by_id(server_id)
svr.agree_eula(user)
return 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).exists()

View File

@ -0,0 +1,210 @@
import logging
import json
import os
from apscheduler.jobstores.base import JobLookupError
from jsonschema import validate
from jsonschema.exceptions import ValidationError
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.shared.file_helpers import FileHelpers
from app.classes.web.base_api_handler import BaseApiHandler
from app.classes.shared.helpers import Helpers
logger = logging.getLogger(__name__)
backup_schema = {
"type": "object",
"properties": {
"filename": {"type": "string", "minLength": 5},
},
"additionalProperties": False,
"minProperties": 1,
}
class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
def get(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsServer.BACKUP
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.finish_json(200, self.controller.management.get_backup_config(server_id))
def delete(self, server_id: str):
auth_data = self.authenticate_user()
backup_conf = self.controller.management.get_backup_config(server_id)
if not auth_data:
return
if (
EnumPermissionsServer.BACKUP
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Schedule permission, return an error
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, backup_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
try:
FileHelpers.del_file(
os.path.join(backup_conf["backup_path"], data["filename"])
)
except Exception:
return self.finish_json(
400, {"status": "error", "error": "NO BACKUP FOUND"}
)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"Edited server {server_id}: removed backup {data['filename']}",
server_id,
self.get_remote_ip(),
)
return self.finish_json(200, {"status": "ok"})
def post(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsServer.BACKUP
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Schedule permission, return an error
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, backup_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
try:
svr_obj = self.controller.servers.get_server_obj(server_id)
server_data = self.controller.servers.get_server_data_by_id(server_id)
zip_name = data["filename"]
# import the server again based on zipfile
if server_data["type"] == "minecraft-java":
backup_path = svr_obj.backup_path
if Helpers.validate_traversal(backup_path, zip_name):
temp_dir = Helpers.unzip_backup_archive(backup_path, zip_name)
new_server = self.controller.import_zip_server(
svr_obj.server_name,
temp_dir,
server_data["executable"],
"1",
"2",
server_data["server_port"],
server_data["created_by"],
)
new_server_id = new_server
new_server = self.controller.servers.get_server_data(new_server)
self.controller.rename_backup_dir(
server_id, new_server_id, new_server["server_uuid"]
)
# preserve current schedules
for schedule in self.controller.management.get_schedules_by_server(
server_id
):
self.tasks_manager.update_job(
schedule.schedule_id, {"server_id": new_server_id}
)
# preserve execution command
new_server_obj = self.controller.servers.get_server_obj(
new_server_id
)
new_server_obj.execution_command = server_data["execution_command"]
# reset executable path
if svr_obj.path in svr_obj.executable:
new_server_obj.executable = str(svr_obj.executable).replace(
svr_obj.path, new_server_obj.path
)
# reset run command path
if svr_obj.path in svr_obj.execution_command:
new_server_obj.execution_command = str(
svr_obj.execution_command
).replace(svr_obj.path, new_server_obj.path)
# reset log path
if svr_obj.path in svr_obj.log_path:
new_server_obj.log_path = str(svr_obj.log_path).replace(
svr_obj.path, new_server_obj.path
)
self.controller.servers.update_server(new_server_obj)
# preserve backup config
backup_config = self.controller.management.get_backup_config(
server_id
)
excluded_dirs = []
server_obj = self.controller.servers.get_server_obj(server_id)
loop_backup_path = self.helper.wtol_path(server_obj.path)
for item in self.controller.management.get_excluded_backup_dirs(
server_id
):
item_path = self.helper.wtol_path(item)
bu_path = os.path.relpath(item_path, loop_backup_path)
bu_path = os.path.join(new_server_obj.path, bu_path)
excluded_dirs.append(bu_path)
self.controller.management.set_backup_config(
new_server_id,
new_server_obj.backup_path,
backup_config["max_backups"],
excluded_dirs,
backup_config["compress"],
backup_config["shutdown"],
)
# remove old server's tasks
try:
self.tasks_manager.remove_all_server_tasks(server_id)
except JobLookupError as e:
logger.info("No active tasks found for server: {e}")
self.controller.remove_server(server_id, True)
except Exception:
return self.finish_json(
400, {"status": "error", "error": "NO BACKUP FOUND"}
)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"Restored server {server_id} backup {data['filename']}",
server_id,
self.get_remote_ip(),
)
return self.finish_json(200, {"status": "ok"})

View File

@ -0,0 +1,123 @@
import logging
import json
from jsonschema import validate
from jsonschema.exceptions import ValidationError
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
backup_patch_schema = {
"type": "object",
"properties": {
"backup_path": {"type": "string", "minLength": 1},
"max_backups": {"type": "integer"},
"compress": {"type": "boolean"},
"shutdown": {"type": "boolean"},
"backup_before": {"type": "string"},
"backup_after": {"type": "string"},
"exclusions": {"type": "array"},
},
"additionalProperties": False,
"minProperties": 1,
}
basic_backup_patch_schema = {
"type": "object",
"properties": {
"max_backups": {"type": "integer"},
"compress": {"type": "boolean"},
"shutdown": {"type": "boolean"},
"backup_before": {"type": "string"},
"backup_after": {"type": "string"},
"exclusions": {"type": "array"},
},
"additionalProperties": False,
"minProperties": 1,
}
class ApiServersServerBackupsIndexHandler(BaseApiHandler):
def get(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsServer.BACKUP
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.finish_json(200, self.controller.management.get_backup_config(server_id))
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:
if auth_data[4]["superuser"]:
validate(data, backup_patch_schema)
else:
validate(data, basic_backup_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.BACKUP
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.controller.management.set_backup_config(
server_id,
data.get(
"backup_path",
self.controller.management.get_backup_config(server_id)["backup_path"],
),
data.get(
"max_backups",
self.controller.management.get_backup_config(server_id)["max_backups"],
),
data.get("exclusions"),
data.get(
"compress",
self.controller.management.get_backup_config(server_id)["compress"],
),
data.get(
"shutdown",
self.controller.management.get_backup_config(server_id)["shutdown"],
),
data.get(
"backup_before",
self.controller.management.get_backup_config(server_id)["before"],
),
data.get(
"backup_after",
self.controller.management.get_backup_config(server_id)["after"],
),
)
return self.finish_json(200, {"status": "ok"})

View File

@ -0,0 +1,555 @@
import os
import logging
import json
import html
from jsonschema import validate
from jsonschema.exceptions import ValidationError
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.shared.helpers import Helpers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
files_get_schema = {
"type": "object",
"properties": {
"page": {"type": "string", "minLength": 1},
"path": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
files_patch_schema = {
"type": "object",
"properties": {
"path": {"type": "string"},
"contents": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
files_unzip_schema = {
"type": "object",
"properties": {
"folder": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
files_create_schema = {
"type": "object",
"properties": {
"parent": {"type": "string"},
"name": {"type": "string"},
"directory": {"type": "boolean"},
},
"additionalProperties": False,
"minProperties": 1,
}
files_rename_schema = {
"type": "object",
"properties": {
"path": {"type": "string"},
"new_name": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
file_delete_schema = {
"type": "object",
"properties": {
"filename": {"type": "string", "minLength": 5},
},
"additionalProperties": False,
"minProperties": 1,
}
class ApiServersServerFilesIndexHandler(BaseApiHandler):
def post(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 (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
or EnumPermissionsServer.BACKUP
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Files or Backup permission, return an error
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, files_get_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
data["path"],
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if os.path.isdir(data["path"]):
# TODO: limit some columns for specific permissions?
folder = data["path"]
return_json = {
"root_path": {
"path": folder,
"top": data["path"]
== self.controller.servers.get_server_data_by_id(server_id)["path"],
}
}
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
for item in file_list:
if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item)
else:
unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold
)
for raw_filename in file_list:
filename = html.escape(raw_filename)
rel = os.path.join(folder, raw_filename)
dpath = os.path.join(folder, filename)
if str(dpath) in self.controller.management.get_excluded_backup_dirs(
server_id
):
if os.path.isdir(rel):
return_json[filename] = {
"path": dpath,
"dir": True,
"excluded": True,
}
else:
return_json[filename] = {
"path": dpath,
"dir": False,
"excluded": True,
}
else:
if os.path.isdir(rel):
return_json[filename] = {
"path": dpath,
"dir": True,
"excluded": False,
}
else:
return_json[filename] = {
"path": dpath,
"dir": False,
"excluded": False,
}
self.finish_json(200, {"status": "ok", "data": return_json})
else:
try:
with open(data["path"], encoding="utf-8") as file:
file_contents = file.read()
except UnicodeDecodeError as ex:
self.finish_json(
400,
{"status": "error", "error": "DECODE_ERROR", "error_data": str(ex)},
)
self.finish_json(200, {"status": "ok", "data": file_contents})
def delete(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 (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Files permission, return an error
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, file_delete_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
data["filename"],
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if os.path.isdir(data["filename"]):
FileHelpers.del_dirs(data["filename"])
else:
FileHelpers.del_file(data["filename"])
return self.finish_json(200, {"status": "ok"})
def patch(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 (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Files permission, return an error
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, files_patch_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
data["path"],
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
file_path = Helpers.get_os_understandable_path(data["path"])
file_contents = data["contents"]
# Open the file in write mode and store the content in file_object
with open(file_path, "w", encoding="utf-8") as file_object:
file_object.write(file_contents)
return self.finish_json(200, {"status": "ok"})
def put(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 (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Files permission, return an error
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, files_create_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
path = os.path.join(data["parent"], data["name"])
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
path,
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if Helpers.check_path_exists(os.path.abspath(path)):
return self.finish_json(
400,
{
"status": "error",
"error": "FILE EXISTS",
"error_data": str(e),
},
)
if data["directory"]:
os.mkdir(path)
else:
# Create the file by opening it
with open(path, "w", encoding="utf-8") as file_object:
file_object.close()
return self.finish_json(200, {"status": "ok"})
class ApiServersServerFilesCreateHandler(BaseApiHandler):
def patch(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 (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Files permission, return an error
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, files_rename_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
path = data["path"]
new_item_name = data["new_name"]
new_item_path = os.path.join(os.path.split(path)[0], new_item_name)
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
path,
) or not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
new_item_path,
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if Helpers.check_path_exists(os.path.abspath(new_item_path)):
return self.finish_json(
400,
{
"status": "error",
"error": "FILE EXISTS",
"error_data": {},
},
)
os.rename(path, new_item_path)
return self.finish_json(200, {"status": "ok"})
def put(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 (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Files permission, return an error
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, files_create_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
path = os.path.join(data["parent"], data["name"])
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
path,
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if Helpers.check_path_exists(os.path.abspath(path)):
return self.finish_json(
400,
{
"status": "error",
"error": "FILE EXISTS",
"error_data": str(e),
},
)
if data["directory"]:
os.mkdir(path)
else:
# Create the file by opening it
with open(path, "w", encoding="utf-8") as file_object:
file_object.close()
return self.finish_json(200, {"status": "ok"})
class ApiServersServerFilesZipHandler(BaseApiHandler):
def post(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 (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Files permission, return an error
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, files_unzip_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
folder = data["folder"]
user_id = auth_data[4]["user_id"]
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
folder,
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if Helpers.check_file_exists(folder):
folder = self.file_helper.unzip_file(folder, user_id)
else:
if user_id:
return self.finish_json(
400,
{
"status": "error",
"error": "FILE_DOES_NOT_EXIST",
"error_data": str(e),
},
)
return self.finish_json(200, {"status": "ok"})

View File

@ -13,20 +13,39 @@ 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},
"java_selection": {"type": "string"},
"auto_start": {"type": "boolean"},
"auto_start_delay": {"type": "integer"},
"auto_start_delay": {"type": "integer", "minimum": 0},
"crash_detection": {"type": "boolean"},
"stop_command": {"type": "string"},
"executable_update_url": {"type": "string", "minLength": 1},
"executable_update_url": {"type": "string"},
"server_ip": {"type": "string", "minLength": 1},
"server_port": {"type": "integer"},
"logs_delete_after": {"type": "integer"},
"type": {"type": "string", "minLength": 1},
"shutdown_timeout": {"type": "integer", "minimum": 0},
"logs_delete_after": {"type": "integer", "minimum": 0},
"ignored_exits": {"type": "string"},
"show_status": {"type": "boolean"},
},
"additionalProperties": False,
"minProperties": 1,
}
basic_server_patch_schema = {
"type": "object",
"properties": {
"server_name": {"type": "string", "minLength": 1},
"executable": {"type": "string"},
"java_selection": {"type": "string"},
"auto_start": {"type": "boolean"},
"auto_start_delay": {"type": "integer", "minimum": 0},
"crash_detection": {"type": "boolean"},
"stop_command": {"type": "string"},
"shutdown_timeout": {"type": "integer"},
"logs_delete_after": {"type": "integer", "minimum": 0},
"ignored_exits": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
@ -63,7 +82,11 @@ class ApiServersServerIndexHandler(BaseApiHandler):
)
try:
validate(data, server_patch_schema)
# prevent general users from becoming bad actors
if auth_data[4]["superuser"]:
validate(data, server_patch_schema)
else:
validate(data, basic_server_patch_schema)
except ValidationError as e:
return self.finish_json(
400,
@ -88,9 +111,24 @@ class ApiServersServerIndexHandler(BaseApiHandler):
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
server_obj = self.controller.servers.get_server_obj(server_id)
java_flag = False
for key in data:
# If we don't validate the input there could be security issues
if key == "java_selection" and data[key] != "none":
try:
command = self.helper.get_execution_java(
data[key], server_obj.execution_command
)
setattr(server_obj, "execution_command", command)
except ValueError:
return self.finish_json(
400, {"status": "error", "error": "INVALID EXECUTION COMMAND"}
)
java_flag = True
if key != "path":
if key == "execution_command" and java_flag:
continue
setattr(server_obj, key, data[key])
self.controller.servers.update_server(server_obj)
@ -134,7 +172,16 @@ class ApiServersServerIndexHandler(BaseApiHandler):
)
self.tasks_manager.remove_all_server_tasks(server_id)
self.controller.remove_server(server_id, remove_files)
failed = False
for item in self.controller.servers.failed_servers[:]:
if item["server_id"] == int(server_id):
self.controller.servers.failed_servers.remove(item)
failed = True
if failed:
self.controller.remove_unloaded_server(server_id)
else:
self.controller.remove_server(server_id, remove_files)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],

View File

@ -74,6 +74,6 @@ class ApiServersServerLogsHandler(BaseApiHandler):
if use_html:
for line in lines:
self.write(f"{line}<br />")
else:
self.finish_json(200, {"status": "ok", "data": lines})
line = f"{line}<br />"
self.finish_json(200, {"status": "ok", "data": lines})

View File

@ -35,7 +35,13 @@ class ApiServersServerStdinHandler(BaseApiHandler):
"Please report this to the devs"
)
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
decoded = self.request.body.decode("utf-8")
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"Sent command ({decoded}) to terminal",
server_id=0,
source_ip=self.get_remote_ip(),
)
if svr.send_command(self.request.body.decode("utf-8")):
return self.finish_json(
200,

View File

@ -1,16 +1,122 @@
# TODO: create and read
import json
import logging
from croniter import croniter
from jsonschema import ValidationError, validate
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
new_task_schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"enabled": {
"type": "boolean",
"default": True,
},
"action": {
"type": "string",
},
"interval": {"type": "integer"},
"interval_type": {
"type": "string",
"enum": [
# Basic tasks
"hours",
"minutes",
"days",
# Chain reaction tasks:
"reaction",
# CRON tasks:
"",
],
},
"start_time": {"type": "string", "pattern": r"\d{1,2}:\d{1,2}"},
"command": {"type": ["string", "null"]},
"one_time": {"type": "boolean", "default": False},
"cron_string": {"type": "string", "default": ""},
"parent": {"type": ["integer", "null"]},
"delay": {"type": "integer", "default": 0},
},
"additionalProperties": False,
"minProperties": 1,
}
class ApiServersServerTasksIndexHandler(BaseApiHandler):
def get(self, server_id: str, task_id: str):
pass
def post(self, server_id: str, task_id: str):
pass
def post(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, new_task_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.SCHEDULE
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
data["server_id"] = server_id
if not data.get("start_time"):
data["start_time"] = "00:00"
# validate cron string
if "cron_string" in data:
if data["cron_string"] != "" and not croniter.is_valid(data["cron_string"]):
return self.finish_json(
405,
{
"status": "error",
"error": self.helper.translation.translate(
"error",
"cronFormat",
self.controller.users.get_user_lang_by_id(
auth_data[4]["user_id"]
),
),
},
)
if "parent" not in data:
data["parent"] = None
task_id = self.tasks_manager.schedule_job(data)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"Edited server {server_id}: added schedule",
server_id,
self.get_remote_ip(),
)
self.tasks_manager.reload_schedule_from_db()
self.finish_json(200, {"status": "ok", "data": {"schedule_id": task_id}})

View File

@ -3,6 +3,7 @@
import json
import logging
from croniter import croniter
from jsonschema import ValidationError, validate
from app.classes.models.server_permissions import EnumPermissionsServer
@ -35,6 +36,7 @@ task_patch_schema = {
"",
],
},
"name": {"type": "string"},
"start_time": {"type": "string", "pattern": r"\d{1,2}:\d{1,2}"},
"command": {"type": ["string", "null"]},
"one_time": {"type": "boolean", "default": False},
@ -49,10 +51,47 @@ task_patch_schema = {
class ApiServersServerTasksTaskIndexHandler(BaseApiHandler):
def get(self, server_id: str, task_id: str):
pass
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsServer.SCHEDULE
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.finish_json(200, self.controller.management.get_scheduled_task(task_id))
def delete(self, server_id: str, task_id: str):
pass
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsServer.SCHEDULE
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
self.tasks_manager.remove_job(task_id)
except Exception:
return self.finish_json(
400, {"status": "error", "error": "NO SCHEDULE FOUND"}
)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"Edited server {server_id}: removed schedule",
server_id,
self.get_remote_ip(),
)
self.tasks_manager.reload_schedule_from_db()
return self.finish_json(200, {"status": "ok"})
def patch(self, server_id: str, task_id: str):
auth_data = self.authenticate_user()
@ -96,6 +135,22 @@ class ApiServersServerTasksTaskIndexHandler(BaseApiHandler):
if str(data.get("parent")) == str(task_id) and data.get("parent") is not None:
data["parent"] = None
data["server_id"] = server_id
if "cron_string" in data:
if data["cron_string"] != "" and not croniter.is_valid(data["cron_string"]):
return self.finish_json(
405,
{
"status": "error",
"error": self.helper.translation.translate(
"error",
"cronFormat",
self.controller.users.get_user_lang_by_id(
auth_data[4]["user_id"]
),
),
},
)
self.tasks_manager.update_job(task_id, data)
self.controller.management.add_to_audit_log(

View File

@ -93,10 +93,17 @@ class ApiUsersIndexHandler(BaseApiHandler):
"error_data": str(e),
},
)
username = data["username"]
username = str(username).lower()
manager = int(user["user_id"])
manager = data.get("manager", None)
if user["superuser"]:
if (
manager == self.controller.users.get_id_by_name("SYSTEM")
or manager == 0
):
manager = None
else:
manager = int(user["user_id"])
password = data["password"]
email = data.get("email", "default@example.com")
enabled = data.get("enabled", True)

View File

@ -0,0 +1,243 @@
import json
import logging
from jsonschema import ValidationError, validate
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
class ApiUsersUserKeyHandler(BaseApiHandler):
def get(self, user_id: str, key_id=None):
auth_data = self.authenticate_user()
if not auth_data:
return
if key_id:
key = self.controller.users.get_user_api_key(key_id)
# does this user id exist?
if key is None:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID DATA",
"error_data": "INVALID KEY",
},
)
if (
str(key.user_id) != str(auth_data[4]["user_id"])
and not auth_data[4]["superuser"]
):
return self.finish_json(
400,
{
"status": "error",
"error": "NOT AUTHORIZED",
"error_data": "TRIED TO EDIT KEY WIHTOUT AUTH",
},
)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"Generated a new API token for the key {key.name} "
f"from user with UID: {key.user_id}",
server_id=0,
source_ip=self.get_remote_ip(),
)
data_key = self.controller.authentication.generate(
key.user_id_id, {"token_id": key.token_id}
)
return self.finish_json(
200,
{"status": "ok", "data": data_key},
)
if (
str(user_id) != str(auth_data[4]["user_id"])
and not auth_data[4]["superuser"]
):
return self.finish_json(
400,
{
"status": "error",
"error": "NOT AUTHORIZED",
"error_data": "TRIED TO EDIT KEY WIHTOUT AUTH",
},
)
keys = []
for key in self.controller.users.get_user_api_keys(str(user_id)):
keys.append(
{
"id": key.token_id,
"name": key.name,
"server_permissions": key.server_permissions,
"crafty_permissions": key.crafty_permissions,
"superuser": key.superuser,
}
)
self.finish_json(
200,
{
"status": "ok",
"data": keys,
},
)
def patch(self, user_id: str):
user_key_schema = {
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 3},
"server_permissions_mask": {
"type": "string",
"pattern": "^[01]{8}$", # 8 bits, see EnumPermissionsServer
},
"crafty_permissions_mask": {
"type": "string",
"pattern": "^[01]{3}$", # 8 bits, see EnumPermissionsCrafty
},
"superuser": {"type": "boolean"},
},
"additionalProperties": False,
"minProperties": 1,
}
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_key_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"]
# does this user id exist?
if not self.controller.users.user_id_exists(user_id):
return self.finish_json(
400,
{
"status": "error",
"error": "USER NOT FOUND",
"error_data": "USER_NOT_FOUND",
},
)
if (
str(user_id) != str(auth_data[4]["user_id"])
and not auth_data[4]["superuser"]
):
return self.finish_json(
400,
{
"status": "error",
"error": "NOT AUTHORIZED",
"error_data": "TRIED TO EDIT KEY WIHTOUT AUTH",
},
)
key_id = self.controller.users.add_user_api_key(
data["name"],
user_id,
data["superuser"],
data["server_permissions_mask"],
data["crafty_permissions_mask"],
)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"Added API key {data['name']} with crafty permissions "
f"{data['crafty_permissions_mask']}"
f" and {data['server_permissions_mask']} for user with UID: {user_id}",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.finish_json(200, {"status": "ok", "data": {"id": key_id}})
def delete(self, _user_id: str, key_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_exec_user_crafty_permissions,
_,
_,
_user,
) = auth_data
if key_id:
key = self.controller.users.get_user_api_key(key_id)
# does this user id exist?
if key is None:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID DATA",
"error_data": "INVALID KEY",
},
)
# does this user id exist?
target_key = self.controller.users.get_user_api_key(key_id)
if not target_key:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID KEY",
"error_data": "INVALID KEY ID",
},
)
if (
target_key.user_id != auth_data[4]["user_id"]
and not auth_data[4]["superuser"]
):
return self.finish_json(
400,
{
"status": "error",
"error": "NOT AUTHORIZED",
"error_data": "TRIED TO EDIT KEY WIHTOUT AUTH",
},
)
self.controller.users.delete_user_api_key(key_id)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"Removed API key {target_key} "
f"(ID: {key_id}) from user {auth_data[4]['user_id']}",
server_id=0,
source_ip=self.get_remote_ip(),
)
return self.finish_json(
200,
{"status": "ok", "data": {"id": key_id}},
)

View File

@ -166,7 +166,13 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
return self.finish_json(
400, {"status": "error", "error": "INVALID_USERNAME"}
)
if self.controller.users.get_id_by_name(data["username"]) is not None:
if self.controller.users.get_id_by_name(
data["username"]
) is not None and self.controller.users.get_id_by_name(
data["username"]
) != int(
user_id
):
return self.finish_json(
400, {"status": "error", "error": "USER_EXISTS"}
)
@ -210,13 +216,13 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
400, {"status": "error", "error": "INVALID_ROLES_MODIFY"}
)
if "password" in data and str(user["user_id"] == str(user_id)):
# TODO: edit your own password
return self.finish_json(
400, {"status": "error", "error": "INVALID_PASSWORD_MODIFY"}
)
user_obj = HelperUsers.get_user_model(user_id)
if "password" in data and str(user["user_id"]) != str(user_id):
if str(user["user_id"]) != str(user_obj.manager):
# TODO: edit your own password
return self.finish_json(
400, {"status": "error", "error": "INVALID_PASSWORD_MODIFY"}
)
if "roles" in data:
roles: t.Set[str] = set(data.pop("roles"))
@ -236,6 +242,12 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
user_id, removed_roles
)
if "manager" in data and (
data["manager"] == self.controller.users.get_id_by_name("SYSTEM")
or data["manager"] == 0
):
data["manager"] = None
if "permissions" in data:
permissions: t.List[UsersController.ApiPermissionDict] = data.pop(
"permissions"
@ -246,7 +258,7 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
limit_role_creation = 0
for permission in permissions:
self.controller.crafty_perms.set_permission(
permissions_mask = self.controller.crafty_perms.set_permission(
permissions_mask,
EnumPermissionsCrafty.__members__[permission["name"]],
"1" if permission["enabled"] else "0",

View File

@ -1,14 +1,10 @@
import json
import logging
import os
import time
import tornado.web
import tornado.escape
import bleach
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.shared.helpers import Helpers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.main_models import DatabaseShortcuts
from app.classes.web.base_handler import BaseHandler
@ -145,7 +141,11 @@ class ServerHandler(BaseHandler):
"not a server creator or server limit reached"
)
return
page_data["server_api"] = False
if page_data["online"]:
page_data["server_api"] = self.helper.check_address_status(
"https://serverjars.com/api/fetchTypes"
)
page_data["server_types"] = self.controller.server_jars.get_serverjar_data()
page_data["js_server_types"] = json.dumps(
self.controller.server_jars.get_serverjar_data()
@ -164,7 +164,7 @@ class ServerHandler(BaseHandler):
"not a server creator or server limit reached"
)
return
page_data["server_api"] = True
template = "server/bedrock_wizard.html"
if page == "steam_cmd_step1":
@ -187,500 +187,3 @@ class ServerHandler(BaseHandler):
data=page_data,
translate=self.translator.translate,
)
@tornado.web.authenticated
def post(self, page):
api_key, _token_data, exec_user = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
template = "public/404.html"
page_data = {
"version_data": "version_data_here", # TODO
"user_data": exec_user,
"show_contribute": self.helper.get_setting("show_contribute_link", True),
"background": self.controller.cached_login,
"lang": self.controller.users.get_user_lang_by_id(exec_user["user_id"]),
"lang_page": Helpers.get_lang_page(
self.controller.users.get_user_lang_by_id(exec_user["user_id"])
),
}
if page == "command":
server_id = bleach.clean(self.get_argument("id", None))
command = bleach.clean(self.get_argument("command", None))
if server_id is not None:
if command == "clone_server":
if (
not superuser
and not self.controller.crafty_perms.can_create_server(
exec_user["user_id"]
)
):
time.sleep(3)
self.helper.websocket_helper.broadcast_user(
exec_user["user_id"],
"send_start_error",
{
"error": "<i class='fas fa-exclamation-triangle'"
" style='font-size:48px;color:red'>"
"</i> Not a server creator or server limit reached."
},
)
return
def is_name_used(name):
for server in self.controller.servers.get_all_defined_servers():
if server["server_name"] == name:
return True
return
template = "/panel/dashboard"
server_data = self.controller.servers.get_server_data_by_id(
server_id
)
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
)
# copy the old server
FileHelpers.copy_dir(server_data.get("path"), new_server_path)
# TODO get old server DB data to individual variables
stop_command = server_data.get("stop_command")
new_server_command = str(server_data.get("execution_command"))
new_executable = server_data.get("executable")
new_server_log_file = str(
Helpers.get_os_understandable_path(server_data.get("log_path"))
)
backup_path = os.path.join(self.helper.backup_path, new_server_uuid)
server_port = server_data.get("server_port")
server_type = server_data.get("type")
created_by = exec_user["user_id"]
new_server_id = self.controller.servers.create_server(
new_server_name,
new_server_uuid,
new_server_path,
backup_path,
new_server_command,
new_executable,
new_server_log_file,
stop_command,
server_type,
created_by,
server_port,
)
if not exec_user["superuser"]:
new_server_uuid = self.controller.servers.get_server_data_by_id(
new_server_id
).get("server_uuid")
role_id = self.controller.roles.add_role(
f"Creator of Server with uuid={new_server_uuid}",
exec_user["user_id"],
)
self.controller.server_perms.add_role_server(
new_server_id, role_id, "11111111"
)
self.controller.users.add_role_to_user(
exec_user["user_id"], role_id
)
self.controller.servers.init_all_servers()
return
self.controller.management.send_command(
exec_user["user_id"], server_id, self.get_remote_ip(), command
)
if page == "step1":
if not superuser and not self.controller.crafty_perms.can_create_server(
exec_user["user_id"]
):
self.redirect(
"/panel/error?error=Unauthorized access: "
"not a server creator or server limit reached"
)
return
if not superuser:
user_roles = self.controller.roles.get_all_roles()
else:
user_roles = self.get_user_roles()
server = bleach.clean(self.get_argument("server", ""))
server_name = bleach.clean(self.get_argument("server_name", ""))
min_mem = bleach.clean(self.get_argument("min_memory", ""))
max_mem = bleach.clean(self.get_argument("max_memory", ""))
port = bleach.clean(self.get_argument("port", ""))
if int(port) < 1 or int(port) > 65535:
self.redirect(
"/panel/error?error=Constraint Error: "
"Port must be greater than 0 and less than 65535"
)
return
import_type = bleach.clean(self.get_argument("create_type", ""))
import_server_path = bleach.clean(self.get_argument("server_path", ""))
import_server_jar = bleach.clean(self.get_argument("server_jar", ""))
server_parts = server.split("|")
captured_roles = []
for role in user_roles:
if bleach.clean(self.get_argument(str(role), "")) == "on":
captured_roles.append(role)
if not server_name:
self.redirect("/panel/error?error=Server name cannot be empty!")
return
if import_type == "import_jar":
if self.helper.is_subdir(
import_server_path, self.controller.project_root
):
self.redirect(
"/panel/error?error=Loop Error: The selected path will cause"
" an infinite copy loop. Make sure Crafty's directory is not"
" in your server path."
)
return
good_path = self.controller.verify_jar_server(
import_server_path, import_server_jar
)
if not good_path:
self.redirect(
"/panel/error?error=Server path or Server Jar not found!"
)
return
new_server_id = self.controller.import_jar_server(
server_name,
import_server_path,
import_server_jar,
min_mem,
max_mem,
port,
exec_user["user_id"],
)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f'imported a jar server named "{server_name}"',
new_server_id,
self.get_remote_ip(),
)
elif import_type == "import_zip":
# here import_server_path means the zip path
zip_path = bleach.clean(self.get_argument("root_path"))
good_path = Helpers.check_path_exists(zip_path)
if not good_path:
self.redirect("/panel/error?error=Temp path not found!")
return
new_server_id = self.controller.import_zip_server(
server_name,
zip_path,
import_server_jar,
min_mem,
max_mem,
port,
exec_user["user_id"],
)
if new_server_id == "false":
self.redirect(
f"/panel/error?error=Zip file not accessible! "
f"You can fix this permissions issue with "
f"sudo chown -R crafty:crafty {import_server_path} "
f"And sudo chmod 2775 -R {import_server_path}"
)
return
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f'imported a zip server named "{server_name}"',
new_server_id,
self.get_remote_ip(),
)
else:
if len(server_parts) != 3:
self.redirect("/panel/error?error=Invalid server data")
return
jar_type, server_type, server_version = server_parts
# TODO: add server type check here and call the correct server
# add functions if not a jar
if server_type == "forge" and not self.helper.detect_java():
translation = self.helper.translation.translate(
"error",
"installerJava",
self.controller.users.get_user_lang_by_id(exec_user["user_id"]),
).format(server_name)
self.redirect(f"/panel/error?error={translation}")
return
new_server_id = self.controller.create_jar_server(
jar_type,
server_type,
server_version,
server_name,
min_mem,
max_mem,
port,
exec_user["user_id"],
)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"created a {server_version} {str(server_type).capitalize()}"
f' server named "{server_name}"',
# Example: Admin created a 1.16.5 Bukkit server named "survival"
new_server_id,
self.get_remote_ip(),
)
# These lines create a new Role for the Server with full permissions
# and add the user to it if he's not a superuser
if len(captured_roles) == 0:
if not superuser:
new_server_uuid = self.controller.servers.get_server_data_by_id(
new_server_id
).get("server_uuid")
role_id = self.controller.roles.add_role(
f"Creator of Server with uuid={new_server_uuid}",
exec_user["user_id"],
)
self.controller.server_perms.add_role_server(
new_server_id, role_id, "11111111"
)
self.controller.users.add_role_to_user(
exec_user["user_id"], role_id
)
else:
for role in captured_roles:
role_id = role
self.controller.server_perms.add_role_server(
new_server_id, role_id, "11111111"
)
self.controller.servers.stats.record_stats()
self.redirect("/panel/dashboard")
if page == "bedrock_step1":
if not superuser and not self.controller.crafty_perms.can_create_server(
exec_user["user_id"]
):
self.redirect(
"/panel/error?error=Unauthorized access: "
"not a server creator or server limit reached"
)
return
if not superuser:
user_roles = self.controller.roles.get_all_roles()
else:
user_roles = self.controller.roles.get_all_roles()
server = bleach.clean(self.get_argument("server", ""))
server_name = bleach.clean(self.get_argument("server_name", ""))
port = bleach.clean(self.get_argument("port", ""))
if not port:
port = 19132
if int(port) < 1 or int(port) > 65535:
self.redirect(
"/panel/error?error=Constraint Error: "
"Port must be greater than 0 and less than 65535"
)
return
import_type = bleach.clean(self.get_argument("create_type", ""))
import_server_path = bleach.clean(self.get_argument("server_path", ""))
import_server_exe = bleach.clean(self.get_argument("server_jar", ""))
server_parts = server.split("|")
captured_roles = []
for role in user_roles:
if bleach.clean(self.get_argument(str(role), "")) == "on":
captured_roles.append(role)
if not server_name:
self.redirect("/panel/error?error=Server name cannot be empty!")
return
if import_type == "import_jar":
if self.helper.is_subdir(
import_server_path, self.controller.project_root
):
self.redirect(
"/panel/error?error=Loop Error: The selected path will cause"
" an infinite copy loop. Make sure Crafty's directory is not"
" in your server path."
)
return
good_path = self.controller.verify_jar_server(
import_server_path, import_server_exe
)
if not good_path:
self.redirect(
"/panel/error?error=Server path or Server Jar not found!"
)
return
new_server_id = self.controller.import_bedrock_server(
server_name,
import_server_path,
import_server_exe,
port,
exec_user["user_id"],
)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f'imported a jar server named "{server_name}"',
new_server_id,
self.get_remote_ip(),
)
elif import_type == "import_zip":
# here import_server_path means the zip path
zip_path = bleach.clean(self.get_argument("root_path"))
good_path = Helpers.check_path_exists(zip_path)
if not good_path:
self.redirect("/panel/error?error=Temp path not found!")
return
new_server_id = self.controller.import_bedrock_zip_server(
server_name,
zip_path,
import_server_exe,
port,
exec_user["user_id"],
)
if new_server_id == "false":
self.redirect(
f"/panel/error?error=Zip file not accessible! "
f"You can fix this permissions issue with"
f"sudo chown -R crafty:crafty {import_server_path} "
f"And sudo chmod 2775 -R {import_server_path}"
)
return
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f'imported a zip server named "{server_name}"',
new_server_id,
self.get_remote_ip(),
)
else:
new_server_id = self.controller.create_bedrock_server(
server_name,
exec_user["user_id"],
)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
"created a Bedrock " f'server named "{server_name}"',
# Example: Admin created a 1.16.5 Bukkit server named "survival"
new_server_id,
self.get_remote_ip(),
)
# These lines create a new Role for the Server with full permissions
# and add the user to it if he's not a superuser
if len(captured_roles) == 0:
if not superuser:
new_server_uuid = self.controller.servers.get_server_data_by_id(
new_server_id
).get("server_uuid")
role_id = self.controller.roles.add_role(
f"Creator of Server with uuid={new_server_uuid}",
exec_user["user_id"],
)
self.controller.server_perms.add_role_server(
new_server_id, role_id, "11111111"
)
self.controller.users.add_role_to_user(
exec_user["user_id"], role_id
)
else:
for role in captured_roles:
role_id = role
self.controller.server_perms.add_role_server(
new_server_id, role_id, "11111111"
)
self.controller.servers.stats.record_stats()
self.redirect("/panel/dashboard")
if page == "steam_cmd_step1":
if not superuser and not self.controller.crafty_perms.can_create_server(
exec_user["user_id"]
):
self.redirect(
"/panel/error?error=Unauthorized access: "
"not a server creator or server limit reached"
)
return
if not superuser:
user_roles = self.controller.roles.get_all_roles()
else:
user_roles = self.get_user_roles()
app_id = bleach.clean(self.get_argument("steam_server", ""))
server_name = bleach.clean(self.get_argument("server_name", ""))
captured_roles = []
for role in user_roles:
if bleach.clean(self.get_argument(str(role), "")) == "on":
captured_roles.append(role)
if not server_name:
self.redirect("/panel/error?error=Server name cannot be empty!")
return
new_server_id = self.controller.create_steam_server(
app_id,
server_name,
exec_user["user_id"],
)
# These lines create a new Role for the Server with full permissions
# and add the user to it if he's not a superuser
if len(captured_roles) == 0:
if not superuser:
new_server_uuid = self.controller.servers.get_server_data_by_id(
new_server_id
).get("server_uuid")
role_id = self.controller.roles.add_role(
f"Creator of Server with uuid={new_server_uuid}",
exec_user["user_id"],
)
self.controller.server_perms.add_role_server(
new_server_id, role_id, "11111111"
)
self.controller.users.add_role_to_user(
exec_user["user_id"], role_id
)
else:
for role in captured_roles:
role_id = role
self.controller.server_perms.add_role_server(
new_server_id, role_id, "11111111"
)
self.controller.servers.stats.record_stats()
self.redirect("/panel/dashboard")
try:
self.render(
template,
data=page_data,
translate=self.translator.translate,
)
except RuntimeError:
self.redirect("/panel/dashboard")

View File

@ -11,6 +11,9 @@ except ModuleNotFoundError as e:
class CustomStaticHandler(tornado.web.StaticFileHandler):
def validate_absolute_path(self, root: str, absolute_path: str) -> Optional[str]:
# This is for the mobile app service worker
if self.request.path.find("service-worker.js") != -1:
self.set_header("Service-Worker-Allowed", "/")
try:
return super().validate_absolute_path(root, absolute_path)
except tornado.web.HTTPError as error:

View File

@ -15,13 +15,11 @@ from app.classes.models.management import HelpersManagement
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 (
ServersStats,
NodeStats,
@ -48,13 +46,14 @@ class Webserver:
controller: Controller
helper: Helpers
def __init__(self, helper, controller, tasks_manager):
def __init__(self, helper, controller, tasks_manager, file_helper):
self.ioloop = None
self.http_server = None
self.https_server = None
self.helper = helper
self.controller = controller
self.tasks_manager = tasks_manager
self.file_helper = file_helper
self._asyncio_patch()
@staticmethod
@ -146,13 +145,12 @@ class Webserver:
"controller": self.controller,
"tasks_manager": self.tasks_manager,
"translator": self.helper.translation,
"file_helper": self.file_helper,
}
handlers = [
(r"/", DefaultHandler, handler_args),
(r"/panel/(.*)", PanelHandler, handler_args),
(r"/server/(.*)", ServerHandler, handler_args),
(r"/ajax/(.*)", AjaxHandler, handler_args),
(r"/files/(.*)", FileHandler, handler_args),
(r"/ws", SocketHandler, handler_args),
(r"/upload", UploadHandler, handler_args),
(r"/status", StatusHandler, handler_args),

View File

@ -25,11 +25,13 @@ class UploadHandler(BaseHandler):
controller: Controller = None,
tasks_manager=None,
translator=None,
file_helper=None,
):
self.helper = helper
self.controller = controller
self.tasks_manager = tasks_manager
self.translator = translator
self.file_helper = file_helper
def prepare(self):
# Class & Function Defination
@ -278,11 +280,11 @@ class UploadHandler(BaseHandler):
filename = self.request.headers.get("X-FileName", None)
full_path = os.path.join(path, filename)
if not Helpers.in_path(
if not self.helper.is_subdir(
full_path,
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
full_path,
):
logger.warning(
f"User {user_id} tried to upload a file to {server_id} "

View File

@ -18,12 +18,18 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
io_loop = None
def initialize(
self, helper=None, controller=None, tasks_manager=None, translator=None
self,
helper=None,
controller=None,
tasks_manager=None,
translator=None,
file_helper=None,
):
self.helper = helper
self.controller = controller
self.tasks_manager = tasks_manager
self.translator = translator
self.file_helper = file_helper
self.io_loop = tornado.ioloop.IOLoop.current()
def get_remote_ip(self):

View File

@ -1,5 +1,5 @@
{
"major": 4,
"minor": 0,
"sub": 23
"minor": 2,
"sub": 0
}

View File

@ -0,0 +1,40 @@
{
"background_color": "#222436",
"description": "Crafty Controller is a free and open-source Minecraft launcher and manager that allows users to start and administer Minecraft servers from a user-friendly interface.",
"dir": "ltr",
"display": "standalone",
"name": "Crafty Controller",
"orientation": "any",
"scope": "/",
"short_name": "Crafty",
"start_url": "/",
"theme_color": "#222436",
"categories": ["utilities"],
"icons": [
{
"src": "/static/assets/images/Crafty_4-0_Logo_square.ico",
"type": "image/x-icon",
"sizes":"128x128"
},
{
"src": "/static/assets/images/Crafty_4-0.png",
"type": "image/png",
"sizes": "144x144",
"purpose": "any"
},
{
"src": "/static/assets/images/crafty-logo-square-1024.png",
"type": "image/png",
"sizes": "1024x1024",
"purpose": "any"
},
{
"src": "/static/assets/images/crafty-logo-square-96.png",
"type": "image/png",
"sizes": "96x96",
"purpose": "any"
}
],
"lang": "en",
"prefer_related_applications": false
}

View File

@ -134,4 +134,59 @@ body {
.accordion .card {
margin-bottom: 0px;
}
}
.bootbox-body {
text-align: center;
}
/**************************************************************/
/* CSS for Froms Displays */
/**************************************************************/
div>.input-group>.custom-file-input {
position: relative !important;
-webkit-box-flex: 1 !important;
-ms-flex: 1 1 auto !important;
flex: 1 1 auto !important;
width: 1% !important;
margin-bottom: 0 !important;
border: 1px solid var(--outline);
}
div>.input-group>.form-control-file {
position: relative !important;
-webkit-box-flex: 1 !important;
-ms-flex: 1 1 auto !important;
flex: 1 1 auto !important;
width: 1% !important;
margin-bottom: 0 !important;
border: 1px solid var(--outline);
}
.custom-picker {
border: 1px solid var(--outline);
}
div>.input-group>.form-control {
position: relative !important;
-webkit-box-flex: 1 !important;
-ms-flex: 1 1 auto !important;
flex: 1 1 auto !important;
width: 1% !important;
margin-bottom: 0 !important;
border: 1px solid var(--outline);
}
.input-group>.input-group-append>button.upload-button {
height: calc(1.5em + 0.75rem + 2px);
}
.no-scroll {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.no-scroll::-webkit-scrollbar {
display: none;
}
/**************************************************************/

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 683.6 143.8" style="enable-background:new 0 0 683.6 143.8;" xml:space="preserve">
<style type="text/css">
.st0{opacity:0.85;fill:#FFFFFF;enable-background:new ;}
.st1{opacity:0.85;}
.st2{fill:#FFFFFF;}
.st3{fill:none;}
.st4{fill:url(#SVGID_1_);}
.st5{fill:url(#SVGID_00000137122815686618769650000009047437546445953421_);}
.st6{fill:url(#SVGID_00000170963539203169094570000007184871682409824703_);}
.st7{fill:url(#SVGID_00000169549353698428389090000007910489870824235905_);}
.st8{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_00000029754379306852418700000008865188217784465572_);}
</style>
<path class="st0" d="M175.8,111.5h17.6v3.8h-13.2v8.9h12.1v3.7h-12.1v11.8h-4.4V111.5z"/>
<path class="st0" d="M196.3,119.1h4.2v3.5h0.1c0.4-2.3,2.4-3.9,4.7-3.9c0.5,0,1,0.1,1.5,0.2v3.9c-0.6-0.2-1.3-0.3-1.9-0.2
c-2.7,0-4.4,1.8-4.4,4.8v12.3h-4.2L196.3,119.1z"/>
<path class="st0" d="M207.2,129.4L207.2,129.4c0-6.6,3.9-10.6,9.7-10.6s9.8,4,9.8,10.6l0,0c0,6.6-3.9,10.7-9.8,10.7
S207.2,136,207.2,129.4z M222.4,129.4L222.4,129.4c0-4.5-2.2-7.1-5.5-7.1s-5.4,2.6-5.4,7.1l0,0c0,4.5,2.2,7.2,5.5,7.2
S222.4,133.9,222.4,129.4L222.4,129.4z"/>
<path class="st0" d="M229.6,119.1h4.2v3.2h0.1c1-2.3,3.2-3.7,5.7-3.6c2.6-0.2,5,1.5,5.7,4h0.1c1.1-2.5,3.6-4.1,6.4-4
c4.1,0,6.7,2.7,6.7,6.8v14.1h-4.2v-13.1c0-2.7-1.4-4.2-3.9-4.2c-2.3,0-4.2,1.8-4.3,4.2c0,0.1,0,0.2,0,0.3v12.9H242v-13.4
c0.2-2-1.3-3.8-3.3-3.9c-0.2,0-0.4,0-0.5,0c-2.4,0-4.3,2-4.3,4.3c0,0.1,0,0.2,0,0.3v12.7h-4.2L229.6,119.1z"/>
<g id="Layer_2_00000138553854520646606810000012156271018779627156_" class="st1">
<g id="Layer_1-2">
<path class="st2" d="M343.7,139.9c-6.9,0-12.5-5.6-12.5-12.5s5.6-12.5,12.5-12.5c2.1,0,4.2,0.5,6,1.5c1.8,1,3.3,2.4,4.3,4.1
l-4.1,2.4c-0.6-1.1-1.5-1.9-2.5-2.5c-3.1-1.6-6.8-1.1-9.4,1.3c-1.5,1.5-2.2,3.6-2.1,5.7c-0.1,2.1,0.7,4.1,2.1,5.7
c1.5,1.5,3.5,2.3,5.7,2.2c1.3,0,2.6-0.3,3.7-0.9c1.1-0.6,2-1.4,2.5-2.5l4.1,2.4c-1,1.7-2.5,3.2-4.3,4.1
C347.8,139.4,345.8,139.9,343.7,139.9z"/>
<path class="st2" d="M361.4,122.3v3c0.3-1,1.1-1.9,2-2.5c1-0.6,2.1-0.9,3.2-0.8v4.9c-1.3-0.2-2.6,0.1-3.6,0.8
c-1.1,0.8-1.7,2.2-1.6,3.5v8.2H357v-17.2H361.4z"/>
<path class="st2" d="M381.6,124.3v-2h4.4v17.2h-4.4v-2c-1.4,1.7-3.4,2.6-5.6,2.5c-2.2,0-4.4-0.9-5.9-2.6c-1.6-1.8-2.5-4.1-2.4-6.5
c-0.1-2.4,0.8-4.7,2.4-6.4c1.5-1.7,3.6-2.7,5.9-2.7C378.1,121.7,380.2,122.6,381.6,124.3z M373.4,134.3c1.9,1.8,4.9,1.8,6.8,0
c0.9-0.9,1.4-2.2,1.4-3.5c0.1-1.3-0.4-2.6-1.4-3.5c-1.9-1.8-4.9-1.8-6.8,0c-0.9,0.9-1.4,2.2-1.3,3.5
C372,132.1,372.5,133.4,373.4,134.3z"/>
<path class="st2" d="M399.2,115v4.2c-2.4-0.2-3.6,0.8-3.7,2.9v0.2h3.6v4.3h-3.6v12.9h-4.4v-12.9h-2.5v-4.2h2.5v-0.2
c-0.2-2,0.6-4.1,2-5.5C394.5,115.3,396.6,114.8,399.2,115z"/>
<path class="st2" d="M411.6,122.3v4.2h-3.9v7.1c0,0.5,0.1,1,0.5,1.3c0.4,0.3,0.8,0.5,1.3,0.5c0.7,0,1.4,0,2.1,0v4
c-3,0.3-5.1,0.1-6.4-0.8s-1.9-2.5-1.9-4.9v-7.1h-3v-4.2h3v-3.5l4.4-1.3v4.8L411.6,122.3z"/>
<path class="st2" d="M427.2,124.3v-2h4.4v17.2h-4.4v-2c-1.4,1.7-3.4,2.6-5.6,2.5c-2.2,0-4.4-0.9-5.9-2.6c-1.6-1.8-2.5-4.1-2.4-6.5
c-0.1-2.4,0.8-4.7,2.4-6.4c1.5-1.7,3.6-2.7,5.9-2.7C423.8,121.7,425.9,122.6,427.2,124.3z M419.1,134.3c1.9,1.8,4.9,1.8,6.8,0
c0.9-0.9,1.4-2.2,1.4-3.5c0-1.3-0.4-2.5-1.4-3.5c-1.9-1.8-4.9-1.8-6.8,0c-0.9,0.9-1.4,2.2-1.3,3.5
C417.7,132.1,418.2,133.4,419.1,134.3L419.1,134.3z"/>
<path class="st2" d="M440.1,122.3v3c0.4-1,1.1-1.9,2-2.5c1-0.6,2.1-0.9,3.2-0.8v4.9c-1.3-0.2-2.6,0.1-3.6,0.8
c-1.1,0.8-1.7,2.2-1.6,3.5v8.2h-4.4v-17.2H440.1z"/>
<path class="st2" d="M461.9,137.3c-3.6,3.6-9.3,3.6-12.9,0s-3.6-9.3,0-12.9l0,0c3.6-3.5,9.3-3.5,12.9,0.1c1.7,1.7,2.6,4,2.6,6.4
C464.5,133.3,463.6,135.6,461.9,137.3z M452.1,134.3c1.9,1.8,4.8,1.8,6.7,0c1.8-1.9,1.8-4.9,0-6.8c-1.9-1.8-4.8-1.8-6.7,0
C450.3,129.4,450.3,132.3,452.1,134.3L452.1,134.3z"/>
<path class="st2" d="M320,137.6l-2.9-20.3c-0.4-2.7-2.7-4.7-5.5-4.7h-9c-0.3,0-0.5,0.2-0.7,0.4l-0.9,2H292l-0.9-2
c-0.1-0.3-0.4-0.4-0.7-0.4h-9c-2.7,0-5.1,2-5.5,4.7l-2.9,20.3c-0.4,3,1.7,5.8,4.7,6.2c0,0,0,0,0,0l0,0c0.3,0,0.5,0.1,0.8,0.1h36
c3,0,5.5-2.5,5.5-5.5l0,0C320,138.1,320,137.8,320,137.6z M287.1,130c-2.7,0-4.9-2.2-4.9-4.9c0-2.7,2.2-4.9,4.9-4.9
c2.7,0,4.9,2.2,4.9,4.9c0,0,0,0,0,0l0,0C292,127.8,289.8,130,287.1,130z M296.5,138c-2.7,0-4.9-2.2-4.9-4.9h9.8
C301.4,135.8,299.3,138,296.5,138L296.5,138L296.5,138z M305.9,130c-2.7,0-4.9-2.2-4.9-4.9c0-2.7,2.2-4.9,4.9-4.9
c2.7,0,4.9,2.2,4.9,4.9c0,0,0,0,0,0l0,0C310.8,127.8,308.6,130,305.9,130L305.9,130z"/>
</g>
</g>
<path class="st2" d="M133.1,19.2H9.7c-1.8,0-3.2-1.4-3.2-3.2V3.2C6.5,1.5,7.9,0,9.7,0h123.4c1.8,0,3.2,1.4,3.2,3.2V16
C136.3,17.8,134.9,19.2,133.1,19.2"/>
<path class="st2" d="M23.6,36.7c-3.4,0-6.7,1.6-8.8,4.3c-2.9,3.6-4.1,8.3-3.2,12.8l9.2,51.9c1.2,6.6,6.2,11.4,12.1,11.4H110
c5.8,0,10.9-4.8,12.1-11.4l9.2-51.9c0.8-4.5-0.4-9.2-3.3-12.8c-2.1-2.7-5.4-4.3-8.8-4.3H23.6z M110,128.3H32.8
c-11.3,0-21-8.7-23.1-20.7L0.5,55.8c-1.5-7.8,0.6-15.9,5.7-22c4.3-5.2,10.7-8.3,17.4-8.3h95.6c6.8,0.1,13.1,3.1,17.4,8.3
c5.1,6.1,7.2,14.2,5.7,22l-9.2,51.9C130.9,119.7,121.2,128.4,110,128.3"/>
<path class="st2" d="M120.8,23.8v-2.2c2,0,3.5-1.6,3.5-3.6c0-1.8-1.5-3.4-3.3-3.5H21.6c-2,0.1-3.5,1.8-3.4,3.7
c0.1,1.8,1.5,3.3,3.4,3.4v2.2c-3.2-0.1-5.7-2.8-5.6-6c0.1-3,2.5-5.4,5.6-5.6h99.2c3.2-0.1,5.9,2.4,6,5.6s-2.4,5.9-5.6,6
C121.1,23.8,121,23.8,120.8,23.8"/>
<path class="st2" d="M120.8,33.1H21.6c-3.2,0-5.8-2.6-5.8-5.8c0-3.2,2.6-5.8,5.8-5.8v2.2c-2,0.1-3.5,1.8-3.4,3.7
c0.1,1.8,1.5,3.3,3.4,3.4h99.2c2,0.1,3.7-1.3,3.8-3.3c0.1-2-1.3-3.7-3.3-3.8c-0.1,0-0.2,0-0.3,0h-0.2v-2.2c3.2-0.1,5.9,2.4,6,5.6
s-2.4,5.9-5.6,6C121.1,33.1,121,33.1,120.8,33.1"/>
<path class="st2" d="M21.6,21.5l36.1,1.1l-36.1,1.1V21.5z"/>
<path class="st2" d="M125.5,23.8l-45.1-1.1l45.1-1.1V23.8z"/>
<rect x="-2.5" y="-1.1" class="st3" width="571.3" height="131.4"/>
<path class="st2" d="M163.8,91.7l7.3-10.9c5.8,5.5,14.3,9.3,22.3,9.3c7.1,0,13.1-3.3,13.1-8.3c0-6-8.1-7.9-15.4-9.6
c-13.7-3.2-24.8-9.8-24.8-22.3c0-12.7,11.1-21,27.1-21c10.7,0,19.4,3.7,24.7,8.9l-6.6,10.8c-4-3.9-11.2-6.9-18.3-6.9
s-12.2,3.2-12.2,7.7c0,5.5,7.4,7.9,14.1,9.3s26.2,6.2,26.2,22.5c0,12.8-12.2,21.6-27.8,21.6C182.6,102.8,171.1,98.4,163.8,91.7z"/>
<path class="st2" d="M281.7,80.1h-40.9c1.9,6.6,7.5,10.9,15.1,10.9c5.6,0.1,10.9-2.3,14.5-6.5l9,7.9c-5.5,6.5-14,10.5-23.9,10.5
c-16.8,0-29.3-12-29.3-27.8c0-15.6,12.1-27.4,28-27.4S282,59.4,282,75.3C282,76.9,281.9,78.5,281.7,80.1z M240.8,70.3h26.9
c-1.7-6.6-6.9-10.9-13.4-10.9C247.7,59.4,242.5,63.8,240.8,70.3L240.8,70.3z"/>
<path class="st2" d="M321.3,48v13.9h-2.3c-9.6,0-15.2,5.7-15.2,14.7v25h-13.4V48.9h13.5v6.8c3.6-4.8,9.2-7.7,15.2-7.7L321.3,48z"/>
<path class="st2" d="M381.9,48.9L360,101.6h-13.9l-21.9-52.8h15.3l13.8,35.9L367,48.9H381.9z"/>
<path class="st2" d="M437.1,80.1h-40.9c1.9,6.6,7.5,10.9,15.1,10.9c5.6,0.1,10.9-2.3,14.5-6.5l9,7.9c-5.5,6.5-14,10.5-23.9,10.5
c-16.8,0-29.3-12-29.3-27.8c0-15.6,12.1-27.4,28-27.4s27.7,11.8,27.7,27.7C437.4,76.9,437.3,78.5,437.1,80.1z M396.1,70.3H423
c-1.7-6.6-6.9-10.9-13.4-10.9S397.7,63.8,396.1,70.3L396.1,70.3z"/>
<path class="st2" d="M476.7,48v13.9h-2.2c-9.6,0-15.2,5.7-15.2,14.7v25h-13.5V48.9h13.5v6.8c3.6-4.8,9.2-7.7,15.2-7.7L476.7,48z"/>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="870.0443" y1="434.2369" x2="907.1767" y2="465.2789" gradientTransform="matrix(1 0 0 1 -374.6 -381.3801)">
<stop offset="0" style="stop-color:#FEAF6F"/>
<stop offset="1" style="stop-color:#FD5E83"/>
</linearGradient>
<path class="st4" d="M492.5,100.6V87c3.2,1.4,6.6,2.1,10,2.2c7.3,0,11.8-3.9,11.8-10.9v-48h14.3V79c0,15-9.8,23.9-24.5,23.9
C500,102.9,496.1,102.1,492.5,100.6z"/>
<linearGradient id="SVGID_00000162328622213414588160000008200821717462734513_" gradientUnits="userSpaceOnUse" x1="920.7661" y1="434.5518" x2="972.3098" y2="477.6348" gradientTransform="matrix(1 0 0 1 -374.6 -381.3801)">
<stop offset="0" style="stop-color:#FEAF6F"/>
<stop offset="1" style="stop-color:#FD5E83"/>
</linearGradient>
<path style="fill:url(#SVGID_00000162328622213414588160000008200821717462734513_);" d="M593.2,48.9v52.8h-13.5v-6.3
c-4.4,4.9-10.6,7.6-17.2,7.5c-14.7,0-25.8-11.9-25.8-27.6s11.1-27.6,25.8-27.6c6.5-0.1,12.8,2.7,17.2,7.5v-6.3L593.2,48.9z
M579.8,75.2c0-8-6.6-14.5-14.6-14.5c-8,0-14.5,6.6-14.5,14.6c0,8,6.5,14.4,14.5,14.5c7.9,0.2,14.4-6,14.6-13.9
C579.8,75.7,579.8,75.5,579.8,75.2z"/>
<linearGradient id="SVGID_00000026849485640012965730000014957007722205225107_" gradientUnits="userSpaceOnUse" x1="973.2171" y1="437.9167" x2="1007.0711" y2="466.2133" gradientTransform="matrix(1 0 0 1 -374.6 -381.3801)">
<stop offset="0" style="stop-color:#FEAF6F"/>
<stop offset="1" style="stop-color:#FD5E83"/>
</linearGradient>
<path style="fill:url(#SVGID_00000026849485640012965730000014957007722205225107_);" d="M635.9,48v13.9h-2.3
c-9.6,0-15.2,5.7-15.2,14.7v25H605V48.9h13.4v6.8c3.6-4.8,9.2-7.7,15.2-7.7L635.9,48z"/>
<linearGradient id="SVGID_00000011000279650532451330000005619277557075874698_" gradientUnits="userSpaceOnUse" x1="1015.3561" y1="439.477" x2="1056.9301" y2="474.2302" gradientTransform="matrix(1 0 0 1 -374.6 -381.3801)">
<stop offset="0" style="stop-color:#FEAF6F"/>
<stop offset="1" style="stop-color:#FD5E83"/>
</linearGradient>
<path style="fill:url(#SVGID_00000011000279650532451330000005619277557075874698_);" d="M638.7,94.8l6.5-8.9
c4.2,3.8,9.7,5.9,15.4,5.9c5.4,0,9.3-1.8,9.3-5c0-3.5-4.6-4.8-10.3-6.1c-8.4-1.9-19.2-4.5-19.2-16.5c0-11.2,9.8-16.7,21.5-16.7
c7.4-0.1,14.6,2.3,20.5,6.9l-6.5,9c-3.9-3.1-8.7-4.8-13.7-4.9c-4.6,0-8.3,1.5-8.3,4.5c0,3.5,4.4,4.7,10.3,5.9
c8.4,1.9,19.2,4.5,19.2,16.4c0,11.2-9.9,17.3-22.6,17.3C652.9,102.9,644.9,100.1,638.7,94.8z"/>
<linearGradient id="SVGID_00000176732902084481618460000012775063734620060048_" gradientUnits="userSpaceOnUse" x1="408.7259" y1="431.5905" x2="485.4144" y2="495.6844" gradientTransform="matrix(1 0 0 1 -374.6 -381.3801)">
<stop offset="0" style="stop-color:#FEAF6F"/>
<stop offset="1" style="stop-color:#FD5E83"/>
</linearGradient>
<path style="fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_00000176732902084481618460000012775063734620060048_);" d="
M124.5,62c-12.7,0.9-27,5.5-35.7,12.3c-38.7,30.3-69.2-6.6-69.3-6.6l6.8,36.8c0.8,4.3,4.6,7.5,9,7.5l73,0.2c4.5,0,8.3-3.2,9.1-7.6
L124.5,62z"/>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 142.71 128.36"><defs><style>.cls-1{fill:#fff;}.cls-2{fill-rule:evenodd;fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" x1="408.73" y1="431.59" x2="485.41" y2="495.68" gradientTransform="translate(-374.6 -381.38)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#feaf6f"/><stop offset="1" stop-color="#fd5e83"/></linearGradient></defs><path class="cls-1" d="M133.09,19.17H9.67A3.24,3.24,0,0,1,6.46,16V3.24A3.24,3.24,0,0,1,9.7,0H133.09a3.25,3.25,0,0,1,3.25,3.24V16a3.25,3.25,0,0,1-3.25,3.24"/><path class="cls-1" d="M23.61,36.67A11.41,11.41,0,0,0,14.8,41a15.79,15.79,0,0,0-3.25,12.8l9.18,51.92c1.17,6.62,6.25,11.42,12.06,11.42H110c5.82,0,10.89-4.8,12.06-11.42l9.18-51.91A15.86,15.86,0,0,0,128,41a11.5,11.5,0,0,0-8.82-4.33ZM110,128.35H32.8c-11.27,0-21-8.7-23.12-20.69L.46,55.75a26.72,26.72,0,0,1,5.71-22,22.77,22.77,0,0,1,17.41-8.34h95.56a22.8,22.8,0,0,1,17.41,8.34,26.79,26.79,0,0,1,5.71,22l-9.19,51.91c-2.12,12-11.84,20.7-23.12,20.7"/><path class="cls-1" d="M120.8,23.76V21.51A3.56,3.56,0,0,0,121,14.4H21.59a3.56,3.56,0,0,0,0,7.11v2.25a5.81,5.81,0,0,1,0-11.61H120.8a5.81,5.81,0,0,1,.48,11.61h-.48"/><path class="cls-1" d="M120.8,33.11H21.59a5.8,5.8,0,0,1,0-11.6v2.24a3.56,3.56,0,0,0,0,7.11H120.8a3.56,3.56,0,0,0,.52-7.1h-.52V21.51a5.81,5.81,0,0,1,.48,11.61,3.84,3.84,0,0,1-.48,0"/><path class="cls-1" d="M21.59,21.51l36.13,1.13L21.59,23.76Z"/><path class="cls-1" d="M125.46,23.76,80.35,22.64l45.11-1.13Z"/><path class="cls-2" d="M124.46,62c-12.72.93-27,5.55-35.7,12.34-38.69,30.34-69.25-6.6-69.28-6.58l6.75,36.83a9.16,9.16,0,0,0,9,7.52l73,.16a9.17,9.17,0,0,0,9.06-7.64Z"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,137 @@
function show_file_tree() {
$("#dir_select").modal();
}
function getDirView(event = false) {
if (event) {
try {
let path = event.target.parentElement.getAttribute('data-path');
if (event.target.parentElement.classList.contains('clicked')) {
if ($(`#${path}span`).hasClass('files-tree-title')) {
$(`#${path}ul`).toggleClass("d-block");
$(`#${path}span`).toggleClass("tree-caret-down");
}
return;
} else {
getTreeView(path);
}
} catch {
console.log("Well that failed");
}
} else if ($("#root_files_button").hasClass("clicked")) {
getTreeView($("#zip_server_path").val(), true);
} else {
getTreeView($("#file-uploaded").val(), true, true);
}
}
async function getTreeView(path, unzip = false, upload = false) {
const token = getCookie("_xsrf");
console.log("IN TREE VIEW")
console.log({ "page": "import", "folder": path, "upload": upload, "unzip": unzip });
let res = await fetch(`/api/v2/import/file/unzip/`, {
method: 'POST',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({ "page": "import", "folder": path, "upload": upload, "unzip": unzip }),
});
let responseData = await res.json();
if (responseData.status === "ok") {
console.log(responseData);
process_tree_response(responseData);
let x = document.querySelector('.bootbox');
if (x) {
x.remove()
}
x = document.querySelector('.modal-backdrop');
if (x) {
x.remove()
}
show_file_tree();
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
function process_tree_response(response) {
const styles = window.getComputedStyle(document.getElementById("lower_half"));
//If this value is still hidden we know the user is executing a zip import and not an upload
if (styles.visibility === "hidden") {
document.getElementById('zip_submit').disabled = false;
} else {
document.getElementById('upload_submit').disabled = false;
}
let path = response.data.root_path.path;
$(".root-input").val(response.data.root_path.path);
let text = `<ul class="tree-nested d-block" id="${path}ul">`;
Object.entries(response.data).forEach(([key, value]) => {
if (key === "root_path" || key === "db_stats") {
//continue is not valid in for each. Return acts as a continue.
return;
}
let dpath = value.path;
let filename = key;
if (value.dir) {
text += `<li class="tree-item" id="${dpath}li" data-path="${dpath}">
<div id="${dpath}" data-path="${dpath}" data-name="${filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="radio" name="root_path" value="${dpath}">
<span id="${dpath}span" class="files-tree-title" data-path="${dpath}" data-name="${filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
${filename}
</span>
</input></div><li>`
} else {
text += `<li
class="d-block tree-ctx-item tree-file"
data-path="${dpath}"
data-name="${filename}"
onclick="" id="${dpath}li"><input type='radio' class="checkBoxClass d-none file-check" name='root_path' value="${dpath}" disabled><span style="margin-right: 6px;">
<i class="far fa-file"></i></span></input>${filename}</li>
`
}
});
text += `</ul>`;
if (response.data.root_path.top) {
try {
document.getElementById('main-tree-div').innerHTML += text;
document.getElementById('main-tree').parentElement.classList.add("clicked");
} catch {
document.getElementById('files-tree').innerHTML = text;
}
} else {
try {
document.getElementById(path + "span").classList.add('tree-caret-down');
document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked");
} catch {
console.log("Bad")
}
let toggler = document.getElementById(path + "span");
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "span").addEventListener("click", function caretListener() {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
});
}
}
}
function getToggleMain(event) {
const path = event.target.parentElement.getAttribute('data-path');
document.getElementById("files-tree").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
document.getElementById(path + "span").classList.toggle("tree-caret");
}

View File

@ -0,0 +1,41 @@
// This is the "Offline page" service worker
importScripts(
"https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js"
);
const CACHE = "crafty-controller";
// TODO: replace the following with the correct offline fallback page i.e.: const offlineFallbackPage = "offline.html";
const offlineFallbackPage = "/offline";
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting();
}
});
if (workbox.navigationPreload.isSupported()) {
workbox.navigationPreload.enable();
}
// self.addEventListener('fetch', (event) => {
// if (event.request.mode === 'navigate') {
// event.respondWith((async () => {
// try {
// const preloadResp = await event.preloadResponse;
// if (preloadResp) {
// return preloadResp;
// }
// const networkResp = await fetch(event.request);
// return networkResp;
// } catch (error) {
// const cache = await caches.open(CACHE);
// const cachedResp = await cache.match(offlineFallbackPage);
// return cachedResp;
// }
// })());
// }
// });

View File

@ -1,14 +0,0 @@
{% for item in data['notify_data'] %}
<!-- <div class="hidden">{{ item['id'] }}</div>-->
<div class="event">
<p class="font-weight-medium">{{ item['title'] }}</p>
<a class="d-flex align-items-center">
<div class="badge badge-primary">{{ item['date'] }}</div>
<span class="text-muted ml-2">{{ item['desc'] }}</span>
</a>
</div>
{% end %}

View File

@ -19,6 +19,16 @@
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css">
<link rel="stylesheet" href="/static/assets/css/crafty.css">
<link rel="manifest" href="/static/assets/crafty.webmanifest">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Crafty">
<link rel="apple-touch-icon" href="../static/assets/images/Crafty_4-0.png">
<link rel="shortcut icon" sizes="192x192" href="../static/assets/images/Crafty_4-0.png">
<!-- endinject -->
<!-- Plugin css for this page-->
@ -247,7 +257,7 @@
const sendWssError = () => wsOpen || warn(
'WebSockets are required for Crafty to work. This websocket connection has been closed. Are you using a reverse proxy?',
'https://wiki.craftycontrol.com/en/4/docs/Reverse%20Proxy%20Examples',
'https://docs.craftycontrol.com/pages/getting-started/proxies/',
'wssError'
)
@ -402,20 +412,26 @@
});
}
function eulaAgree(server_id, command) {
async function eulaAgree(server_id, command) {
//< !--this getCookie function is in base.html-- >
var token = getCookie("_xsrf");
const token = getCookie("_xsrf");
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/eula?id=' + server_id,
success: function (data) {
console.log("got response:");
console.log(data);
location.reload();
}
let res = await fetch(`/api/v2/servers/${server_id}/action/eula/`, {
method: 'POST',
headers: {
'X-XSRFToken': token
},
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.reload();
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
}
@ -526,6 +542,14 @@
});
});
$(document).ready(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/static/assets/js/shared/service-worker.js', { scope: '/' })
.then(function (registration) {
console.log('Service Worker Registered');
});
}
});
</script>
{% block js %}

View File

@ -14,6 +14,13 @@
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css">
<link rel="stylesheet" href="/static/assets/vendors/fontawesome6/css/all.css">
<link rel="stylesheet" href="/static/assest/css/crafty.css">
<link rel="manifest" href="/static/assets/crafty.webmanifest">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Crafty">
<link rel="apple-touch-icon" href="../static/assets/images/Crafty_4-0.png">
<!-- endinject -->
<!-- Plugin css for this page -->
<!-- End Plugin css for this page -->

View File

@ -100,7 +100,7 @@
</li>
<li class="nav-item">
<a class="nav-link" href="https://wiki.craftycontrol.com" target="_blank">
<a class="nav-link" href="https://docs.craftycontrol.com" target="_blank">
<i class="fas fa-book"></i> &nbsp;
<span class="menu-title">{{ translate('sidebar', 'documentation', data['lang']) }}</span>
</a>
@ -109,7 +109,7 @@
<li class="nav-item">
<a class="nav-link" href="/panel/wiki">
<i class="fa fa-info-circle"></i> &nbsp;
<span class="menu-title">Wiki</span>
<span class="menu-title">{{ translate('sidebar', 'inApp', data['lang']) }}</span>
</a>
</li>

View File

@ -1,27 +1,32 @@
<ul class="navbar-nav ml-auto">
<li class="nav-item dropdown">
<a class="nav-link count-indicator">
<a class="nav-link count-indicator dropdown-toggle" id="notifDropdown" href="#" data-toggle="dropdown"
aria-expanded="false">
<i class="fas fa-broadcast-tower
{% if data.get('update_available') %}
text-danger
{% end %}
"></i>
<!-- <span class="count bg-success">3</span>-->
</a>
"></i><span id="notif-count" class="badge badge-notify"></span> </a>
<div class="dropdown-menu dropdown-menu-right navbar-dropdown notif-div" style="width: 40vw; max-height: 80vh;" aria-labelledby="notifDropdown">
<ul style="padding-top: 10px;" id="announcements">
</ul>
</div>
</li>
<li class="nav-item dropdown">
<a class="nav-link count-indicator" href="/panel/panel_config">
<a class="nav-link" href="/panel/panel_config">
<i class="fas fa-cogs"></i>
</a>
</li>
<li class="nav-item dropdown user-dropdown">
<a class="nav-link dropdown-toggle" id="UserDropdown" href="#" data-toggle="dropdown" aria-expanded="false">
<img class="img-xs rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}" alt="Profile image"> </a>
<img class="img-xs rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}"
alt="Profile image"> </a>
<div class="dropdown-menu dropdown-menu-right navbar-dropdown" aria-labelledby="UserDropdown">
<div class="dropdown-header text-center">
<img class="img-md rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}" alt="Profile image">
<img class="img-md rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}"
alt="Profile image">
<p class="mb-1 mt-3 font-weight-semibold">{{ data['user_data']['username'] }}</p>
<p class="font-weight-light text-muted mb-0">Roles: </p>
{% for r in data['user_role'] %}
@ -33,27 +38,130 @@
<p class="font-weight-light text-muted mb-0">Email: {{ data['user_data']['email'] }}</p>
</div>
{% if data['user_data']['preparing'] %}
<span class="dropdown-item" id="support_progress"><i class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs', data['lang']) }}<br><br></span>
<span class="dropdown-item" id="support_progress"><i
class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs',
data['lang']) }}<br><br></span>
<span class="dropdown-item" id="support_progress">
<div class="support_progress" style="height: 15px; width: 100%;">
<div class="progress-bar progress-bar-striped progress-bar-animated" id="logs_progress_bar" role="progressbar" style="width:0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
<div class="progress-bar progress-bar-striped progress-bar-animated" id="logs_progress_bar" role="progressbar"
style="width:0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
</div>
</span>
{% else %}
<a class="dropdown-item" id="support_logs"><i class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs', data['lang']) }}</i></a>
<a class="dropdown-item" id="support_logs"><i
class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs',
data['lang']) }}</i></a>
{% end %}
{% if data['superuser'] %}
<a class="dropdown-item" href="/panel/activity_logs"><i class="dropdown-item-icon mdi mdi-calendar-check-outline text-primary"></i>{{ translate('notify', 'activityLog', data['lang']) }}</a>
<a class="dropdown-item" href="/panel/activity_logs"><i
class="dropdown-item-icon mdi mdi-calendar-check-outline text-primary"></i>{{ translate('notify',
'activityLog', data['lang']) }}</a>
{% end %}
<a class="dropdown-item" href="/logout"><i class="dropdown-item-icon mdi mdi-power text-primary"></i>{{ translate('notify', 'logout', data['lang']) }}</a>
<a class="dropdown-item" href="/logout"><i class="dropdown-item-icon mdi mdi-power text-primary"></i>{{
translate('notify', 'logout', data['lang']) }}</a>
</div>
</li>
</ul>
<style>
.badge-notify {
background: var(--purple);
position: absolute;
-moz-transform: translate(-70%, -70%);
/* For Firefox */
-ms-transform: translate(-70%, -70%);
/* for IE */
-webkit-transform: translate(-70%, -70%);
/* For Safari, Chrome, iOS */
-o-transform: translate(-70%, -70%);
}
.clear-button:hover {
cursor: pointer;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.notif-div::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.notif-div {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
</style>
<script>
function pfpError(image) {
image.onerror = "";
image.src = "/static/assets/images/faces-clipart/pic-3.png";
return true;
}
function updateAnnouncements(data) {
console.log(data)
let text = "";
for (let value of data) {
text += `<li class="card-header header-sm justify-content-between align-items-center" id="${value.id}"><p style="float: right;"><i data-id="${value.id}"class="clear-button fa-regular fa-x"></i></p><a style="color: var(--purple);" href=${value.link} target="_blank"><h6>${value.title}</h6><small><p>${value.date}</p></small><p>${value.desc}</p></li></a>`
}
if (data.length > 0) {
localStorage.setItem("notif-count", data.length);
$("#notif-count").html(data.length);
$("#announcements").html(text);
} else {
$("#announcements").html(`<p style='margin-top: 15px;' class='text-center'><i class="fa fa-bell-slash" aria-hidden="true"></i>
</p>`);
}
$(".clear-button").on("click", function (event) {
console.log("CLEAR BUTTON")
let uuid = event.target.getAttribute("data-id");
$(`#${uuid}`).remove();
send_clear(uuid);
let notif_count = localStorage.getItem("notif-count") - 1;
if (notif_count > 0) {
localStorage.setItem("notif-count", notif_count);
$("#notif-count").html(notif_count);
} else {
$("#announcements").html(`<p style='margin-top: 15px;' class='text-center'><i class="fa fa-bell-slash" aria-hidden="true"></i>
</p>`)
$("#notif-count").html("");
}
});
}
async function getAnnouncements() {
var token = getCookie("_xsrf");
let res = await fetch(`/api/v2/crafty/announcements/`, {
method: 'GET',
headers: {
'X-XSRFToken': token
},
});
let responseData = await res.json();
console.log(responseData);
if (responseData.status === "ok") {
updateAnnouncements(responseData.data)
} else {
updateAnnouncements("<li><p>Trouble Getting Annoucements</p></li>")
}
}
async function send_clear(uuid) {
var token = getCookie("_xsrf");
let body = JSON.stringify({ "id": uuid });
console.log(body)
let res = await fetch(`/api/v2/crafty/announcements/`, {
method: 'POST',
headers: {
'X-XSRFToken': token,
},
body: body,
});
let responseData = await res.json();
console.log(responseData);
if (responseData.status === "ok") {
return
} else {
bootbox.alert(responseData.error)
}
}
$(document).ready(function () {
getAnnouncements();
})
</script>

View File

@ -30,7 +30,12 @@
<div class="card-body">
{% if data['superuser'] %}
{% include "parts/crafty_config_list.html %}
<span class="d-none d-sm-block">
{% include "parts/crafty_config_list.html %}
</span>
<span class="d-block d-sm-none">
{% include "parts/m_crafty_config_list.html %}
</span>
{% end %}
<!-- Page Title Header Starts-->
@ -46,7 +51,6 @@
<!-- Page Title Header Ends-->
<form id="config-form" class="forms-sample" method="post" action="/panel/config_json">
{% raw xsrf_form_html() %}
{% for item in data['config-json'].items() %}
{% if item[0] == "reset_secrets_on_next_boot" %}
@ -69,11 +73,11 @@
</select>
{% elif item[0] == 'disabled_language_files' %}
<div class="input-group">
<button type="button" class="btn btn-outline-default custom-picker" onclick="$('option', $('#lang_select')).each(function(element) {
$(this).removeAttr('selected').prop('selected', false); $('.selectpicker').selectpicker('refresh')
});">{{ translate('panelConfig', 'enableLang', data['lang']) }}</button>
<select id="lang_select" class="form-control selectpicker show-tick" data-icon-base="fas"
data-tick-icon="fa-check" multiple data-style="custom-picker">
<button type="button" class="btn btn-outline-default custom-picker"
onclick="$('option', $('#lang_select')).each(function(element) {$(this).removeAttr('selected').prop('selected', false); $('.selectpicker').selectpicker('refresh')});">{{
translate('panelConfig', 'enableLang', data['lang']) }}</button>
<select id="lang_select" class="form-control selectpicker show-tick custom-picker"
data-icon-base="fas" data-tick-icon="fa-check" multiple data-style="custom-picker">
{% for lang in data['all_languages'] %}
{% if lang in item[1] %}
<option selected>{{lang}}</option>
@ -88,9 +92,9 @@
</div>
{% elif item[0] == 'monitored_mounts'%}
<div class="input-group">
<button type="button" class="btn btn-outline-default custom-picker" onclick="$('option', $('#mount_select')).each(function(element) {
$(this).removeAttr('selected').prop('selected', false); $('.selectpicker').selectpicker('refresh')
});">{{ translate('panelConfig', 'noMounts', data['lang']) }}</button>
<button type="button" class="btn btn-outline-default custom-picker"
onclick="$('option', $('#mount_select')).each(function(element) {$(this).removeAttr('selected').prop('selected', false); $('.selectpicker').selectpicker('refresh')});">{{
translate('panelConfig', 'noMounts', data['lang']) }}</button>
<select id="mount_select" class="form-control selectpicker show-tick" data-icon-base="fas"
data-tick-icon="fa-check" multiple data-style="custom-picker">
{% for mount in data['all_partitions'] %}
@ -106,7 +110,7 @@
hidden>{{','.join(item[1])}}</textarea>
</div>
{% elif isinstance(item[1], list) %}
<textarea value="{{','.join(item[1])}}" type="text" name="{{item[0]}}"
<textarea id="{{item[0]}}" value="{{','.join(item[1])}}" type="text" name="{{item[0]}}"
class="form-control list">{{','.join(item[1])}}</textarea>
{% elif isinstance(item[1], bool) %}
<div style="margin-left: 30px;">
@ -142,10 +146,6 @@
</div>
<style>
.custom-picker {
border: 1px solid var(--outline);
}
.dropdown-menu.inner {
display: inline-block !important;
}
@ -169,36 +169,66 @@
{% block js %}
<script>
$("#config-form").submit(function (e) {
let uuid = uuidv4();
var token = getCookie("_xsrf")
function replacer(key, value) {
if (key == "disabled_language_files") {
if (value == 0) {
return []
} else {
return value
}
}
if (typeof value == "boolean") {
return value
} else {
return (isNaN(value) ? value : +value);
}
}
$("#config-form").on("submit", async function (e) {
e.preventDefault();
$("#submit-status").html('<i class="fa fa-spinner fa-spin"></i>');
/* Convert multiple select to text list */
let selected_Lang = $('#lang_select').val();
$('#disabled_lang').val(selected_Lang);
const token = getCookie("_xsrf")
let configForm = document.getElementById("config-form");
let mounts = $('#mount_select').val();
$('#monitored_mounts').val(mounts);
let formData = new FormData(configForm);
formData.delete("disabled_lang");
formData.delete("lang_select");
let class_list = document.getElementsByClassName("list");
let form_json = convertFormToJSON($("#config-form"));
for (let i = 0; i < class_list.length; i++) {
let str = String($(class_list.item(i)).val())
form_json[$(class_list.item(i)).attr("name")] = uuid + "," + str.replace(/\s/g, '');
};
form_json['uuid'] = uuid;
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
dataType: "text",
url: '/panel/config_json',
data: form_json,
success: function (data) {
$("#submit-status").html('<i class="fa fa-check"></i>');
},
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
//We need to make sure these are sent regardless of whether or not they're checked
formDataObject.disabled_language_files = $('#lang_select').val();
formDataObject.monitored_mounts = $('#mount_select').val();
formDataObject.keywords = $('#keywords').val().split(",");
$('#config-form input[type="radio"]:checked').each(function () {
if ($(this).val() == 'True') {
formDataObject[this.name] = true;
} else {
formDataObject[this.name] = false;
}
});
console.log(formDataObject);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer);
console.log(formDataJsonString);
let res = await fetch(`/api/v2/crafty/config/`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
$("#submit-status").html('<i class="fa fa-check"></i>');
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
});
function uuidv4() {
@ -270,7 +300,7 @@
});
$('.clear-comm').click(function () {
var token = getCookie("_xsrf")
const token = getCookie("_xsrf")
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
@ -281,7 +311,7 @@
})
$('.delete-photo').click(function () {
var token = getCookie("_xsrf")
const token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val();
$.ajax({
type: "POST",
@ -294,7 +324,7 @@
})
$('.select-photo').click(function () {
var token = getCookie("_xsrf")
const token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val();
$.ajax({
type: "POST",

View File

@ -28,7 +28,12 @@
{% if data['superuser'] %}
{% include "parts/crafty_config_list.html %}
<span class="d-none d-sm-block">
{% include "parts/crafty_config_list.html %}
</span>
<span class="d-block d-sm-none">
{% include "parts/m_crafty_config_list.html %}
</span>
{% end %}
<!-- Page Title Header Starts-->
@ -51,16 +56,22 @@
<div class="col-12">
<h4>{{ translate('customLogin', 'loginImage', data['lang']) }}</h4>
<hr>
<form class="form-row" name="zip" method="post" class="server-wizard" onSubmit="wait_msg(true)">
<form class="form" name="zip" method="post" class="server-wizard" onSubmit="wait_msg(true)">
{% raw xsrf_form_html() %}
<input type="hidden" value="import_zip" name="create_type">
<div class="col form-group">
<span id="upload_input"><input type="file" class="form-control-file" id="file" name="file"
multiple="false" required></span>
</div>
<div class="col form-group">
<button type="button" class="btn btn-info" id="upload-button" onclick="sendFile()"
disabled>UPLOAD</button>
<div class="form-group">
<div id="upload_input" class="input-group">
<div class="custom-file">
<input type="file" class="custom-file-input" id="file" name="file" multiple="false"
required>
<label id="fileLabel" class="custom-file-label" for="file">{{ translate('customLogin',
'labelLoginImage', data['lang']) }}</label>
</div>
<div class="input-group-append">
<button type="button" class="btn btn-info upload-button" id="upload-button"
onclick="sendFile()" disabled>UPLOAD</button>
</div>
</div>
</div>
</form>
<hr>
@ -228,6 +239,10 @@
</div>
<style>
.img-fluid {
margin-bottom: 1rem;
}
.popover-body {
color: white !important;
;
@ -272,6 +287,7 @@
console.log("File changed");
if ($('#file').val()) {
$('#upload-button').prop("disabled", false);
document.getElementById("fileLabel").innerHTML = $('#file').val().split('\\').pop().split('/').pop();
console.log("File changed good");
}
});
@ -293,33 +309,50 @@
});
});
$('.delete-photo').click(function () {
var token = getCookie("_xsrf")
$('.delete-photo').click(async function () {
let photo = $('#photo').find(":selected").val();
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/delete_photo?photo=' + encodeURIComponent(photo),
success: function (data) {
location.reload();
const token = getCookie("_xsrf")
let res = await fetch(`/api/v2/crafty/config/customize`, {
method: 'DELETE',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({ "photo": photo }),
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.reload();
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
})
$('.select-photo').click(function () {
var token = getCookie("_xsrf")
$('.select-photo').click(async function () {
let photo = $('#photo').find(":selected").val();
let opacity = $('#modal_opacity').val();
let enc_photo = encodeURIComponent(photo);
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/select_photo?photo=' + enc_photo + '&opacity=' + opacity,
success: function (data) {
window.location.reload();
console.log(JSON.stringify({ "photo": photo, "opacity": opacity }))
const token = getCookie("_xsrf")
let res = await fetch(`/api/v2/crafty/config/customize`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({ "photo": photo, "opacity": opacity }),
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.reload();
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
})
$(document).ready(function () {
@ -352,7 +385,7 @@
var file;
function sendFile() {
file = $("#file")[0].files[0]
document.getElementById("upload_input").innerHTML = '<div class="progress"><div id="upload-progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>';
document.getElementById("upload_input").innerHTML = '<div class="progress" style="width: 100%"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>';
let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf")
let fileName = file.name
@ -380,7 +413,7 @@
xmlHttpRequest.addEventListener('load', (event) => {
if (event.target.responseText == 'success') {
console.log('Upload for file', file.name, 'was successful!')
document.getElementById("upload_input").innerHTML = '<div class="card-header header-sm d-flex justify-content-between align-items-center"><span id="file-uploaded" style="color: gray;">' + fileName + '</span> 🔒</div>';
document.getElementById("upload_input").innerHTML = '<div class="card-header header-sm d-flex justify-content-between align-items-center" style="width: 100%"><span id="file-uploaded" style="color: gray;">' + fileName + '</span> 🔒</div>';
setTimeout(function () {
window.location.reload();
}, 2000);

View File

@ -647,10 +647,13 @@
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/server/command?command=' + command + '&id=' + server_id,
url: `/api/v2/servers/${server_id}/action/${command}`,
success: function (data) {
console.log("got response:");
console.log(data);
if (command === "clone_server" && data.status === "ok") {
window.location.reload();
}
/*setTimeout(function () {
if (command != 'start_server') {
location.reload();
@ -705,24 +708,6 @@
document.querySelector('.dynamicMsg').appendChild(parentEl);
}
function send_kill(server_id) {
/* this getCookie function is in base.html */
const token = getCookie("_xsrf");
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/kill?id=' + server_id,
success: function (data) {
console.log("got response:");
console.log(data);
/*setTimeout(function () {
location.reload();
}, 10000);*/
}
});
}
function update_one_server_status(server) {
/* Mobile view update */
server_cpu = document.getElementById('server_cpu_' + server.id);
@ -901,17 +886,11 @@
},
callback: function (result) {
if (result) {
send_kill(server_id);
send_command(server_id, "kill_server");
let dialog = bootbox.dialog({
title: '{% raw translate("dashboard", "killing", data["lang"]) %}',
message: '<p><i class="fa fa-spin fa-spinner"></i> Loading...</p>'
});
dialog.init(function () {
setTimeout(function () {
location.reload();
}, 15000);
});
}
}
});
@ -1000,7 +979,13 @@
},
callback: function (result) {
if (result) {
cloneServer(server_id);
send_command(server_id, 'clone_server');
bootbox.dialog({
backdrop: true,
title: '{% raw translate("dashboard", "sendingCommand", data["lang"]) %}',
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("dashboard", "bePatientClone", data["lang"]) %} </div>',
closeButton: false,
});
}
}
@ -1008,16 +993,6 @@
});
});
function cloneServer(server_id) {
send_command(server_id, 'clone_server');
bootbox.dialog({
backdrop: true,
title: '{% raw translate("dashboard", "sendingCommand", data["lang"]) %}',
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("dashboard", "bePatientClone", data["lang"]) %} </div>',
closeButton: false,
});
}
</script>
<script src="/static/assets/vendors/js/jquery-ui.js"></script>
<link rel="stylesheet" href="/static/assets/vendors/css/jquery-ui.css">
@ -1069,12 +1044,12 @@
const token = getCookie("_xsrf")
$.ajax({
type: "POST",
type: "PATCH",
headers: { 'X-XSRFToken': token },
url: '/ajax/send_order?order=' + id_string,
data: {
order: id_string,
},
url: `/api/v2/users/@me`,
data: JSON.stringify({
server_order: id_string,
}),
success: function (data) {
console.log("got response:");
console.log(data);

View File

@ -1,82 +1,103 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Crafty Controller</title>
<!-- plugins:css -->
<link rel="stylesheet" href="/static/assets/vendors/mdi/css/materialdesignicons.min.css" />
<link rel="stylesheet" href="/static/assets/vendors/flag-icon-css/css/flag-icon.min.css" />
<link rel="stylesheet" href="/static/assets/vendors/ti-icons/css/themify-icons.css" />
<link rel="stylesheet" href="/static/assets/vendors/typicons/typicons.css" />
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css" />
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Crafty Controller</title>
<!-- plugins:css -->
<link rel="stylesheet" href="/static/assets/vendors/mdi/css/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/assets/vendors/flag-icon-css/css/flag-icon.min.css">
<link rel="stylesheet" href="/static/assets/vendors/ti-icons/css/themify-icons.css">
<link rel="stylesheet" href="/static/assets/vendors/typicons/typicons.css">
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css">
<!-- endinject -->
<!-- Plugin css for this page -->
<!-- End Plugin css for this page -->
<!-- Layout styles -->
<link rel="stylesheet" href="/static/assets/css/dark/style.css">
<!-- End Layout styles -->
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/images/logo_small.svg">
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
</head>
<style>
.auth.auth-bg-1 {
background: url("../../static/assets/images/auth/{% raw data['background'] %}"),
url("../../static/assets/images/auth/login-1.jpg");
background-size: cover;
}
</style>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="Crafty" />
<link rel="apple-touch-icon" href="../static/assets/images/Crafty_4-0.png" />
<body class="dark-theme">
<div class="container-scroller">
<div class="container-fluid page-body-wrapper full-page-wrapper">
<div class="content-wrapper d-flex align-items-center auth auth-bg-1 theme-one">
<div class="row w-100">
<div class="col-lg-4 mx-auto">
<!-- endinject -->
<!-- Plugin css for this page -->
<!-- End Plugin css for this page -->
<!-- Layout styles -->
<link rel="stylesheet" href="/static/assets/css/dark/style.css" />
<!-- End Layout styles -->
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/images/logo_small.svg" />
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
</head>
<style>
.auth.auth-bg-1 {
background: url("../../static/assets/images/auth/{% raw data['background'] %}"),
url("/static/assets/images/auth/login_1.jpg");
background-size: cover;
}
</style>
<div class="auto-form-wrapper">
<div class="text-center">
<img src="/static/assets/images/logo_long.svg"><br /><br />
<div class="col-sm-12 grid-margin stretch-card">
<div class="card card-statistics social-card google-card card-colored">
<div class="card-body">
<h4 class="platform-name mb-3 mt-4 font-weight-semibold user-name">{{ translate('accessDenied',
'accessDenied', data['lang']) }}</h4>
<h5 class="headline font-weight-medium">{{ translate('accessDenied', 'noAccess', data['lang']) }}
</h5>
<p class="mb-2 comment font-weight-light">
{{ translate('accessDenied', 'contactAdmin', data['lang']) }}<br /><br />
<a class="d-inline font-weight-medium" href="https://discord.gg/9VJPhCE"> {{
translate('accessDenied', 'contact', data['lang']) }}</a>
</p>
<body class="dark-theme">
<div class="container-scroller">
<div class="container-fluid page-body-wrapper full-page-wrapper">
<div class="content-wrapper d-flex align-items-center auth auth-bg-1 theme-one" >
<div class="row w-100">
<div class="col-lg-4 mx-auto">
<div class="auto-form-wrapper">
<div class="text-center">
<img alt="Crafty Logo" src="/static/assets/images/logo_long.svg" /><br /><br />
<div class="col-sm-12 grid-margin stretch-card">
<div class="card card-statistics social-card google-card card-colored" >
<div class="card-body">
<h4 class="platform-name mb-3 mt-4 font-weight-semibold user-name" >
{{ translate('accessDenied', 'accessDenied', data['lang']) }}
</h4>
<h5 class="headline font-weight-medium">
{{ translate('accessDenied', 'noAccess', data['lang']) }}
</h5>
<p class="mb-2 comment font-weight-light">
{{ translate('accessDenied', 'contactAdmin',
data['lang']) }}<br /><br />
<a class="d-inline font-weight-medium" href="https://discord.gg/9VJPhCE" > {{ translate('accessDenied', 'contact', data['lang']) }}</a>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- content-wrapper ends -->
</div>
<!-- content-wrapper ends -->
<!-- page-body-wrapper ends -->
</div>
<!-- page-body-wrapper ends -->
</div>
<!-- container-scroller -->
<!-- plugins:js -->
<script src="/static/assets/vendors/js/vendor.bundle.base.js"></script>
<!-- endinject -->
<!-- inject:js -->
<script src="/static/assets/js/shared/off-canvas.js"></script>
<script src="/static/assets/js/shared/hoverable-collapse.js"></script>
<script src="/static/assets/js/shared/misc.js"></script>
<script src="/static/assets/js/shared/settings.js"></script>
<script src="/static/assets/js/shared/todolist.js"></script>
<!-- endinject -->
</body>
</html>
<!-- container-scroller -->
<!-- plugins:js -->
<script src="/static/assets/vendors/js/vendor.bundle.base.js"></script>
<!-- endinject -->
<!-- inject:js -->
<script src="/static/assets/js/shared/off-canvas.js"></script>
<script src="/static/assets/js/shared/hoverable-collapse.js"></script>
<script src="/static/assets/js/shared/misc.js"></script>
<script src="/static/assets/js/shared/settings.js"></script>
<script src="/static/assets/js/shared/todolist.js"></script>
<!-- endinject -->
<script>
$(document).ready(function () {
let login_opacity_div = document.getElementById("login_opacity");
let opacity = login_opacity_div.getAttribute("data-value");
document.getElementById("login-form-background").style.background =
"rgb(34, 36, 55, " + opacity / 100 + ")";
//Register Service worker for mobile app
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/static/assets/js/shared/service-worker.js", {
scope: "/",
})
.then(function (registration) {
console.log("Service Worker Registered");
});
}
});
</script>
</body>
</html>

View File

@ -28,7 +28,12 @@
{% if data['superuser'] %}
{% include "parts/crafty_config_list.html %}
<span class="d-none d-sm-block">
{% include "parts/crafty_config_list.html %}
</span>
<span class="d-block d-sm-none">
{% include "parts/m_crafty_config_list.html %}
</span>
{% end %}
<!-- Page Title Header Starts-->
@ -321,24 +326,30 @@
});
}
$("#server-path").submit(function (e) {
var token = getCookie("_xsrf")
$("#server-path").submit(async function (e) {
const token = getCookie("_xsrf")
e.preventDefault();
$("#submit-status").html('<i class="fa fa-spinner fa-spin"></i>');
let path = $("#global_server_path").val();
let encoded = encodeURIComponent(path);
console.log(path)
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
dataType: "text",
url: '/ajax/update_server_dir',
data: {
"server_dir": encoded,
let res = await fetch(`/api/v2/crafty/config/servers_dir`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({ "new_dir": path }),
});
let responseData = await res.json();
if (responseData.status === "ok") {
return
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
});
$(document).ready(function () {

View File

@ -49,10 +49,7 @@
</ul>
<div class="">
<div class="">
<form id="role_form" class="forms-sample" method="post" action="{{ '/panel/add_role' if data['new_role'] else '/panel/edit_role' }}">
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['role']['role_id'] }}">
<input type="hidden" name="subpage" value="config">
<form id="role_form" class="forms-sample">
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
@ -61,7 +58,7 @@
<div class="card-body">
<div class="form-group">
<label for="role_name">{{ translate('rolesConfig', 'roleName', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('rolesConfig', 'roleDesc', data['lang']) }}</small> </label>
<input type="text" class="form-control" name="role_name" id="role_name" value="{{ data['role']['role_name'] }}" placeholder="Role Name" >
<input type="text" class="form-control" name="name" id="role_name" value="{{ data['role']['role_name'] }}" placeholder="Role Name" >
</div>
<br />
@ -188,11 +185,11 @@
<tr>
<td>{{ server['server_name'] }}</td>
<td>
<input type="checkbox" class="" onclick="enable_disable(event)" data-id="{{server['server_id']}}"
<input type="checkbox" class="access" onclick="enable_disable(event)" data-id="{{server['server_id']}}"
id="server_{{ server['server_id'] }}_access"
name="server_{{ server['server_id'] }}_access"
{{ 'checked' if server['server_id'] in data['role']['servers'] else '' }}
autocomplete="off" value="1">
autocomplete="off" value="1" form="dummy">
</td>
{% for permission in data['permissions_all'] %}
{% if server['server_id'] in data['role']['servers'] %}
@ -201,14 +198,14 @@
id="permission_{{ server['server_id'] }}_{{ permission.name }}"
name="permission_{{ server['server_id'] }}_{{ permission.name }}"
{{ 'checked' if permission in data['permissions_dict'].get(server['server_id'], []) else '' }}
autocomplete="off" value="1">
autocomplete="off" value="1" form="dummy">
</td>
{% else %}
<td>
<input type="checkbox" class="{{server['server_id']}}_perms"
id="permission_{{ server['server_id'] }}_{{ permission.name }}"
name="permission_{{ server['server_id'] }}_{{ permission.name }}"
autocomplete="off" value="1" disabled>
autocomplete="off" value="1" disabled form="dummy">
</td>
{% end %}
{% end %}
@ -284,7 +281,7 @@
<a class="btn btn-sm btn-danger disabled"><i class="fas fa-trash"></i>{{ translate('rolesConfig', 'delRole', data['lang']) }}</a><br />
<small>{{ translate('rolesConfig', 'doesNotExist', data['lang']) }}</small>
{% else %}
<a href="/panel/remove_role?id={{ data['role']['role_id'] }}" class="btn btn-sm btn-danger"><i class="fas fa-trash"></i>{{ translate('rolesConfig', 'delRole', data['lang']) }}</a>
<button onclick="del_role()" class="btn btn-sm btn-danger"><i class="fas fa-trash"></i>{{ translate('rolesConfig', 'delRole', data['lang']) }}</button>
{% end %}
</div>
</div>
@ -321,9 +318,123 @@
return r ? r[1] : undefined;
}
function gather_server_json() {
servers = [];
for (s = 0; s < page_servers.length; s++){
mask = ""
for (i = 0; i < permissions.length; i++){
if ($(`#permission_${page_servers[s].id}_${permissions[i]}`).prop('checked')){
mask += "1"
}else{
mask += "0"
}
}
servers.push(JSON.stringify({"id": page_servers[s].id, "permissions": mask}));
}
return servers;
}
$( document ).ready(function() {
console.log( "ready!" );
});
const roleId = new URLSearchParams(document.location.search).get('id');
function replacer(key, value) {
if (key === "permissions"){
return value;
}
if (key === "servers" && value.length === 0){
return value;
}
if (typeof value == "boolean") {
console.log(value);
return value
} else {
return (isNaN(value) ? value : +value);
}
}
async function del_role(){
const token = getCookie("_xsrf")
let res = await fetch(`/api/v2/roles/${roleId}`, {
method: "DELETE",
headers: {
'X-XSRFToken': token
},
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = "/panel/panel_config";
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
}
$("#role_form").on("submit", async function (e) {
e.preventDefault();
const token = getCookie("_xsrf")
let roleForm = document.getElementById("role_form");
let server_ids = $('.access').map(function() {
if ($(this).is(':checked')){
return $(this).data('id');
}
}).get();
let servers = []
for(i=0; i < server_ids.length; i++){
let arrchecked = $(`.${server_ids[i]}_perms`).map(function() {
if(this.checked){
return "1";
}else{
return "0"
}
}).get();
servers.push({"server_id": server_ids[i], "permissions": arrchecked.join("")});
}
console.log(servers)
let formData = new FormData(roleForm);
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
formDataObject.servers = servers;
console.log(formDataObject);
//We need to make sure these are sent regardless of whether or not they're checked
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer);
console.log(formDataJsonString);
let url = `/api/v2/roles/`
let method = 'POST'
if (roleId){
url = `/api/v2/roles/${roleId}`
method = 'PATCH'
}
let res = await fetch(url, {
method: method,
headers: {
'X-XSRFToken': token
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = "/panel/panel_config";
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
});
</script>

View File

@ -58,13 +58,11 @@ data['lang']) }}{% end %}
<div class="row">
<div class="col-md-6 col-sm-12">
{% if data['new_user'] %}
<form id="user_form" class="forms-sample" method="post" action="/panel/add_user">
<form id="user_form" class="forms-sample">
{% else %}
<form id="user_form" class="forms-sample" method="post" action="/panel/edit_user">
<form id="user_form" class="forms-sample">
{% end %}
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['user']['user_id'] }}">
<input type="hidden" name="subpage" value="config">
<div class="card">
@ -85,7 +83,7 @@ data['lang']) }}{% end %}
}}<small class="text-muted ml-1"> - {{ translate('userConfig', 'leaveBlank', data['lang']) }}
</small> </label>
<input type="password" class="form-control" name="password0" id="password0" value=""
autocomplete="new-password" data-lpignore="true" placeholder="Password">
autocomplete="new-password" data-lpignore="true" placeholder="Password" form="dummy">
<span class="passwords-match" ,
data-content="{{ translate('panelConfig', 'match', data['lang']) }}" ,
data-placement="right"></span>
@ -95,7 +93,7 @@ data['lang']) }}{% end %}
<small class="text-muted ml-1"> - {{ translate('userConfig', 'leaveBlank', data['lang'])
}}</small> </label>
<input type="password" class="form-control" name="password1" id="password1" value=""
autocomplete="new-password" data-lpignore="true" placeholder="Repeat Password">
autocomplete="new-password" data-lpignore="true" placeholder="Repeat Password" form="dummy">
<span class="passwords-match" ,
data-content="{{ translate('panelConfig', 'match', data['lang']) }}" ,
data-placement="right"></span>
@ -111,7 +109,7 @@ data['lang']) }}{% end %}
<label class="form-label" for="language">{{ translate('userConfig', 'userLang', data['lang'])
}}</label>
<select class="form-select form-control form-control-lg select-css" id="language"
name="language" form="user_form">
name="lang" form="user_form">
{% for lang in data['languages'] %}
{% if not 'incomplete' in lang %}
<option value="{{lang}}">{{lang}}</option>
@ -182,18 +180,18 @@ data['lang']) }}{% end %}
<td>
{% if role.role_id in data['user']['roles'] %}
{% if role.manager == data['exec_user'] or data['superuser'] %}
<input type="checkbox" class="form-check-input"
<input type="checkbox" class="form-check-input role_check"
id="role_{{ role.role_id }}_membership" name="role_{{ role.role_id }}_membership"
checked="" value="1">
checked="" value="{{role.role_id}}" form="dummy">
{% else %}
<input type="checkbox" class="form-check-input"
<input type="checkbox" class="form-check-input role_check"
id="role_{{ role.role_id }}_membership" name="role_{{ role.role_id }}_membership"
checked="" value="1" disabled>
checked="" value="{{role.role_id}}" disabled form="dummy">
{% end %}
{% elif data['superuser'] or role.manager == data['exec_user'] %}
<input type="checkbox" class="form-check-input"
<input type="checkbox" class="form-check-input role_check"
id="role_{{ role.role_id }}_membership" name="role_{{ role.role_id }}_membership"
value="1">
value="{{role.role_id}}" form="dummy">
{% end %}
</td>
@ -219,7 +217,7 @@ data['lang']) }}{% end %}
<div class="card-body">
<div class="form-group">
<div class="table-responsive">
<table class="table table-hover">
<table id="permissions" aria-describedby="User Crafty Permissions" class="table table-hover">
<thead>
<tr class="rounded">
<th>{{ translate('userConfig', 'permName', data['lang']) }}</th>
@ -233,16 +231,16 @@ data['lang']) }}{% end %}
<td>{{ permission.name }}</td>
<td>
{% if permission in data['permissions_list'] %}
<input type="checkbox" class="form-check-input" id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" checked="" value="1">
<input type="checkbox" class="form-check-input perm-name" id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" checked="" value="1" data-perm="{{permission.name}}" form="dummy">
{% else %}
<input type="checkbox" class="form-check-input" id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" value="1">
<input type="checkbox" class="form-check-input perm-name" id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" value="1" data-perm="{{permission.name}}" form="dummy">
{% end %}
</td>
<td><input type="text" class="form-control" name="quantity_{{ permission.name }}"
id="quantity_{{ permission.name }}"
value="{{ data['quantity_server'][permission.name] }}"></td>
value="{{ data['quantity_server'][permission.name] }}" data-perm="{{permission.name}}" form="dummy"></td>
</tr>
{% end %}
</tbody>
@ -287,7 +285,7 @@ data['lang']) }}{% end %}
</div>
<button class="btn btn-success mr-2" onclick="submit_user(event);"><i class="fas fa-save"></i> {{
<button type="submit" class="btn btn-success mr-2"><i class="fas fa-save"></i> {{
translate('panelConfig', 'save', data['lang']) }}</button>
<button type="reset" onclick="location.href='/panel/panel_config'" class="btn btn-light"><i
class="fas fa-undo-alt"></i> {{ translate('panelConfig', 'cancel', data['lang']) }}</button>
@ -363,9 +361,12 @@ data['lang']) }}{% end %}
}
}
function validateForm() {
let password0 = document.getElementById("password0").value
let password1 = document.getElementById("password1").value
if (password0 != password1) {
let password0 = document.getElementById("password0").value;
let password1 = document.getElementById("password1").value;
if (password0 === "" && password1 === "" && userId){
return true
}
else if (password0 != password1) {
$('.passwords-match').popover('show');
$('.popover-body').click(function () {
$('.passwords-match').popover("hide");
@ -376,11 +377,103 @@ data['lang']) }}{% end %}
$("#password1").css("outline", "1px solid red");
return false;
} else {
return true;
return password1;
}
}
function replacer(key, value) {
if (typeof value == "boolean" || key === "email" || key === "permissions" || key === "roles") {
return value
} else {
console.log(key, value)
return (isNaN(value) ? value : +value);
}
}
const userId = new URLSearchParams(document.location.search).get('id')
$("#user_form").on("submit", async function (e) {
e.preventDefault();
let password = validateForm();
if (!password){
return;
}
const token = getCookie("_xsrf")
let userForm = document.getElementById("user_form");
let disabled_flag = false;
let roles = $('.role_check').map(function() {
if ($(this).attr("disabled")){
disabled_flag = true;
}
if ($(this).is(':checked')){
return $(this).val();
}
}).get();
let avail_permissions = $('.perm-name').map(function() {
return $(this).data("perm");
}).get();
permissions = []
for(i=0; i < avail_permissions.length; i++){
permissions.push({"name": avail_permissions[i], "quantity": $(`#quantity_${avail_permissions[i]}`).val(), "enabled": $(`#permission_${avail_permissions[i]}`).is(':checked')})
}
console.log(permissions);
let formData = new FormData(userForm);
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
if (!disabled_flag){
formDataObject.roles = roles;
}
if ($("#permissions").length){
formDataObject.permissions = permissions;
}
if(typeof password === "string"){
formDataObject.password = password;
}
formDataObject.enabled = $("#enabled").is(":checked");
if ($("#superuser").is(":enabled")){
formDataObject.superuser = $("#superuser").is(":checked");
}
formDataObject.hints = $("#hints").is(":checked");
console.log(formDataObject);
//We need to make sure these are sent regardless of whether or not they're checked
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer);
console.log(formDataJsonString);
if (userId){
url = `/api/v2/users/${userId}`
method = 'PATCH'
}else{
url = `/api/v2/users/`
method = 'POST'
}
let res = await fetch(url, {
method: method,
headers: {
'X-XSRFToken': token
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = "/panel/panel_config";
} else {
if (responseData.hasOwnProperty("error_data")){
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}else{
bootbox.alert(responseData.error
);
}
}
});
$(".delete-user").click(function () {
var file_to_del = $(this).data("file");
@ -398,10 +491,26 @@ data['lang']) }}{% end %}
label: '<i class="fas fa-check"></i> {{ translate("serverBackups", "confirm", data['lang']) }}'
}
},
callback: function (result) {
callback: async function (result) {
console.log(result);
if (result === true) {
location.href = "/panel/remove_user?id=" + userId;
const token = getCookie("_xsrf")
let res = await fetch(`/api/v2/users/${userId}`, {
method: "DELETE",
headers: {
'X-XSRFToken': token
},
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = "/panel/panel_config";
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error
});
}
}
}
});

View File

@ -86,17 +86,14 @@
apikey.server_permissions }}
{{ translate('apiKeys', 'crafty', data['lang']) }} {{
apikey.crafty_permissions }}</td>
<td>
<button class="btn btn-danger delete-api-key"
<td><button class="btn btn-danger delete-api-key"
data-key-id="{{ apikey.token_id }}"
data-key-name="{{ apikey.name }}">{{
translate('panelConfig', 'delete', data['lang'])
}}</button>
data-key-name="{{ apikey.name }}">{{translate('panelConfig',
'delete', data['lang'])}}</button>
<button class="btn btn-outline-primary get-a-token"
data-key-id="{{ apikey.token_id }}"
data-key-name="{{ apikey.name }}">{{
translate('apiKeys', 'getToken', data['lang']) }}
</button>
data-key-name="{{ apikey.name }}">{{translate('apiKeys',
'getToken', data['lang'])}}</button>
</td>
</tr>
{% end %}
@ -115,10 +112,7 @@
'createNew', data['lang']) }}</h4>
</div>
<div class="card-body">
<form id="user_form" class="forms-sample" method="post"
action="/panel/edit_user_apikeys">
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['user']['user_id'] }}">
<form id="user_api_form" class="forms-sample">
<div class="form-group">
<label class="form-label" for="username">{{ translate('apiKeys', 'name',
@ -142,7 +136,7 @@
}}</label>
</td>
<td>
<input type="checkbox" class=""
<input type="checkbox" class="server_perm"
id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" value="1">
</td>
@ -154,7 +148,7 @@
}}</label>
</td>
<td>
<input type="checkbox" class=""
<input type="checkbox" class="crafty_perm"
id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" value="1">
</td>
@ -201,56 +195,122 @@
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
const userId = new URLSearchParams(document.location.search).get('id')
$(document).ready(function () {
$("#user_api_form").on("submit", async function (e) {
e.preventDefault();
const token = getCookie("_xsrf")
let apiForm = document.getElementById("user_api_form");
let formData = new FormData(apiForm);
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
//We need to make sure these are sent regardless of whether or not they're checked
formDataObject.disabled_language_files = $('#lang_select').val();
$('#user_api_form input[type="checkbox"]:checked').each(function () {
if ($(this).val() == 'True') {
formDataObject[this.name] = true;
} else {
formDataObject[this.name] = false;
}
});
let server_permissions = $('.server_perm').map(function () {
if (this.checked) {
return "1";
} else {
return "0"
}
}).get();
server_permissions = server_permissions.join("");
let crafty_permissions = $('.crafty_perm').map(function () {
if (this.checked) {
return "1";
} else {
return "0"
}
}).get();
crafty_permissions = crafty_permissions.join("");
console.log(server_permissions);
console.log(crafty_permissions);
console.log(formDataObject);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify({
"name": formDataObject.name,
"server_permissions_mask": server_permissions,
"crafty_permissions_mask": crafty_permissions,
"superuser": $("#superuser").prop('checked'),
});
console.log(formDataJsonString);
let res = await fetch(`/api/v2/users/${userId}/key/`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
location.reload();
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
});
});
$(document).ready(function () {
console.log("ready!");
$('.delete-api-key').click(function () {
var keyId = $(this).data("key-id");
var keyName = $(this).data("key-name");
bootbox.confirm({
title: `Remove API key ${keyName}?`,
message: "Do you want to delete this API key? This cannot be undone.",
buttons: {
cancel: {
label: '<i class="fas fa-times"></i> {{ translate("panelConfig", "cancel", data['lang']) }}'
},
confirm: {
label: '<i class="fas fa-check"></i> {{ translate("serverBackups", "confirm", data['lang']) }}'
}
},
callback: function (result) {
if (result) {
var token = getCookie("_xsrf")
$.ajax({
type: "DELETE",
headers: { 'X-XSRFToken': token },
url: '/panel/remove_apikey?id=' + keyId,
success: function (data) {
location.reload();
},
});
}
}
});
})
$('.get-a-token').click(function () {
var keyId = $(this).data("key-id");
var keyName = $(this).data("key-name");
var token = getCookie("_xsrf")
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/panel/get_token?id=' + keyId,
success: function (data) {
bootbox.alert({
title: `API token for ${keyName}`,
message: `Here is an API token for ${keyName}:\n<pre style="white-space: pre-wrap;color:white;word-break:break-all;background: grey;border-radius: 5px;">${data}</pre>`
});
$('.delete-api-key').click(async function () {
let keyId = $(this).data("key-id");
let token = getCookie("_xsrf");
let res = await fetch(`/api/v2/users/${userId}/key/${keyId}`, {
method: 'DELETE',
headers: {
'X-XSRFToken': token
},
});
})
});
let responseData = await res.json();
if (responseData.status === "ok") {
location.reload()
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
})
$('.get-a-token').click(async function () {
let keyId = $(this).data("key-id");
let keyName = $(this).data("key-name");
let token = getCookie("_xsrf");
let res = await fetch(`/api/v2/users/${userId}/key/${keyId}`, {
method: 'GET',
headers: {
'X-XSRFToken': token
},
});
let responseData = await res.json();
if (responseData.status === "ok") {
bootbox.alert({
title: `API token for ${keyName}`,
message: `Here is an API token for ${keyName}:\n<pre style="white-space: pre-wrap;color:white;word-break:break-all;background: grey;border-radius: 5px;">${responseData.data}</pre>`
});
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
});
});
</script>

View File

@ -1,17 +1,14 @@
<ul class="nav nav-tabs col-md-12 tab-simple-styled" role="tablist" style="margin-top: 0;">
<li class="nav-item term-nav-item">
<a class="nav-link {% if data['active_link'] == 'panel_config' %}active{% end %}" href="/panel/panel_config"
role="tab" aria-selected="false">
<i class="fa-solid fa-wrench"></i>{{ translate('panelConfig', 'pageTitle', data['lang']) }}</a>
<a class="nav-link {% if data['active_link'] == 'panel_config' %}active{% end %}" href="/panel/panel_config" role="tab" aria-selected="false">
<i class="fas fa-wrench"></i>{{ translate('panelConfig', 'pageTitle', data['lang']) }}</a>
</li>
<li class="nav-item term-nav-item">
<a class="nav-link {% if data['active_link'] == 'config_json' %}active{% end %}" href="/panel/config_json"
role="tab" aria-selected="false">
<i class="fa-solid fa-code"></i>{{ translate('panelConfig', 'json', data['lang']) }}</a>
<a class="nav-link {% if data['active_link'] == 'config_json' %}active{% end %}" href="/panel/config_json" role="tab" aria-selected="false">
<i class="fas fa-code"></i>{{ translate('panelConfig', 'json', data['lang']) }}</a>
</li>
<li class="nav-item term-nav-item">
<a class="nav-link {% if data['active_link'] == 'custom_login' %}active{% end %}" href="/panel/custom_login"
role="tab" aria-selected="false">
<i class="fa fa-palette"></i>{{ translate('panelConfig', 'custom', data['lang']) }}</a>
<a class="nav-link {% if data['active_link'] == 'custom_login' %}active{% end %}" href="/panel/custom_login" role="tab" aria-selected="false">
<i class="fas fa-palette"></i>{{ translate('panelConfig', 'custom', data['lang']) }}</a>
</li>
</ul>

View File

@ -228,6 +228,24 @@
}
initParser('input_motd', 'input_motd');
let text = ""
let players = server.players_cache;
for(let i=0; i < players.length; i++){
text += `<tr id="playerItem-${ players[i]["name"] }" class="playerItem--" style="text-align: center;">`;
text += `<td class="no-scroll" style="overflow: scroll;"><strong>${players[i]["name"]}</strong></td>`;
if(players[i]["status"] === "Online"){
text += `<td><span class="text-success"><i class="fas fa-signal"></i> ${ players[i]['status'] }</span></td>`
}else{
text += `<td><span class="text-warning"><i class="fa-regular fa-circle-xmark"></i><span class="offline-status">&nbsp;${ players[i]['status'] }</span><span class="conn-break"> Last connection :<br> ${ players[i]['last_seen'] }</span></td>`
}
if(server["running"]){
text += `<td><button onclick="send_command_to_server('ban ${ players[i]['name'] }')" type="button" class="btn btn-danger controls">Ban</button><br class="mobile-break"><button onclick="send_command_to_server('kick ${ players[i]['name'] }')" type="button" class="btn btn-outline-danger controls">Kick</button><br><button onclick="send_command_to_server('op ${ players[i]['name'] }')" type="button" class="btn btn-warning controls">OP</button><br class="mobile-break"><button onclick="send_command_to_server('deop ${ players[i]['name'] }')" type="button" class="btn btn-outline-warning controls">De-OP</button></td>`
}else{
text += `<td><span> Unavailable<br> (Server Offline)</span></td>`
}
}
$("#player-body").html(text);
}

View File

@ -0,0 +1,15 @@
<div class="col-sm-12 mt-4 mb-4">
<div class="dropdown">
<button class="btn btn-outline dropdown-toggle custom-picker" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-expanded="false">
<i class="fas fa-bars"></i> Crafty Config
</button>
<div class="dropdown-menu col-md-12" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item {% if data['active_link'] == 'panel_config' %}active{% end %}" href="/panel/panel_config" role="tab" aria-selected="false">
<i class="fas fa-wrench"></i> Panel Config</a>
<a class="dropdown-item {% if data['active_link'] == 'config_json' %}active{% end %}" href="/panel/config_json" role="tab" aria-selected="false">
<i class="fas fa-code"></i> Config.json</a>
<a class="dropdown-item {% if data['active_link'] == 'custom_login' %}active{% end %}" href="/panel/custom_login" role="tab" aria-selected="false">
<i class="fas fa-palette"></i> Custom Login</a>
</div>
</div>
</div>

View File

@ -1,7 +1,7 @@
<div class="col-sm-12 mt-4 mb-4">
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-expanded="false">
Server Controls
<button class="btn btn-outline dropdown-toggle custom-picker" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-expanded="false">
<i class="fas fa-bars"></i> Server Controls
</button>
<div class="dropdown-menu col-md-12" aria-labelledby="dropdownMenuButton">
{% if data['permissions']['Terminal'] in data['user_permissions'] %}
@ -12,7 +12,7 @@
<a class="dropdown-item {% if data['active_link'] == 'logs' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=logs" role="tab" aria-selected="false"><i class="fa-solid fa-book-open-reader"></i> {{ translate('serverDetails', 'logs', data['lang']) }}</a>
{% end %}
{% if data['permissions']['Schedule'] in data['user_permissions'] %}
<a class="dropdown-item {% if data['active_link'] == 'schedules' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=schedules" role="tab" aria-selected="false"><i class="fas fa-clock"></i> {{ translate('serverDetails', 'schedule', data['lang']) }}</a>
<a class="dropdown-item {% if data['active_link'] == 'schedules' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=schedules" role="tab" aria-selected="false"><i class="fa-solid fa-clock"></i> {{ translate('serverDetails', 'schedule', data['lang']) }}</a>
{% end %}
{% if data['permissions']['Backup'] in data['user_permissions'] %}
{% if data['backup_failed'] %}

View File

@ -0,0 +1,96 @@
<div class="col-xl-6 col-lg-12 col-md-12 col-sm-12">
<h2>{{ translate('serverPlayerManagement', 'players', data['lang']) }}:</h2>
<table class="table table-sm-responsive">
<thead class="thead">
<tr>
<th scope="col">Player</th>
<th scope="col">Status</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody id="player-body">
{% for player in data['cached_players'] %}
<tr id="playerItem-{{ player['name'] }}" class="playerItem--" style="text-align: center;">
<td>
<strong> {{ player['name'] }}</strong>
</td>
{% if player['status'] == 'Online' %}
<td style="overflow: scroll;"><span class="text-success"><i class="fas fa-signal"></i> {{ player['status'] }}</span></td>
{% elif player['status'] == 'Offline' %}
<td><span class="text-warning"><i class="fa-regular fa-circle-xmark"></i><span class="offline-status">&nbsp;{{ player['status'] }}</span><span class="conn-break"> Last connection :<br> {{ player['last_seen'] }}</span></span></td>
{% end %}
<td class="buttons" style="text-align: center;">
{% if data['server_stats']['running'] %}
<button onclick="send_command_to_server(`ban {{ player['name'] }}`)" type="button" class="btn btn-danger controls">Ban</button>
<br class="mobile-break"/>
<button onclick="send_command_to_server(`kick {{ player['name'] }}`)" type="button" class="btn btn-outline-danger controls">Kick</button>
<br>
<button onclick="send_command_to_server(`op {{ player['name'] }}`)" type="button" class="btn btn-warning controls">OP</button>
<br class="mobile-break"/>
<button onclick="send_command_to_server(`deop {{ player['name'] }}`)" type="button" class="btn btn-outline-warning controls">De-OP</button>
{% else %}
<span> Unavailable <br>(Server Offline)</span>
{% end %}
</td>
</tr>
{% end %}
</tbody>
</table>
</div>
<style>
@media (min-width: 600px) {
.mobile-break { display: none;}
.offline-status {
display: none;
}
}
@media screen and (max-width: 600px) {
.conn-break { display: none; }
}
button.controls {
width: 70px;
}
</style>
<div class="col-xl-6 col-lg-12 col-md-12 col-sm-12 no-scroll" width="100%">
<h2>{{ translate('serverPlayerManagement', 'bannedPlayers', data['lang']) }}:</h2>
<table class="table table-sm-responsive d-none d-lg-block no-scroll" style="width: 100%;">
<thead class="thead">
<tr>
<th scope="col">Player</th>
<th scope="col">Status</th>
<th scope="col">Reason</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% for player in data['banned_players'] %}
<tr id="playerItem-{{ player }}" class="playerItem--">
<td><strong> {{ player['name'] }}</strong></td>
<td>Banned on {{ player['banned_on'] }}</td>
<td>Banned by : {{ player['source'] }} <br />Reason : {{ player['reason'] }}</td>
<td class="buttons">
<button onclick="send_command_to_server(`pardon {{ player['name'] }} `)" type="button" class="btn btn-danger">Unban</button>
</td>
</tr>
{% end %}
</tbody>
</table>
<table class="table table-sm-responsive d-block d-lg-none" style="width: 100%;">
<thead class="thead ">
<tr>
<th scope="col">Player</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% for player in data['banned_players'] %}
<tr id="playerItem-{{ player }}" class="playerItem--">
<td><strong> {{ player['name'] }}</strong></td>
<td class="buttons">
<button onclick="send_command_to_server(`pardon {{ player['name'] }} `)" type="button" class="btn btn-danger">Unban</button>
</td>
</tr>
{% end %}
</tbody>
</table>
</div>

View File

@ -14,7 +14,8 @@
<div class="col-12">
<div class="page-header">
<h4 class="page-title">
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{ data['server_stats']['server_id']['server_name'] }}
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
</h4>
@ -39,63 +40,11 @@
</span>
<div class="row">
<div class="col-md-6 col-sm-12">
<style>
.playerItem {
padding: 1rem;
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: space-between;
margin: 1rem 0px 1rem 0px;
}
.playerItem h3 {
vertical-align: middle;
padding: 0px;
margin: 0px;
margin-right: 1.5rem;
}
.playerItem button {
vertical-align: middle;
margin: 0.25rem;
}
.playerUnban {
margin-bottom: 1rem;
}
.banned span {
font-size: 1.1rem;
}
</style>
<h2>{{ translate('serverPlayerManagement', 'players', data['lang']) }}:</h2>
<ul style="list-style: none;padding: 0px;margin: 0px; margin-bottom: 1rem;gap: 1rem;">
{% for player in data['get_players'] %}
<li class="playerItem">
<h3>{{ player }}</h3>
<div class="buttons">
<button onclick="send_command_to_server('ban {{ player }}')" type="button" class="btn btn-danger">Ban</button>
<button onclick="send_command_to_server('kick {{ player }}')" type="button" class="btn btn-outline-danger">Kick</button>
<button onclick="send_command_to_server('op {{ player }}')" type="button" class="btn btn-warning">OP</button>
<button onclick="send_command_to_server('deop {{ player }}')" type="button" class="btn btn-outline-warning">De-OP</button>
</div>
</li>
{% end %}
</ul>
</div>
<div class="col-md-6 col-sm-12">
<h2>{{ translate('serverPlayerManagement', 'bannedPlayers', data['lang']) }}:</h2>
<ul id="bannedPlayers" style="list-style: none;padding: 0px;margin: 0px; margin-bottom: 1rem;gap: 1rem;">
<li class="playerItem banned">
<h3>{{ translate('serverPlayerManagement', 'loadingBannedPlayers', data['lang']) }}</h3>
</li>
</ul>
</div>
{% include "parts/server_players.html %}
</div>
<hr />
</div>
</div>
</div>
@ -128,7 +77,7 @@
$(document).ready(function () {
console.log("ready!");
var bannedPlayers = `{{ data['banned_players'] }}`;
var bannedPlayers = `{{ data['banned_players_html'] }}`;
var bannedPlayersDecoded = htmlDecode(bannedPlayers);
@ -136,21 +85,22 @@
});
function send_command_to_server(command) {
async function send_command_to_server(command) {
console.log(command)
var token = getCookie("_xsrf")
const token = getCookie("_xsrf")
console.log('sending command: ' + command)
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/send_command?id=' + serverId,
data: { command },
success: function (data) {
console.log("got response:");
console.log(data);
let res = await fetch(`/api/v2/servers/${serverId}/stdin`, {
method: 'POST',
headers: {
'X-XSRFToken': token
},
body: command,
});
let responseData = await res.text();
console.log("got response:");
console.log(responseData);
}

View File

@ -44,12 +44,7 @@
<div class="col-md-6 col-sm-12">
<br>
<br>
<form class="forms-sample" method="post" action="/panel/server_backup">
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['server_stats']['server_id']['server_id'] }}">
<input type="hidden" name="subpage" value="backup">
<form id="backup-form" class="forms-sample">
{% if data['backing_up'] %}
<div class="progress" style="height: 15px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar"
@ -149,8 +144,6 @@
data-server_path="{{ data['server_stats']['server_id']['path']}}" type="button">{{
translate('serverBackups', 'clickExclude', data['lang']) }}</button>
</div>
<input type="number" class="form-control" name="changed" id="changed" value="0"
style="visibility: hidden;"></input>
<div class="modal fade" id="dir_select" tabindex="-1" role="dialog" aria-labelledby="dir_select"
aria-hidden="true">
<div class="modal-dialog" role="document">
@ -175,10 +168,8 @@
</div>
</div>
<div class="modal-footer">
<button type="button" id="modal-cancel" class="btn btn-secondary" data-dismiss="modal">{{
translate('serverBackups', 'cancel', data['lang']) }}</button>
<button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary">{{
translate('serverWizard', 'save', data['lang']) }}</button>
<button type="button" id="modal-cancel" class="btn btn-secondary" data-dismiss="modal"><i class="fa-solid fa-xmark"></i></button>
<button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary"><i class="fa-solid fa-thumbs-up"></i></button>
</div>
</div>
</div>
@ -316,66 +307,73 @@
return r ? r[1] : undefined;
}
function backup_started() {
var token = getCookie("_xsrf")
async function backup_started() {
const token = getCookie("_xsrf")
document.getElementById('backup_button').style.visibility = 'hidden';
var dialog = bootbox.dialog({
message: "{{ translate('serverBackups', 'backupTask', data['lang']) }}",
closeButton: false
});
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/backup_now?id=' + server_id,
success: function (data) {
return;
},
});
let res = await fetch(`/api/v2/servers/${server_id}/action/backup_server`, {
method: 'POST',
headers: {
'X-XSRFToken': token
}
});
let responseData = await res.json();
if (responseData.status === "ok") {
console.log(responseData);
process_tree_response(responseData);
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
return;
}
function del_backup(filename, id) {
var token = getCookie("_xsrf")
data_to_send = { file_name: filename }
console.log('Sending Command to delete backup: ' + filename)
$.ajax({
type: "DELETE",
headers: { 'X-XSRFToken': token },
url: '/ajax/del_backup?server_id=' + id,
data: {
file_path: filename,
id: id
},
success: function (data) {
location.reload();
async function del_backup(filename, id) {
const token = getCookie("_xsrf")
let contents = JSON.stringify({"filename": filename})
let res = await fetch(`/api/v2/servers/${id}/backups/backup/`, {
method: 'DELETE',
headers: {
'token': token,
},
body: contents
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.reload();
}else{
bootbox.alert({"title": responseData.status,
"message": responseData.error})
}
}
function restore_backup(filename, id) {
var token = getCookie("_xsrf")
async function restore_backup(filename, id) {
const token = getCookie("_xsrf")
let contents = JSON.stringify({"filename": filename})
var dialog = bootbox.dialog({
message: "<i class='fa fa-spin fa-spinner'></i> {{ translate('serverBackups', 'restoring', data['lang']) }}",
closeButton: false
});
console.log('Sending Command to restore backup: ' + filename)
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/restore_backup?server_id=' + id,
data: {
zip_file: filename,
id: id
},
success: function (data) {
setTimeout(function () {
location.href = ('/panel/dashboard');
}, 15000);
let res = await fetch(`/api/v2/servers/${id}/backups/backup/`, {
method: 'POST',
headers: {
'token': token,
},
body: contents
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = "/panel/dashboard";
}else{
bootbox.alert({"title": responseData.status,
"message": responseData.error})
}
}
$("#before-check").on("click", function () {
@ -395,7 +393,66 @@
}
});
function replacer(key, value) {
if (key != "backup_before" && key != "backup_after") {
if (typeof value == "boolean" || key === "executable_update_url") {
return value
} else {
return (isNaN(value) ? value : +value);
}
} else {
return value;
}
}
$(document).ready(function () {
$("#backup-form").on("submit", async function (e) {
e.preventDefault();
const token = getCookie("_xsrf")
let backupForm = document.getElementById("backup-form");
let formData = new FormData(backupForm);
//Remove checks that we don't need in form data.
formData.delete("after-check");
formData.delete("before-check");
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
//We need to make sure these are sent regardless of whether or not they're checked
formDataObject.compress = $("#compress").prop('checked');
formDataObject.shutdown = $("#shutdown").prop('checked');
let excluded = [];
$('input.excluded:checkbox:checked').each(function () {
excluded.push($(this).val());
});
if ($("#root_files_button").hasClass("clicked")){
formDataObject.exclusions = excluded;
}
console.log(excluded);
console.log(formDataObject);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer);
console.log(formDataJsonString);
let res = await fetch(`/api/v2/servers/${server_id}/backups/`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.reload();
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
});
try {
if ($('#backup_path').val() == '') {
console.log('true')
@ -457,7 +514,7 @@
console.log(result);
if (result == true) {
var full_path = backup_path + '/' + file_to_del;
del_backup(full_path, server_id);
del_backup(file_to_del, server_id);
}
}
});
@ -505,27 +562,15 @@
return;
} else {
document.getElementById('root_files_button').classList.add('clicked');
document.getElementById("changed").value = 1;
}
path = $("#root_files_button").data('server_path')
console.log($("#root_files_button").data('server_path'))
var token = getCookie("_xsrf");
const token = getCookie("_xsrf");
var dialog = bootbox.dialog({
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
closeButton: false
});
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/backup_select?id=' + server_id + '&path=' + path,
});
} else {
bootbox.alert("You must input a path before selecting this button");
}
});
if (webSocket) {
webSocket.on('send_temp_path', function (data) {
setTimeout(function () {
var x = document.querySelector('.bootbox');
if (x) {
@ -535,13 +580,15 @@
if (x) {
x.remove()
}
document.getElementById('main-tree-input').setAttribute('value', data.path)
getTreeView(data.path);
document.getElementById('main-tree-input').setAttribute('value', path)
getTreeView(path);
show_file_tree();
}, 5000);
});
}
} else {
bootbox.alert("You must input a path before selecting this button");
}
});
if (webSocket) {
webSocket.on('backup_status', function (backup) {
if (backup.percent >= 100) {
@ -558,68 +605,82 @@
});
}
function getTreeView(path) {
path = path
function getDirView(event){
let path = event.target.parentElement.getAttribute("data-path");
if (document.getElementById(path).classList.contains('clicked')) {
return;
}else{
getTreeView(path);
}
$.ajax({
type: "GET",
url: '/ajax/get_backup_tree?id=' + server_id + '&path=' + path,
dataType: 'text',
success: function (data) {
console.log("got response:");
console.log(data);
}
async function getTreeView(path){
console.log(path)
const token = getCookie("_xsrf");
let res = await fetch(`/api/v2/servers/${server_id}/files`, {
method: 'POST',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({"page": "backups", "path": path}),
});
let responseData = await res.json();
if (responseData.status === "ok") {
console.log(responseData);
process_tree_response(responseData);
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
} else {
try {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
function process_tree_response(response) {
let path = response.data.root_path.path;
let text = `<ul class="tree-nested d-block" id="${path}ul">`;
Object.entries(response.data).forEach(([key, value]) => {
if (key === "root_path" || key === "db_stats"){
//continue is not valid in for each. Return acts as a continue.
return;
}
let checked = ""
let dpath = value.path;
let filename = key;
if (value.dir){
if (value.excluded){
checked = "checked"
}
text += `<li class="tree-item" data-path="${dpath}">
\n<div id="${dpath}" data-path="${dpath}" data-name="${filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" class="checkBoxClass excluded" value="${dpath}" ${checked}>
<span id="${dpath}span" class="files-tree-title" data-path="${dpath}" data-name="${filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
<strong>${filename}</strong>
</span>
</input></div><li>`
}else{
text += `<li
class="d-block tree-ctx-item tree-file"
data-path="${dpath}"
data-name="${filename}"
onclick=""><input type='checkbox' class="checkBoxClass excluded" name='root_path' value="${dpath}" ${checked}><span style="margin-right: 6px;">
<i class="far fa-file"></i></span></input>${filename}</li>`
}
});
text += `</ul>`;
if(response.data.root_path.top){
try {
document.getElementById('main-tree-div').innerHTML += text;
document.getElementById('main-tree').parentElement.classList.add("clicked");
} catch {
document.getElementById('files-tree').innerHTML = text;
}
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-path', serverDir);
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-name', 'Files');
},
});
}
function getToggleMain(event) {
path = event.target.parentElement.getAttribute('data-path');
document.getElementById("files-tree").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
document.getElementById(path + "span").classList.toggle("tree-caret");
}
function getDirView(event) {
path = event.target.parentElement.getAttribute('data-path');
if (document.getElementById(path).classList.contains('clicked')) {
var toggler = document.getElementById(path + "span");
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
}
return;
} else {
$.ajax({
type: "GET",
url: '/ajax/get_backup_dir?id=' + server_id + '&path=' + path,
dataType: 'text',
success: function (data) {
console.log("got response:");
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
try {
}else{
try {
document.getElementById(path + "span").classList.add('tree-caret-down');
document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked");
@ -627,7 +688,7 @@
console.log("Bad")
}
var toggler = document.getElementById(path);
var toggler = document.getElementById(path + "span");
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "span").addEventListener("click", function caretListener() {
@ -635,10 +696,15 @@
document.getElementById(path + "span").classList.toggle("tree-caret-down");
});
}
},
});
}
}
function getToggleMain(event) {
path = event.target.parentElement.getAttribute('data-path');
document.getElementById("files-tree").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
document.getElementById(path + "span").classList.toggle("tree-caret");
}
function show_file_tree() {
$("#dir_select").modal();
}

View File

@ -43,49 +43,36 @@
<div class="row">
<div class="col-md-6 col-sm-12">
<form class="forms-sample" method="post" id="config_form" action="/panel/server_detail">
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['server_stats']['server_id']['server_id'] }}">
<input type="hidden" name="subpage" value="config">
<form class="forms-sample" method="post" id="config_form">
<div class="form-group">
<label for="server_name">{{ translate('serverConfig', 'serverName', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverNameDesc', data['lang']) }}</small>
<label for="server_name">{{ translate('serverConfig', 'serverName', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverNameDesc', data['lang']) }}</small>
</label>
<input type="text" class="form-control" name="server_name" id="server_name"
value="{{ data['server_stats']['server_id']['server_name'] }}"
placeholder="{{ translate('serverConfig', 'serverName', data['lang']) }}" required>
<input type="text" class="form-control" name="server_name" id="server_name" value="{{ data['server_stats']['server_id']['server_name'] }}" placeholder="{{ translate('serverConfig', 'serverName', data['lang']) }}" required>
</div>
{% if data['super_user'] %}
<div class="form-group">
<label for="server_path">{{ translate('serverConfig', 'serverPath', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPathDesc', data['lang']) }}</small>
<label for="server_path">{{ translate('serverConfig', 'serverPath', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPathDesc', data['lang']) }}</small>
</label>
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<span style="color: gray; font-size: 12px;">{{ data['server_stats']['server_id']['path'] }}</span>
<span style="color: gray; font-size: .9vw;">{{ data['server_stats']['server_id']['path'] }}</span>
🔒
</div>
</div>
{% if data['server_stats']['server_type'] != "minecraft-bedrock" %}
<div class="form-group">
<label for="log_path">{{ translate('serverConfig', 'serverLogLocation', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverLogLocationDesc', data['lang'])
<label for="log_path">{{ translate('serverConfig', 'serverLogLocation', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverLogLocationDesc', data['lang'])
}}</small> </label>
<input type="text" class="form-control" name="log_path" id="log_path"
value="{{ data['server_stats']['server_id']['log_path'] }}"
placeholder="{{ translate('serverConfig', 'serverLogLocation', data['lang']) }}" required>
<input type="text" class="form-control" name="log_path" id="log_path" value="{{ data['server_stats']['server_id']['log_path'] }}" placeholder="{{ translate('serverConfig', 'serverLogLocation', data['lang']) }}" required>
</div>
{% end %}
<div class="form-group">
<label for="executable">{{ translate('serverConfig', 'serverExecutable', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverExecutableDesc', data['lang'])
<label for="executable">{{ translate('serverConfig', 'serverExecutable', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverExecutableDesc', data['lang'])
}}</small> </label>
<input type="text" class="form-control" name="executable" id="executable"
value="{{ data['server_stats']['server_id']['executable'] }}"
placeholder="{{ translate('serverConfig', 'serverExecutable', data['lang']) }}" required>
<input type="text" class="form-control" name="executable" id="executable" value="{{ data['server_stats']['server_id']['executable'] }}" placeholder="{{ translate('serverConfig', 'serverExecutable', data['lang']) }}" required>
</div>
{% end %}
{% if data['server_stats']['server_type'] == "minecraft-java" %}
@ -94,10 +81,8 @@
<small class="text-muted ml-1">{{ translate('serverConfig', 'javaVersionDesc', data['lang'])
}}</small>
</label>
<select class="form-select form-control form-control-lg select-css" id="java_selection"
name="java_selection" form="config_form">
<option value="">{{ translate('serverConfig',
'javaNoChange', data['lang'])}}</option>
<select class="form-select form-control form-control-lg select-css" id="java_selection" name="java_selection" form="config_form">
<option value="none">{{ translate('serverConfig', 'javaNoChange', data['lang'])}}</option>
{% for path in data['java_versions'] %}
<option value="{{path}}">{{path}}</option>
{% end %}
@ -110,33 +95,26 @@
<label for="execution_command">{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }}
<small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverExecutionCommandDesc',
data['lang']) }}</small> </label>
<input type="text" class="form-control" name="execution_command" id="execution_command"
value="{{ data['server_stats']['server_id']['execution_command'] }}"
placeholder="{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }}" required>
<input type="text" class="form-control" name="execution_command" id="execution_command" value="{{ data['server_stats']['server_id']['execution_command'] }}" placeholder="{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }}" required>
</div>
{% else %}
<label for="execution_command">{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }}
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<span style="color: gray;">{{ data['server_stats']['server_id']['execution_command'] }}</span> 🔒
<span style="color: gray; font-size: .9vw;">{{ data['server_stats']['server_id']['execution_command'] }}</span> 🔒
</div>
<br>
{% end %}
<div class="form-group">
<label for="stop_command">{{ translate('serverConfig', 'serverStopCommand', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverStopCommandDesc', data['lang'])
<label for="stop_command">{{ translate('serverConfig', 'serverStopCommand', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverStopCommandDesc', data['lang'])
}}</small> </label>
<input type="text" class="form-control" name="stop_command" id="stop_command"
value="{{ data['server_stats']['server_id']['stop_command'] }}"
placeholder="{{ translate('serverConfig', 'serverStopCommand', data['lang']) }}" required>
<input type="text" class="form-control" name="stop_command" id="stop_command" value="{{ data['server_stats']['server_id']['stop_command'] }}" placeholder="{{ translate('serverConfig', 'serverStopCommand', data['lang']) }}">
</div>
<div class="form-group">
<label for="auto_start_delay">{{ translate('serverConfig', 'serverAutostartDelay', data['lang']) }}
<small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverAutostartDelayDesc',
data['lang']) }}</small> </label>
<input type="number" class="form-control" name="auto_start_delay" id="auto_start_delay"
value="{{ data['server_stats']['server_id']['auto_start_delay'] }}" step="1" max="999" min="10"
required>
<input type="number" class="form-control" name="auto_start_delay" id="auto_start_delay" value="{{ data['server_stats']['server_id']['auto_start_delay'] }}" step="1" max="999" min="10" required>
</div>
{% if data['super_user'] %}
@ -145,31 +123,21 @@
<label for="executable_update_url">{{ translate('serverConfig', 'exeUpdateURL', data['lang']) }}
<small class="text-muted ml-1"> - {{ translate('serverConfig', 'exeUpdateURLDesc', data['lang'])
}}</small> </label>
<input type="text" class="form-control" name="executable_update_url" id="executable_update_url"
value="{{ data['server_stats']['server_id']['executable_update_url'] }}"
placeholder="{{ translate('serverConfig', 'exeUpdateURL', data['lang']) }}">
<input type="text" class="form-control" name="executable_update_url" id="executable_update_url" value="{{ data['server_stats']['server_id']['executable_update_url'] }}" placeholder="{{ translate('serverConfig', 'exeUpdateURL', data['lang']) }}">
</div>
{% end %}
<div class="form-group">
<label for="server_ip">{{ translate('serverConfig', 'serverIP', data['lang']) }} <small
class="text-muted ml-1">- {{ translate('serverConfig', 'serverIPDesc', data['lang']) }}</small>
<label for="server_ip">{{ translate('serverConfig', 'serverIP', data['lang']) }} <small class="text-muted ml-1">- {{ translate('serverConfig', 'serverIPDesc', data['lang']) }}</small>
</label>
<input type="text" class="form-control" name="server_ip" id="server_ip"
value="{{ data['server_stats']['server_id']['server_ip'] }}" required>
<input type="text" class="form-control" name="server_ip" id="server_ip" value="{{ data['server_stats']['server_id']['server_ip'] }}" required>
</div>
<div class="form-group">
<label for="server_port">{{ translate('serverConfig', 'serverPort', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPortDesc', data['lang']) }}
<label for="server_port">{{ translate('serverConfig', 'serverPort', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPortDesc', data['lang']) }}
</small> </label>
<input type="number" class="form-control" name="server_port" id="server_port"
value="{{ data['server_stats']['server_id']['server_port'] }}" step="1" max="65566" min="1"
required>
<span data-html="true" class="port-hint text-center"
title="<i class='fal fa-exclamation-triangle'></i> " ,
data-content="{{
translate('serverConfig', 'statsHint1' , data['lang'])}} <br> <br> <strong>{{ translate('serverConfig', 'statsHint2', data['lang'])}}</strong>" ,
data-placement="right"></span>
<input type="number" class="form-control" name="server_port" id="server_port" value="{{ data['server_stats']['server_id']['server_port'] }}" step="1" max="65566" min="1" required>
<span data-html="true" class="port-hint text-center" title="<i class='fal fa-exclamation-triangle'></i> " , data-content="{{
translate('serverConfig', 'statsHint1' , data['lang'])}} <br> <br> <strong>{{ translate('serverConfig', 'statsHint2', data['lang'])}}</strong>" , data-placement="right"></span>
</div>
{% end %}
@ -180,9 +148,7 @@
{{ data['server_stats']['server_id']['stop_command'] }}&nbsp;{{ translate('serverConfig',
'timeoutExplain2', data['lang']) }}
</small> </label>
<input type="number" class="form-control" name="shutdown_timeout" id="shutdown_timeout"
value="{{ data['server_stats']['server_id']['shutdown_timeout'] }}" step="2" max="300" min="60"
required>
<input type="number" class="form-control" name="shutdown_timeout" id="shutdown_timeout" value="{{ data['server_stats']['server_id']['shutdown_timeout'] }}" step="2" max="300" min="60" required>
</div>
<div class="form-group">
<label for="ignored_exits">{{ translate('serverConfig', 'ignoredExits', data['lang']) }}
@ -190,57 +156,50 @@
data['lang'])
}}
</small> </label>
<input type="text" class="form-control" name="ignored_exits" id="ignored_exits"
value="{{ data['server_stats']['server_id']['ignored_exits'] }}">
<input type="text" class="form-control" name="ignored_exits" id="ignored_exits" value="{{ data['server_stats']['server_id']['ignored_exits'] }}">
</div>
<div class="form-group">
<label for="logs_delete_after">{{ translate('serverConfig', 'removeOldLogsAfter', data['lang']) }}
<small class="text-muted ml-1"> - {{ translate('serverConfig', 'removeOldLogsAfterDesc',
data['lang']) }}</small> </label>
<input type="number" class="form-control" name="logs_delete_after" id="logs_delete_after"
value="{{ data['server_stats']['server_id']['logs_delete_after'] }}" step="1" max="365" min="0"
required>
<input type="number" class="form-control" name="logs_delete_after" id="logs_delete_after" value="{{ data['server_stats']['server_id']['logs_delete_after'] }}" step="1" max="365" min="0" required>
</div>
<div class="form-check-flat">
<label for="auto_start" class="form-check-label ml-4 mb-4">
<div class="form-group">
<div class="custom-control custom-switch">
{% if data['server_stats']['server_id']['auto_start'] %}
<input type="checkbox" class="form-check-input" id="auto_start" name="auto_start" checked=""
data-toggle="toggle" value="1">&nbsp;&nbsp;{{ translate('serverConfig', 'serverAutoStart',
data['lang']) }}
<input type="checkbox" class="custom-control-input" id="auto_start" name="auto_start" checked="" value="1">
<label class="custom-control-label" for="auto_start">&nbsp;&nbsp;{{ translate('serverConfig', 'serverAutoStart', data['lang']) }}</label>
{% else %}
<input type="checkbox" class="form-check-input" id="auto_start" name="auto_start" value="1"
data-toggle="toggle">&nbsp;&nbsp;{{
translate('serverConfig', 'serverAutoStart', data['lang']) }}
<input type="checkbox" class="custom-control-input" id="auto_start" name="auto_start" value="1">
<label class="custom-control-label" for="auto_start">&nbsp;&nbsp;{{ translate('serverConfig', 'serverAutoStart', data['lang']) }}</label>
{% end %}
</label>
<label for="crash_detection" class="form-check-label ml-4 mb-4">
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
{% if data['server_stats']['server_id']['crash_detection'] %}
<input type="checkbox" class="form-check-input" id="crash_detection" name="crash_detection"
data-toggle="toggle" checked="" value="1">&nbsp;&nbsp;{{ translate('serverConfig',
'serverCrashDetection', data['lang']) }}
<input type="checkbox" class="custom-control-input" id="crash_detection" name="crash_detection" checked="" value="1">
<label class="custom-control-label" for="crash_detection">&nbsp;&nbsp;{{ translate('serverConfig', 'serverCrashDetection', data['lang']) }}</label>
{% else %}
<input type="checkbox" class="form-check-input" id="crash_detection" name="crash_detection"
data-toggle="toggle" value="1">&nbsp;&nbsp;{{ translate('serverConfig', 'serverCrashDetection',
data['lang']) }}
<input type="checkbox" class="custom-control-input" id="crash_detection" name="crash_detection" value="1">
<label class="custom-control-label" for="crash_detection">&nbsp;&nbsp;{{ translate('serverConfig', 'serverCrashDetection', data['lang']) }}</label>
{% end %}
</label>
{% if data['super_user'] %}
<label for="show_status" class="form-check-label ml-4 mb-4">
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
{% if data['super_user'] %}
{% if data['server_stats']['server_id']['show_status'] %}
<input type="checkbox" class="form-check-input" id="show_status" name="show_status"
data-toggle="toggle" checked="" value="1">&nbsp;&nbsp;{{ translate('serverConfig', 'showStatus',
data['lang']) }}
<input type="checkbox" class="custom-control-input" id="show_status" name="show_status" checked="" value="1">
<label class="custom-control-label" for="show_status">&nbsp;&nbsp;{{ translate('serverConfig', 'showStatus', data['lang']) }}</label>
{% else %}
<input type="checkbox" class="form-check-input" id="show_status" name="show_status"
data-toggle="toggle" value="1">&nbsp;&nbsp;{{ translate('serverConfig', 'showStatus',
data['lang']) }}
<input type="checkbox" class="custom-control-input" id="show_status" name="show_status" value="1">&nbsp;&nbsp;
<label class="custom-control-label" for="show_status">&nbsp;&nbsp;{{ translate('serverConfig', 'showStatus', data['lang']) }}</label>
{% end %}
</label>
{% end %}
{% end %}
</div>
</div>
<button type="submit" class="btn btn-success mr-2"><i class="fas fa-save"></i> {{
@ -265,14 +224,10 @@
<div class="text-center">
{% if data['server_stats']['running'] %}
{% if data['server_stats']['updating'] %}
<i id="update-spinner" class="fa fa-spinner fa-spin"></i>&nbsp;<button
onclick="send_command(serverId, 'update_executable');" id="update_executable" style="max-width: 7rem;"
class="btn btn-warning m-1 flex-grow-1 disabled">{{ translate('serverConfig',
<i id="update-spinner" class="fa fa-spinner fa-spin"></i>&nbsp;<button onclick="send_command(serverId, 'update_executable');" id="update_executable" style="max-width: 7rem;" class="btn btn-warning m-1 flex-grow-1 disabled">{{ translate('serverConfig',
'update', data['lang']) }}</button>
{% else %}
<i style="visibility: hidden;" id="update-spinner" class="fa fa-spinner fa-spin"></i>&nbsp;<button
onclick="send_command(serverId, 'update_executable');" id="update_executable" style="max-width: 7rem;"
class="btn btn-warning m-1 flex-grow-1 disabled">{{ translate('serverConfig',
<i style="visibility: hidden;" id="update-spinner" class="fa fa-spinner fa-spin"></i>&nbsp;<button onclick="send_command(serverId, 'update_executable');" id="update_executable" style="max-width: 7rem;" class="btn btn-warning m-1 flex-grow-1 disabled">{{ translate('serverConfig',
'update', data['lang']) }}</button>
{% end %}
<a class="btn btn-sm btn-danger disabled">{{ translate('serverConfig', 'deleteServer', data['lang'])
@ -281,14 +236,10 @@
{% else %}
{% if not data['failed'] %}
{% if data['server_stats']['updating'] %}
<i id="update-spinner" class="fa fa-spinner fa-spin"></i>&nbsp;<button
onclick="send_command(serverId, 'update_executable');" id="update_executable" style="max-width: 7rem;"
class="btn btn-warning m-1 flex-grow-1">{{ translate('serverConfig',
<i id="update-spinner" class="fa fa-spinner fa-spin"></i>&nbsp;<button onclick="send_command(serverId, 'update_executable');" id="update_executable" style="max-width: 7rem;" class="btn btn-warning m-1 flex-grow-1">{{ translate('serverConfig',
'update', data['lang']) }}</button>
{% else %}
<i style="visibility: hidden;" id="update-spinner" class="fa fa-spinner fa-spin"></i>&nbsp;<button
onclick="send_command(serverId, 'update_executable');" id="update_executable" style="max-width: 7rem;"
class="btn btn-warning m-1 flex-grow-1">{{ translate('serverConfig',
<i style="visibility: hidden;" id="update-spinner" class="fa fa-spinner fa-spin"></i>&nbsp;<button onclick="send_command(serverId, 'update_executable');" id="update_executable" style="max-width: 7rem;" class="btn btn-warning m-1 flex-grow-1">{{ translate('serverConfig',
'update', data['lang']) }}</button>
{% end %}
{% end %}
@ -304,28 +255,26 @@
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.toggle-handle {
background-color: white !important;
}
.toggle-on {
<style>
.custom-control-input:checked~.custom-control-label::before {
color: black !important;
background-color: blueviolet !important;
border-color: var(--outline) !important;
}
.toggle {
height: 0px !important;
background-color: grey !important;
.custom-control-label::before {
background-color: white !important;
top: calc(-0.2rem);
}
.custom-switch .custom-control-label::after {
top: calc(-0.125rem + 1px);
}
</style>
<!-- content-wrapper ends -->
@ -355,11 +304,11 @@
});
function deleteServerE(callback) {
var token = getCookie("_xsrf")
const token = getCookie("_xsrf")
$.ajax({
type: "DELETE",
headers: { 'X-XSRFToken': token },
url: '/ajax/delete_server?id=' + serverId,
url: `/api/v2/servers/${serverId}`,
data: {
},
success: function (data) {
@ -369,11 +318,11 @@
});
}
function deleteServerFilesE(path, callback) {
var token = getCookie("_xsrf")
const token = getCookie("_xsrf")
$.ajax({
type: "DELETE",
headers: { 'X-XSRFToken': token },
url: '/ajax/delete_server_files?id=' + serverId,
url: `/api/v2/servers/${serverId}?files=true`,
data: {
},
success: function (data) {
@ -385,7 +334,7 @@
function send_command(serverId, command) {
//<!-- this getCookie function is in base.html-->
var token = getCookie("_xsrf");
const token = getCookie("_xsrf");
if (command == "update_executable") {
document.getElementById("update-spinner").style.visibility = "visible";
}
@ -393,7 +342,7 @@
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/server/command?command=' + command + '&id=' + serverId,
url: `/api/v2/servers/${serverId}/action/${command}`,
success: function (data) {
console.log("got response:");
console.log(data);
@ -511,7 +460,7 @@
return;
}
else {
var token = getCookie("_xsrf")
const token = getCookie("_xsrf")
setTimeout(function () { window.location = '/panel/dashboard'; }, 5000);
bootbox.dialog({
backdrop: true,
@ -522,7 +471,7 @@
$.ajax({
type: "DELETE",
headers: { 'X-XSRFToken': token },
url: '/ajax/delete_unloaded_server?id=' + serverId,
url: `/api/v2/servers/${serverId}`,
data: {
},
success: function (data) {
@ -550,12 +499,93 @@
$('.port-hint').popover("hide");
});
async function postFormFieldsAsJson({ url, formData }) {
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject);
//Set the fetch options (headers, body)
let fetchOptions = {
//HTTP method set to POST.
method: "PATCH",
//Set the headers that specify you're sending a JSON body request and accepting JSON response
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
// POST request body as JSON string.
body: formDataJsonString,
};
//Get the response body as JSON.
//If the response was not OK, throw an error.
let res = await fetch(url, fetchOptions);
//If the response is not ok throw an error (for debugging)
if (!res.ok) {
let error = await res.text();
throw new Error(error);
}
//If the response was OK, return the response body.
return res.json();
}
function replacer(key, value) {
if (key != "ignored_exits") {
if (typeof value == "boolean" || key === "executable_update_url" || value === '') {
return value
} else {
return (isNaN(value) ? value : +value);
}
} else {
return value;
}
}
$(document).ready(function () {
let token = getCookie("_xsrf")
webSocket.on('remove_spinner', function () {
document.getElementById("update-spinner").style.visibility = "hidden";
});
$("#config_form").on("submit", async function (e) {
e.preventDefault();
const token = getCookie("_xsrf")
let configForm = document.getElementById("config_form");
let formData = new FormData(configForm);
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
//We need to make sure these are sent regardless of whether or not they're checked
formDataObject.show_status = $("#show_status").prop('checked');
formDataObject.crash_detection = $("#crash_detection").prop('checked');
formDataObject.auto_start = $("#auto_start").prop('checked');
console.log(formDataObject);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer);
formDataJsonString["ignored_exits"] = toString(formDataJsonString["ignored_exits"]);
console.log(formDataJsonString.ignored_exits)
console.log(formDataJsonString);
let res = await fetch(`/api/v2/servers/${serverId}`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
location.reload(true);
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
});
});
</script>
{% end %}
{% end %}

View File

@ -67,7 +67,7 @@
translate('serverFiles', 'download', data['lang']) }}</a>
<a onclick="deleteFileE(event)" href="javascript:void(0)" id="deleteFile" href="#"
style="color: red">{{ translate('serverFiles', 'delete', data['lang']) }}</a>
<a onclick="deleteDirE(event)" href="javascript:void(0)" id="deleteDir" href="#" style="color: red">{{
<a onclick="deleteFileE(event)" href="javascript:void(0)" id="deleteDir" href="#" style="color: red">{{
translate('serverFiles', 'delete', data['lang']) }}</a>
<a href="javascript:void(0)" class="closebtn" style="color: var(--info);"
onclick="document.getElementById('files-tree-nav').style.display = 'none';">{{
@ -398,32 +398,36 @@
},
];
let filePath = '', serverFileContent = '';
let path = '', serverFileContent = '';
function clickOnFile(event) {
filePath = event.target.getAttribute('data-path');
$.ajax({
type: 'GET',
url: "/files/get_file?id=" + serverId + "&file_path=" + encodeURIComponent(filePath),
dataType: 'text',
success: function (data) {
console.log('Got File Contents From Server');
json = JSON.parse(data)
if (json.error) {
$('#editorParent').toggle(false) // hide
$('#fileError').toggle(true) // show
$('#fileError').text("{{ translate('serverFiles', 'fileReadError', data['lang']) }}: " + json.error) // show error
editor.blur()
} else {
$('#editorParent').toggle(true) // show
$('#fileError').toggle(false) // hide
setFileName(event.target.innerText);
editor.session.setValue(json.content);
serverFileContent = json.content;
setSaveStatus(true);
}
async function clickOnFile(event) {
const token = getCookie("_xsrf");
path = event.target.getAttribute('data-path');
let res = await fetch(`/api/v2/servers/${serverId}/files`, {
method: 'POST',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({ "page": "files", "path": path }),
});
let responseData = await res.json();
console.log(responseData)
if (responseData.status === "ok") {
console.log('Got File Contents From Server');
$('#editorParent').toggle(true) // show
$('#fileError').toggle(false) // hide
setFileName(event.target.innerText);
editor.session.setValue(responseData.data);
serverFileContent = responseData.data;
setSaveStatus(true);
}
else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
function setFileName(name) {
@ -577,124 +581,141 @@
}
function save() {
async function save() {
let text = editor.session.getValue();
var token = getCookie("_xsrf")
$.ajax({
type: "PUT",
headers: { 'X-XSRFToken': token },
url: "/files/save_file?id=" + serverId,
data: {
file_contents: text,
file_path: filePath
const token = getCookie("_xsrf")
let res = await fetch(`/api/v2/servers/${serverId}/files`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
success: (data) => {
serverFileContent = text;
setSaveStatus(true)
}
body: JSON.stringify({ "path": path, "contents": text }),
});
let responseData = await res.json();
if (responseData.status === "ok") {
serverFileContent = text;
setSaveStatus(true)
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
function createFile(parent, name, callback) {
var token = getCookie("_xsrf")
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: "/files/create_file?id=" + serverId,
data: {
file_parent: parent,
file_name: name
},
success: function (data) {
console.log("got response:");
callback();
async function createFile(parent, name, callback) {
const token = getCookie("_xsrf")
let res = await fetch(`/api/v2/servers/${serverId}/files/create/`, {
method: 'PUT',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({ "parent": parent, "name": name, "directory": false }),
});
let responseData = await res.json();
if (responseData.status === "ok") {
getTreeView($('#root_dir').data('path'));
setTreeViewContext();
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
function createDir(parent, name, callback) {
var token = getCookie("_xsrf")
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: "/files/create_dir?id=" + serverId,
data: {
dir_parent: parent,
dir_name: name
},
success: function (data) {
console.log("got response:");
callback();
async function createDir(parent, name, callback) {
const token = getCookie("_xsrf")
let res = await fetch(`/api/v2/servers/${serverId}/files/create/`, {
method: 'PUT',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({ "parent": parent, "name": name, "directory": true }),
});
let responseData = await res.json();
if (responseData.status === "ok") {
getTreeView($('#root_dir').data('path'));
setTreeViewContext();
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
function renameItem(path, name, callback) {
var token = getCookie("_xsrf")
$.ajax({
type: "PATCH",
headers: { 'X-XSRFToken': token },
url: "/files/rename_file?id=" + serverId,
data: {
item_path: path,
new_item_name: name
},
success: function (data) {
console.log("got response:");
callback();
async function renameItem(path, name, callback) {
const token = getCookie("_xsrf")
let res = await fetch(`/api/v2/servers/${serverId}/files/create/`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({ "path": path, "new_name": name }),
});
let responseData = await res.json();
if (responseData.status === "ok") {
getTreeView($('#root_dir').data('path'));
setTreeViewContext();
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
function deleteFile(path, callback) {
console.log('Deleting: ' + path)
var token = getCookie("_xsrf")
$.ajax({
type: "DELETE",
headers: { 'X-XSRFToken': token },
url: "/files/del_file?id=" + serverId,
data: {
file_path: path
},
success: function (data) {
console.log("got response:");
callback();
async function deleteItem(path, el, callback) {
const token = getCookie("_xsrf");
let res = await fetch(`/api/v2/servers/${serverId}/files`, {
method: 'DELETE',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({ "filename": path }),
});
let responseData = await res.json();
if (responseData.status === "ok") {
el = document.getElementById(path + "li");
$(el).remove();
document.getElementById('files-tree-nav').style.display = 'none';
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
function deleteDir(path, callback) {
var token = getCookie("_xsrf")
$.ajax({
type: "DELETE",
headers: { 'X-XSRFToken': token },
url: "/files/del_dir?id=" + serverId,
data: {
dir_path: path
},
success: function (data) {
console.log("got response:");
callback();
async function unZip(path, callback) {
const token = getCookie("_xsrf")
let res = await fetch(`/api/v2/servers/${serverId}/files/zip/`, {
method: 'POST',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({ "folder": path }),
});
}
let responseData = await res.json();
if (responseData.status === "ok") {
getTreeView($('#root_dir').data('path'));
setTreeViewContext();
} else {
function unZip(path, callback) {
console.log('path: ', path)
var token = getCookie("_xsrf")
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: "/files/unzip_file?id=" + serverId,
data: {
path: path
},
success: function (data) {
window.location.href = "/panel/server_detail?id=" + serverId + "&subpage=files";
},
});
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
async function sendFile(file, path, serverId, left, i, onProgress) {
@ -882,36 +903,104 @@
});
}
function getTreeView(event) {
const path = $('#root_dir').data('path');;
function getDirView(event) {
let path = event.target.parentElement.getAttribute("data-path");
if (document.getElementById(path).classList.contains('clicked')) {
return;
} else {
getTreeView(path);
}
$.ajax({
type: "GET",
url: "/files/get_tree?id=" + serverId + "&path=" + path,
dataType: 'text',
success: function (data) {
console.log("got response:");
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
try {
document.getElementById(path).innerHTML += text;
event.target.parentElement.classList.add("clicked");
} catch {
document.getElementById('files-tree').innerHTML = text;
}
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-path', serverDir);
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-name', 'Files');
setTimeout(function () { setTreeViewContext() }, 1000);
}
async function getTreeView(path) {
const token = getCookie("_xsrf");
let res = await fetch(`/api/v2/servers/${serverId}/files`, {
method: 'POST',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({ "page": "files", "path": path }),
});
let responseData = await res.json();
if (responseData.status === "ok") {
console.log(responseData);
process_tree_response(responseData);
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
function process_tree_response(response) {
let path = response.data.root_path.path;
let text = ``;
if (!response.data.root_path.top) {
text = `<ul class="tree-nested d-block" id="${path}ul">`;
}
Object.entries(response.data).forEach(([key, value]) => {
if (key === "root_path" || key === "db_stats") {
//continue is not valid in for each. Return acts as a continue.
return;
}
let checked = ""
let dpath = value.path;
let filename = key;
if (value.dir) {
if (value.excluded) {
checked = "checked"
}
text += `<li class="tree-item" id="${dpath}li" data-path="${dpath}">
\n<div id="${dpath}" data-path="${dpath}" data-name="${filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" class="checkBoxClass d-none file-check" name="root_path" value="${dpath}" ${checked}>
<span id="${dpath}span" class="files-tree-title" data-path="${dpath}" data-name="${filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
${filename}
</span>
</input></div></li>`
} else {
text += `<li
class="d-block tree-ctx-item tree-file"
data-path="${dpath}"
data-name="${filename}"
onclick="clickOnFile(event)" id="${dpath}li"><input type='checkbox' class="checkBoxClass d-none file-check" name='root_path' value="${dpath}" ${checked}><span style="margin-right: 6px;">
<i class="far fa-file"></i></span></input>${filename}</li>`
}
});
if (!response.data.root_path.top) {
text += `</ul>`;
}
if (response.data.root_path.top) {
try {
document.getElementById('main-tree-div').innerHTML += text;
document.getElementById('main-tree').parentElement.classList.add("clicked");
} catch {
document.getElementById('files-tree').innerHTML = text;
}
} else {
try {
document.getElementById(path + "span").classList.add('tree-caret-down');
document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked");
} catch {
console.log("Bad")
}
var toggler = document.getElementById(path + "span");
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "span").addEventListener("click", function caretListener() {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
});
}
}
setTimeout(function () { setTreeViewContext() }, 1000);
}
function getToggleMain(event) {
path = event.target.parentElement.getAttribute('data-path');
document.getElementById("files-tree").classList.toggle("d-block");
@ -919,53 +1008,6 @@
document.getElementById(path + "span").classList.toggle("tree-caret");
}
function getDirView(event) {
let path = event.target.parentElement.getAttribute('data-path');
if (document.getElementById(path).classList.contains('clicked')) {
var toggler = document.getElementById(path + "span");
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
}
return;
} else {
$.ajax({
type: "GET",
url: "/files/get_dir?id=" + serverId + "&path=" + path,
dataType: 'text',
success: function (data) {
console.log("got response:");
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
try {
document.getElementById(path + "span").classList.add('tree-caret-down');
document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked");
} catch {
console.log("Bad")
}
setTimeout(function () { setTreeViewContext() }, 1000);
var toggler = document.getElementById(path);
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "span").addEventListener("click", function caretListener() {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
});
}
},
});
}
}
function setTreeViewContext() {
var treeItems = Array.from(document.getElementsByClassName('tree-ctx-item'));
@ -1134,45 +1176,12 @@
},
callback: function (result) {
if (!result) return;
deleteFile(path, function () {
el = document.getElementById(path + "li");
$(el).remove();
document.getElementById('files-tree-nav').style.display = 'none';
});
deleteItem(path);
}
});
}
function deleteDirE(event) {
path = event.target.parentElement.getAttribute('data-path');
name = event.target.parentElement.getAttribute('data-name');
bootbox.confirm({
size: "",
title: "{% raw translate('serverFiles', 'deleteItemQuestion', data['lang']) %}",
closeButton: false,
message: "{% raw translate('serverFiles', 'deleteItemQuestionMessage', data['lang']) %}",
buttons: {
confirm: {
label: "{{ translate('serverFiles', 'yesDelete', data['lang']) }}",
className: 'btn-danger'
},
cancel: {
label: "{{ translate('serverFiles', 'noDelete', data['lang']) }}",
className: 'btn-link'
}
},
callback: function (result) {
if (!result) return;
deleteDir(path, function () {
el = document.getElementById(path + "li");
$(el).remove();
document.getElementById('files-tree-nav').style.display = 'none';
});
}
});
}
getTreeView();
getTreeView($('#root_dir').data('path'));
setTreeViewContext();
function setKeyboard(target) {

View File

@ -77,8 +77,8 @@
{% block js %}
<script>
// ##### Log Filter Block #####
var lines = [];
var words = [];
let lines = [];
let words = [];
if (localStorage.getItem("words")) {
try {
words = JSON.parse(localStorage.getItem("words"));
@ -188,27 +188,40 @@
// Populate logs and filter if present
const serverId = new URLSearchParams(document.location.search).get('id')
function get_server_log() {
async function get_server_log() {
const token = getCookie("_xsrf")
let colors = true;
if (!$("#stop_scroll").is(':checked')) {
$.ajax({
type: 'GET',
url: '/ajax/server_log?id=' + serverId + '&full=1',
dataType: 'text',
success: function (data) {
console.log('Got Log From Server')
$('#virt_console').html(data);
scroll();
lines = document.querySelectorAll('.box');
hideFilteredWords();
let res = await fetch(`/api/v2/servers/${serverId}/logs?colors=${colors}`, {
method: 'GET',
headers: {
'X-XSRFToken': token
},
});
let responseData = await res.json();
let html = ``
if (responseData.status === "ok") {
for (let value of responseData.data) {
html += `<span class='box'>${value}<br /></span>`
}
console.log('Got Log From Server')
$('#virt_console').html(html);
scroll();
lines = document.querySelectorAll('.box');
hideFilteredWords();
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
}
$(document).ready(function () {
console.log("ready!");
get_server_log();
populateWords();
});
</script>
{% end %}
{% end %}

View File

@ -37,15 +37,12 @@
<div class="row">
<div class="col-md-8 col-sm-8">
{% if data['new_schedule'] == True %}
<form class="forms-sample" method="post"
<form class="forms-sample" method="post" id="new_schedule_form"
action="/panel/new_schedule?id={{ data['server_stats']['server_id']['server_id'] }}">
{% else %}
<form class="forms-sample" method="post"
<form class="forms-sample" method="post" id="schedule_form"
action="/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{ data['schedule']['schedule_id'] }}">
{% end %}
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['server_stats']['server_id']['server_id'] }}">
<input type="hidden" name="subpage" value="config">
<div class="form-group">
<label for="name">{{ translate('serverSchedules', 'name' , data['lang']) }}</label>
@ -89,7 +86,7 @@
class="text-muted ml-1"> - {{ translate('serverScheduleConfig', 'interval-explain' ,
data['lang']) }}</small> </label>
<input type="number" class="form-control" name="interval" id="interval"
value="{{ data['schedule']['interval'] }}" placeholder="Interval" required>
value="{{ data['schedule']['interval'] }}" placeholder="Interval" required min="1">
<br>
<br>
<select id="interval_type" onchange="ifDays(this);" name="interval_type"
@ -108,7 +105,7 @@
<label for="time">{{ translate('serverScheduleConfig', 'time' , data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverScheduleConfig', 'time-explain' ,
data['lang']) }}</small> </label>
<input type="time" class="form-control" name="time" id="time"
<input type="time" class="form-control" name="start_time" id="time"
value="{{ data['schedule']['time'] }}" placeholder="Time" required>
</div>
</div>
@ -127,7 +124,7 @@
<label for="cron">{{ translate('serverScheduleConfig', 'cron' , data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverScheduleConfig', 'cron-explain' , data['lang'])
}}</small> </label>
<input type="input" class="form-control" name="cron" id="cron"
<input type="input" class="form-control" name="cron_string" id="cron"
value="{{ data['schedule']['cron_string'] }}" placeholder="* * * * *">
</div>
</div>
@ -234,8 +231,122 @@
return r ? r[1] : undefined;
}
function replacer(key, value) {
if (key != "start_time" && key != "cron_string" && key != "interval_type") {
if (typeof value == "boolean") {
return value
}
console.log(key)
if (key === "interval" && value === ""){
return 0;
}
if (key === "command" && typeof(value === "integer")){
return value.toString();
}else {
return (isNaN(value) ? value : +value);
}
} else if (value === "" && key == "start_time"){
return "00:00";
}else{
return value;
}
}
const serverId = new URLSearchParams(document.location.search).get('id');
const schId = new URLSearchParams(document.location.search).get('sch_id');
$(document).ready(function () {
console.log("ready!");
$("#new_schedule_form").on("submit", async function (e) {
e.preventDefault();
const token = getCookie("_xsrf")
let schForm = document.getElementById("new_schedule_form");
let formData = new FormData(schForm);
formData.delete("difficulty");
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
//We need to make sure these are sent regardless of whether or not they're checked
formDataObject.enabled = $("#enabled").prop('checked');
formDataObject.one_time = $("#one_time").prop('checked');
if ($("#difficulty").val() == "reaction"){
formDataObject.interval_type = "reaction";
}
if ($("#action").val() != "command"){
formDataObject.command = formDataObject.action + "_server";
}
if (formDataObject.cron_string != ""){
formDataObject.interval_type = '';
}
console.log(formDataObject);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer);
let res = await fetch(`/api/v2/servers/${serverId}/tasks/`, {
method: 'POST',
headers: {
'X-XSRFToken': token,
"Content-Type": "application/json",
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = `/panel/server_detail?id=${serverId}&subpage=schedules`;
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
});
$("#schedule_form").on("submit", async function (e) {
e.preventDefault();
const token = getCookie("_xsrf")
let schForm = document.getElementById("schedule_form");
let formData = new FormData(schForm);
formData.delete("difficulty");
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
//We need to make sure these are sent regardless of whether or not they're checked
formDataObject.enabled = $("#enabled").prop('checked');
formDataObject.one_time = $("#one_time").prop('checked');
if ($("#difficulty").val() == "reaction"){
formDataObject.interval_type = "reaction";
}
if ($("#action").val() != "command"){
formDataObject.command = formDataObject.action + "_server";
}
if (formDataObject.cron_string != ""){
formDataObject.interval_type = '';
}
console.log(formDataObject);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer);
console.log(formDataJsonString);
let res = await fetch(`/api/v2/servers/${serverId}/tasks/${schId}`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token,
"Content-Type": "application/json",
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = `/panel/server_detail?id=${serverId}&subpage=schedules`;
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
});
});
@ -265,6 +376,7 @@
document.getElementById("parent").required = true;
document.getElementById("interval").required = false;
document.getElementById("time").required = false;
$("#cron").val("");
}
else {
document.getElementById("ifAdvanced").style.display = "none";
@ -274,6 +386,7 @@
document.getElementById("parent").required = false;
document.getElementById("interval").required = true;
document.getElementById("time").required = true;
$("#cron").val("");
}
}
function ifDays() {
@ -286,22 +399,6 @@
}
}
function del_task(sch_id, id) {
var token = getCookie("_xsrf")
$.ajax({
type: "DELETE",
headers: { 'X-XSRFToken': token },
url: '/ajax/del_task?server_id=' + id + '&schedule_id=' + sch_id,
data: {
schedule_id: sch_id,
id: id
},
success: function (data) {
location.reload();
},
});
}
function startup() {
try {
document.getElementById("{{ data['schedule']['interval_type'] }}").setAttribute('selected', true);

View File

@ -51,14 +51,16 @@
data-content="{{ translate('serverSchedules', 'cannotSeeOnMobile', data['lang']) }}" ,
data-placement="bottom"></span>
{% end %}
<div><button
<div>
<button
onclick="location.href=`/panel/add_schedule?id={{ data['server_stats']['server_id']['server_id'] }}`"
class="btn btn-info">{{ translate('serverSchedules', 'create', data['lang']) }}<i
class="fas fa-pencil-alt"></i></button></div>
class="fas fa-pencil-alt"></i></button>
</div>
</div>
<div class="card-body">
<table class="table table-hover d-none d-lg-block responsive-table" id="schedule_table" width="100%"
style="table-layout:fixed;">
<table class="table table-hover d-none d-lg-block responsive-table" id="schedule_table"
style="table-layout:fixed;" aria-describedby="Schedule List">
<thead>
<tr class="rounded">
<th style="width: 2%; min-width: 10px;">{{ translate('serverSchedules', 'name', data['lang']) }}
@ -87,10 +89,10 @@
<p>{{schedule.action}}</p>
</td>
<td id="{{schedule.command}}" class="action" style="overflow: scroll; max-width: 30px;">
<p>{{schedule.command}}</p>
<p style="overflow: scroll;" class="no-scroll">{{schedule.command}}</p>
</td>
<td id="{{schedule.interval}}" class="action">
{% if schedule.interval != '' %}
{% if schedule.interval_type != '' and schedule.interval_type != 'reaction' %}
<p>{{ translate('serverSchedules', 'every', data['lang']) }}</p>
<p>{{schedule.interval}} {{schedule.interval_type}}</p>
{% elif schedule.interval_type == 'reaction' %}
@ -126,8 +128,8 @@
</tbody>
</table>
<hr />
<table class="table table-hover d-block d-lg-none" id="mini_schedule_table" width="100%"
style="table-layout:fixed;">
<table class="table table-hover d-block d-lg-none" id="mini_schedule_table"
style="table-layout:fixed;" aria-describedby="Schedule List Mobile">
<thead>
<tr class="rounded">
<th style="width: 25%; min-width: 50px;">{{ translate('serverSchedules', 'action', data['lang'])
@ -145,7 +147,7 @@
<p>{{schedule.action}}</p>
</td>
<td id="{{schedule.command}}" class="action" style="overflow: scroll; max-width: 30px;">
<p>{{schedule.command}}</p>
<p style="overflow: scroll;">{{schedule.command}}</p>
</td>
<td id="{{schedule.enabled}}" class="action">
{% if schedule.enabled %}
@ -325,7 +327,7 @@
const serverId = new URLSearchParams(document.location.search).get('id')
$(document).ready(function () {
console.log('ready for JS!')
console.log('ready for JS!');
$('#schedule_table').DataTable({
'order': [4, 'asc'],
}
@ -381,39 +383,6 @@
console.log("ready!");
});
function yesnoCheck(that) {
if (that.value == "command") {
document.getElementById("ifYes").style.display = "block";
document.getElementById("command").required = true;
} else {
document.getElementById("ifYes").style.display = "none";
document.getElementById("command").required = false;
}
}
function basicAdvanced(that) {
if (that.value == "advanced") {
document.getElementById("ifAdvanced").style.display = "block";
document.getElementById("ifBasic").style.display = "none";
document.getElementById("interval").required = false;
document.getElementById("time").required = false;
} else {
document.getElementById("ifAdvanced").style.display = "none";
document.getElementById("ifBasic").style.display = "block";
document.getElementById("interval").required = true;
document.getElementById("time").required = true;
}
}
function ifDays(that) {
if (that.value == "days") {
document.getElementById("ifDays").style.display = "block";
document.getElementById("time").required = true;
} else {
document.getElementById("ifDays").style.display = "none";
document.getElementById("time").required = false;
}
}
$(".del_button").click(function () {
var sch_id = $(this).data('sch');
@ -440,21 +409,19 @@
});
});
function del_task(sch_id, id) {
var token = getCookie("_xsrf")
async function del_task(sch_id, id) {
const token = getCookie("_xsrf")
$.ajax({
type: "DELETE",
headers: { 'X-XSRFToken': token },
url: '/ajax/del_task?server_id=' + id + '&schedule_id=' + sch_id,
data: {
schedule_id: sch_id,
id: id
},
success: function (data) {
location.reload();
let res = await fetch(`/api/v2/servers/${id}/tasks/${sch_id}`, {
method: 'DELETE',
headers: {
'token': token,
},
});
let responseData = await res;
if (responseData.statusText === "OK") {
window.location.reload();
}
}
</script>

View File

@ -41,10 +41,9 @@
</span>
<div class="col-md-12">
<button id="to-bottom" style="visibility: hidden; float: right;" class="btn btn-outline-success">{{
<button id="to-bottom" style="visibility: hidden; float: right;" class="btn btn-outline-success" hidden>{{
translate('serverDetails', 'reset', data['lang']) }}</button>
<br />
<br />
<div class="input-group">
<div id="virt_console" class=""
style="width: 100%; font-size: .8em; padding: 5px 10px; border: 1px solid var(--outline); background-color:var(--card-banner-bg);height:500px; overflow: scroll;">
@ -68,7 +67,8 @@
style="visibility: visible">
<button onclick="" id="start-btn" style="max-width: 7rem;"
class="btn btn-warning m-1 flex-grow-1 disabled"><i
class="fa fa-spinner fa-spin"></i>&nbsp;{{translate('serverTerm', 'installing', data['lang']) }}</button>
class="fa fa-spinner fa-spin"></i>&nbsp;{{translate('serverTerm', 'installing', data['lang'])
}}</button>
<button onclick="" id="restart-btn" style="max-width: 7rem;"
class="btn btn-outline-primary m-1 flex-grow-1 disabled">{% raw translate('serverTerm', 'restart',
data['lang']) %}</button>
@ -151,6 +151,8 @@
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
font-family: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace;
white-space: pre-wrap;
}
</style>
<!-- content-wrapper ends -->
@ -174,12 +176,12 @@
stopBtn.setAttribute('disabled', 'disabled');
}
//<!-- this getCookie function is in base.html-->
var token = getCookie("_xsrf");
const token = getCookie("_xsrf");
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/server/command?command=' + command + '&id=' + serverId,
url: `/api/v2/servers/${serverId}/action/${command}`,
success: function (data) {
console.log("got response:");
console.log(data);
@ -194,12 +196,10 @@
document.getElementById('control_buttons').innerHTML = '<button onclick="" id="start-btn" style="max-width: 7rem;" class="btn btn-primary m-1 flex-grow-1">{{ translate("serverTerm", "updating", data["lang"]) }}</button><button onclick="" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate("serverTerm", "restart", data["lang"]) %}</button><button onclick="" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate("serverTerm", "stop", data["lang"]) }}</button>';
}
}
else {
if (updateButton.server_id == serverId) {
else if (updateButton.server_id == serverId) {
window.location.reload()
document.getElementById('update_control_buttons').innerHTML = '<button onclick="send_command(serverId, "start_server");" id="start-btn" style="max-width: 7rem;" class="btn btn-primary m-1 flex-grow-1">{{ translate("serverTerm", "start", data["lang"]) }}</button><button onclick="send_command(serverId, "restart_server");" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate("serverTerm", "restart", data["lang"]) %}</button><button onclick="" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate("serverTerm", "stop", data["lang"]) }}</button>';
}
}
});
}
// Convert running to lower case (example: 'True' converts to 'true') and
@ -228,17 +228,31 @@
}
//{% end %}
function get_server_log() {
$.ajax({
type: 'GET',
url: '/ajax/server_log?id=' + serverId,
dataType: 'text',
success: function (data) {
console.log('Got Log From Server')
$('#virt_console').html(data);
scrollConsole();
async function get_server_log() {
const token = getCookie("_xsrf")
let colors = true;
let res = await fetch(`/api/v2/servers/${serverId}/logs?colors=${colors}`, {
method: 'GET',
headers: {
'X-XSRFToken': token
},
});
let responseData = await res.json();
let html = ``
if (responseData.status === "ok") {
for (let value of responseData.data) {
html += `<span class='box'>${value}<br /></span>`
}
console.log('Got Log From Server')
$('#virt_console').html(html);
scrollConsole();
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
@ -257,7 +271,7 @@
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
let r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
@ -292,7 +306,7 @@
});
function scrollConsole() {
var logview = $('#virt_console');
let logview = $('#virt_console');
if (logview.length)
logview.scrollTop(logview[0].scrollHeight - logview.height());
}
@ -311,12 +325,12 @@
formdata.append('command', serverCommand)
console.log('sending command: ' + serverCommand)
let res = await fetch("/ajax/send_command?id=" + serverId, {
let res = await fetch(`/api/v2/servers/${serverId}/stdin`, {
method: 'POST',
headers: {
'X-XSRFToken': token
},
body: formdata,
body: serverCommand,
});
let responseData = await res.text();
@ -354,9 +368,11 @@
const elem = $(e.currentTarget);
if (Math.round(elem[0].scrollHeight - elem.scrollTop()) <= elem.outerHeight()) {
document.getElementById("to-bottom").style.visibility = "hidden";
document.getElementById("to-bottom").hidden = true;
scrolled = false;
} else {
document.getElementById("to-bottom").style.visibility = "visible";
document.getElementById("to-bottom").hidden = false;
scrolled = true;
}
}
@ -369,7 +385,6 @@
}
$(document).ready(() => {
var scrolled = false;
$('#virt_console').on('scroll', chkScroll);
$('#to-bottom').on('click', scrollToBottom)
});

View File

@ -7,44 +7,54 @@
{% block content %}
<div class="content-wrapper">
<!-- Desktop View -->
<div class="d-none d-sm-block content-wrapper">
<!-- Page Title Header Starts-->
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<h4 class="page-title">Wiki</h4>
<h4 class="page-title">{{ translate('sidebar', 'documentation', data['lang']) }}</h4>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 grid-margin">
<iframe src="https://wiki.craftycontrol.com" width=100% height=2200px title="crafty's wiki"></iframe>
<div class="row iframe-row">
<div class="col-12 iframe-col">
<div class="iframe-wrapper">
<iframe title="crafty's docs" src="https://docs.craftycontrol.com/" class="iframe-item"></iframe>
</div>
</div>
</div>
<!-- content-wrapper ends -->
<style>
.popover-body {
color: white !important;
;
}
</div>
<!-- Mobile View -->
<div class="d-sm-none content-wrapper mobile-content-wrapper">
<iframe title="crafty's docs" src="https://docs.craftycontrol.com/" class="iframe-item"></iframe>
</div>
<!-- content-wrapper ends -->
<style>
.iframe-item {
height: 100%;
width: 100%;
border: none;
}
#desc_id {
-ms-overflow-style: none;
/* for Internet Explorer, Edge */
scrollbar-width: none;
/* for Firefox */
overflow-y: scroll;
}
.iframe-wrapper {
height: 100%;
}
#desc_id::-webkit-scrollbar {
display: none;
/* for Chrome, Safari, and Opera */
}
</style>
.iframe-col {
height: 100%;
}
.iframe-row {
height: 100%;
max-height: calc(100% - 63px);
padding-bottom: 1rem;
}
.mobile-content-wrapper {
padding: 0;
}
</style>
{% end %}
{% end %}

View File

@ -12,6 +12,14 @@
<link rel="stylesheet" href="/static/assets/vendors/ti-icons/css/themify-icons.css">
<link rel="stylesheet" href="/static/assets/vendors/typicons/typicons.css">
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css">
<link rel="manifest" href="/static/assets/crafty.webmanifest">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Crafty">
<link rel="apple-touch-icon" href="../static/assets/images/Crafty_4-0.png">
<!-- endinject -->
<!-- Plugin css for this page -->
<!-- End Plugin css for this page -->
@ -24,7 +32,7 @@
<style>
.auth.auth-bg-1 {
background: url("../../static/assets/images/auth/{% raw data['background'] %}"),
url("../../static/assets/images/auth/login-1.jpg");
url("/static/assets/images/auth/login_1.jpg");
background-size: cover;
}
</style>

View File

@ -12,6 +12,14 @@
<link rel="stylesheet" href="/static/assets/vendors/ti-icons/css/themify-icons.css">
<link rel="stylesheet" href="/static/assets/vendors/typicons/typicons.css">
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css">
<link rel="manifest" href="/static/assets/crafty.webmanifest">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Crafty">
<link rel="apple-touch-icon" href="../static/assets/images/Crafty_4-0.png">
<!-- endinject -->
<!-- Plugin css for this page -->
<!-- End Plugin css for this page -->
@ -24,7 +32,7 @@
<style>
.auth.auth-bg-1 {
background: url("../../static/assets/images/auth/{% raw data['background'] %}"),
url("../../static/assets/images/auth/login-1.jpg");
url("/static/assets/images/auth/login_1.jpg");
background-size: cover;
}
</style>

View File

@ -12,6 +12,13 @@
<link rel="stylesheet" href="/static/assets/vendors/ti-icons/css/themify-icons.css">
<link rel="stylesheet" href="/static/assets/vendors/typicons/typicons.css">
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css">
<link rel="manifest" href="/static/assets/crafty.webmanifest">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<!-- <meta name="apple-mobile-web-app-title" content="Crafty Controller 4"> -->
<link rel="apple-touch-icon" href="../static/assets/images/Crafty_4-0.png">
<!-- endinject -->
<!-- Plugin css for this page -->
<!-- End Plugin css for this page -->
@ -24,7 +31,7 @@
<style>
.auth.auth-bg-1 {
background: url("../../static/assets/images/auth/{% raw data['background'] %}"),
url("../../static/assets/images/auth/login-1.jpg");
url("/static/assets/images/auth/login_1.jpg");
background-size: cover;
background-position: center;
}
@ -113,6 +120,11 @@
<span class="text-small font-weight-semibold"><a href="https://craftycontrol.com/">Crafty Control
{{data['version'] }}</a> </span>
</div>
<div class="text-block text-center my-3">
<a href="/status" class="text-small forgot-password">{{ translate('login', 'viewStatus',
data['lang']) }}</a>
</div>
</form>
</div>
@ -140,7 +152,15 @@
let login_opacity_div = document.getElementById('login_opacity');
let opacity = login_opacity_div.getAttribute('data-value');
document.getElementById('login-form-background').style.background = 'rgb(34, 36, 55, ' + (opacity / 100) + ')';
//Register Service worker for mobile app
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/static/assets/js/shared/service-worker.js', {scope: '/'})
.then(function (registration) {
console.log('Service Worker Registered');
});
}
});
</script>
</body>

View File

@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Crafty Controller</title>
<!-- plugins:css -->
<link rel="stylesheet" href="/static/assets/vendors/mdi/css/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/assets/vendors/flag-icon-css/css/flag-icon.min.css">
<link rel="stylesheet" href="/static/assets/vendors/ti-icons/css/themify-icons.css">
<link rel="stylesheet" href="/static/assets/vendors/typicons/typicons.css">
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css">
<link rel="manifest" href="/static/assets/crafty.webmanifest">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Crafty">
<link rel="apple-touch-icon" href="../static/assets/images/Crafty_4-0.png">
<!-- endinject -->
<!-- Plugin css for this page -->
<!-- End Plugin css for this page -->
<!-- Layout styles -->
<link rel="stylesheet" href="/static/assets/css/dark/style.css">
<!-- End Layout styles -->
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/images/logo_small.svg">
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
</head>
<style>
.auth.auth-bg-1 {
background: url("../../static/assets/images/auth/{% raw data['background'] %}"),
url("/static/assets/images/auth/login_1.jpg");
background-size: cover;
}
</style>
<body class="dark-theme">
<div class="container-scroller">
<div class="container-fluid page-body-wrapper full-page-wrapper">
<div class="content-wrapper d-flex align-items-center auth auth-bg-1 theme-one">
<div class="row w-100">
<div class="col-lg-4 mx-auto">
<div class="auto-form-wrapper">
<div class="text-center">
<img src="/static/assets/images/logo_long.svg"><br /><br />
<div class="col-sm-12 grid-margin stretch-card">
<div class="card card-statistics social-card facebook-card card-colored">
<div class="card-body">
<h4 class="platform-name mb-3 mt-4 font-weight-semibold user-name">{{ translate('offline', 'offline', data['lang']) }}</h4>
<h5 class="headline font-weight-medium"></h5>
<p class="mb-2 comment font-weight-light">
{{ translate('offline', 'pleaseConnect', data['lang']) }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- content-wrapper ends -->
</div>
<!-- page-body-wrapper ends -->
</div>
<!-- container-scroller -->
<!-- plugins:js -->
<script src="/static/assets/vendors/js/vendor.bundle.base.js"></script>
<!-- endinject -->
<!-- inject:js -->
<script src="/static/assets/js/shared/off-canvas.js"></script>
<script src="/static/assets/js/shared/hoverable-collapse.js"></script>
<script src="/static/assets/js/shared/misc.js"></script>
<script src="/static/assets/js/shared/settings.js"></script>
<script src="/static/assets/js/shared/todolist.js"></script>
<!-- endinject -->
</body>
</html>

View File

@ -10,7 +10,7 @@
<style>
.auth.auth-bg-1 {
background: url("../../static/assets/images/auth/{% raw data['background'] %}"),
url("../../static/assets/images/auth/login-1.jpg");
url("/static/assets/images/auth/login_1.jpg");
background-size: cover;
}
</style>
@ -89,7 +89,7 @@
</div>
<!-- View for Small screen -->
<div class="row justify-content-center align-items-sm-center">
<div class="content-wrapper login-modal d-sm-none d-block">
<div class="content-wrapper login-modal d-sm-none d-block" style="background-color: var(--dropdown-bg);">
<img src="/static/assets/images/logo_long.png" style='width: 100%;'>
<hr />
{% if data['running'] != 0 %}
@ -97,6 +97,7 @@
{% end %}
<div class="accordion" id="accordionServers">
{% for server in data['servers'] %}
{% if server['server_data']['show_status'] %}
<div class="card mb-0">
<div class="card-header" id="heading-{{server['server_data']['server_id']}}">
<h2 class="mb-0 container overflow-hidden">
@ -167,6 +168,7 @@
</div>
</div>
{% end %}
{% end %}
</div>
</div>
</div>

View File

@ -1,114 +1,167 @@
<!DOCTYPE html>
<html lang="{{ data['lang_page'] }}" class="default">
<head>
<!-- Required meta tags -->
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
{% block meta %}{% end %}
<title>{% block title %}{{ _('Default') }}{% end %}</title>
<!-- plugins:css -->
<link
rel="stylesheet"
href="/static/assets/vendors/mdi/css/materialdesignicons.min.css"
/>
<link
rel="stylesheet"
href="/static/assets/vendors/flag-icon-css/css/flag-icon.min.css"
/>
<link
rel="stylesheet"
href="/static/assets/vendors/ti-icons/css/themify-icons.css"
/>
<link
rel="stylesheet"
href="/static/assets/vendors/typicons/typicons.css"
/>
<link
rel="stylesheet"
href="/static/assets/vendors/css/vendor.bundle.base.css"
/>
<link
rel="stylesheet"
href="/static/assets/vendors/fontawesome6/css/all.css"
/>
<link rel="manifest" href="/static/assets/crafty.webmanifest" />
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{% block meta %}{% end %}
<title>{% block title %}{{ _('Default') }}{% end %}</title>
<!-- plugins:css -->
<link rel="stylesheet" href="/static/assets/vendors/mdi/css/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/assets/vendors/flag-icon-css/css/flag-icon.min.css">
<link rel="stylesheet" href="/static/assets/vendors/ti-icons/css/themify-icons.css">
<link rel="stylesheet" href="/static/assets/vendors/typicons/typicons.css">
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css">
<link rel="stylesheet" href="/static/assets/vendors/fontawesome6/css/all.css">
<!-- endinject -->
<!-- Plugin css for this page -->
<!-- End Plugin css for this page -->
<!-- Layout styles -->
<link rel="stylesheet" href="/static/assets/css/dark/style.css">
<!-- End Layout styles -->
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/images/logo_small.svg">
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
</head>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="Crafty" />
<link
rel="apple-touch-icon"
href="../static/assets/images/Crafty_4-0.png"
/>
<!-- endinject -->
<!-- Plugin css for this page -->
<!-- End Plugin css for this page -->
<!-- Layout styles -->
<link rel="stylesheet" href="/static/assets/css/dark/style.css" />
<!-- End Layout styles -->
<link
rel="shortcut icon"
type="image/svg+xml"
href="/static/assets/images/logo_small.svg"
/>
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
</head>
<body>
<div class="container-scroller">
<div class="container-fluid page-body-wrapper full-page-wrapper">
<div class="content-wrapper d-flex align-items-sm-center auth auth-bg-1 theme-one">
<div class="mx-auto">
<div class="auto-form-wrapper">
{% block content %}
{% end %}
<body>
<div class="container-scroller">
<div class="container-fluid page-body-wrapper full-page-wrapper">
<div
class="content-wrapper d-flex align-items-sm-center auth auth-bg-1 theme-one"
>
<div class="mx-auto">
<div class="auto-form-wrapper">{% block content %} {% end %}</div>
</div>
</div>
<!-- content-wrapper ends -->
</div>
<!-- content-wrapper ends -->
<!-- page-body-wrapper ends -->
</div>
<!-- page-body-wrapper ends -->
</div>
<!-- container-scroller -->
<!-- plugins:js -->
<script src="/static/assets/vendors/js/vendor.bundle.base.js"></script>
<!-- endinject -->
<!-- inject:js -->
<script src="/static/assets/js/shared/off-canvas.js"></script>
<script src="/static/assets/js/shared/hoverable-collapse.js"></script>
<script src="/static/assets/js/shared/misc.js"></script>
<!-- endinject -->
<script>
<!-- container-scroller -->
<!-- plugins:js -->
<script src="/static/assets/vendors/js/vendor.bundle.base.js"></script>
<!-- endinject -->
<!-- inject:js -->
<script src="/static/assets/js/shared/off-canvas.js"></script>
<script src="/static/assets/js/shared/hoverable-collapse.js"></script>
<script src="/static/assets/js/shared/misc.js"></script>
<!-- endinject -->
<script>
// {% if request.protocol == 'https' %}
let usingWebSockets = true;
// {% if request.protocol == 'https' %}
let usingWebSockets = true;
let listenEvents = [];
let listenEvents = [];
let pageQueryParams, page;
try {
pageQueryParams = 'page_query_params=' + encodeURIComponent(location.search)
page = 'page=' + encodeURIComponent(location.pathname)
var wsInternal = new WebSocket('wss://' + location.host + '/ws?' + page + '&' + pageQueryParams);
wsInternal.onopen = function () {
console.log('opened WebSocket connection:', wsInternal)
};
wsInternal.onmessage = function (rawMessage) {
var message = JSON.parse(rawMessage.data);
try {
pageQueryParams =
"page_query_params=" + encodeURIComponent(location.search);
page = "page=" + encodeURIComponent(location.pathname);
var wsInternal = new WebSocket(
"wss://" + location.host + "/ws?" + page + "&" + pageQueryParams
);
wsInternal.onopen = function () {
console.log("opened WebSocket connection:", wsInternal);
};
wsInternal.onmessage = function (rawMessage) {
var message = JSON.parse(rawMessage.data);
console.log('got message: ', message)
console.log("got message: ", message);
listenEvents
.filter(listenedEvent => listenedEvent.event == message.event)
.forEach(listenedEvent => listenedEvent.callback(message.data))
};
wsInternal.onerror = function (errorEvent) {
console.error('WebSocket Error', errorEvent);
};
wsInternal.onclose = function (closeEvent) {
console.log('Closed WebSocket', closeEvent);
};
listenEvents
.filter((listenedEvent) => listenedEvent.event == message.event)
.forEach((listenedEvent) => listenedEvent.callback(message.data));
};
wsInternal.onerror = function (errorEvent) {
console.error("WebSocket Error", errorEvent);
};
wsInternal.onclose = function (closeEvent) {
console.log("Closed WebSocket", closeEvent);
};
webSocket = {
on: function (event, callback) {
console.log("registered " + event + " event");
listenEvents.push({ event: event, callback: callback });
},
emit: function (event, data) {
var message = {
event: event,
data: data,
};
webSocket = {
on: function (event, callback) {
console.log('registered ' + event + ' event');
listenEvents.push({ event: event, callback: callback })
},
emit: function (event, data) {
var message = {
event: event,
data: data
}
wsInternal.send(JSON.stringify(message));
}
wsInternal.send(JSON.stringify(message));
},
};
} catch (error) {
console.error("Error while making websocket helpers", error);
usingWebSockets = false;
}
} catch (error) {
console.error('Error while making websocket helpers', error);
// {% else %}
usingWebSockets = false;
}
// {% else %}
let usingWebSockets = false;
warn('WebSockets are not supported in Crafty if not using the https protocol')
var webSocket;
// {% end%}
</script>
{% block js %}
<!-- Custom js for this page -->
<!-- End custom js for this page -->
{% end %}
</body>
</html>
warn(
"WebSockets are not supported in Crafty if not using the https protocol"
);
var webSocket;
// {% end%}
</script>
{% block js %}
<!-- Custom js for this page -->
<script>
$(document).ready(function () {
let login_opacity_div = document.getElementById("login_opacity");
let opacity = login_opacity_div.getAttribute("data-value");
document.getElementById("login-form-background").style.background =
"rgb(34, 36, 55, " + opacity / 100 + ")";
//Register Service worker for mobile app
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/static/assets/js/shared/service-worker.js", {
scope: "/",
})
.then(function (registration) {
console.log("Service Worker Registered");
});
}
});
</script>
<!-- End custom js for this page -->
{% end %}
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -32,15 +32,15 @@
<h4>{{ translate('serverWizard', 'newServer', data['lang']) }}</h4>
<br />
<p class="card-description">
<form method="post" class="server-wizard" onSubmit="wait_msg()">
{% raw xsrf_form_html() %}
<form method="submit" id="server_creation" class="server-wizard">
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label for="server_jar">{{ translate('serverWizard', 'serverType', data['lang'])
}}</label></br>
<select style="width: 80%" required class="selectpicker form-control form-control-lg select-css"
id="steam_server" data-live-search="true" name="steam_server">
<div class="input-group">
<select data-container="body" required class="selectpicker form-control form-control-lg select-css"
id="steam_server" data-live-search="true" name="app_id">
<option value="None">None</option>
{% for s in data['servers'] %}
{% if data["windows"] and s["windows"] %}
@ -50,16 +50,19 @@
{% end %}
{% end %}
</select>
{% if data['super_user'] %}
&nbsp;&nbsp;<i onclick="refreshCache()" style="float: left;" id="refresh-cache"
class="refresh-class fas fa-sync"></i>
<div class="input-group-append">
<button class="btn custom-picker" type="button" onclick="refreshCache()"><i id="refresh-cache" class="refresh-class fas fa-sync"></i></button>
</div>
{% end %}
</div>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label for="server_name">{{ translate('serverWizard', 'serverName', data['lang']) }}</label>
<input type="text" class="form-control" id="server_name" name="server_name"
<input type="text" class="form-control" id="server_name" name="name"
placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}" required>
</div>
</div>
@ -345,142 +348,114 @@
{% end %}
{% block js%}
<script>
document.getElementById("root_files_button").addEventListener("click", function () {
if (document.forms["zip"]["server_path"].value != "") {
if (document.getElementById('root_files_button').classList.contains('clicked')) {
document.getElementById('main-tree-div').innerHTML = '<input type="radio" id="main-tree-input" name="root_path" value="" checked><span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path=""><i class="far fa-folder"></i><i class="far fa-folder-open"></i>{{ translate("serverFiles", "files", data["lang"]) }}</span></input>'
} else {
document.getElementById('root_files_button').classList.add('clicked')
}
path = document.forms["zip"]["server_path"].value;
console.log(document.forms["zip"]["server_path"].value)
var token = getCookie("_xsrf");
var dialog = bootbox.dialog({
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
closeButton: false
});
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/unzip_server?id=-1&path=' + encodeURIComponent(path),
});
} else {
bootbox.alert("You must input a path before selecting this button");
}
</script>
{% end %}
{% block js %}
<script>
var upload = false;
var file;
function sendFile() {
file = $("#file")[0].files[0]
document.getElementById("upload_input").innerHTML = '<div class="progress"><div id="upload-progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>'
let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf")
let fileName = file.name
let target = '/upload'
let mimeType = file.type
let size = file.size
let type = 'server_import'
xmlHttpRequest.upload.addEventListener('progress', function (e) {
if (e.loaded <= size) {
var percent = Math.round(e.loaded / size * 100);
$(`#upload-progress-bar`).css('width', percent + '%');
$(`#upload-progress-bar`).html(percent + '%');
}
});
xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
xmlHttpRequest.setRequestHeader('X-Content-Length', size);
xmlHttpRequest.setRequestHeader('X-Content-Disposition', 'attachment; filename="' + fileName + '"');
xmlHttpRequest.setRequestHeader('X-Content-Upload-Type', type);
xmlHttpRequest.setRequestHeader('X-FileName', fileName);
xmlHttpRequest.addEventListener('load', (event) => {
if (event.target.responseText == 'success') {
console.log('Upload for file', file.name, 'was successful!')
document.getElementById("upload_input").innerHTML = '<div class="card-header header-sm d-flex justify-content-between align-items-center"><span id="file-uploaded" style="color: gray;">' + fileName + '</span> 🔒</div>';
document.getElementById("lower_half").style.visibility = "visible";
}
else {
let response_text = JSON.parse(event.target.responseText);
var x = document.querySelector('.bootbox');
console.log(JSON.parse(event.target.responseText).info)
bootbox.alert({
message: JSON.parse(event.target.responseText).info,
callback: function () {
window.location.reload();
}
async function send_server(data) {
let token = getCookie("_xsrf")
let res = await fetch(`/api/v2/servers/`, {
method: 'POST',
headers: {
'X-XSRFToken': token
},
body: data,
});
doUpload = false;
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = '/panel/dashboard';
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
}
}, false);
xmlHttpRequest.addEventListener('error', (e) => {
console.error('Error while uploading file', file.name + '.', 'Event:', e)
}, false);
xmlHttpRequest.send(file);
async function refreshCache() {
document.getElementById("refresh-cache").classList.add("fa-spin")
let token = getCookie("_xsrf")
let res = await fetch(`/api/v2/crafty/SteamCache/`, {
method: 'GET',
headers: {
'X-XSRFToken': token
},
});
let responseData = await res.json();
if (responseData.status === "ok") {
document.getElementById("refresh-cache").classList.remove("fa-sync");
document.getElementById("refresh-cache").classList.remove("fa-spin");
document.getElementById("refresh-cache").classList.add("fa-check");
setTimeout(() => {
location.reload();
}, 2000);
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error
});
}
}
$("#server_creation").on("submit", async function (e) {
wait_msg();
e.preventDefault();
let jarForm = document.getElementById("server_creation");
document.getElementById("root_files_button").addEventListener("click", function () {
if (document.forms["zip"]["server_path"].value != "") {
if (document.getElementById('root_files_button').classList.contains('clicked')) {
document.getElementById('main-tree-div').innerHTML = '<input type="radio" id="main-tree-input" name="root_path" value="" checked><span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path=""><i class="far fa-folder"></i><i class="far fa-folder-open"></i>{{ translate("serverFiles", "files", data["lang"]) }}</span></input>'
} else {
document.getElementById('root_files_button').classList.add('clicked')
let formData = new FormData(jarForm);
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
console.log(formDataObject);
let send_data = {
"name": formDataObject.name,
"roles": calcRoles(),
"monitoring_type": "steam_cmd",
"steam_cmd_monitoring_data": {
"host": "127.0.0.1",
"port": "27015"
},
"create_type": "steam_cmd",
"steam_cmd_create_data": {
"create_type": "download_exe",
"download_exe_create_data": {
"app_id": formDataObject.app_id,
}
}
path = document.forms["zip"]["server_path"].value;
console.log(document.forms["zip"]["server_path"].value)
var token = getCookie("_xsrf");
var dialog = bootbox.dialog({
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
closeButton: false
});
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/unzip_server?id=-1&path=' + encodeURIComponent(path),
});
} else {
bootbox.alert("You must input a path before selecting this button");
}
});
console.log(send_data);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(send_data, replacer);
document.getElementById("root_upload_button").addEventListener("click", function () {
if (file) {
upload = true;
if (document.getElementById('root_upload_button').classList.contains('clicked')) {
document.getElementById('main-tree-div-upload').innerHTML = '<input type="radio" id="main-tree-input-upload" name="root_path" value="" checked><span id="main-tree-upload" class="files-tree-title tree-caret-down root-dir" data-path=""><i class="far fa-folder"></i><i class="far fa-folder-open"></i>{{ translate("serverFiles", "files", data["lang"]) }}</span></input>'
} else {
document.getElementById('root_upload_button').classList.add('clicked')
}
var token = getCookie("_xsrf");
var dialog = bootbox.dialog({
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
closeButton: false
});
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/unzip_server?id=-1&file=' + encodeURIComponent(file.name),
});
console.log(formDataJsonString);
send_server(formDataJsonString);
});
function replacer(key, value) {
if (key === "roles") {
return value
}
if (key != "ignored_exits") {
if (typeof value == "boolean" || key === "host" || key === "version") {
return value
} else {
bootbox.alert("You must input a path before selecting this button");
return (isNaN(value) ? value : +value);
}
});
</script>
<script>
$(".tree-reset").on("click", function () {
location.href = "/server/step1";
});
} else {
return value;
}
}
function calcRoles() {
let role_ids = $('.roles').map(function () {
if ($(this).is(':checked')) {
return $(this).val();
}
}).get();
console.log(role_ids)
return role_ids
}
function dropDown(event) {
event.target.parentElement.children[1].classList.remove("d-none");
document.getElementById("overlay").classList.remove("d-none");
@ -519,144 +494,6 @@
});
}
function show_file_tree() {
if (upload) {
$("#dir_upload_select").modal();
} else {
$("#dir_select").modal();
}
}
function check_sizes(a, b, changed) {
max_mem = parseFloat(a.val());
min_mem = parseFloat(b.val());
if (max_mem < min_mem && changed === 'min') {
a.val(min_mem)
}
if (max_mem < min_mem && changed === 'max') {
b.val(max_mem)
}
}
function getTreeView(path) {
const styles = window.getComputedStyle(document.getElementById("lower_half"));
//If this value is still hidden we know the user is executing a zip import and not an upload
if (styles.visibility === "hidden") {
document.getElementById('zip_submit').disabled = false;
} else {
document.getElementById('upload_submit').disabled = false;
}
path = path
$.ajax({
type: "GET",
url: '/ajax/get_zip_tree?id=-1&path=' + path,
dataType: 'text',
success: function (data) {
console.log("got response:");
console.log(data);
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
if (styles.visibility === "hidden") {
try {
document.getElementById('main-tree-div').innerHTML += text;
document.getElementById('main-tree').parentElement.classList.add("clicked");
} catch {
document.getElementById('files-tree').innerHTML = text;
}
} else {
try {
document.getElementById('main-tree-div-upload').innerHTML += text;
document.getElementById('main-tree-upload').parentElement.classList.add("clicked");
} catch {
document.getElementById('files-tree').innerHTML = text;
}
}
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-path', serverDir);
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-name', 'Files');
},
});
}
function getToggleMain(event) {
path = event.target.parentElement.getAttribute('data-path');
document.getElementById("files-tree").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
document.getElementById(path + "span").classList.toggle("tree-caret");
}
function getDirView(event) {
path = event.target.parentElement.getAttribute('data-path');
if (document.getElementById(path).classList.contains('clicked')) {
var toggler = document.getElementById(path + "span");
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
}
return;
} else {
$.ajax({
type: "GET",
url: '/ajax/get_zip_dir?id=-1&path=' + path,
dataType: 'text',
success: function (data) {
console.log("got response:");
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
try {
document.getElementById(path + "span").classList.add('tree-caret-down');
document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked");
} catch {
console.log("Bad")
}
var toggler = document.getElementById(path);
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "span").addEventListener("click", function caretListener() {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
});
}
},
});
}
}
if (webSocket) {
webSocket.on('send_temp_path', function (data) {
setTimeout(function () {
var x = document.querySelector('.bootbox');
if (x) {
x.remove()
}
var x = document.querySelector('.modal-backdrop');
if (x) {
x.remove()
}
document.getElementById('main-tree-input').setAttribute('value', data.path)
document.getElementById('main-tree-input-upload').setAttribute('value', data.path)
getTreeView(data.path);
show_file_tree();
$("#root_files_button").attr("disabled", "disabled");
$("#root_upload_button").attr("disabled", "disabled");
}, 5000);
});
}
function refreshCache() {
var token = getCookie("_xsrf")
document.getElementById("refresh-cache").classList.add("fa-spin")

Some files were not shown because too many files have changed in this diff Show More