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

View File

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

View File

@ -57,3 +57,27 @@ pylint:
reports: reports:
codequality: codeclimate.json codequality: codeclimate.json
when: always 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 # Exceptions that will emit a warning when being caught. Defaults to
# "BaseException, Exception". # "BaseException, Exception".
overgeneral-exceptions=BaseException, overgeneral-exceptions=builtins.BaseException,
Exception builtins.Exception

View File

@ -1,15 +1,80 @@
# Changelog # Changelog
## --- [4.0.23] - 2023/TBD ## --- [4.2.0] - 2023/TBD
### New features ### New features
TBD - Finish and Activate Arcadia notification backend ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/621))
### Bug fixes ### 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 ### 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 ### Lang
TBD TBD
<br><br> <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 ## --- [4.0.22] - 2023/04/08
### Bug fixes ### 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)) - 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-11-jre-headless \
openjdk-17-jre-headless \ openjdk-17-jre-headless \
lib32stdc++6 \ lib32stdc++6 \
tzdata \
&& apt-get autoremove \ && apt-get autoremove \
&& apt-get clean && apt-get clean
@ -68,7 +69,7 @@ LABEL \
org.opencontainers.image.title="Crafty Controller" \ org.opencontainers.image.title="Crafty Controller" \
org.opencontainers.image.description="A Game Server Control Panel / Launcher" \ org.opencontainers.image.description="A Game Server Control Panel / Launcher" \
org.opencontainers.image.url="https://craftycontrol.com/" \ 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.source="https://gitlab.com/crafty-controller/crafty-4" \
org.opencontainers.image.vendor="Arcadia Technology, LLC." \ org.opencontainers.image.vendor="Arcadia Technology, LLC." \
org.opencontainers.image.licenses="GPL-3.0" 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 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 > Python based Control Panel for your Minecraft Server
## What is Crafty Controller? ## 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. is compatible with Docker, Linux, Windows 7, Windows 8 and Windows 10.
## Documentation ## Documentation
Documentation available on [wiki.craftycontrol.com](https://craftycontrol.com) Documentation available on [Crafty Docs](https://docs.craftycontrol.com)
## Meta ## Meta
Project Homepage - https://craftycontrol.com Project Homepage - https://craftycontrol.com

View File

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

View File

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

View File

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

View File

@ -16,6 +16,12 @@ logger = logging.getLogger(__name__)
class Server: class Server:
def __init__(self, data): 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") self.description = data.get("description")
# print(self.description) # print(self.description)
if isinstance(self.description, dict): if isinstance(self.description, dict):

View File

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

View File

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

View File

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

View File

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

View File

@ -92,6 +92,9 @@ class MainPrompt(cmd.Cmd):
self.controller.users.update_user(user_id, {"password": new_pass}) 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 @staticmethod
def do_threads(_line): def do_threads(_line):
for thread in threading.enumerate(): for thread in threading.enumerate():

View File

@ -325,3 +325,12 @@ class FileHelpers:
else: else:
return "false" return "false"
return 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 pathlib
import ctypes import ctypes
import shutil import shutil
import shlex
import subprocess import subprocess
import itertools import itertools
from datetime import datetime from datetime import datetime
@ -148,6 +149,29 @@ class Helpers:
logger.error(f"Unable to resolve remote bedrock download url! \n{e}") logger.error(f"Unable to resolve remote bedrock download url! \n{e}")
return False 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): def detect_java(self):
if len(self.find_java_installs()) > 0: if len(self.find_java_installs()) > 0:
return True return True
@ -302,6 +326,16 @@ class Helpers:
except Exception: except Exception:
return False 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 @staticmethod
def check_port(server_port): def check_port(server_port):
try: try:
@ -474,9 +508,9 @@ class Helpers:
return mounts return mounts
def is_subdir(self, server_path, root_dir): def is_subdir(self, child_path, parent_path):
server_path = os.path.realpath(server_path) server_path = os.path.realpath(child_path)
root_dir = os.path.realpath(root_dir) root_dir = os.path.realpath(parent_path)
if self.is_os_windows(): if self.is_os_windows():
try: try:
@ -546,20 +580,16 @@ class Helpers:
return version_data return version_data
@staticmethod def get_announcements(self):
def get_announcements(): data = []
data = (
'[{"id":"1","date":"Unknown",'
'"title":"Error getting Announcements",'
'"desc":"Error getting Announcements","link":""}]'
)
try: 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) data = json.loads(response.content)
except Exception as e: except Exception as e:
logger.error(f"Failed to fetch notifications with error: {e}") logger.error(f"Failed to fetch notifications with error: {e}")
if self.update_available:
data.append(self.update_available)
return data return data
def get_version_string(self): def get_version_string(self):
@ -1059,87 +1089,6 @@ class Helpers:
return data 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 @staticmethod
def generate_zip_tree(folder, output=""): def generate_zip_tree(folder, output=""):
file_list = os.listdir(folder) file_list = os.listdir(folder)
@ -1183,23 +1132,6 @@ class Helpers:
</input></div><li>""" </input></div><li>"""
return output 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 @staticmethod
def unzip_backup_archive(backup_path, zip_name): def unzip_backup_archive(backup_path, zip_name):
zip_path = os.path.join(backup_path, zip_name) zip_path = os.path.join(backup_path, zip_name)
@ -1211,22 +1143,6 @@ class Helpers:
return temp_dir return temp_dir
return False 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 @staticmethod
def download_file(executable_url, jar_path): def download_file(executable_url, jar_path):
try: try:
@ -1261,3 +1177,24 @@ class Helpers:
if region == "EN": if region == "EN":
return "en" return "en"
return lang + "-" + region 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.controllers.servers_controller import ServersController
from app.classes.shared.helpers import Helpers from app.classes.shared.helpers import Helpers
from app.classes.shared.file_helpers import FileHelpers 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__) logger = logging.getLogger(__name__)
@ -19,6 +21,8 @@ class ImportHelpers:
def __init__(self, helper, file_helper): def __init__(self, helper, file_helper):
self.file_helper: FileHelpers = file_helper self.file_helper: FileHelpers = file_helper
self.helper: Helpers = 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): def import_jar_server(self, server_path, new_server_dir, port, new_id):
import_thread = threading.Thread( import_thread = threading.Thread(
@ -216,6 +220,34 @@ class ImportHelpers:
# deletes temp dir # deletes temp dir
FileHelpers.del_dirs(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): def download_bedrock_server(self, path, new_id):
download_thread = threading.Thread( download_thread = threading.Thread(
target=self.download_threaded_bedrock_server, target=self.download_threaded_bedrock_server,

View File

@ -5,6 +5,7 @@ from datetime import datetime
import platform import platform
import shutil import shutil
import time import time
import json
import logging import logging
import threading import threading
from peewee import DoesNotExist 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.shared.import_helper import ImportHelpers
from app.classes.minecraft.serverjars import ServerJars from app.classes.minecraft.serverjars import ServerJars
from app.classes.steamcmd.serverapps import SteamApps from app.classes.steamcmd.serverapps import SteamApps
from app.classes.steamcmd.steamcmd import SteamCMD
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -45,7 +45,6 @@ class Controller:
self.import_helper: ImportHelpers = import_helper self.import_helper: ImportHelpers = import_helper
self.server_jars: ServerJars = ServerJars(helper) self.server_jars: ServerJars = ServerJars(helper)
self.steam_apps: SteamApps = SteamApps(helper) self.steam_apps: SteamApps = SteamApps(helper)
self.steam: SteamCMD()
self.users_helper: HelperUsers = HelperUsers(database, self.helper) self.users_helper: HelperUsers = HelperUsers(database, self.helper)
self.roles_helper: HelperRoles = HelperRoles(database) self.roles_helper: HelperRoles = HelperRoles(database)
self.servers_helper: HelperServers = HelperServers(database) self.servers_helper: HelperServers = HelperServers(database)
@ -88,6 +87,17 @@ class Controller:
def set_project_root(self, root_dir): def set_project_root(self, root_dir):
self.project_root = 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): def package_support_logs(self, exec_user):
if exec_user["preparing"]: if exec_user["preparing"]:
return return
@ -304,15 +314,6 @@ class Controller:
Helpers.ensure_dir_exists(new_server_path) Helpers.ensure_dir_exists(new_server_path)
Helpers.ensure_dir_exists(backup_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): def _create_server_properties_if_needed(port, empty=False):
properties_file = os.path.join(new_server_path, "server.properties") properties_file = os.path.join(new_server_path, "server.properties")
has_properties = os.path.exists(properties_file) has_properties = os.path.exists(properties_file)
@ -340,22 +341,25 @@ class Controller:
server_file = f"{create_data['type']}-{create_data['version']}.jar" server_file = f"{create_data['type']}-{create_data['version']}.jar"
# Create an EULA file # Create an EULA file
with open( if "agree_to_eula" in create_data:
os.path.join(new_server_path, "eula.txt"), "w", encoding="utf-8" with open(
) as file: os.path.join(new_server_path, "eula.txt"), "w", encoding="utf-8"
file.write( ) as file:
"eula=" + ("true" if create_data["agree_to_eula"] else "false") file.write(
) "eula="
+ ("true" if create_data["agree_to_eula"] else "false")
)
elif root_create_data["create_type"] == "import_server": elif root_create_data["create_type"] == "import_server":
_copy_import_dir_files(create_data["existing_server_path"])
server_file = create_data["jarfile"] server_file = create_data["jarfile"]
elif root_create_data["create_type"] == "import_zip": elif root_create_data["create_type"] == "import_zip":
# TODO: Copy files from the zip file to the new server directory # TODO: Copy files from the zip file to the new server directory
server_file = create_data["jarfile"] server_file = create_data["jarfile"]
raise NotImplementedError("Not yet implemented") raise NotImplementedError("Not yet implemented")
_create_server_properties_if_needed( # self.import_helper.import_java_zip_server()
create_data["server_properties_port"], if data["create_type"] == "minecraft_java":
) _create_server_properties_if_needed(
create_data["server_properties_port"],
)
min_mem = create_data["mem_min"] min_mem = create_data["mem_min"]
max_mem = create_data["mem_max"] max_mem = create_data["mem_max"]
@ -368,30 +372,72 @@ class Controller:
def _wrap_jar_if_windows(): def _wrap_jar_if_windows():
return f'"{server_file}"' if Helpers.is_os_windows() else server_file return f'"{server_file}"' if Helpers.is_os_windows() else server_file
server_command = ( if root_create_data["create_type"] == "download_jar":
f"java -Xms{_gibs_to_mibs(min_mem)}M " if Helpers.is_os_windows():
f"-Xmx{_gibs_to_mibs(max_mem)}M " # Let's check for and setup for install server commands
f"-jar {_wrap_jar_if_windows()} nogui" 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": elif data["create_type"] == "minecraft_bedrock":
if root_create_data["create_type"] == "import_server": if root_create_data["create_type"] == "import_server":
existing_server_path = Helpers.get_os_understandable_path( existing_server_path = Helpers.get_os_understandable_path(
create_data["existing_server_path"] create_data["existing_server_path"]
) )
try: if Helpers.is_os_windows():
FileHelpers.copy_dir(existing_server_path, new_server_path, True) server_command = (
except shutil.Error as ex: f'"{os.path.join(new_server_path, create_data["executable"])}"'
logger.error(f"Server import failed with error: {ex}") )
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": elif root_create_data["create_type"] == "import_zip":
# TODO: Copy files from the zip file to the new server directory # TODO: Copy files from the zip file to the new server directory
raise NotImplementedError("Not yet implemented") 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) _create_server_properties_if_needed(0, True)
server_command = create_data["command"] server_command = create_data.get("command", server_command)
server_file = (
"./bedrock_server" # HACK: This is a hack to make the server start
)
elif data["create_type"] == "custom": elif data["create_type"] == "custom":
# TODO: working_directory, executable_update # TODO: working_directory, executable_update
if root_create_data["create_type"] == "raw_exec": if root_create_data["create_type"] == "raw_exec":
@ -416,7 +462,13 @@ class Controller:
if server_file_new != "": if server_file_new != "":
# HACK: Horrible hack to make the server start # HACK: Horrible hack to make the server start
server_file = server_file_new 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", "") stop_command = data.get("stop_command", "")
if stop_command == "": if stop_command == "":
# TODO: different default stop commands for server creation types # TODO: different default stop commands for server creation types
@ -433,7 +485,11 @@ class Controller:
elif data["monitoring_type"] == "minecraft_bedrock": elif data["monitoring_type"] == "minecraft_bedrock":
monitoring_port = data["minecraft_bedrock_monitoring_data"]["port"] monitoring_port = data["minecraft_bedrock_monitoring_data"]["port"]
monitoring_host = data["minecraft_bedrock_monitoring_data"]["host"] 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": elif data["monitoring_type"] == "none":
# TODO: this needs to be NUKED.. # TODO: this needs to be NUKED..
# There shouldn't be anything set if there is nothing to monitor # There shouldn't be anything set if there is nothing to monitor
@ -455,131 +511,94 @@ class Controller:
server_host=monitoring_host, server_host=monitoring_host,
server_type=monitoring_type, server_type=monitoring_type,
) )
if data["create_type"] == "minecraft_java":
if ( if root_create_data["create_type"] == "download_jar":
data["create_type"] == "minecraft_java" # modded update urls from server jars will only update the installer
and root_create_data["create_type"] == "download_jar" if create_data["category"] != "modded":
): server_obj = self.servers.get_server_obj(new_server_id)
# modded update urls from server jars will only update the installer url = (
if create_data["category"] != "modded": f"https://serverjars.com/api/fetchJar/{create_data['category']}"
server_obj = self.servers.get_server_obj(new_server_id) f"/{create_data['type']}/{create_data['version']}"
url = ( )
f"https://serverjars.com/api/fetchJar/{create_data['category']}" server_obj.executable_update_url = url
f"/{create_data['type']}/{create_data['version']}" 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 elif root_create_data["create_type"] == "import_server":
self.servers.update_server(server_obj) ServersController.set_import(new_server_id)
self.server_jars.download_jar( self.import_helper.import_jar_server(
create_data["category"], create_data["existing_server_path"],
create_data["type"], new_server_path,
create_data["version"], monitoring_port,
full_jar_path, new_server_id,
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 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 @staticmethod
def verify_jar_server(server_path: str, server_jar: str): def verify_jar_server(server_path: str, server_jar: str):
server_path = Helpers.get_os_understandable_path(server_path) server_path = Helpers.get_os_understandable_path(server_path)
@ -597,123 +616,6 @@ class Controller:
return False return False
return True 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 # BEDROCK IMPORTS
# ********************************************************************************** # **********************************************************************************
@ -768,54 +670,6 @@ class Controller:
) )
return new_id 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): def create_bedrock_server(self, server_name, user_id):
server_id = Helpers.create_uuid() server_id = Helpers.create_uuid()
new_server_dir = os.path.join(self.helper.servers_dir, server_id) new_server_dir = os.path.join(self.helper.servers_dir, server_id)
@ -1120,6 +974,8 @@ class Controller:
"the new directory." "the new directory."
}, },
) )
self.helper.dir_migration = False
return return
# set the cached serve dir # set the cached serve dir
self.helper.servers_dir = new_server_path self.helper.servers_dir = new_server_path

