Merge branch 'dev' into feature/steamcmd

This commit is contained in:
Zedifus 2022-10-02 22:25:32 +01:00
commit d8ddc1ce27
43 changed files with 45225 additions and 22986 deletions

1
.gitignore vendored
View File

@ -18,6 +18,7 @@ env.bak/
venv.bak/
.idea/
/imports/
/servers/
/backups/
/temp/

View File

@ -51,20 +51,9 @@ pylint:
- if: "$CODE_QUALITY_DISABLED"
when: never
- if: "$CI_COMMIT_TAG || $CI_COMMIT_BRANCH"
before_script:
- mkdir -p public/badges public/lint
- echo undefined > public/badges/$CI_JOB_NAME.score
script:
- pylint --exit-zero --output-format=text $(find -type f -name "*.py" ! -path "**/.venv/**" ! -path "**/app/migrations/**") | tee /tmp/pylint.txt
- sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' /tmp/pylint.txt > public/badges/$CI_JOB_NAME.score
- pylint --exit-zero --output-format=pylint_gitlab.GitlabCodeClimateReporter $(find -type f -name "*.py" ! -path "**/.venv/**" ! -path "**/app/migrations/**") > codeclimate.json
after_script:
- anybadge --overwrite --label $CI_JOB_NAME --value=$(cat public/badges/$CI_JOB_NAME.score) --file=public/badges/$CI_JOB_NAME.svg 4=red 6=orange 8=yellow 10=green
- |
echo "Your score is: $(cat public/badges/$CI_JOB_NAME.score)"
- pylint --exit-zero --load-plugins=pylint_gitlab --output-format=gitlab-codeclimate:codeclimate.json $(find -type f -name "*.py" ! -path "**/.venv/**" ! -path "**/app/migrations/**")
artifacts:
paths:
- public
reports:
codequality: codeclimate.json
when: always

View File

@ -1,5 +1,5 @@
# Changelog
## --- [4.0.13] - 2022/TBD
## --- [4.0.16] - 2022/TBD
### New features
TBD
### Bug fixes
@ -10,6 +10,44 @@ TBD
TBD
<br><br>
## --- [4.0.15] - 2022/10/02
### New features
- Base Theme Switching (Dark, Light, Default) 🤩🎨 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/471))
- Upload Zip functionality for server imports 🏗️🎉 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/472))
### Bug fixes
- Fix traceback on basic schedule with "days" interval ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/469))
- Fix bad method call with API stdin ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/470))<br>
*(Thank you ['IWant2Tryhard'](https://github.com/MyNameTsThad) for catching that 🐛)*
- Fix clients variable as static to prevent crash if client list changed while sending a websocket ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/473))
<br><br>
## --- [4.0.14] - 2022/09/23
### Bug fixes
- HOTFIX - Rollback breaking websockets change !461 (self.clients was already a set and we tried to subscript a set of a set) ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/467))
<br><br>
## --- [4.0.13] - 2022/09/20
### Bug fixes
- Fix bug where trying to reconfigure unloaded server would stack ([Commit](https://gitlab.com/crafty-controller/crafty-4/-/commit/1b2fef06fb3b02b76c9506caf7e07e932df95fab) | [Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/460))
- Fix traceback error when a user click the roles config tab while already on the roles config page; **this is for new role creation only** ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/452))
- Fix logic issue when removing items from backup exclusions ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/453))
- Cleanup various JS errors ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/455))
- Temp fix for `&amp;` issue in pathing and minecraft colour codes ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/457))
- Cache Gravatar pfp's as to not query every page load ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/459))
- Fix crash on client list changing while sending websockets ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/461))
- Set default parent option on edit of reaction schedule ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/462))
- Fix wtol Nonetype error on server start when 'which java' returns `none` ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/463))
### Tweaks
- Add button to scroll to bottom of vterm ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/454))
- Persist schedules and execution commands across backup restores ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/458))
### Release Testing- Bug fixes
- Fix bug with logical issues surrounding gravatar caching ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/465))
- Fix bug where server terminal would not scroll on startup ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/465))
- Fix issue on post with adding users when no email is included (this also affected editing users) ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/466))
- Fix issue with schedules allowing days to be more than 30 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/466))
- Fix issue with schedules when trying to edit a cron task ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/466))
<br><br>
## --- [4.0.12] - 2022/09/04
### New features
- Win Portable Updater will now be included in Windows Package ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/446))

View File

