Merge branch 'dev' into tweak/master-server-dir-config

This commit is contained in:
amcmanu3 2023-01-30 12:14:58 -05:00
commit 3a763178d7
39 changed files with 1711 additions and 643 deletions

View File

@ -1,5 +1,5 @@
# Changelog
## --- [4.0.20] - 2022/TBD
## --- [4.0.21] - 2023/TBD
### New features
TBD
### Bug fixes
@ -10,7 +10,27 @@ TBD
TBD
<br><br>
## --- [4.0.19] - 2022/01/07
## --- [4.0.20] - 2023/01/29
### New features
- Add option to run command before backup. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/536))
- Make Config.json editable from panel. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/532))
- Managed config.json refector (See MR for details). ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/485))
### Bug fixes
- Fix local java server imports. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/529))
- Fix Schedule Restore | Add Backup Config Preservation. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/533))
- Rework `/public` Route. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/538))
### Tweaks
- Hide stats DB directory from files tree. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/530))
- Make it so file tree doesn't reload on upload/delete. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/541))
- Add upload completed feedback to file upload. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/541))
- Added further login screen customisation settings. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/531))
- Set backup filename to use same time as schedule. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/534))
- Move Schedules to from DB to Queue Datatype. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/535))
- Move raknet icon failure to a debug log. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/537))
- Add Default redirection to Dashboard if the user is connected. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/540))
<br><br>
## --- [4.0.19] - 2023/01/07
### Bug fixes
- Fix port tooltip not showing on dash while server online. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/503))
- Fix '+' char in path causing any file operation to fail. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/502))

View File

@ -1,5 +1,5 @@
[![Crafty Logo](app/frontend/static/assets/images/logo_long.svg)](https://craftycontrol.com)
# Crafty Controller 4.0.20
# Crafty Controller 4.0.21
> Python based Control Panel for your Minecraft Server
## What is Crafty Controller?

View File

@ -1,4 +1,5 @@
import logging
import queue
from app.classes.models.management import HelpersManagement
from app.classes.models.servers import HelperServers
@ -9,6 +10,26 @@ logger = logging.getLogger(__name__)
class ManagementController:
def __init__(self, management_helper):
self.management_helper = management_helper
self.command_queue = queue.Queue()
# **********************************************************************************
# Config Methods
# **********************************************************************************
@staticmethod
def set_login_image(path):
HelpersManagement.set_login_image(path)
@staticmethod
def get_login_image():
return HelpersManagement.get_login_image()
@staticmethod
def set_login_opacity(opacity):
return HelpersManagement.set_login_opacity(opacity)
@staticmethod
def get_login_opacity():
return HelpersManagement.get_login_opacity()
# **********************************************************************************
# Host_Stats Methods
@ -25,12 +46,17 @@ class ManagementController:
def get_crafty_api_key():
return HelpersManagement.get_secret_api_key()
@staticmethod
def set_cookie_secret(key):
HelpersManagement.set_cookie_secret(key)
@staticmethod
def add_crafty_row():
HelpersManagement.create_crafty_row()
# **********************************************************************************
# Commands Methods
# **********************************************************************************
@staticmethod
def get_unactioned_commands():
return HelpersManagement.get_unactioned_commands()
def send_command(self, user_id, server_id, remote_ip, command):
server_name = HelperServers.get_server_friendly_name(server_id)
@ -42,11 +68,12 @@ class ManagementController:
server_id,
remote_ip,
)
HelpersManagement.add_command(server_id, user_id, remote_ip, command)
self.queue_command(
{"server_id": server_id, "user_id": user_id, "command": command}
)
@staticmethod
def mark_command_complete(command_id=None):
return HelpersManagement.mark_command_complete(command_id)
def queue_command(self, command_data):
self.command_queue.put(command_data)
# **********************************************************************************
# Audit_Log Methods
@ -78,6 +105,10 @@ class ManagementController:
command,
name,
enabled=True,
one_time=False,
cron_string="* * * * *",
parent=None,
delay=0,
):
return HelpersManagement.create_scheduled_task(
server_id,
@ -88,20 +119,16 @@ class ManagementController:
command,
name,
enabled,
one_time,
cron_string,
parent,
delay,
)
@staticmethod
def delete_scheduled_task(schedule_id):
return HelpersManagement.delete_scheduled_task(schedule_id)
@staticmethod
def set_login_image(path):
HelpersManagement.set_login_image(path)
@staticmethod
def get_login_image():
return HelpersManagement.get_login_image()
@staticmethod
def update_scheduled_task(schedule_id, updates):
return HelpersManagement.update_scheduled_task(schedule_id, updates)
@ -145,9 +172,18 @@ class ManagementController:
excluded_dirs: list = None,
compress: bool = False,
shutdown: bool = False,
before: str = "",
after: str = "",
):
return self.management_helper.set_backup_config(
server_id, backup_path, max_backups, excluded_dirs, compress, shutdown
server_id,
backup_path,
max_backups,
excluded_dirs,
compress,
shutdown,
before,
after,
)
@staticmethod

View File

@ -300,7 +300,7 @@ class Stats:
server_icon = base64.encodebytes(ping_obj["icon"])
except Exception as e:
server_icon = False
logger.info(
logger.debug(
"Unable to read the server icon due to the following error:", exc_info=e
)
ping_data = {

View File

@ -13,7 +13,7 @@ from peewee import (
from playhouse.shortcuts import model_to_dict
from app.classes.models.base_model import BaseModel
from app.classes.models.users import Users, HelperUsers
from app.classes.models.users import HelperUsers
from app.classes.models.servers import Servers
from app.classes.models.server_permissions import PermissionsServers
from app.classes.shared.main_models import DatabaseShortcuts
@ -43,7 +43,9 @@ class AuditLog(BaseModel):
# **********************************************************************************
class CraftySettings(BaseModel):
secret_api_key = CharField(default="")
cookie_secret = CharField(default="")
login_photo = CharField(default="login_1.jpg")
login_opacity = IntegerField(default=100)
master_server_dir = CharField(default="")
class Meta:
@ -69,22 +71,6 @@ class HostStats(BaseModel):
table_name = "host_stats"
# **********************************************************************************
# Commands Class
# **********************************************************************************
class Commands(BaseModel):
command_id = AutoField()
created = DateTimeField(default=datetime.datetime.now)
server_id = ForeignKeyField(Servers, backref="server", index=True)
user = ForeignKeyField(Users, backref="user", index=True)
source_ip = CharField(default="127.0.0.1")
command = CharField(default="")
executed = BooleanField(default=False)
class Meta:
table_name = "commands"
# **********************************************************************************
# Webhooks Class
# **********************************************************************************
@ -132,6 +118,8 @@ class Backups(BaseModel):
server_id = ForeignKeyField(Servers, backref="backups_server")
compress = BooleanField(default=False)
shutdown = BooleanField(default=False)
before = CharField(default="")
after = CharField(default="")
class Meta:
table_name = "backups"
@ -151,33 +139,6 @@ class HelpersManagement:
query = HostStats.select().order_by(HostStats.id.desc()).get()
return model_to_dict(query)
# **********************************************************************************
# Commands Methods
# **********************************************************************************
@staticmethod
def add_command(server_id, user_id, remote_ip, command):
Commands.insert(
{
Commands.server_id: server_id,
Commands.user: user_id,
Commands.source_ip: remote_ip,
Commands.command: command,
}
).execute()
@staticmethod
def get_unactioned_commands():
query = Commands.select().where(Commands.executed == 0)
return query
@staticmethod
def mark_command_complete(command_id=None):
if command_id is not None:
logger.debug(f"Marking Command {command_id} completed")
Commands.update({Commands.executed: True}).where(
Commands.command_id == command_id
).execute()
# **********************************************************************************
# Audit_Log Methods
# **********************************************************************************
@ -245,9 +206,22 @@ class HelpersManagement:
else:
return
@staticmethod
def create_crafty_row():
CraftySettings.insert(
{
CraftySettings.secret_api_key: "",
CraftySettings.cookie_secret: "",
CraftySettings.login_photo: "login_1.jpg",
CraftySettings.login_opacity: 100,
}
).execute()
@staticmethod
def set_secret_api_key(key):
CraftySettings.insert(secret_api_key=key).execute()
CraftySettings.update({CraftySettings.secret_api_key: key}).where(
CraftySettings.id == 1
).execute()
@staticmethod
def get_secret_api_key():
@ -256,6 +230,22 @@ class HelpersManagement:
)
return settings[0].secret_api_key
@staticmethod
def get_cookie_secret():
settings = CraftySettings.select(CraftySettings.cookie_secret).where(
CraftySettings.id == 1
)
return settings[0].cookie_secret
@staticmethod
def set_cookie_secret(key):
CraftySettings.update({CraftySettings.cookie_secret: key}).where(
CraftySettings.id == 1
).execute()
# **********************************************************************************
# Config Methods
# **********************************************************************************
@staticmethod
def get_login_image():
settings = CraftySettings.select(CraftySettings.login_photo).where(
@ -269,6 +259,19 @@ class HelpersManagement:
CraftySettings.id == 1
).execute()
@staticmethod
def get_login_opacity():
settings = CraftySettings.select(CraftySettings.login_opacity).where(
CraftySettings.id == 1
)
return settings[0].login_opacity
@staticmethod
def set_login_opacity(opacity):
CraftySettings.update({CraftySettings.login_opacity: opacity}).where(
CraftySettings.id == 1
).execute()
@staticmethod
def get_master_server_dir():
settings = CraftySettings.select(CraftySettings.master_server_dir).where(
@ -383,6 +386,8 @@ class HelpersManagement:
"server_id": row.server_id_id,
"compress": row.compress,
"shutdown": row.shutdown,
"before": row.before,
"after": row.after,
}
except IndexError:
conf = {
@ -392,6 +397,8 @@ class HelpersManagement:
"server_id": server_id,
"compress": False,
"shutdown": False,
"before": "",
"after": "",
}
return conf
@ -407,6 +414,8 @@ class HelpersManagement:
excluded_dirs: list = None,
compress: bool = False,
shutdown: bool = False,
before: str = "",
after: str = "",
):
logger.debug(f"Updating server {server_id} backup config with {locals()}")
if Backups.select().where(Backups.server_id == server_id).exists():
@ -419,6 +428,8 @@ class HelpersManagement:
"server_id": server_id,
"compress": False,
"shutdown": False,
"before": "",
"after": "",
}
new_row = True
if max_backups is not None:
@ -428,6 +439,8 @@ class HelpersManagement:
conf["excluded_dirs"] = dirs_to_exclude
conf["compress"] = compress
conf["shutdown"] = shutdown
conf["before"] = before
conf["after"] = after
if not new_row:
with self.database.atomic():
if backup_path is not None:
@ -487,9 +500,3 @@ class HelpersManagement:
f"Not removing {dir_to_del} from excluded directories - "
f"not in the excluded directory list for server ID {server_id}"
)
@staticmethod
def clear_unexecuted_commands():
Commands.update({Commands.executed: True}).where(
Commands.executed == False # pylint: disable=singleton-comparison
).execute()

