From c65b4a267e3d30e8ae219c66875081afd2ee72e6 Mon Sep 17 00:00:00 2001 From: computergeek125 Date: Sat, 13 Mar 2021 11:07:04 -0600 Subject: [PATCH 1/3] Staff, patreon, and translations all proceedurally generate now --- app/classes/shared/helpers.py | 1 + app/classes/web/panel_handler.py | 9 +- .../assets/images/credits/isilverfyre.png | Bin 0 -> 25236 bytes .../static/assets/images/credits/qub3d.png | Bin 0 -> 31619 bytes .../assets/images/credits/silversthorn.png | Bin 0 -> 35186 bytes app/frontend/templates/panel/credits.html | 479 +++++------------- 6 files changed, 136 insertions(+), 353 deletions(-) create mode 100644 app/frontend/static/assets/images/credits/isilverfyre.png create mode 100644 app/frontend/static/assets/images/credits/qub3d.png create mode 100644 app/frontend/static/assets/images/credits/silversthorn.png diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index e010df81..4a1bc4ed 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -43,6 +43,7 @@ class Helpers: self.ensure_dir_exists(os.path.join(self.root_dir, 'app', 'config', 'db')) self.db_path = os.path.join(self.root_dir, 'app', 'config', 'db', 'crafty.sqlite') self.serverjar_cache = os.path.join(self.config_dir, 'serverjars.json') + self.credits_cache = os.path.join(self.config_dir, 'credits.json') self.passhasher = PasswordHasher() self.exiting = False diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py index 8241c7f3..66a266fd 100644 --- a/app/classes/web/panel_handler.py +++ b/app/classes/web/panel_handler.py @@ -61,6 +61,11 @@ class PanelHandler(BaseHandler): template = "public/error.html" elif page == 'credits': + with open(helper.credits_cache) as republic_credits_will_do: + credits = json.load(republic_credits_will_do) + page_data["patreons"] = credits["patreons"] + page_data["staff"] = credits["staff"] + page_data["translations"] = credits["translations"] template = "panel/credits.html" elif page == 'contribute': @@ -324,7 +329,7 @@ class PanelHandler(BaseHandler): user_data = json.loads(self.get_secure_cookie("user_data")) exec_user = db_helper.get_user(user_data['user_id']) - if not exec_user.superuser: + if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") return False elif server_id is None: @@ -569,4 +574,4 @@ class PanelHandler(BaseHandler): "Edited role {} (RID:{}) with servers {}".format(role_name, role_id, servers), server_id=0, source_ip=self.get_remote_ip()) - self.redirect("/panel/panel_config") \ No newline at end of file + self.redirect("/panel/panel_config") diff --git a/app/frontend/static/assets/images/credits/isilverfyre.png b/app/frontend/static/assets/images/credits/isilverfyre.png new file mode 100644 index 0000000000000000000000000000000000000000..75270fafe45bd8afc6448f87186124da9ad1fe57 GIT binary patch literal 25236 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:#F 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 - -
- -
-
-
- -
-
- -
- profile image -
- -
-
-

UltraBlack

-
- -
- -

Bavaria, DE

-
-
-
- -
-
- Staff - Idea Manager -
- -
- Hi, my name is Tim, and I'm a huge fan of linux. - I'm often gaming and occasionally coding. -
-
-
- -
-
-
- -
-
-
- -
-
- -
- profile image -
- -
-
-

Mac Geek

-
- -
- -

Eastern, USA

-
-
-
- -
-
- Staff - Developer - Support Manager -
- -
- His interests include all things programming, and Pokemon. - He enjoys a good tech tangent, gaming, and playing on his phone. -
-
-
- -
-
-
- -
-
-
- -
-
- -
- profile image -
- -
-
-

ThatOneLukas

-
- -
- -

Helsinki, FI

-
-
-
- -
-
- Staff - Developer -
- -
- His interests include programming, gaming, and electronics. - He likes gaming, programming, messing around with electronics, and time with his family. + {% if person['title'] %} + Crafty's {{ person['title'] }}