@ -2,11 +2,11 @@
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Supported Python Versions](https://shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20-blue)](https://www.python.org)
[![Version(temp-hardcoded)](https://img.shields.io/badge/release-v4.0.13--beta-orange)](https://gitlab.com/crafty-controller/crafty-4/-/releases)
[![Version(temp-hardcoded)](https://img.shields.io/badge/release-v4.0.16--beta-orange)](https://gitlab.com/crafty-controller/crafty-4/-/releases)
[![Code Quality(temp-hardcoded)](https://img.shields.io/badge/code%20quality-10-brightgreen)](https://gitlab.com/crafty-controller/crafty-4)
[![Build Status](https://gitlab.com/crafty-controller/crafty-4/badges/master/pipeline.svg)](https://gitlab.com/crafty-controller/crafty-4/-/commits/master)
# Crafty Controller 4.0.13-beta
# Crafty Controller 4.0.15-beta
> Python based Control Panel for your Minecraft Server
## What is Crafty Controller?

View File

@ -4,7 +4,6 @@ import time
import json
import pathlib
import typing as t
import datetime
from app.classes.controllers.roles_controller import RolesController
from app.classes.shared.file_helpers import FileHelpers

View File

@ -147,14 +147,28 @@ class UsersController:
return HelperServers.get_total_owned_servers(exec_user_id)
def update_user(self, user_id: str, user_data=None, user_crafty_data=None):
# check if user crafty perms were updated
if user_crafty_data is None:
user_crafty_data = {}
# check if general user data was updated
if user_data is None:
user_data = {}
# get current user data
base_data = HelperUsers.get_user(user_id)
up_data = {}
# check if we updated user email. If so we update gravatar
try:
if user_data["email"] != base_data["email"]:
pfp = self.helper.get_gravatar_image(user_data["email"])
up_data["pfp"] = pfp
except KeyError:
logger.debug("Email not updated")
# email not updated
# create sets to store role data
added_roles = set()
removed_roles = set()
# search for changes in user data
for key in user_data:
if key == "user_id":
continue
@ -174,8 +188,10 @@ class UsersController:
up_data["hints"] = user_data["hints"]
elif base_data[key] != user_data[key]:
up_data[key] = user_data[key]
# change last update for user
up_data["last_update"] = self.helper.get_time_as_string()
logger.debug(f"user: {user_data} +role:{added_roles} -role:{removed_roles}")
for role in added_roles:
HelperUsers.get_or_create(user_id=user_id, role_id=role)
permissions_mask = user_crafty_data.get("permissions_mask", "000")
@ -225,6 +241,7 @@ class UsersController:
email="default@example.com",
enabled: bool = True,
superuser: bool = False,
theme="default",
):
return self.users_helper.add_user(
username,
@ -233,6 +250,7 @@ class UsersController:
email=email,
enabled=enabled,
superuser=superuser,
theme=theme,
)
@staticmethod

View File

@ -42,6 +42,8 @@ class Users(BaseModel):
preparing = BooleanField(default=False)
hints = BooleanField(default=True)
manager = IntegerField(default=None, null=True)
pfp = CharField(default="/static/assets/images/faces-clipart/pic-3.png")
theme = CharField(default="default")
class Meta:
table_name = "users"
@ -209,6 +211,7 @@ class HelperUsers:
email: t.Optional[str] = None,
enabled: bool = True,
superuser: bool = False,
theme: str = "default",
) -> str:
if password is not None:
pw_enc = self.helper.encode_pass(password)
@ -220,9 +223,11 @@ class HelperUsers:
Users.password: pw_enc,
Users.email: email,
Users.enabled: enabled,
Users.pfp: self.helper.get_gravatar_image(email),
Users.superuser: superuser,
Users.created: Helpers.get_time_as_string(),
Users.manager: manager,
Users.theme: theme,
}
).execute()
return user_id

View File

@ -226,18 +226,24 @@ class FileHelpers:
comment, "utf-8"
) # comments over 65535 bytes will be truncated
for root, dirs, files in os.walk(path_to_zip, topdown=True):
for l_dir in dirs:
for l_dir in dirs[:]:
# make all paths in exclusions a unix style slash
# to match directories.
if str(os.path.join(root, l_dir)).replace("\\", "/") in ex_replace:
dirs.remove(l_dir)
ziproot = path_to_zip
# iterate through list of files
for file in files:
# check if file/dir is in exclusions list.
# Only proceed if not exluded.
if (
str(os.path.join(root, file)).replace("\\", "/")
not in ex_replace
and file != "crafty.sqlite"
):
try:
logger.info(f"backing up: {os.path.join(root, file)}")
logger.debug(f"backing up: {os.path.join(root, file)}")
# add trailing slash to zip root dir if not windows.
if os.name == "nt":
zip_file.write(
os.path.join(root, file),
@ -254,12 +260,20 @@ class FileHelpers:
f"Error backing up: {os.path.join(root, file)}!"
f" - Error was: {e}"
)
# debug logging for exlusions list
else:
logger.debug(f"Found {file} in exclusion list. Skipping...")
# add current file bytes to total bytes.
total_bytes += os.path.getsize(os.path.join(root, file))
# calcualte percentage based off total size and current archive size
percent = round((total_bytes / dir_bytes) * 100, 2)
# package results
results = {
"percent": percent,
"total_files": self.helper.human_readable_file_size(dir_bytes),
}
# send status results to page.
self.helper.websocket_helper.broadcast_page_params(
"/panel/server_detail",
{"id": str(server_id)},

View File

@ -20,6 +20,7 @@ import itertools
from datetime import datetime
from socket import gethostname
from contextlib import redirect_stderr, suppress
import libgravatar
from packaging import version as pkg_version
from app.classes.shared.null_writer import NullWriter
@ -399,6 +400,10 @@ class Helpers:
)
return False
@staticmethod
def get_themes():
return ["default", "dark", "light", "ronald"]
@staticmethod
def get_local_ip():
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@ -659,6 +664,33 @@ class Helpers:
return True
return False
def get_gravatar_image(self, email):
profile_url = "/static/assets/images/faces-clipart/pic-3.png"
# http://en.gravatar.com/site/implement/images/#rating
if self.get_setting("allow_nsfw_profile_pictures"):
rating = "x"
else:
rating = "g"
# Get grvatar hash for profile pictures
if self.check_internet() and email != "default@example.com" and email:
gravatar = libgravatar.Gravatar(libgravatar.sanitize_email(email))
url = gravatar.get_image(
size=80,
default="404",
force_default=False,
rating=rating,
filetype_extension=False,
use_ssl=True,
) # + "?d=404"
try:
if requests.head(url).status_code != 404:
profile_url = url
except Exception as e:
logger.debug(f"Could not pull resource from Gravatar with error {e}")
return profile_url
@staticmethod
def get_file_contents(path: str, lines=100):

View File

@ -242,32 +242,36 @@ class ServerInstance:
"Detected nebulous java in start command. "
"Replacing with full java path."
)
# Checks for Oracle Java. Only Oracle Java's helper will cause a re-exec.
if "/Oracle/Java/" in str(self.helper.wtol_path(shutil.which("java"))):
logger.info(
"Oracle Java detected. Changing start command to avoid re-exec."
)
which_java_raw = self.helper.which_java()
try:
java_path = which_java_raw + "\\bin\\java"
except TypeError:
logger.warning(
"Could not find java in the registry even though"
" Oracle java is installed. Re-exec expected, but we have no"
" other options. CPU stats will not work for process."
oracle_path = shutil.which("java")
if oracle_path:
# Checks for Oracle Java. Only Oracle Java's helper will cause a re-exec
if "/Oracle/Java/" in str(self.helper.wtol_path(oracle_path)):
logger.info(
"Oracle Java detected. Changing"
" start command to avoid re-exec."
)
java_path = ""
if str(which_java_raw) != str(self.helper.get_servers_root_dir) or str(
self.helper.get_servers_root_dir
) in str(which_java_raw):
if java_path != "":
self.server_command[0] = java_path
else:
logger.critcal(
"Possible attack detected. User attempted to exec "
"java binary from server directory."
)
return
which_java_raw = self.helper.which_java()
try:
java_path = which_java_raw + "\\bin\\java"
except TypeError:
logger.warning(
"Could not find java in the registry even though"
" Oracle java is installed."
" Re-exec expected, but we have no"
" other options. CPU stats will not work for process."
)
java_path = ""
if str(which_java_raw) != str(
self.helper.get_servers_root_dir
) or str(self.helper.get_servers_root_dir) in str(which_java_raw):
if java_path != "":
self.server_command[0] = java_path
else:
logger.critcal(
"Possible attack detected. User attempted to exec "
"java binary from server directory."
)
return
self.server_path = Helpers.get_os_understandable_path(self.settings["path"])
# let's do some quick checking to make sure things actually exists

View File

@ -678,6 +678,24 @@ class TasksManager:
logger.info(
"No updates found! You are on the most up to date Crafty version."
)
logger.info("Refreshing Gravatar PFPs...")
for user in HelperUsers.get_all_users():
if user.email:
HelperUsers.update_user(
user.id, {"pfp": self.helper.get_gravatar_image(user.email)}
)
# Search for old files in imports
self.helper.ensure_dir_exists(
os.path.join(self.controller.project_root, "imports")
)
for file in os.listdir(os.path.join(self.controller.project_root, "imports")):
if self.helper.is_file_older_than_x_days(
os.path.join(self.controller.project_root, "imports", file)
):
try:
os.remove(os.path.join(file))
except:
logger.debug("Could not clear out file from import directory")
def log_watcher(self):
self.controller.servers.check_for_old_logs()

View File

@ -383,6 +383,8 @@ class AjaxHandler(BaseHandler):
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):
@ -401,6 +403,27 @@ class AjaxHandler(BaseHandler):
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.controller.management.create_scheduled_task(
new_server_id,
schedule.action,
schedule.interval,
schedule.interval_type,
schedule.start_time,
schedule.command,
schedule.name,
schedule.enabled,
)
# 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)
# remove old server's tasks
try:
self.tasks_manager.remove_all_server_tasks(server_id)
except:
@ -424,6 +447,26 @@ class AjaxHandler(BaseHandler):
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.controller.management.create_scheduled_task(
new_server_id,
schedule.action,
schedule.interval,
schedule.interval_type,
schedule.start_time,
schedule.command,
schedule.name,
schedule.enabled,
)
# 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)
try:
self.tasks_manager.remove_all_server_tasks(server_id)
except:
@ -433,6 +476,12 @@ class AjaxHandler(BaseHandler):
elif page == "unzip_server":
path = self.get_argument("path", None)
if not path:
path = os.path.join(
self.controller.project_root,
"imports",
self.get_argument("file", ""),
)
if Helpers.check_file_exists(path):
self.helper.unzip_server(path, exec_user["user_id"])
else:

View File

@ -104,7 +104,10 @@ class BaseHandler(tornado.web.RequestHandler):
strip: bool = True,
) -> t.Optional[str]:
arg = self._get_argument(name, default, self.request.arguments, strip)
return self.autobleach(name, arg)
bleached = self.autobleach(name, arg)
if "&amp;" in str(bleached):
bleached = bleached.replace("&amp;", "&")
return bleached
def get_arguments(self, name: str, strip: bool = True) -> t.List[str]:
if not isinstance(strip, bool):

View File

@ -8,7 +8,6 @@ import logging
import threading
import shlex
import bleach
import libgravatar
import requests
import tornado.web
import tornado.escape
@ -331,37 +330,6 @@ class PanelHandler(BaseHandler):
"superuser": superuser,
}
# http://en.gravatar.com/site/implement/images/#rating
if self.helper.get_setting("allow_nsfw_profile_pictures"):
rating = "x"
else:
rating = "g"
# Get grvatar hash for profile pictures
if exec_user["email"] != "default@example.com" or "":
gravatar = libgravatar.Gravatar(
libgravatar.sanitize_email(exec_user["email"])
)
url = gravatar.get_image(
size=80,
default="404",
force_default=False,
rating=rating,
filetype_extension=False,
use_ssl=True,
) # + "?d=404"
try:
if requests.head(url).status_code != 404:
profile_url = url
else:
profile_url = "/static/assets/images/faces-clipart/pic-3.png"
except:
profile_url = "/static/assets/images/faces-clipart/pic-3.png"
else:
profile_url = "/static/assets/images/faces-clipart/pic-3.png"
page_data["user_image"] = profile_url
if page == "unauthorized":
template = "panel/denied.html"
@ -549,7 +517,7 @@ class PanelHandler(BaseHandler):
"log_path": server_temp_obj["log_path"],
"executable": server_temp_obj["executable"],
"execution_command": server_temp_obj["execution_command"],
"shutdown_timeout": server_obj["shutdown_timeout"],
"shutdown_timeout": server_temp_obj["shutdown_timeout"],
"stop_command": server_temp_obj["stop_command"],
"executable_update_url": server_temp_obj[
"executable_update_url"
@ -910,6 +878,7 @@ class PanelHandler(BaseHandler):
page_data["user"]["roles"] = set()
page_data["user"]["hints"] = True
page_data["superuser"] = superuser
page_data["themes"] = self.helper.get_themes()
if EnumPermissionsCrafty.USER_CONFIG not in exec_user_crafty_permissions:
self.redirect(
@ -1009,6 +978,7 @@ class PanelHandler(BaseHandler):
# We'll just default to basic for new schedules
page_data["schedule"]["difficulty"] = "basic"
page_data["schedule"]["interval_type"] = "days"
page_data["parent"] = None
if not EnumPermissionsServer.SCHEDULE in page_data["user_permissions"]:
if not superuser:
@ -1091,10 +1061,15 @@ class PanelHandler(BaseHandler):
page_data["schedule"]["interval_type"] = schedule.interval_type
if schedule.interval_type == "reaction":
difficulty = "reaction"
page_data["parent"] = self.controller.management.get_scheduled_task(
schedule.parent
)
elif schedule.cron_string == "":
difficulty = "basic"
page_data["parent"] = None
else:
difficulty = "advanced"
page_data["parent"] = None
page_data["schedule"]["difficulty"] = difficulty
if not EnumPermissionsServer.SCHEDULE in page_data["user_permissions"]:
@ -1118,6 +1093,7 @@ class PanelHandler(BaseHandler):
page_data["exec_user"] = exec_user["user_id"]
page_data["servers_all"] = self.controller.servers.get_all_defined_servers()
page_data["superuser"] = superuser
page_data["themes"] = self.helper.get_themes()
if page_data["user"]["manager"] is not None:
page_data["manager"] = self.controller.users.get_user_by_id(
page_data["user"]["manager"]
@ -1726,8 +1702,14 @@ class PanelHandler(BaseHandler):
# only check for time if it's number of days
if interval_type == "days":
sch_time = bleach.clean(self.get_argument("time", None))
if int(interval) > 30:
self.redirect(
"/panel/error?error=Invalid argument."
" Days must be 30 or fewer."
)
return
if action == "command":
command = bleach.clean(self.get_argument("command", None))
command = self.get_argument("command", None)
elif action == "start":
command = "start_server"
elif action == "stop":
@ -1743,7 +1725,7 @@ class PanelHandler(BaseHandler):
delay = bleach.clean(self.get_argument("delay", None))
parent = bleach.clean(self.get_argument("parent", None))
if action == "command":
command = bleach.clean(self.get_argument("command", None))
command = self.get_argument("command", None)
elif action == "start":
command = "start_server"
elif action == "stop":
@ -1763,7 +1745,7 @@ class PanelHandler(BaseHandler):
return
action = bleach.clean(self.get_argument("action", None))
if action == "command":
command = bleach.clean(self.get_argument("command", None))
command = self.get_argument("command", None)
elif action == "start":
command = "start_server"
elif action == "stop":
@ -1888,8 +1870,14 @@ class PanelHandler(BaseHandler):
# only check for time if it's number of days
if interval_type == "days":
sch_time = bleach.clean(self.get_argument("time", None))
if int(interval) > 30:
self.redirect(
"/panel/error?error=Invalid argument."
" Days must be 30 or fewer."
)
return
if action == "command":
command = bleach.clean(self.get_argument("command", None))
command = self.get_argument("command", None)
elif action == "start":
command = "start_server"
elif action == "stop":
@ -1904,7 +1892,7 @@ class PanelHandler(BaseHandler):
delay = bleach.clean(self.get_argument("delay", None))
parent = bleach.clean(self.get_argument("parent", None))
if action == "command":
command = bleach.clean(self.get_argument("command", None))
command = self.get_argument("command", None)
elif action == "start":
command = "start_server"
elif action == "stop":
@ -1924,7 +1912,7 @@ class PanelHandler(BaseHandler):
return
action = bleach.clean(self.get_argument("action", None))
if action == "command":
command = bleach.clean(self.get_argument("command", None))
command = self.get_argument("command", None)
elif action == "start":
command = "start_server"
elif action == "stop":
@ -2025,6 +2013,7 @@ class PanelHandler(BaseHandler):
user_id = bleach.clean(self.get_argument("id", None))
user = self.controller.users.get_user_by_id(user_id)
username = bleach.clean(self.get_argument("username", None).lower())
theme = bleach.clean(self.get_argument("theme", "default"))
if (
username != self.controller.users.get_user_by_id(user_id)["username"]
and username in self.controller.users.get_all_usernames()
@ -2091,6 +2080,7 @@ class PanelHandler(BaseHandler):
"email": email,
"lang": lang,
"hints": hints,
"theme": theme,
}
self.controller.users.update_user(user_id, user_data=user_data)
@ -2127,6 +2117,7 @@ class PanelHandler(BaseHandler):
"lang": lang,
"superuser": superuser,
"hints": hints,
"theme": theme,
}
user_crafty_data = {
"permissions_mask": permissions_mask,
@ -2238,6 +2229,7 @@ class PanelHandler(BaseHandler):
password1 = bleach.clean(self.get_argument("password1", None))
email = bleach.clean(self.get_argument("email", "default@example.com"))
enabled = int(float(self.get_argument("enabled", "0")))
theme = bleach.clean(self.get_argument("theme"), "default")
hints = True
lang = bleach.clean(
self.get_argument("lang", self.helper.get_setting("language"))
@ -2293,6 +2285,7 @@ class PanelHandler(BaseHandler):
email=email,
enabled=enabled,
superuser=new_superuser,
theme=theme,
)
user_data = {"roles": roles, "lang": lang, "hints": True}
user_crafty_data = {

View File

@ -26,7 +26,7 @@ class ApiServersServerStdinHandler(BaseApiHandler):
# if the user doesn't have Commands permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
svr = self.controller.get_server_obj_optional(server_id)
svr = self.controller.servers.get_server_obj_optional(server_id)
if svr is None:
# It's in auth_data[0] but not as a Server object
logger.critical(

View File

@ -105,6 +105,7 @@ class ApiUsersIndexHandler(BaseApiHandler):
permissions = data.get("permissions", None)
roles = data.get("roles", None)
hints = data.get("hints", True)
theme = data.get("theme", "default")
if username.lower() in ["system", ""]:
return self.finish_json(
@ -155,6 +156,7 @@ class ApiUsersIndexHandler(BaseApiHandler):
email,
enabled,
new_superuser,
theme,
)
self.controller.users.update_user(
user_id,

View File

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

View File

@ -5,8 +5,6 @@ import time
import tornado.web
import tornado.escape
import bleach
import libgravatar
import requests
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.shared.helpers import Helpers
@ -133,34 +131,6 @@ class ServerHandler(BaseHandler):
"superuser": superuser,
}
if self.helper.get_setting("allow_nsfw_profile_pictures"):
rating = "x"
else:
rating = "g"
if exec_user["email"] != "default@example.com" or "":
gravatar = libgravatar.Gravatar(
libgravatar.sanitize_email(exec_user["email"])
)
url = gravatar.get_image(
size=80,
default="404",
force_default=False,
rating=rating,
filetype_extension=False,
use_ssl=True,
) # + "?d=404"
try:
if requests.head(url).status_code != 404:
profile_url = url
else:
profile_url = "/static/assets/images/faces-clipart/pic-3.png"
except:
profile_url = "/static/assets/images/faces-clipart/pic-3.png"
else:
profile_url = "/static/assets/images/faces-clipart/pic-3.png"
page_data["user_image"] = profile_url
if superuser:
page_data["roles"] = list_roles

View File

@ -4,6 +4,7 @@ import time
import tornado.web
import tornado.options
import tornado.httpserver
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.shared.console import Console
@ -33,115 +34,295 @@ class UploadHandler(BaseHandler):
def prepare(self):
# Class & Function Defination
api_key, _token_data, exec_user = self.current_user
server_id = self.get_argument("server_id", None)
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
user_id = exec_user["user_id"]
stream_size_value = self.helper.get_setting("stream_size_GB")
self.upload_type = str(self.request.headers.get("X-Content-Upload-Type"))
max_streamed_size = (1024 * 1024 * 1024) * stream_size_value
if self.upload_type == "server_import":
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
user_id = exec_user["user_id"]
stream_size_value = self.helper.get_setting("stream_size_GB")
self.content_len = int(self.request.headers.get("Content-Length"))
if self.content_len > max_streamed_size:
logger.error(
f"User with ID {user_id} attempted to upload a file that"
f" exceeded the max body size."
)
self.helper.websocket_helper.broadcast_user(
user_id,
"send_start_error",
{
"error": self.helper.translation.translate(
"error",
"fileTooLarge",
self.controller.users.get_user_lang_by_id(user_id),
),
},
)
return
self.do_upload = True
max_streamed_size = (1024 * 1024 * 1024) * stream_size_value
if superuser:
exec_user_server_permissions = (
self.controller.server_perms.list_defined_permissions()
)
elif api_key is not None:
exec_user_server_permissions = (
self.controller.server_perms.get_api_key_permissions_list(
api_key, server_id
self.content_len = int(self.request.headers.get("Content-Length"))
if self.content_len > max_streamed_size:
logger.error(
f"User with ID {user_id} attempted to upload a file that"
f" exceeded the max body size."
)
)
else:
exec_user_server_permissions = (
self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
self.helper.websocket_helper.broadcast_user(
user_id,
"send_start_error",
{
"error": self.helper.translation.translate(
"error",
"fileTooLarge",
self.controller.users.get_user_lang_by_id(user_id),
),
},
)
)
return
self.do_upload = True
server_id = self.request.headers.get("X-ServerId", None)
if superuser:
exec_user_server_permissions = (
self.controller.server_perms.list_defined_permissions()
)
elif api_key is not None:
exec_user_server_permissions = (
self.controller.crafty_perms.get_api_key_permissions_list(api_key)
)
else:
exec_user_server_permissions = (
self.controller.crafty_perms.get_crafty_permissions_list(
exec_user["user_id"]
)
)
if user_id is None:
logger.warning("User ID not found in upload handler call")
Console.warning("User ID not found in upload handler call")
self.do_upload = False
if user_id is None:
logger.warning("User ID not found in upload handler call")
Console.warning("User ID not found in upload handler call")
self.do_upload = False
if server_id is None:
logger.warning("Server ID not found in upload handler call")
Console.warning("Server ID not found in upload handler call")
self.do_upload = False
if (
EnumPermissionsCrafty.SERVER_CREATION
not in exec_user_server_permissions
and not exec_user["superuser"]
):
logger.warning(
f"User {user_id} tried to upload a server" " without permissions!"
)
Console.warning(
f"User {user_id} tried to upload a server" " without permissions!"
)
self.do_upload = False
if EnumPermissionsServer.FILES not in exec_user_server_permissions:
logger.warning(
f"User {user_id} tried to upload a file to "
f"{server_id} without permissions!"
)
Console.warning(
f"User {user_id} tried to upload a file to "
f"{server_id} without permissions!"
)
self.do_upload = False
path = os.path.join(self.controller.project_root, "imports")
# Delete existing files
if len(os.listdir(path)) > 0:
for item in os.listdir():
try:
os.remove(os.path.join(path, item))
except:
logger.debug("Could not delete file on user server upload")
path = self.request.headers.get("X-Path", None)
filename = self.request.headers.get("X-FileName", None)
full_path = os.path.join(path, filename)
self.helper.ensure_dir_exists(path)
filename = self.request.headers.get("X-FileName", None)
if not str(filename).endswith(".zip"):
self.helper.websocket_helper.broadcast("close_upload_box", "error")
self.finish("error")
full_path = os.path.join(path, filename)
if not Helpers.in_path(
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
full_path,
):
print(
user_id,
server_id,
if self.do_upload:
try:
self.f = open(full_path, "wb")
except Exception as e:
logger.error(f"Upload failed with error: {e}")
self.do_upload = False
# If max_body_size is not set, you cannot upload files > 100MB
self.request.connection.set_max_body_size(max_streamed_size)
elif self.upload_type == "background":
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
user_id = exec_user["user_id"]
stream_size_value = self.helper.get_setting("stream_size_GB")
max_streamed_size = (1024 * 1024 * 1024) * stream_size_value
self.content_len = int(self.request.headers.get("Content-Length"))
if self.content_len > max_streamed_size:
logger.error(
f"User with ID {user_id} attempted to upload a file that"
f" exceeded the max body size."
)
self.helper.websocket_helper.broadcast_user(
user_id,
"send_start_error",
{
"error": self.helper.translation.translate(
"error",
"fileTooLarge",
self.controller.users.get_user_lang_by_id(user_id),
),
},
)
return
self.do_upload = True
if superuser:
exec_user_server_permissions = (
self.controller.server_perms.list_defined_permissions()
)
elif api_key is not None:
exec_user_server_permissions = (
self.controller.server_perms.get_api_key_permissions_list(
api_key, server_id
)
)
else:
exec_user_server_permissions = (
self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
)
server_id = self.request.headers.get("X-ServerId", None)
if server_id is None:
logger.warning("Server ID not found in upload handler call")
Console.warning("Server ID not found in upload handler call")
self.do_upload = False
if user_id is None:
logger.warning("User ID not found in upload handler call")
Console.warning("User ID not found in upload handler call")
self.do_upload = False
if EnumPermissionsServer.FILES not in exec_user_server_permissions:
logger.warning(
f"User {user_id} tried to upload a file to "
f"{server_id} without permissions!"
)
Console.warning(
f"User {user_id} tried to upload a file to "
f"{server_id} without permissions!"
)
self.do_upload = False
path = self.request.headers.get("X-Path", None)
filename = self.request.headers.get("X-FileName", None)
full_path = os.path.join(path, filename)
if not Helpers.in_path(
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
full_path,
)
logger.warning(
f"User {user_id} tried to upload a file to {server_id} "
f"but the path is not inside of the server!"
)
Console.warning(
f"User {user_id} tried to upload a file to {server_id} "
f"but the path is not inside of the server!"
)
self.do_upload = False
if self.do_upload:
try:
self.f = open(full_path, "wb")
except Exception as e:
logger.error(f"Upload failed with error: {e}")
):
logger.warning(
f"User {user_id} tried to upload a file to {server_id} "
f"but the path is not inside of the server!"
)
Console.warning(
f"User {user_id} tried to upload a file to {server_id} "
f"but the path is not inside of the server!"
)
self.do_upload = False
# If max_body_size is not set, you cannot upload files > 100MB
self.request.connection.set_max_body_size(max_streamed_size)
if self.do_upload:
try:
self.f = open(full_path, "wb")
except Exception as e:
logger.error(f"Upload failed with error: {e}")
self.do_upload = False
# If max_body_size is not set, you cannot upload files > 100MB
self.request.connection.set_max_body_size(max_streamed_size)
else:
server_id = self.get_argument("server_id", None)
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
user_id = exec_user["user_id"]
stream_size_value = self.helper.get_setting("stream_size_GB")
max_streamed_size = (1024 * 1024 * 1024) * stream_size_value
self.content_len = int(self.request.headers.get("Content-Length"))
if self.content_len > max_streamed_size:
logger.error(
f"User with ID {user_id} attempted to upload a file that"
f" exceeded the max body size."
)
self.helper.websocket_helper.broadcast_user(
user_id,
"send_start_error",
{
"error": self.helper.translation.translate(
"error",
"fileTooLarge",
self.controller.users.get_user_lang_by_id(user_id),
),
},
)
return
self.do_upload = True
if superuser:
exec_user_server_permissions = (
self.controller.server_perms.list_defined_permissions()
)
elif api_key is not None:
exec_user_server_permissions = (
self.controller.server_perms.get_api_key_permissions_list(
api_key, server_id
)
)
else:
exec_user_server_permissions = (
self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
)
server_id = self.request.headers.get("X-ServerId", None)
if server_id is None:
logger.warning("Server ID not found in upload handler call")
Console.warning("Server ID not found in upload handler call")
self.do_upload = False
if user_id is None:
logger.warning("User ID not found in upload handler call")
Console.warning("User ID not found in upload handler call")
self.do_upload = False
if EnumPermissionsServer.FILES not in exec_user_server_permissions:
logger.warning(
f"User {user_id} tried to upload a file to "
f"{server_id} without permissions!"
)
Console.warning(
f"User {user_id} tried to upload a file to "
f"{server_id} without permissions!"
)
self.do_upload = False
path = self.request.headers.get("X-Path", None)
filename = self.request.headers.get("X-FileName", None)
full_path = os.path.join(path, filename)
if not Helpers.in_path(
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
full_path,
):
logger.warning(
f"User {user_id} tried to upload a file to {server_id} "
f"but the path is not inside of the server!"
)
Console.warning(
f"User {user_id} tried to upload a file to {server_id} "
f"but the path is not inside of the server!"
)
self.do_upload = False
if self.do_upload:
try:
self.f = open(full_path, "wb")
except Exception as e:
logger.error(f"Upload failed with error: {e}")
self.do_upload = False
# If max_body_size is not set, you cannot upload files > 100MB
self.request.connection.set_max_body_size(max_streamed_size)
def post(self):
logger.info("Upload completed")
files_left = int(self.request.headers.get("X-Files-Left", None))
if self.upload_type == "server_files":
files_left = int(self.request.headers.get("X-Files-Left", None))
else:
files_left = 0
if self.do_upload:
time.sleep(5)

View File

@ -85,13 +85,16 @@ class WebSocketHelper:
self.broadcast_with_fn(filter_fn, event_type, data)
def broadcast_with_fn(self, filter_fn, event_type: str, data):
clients = list(filter(filter_fn, self.clients))
# assign self.clients to a static variable here so hopefully
# the set size won't change
static_clients = self.clients
clients = list(filter(filter_fn, static_clients))
logger.debug(
f"Sending to {len(clients)} out of {len(self.clients)} "
f"clients: {json.dumps({'event': event_type, 'data': data})}"
)
for client in clients:
for client in clients[:]:
try:
self.send_message(client, event_type, data)
except Exception as e:

View File

@ -1,6 +1,6 @@
{
"major": 4,
"minor": 0,
"sub": 13,
"sub": 16,
"meta": "beta"
}

View File

@ -1,11 +1,11 @@
.select-css option {
background-color: #1C1E2F;
color: white
background-color: var(--deep-bg);
color: var(--base-text)
}
.select-css option:checked {
background-color: #1C1E2F;
color: white
background-color: var(--deep-bg);
color: var(--base-text)
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3,68 +3,122 @@
* License - https://fontawesome.com/license (Commercial License)
*/
svg:not(:root).svg-inline--fa {
overflow: visible; }
overflow: visible;
}
.svg-inline--fa {
display: inline-block;
font-size: inherit;
height: 1em;
overflow: visible;
vertical-align: -.125em; }
.svg-inline--fa.fa-lg {
vertical-align: -.225em; }
.svg-inline--fa.fa-w-1 {
width: 0.0625em; }
.svg-inline--fa.fa-w-2 {
width: 0.125em; }
.svg-inline--fa.fa-w-3 {
width: 0.1875em; }
.svg-inline--fa.fa-w-4 {
width: 0.25em; }
.svg-inline--fa.fa-w-5 {
width: 0.3125em; }
.svg-inline--fa.fa-w-6 {
width: 0.375em; }
.svg-inline--fa.fa-w-7 {
width: 0.4375em; }
.svg-inline--fa.fa-w-8 {
width: 0.5em; }
.svg-inline--fa.fa-w-9 {
width: 0.5625em; }
.svg-inline--fa.fa-w-10 {
width: 0.625em; }
.svg-inline--fa.fa-w-11 {
width: 0.6875em; }
.svg-inline--fa.fa-w-12 {
width: 0.75em; }
.svg-inline--fa.fa-w-13 {
width: 0.8125em; }
.svg-inline--fa.fa-w-14 {
width: 0.875em; }
.svg-inline--fa.fa-w-15 {
width: 0.9375em; }
.svg-inline--fa.fa-w-16 {
width: 1em; }
.svg-inline--fa.fa-w-17 {
width: 1.0625em; }
.svg-inline--fa.fa-w-18 {
width: 1.125em; }
.svg-inline--fa.fa-w-19 {
width: 1.1875em; }
.svg-inline--fa.fa-w-20 {
width: 1.25em; }
.svg-inline--fa.fa-pull-left {
margin-right: .3em;
width: auto; }
.svg-inline--fa.fa-pull-right {
margin-left: .3em;
width: auto; }
.svg-inline--fa.fa-border {
height: 1.5em; }
.svg-inline--fa.fa-li {
width: 2em; }
.svg-inline--fa.fa-fw {
width: 1.25em; }
vertical-align: -.125em;
}
.svg-inline--fa.fa-lg {
vertical-align: -.225em;
}
.svg-inline--fa.fa-w-1 {
width: 0.0625em;
}
.svg-inline--fa.fa-w-2 {
width: 0.125em;
}
.svg-inline--fa.fa-w-3 {
width: 0.1875em;
}
.svg-inline--fa.fa-w-4 {
width: 0.25em;
}
.svg-inline--fa.fa-w-5 {
width: 0.3125em;
}
.svg-inline--fa.fa-w-6 {
width: 0.375em;
}
.svg-inline--fa.fa-w-7 {
width: 0.4375em;
}
.svg-inline--fa.fa-w-8 {
width: 0.5em;
}
.svg-inline--fa.fa-w-9 {
width: 0.5625em;
}
.svg-inline--fa.fa-w-10 {
width: 0.625em;
}
.svg-inline--fa.fa-w-11 {
width: 0.6875em;
}
.svg-inline--fa.fa-w-12 {
width: 0.75em;
}
.svg-inline--fa.fa-w-13 {
width: 0.8125em;
}
.svg-inline--fa.fa-w-14 {
width: 0.875em;
}
.svg-inline--fa.fa-w-15 {
width: 0.9375em;
}
.svg-inline--fa.fa-w-16 {
width: 1em;
}
.svg-inline--fa.fa-w-17 {
width: 1.0625em;
}
.svg-inline--fa.fa-w-18 {
width: 1.125em;
}
.svg-inline--fa.fa-w-19 {
width: 1.1875em;
}
.svg-inline--fa.fa-w-20 {
width: 1.25em;
}
.svg-inline--fa.fa-pull-left {
margin-right: .3em;
width: auto;
}
.svg-inline--fa.fa-pull-right {
margin-left: .3em;
width: auto;
}
.svg-inline--fa.fa-border {
height: 1.5em;
}
.svg-inline--fa.fa-li {
width: 2em;
}
.svg-inline--fa.fa-fw {
width: 1.25em;
}
.fa-layers svg.svg-inline--fa {
bottom: 0;
@ -72,7 +126,8 @@ svg:not(:root).svg-inline--fa {
margin: auto;
position: absolute;
right: 0;
top: 0; }
top: 0;
}
.fa-layers {
display: inline-block;
@ -80,30 +135,36 @@ svg:not(:root).svg-inline--fa {
position: relative;
text-align: center;
vertical-align: -.125em;
width: 1em; }
.fa-layers svg.svg-inline--fa {
-webkit-transform-origin: center center;
transform-origin: center center; }
width: 1em;
}
.fa-layers-text, .fa-layers-counter {
.fa-layers svg.svg-inline--fa {
-webkit-transform-origin: center center;
transform-origin: center center;
}
.fa-layers-text,
.fa-layers-counter {
display: inline-block;
position: absolute;
text-align: center; }
text-align: center;
}
.fa-layers-text {
left: 50%;
top: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
-webkit-transform-origin: center center;
transform-origin: center center; }
transform-origin: center center;
}
.fa-layers-counter {
background-color: #ff253a;
border-radius: 1em;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #fff;
box-sizing: border-box;
var(--base-text);
height: 1.5em;
line-height: 1;
max-width: 5em;
@ -114,18 +175,20 @@ svg:not(:root).svg-inline--fa {
text-overflow: ellipsis;
top: 0;
-webkit-transform: scale(0.25);
transform: scale(0.25);
transform: scale(0.25);
-webkit-transform-origin: top right;
transform-origin: top right; }
transform-origin: top right;
}
.fa-layers-bottom-right {
bottom: 0;
right: 0;
top: auto;
-webkit-transform: scale(0.25);
transform: scale(0.25);
transform: scale(0.25);
-webkit-transform-origin: bottom right;
transform-origin: bottom right; }
transform-origin: bottom right;
}
.fa-layers-bottom-left {
bottom: 0;
@ -133,164 +196,207 @@ svg:not(:root).svg-inline--fa {
right: auto;
top: auto;
-webkit-transform: scale(0.25);
transform: scale(0.25);
transform: scale(0.25);
-webkit-transform-origin: bottom left;
transform-origin: bottom left; }
transform-origin: bottom left;
}
.fa-layers-top-right {
right: 0;
top: 0;
-webkit-transform: scale(0.25);
transform: scale(0.25);
transform: scale(0.25);
-webkit-transform-origin: top right;
transform-origin: top right; }
transform-origin: top right;
}
.fa-layers-top-left {
left: 0;
right: auto;
top: 0;
-webkit-transform: scale(0.25);
transform: scale(0.25);
transform: scale(0.25);
-webkit-transform-origin: top left;
transform-origin: top left; }
transform-origin: top left;
}
.fa-lg {
font-size: 1.33333em;
line-height: 0.75em;
vertical-align: -.0667em; }
vertical-align: -.0667em;
}
.fa-xs {
font-size: .75em; }
font-size: .75em;
}
.fa-sm {
font-size: .875em; }
font-size: .875em;
}
.fa-1x {
font-size: 1em; }
font-size: 1em;
}
.fa-2x {
font-size: 2em; }
font-size: 2em;
}
.fa-3x {
font-size: 3em; }
font-size: 3em;
}
.fa-4x {
font-size: 4em; }
font-size: 4em;
}
.fa-5x {
font-size: 5em; }
font-size: 5em;
}
.fa-6x {
font-size: 6em; }
font-size: 6em;
}
.fa-7x {
font-size: 7em; }
font-size: 7em;
}
.fa-8x {
font-size: 8em; }
font-size: 8em;
}
.fa-9x {
font-size: 9em; }
font-size: 9em;
}
.fa-10x {
font-size: 10em; }
font-size: 10em;
}
.fa-fw {
text-align: center;
width: 1.25em; }
width: 1.25em;
}
.fa-ul {
list-style-type: none;
margin-left: 2.5em;
padding-left: 0; }
.fa-ul > li {
position: relative; }
padding-left: 0;
}
.fa-ul>li {
position: relative;
}
.fa-li {
left: -2em;
position: absolute;
text-align: center;
width: 2em;
line-height: inherit; }
line-height: inherit;
}
.fa-border {
border: solid 0.08em #eee;
border-radius: .1em;
padding: .2em .25em .15em; }
padding: .2em .25em .15em;
}
.fa-pull-left {
float: left; }
float: left;
}
.fa-pull-right {
float: right; }
float: right;
}
.fa.fa-pull-left,
.fas.fa-pull-left,
.far.fa-pull-left,
.fal.fa-pull-left,
.fab.fa-pull-left {
margin-right: .3em; }
margin-right: .3em;
}
.fa.fa-pull-right,
.fas.fa-pull-right,
.far.fa-pull-right,
.fal.fa-pull-right,
.fab.fa-pull-right {
margin-left: .3em; }
margin-left: .3em;
}
.fa-spin {
-webkit-animation: fa-spin 2s infinite linear;
animation: fa-spin 2s infinite linear; }
animation: fa-spin 2s infinite linear;
}
.fa-pulse {
-webkit-animation: fa-spin 1s infinite steps(8);
animation: fa-spin 1s infinite steps(8); }
animation: fa-spin 1s infinite steps(8);
}
@-webkit-keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); }
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg); } }
transform: rotate(360deg);
}
}
@keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); }
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg); } }
transform: rotate(360deg);
}
}
.fa-rotate-90 {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";
-webkit-transform: rotate(90deg);
transform: rotate(90deg); }
transform: rotate(90deg);
}
.fa-rotate-180 {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";
-webkit-transform: rotate(180deg);
transform: rotate(180deg); }
transform: rotate(180deg);
}
.fa-rotate-270 {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";
-webkit-transform: rotate(270deg);
transform: rotate(270deg); }
transform: rotate(270deg);
}
.fa-flip-horizontal {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";
-webkit-transform: scale(-1, 1);
transform: scale(-1, 1); }
transform: scale(-1, 1);
}
.fa-flip-vertical {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
-webkit-transform: scale(1, -1);
transform: scale(1, -1); }
transform: scale(1, -1);
}
.fa-flip-both, .fa-flip-horizontal.fa-flip-vertical {
.fa-flip-both,
.fa-flip-horizontal.fa-flip-vertical {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
-webkit-transform: scale(-1, -1);
transform: scale(-1, -1); }
transform: scale(-1, -1);
}
:root .fa-rotate-90,
:root .fa-rotate-180,
@ -299,13 +405,15 @@ svg:not(:root).svg-inline--fa {
:root .fa-flip-vertical,
:root .fa-flip-both {
-webkit-filter: none;
filter: none; }
filter: none;
}
.fa-stack {
display: inline-block;
height: 2em;
position: relative;
width: 2.5em; }
width: 2.5em;
}
.fa-stack-1x,
.fa-stack-2x {
@ -314,18 +422,22 @@ svg:not(:root).svg-inline--fa {
margin: auto;
position: absolute;
right: 0;
top: 0; }
top: 0;
}
.svg-inline--fa.fa-stack-1x {
height: 1em;
width: 1.25em; }
width: 1.25em;
}
.svg-inline--fa.fa-stack-2x {
height: 2em;
width: 2.5em; }
width: 2.5em;
}
.fa-inverse {
color: #fff; }
var(--base-text);
}
.sr-only {
border: 0;
@ -335,37 +447,46 @@ svg:not(:root).svg-inline--fa {
overflow: hidden;
padding: 0;
position: absolute;
width: 1px; }
width: 1px;
}
.sr-only-focusable:active, .sr-only-focusable:focus {
.sr-only-focusable:active,
.sr-only-focusable:focus {
clip: auto;
height: auto;
margin: 0;
overflow: visible;
position: static;
width: auto; }
width: auto;
}
.svg-inline--fa .fa-primary {
fill: var(--fa-primary-color, currentColor);
opacity: 1;
opacity: var(--fa-primary-opacity, 1); }
opacity: var(--fa-primary-opacity, 1);
}
.svg-inline--fa .fa-secondary {
fill: var(--fa-secondary-color, currentColor);
opacity: 0.4;
opacity: var(--fa-secondary-opacity, 0.4); }
opacity: var(--fa-secondary-opacity, 0.4);
}
.svg-inline--fa.fa-swap-opacity .fa-primary {
opacity: 0.4;
opacity: var(--fa-secondary-opacity, 0.4); }
opacity: var(--fa-secondary-opacity, 0.4);
}
.svg-inline--fa.fa-swap-opacity .fa-secondary {
opacity: 1;
opacity: var(--fa-primary-opacity, 1); }
opacity: var(--fa-primary-opacity, 1);
}
.svg-inline--fa mask .fa-primary,
.svg-inline--fa mask .fa-secondary {
fill: black; }
fill: black;
}
.fad.fa-inverse {
color: #fff; }
var(--base-text);
}

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="{{ data['lang_page'] }}">
<html lang="{{ data['lang_page'] }}" class="{{data['user_data'].get('theme', 'default')}}">
<head>
<!-- Required meta tags -->
@ -54,7 +54,7 @@
</head>
<body class="dark-theme">
<body>
<div class="container-scroller">
<!-- partial:partials/_navbar.html -->
<nav class="navbar default-layout col-lg-12 col-12 p-0 fixed-top d-flex flex-row">
@ -127,7 +127,7 @@
width: 180px;
margin-left: 10px;
margin-right: 10px;
background: #282a40;
background: var(--card-banner-bg);
transition: right 0.75s, opacity 0.75s, top 0.75s;
right: -6rem;
opacity: 0.1;
@ -525,10 +525,6 @@
});
});
$(window).unload(function () {
jQuery.get("/public/logout")
});
</script>
{% block js %}