View File

@ -11,6 +11,7 @@ import subprocess
import html import html
import urllib.request import urllib.request
import glob import glob
import json
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
@ -132,6 +133,15 @@ class ServerInstance:
self.server_object = HelperServers.get_server_obj(self.server_id) self.server_object = HelperServers.get_server_obj(self.server_id)
self.stats_helper = HelperServerStats(self.server_id) self.stats_helper = HelperServerStats(self.server_id)
self.last_backup_failed = False 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: try:
self.tz = get_localzone() self.tz = get_localzone()
except ZoneInfoNotFoundError as e: except ZoneInfoNotFoundError as e:
@ -460,7 +470,47 @@ class ServerInstance:
# STEAM SERVERS # 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: try:
self.process = subprocess.Popen( self.process = subprocess.Popen(
self.server_command, self.server_command,
@ -468,6 +518,7 @@ class ServerInstance:
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
env=my_env,
) )
except Exception as ex: except Exception as ex:
logger.error( logger.error(
@ -486,6 +537,9 @@ class ServerInstance:
return return
else: else:
logger.debug(
f"Starting server {self.server_id} with unknown type {HelperServers.get_server_type_by_id(self.server_id)}"
)
try: try:
self.process = subprocess.Popen( self.process = subprocess.Popen(
self.server_command, self.server_command,
@ -799,6 +853,7 @@ class ServerInstance:
) )
if self.settings["stop_command"]: if self.settings["stop_command"]:
self.send_command(self.settings["stop_command"]) self.send_command(self.settings["stop_command"])
self.write_player_cache()
else: else:
# windows will need to be handled separately for Ctrl+C # windows will need to be handled separately for Ctrl+C
self.process.terminate() self.process.terminate()
@ -1247,6 +1302,40 @@ class ServerInstance:
) )
update_thread.start() 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): def check_update(self):
return self.stats_helper.get_server_stats()["updating"] return self.stats_helper.get_server_stats()["updating"]
@ -1433,6 +1522,12 @@ class ServerInstance:
minutes=self.helper.get_setting("dir_size_poll_freq_minutes"), minutes=self.helper.get_setting("dir_size_poll_freq_minutes"),
id=str(self.server_id) + "_dir_poll", 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): def calc_dir_size(self):
server_dt = HelperServers.get_server_data_by_id(self.server_id) server_dt = HelperServers.get_server_data_by_id(self.server_id)
@ -1502,6 +1597,7 @@ class ServerInstance:
"created": datetime.datetime.now().strftime( "created": datetime.datetime.now().strftime(
"%Y/%m/%d, %H:%M:%S" "%Y/%m/%d, %H:%M:%S"
), ),
"players_cache": self.player_cache,
}, },
) )
total_players += int(raw_ping_result.get("online")) total_players += int(raw_ping_result.get("online"))
@ -1543,7 +1639,10 @@ class ServerInstance:
server_name = server.get("server_name", f"ID#{server_id}") server_name = server.get("server_name", f"ID#{server_id}")
logger.debug(f"Pinging server '{server}' on {internal_ip}:{server_port}") 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)) int_mc_ping = ping_raknet(internal_ip, int(server_port))
else: else:
try: try:
@ -1557,10 +1656,10 @@ class ServerInstance:
# if we got a good ping return, let's parse it # if we got a good ping return, let's parse it
if int_mc_ping: if int_mc_ping:
int_data = True int_data = True
if HelperServers.get_server_type_by_id( if (
server["server_id"] HelperServers.get_server_type_by_id(server["server_id"])
) == "minecraft-bedrock" or HelperServers.get_server_type_by_id( == "minecraft-bedrock"
server["server_id"] == "steam" or HelperServers.get_server_type_by_id(server["server_id"]) == "raknet"
): ):
ping_data = Stats.parse_server_raknet_ping(int_mc_ping) ping_data = Stats.parse_server_raknet_ping(int_mc_ping)
else: else:
@ -1670,7 +1769,10 @@ class ServerInstance:
server_port = server_dt["server_port"] server_port = server_dt["server_port"]
logger.debug(f"Pinging server '{self.name}' on {internal_ip}:{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)) int_mc_ping = ping_raknet(internal_ip, int(server_port))
else: else:
int_mc_ping = ping(internal_ip, int(server_port)) int_mc_ping = ping(internal_ip, int(server_port))

View File

@ -41,10 +41,10 @@ scheduler_intervals = {
class TasksManager: class TasksManager:
controller: Controller controller: Controller
def __init__(self, helper, controller): def __init__(self, helper, controller, file_helper):
self.helper: Helpers = helper self.helper: Helpers = helper
self.controller: Controller = controller self.controller: Controller = controller
self.tornado: Webserver = Webserver(helper, controller, self) self.tornado: Webserver = Webserver(helper, controller, self, file_helper)
try: try:
self.tz = get_localzone() self.tz = get_localzone()
except ZoneInfoNotFoundError as e: except ZoneInfoNotFoundError as e:
@ -421,6 +421,7 @@ class TasksManager:
) )
for item in jobs: for item in jobs:
logger.info(f"JOB: {item}") logger.info(f"JOB: {item}")
return task.schedule_id
def remove_all_server_tasks(self, server_id): def remove_all_server_tasks(self, server_id):
schedules = HelpersManagement.get_schedules_by_server(server_id) schedules = HelpersManagement.get_schedules_by_server(server_id)
@ -450,7 +451,6 @@ class TasksManager:
# created task a child of itself. # created task a child of itself.
if str(job_data.get("parent")) == str(sch_id): if str(job_data.get("parent")) == str(sch_id):
job_data["parent"] = None job_data["parent"] = None
HelpersManagement.update_scheduled_task(sch_id, job_data) HelpersManagement.update_scheduled_task(sch_id, job_data)
if not ( if not (
@ -738,12 +738,21 @@ class TasksManager:
def check_for_updates(self): def check_for_updates(self):
logger.info("Checking for Crafty updates...") logger.info("Checking for Crafty updates...")
self.helper.update_available = self.helper.check_remote_version() self.helper.update_available = self.helper.check_remote_version()
remote = self.helper.update_available
if self.helper.update_available: if self.helper.update_available:
logger.info(f"Found new version {self.helper.update_available}") logger.info(f"Found new version {self.helper.update_available}")
else: else:
logger.info( logger.info(
"No updates found! You are on the most up to date Crafty version." "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...") logger.info("Refreshing Gravatar PFPs...")
for user in HelperUsers.get_all_users(): for user in HelperUsers.get_all_users():
if user.email: if user.email:
@ -775,31 +784,37 @@ class TasksManager:
def check_for_old_logs(self): def check_for_old_logs(self):
# check for server logs first # check for server logs first
self.controller.servers.check_for_old_logs() self.controller.servers.check_for_old_logs()
# check for crafty logs now try:
logs_path = os.path.join(self.controller.project_root, "logs") # check for crafty logs now
logs_delete_after = int( logs_path = os.path.join(self.controller.project_root, "logs")
self.helper.get_setting("crafty_logs_delete_after_days") logs_delete_after = int(
) self.helper.get_setting("crafty_logs_delete_after_days")
latest_log_files = [ )
"session.log", latest_log_files = [
"schedule.log", "session.log",
"tornado-access.log", "schedule.log",
"session.log", "tornado-access.log",
"commander.log", "session.log",
] "commander.log",
# we won't delete if delete logs after is set to 0 ]
if logs_delete_after != 0: # we won't delete if delete logs after is set to 0
log_files = list( if logs_delete_after != 0:
filter( log_files = list(
lambda val: val not in latest_log_files, filter(
os.listdir(logs_path), 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.crafty_permissions import EnumPermissionsCrafty
from app.classes.models.users import ApiKeys from app.classes.models.users import ApiKeys
from app.classes.shared.helpers import Helpers from app.classes.shared.helpers import Helpers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.main_controller import Controller from app.classes.shared.main_controller import Controller
from app.classes.shared.translation import Translation from app.classes.shared.translation import Translation
from app.classes.models.management import DatabaseShortcuts from app.classes.models.management import DatabaseShortcuts
@ -24,15 +25,22 @@ class BaseHandler(tornado.web.RequestHandler):
helper: Helpers helper: Helpers
controller: Controller controller: Controller
translator: Translation translator: Translation
file_helper: FileHelpers
# noinspection PyAttributeOutsideInit # noinspection PyAttributeOutsideInit
def initialize( def initialize(
self, helper=None, controller=None, tasks_manager=None, translator=None self,
helper=None,
controller=None,
tasks_manager=None,
translator=None,
file_helper=None,
): ):
self.helper = helper self.helper = helper
self.controller = controller self.controller = controller
self.tasks_manager = tasks_manager self.tasks_manager = tasks_manager
self.translator = translator self.translator = translator
self.file_helper = file_helper
def set_default_headers(self) -> None: 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": if page == "login":
template = "public/login.html" template = "public/login.html"
elif page == 404: elif page == "404":
template = "public/404.html" template = "public/404.html"
elif page == "error": elif page == "error":
template = "public/error.html" template = "public/error.html"
elif page == "offline":
template = "public/offline.html"
elif page == "logout": elif page == "logout":
self.clear_cookie("token") self.clear_cookie("token")
# self.clear_cookie("user") # 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 ( from app.classes.web.routes.api.servers.server.tasks.index import (
ApiServersServerTasksIndexHandler, 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 ( from app.classes.web.routes.api.servers.server.tasks.task.children import (
ApiServersServerTasksTaskChildrenHandler, 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 ( from app.classes.web.routes.api.users.user.permissions import (
ApiUsersUserPermissionsHandler, 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.pfp import ApiUsersUserPfpHandler
from app.classes.web.routes.api.users.user.public import ApiUsersUserPublicHandler 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): def api_handlers(handler_args):
@ -55,12 +83,52 @@ def api_handlers(handler_args):
ApiAuthInvalidateTokensHandler, ApiAuthInvalidateTokensHandler,
handler_args, 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 # User routes
( (
r"/api/v2/users/?", r"/api/v2/users/?",
ApiUsersIndexHandler, ApiUsersIndexHandler,
handler_args, 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]+)/?", r"/api/v2/users/([0-9]+)/?",
ApiUsersUserIndexHandler, ApiUsersUserIndexHandler,
@ -107,11 +175,46 @@ def api_handlers(handler_args):
ApiServersIndexHandler, ApiServersIndexHandler,
handler_args, handler_args,
), ),
(
r"/api/v2/crafty/JarCache/?",
ApiCraftyJarCacheIndexHandler,
handler_args,
),
(
r"/api/v2/crafty/SteamCache/?",
ApiCraftySteamCacheIndexHandler,
handler_args,
),
( (
r"/api/v2/servers/([0-9]+)/?", r"/api/v2/servers/([0-9]+)/?",
ApiServersServerIndexHandler, ApiServersServerIndexHandler,
handler_args, 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/?", r"/api/v2/servers/([0-9]+)/tasks/?",
ApiServersServerTasksIndexHandler, 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"], "required": ["server_id", "permissions"],
}, },
}, },
"manager": {"type": ["integer", "null"]},
}, },
"required": ["name"],
"additionalProperties": False, "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: 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: except ValidationError as e:
return self.finish_json( return self.finish_json(
400, 400,
@ -98,6 +131,9 @@ class ApiRolesIndexHandler(BaseApiHandler):
) )
role_name = data["name"] 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 # Get the servers
servers_dict = {server["server_id"]: server for server in data["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"} 400, {"status": "error", "error": "ROLE_NAME_ALREADY_EXISTS"}
) )
role_id = self.controller.roles.add_role_advanced( role_id = self.controller.roles.add_role_advanced(role_name, servers, manager)
role_name, servers, user["user_id"]
)
self.controller.management.add_to_audit_log( self.controller.management.add_to_audit_log(
user["user_id"], user["user_id"],

View File

@ -4,6 +4,36 @@ from peewee import DoesNotExist
from app.classes.web.base_api_handler import BaseApiHandler from app.classes.web.base_api_handler import BaseApiHandler
modify_role_schema = { 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", "type": "object",
"properties": { "properties": {
"name": { "name": {
@ -109,7 +139,10 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
) )
try: 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: except ValidationError as e:
return self.finish_json( return self.finish_json(
400, 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: try:
self.controller.roles.update_role_advanced( 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: except DoesNotExist:
return self.finish_json(404, {"status": "error", "error": "ROLE_NOT_FOUND"}) return self.finish_json(404, {"status": "error", "error": "ROLE_NOT_FOUND"})

View File

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

View File

@ -31,6 +31,8 @@ class ApiServersServerActionHandler(BaseApiHandler):
if action == "clone_server": if action == "clone_server":
return self._clone_server(server_id, auth_data[4]["user_id"]) 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( self.controller.management.send_command(
auth_data[4]["user_id"], server_id, self.get_remote_ip(), action auth_data[4]["user_id"], server_id, self.get_remote_ip(), action
@ -41,6 +43,11 @@ class ApiServersServerActionHandler(BaseApiHandler):
{"status": "ok"}, {"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 _clone_server(self, server_id, user_id):
def is_name_used(name): def is_name_used(name):
return Servers.select().where(Servers.server_name == name).exists() 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", "type": "object",
"properties": { "properties": {
"server_name": {"type": "string", "minLength": 1}, "server_name": {"type": "string", "minLength": 1},
"path": {"type": "string", "minLength": 1},
"backup_path": {"type": "string"}, "backup_path": {"type": "string"},
"executable": {"type": "string"}, "executable": {"type": "string"},
"log_path": {"type": "string", "minLength": 1}, "log_path": {"type": "string", "minLength": 1},
"execution_command": {"type": "string", "minLength": 1}, "execution_command": {"type": "string", "minLength": 1},
"java_selection": {"type": "string"},
"auto_start": {"type": "boolean"}, "auto_start": {"type": "boolean"},
"auto_start_delay": {"type": "integer"}, "auto_start_delay": {"type": "integer", "minimum": 0},
"crash_detection": {"type": "boolean"}, "crash_detection": {"type": "boolean"},
"stop_command": {"type": "string"}, "stop_command": {"type": "string"},
"executable_update_url": {"type": "string", "minLength": 1}, "executable_update_url": {"type": "string"},
"server_ip": {"type": "string", "minLength": 1}, "server_ip": {"type": "string", "minLength": 1},
"server_port": {"type": "integer"}, "server_port": {"type": "integer"},
"logs_delete_after": {"type": "integer"}, "shutdown_timeout": {"type": "integer", "minimum": 0},
"type": {"type": "string", "minLength": 1}, "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, "additionalProperties": False,
"minProperties": 1, "minProperties": 1,
@ -63,7 +82,11 @@ class ApiServersServerIndexHandler(BaseApiHandler):
) )
try: 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: except ValidationError as e:
return self.finish_json( return self.finish_json(
400, 400,
@ -88,9 +111,24 @@ class ApiServersServerIndexHandler(BaseApiHandler):
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
server_obj = self.controller.servers.get_server_obj(server_id) server_obj = self.controller.servers.get_server_obj(server_id)
java_flag = False
for key in data: for key in data:
# If we don't validate the input there could be security issues # 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 != "path":
if key == "execution_command" and java_flag:
continue
setattr(server_obj, key, data[key]) setattr(server_obj, key, data[key])
self.controller.servers.update_server(server_obj) self.controller.servers.update_server(server_obj)
@ -134,7 +172,16 @@ class ApiServersServerIndexHandler(BaseApiHandler):
) )
self.tasks_manager.remove_all_server_tasks(server_id) 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( self.controller.management.add_to_audit_log(
auth_data[4]["user_id"], auth_data[4]["user_id"],

View File

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

View File

@ -35,7 +35,13 @@ class ApiServersServerStdinHandler(BaseApiHandler):
"Please report this to the devs" "Please report this to the devs"
) )
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) 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")): if svr.send_command(self.request.body.decode("utf-8")):
return self.finish_json( return self.finish_json(
200, 200,

View File

@ -1,16 +1,122 @@
# TODO: create and read # TODO: create and read
import json
import logging 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 from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__) 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): class ApiServersServerTasksIndexHandler(BaseApiHandler):
def get(self, server_id: str, task_id: str): def get(self, server_id: str, task_id: str):
pass pass
def post(self, server_id: str, task_id: str): def post(self, server_id: str):
pass 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 json
import logging import logging
from croniter import croniter
from jsonschema import ValidationError, validate from jsonschema import ValidationError, validate
from app.classes.models.server_permissions import EnumPermissionsServer 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}"}, "start_time": {"type": "string", "pattern": r"\d{1,2}:\d{1,2}"},
"command": {"type": ["string", "null"]}, "command": {"type": ["string", "null"]},
"one_time": {"type": "boolean", "default": False}, "one_time": {"type": "boolean", "default": False},
@ -49,10 +51,47 @@ task_patch_schema = {
class ApiServersServerTasksTaskIndexHandler(BaseApiHandler): class ApiServersServerTasksTaskIndexHandler(BaseApiHandler):
def get(self, server_id: str, task_id: str): 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): 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): def patch(self, server_id: str, task_id: str):
auth_data = self.authenticate_user() 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: if str(data.get("parent")) == str(task_id) and data.get("parent") is not None:
data["parent"] = 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.tasks_manager.update_job(task_id, data)
self.controller.management.add_to_audit_log( self.controller.management.add_to_audit_log(

View File

@ -93,10 +93,17 @@ class ApiUsersIndexHandler(BaseApiHandler):
"error_data": str(e), "error_data": str(e),
}, },
) )
username = data["username"] username = data["username"]
username = str(username).lower() 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"] password = data["password"]
email = data.get("email", "default@example.com") email = data.get("email", "default@example.com")
enabled = data.get("enabled", True) 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( return self.finish_json(
400, {"status": "error", "error": "INVALID_USERNAME"} 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( return self.finish_json(
400, {"status": "error", "error": "USER_EXISTS"} 400, {"status": "error", "error": "USER_EXISTS"}
) )
@ -210,13 +216,13 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
400, {"status": "error", "error": "INVALID_ROLES_MODIFY"} 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) 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: if "roles" in data:
roles: t.Set[str] = set(data.pop("roles")) roles: t.Set[str] = set(data.pop("roles"))
@ -236,6 +242,12 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
user_id, removed_roles 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: if "permissions" in data:
permissions: t.List[UsersController.ApiPermissionDict] = data.pop( permissions: t.List[UsersController.ApiPermissionDict] = data.pop(
"permissions" "permissions"
@ -246,7 +258,7 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
limit_role_creation = 0 limit_role_creation = 0
for permission in permissions: for permission in permissions:
self.controller.crafty_perms.set_permission( permissions_mask = self.controller.crafty_perms.set_permission(
permissions_mask, permissions_mask,
EnumPermissionsCrafty.__members__[permission["name"]], EnumPermissionsCrafty.__members__[permission["name"]],
"1" if permission["enabled"] else "0", "1" if permission["enabled"] else "0",

View File

@ -1,14 +1,10 @@
import json import json
import logging import logging
import os
import time
import tornado.web import tornado.web
import tornado.escape import tornado.escape
import bleach
from app.classes.models.crafty_permissions import EnumPermissionsCrafty from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.shared.helpers import Helpers 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.shared.main_models import DatabaseShortcuts
from app.classes.web.base_handler import BaseHandler from app.classes.web.base_handler import BaseHandler
@ -145,7 +141,11 @@ class ServerHandler(BaseHandler):
"not a server creator or server limit reached" "not a server creator or server limit reached"
) )
return 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["server_types"] = self.controller.server_jars.get_serverjar_data()
page_data["js_server_types"] = json.dumps( page_data["js_server_types"] = json.dumps(
self.controller.server_jars.get_serverjar_data() self.controller.server_jars.get_serverjar_data()
@ -164,7 +164,7 @@ class ServerHandler(BaseHandler):
"not a server creator or server limit reached" "not a server creator or server limit reached"
) )
return return
page_data["server_api"] = True
template = "server/bedrock_wizard.html" template = "server/bedrock_wizard.html"
if page == "steam_cmd_step1": if page == "steam_cmd_step1":
@ -187,500 +187,3 @@ class ServerHandler(BaseHandler):
data=page_data, data=page_data,
translate=self.translator.translate, 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): class CustomStaticHandler(tornado.web.StaticFileHandler):
def validate_absolute_path(self, root: str, absolute_path: str) -> Optional[str]: 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: try:
return super().validate_absolute_path(root, absolute_path) return super().validate_absolute_path(root, absolute_path)
except tornado.web.HTTPError as error: 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.console import Console
from app.classes.shared.helpers import Helpers from app.classes.shared.helpers import Helpers
from app.classes.shared.main_controller import Controller from app.classes.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.public_handler import PublicHandler
from app.classes.web.panel_handler import PanelHandler from app.classes.web.panel_handler import PanelHandler
from app.classes.web.default_handler import DefaultHandler from app.classes.web.default_handler import DefaultHandler
from app.classes.web.routes.api.api_handlers import api_handlers from app.classes.web.routes.api.api_handlers import api_handlers
from app.classes.web.server_handler import ServerHandler from app.classes.web.server_handler import ServerHandler
from app.classes.web.ajax_handler import AjaxHandler
from app.classes.web.api_handler import ( from app.classes.web.api_handler import (
ServersStats, ServersStats,
NodeStats, NodeStats,
@ -48,13 +46,14 @@ class Webserver:
controller: Controller controller: Controller
helper: Helpers helper: Helpers
def __init__(self, helper, controller, tasks_manager): def __init__(self, helper, controller, tasks_manager, file_helper):
self.ioloop = None self.ioloop = None
self.http_server = None self.http_server = None
self.https_server = None self.https_server = None
self.helper = helper self.helper = helper
self.controller = controller self.controller = controller
self.tasks_manager = tasks_manager self.tasks_manager = tasks_manager
self.file_helper = file_helper
self._asyncio_patch() self._asyncio_patch()
@staticmethod @staticmethod
@ -146,13 +145,12 @@ class Webserver:
"controller": self.controller, "controller": self.controller,
"tasks_manager": self.tasks_manager, "tasks_manager": self.tasks_manager,
"translator": self.helper.translation, "translator": self.helper.translation,
"file_helper": self.file_helper,
} }
handlers = [ handlers = [
(r"/", DefaultHandler, handler_args), (r"/", DefaultHandler, handler_args),
(r"/panel/(.*)", PanelHandler, handler_args), (r"/panel/(.*)", PanelHandler, handler_args),
(r"/server/(.*)", ServerHandler, handler_args), (r"/server/(.*)", ServerHandler, handler_args),
(r"/ajax/(.*)", AjaxHandler, handler_args),
(r"/files/(.*)", FileHandler, handler_args),
(r"/ws", SocketHandler, handler_args), (r"/ws", SocketHandler, handler_args),
(r"/upload", UploadHandler, handler_args), (r"/upload", UploadHandler, handler_args),
(r"/status", StatusHandler, handler_args), (r"/status", StatusHandler, handler_args),

View File

@ -25,11 +25,13 @@ class UploadHandler(BaseHandler):
controller: Controller = None, controller: Controller = None,
tasks_manager=None, tasks_manager=None,
translator=None, translator=None,
file_helper=None,
): ):
self.helper = helper self.helper = helper
self.controller = controller self.controller = controller
self.tasks_manager = tasks_manager self.tasks_manager = tasks_manager
self.translator = translator self.translator = translator
self.file_helper = file_helper
def prepare(self): def prepare(self):
# Class & Function Defination # Class & Function Defination
@ -278,11 +280,11 @@ class UploadHandler(BaseHandler):
filename = self.request.headers.get("X-FileName", None) filename = self.request.headers.get("X-FileName", None)
full_path = os.path.join(path, filename) 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( Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"] self.controller.servers.get_server_data_by_id(server_id)["path"]
), ),
full_path,
): ):
logger.warning( logger.warning(
f"User {user_id} tried to upload a file to {server_id} " 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 io_loop = None
def initialize( 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.helper = helper
self.controller = controller self.controller = controller
self.tasks_manager = tasks_manager self.tasks_manager = tasks_manager
self.translator = translator self.translator = translator
self.file_helper = file_helper
self.io_loop = tornado.ioloop.IOLoop.current() self.io_loop = tornado.ioloop.IOLoop.current()
def get_remote_ip(self): def get_remote_ip(self):

View File

@ -1,5 +1,5 @@
{ {
"major": 4, "major": 4,
"minor": 0, "minor": 2,
"sub": 23 "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 { .accordion .card {
margin-bottom: 0px; 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/vendors/css/vendor.bundle.base.css">
<link rel="stylesheet" href="/static/assets/css/crafty.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 --> <!-- endinject -->
<!-- Plugin css for this page--> <!-- Plugin css for this page-->
@ -247,7 +257,7 @@
const sendWssError = () => wsOpen || warn( const sendWssError = () => wsOpen || warn(
'WebSockets are required for Crafty to work. This websocket connection has been closed. Are you using a reverse proxy?', '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' 'wssError'
) )
@ -402,20 +412,26 @@
}); });
} }
function eulaAgree(server_id, command) { async function eulaAgree(server_id, command) {
//< !--this getCookie function is in base.html-- > //< !--this getCookie function is in base.html-- >
var token = getCookie("_xsrf"); const token = getCookie("_xsrf");
$.ajax({ let res = await fetch(`/api/v2/servers/${server_id}/action/eula/`, {
type: "POST", method: 'POST',
headers: { 'X-XSRFToken': token }, headers: {
url: '/ajax/eula?id=' + server_id, 'X-XSRFToken': token
success: function (data) { },
console.log("got response:");
console.log(data);
location.reload();
}
}); });
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> </script>
{% block js %} {% 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/css/vendor.bundle.base.css">
<link rel="stylesheet" href="/static/assets/vendors/fontawesome6/css/all.css"> <link rel="stylesheet" href="/static/assets/vendors/fontawesome6/css/all.css">
<link rel="stylesheet" href="/static/assest/css/crafty.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 --> <!-- endinject -->
<!-- Plugin css for this page --> <!-- Plugin css for this page -->
<!-- End Plugin css for this page --> <!-- End Plugin css for this page -->

View File

@ -100,7 +100,7 @@
</li> </li>
<li class="nav-item"> <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; <i class="fas fa-book"></i> &nbsp;
<span class="menu-title">{{ translate('sidebar', 'documentation', data['lang']) }}</span> <span class="menu-title">{{ translate('sidebar', 'documentation', data['lang']) }}</span>
</a> </a>
@ -109,7 +109,7 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/panel/wiki"> <a class="nav-link" href="/panel/wiki">
<i class="fa fa-info-circle"></i> &nbsp; <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> </a>
</li> </li>

View File

@ -1,27 +1,32 @@
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<li class="nav-item dropdown"> <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 <i class="fas fa-broadcast-tower
{% if data.get('update_available') %} {% if data.get('update_available') %}
text-danger text-danger
{% end %} {% end %}
"></i> "></i><span id="notif-count" class="badge badge-notify"></span> </a>
<!-- <span class="count bg-success">3</span>--> <div class="dropdown-menu dropdown-menu-right navbar-dropdown notif-div" style="width: 40vw; max-height: 80vh;" aria-labelledby="notifDropdown">
</a> <ul style="padding-top: 10px;" id="announcements">
</ul>
</div>
</li> </li>
<li class="nav-item dropdown"> <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> <i class="fas fa-cogs"></i>
</a> </a>
</li> </li>
<li class="nav-item dropdown user-dropdown"> <li class="nav-item dropdown user-dropdown">
<a class="nav-link dropdown-toggle" id="UserDropdown" href="#" data-toggle="dropdown" aria-expanded="false"> <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-menu dropdown-menu-right navbar-dropdown" aria-labelledby="UserDropdown">
<div class="dropdown-header text-center"> <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="mb-1 mt-3 font-weight-semibold">{{ data['user_data']['username'] }}</p>
<p class="font-weight-light text-muted mb-0">Roles: </p> <p class="font-weight-light text-muted mb-0">Roles: </p>
{% for r in data['user_role'] %} {% 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> <p class="font-weight-light text-muted mb-0">Email: {{ data['user_data']['email'] }}</p>
</div> </div>
{% if data['user_data']['preparing'] %} {% 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"> <span class="dropdown-item" id="support_progress">
<div class="support_progress" style="height: 15px; width: 100%;"> <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> </div>
</span> </span>
{% else %} {% 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 %} {% end %}
{% if data['superuser'] %} {% 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 %} {% 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> </div>
</li> </li>
</ul> </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> <script>
function pfpError(image) { function pfpError(image) {
image.onerror = ""; image.onerror = "";
image.src = "/static/assets/images/faces-clipart/pic-3.png"; image.src = "/static/assets/images/faces-clipart/pic-3.png";
return true; 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> </script>

View File

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

View File

@ -28,7 +28,12 @@
{% if data['superuser'] %} {% 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 %} {% end %}
<!-- Page Title Header Starts--> <!-- Page Title Header Starts-->
@ -51,16 +56,22 @@
<div class="col-12"> <div class="col-12">
<h4>{{ translate('customLogin', 'loginImage', data['lang']) }}</h4> <h4>{{ translate('customLogin', 'loginImage', data['lang']) }}</h4>
<hr> <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() %} {% raw xsrf_form_html() %}
<input type="hidden" value="import_zip" name="create_type"> <input type="hidden" value="import_zip" name="create_type">
<div class="col form-group"> <div class="form-group">
<span id="upload_input"><input type="file" class="form-control-file" id="file" name="file" <div id="upload_input" class="input-group">
multiple="false" required></span> <div class="custom-file">
</div> <input type="file" class="custom-file-input" id="file" name="file" multiple="false"
<div class="col form-group"> required>
<button type="button" class="btn btn-info" id="upload-button" onclick="sendFile()" <label id="fileLabel" class="custom-file-label" for="file">{{ translate('customLogin',
disabled>UPLOAD</button> '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> </div>
</form> </form>
<hr> <hr>
@ -228,6 +239,10 @@
</div> </div>
<style> <style>
.img-fluid {
margin-bottom: 1rem;
}
.popover-body { .popover-body {
color: white !important; color: white !important;
; ;
@ -272,6 +287,7 @@
console.log("File changed"); console.log("File changed");
if ($('#file').val()) { if ($('#file').val()) {
$('#upload-button').prop("disabled", false); $('#upload-button').prop("disabled", false);
document.getElementById("fileLabel").innerHTML = $('#file').val().split('\\').pop().split('/').pop();
console.log("File changed good"); console.log("File changed good");
} }
}); });
@ -293,33 +309,50 @@
}); });
}); });
$('.delete-photo').click(function () { $('.delete-photo').click(async function () {
var token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val(); let photo = $('#photo').find(":selected").val();
$.ajax({ const token = getCookie("_xsrf")
type: "POST", let res = await fetch(`/api/v2/crafty/config/customize`, {
headers: { 'X-XSRFToken': token }, method: 'DELETE',
url: '/ajax/delete_photo?photo=' + encodeURIComponent(photo), headers: {
success: function (data) { 'X-XSRFToken': token
location.reload();
}, },
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 () { $('.select-photo').click(async function () {
var token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val(); let photo = $('#photo').find(":selected").val();
let opacity = $('#modal_opacity').val(); let opacity = $('#modal_opacity').val();
let enc_photo = encodeURIComponent(photo); console.log(JSON.stringify({ "photo": photo, "opacity": opacity }))
const token = getCookie("_xsrf")
$.ajax({ let res = await fetch(`/api/v2/crafty/config/customize`, {
type: "POST", method: 'PATCH',
headers: { 'X-XSRFToken': token }, headers: {
url: '/ajax/select_photo?photo=' + enc_photo + '&opacity=' + opacity, 'X-XSRFToken': token
success: function (data) {
window.location.reload();
}, },
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 () { $(document).ready(function () {
@ -352,7 +385,7 @@
var file; var file;
function sendFile() { function sendFile() {
file = $("#file")[0].files[0] 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 xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf") let token = getCookie("_xsrf")
let fileName = file.name let fileName = file.name
@ -380,7 +413,7 @@
xmlHttpRequest.addEventListener('load', (event) => { xmlHttpRequest.addEventListener('load', (event) => {
if (event.target.responseText == 'success') { if (event.target.responseText == 'success') {
console.log('Upload for file', file.name, 'was successful!') 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 () { setTimeout(function () {
window.location.reload(); window.location.reload();
}, 2000); }, 2000);

View File

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

View File

@ -1,82 +1,103 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <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> <meta name="apple-mobile-web-app-capable" content="yes" />
<!-- Required meta tags --> <meta name="mobile-web-app-capable" content="yes" />
<meta charset="utf-8"> <meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="apple-mobile-web-app-title" content="Crafty" />
<title>Crafty Controller</title> <link rel="apple-touch-icon" href="../static/assets/images/Crafty_4-0.png" />
<!-- 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>
<body class="dark-theme"> <!-- endinject -->
<div class="container-scroller"> <!-- Plugin css for this page -->
<div class="container-fluid page-body-wrapper full-page-wrapper"> <!-- End Plugin css for this page -->
<div class="content-wrapper d-flex align-items-center auth auth-bg-1 theme-one"> <!-- Layout styles -->
<div class="row w-100"> <link rel="stylesheet" href="/static/assets/css/dark/style.css" />
<div class="col-lg-4 mx-auto"> <!-- 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"> <body class="dark-theme">
<div class="text-center"> <div class="container-scroller">
<img src="/static/assets/images/logo_long.svg"><br /><br /> <div class="container-fluid page-body-wrapper full-page-wrapper">
<div class="col-sm-12 grid-margin stretch-card"> <div class="content-wrapper d-flex align-items-center auth auth-bg-1 theme-one" >
<div class="card card-statistics social-card google-card card-colored"> <div class="row w-100">
<div class="card-body"> <div class="col-lg-4 mx-auto">
<h4 class="platform-name mb-3 mt-4 font-weight-semibold user-name">{{ translate('accessDenied', <div class="auto-form-wrapper">
'accessDenied', data['lang']) }}</h4> <div class="text-center">
<h5 class="headline font-weight-medium">{{ translate('accessDenied', 'noAccess', data['lang']) }} <img alt="Crafty Logo" src="/static/assets/images/logo_long.svg" /><br /><br />
</h5> <div class="col-sm-12 grid-margin stretch-card">
<p class="mb-2 comment font-weight-light"> <div class="card card-statistics social-card google-card card-colored" >
{{ translate('accessDenied', 'contactAdmin', data['lang']) }}<br /><br /> <div class="card-body">
<a class="d-inline font-weight-medium" href="https://discord.gg/9VJPhCE"> {{ <h4 class="platform-name mb-3 mt-4 font-weight-semibold user-name" >
translate('accessDenied', 'contact', data['lang']) }}</a> {{ translate('accessDenied', 'accessDenied', data['lang']) }}
</p> </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> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- content-wrapper ends -->
</div> </div>
<!-- content-wrapper ends --> <!-- page-body-wrapper ends -->
</div> </div>
<!-- page-body-wrapper ends --> <!-- container-scroller -->
</div> <!-- plugins:js -->
<!-- container-scroller --> <script src="/static/assets/vendors/js/vendor.bundle.base.js"></script>
<!-- plugins:js --> <!-- endinject -->
<script src="/static/assets/vendors/js/vendor.bundle.base.js"></script> <!-- inject:js -->
<!-- endinject --> <script src="/static/assets/js/shared/off-canvas.js"></script>
<!-- inject:js --> <script src="/static/assets/js/shared/hoverable-collapse.js"></script>
<script src="/static/assets/js/shared/off-canvas.js"></script> <script src="/static/assets/js/shared/misc.js"></script>
<script src="/static/assets/js/shared/hoverable-collapse.js"></script> <script src="/static/assets/js/shared/settings.js"></script>
<script src="/static/assets/js/shared/misc.js"></script> <script src="/static/assets/js/shared/todolist.js"></script>
<script src="/static/assets/js/shared/settings.js"></script> <!-- endinject -->
<script src="/static/assets/js/shared/todolist.js"></script> <script>
<!-- endinject --> $(document).ready(function () {
</body> let login_opacity_div = document.getElementById("login_opacity");
let opacity = login_opacity_div.getAttribute("data-value");
</html> 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'] %} {% 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 %} {% end %}
<!-- Page Title Header Starts--> <!-- Page Title Header Starts-->
@ -321,24 +326,30 @@
}); });
} }
$("#server-path").submit(function (e) { $("#server-path").submit(async function (e) {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
e.preventDefault(); e.preventDefault();
$("#submit-status").html('<i class="fa fa-spinner fa-spin"></i>'); $("#submit-status").html('<i class="fa fa-spinner fa-spin"></i>');
let path = $("#global_server_path").val(); let path = $("#global_server_path").val();
let encoded = encodeURIComponent(path); let res = await fetch(`/api/v2/crafty/config/servers_dir`, {
console.log(path) method: 'PATCH',
$.ajax({ headers: {
type: "POST", 'X-XSRFToken': token
headers: { 'X-XSRFToken': token },
dataType: "text",
url: '/ajax/update_server_dir',
data: {
"server_dir": encoded,
}, },
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 () { $(document).ready(function () {

View File

@ -49,10 +49,7 @@
</ul> </ul>
<div class=""> <div class="">
<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' }}"> <form id="role_form" class="forms-sample">
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['role']['role_id'] }}">
<input type="hidden" name="subpage" value="config">
<div class="card"> <div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center"> <div class="card-header header-sm d-flex justify-content-between align-items-center">
@ -61,7 +58,7 @@
<div class="card-body"> <div class="card-body">
<div class="form-group"> <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> <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> </div>
<br /> <br />
@ -188,11 +185,11 @@
<tr> <tr>
<td>{{ server['server_name'] }}</td> <td>{{ server['server_name'] }}</td>
<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" id="server_{{ server['server_id'] }}_access"
name="server_{{ server['server_id'] }}_access" name="server_{{ server['server_id'] }}_access"
{{ 'checked' if server['server_id'] in data['role']['servers'] else '' }} {{ 'checked' if server['server_id'] in data['role']['servers'] else '' }}
autocomplete="off" value="1"> autocomplete="off" value="1" form="dummy">
</td> </td>
{% for permission in data['permissions_all'] %} {% for permission in data['permissions_all'] %}
{% if server['server_id'] in data['role']['servers'] %} {% if server['server_id'] in data['role']['servers'] %}
@ -201,14 +198,14 @@
id="permission_{{ server['server_id'] }}_{{ permission.name }}" id="permission_{{ server['server_id'] }}_{{ permission.name }}"
name="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 '' }} {{ 'checked' if permission in data['permissions_dict'].get(server['server_id'], []) else '' }}
autocomplete="off" value="1"> autocomplete="off" value="1" form="dummy">
</td> </td>
{% else %} {% else %}
<td> <td>
<input type="checkbox" class="{{server['server_id']}}_perms" <input type="checkbox" class="{{server['server_id']}}_perms"
id="permission_{{ server['server_id'] }}_{{ permission.name }}" id="permission_{{ server['server_id'] }}_{{ permission.name }}"
name="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> </td>
{% end %} {% end %}
{% 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 /> <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> <small>{{ translate('rolesConfig', 'doesNotExist', data['lang']) }}</small>
{% else %} {% 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 %} {% end %}
</div> </div>
</div> </div>
@ -321,9 +318,123 @@
return r ? r[1] : undefined; 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() { $( document ).ready(function() {
console.log( "ready!" ); 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> </script>

View File

@ -58,13 +58,11 @@ data['lang']) }}{% end %}
<div class="row"> <div class="row">
<div class="col-md-6 col-sm-12"> <div class="col-md-6 col-sm-12">
{% if data['new_user'] %} {% 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 %} {% else %}
<form id="user_form" class="forms-sample" method="post" action="/panel/edit_user"> <form id="user_form" class="forms-sample">
{% end %} {% 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"> <div class="card">
@ -85,7 +83,7 @@ data['lang']) }}{% end %}
}}<small class="text-muted ml-1"> - {{ translate('userConfig', 'leaveBlank', data['lang']) }} }}<small class="text-muted ml-1"> - {{ translate('userConfig', 'leaveBlank', data['lang']) }}
</small> </label> </small> </label>
<input type="password" class="form-control" name="password0" id="password0" value="" <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" , <span class="passwords-match" ,
data-content="{{ translate('panelConfig', 'match', data['lang']) }}" , data-content="{{ translate('panelConfig', 'match', data['lang']) }}" ,
data-placement="right"></span> data-placement="right"></span>
@ -95,7 +93,7 @@ data['lang']) }}{% end %}
<small class="text-muted ml-1"> - {{ translate('userConfig', 'leaveBlank', data['lang']) <small class="text-muted ml-1"> - {{ translate('userConfig', 'leaveBlank', data['lang'])
}}</small> </label> }}</small> </label>
<input type="password" class="form-control" name="password1" id="password1" value="" <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" , <span class="passwords-match" ,
data-content="{{ translate('panelConfig', 'match', data['lang']) }}" , data-content="{{ translate('panelConfig', 'match', data['lang']) }}" ,
data-placement="right"></span> data-placement="right"></span>
@ -111,7 +109,7 @@ data['lang']) }}{% end %}
<label class="form-label" for="language">{{ translate('userConfig', 'userLang', data['lang']) <label class="form-label" for="language">{{ translate('userConfig', 'userLang', data['lang'])
}}</label> }}</label>
<select class="form-select form-control form-control-lg select-css" id="language" <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'] %} {% for lang in data['languages'] %}
{% if not 'incomplete' in lang %} {% if not 'incomplete' in lang %}
<option value="{{lang}}">{{lang}}</option> <option value="{{lang}}">{{lang}}</option>
@ -182,18 +180,18 @@ data['lang']) }}{% end %}
<td> <td>
{% if role.role_id in data['user']['roles'] %} {% if role.role_id in data['user']['roles'] %}
{% if role.manager == data['exec_user'] or data['superuser'] %} {% 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" id="role_{{ role.role_id }}_membership" name="role_{{ role.role_id }}_membership"
checked="" value="1"> checked="" value="{{role.role_id}}" form="dummy">
{% else %} {% 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" 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 %} {% end %}
{% elif data['superuser'] or role.manager == data['exec_user'] %} {% 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" id="role_{{ role.role_id }}_membership" name="role_{{ role.role_id }}_membership"
value="1"> value="{{role.role_id}}" form="dummy">
{% end %} {% end %}
</td> </td>
@ -219,7 +217,7 @@ data['lang']) }}{% end %}
<div class="card-body"> <div class="card-body">
<div class="form-group"> <div class="form-group">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover"> <table id="permissions" aria-describedby="User Crafty Permissions" class="table table-hover">
<thead> <thead>
<tr class="rounded"> <tr class="rounded">
<th>{{ translate('userConfig', 'permName', data['lang']) }}</th> <th>{{ translate('userConfig', 'permName', data['lang']) }}</th>
@ -233,16 +231,16 @@ data['lang']) }}{% end %}
<td>{{ permission.name }}</td> <td>{{ permission.name }}</td>
<td> <td>
{% if permission in data['permissions_list'] %} {% if permission in data['permissions_list'] %}
<input type="checkbox" class="form-check-input" id="permission_{{ permission.name }}" <input type="checkbox" class="form-check-input perm-name" id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" checked="" value="1"> name="permission_{{ permission.name }}" checked="" value="1" data-perm="{{permission.name}}" form="dummy">
{% else %} {% else %}
<input type="checkbox" class="form-check-input" id="permission_{{ permission.name }}" <input type="checkbox" class="form-check-input perm-name" id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" value="1"> name="permission_{{ permission.name }}" value="1" data-perm="{{permission.name}}" form="dummy">
{% end %} {% end %}
</td> </td>
<td><input type="text" class="form-control" name="quantity_{{ permission.name }}" <td><input type="text" class="form-control" name="quantity_{{ permission.name }}"
id="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> </tr>
{% end %} {% end %}
</tbody> </tbody>
@ -287,7 +285,7 @@ data['lang']) }}{% end %}
</div> </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> translate('panelConfig', 'save', data['lang']) }}</button>
<button type="reset" onclick="location.href='/panel/panel_config'" class="btn btn-light"><i <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> class="fas fa-undo-alt"></i> {{ translate('panelConfig', 'cancel', data['lang']) }}</button>
@ -363,9 +361,12 @@ data['lang']) }}{% end %}
} }
} }
function validateForm() { function validateForm() {
let password0 = document.getElementById("password0").value let password0 = document.getElementById("password0").value;
let password1 = document.getElementById("password1").value let password1 = document.getElementById("password1").value;
if (password0 != password1) { if (password0 === "" && password1 === "" && userId){
return true
}
else if (password0 != password1) {
$('.passwords-match').popover('show'); $('.passwords-match').popover('show');
$('.popover-body').click(function () { $('.popover-body').click(function () {
$('.passwords-match').popover("hide"); $('.passwords-match').popover("hide");
@ -376,11 +377,103 @@ data['lang']) }}{% end %}
$("#password1").css("outline", "1px solid red"); $("#password1").css("outline", "1px solid red");
return false; return false;
} else { } 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') 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 () { $(".delete-user").click(function () {
var file_to_del = $(this).data("file"); 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']) }}' label: '<i class="fas fa-check"></i> {{ translate("serverBackups", "confirm", data['lang']) }}'
} }
}, },
callback: function (result) { callback: async function (result) {
console.log(result); console.log(result);
if (result === true) { 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 }} apikey.server_permissions }}
{{ translate('apiKeys', 'crafty', data['lang']) }} {{ {{ translate('apiKeys', 'crafty', data['lang']) }} {{
apikey.crafty_permissions }}</td> apikey.crafty_permissions }}</td>
<td> <td><button class="btn btn-danger delete-api-key"
<button class="btn btn-danger delete-api-key"
data-key-id="{{ apikey.token_id }}" data-key-id="{{ apikey.token_id }}"
data-key-name="{{ apikey.name }}">{{ data-key-name="{{ apikey.name }}">{{translate('panelConfig',
translate('panelConfig', 'delete', data['lang']) 'delete', data['lang'])}}</button>
}}</button>
<button class="btn btn-outline-primary get-a-token" <button class="btn btn-outline-primary get-a-token"
data-key-id="{{ apikey.token_id }}" data-key-id="{{ apikey.token_id }}"
data-key-name="{{ apikey.name }}">{{ data-key-name="{{ apikey.name }}">{{translate('apiKeys',
translate('apiKeys', 'getToken', data['lang']) }} 'getToken', data['lang'])}}</button>
</button>
</td> </td>
</tr> </tr>
{% end %} {% end %}
@ -115,10 +112,7 @@
'createNew', data['lang']) }}</h4> 'createNew', data['lang']) }}</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<form id="user_form" class="forms-sample" method="post" <form id="user_api_form" class="forms-sample">
action="/panel/edit_user_apikeys">
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['user']['user_id'] }}">
<div class="form-group"> <div class="form-group">
<label class="form-label" for="username">{{ translate('apiKeys', 'name', <label class="form-label" for="username">{{ translate('apiKeys', 'name',
@ -142,7 +136,7 @@
}}</label> }}</label>
</td> </td>
<td> <td>
<input type="checkbox" class="" <input type="checkbox" class="server_perm"
id="permission_{{ permission.name }}" id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" value="1"> name="permission_{{ permission.name }}" value="1">
</td> </td>
@ -154,7 +148,7 @@
}}</label> }}</label>
</td> </td>
<td> <td>
<input type="checkbox" class="" <input type="checkbox" class="crafty_perm"
id="permission_{{ permission.name }}" id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" value="1"> name="permission_{{ permission.name }}" value="1">
</td> </td>
@ -201,56 +195,122 @@
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined; 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 () { $(document).ready(function () {
console.log("ready!"); console.log("ready!");
$('.delete-api-key').click(function () { $('.delete-api-key').click(async function () {
var keyId = $(this).data("key-id"); let keyId = $(this).data("key-id");
var keyName = $(this).data("key-name"); let token = getCookie("_xsrf");
bootbox.confirm({ let res = await fetch(`/api/v2/users/${userId}/key/${keyId}`, {
title: `Remove API key ${keyName}?`, method: 'DELETE',
message: "Do you want to delete this API key? This cannot be undone.", headers: {
buttons: { 'X-XSRFToken': token
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>`
});
}, },
}); });
}) 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> </script>

View File

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

View File

@ -228,6 +228,24 @@
} }
initParser('input_motd', 'input_motd'); 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="col-sm-12 mt-4 mb-4">
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-expanded="false"> <button class="btn btn-outline dropdown-toggle custom-picker" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-expanded="false">
Server Controls <i class="fas fa-bars"></i> Server Controls
</button> </button>
<div class="dropdown-menu col-md-12" aria-labelledby="dropdownMenuButton"> <div class="dropdown-menu col-md-12" aria-labelledby="dropdownMenuButton">
{% if data['permissions']['Terminal'] in data['user_permissions'] %} {% 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> <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 %} {% end %}
{% if data['permissions']['Schedule'] in data['user_permissions'] %} {% 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 %} {% end %}
{% if data['permissions']['Backup'] in data['user_permissions'] %} {% if data['permissions']['Backup'] in data['user_permissions'] %}
{% if data['backup_failed'] %} {% 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="col-12">
<div class="page-header"> <div class="page-header">
<h4 class="page-title"> <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 /> <br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small> <small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
</h4> </h4>
@ -39,63 +40,11 @@
</span> </span>
<div class="row"> <div class="row">
<div class="col-md-6 col-sm-12"> {% include "parts/server_players.html %}
<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>
</div> </div>
<hr />
</div> </div>
</div> </div>
</div> </div>
@ -128,7 +77,7 @@
$(document).ready(function () { $(document).ready(function () {
console.log("ready!"); console.log("ready!");
var bannedPlayers = `{{ data['banned_players'] }}`; var bannedPlayers = `{{ data['banned_players_html'] }}`;
var bannedPlayersDecoded = htmlDecode(bannedPlayers); var bannedPlayersDecoded = htmlDecode(bannedPlayers);
@ -136,21 +85,22 @@
}); });
function send_command_to_server(command) { async function send_command_to_server(command) {
console.log(command) console.log(command)
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
console.log('sending command: ' + command) console.log('sending command: ' + command)
$.ajax({ let res = await fetch(`/api/v2/servers/${serverId}/stdin`, {
type: "POST", method: 'POST',
headers: { 'X-XSRFToken': token }, headers: {
url: '/ajax/send_command?id=' + serverId, 'X-XSRFToken': token
data: { command },
success: function (data) {
console.log("got response:");
console.log(data);
}, },
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"> <div class="col-md-6 col-sm-12">
<br> <br>
<br> <br>
<form class="forms-sample" method="post" action="/panel/server_backup"> <form id="backup-form" class="forms-sample">
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['server_stats']['server_id']['server_id'] }}">
<input type="hidden" name="subpage" value="backup">
{% if data['backing_up'] %} {% if data['backing_up'] %}
<div class="progress" style="height: 15px;"> <div class="progress" style="height: 15px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar" <div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar"
@ -149,8 +144,6 @@
data-server_path="{{ data['server_stats']['server_id']['path']}}" type="button">{{ data-server_path="{{ data['server_stats']['server_id']['path']}}" type="button">{{
translate('serverBackups', 'clickExclude', data['lang']) }}</button> translate('serverBackups', 'clickExclude', data['lang']) }}</button>
</div> </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" <div class="modal fade" id="dir_select" tabindex="-1" role="dialog" aria-labelledby="dir_select"
aria-hidden="true"> aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
@ -175,10 +168,8 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" id="modal-cancel" class="btn btn-secondary" data-dismiss="modal">{{ <button type="button" id="modal-cancel" class="btn btn-secondary" data-dismiss="modal"><i class="fa-solid fa-xmark"></i></button>
translate('serverBackups', 'cancel', data['lang']) }}</button> <button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary"><i class="fa-solid fa-thumbs-up"></i></button>
<button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary">{{
translate('serverWizard', 'save', data['lang']) }}</button>
</div> </div>
</div> </div>
</div> </div>
@ -316,66 +307,73 @@
return r ? r[1] : undefined; return r ? r[1] : undefined;
} }
function backup_started() { async function backup_started() {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
document.getElementById('backup_button').style.visibility = 'hidden'; document.getElementById('backup_button').style.visibility = 'hidden';
var dialog = bootbox.dialog({ var dialog = bootbox.dialog({
message: "{{ translate('serverBackups', 'backupTask', data['lang']) }}", message: "{{ translate('serverBackups', 'backupTask', data['lang']) }}",
closeButton: false closeButton: false
}); });
$.ajax({ let res = await fetch(`/api/v2/servers/${server_id}/action/backup_server`, {
type: "POST", method: 'POST',
headers: { 'X-XSRFToken': token }, headers: {
url: '/ajax/backup_now?id=' + server_id, 'X-XSRFToken': token
success: function (data) { }
return; });
}, 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; return;
} }
async function del_backup(filename, id) {
function del_backup(filename, id) { const token = getCookie("_xsrf")
var token = getCookie("_xsrf") let contents = JSON.stringify({"filename": filename})
let res = await fetch(`/api/v2/servers/${id}/backups/backup/`, {
data_to_send = { file_name: filename } method: 'DELETE',
headers: {
console.log('Sending Command to delete backup: ' + filename) 'token': token,
$.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();
}, },
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) { async function restore_backup(filename, id) {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
let contents = JSON.stringify({"filename": filename})
var dialog = bootbox.dialog({ var dialog = bootbox.dialog({
message: "<i class='fa fa-spin fa-spinner'></i> {{ translate('serverBackups', 'restoring', data['lang']) }}", message: "<i class='fa fa-spin fa-spinner'></i> {{ translate('serverBackups', 'restoring', data['lang']) }}",
closeButton: false closeButton: false
}); });
let res = await fetch(`/api/v2/servers/${id}/backups/backup/`, {
console.log('Sending Command to restore backup: ' + filename) method: 'POST',
$.ajax({ headers: {
type: "POST", 'token': token,
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);
}, },
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 () { $("#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 () { $(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 { try {
if ($('#backup_path').val() == '') { if ($('#backup_path').val() == '') {
console.log('true') console.log('true')
@ -457,7 +514,7 @@
console.log(result); console.log(result);
if (result == true) { if (result == true) {
var full_path = backup_path + '/' + file_to_del; 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; return;
} else { } else {
document.getElementById('root_files_button').classList.add('clicked'); document.getElementById('root_files_button').classList.add('clicked');
document.getElementById("changed").value = 1;
} }
path = $("#root_files_button").data('server_path') path = $("#root_files_button").data('server_path')
console.log($("#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({ 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>', 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 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 () { setTimeout(function () {
var x = document.querySelector('.bootbox'); var x = document.querySelector('.bootbox');
if (x) { if (x) {
@ -535,13 +580,15 @@
if (x) { if (x) {
x.remove() x.remove()
} }
document.getElementById('main-tree-input').setAttribute('value', data.path) document.getElementById('main-tree-input').setAttribute('value', path)
getTreeView(data.path); getTreeView(path);
show_file_tree(); show_file_tree();
}, 5000); }, 5000);
}); } else {
} bootbox.alert("You must input a path before selecting this button");
}
});
if (webSocket) { if (webSocket) {
webSocket.on('backup_status', function (backup) { webSocket.on('backup_status', function (backup) {
if (backup.percent >= 100) { if (backup.percent >= 100) {
@ -558,68 +605,82 @@
}); });
} }
function getTreeView(path) { function getDirView(event){
path = path let path = event.target.parentElement.getAttribute("data-path");
if (document.getElementById(path).classList.contains('clicked')) {
return;
}else{
getTreeView(path);
}
$.ajax({ }
type: "GET", async function getTreeView(path){
url: '/ajax/get_backup_tree?id=' + server_id + '&path=' + path, console.log(path)
dataType: 'text', const token = getCookie("_xsrf");
success: function (data) { let res = await fetch(`/api/v2/servers/${server_id}/files`, {
console.log("got response:"); method: 'POST',
console.log(data); 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'); } else {
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
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-div').innerHTML += text;
document.getElementById('main-tree').parentElement.classList.add("clicked"); document.getElementById('main-tree').parentElement.classList.add("clicked");
} catch { } catch {
document.getElementById('files-tree').innerHTML = text; document.getElementById('files-tree').innerHTML = text;
} }
}else{
try {
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 {
document.getElementById(path + "span").classList.add('tree-caret-down'); document.getElementById(path + "span").classList.add('tree-caret-down');
document.getElementById(path).innerHTML += text; document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked"); document.getElementById(path).classList.add("clicked");
@ -627,7 +688,7 @@
console.log("Bad") console.log("Bad")
} }
var toggler = document.getElementById(path); var toggler = document.getElementById(path + "span");
if (toggler.classList.contains('files-tree-title')) { if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "span").addEventListener("click", function caretListener() { document.getElementById(path + "span").addEventListener("click", function caretListener() {
@ -635,10 +696,15 @@
document.getElementById(path + "span").classList.toggle("tree-caret-down"); 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() { function show_file_tree() {
$("#dir_select").modal(); $("#dir_select").modal();
} }

View File

@ -43,49 +43,36 @@
<div class="row"> <div class="row">
<div class="col-md-6 col-sm-12"> <div class="col-md-6 col-sm-12">
<form class="forms-sample" method="post" id="config_form" action="/panel/server_detail"> <form class="forms-sample" method="post" id="config_form">
{% 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"> <div class="form-group">
<label for="server_name">{{ translate('serverConfig', 'serverName', data['lang']) }} <small <label for="server_name">{{ translate('serverConfig', 'serverName', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverNameDesc', data['lang']) }}</small>
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverNameDesc', data['lang']) }}</small>
</label> </label>
<input type="text" class="form-control" name="server_name" id="server_name" <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>
value="{{ data['server_stats']['server_id']['server_name'] }}"
placeholder="{{ translate('serverConfig', 'serverName', data['lang']) }}" required>
</div> </div>
{% if data['super_user'] %} {% if data['super_user'] %}
<div class="form-group"> <div class="form-group">
<label for="server_path">{{ translate('serverConfig', 'serverPath', data['lang']) }} <small <label for="server_path">{{ translate('serverConfig', 'serverPath', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPathDesc', data['lang']) }}</small>
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPathDesc', data['lang']) }}</small>
</label> </label>
<div class="card-header header-sm d-flex justify-content-between align-items-center"> <div class="card-header header-sm d-flex justify-content-between align-items-center">
<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>
</div> </div>
{% if data['server_stats']['server_type'] != "minecraft-bedrock" %} {% if data['server_stats']['server_type'] != "minecraft-bedrock" %}
<div class="form-group"> <div class="form-group">
<label for="log_path">{{ translate('serverConfig', 'serverLogLocation', data['lang']) }} <small <label for="log_path">{{ translate('serverConfig', 'serverLogLocation', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverLogLocationDesc', data['lang'])
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverLogLocationDesc', data['lang'])
}}</small> </label> }}</small> </label>
<input type="text" class="form-control" name="log_path" id="log_path" <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>
value="{{ data['server_stats']['server_id']['log_path'] }}"
placeholder="{{ translate('serverConfig', 'serverLogLocation', data['lang']) }}" required>
</div> </div>
{% end %} {% end %}
<div class="form-group"> <div class="form-group">
<label for="executable">{{ translate('serverConfig', 'serverExecutable', data['lang']) }} <small <label for="executable">{{ translate('serverConfig', 'serverExecutable', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverExecutableDesc', data['lang'])
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverExecutableDesc', data['lang'])
}}</small> </label> }}</small> </label>
<input type="text" class="form-control" name="executable" id="executable" <input type="text" class="form-control" name="executable" id="executable" value="{{ data['server_stats']['server_id']['executable'] }}" placeholder="{{ translate('serverConfig', 'serverExecutable', data['lang']) }}" required>
value="{{ data['server_stats']['server_id']['executable'] }}"
placeholder="{{ translate('serverConfig', 'serverExecutable', data['lang']) }}" required>
</div> </div>
{% end %} {% end %}
{% if data['server_stats']['server_type'] == "minecraft-java" %} {% if data['server_stats']['server_type'] == "minecraft-java" %}
@ -94,10 +81,8 @@
<small class="text-muted ml-1">{{ translate('serverConfig', 'javaVersionDesc', data['lang']) <small class="text-muted ml-1">{{ translate('serverConfig', 'javaVersionDesc', data['lang'])
}}</small> }}</small>
</label> </label>
<select class="form-select form-control form-control-lg select-css" id="java_selection" <select class="form-select form-control form-control-lg select-css" id="java_selection" name="java_selection" form="config_form">
name="java_selection" form="config_form"> <option value="none">{{ translate('serverConfig', 'javaNoChange', data['lang'])}}</option>
<option value="">{{ translate('serverConfig',
'javaNoChange', data['lang'])}}</option>
{% for path in data['java_versions'] %} {% for path in data['java_versions'] %}
<option value="{{path}}">{{path}}</option> <option value="{{path}}">{{path}}</option>
{% end %} {% end %}
@ -110,33 +95,26 @@
<label for="execution_command">{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }} <label for="execution_command">{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }}
<small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverExecutionCommandDesc', <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverExecutionCommandDesc',
data['lang']) }}</small> </label> data['lang']) }}</small> </label>
<input type="text" class="form-control" name="execution_command" id="execution_command" <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>
value="{{ data['server_stats']['server_id']['execution_command'] }}"
placeholder="{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }}" required>
</div> </div>
{% else %} {% else %}
<label for="execution_command">{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }} <label for="execution_command">{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }}
<div class="card-header header-sm d-flex justify-content-between align-items-center"> <div class="card-header header-sm d-flex justify-content-between align-items-center">
<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> </div>
<br> <br>
{% end %} {% end %}
<div class="form-group"> <div class="form-group">
<label for="stop_command">{{ translate('serverConfig', 'serverStopCommand', data['lang']) }} <small <label for="stop_command">{{ translate('serverConfig', 'serverStopCommand', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverStopCommandDesc', data['lang'])
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverStopCommandDesc', data['lang'])
}}</small> </label> }}</small> </label>
<input type="text" class="form-control" name="stop_command" id="stop_command" <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']) }}">
value="{{ data['server_stats']['server_id']['stop_command'] }}"
placeholder="{{ translate('serverConfig', 'serverStopCommand', data['lang']) }}" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="auto_start_delay">{{ translate('serverConfig', 'serverAutostartDelay', data['lang']) }} <label for="auto_start_delay">{{ translate('serverConfig', 'serverAutostartDelay', data['lang']) }}
<small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverAutostartDelayDesc', <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverAutostartDelayDesc',
data['lang']) }}</small> </label> data['lang']) }}</small> </label>
<input type="number" class="form-control" name="auto_start_delay" id="auto_start_delay" <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>
value="{{ data['server_stats']['server_id']['auto_start_delay'] }}" step="1" max="999" min="10"
required>
</div> </div>
{% if data['super_user'] %} {% if data['super_user'] %}
@ -145,31 +123,21 @@
<label for="executable_update_url">{{ translate('serverConfig', 'exeUpdateURL', data['lang']) }} <label for="executable_update_url">{{ translate('serverConfig', 'exeUpdateURL', data['lang']) }}
<small class="text-muted ml-1"> - {{ translate('serverConfig', 'exeUpdateURLDesc', data['lang']) <small class="text-muted ml-1"> - {{ translate('serverConfig', 'exeUpdateURLDesc', data['lang'])
}}</small> </label> }}</small> </label>
<input type="text" class="form-control" name="executable_update_url" id="executable_update_url" <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']) }}">
value="{{ data['server_stats']['server_id']['executable_update_url'] }}"
placeholder="{{ translate('serverConfig', 'exeUpdateURL', data['lang']) }}">
</div> </div>
{% end %} {% end %}
<div class="form-group"> <div class="form-group">
<label for="server_ip">{{ translate('serverConfig', 'serverIP', data['lang']) }} <small <label for="server_ip">{{ translate('serverConfig', 'serverIP', data['lang']) }} <small class="text-muted ml-1">- {{ translate('serverConfig', 'serverIPDesc', data['lang']) }}</small>
class="text-muted ml-1">- {{ translate('serverConfig', 'serverIPDesc', data['lang']) }}</small>
</label> </label>
<input type="text" class="form-control" name="server_ip" id="server_ip" <input type="text" class="form-control" name="server_ip" id="server_ip" value="{{ data['server_stats']['server_id']['server_ip'] }}" required>
value="{{ data['server_stats']['server_id']['server_ip'] }}" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="server_port">{{ translate('serverConfig', 'serverPort', data['lang']) }} <small <label for="server_port">{{ translate('serverConfig', 'serverPort', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPortDesc', data['lang']) }}
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPortDesc', data['lang']) }}
</small> </label> </small> </label>
<input type="number" class="form-control" name="server_port" id="server_port" <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>
value="{{ data['server_stats']['server_id']['server_port'] }}" step="1" max="65566" min="1" <span data-html="true" class="port-hint text-center" title="<i class='fal fa-exclamation-triangle'></i> " , data-content="{{
required> translate('serverConfig', 'statsHint1' , data['lang'])}} <br> <br> <strong>{{ translate('serverConfig', 'statsHint2', data['lang'])}}</strong>" , data-placement="right"></span>
<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> </div>
{% end %} {% end %}
@ -180,9 +148,7 @@
{{ data['server_stats']['server_id']['stop_command'] }}&nbsp;{{ translate('serverConfig', {{ data['server_stats']['server_id']['stop_command'] }}&nbsp;{{ translate('serverConfig',
'timeoutExplain2', data['lang']) }} 'timeoutExplain2', data['lang']) }}
</small> </label> </small> </label>
<input type="number" class="form-control" name="shutdown_timeout" id="shutdown_timeout" <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>
value="{{ data['server_stats']['server_id']['shutdown_timeout'] }}" step="2" max="300" min="60"
required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="ignored_exits">{{ translate('serverConfig', 'ignoredExits', data['lang']) }} <label for="ignored_exits">{{ translate('serverConfig', 'ignoredExits', data['lang']) }}
@ -190,57 +156,50 @@
data['lang']) data['lang'])
}} }}
</small> </label> </small> </label>
<input type="text" class="form-control" name="ignored_exits" id="ignored_exits" <input type="text" class="form-control" name="ignored_exits" id="ignored_exits" value="{{ data['server_stats']['server_id']['ignored_exits'] }}">
value="{{ data['server_stats']['server_id']['ignored_exits'] }}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="logs_delete_after">{{ translate('serverConfig', 'removeOldLogsAfter', data['lang']) }} <label for="logs_delete_after">{{ translate('serverConfig', 'removeOldLogsAfter', data['lang']) }}
<small class="text-muted ml-1"> - {{ translate('serverConfig', 'removeOldLogsAfterDesc', <small class="text-muted ml-1"> - {{ translate('serverConfig', 'removeOldLogsAfterDesc',
data['lang']) }}</small> </label> data['lang']) }}</small> </label>
<input type="number" class="form-control" name="logs_delete_after" id="logs_delete_after" <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>
value="{{ data['server_stats']['server_id']['logs_delete_after'] }}" step="1" max="365" min="0"
required>
</div> </div>
<div class="form-check-flat"> <div class="form-group">
<label for="auto_start" class="form-check-label ml-4 mb-4"> <div class="custom-control custom-switch">
{% if data['server_stats']['server_id']['auto_start'] %} {% if data['server_stats']['server_id']['auto_start'] %}
<input type="checkbox" class="form-check-input" id="auto_start" name="auto_start" checked="" <input type="checkbox" class="custom-control-input" id="auto_start" name="auto_start" checked="" value="1">
data-toggle="toggle" value="1">&nbsp;&nbsp;{{ translate('serverConfig', 'serverAutoStart', <label class="custom-control-label" for="auto_start">&nbsp;&nbsp;{{ translate('serverConfig', 'serverAutoStart', data['lang']) }}</label>
data['lang']) }}
{% else %} {% else %}
<input type="checkbox" class="form-check-input" id="auto_start" name="auto_start" value="1" <input type="checkbox" class="custom-control-input" id="auto_start" name="auto_start" value="1">
data-toggle="toggle">&nbsp;&nbsp;{{ <label class="custom-control-label" for="auto_start">&nbsp;&nbsp;{{ translate('serverConfig', 'serverAutoStart', data['lang']) }}</label>
translate('serverConfig', 'serverAutoStart', data['lang']) }}
{% end %} {% end %}
</label> </div>
</div>
<label for="crash_detection" class="form-check-label ml-4 mb-4"> <div class="form-group">
<div class="custom-control custom-switch">
{% if data['server_stats']['server_id']['crash_detection'] %} {% if data['server_stats']['server_id']['crash_detection'] %}
<input type="checkbox" class="form-check-input" id="crash_detection" name="crash_detection" <input type="checkbox" class="custom-control-input" id="crash_detection" name="crash_detection" checked="" value="1">
data-toggle="toggle" checked="" value="1">&nbsp;&nbsp;{{ translate('serverConfig', <label class="custom-control-label" for="crash_detection">&nbsp;&nbsp;{{ translate('serverConfig', 'serverCrashDetection', data['lang']) }}</label>
'serverCrashDetection', data['lang']) }}
{% else %} {% else %}
<input type="checkbox" class="form-check-input" id="crash_detection" name="crash_detection" <input type="checkbox" class="custom-control-input" id="crash_detection" name="crash_detection" value="1">
data-toggle="toggle" value="1">&nbsp;&nbsp;{{ translate('serverConfig', 'serverCrashDetection', <label class="custom-control-label" for="crash_detection">&nbsp;&nbsp;{{ translate('serverConfig', 'serverCrashDetection', data['lang']) }}</label>
data['lang']) }}
{% end %} {% end %}
</label> </div>
</div>
{% if data['super_user'] %} <div class="form-group">
<label for="show_status" class="form-check-label ml-4 mb-4"> <div class="custom-control custom-switch">
{% if data['super_user'] %}
{% if data['server_stats']['server_id']['show_status'] %} {% if data['server_stats']['server_id']['show_status'] %}
<input type="checkbox" class="form-check-input" id="show_status" name="show_status" <input type="checkbox" class="custom-control-input" id="show_status" name="show_status" checked="" value="1">
data-toggle="toggle" checked="" value="1">&nbsp;&nbsp;{{ translate('serverConfig', 'showStatus', <label class="custom-control-label" for="show_status">&nbsp;&nbsp;{{ translate('serverConfig', 'showStatus', data['lang']) }}</label>
data['lang']) }}
{% else %} {% else %}
<input type="checkbox" class="form-check-input" id="show_status" name="show_status" <input type="checkbox" class="custom-control-input" id="show_status" name="show_status" value="1">&nbsp;&nbsp;
data-toggle="toggle" value="1">&nbsp;&nbsp;{{ translate('serverConfig', 'showStatus', <label class="custom-control-label" for="show_status">&nbsp;&nbsp;{{ translate('serverConfig', 'showStatus', data['lang']) }}</label>
data['lang']) }}
{% end %} {% end %}
</label> {% end %}
{% end %} </div>
</div> </div>
<button type="submit" class="btn btn-success mr-2"><i class="fas fa-save"></i> {{ <button type="submit" class="btn btn-success mr-2"><i class="fas fa-save"></i> {{
@ -265,14 +224,10 @@
<div class="text-center"> <div class="text-center">
{% if data['server_stats']['running'] %} {% if data['server_stats']['running'] %}
{% if data['server_stats']['updating'] %} {% if data['server_stats']['updating'] %}
<i id="update-spinner" class="fa fa-spinner fa-spin"></i>&nbsp;<button <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',
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> 'update', data['lang']) }}</button>
{% else %} {% else %}
<i style="visibility: hidden;" id="update-spinner" class="fa fa-spinner fa-spin"></i>&nbsp;<button <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',
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> 'update', data['lang']) }}</button>
{% end %} {% end %}
<a class="btn btn-sm btn-danger disabled">{{ translate('serverConfig', 'deleteServer', data['lang']) <a class="btn btn-sm btn-danger disabled">{{ translate('serverConfig', 'deleteServer', data['lang'])
@ -281,14 +236,10 @@
{% else %} {% else %}
{% if not data['failed'] %} {% if not data['failed'] %}
{% if data['server_stats']['updating'] %} {% if data['server_stats']['updating'] %}
<i id="update-spinner" class="fa fa-spinner fa-spin"></i>&nbsp;<button <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',
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> 'update', data['lang']) }}</button>
{% else %} {% else %}
<i style="visibility: hidden;" id="update-spinner" class="fa fa-spinner fa-spin"></i>&nbsp;<button <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',
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> 'update', data['lang']) }}</button>
{% end %} {% end %}
{% end %} {% end %}
@ -304,28 +255,26 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</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; color: black !important;
background-color: blueviolet !important; background-color: blueviolet !important;
border-color: var(--outline) !important;
} }
.toggle { .custom-control-label::before {
height: 0px !important; background-color: white !important;
background-color: grey !important; top: calc(-0.2rem);
}
.custom-switch .custom-control-label::after {
top: calc(-0.125rem + 1px);
} }
</style> </style>
<!-- content-wrapper ends --> <!-- content-wrapper ends -->
@ -355,11 +304,11 @@
}); });
function deleteServerE(callback) { function deleteServerE(callback) {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
$.ajax({ $.ajax({
type: "DELETE", type: "DELETE",
headers: { 'X-XSRFToken': token }, headers: { 'X-XSRFToken': token },
url: '/ajax/delete_server?id=' + serverId, url: `/api/v2/servers/${serverId}`,
data: { data: {
}, },
success: function (data) { success: function (data) {
@ -369,11 +318,11 @@
}); });
} }
function deleteServerFilesE(path, callback) { function deleteServerFilesE(path, callback) {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
$.ajax({ $.ajax({
type: "DELETE", type: "DELETE",
headers: { 'X-XSRFToken': token }, headers: { 'X-XSRFToken': token },
url: '/ajax/delete_server_files?id=' + serverId, url: `/api/v2/servers/${serverId}?files=true`,
data: { data: {
}, },
success: function (data) { success: function (data) {
@ -385,7 +334,7 @@
function send_command(serverId, command) { function send_command(serverId, command) {
//<!-- this getCookie function is in base.html--> //<!-- this getCookie function is in base.html-->
var token = getCookie("_xsrf"); const token = getCookie("_xsrf");
if (command == "update_executable") { if (command == "update_executable") {
document.getElementById("update-spinner").style.visibility = "visible"; document.getElementById("update-spinner").style.visibility = "visible";
} }
@ -393,7 +342,7 @@
$.ajax({ $.ajax({
type: "POST", type: "POST",
headers: { 'X-XSRFToken': token }, headers: { 'X-XSRFToken': token },
url: '/server/command?command=' + command + '&id=' + serverId, url: `/api/v2/servers/${serverId}/action/${command}`,
success: function (data) { success: function (data) {
console.log("got response:"); console.log("got response:");
console.log(data); console.log(data);
@ -511,7 +460,7 @@
return; return;
} }
else { else {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
setTimeout(function () { window.location = '/panel/dashboard'; }, 5000); setTimeout(function () { window.location = '/panel/dashboard'; }, 5000);
bootbox.dialog({ bootbox.dialog({
backdrop: true, backdrop: true,
@ -522,7 +471,7 @@
$.ajax({ $.ajax({
type: "DELETE", type: "DELETE",
headers: { 'X-XSRFToken': token }, headers: { 'X-XSRFToken': token },
url: '/ajax/delete_unloaded_server?id=' + serverId, url: `/api/v2/servers/${serverId}`,
data: { data: {
}, },
success: function (data) { success: function (data) {
@ -550,12 +499,93 @@
$('.port-hint').popover("hide"); $('.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 () { $(document).ready(function () {
let token = getCookie("_xsrf")
webSocket.on('remove_spinner', function () { webSocket.on('remove_spinner', function () {
document.getElementById("update-spinner").style.visibility = "hidden"; 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> </script>
{% end %} {% end %}

View File

@ -67,7 +67,7 @@
translate('serverFiles', 'download', data['lang']) }}</a> translate('serverFiles', 'download', data['lang']) }}</a>
<a onclick="deleteFileE(event)" href="javascript:void(0)" id="deleteFile" href="#" <a onclick="deleteFileE(event)" href="javascript:void(0)" id="deleteFile" href="#"
style="color: red">{{ translate('serverFiles', 'delete', data['lang']) }}</a> 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> translate('serverFiles', 'delete', data['lang']) }}</a>
<a href="javascript:void(0)" class="closebtn" style="color: var(--info);" <a href="javascript:void(0)" class="closebtn" style="color: var(--info);"
onclick="document.getElementById('files-tree-nav').style.display = 'none';">{{ onclick="document.getElementById('files-tree-nav').style.display = 'none';">{{
@ -398,32 +398,36 @@
}, },
]; ];
let filePath = '', serverFileContent = ''; let path = '', serverFileContent = '';
function clickOnFile(event) { async function clickOnFile(event) {
filePath = event.target.getAttribute('data-path'); const token = getCookie("_xsrf");
$.ajax({ path = event.target.getAttribute('data-path');
type: 'GET', let res = await fetch(`/api/v2/servers/${serverId}/files`, {
url: "/files/get_file?id=" + serverId + "&file_path=" + encodeURIComponent(filePath), method: 'POST',
dataType: 'text', headers: {
success: function (data) { 'X-XSRFToken': token
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);
}
}, },
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) { function setFileName(name) {
@ -577,124 +581,141 @@
} }
async function save() {
function save() {
let text = editor.session.getValue(); let text = editor.session.getValue();
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
$.ajax({ let res = await fetch(`/api/v2/servers/${serverId}/files`, {
type: "PUT", method: 'PATCH',
headers: { 'X-XSRFToken': token }, headers: {
url: "/files/save_file?id=" + serverId, 'X-XSRFToken': token
data: {
file_contents: text,
file_path: filePath
}, },
success: (data) => { body: JSON.stringify({ "path": path, "contents": text }),
serverFileContent = text;
setSaveStatus(true)
}
}); });
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) { async function createFile(parent, name, callback) {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
$.ajax({ let res = await fetch(`/api/v2/servers/${serverId}/files/create/`, {
type: "POST", method: 'PUT',
headers: { 'X-XSRFToken': token }, headers: {
url: "/files/create_file?id=" + serverId, 'X-XSRFToken': token
data: {
file_parent: parent,
file_name: name
},
success: function (data) {
console.log("got response:");
callback();
}, },
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") async function createDir(parent, name, callback) {
$.ajax({ const token = getCookie("_xsrf")
type: "POST", let res = await fetch(`/api/v2/servers/${serverId}/files/create/`, {
headers: { 'X-XSRFToken': token }, method: 'PUT',
url: "/files/create_dir?id=" + serverId, headers: {
data: { 'X-XSRFToken': token
dir_parent: parent,
dir_name: name
},
success: function (data) {
console.log("got response:");
callback();
}, },
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) { async function renameItem(path, name, callback) {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
$.ajax({ let res = await fetch(`/api/v2/servers/${serverId}/files/create/`, {
type: "PATCH", method: 'PATCH',
headers: { 'X-XSRFToken': token }, headers: {
url: "/files/rename_file?id=" + serverId, 'X-XSRFToken': token
data: {
item_path: path,
new_item_name: name
},
success: function (data) {
console.log("got response:");
callback();
}, },
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) { async function deleteItem(path, el, callback) {
console.log('Deleting: ' + path) const token = getCookie("_xsrf");
var token = getCookie("_xsrf") let res = await fetch(`/api/v2/servers/${serverId}/files`, {
$.ajax({ method: 'DELETE',
type: "DELETE", headers: {
headers: { 'X-XSRFToken': token }, 'X-XSRFToken': token
url: "/files/del_file?id=" + serverId,
data: {
file_path: path
},
success: function (data) {
console.log("got response:");
callback();
}, },
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) { async function unZip(path, callback) {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
$.ajax({ let res = await fetch(`/api/v2/servers/${serverId}/files/zip/`, {
type: "DELETE", method: 'POST',
headers: { 'X-XSRFToken': token }, headers: {
url: "/files/del_dir?id=" + serverId, 'X-XSRFToken': token
data: {
dir_path: path
},
success: function (data) {
console.log("got response:");
callback();
}, },
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) { bootbox.alert({
console.log('path: ', path) title: responseData.status,
var token = getCookie("_xsrf") message: responseData.error
$.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";
},
});
} }
async function sendFile(file, path, serverId, left, i, onProgress) { async function sendFile(file, path, serverId, left, i, onProgress) {
@ -882,36 +903,104 @@
}); });
} }
function getTreeView(event) { function getDirView(event) {
const path = $('#root_dir').data('path');; let path = event.target.parentElement.getAttribute("data-path");
if (document.getElementById(path).classList.contains('clicked')) {
return;
} else {
getTreeView(path);
}
$.ajax({ }
type: "GET", async function getTreeView(path) {
url: "/files/get_tree?id=" + serverId + "&path=" + path, const token = getCookie("_xsrf");
dataType: 'text', let res = await fetch(`/api/v2/servers/${serverId}/files`, {
success: function (data) { method: 'POST',
console.log("got response:"); headers: {
'X-XSRFToken': token
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);
}, },
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) { function getToggleMain(event) {
path = event.target.parentElement.getAttribute('data-path'); path = event.target.parentElement.getAttribute('data-path');
document.getElementById("files-tree").classList.toggle("d-block"); document.getElementById("files-tree").classList.toggle("d-block");
@ -919,53 +1008,6 @@
document.getElementById(path + "span").classList.toggle("tree-caret"); 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() { function setTreeViewContext() {
var treeItems = Array.from(document.getElementsByClassName('tree-ctx-item')); var treeItems = Array.from(document.getElementsByClassName('tree-ctx-item'));
@ -1134,45 +1176,12 @@
}, },
callback: function (result) { callback: function (result) {
if (!result) return; if (!result) return;
deleteFile(path, function () { deleteItem(path);
el = document.getElementById(path + "li");
$(el).remove();
document.getElementById('files-tree-nav').style.display = 'none';
});
} }
}); });
} }
function deleteDirE(event) { getTreeView($('#root_dir').data('path'));
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();
setTreeViewContext(); setTreeViewContext();
function setKeyboard(target) { function setKeyboard(target) {

View File

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

View File

@ -37,15 +37,12 @@
<div class="row"> <div class="row">
<div class="col-md-8 col-sm-8"> <div class="col-md-8 col-sm-8">
{% if data['new_schedule'] == True %} {% 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'] }}"> action="/panel/new_schedule?id={{ data['server_stats']['server_id']['server_id'] }}">
{% else %} {% 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'] }}"> action="/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{ data['schedule']['schedule_id'] }}">
{% end %} {% 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"> <div class="form-group">
<label for="name">{{ translate('serverSchedules', 'name' , data['lang']) }}</label> <label for="name">{{ translate('serverSchedules', 'name' , data['lang']) }}</label>
@ -89,7 +86,7 @@
class="text-muted ml-1"> - {{ translate('serverScheduleConfig', 'interval-explain' , class="text-muted ml-1"> - {{ translate('serverScheduleConfig', 'interval-explain' ,
data['lang']) }}</small> </label> data['lang']) }}</small> </label>
<input type="number" class="form-control" name="interval" id="interval" <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>
<br> <br>
<select id="interval_type" onchange="ifDays(this);" name="interval_type" <select id="interval_type" onchange="ifDays(this);" name="interval_type"
@ -108,7 +105,7 @@
<label for="time">{{ translate('serverScheduleConfig', 'time' , data['lang']) }} <small <label for="time">{{ translate('serverScheduleConfig', 'time' , data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverScheduleConfig', 'time-explain' , class="text-muted ml-1"> - {{ translate('serverScheduleConfig', 'time-explain' ,
data['lang']) }}</small> </label> 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> value="{{ data['schedule']['time'] }}" placeholder="Time" required>
</div> </div>
</div> </div>
@ -127,7 +124,7 @@
<label for="cron">{{ translate('serverScheduleConfig', 'cron' , data['lang']) }} <small <label for="cron">{{ translate('serverScheduleConfig', 'cron' , data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverScheduleConfig', 'cron-explain' , data['lang']) class="text-muted ml-1"> - {{ translate('serverScheduleConfig', 'cron-explain' , data['lang'])
}}</small> </label> }}</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="* * * * *"> value="{{ data['schedule']['cron_string'] }}" placeholder="* * * * *">
</div> </div>
</div> </div>
@ -234,8 +231,122 @@
return r ? r[1] : undefined; 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 () { $(document).ready(function () {
console.log("ready!"); 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("parent").required = true;
document.getElementById("interval").required = false; document.getElementById("interval").required = false;
document.getElementById("time").required = false; document.getElementById("time").required = false;
$("#cron").val("");
} }
else { else {
document.getElementById("ifAdvanced").style.display = "none"; document.getElementById("ifAdvanced").style.display = "none";
@ -274,6 +386,7 @@
document.getElementById("parent").required = false; document.getElementById("parent").required = false;
document.getElementById("interval").required = true; document.getElementById("interval").required = true;
document.getElementById("time").required = true; document.getElementById("time").required = true;
$("#cron").val("");
} }
} }
function ifDays() { 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() { function startup() {
try { try {
document.getElementById("{{ data['schedule']['interval_type'] }}").setAttribute('selected', true); document.getElementById("{{ data['schedule']['interval_type'] }}").setAttribute('selected', true);

View File

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

View File

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

View File

@ -7,44 +7,54 @@
{% block content %} {% block content %}
<div class="content-wrapper"> <!-- Desktop View -->
<div class="d-none d-sm-block content-wrapper">
<!-- Page Title Header Starts--> <!-- Page Title Header Starts-->
<div class="row page-title-header"> <div class="row page-title-header">
<div class="col-12"> <div class="col-12">
<div class="page-header"> <div class="page-header">
<h4 class="page-title">Wiki</h4> <h4 class="page-title">{{ translate('sidebar', 'documentation', data['lang']) }}</h4>
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row iframe-row">
<div class="col-md-12 grid-margin"> <div class="col-12 iframe-col">
<iframe src="https://wiki.craftycontrol.com" width=100% height=2200px title="crafty's wiki"></iframe> <div class="iframe-wrapper">
<iframe title="crafty's docs" src="https://docs.craftycontrol.com/" class="iframe-item"></iframe>
</div>
</div> </div>
</div> </div>
<!-- content-wrapper ends --> </div>
<style> <!-- Mobile View -->
.popover-body { <div class="d-sm-none content-wrapper mobile-content-wrapper">
color: white !important; <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 { .iframe-wrapper {
-ms-overflow-style: none; height: 100%;
/* for Internet Explorer, Edge */ }
scrollbar-width: none;
/* for Firefox */
overflow-y: scroll;
}
#desc_id::-webkit-scrollbar { .iframe-col {
display: none; height: 100%;
/* for Chrome, Safari, and Opera */ }
}
</style> .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/ti-icons/css/themify-icons.css">
<link rel="stylesheet" href="/static/assets/vendors/typicons/typicons.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/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 --> <!-- endinject -->
<!-- Plugin css for this page --> <!-- Plugin css for this page -->
<!-- End Plugin css for this page --> <!-- End Plugin css for this page -->
@ -24,7 +32,7 @@
<style> <style>
.auth.auth-bg-1 { .auth.auth-bg-1 {
background: url("../../static/assets/images/auth/{% raw data['background'] %}"), 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-size: cover;
} }
</style> </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/ti-icons/css/themify-icons.css">
<link rel="stylesheet" href="/static/assets/vendors/typicons/typicons.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/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 --> <!-- endinject -->
<!-- Plugin css for this page --> <!-- Plugin css for this page -->
<!-- End Plugin css for this page --> <!-- End Plugin css for this page -->
@ -24,7 +32,7 @@
<style> <style>
.auth.auth-bg-1 { .auth.auth-bg-1 {
background: url("../../static/assets/images/auth/{% raw data['background'] %}"), 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-size: cover;
} }
</style> </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/ti-icons/css/themify-icons.css">
<link rel="stylesheet" href="/static/assets/vendors/typicons/typicons.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/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 --> <!-- endinject -->
<!-- Plugin css for this page --> <!-- Plugin css for this page -->
<!-- End Plugin css for this page --> <!-- End Plugin css for this page -->
@ -24,7 +31,7 @@
<style> <style>
.auth.auth-bg-1 { .auth.auth-bg-1 {
background: url("../../static/assets/images/auth/{% raw data['background'] %}"), 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-size: cover;
background-position: center; background-position: center;
} }
@ -113,6 +120,11 @@
<span class="text-small font-weight-semibold"><a href="https://craftycontrol.com/">Crafty Control <span class="text-small font-weight-semibold"><a href="https://craftycontrol.com/">Crafty Control
{{data['version'] }}</a> </span> {{data['version'] }}</a> </span>
</div> </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> </form>
</div> </div>
@ -140,7 +152,15 @@
let login_opacity_div = document.getElementById('login_opacity'); let login_opacity_div = document.getElementById('login_opacity');
let opacity = login_opacity_div.getAttribute('data-value'); let opacity = login_opacity_div.getAttribute('data-value');
document.getElementById('login-form-background').style.background = 'rgb(34, 36, 55, ' + (opacity / 100) + ')'; 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> </script>
</body> </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> <style>
.auth.auth-bg-1 { .auth.auth-bg-1 {
background: url("../../static/assets/images/auth/{% raw data['background'] %}"), 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-size: cover;
} }
</style> </style>
@ -89,7 +89,7 @@
</div> </div>
<!-- View for Small screen --> <!-- View for Small screen -->
<div class="row justify-content-center align-items-sm-center"> <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%;'> <img src="/static/assets/images/logo_long.png" style='width: 100%;'>
<hr /> <hr />
{% if data['running'] != 0 %} {% if data['running'] != 0 %}
@ -97,6 +97,7 @@
{% end %} {% end %}
<div class="accordion" id="accordionServers"> <div class="accordion" id="accordionServers">
{% for server in data['servers'] %} {% for server in data['servers'] %}
{% if server['server_data']['show_status'] %}
<div class="card mb-0"> <div class="card mb-0">
<div class="card-header" id="heading-{{server['server_data']['server_id']}}"> <div class="card-header" id="heading-{{server['server_data']['server_id']}}">
<h2 class="mb-0 container overflow-hidden"> <h2 class="mb-0 container overflow-hidden">
@ -167,6 +168,7 @@
</div> </div>
</div> </div>
{% end %} {% end %}
{% end %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,114 +1,167 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ data['lang_page'] }}" class="default"> <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> <meta name="apple-mobile-web-app-capable" content="yes" />
<!-- Required meta tags --> <meta name="mobile-web-app-capable" content="yes" />
<meta charset="utf-8"> <meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="apple-mobile-web-app-title" content="Crafty" />
{% block meta %}{% end %} <link
<title>{% block title %}{{ _('Default') }}{% end %}</title> rel="apple-touch-icon"
<!-- plugins:css --> href="../static/assets/images/Crafty_4-0.png"
<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"> <!-- endinject -->
<link rel="stylesheet" href="/static/assets/vendors/ti-icons/css/themify-icons.css"> <!-- Plugin css for this page -->
<link rel="stylesheet" href="/static/assets/vendors/typicons/typicons.css"> <!-- End Plugin css for this page -->
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css"> <!-- Layout styles -->
<link rel="stylesheet" href="/static/assets/vendors/fontawesome6/css/all.css"> <link rel="stylesheet" href="/static/assets/css/dark/style.css" />
<!-- endinject --> <!-- End Layout styles -->
<!-- Plugin css for this page --> <link
<!-- End Plugin css for this page --> rel="shortcut icon"
<!-- Layout styles --> type="image/svg+xml"
<link rel="stylesheet" href="/static/assets/css/dark/style.css"> href="/static/assets/images/logo_small.svg"
<!-- 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" />
<link rel="alternate icon" href="/static/assets/images/favicon.png" /> </head>
</head>
<body> <body>
<div class="container-scroller"> <div class="container-scroller">
<div class="container-fluid page-body-wrapper full-page-wrapper"> <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
<div class="mx-auto"> class="content-wrapper d-flex align-items-sm-center auth auth-bg-1 theme-one"
<div class="auto-form-wrapper"> >
{% block content %} <div class="mx-auto">
{% end %} <div class="auto-form-wrapper">{% block content %} {% end %}</div>
</div> </div>
</div> </div>
<!-- content-wrapper ends -->
</div> </div>
<!-- content-wrapper ends --> <!-- page-body-wrapper ends -->
</div> </div>
<!-- page-body-wrapper ends --> <!-- container-scroller -->
</div> <!-- plugins:js -->
<!-- container-scroller --> <script src="/static/assets/vendors/js/vendor.bundle.base.js"></script>
<!-- plugins:js --> <!-- endinject -->
<script src="/static/assets/vendors/js/vendor.bundle.base.js"></script> <!-- inject:js -->
<!-- endinject --> <script src="/static/assets/js/shared/off-canvas.js"></script>
<!-- inject:js --> <script src="/static/assets/js/shared/hoverable-collapse.js"></script>
<script src="/static/assets/js/shared/off-canvas.js"></script> <script src="/static/assets/js/shared/misc.js"></script>
<script src="/static/assets/js/shared/hoverable-collapse.js"></script> <!-- endinject -->
<script src="/static/assets/js/shared/misc.js"></script> <script>
<!-- endinject --> // {% if request.protocol == 'https' %}
<script> let usingWebSockets = true;
// {% if request.protocol == 'https' %} let listenEvents = [];
let usingWebSockets = true;
let listenEvents = []; let pageQueryParams, page;
try { try {
pageQueryParams = 'page_query_params=' + encodeURIComponent(location.search) pageQueryParams =
page = 'page=' + encodeURIComponent(location.pathname) "page_query_params=" + encodeURIComponent(location.search);
var wsInternal = new WebSocket('wss://' + location.host + '/ws?' + page + '&' + pageQueryParams); page = "page=" + encodeURIComponent(location.pathname);
wsInternal.onopen = function () { var wsInternal = new WebSocket(
console.log('opened WebSocket connection:', wsInternal) "wss://" + location.host + "/ws?" + page + "&" + pageQueryParams
}; );
wsInternal.onmessage = function (rawMessage) { wsInternal.onopen = function () {
var message = JSON.parse(rawMessage.data); 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 listenEvents
.filter(listenedEvent => listenedEvent.event == message.event) .filter((listenedEvent) => listenedEvent.event == message.event)
.forEach(listenedEvent => listenedEvent.callback(message.data)) .forEach((listenedEvent) => listenedEvent.callback(message.data));
}; };
wsInternal.onerror = function (errorEvent) { wsInternal.onerror = function (errorEvent) {
console.error('WebSocket Error', errorEvent); console.error("WebSocket Error", errorEvent);
}; };
wsInternal.onclose = function (closeEvent) { wsInternal.onclose = function (closeEvent) {
console.log('Closed WebSocket', 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 = { wsInternal.send(JSON.stringify(message));
on: function (event, callback) { },
console.log('registered ' + event + ' event'); };
listenEvents.push({ event: event, callback: callback }) } catch (error) {
}, console.error("Error while making websocket helpers", error);
emit: function (event, data) { usingWebSockets = false;
var message = {
event: event,
data: data
}
wsInternal.send(JSON.stringify(message));
}
} }
} catch (error) { // {% else %}
console.error('Error while making websocket helpers', error);
usingWebSockets = false; usingWebSockets = false;
} warn(
// {% else %} "WebSockets are not supported in Crafty if not using the https protocol"
let usingWebSockets = false; );
warn('WebSockets are not supported in Crafty if not using the https protocol') var webSocket;
var webSocket; // {% end%}
// {% end%} </script>
{% block js %}
</script> <!-- Custom js for this page -->
{% block js %} <script>
<!-- Custom js for this page --> $(document).ready(function () {
<!-- End custom js for this page --> let login_opacity_div = document.getElementById("login_opacity");
{% end %} let opacity = login_opacity_div.getAttribute("data-value");
document.getElementById("login-form-background").style.background =
</body> "rgb(34, 36, 55, " + opacity / 100 + ")";
//Register Service worker for mobile app
</html> 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> <h4>{{ translate('serverWizard', 'newServer', data['lang']) }}</h4>
<br /> <br />
<p class="card-description"> <p class="card-description">
<form method="post" class="server-wizard" onSubmit="wait_msg()"> <form method="submit" id="server_creation" class="server-wizard">
{% raw xsrf_form_html() %}
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<div class="form-group"> <div class="form-group">
<label for="server_jar">{{ translate('serverWizard', 'serverType', data['lang']) <label for="server_jar">{{ translate('serverWizard', 'serverType', data['lang'])
}}</label></br> }}</label></br>
<select style="width: 80%" required class="selectpicker form-control form-control-lg select-css" <div class="input-group">
id="steam_server" data-live-search="true" name="steam_server"> <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> <option value="None">None</option>
{% for s in data['servers'] %} {% for s in data['servers'] %}
{% if data["windows"] and s["windows"] %} {% if data["windows"] and s["windows"] %}
@ -50,16 +50,19 @@
{% end %} {% end %}
{% end %} {% end %}
</select> </select>
{% if data['super_user'] %} {% if data['super_user'] %}
&nbsp;&nbsp;<i onclick="refreshCache()" style="float: left;" id="refresh-cache" <div class="input-group-append">
class="refresh-class fas fa-sync"></i> <button class="btn custom-picker" type="button" onclick="refreshCache()"><i id="refresh-cache" class="refresh-class fas fa-sync"></i></button>
</div>
{% end %} {% end %}
</div>
</div> </div>
</div> </div>
<div class="col-sm-12"> <div class="col-sm-12">
<div class="form-group"> <div class="form-group">
<label for="server_name">{{ translate('serverWizard', 'serverName', data['lang']) }}</label> <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> placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}" required>
</div> </div>
</div> </div>
@ -345,142 +348,114 @@
{% end %} {% end %}
{% block js%} {% 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 %} {% end %}
{% block js %} {% block js %}
<script> <script>
var upload = false; async function send_server(data) {
var file; let token = getCookie("_xsrf")
function sendFile() { let res = await fetch(`/api/v2/servers/`, {
file = $("#file")[0].files[0] method: 'POST',
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>' headers: {
let xmlHttpRequest = new XMLHttpRequest(); 'X-XSRFToken': token
let token = getCookie("_xsrf") },
let fileName = file.name body: data,
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();
}
}); });
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) => { async function refreshCache() {
console.error('Error while uploading file', file.name + '.', 'Event:', e) document.getElementById("refresh-cache").classList.add("fa-spin")
}, false); let token = getCookie("_xsrf")
xmlHttpRequest.send(file); 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 () { let formData = new FormData(jarForm);
if (document.forms["zip"]["server_path"].value != "") { //Create an object from the form data entries
if (document.getElementById('root_files_button').classList.contains('clicked')) { let formDataObject = Object.fromEntries(formData.entries());
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>' console.log(formDataObject);
} else { let send_data = {
document.getElementById('root_files_button').classList.add('clicked') "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 () { console.log(formDataJsonString);
if (file) { send_server(formDataJsonString);
upload = true; });
if (document.getElementById('root_upload_button').classList.contains('clicked')) { function replacer(key, value) {
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>' if (key === "roles") {
} else { return value
document.getElementById('root_upload_button').classList.add('clicked') }
} if (key != "ignored_exits") {
var token = getCookie("_xsrf"); if (typeof value == "boolean" || key === "host" || key === "version") {
var dialog = bootbox.dialog({ return value
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),
});
} else { } else {
bootbox.alert("You must input a path before selecting this button"); return (isNaN(value) ? value : +value);
} }
}); } else {
</script> return value;
}
<script> }
$(".tree-reset").on("click", function () { function calcRoles() {
location.href = "/server/step1"; let role_ids = $('.roles').map(function () {
}); if ($(this).is(':checked')) {
return $(this).val();
}
}).get();
console.log(role_ids)
return role_ids
}
function dropDown(event) { function dropDown(event) {
event.target.parentElement.children[1].classList.remove("d-none"); event.target.parentElement.children[1].classList.remove("d-none");
document.getElementById("overlay").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() { function refreshCache() {
var token = getCookie("_xsrf") var token = getCookie("_xsrf")
document.getElementById("refresh-cache").classList.add("fa-spin") document.getElementById("refresh-cache").classList.add("fa-spin")

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