+ {% end %} + {{ person['blurb'] }}
@@ -415,9 +212,9 @@
+ {% end %}
- @@ -438,31 +235,22 @@ - + {% for pat in data["patreons"] %} - Richard B + {{ pat["name"] }} + {% if pat["level"] == "substainer" %} Substainer - - - - John C - + {% elif pat["level"] == "advocate" %} Advocate + {% elif pat["level"] == "supporter" %} + Supporter + {% else %} + Other + {% end %} - - Nicolas T - - Substainer - - - - Lightkeeper - - Substainer - - + {% end %} @@ -483,27 +271,16 @@ - + {% for person in data['translations'] %} - Ultrablack + {{ person }} - German + {% for language in data['translations'][person] %} + {{ language }} + {% end %} - - Manu - - French - - - - ptarrant - - Sarcasm - - - - + {% end %} From 12cc971c640302be3caa299f6646a6a28fb39f53 Mon Sep 17 00:00:00 2001 From: computergeek125 Date: Sun, 21 Mar 2021 23:00:19 -0500 Subject: [PATCH 2/3] Whoops forgot to add the credits --- app/config/credits.json | 137 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 app/config/credits.json diff --git a/app/config/credits.json b/app/config/credits.json new file mode 100644 index 00000000..7b47b31f --- /dev/null +++ b/app/config/credits.json @@ -0,0 +1,137 @@ +{ + "patreons": [ + { + "name": "Richard B", + "level": "substainer" + }, + { + "name": "John C", + "level": "advocate" + }, + { + "name": "Nicolas T", + "level": "substainer" + }, + { + "name": "Lightkeeper", + "level": "substainer" + }, + { + "name": "test user 1", + "level": "supporter" + }, + { + "name": "test user 2", + "level": "other" + } + ], + "staff": { + "development": [ + { + "name": "Phil Tarrant", + "title": "Benevolent Dictator for Life", + "loc": "Atlanta, GA", + "tags": [ "Staff", "Developer", [ "BDFL", "https://en.wikipedia.org/wiki/Benevolent_dictator_for_life" ] ], + "blurb": "For best results, apply a thin layer of Phillip to code, cyber security, parenthood for maximum effectiveness. Phillip often spends too much time with his chickens.", + "pic": "/static/assets/images/credits/ptarrant_cropped.png" + }, + { + "name": "Pita Bread", + "title": null, + "loc": "Midwest, USA", + "tags": [ "Staff", null, "Community Leader" ], + "blurb": "His interests include bread, Linux, and networking. He enjoys pumpkins, organizing, and long-winded emails, but hates wifi.", + "pic": "/static/assets/images/credits/pita_cropped.png" + }, + { + "name": "macgeek", + "title": null, + "loc": "Midwest, USA", + "tags": [ "Staff", "Developer", "Project Manager" ], + "blurb": "Sysadmin for work and sysadmin for fun (avid homelabber). He enjoys a good tech tangent and gaming.", + "pic": "/static/assets/images/credits/macgeek_cropped.png" + }, + { + "name": "parzivaldewey", + "title": null, + "loc": "East Coast, USA", + "tags": [ "Staff", "Developer", "Support Manager" ], + "blurb": "His interests include Linux, gaming, and helping others. When he's able to unplug he enjoys biking, hiking, and playing soccer.", + "pic": "/static/assets/images/credits/andrew_cropped.png" + }, + { + "name": "MC Gaming", + "title": null, + "loc": "Central, UK", + "tags": [ "Staff", "Developer", null ], + "blurb": "His interests include learning, Linux, programming. He loves pentesting apps and gaming.", + "pic": "/static/assets/images/credits/mcgaming.png" + }, + { + "name": "Silversthorn", + "title": null, + "loc": null, + "tags": [ "Staff", "Developer", null ], + "blurb": "Often in it's cave, he sometimes goes out to help or do silly jokes. He's an IT clown (not the film), but seriously do the job when it's needed.", + "pic": "/static/assets/images/credits/silversthorn.png" + }, + { + "name": "ThatOneLukas", + "title": null, + "loc": "Helsinki, FI", + "tags": [ "Staff", "Developer", null ], + "blurb": "Lukas enjoys bashing his head at the table while his code does not work", + "pic": "/static/assets/images/credits/lukas_cropped.png" + } + ], + "support": [ + { + "name": "iSilverfyre", + "title": null, + "loc": null, + "tags": [ "Staff", "Wiki", null ], + "blurb": "Silver enjoys helping others with their computer needs, writing documentation and loving her cat.", + "pic": "/static/assets/images/credits/isilverfyre.png" + }, + { + "name": "Quentin", + "title": null, + "loc": null, + "tags": [ "Staff", "Developer", null ], + "blurb": "Hosts Minecraft servers for his weird friends, works for an IoT company as his dayjob. The 's' in IoT stands for 'secure'.", + "pic": "/static/assets/images/credits/qub3d.png" + } + ], + "retired": [ + { + "name": "Kev Dagoat", + "title": "Head of Development", + "loc": "East Coast, AU", + "tags": [ "Staff", "Developer", "HOD" ], + "blurb": "His interests include Linux, programming, and goats of course. He enjoys building APIs, K8s, and Geeking over video cards.", + "pic": "/static/assets/images/credits/kevdagoat.jpeg" + }, + { + "name": "Manu", + "title": null, + "loc": "Eastern, CA", + "tags": [ "Staff", "Developer", "Project Manager" ], + "blurb": "His interests include learning, Linux, and programming. He enjoys speaking French, doing 6 things at once, and testing software.", + "pic": "/static/assets/images/credits/manu_cropped.png" + }, + { + "name": "UltraBlack", + "title": null, + "loc": "Bavaria, DE", + "tags": [ "Staff", null, "Idea Manager" ], + "blurb": "Hi, my name is Tim, and I'm a huge fan of linux. I'm often gaming and occasionally coding.", + "pic": "/static/assets/images/credits/ultrablack_cropped.png" + } + ] + }, + "translations": { + "UltraBlack": [ "German" ], + "Manu": [ "French" ], + "ptarrant": [ "Sarcasm", "Wit" ] + } +} \ No newline at end of file From 81c60d6dac48dfb9dab9f16a8cac11fd3e9c8f9b Mon Sep 17 00:00:00 2001 From: computergeek125 Date: Sun, 21 Mar 2021 23:02:18 -0500 Subject: [PATCH 3/3] One monster truck commit for rework and backups --- .gitignore | 1 + app/classes/minecraft/serverjars.py | 1 - app/classes/minecraft/stats.py | 12 +- app/classes/shared/cmd.py | 9 +- app/classes/shared/controller.py | 26 +- app/classes/shared/exceptions.py | 8 + app/classes/shared/helpers.py | 28 +- app/classes/shared/models.py | 240 ++++++++++++++--- app/classes/shared/server.py | 33 ++- app/classes/shared/tasks.py | 80 ++++-- app/classes/web/ajax_handler.py | 16 +- app/classes/web/api_handler.py | 13 +- app/classes/web/base_handler.py | 35 +++ app/classes/web/panel_handler.py | 224 ++++++++++++---- app/classes/web/server_handler.py | 24 +- app/classes/web/tornado.py | 23 +- app/classes/web/websocket_handler.py | 4 + app/frontend/templates/panel/credits.html | 6 + app/frontend/templates/panel/dashboard.html | 42 +-- .../templates/panel/parts/details_stats.html | 26 +- .../panel/server_admin_controls.html | 18 +- .../templates/panel/server_backup.html | 247 ++++++++++++++++++ .../templates/panel/server_config.html | 48 ++-- .../templates/panel/server_files.html | 34 +-- app/frontend/templates/panel/server_logs.html | 20 +- app/frontend/templates/panel/server_term.html | 26 +- main.py | 22 +- 27 files changed, 977 insertions(+), 289 deletions(-) create mode 100644 app/classes/shared/exceptions.py create mode 100644 app/frontend/templates/panel/server_backup.html diff --git a/.gitignore b/.gitignore index 7afd4ec3..b49824d0 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ venv.bak/ .idea/ servers/ +backups/ session.lock .header default.json diff --git a/app/classes/minecraft/serverjars.py b/app/classes/minecraft/serverjars.py index 9a1a2505..8c91cd96 100644 --- a/app/classes/minecraft/serverjars.py +++ b/app/classes/minecraft/serverjars.py @@ -9,7 +9,6 @@ from datetime import datetime from app.classes.shared.helpers import helper from app.classes.shared.console import console from app.classes.shared.models import Servers -# from app.classes.shared.controller import controller from app.classes.minecraft.server_props import ServerProps logger = logging.getLogger(__name__) diff --git a/app/classes/minecraft/stats.py b/app/classes/minecraft/stats.py index 2d4a4209..01e060a4 100644 --- a/app/classes/minecraft/stats.py +++ b/app/classes/minecraft/stats.py @@ -8,7 +8,6 @@ import datetime from app.classes.shared.helpers import helper from app.classes.minecraft.mc_ping import ping -from app.classes.shared.controller import controller from app.classes.shared.models import db_helper from app.classes.shared.models import Host_Stats, Server_Stats @@ -17,6 +16,9 @@ logger = logging.getLogger(__name__) class Stats: + def __init__(self, controller): + self.controller = controller + def get_node_stats(self): boot_time = datetime.datetime.fromtimestamp(psutil.boot_time()) data = {} @@ -184,7 +186,7 @@ class Stats: server_stats_list = [] server_stats = {} - servers = controller.servers_list + servers = self.controller.servers_list logger.info("Getting Stats for all servers...") @@ -210,7 +212,7 @@ class Stats: internal_ip = server_data.get('server_ip', "127.0.0.1") server_port = server_settings.get('server_port', "25565") - logger.debug("Pinging {} on port {}".format(internal_ip, server_port)) + logger.debug("Pinging server '{}' on {}:{}".format(s.get('server_name', "ID#{}".format(server_id)), internal_ip, server_port)) int_mc_ping = ping(internal_ip, int(server_port)) int_data = False @@ -287,6 +289,4 @@ class Stats: last_week = now.day - max_age Host_Stats.delete().where(Host_Stats.time < last_week).execute() - Server_Stats.delete().where(Server_Stats.created < last_week).execute() - -stats = Stats() + Server_Stats.delete().where(Server_Stats.created < last_week).execute() \ No newline at end of file diff --git a/app/classes/shared/cmd.py b/app/classes/shared/cmd.py index 06f29434..b7124f79 100644 --- a/app/classes/shared/cmd.py +++ b/app/classes/shared/cmd.py @@ -9,7 +9,6 @@ logger = logging.getLogger(__name__) from app.classes.shared.console import console from app.classes.shared.helpers import helper -from app.classes.shared.tasks import tasks_manager from app.classes.web.websocket_helper import websocket_helper try: @@ -21,7 +20,11 @@ except ModuleNotFoundError as e: sys.exit(1) -class MainPrompt(cmd.Cmd): +class MainPrompt(cmd.Cmd, object): + + def __init__(self, tasks_manager): + super().__init__() + self.tasks_manager = tasks_manager # overrides the default Prompt prompt = "Crafty Controller v{} > ".format(helper.get_version_string()) @@ -48,7 +51,7 @@ class MainPrompt(cmd.Cmd): self._clean_shutdown() console.info('Waiting for main thread to stop') while True: - if tasks_manager.get_main_thread_run_status(): + if self.tasks_manager.get_main_thread_run_status(): sys.exit(0) time.sleep(1) diff --git a/app/classes/shared/controller.py b/app/classes/shared/controller.py index cf849c40..35922477 100644 --- a/app/classes/shared/controller.py +++ b/app/classes/shared/controller.py @@ -15,6 +15,7 @@ from app.classes.shared.models import db_helper, Servers from app.classes.shared.server import Server from app.classes.minecraft.server_props import ServerProps from app.classes.minecraft.serverjars import server_jar_obj +from app.classes.minecraft.stats import Stats logger = logging.getLogger(__name__) @@ -23,6 +24,7 @@ class Controller: def __init__(self): self.servers_list = [] + self.stats = Stats(self) def check_server_loaded(self, server_id_to_check: int): @@ -72,7 +74,7 @@ class Controller: temp_server_dict = { 'server_id': s.get('server_id'), 'server_data_obj': s, - 'server_obj': Server(), + 'server_obj': Server(self.stats), 'server_settings': settings.props } @@ -94,7 +96,6 @@ class Controller: server_obj.reload_server_settings() def get_server_obj(self, server_id): - for s in self.servers_list: if int(s['server_id']) == int(server_id): return s['server_obj'] @@ -190,12 +191,14 @@ class Controller: def create_jar_server(self, server: str, version: str, name: str, min_mem: int, max_mem: int, port: int): server_id = helper.create_uuid() server_dir = os.path.join(helper.servers_dir, server_id) + backup_path = os.path.join(helper.backup_path, server_id) server_file = "{server}-{version}.jar".format(server=server, version=version) full_jar_path = os.path.join(server_dir, server_file) # make the dir - perhaps a UUID? helper.ensure_dir_exists(server_dir) + helper.ensure_dir_exists(backup_path) try: # do a eula.txt @@ -221,7 +224,7 @@ class Controller: # download the jar server_jar_obj.download_jar(server, version, full_jar_path) - new_id = self.register_server(name, server_id, server_dir, server_command, server_file, server_log_file, server_stop) + new_id = self.register_server(name, server_id, server_dir, backup_path, server_command, server_file, server_log_file, server_stop) return new_id @staticmethod @@ -242,8 +245,10 @@ class Controller: def import_jar_server(self, server_name: str, server_path: str, server_jar: str, min_mem: int, max_mem: int, port: int): server_id = helper.create_uuid() new_server_dir = os.path.join(helper.servers_dir, server_id) + backup_path = os.path.join(helper.backup_path, server_id) helper.ensure_dir_exists(new_server_dir) + helper.ensure_dir_exists(backup_path) dir_util.copy_tree(server_path, new_server_dir) full_jar_path = os.path.join(new_server_dir, server_jar) @@ -253,15 +258,18 @@ class Controller: server_log_file = "{}/logs/latest.log".format(new_server_dir) server_stop = "stop" - new_id = self.register_server(server_name, server_id, new_server_dir, server_command, server_jar, + new_id = self.register_server(server_name, server_id, new_server_dir, backup_path, server_command, server_jar, server_log_file, server_stop, port) return new_id def import_zip_server(self, server_name: str, zip_path: str, server_jar: str, min_mem: int, max_mem: int, port: int): server_id = helper.create_uuid() new_server_dir = os.path.join(helper.servers_dir, server_id) + backup_path = os.path.join(helper.backup_path, server_id) + if helper.check_file_perms(zip_path): helper.ensure_dir_exists(new_server_dir) + helper.ensure_dir_exists(backup_path) with zipfile.ZipFile(zip_path, 'r') as zip_ref: zip_ref.extractall(new_server_dir) else: @@ -275,11 +283,11 @@ class Controller: server_log_file = "{}/logs/latest.log".format(new_server_dir) server_stop = "stop" - new_id = self.register_server(server_name, server_id, new_server_dir, server_command, server_jar, + new_id = self.register_server(server_name, server_id, new_server_dir, backup_path, server_command, server_jar, server_log_file, server_stop, port) return new_id - def register_server(self, name: str, server_id: str, server_dir: str, server_command: str, server_file: str, server_log_file: str, server_stop: str, server_port=25565): + def register_server(self, name: str, server_id: str, server_dir: str, backup_path: str, server_command: str, server_file: str, server_log_file: str, server_stop: str, server_port=25565): # put data in the db new_id = Servers.insert({ Servers.server_name: name, @@ -292,7 +300,8 @@ class Controller: Servers.crash_detection: False, Servers.log_path: server_log_file, Servers.server_port: server_port, - Servers.stop_command: server_stop + Servers.stop_command: server_stop, + Servers.backup_path: backup_path }).execute() try: @@ -336,6 +345,3 @@ class Controller: self.servers_list.pop(counter) counter += 1 - - -controller = Controller() diff --git a/app/classes/shared/exceptions.py b/app/classes/shared/exceptions.py new file mode 100644 index 00000000..a5de82fa --- /dev/null +++ b/app/classes/shared/exceptions.py @@ -0,0 +1,8 @@ +class CraftyException(Exception): + pass + +class DatabaseException(CraftyException): + pass + +class SchemaError(DatabaseException): + pass \ No newline at end of file diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 4a1bc4ed..927c583e 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -10,6 +10,8 @@ import socket import random import logging import html +import zipfile +import pathlib from datetime import datetime from socket import gethostname @@ -36,6 +38,7 @@ class Helpers: self.config_dir = os.path.join(self.root_dir, 'app', 'config') self.webroot = os.path.join(self.root_dir, 'app', 'frontend') self.servers_dir = os.path.join(self.root_dir, 'servers') + self.backup_path = os.path.join(self.root_dir, 'backups') self.session_file = os.path.join(self.root_dir, 'app', 'config', 'session.lock') self.settings_file = os.path.join(self.root_dir, 'app', 'config', 'config.json') @@ -306,6 +309,8 @@ class Helpers: @staticmethod def check_path_exists(path: str): + if not path: + return False logger.debug('Looking for path: {}'.format(path)) if os.path.exists(path): @@ -372,6 +377,19 @@ class Helpers: total += entry.stat(follow_symlinks=False).st_size return total + @staticmethod + def list_dir_by_date(path: str, reverse=False): + return [str(p) for p in sorted(pathlib.Path(path).iterdir(), key=os.path.getmtime, reverse=reverse)] + + def get_human_readable_files_sizes(self, paths: list): + sizes = [] + for p in paths: + sizes.append({ + "path": p, + "size": self.human_readable_file_size(os.stat(p).st_size) + }) + return sizes + @staticmethod def base64_encode_string(string: str): s_bytes = str(string).encode('utf-8') @@ -521,7 +539,7 @@ class Helpers: @staticmethod def get_banned_players(server_id, db_helper): stats = db_helper.get_server_stats_by_id(server_id) - server_path = stats[0]['server_id']['path'] + server_path = stats['server_id']['path'] path = os.path.join(server_path, 'banned-players.json') try: @@ -534,5 +552,13 @@ class Helpers: return json.loads(content) + @staticmethod + def zip_directory(file, path, compression=zipfile.ZIP_LZMA): + with zipfile.ZipFile(file, 'w', compression) as zf: + for root, dirs, files in os.walk(path): + for file in files: + zf.write(os.path.join(root, file), + os.path.relpath(os.path.join(root, file), + os.path.join(path, '..'))) helper = Helpers() diff --git a/app/classes/shared/models.py b/app/classes/shared/models.py index 2ac6f602..634234c7 100644 --- a/app/classes/shared/models.py +++ b/app/classes/shared/models.py @@ -9,6 +9,8 @@ from app.classes.minecraft.server_props import ServerProps from app.classes.web.websocket_helper import websocket_helper logger = logging.getLogger(__name__) +peewee_logger = logging.getLogger('peewee') +peewee_logger.setLevel(logging.INFO) try: from peewee import * @@ -20,15 +22,30 @@ except ModuleNotFoundError as e: console.critical("Import Error: Unable to load {} module".format(e, e.name)) sys.exit(1) +schema_version = (0, 1, 0) # major, minor, patch semver + database = SqliteDatabase(helper.db_path, pragmas={ 'journal_mode': 'wal', 'cache_size': -1024 * 10}) - class BaseModel(Model): class Meta: database = database +class SchemaVersion(BaseModel): + # DO NOT EVER CHANGE THE SCHEMA OF THIS TABLE + # (unless we have a REALLY good reason to) + # There will only ever be one row, and it allows the database loader to detect + # what it needs to do on major version upgrades so you don't have to wipe the DB + # every time you upgrade + schema_major = IntegerField() + schema_minor = IntegerField() + schema_patch = IntegerField() + + class Meta: + table_name = 'schema_version' + primary_key = CompositeKey('schema_major', 'schema_minor', 'schema_patch') + class Users(BaseModel): user_id = AutoField() @@ -97,6 +114,7 @@ class Servers(BaseModel): server_uuid = CharField(default="", index=True) server_name = CharField(default="Server", index=True) path = CharField(default="") + backup_path = CharField(default="") executable = CharField(default="") log_path = CharField(default="") execution_command = CharField(default="") @@ -111,16 +129,6 @@ class Servers(BaseModel): class Meta: table_name = "servers" - -class User_Servers(BaseModel): - user_id = ForeignKeyField(Users, backref='user_server') - server_id = ForeignKeyField(Servers, backref='user_server') - - class Meta: - table_name = 'user_servers' - primary_key = CompositeKey('user_id', 'server_id') - - class Role_Servers(BaseModel): role_id = ForeignKeyField(Roles, backref='role_server') server_id = ForeignKeyField(Servers, backref='role_server') @@ -178,12 +186,25 @@ class Webhooks(BaseModel): class Meta: table_name = "webhooks" +class Schedules(BaseModel): + schedule_id = IntegerField(unique=True, primary_key=True) + server_id = ForeignKeyField(Servers, backref='schedule_server') + enabled = BooleanField() + action = CharField() + interval = IntegerField() + interval_type = CharField() + start_time = CharField(null=True) + command = CharField(null=True) + comment = CharField() + + class Meta: + table_name = 'schedules' class Backups(BaseModel): - directories = CharField() - storage_location = CharField() + directories = CharField(null=True) max_backups = IntegerField() - server_id = IntegerField(index=True) + server_id = ForeignKeyField(Servers, backref='backups_server') + schedule_id = ForeignKeyField(Schedules, backref='backups_schedule') class Meta: table_name = 'backups' @@ -202,17 +223,23 @@ class db_builder: Host_Stats, Webhooks, Servers, - User_Servers, Role_Servers, Server_Stats, Commands, - Audit_Log + Audit_Log, + SchemaVersion, + Schedules ]) @staticmethod def default_settings(): logger.info("Fresh Install Detected - Creating Default Settings") console.info("Fresh Install Detected - Creating Default Settings") + SchemaVersion.insert({ + SchemaVersion.schema_major: schema_version[0], + SchemaVersion.schema_minor: schema_version[1], + SchemaVersion.schema_patch: schema_version[2] + }).execute() default_data = helper.find_default_password() username = default_data.get("username", 'admin') @@ -239,9 +266,39 @@ class db_builder: return True pass + @staticmethod + def check_schema_version(): + svs = SchemaVersion.select().execute() + if len(svs) != 1: + raise exceptions.SchemaError("Multiple or no schema versions detected - potentially a failed upgrade?") + sv = svs[0] + svt = (sv.schema_major, sv.schema_minor, sv.schema_patch) + logger.debug("Schema: found {}, expected {}".format(svt, schema_version)) + console.debug("Schema: found {}, expected {}".format(svt, schema_version)) + if sv.schema_major > schema_version[0]: + raise exceptions.SchemaError("Major version mismatch - possible code reversion") + elif sv.schema_major < schema_version[0]: + db_shortcuts.upgrade_schema() + + if sv.schema_minor > schema_version[1]: + logger.warning("Schema minor mismatch detected: found {}, expected {}. Proceed with caution".format(svt, schema_version)) + console.warning("Schema minor mismatch detected: found {}, expected {}. Proceed with caution".format(svt, schema_version)) + elif sv.schema_minor < schema_version[1]: + db_shortcuts.upgrade_schema() + + if sv.schema_patch > schema_version[2]: + logger.info("Schema patch mismatch detected: found {}, expected {}. Proceed with caution".format(svt, schema_version)) + console.info("Schema patch mismatch detected: found {}, expected {}. Proceed with caution".format(svt, schema_version)) + elif sv.schema_patch < schema_version[2]: + db_shortcuts.upgrade_schema() + logger.info("Schema validation successful! {}".format(schema_version)) class db_shortcuts: + @staticmethod + def upgrade_schema(): + raise NotImplemented("I don't know who you are or how you reached this code, but this should NOT have happened. Please report it to the developer with due haste.") + @staticmethod def return_rows(query): rows = [] @@ -258,13 +315,12 @@ class db_shortcuts: @staticmethod def get_server_data_by_id(server_id): + query = Servers.select().where(Servers.server_id == server_id).limit(1) try: - query = Servers.get_by_id(server_id) - except DoesNotExist: + return db_helper.return_rows(query)[0] + except IndexError: return {} - return model_to_dict(query) - @staticmethod def get_all_defined_servers(): query = Servers.select() @@ -277,13 +333,13 @@ class db_shortcuts: for s in servers: latest = Server_Stats.select().where(Server_Stats.server_id == s.get('server_id')).order_by(Server_Stats.created.desc()).limit(1) - server_data.append({'server_data': s, "stats": db_helper.return_rows(latest)}) + server_data.append({'server_data': s, "stats": db_helper.return_rows(latest)[0]}) return server_data @staticmethod def get_server_stats_by_id(server_id): stats = Server_Stats.select().where(Server_Stats.server_id == server_id).order_by(Server_Stats.created.desc()).limit(1) - return db_helper.return_rows(stats) + return db_helper.return_rows(stats)[0] @staticmethod def server_id_exists(server_id): @@ -316,6 +372,8 @@ class db_shortcuts: @staticmethod def get_userid_by_name(username): + if username == "SYSTEM": + return 0 try: return (Users.get(Users.username == username)).user_id except DoesNotExist: @@ -323,6 +381,21 @@ class db_shortcuts: @staticmethod def get_user(user_id): + if user_id == 0: + return { + user_id: 0, + created: None, + last_login: None, + last_update: None, + last_ip: "127.27.23.89", + username: "SYSTEM", + password: None, + enabled: True, + superuser: False, + api_token: None, + roles: [], + servers: [] + } user = model_to_dict(Users.get(Users.user_id == user_id)) if user: @@ -331,13 +404,13 @@ class db_shortcuts: roles = set() for r in roles_query: roles.add(r.role_id.role_id) - servers_query = User_Servers.select().join(Servers, JOIN.INNER).where(User_Servers.user_id == user_id) - # TODO: this query needs to be narrower + #servers_query = User_Servers.select().join(Servers, JOIN.INNER).where(User_Servers.user_id == user_id) + ## TODO: this query needs to be narrower servers = set() - for s in servers_query: - servers.add(s.server_id.server_id) + #for s in servers_query: + # servers.add(s.server_id.server_id) user['roles'] = roles - user['servers'] = servers + #user['servers'] = servers logger.debug("user: ({}) {}".format(user_id, user)) return user else: @@ -377,10 +450,10 @@ class db_shortcuts: # TODO: This is horribly inefficient and we should be using bulk queries but im going for functionality at this point User_Roles.delete().where(User_Roles.user_id == user_id).where(User_Roles.role_id.in_(removed_roles)).execute() - for server in added_servers: - User_Servers.get_or_create(user_id=user_id, server_id=server) - # TODO: This is horribly inefficient and we should be using bulk queries but im going for functionality at this point - User_Servers.delete().where(User_Servers.user_id == user_id).where(User_Servers.server_id.in_(removed_servers)).execute() + #for server in added_servers: + # User_Servers.get_or_create(user_id=user_id, server_id=server) + # # TODO: This is horribly inefficient and we should be using bulk queries but im going for functionality at this point + #User_Servers.delete().where(User_Servers.user_id == user_id).where(User_Servers.server_id.in_(removed_servers)).execute() if up_data: Users.update(up_data).where(Users.user_id == user_id).execute() @@ -555,8 +628,111 @@ class db_shortcuts: Audit_Log.source_ip: source_ip }).execute() + @staticmethod + def create_scheduled_task(server_id, action, interval, interval_type, start_time, command, comment=None, enabled=True): + sch_id = Schedules.insert({ + Schedules.server_id: server_id, + Schedules.action: action, + Schedules.enabled: enabled, + Schedules.interval: interval, + Schedules.interval_type: interval_type, + Schedules.start_time: start_time, + Schedules.command: command, + Schedules.comment: comment + }).execute() + return sch_id + @staticmethod + def delete_scheduled_task(schedule_id): + sch = Schedules.get(Schedules.schedule_id == schedule_id) + return Schedules.delete_instance(sch) + @staticmethod + def update_scheduled_task(schedule_id, updates): + Schedules.update(updates).where(Schedules.schedule_id == schedule_id).execute() + + @staticmethod + def get_scheduled_task(schedule_id): + return model_to_dict(Schedules.get(Schedules.schedule_id == schedule_id)).execute() + + @staticmethod + def get_schedules_by_server(server_id): + return Schedules.select().where(Schedules.server_id == server_id).execute() + + @staticmethod + def get_schedules_all(): + return Schedules.select().execute() + + @staticmethod + def get_schedules_enabled(): + return Schedules.select().where(Schedules.enabled == True).execute() + + @staticmethod + def get_backup_config(server_id): + try: + row = Backups.select().where(Backups.server_id == server_id).join(Schedules).join(Servers)[0] + conf = { + "backup_path": row.server_id.backup_path, + "directories": row.directories, + "max_backups": row.max_backups, + "auto_enabled": row.schedule_id.enabled, + "server_id": row.server_id.server_id + } + except IndexError: + conf = { + "backup_path": None, + "directories": None, + "max_backups": 0, + "auto_enabled": True, + "server_id": server_id + } + return conf + + @staticmethod + def set_backup_config(server_id: int, backup_path: str = None, max_backups: int = None, auto_enabled: bool = True): + logger.debug("Updating server {} backup config with {}".format(server_id, locals())) + try: + row = Backups.select().where(Backups.server_id == server_id).join(Schedules).join(Servers)[0] + new_row = False + conf = {} + schd = {} + except IndexError: + conf = { + "directories": None, + "max_backups": 0, + "server_id": server_id + } + schd = { + "enabled": True, + "action": "backup_server", + "interval_type": "days", + "interval": 1, + "start_time": "00:00", + "server_id": server_id, + "comment": "Default backup job" + } + new_row = True + if max_backups is not None: + conf['max_backups'] = max_backups + schd['enabled'] = bool(auto_enabled) + if not new_row: + with database.atomic(): + if backup_path is not None: + u1 = Servers.update(backup_path=backup_path).where(Servers.server_id == server_id).execute() + else: + u1 = 0 + u2 = Backups.update(conf).where(Backups.server_id == server_id).execute() + u3 = Schedules.update(schd).where(Schedules.schedule_id == row.schedule_id).execute() + logger.debug("Updating existing backup record. {}+{}+{} rows affected".format(u1, u2, u3)) + else: + with database.atomic(): + conf["server_id"] = server_id + if backup_path is not None: + u = Servers.update(backup_path=backup_path).where(Servers.server_id == server_id) + s = Schedules.create(**schd) + conf['schedule_id'] = s.schedule_id + b = Backups.create(**conf) + logger.debug("Creating new backup record.") installer = db_builder() -db_helper = db_shortcuts() +db_helper = db_shortcuts() \ No newline at end of file diff --git a/app/classes/shared/server.py b/app/classes/shared/server.py index 6f5c27ec..6d592076 100644 --- a/app/classes/shared/server.py +++ b/app/classes/shared/server.py @@ -9,6 +9,7 @@ import datetime import threading import schedule import logging.config +import zipfile from app.classes.shared.helpers import helper @@ -29,7 +30,7 @@ except ModuleNotFoundError as e: class Server: - def __init__(self): + def __init__(self, stats): # holders for our process self.process = None self.line = False @@ -45,6 +46,7 @@ class Server: self.is_crashed = False self.restart_count = 0 self.crash_watcher_schedule = None + self.stats = stats def reload_server_settings(self): server_data = db_helper.get_server_data_by_id(self.server_id) @@ -108,7 +110,6 @@ class Server: helper.do_exit() def start_server(self): - from app.classes.minecraft.stats import stats # fail safe in case we try to start something already running if self.check_running(): @@ -160,7 +161,7 @@ class Server: logger.info("Server {} running with PID {}".format(self.name, self.PID)) console.info("Server {} running with PID {}".format(self.name, self.PID)) self.is_crashed = False - stats.record_stats() + self.stats.record_stats() else: logger.warning("Server PID {} died right after starting - is this a server config issue?".format(self.PID)) console.warning("Server PID {} died right after starting - is this a server config issue?".format(self.PID)) @@ -178,7 +179,6 @@ class Server: self.server_thread.join() def stop_server(self): - from app.classes.minecraft.stats import stats if self.settings['stop_command']: self.send_command(self.settings['stop_command']) @@ -212,7 +212,7 @@ class Server: # massive resetting of variables self.cleanup_server_object() - stats.record_stats() + self.stats.record_stats() def restart_threaded_server(self): @@ -337,3 +337,26 @@ class Server: logger.info("Removing old crash detection watcher thread") console.info("Removing old crash detection watcher thread") schedule.clear(self.name) + + def backup_server(self): + logger.info("Starting server {} (ID {}) backup".format(self.name, self.server_id)) + conf = db_helper.get_backup_config(self.server_id) + try: + backup_filename = "{}/{}.zip".format(conf['backup_path'], datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')) + logger.info("Creating backup of server '{}' (ID#{}) at '{}'".format(self.settings['server_name'], self.server_id, backup_filename)) + helper.zip_directory(backup_filename, self.server_path) + backup_list = self.list_backups() + if len(self.list_backups()) > conf["max_backups"]: + oldfile = backup_list[0] + logger.info("Removing old backup '{}'".format(oldfile)) + os.remove(oldfile) + except: + logger.exception("Failed to create backup of server {} (ID {})".format(self.name, self.server_id)) + + def list_backups(self): + conf = db_helper.get_backup_config(self.server_id) + if helper.check_path_exists(self.settings['backup_path']): + files = helper.get_human_readable_files_sizes(helper.list_dir_by_date(self.settings['backup_path'])) + return [{"path": os.path.relpath(f['path'], start=conf['backup_path']), "size": f["size"]} for f in files] + else: + return [] diff --git a/app/classes/shared/tasks.py b/app/classes/shared/tasks.py index f1a060c3..e8274b67 100644 --- a/app/classes/shared/tasks.py +++ b/app/classes/shared/tasks.py @@ -8,11 +8,9 @@ import asyncio from app.classes.shared.helpers import helper from app.classes.shared.console import console -from app.classes.web.tornado import webserver +from app.classes.web.tornado import Webserver from app.classes.web.websocket_helper import websocket_helper -from app.classes.minecraft.stats import stats -from app.classes.shared.controller import controller from app.classes.minecraft.serverjars import server_jar_obj from app.classes.shared.models import db_helper @@ -26,11 +24,26 @@ except ModuleNotFoundError as e: console.critical("Import Error: Unable to load {} module".format(e, e.name)) sys.exit(1) +scheduler_intervals = { 'seconds', + 'minutes', + 'hours', + 'days', + 'weeks', + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', + 'sunday' + } class TasksManager: - def __init__(self): - self.tornado = webserver() + def __init__(self, controller): + self.controller = controller + self.tornado = Webserver(controller, self) + self.webserver_thread = threading.Thread(target=self.tornado.run_tornado, daemon=True, name='tornado_thread') self.main_kill_switch_thread = threading.Thread(target=self.main_kill_switch, daemon=True, name="main_loop") @@ -39,13 +52,13 @@ class TasksManager: self.schedule_thread = threading.Thread(target=self.scheduler_thread, daemon=True, name="scheduler") self.log_watcher_thread = threading.Thread(target=self.log_watcher, daemon=True, name="log_watcher") - self.log_watcher_thread.start() self.command_thread = threading.Thread(target=self.command_watcher, daemon=True, name="command_watcher") - self.command_thread.start() self.realtime_thread = threading.Thread(target=self.realtime, daemon=True, name="realtime") - self.realtime_thread.start() + + self.reload_schedule_from_db() + def get_main_thread_run_status(self): return self.main_thread_exiting @@ -60,14 +73,29 @@ class TasksManager: self._main_graceful_exit() time.sleep(5) - @staticmethod - def command_watcher(): + def reload_schedule_from_db(self): + jobs = db_helper.get_schedules_enabled() + schedule.clear(tag='backup') + schedule.clear(tag='db') + for j in jobs: + if j.interval_type in scheduler_intervals: + logger.info("Loading schedule ID#{i}: '{a}' every {n} {t} at {s}".format( + i=j.schedule_id, a=j.action, n=j.interval, t=j.interval_type, s=j.start_time)) + try: + getattr(schedule.every(j.interval), j.interval_type).at(j.start_time).do( + db_helper.send_command, 0, j.server_id, "127.27.23.89", j.action) + except schedule.ScheduleValueError as e: + logger.critical("Scheduler value error occurred: {} on ID#{}".format(e, j.schedule_id)) + else: + logger.critical("Unknown schedule job type '{}' at id {}, skipping".format(j.interval_type, j.schedule_id)) + + def command_watcher(self): while True: # select any commands waiting to be processed commands = db_helper.get_unactioned_commands() for c in commands: - svr = controller.get_server_obj(c['server_id']['server_id']) + svr = self.controller.get_server_obj(c['server_id']['server_id']) command = c.get('command', None) if command == 'start_server': @@ -79,6 +107,9 @@ class TasksManager: elif command == "restart_server": svr.restart_threaded_server() + elif command == "backup_server": + svr.backup_server() + db_helper.mark_command_complete(c.get('command_id', None)) time.sleep(1) @@ -88,7 +119,7 @@ class TasksManager: os.remove(helper.session_file) os.remove(os.path.join(helper.root_dir, 'exit.txt')) os.remove(os.path.join(helper.root_dir, '.header')) - controller.stop_all_servers() + self.controller.stop_all_servers() except: logger.info("Caught error during shutdown", exc_info=True) @@ -113,6 +144,15 @@ class TasksManager: logger.info("Launching Scheduler Thread...") console.info("Launching Scheduler Thread...") self.schedule_thread.start() + logger.info("Launching command thread...") + console.info("Launching command thread...") + self.command_thread.start() + logger.info("Launching log watcher...") + console.info("Launching log watcher...") + self.log_watcher_thread.start() + logger.info("Launching realtime thread...") + console.info("Launching realtime thread...") + self.realtime_thread.start() @staticmethod def scheduler_thread(): @@ -120,17 +160,16 @@ class TasksManager: schedule.run_pending() time.sleep(1) - @staticmethod - def start_stats_recording(): + def start_stats_recording(self): stats_update_frequency = helper.get_setting('stats_update_frequency') logger.info("Stats collection frequency set to {stats} seconds".format(stats=stats_update_frequency)) console.info("Stats collection frequency set to {stats} seconds".format(stats=stats_update_frequency)) # one for now, - stats.record_stats() + self.controller.stats.record_stats() # one for later - schedule.every(stats_update_frequency).seconds.do(stats.record_stats) + schedule.every(stats_update_frequency).seconds.do(self.controller.stats.record_stats).tag('stats-recording') @staticmethod def serverjar_cache_refresher(): @@ -138,7 +177,7 @@ class TasksManager: server_jar_obj.refresh_cache() logger.info("Scheduling Serverjars.com cache refresh service every 12 hours") - schedule.every(12).hours.do(server_jar_obj.refresh_cache) + schedule.every(12).hours.do(server_jar_obj.refresh_cache).tag('serverjars') @staticmethod def realtime(): @@ -174,9 +213,4 @@ class TasksManager: def log_watcher(self): console.debug('in log_watcher') helper.check_for_old_logs(db_helper) - schedule.every(6).hours.do(lambda: helper.check_for_old_logs(db_helper)) - - - - -tasks_manager = TasksManager() + schedule.every(6).hours.do(lambda: helper.check_for_old_logs(db_helper)).tag('log-mgmt') \ No newline at end of file diff --git a/app/classes/web/ajax_handler.py b/app/classes/web/ajax_handler.py index af09a8b8..59712bac 100644 --- a/app/classes/web/ajax_handler.py +++ b/app/classes/web/ajax_handler.py @@ -9,7 +9,6 @@ import shutil from app.classes.shared.console import console from app.classes.shared.models import Users, installer from app.classes.web.base_handler import BaseHandler -from app.classes.shared.controller import controller from app.classes.shared.models import db_helper from app.classes.shared.helpers import helper @@ -56,8 +55,8 @@ class AjaxHandler(BaseHandler): logger.warning("Server Data not found in server_log ajax call") self.redirect("/panel/error?error=Server ID Not Found") - if server_data['log_path']: - logger.warning("Server ID not found in server_log ajax call ({})".format(server_id)) + if not server_data['log_path']: + logger.warning("Log path not found in server_log ajax call ({})".format(server_id)) if full_log: log_lines = helper.get_setting('max_log_lines') @@ -149,7 +148,7 @@ class AjaxHandler(BaseHandler): logger.warning("Server ID not found in send_command ajax call") console.warning("Server ID not found in send_command ajax call") - srv_obj = controller.get_server_obj(server_id) + srv_obj = self.controller.get_server_obj(server_id) if command: if srv_obj.check_running(): @@ -219,7 +218,6 @@ class AjaxHandler(BaseHandler): if page == "del_file": file_path = self.get_body_argument('file_path', default=None, strip=True) server_id = self.get_argument('id', None) - print(server_id) if server_id is None: logger.warning("Server ID not found in del_file ajax call") @@ -234,7 +232,9 @@ class AjaxHandler(BaseHandler): console.warning("Server ID not found in del_file ajax call ({})".format(server_id)) return False - if not helper.in_path(db_helper.get_server_data_by_id(server_id)['path'], file_path) \ + server_info = db_helper.get_server_data_by_id(server_id) + if not helper.in_path(server_info['path'], file_path) \ + or not helper.in_path(server_info['backup_path'], file_path) \ or not helper.check_file_exists(os.path.abspath(file_path)): logger.warning("Invalid path in del_file ajax call ({})".format(file_path)) console.warning("Invalid path in del_file ajax call ({})".format(file_path)) @@ -261,7 +261,9 @@ class AjaxHandler(BaseHandler): console.warning("Server ID not found in del_file ajax call ({})".format(server_id)) return False - if not helper.in_path(db_helper.get_server_data_by_id(server_id)['path'], dir_path) \ + server_info = db_helper.get_server_data_by_id(server_id) + if not helper.in_path(server_info['path'], dir_path) \ + or not helper.in_path(server_info['backup_path'], dir_path) \ or not helper.check_path_exists(os.path.abspath(dir_path)): logger.warning("Invalid path in del_file ajax call ({})".format(dir_path)) console.warning("Invalid path in del_file ajax call ({})".format(dir_path)) diff --git a/app/classes/web/api_handler.py b/app/classes/web/api_handler.py index cb87a8ea..f0e57e07 100644 --- a/app/classes/web/api_handler.py +++ b/app/classes/web/api_handler.py @@ -5,13 +5,13 @@ import tornado.web import tornado.escape import logging +from app.classes.web.base_handler import BaseHandler from app.classes.shared.models import Users -from app.classes.minecraft.stats import stats log = logging.getLogger(__name__) -class BaseHandler(tornado.web.RequestHandler): +class ApiHandler(BaseHandler): def return_response(self, data: dict): # Define a standardized response @@ -25,6 +25,7 @@ class BaseHandler(tornado.web.RequestHandler): def authenticate_user(self): try: log.debug("Searching for specified token") + # TODO: YEET THIS user_data = Users.get(api_token=self.get_argument('token')) log.debug("Checking results") if user_data: @@ -40,19 +41,19 @@ class BaseHandler(tornado.web.RequestHandler): pass -class ServersStats(BaseHandler): +class ServersStats(ApiHandler): def get(self): """Get details about all servers""" self.authenticate_user() # Get server stats - self.finish(self.write({"servers": stats.get_servers_stats()})) + self.finish(self.write({"servers": self.controller.stats.get_servers_stats()})) -class NodeStats(BaseHandler): +class NodeStats(ApiHandler): def get(self): """Get stats for particular node""" self.authenticate_user() # Get node stats - node_stats = stats.get_node_stats() + node_stats = self.controller.stats.get_node_stats() node_stats.pop("servers") self.finish(self.write(node_stats)) diff --git a/app/classes/web/base_handler.py b/app/classes/web/base_handler.py index c8956894..7fe05c1b 100644 --- a/app/classes/web/base_handler.py +++ b/app/classes/web/base_handler.py @@ -1,11 +1,21 @@ import logging import tornado.web +import bleach +from typing import ( + Union, + List, + Optional +) logger = logging.getLogger(__name__) class BaseHandler(tornado.web.RequestHandler): + def initialize(self, controller=None, tasks_manager=None): + self.controller = controller + self.tasks_manager = tasks_manager + def get_remote_ip(self): remote_ip = self.request.headers.get("X-Real-IP") or \ self.request.headers.get("X-Forwarded-For") or \ @@ -14,3 +24,28 @@ class BaseHandler(tornado.web.RequestHandler): def get_current_user(self): return self.get_secure_cookie("user", max_age_days=1) + + def autobleach(self, text): + if type(text) is bool: + return text + else: + return text + + def get_argument( + self, + name: str, + default: Union[None, str, tornado.web._ArgDefaultMarker] = tornado.web._ARG_DEFAULT, + strip: bool = True, + ) -> Optional[str]: + arg = self._get_argument(name, default, self.request.arguments, strip) + logger.debug("Bleaching {}: {}".format(name, arg)) + return bleach.clean(arg) + + def get_arguments(self, name: str, strip: bool = True) -> List[str]: + assert isinstance(strip, bool) + args = self._get_arguments(name, self.request.arguments, strip) + args_ret = [] + for arg in args: + logger.debug("Bleaching {}: {}".format(name, arg)) + args_ret += bleach.clean(arg) + return args_ret diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py index 66a266fd..2ee22345 100644 --- a/app/classes/web/panel_handler.py +++ b/app/classes/web/panel_handler.py @@ -5,14 +5,13 @@ import tornado.escape import bleach import time import datetime +import os from app.classes.shared.console import console from app.classes.shared.models import Users, installer from app.classes.web.base_handler import BaseHandler -from app.classes.shared.controller import controller from app.classes.shared.models import db_helper, Servers from app.classes.shared.helpers import helper -from app.classes.minecraft.stats import stats logger = logging.getLogger(__name__) @@ -29,7 +28,7 @@ class PanelHandler(BaseHandler): now = time.time() formatted_time = str(datetime.datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')) - defined_servers = controller.list_defined_servers() + defined_servers = self.controller.list_defined_servers() page_data = { # todo: make this actually pull and compare version data @@ -38,8 +37,8 @@ class PanelHandler(BaseHandler): 'user_data': user_data, 'server_stats': { 'total': len(defined_servers), - 'running': len(controller.list_running_servers()), - 'stopped': (len(controller.list_defined_servers()) - len(controller.list_running_servers())) + 'running': len(self.controller.list_running_servers()), + 'stopped': (len(self.controller.list_defined_servers()) - len(self.controller.list_running_servers())) }, 'menu_servers': defined_servers, 'hosts_data': db_helper.get_latest_hosts_stats(), @@ -52,7 +51,7 @@ class PanelHandler(BaseHandler): if page_data['server_stats']['total'] == 0 and page != "error": self.set_status(301) self.redirect("/server/step1") - return False + return if page == 'unauthorized': template = "panel/denied.html" @@ -81,13 +80,12 @@ class PanelHandler(BaseHandler): server_id, self.get_remote_ip()) - controller.remove_server(server_id) + self.controller.remove_server(server_id) self.redirect("/panel/dashboard") return elif page == 'dashboard': page_data['servers'] = db_helper.get_all_servers_stats() - for s in page_data['servers']: try: data = json.loads(s['int_ping_results']) @@ -103,26 +101,30 @@ class PanelHandler(BaseHandler): if server_id is None: self.redirect("/panel/error?error=Invalid Server ID") - return False + return else: server_id = bleach.clean(server_id) # does this server id exist? if not db_helper.server_id_exists(server_id): self.redirect("/panel/error?error=Invalid Server ID") - return False + return - valid_subpages = ['term', 'logs', 'config', 'files', 'admin_controls'] + valid_subpages = ['term', 'logs', 'backup', 'config', 'files', 'admin_controls'] if subpage not in valid_subpages: logger.debug('not a valid subpage') subpage = 'term' logger.debug('Subpage: "{}"'.format(subpage)) + server = self.controller.get_server_obj(server_id) # server_data isn't needed since the server_stats also pulls server data - # page_data['server_data'] = db_helper.get_server_data_by_id(server_id) + page_data['server_data'] = db_helper.get_server_data_by_id(server_id) page_data['server_stats'] = db_helper.get_server_stats_by_id(server_id) - page_data['get_players'] = lambda: stats.get_server_players(server_id) + page_data['get_players'] = lambda: self.controller.stats.get_server_players(server_id) + if subpage == "backup": + page_data['backup_config'] = db_helper.get_backup_config(server_id) + page_data['backup_list'] = server.list_backups() def get_banned_players_html(): banned_players = helper.get_banned_players(server_id, db_helper) @@ -149,6 +151,79 @@ class PanelHandler(BaseHandler): # template = "panel/server_details.html" template = "panel/server_{subpage}.html".format(subpage=subpage) + elif page == 'download_backup': + server_id = self.get_argument('id', None) + file = self.get_argument('file', "") + + if server_id is None: + self.redirect("/panel/error?error=Invalid Server ID") + return + else: + server_id = bleach.clean(server_id) + + # does this server id exist? + if not db_helper.server_id_exists(server_id): + self.redirect("/panel/error?error=Invalid Server ID") + return + + exec_user = db_helper.get_user(user_data['user_id']) + + if not exec_user['superuser']: + self.redirect("/panel/error?error=Unauthorized access: not superuser") + return + + server_info = db_helper.get_server_data_by_id(server_id) + backup_file = os.path.abspath(os.path.join(server_info["backup_path"], file)) + if not helper.in_path(server_info["backup_path"], backup_file) \ + or not os.path.isfile(backup_file): + self.redirect("/panel/error?error=Invalid path detected") + return + + self.set_header('Content-Type', 'application/octet-stream') + self.set_header('Content-Disposition', 'attachment; filename=' + file) + chunk_size = 1024 * 1024 * 4 # 4 MiB + + with open(backup_file, 'rb') as f: + while True: + chunk = f.read(chunk_size) + if not chunk: + break + try: + self.write(chunk) # write the chunk to response + self.flush() # send the chunk to client + except iostream.StreamClosedError: + # this means the client has closed the connection + # so break the loop + break + finally: + # deleting the chunk is very important because + # if many clients are downloading files at the + # same time, the chunks in memory will keep + # increasing and will eat up the RAM + del chunk + self.redirect("/panel/server_detail?id={}&subpage=backup".format(server_id)) + + elif page == 'backup_now': + server_id = self.get_argument('id', None) + + if server_id is None: + self.redirect("/panel/error?error=Invalid Server ID") + return + else: + # does this server id exist? + if not db_helper.server_id_exists(server_id): + self.redirect("/panel/error?error=Invalid Server ID") + return + + exec_user = db_helper.get_user(user_data['user_id']) + + if not exec_user['superuser']: + self.redirect("/panel/error?error=Unauthorized access: not superuser") + return + + server = self.controller.get_server_obj(server_id).backup_server() + self.redirect("/panel/server_detail?id={}&subpage=backup".format(server_id)) + elif page == 'panel_config': page_data['users'] = db_helper.get_all_users() page_data['roles'] = db_helper.get_all_roles() @@ -177,10 +252,10 @@ class PanelHandler(BaseHandler): if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return page_data['roles_all'] = db_helper.get_all_roles() - page_data['servers_all'] = controller.list_defined_servers() + page_data['servers_all'] = self.controller.list_defined_servers() template = "panel/panel_edit_user.html" elif page == "edit_user": @@ -188,16 +263,16 @@ class PanelHandler(BaseHandler): user_id = self.get_argument('id', None) page_data['user'] = db_helper.get_user(user_id) page_data['roles_all'] = db_helper.get_all_roles() - page_data['servers_all'] = controller.list_defined_servers() + page_data['servers_all'] = self.controller.list_defined_servers() exec_user = db_helper.get_user(user_data['user_id']) if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return elif user_id is None: self.redirect("/panel/error?error=Invalid User ID") - return False + return if exec_user['user_id'] != page_data['user']['user_id']: page_data['user']['api_token'] = "********" @@ -211,19 +286,19 @@ class PanelHandler(BaseHandler): if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return elif user_id is None: self.redirect("/panel/error?error=Invalid User ID") - return False + return else: # does this user id exist? target_user = db_helper.get_user(user_id) if not target_user: self.redirect("/panel/error?error=Invalid User ID") - return False + return elif target_user['superuser']: self.redirect("/panel/error?error=Cannot remove a superuser") - return False + return db_helper.remove_user(user_id) @@ -246,25 +321,25 @@ class PanelHandler(BaseHandler): if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return - page_data['servers_all'] = controller.list_defined_servers() + page_data['servers_all'] = self.controller.list_defined_servers() template = "panel/panel_edit_role.html" elif page == "edit_role": page_data['new_role'] = False role_id = self.get_argument('id', None) page_data['role'] = db_helper.get_role(role_id) - page_data['servers_all'] = controller.list_defined_servers() + page_data['servers_all'] = self.controller.list_defined_servers() exec_user = db_helper.get_user(user_data['user_id']) if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return elif role_id is None: self.redirect("/panel/error?error=Invalid Role ID") - return False + return template = "panel/panel_edit_role.html" @@ -276,16 +351,16 @@ class PanelHandler(BaseHandler): if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return elif role_id is None: self.redirect("/panel/error?error=Invalid Role ID") - return False + return else: # does this user id exist? target_role = db_helper.get_user(role_id) if not target_role: self.redirect("/panel/error?error=Invalid Role ID") - return False + return db_helper.remove_role(role_id) @@ -331,17 +406,15 @@ class PanelHandler(BaseHandler): if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return elif server_id is None: self.redirect("/panel/error?error=Invalid Server ID") - return False + return else: - server_id = bleach.clean(server_id) - # does this server id exist? if not db_helper.server_id_exists(server_id): self.redirect("/panel/error?error=Invalid Server ID") - return False + return Servers.update({ Servers.server_name: server_name, @@ -358,7 +431,7 @@ class PanelHandler(BaseHandler): Servers.logs_delete_after: logs_delete_after, }).where(Servers.server_id == server_id).execute() - controller.refresh_server_settings(server_id) + self.controller.refresh_server_settings(server_id) db_helper.add_to_audit_log(user_data['user_id'], "Edited server {} named {}".format(server_id, server_name), @@ -367,6 +440,41 @@ class PanelHandler(BaseHandler): self.redirect("/panel/server_detail?id={}&subpage=config".format(server_id)) + if page == "server_backup": + logger.debug(self.request.arguments) + server_id = self.get_argument('id', None) + backup_path = bleach.clean(self.get_argument('backup_path', None)) + max_backups = bleach.clean(self.get_argument('max_backups', None)) + enabled = int(float(bleach.clean(self.get_argument('auto_enabled'), '0'))) + + user_data = json.loads(self.get_secure_cookie("user_data")) + exec_user = db_helper.get_user(user_data['user_id']) + + if not exec_user['superuser']: + self.redirect("/panel/error?error=Unauthorized access: not superuser") + return + elif server_id is None: + self.redirect("/panel/error?error=Invalid Server ID") + return + else: + # does this server id exist? + if not db_helper.server_id_exists(server_id): + self.redirect("/panel/error?error=Invalid Server ID") + return + + if backup_path is not None: + Servers.update({ + Servers.backup_path: backup_path + }).where(Servers.server_id == server_id).execute() + db_helper.set_backup_config(server_id, max_backups=max_backups) + + db_helper.add_to_audit_log(user_data['user_id'], + "Edited server {}: updated backups".format(server_id), + server_id, + self.get_remote_ip()) + self.tasks_manager.reload_schedule_from_db() + self.redirect("/panel/server_detail?id={}&subpage=backup".format(server_id)) + elif page == "edit_user": user_id = bleach.clean(self.get_argument('id', None)) username = bleach.clean(self.get_argument('username', None)) @@ -380,22 +488,22 @@ class PanelHandler(BaseHandler): if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return elif username is None or username == "": self.redirect("/panel/error?error=Invalid username") - return False + return elif user_id is None: self.redirect("/panel/error?error=Invalid User ID") - return False + return else: # does this user id exist? if not db_helper.user_id_exists(user_id): self.redirect("/panel/error?error=Invalid User ID") - return False + return if password0 != password1: self.redirect("/panel/error?error=Passwords must match") - return False + return roles = set() for role in db_helper.get_all_roles(): @@ -408,7 +516,7 @@ class PanelHandler(BaseHandler): roles.add(role.role_id) servers = set() - for server in controller.list_defined_servers(): + for server in self.controller.list_defined_servers(): argument = int(float( bleach.clean( self.get_argument('server_{}_access'.format(server['server_id']), '0') @@ -444,19 +552,19 @@ class PanelHandler(BaseHandler): exec_user = db_helper.get_user(user_data['user_id']) if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return elif username is None or username == "": self.redirect("/panel/error?error=Invalid username") - return False + return else: # does this user id exist? if db_helper.get_userid_by_name(username) is not None: self.redirect("/panel/error?error=User exists") - return False + return if password0 != password1: self.redirect("/panel/error?error=Passwords must match") - return False + return roles = set() for role in db_helper.get_all_roles(): @@ -469,7 +577,7 @@ class PanelHandler(BaseHandler): roles.add(role['role_id']) servers = set() - for server in controller.list_defined_servers(): + for server in self.controller.list_defined_servers(): argument = int(float( bleach.clean( self.get_argument('server_{}_access'.format(server['server_id']), '0') @@ -500,21 +608,21 @@ class PanelHandler(BaseHandler): if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return elif role_name is None or role_name == "": self.redirect("/panel/error?error=Invalid username") - return False + return elif role_id is None: self.redirect("/panel/error?error=Invalid Role ID") - return False + return else: # does this user id exist? if not db_helper.role_id_exists(role_id): self.redirect("/panel/error?error=Invalid Role ID") - return False + return servers = set() - for server in controller.list_defined_servers(): + for server in self.controller.list_defined_servers(): argument = int(float( bleach.clean( self.get_argument('server_{}_access'.format(server['server_id']), '0') @@ -543,18 +651,18 @@ class PanelHandler(BaseHandler): exec_user = db_helper.get_user(user_data['user_id']) if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return elif role_name is None or role_name == "": self.redirect("/panel/error?error=Invalid role name") - return False + return else: # does this user id exist? if db_helper.get_roleid_by_name(role_name) is not None: self.redirect("/panel/error?error=Role exists") - return False + return servers = set() - for server in controller.list_defined_servers(): + for server in self.controller.list_defined_servers(): argument = int(float( bleach.clean( self.get_argument('server_{}_access'.format(server['server_id']), '0') @@ -575,3 +683,7 @@ class PanelHandler(BaseHandler): server_id=0, source_ip=self.get_remote_ip()) self.redirect("/panel/panel_config") + + else: + self.set_status(404) + self.render("public/404.html") \ No newline at end of file diff --git a/app/classes/web/server_handler.py b/app/classes/web/server_handler.py index 381c0827..73bf17f4 100644 --- a/app/classes/web/server_handler.py +++ b/app/classes/web/server_handler.py @@ -6,10 +6,8 @@ import shutil from app.classes.shared.console import console from app.classes.web.base_handler import BaseHandler -from app.classes.shared.controller import controller from app.classes.shared.models import db_helper, Servers from app.classes.minecraft.serverjars import server_jar_obj -from app.classes.minecraft.stats import stats from app.classes.shared.helpers import helper @@ -35,15 +33,15 @@ class ServerHandler(BaseHandler): template = "public/404.html" - defined_servers = controller.list_defined_servers() + defined_servers = self.controller.list_defined_servers() page_data = { 'version_data': helper.get_version_string(), 'user_data': user_data, 'server_stats': { - 'total': len(controller.list_defined_servers()), - 'running': len(controller.list_running_servers()), - 'stopped': (len(controller.list_defined_servers()) - len(controller.list_running_servers())) + 'total': len(self.controller.list_defined_servers()), + 'running': len(self.controller.list_running_servers()), + 'stopped': (len(self.controller.list_defined_servers()) - len(self.controller.list_running_servers())) }, 'hosts_data': db_helper.get_latest_hosts_stats(), 'menu_servers': defined_servers, @@ -130,7 +128,7 @@ class ServerHandler(BaseHandler): Servers.stop_command: stop_command }).execute() - controller.init_all_servers() + self.controller.init_all_servers() console.debug('initted all servers') return @@ -150,26 +148,26 @@ class ServerHandler(BaseHandler): server_parts = server.split("|") if import_type == 'import_jar': - good_path = controller.verify_jar_server(import_server_path, import_server_jar) + good_path = self.controller.verify_jar_server(import_server_path, import_server_jar) if not good_path: self.redirect("/panel/error?error=Server path or Server Jar not found!") return False - new_server_id = controller.import_jar_server(server_name, import_server_path,import_server_jar, min_mem, max_mem, port) + new_server_id = self.controller.import_jar_server(server_name, import_server_path,import_server_jar, min_mem, max_mem, port) elif import_type == 'import_zip': - good_path = controller.verify_zip_server(import_server_path) + good_path = self.controller.verify_zip_server(import_server_path) if not good_path: self.redirect("/panel/error?error=Zip file not found!") return False - new_server_id = controller.import_zip_server(server_name, import_server_path,import_server_jar, min_mem, max_mem, port) + new_server_id = self.controller.import_zip_server(server_name, import_server_path,import_server_jar, min_mem, max_mem, port) if new_server_id == "false": self.redirect("/panel/error?error=ZIP file not accessible! You can fix this permissions issue with sudo chown -R crafty:crafty {} And sudo chmod 2775 -R {}".format(import_server_path, import_server_path)) return False else: # todo: add server type check here and call the correct server add functions if not a jar - new_server_id = controller.create_jar_server(server_parts[0], server_parts[1], server_name, min_mem, max_mem, port) + new_server_id = self.controller.create_jar_server(server_parts[0], server_parts[1], server_name, min_mem, max_mem, port) if new_server_id: db_helper.add_to_audit_log(user_data['user_id'], @@ -180,7 +178,7 @@ class ServerHandler(BaseHandler): logger.error("Unable to create server") console.error("Unable to create server") - stats.record_stats() + self.controller.stats.record_stats() self.redirect("/panel/dashboard") self.render( diff --git a/app/classes/web/tornado.py b/app/classes/web/tornado.py index 866d0a10..096dd746 100644 --- a/app/classes/web/tornado.py +++ b/app/classes/web/tornado.py @@ -33,12 +33,14 @@ except ModuleNotFoundError as e: -class webserver: +class Webserver: - def __init__(self): + def __init__(self, controller, tasks_manager): self.ioloop = None self.HTTP_Server = None self.HTTPS_Server = None + self.controller = controller + self.tasks_manager = tasks_manager self._asyncio_patch() @@ -118,15 +120,16 @@ class webserver: tornado.locale.set_default_locale(lang) + handler_args = {"controller": self.controller, "tasks_manager": self.tasks_manager} handlers = [ - (r'/', DefaultHandler), - (r'/public/(.*)', PublicHandler), - (r'/panel/(.*)', PanelHandler), - (r'/server/(.*)', ServerHandler), - (r'/ajax/(.*)', AjaxHandler), - (r'/api/stats/servers', ServersStats), - (r'/api/stats/node', NodeStats), - (r'/ws', SocketHandler), + (r'/', DefaultHandler, handler_args), + (r'/public/(.*)', PublicHandler, handler_args), + (r'/panel/(.*)', PanelHandler, handler_args), + (r'/server/(.*)', ServerHandler, handler_args), + (r'/ajax/(.*)', AjaxHandler, handler_args), + (r'/api/stats/servers', ServersStats, handler_args), + (r'/api/stats/node', NodeStats, handler_args), + (r'/ws', SocketHandler, handler_args), ] app = tornado.web.Application( diff --git a/app/classes/web/websocket_handler.py b/app/classes/web/websocket_handler.py index 5abd1684..a33707f6 100644 --- a/app/classes/web/websocket_handler.py +++ b/app/classes/web/websocket_handler.py @@ -8,6 +8,10 @@ from app.classes.web.websocket_helper import websocket_helper class SocketHandler(tornado.websocket.WebSocketHandler): + def initialize(self, controller=None, tasks_manager=None): + self.controller = controller + self.tasks_manager = tasks_manager + def get_remote_ip(self): remote_ip = self.request.headers.get("X-Real-IP") or \ self.request.headers.get("X-Forwarded-For") or \ diff --git a/app/frontend/templates/panel/credits.html b/app/frontend/templates/panel/credits.html index 484193ca..2efee656 100644 --- a/app/frontend/templates/panel/credits.html +++ b/app/frontend/templates/panel/credits.html @@ -50,8 +50,10 @@
+ {% if person['loc'] %}