View File

@ -18,10 +18,10 @@
<li class="nav-item dropdown user-dropdown">
<a class="nav-link dropdown-toggle" id="UserDropdown" href="#" data-toggle="dropdown" aria-expanded="false">
<img class="img-xs rounded-circle profile-picture" src="{{ data['user_image'] }}" alt="Profile image"> </a>
<img class="img-xs rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}" alt="Profile image"> </a>
<div class="dropdown-menu dropdown-menu-right navbar-dropdown" aria-labelledby="UserDropdown">
<div class="dropdown-header text-center">
<img class="img-md rounded-circle profile-picture" src="{{ data['user_image'] }}" alt="Profile image">
<img class="img-md rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}" alt="Profile image">
<p class="mb-1 mt-3 font-weight-semibold">{{ data['user_data']['username'] }}</p>
<p class="font-weight-light text-muted mb-0">Roles: </p>
{% for r in data['user_role'] %}
@ -46,4 +46,12 @@
<a class="dropdown-item" href="/public/logout"><i class="dropdown-item-icon mdi mdi-power text-primary"></i>{{ translate('notify', 'logout', data['lang']) }}</a>
</div>
</li>
</ul>
</ul>
<script>
function pfpError(image) {
image.onerror = "";
image.src = "/static/assets/images/faces-clipart/pic-3.png";
return true;
}
</script>