View File

@ -78,6 +78,7 @@ class Helpers:
self.websocket_helper = WebSocketHelper(self)
self.translation = Translation(self)
self.update_available = False
self.ignored_names = ["crafty_managed.txt", "db_stats"]
@staticmethod
def auto_installer_fix(ex):
@ -376,6 +377,64 @@ class Helpers:
return default_return
def set_settings(self, data):
try:
with open(self.settings_file, "w", encoding="utf-8") as f:
json.dump(data, f, indent=4)
except Exception as e:
logger.critical(
f"Config File Error: Unable to read {self.settings_file} due to {e}"
)
Console.critical(
f"Config File Error: Unable to read {self.settings_file} due to {e}"
)
return False
return True
@staticmethod
def get_master_config():
# Make changes for users' local config.json files here. As of 4.0.20
# Config.json was removed from the repo to make it easier for users
# To make non-breaking changes to the file.
return {
"http_port": 8000,
"https_port": 8443,
"language": "en_EN",
"cookie_expire": 30,
"show_errors": True,
"history_max_age": 7,
"stats_update_frequency": 30,
"delete_default_json": False,
"show_contribute_link": True,
"virtual_terminal_lines": 70,
"max_log_lines": 700,
"max_audit_entries": 300,
"disabled_language_files": [],
"stream_size_GB": 1,
"keywords": ["help", "chunk"],
"allow_nsfw_profile_pictures": False,
"enable_user_self_delete": False,
"reset_secrets_on_next_boot": False,
}
def get_all_settings(self):
try:
with open(self.settings_file, "r", encoding="utf-8") as f:
data = json.load(f)
except Exception as e:
data = {}
logger.critical(
f"Config File Error: Unable to read {self.settings_file} due to {e}"
)
Console.critical(
f"Config File Error: Unable to read {self.settings_file} due to {e}"
)
return data
@staticmethod
def is_subdir(server_path, root_dir):
server_path = os.path.realpath(server_path)
@ -947,8 +1006,7 @@ class Helpers:
return data
@staticmethod
def generate_tree(folder, output=""):
def generate_tree(self, folder, output=""):
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
@ -965,9 +1023,13 @@ class Helpers:
rel = os.path.join(folder, raw_filename)
dpath = os.path.join(folder, filename)
if os.path.isdir(rel):
output += f"""<li class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
if filename not in self.ignored_names:
output += f"""<li id="{dpath}li" class="tree-item"
data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}"
class="tree-caret tree-ctx-item tree-folder">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}"
data-name="{filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
{filename}
@ -975,8 +1037,8 @@ class Helpers:
</div><li>
\n"""
else:
if filename != "crafty_managed.txt":
output += f"""<li
if filename not in self.ignored_names:
output += f"""<li id="{dpath}li"
class="d-block tree-ctx-item tree-file tree-item"
data-path="{dpath}"
data-name="{filename}"
@ -984,8 +1046,7 @@ class Helpers:
<i class="far fa-file"></i></span>{filename}</li>"""
return output
@staticmethod
def generate_dir(folder, output=""):
def generate_dir(self, folder, output=""):
dir_list = []
unsorted_files = []
@ -1004,17 +1065,21 @@ class Helpers:
dpath = os.path.join(folder, filename)
rel = os.path.join(folder, raw_filename)
if os.path.isdir(rel):
output += f"""<li class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
if filename not in self.ignored_names:
output += f"""<li id="{dpath}li" class="tree-item"
data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}"
class="tree-caret tree-ctx-item tree-folder">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}"
data-name="{filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
{filename}
</span>
</div><li>"""
else:
if filename != "crafty_managed.txt":
output += f"""<li
if filename not in self.ignored_names:
output += f"""<li id="{dpath}li"
class="d-block tree-ctx-item tree-file tree-item"
data-path="{dpath}"
data-name="{filename}"

View File

@ -244,6 +244,41 @@ class Controller:
exec_user["user_id"], "support_status_update", results
)
def get_config_diff(self):
master_config = Helpers.get_master_config()
try:
user_config = self.helper.get_all_settings()
except:
# Call helper to set updated config.
Console.warning("No Config found. Setting Default Config.json")
user_config = master_config
keys = list(user_config.keys())
keys.sort()
sorted_data = {i: user_config[i] for i in keys}
self.helper.set_settings(user_config)
return
items_to_del = []
# Iterate through user's config.json and check for
# Keys/values that need to be removed
for key in user_config:
if key not in master_config.keys():
items_to_del.append(key)
# Remove key/values from user config that were staged
for item in items_to_del[:]:
del user_config[item]
# Add new keys to user config.
for key, value in master_config.items():
if key not in user_config.keys():
user_config[key] = value
# Call helper to set updated config.
keys = list(user_config.keys())
keys.sort()
sorted_data = {i: user_config[i] for i in keys}
self.helper.set_settings(sorted_data)
def send_log_status(self):
try:
return self.log_stats
@ -965,10 +1000,6 @@ class Controller:
# remove the server from the DB
self.servers.remove_server(server_id)
@staticmethod
def clear_unexecuted_commands():
HelpersManagement.clear_unexecuted_commands()
@staticmethod
def clear_support_status():
HelperUsers.clear_support_status()

View File

@ -8,9 +8,10 @@ logger = logging.getLogger(__name__)
class DatabaseBuilder:
def __init__(self, database, helper, users_helper):
def __init__(self, database, helper, users_helper, management_helper):
self.database = database
self.helper = helper
self.management_helper = management_helper
self.users_helper = users_helper
def default_settings(self):
@ -29,6 +30,8 @@ class DatabaseBuilder:
manager=None,
)
self.management_helper.create_crafty_row()
def is_fresh_install(self):
try:
num_user = self.users_helper.get_user_total()

View File

@ -131,13 +131,13 @@ class ServerInstance:
self.stats_helper = HelperServerStats(self.server_id)
self.last_backup_failed = False
try:
tz = get_localzone()
self.tz = get_localzone()
except ZoneInfoNotFoundError:
logger.error(
"Could not capture time zone from system. Falling back to Europe/London"
)
tz = "Europe/London"
self.server_scheduler = BackgroundScheduler(timezone=str(tz))
self.tz = "Europe/London"
self.server_scheduler = BackgroundScheduler(timezone=str(self.tz))
self.server_scheduler.start()
self.backup_thread = threading.Thread(
target=self.a_backup_server, daemon=True, name=f"backup_{self.name}"
@ -175,7 +175,6 @@ class ServerInstance:
self.name = server_name
self.settings = server_data_obj
self.stats_helper.init_database(server_id)
self.record_server_stats()
# build our server run command
@ -1024,7 +1023,17 @@ class ServerInstance:
)
time.sleep(3)
conf = HelpersManagement.get_backup_config(self.server_id)
if conf["before"]:
if self.check_running():
logger.debug(
"Found running server and send command option. Sending command"
)
self.send_command(conf["before"])
if conf["shutdown"]:
if conf["before"]:
# pause to let people read message.
time.sleep(5)
logger.info(
"Found shutdown preference. Delaying"
+ "backup start. Shutting down server."
@ -1037,7 +1046,7 @@ class ServerInstance:
try:
backup_filename = (
f"{self.settings['backup_path']}/"
f"{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}"
f"{datetime.datetime.now().astimezone(self.tz).strftime('%Y-%m-%d_%H-%M-%S')}" # pylint: disable=line-too-long
)
logger.info(
f"Creating backup of server '{self.settings['server_name']}'"
@ -1105,6 +1114,14 @@ class ServerInstance:
self.run_threaded_server(HelperUsers.get_user_id_by_name("system"))
time.sleep(3)
self.last_backup_failed = False
if conf["after"]:
if self.check_running():
logger.debug(
"Found running server and send command option. Sending command"
)
self.send_command(conf["after"])
# pause to let people read message.
time.sleep(5)
except:
logger.exception(
f"Failed to create backup of server {self.name} (ID {self.server_id})"

View File

@ -91,22 +91,21 @@ class TasksManager:
def command_watcher(self):
while True:
# select any commands waiting to be processed
commands = HelpersManagement.get_unactioned_commands()
for cmd in commands:
if not self.controller.management.command_queue.empty():
cmd = self.controller.management.command_queue.get()
try:
svr = self.controller.servers.get_server_instance_by_id(
cmd.server_id.server_id
cmd["server_id"]
)
except:
logger.error(
"Server value requested does not exist! "
"Purging item from waiting commands."
)
HelpersManagement.mark_command_complete(cmd.command_id)
continue
user_id = cmd.user_id
command = cmd.command
user_id = cmd["user_id"]
command = cmd["command"]
if command == "start_server":
svr.run_threaded_server(user_id)
@ -136,8 +135,6 @@ class TasksManager:
else:
svr.send_command(command)
HelpersManagement.mark_command_complete(cmd.command_id)
time.sleep(1)
def _main_graceful_exit(self):
@ -212,16 +209,19 @@ class TasksManager:
if schedule.cron_string != "":
try:
new_job = self.scheduler.add_job(
HelpersManagement.add_command,
self.controller.management.queue_command,
CronTrigger.from_crontab(
schedule.cron_string, timezone=str(self.tz)
),
id=str(schedule.schedule_id),
args=[
schedule.server_id,
self.users_controller.get_id_by_name("system"),
"127.0.0.1",
schedule.command,
{
"server_id": schedule.server_id.server_id,
"user_id": self.users_controller.get_id_by_name(
"system"
),
"command": schedule.command,
}
],
)
except Exception as e:
@ -237,45 +237,54 @@ class TasksManager:
else:
if schedule.interval_type == "hours":
new_job = self.scheduler.add_job(
HelpersManagement.add_command,
self.controller.management.queue_command,
"cron",
minute=0,
hour="*/" + str(schedule.interval),
id=str(schedule.schedule_id),
args=[
schedule.server_id,
self.users_controller.get_id_by_name("system"),
"127.0.0.1",
schedule.command,
{
"server_id": schedule.server_id.server_id,
"user_id": self.users_controller.get_id_by_name(
"system"
),
"command": schedule.command,
}
],
)
elif schedule.interval_type == "minutes":
new_job = self.scheduler.add_job(
HelpersManagement.add_command,
self.controller.management.queue_command,
"cron",
minute="*/" + str(schedule.interval),
id=str(schedule.schedule_id),
args=[
schedule.server_id,
self.users_controller.get_id_by_name("system"),
"127.0.0.1",
schedule.command,
{
"server_id": schedule.server_id.server_id,
"user_id": self.users_controller.get_id_by_name(
"system"
),
"command": schedule.command,
}
],
)
elif schedule.interval_type == "days":
curr_time = schedule.start_time.split(":")
new_job = self.scheduler.add_job(
HelpersManagement.add_command,
self.controller.management.queue_command,
"cron",
day="*/" + str(schedule.interval),
hour=curr_time[0],
minute=curr_time[1],
id=str(schedule.schedule_id),
args=[
schedule.server_id,
self.users_controller.get_id_by_name("system"),
"127.0.0.1",
schedule.command,
{
"server_id": schedule.server_id.server_id,
"user_id": self.users_controller.get_id_by_name(
"system"
),
"command": schedule.command,
}
],
)
if new_job != "error":
@ -322,16 +331,19 @@ class TasksManager:
if job_data["cron_string"] != "":
try:
new_job = self.scheduler.add_job(
HelpersManagement.add_command,
self.controller.management.queue_command,
CronTrigger.from_crontab(
job_data["cron_string"], timezone=str(self.tz)
),
id=str(sch_id),
args=[
job_data["server_id"],
self.users_controller.get_id_by_name("system"),
"127.0.0.1",
job_data["command"],
{
"server_id": job_data["server_id"],
"user_id": self.users_controller.get_id_by_name(
"system"
),
"command": job_data["command"],
}
],
)
except Exception as e:
@ -345,45 +357,54 @@ class TasksManager:
else:
if job_data["interval_type"] == "hours":
new_job = self.scheduler.add_job(
HelpersManagement.add_command,
self.controller.management.queue_command,
"cron",
minute=0,
hour="*/" + str(job_data["interval"]),
id=str(sch_id),
args=[
job_data["server_id"],
self.users_controller.get_id_by_name("system"),
"127.0.0.1",
job_data["command"],
{
"server_id": job_data["server_id"],
"user_id": self.users_controller.get_id_by_name(
"system"
),
"command": job_data["command"],
}
],
)
elif job_data["interval_type"] == "minutes":
new_job = self.scheduler.add_job(
HelpersManagement.add_command,
self.controller.management.queue_command,
"cron",
minute="*/" + str(job_data["interval"]),
id=str(sch_id),
args=[
job_data["server_id"],
self.users_controller.get_id_by_name("system"),
"127.0.0.1",
job_data["command"],
{
"server_id": job_data["server_id"],
"user_id": self.users_controller.get_id_by_name(
"system"
),
"command": job_data["command"],
}
],
)
elif job_data["interval_type"] == "days":
curr_time = job_data["start_time"].split(":")
new_job = self.scheduler.add_job(
HelpersManagement.add_command,
self.controller.management.queue_command,
"cron",
day="*/" + str(job_data["interval"]),
hour=curr_time[0],
minute=curr_time[1],
id=str(sch_id),
args=[
job_data["server_id"],
self.users_controller.get_id_by_name("system"),
"127.0.0.1",
job_data["command"],
{
"server_id": job_data["server_id"],
"user_id": self.users_controller.get_id_by_name(
"system"
),
"command": job_data["command"],
}
],
)
logger.info("Added job. Current enabled schedules: ")
@ -460,16 +481,19 @@ class TasksManager:
if job_data["cron_string"] != "":
try:
new_job = self.scheduler.add_job(
HelpersManagement.add_command,
self.controller.management.queue_command,
CronTrigger.from_crontab(
job_data["cron_string"], timezone=str(self.tz)
),
id=str(sch_id),
args=[
job_data["server_id"],
self.users_controller.get_id_by_name("system"),
"127.0.0.1",
job_data["command"],
{
"server_id": job_data["server_id"],
"user_id": self.users_controller.get_id_by_name(
"system"
),
"command": job_data["command"],
}
],
)
except Exception as e:
@ -480,45 +504,54 @@ class TasksManager:
else:
if job_data["interval_type"] == "hours":
new_job = self.scheduler.add_job(
HelpersManagement.add_command,
self.controller.management.queue_command,
"cron",
minute=0,
hour="*/" + str(job_data["interval"]),
id=str(sch_id),
args=[
job_data["server_id"],
self.users_controller.get_id_by_name("system"),
"127.0.0.1",
job_data["command"],
{
"server_id": job_data["server_id"],
"user_id": self.users_controller.get_id_by_name(
"system"
),
"command": job_data["command"],
}
],
)
elif job_data["interval_type"] == "minutes":
new_job = self.scheduler.add_job(
HelpersManagement.add_command,
self.controller.management.queue_command,
"cron",
minute="*/" + str(job_data["interval"]),
id=str(sch_id),
args=[
job_data["server_id"],
self.users_controller.get_id_by_name("system"),
"127.0.0.1",
job_data["command"],
{
"server_id": job_data["server_id"],
"user_id": self.users_controller.get_id_by_name(
"system"
),
"command": job_data["command"],
}
],
)
elif job_data["interval_type"] == "days":
curr_time = job_data["start_time"].split(":")
new_job = self.scheduler.add_job(
HelpersManagement.add_command,
self.controller.management.queue_command,
"cron",
day="*/" + str(job_data["interval"]),
hour=curr_time[0],
minute=curr_time[1],
id=str(sch_id),
args=[
job_data["server_id"],
self.users_controller.get_id_by_name("system"),
"127.0.0.1",
job_data["command"],
{
"server_id": job_data["server_id"],
"user_id": self.users_controller.get_id_by_name(
"system"
),
"command": job_data["command"],
}
],
)
if new_job != "error":
@ -579,15 +612,18 @@ class TasksManager:
seconds=schedule.delay
)
self.scheduler.add_job(
HelpersManagement.add_command,
self.controller.management.queue_command,
"date",
run_date=delaytime,
id=str(schedule.schedule_id),
args=[
schedule.server_id,
self.users_controller.get_id_by_name("system"),
"127.0.0.1",
schedule.command,
{
"server_id": schedule.server_id.server_id,
"user_id": self.users_controller.get_id_by_name(
"system"
),
"command": schedule.command,
}
],
)
else:

View File

@ -348,14 +348,11 @@ class AjaxHandler(BaseHandler):
server.backup_server()
elif page == "clear_comms":
if exec_user["superuser"]:
self.controller.clear_unexecuted_commands()
return
elif page == "select_photo":
if exec_user["superuser"]:
photo = self.get_argument("photo", None)
photo = urllib.parse.unquote(self.get_argument("photo", ""))
opacity = self.get_argument("opacity", 100)
self.controller.management.set_login_opacity(int(opacity))
if photo == "login_1.jpg":
self.controller.management.set_login_image("login_1.jpg")
self.controller.cached_login = f"{photo}"
@ -366,7 +363,7 @@ class AjaxHandler(BaseHandler):
elif page == "delete_photo":
if exec_user["superuser"]:
photo = self.get_argument("photo", None)
photo = urllib.parse.unquote(self.get_argument("photo", None))
if photo and photo != "login_1.jpg":
os.remove(
os.path.join(
@ -440,15 +437,8 @@ class AjaxHandler(BaseHandler):
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,
self.tasks_manager.update_job(
schedule.schedule_id, {"server_id": new_server_id}
)
# preserve execution command
new_server_obj = self.controller.servers.get_server_obj(
@ -456,6 +446,29 @@ class AjaxHandler(BaseHandler):
)
new_server_obj.execution_command = server_data["execution_command"]
self.controller.servers.update_server(new_server_obj)
# preserve backup config
backup_config = self.controller.management.get_backup_config(
server_id
)
excluded_dirs = []
server_obj = self.controller.servers.get_server_obj(server_id)
loop_backup_path = self.helper.wtol_path(server_obj.path)
for item in self.controller.management.get_excluded_backup_dirs(
server_id
):
item_path = self.helper.wtol_path(item)
bu_path = os.path.relpath(item_path, loop_backup_path)
bu_path = os.path.join(new_server_obj.path, bu_path)
excluded_dirs.append(bu_path)
self.controller.management.set_backup_config(
new_server_id,
new_server_obj.backup_path,
backup_config["max_backups"],
excluded_dirs,
backup_config["compress"],
backup_config["shutdown"],
)
# remove old server's tasks
try:
self.tasks_manager.remove_all_server_tasks(server_id)
@ -484,15 +497,8 @@ class AjaxHandler(BaseHandler):
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,
self.tasks_manager.update_job(
schedule.schedule_id, {"server_id": new_server_id}
)
# preserve execution command
new_server_obj = self.controller.servers.get_server_obj(
@ -500,6 +506,29 @@ class AjaxHandler(BaseHandler):
)
new_server_obj.execution_command = server_data["execution_command"]
self.controller.servers.update_server(new_server_obj)
# preserve backup config
backup_config = self.controller.management.get_backup_config(
server_id
)
excluded_dirs = []
server_obj = self.controller.servers.get_server_obj(server_id)
loop_backup_path = self.helper.wtol_path(server_obj.path)
for item in self.controller.management.get_excluded_backup_dirs(
server_id
):
item_path = self.helper.wtol_path(item)
bu_path = os.path.relpath(item_path, loop_backup_path)
bu_path = os.path.join(new_server_obj.path, bu_path)
excluded_dirs.append(bu_path)
self.controller.management.set_backup_config(
new_server_id,
new_server_obj.backup_path,
backup_config["max_backups"],
excluded_dirs,
backup_config["compress"],
backup_config["shutdown"],
)
try:
self.tasks_manager.remove_all_server_tasks(server_id)
except:

View File

@ -17,6 +17,5 @@ class DefaultHandler(BaseHandler):
)
else:
self.redirect(
"/public/login",
# translate=self.translator.translate,
"/panel/dashboard",
)

View File

@ -100,7 +100,7 @@ class FileHandler(BaseHandler):
self.write(
Helpers.get_os_understandable_path(path)
+ "\n"
+ Helpers.generate_tree(path)
+ self.helper.generate_tree(path)
)
self.finish()
@ -121,7 +121,7 @@ class FileHandler(BaseHandler):
self.write(
Helpers.get_os_understandable_path(path)
+ "\n"
+ Helpers.generate_dir(path)
+ self.helper.generate_dir(path)
)
self.finish()

View File

@ -291,6 +291,7 @@ class PanelHandler(BaseHandler):
# todo: make this actually pull and compare version data
"update_available": self.helper.update_available,
"background": self.controller.cached_login,
"login_opacity": self.controller.management.get_login_opacity(),
"serverTZ": tz,
"version_data": self.helper.get_version_string(),
"failed_servers": self.controller.servers.failed_servers,
@ -857,13 +858,51 @@ class PanelHandler(BaseHandler):
page_data["roles"] = self.controller.roles.get_all_roles()
page_data["auth-servers"][user.user_id] = super_auth_servers
page_data["managed_users"] = []
else:
page_data["managed_users"] = self.controller.users.get_managed_users(
exec_user["user_id"]
)
page_data["assigned_roles"] = []
for item in page_data["roles"]:
page_data["assigned_roles"].append(item.role_id)
page_data["managed_roles"] = self.controller.users.get_managed_roles(
exec_user["user_id"]
)
page_data["active_link"] = "panel_config"
template = "panel/panel_config.html"
elif page == "config_json":
if exec_user["superuser"]:
with open(self.helper.settings_file, "r", encoding="utf-8") as f:
data = json.load(f)
page_data["config-json"] = data
page_data["availables_languages"] = []
page_data["all_languages"] = []
for file in sorted(
os.listdir(
os.path.join(self.helper.root_dir, "app", "translations")
)
):
if file.endswith(".json"):
if file.split(".")[0] not in self.helper.get_setting(
"disabled_language_files"
):
page_data["availables_languages"].append(file.split(".")[0])
page_data["all_languages"].append(file.split(".")[0])
page_data["active_link"] = "config_json"
template = "panel/config_json.html"
elif page == "custom_login":
if exec_user["superuser"]:
page_data["backgrounds"] = []
cached_split = self.controller.cached_login.split("/")
if len(cached_split) == 1:
page_data["backgrounds"].append(
self.controller.cached_login
)
page_data["backgrounds"].append(self.controller.cached_login)
else:
page_data["backgrounds"].append(cached_split[1])
if "login_1.jpg" not in page_data["backgrounds"]:
@ -883,19 +922,12 @@ class PanelHandler(BaseHandler):
if item not in page_data["backgrounds"]:
page_data["backgrounds"].append(item)
page_data["background"] = self.controller.cached_login
else:
page_data["managed_users"] = self.controller.users.get_managed_users(
exec_user["user_id"]
)
page_data["assigned_roles"] = []
for item in page_data["roles"]:
page_data["assigned_roles"].append(item.role_id)
page_data[
"login_opacity"
] = self.controller.management.get_login_opacity()
page_data["managed_roles"] = self.controller.users.get_managed_roles(
exec_user["user_id"]
)
template = "panel/panel_config.html"
page_data["active_link"] = "custom_login"
template = "panel/custom_login.html"
elif page == "add_user":
page_data["new_user"] = True
@ -953,7 +985,9 @@ class PanelHandler(BaseHandler):
os.listdir(os.path.join(self.helper.root_dir, "app", "translations"))
):
if file.endswith(".json"):
if file not in self.helper.get_setting("disabled_language_files"):
if file.split(".")[0] not in self.helper.get_setting(
"disabled_language_files"
):
if file != str(page_data["languages"][0] + ".json"):
page_data["languages"].append(file.split(".")[0])
@ -1164,7 +1198,9 @@ class PanelHandler(BaseHandler):
os.listdir(os.path.join(self.helper.root_dir, "app", "translations"))
):
if file.endswith(".json"):
if file not in self.helper.get_setting("disabled_language_files"):
if file.split(".")[0] not in self.helper.get_setting(
"disabled_language_files"
):
if file != str(page_data["languages"][0] + ".json"):
page_data["languages"].append(file.split(".")[0])
@ -1678,6 +1714,8 @@ class PanelHandler(BaseHandler):
compress = self.get_argument("compress", False)
shutdown = self.get_argument("shutdown", False)
check_changed = self.get_argument("changed")
before = self.get_argument("backup_before", "")
after = self.get_argument("backup_after", "")
if str(check_changed) == str(1):
checked = self.get_body_arguments("root_path")
else:
@ -1701,6 +1739,8 @@ class PanelHandler(BaseHandler):
excluded_dirs=checked,
compress=bool(compress),
shutdown=bool(shutdown),
before=before,
after=after,
)
self.controller.management.add_to_audit_log(
@ -1712,6 +1752,41 @@ class PanelHandler(BaseHandler):
self.tasks_manager.reload_schedule_from_db()
self.redirect(f"/panel/server_detail?id={server_id}&subpage=backup")
elif page == "config_json":
try:
data = {}
with open(self.helper.settings_file, "r", encoding="utf-8") as f:
keys = json.load(f).keys()
this_uuid = self.get_argument("uuid")
for key in keys:
arg_data = self.get_argument(key)
if arg_data.startswith(this_uuid):
arg_data = arg_data.split(",")
arg_data.pop(0)
data[key] = arg_data
else:
try:
data[key] = int(arg_data)
except:
if arg_data == "True":
data[key] = True
elif arg_data == "False":
data[key] = False
else:
data[key] = arg_data
keys = list(data.keys())
keys.sort()
sorted_data = {i: data[i] for i in keys}
with open(self.helper.settings_file, "w", encoding="utf-8") as f:
json.dump(sorted_data, f, indent=4)
except Exception as e:
logger.critical(
"Config File Error: Unable to read "
f"{self.helper.settings_file} due to {e}"
)
self.redirect("/panel/config_json")
if page == "new_schedule":
server_id = self.check_server_id()
if not server_id:

View File

@ -40,6 +40,7 @@ class PublicHandler(BaseHandler):
"lang_page": self.helper.get_lang_page(self.helper.get_setting("language")),
"query": "",
"background": self.controller.cached_login,
"login_opacity": self.controller.management.get_login_opacity(),
}
if self.request.query:
@ -61,15 +62,15 @@ class PublicHandler(BaseHandler):
self.clear_cookie("token")
# self.clear_cookie("user")
# self.clear_cookie("user_data")
self.redirect("/public/login")
self.redirect("/login")
return
# if we have no page, let's go to login
else:
if self.request.query:
self.redirect("/public/login?" + self.request.query)
self.redirect("/login?" + self.request.query)
else:
self.redirect("/public/login")
self.redirect("/login")
return
self.render(
@ -96,9 +97,9 @@ class PublicHandler(BaseHandler):
if page == "login":
next_page = "/public/login"
next_page = "/login"
if self.request.query:
next_page = "/public/login?" + self.request.query
next_page = "/login?" + self.request.query
entered_username = bleach.clean(self.get_argument("username"))
entered_password = bleach.clean(self.get_argument("password"))
@ -113,11 +114,9 @@ class PublicHandler(BaseHandler):
# self.clear_cookie("user_data")
self.clear_cookie("token")
if self.request.query:
self.redirect(
f"/public/login?error_msg={error_msg}&{self.request.query}"
)
self.redirect(f"/login?error_msg={error_msg}&{self.request.query}")
else:
self.redirect(f"/public/login?error_msg={error_msg}")
self.redirect(f"/login?error_msg={error_msg}")
return
# if we don't have a user
@ -127,11 +126,9 @@ class PublicHandler(BaseHandler):
# self.clear_cookie("user_data")
self.clear_cookie("token")
if self.request.query:
self.redirect(
f"/public/login?error_msg={error_msg}&{self.request.query}"
)
self.redirect(f"/login?error_msg={error_msg}&{self.request.query}")
else:
self.redirect(f"/public/login?error_msg={error_msg}")
self.redirect(f"/login?error_msg={error_msg}")
return
# if they are disabled
@ -144,11 +141,9 @@ class PublicHandler(BaseHandler):
# self.clear_cookie("user_data")
self.clear_cookie("token")
if self.request.query:
self.redirect(
f"/public/login?error_msg={error_msg}&{self.request.query}"
)
self.redirect(f"/login?error_msg={error_msg}&{self.request.query}")
else:
self.redirect(f"/public/login?error_msg={error_msg}")
self.redirect(f"/login?error_msg={error_msg}")
return
login_result = self.helper.verify_pass(entered_password, user_data.password)
@ -187,13 +182,11 @@ class PublicHandler(BaseHandler):
user_data.user_id, "Tried to log in", 0, self.get_remote_ip()
)
if self.request.query:
self.redirect(
f"/public/login?error_msg={error_msg}&{self.request.query}"
)
self.redirect(f"/login?error_msg={error_msg}&{self.request.query}")
else:
self.redirect(f"/public/login?error_msg={error_msg}")
self.redirect(f"/login?error_msg={error_msg}")
else:
if self.request.query:
self.redirect("/public/login?" + self.request.query)
self.redirect("/login?" + self.request.query)
else:
self.redirect("/public/login")
self.redirect("/login")

View File

@ -331,7 +331,7 @@ class ServerHandler(BaseHandler):
return
if import_type == "import_jar":
if not self.helper.is_subdir(
if self.helper.is_subdir(
import_server_path, self.controller.project_root
):
self.redirect(

View File

@ -11,6 +11,7 @@ import tornado.escape
import tornado.locale
import tornado.httpserver
from app.classes.models.management import HelpersManagement
from app.classes.shared.console import Console
from app.classes.shared.helpers import Helpers
from app.classes.shared.main_controller import Controller
@ -110,10 +111,13 @@ class Webserver:
https_port = self.helper.get_setting("https_port")
debug_errors = self.helper.get_setting("show_errors")
cookie_secret = self.helper.get_setting("cookie_secret")
if cookie_secret is False:
try:
cookie_secret = HelpersManagement.get_cookie_secret()
except:
cookie_secret = False
if cookie_secret is False or cookie_secret == "":
cookie_secret = self.helper.random_string_generator(32)
HelpersManagement.set_cookie_secret(cookie_secret)
if not http_port:
http_port = 8000
@ -147,7 +151,6 @@ class Webserver:
}
handlers = [
(r"/", DefaultHandler, handler_args),
(r"/public/(.*)", PublicHandler, handler_args),
(r"/panel/(.*)", PanelHandler, handler_args),
(r"/server/(.*)", ServerHandler, handler_args),
(r"/ajax/(.*)", AjaxHandler, handler_args),
@ -168,6 +171,9 @@ class Webserver:
(r"/api/v1/users/delete_user", DeleteUser, handler_args),
# API Routes V2
*api_handlers(handler_args),
# Using this one at the end
# to catch all the other requests to Public Handler
(r"/(.*)", PublicHandler, handler_args),
]
app = tornado.web.Application(
@ -179,21 +185,14 @@ class Webserver:
xsrf_cookies=True,
autoreload=False,
log_function=self.log_function,
login_url="/public/login",
login_url="/login",
default_handler_class=PublicHandler,
static_handler_class=CustomStaticHandler,
serve_traceback=debug_errors,
)
http_handers = [
(r"/", HTTPHandler, handler_args),
(r"/public/(.*)", HTTPHandlerPage, handler_args),
(r"/panel/(.*)", HTTPHandlerPage, handler_args),
(r"/server/(.*)", HTTPHandlerPage, handler_args),
(r"/ajax/(.*)", HTTPHandlerPage, handler_args),
(r"/api/stats/servers", HTTPHandlerPage, handler_args),
(r"/api/stats/node", HTTPHandlerPage, handler_args),
(r"/ws", HTTPHandlerPage, handler_args),
(r"/upload", HTTPHandlerPage, handler_args),
(r"/(.+)", HTTPHandlerPage, handler_args),
]
http_app = tornado.web.Application(
http_handers,
@ -205,7 +204,7 @@ class Webserver:
autoreload=False,
log_function=self.log_function,
default_handler_class=HTTPHandler,
login_url="/public/login",
login_url="/login",
serve_traceback=debug_errors,
)

View File

@ -1,27 +0,0 @@
{
"http_port": 8000,
"https_port": 8443,
"language": "en_EN",
"cookie_expire": 30,
"cookie_secret": "random",
"apikey_secret": "random",
"show_errors": true,
"history_max_age": 7,
"stats_update_frequency": 30,
"delete_default_json": false,
"show_contribute_link": true,
"virtual_terminal_lines": 70,
"max_log_lines": 700,
"max_audit_entries": 300,
"disabled_language_files": [
"lol_EN.json",
""
],
"stream_size_GB": 1,
"keywords": [
"help",
"chunk"
],
"allow_nsfw_profile_pictures": false,
"enable_user_self_delete": false
}

View File

@ -1,5 +1,5 @@
{
"major": 4,
"minor": 0,
"sub": 20
"sub": 21
}

View File

@ -34,16 +34,18 @@
</div>
{% if data['user_data']['preparing'] %}
<span class="dropdown-item" id="support_progress"><i class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs', data['lang']) }}<br><br></span>
<span class="dropdown-item" id="support_progress"><div class="support_progress" style="height: 15px; width: 100%;">
<span class="dropdown-item" id="support_progress">
<div class="support_progress" style="height: 15px; width: 100%;">
<div class="progress-bar progress-bar-striped progress-bar-animated" id="logs_progress_bar" role="progressbar" style="width:0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
</div></span>
</div>
</span>
{% else %}
<a class="dropdown-item" id="support_logs"><i class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs', data['lang']) }}</i></a>
{% end %}
{% if data['superuser'] %}
<a class="dropdown-item" href="/panel/activity_logs"><i class="dropdown-item-icon mdi mdi-calendar-check-outline text-primary"></i>{{ translate('notify', 'activityLog', data['lang']) }}</a>
{% end %}
<a class="dropdown-item" href="/public/logout"><i class="dropdown-item-icon mdi mdi-power text-primary"></i>{{ translate('notify', 'logout', data['lang']) }}</a>
<a class="dropdown-item" href="/logout"><i class="dropdown-item-icon mdi mdi-power text-primary"></i>{{ translate('notify', 'logout', data['lang']) }}</a>
</div>
</li>
</ul>

View File

@ -0,0 +1,320 @@
{% extends ../base.html %}
{% block meta %}
{% end %}
{% block title %}Crafty Controller - {{ translate('panelConfig', 'pageTitle', data['lang']) }}{% end %}
{% block content %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/css/bootstrap-select.min.css">
<div class="content-wrapper">
<!-- Page Title Header Starts-->
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<h4 class="page-title">
{{ translate('panelConfig', 'title', data['lang']) }}
<br />
</h4>
</div>
</div>
</div>
<!-- Page Title Header Ends-->
<div class="row">
<div class="col-md-12 grid-margin">
<div class="card">
<div class="card-body">
{% if data['superuser'] %}
{% include "parts/crafty_config_list.html %}
{% end %}
<!-- Page Title Header Starts-->
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<!-- TODO: Translate the following -->
<h4 class="page-title">Config.json</h4>
</div>
</div>
</div>
<!-- Page Title Header Ends-->
<form id="config-form" class="forms-sample" method="post" action="/panel/config_json">
{% raw xsrf_form_html() %}
{% for item in data['config-json'].items() %}
{% if item[0] == "reset_secrets_on_next_boot" %}
<div class="form-group" style="background: rgba(243, 21, 6, 0.3); outline: 1px solid red; padding: 10px;">
{% else %}
<div class="form-group">
{% end %}
<label class="form" for="{{item[0]}}">{{item[0]}}
<small class="text-muted ml-1">
</small> </label><br />
{% if item[0] == 'language' %}
<select name="{{item[0]}}" class="form-control">
{% for lang in data['availables_languages'] %}
{% if lang == item[1] %}
<option selected>{{lang}}</option>
{% else %}
<option>{{lang}}</option>
{% end %}
{% end %}
</select>
{% elif item[0] == 'disabled_language_files' %}
<div class="input-group">
<button type="button" class="btn btn-outline-default custom-picker" onclick="$('option', $('#lang_select')).each(function(element) {
$(this).removeAttr('selected').prop('selected', false); $('.selectpicker').selectpicker('refresh')
});">Enable all Languages</button>
<select id="lang_select" class="form-control selectpicker show-tick" data-icon-base="fas" data-tick-icon="fa-check" multiple data-style="custom-picker">
{% for lang in data['all_languages'] %}
{% if lang in item[1] %}
<option selected>{{lang}}</option>
{% else %}
<option>{{lang}}</option>
{% end %}
{% end %}
</select>
<textarea id="disabled_lang" name="{{item[0]}}" class="form-control list hidden" rows="{{ len(data['all_languages']) }}" value="{{','.join(item[1])}}" hidden>{{','.join(item[1])}}</textarea>
</div>
{% elif isinstance(item[1], list) %}
<textarea value="{{','.join(item[1])}}" type="text" name="{{item[0]}}" class="form-control list">{{','.join(item[1])}}</textarea>
{% elif isinstance(item[1], bool) %}
<div style="margin-left: 30px;">
{% if item[1] == True %}
<input type="radio" class="form-check-input" name="{{item[0]}}" id="True" value="True" checked>
 <label for="True">True</label><br>
<input type="radio" class="form-check-input" name="{{item[0]}}" id="False" value="False">
 <label for="False">False</label>
{% else %}
<input type="radio" class="form-check-input" name="{{item[0]}}" id="True" value="True">
 <label for="True">True</label><br>
<input type="radio" class="form-check-input" name="{{item[0]}}" id="False" value="False" checked>
 <label for="False">False</label>
{% end %}
</div>
{% elif isinstance(item[1], int) %}
<input type="number" class="form-control" name="{{item[0]}}" id="{{item[0]}}" value="{{ item[1] }}" step="1" min="0" required>
{% else %}
<input type="text" class="form-control" name="{{item[0]}}" id="{{item[0]}}" value="{{ item[1] }}" step="2" min="0" required>
{% end %}
</div>
{% end %}
<button class="btn btn-success" type="submit">Submit</button>&nbsp;<span id="submit-status"></span>
</form>
</div>
</div>
</div>
</div>
</div>
<style>
.custom-picker {
border: 1px solid var(--outline);
}
.dropdown-menu.inner {
display: inline-block !important;
}
.popover-body {
color: white !important;
;
}
input[type="radio"] {
-ms-transform: scale(1.5);
/* IE 9 */
-webkit-transform: scale(1.5);
/* Chrome, Safari, Opera */
transform: scale(1.5);
}
</style>
<!-- content-wrapper ends -->
{% end %}
{% block js %}
<script>
$("#config-form").submit(function (e) {
let uuid = uuidv4();
var token = getCookie("_xsrf")
e.preventDefault();
$("#submit-status").html('<i class="fa fa-spinner fa-spin"></i>');
/* Convert multiple select to text list */
let selected_Lang = $('#lang_select').val();
$('#disabled_lang').val(selected_Lang);
let class_list = document.getElementsByClassName("list");
let form_json = convertFormToJSON($("#config-form"));
for (let i = 0; i < class_list.length; i++) {
let str = String($(class_list.item(i)).val())
form_json[$(class_list.item(i)).attr("name")] = uuid + "," + str.replace(/\s/g, '');
};
form_json['uuid'] = uuid;
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
dataType: "text",
url: '/panel/config_json',
data: form_json,
success: function (data) {
$("#submit-status").html('<i class="fa fa-check"></i>');
},
});
});
function uuidv4() {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
function convertFormToJSON(form) {
const array = $(form).serializeArray(); // Encodes the set of form elements as an array of names and values.
const json = {};
$.each(array, function () {
json[this.name] = this.value || "";
});
return json;
}
$(document).ready(function () {
$('[data-toggle="popover"]').popover();
if ($(window).width() < 1000) {
$('.too_small').popover("show");
$('.too_small2').popover("show");
}
});
$(window).ready(function () {
$('body').click(function () {
$('.too_small').popover("hide");
$('.too_small2').popover("hide");
});
});
$(window).resize(function () {
// This will execute whenever the window is resized
if ($(window).width() < 1000) {
$('.too_small').popover("show");
}
else {
$('.too_small').popover("hide");
} // New width
if ($(window).width() < 1000) {
$('.too_small2').popover("show");
}
else {
$('.too_small2').popover("hide");
} // New width
});
$('#file').change(function () {
console.log("File changed");
if ($('#file').val()) {
$('#upload-button').prop("disabled", false);
console.log("File changed good");
}
});
</script>
<script>
$(document).ready(function () {
console.log('ready for JS!');
$('.selectpicker').selectpicker("refresh");
});
$(".show_button").click(function () {
console.log("showing key");
api_key = $(this).attr("data-id");
bootbox.alert({
backdrop: true,
title: '',
message: api_key,
});
});
$('.clear-comm').click(function () {
var token = getCookie("_xsrf")
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/clear_comm',
success: function (data) {
},
});
})
$('.delete-photo').click(function () {
var token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val();
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/delete_photo?photo=' + photo,
success: function (data) {
location.reload();
},
});
})
$('.select-photo').click(function () {
var token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val();
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/select_photo?photo=' + photo,
success: function (data) {
window.location.reload();
},
});
})
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 = 'background'
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>';
setTimeout(function () {
window.location.reload();
}, 2000);
}
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);
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/js/bootstrap-select.min.js">
</script>
{% end %}

View File

@ -0,0 +1,391 @@
{% extends ../base.html %}
{% block meta %}
{% end %}
{% block title %}Crafty Controller - {{ translate('customLogin', 'pageTitle', data['lang']) }}{% end %}
{% block content %}
<div class="content-wrapper">
<!-- Page Title Header Starts-->
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<h4 class="page-title">
{{ translate('panelConfig', 'title', data['lang']) }}
<br />
</h4>
</div>
</div>
</div>
<!-- Page Title Header Ends-->
<div class="row">
<div class="col-md-12 grid-margin">
<div class="card">
<div class="card-body">
{% if data['superuser'] %}
{% include "parts/crafty_config_list.html %}
{% end %}
<!-- Page Title Header Starts-->
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<!-- TODO: Translate the following -->
<h4 class="page-title">{{ translate('customLogin', 'customLoginPage', data['lang']) }}</h4>
</div>
</div>
</div>
<!-- Page Title Header Ends-->
<div class="row">
<div class="col-md-12 col-lg-12 grid-margin stretch-card">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-12">
<h4>{{ translate('customLogin', 'loginImage', data['lang']) }}</h4>
<hr>
<form class="form-row" name="zip" method="post" class="server-wizard" onSubmit="wait_msg(true)">
{% raw xsrf_form_html() %}
<input type="hidden" value="import_zip" name="create_type">
<div class="col form-group">
<span id="upload_input"><input type="file" class="form-control-file" id="file" name="file"
multiple="false" required></span>
</div>
<div class="col form-group">
<button type="button" class="btn btn-info" id="upload-button" onclick="sendFile()"
disabled>UPLOAD</button>
</div>
</form>
<hr>
<hr />
</div>
<div class="col-12">
<div>
<h6>{{ translate('customLogin', 'preview', data['lang']) }}:</h6>
<form id="photo_form">
<div class="form-group row">
<label for="photo" class="col-sm-6 col-form-label">Selected Background Image</label>
<div class="col-sm-6">
<select class="form-select form-control form-control-lg select-css form-control-plaintext"
id="photo" name="photo" form="photo_form" onchange="updateBackgroundPreview()">
{% for image in data["backgrounds"] %}
<option value="{{image}}">{{image}}</option>
{% end %}
</select>
</div>
</div>
<div id="photo_loading" class="form-group" hidden>
<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>
</div>
<div class="form-group row">
<label class="col-sm-3" for="formControlRange">{{ translate('customLogin', 'loginOpacity',
data['lang']) }}</label>
<label class="col-sm-1" id="opacityValue">{{ data['login_opacity'] }}%</label>
<div class="range col-sm-8">
<input type="range" class="form-control-range" id="modal_opacity" name="modal_opacity"
onchange="previewOpacity()" min="0" max="100" value="{{ data['login_opacity'] }}">
</div>
</div>
<div id="login_preview" style="position: relative;">
<img id="bg-preview" src="../../static/assets/images/auth/{{ data['background'] }}"
class="img-fluid" alt="Responsive image">
<div id="login-form-preview">
<div id="login-form-background" class="auto-form-wrapper login-modal">
<div class="text-center auto-form-logo">
<img src="/static/assets/images/logo_long.svg">
</div>
<style>
#login-form-preview {
display: flex;
position: absolute;
overflow: hidden;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
max-width: 90%;
max-height: 90%;
}
.auto-form-wrapper {
background: rgb(34, 36, 55, 1);
padding: 2rem 2rem 0.5rem;
border-radius: 4px;
-webkit-box-shadow: 0 -25px 37.7px 11.3px rgb(8 143 220 / 7%);
box-shadow: 0 -25px 37.7px 11.3px rgb(8 143 220 / 7%);
color: #fff;
}
/*.auto-form-logo {
background: #222437;
padding: 0rem;
margin: 0.5rem 0rem;
border-radius: 0.2rem;
color: #fff;
}*/
.login-modal {
border-radius: 0.4rem !important;
box-shadow: 0 8px 12px 0 hsla(0, 0%, 0%, 0.2) !important;
}
.login-text-input {
border: none !important;
background-color: hsl(234, 30%, 45%);
color: var(--white) !important;
}
.login-text-input:hover,
.login-text-input:focus {
background-color: hsl(234, 30%, 39%) !important;
}
.login-input {
border-radius: 0.4rem !important;
box-shadow: 0 8px 12px 0 hsla(0, 0%, 0%, 0.2);
transition: all 0.3s ease-in-out;
}
.login-input:hover,
.login-input:focus {
box-shadow: 0 12px 16px 0 hsla(0, 0%, 0%, 0.4);
}
</style>
<div id="login_form_data">
<input type="hidden" name="_xsrf"
value="2|1d603267|809fb6bd82f677d440e484dde7c3a310|1671726040" disabled>
<div class="form-group">
<label class="label">Username</label>
<div class="input-group">
<input type="text" class="form-control login-text-input login-input"
placeholder="Username" name="username" id="username" required="true" disabled>
</div>
</div>
<div class="form-group">
<label class="label">Password</label>
<div class="input-group">
<input type="password" class="form-control login-text-input login-input"
placeholder="Password" name="password" id="password" required="true" disabled>
</div>
</div>
<div class="form-group">
<button class="login-input btn btn-primary submit-btn btn-block" disabled>Log
In</button>
</div>
<fieldset style="color: red; text-align: center;">
<span></span>
</fieldset>
<div class="form-group d-flex justify-content-between">
<div class="form-check form-check-flat mt-0">
&nbsp;
</div>
<a href="#" class="text-small forgot-password" disabled>Forgot Password</a>
</div>
<div class="text-block text-center my-3">
<span class="text-small font-weight-semibold"><a
href="https://craftycontrol.com/">Crafty Control
4.0.20</a> </span>
</div>
</div>
</div>
</div>
</div>
<div class="form-group">
<button class="btn btn-outline-success select-photo" type="button">{{
translate('customLogin',
'apply', data['lang']) }}</button>
<button class="btn btn-outline-danger delete-photo" type="button">{{
translate('customLogin',
'delete', data['lang']) }}</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.popover-body {
color: white !important;
;
}
</style>
<!-- content-wrapper ends -->
{% end %}
{% block js %}
<script>
$(document).ready(function () {
$('[data-toggle="popover"]').popover();
if ($(window).width() < 1000) {
$('.too_small').popover("show");
$('.too_small2').popover("show");
}
});
$(window).ready(function () {
$('body').click(function () {
$('.too_small').popover("hide");
$('.too_small2').popover("hide");
});
});
$(window).resize(function () {
// This will execute whenever the window is resized
if ($(window).width() < 1000) {
$('.too_small').popover("show");
}
else {
$('.too_small').popover("hide");
} // New width
if ($(window).width() < 1000) {
$('.too_small2').popover("show");
}
else {
$('.too_small2').popover("hide");
} // New width
});
$('#file').change(function () {
console.log("File changed");
if ($('#file').val()) {
$('#upload-button').prop("disabled", false);
console.log("File changed good");
}
});
</script>
<script>
$(document).ready(function () {
console.log('ready for JS!')
});
$(".show_button").click(function () {
console.log("showing key");
api_key = $(this).attr("data-id");
bootbox.alert({
backdrop: true,
title: '',
message: api_key,
});
});
$('.delete-photo').click(function () {
var token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val();
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/delete_photo?photo=' + encodeURIComponent(photo),
success: function (data) {
location.reload();
},
});
})
$('.select-photo').click(function () {
var token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val();
let opacity = $('#modal_opacity').val();
let enc_photo = encodeURIComponent(photo);
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/select_photo?photo=' + enc_photo + '&opacity=' + opacity,
success: function (data) {
window.location.reload();
},
});
})
$(document).ready(function () {
let opacity = parseInt($("#modal_opacity").val());
document.getElementById('login-form-background').style.background = 'rgb(34, 36, 55, ' + (opacity / 100) + ')';
});
function previewOpacity() {
let opacity = parseInt($("#modal_opacity").val())
console.debug("Selected Opacity = " + opacity + "%");
document.getElementById('opacityValue').innerHTML = (opacity) + "%";
document.getElementById('login-form-background').style.background = 'rgb(34, 36, 55, ' + (opacity / 100) + ')';
}
function updateBackgroundSelect() {
$("#photo").val($("#try_photo").val()).change();
}
function updateBackgroundPreview() {
var img = document.getElementById('bg-preview');
if ($("#photo").val() == "login_1.jpg") {
var src_path = "../../static/assets/images/auth/".concat($("#photo").val());
}
else {
var src_path = "../../static/assets/images/auth/custom/".concat($("#photo").val());
}
img.src = src_path;
}
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 = 'background'
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>';
setTimeout(function () {
window.location.reload();
}, 2000);
}
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);
}
</script>
{% end %}

View File

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
@ -20,6 +21,14 @@
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/images/logo_small.svg">
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
</head>
<style>
.auth.auth-bg-1 {
background: url("../../static/assets/images/auth/{% raw data['background'] %}"),
url("../../static/assets/images/auth/login-1.jpg");
background-size: cover;
}
</style>
<body class="dark-theme">
<div class="container-scroller">
<div class="container-fluid page-body-wrapper full-page-wrapper">
@ -33,11 +42,14 @@
<div class="col-sm-12 grid-margin stretch-card">
<div class="card card-statistics social-card google-card card-colored">
<div class="card-body">
<h4 class="platform-name mb-3 mt-4 font-weight-semibold user-name">{{ translate('accessDenied', 'accessDenied', data['lang']) }}</h4>
<h5 class="headline font-weight-medium">{{ translate('accessDenied', 'noAccess', data['lang']) }}</h5>
<h4 class="platform-name mb-3 mt-4 font-weight-semibold user-name">{{ translate('accessDenied',
'accessDenied', data['lang']) }}</h4>
<h5 class="headline font-weight-medium">{{ translate('accessDenied', 'noAccess', data['lang']) }}
</h5>
<p class="mb-2 comment font-weight-light">
{{ translate('accessDenied', 'contactAdmin', data['lang']) }}<br /><br />
<a class="d-inline font-weight-medium" href="https://discord.gg/9VJPhCE"> {{ translate('accessDenied', 'contact', data['lang']) }}</a>
<a class="d-inline font-weight-medium" href="https://discord.gg/9VJPhCE"> {{
translate('accessDenied', 'contact', data['lang']) }}</a>
</p>
</div>
</div>
@ -66,4 +78,5 @@
<script src="/static/assets/js/shared/todolist.js"></script>
<!-- endinject -->
</body>
</html>

View File

@ -8,6 +8,28 @@
{% block content %}
<div class="content-wrapper">
<!-- Page Title Header Starts-->
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<h4 class="page-title">
{{ translate('panelConfig', 'title', data['lang']) }}
<br />
</h4>
</div>
</div>
</div>
<!-- Page Title Header Ends-->
<div class="row">
<div class="col-md-12 grid-margin">
<div class="card">
<div class="card-body">
{% if data['superuser'] %}
{% include "parts/crafty_config_list.html %}
{% end %}
<!-- Page Title Header Starts-->
<div class="row page-title-header">
@ -21,11 +43,6 @@
</div>
<!-- Page Title Header Ends-->
<div class="row">
<div class="col-md-12 grid-margin">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-12 col-lg-12 grid-margin stretch-card">
<div class="card">
@ -33,9 +50,7 @@
<h4 class="card-title"><i class="fas fa-users"></i> {{ translate('panelConfig', 'users', data['lang'])
}}</h4>
{% if data['user_data']['hints'] %}
<span class="too_small" title="{{ translate('dashboard', 'cannotSee', data['lang']) }}" ,
data-content="{{ translate('dashboard', 'cannotSeeOnMobile2', data['lang']) }}" ,
data-placement="top"></span>
<span class="too_small" title="{{ translate('dashboard', 'cannotSee', data['lang']) }}" , data-content="{{ translate('dashboard', 'cannotSeeOnMobile2', data['lang']) }}" , data-placement="top"></span>
{% end %}
<!-- TODO: Translate the following -->
<div><a class="nav-link" href="/panel/add_user"><i class="fas fa-plus-circle"></i> &nbsp; {{
@ -133,9 +148,7 @@
<h4 class="card-title"><i class="fas fa-user-tag"></i> {{ translate('panelConfig', 'roles',
data['lang']) }}</h4>
{% if data['user_data']['hints'] %}
<span class="too_small2" title="{{ translate('dashboard', 'cannotSee', data['lang']) }}" ,
data-content="{{ translate('dashboard', 'cannotSeeOnMobile2', data['lang']) }}" ,
data-placement="top"></span>
<span class="too_small2" title="{{ translate('dashboard', 'cannotSee', data['lang']) }}" , data-content="{{ translate('dashboard', 'cannotSeeOnMobile2', data['lang']) }}" , data-placement="top"></span>
{% end %}
<div><a class="nav-link" href="/panel/add_role"><i class="fas fa-plus-circle"></i> &nbsp; {{
translate('panelConfig', 'newRole', data['lang']) }}</a></div>
@ -221,74 +234,6 @@
<h4 class="card-title"><i class="fas fa-user-tag"></i> {{ translate('panelConfig', 'adminControls',
data['lang']) }}</h4>
</div>
<div class="card-body">
<button type="button" class="btn btn-outline-danger clear-comm">{{ translate('panelConfig',
'clearComms', data['lang']) }}</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6 grid-margin stretch-card">
<div class="card">
<div class="card-body">
<h4>{{ translate('panelConfig', 'loginImage', 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">{{ translate('panelConfig', 'backgroundUpload', data['lang'])
}}</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" id="upload-button" onclick="sendFile()"
disabled>UPLOAD</button>
</span>
</div>
</div>
</div>
</div>
</div>
</form>
</p>
</div>
</div>
<div class="col-sm-6 grid-margin stretch-card">
<div class="card">
<div class="card-body">
<h4>{{ translate('panelConfig', 'loginBackground', data['lang']) }}</h4><br /><br><br />
<form id="photo_form">
<select class="form-select form-control form-control-lg select-css" id="photo" name="photo"
form="photo_form">
{% for image in data["backgrounds"] %}
<option value="{{image}}">{{image}}</option>
{% end %}
</select>
<div>
<br>
<h6>{{ translate('panelConfig', 'preview', data['lang']) }}:</h6>
<img style="width: 200px; height: 113px;"
src="../../static/assets/images/auth/{{ data['background'] }}">
</div>
<br />
<br />
<button class="btn btn-outline-success select-photo" type="button">{{ translate('panelConfig',
'select', data['lang']) }}</button>
<button class="btn btn-outline-danger delete-photo" type="button">{{ translate('panelConfig',
'delete', data['lang']) }}</button>
</form>
</div>
</div>
</div>
</div>
@ -350,97 +295,5 @@
}
});
</script>
<script>
$(document).ready(function () {
console.log('ready for JS!')
});
$(".show_button").click(function () {
console.log("showing key");
api_key = $(this).attr("data-id");
bootbox.alert({
backdrop: true,
title: '',
message: api_key,
});
});
$('.clear-comm').click(function () {
var token = getCookie("_xsrf")
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/clear_comm',
success: function (data) {
},
});
})
$('.delete-photo').click(function () {
var token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val();
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/delete_photo?photo=' + photo,
success: function (data) {
location.reload();
},
});
})
$('.select-photo').click(function () {
var token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val();
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/select_photo?photo=' + photo,
success: function (data) {
window.location.reload();
},
});
})
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 = 'background'
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>';
setTimeout(function () {
window.location.reload();
}, 2000);
}
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);
}
</script>
{% end %}

View File

@ -0,0 +1,14 @@
<ul class="nav nav-tabs col-md-12 tab-simple-styled" role="tablist" style="margin-top: 0;">
<li class="nav-item term-nav-item">
<a class="nav-link {% if data['active_link'] == 'panel_config' %}active{% end %}" href="/panel/panel_config" role="tab" aria-selected="false">
<i class="fas fa-file-signature"></i>Panel Config</a>
</li>
<li class="nav-item term-nav-item">
<a class="nav-link {% if data['active_link'] == 'config_json' %}active{% end %}" href="/panel/config_json" role="tab" aria-selected="false">
<i class="fas fa-file-signature"></i>Config.json</a>
</li>
<li class="nav-item term-nav-item">
<a class="nav-link {% if data['active_link'] == 'custom_login' %}active{% end %}" href="/panel/custom_login" role="tab" aria-selected="false">
<i class="fas fa-file-signature"></i>Custom Login</a>
</li>
</ul>

View File

@ -107,6 +107,40 @@
translate('serverBackups', 'shutdown', data['lang']) }}
{% end %}
</div>
<div class="form-group">
<label for="command-check" class="form-check-label ml-4 mb-4"></label>
{% if data['backup_config']['before'] %}
<input type="checkbox" class="form-check-input" id="before-check" name="before-check" checked>Run
Command Before Backup
<br>
<input type="text" class="form-control" name="backup_before" id="backup_before"
value="{{ data['backup_config']['before'] }}" placeholder="We enter the / for you"
style="display: inline-block;">
{% else %}
<input type="checkbox" class="form-check-input" id="before-check" name="before-check">Run Command
Before Backup
<br>
<input type="text" class="form-control" name="backup_before" id="backup_before" value=""
placeholder="We enter the / for you." style="display: none;">
{% end %}
</div>
<div class="form-group">
<label for="command-check" class="form-check-label ml-4 mb-4"></label>
{% if data['backup_config']['after'] %}
<input type="checkbox" class="form-check-input" id="after-check" name="after-check" checked>Run
Command After Backup
<br>
<input type="text" class="form-control" name="backup_after" id="backup_after"
value="{{ data['backup_config']['after'] }}" placeholder="We enter the / for you"
style="display: inline-block;">
{% else %}
<input type="checkbox" class="form-check-input" id="after-check" name="after-check">Run Command
Before Backup
<br>
<input type="text" class="form-control" name="backup_after" id="backup_after" value=""
placeholder="We enter the / for you." style="display: none;">
{% end %}
</div>
<div class="form-group">
<label for="server">{{ translate('serverBackups', 'exclusionsTitle', data['lang']) }} <small> - {{
translate('serverBackups', 'excludedChoose', data['lang']) }}</small></label>
@ -344,6 +378,22 @@
});
}
$("#before-check").on("click", function () {
if ($("#before-check:checked").val()) {
$("#backup_before").css("display", "inline-block");
} else {
$("#backup_before").css("display", "none");
$("#backup_before").val("");
}
});
$("#after-check").on("click", function () {
if ($("#after-check:checked").val()) {
$("#backup_after").css("display", "inline-block");
} else {
$("#backup_after").css("display", "none");
$("#backup_after").val("");
}
});
$(document).ready(function () {
try {

View File

@ -697,7 +697,7 @@
});
}
function sendFile(file, path, serverId, left, onProgress) {
async function sendFile(file, path, serverId, left, i, onProgress) {
let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf")
let fileName = file.name
@ -719,7 +719,36 @@
onProgress(Math.floor(event.loaded / event.total * 100)), false);
xmlHttpRequest.addEventListener('load', (event) => {
if (event.target.responseText == 'success') {
console.log('Upload for file', file.name, 'was successful!')
console.log('Upload for file', file.name, 'was successful!');
let caught = false;
try {
var par_el = document.getElementById(path + "ul");
var items = par_el.children;
} catch {
caught = true;
var par_el = document.getElementById("files-tree");
var items = par_el.children;
}
let name = file.name;
console.log(par_el)
let full_path = path + '/' + name
let flag = false;
for (var k = 0; k < items.length; ++k) {
if ($(items[k]).attr("data-name") == name) {
flag = true;
}
}
if (!flag) {
if (caught) {
$(par_el).append('<li id=' + '"' + full_path.toString() + 'li' + '"' + 'class="d-block tree-ctx-item tree-file tree-item" data-path=' + '"' + full_path.toString() + '"' + ' data-name=' + '"' + name.toString() + '"' + ' onclick="clickOnFile(event)" ><span style="margin-right: 6px;"><i class="far fa-file"></i></span>' + name + '</li>');
} else {
$(par_el).append('<li id=' + '"' + full_path.toString() + 'li' + '"' + 'class="tree-ctx-item tree-file tree-item" data-path=' + '"' + full_path.toString() + '"' + ' data-name=' + '"' + name.toString() + '"' + ' onclick="clickOnFile(event)" ><span style="margin-right: 6px;"><i class="far fa-file"></i></span>' + name + '</li>');
}
setTreeViewContext();
}
$(`#upload-progress-bar-${i + 1}`).removeClass("progress-bar-striped");
$(`#upload-progress-bar-${i + 1}`).addClass("bg-success");
$(`#upload-progress-bar-${i + 1}`).html('<i style="color: black;" class="fas fa-box-check"></i>')
}
else {
alert('Upload failed with response: ' + event.target.responseText);
@ -735,7 +764,7 @@
let uploadWaitDialog;
let doUpload = true;
function uploadFilesE(event) {
async function uploadFilesE(event) {
path = event.target.parentElement.getAttribute('data-path');
$(function () {
var uploadHtml = "<div>" +
@ -795,7 +824,7 @@
`;
$('#upload-progress-bar-parent').append(progressHtml);
sendFile(files.files[i], path, serverId, nFiles - i - 1, (progress) => {
await sendFile(files.files[i], path, serverId, nFiles - i - 1, i, (progress) => {
$(`#upload-progress-bar-${i + 1}`).attr('aria-valuenow', progress)
$(`#upload-progress-bar-${i + 1}`).css('width', progress + '%')
});
@ -996,7 +1025,6 @@
function hideUploadBox() {
if (!uploadWaitDialog) return;
uploadWaitDialog.modal('hide');
getTreeView();
}
function createFileE(event) {
@ -1074,7 +1102,8 @@
callback: function (result) {
if (!result) return;
deleteFile(path, function () {
getTreeView()
el = document.getElementById(path + "li");
$(el).remove();
document.getElementById('files-tree-nav').style.display = 'none';
});
}
@ -1102,7 +1131,8 @@
callback: function (result) {
if (!result) return;
deleteDir(path, function () {
getTreeView()
el = document.getElementById(path + "li");
$(el).remove();
document.getElementById('files-tree-nav').style.display = 'none';
});
}

View File

@ -23,7 +23,7 @@
</head>
<style>
.auth.auth-bg-1 {
background: url("../../static/assets/images/auth/{{data['background']}}"),
background: url("../../static/assets/images/auth/{% raw data['background'] %}"),
url("../../static/assets/images/auth/login-1.jpg");
background-size: cover;
}

View File

@ -23,7 +23,7 @@
</head>
<style>
.auth.auth-bg-1 {
background: url("../../static/assets/images/auth/{{data['background']}}"),
background: url("../../static/assets/images/auth/{% raw data['background'] %}"),
url("../../static/assets/images/auth/login-1.jpg");
background-size: cover;
}

View File

@ -23,9 +23,10 @@
</head>
<style>
.auth.auth-bg-1 {
background: url("../../static/assets/images/auth/{{data['background']}}"),
background: url("../../static/assets/images/auth/{% raw data['background'] %}"),
url("../../static/assets/images/auth/login-1.jpg");
background-size: cover;
background-position: center;
}
</style>
@ -36,8 +37,9 @@
<div class="row w-100">
<div class="col-lg-4 mx-auto">
<div class="auto-form-wrapper login-modal">
<div class="text-center">
<div id="login-form-background" class="auto-form-wrapper login-modal">
<div id="login_opacity" data-value="{{ data['login_opacity'] }}" hidden></div>
<div class="text-center auto-form-logo">
<img src="/static/assets/images/logo_long.svg">
</div>
<style>
@ -69,9 +71,9 @@
}
</style>
{% if data['query'] %}
<form action="/public/login?{{ data['query'] }}" method="post">
<form action="/login?{{ data['query'] }}" method="post">
{% else %}
<form action="/public/login" method="post">
<form action="/login" method="post">
{% end %}
{% raw xsrf_form_html() %}
<div class="form-group">
@ -133,6 +135,13 @@
<script src="/static/assets/js/shared/settings.js"></script>
<script src="/static/assets/js/shared/todolist.js"></script>
<!-- endinject -->
<script>
$(document).ready(function () {
let login_opacity_div = document.getElementById('login_opacity');
let opacity = login_opacity_div.getAttribute('data-value');
document.getElementById('login-form-background').style.background = 'rgb(34, 36, 55, ' + (opacity / 100) + ')';
});
</script>
</body>
</html>

View File

@ -9,7 +9,7 @@
<!-- View for Large screen -->
<style>
.auth.auth-bg-1 {
background: url("../../static/assets/images/auth/{{data['background']}}"),
background: url("../../static/assets/images/auth/{% raw data['background'] %}"),
url("../../static/assets/images/auth/login-1.jpg");
background-size: cover;
}

View File

@ -9,7 +9,7 @@
<!-- <img src="/static/assets/images/logo_long.svg">-->
{{ _('Configure Your Existing Server') }}<br /><br />
</div>
<form action="/public/login" method="post">
<form action="/login" method="post">
{% raw xsrf_form_html() %}
<div class="form-group">

View File

@ -0,0 +1,18 @@
# Generated by database migrator
import peewee
def migrate(migrator, database, **kwargs):
migrator.add_columns(
"crafty_settings", login_opacity=peewee.IntegerField(default=100)
)
"""
Write your migrations here.
"""
def rollback(migrator, database, **kwargs):
migrator.drop_columns("crafty_settings", ["login_opacity"])
"""
Write your rollback migrations here.
"""

View File

@ -0,0 +1,35 @@
# Generated by database migrator
import datetime
from peewee import *
from app.classes.models.users import Users
from app.classes.models.servers import Servers
def migrate(migrator, database, **kwargs):
migrator.drop_table("commands")
"""
Write your migrations here.
"""
def rollback(migrator, database, **kwargs):
db = database
class Commands(Model):
command_id = AutoField()
created = DateTimeField(default=datetime.datetime.now)
server_id = ForeignKeyField(Servers, backref="server", index=True)
user = ForeignKeyField(Users, backref="user", index=True)
source_ip = CharField(default="127.0.0.1")
command = CharField(default="")
executed = BooleanField(default=False)
class Meta:
table_name = "commands"
database = db
migrator.create_table(Commands)
"""
Write your rollback migrations here.
"""

View File

@ -0,0 +1,18 @@
# Generated by database migrator
import peewee
def migrate(migrator, database, **kwargs):
migrator.add_columns("backups", before=peewee.CharField(default=""))
migrator.add_columns("backups", after=peewee.CharField(default=""))
"""
Write your migrations here.
"""
def rollback(migrator, database, **kwargs):
migrator.drop_columns("backups", ["before"])
migrator.drop_columns("backups", ["after"])
"""
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("crafty_settings", cookie_secret=peewee.CharField(default=""))
"""
Write your migrations here.
"""
def rollback(migrator, database, **kwargs):
migrator.drop_columns("crafty_settings", ["cookie_secret"])
"""
Write your rollback migrations here.
"""