{{ person['loc'] }}

+ {% end %}
@@ -113,8 +115,10 @@
+ {% if person['loc'] %}

{{ person['loc'] }}

+ {% end %}
@@ -177,8 +181,10 @@
+ {% if person['loc'] %}

{{ person['loc'] }}

+ {% end %}
diff --git a/app/frontend/templates/panel/dashboard.html b/app/frontend/templates/panel/dashboard.html index 4085c250..1ce71648 100644 --- a/app/frontend/templates/panel/dashboard.html +++ b/app/frontend/templates/panel/dashboard.html @@ -125,7 +125,7 @@ - {% if server['stats'][0]['running'] %} + {% if server['stats']['running'] %}     {% else %} @@ -136,59 +136,59 @@ -
+
+ " role="progressbar" style="width: {{server['stats']['cpu']}}%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
- {{server['stats'][0]['cpu']}}% + {{server['stats']['cpu']}}% -
+
+ " role="progressbar" style="width: {{server['stats']['mem_percent']}}%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
- {{server['stats'][0]['mem_percent']}}% - + {{server['stats']['mem_percent']}}% - - {% if server['stats'][0]['mem'] == 0 %} + {% if server['stats']['mem'] == 0 %} 0 MB {% else %} - {{server['stats'][0]['mem']}} + {{server['stats']['mem']}} {% end %} - {{ server['stats'][0]['world_name'] }} : {{ server['stats'][0]['world_size'] }} + {{ server['stats']['world_name'] }} : {{ server['stats']['world_size'] }} - {% if server['stats'][0]['int_ping_results'] %} - {{ server['stats'][0]['online'] }} / {{ server['stats'][0]['max'] }} Max
+ {% if server['stats']['int_ping_results'] %} + {{ server['stats']['online'] }} / {{ server['stats']['max'] }} Max
- {% if server['stats'][0]['desc'] != 'False' %} - {{ server['stats'][0]['desc'] }}
+ {% if server['stats']['desc'] != 'False' %} + {{ server['stats']['desc'] }}
{% end %} - {% if server['stats'][0]['version'] != 'False' %} - {{ server['stats'][0]['version'] }} + {% if server['stats']['version'] != 'False' %} + {{ server['stats']['version'] }} {% end %} {% end %} - {% if server['stats'][0]['running'] %} + {% if server['stats']['running'] %} Online {% else %} Offline diff --git a/app/frontend/templates/panel/parts/details_stats.html b/app/frontend/templates/panel/parts/details_stats.html index 4d44c117..204ad6c1 100644 --- a/app/frontend/templates/panel/parts/details_stats.html +++ b/app/frontend/templates/panel/parts/details_stats.html @@ -4,9 +4,9 @@
- {% if data['server_stats'][0]['running'] %} + {% if data['server_stats']['running'] %} Server Status: Online
- Server Started: {{ data['server_stats'][0]['started'] }} (Server Time)
+ Server Started: {{ data['server_stats']['started'] }} (Server Time)
Server Uptime: Error Calculating {% else %} Server Status: Offline
@@ -16,19 +16,19 @@
- CPU: {{ data['server_stats'][0]['cpu'] }}%
- Mem: {{ data['server_stats'][0]['mem'] }}
- {% if data['server_stats'][0]['int_ping_results'] %} - Players: {{ data['server_stats'][0]['online'] }} / {{ data['server_stats'][0]['max'] }}
+ CPU: {{ data['server_stats']['cpu'] }}%
+ Mem: {{ data['server_stats']['mem'] }}
+ {% if data['server_stats']['int_ping_results'] %} + Players: {{ data['server_stats']['online'] }} / {{ data['server_stats']['max'] }}
{% else %} Players: 0/0
{% end %}
- {% if data['server_stats'][0]['version'] != 'False' %} - Server: {{ data['server_stats'][0]['version'] }}
- Desc: {{ data['server_stats'][0]['desc'] }}
+ {% if data['server_stats']['version'] != 'False' %} + Server: {{ data['server_stats']['version'] }}
+ Desc: {{ data['server_stats']['desc'] }}
{% else %} Server: Unable To Connect
Desc: Unable To Connect
@@ -86,9 +86,9 @@ let startedLocal; if (started != null) { - console.log('88', '{{ data['server_stats'][0]['started'] }}'); - {% if data['server_stats'][0]['started'] != 'False' %} - startedUTC = '{{ (datetime.datetime.strptime(data['server_stats'][0]['started'], '%Y-%m-%d %H:%M:%S') - datetime.timedelta(seconds=-time.timezone)).strftime('%Y-%m-%d %H:%M:%S') }}'; + console.log('88', '{{ data['server_stats']['started'] }}'); + {% if data['server_stats']['started'] != 'False' %} + startedUTC = '{{ (datetime.datetime.strptime(data['server_stats']['started'], '%Y-%m-%d %H:%M:%S') - datetime.timedelta(seconds=-time.timezone)).strftime('%Y-%m-%d %H:%M:%S') }}'; {% end %} console.log('utc', startedUTC); startedUTC = moment.utc(startedUTC, 'YYYY-MM-DD HH:mm:ss'); @@ -105,7 +105,7 @@ } let nowServerTime = '{{ data['time'] }}'; - let startedServerTime = '{{ data['server_stats'][0]['started'] }}'; + let startedServerTime = '{{ data['server_stats']['started'] }}'; if (uptime != null && started != null) { diff --git a/app/frontend/templates/panel/server_admin_controls.html b/app/frontend/templates/panel/server_admin_controls.html index 1cc8348d..2e35fc2f 100644 --- a/app/frontend/templates/panel/server_admin_controls.html +++ b/app/frontend/templates/panel/server_admin_controls.html @@ -15,9 +15,9 @@
@@ -34,31 +34,31 @@
+
+

 Development Team

+
+
+ +
+

 Support and Documentation Team

+
- + {% for person in data['staff']['support'] %}
@@ -38,74 +103,44 @@
- profile image
-

Phillip Tarrant

+

{{ person['name'] }}

-

Atlanta, GA

+

{{ person['loc'] }}

- Staff - Developer - BDFL + {% if person['tags'][0] %} + {{ person['tags'][0] }} + {% end %} + {% if person['tags'][1] %} + {{ person['tags'][1] }} + {% end %} + {% if person['tags'][2] %} + {% if type(person['tags'][2]) is list %} + {{ person['tags'][2][0] }} + {% else %} + {{ person['tags'][2] }} + {% end %} + {% end %}
- Crafty's Benevolent Dictator for Life.

- His interests include Linux, cybersecurity, hacking, and gaming. - He enjoys downtime with the family, and playing with his chickens. -
-
-
- -
-
-
- -
-
-
- -
-
- -
- profile image -
- -
-
-

Pita Bread

-
- -
- -

Midwest, USA

-
-
-
- -
-
- Staff - Community Leader -
- -
- His interests include bread, Linux, and networking. - He enjoys pumpkins, organizing, and long-winded emails, but hates wifi. + {% if person['title'] %} + Crafty's {{ person['title'] }}

+ {% end %} + {{ person['blurb'] }}
@@ -113,99 +148,17 @@
+ {% end %}
-
- -
-
-
- -
-
- -
- profile image -
- -
-
-

Kev Dagoat

-
- -
- -

East Coast, AU

-
-
-
- -
-
- Staff - Developer - HOD -
- -
- Crafty's Head Of Development

- His interests include Linux, programming, and goats of course. - He enjoys building APIs, K8s, and Geeking over video cards. -
-
-
- -
-
-
- - -
-
-
- -
-
- -
- profile image -
- -
-
-

MC Gaming

-
- -
- -

Central, UK

-
-
-
- -
-
- Staff - Developer -
- -
- His interests include learning, Linux, programming. - He loves pentesting apps and gaming. -
-
-
- -
-
-
-
+
+

 Retired Staff

+
+ {% for person in data['staff']['retired'] %}
@@ -214,200 +167,44 @@
- profile image
-

Andrew Redacted

+

{{ person['name'] }}

-

East Coast, USA

+

{{ person['loc'] }}

- Staff - Support Manager + {% if person['tags'][0] %} + {{ person['tags'][0] }} + {% end %} + {% if person['tags'][1] %} + {{ person['tags'][1] }} + {% end %} + {% if person['tags'][2] %} + {% if type(person['tags'][2]) is list %} + {{ person['tags'][2][0] }} + {% else %} + {{ person['tags'][2] }} + {% end %} + {% end %}
- His interests include Linux, gaming, and helping others. When he's able to - unplug he enjoys biking, hiking, and playing soccer. -
-
-
- -
-
-
- -
-
-
- -
-
- -
- profile image -
- -
-
-

Manu Redacted

-
- -
- -

Eastern, CA

-
-
-
- -
-
- Staff - Developer - Project Manager -
- -
- His interests include learning, Linux, and programming. - He enjoys speaking French, doing 6 things at once, and testing software. -
-
-
- -
-
-
-