View File

@ -39,7 +39,7 @@
<div class="card-body pt-0">
<ul class="nav nav-tabs col-md-12 tab-simple-styled " role="tablist">
<li class="nav-item">
<a class="nav-link active" href="/panel/edit_role?id={{ data['role']['role_id'] }}&subpage=config" role="tab" aria-selected="true">
<a class="nav-link active" href="" role="tab" aria-selected="true">
<i class="fas fa-cogs"></i>{{ translate('rolesConfig', 'config', data['lang']) }}</a>
</li>
<!-- <li class="nav-item">
@ -152,13 +152,13 @@
position: absolute;
bottom: 0;
left: 0;
border-bottom: var(--table-border-width) solid #383e5d;
border-bottom: var(--table-border-width) solid var(--outline);
transition: border-bottom-color 500ms;
padding-bottom: 5px;
user-select: none;
}
table.rotate-table > tbody td {
border-right: var(--table-border-width) solid #383e5d;
border-right: var(--table-border-width) solid var(--outline);
/* make sure this is at least as wide as sqrt(2) * height of the tallest letter in your font or the headers will overlap each other*/
min-width: 30px;
padding-top: 2px;

View File

@ -121,6 +121,19 @@ data['lang']) }}{% end %}
{% end %}
</select>
</div>
<div class="form-group">
<label class="form-label" for="theme">{{ translate('userConfig', 'userTheme', data['lang'])
}}</label>
<select class="form-select form-control form-control-lg select-css" id="language"
name="theme" form="user_form">
<option value="{{data['user'].get('theme', 'default')}}">{{data['user'].get('theme', 'default')}}</option>
{% for theme in data['themes'] %}
{% if theme != data['user'].get('theme', 'default') %}
<option value="{{theme}}">{{theme}}</option>
{% end %}
{% end %}
</select>
</div>
{% if data['superuser'] %}
<div class="form-group">
<label class="form-label" for="manager">{{ translate('userConfig', 'selectManager',

View File

@ -42,7 +42,6 @@
<div class="col-md-6 col-sm-12">
<style>
.playerItem {
background: #1c1e2f;
padding: 1rem;
display: flex;
flex-flow: row wrap;

View File

@ -711,6 +711,7 @@
xmlHttpRequest.setRequestHeader('X-Content-Length', size);
xmlHttpRequest.setRequestHeader('X-Content-Disposition', 'attachment; filename="' + fileName + '"');
xmlHttpRequest.setRequestHeader('X-Path', path);
xmlHttpRequest.setRequestHeader('X-Content-Upload-Type', 'server_files')
xmlHttpRequest.setRequestHeader('X-Files-Left', left);
xmlHttpRequest.setRequestHeader('X-FileName', fileName);
xmlHttpRequest.setRequestHeader('X-ServerId', serverId);

View File

@ -42,7 +42,7 @@
<div class="col-md-12">
<div class="input-group">
<div id="virt_console" class=""
style="font-size: .8em; padding: 5px 10px; border: 1px solid #383e5d; background-color:#2a2c44;height:500px; overflow: scroll;">
style="font-size: .8em; padding: 5px 10px; border: 1px solid var(--outline); background-color:var(--card-banner-bg);height:500px; overflow: scroll;">
</div>
</div>
<br />

View File

@ -145,17 +145,35 @@
data['lang']) }}</small> </label>
<select id="parent" name="parent" class="form-control form-control-lg select-css"
value="{{ data['schedule']['action'] }}">
{% for schedule in data['schedules'] %}
{% if schedule.schedule_id != data['schedule']['schedule_id'] %}
{% if schedule.interval != '' %}
<option id="{{schedule.schedule_id}}" value="{{schedule.schedule_id}}">
{{schedule.name}} | {{schedule.command}} | {{schedule.interval}} {{
schedule.interval_type}}</option>
{% if data['parent'] %}
<option id="{{data['parent']['schedule_id']}}" value="{{data['parent']['schedule_id']}}">
{{data['parent']['name']}} | {{data['parent']['command']}} | {{data['parent']['interval']}}
</option>
{% for schedule in data['schedules'] %}
{% if schedule.schedule_id != data['schedule']['schedule_id'] and schedule.schedule_id != data['parent']['schedule_id'] %}
{% if schedule.interval != '' %}
<option id="{{schedule.schedule_id}}" value="{{schedule.schedule_id}}">
{{schedule.name}} | {{schedule.command}} | {{schedule.interval}} {{
schedule.interval_type}}</option>
{% else %}
<option id="{{schedule.schedule_id}}" value="{{schedule.schedule_id}}">
{{schedule.name}} | {{schedule.command}} | {{schedule.cron_string}}</option>
{% end %}
{% end %}
{% end %}
{% else %}
<option id="{{schedule.schedule_id}}" value="{{schedule.schedule_id}}">
{{schedule.name}} | {{schedule.command}} | {{schedule.cron_string}}</option>
{% end %}
{% end %}
{% for schedule in data['schedules'] %}
{% if schedule.schedule_id != data['schedule']['schedule_id'] and schedule.schedule_id %}
{% if schedule.interval != '' %}
<option id="{{schedule.schedule_id}}" value="{{schedule.schedule_id}}">
{{schedule.name}} | {{schedule.command}} | {{schedule.interval}} {{
schedule.interval_type}}</option>
{% else %}
<option id="{{schedule.schedule_id}}" value="{{schedule.schedule_id}}">
{{schedule.name}} | {{schedule.command}} | {{schedule.cron_string}}</option>
{% end %}
{% end %}
{% end %}
{% end %}
</select>
</div>

View File

@ -14,7 +14,8 @@
<div class="col-12">
<div class="page-header">
<h4 class="page-title">
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{ data['server_stats']['server_id']['server_name'] }}
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
</h4>
@ -40,13 +41,21 @@
</span>
<div class="col-md-12">
<button id="to-bottom" style="visibility: hidden; float: right;" class="btn btn-outline-success">{{
translate('serverDetails', 'reset', data['lang']) }}</button>
<br />
<br />
<div class="input-group">
<div id="virt_console" class="" style="width: 100%; font-size: .8em; padding: 5px 10px; border: 1px solid #383e5d; background-color:#2a2c44;height:500px; overflow: scroll;"></div>
<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;">
</div>
</div>
<br />
<div style="gap: 0.5rem;" class="input-group flex-wrap">
<input style="min-width: 10rem;" type="text" class="form-control" id="server_command" name="server_command" placeholder="{{ translate('serverTerm', 'commandInput', data['lang']) }}" autofocus="">
<input style="min-width: 10rem;" type="text" class="form-control" id="server_command"
name="server_command" placeholder="{{ translate('serverTerm', 'commandInput', data['lang']) }}"
autofocus="">
<span class="input-group-btn ml-5">
<button id="submit" class="btn btn-sm btn-info" type="button">{{ translate('serverTerm', 'sendCommand',
data['lang']) }}</button>
@ -54,30 +63,60 @@
</div>
{% if data['permissions']['Commands'] in data['user_permissions'] %}
{% if data['server_stats']['updating']%}
<div id="update_control_buttons" class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0" style="visibility: visible">
<button onclick="" id="start-btn" style="max-width: 7rem;" class="btn btn-warning m-1 flex-grow-1 disabled"><i
class="fa fa-spinner fa-spin"></i>&nbsp;{{ translate('serverTerm', 'updating', data['lang']) }}</button>
<button onclick="" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1 disabled">{% raw translate('serverTerm', 'restart', data['lang']) %}</button>
<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>
<div id="update_control_buttons"
class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0"
style="visibility: visible">
<button onclick="" id="start-btn" style="max-width: 7rem;"
class="btn btn-warning m-1 flex-grow-1 disabled"><i class="fa fa-spinner fa-spin"></i>&nbsp;{{
translate('serverTerm', 'updating', data['lang']) }}</button>
<button onclick="" id="restart-btn" style="max-width: 7rem;"
class="btn btn-outline-primary m-1 flex-grow-1 disabled">{% raw translate('serverTerm', 'restart',
data['lang']) %}</button>
<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>
</div>
{% elif data['waiting_start'] %}
<div id="control_buttons" class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0" style="visibility: visible">
<button onclick="" id="start-btn" style="max-width: 7rem; white-space: nowrap;" class="btn btn-secondary m-1 flex-grow-1 disabled" data-toggle="tooltip" title="{{ translate('serverTerm', 'delay-explained', data['lang'])}}">{{ translate('serverTerm', 'starting', data['lang']) }}</button>
<button onclick="" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1 disabled">{% raw translate('serverTerm', 'restart', data['lang']) %}</button>
<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>
<div id="control_buttons"
class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0"
style="visibility: visible">
<button onclick="" id="start-btn" style="max-width: 7rem; white-space: nowrap;"
class="btn btn-secondary m-1 flex-grow-1 disabled" data-toggle="tooltip"
title="{{ translate('serverTerm', 'delay-explained', data['lang'])}}">{{ translate('serverTerm',
'starting', data['lang']) }}</button>
<button onclick="" id="restart-btn" style="max-width: 7rem;"
class="btn btn-outline-primary m-1 flex-grow-1 disabled">{% raw translate('serverTerm', 'restart',
data['lang']) %}</button>
<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>
</div>
{% elif data['importing'] %}
<div id="control_buttons" class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0" style="visibility: visible">
<button onclick="" id="start-btn" style="max-width: 12rem; white-space: nowrap;" class="btn btn-secondary m-1 flex-grow-1 disabled"><i class="fa fa-spinner fa-spin"></i> {{ translate('serverTerm', 'importing',
<div id="control_buttons"
class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0"
style="visibility: visible">
<button onclick="" id="start-btn" style="max-width: 12rem; white-space: nowrap;"
class="btn btn-secondary m-1 flex-grow-1 disabled"><i class="fa fa-spinner fa-spin"></i> {{
translate('serverTerm', 'importing',
data['lang']) }}</button>
<button onclick="" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1 disabled">{% raw translate('serverTerm', 'restart', data['lang']) %}</button>
<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>
<button onclick="" id="restart-btn" style="max-width: 7rem;"
class="btn btn-outline-primary m-1 flex-grow-1 disabled">{% raw translate('serverTerm', 'restart',
data['lang']) %}</button>
<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>
</div>
{% else %}
<div id="control_buttons" class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0" style="visibility: visible">
<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="send_command(serverId, 'stop_server');" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1">{{ translate('serverTerm', 'stop', data['lang']) }}</button>
<div id="control_buttons"
class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0"
style="visibility: visible">
<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="send_command(serverId, 'stop_server');" id="stop-btn" style="max-width: 7rem;"
class="btn btn-danger m-1 flex-grow-1">{{ translate('serverTerm', 'stop', data['lang']) }}</button>
</div>
{% end %}
{% end %}
@ -193,9 +232,12 @@
function new_line_handler(data) {
$('#virt_console').append(data.line)
const elem = document.getElementById('virt_console');
const scrollDiff = (elem.scrollHeight - elem.scrollTop) - elem.clientHeight;
if (!$("#stop_scroll").is(':checked') && scrollDiff < 450) {
scrollConsole()
try {
if (!scrolled) {
scrollConsole();
}
} catch {
scrollConsole();
}
}
@ -293,6 +335,30 @@
return nextCommand;
}
}
const chkScroll = (e) => {
const elem = $(e.currentTarget);
if (Math.round(elem[0].scrollHeight - elem.scrollTop()) <= elem.outerHeight()) {
document.getElementById("to-bottom").style.visibility = "hidden";
scrolled = false;
} else {
document.getElementById("to-bottom").style.visibility = "visible";
scrolled = true;
}
}
const scrollToBottom = (id) => {
const element = $(`#virt_console`);
element.animate({
scrollTop: element.prop("scrollHeight")
}, 500);
}
$(document).ready(() => {
var scrolled = false;
$('#virt_console').on('scroll', chkScroll);
$('#to-bottom').on('click', scrollToBottom)
});
</script>
{% end %}

View File

@ -8,7 +8,7 @@
{% block content %}
<!-- View for Large screen -->
<div class="row justify-content-center">
<div class="content-wrapper col-md login-modal d-none d-sm-block" style="background-color: #222437;">
<div class="content-wrapper col-md login-modal d-none d-sm-block" style="background-color: var(--dropdown-bg);">
<img src="/static/assets/images/logo_long.png" style='width: 25%; margin-left: 38%;'>
<hr />
<div class="table-responsive">
@ -40,7 +40,8 @@
</td>
<td id="server_motd_{{ server['stats']['server_id']['server_id'] }}">
{% if server['stats']['desc'] != 'False' %}
<img src="/static/assets/images/pack.png" alt="icon" style="-webkit-filter:grayscale(100%); filter:grayscale(100%)" />
<img src="/static/assets/images/pack.png" alt="icon"
style="-webkit-filter:grayscale(100%); filter:grayscale(100%)" />
<span id="input_motd_{{ server['stats']['server_id']['server_id'] }}" class="input_motd">{{
server['stats']['desc'] }}</span> <br />
{% end %}
@ -94,21 +95,25 @@
<h2 class="mb-0 container overflow-hidden">
<div class="row">
<div class="col-8 mx-0 px-0">
<a id="m_server_name_{{ server['stats']['server_id']['server_id'] }}" class="btn btn-link d-flex justify-content-center" type="button" data-toggle="collapse"
data-target="#collapse-{{server['server_data']['server_id']}}" aria-expanded="false"
aria-controls="collapse-{{server['server_data']['server_id']}}">
<a id="m_server_name_{{ server['stats']['server_id']['server_id'] }}"
class="btn btn-link d-flex justify-content-center" type="button" data-toggle="collapse"
data-target="#collapse-{{server['server_data']['server_id']}}" aria-expanded="false"
aria-controls="collapse-{{server['server_data']['server_id']}}">
<i class="fas fa-server"></i>
{{ server['server_data']['server_name'] }}
</a>
</div>
<div class="col-4 mx-0 px-0">
<a id="m_server_online_status_{{ server['stats']['server_id']['server_id'] }}" class="btn btn-link d-flex justify-content-center" type="button">
<a id="m_server_online_status_{{ server['stats']['server_id']['server_id'] }}"
class="btn btn-link d-flex justify-content-center" type="button">
{% if server['stats']['running'] %}
<div id="m_server_players_{{ server['stats']['server_id']['server_id'] }}">
<span class="text-success"><i class="fas fa-signal"></i> {{ server['stats']['online'] }} / {{ server['stats']['max'] }}</span>
<span class="text-success"><i class="fas fa-signal"></i> {{ server['stats']['online'] }} / {{
server['stats']['max'] }}</span>
</div>
{% else %}
<span class="text-danger"><i class="fas fa-ban"></i> {{ translate('dashboard', 'offline', data['lang']) }}</span>
<span class="text-danger"><i class="fas fa-ban"></i> {{ translate('dashboard', 'offline',
data['lang']) }}</span>
{% end %}
</a>
</div>
@ -117,12 +122,13 @@
</div>
<div id="collapse-{{server['server_data']['server_id']}}" class="collapse"
aria-labelledby="heading-{{server['server_data']['server_id']}}" data-parent="#accordionServers">
aria-labelledby="heading-{{server['server_data']['server_id']}}" data-parent="#accordionServers">
<div class="card-body">
{% if server['stats']['int_ping_results'] != 'False' %}
<div id="m_server_motd_{{ server['stats']['server_id']['server_id'] }}" class="media">
{% if server['stats']['desc'] != 'False' %}
<img src="/static/assets/images/pack.png" class="w-25 mr-3" alt="icon" style="-webkit-filter:grayscale(100%); filter:grayscale(100%);" />
<img src="/static/assets/images/pack.png" class="w-25 mr-3" alt="icon"
style="-webkit-filter:grayscale(100%); filter:grayscale(100%);" />
{% end %}
<div class="media-body">
{% if server['stats']['desc'] != 'False' %}
@ -143,7 +149,8 @@
<div class="row">
<div class="col-12">
<div id="m_server_motd_{{ server['stats']['server_id']['server_id'] }}">
<span class="text-warning"><i class="fas fa-exclamation-triangle"></i> Crafty can't get infos from this Server </span>
<span class="text-warning"><i class="fas fa-exclamation-triangle"></i> Crafty can't get infos from
this Server </span>
</div>
<div id="m_server_version_{{ server['stats']['server_id']['server_id'] }}"></div>
</div>

View File

@ -313,7 +313,8 @@
<button id="zip_submit" type="submit" title="You must select server root dir first" disabled
class="btn btn-primary mr-2">{{ translate('serverWizard', 'importServerButton', data['lang'])
}}</button>
<button type="reset" class="btn btn-danger mr-2">{{ translate('serverWizard', 'resetForm', data['lang'])
<button type="button" class="btn btn-danger mr-2 tree-reset">{{ translate('serverWizard', 'resetForm',
data['lang'])
}}</button>
</div>
</div>
@ -321,9 +322,154 @@
</p>
</div>
</div>
<div class="col-sm-6 grid-margin">
<img id="op_logo" style="filter: grayscale(10%); opacity: .1;" src="../../static/assets/images/logo_small.svg"
alt="Crafty logo" />
<div class="col-sm-6 grid-margin stretch-card">
<div class="card">
<div class="card-body">
<h4>{{ translate('serverWizard', 'uploadZip', data['lang']) }}</h4>
<br />
<p class="card-description">
<form name="zip" method="post" class="server-wizard" onSubmit="wait_msg(true)">
{% raw xsrf_form_html() %}
<input type="hidden" value="import_zip" name="create_type">
<div class="row">
<div class="col-sm-12">
<div class="col-sm-12">
<div class="form-group">
<label for="server_name">{{ translate('serverWizard', 'serverName', data['lang']) }}</label>
<input type="text" class="form-control" id="server_name" name="server_name" value=""
placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}" required>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label for="server">Server Upload </label><br>
<span id="upload_input">
<input type="file" multiple="false" class="form-control" id="file" name="file" required
style="width: 70%;">
<button type="button" class="btn btn-info" onclick="sendFile()">UPLOAD</button>
</span>
</div>
</div>
</div>
<div id="lower_half" style="visibility: hidden;">
<div class="col-sm-12">
<div class="form-group">
<label for="server">{{ translate('serverWizard', 'selectRoot', data['lang']) }} <small>{{
translate('serverWizard', 'explainRoot', data['lang']) }}</small></label>
<br>
<button class="btn btn-primary mr-2" id="root_upload_button" type="button">{{
translate('serverWizard', 'clickRoot', data['lang']) }}</button>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label for="server_jar">{{ translate('serverWizard', 'serverJar', data['lang']) }}</label>
<input type="text" class="form-control" id="server_jar" name="server_jar" value=""
placeholder="paper.jar" required>
</div>
</div>
<div class="col-sm-12">
<h4 class="card-title">{{ translate('serverWizard', 'quickSettings', data['lang']) }} <small
style="text-transform: none;"> - {{ translate('serverWizard', 'quickSettingsDescription',
data['lang']) }}</small></h4>
<hr>
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label for="port3">{{ translate('serverWizard', 'serverPort', data['lang']) }} <small> - {{
translate('serverWizard', 'defaultPort', data['lang']) }}</small></label>
<input type="number" class="form-control" id="port4" name="port" value="19132" step="1" min="1"
required>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<div id="accordion-3">
<div class="card">
<div class="card-header p-2" id="Role-3">
<p class="mb-0 p-0" data-toggle="collapse" data-target="#collapseRole-3"
aria-expanded="true" aria-controls="collapseRole-3">
<i class="fas fa-chevron-down"></i> {{ translate('serverWizard', 'addRole',
data['lang'])
}} <small style="text-transform: none;"> - {{ translate('serverWizard', 'autoCreate',
data['lang']) }}</small>
</p>
</div>
<div id="collapseRole-3" class="collapse" aria-labelledby="Role-3" data-parent="">
<div class="card-body scroll">
<div class="form-group">
{% for r in data['roles'] %}
<span class="d-block menu-option"><label><input name="{{ r['role_id'] }}"
type="checkbox">&nbsp;
{{ r['role_name'].capitalize() }}</label></span>
{% end %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-12" style="visibility: hidden;">
<div class="form-group">
<input type="text" class="form-control" id="zip_root_path" name="zip_root_path">
</div>
</div>
<div class="modal fade" id="dir_select" tabindex="-1" role="dialog" aria-labelledby="dir_select"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">{{ translate('serverWizard',
'selectZipDir', data['lang']) }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="tree-ctx-item" id="main-tree-div" data-path=""
style="overflow: scroll; max-height:75%;">
<input type="radio" id="main-tree-input-upload" 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>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{
translate('serverWizard', 'close', data['lang']) }}</button>
<button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary">{{
translate('serverWizard', 'save', data['lang']) }}</button>
</div>
</div>
</div>
</div>
</div>
</div>
<button id="upload_submit" type="submit" title="You must select server root dir first" disabled
class="btn btn-primary mr-2">{{ translate('serverWizard', 'importServerButton', data['lang'])
}}</button>
<button type="button" class="btn btn-danger mr-2 tree-reset">{{ translate('serverWizard', 'resetForm',
data['lang'])
}}</button>
</div>
</div>
</form>
</p>
</div>
</div>
</div>
</div>
</div>
@ -343,7 +489,7 @@
z-index: 200;
margin-top: 4px;
position: absolute;
background-color: #2a2c44;
background-color: var(--card-banner-bg);
}
.menu-option {
@ -412,6 +558,65 @@
{% block js%}
<script>
var file;
function sendFile() {
file = $("#file")[0].files[0]
document.getElementById("upload_input").innerHTML = '<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>'
let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf")
let fileName = file.name
let target = '/upload'
let mimeType = file.type
let size = file.size
let type = 'server_import'
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 {
alert('Upload failed with response: ' + event.target.responseText);
doUpload = false;
}
}, false);
xmlHttpRequest.addEventListener('error', (e) => {
console.error('Error while uploading file', file.name + '.', 'Event:', e)
}, false);
xmlHttpRequest.send(file);
}
document.getElementById("root_upload_button").addEventListener("click", function () {
if (file) {
if (document.getElementById('root_upload_button').classList.contains('clicked')) {
document.getElementById('main-tree-div').innerHTML = '<input type="radio" id="main-tree-input-upload" 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_upload_button').classList.add('clicked')
}
var token = getCookie("_xsrf");
var dialog = bootbox.dialog({
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
closeButton: false
});
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/unzip_server?id=-1&file=' + file.name,
});
} else {
bootbox.alert("You must input a path before selecting this button");
}
});
function eula_confirm() {
bootbox.confirm({
title: "{% raw translate('error', 'eulaTitle', data['lang']) %}",
@ -438,6 +643,9 @@
}
</script>
<script>
$(".tree-reset").on("click", function () {
location.href = "/server/bedrock_step1";
});
document.getElementById("root_files_button").addEventListener("click", function () {
if (document.forms["zip"]["server_path"].value != "") {
if (document.getElementById('root_files_button').classList.contains('clicked')) {
@ -488,7 +696,11 @@
}
function getTreeView(path) {
document.getElementById('zip_submit').disabled = false;
if (document.getElementById("lower_half").visibility == "hidden") {
document.getElementById('zip_submit').disabled = false;
} else {
document.getElementById('upload_submit').disabled = false;
}
path = path
$.ajax({
@ -582,8 +794,11 @@
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);
});

View File

@ -423,7 +423,8 @@
<button id="zip_submit" type="submit" title="You must select server root dir first" disabled
class="btn btn-primary mr-2">{{ translate('serverWizard', 'importServerButton', data['lang'])
}}</button>
<button type="reset" class="btn btn-danger mr-2">{{ translate('serverWizard', 'resetForm', data['lang'])
<button type="button" class="btn btn-danger mr-2 tree-reset">{{ translate('serverWizard', 'resetForm',
data['lang'])
}}</button>
</div>
</div>
@ -431,9 +432,172 @@
</p>
</div>
</div>
<div class="col-sm-6 grid-margin">
<img id="op_logo" style="filter: grayscale(10%); opacity: .1;" src="../../static/assets/images/logo_small.svg"
alt="Crafty logo" />
<div class="col-sm-6 grid-margin stretch-card">
<div class="card">
<div class="card-body">
<h4>{{ translate('serverWizard', 'uploadZip', data['lang']) }}</h4>
<br />
<p class="card-description">
<form name="zip" method="post" class="server-wizard" onSubmit="wait_msg(true)">
{% raw xsrf_form_html() %}
<input type="hidden" value="import_zip" name="create_type">
<div class="row">
<div class="col-sm-12">
<div class="col-sm-12">
<div class="form-group">
<label for="server_name">{{ translate('serverWizard', 'serverName', data['lang']) }}</label>
<input type="text" class="form-control" id="server_name" name="server_name" value=""
placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}" required>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label for="server">Server Upload </label><br>
<span id="upload_input">
<input type="file" multiple="false" class="form-control" id="file" name="file" required
style="width: 70%;">
<button type="button" class="btn btn-info" onclick="sendFile()">UPLOAD</button>
</span>
</div>
</div>
</div>
<div id="lower_half" style="visibility: hidden;">
<div class="col-sm-12">
<div class="form-group">
<label for="server">{{ translate('serverWizard', 'selectRoot', data['lang']) }} <small>{{
translate('serverWizard', 'explainRoot', data['lang']) }}</small></label>
<br>
<button class="btn btn-primary mr-2" id="root_upload_button" type="button">{{
translate('serverWizard', 'clickRoot', data['lang']) }}</button>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label for="server_jar">{{ translate('serverWizard', 'serverJar', data['lang']) }}</label>
<input type="text" class="form-control" id="server_jar" name="server_jar" value=""
placeholder="paper.jar" required>
</div>
</div>
<div class="col-sm-12">
<h4 class="card-title">{{ translate('serverWizard', 'quickSettings', data['lang']) }} <small
style="text-transform: none;"> - {{ translate('serverWizard', 'quickSettingsDescription',
data['lang']) }}</small></h4>
<hr>
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label for="min_memory3">{{ translate('serverWizard', 'minMem', data['lang']) }} <small> - {{
translate('serverWizard', 'sizeInGB', data['lang']) }}</small></label>
<input type="number" class="form-control" id="min_memory3" name="min_memory" value="1"
step="0.5" min="0.5" required>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label for="max_memory3">{{ translate('serverWizard', 'maxMem', data['lang']) }} <small> - {{
translate('serverWizard', 'sizeInGB', data['lang']) }}</small></label>
<input type="number" class="form-control" id="max_memory3" name="max_memory" value="2"
step="0.5" min="0.5" required>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label for="port3">{{ translate('serverWizard', 'serverPort', data['lang']) }} <small> - {{
translate('serverWizard', 'defaultPort', data['lang']) }}</small></label>
<input type="number" class="form-control" id="port3" name="port" value="25565" step="1" min="1"
required>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<div id="accordion-3">
<div class="card">
<div class="card-header p-2" id="Role-3">
<p class="mb-0 p-0" data-toggle="collapse" data-target="#collapseRole-3"
aria-expanded="true" aria-controls="collapseRole-3">
<i class="fas fa-chevron-down"></i> {{ translate('serverWizard', 'addRole',
data['lang'])
}} <small style="text-transform: none;"> - {{ translate('serverWizard', 'autoCreate',
data['lang']) }}</small>
</p>
</div>
<div id="collapseRole-3" class="collapse" aria-labelledby="Role-3" data-parent="">
<div class="card-body scroll">
<div class="form-group">
{% for r in data['roles'] %}
<span class="d-block menu-option"><label><input name="{{ r['role_id'] }}"
type="checkbox">&nbsp;
{{ r['role_name'].capitalize() }}</label></span>
{% end %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-12" style="visibility: hidden;">
<div class="form-group">
<input type="text" class="form-control" id="zip_root_path" name="zip_root_path">
</div>
</div>
<div class="modal fade" id="dir_select" tabindex="-1" role="dialog" aria-labelledby="dir_select"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">{{ translate('serverWizard',
'selectZipDir', data['lang']) }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="tree-ctx-item" id="main-tree-div" data-path=""
style="overflow: scroll; max-height:75%;">
<input type="radio" id="main-tree-input-upload" 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>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{
translate('serverWizard', 'close', data['lang']) }}</button>
<button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary">{{
translate('serverWizard', 'save', data['lang']) }}</button>
</div>
</div>
</div>
</div>
</div>
</div>
<button id="upload_submit" type="submit" title="You must select server root dir first" disabled
class="btn btn-primary mr-2">{{ translate('serverWizard', 'importServerButton', data['lang'])
}}</button>
<button type="button" class="btn btn-danger mr-2 tree-reset">{{ translate('serverWizard', 'resetForm',
data['lang'])
}}</button>
</div>
</div>
</form>
</p>
</div>
</div>
</div>
</div>
</div>
@ -457,7 +621,7 @@
z-index: 200;
margin-top: 4px;
position: absolute;
background-color: #2a2c44;
background-color: var(--card-banner-bg);
}
.menu-option {
@ -548,271 +712,449 @@
} else {
bootbox.alert("You must input a path before selecting this button");
}
});
.scroll {
max - height: 12em;
overflow - y: auto;
}
.menu - btn {
font - size: 0.9em;
padding: 2px 10px;
}
.menu {
padding - top: 10px;
z - index: 200;
margin - top: 4px;
position: absolute;
background - color: #2a2c44;
}
.menu - option {
padding: 6px 20px 6px;
color: white;
}
#overlay {
position: absolute;
top: 0px;
left: 0px;
width: 100 %;
height: 100 %;
z - index: 100;
}
</style >
<style>
/* Remove default bullets */
.tree-view,
.tree-nested {
list - style - type: none;
margin: 0;
padding: 0;
margin-left: 10px;
}
/* Style the items */
.tree-item,
.files-tree-title {
cursor: pointer;
user-select: none;
/* Prevent text selection */
}
/* Create the caret/arrow with a unicode, and style it */
.tree-caret .fa-folder {
display: inline-block;
}
.tree-caret .fa-folder-open {
display: none;
}
/* Rotate the caret/arrow icon when clicked on (using JavaScript) */
.tree-caret-down .fa-folder {
display: none;
}
.tree-caret-down .fa-folder-open {
display: inline-block;
}
/* Hide the nested list */
.tree-nested {
display: none;
}
#op_logo {
position: relative;
top: 50%;
transform: translateY(-50%);
}
</style>
{% end %}
{% block js %}
<script>
var file;
function sendFile() {
file = $("#file")[0].files[0]
document.getElementById("upload_input").innerHTML = '<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>'
let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf")
let fileName = file.name
let target = '/upload'
let mimeType = file.type
let size = file.size
let type = 'server_import'
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 {
alert('Upload failed with response: ' + event.target.responseText);
doUpload = false;
}
}, false);
xmlHttpRequest.addEventListener('error', (e) => {
console.error('Error while uploading file', file.name + '.', 'Event:', e)
}, false);
xmlHttpRequest.send(file);
}
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=' + path,
});
} else {
bootbox.alert("You must input a path before selecting this button");
}
});
document.getElementById("root_upload_button").addEventListener("click", function () {
if (file) {
if (document.getElementById('root_upload_button').classList.contains('clicked')) {
document.getElementById('main-tree-div').innerHTML = '<input type="radio" id="main-tree-input-upload" 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_upload_button').classList.add('clicked')
}
var token = getCookie("_xsrf");
var dialog = bootbox.dialog({
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
closeButton: false
});
$.ajax({
type: "POST",
headers: {'X-XSRFToken': token },
url: '/ajax/unzip_server?id=-1&file=' + file.name,
});
} else {
bootbox.alert("You must input a path before selecting this button");
}
});
</script>
<script>
function dropDown(event) {
event.target.parentElement.children[1].classList.remove("d-none");
document.getElementById("overlay").classList.remove("d-none");
}
$(".tree-reset").on("click", function () {
location.href = "/server/step1";
});
function dropDown(event) {
event.target.parentElement.children[1].classList.remove("d-none");
document.getElementById("overlay").classList.remove("d-none");
}
function hide(event) {
var items = document.getElementsByClassName('menu');
items.forEach(item => {
item.classList.add("d-none");
})
function hide(event) {
var items = document.getElementsByClassName('menu');
items.forEach(item => {
item.classList.add("d-none");
})
document.getElementById("overlay").classList.add("d-none");
}
document.getElementById("overlay").classList.add("d-none");
}
$(document).ready(function () {
console.log('ready');
var forms = $('form.server-wizard');
forms.each(function (i, formEl) {
var form = $(formEl);
$(document).ready(function () {
console.log('ready');
var forms = $('form.server-wizard');
forms.each(function (i, formEl) {
var form = $(formEl);
var min = form.find('[name=min_memory]');
var max = form.find('[name=max_memory]');
console.log(form, min, max)
min.change(function () {
check_sizes(max, min, 'min');
});
});
max.change(function () {
check_sizes(max, min, 'max');
});
});
});
});
function wait_msg(importing) {
bootbox.alert({
title: importing ? '{% raw translate("serverWizard", "importing", data["lang"]) %}' : '{% raw translate("serverWizard", "downloading", data["lang"]) %}',
message: '<i class="fas fa-cloud-download"></i> {% raw translate("serverWizard", "bePatient", data["lang"]) %}',
});
}
function show_file_tree() {
$("#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)
function wait_msg(importing) {
bootbox.alert({
title: importing ? '{% raw translate("serverWizard", "importing", data["lang"]) %}' : '{% raw translate("serverWizard", "downloading", data["lang"]) %}',
message: '<i class="fas fa-cloud-download"></i> {% raw translate("serverWizard", "bePatient", data["lang"]) %}',
});
}
if (max_mem < min_mem && changed === 'max') {
b.val(max_mem)
function show_file_tree() {
$("#dir_select").modal();
}
}
function getTreeView(path) {
document.getElementById('zip_submit').disabled = false;
path = path
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)
}
}
$.ajax({
type: "GET",
function getTreeView(path) {
//If this value is still hidden we know the user is executing a zip import and not an upload
if (document.getElementById("lower_half").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);
console.log(data);
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
try {
document.getElementById('main-tree-div').innerHTML += text;
document.getElementById('main-tree').parentElement.classList.add("clicked");
} catch {
document.getElementById('files-tree').innerHTML = text;
}
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-path', serverDir);
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-name', 'Files');
},
});
}
function getToggleMain(event) {
path = event.target.parentElement.getAttribute('data-path');
document.getElementById("files-tree").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
document.getElementById(path + "span").classList.toggle("tree-caret");
}
function getDirView(event) {
path = event.target.parentElement.getAttribute('data-path');
if (document.getElementById(path).classList.contains('clicked')) {
var toggler = document.getElementById(path + "span");
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
}
return;
} else {
$.ajax({
type: "GET",
url: '/ajax/get_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");
try {
document.getElementById('main-tree-div').innerHTML += text;
document.getElementById('main-tree').parentElement.classList.add("clicked");
} catch {
console.log("Bad")
document.getElementById('files-tree').innerHTML = text;
}
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");
});
}
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-path', serverDir);
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-name', 'Files');
},
});
}
}
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)
getTreeView(data.path);
show_file_tree();
}, 5000);
});
}
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 refreshCache() {
var token = getCookie("_xsrf")
document.getElementById("refresh-cache").classList.add("fa-spin")
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
function getDirView(event) {
path = event.target.parentElement.getAttribute('data-path');
if (document.getElementById(path).classList.contains('clicked')) {
var toggler = document.getElementById(path + "span");
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
}
return;
} else {
$.ajax({
type: "GET",
url: '/ajax/get_zip_dir?id=-1&path=' + path,
dataType: 'text',
success: function (data) {
console.log("got response:");
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
try {
document.getElementById(path + "span").classList.add('tree-caret-down');
document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked");
} catch {
console.log("Bad")
}
var toggler = document.getElementById(path);
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "span").addEventListener("click", function caretListener() {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
});
}
},
});
}
}
if (webSocket) {
webSocket.on('send_temp_path', function (data) {
setTimeout(function () {
var x = document.querySelector('.bootbox');
if (x) {
x.remove()
}
var x = document.querySelector('.modal-backdrop');
if (x) {
x.remove()
}
document.getElementById('main-tree-input').setAttribute('value', data.path)
document.getElementById('main-tree-input-upload').setAttribute('value', data.path)
getTreeView(data.path);
show_file_tree();
$("#root_files_button").attr("disabled", "disabled");
$("#root_upload_button").attr("disabled", "disabled");
}, 5000);
});
}
function refreshCache() {
var token = getCookie("_xsrf")
document.getElementById("refresh-cache").classList.add("fa-spin")
$.ajax({
type: "POST",
headers: {'X-XSRFToken': token },
url: '/ajax/jar_cache',
success: function () {
document.getElementById("refresh-cache").classList.remove("fa-sync");
document.getElementById("refresh-cache").classList.remove("fa-spin");
document.getElementById("refresh-cache").classList.add("fa-check");
document.getElementById("refresh-cache").classList.remove("fa-spin");
document.getElementById("refresh-cache").classList.add("fa-check");
setTimeout(function () {
location.reload();
}, 2000);
},
});
}
setTimeout(function () {
location.reload();
}, 2000);
},
});
}
</script>
<script type="text/javascript">
var text = '{% raw data["js_server_types"] %}';
var serverTypesLists = JSON.parse(text);
/* CountryChange() is called from the onchange event of a select element.
* param selectObj - the select object which fired the on change event.
*/
function serverTypeChange(selectObj) {
// get the index of the selected option
var idx = document.getElementById('server_type').selectedIndex;
// get the value of the selected option
var cSelect = document.getElementById("server");
try {
var which = document.getElementById('server_type').options[idx].value;
} catch {
var text = '{% raw data["js_server_types"] %}';
var serverTypesLists = JSON.parse(text);
/* CountryChange() is called from the onchange event of a select element.
* param selectObj - the select object which fired the on change event.
*/
function serverTypeChange(selectObj) {
// get the index of the selected option
var idx = document.getElementById('server_type').selectedIndex;
// get the value of the selected option
var cSelect = document.getElementById("server");
try {
var which = document.getElementById('server_type').options[idx].value;
} catch {
while (cSelect.options.length > 0) {
cSelect.remove(0);
}
return;
}
let server_type = which.split('|')[0];
let server = which.split('|')[1];
// use the selected option value to retrieve the list of items from the serverTypesLists array
let cList = serverTypesLists[server_type];
// get the country select element via its known id
var cSelect = document.getElementById("server");
// remove the current options from the country select
var len = cSelect.options.length;
while (cSelect.options.length > 0) {
cSelect.remove(0);
}
return;
}
let server_type = which.split('|')[0];
let server = which.split('|')[1];
// use the selected option value to retrieve the list of items from the serverTypesLists array
let cList = serverTypesLists[server_type];
// get the country select element via its known id
var cSelect = document.getElementById("server");
// remove the current options from the country select
var len = cSelect.options.length;
while (cSelect.options.length > 0) {
cSelect.remove(0);
}
var newOption;
// create new options ordered by ascending
cList[server].forEach(type => {
newOption = document.createElement("option");
var newOption;
// create new options ordered by ascending
cList[server].forEach(type => {
newOption = document.createElement("option");
newOption.value = which + "|" + type; // assumes option string and value are the same
newOption.text = type;
// add the new option
try {
cSelect.add(newOption); // this will fail in DOM browsers but is needed for IE
}
}
catch (e) {
cSelect.appendChild(newOption);
}
})
}
}
})
}
function serverJarChange(selectObj) {
let type_select = document.getElementById('server_jar')
let tidx = type_select.selectedIndex;
let val = type_select.options[tidx].value;
if (val == 'None') {
function serverJarChange(selectObj) {
let type_select = document.getElementById('server_jar')
let tidx = type_select.selectedIndex;
let val = type_select.options[tidx].value;
if (val == 'None') {
var jcSelect = document.getElementById("server_type");
while (jcSelect.options.length > 0) {
jcSelect.remove(0);
}
serverTypeChange(selectObj);
return;
}
// get the index of the selected option
var jidx = selectObj.selectedIndex;
// get the value of the selected option
var jwhich = selectObj.options[jidx].value;
// use the selected option value to retrieve the list of items from the serverTypesLists array
jcList = Object.keys(serverTypesLists[jwhich]);
// get the country select element via its known id
var jcSelect = document.getElementById("server_type");
// remove the current options from the country select
var jlen = jcSelect.options.length;
while (jcSelect.options.length > 0) {
jcSelect.remove(0);
}
serverTypeChange(selectObj);
return;
}
// get the index of the selected option
var jidx = selectObj.selectedIndex;
// get the value of the selected option
var jwhich = selectObj.options[jidx].value;
// use the selected option value to retrieve the list of items from the serverTypesLists array
jcList = Object.keys(serverTypesLists[jwhich]);
// get the country select element via its known id
var jcSelect = document.getElementById("server_type");
// remove the current options from the country select
var jlen = jcSelect.options.length;
while (jcSelect.options.length > 0) {
jcSelect.remove(0);
}
var jnewOption;
// create new options ordered by ascending
jcList.forEach(type => {
jnewOption = document.createElement("option");
var jnewOption;
// create new options ordered by ascending
jcList.forEach(type => {
jnewOption = document.createElement("option");
jnewOption.value = jwhich + "|" + type; // assumes option string and value are the same
jnewOption.text = type;
// add the new option
try {
jcSelect.add(jnewOption); // this will fail in DOM browsers but is needed for IE
}
}
catch (e) {
jcSelect.appendChild(jnewOption);
}
})
serverTypeChange(selectObj);
}
}
})
serverTypeChange(selectObj);
}
</script>
{% end %}