View File

@ -213,6 +213,8 @@
"assignedRoles": "Assigned Roles",
"cancel": "Cancel",
"clearComms": "Clear Un-executed Commands",
"select": "Select",
"apply": "Apply",
"delete": "Delete",
"edit": "Edit",
"enabled": "Enabled",
@ -228,12 +230,20 @@
"superConfirmTitle": "Enable superuser? Are you sure?",
"user": "User",
"users": "Users",
"title": "Crafty Configuration"
},
"customLogin": {
"customLoginPage": "Customise the Login Page",
"loginImage": "Upload a background image for the login screen.",
"backgroundUpload": "Background Upload",
"loginBackground": "Login Background Image",
"loginOpacity": "Select the Login Window Opacity",
"select": "Select",
"apply": "Apply",
"delete": "Delete",
"selectImage": "Select an image",
"preview": "Preview"
"preview": "Preview",
"pageTitle": "Custom Login Page"
},
"rolesConfig": {
"config": "Role Config",

View File

@ -18,16 +18,6 @@ if [ ! "$(ls -A --ignore=.gitkeep ./app/config)" ]; then
else
# Keep version file up to date with image
cp -f ./app/config_original/version.json ./app/config/version.json
# Compare if user's config is different from image, and show differences.
echo "\033[36mWrapper | \033[35m🏗 Checking for config.json changes..."
cp -f ./app/config_original/config.json ./app/config/config_image_template
if [ "$(diff -q ./app/config/config.json ./app/config/config_image_template)" ]; then
echo "\033[36mWrapper | \033[33m👷 We've found differences in your local config, please review!,"
echo "\033[36m | \033[33m (This could be an outdated config.json)"
else
echo "\033[36mWrapper | \033[32m✅ Config good! Proceeding..."
fi
fi

20
main.py
View File

@ -14,6 +14,7 @@ from app.classes.shared.import3 import Import3
from app.classes.shared.console import Console
from app.classes.shared.helpers import Helpers
from app.classes.models.users import HelperUsers
from app.classes.models.management import HelpersManagement
from app.classes.shared.import_helper import ImportHelpers
console = Console()
@ -53,6 +54,9 @@ def do_intro():
"""
Console.magenta(intro)
if not helper.check_file_exists(helper.settings_file):
Console.debug("No settings file detected. Creating one.")
helper.set_settings(Helpers.get_master_config())
def setup_logging(debug=True):
@ -121,7 +125,8 @@ if __name__ == "__main__":
# do our installer stuff
user_helper = HelperUsers(database, helper)
installer = DatabaseBuilder(database, helper, user_helper)
management_helper = HelpersManagement(database, helper)
installer = DatabaseBuilder(database, helper, user_helper, management_helper)
FRESH_INSTALL = installer.is_fresh_install()
if FRESH_INSTALL:
@ -135,10 +140,22 @@ if __name__ == "__main__":
installer.default_settings()
else:
Console.debug("Existing install detected")
Console.info("Checking for reset secret flag")
if helper.get_setting("reset_secrets_on_next_boot"):
Console.info("Found Reset")
management_helper.set_secret_api_key(str(helper.random_string_generator(64)))
management_helper.set_cookie_secret(str(helper.random_string_generator(32)))
helper.set_setting("reset_secrets_on_next_boot", False)
else:
Console.info("No flag found. Secrets are staying")
file_helper = FileHelpers(helper)
import_helper = ImportHelpers(helper, file_helper)
# now the tables are created, we can load the tasks_manager and server controller
controller = Controller(database, helper, file_helper, import_helper)
Console.info("Checking for remote changes to config.json")
controller.get_config_diff()
Console.info("Remote change complete.")
import3 = Import3(helper, controller)
tasks_manager = TasksManager(helper, controller)
tasks_manager.start_webserver()
@ -222,7 +239,6 @@ if __name__ == "__main__":
Console.debug(f"Execution Mode: {running_mode}")
Console.debug(f"Application path : '{application_path}'")
controller.clear_unexecuted_commands()
controller.clear_support_status()
crafty_prompt = MainPrompt(