View File

@ -0,0 +1,19 @@
# Generated by database migrator
import peewee
def migrate(migrator, database, **kwargs):
migrator.add_columns(
"users",
pfp=peewee.CharField(default="/static/assets/images/faces-clipart/pic-3.png"),
)
"""
Write your migrations here.
"""
def rollback(migrator, database, **kwargs):
migrator.drop_columns("users", ["pfp"])
"""
Write your rollback migrations here.
"""

View File

@ -0,0 +1,16 @@
# Generated by database migrator
import peewee
def migrate(migrator, database, **kwargs):
migrator.add_columns("users", theme=peewee.CharField(default="default"))
"""
Write your migrations here.
"""
def rollback(migrator, database, **kwargs):
migrator.drop_columns("users", ["theme"])
"""
Write your rollback migrations here.
"""

View File

@ -457,7 +457,7 @@
"absoluteZipPath": "Absoluter Pfad zu dem Server",
"addRole": "Server zu existierender Rolle hinzufügen",
"autoCreate": "Wenn keine ausgewählt werden, wird Crafty eine erstellen!",
"bePatient": "Bitte haben Sie etwas Geduld, da wir ' + (importing ? 'import' : 'download')",
"bePatient": "Bitte haben Sie etwas Geduld, da wir ' + (importing ? 'import' : 'download') + '",
"buildServer": "Server erstellen!",
"clickRoot": "Hier klicken, um das Stammverzeichnis auszuwählen",
"close": "Schließen",

View File

@ -359,7 +359,8 @@
"schedule": "Schedule",
"serverDetails": "Server Details",
"terminal": "Terminal",
"metrics": "Metrics"
"metrics": "Metrics",
"reset": "Reset Scroll"
},
"serverFiles": {
"clickUpload": "Click here to select your files",
@ -501,6 +502,7 @@
"importServer": "Import an Existing Server",
"importServerButton": "Import Server!",
"importZip": "Import from a Zip File",
"uploadZip": "Upload Zip File For Server Import",
"maxMem": "Maximum Memory",
"minMem": "Minimum Memory",
"myNewServer": "My New Server",
@ -564,6 +566,7 @@
"roleName": "Role Name",
"super": "Super User",
"userLang": "User Language",
"userTheme": "UI Theme",
"userName": "User Name",
"userNameDesc": "What do you want to call this user?",
"userRoles": "User Roles",