Merge branch 'dev' into refactor/server_ids

This commit is contained in:
Zedifus 2023-12-13 02:52:02 +00:00
commit a564fac8cd
53 changed files with 1002 additions and 668 deletions

1
.gitignore vendored
View File

@ -37,3 +37,4 @@ app/config/
docker/* docker/*
!docker/docker-compose.yml !docker/docker-compose.yml
lang_sort_log.txt lang_sort_log.txt
lang_sort.txt

72
.gitlab/scripts/sort.py Normal file
View File

@ -0,0 +1,72 @@
import json
import os
def get_missing_keys_and_values(obj1, obj2, path=None):
if path is None:
path = []
missing_keys_and_values = {}
if isinstance(obj1, dict) and isinstance(obj2, dict):
for key in obj1:
if key not in obj2:
missing_keys_and_values[key] = obj1[key]
elif isinstance(obj1[key], (dict, list)) and isinstance(
obj2[key], (dict, list)
):
sub_missing = get_missing_keys_and_values(
obj1[key], obj2[key], path + [key]
)
if sub_missing:
missing_keys_and_values[key] = sub_missing
return missing_keys_and_values
def main():
project_dir = os.getcwd()
os.chdir("../../app/translations") # Change the working directory
dir_path = os.getcwd() # Get the current working directory
en_en_path = os.path.join(dir_path, "en_EN.json")
if not os.path.isfile(en_en_path):
print(
f"The file en_EN.json does not exist in {dir_path}. Ensure you have the right directory, Exiting."
)
return
result = {} # JSON object to store missing keys and values
for root, _, files in os.walk(dir_path):
for file in files:
if (
"_incomplete" not in file
and file != "en_EN.json"
and file.endswith(".json")
):
file_path = os.path.join(root, file)
with open(file_path, "r", encoding="utf-8") as current_file:
current_data = json.load(current_file)
with open(en_en_path, "r", encoding="utf-8") as en_en_file:
en_en_data = json.load(en_en_file)
missing_keys_and_values = get_missing_keys_and_values(
en_en_data, current_data
)
if missing_keys_and_values:
result[file] = missing_keys_and_values
# Write the JSON object to lang_sort.txt
with open(
os.path.join(project_dir, "lang_sort.txt"),
"w",
encoding="utf-8",
) as output_file:
json.dump(result, output_file, indent=4)
main()

View File

@ -1,9 +1,53 @@
# Changelog # Changelog
## --- [4.2.1] - 2023/TBD ## --- [4.2.3] - 2023/TBD
### New features ### New features
TBD TBD
### Bug fixes ### Bug fixes
TBD
### Tweaks
TBD
### Lang
TBD
<br><br>
## --- [4.2.2] - 2023/12/13
### New features
- Loading Screen for Crafty during startup ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/668))
### Refactor
- Remove deprecated API V1 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/670))
- Tidy up main.py to be more comprehensive ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/668))
- Force random password on first run. Stop using common default password ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/672) | [Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/673))
### Bug fixes
- Remove webhook `custom` option from webook provider list as it's not currently an option ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/664))
- Bump cryptography for CVE-2023-49083 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/680))
- Fix bug where su cannot edit general user password ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/676))
- Fix bug where no file error on import root dir ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/677))
- Fix Unban button failing to pardon users ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/671))
- Fix stack in API error handling ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/674))
- Fix bug where you cannot select "do not monitor mounts" from `config.json` ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/678))
- Fix support log 'x' button still downloading logs ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/679))
- Fix bug where servers are created without bu dir ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/682))
### Tweaks
- Homogenize Panel logos/branding ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/666))
- Retain previous tab when revisiting server details page (#272)([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/667))
- Add server name tag in panel header (#272)([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/667))
- Setup logging for panel authentication attempts ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/669))
- Update minimum password length from 6 to 8, and unrestrict maximum password length ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/669))
- Give better feedback when backup delete fails ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/681))
- Add user queue debug logging ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/683))
### Lang
- Update `de_DE, en_EN, fr_FR, lol_EN, lv_LV, nl_BE, pl_PL, zh_CN` translations for `4.2.2` ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/684))
- Mark `es_ES` as incomplete ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/684))
- Mark `he_IL` as active ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/684))
- pl_PL Minor fixes ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/675))
<br><br>
## --- [4.2.1] - 2023/11/01
### Bug fixes
- Fix logic issue with `get_files` API permissions check ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/654)) - Fix logic issue with `get_files` API permissions check ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/654))
- Fix notifications not showing up/being reset #298 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/660))
- Fix users not being able to be deleted since the prompt fails to display ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/661))
- Fix duplicate function naming on dashboard ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/662))
### Tweaks ### Tweaks
- Auto refresh Crafty Announcements on 30m interval ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/653)) - Auto refresh Crafty Announcements on 30m interval ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/653))
- Improve Crafty toggle buttons and Webhooks page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/656)) - Improve Crafty toggle buttons and Webhooks page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/656))
@ -12,6 +56,9 @@ TBD
- Update `es_ES` lang file ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/655)) - Update `es_ES` lang file ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/655))
- Clean up wording in `pl_PL` lang file ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/656)) - Clean up wording in `pl_PL` lang file ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/656))
- Add `de_DE`, `es_ES` `fr_FR`, `lol_EN`, `lv_LV`, `nl_BE` `pl_PL` & `zh_CN` translations for !656 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/656)) - Add `de_DE`, `es_ES` `fr_FR`, `lol_EN`, `lv_LV`, `nl_BE` `pl_PL` & `zh_CN` translations for !656 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/656))
### Docs
- [(New) Server Webhook Documentation](https://docs.craftycontrol.com/pages/user-guide/webhooks/)
- [(Edit) Image Context in Windows Service - Install steps, with slight wording improvement](https://docs.craftycontrol.com/pages/getting-started/installation/windows/#install-steps)
<br><br> <br><br>
## --- [4.2.0] - 2023/10/18 ## --- [4.2.0] - 2023/10/18

View File

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

View File

@ -22,6 +22,7 @@ from app.classes.models.server_permissions import (
PermissionsServers, PermissionsServers,
EnumPermissionsServer, EnumPermissionsServer,
) )
from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -36,6 +37,8 @@ class ServersController(metaclass=Singleton):
self.management_helper = management_helper self.management_helper = management_helper
self.servers_list = [] self.servers_list = []
self.stats = Stats(self.helper, self) self.stats = Stats(self.helper, self)
self.web_sock = WebSocketManager()
self.server_subpage = {}
# ********************************************************************************** # **********************************************************************************
# Generic Servers Methods # Generic Servers Methods
@ -169,8 +172,15 @@ class ServersController(metaclass=Singleton):
def init_all_servers(self): def init_all_servers(self):
servers = self.get_all_defined_servers() servers = self.get_all_defined_servers()
self.failed_servers = [] self.failed_servers = []
for server in servers: for server in servers:
self.web_sock.broadcast_to_admins(
"update",
{"section": "server", "server": server["server_name"]},
)
self.web_sock.broadcast_to_non_admins(
"update",
{"section": "init"},
)
server_id = server.get("server_id") server_id = server.get("server_id")
# if we have already initialized this server, let's skip it. # if we have already initialized this server, let's skip it.

View File

@ -45,8 +45,7 @@ class UsersController:
}, },
"password": { "password": {
"type": "string", "type": "string",
"maxLength": 20, "minLength": 8,
"minLength": 6,
"examples": ["crafty"], "examples": ["crafty"],
"title": "Password", "title": "Password",
}, },

View File

@ -80,6 +80,7 @@ class Helpers:
self.translation = Translation(self) self.translation = Translation(self)
self.update_available = False self.update_available = False
self.ignored_names = ["crafty_managed.txt", "db_stats"] self.ignored_names = ["crafty_managed.txt", "db_stats"]
self.crafty_starting = False
@staticmethod @staticmethod
def auto_installer_fix(ex): def auto_installer_fix(ex):
@ -361,6 +362,42 @@ class Helpers:
return result_of_check == 0 return result_of_check == 0
def create_pass(self):
# Maximum length of password needed
max_len = 64
# Declare string of the character that we need in our password
digits = string.digits
locase = string.ascii_lowercase
upcase = string.ascii_uppercase
symbols = "!@#$%^&*" # Reducing to avoid issues with ([]{}<>,'`) etc
# Combine all the character strings above to form one string
combo = digits + upcase + locase + symbols
# Randomly select at least one character from each character set above
rand_digit = secrets.choice(digits)
rand_upper = secrets.choice(upcase)
rand_lower = secrets.choice(locase)
rand_symbol = secrets.choice(symbols)
# Combine the character randomly selected above
temp_pass = rand_digit + rand_upper + rand_lower + rand_symbol
# Fill the rest of the password length by selecting randomly char list
for _ in range(max_len - 4):
temp_pass += secrets.choice(combo)
# Shuffle the temporary password to prevent predictable patterns
temp_pass_list = list(temp_pass)
secrets.SystemRandom().shuffle(temp_pass_list)
# Form the password by concatenating the characters
password = "".join(temp_pass_list)
# Return completed password
return password
@staticmethod @staticmethod
def cmdparse(cmd_in): def cmdparse(cmd_in):
# Parse a string into arguments # Parse a string into arguments
@ -578,16 +615,19 @@ class Helpers:
return version_data return version_data
def get_announcements(self): def get_announcements(self):
data = []
try: try:
data = []
response = requests.get("https://craftycontrol.com/notify", timeout=2) response = requests.get("https://craftycontrol.com/notify", timeout=2)
data = json.loads(response.content) data = json.loads(response.content)
if self.update_available:
data.append(self.update_available)
return data
except Exception as e: except Exception as e:
logger.error(f"Failed to fetch notifications with error: {e}") logger.error(f"Failed to fetch notifications with error: {e}")
if self.update_available:
if self.update_available: data = [self.update_available]
data.append(self.update_available) else:
return data return False
def get_version_string(self): def get_version_string(self):
version_data = self.get_version() version_data = self.get_version()

View File

@ -78,6 +78,37 @@ class Controller:
self.first_login = False self.first_login = False
self.cached_login = self.management.get_login_image() self.cached_login = self.management.get_login_image()
self.support_scheduler.start() self.support_scheduler.start()
try:
with open(
os.path.join(os.path.curdir, "logs", "auth_tracker.log"),
"r",
encoding="utf-8",
) as f:
self.auth_tracker = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
self.auth_tracker = {}
def log_attempt(self, remote_ip, username):
remote = self.auth_tracker.get(str(remote_ip), None)
if remote:
remote["names"].append(username)
remote["attempts"] += 1
remote["times"].append(datetime.now().strftime("%d/%m/%Y %H:%M:%S"))
self.auth_tracker[str(remote_ip)] = remote
else:
self.auth_tracker[str(remote_ip)] = {
"names": [username],
"attempts": 1,
"times": [datetime.now().strftime("%d/%m/%Y %H:%M:%S")],
}
def write_auth_tracker(self):
with open(
os.path.join(os.path.curdir, "logs", "auth_tracker.log"),
"w",
encoding="utf-8",
) as f:
json.dump(self.auth_tracker, f, indent=4)
@staticmethod @staticmethod
def check_system_user(): def check_system_user():
@ -498,6 +529,10 @@ class Controller:
server_host=monitoring_host, server_host=monitoring_host,
server_type=monitoring_type, server_type=monitoring_type,
) )
self.management.set_backup_config(
new_server_id,
backup_path,
)
if data["create_type"] == "minecraft_java": if data["create_type"] == "minecraft_java":
if root_create_data["create_type"] == "download_jar": if root_create_data["create_type"] == "download_jar":
# modded update urls from server jars will only update the installer # modded update urls from server jars will only update the installer

View File

@ -14,13 +14,17 @@ class DatabaseBuilder:
self.management_helper = management_helper self.management_helper = management_helper
self.users_helper = users_helper self.users_helper = users_helper
def default_settings(self): def default_settings(self, password="crafty"):
logger.info("Fresh Install Detected - Creating Default Settings") logger.info("Fresh Install Detected - Creating Default Settings")
Console.info("Fresh Install Detected - Creating Default Settings") Console.info("Fresh Install Detected - Creating Default Settings")
default_data = self.helper.find_default_password() default_data = self.helper.find_default_password()
if password not in default_data:
Console.help(
"No default password found. Using password created "
"by Crafty. Find it in app/config/default-creds.txt"
)
username = default_data.get("username", "admin") username = default_data.get("username", "admin")
password = default_data.get("password", "crafty") password = default_data.get("password", password)
self.users_helper.add_user( self.users_helper.add_user(
username=username, username=username,

View File

@ -23,6 +23,7 @@ from app.classes.web.tornado_handler import Webserver
from app.classes.shared.websocket_manager import WebSocketManager from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger("apscheduler") logger = logging.getLogger("apscheduler")
command_log = logging.getLogger("cmd_queue")
scheduler_intervals = { scheduler_intervals = {
"seconds", "seconds",
"minutes", "minutes",
@ -94,7 +95,15 @@ class TasksManager:
def command_watcher(self): def command_watcher(self):
while True: while True:
# select any commands waiting to be processed # select any commands waiting to be processed
command_log.debug(
"Queue currently has "
f"{self.controller.management.command_queue.qsize()} queued commands."
)
if not self.controller.management.command_queue.empty(): if not self.controller.management.command_queue.empty():
command_log.info(
"Current queued commands: "
f"{list(self.controller.management.command_queue.queue)}"
)
cmd = self.controller.management.command_queue.get() cmd = self.controller.management.command_queue.get()
try: try:
svr = self.controller.servers.get_server_instance_by_id( svr = self.controller.servers.get_server_instance_by_id(
@ -201,6 +210,13 @@ class TasksManager:
id="update_watcher", id="update_watcher",
start_date=datetime.datetime.now(), start_date=datetime.datetime.now(),
) )
self.scheduler.add_job(
self.controller.write_auth_tracker,
"interval",
minutes=5,
id="auth_tracker_write",
start_date=datetime.datetime.now(),
)
# self.scheduler.add_job( # self.scheduler.add_job(
# self.scheduler.print_jobs, "interval", seconds=10, id="-1" # self.scheduler.print_jobs, "interval", seconds=10, id="-1"
# ) # )

View File

@ -37,7 +37,15 @@ class WebSocketManager(metaclass=Singleton):
def broadcast_to_admins(self, event_type: str, data): def broadcast_to_admins(self, event_type: str, data):
def filter_fn(client): def filter_fn(client):
if client.get_user_id in HelperUsers.get_super_user_list(): if str(client.get_user_id()) in str(HelperUsers.get_super_user_list()):
return True
return False
self.broadcast_with_fn(filter_fn, event_type, data)
def broadcast_to_non_admins(self, event_type: str, data):
def filter_fn(client):
if str(client.get_user_id()) not in str(HelperUsers.get_super_user_list()):
return True return True
return False return False

View File

@ -1,446 +0,0 @@
from datetime import datetime
import logging
import re
from app.classes.controllers.crafty_perms_controller import EnumPermissionsCrafty
from app.classes.controllers.server_perms_controller import EnumPermissionsServer
from app.classes.web.base_handler import BaseHandler
from app.classes.models.management import DatabaseShortcuts
logger = logging.getLogger(__name__)
bearer_pattern = re.compile(r"^Bearer", flags=re.IGNORECASE)
class ApiHandler(BaseHandler):
def return_response(self, status: int, data: dict):
# Define a standardized response
self.set_status(status)
self.write(data)
def check_xsrf_cookie(self):
# Disable CSRF protection on API routes
pass
def access_denied(self, user, reason=""):
if reason:
reason = " because " + reason
logger.info(
"User %s from IP %s was denied access to the API route "
+ self.request.path
+ reason,
user,
self.get_remote_ip(),
)
self.finish(
self.return_response(
403,
{
"error": "ACCESS_DENIED",
"info": "You were denied access to the requested resource",
},
)
)
def authenticate_user(self) -> bool:
self.permissions = {
"Commands": EnumPermissionsServer.COMMANDS,
"Terminal": EnumPermissionsServer.TERMINAL,
"Logs": EnumPermissionsServer.LOGS,
"Schedule": EnumPermissionsServer.SCHEDULE,
"Backup": EnumPermissionsServer.BACKUP,
"Files": EnumPermissionsServer.FILES,
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
"Server_Creation": EnumPermissionsCrafty.SERVER_CREATION,
"User_Config": EnumPermissionsCrafty.USER_CONFIG,
"Roles_Config": EnumPermissionsCrafty.ROLES_CONFIG,
}
try:
logger.debug("Searching for specified token")
api_token = self.get_argument("token", "")
self.api_token = api_token
if api_token is None and self.request.headers.get("Authorization"):
api_token = bearer_pattern.sub(
"", self.request.headers.get("Authorization")
)
elif api_token is None:
api_token = self.get_cookie("token")
user_data = self.controller.users.get_user_by_api_token(api_token)
logger.debug("Checking results")
if user_data:
# Login successful! Check perms
logger.info(f"User {user_data['username']} has authenticated to API")
return True # This is to set the "authenticated"
logging.debug("Auth unsuccessful")
self.access_denied("unknown", "the user provided an invalid token")
return False
except Exception as e:
logger.warning("An error occured while authenticating an API user: %s", e)
self.finish(
self.return_response(
403,
{
"error": "ACCESS_DENIED",
"info": "An error occured while authenticating the user",
},
)
)
return False
class ServersStats(ApiHandler):
def get(self):
"""Get details about all servers"""
authenticated = self.authenticate_user()
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
if not authenticated:
return
if user_obj["superuser"]:
raw_stats = self.controller.servers.get_all_servers_stats()
else:
raw_stats = self.controller.servers.get_authorized_servers_stats(
user_obj["user_id"]
)
stats = []
for rs in raw_stats:
s = {}
for k, v in rs["server_data"].items():
if isinstance(v, datetime):
s[k] = v.timestamp()
else:
s[k] = v
stats.append(s)
# Get server stats
# TODO Check perms
self.finish(self.write({"servers": stats}))
class NodeStats(ApiHandler):
def get(self):
"""Get stats for particular node"""
authenticated = self.authenticate_user()
if not authenticated:
return
# Get node stats
node_stats = self.controller.servers.stats.get_node_stats()
self.return_response(200, {"code": node_stats["node_stats"]})
class SendCommand(ApiHandler):
def post(self):
user = self.authenticate_user()
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
if user is None:
self.access_denied("unknown")
return
server_id = self.get_argument("id")
if (
not user_obj["user_id"]
in self.controller.server_perms.get_server_user_list(server_id)
and not user_obj["superuser"]
):
self.access_denied("unknown")
return
if not self.permissions[
"Commands"
] in self.controller.server_perms.get_api_key_permissions_list(
self.controller.users.get_api_key_by_token(self.api_token), server_id
):
self.access_denied(user)
return
command = self.get_argument("command", default=None, strip=True)
server_id = self.get_argument("id")
if command:
server = self.controller.servers.get_server_instance_by_id(server_id)
if server.check_running:
server.send_command(command)
self.return_response(200, {"run": True})
else:
self.return_response(200, {"error": "SER_NOT_RUNNING"})
else:
self.return_response(200, {"error": "NO_COMMAND"})
class ServerBackup(ApiHandler):
def post(self):
user = self.authenticate_user()
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
if user is None:
self.access_denied("unknown")
return
server_id = self.get_argument("id")
if (
not user_obj["user_id"]
in self.controller.server_perms.get_server_user_list(server_id)
and not user_obj["superuser"]
):
self.access_denied("unknown")
return
if not self.permissions[
"Backup"
] in self.controller.server_perms.get_api_key_permissions_list(
self.controller.users.get_api_key_by_token(self.api_token), server_id
):
self.access_denied(user)
return
server = self.controller.servers.get_server_instance_by_id(server_id)
server.backup_server()
self.return_response(200, {"code": "SER_BAK_CALLED"})
class StartServer(ApiHandler):
def post(self):
user = self.authenticate_user()
remote_ip = self.get_remote_ip()
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
if user is None:
self.access_denied("unknown")
return
server_id = self.get_argument("id")
if (
not user_obj["user_id"]
in self.controller.server_perms.get_server_user_list(server_id)
and not user_obj["superuser"]
):
self.access_denied("unknown")
return
if not self.permissions[
"Commands"
] in self.controller.server_perms.get_api_key_permissions_list(
self.controller.users.get_api_key_by_token(self.api_token), server_id
):
self.access_denied("unknown")
return
server = self.controller.servers.get_server_instance_by_id(server_id)
if not server.check_running():
self.controller.management.send_command(
user_obj["user_id"], server_id, remote_ip, "start_server"
)
self.return_response(200, {"code": "SER_START_CALLED"})
else:
self.return_response(500, {"error": "SER_RUNNING"})
class StopServer(ApiHandler):
def post(self):
user = self.authenticate_user()
remote_ip = self.get_remote_ip()
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
if user is None:
self.access_denied("unknown")
return
server_id = self.get_argument("id")
if (
not user_obj["user_id"]
in self.controller.server_perms.get_server_user_list(server_id)
and not user_obj["superuser"]
):
self.access_denied("unknown")
if not self.permissions[
"Commands"
] in self.controller.server_perms.get_api_key_permissions_list(
self.controller.users.get_api_key_by_token(self.api_token), server_id
):
self.access_denied(user)
return
server = self.controller.servers.get_server_instance_by_id(server_id)
if server.check_running():
self.controller.management.send_command(
user, server_id, remote_ip, "stop_server"
)
self.return_response(200, {"code": "SER_STOP_CALLED"})
else:
self.return_response(500, {"error": "SER_NOT_RUNNING"})
class RestartServer(ApiHandler):
def post(self):
user = self.authenticate_user()
remote_ip = self.get_remote_ip()
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
if user is None:
self.access_denied("unknown")
return
server_id = self.get_argument("id")
if not user_obj["user_id"] in self.controller.server_perms.get_server_user_list(
server_id
):
self.access_denied("unknown")
if not self.permissions[
"Commands"
] in self.controller.server_perms.get_api_key_permissions_list(
self.controller.users.get_api_key_by_token(self.api_token), server_id
):
self.access_denied(user)
self.controller.management.send_command(
user, server_id, remote_ip, "restart_server"
)
self.return_response(200, {"code": "SER_RESTART_CALLED"})
class CreateUser(ApiHandler):
def post(self):
user = self.authenticate_user()
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
user_perms = self.controller.crafty_perms.get_crafty_permissions_list(
user_obj["user_id"]
)
if (
not self.permissions["User_Config"] in user_perms
and not user_obj["superuser"]
):
self.access_denied("unknown")
return
if user is None:
self.access_denied("unknown")
return
if not self.permissions[
"User_Config"
] in self.controller.crafty_perms.get_api_key_permissions_list(
self.controller.users.get_api_key_by_token(self.api_token)
):
self.access_denied(user)
return
new_username = self.get_argument("username").lower()
new_pass = self.get_argument("password")
manager = int(user_obj["user_id"])
if new_username:
self.controller.users.add_user(
new_username, manager, new_pass, "default@example.com", True, False
)
self.return_response(
200,
{
"code": "COMPLETE",
"username": new_username,
"password": new_pass,
},
)
else:
self.return_response(
500,
{
"error": "MISSING_PARAMS",
"info": "Some paramaters failed validation",
},
)
class DeleteUser(ApiHandler):
def post(self):
user = self.authenticate_user()
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
user_perms = self.controller.crafty_perms.get_crafty_permissions_list(
user_obj["user_id"]
)
if (
not self.permissions["User_Config"] in user_perms
and not user_obj["superuser"]
):
self.access_denied("unknown")
return
if user is None:
self.access_denied("unknown")
return
if not self.permissions[
"User_Config"
] in self.controller.crafty_perms.get_api_key_permissions_list(
self.controller.users.get_api_key_by_token(self.api_token)
):
self.access_denied(user)
return
user_id = self.get_argument("user_id", None, True)
user_to_del = self.controller.users.get_user_by_id(user_id)
if user_to_del["superuser"]:
self.return_response(
500,
{"error": "NOT_ALLOWED", "info": "You cannot delete a super user"},
)
else:
if user_id:
self.controller.users.remove_user(user_id)
self.return_response(200, {"code": "COMPLETED"})
class ListServers(ApiHandler):
def get(self):
user = self.authenticate_user()
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
if user is None:
self.access_denied("unknown")
return
if self.api_token is None:
self.access_denied("unknown")
return
if user_obj["superuser"]:
servers = self.controller.servers.get_all_defined_servers()
servers = [str(i) for i in servers]
else:
servers = self.controller.servers.get_authorized_servers(
user_obj["user_id"]
)
page_servers = []
for server in servers:
if server not in page_servers:
page_servers.append(
DatabaseShortcuts.get_data_obj(server.server_object)
)
servers = page_servers
servers = [str(i) for i in servers]
self.return_response(
200,
{
"code": "COMPLETED",
"servers": servers,
},
)

View File

@ -14,6 +14,7 @@ from app.classes.shared.translation import Translation
from app.classes.shared.main_models import DatabaseShortcuts from app.classes.shared.main_models import DatabaseShortcuts
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
auth_log = logging.getLogger("auth")
bearer_pattern = re.compile(r"^Bearer ", flags=re.IGNORECASE) bearer_pattern = re.compile(r"^Bearer ", flags=re.IGNORECASE)
@ -231,9 +232,16 @@ class BaseHandler(tornado.web.RequestHandler):
user, user,
) )
logging.debug("Auth unsuccessful") logging.debug("Auth unsuccessful")
auth_log.error(
f"Authentication attempted from {self.get_remote_ip()}. Invalid token"
)
self.access_denied(None, "the user provided an invalid token") self.access_denied(None, "the user provided an invalid token")
return None return None
except Exception as auth_exception: except Exception as auth_exception:
auth_log.error(
f"Authentication attempted from {self.get_remote_ip()}."
f" Error: {auth_exception}"
)
logger.debug( logger.debug(
"An error occured while authenticating an API user:", "An error occured while authenticating an API user:",
exc_info=auth_exception, exc_info=auth_exception,

View File

@ -210,6 +210,8 @@ class PanelHandler(BaseHandler):
error = self.get_argument("error", "WTF Error!") error = self.get_argument("error", "WTF Error!")
template = "panel/denied.html" template = "panel/denied.html"
if self.helper.crafty_starting:
page = "loading"
now = time.time() now = time.time()
formatted_time = str( formatted_time = str(
@ -243,9 +245,13 @@ class PanelHandler(BaseHandler):
for r in exec_user["roles"]: for r in exec_user["roles"]:
role = self.controller.roles.get_role(r) role = self.controller.roles.get_role(r)
exec_user_role.add(role["role_name"]) exec_user_role.add(role["role_name"])
defined_servers = self.controller.servers.get_authorized_servers( # get_auth_servers will throw an exception if run while Crafty is starting
exec_user["user_id"] if not self.helper.crafty_starting:
) defined_servers = self.controller.servers.get_authorized_servers(
exec_user["user_id"]
)
else:
defined_servers = []
user_order = self.controller.users.get_user_by_id(exec_user["user_id"]) user_order = self.controller.users.get_user_by_id(exec_user["user_id"])
user_order = user_order["server_order"].split(",") user_order = user_order["server_order"].split(",")
@ -481,6 +487,12 @@ class PanelHandler(BaseHandler):
subpage = nh3.clean(self.get_argument("subpage", "")) subpage = nh3.clean(self.get_argument("subpage", ""))
server_id = self.check_server_id() server_id = self.check_server_id()
# load page the user was on last
server_subpage = self.controller.servers.server_subpage.get(server_id, "")
if subpage == "" and server_subpage != "":
subpage = self.controller.servers.server_subpage.get(server_id, "")
else:
self.controller.servers.server_subpage[server_id] = subpage
if server_id is None: if server_id is None:
return return
if not self.failed_server: if not self.failed_server:
@ -1609,7 +1621,8 @@ class PanelHandler(BaseHandler):
logs_thread.start() logs_thread.start()
self.redirect("/panel/dashboard") self.redirect("/panel/dashboard")
return return
if self.helper.crafty_starting:
template = "panel/loading.html"
self.render( self.render(
template, template,
data=page_data, data=page_data,

View File

@ -6,6 +6,7 @@ from app.classes.models.users import HelperUsers
from app.classes.web.base_handler import BaseHandler from app.classes.web.base_handler import BaseHandler
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
auth_log = logging.getLogger("auth")
class PublicHandler(BaseHandler): class PublicHandler(BaseHandler):
@ -96,18 +97,27 @@ class PublicHandler(BaseHandler):
page_data["query"] = self.request.query page_data["query"] = self.request.query
if page == "login": if page == "login":
auth_log.info(
f"User attempting to authenticate from {self.get_remote_ip()}"
)
next_page = "/login" next_page = "/login"
if self.request.query: if self.request.query:
next_page = "/login?" + self.request.query next_page = "/login?" + self.request.query
entered_username = nh3.clean(self.get_argument("username")) entered_username = nh3.clean(self.get_argument("username"))
entered_password = nh3.clean(self.get_argument("password")) entered_password = self.get_argument("password")
# pylint: disable=no-member # pylint: disable=no-member
try: try:
user_id = HelperUsers.get_user_id_by_name(entered_username.lower()) user_id = HelperUsers.get_user_id_by_name(entered_username.lower())
user_data = HelperUsers.get_user_model(user_id) user_data = HelperUsers.get_user_model(user_id)
except: except:
self.controller.log_attempt(self.get_remote_ip(), entered_username)
auth_log.error(
f"User attempted to log into {entered_username}."
f" Authentication failed from remote IP {self.get_remote_ip()}"
" Users does not exist."
)
error_msg = "Incorrect username or password. Please try again." error_msg = "Incorrect username or password. Please try again."
# self.clear_cookie("user") # self.clear_cookie("user")
# self.clear_cookie("user_data") # self.clear_cookie("user_data")
@ -120,6 +130,12 @@ class PublicHandler(BaseHandler):
# if we don't have a user # if we don't have a user
if not user_data: if not user_data:
auth_log.error(
f"User attempted to log into {entered_username}. Authentication"
f" failed from remote IP {self.get_remote_ip()}"
" User does not exist."
)
self.controller.log_attempt(self.get_remote_ip(), entered_username)
error_msg = "Incorrect username or password. Please try again." error_msg = "Incorrect username or password. Please try again."
# self.clear_cookie("user") # self.clear_cookie("user")
# self.clear_cookie("user_data") # self.clear_cookie("user_data")
@ -132,6 +148,12 @@ class PublicHandler(BaseHandler):
# if they are disabled # if they are disabled
if not user_data.enabled: if not user_data.enabled:
auth_log.error(
f"User attempted to log into {entered_username}. "
f"Authentication failed from remote IP {self.get_remote_ip()}."
" User account disabled"
)
self.controller.log_attempt(self.get_remote_ip(), entered_username)
error_msg = ( error_msg = (
"User account disabled. Please contact " "User account disabled. Please contact "
"your system administrator for more info." "your system administrator for more info."
@ -159,7 +181,11 @@ class PublicHandler(BaseHandler):
user_data.last_ip = self.get_remote_ip() user_data.last_ip = self.get_remote_ip()
user_data.last_login = Helpers.get_time_as_string() user_data.last_login = Helpers.get_time_as_string()
user_data.save() user_data.save()
auth_log.info(
f"{entered_username} successfully"
" authenticated and logged"
f" into panel from remote IP {self.get_remote_ip()}"
)
# log this login # log this login
self.controller.management.add_to_audit_log( self.controller.management.add_to_audit_log(
user_data.user_id, "Logged in", 0, self.get_remote_ip() user_data.user_id, "Logged in", 0, self.get_remote_ip()
@ -172,6 +198,11 @@ class PublicHandler(BaseHandler):
self.redirect(next_page) self.redirect(next_page)
else: else:
auth_log.error(
f"User attempted to log into {entered_username}."
f" Authentication failed from remote IP {self.get_remote_ip()}"
)
self.controller.log_attempt(self.get_remote_ip(), entered_username)
# self.clear_cookie("user") # self.clear_cookie("user")
# self.clear_cookie("user_data") # self.clear_cookie("user_data")
self.clear_cookie("token") self.clear_cookie("token")

View File

@ -7,7 +7,7 @@ from app.classes.shared.helpers import Helpers
from app.classes.web.base_api_handler import BaseApiHandler from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
auth_log = logging.getLogger("auth")
login_schema = { login_schema = {
"type": "object", "type": "object",
"properties": { "properties": {
@ -29,6 +29,10 @@ class ApiAuthLoginHandler(BaseApiHandler):
try: try:
data = json.loads(self.request.body) data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e: except json.decoder.JSONDecodeError as e:
logger.error(
"Invalid JSON schema for API"
f" login attempt from {self.get_remote_ip()}"
)
return self.finish_json( return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)} 400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
) )
@ -36,6 +40,10 @@ class ApiAuthLoginHandler(BaseApiHandler):
try: try:
validate(data, login_schema) validate(data, login_schema)
except ValidationError as e: except ValidationError as e:
logger.error(
"Invalid JSON schema for API"
f" login attempt from {self.get_remote_ip()}"
)
return self.finish_json( return self.finish_json(
400, 400,
{ {
@ -52,12 +60,23 @@ class ApiAuthLoginHandler(BaseApiHandler):
user_data = Users.get_or_none(Users.username == username) user_data = Users.get_or_none(Users.username == username)
if user_data is None: if user_data is None:
self.controller.log_attempt(self.get_remote_ip(), username)
auth_log.error(
f"User attempted to log into {username}."
" Authentication failed from remote IP"
f" {self.get_remote_ip()}. User not found"
)
return self.finish_json( return self.finish_json(
401, 401,
{"status": "error", "error": "INCORRECT_CREDENTIALS", "token": None}, {"status": "error", "error": "INCORRECT_CREDENTIALS", "token": None},
) )
if not user_data.enabled: if not user_data.enabled:
auth_log.error(
f"User attempted to log into {username}."
" Authentication failed from remote"
f" IP {self.get_remote_ip()} account disabled"
)
self.finish_json( self.finish_json(
403, {"status": "error", "error": "ACCOUNT_DISABLED", "token": None} 403, {"status": "error", "error": "ACCOUNT_DISABLED", "token": None}
) )
@ -67,6 +86,11 @@ class ApiAuthLoginHandler(BaseApiHandler):
# Valid Login # Valid Login
if login_result: if login_result:
auth_log.info(
f"{username} successfully"
" authenticated and logged"
f" into panel from remote IP {self.get_remote_ip()}"
)
logger.info(f"User: {user_data} Logged in from IP: {self.get_remote_ip()}") logger.info(f"User: {user_data} Logged in from IP: {self.get_remote_ip()}")
# record this login # record this login

View File

@ -29,6 +29,14 @@ class ApiAnnounceIndexHandler(BaseApiHandler):
) = auth_data ) = auth_data
data = self.helper.get_announcements() data = self.helper.get_announcements()
if not data:
return self.finish_json(
424,
{
"status": "error",
"data": "Failed to get announcements",
},
)
cleared = str( cleared = str(
self.controller.users.get_user_by_id(auth_data[4]["user_id"])[ self.controller.users.get_user_by_id(auth_data[4]["user_id"])[
"cleared_notifs" "cleared_notifs"
@ -84,6 +92,14 @@ class ApiAnnounceIndexHandler(BaseApiHandler):
}, },
) )
announcements = self.helper.get_announcements() announcements = self.helper.get_announcements()
if not announcements:
return self.finish_json(
424,
{
"status": "error",
"data": "Failed to get current announcements",
},
)
res = [d.get("id", None) for d in announcements] res = [d.get("id", None) for d in announcements]
cleared_notifs = str( cleared_notifs = str(
self.controller.users.get_user_by_id(auth_data[4]["user_id"])[ self.controller.users.get_user_by_id(auth_data[4]["user_id"])[

View File

@ -7,6 +7,7 @@ from jsonschema.exceptions import ValidationError
from app.classes.models.crafty_permissions import EnumPermissionsCrafty from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.shared.helpers import Helpers from app.classes.shared.helpers import Helpers
from app.classes.web.base_api_handler import BaseApiHandler from app.classes.web.base_api_handler import BaseApiHandler
from app.classes.web.websocket_handler import WebSocketManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
files_get_schema = { files_get_schema = {
@ -73,7 +74,7 @@ class ApiImportFilesIndexHandler(BaseApiHandler):
else: else:
if user_id: if user_id:
user_lang = self.controller.users.get_user_lang_by_id(user_id) user_lang = self.controller.users.get_user_lang_by_id(user_id)
self.helper.websocket_helper.broadcast_user( WebSocketManager().broadcast_user(
user_id, user_id,
"send_start_error", "send_start_error",
{ {
@ -85,7 +86,7 @@ class ApiImportFilesIndexHandler(BaseApiHandler):
else: else:
if not self.helper.check_path_exists(folder) and user_id: if not self.helper.check_path_exists(folder) and user_id:
user_lang = self.controller.users.get_user_lang_by_id(user_id) user_lang = self.controller.users.get_user_lang_by_id(user_id)
self.helper.websocket_helper.broadcast_user( WebSocketManager().broadcast_user(
user_id, user_id,
"send_start_error", "send_start_error",
{ {

View File

@ -110,7 +110,7 @@ class ApiRolesIndexHandler(BaseApiHandler):
try: try:
data = orjson.loads(self.request.body) data = orjson.loads(self.request.body)
except orjson.decoder.JSONDecodeError as e: except orjson.JSONDecodeError as e:
return self.finish_json( return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)} 400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
) )

View File

@ -72,9 +72,9 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
FileHelpers.del_file( FileHelpers.del_file(
os.path.join(backup_conf["backup_path"], data["filename"]) os.path.join(backup_conf["backup_path"], data["filename"])
) )
except Exception: except Exception as e:
return self.finish_json( return self.finish_json(
400, {"status": "error", "error": "NO BACKUP FOUND"} 400, {"status": "error", "error": f"DELETE FAILED with error {e}"}
) )
self.controller.management.add_to_audit_log( self.controller.management.add_to_audit_log(
auth_data[4]["user_id"], auth_data[4]["user_id"],

View File

@ -215,7 +215,7 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
user_obj = HelperUsers.get_user_model(user_id) user_obj = HelperUsers.get_user_model(user_id)
if "password" in data and str(user["user_id"]) != str(user_id): if "password" in data and str(user["user_id"]) != str(user_id):
if str(user["user_id"]) != str(user_obj.manager): if str(user["user_id"]) != str(user_obj.manager) and not user["superuser"]:
# TODO: edit your own password # TODO: edit your own password
return self.finish_json( return self.finish_json(
400, {"status": "error", "error": "INVALID_PASSWORD_MODIFY"} 400, {"status": "error", "error": "INVALID_PASSWORD_MODIFY"}

View File

@ -22,18 +22,6 @@ from app.classes.web.default_handler import DefaultHandler
from app.classes.web.routes.api.api_handlers import api_handlers from app.classes.web.routes.api.api_handlers import api_handlers
from app.classes.web.routes.metrics.metrics_handlers import metrics_handlers from app.classes.web.routes.metrics.metrics_handlers import metrics_handlers
from app.classes.web.server_handler import ServerHandler from app.classes.web.server_handler import ServerHandler
from app.classes.web.api_handler import (
ServersStats,
NodeStats,
ServerBackup,
StartServer,
StopServer,
RestartServer,
CreateUser,
DeleteUser,
ListServers,
SendCommand,
)
from app.classes.web.websocket_handler import WebSocketHandler from app.classes.web.websocket_handler import WebSocketHandler
from app.classes.web.static_handler import CustomStaticHandler from app.classes.web.static_handler import CustomStaticHandler
from app.classes.web.upload_handler import UploadHandler from app.classes.web.upload_handler import UploadHandler
@ -162,17 +150,6 @@ class Webserver:
(r"/ws", WebSocketHandler, handler_args), (r"/ws", WebSocketHandler, handler_args),
(r"/upload", UploadHandler, handler_args), (r"/upload", UploadHandler, handler_args),
(r"/status", StatusHandler, handler_args), (r"/status", StatusHandler, handler_args),
# API Routes V1
(r"/api/v1/stats/servers", ServersStats, handler_args),
(r"/api/v1/stats/node", NodeStats, handler_args),
(r"/api/v1/server/send_command", SendCommand, handler_args),
(r"/api/v1/server/backup", ServerBackup, handler_args),
(r"/api/v1/server/start", StartServer, handler_args),
(r"/api/v1/server/stop", StopServer, handler_args),
(r"/api/v1/server/restart", RestartServer, handler_args),
(r"/api/v1/list_servers", ListServers, handler_args),
(r"/api/v1/users/create_user", CreateUser, handler_args),
(r"/api/v1/users/delete_user", DeleteUser, handler_args),
# API Routes V2 # API Routes V2
*api_handlers(handler_args), *api_handlers(handler_args),
# API Routes OpenMetrics # API Routes OpenMetrics

View File

@ -10,16 +10,20 @@
}, },
"schedule": { "schedule": {
"format": "%(asctime)s - [Schedules] - %(levelname)s - %(message)s" "format": "%(asctime)s - [Schedules] - %(levelname)s - %(message)s"
},
"auth": {
"format": "%(asctime)s - [AUTH] - %(levelname)s - %(message)s"
},
"cmd_queue": {
"format": "%(asctime)s - [CMD_QUEUE] - %(levelname)s - %(message)s"
} }
}, },
"handlers": { "handlers": {
"console": { "console": {
"class": "logging.StreamHandler", "class": "logging.StreamHandler",
"formatter": "commander", "formatter": "commander",
"stream": "ext://sys.stdout" "stream": "ext://sys.stdout"
}, },
"main_file_handler": { "main_file_handler": {
"class": "logging.handlers.RotatingFileHandler", "class": "logging.handlers.RotatingFileHandler",
"formatter": "commander", "formatter": "commander",
@ -50,24 +54,60 @@
"maxBytes": 10485760, "maxBytes": 10485760,
"backupCount": 20, "backupCount": 20,
"encoding": "utf8" "encoding": "utf8"
},
"auth_file_handler": {
"class": "logging.handlers.RotatingFileHandler",
"formatter": "auth",
"filename": "logs/auth.log",
"maxBytes": 10485760,
"backupCount": 20,
"encoding": "utf8"
},
"cmd_queue_file_handler": {
"class": "logging.handlers.RotatingFileHandler",
"formatter": "auth",
"filename": "logs/cmd_queue.log",
"maxBytes": 10485760,
"backupCount": 20,
"encoding": "utf8"
} }
}, },
"loggers": { "loggers": {
"": { "": {
"level": "INFO", "level": "INFO",
"handlers": ["main_file_handler", "session_file_handler"], "handlers": [
"main_file_handler",
"session_file_handler"
],
"propagate": false "propagate": false
}, },
"tornado.access": { "tornado.access": {
"level": "INFO", "level": "INFO",
"handlers": ["tornado_access_file_handler"], "handlers": [
"tornado_access_file_handler"
],
"propagate": false "propagate": false
}, },
"apscheduler": { "apscheduler": {
"level": "INFO", "level": "INFO",
"handlers": ["schedule_file_handler"], "handlers": [
"schedule_file_handler"
],
"propagate": false
},
"auth": {
"level": "INFO",
"handlers": [
"auth_file_handler"
],
"propagate": false
},
"cmd_queue": {
"level": "INFO",
"handlers": [
"cmd_queue_file_handler"
],
"propagate": false "propagate": false
} }
} }
} }

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 425 425">
<!-- Background -->
<rect x="0" y="0" rx="105" ry="105" width="425" height="425" style="fill: #22283A"/>
<!-- green C -->
<path fill="hsl(160, 59%, 45%)" d="M 162.5 182.5 l 38 -15 q 0 -40 -40 -40 l -95 0 q -40 0 -40 40 l 0 90 q 0 40 40 40 l 95 0 q 40 0 40 -40 l -38 -20 l 0 28 l -100 0 l 0 -108 l 100 0 z"/>
<!-- blue C -->
<path fill="hsl(192, 99%, 45%)" d="M 262.5 182.5 l -38 -15 q 0 -40 40 -40 l 95 0 q 40 0 40 40 l 0 90 q 0 40 -40 40 l -95 0 q -40 0 -40 -40 l 38 -20 l 0 28 l 100 0 l 0 -108 l -100 0 z"/>
<!-- line thing in middle of Cs -->
<path fill="hsl(266, 71%, 57%)" d="M 142.5 191.5 q -10 0 -10 10 l 0 19 q 0 10 10 10 l 140 0 q 10 0 10 -10 l 0 -19 q 0 -10 -10 -10 z"/>
</svg>

After

Width:  |  Height:  |  Size: 750 B

View File

@ -82,6 +82,9 @@
<span class="mdi mdi-chevron-double-left"></span> <span class="mdi mdi-chevron-double-left"></span>
<span class="mdi mdi-chevron-double-right"></span> <span class="mdi mdi-chevron-double-right"></span>
</button> </button>
&nbsp;&nbsp;&nbsp;
<span class="badge-pill badge-outline-primary" id="server-name-nav" style="display: none;"></span>
{% include notify.html %} {% include notify.html %}
@ -381,18 +384,21 @@
if (x) { if (x) {
x.remove() x.remove()
} }
bootbox.alert({ bootbox.confirm({
title: "{{ translate('notify', 'downloadLogs', data['lang']) }}", title: "{{ translate('notify', 'downloadLogs', data['lang']) }}",
message: "{{ translate('notify', 'finishedPreparing', data['lang']) }}", message: "{{ translate('notify', 'finishedPreparing', data['lang']) }}",
buttons: { buttons: {
ok: { confirm: {
label: 'Download', label: 'Download',
className: 'btn-info' className: 'btn-info'
} }
}, },
callback: function () { callback: function (result) {
console.log("in callback") if (result){
location.href = "/panel/download_support_package"; location.href = "/panel/download_support_package";
} else {
bootbox.close();
}
} }
}); });
}); });
@ -602,4 +608,4 @@
</body> </body>
</html> </html>

View File

@ -93,7 +93,7 @@
return true; return true;
} }
function updateAnnouncements(data) { function updateAnnouncements(data) {
console.log(data) console.log(data);
let text = ""; let text = "";
for (let value of data) { for (let value of data) {
text += `<li class="card-header header-sm justify-content-between align-items-center" id="${value.id}"><p style="float: right;"><i data-id="${value.id}"class="clear-button fa-regular fa-x"></i></p><a style="color: var(--purple);" href=${value.link} target="_blank"><h6>${value.title}</h6><small><p>${value.date}</p></small><p>${value.desc}</p></li></a>` text += `<li class="card-header header-sm justify-content-between align-items-center" id="${value.id}"><p style="float: right;"><i data-id="${value.id}"class="clear-button fa-regular fa-x"></i></p><a style="color: var(--purple);" href=${value.link} target="_blank"><h6>${value.title}</h6><small><p>${value.date}</p></small><p>${value.desc}</p></li></a>`
@ -138,12 +138,12 @@
console.log(responseData); console.log(responseData);
setTimeout(function() { setTimeout(function() {
getAnnouncements(); getAnnouncements();
}, 1800); }, 1800000); //Wait 30 minutes in miliseconds
console.log("Registered annoucement fetch event in 30 minutes.") console.log("Registered annoucement fetch event in 30 minutes.")
if (responseData.status === "ok") { if (responseData.status === "ok") {
updateAnnouncements(responseData.data) updateAnnouncements(responseData.data)
} else { } else {
updateAnnouncements("<li><p>Trouble Getting Annoucements</p></li>") updateAnnouncements([])
} }
} }
async function send_clear(uuid) { async function send_clear(uuid) {

View File

@ -170,7 +170,7 @@
{% block js %} {% block js %}
<script> <script>
function replacer(key, value) { function replacer(key, value) {
if (key == "disabled_language_files") { if (key == "disabled_language_files" || key == "monitored_mounts") {
if (value == 0) { if (value == 0) {
return [] return []
} else { } else {

View File

@ -102,8 +102,11 @@
<div class="col-12 mt-4"> <div class="col-12 mt-4">
<div class="d-flex"> <div class="d-flex">
<div class="wrapper" style="width: 100%;"> <div class="wrapper" style="width: 100%;">
<h5 class="mb-1 font-weight-medium text-primary">Storage {% if len(data["monitored"]) > 0 %}
<h5 class="mb-1 font-weight-medium text-primary">{{ translate('dashboard', 'storage',
data['lang']) }}
</h5> </h5>
{% end %}
<div id="storage_data"> <div id="storage_data">
<div class="row"> <div class="row">
{% for item in data['hosts_data']['disk_json'] %} {% for item in data['hosts_data']['disk_json'] %}
@ -615,7 +618,7 @@
} }
function warn(message, link = null, className = null) { function warnServer(message, link = null, className = null) {
var closeEl = document.createElement('span'); var closeEl = document.createElement('span');
var strongEL = document.createElement('strong'); var strongEL = document.createElement('strong');
var msgEl = document.createElement('div'); var msgEl = document.createElement('div');
@ -799,7 +802,7 @@
setTimeout(finishTimeout, 60000); setTimeout(finishTimeout, 60000);
}); });
function finishTimeout() { function finishTimeout() {
warn("It seems this is taking a while...it's possible you're using UBlock or a similar ad blocker and it's causing some of our connections to not make it to the server. Try disabling your ad blocker.", warnServer("It seems this is taking a while...it's possible you're using UBlock or a similar ad blocker and it's causing some of our connections to not make it to the server. Try disabling your ad blocker.",
null, 'wssError'); null, 'wssError');
} }
$(".stop_button").click(function () { $(".stop_button").click(function () {

View File

@ -0,0 +1,73 @@
{% extends ../base.html %}
{% block meta %}
{% end %}
{% block title %}Crafty Controller Starting{% end %}
{% block content %}
<div class="content-wrapper">
<div class="card-header justify-content-between align-items-center" style="border: none;">
<div id="image-div" style="width: 100%;">
<img class="img-center" id="logo-animate" src="../static/assets/images/crafty-logo-square-1024.png"
alt="Crafty Logo, Crafty is loading" width="20%" style="clear: both;">
</div>
<br>
</br>
<div id="text-div" style="width: 100%; text-align: center;">
<h2 id="status" style="display: block;" data-init="{{ translate('startup', 'serverInit', data['lang']) }}"
data-server="{{ translate('startup', 'server', data['lang']) }}"
data-internet="{{ translate('startup', 'internet', data['lang']) }}"
data-tasks="{{ translate('startup', 'tasks', data['lang']) }}"
data-internals="{{ translate('startup', 'internals', data['lang']) }}"
data-almost="{{ translate('startup', 'almost', data['lang']) }}">
{{ translate('startup', 'starting', data['lang']) }}</h2>
</div>
</div>
</div>
<style>
.img-center {
display: block;
margin-left: auto;
margin-right: auto;
}
</style>
<script>
function rotateImage(degree) {
$('#logo-animate').animate({ transform: degree }, {
step: function (now, fx) {
$(this).css({
'-webkit-transform': 'rotate(' + now + 'deg)',
'-moz-transform': 'rotate(' + now + 'deg)',
'transform': 'rotate(' + now + 'deg)'
});
}
});
setTimeout(function () {
rotateImage(360);
}, 2000);
}
$(document).ready(function () {
setTimeout(function () {
rotateImage(360);
}, 2000);
if (webSocket) {
webSocket.on('update', function (data) {
if ("server" in data) {
$("#status").html(`${$("#status").data(data.section)} ${data.server}...`);
} else {
$("#status").html($("#status").data(data.section));
}
});
webSocket.on('send_start_reload', function () {
setTimeout(function () {
location.href = '/panel/dashboard'
}, 5000);
});
}
});
</script>
{% end %}

View File

@ -363,6 +363,7 @@ data['lang']) }}{% end %}
{% block js %} {% block js %}
<script> <script>
const userId = new URLSearchParams(document.location.search).get('id');
function submit_user(event) { function submit_user(event) {
if (!validateForm()) { if (!validateForm()) {
event.preventDefault(); event.preventDefault();
@ -399,8 +400,6 @@ data['lang']) }}{% end %}
} }
} }
$("#user_form").on("submit", async function (e) { $("#user_form").on("submit", async function (e) {
const userId = new URLSearchParams(document.location.search).get('id');
console.log(userId)
e.preventDefault(); e.preventDefault();
let password = null; let password = null;
if(!userId){ if(!userId){

View File

@ -248,12 +248,38 @@
$("#player-body").html(text); $("#player-body").html(text);
} }
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
const token = getCookie("_xsrf")
$(window).ready(function () { $(window).ready(function () {
console.log("ready!"); console.log("ready!");
//if (webSocket) { //if (webSocket) {
webSocket.on('update_server_details', update_server_details); webSocket.on('update_server_details', update_server_details);
add_server_name();
//} //}
}); });
async function add_server_name(){
let res = await fetch(`/api/v2/servers/${serverId}`, {
method: 'GET',
headers: {
'X-XSRFToken': token
},
});
let responseData = await res.json();
if (responseData.status === "ok") {
console.log(responseData)
$("#server-name-nav").html(`${responseData.data['server_name']}`)
$("#server-name-nav").show();
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
}
</script> </script>

View File

@ -69,7 +69,7 @@
<td>Banned on {{ player['banned_on'] }}</td> <td>Banned on {{ player['banned_on'] }}</td>
<td>Banned by : {{ player['source'] }} <br />Reason : {{ player['reason'] }}</td> <td>Banned by : {{ player['source'] }} <br />Reason : {{ player['reason'] }}</td>
<td class="buttons"> <td class="buttons">
<button onclick="send_command_to_server(`pardon {{ player['name'] }} `)" type="button" class="btn btn-danger">Unban</button> <button onclick="send_command_to_server(`pardon {{ player['name'] }}`)" type="button" class="btn btn-danger">Unban</button>
</td> </td>
</tr> </tr>
{% end %} {% end %}

View File

@ -428,6 +428,7 @@
if ($("#root_files_button").hasClass("clicked")){ if ($("#root_files_button").hasClass("clicked")){
formDataObject.exclusions = excluded; formDataObject.exclusions = excluded;
} }
delete formDataObject.root_path
console.log(excluded); console.log(excluded);
console.log(formDataObject); console.log(formDataObject);
// Format the plain form data as JSON // Format the plain form data as JSON
@ -650,10 +651,10 @@
let checked = "" let checked = ""
let dpath = value.path; let dpath = value.path;
let filename = key; let filename = key;
if (value.excluded){
checked = "checked"
}
if (value.dir){ if (value.dir){
if (value.excluded){
checked = "checked"
}
text += `<li class="tree-item" data-path="${dpath}"> text += `<li class="tree-item" data-path="${dpath}">
\n<div id="${dpath}" data-path="${dpath}" data-name="${filename}" class="tree-caret tree-ctx-item tree-folder"> \n<div id="${dpath}" data-path="${dpath}" data-name="${filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" class="checkBoxClass excluded" value="${dpath}" ${checked}> <input type="checkbox" class="checkBoxClass excluded" value="${dpath}" ${checked}>

View File

@ -43,8 +43,10 @@
<form class="forms-sample" method="post" id="webhook_form" <form class="forms-sample" method="post" id="webhook_form"
action="/panel/edit_webhook?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{ data['webhook']['id'] }}"> action="/panel/edit_webhook?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{ data['webhook']['id'] }}">
{% end %} {% end %}
<select class="form-select form-control form-control-lg select-css" id="webhook_type" name="webhook_type"> <select class="form-select form-control form-control-lg select-css" id="webhook_type" name="webhook_type">
{% if data['new_webhook'] == False %}
<option value="{{data['webhook']['webhook_type']}}">{{data['webhook']['webhook_type']}}</option> <option value="{{data['webhook']['webhook_type']}}">{{data['webhook']['webhook_type']}}</option>
{% end %}
{% for type in data['providers'] %} {% for type in data['providers'] %}
{% if type != data['webhook']['webhook_type'] %} {% if type != data['webhook']['webhook_type'] %}
<option value="{{type}}">{{type}}</option> <option value="{{type}}">{{type}}</option>

View File

@ -111,6 +111,7 @@
"starting": "Verzögerter Start", "starting": "Verzögerter Start",
"status": "Status", "status": "Status",
"stop": "Stoppen", "stop": "Stoppen",
"storage": "Speicher",
"version": "Version", "version": "Version",
"welcome": "Willkommen bei Crafty Controller" "welcome": "Willkommen bei Crafty Controller"
}, },
@ -591,6 +592,15 @@
"newServer": "Neuen Server erstellen", "newServer": "Neuen Server erstellen",
"servers": "Server" "servers": "Server"
}, },
"startup": {
"almost": "Nur noch einen Moment, fast geschafft",
"internals": "Crafty's interne Komponneten initialisieren und starten",
"internet": "Verbindung zum Internet überprüfen",
"server": "initialisieren ",
"serverInit": "Server initialisieren",
"starting": "Crafty startet...",
"tasks": "Zeitplan-Aufgaben werden geladen"
},
"userConfig": { "userConfig": {
"apiKey": "API Schlüssel", "apiKey": "API Schlüssel",
"auth": "Autorisiert? ", "auth": "Autorisiert? ",

View File

@ -111,6 +111,7 @@
"starting": "Delayed-Start", "starting": "Delayed-Start",
"status": "Status", "status": "Status",
"stop": "Stop", "stop": "Stop",
"storage": "Storage",
"version": "Version", "version": "Version",
"welcome": "Welcome to Crafty Controller" "welcome": "Welcome to Crafty Controller"
}, },
@ -590,6 +591,15 @@
"newServer": "Create New Server", "newServer": "Create New Server",
"servers": "Servers" "servers": "Servers"
}, },
"startup": {
"almost": "Finishing up. Hang on tight...",
"internals": "Configuring and starting Crafty's internal components",
"internet": "Checking for internet connection",
"server": "Initializing ",
"serverInit": "Initializing Servers",
"starting": "Crafty Is Starting...",
"tasks": "Starting Tasks Scheduler"
},
"userConfig": { "userConfig": {
"apiKey": "API Keys", "apiKey": "API Keys",
"auth": "Authorized? ", "auth": "Authorized? ",

View File

@ -111,6 +111,7 @@
"starting": "Démarrage retardé", "starting": "Démarrage retardé",
"status": "Statut", "status": "Statut",
"stop": "Arrêter", "stop": "Arrêter",
"storage": "Stockage",
"version": "Version", "version": "Version",
"welcome": "Bienvenue sur Crafty Controller" "welcome": "Bienvenue sur Crafty Controller"
}, },
@ -591,6 +592,15 @@
"newServer": "Créer un Nouveau Serveur", "newServer": "Créer un Nouveau Serveur",
"servers": "Serveurs" "servers": "Serveurs"
}, },
"startup": {
"almost": "Finalisation. Patienter ...",
"internals": "Configuration et Démarrage des composants internes de Crafty",
"internet": "Vérification de la connexion à Internet",
"server": "Initialisation ",
"serverInit": "Initialisation des Serveurs",
"starting": "Crafty Démarre ...",
"tasks": "Démarrage du planificateur de tâches"
},
"userConfig": { "userConfig": {
"apiKey": "Clés API", "apiKey": "Clés API",
"auth": "Authorisé ? ", "auth": "Authorisé ? ",

View File

@ -53,6 +53,20 @@
"translationTitle": "שפת התרגום", "translationTitle": "שפת התרגום",
"translator": "מתרגמים" "translator": "מתרגמים"
}, },
"customLogin": {
"apply": "החל",
"backgroundUpload": "העלאת רקע",
"customLoginPage": "התאמת דף הכניסה",
"delete": "מחק",
"labelLoginImage": "בחר את רקע כניסתך",
"loginBackground": "תמונת רקע לכניסה",
"loginImage": "העלה תמונת רקע למסך הכניסה.",
"loginOpacity": "בחר את שקיפות חלון הכניסה",
"pageTitle": "דף כניסה מותאם אישית",
"preview": "תצוגה מקדימה",
"select": "בחר",
"selectImage": "בחר תמונה"
},
"dashboard": { "dashboard": {
"actions": "פעולות", "actions": "פעולות",
"allServers": "כל השרתים", "allServers": "כל השרתים",
@ -75,6 +89,7 @@
"dashboard": "פאנל", "dashboard": "פאנל",
"delay-explained": "השירות/סוכן התחיל לאחרונה והוא מעכב את הדלקת שרת המיינקראפט", "delay-explained": "השירות/סוכן התחיל לאחרונה והוא מעכב את הדלקת שרת המיינקראפט",
"host": "אחסון", "host": "אחסון",
"installing": "מתקין...",
"kill": "כיבוי מידי", "kill": "כיבוי מידי",
"killing": "...מכבה מידית", "killing": "...מכבה מידית",
"lastBackup": "אחרון:", "lastBackup": "אחרון:",
@ -96,6 +111,7 @@
"starting": "התחלה בעיכוב", "starting": "התחלה בעיכוב",
"status": "סטאטוס", "status": "סטאטוס",
"stop": "עצור", "stop": "עצור",
"storage": "שטח אחסון",
"version": "גרסה", "version": "גרסה",
"welcome": "ברוכים הבאים ל-פאנל קראפטי" "welcome": "ברוכים הבאים ל-פאנל קראפטי"
}, },
@ -164,20 +180,33 @@
} }
}, },
"error": { "error": {
"agree": "מסכים",
"bedrockError": "הורדות Bedrock אינן זמינות. אנא בדוק",
"cancel": "בטל",
"contact": "בבקשה צרו קשר עם תמיכת פאנל קראפטי באמצעות דיסקורד", "contact": "בבקשה צרו קשר עם תמיכת פאנל קראפטי באמצעות דיסקורד",
"craftyStatus": "דף המצב של Crafty",
"cronFormat": "זוהה פורמט Cron לא תקין",
"embarassing": "אוי, טוב, זה מביך.", "embarassing": "אוי, טוב, זה מביך.",
"error": "שגיאה!", "error": "שגיאה!",
"eulaAgree": "אתם מסכימים?", "eulaAgree": "אתם מסכימים?",
"eulaMsg": "עליכם להסכים להסכם הרישיון למשתמש הקצה. עותק של הסכם הרישיון למשתמש הקצה של מוג'אנג מקושר תחת הודעה זו.", "eulaMsg": "עליכם להסכים להסכם הרישיון למשתמש הקצה. עותק של הסכם הרישיון למשתמש הקצה של מוג'אנג מקושר תחת הודעה זו.",
"eulaTitle": "להסכים להסכם רישיון משתמש קצה של מוג'אנג", "eulaTitle": "להסכים להסכם רישיון משתמש קצה של מוג'אנג",
"fileError": "סוג הקובץ חייב להיות תמונה.",
"fileTooLarge": "העלאה נכשלה. העלאת הקובץ גדולה מדי. פנה למנהל המערכת לקבלת סיוע.", "fileTooLarge": "העלאה נכשלה. העלאת הקובץ גדולה מדי. פנה למנהל המערכת לקבלת סיוע.",
"hereIsTheError": "הנה השגיאה", "hereIsTheError": "הנה השגיאה",
"installerJava": "נכשל בהתקנת {} : התקנות של שרת Forge דורשות Java. זוהה ש-Java אינו מותקן. אנא התקן את Java ואז התקן את השרת.",
"internet": "גילינו שלמכונה(מחשב) שמריצה את קראפטי אין חיבור לאינטרנט. חיבורי לקוחות לשרת עשויים להיות מוגבלים.", "internet": "גילינו שלמכונה(מחשב) שמריצה את קראפטי אין חיבור לאינטרנט. חיבורי לקוחות לשרת עשויים להיות מוגבלים.",
"migration": "אחסון השרת הראשי של Crafty מועבר למיקום חדש. כל הפעלות השרתים נעצרו במהלך זמן זה. אנא המתן בזמן שאנו מסיימים את המעבר הזה",
"no-file": "נראה שאיננו מצליחים לאתר את הקובץ המבוקש. בדוק שוב את הנתיב. האם ל-קראפטי יש הרשאות מתאימות?", "no-file": "נראה שאיננו מצליחים לאתר את הקובץ המבוקש. בדוק שוב את הנתיב. האם ל-קראפטי יש הרשאות מתאימות?",
"noInternet": "Crafty נתקל בבעיות גישה לאינטרנט. יצירת שרתים הושבתה. אנא בדוק את חיבור האינטרנט שלך ורענן את הדף הזה.",
"noJava": "השרת {} לא הצליח להתחיל עם קוד השגיאה: גילינו ש-Java אינו מותקן. אנא התקינו את Java ואז הפעילו את השרת.", "noJava": "השרת {} לא הצליח להתחיל עם קוד השגיאה: גילינו ש-Java אינו מותקן. אנא התקינו את Java ואז הפעילו את השרת.",
"not-downloaded": "לא הצלחנו למצוא את קובץ ההפעלה שלך. האם זה סיים להוריד? האם ההרשאות מוגדרות בשביל הפעלה?", "not-downloaded": "לא הצלחנו למצוא את קובץ ההפעלה שלך. האם זה סיים להוריד? האם ההרשאות מוגדרות בשביל הפעלה?",
"portReminder": "זיהינו שזו הפעם הראשונה ש-{} מופעל. הקפידו להעביר את היציאה {} דרך הנתב/חומת האש שלכם כדי להפוך אותה לנגישה מרחוק מהאינטרנט.", "portReminder": "זיהינו שזו הפעם הראשונה ש-{} מופעל. הקפידו להעביר את היציאה {} דרך הנתב/חומת האש שלכם כדי להפוך אותה לנגישה מרחוק מהאינטרנט.",
"privMsg": "וה",
"serverJars1": "API של צנצנות השרת אינו נגיש. אנא בדוק",
"serverJars2": "למידע מעודכן ביותר.",
"start-error": "השרת {} לא הצליח להתחיל עם קוד שגיאה: {}", "start-error": "השרת {} לא הצליח להתחיל עם קוד שגיאה: {}",
"superError": "חובה להיות משתמש על כדי לבצע פעולה זו.",
"terribleFailure": "איזה כישלון נורא!" "terribleFailure": "איזה כישלון נורא!"
}, },
"footer": { "footer": {
@ -189,7 +218,8 @@
"forgotPassword": "שכחתי סיסמה", "forgotPassword": "שכחתי סיסמה",
"login": "התחברות", "login": "התחברות",
"password": "סיסמה", "password": "סיסמה",
"username": "שם משתמש" "username": "שם משתמש",
"viewStatus": "צפה בדף המצב הציבורי"
}, },
"notify": { "notify": {
"activityLog": "יומני פעילות", "activityLog": "יומני פעילות",
@ -201,24 +231,38 @@
"preparingLogs": "אנא המתינו בזמן שאנו מכינים את היומנים שלכם... נשלח הודעה כשהם יהיו מוכנים. זה עשוי להימשך זמן מה לפריסות שרתים גדולות.", "preparingLogs": "אנא המתינו בזמן שאנו מכינים את היומנים שלכם... נשלח הודעה כשהם יהיו מוכנים. זה עשוי להימשך זמן מה לפריסות שרתים גדולות.",
"supportLogs": "יומני תמיכה" "supportLogs": "יומני תמיכה"
}, },
"offline": {
"offline": "מנותק",
"pleaseConnect": "אנא חבר לאינטרנט כדי להשתמש ב-Crafty."
},
"panelConfig": { "panelConfig": {
"adminControls": "בקרות מנהל", "adminControls": "בקרות מנהל",
"allowedServers": "שרתים מורשים", "allowedServers": "שרתים מורשים",
"apply": "החל",
"assignedRoles": "תפקידים שהוקצו", "assignedRoles": "תפקידים שהוקצו",
"cancel": "ביטול", "cancel": "ביטול",
"clearComms": "ניקוי פקודות שלא בוצעו", "clearComms": "ניקוי פקודות שלא בוצעו",
"custom": "התאמת Crafty",
"delete": "מחיקה", "delete": "מחיקה",
"edit": "עריכה", "edit": "עריכה",
"enableLang": "אפשר כל השפות",
"enabled": "מופעל", "enabled": "מופעל",
"globalExplain": "היכן Crafty שומר את כל קבצי השרת שלך. (נוסיף את הנתיב עם /servers/[uuid של השרת])",
"globalServer": "תיקיית שרתים גלובלית",
"json": "Config.json",
"match": "הסיסמאות חייבות להתאים",
"newRole": "הוספת תפקיד חדש", "newRole": "הוספת תפקיד חדש",
"newUser": "הוספת משתמש חדש", "newUser": "הוספת משתמש חדש",
"noMounts": "אל תציג נקודות עגינה בלוח המחוונים",
"pageTitle": "הגדרת פאנל", "pageTitle": "הגדרת פאנל",
"role": "תפקיד", "role": "תפקיד",
"roleUsers": "תפקידי משתמשים", "roleUsers": "תפקידי משתמשים",
"roles": "תפקידים", "roles": "תפקידים",
"save": "שמירה", "save": "שמירה",
"select": "בחר",
"superConfirm": "המשיכו רק אם אתם רוצים שלמשתמש זה תהיה גישה להכל (כל חשבונות המשתמש, השרתים, הגדרות הפאנל וכו'). הם יכולים אפילו למחוק את זכויות משתמש העל שלך.", "superConfirm": "המשיכו רק אם אתם רוצים שלמשתמש זה תהיה גישה להכל (כל חשבונות המשתמש, השרתים, הגדרות הפאנל וכו'). הם יכולים אפילו למחוק את זכויות משתמש העל שלך.",
"superConfirmTitle": "להפעיל משתמש-על? האם אתם בטוחים?", "superConfirmTitle": "להפעיל משתמש-על? האם אתם בטוחים?",
"title": "תצורת Crafty",
"user": "משתמש", "user": "משתמש",
"users": "משתמשים" "users": "משתמשים"
}, },
@ -242,14 +286,17 @@
"roleTitle": "הגדרות תפקידים", "roleTitle": "הגדרות תפקידים",
"roleUserName": "שם משתמש", "roleUserName": "שם משתמש",
"roleUsers": "תפקידי המשתמשים: ", "roleUsers": "תפקידי המשתמשים: ",
"selectManager": "בחר מנהל לתפקיד זה",
"serverAccess": "?גישה", "serverAccess": "?גישה",
"serverName": "שם שרת", "serverName": "שם שרת",
"serversDesc": "לשרתים מותר לגשת לתפקיד זה" "serversDesc": "לשרתים מותר לגשת לתפקיד זה"
}, },
"serverBackups": { "serverBackups": {
"after": "הרץ פקודה לאחר הגיבוי",
"backupAtMidnight": "גיבוי אוטומטי בחצות?", "backupAtMidnight": "גיבוי אוטומטי בחצות?",
"backupNow": "!גיבוי עכשיו", "backupNow": "!גיבוי עכשיו",
"backupTask": "החלה משימת גיבוי.", "backupTask": "החלה משימת גיבוי.",
"before": "הרץ פקודה לפני הגיבוי",
"cancel": "לבטל", "cancel": "לבטל",
"clickExclude": "לחצו כדי לבחור מה לא יהיה בגיבוי", "clickExclude": "לחצו כדי לבחור מה לא יהיה בגיבוי",
"compress": "דחוס גיבוי", "compress": "דחוס גיבוי",
@ -289,6 +336,8 @@
"deleteServerQuestionMessage": "האם אתם בטוחים שברצונכם למחוק את השרת הזה? אחרי זה אין דרך חזרה...", "deleteServerQuestionMessage": "האם אתם בטוחים שברצונכם למחוק את השרת הזה? אחרי זה אין דרך חזרה...",
"exeUpdateURL": "כתובת ה-URL של עדכון השרת הניתן להפעלה", "exeUpdateURL": "כתובת ה-URL של עדכון השרת הניתן להפעלה",
"exeUpdateURLDesc": "כתובת אתר להורדה ישירה לקבלת עדכונים.", "exeUpdateURLDesc": "כתובת אתר להורדה ישירה לקבלת עדכונים.",
"ignoredExits": "קודי יציאה של קריסה שלא ישמעו",
"ignoredExitsExplain": "קודי יציאה שגילוי הקריסות של Crafty צריך להתעלם מהם כמו 'עצירה' רגילה (מופרדים בפסיקים)",
"javaNoChange": "לא למחוק", "javaNoChange": "לא למחוק",
"javaVersion": "כן למחוק את גרסאת ה-Java המותקנת כרגע", "javaVersion": "כן למחוק את גרסאת ה-Java המותקנת כרגע",
"javaVersionDesc": "אם אתה מתכוון לעקוף את Java, ודא שנתיב ה-Java הנוכחי שלך ב'פקודה לביצוע' עטוף במרכאות (משתנה ברירת המחדל 'java' לא נכלל)", "javaVersionDesc": "אם אתה מתכוון לעקוף את Java, ודא שנתיב ה-Java הנוכחי שלך ב'פקודה לביצוע' עטוף במרכאות (משתנה ברירת המחדל 'java' לא נכלל)",
@ -319,7 +368,13 @@
"serverPortDesc": "קראפטי צריך פורט בשביל להתחבר לנתונים סטטיסטיים", "serverPortDesc": "קראפטי צריך פורט בשביל להתחבר לנתונים סטטיסטיים",
"serverStopCommand": "פקודת עצירת שרת", "serverStopCommand": "פקודת עצירת שרת",
"serverStopCommandDesc": "פקודה לשלוח את התוכנית כדי לעצור אותה", "serverStopCommandDesc": "פקודה לשלוח את התוכנית כדי לעצור אותה",
"showStatus": "הצג בדף המצב הציבורי",
"shutdownTimeout": "זמן קצוב לכיבוי",
"statsHint1": "הפורט שבו השרת שלך פועל צריך להיות כאן. זה רק איך Crafty פותח חיבור לשרת שלך לצורך סטטיסטיקות.",
"statsHint2": "זה לא משנה את פורט השרת שלך. עדיין צריך לשנות את הפורט בקובץ התצורה של השרת שלך.",
"stopBeforeDeleting": "בבקשה לעצור את השרת לפני מחיקתו", "stopBeforeDeleting": "בבקשה לעצור את השרת לפני מחיקתו",
"timeoutExplain1": "כמה זמן Crafty יחכה לשרת שלך להיכבות לאחר ביצוע ה",
"timeoutExplain2": "פקודה לפני שהוא יכריח את התהליך לרדת.",
"update": "עדכנו את קובץ ההפעלה", "update": "עדכנו את קובץ ההפעלה",
"yesDelete": "כן, למחוק", "yesDelete": "כן, למחוק",
"yesDeleteFiles": "כן, מחק קבצים" "yesDeleteFiles": "כן, מחק קבצים"
@ -345,8 +400,12 @@
"backup": "גיבוי", "backup": "גיבוי",
"config": "הגדרות", "config": "הגדרות",
"files": "קבצים", "files": "קבצים",
"filter": "סינון יומנים",
"filterList": "מילים מסוננות",
"logs": "לוג", "logs": "לוג",
"metrics": "מדדים",
"playerControls": "ניהול שחקנים", "playerControls": "ניהול שחקנים",
"reset": "אפס גלילה",
"schedule": "לוח זמנים", "schedule": "לוח זמנים",
"serverDetails": "פרטי שרת", "serverDetails": "פרטי שרת",
"terminal": "מסוף פקודות" "terminal": "מסוף פקודות"
@ -383,6 +442,11 @@
"waitUpload": "אנא המתן בזמן שאנו מעלים את הקבצים שלך... זה עשוי לקחת זמן מה.", "waitUpload": "אנא המתן בזמן שאנו מעלים את הקבצים שלך... זה עשוי לקחת זמן מה.",
"yesDelete": "כן, אני מבין.ה את ההשלכות" "yesDelete": "כן, אני מבין.ה את ההשלכות"
}, },
"serverMetrics": {
"resetZoom": "אפס זום",
"zoomHint1": "כדי להגדיל על הגרף החזק את מקש ה-shift והשתמש בגלגלת הגלילה שלך.",
"zoomHint2": "חלופית, החזק את מקש ה-shift ולחץ וגרור את האזור שברצונך להגדיל עליו."
},
"serverPlayerManagement": { "serverPlayerManagement": {
"bannedPlayers": "שחקנים בבאן", "bannedPlayers": "שחקנים בבאן",
"loadingBannedPlayers": "טוען שחקנים שהם בבאן", "loadingBannedPlayers": "טוען שחקנים שהם בבאן",
@ -410,18 +474,36 @@
"parent-explain": "איזה לוח זמנים צריך להפעיל את זה?", "parent-explain": "איזה לוח זמנים צריך להפעיל את זה?",
"reaction": "תגובה", "reaction": "תגובה",
"restart": "הפעלה מחדש", "restart": "הפעלה מחדש",
"select": "בחר בסיסי / Cron / תגובת שרשרת",
"start": "הדלקת שרת", "start": "הדלקת שרת",
"stop": "כיבוי שרת", "stop": "כיבוי שרת",
"time": "זמן", "time": "זמן",
"time-explain": "באיזו שעה אתה רוצה שהלוח שלך יתבצע?" "time-explain": "באיזו שעה אתה רוצה שהלוח שלך יתבצע?"
}, },
"serverSchedules": { "serverSchedules": {
"action": "פעולה",
"areYouSure": "למחוק משימה מתוזמנת?", "areYouSure": "למחוק משימה מתוזמנת?",
"cancel": "לבטל", "cancel": "לבטל",
"cannotSee": "לא רואים הכל?", "cannotSee": "לא רואים הכל?",
"cannotSeeOnMobile": "נסה ללחוץ על משימה מתוזמנת לפרטים מלאים.", "cannotSeeOnMobile": "נסה ללחוץ על משימה מתוזמנת לפרטים מלאים.",
"child": "ילד של לוח זמנים עם מזהה ",
"close": "סגור",
"command": "פקודה",
"confirm": "אישור", "confirm": "אישור",
"confirmDelete": "האם ברצונך למחוק את המשימה המתוזמנת הזו? אי אפשר לבטל את זה." "confirmDelete": "האם ברצונך למחוק את המשימה המתוזמנת הזו? אי אפשר לבטל את זה.",
"create": "צור לוח זמנים חדש",
"cron": "מחרוזת Cron",
"delete": "מחק",
"details": "פרטי לוח זמנים",
"edit": "ערוך",
"enabled": "מופעל",
"every": "כל",
"interval": "מרווח",
"name": "שם",
"nextRun": "הריצה הבאה",
"no": "לא",
"scheduledTasks": "משימות מתוזמנות",
"yes": "כן"
}, },
"serverStats": { "serverStats": {
"cpuUsage": "שימוש במעבד", "cpuUsage": "שימוש במעבד",
@ -444,6 +526,8 @@
"commandInput": "הקלידו את הפקודה שלכם", "commandInput": "הקלידו את הפקודה שלכם",
"delay-explained": "השירות/סוכן התחיל לאחרונה והוא מעכב את הדלקת שרת המיינקראפט", "delay-explained": "השירות/סוכן התחיל לאחרונה והוא מעכב את הדלקת שרת המיינקראפט",
"downloading": "...מוריד", "downloading": "...מוריד",
"importing": "מייבא...",
"installing": "מתקין...",
"restart": "הפעלה מחדש", "restart": "הפעלה מחדש",
"sendCommand": "שליחת פקודה", "sendCommand": "שליחת פקודה",
"start": "התחלה", "start": "התחלה",
@ -468,6 +552,7 @@
"importServerButton": "ייבוא שרת!", "importServerButton": "ייבוא שרת!",
"importZip": "ייבוא מקובץ Zip", "importZip": "ייבוא מקובץ Zip",
"importing": "מייבא שרת...", "importing": "מייבא שרת...",
"labelZipFile": "בחר את קובץ ה-Zip שלך",
"maxMem": "מקסימום זיכרון", "maxMem": "מקסימום זיכרון",
"minMem": "מינימום זיכרון", "minMem": "מינימום זיכרון",
"myNewServer": "השרת החדש שלי", "myNewServer": "השרת החדש שלי",
@ -478,6 +563,7 @@
"save": "שמור", "save": "שמור",
"selectRole": "בחר תפקידים", "selectRole": "בחר תפקידים",
"selectRoot": "בחר ארכיון שורש Dir", "selectRoot": "בחר ארכיון שורש Dir",
"selectServer": "בחר שרת",
"selectType": "בחר סוג", "selectType": "בחר סוג",
"selectVersion": "בחר גרסה", "selectVersion": "בחר גרסה",
"selectZipDir": "בחר את הספרייה בארכיון שממנו אתה רוצה שנפתח קבצים", "selectZipDir": "בחר את הספרייה בארכיון שממנו אתה רוצה שנפתח קבצים",
@ -485,9 +571,13 @@
"serverName": "שם השרת", "serverName": "שם השרת",
"serverPath": "נתיב שרת", "serverPath": "נתיב שרת",
"serverPort": "פורט שרת", "serverPort": "פורט שרת",
"serverSelect": "בחירת שרת",
"serverType": "סוג השרת", "serverType": "סוג השרת",
"serverUpload": "העלה שרת מכווץ",
"serverVersion": "גרסת השרת", "serverVersion": "גרסת השרת",
"sizeInGB": "גודל ב-GB", "sizeInGB": "גודל ב-GB",
"uploadButton": "העלה",
"uploadZip": "העלה קובץ Zip לייבוא שרת",
"zipPath": "נתיב שרת" "zipPath": "נתיב שרת"
}, },
"sidebar": { "sidebar": {
@ -495,10 +585,20 @@
"credits": "קרדיט", "credits": "קרדיט",
"dashboard": "פאנל", "dashboard": "פאנל",
"documentation": "ויקיפדייה", "documentation": "ויקיפדייה",
"inApp": "מסמכים באפליקציה",
"navigation": "ניווט", "navigation": "ניווט",
"newServer": "צור שרת חדש", "newServer": "צור שרת חדש",
"servers": "שרתים" "servers": "שרתים"
}, },
"startup": {
"almost": "מסיימים. תחזיקו חזק...",
"internals": "הגדרה והפעלה של הרכיבים הפנימיים של Crafty",
"internet": "בודק את חיבור האינטרנט",
"server": "אתחול ",
"serverInit": "מפעיל שרתים",
"starting": "מתחילים בהפעלת מערכת Crafty...",
"tasks": "מתזמן את מחולל המשימות"
},
"userConfig": { "userConfig": {
"apiKey": "API מפתחות", "apiKey": "API מפתחות",
"auth": "מורשה? ", "auth": "מורשה? ",
@ -519,6 +619,7 @@
"lastLogin": "כניסה אחרונה: ", "lastLogin": "כניסה אחרונה: ",
"lastUpdate": "עדכון אחרון: ", "lastUpdate": "עדכון אחרון: ",
"leaveBlank": "כדי לערוך משתמש מבלי לשנות סיסמה השאר אותו ריק.", "leaveBlank": "כדי לערוך משתמש מבלי לשנות סיסמה השאר אותו ריק.",
"manager": "מנהל",
"member": "חבר?", "member": "חבר?",
"notExist": "אתה לא יכול למחוק משהו שלא קיים!", "notExist": "אתה לא יכול למחוק משהו שלא קיים!",
"pageTitle": "ערוך משתמש", "pageTitle": "ערוך משתמש",
@ -527,6 +628,7 @@
"permName": "שם הרשאה", "permName": "שם הרשאה",
"repeat": "חזור על הסיסמה", "repeat": "חזור על הסיסמה",
"roleName": "שם התפקיד", "roleName": "שם התפקיד",
"selectManager": "בחר מנהל למשתמש",
"super": "משתמש על", "super": "משתמש על",
"userLang": "שפת משתמש", "userLang": "שפת משתמש",
"userName": "שם משתמש", "userName": "שם משתמש",
@ -534,6 +636,30 @@
"userRoles": "תפקידי משתמש", "userRoles": "תפקידי משתמש",
"userRolesDesc": "תפקידים שמשתמש זה חבר בהם", "userRolesDesc": "תפקידים שמשתמש זה חבר בהם",
"userSettings": "הגדרות משתמש", "userSettings": "הגדרות משתמש",
"userTheme": "ערכת נושא UI",
"uses": "מספר השימושים המותרים (-1==ללא הגבלה)" "uses": "מספר השימושים המותרים (-1==ללא הגבלה)"
},
"webhooks": {
"areYouSureDel": "האם אתה בטוח שברצונך למחוק את ה-Webhook הזה?",
"areYouSureRun": "האם אתה בטוח שברצונך לבדוק את ה-Webhook הזה?",
"backup_server": "גיבוי השרת הושלם",
"bot_name": "שם הבוט",
"color": "בחר גוון צבע",
"crash_detected": "השרת קרס",
"edit": "ערוך",
"enabled": "מופעל",
"jar_update": "השרת הביצועי עודכן",
"kill": "השרת נסגר",
"name": "שם",
"new": "Webhook חדש",
"run": "הרץ Webhook לבדיקה",
"send_command": "פקודת שרת התקבלה",
"start_server": "השרת הופעל",
"stop_server": "השרת נעצר",
"trigger": "גרם",
"type": "סוג ה-Webhook",
"url": "URL של ה-Webhook",
"webhook_body": "גוף ה-Webhook",
"webhooks": "Webhooks"
} }
} }

View File

@ -111,6 +111,7 @@
"starting": "I WAITZ B4 I START", "starting": "I WAITZ B4 I START",
"status": "STATUZ", "status": "STATUZ",
"stop": "STAHP PLZ", "stop": "STAHP PLZ",
"storage": "STASH BOX",
"version": "VERZHUN", "version": "VERZHUN",
"welcome": "WELCOM 2 CWAFTY CONTROLLR" "welcome": "WELCOM 2 CWAFTY CONTROLLR"
}, },
@ -591,6 +592,15 @@
"newServer": "CONSTWUCT A SERVR", "newServer": "CONSTWUCT A SERVR",
"servers": "SERVRS" "servers": "SERVRS"
}, },
"startup": {
"almost": "ALMOST DUN. HOLD ON TO YER WHISKERS...",
"internals": "SETTIN' UP AN' STARTIN' CWAFTY'S INSIDE BITZ",
"internet": "LOOKIN' FOR OUTER SPACE TALKY",
"server": "WAKIN' UPZ ",
"serverInit": "MAKIN' SERVERZ READY",
"starting": "CWAFTY WAKING UP...",
"tasks": "STARTIN' TASK SCHEDULERZ"
},
"userConfig": { "userConfig": {
"apiKey": "API KEYS", "apiKey": "API KEYS",
"auth": "PERMISHUN TO ACESS? ", "auth": "PERMISHUN TO ACESS? ",

View File

@ -112,6 +112,7 @@
"starting": "Aizkavēts-Starts", "starting": "Aizkavēts-Starts",
"status": "Statuss", "status": "Statuss",
"stop": "Apturēt", "stop": "Apturēt",
"storage": "Glabātuve",
"version": "Versija", "version": "Versija",
"welcome": "Esiet sveicināts Crafty Controller" "welcome": "Esiet sveicināts Crafty Controller"
}, },
@ -592,6 +593,15 @@
"newServer": "Izveidot Jaunu Serveri", "newServer": "Izveidot Jaunu Serveri",
"servers": "Serveri" "servers": "Serveri"
}, },
"startup": {
"almost": "Pabeidz. Vēl tik nedaudz...",
"internals": "Konfigurē un Startē Crafty iekšējās komponenetes",
"internet": "Pārbauda interneta savienojumu",
"server": "Inicializē ",
"serverInit": "Inicializē Serverus",
"starting": "Crafty Startējas...",
"tasks": "Sāknē Notikumu Plānotāju"
},
"userConfig": { "userConfig": {
"apiKey": "API Atslēgas", "apiKey": "API Atslēgas",
"auth": "Authorizēts? ", "auth": "Authorizēts? ",

View File

@ -111,6 +111,7 @@
"starting": "Vertraagde start", "starting": "Vertraagde start",
"status": "Toestand", "status": "Toestand",
"stop": "Stoppen", "stop": "Stoppen",
"storage": "Opslagruimte",
"version": "Versie", "version": "Versie",
"welcome": "Welkom bij Crafty Controller " "welcome": "Welkom bij Crafty Controller "
}, },
@ -591,6 +592,15 @@
"newServer": "Nieuwe server maken", "newServer": "Nieuwe server maken",
"servers": "Servers" "servers": "Servers"
}, },
"startup": {
"almost": "De laatste hand leggen. Houd je vast...",
"internals": "Crafty's interne componenten configureren en starten",
"internet": "Controleren op internetverbinding",
"server": "Initialiseren ",
"serverInit": "Servers initialiseren",
"starting": "Crafty wordt gestart...",
"tasks": "Start takenplanner"
},
"userConfig": { "userConfig": {
"apiKey": "API Sleutels", "apiKey": "API Sleutels",
"auth": "Bevoegd? ", "auth": "Bevoegd? ",

View File

@ -111,6 +111,7 @@
"starting": "Opóźniony-Start", "starting": "Opóźniony-Start",
"status": "Status", "status": "Status",
"stop": "Zatrzymaj", "stop": "Zatrzymaj",
"storage": "Przestrzeń dyskowa",
"version": "Wersja", "version": "Wersja",
"welcome": "Witamy w Crafty Controller" "welcome": "Witamy w Crafty Controller"
}, },
@ -326,8 +327,8 @@
"bePatientDeleteFiles": "Poczekaj, aż usuniemy twój serwer i jego pliki. Strona za chwilę się zamknie.", "bePatientDeleteFiles": "Poczekaj, aż usuniemy twój serwer i jego pliki. Strona za chwilę się zamknie.",
"bePatientUpdate": "Poczekaj kiedy my aktualizujemy twój serwer. Pobieranie zależy od prędkości twojego internetu.<br /> Strona się odświeży za chwile.", "bePatientUpdate": "Poczekaj kiedy my aktualizujemy twój serwer. Pobieranie zależy od prędkości twojego internetu.<br /> Strona się odświeży za chwile.",
"cancel": "Anuluj", "cancel": "Anuluj",
"crashTime": "Crash wyszedł poza limit czasu", "crashTime": "Crash serwera wyszedł poza limit czasu",
"crashTimeDesc": "How long should we wait before we consider your server as crashed?", "crashTimeDesc": "Jak długo powinniśmy poczekać zanim uznać serwer za zcrashowany?",
"deleteFilesQuestion": "Usuń pliki serwera z maszyny?", "deleteFilesQuestion": "Usuń pliki serwera z maszyny?",
"deleteFilesQuestionMessage": "Czy chcesz aby Crafty usunął wszystkie pliki tego serwera? <br><br><strong>To zawiera backupy.</strong>", "deleteFilesQuestionMessage": "Czy chcesz aby Crafty usunął wszystkie pliki tego serwera? <br><br><strong>To zawiera backupy.</strong>",
"deleteServer": "Usuń serwer", "deleteServer": "Usuń serwer",
@ -403,7 +404,7 @@
"filterList": "Filtrowane słowa", "filterList": "Filtrowane słowa",
"logs": "Logi", "logs": "Logi",
"metrics": "Statystyki", "metrics": "Statystyki",
"playerControls": "Player Management", "playerControls": "Zarządzanie użytkownikami",
"reset": "Resetuj Scrolla", "reset": "Resetuj Scrolla",
"schedule": "Harmonogram", "schedule": "Harmonogram",
"serverDetails": "Detale serwera", "serverDetails": "Detale serwera",
@ -421,7 +422,7 @@
"deleteItemQuestion": "Czy jesteś pewien że chcesz usunąć \" + name + \"?", "deleteItemQuestion": "Czy jesteś pewien że chcesz usunąć \" + name + \"?",
"deleteItemQuestionMessage": "Usuwasz \\\"\" + path + \"\\\"!<br/><br/>Ta akcja jest nieodwracalna i zostanie usunięta na zawsze!", "deleteItemQuestionMessage": "Usuwasz \\\"\" + path + \"\\\"!<br/><br/>Ta akcja jest nieodwracalna i zostanie usunięta na zawsze!",
"download": "Pobierz", "download": "Pobierz",
"editingFile": "Edytuję plik", "editingFile": "Edytuj plik",
"error": "Error while getting files", "error": "Error while getting files",
"fileReadError": "Error odczytu pliku", "fileReadError": "Error odczytu pliku",
"files": "Pliki", "files": "Pliki",
@ -432,7 +433,7 @@
"rename": "Zmień nazwę", "rename": "Zmień nazwę",
"renameItemQuestion": "Jaka ma być nowa nazwa?", "renameItemQuestion": "Jaka ma być nowa nazwa?",
"save": "Zapisz", "save": "Zapisz",
"size": "Włącz zmienianie rozmiaru edytora", "size": "Włącz rozszerzanie i zmniejszanie edytora",
"stayHere": "NIE WYCHODŹ Z TEJ STRONY!", "stayHere": "NIE WYCHODŹ Z TEJ STRONY!",
"unsupportedLanguage": "Uwaga: To nie jest wspierany typ pliku", "unsupportedLanguage": "Uwaga: To nie jest wspierany typ pliku",
"unzip": "Rozpakuj", "unzip": "Rozpakuj",
@ -545,7 +546,7 @@
"buildServer": "Zbuduj serwer!", "buildServer": "Zbuduj serwer!",
"clickRoot": "Kilknij tutaj aby zaznaczyć główną ścieżkę", "clickRoot": "Kilknij tutaj aby zaznaczyć główną ścieżkę",
"close": "Zamknij", "close": "Zamknij",
"defaultPort": "25565 podstawowy", "defaultPort": "Domyślnie 25565",
"downloading": "Pobieranie serwera...", "downloading": "Pobieranie serwera...",
"explainRoot": "Proszę, kliknij przycisk poniżej aby zaznaczyć główną ścieżkę w tym archiwum", "explainRoot": "Proszę, kliknij przycisk poniżej aby zaznaczyć główną ścieżkę w tym archiwum",
"importServer": "Importuj egzystujący serwer", "importServer": "Importuj egzystujący serwer",
@ -590,6 +591,15 @@
"newServer": "Stwórz nowy serwer", "newServer": "Stwórz nowy serwer",
"servers": "Serwery" "servers": "Serwery"
}, },
"startup": {
"almost": "Prawie gotowe! Jeszcze tylko chwilka...",
"internals": "Konfigurowanie i włączanie backendu...",
"internet": "Sprawdzam połączenie z internetem",
"server": "Włączanie ",
"serverInit": "Ładuje serwery...",
"starting": "Włączam Craftiego...",
"tasks": "Włączanie harmonogramu..."
},
"userConfig": { "userConfig": {
"apiKey": "Klucze API", "apiKey": "Klucze API",
"auth": "Autoryzacja? ", "auth": "Autoryzacja? ",
@ -640,12 +650,12 @@
"edit": "Edytuj", "edit": "Edytuj",
"enabled": "Włączony", "enabled": "Włączony",
"jar_update": "Plik startowy zaktualizowany", "jar_update": "Plik startowy zaktualizowany",
"kill": "Serwer zatrzymany", "kill": "Serwer został zabity",
"name": "Nazwa", "name": "Nazwa",
"new": "Nowy Webhook", "new": "Nowy Webhook",
"newWebhook": "Nowy Webhook", "newWebhook": "Nowy Webhook",
"no-webhook": "Nie posiadasz aktualnie żadnych Webhooków dla tego serwera. Aby dodać webhook kliknij na", "no-webhook": "Nie posiadasz aktualnie żadnych Webhooków dla tego serwera. Aby dodać webhook kliknij na",
"run": "Włącz Webhook", "run": "Przetestuj Webhook",
"send_command": "Komenda serwera otrzymana!", "send_command": "Komenda serwera otrzymana!",
"start_server": "Serwer włączony", "start_server": "Serwer włączony",
"stop_server": "Serwer wyłączony", "stop_server": "Serwer wyłączony",

View File

@ -111,6 +111,7 @@
"starting": "延迟启动", "starting": "延迟启动",
"status": "状态", "status": "状态",
"stop": "停止", "stop": "停止",
"storage": "存储",
"version": "版本", "version": "版本",
"welcome": "欢迎来到 Crafty Controller" "welcome": "欢迎来到 Crafty Controller"
}, },
@ -591,6 +592,15 @@
"newServer": "创建新服务器", "newServer": "创建新服务器",
"servers": "服务器" "servers": "服务器"
}, },
"startup": {
"almost": "即将完成。请稍候……",
"internals": "正在配置并启动 Crafty 的内部组件",
"internet": "正在检查网络连接",
"server": "正在初始化 ",
"serverInit": "正在初始化服务器",
"starting": "Crafty 正在启动……",
"tasks": "正在启动任务计划器"
},
"userConfig": { "userConfig": {
"apiKey": "API 密钥", "apiKey": "API 密钥",
"auth": "已授权?", "auth": "已授权?",

View File

@ -21,7 +21,7 @@
</Branch> </Branch>
<Screenshot>https://wiki.craftycontrol.com/uploads/en/crafty%204%20dashboard%20with%20one%20server.jpeg</Screenshot> <Screenshot>https://wiki.craftycontrol.com/uploads/en/crafty%204%20dashboard%20with%20one%20server.jpeg</Screenshot>
<Screenshot>https://wiki.craftycontrol.com/uploads/en/crafty%204%20server%20setup%20details.png</Screenshot> <Screenshot>https://wiki.craftycontrol.com/uploads/en/crafty%204%20server%20setup%20details.png</Screenshot>
<Overview>Crafty 4 is the next iteration of our Minecraft Server Wrapper / Controller / Launcher. [br]Boasting a clean new look, rebuilt from the ground up. [br] [br] Crafty 4 brings a whole host of new features such as Bedrock support. [br] With SteamCMD support on the way![br] **Default login Credentrails are username: "admin" password: "crafty". ** [br]Crafty 4 is the successor of Crafty Controller. [br]For official support join the Discord server https://discord.gg/9VJPhCE [br] For migration from 3.x please refer to the documentation: https://wiki.craftycontrol.com/en/4/ <Overview>Crafty 4 is the next iteration of our Minecraft Server Wrapper / Controller / Launcher. [br]Boasting a clean new look, rebuilt from the ground up. [br] [br] Crafty 4 brings a whole host of new features such as Bedrock support. [br] With SteamCMD support on the way![br] **Default login Credentrails are stored in your Crafty Configuration location in the file default-creds.txt ** [br]Crafty 4 is the successor of Crafty Controller. [br]For official support join the Discord server https://discord.gg/9VJPhCE [br] For migration from 3.x please refer to the documentation: https://wiki.craftycontrol.com/en/4/
</Overview> </Overview>
<Category>GameServers: Other:</Category> <Category>GameServers: Other:</Category>
<WebUI>https://[IP]:[PORT:8443]/</WebUI> <WebUI>https://[IP]:[PORT:8443]/</WebUI>

370
main.py
View File

@ -21,6 +21,18 @@ from app.classes.shared.websocket_manager import WebSocketManager
console = Console() console = Console()
helper = Helpers() helper = Helpers()
# Get the path our application is running on.
if getattr(sys, "frozen", False):
APPLICATION_PATH = os.path.dirname(sys.executable)
RUNNING_MODE = "Frozen/executable"
else:
try:
app_full_path = os.path.realpath(__file__)
APPLICATION_PATH = os.path.dirname(app_full_path)
RUNNING_MODE = "Non-interactive (e.g. 'python main.py')"
except NameError:
APPLICATION_PATH = os.getcwd()
RUNNING_MODE = "Interactive"
if helper.check_root(): if helper.check_root():
Console.critical( Console.critical(
"Root detected. Root/Admin access denied. " "Root detected. Root/Admin access denied. "
@ -40,6 +52,7 @@ if not (sys.version_info.major == 3 and sys.version_info.minor >= 9):
time.sleep(3) time.sleep(3)
Console.info("Crafty stopped. Exiting...") Console.info("Crafty stopped. Exiting...")
sys.exit(0) sys.exit(0)
# pylint: disable=wrong-import-position # pylint: disable=wrong-import-position
try: try:
from app.classes.models.base_model import database_proxy from app.classes.models.base_model import database_proxy
@ -52,7 +65,180 @@ except ModuleNotFoundError as err:
helper.auto_installer_fix(err) helper.auto_installer_fix(err)
def internet_check():
"""
This checks to see if the Crafty host is connected to the
internet. This will show a warning in the console if no interwebs.
"""
print()
logger.info("Checking Internet. This may take a minute.")
Console.info("Checking Internet. This may take a minute.")
if not helper.check_internet():
logger.warning(
"We have detected the machine running Crafty has no "
"connection to the internet. Client connections to "
"the server may be limited."
)
Console.warning(
"We have detected the machine running Crafty has no "
"connection to the internet. Client connections to "
"the server may be limited."
)
def controller_setup():
"""
Method sets up the software controllers.
This also sets the application path as well as the
master server dir (if not set).
This also clears the support logs status.
"""
if not controller.check_system_user():
controller.add_system_user()
master_server_dir = controller.management.get_master_server_dir()
if master_server_dir == "":
logger.debug("Could not find master server path. Setting default")
controller.set_master_server_dir(
os.path.join(controller.project_root, "servers")
)
else:
helper.servers_dir = master_server_dir
logger.info(f"Execution Mode: {RUNNING_MODE}")
logger.info(f"Application path: '{APPLICATION_PATH}'")
Console.info(f"Execution Mode: {RUNNING_MODE}")
Console.info(f"Application path: '{APPLICATION_PATH}'")
controller.clear_support_status()
def tasks_starter():
"""
Method starts stats recording, app scheduler, and
serverjars/steamCMD cache refreshers
"""
# start stats logging
tasks_manager.start_stats_recording()
# once the controller is up and stats are logging, we can kick off
# the scheduler officially
tasks_manager.start_scheduler()
# refresh our cache and schedule for every 12 hoursour cache refresh
# for serverjars.com
tasks_manager.serverjar_cache_refresher()
def signal_handler(signum, _frame):
"""
Method handles sigterm and shuts the app down.
"""
if not args.daemon:
print() # for newline after prompt
signame = signal.Signals(signum).name
logger.info(f"Recieved signal {signame} [{signum}], stopping Crafty...")
Console.info(f"Recieved signal {signame} [{signum}], stopping Crafty...")
tasks_manager._main_graceful_exit()
crafty_prompt.universal_exit()
def do_cleanup():
"""
Checks Crafty's temporary directory and clears it out on boot.
"""
try:
logger.info("Removing old temp dirs")
FileHelpers.del_dirs(os.path.join(controller.project_root, "temp"))
except:
logger.info("Did not find old temp dir.")
os.mkdir(os.path.join(controller.project_root, "temp"))
def do_version_check():
"""
Checks for remote version differences.
Prints in terminal with differences if true.
Also sets helper variable to update available when pages
are served.
"""
# Check if new version available
remote_ver = helper.check_remote_version()
if remote_ver:
notice = f"""
A new version of Crafty is available!
{'/' * 37}
New version available: {remote_ver}
Current version: {pkg_version.parse(helper.get_version_string())}
{'/' * 37}
"""
Console.yellow(notice)
crafty_prompt.prompt = f"Crafty Controller v{helper.get_version_string()} > "
def setup_starter():
"""
This method starts our setup threads.
(tasks scheduler, internet checks, controller setups)
Once our threads complete we will set our startup
variable to false and send a reload to any clients waiting.
"""
if not args.daemon:
time.sleep(0.01) # Wait for the prompt to start
print() # Make a newline after the prompt so logs are on an empty line
else:
time.sleep(0.01) # Wait for the daemon info message
Console.info("Setting up Crafty's internal components...")
# Start the setup threads
web_sock.broadcast("update", {"section": "tasks"})
time.sleep(2)
tasks_starter_thread.start()
web_sock.broadcast("update", {"section": "internet"})
time.sleep(2)
internet_check_thread.start()
web_sock.broadcast(
"update",
{"section": "internals"},
)
time.sleep(2)
controller_setup_thread.start()
# Wait for the setup threads to finish
web_sock.broadcast(
"update",
{"section": "almost"},
)
tasks_starter_thread.join()
internet_check_thread.join()
controller_setup_thread.join()
helper.crafty_starting = False
web_sock.broadcast("send_start_reload", "")
do_version_check()
Console.info("Crafty has fully started and is now ready for use!")
do_cleanup()
if not args.daemon:
# Put the prompt under the cursor
crafty_prompt.print_prompt()
def do_intro(): def do_intro():
"""
Runs the Crafty Controller Terminal Intro with information about the software
This method checks for a "settings file" or config.json. If it does not find
one it will create one.
"""
logger.info("***** Crafty Controller Started *****") logger.info("***** Crafty Controller Started *****")
version = helper.get_version_string() version = helper.get_version_string()
@ -73,7 +259,22 @@ def do_intro():
def setup_logging(debug=True): def setup_logging(debug=True):
logging_config_file = os.path.join(os.path.curdir, "app", "config", "logging.json") """
This method sets up our logging for Crafty. It takes
one optional (defaulted to True) parameter which
determines whether or not the logging level is "debug" or verbose.
"""
logging_config_file = os.path.join(
APPLICATION_PATH, "app", "config", "logging.json"
)
if not helper.check_file_exists(
os.path.join(APPLICATION_PATH, "logs", "auth_tracker.log")
):
open(
os.path.join(APPLICATION_PATH, "logs", "auth_tracker.log"),
"a",
encoding="utf-8",
).close()
if os.path.exists(logging_config_file): if os.path.exists(logging_config_file):
# open our logging config file # open our logging config file
@ -110,11 +311,11 @@ if __name__ == "__main__":
) )
args = parser.parse_args() args = parser.parse_args()
helper.ensure_logging_setup() helper.ensure_logging_setup()
helper.crafty_starting = True
# Init WebSocket Manager Here
web_sock = WebSocketManager()
setup_logging(debug=args.verbose) setup_logging(debug=args.verbose)
if args.verbose: if args.verbose:
Console.level = "debug" Console.level = "debug"
@ -126,20 +327,18 @@ if __name__ == "__main__":
# print our pretty start message # print our pretty start message
do_intro() do_intro()
# our session file, helps prevent multiple controller agents on the same machine. # our session file, helps prevent multiple controller agents on the same machine.
helper.create_session_file(ignore=args.ignore) helper.create_session_file(ignore=args.ignore)
# start the database # start the database
database = peewee.SqliteDatabase( database = peewee.SqliteDatabase(
helper.db_path, pragmas={"journal_mode": "wal", "cache_size": -1024 * 10} helper.db_path, pragmas={"journal_mode": "wal", "cache_size": -1024 * 10}
) )
database_proxy.initialize(database) database_proxy.initialize(database)
migration_manager = MigrationManager(database, helper) migration_manager = MigrationManager(database, helper)
migration_manager.up() # Automatically runs migrations migration_manager.up() # Automatically runs migrations
# do our installer stuff # init classes
# now the tables are created, we can load the tasks_manager and server controller
user_helper = HelperUsers(database, helper) user_helper = HelperUsers(database, helper)
management_helper = HelpersManagement(database, helper) management_helper = HelpersManagement(database, helper)
installer = DatabaseBuilder(database, helper, user_helper, management_helper) installer = DatabaseBuilder(database, helper, user_helper, management_helper)
@ -153,7 +352,19 @@ if __name__ == "__main__":
f"through your router/firewall if you would like to be able " f"through your router/firewall if you would like to be able "
f"to access Crafty remotely." f"to access Crafty remotely."
) )
installer.default_settings() PASSWORD = helper.create_pass()
installer.default_settings(PASSWORD)
with open(
os.path.join(APPLICATION_PATH, "app", "config", "default-creds.txt"),
"w",
encoding="utf-8",
) as cred_file:
cred_file.write(
json.dumps({"username": "admin", "password": PASSWORD}, indent=4)
)
os.chmod(
os.path.join(APPLICATION_PATH, "app", "config", "default-creds.txt"), 0o600
)
else: else:
Console.debug("Existing install detected") Console.debug("Existing install detected")
Console.info("Checking for reset secret flag") Console.info("Checking for reset secret flag")
@ -164,154 +375,55 @@ if __name__ == "__main__":
helper.set_setting("reset_secrets_on_next_boot", False) helper.set_setting("reset_secrets_on_next_boot", False)
else: else:
Console.info("No flag found. Secrets are staying") Console.info("No flag found. Secrets are staying")
# now we've initialized our database for fresh install we
# can finishing initializing our controllers/modules
file_helper = FileHelpers(helper) file_helper = FileHelpers(helper)
import_helper = ImportHelpers(helper, file_helper) import_helper = ImportHelpers(helper, file_helper)
# Init WebSocket Manager Here
WebSocketManager()
# now the tables are created, we can load the tasks_manager and server controller
controller = Controller(database, helper, file_helper, import_helper) controller = Controller(database, helper, file_helper, import_helper)
controller.set_project_root(APPLICATION_PATH)
tasks_manager = TasksManager(helper, controller, file_helper)
import3 = Import3(helper, controller)
migrate_uuid = MigrateUUID(helper, controller)
# Check to see if client config.json version is different than the
# Master config.json in helpers.py
Console.info("Checking for remote changes to config.json") Console.info("Checking for remote changes to config.json")
controller.get_config_diff() controller.get_config_diff()
Console.info("Remote change complete.") Console.info("Remote change complete.")
import3 = Import3(helper, controller) # startup the web server
migrate_uuid = MigrateUUID(helper, controller)
tasks_manager = TasksManager(helper, controller, file_helper)
tasks_manager.start_webserver() tasks_manager.start_webserver()
def signal_handler(signum, _frame):
if not args.daemon:
print() # for newline after prompt
signame = signal.Signals(signum).name
logger.info(f"Recieved signal {signame} [{signum}], stopping Crafty...")
Console.info(f"Recieved signal {signame} [{signum}], stopping Crafty...")
tasks_manager._main_graceful_exit()
crafty_prompt.universal_exit()
signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)
# init servers # init servers
logger.info("Initializing all servers defined") logger.info("Initializing all servers defined")
Console.info("Initializing all servers defined") Console.info("Initializing all servers defined")
web_sock.broadcast(
"update",
{"section": "serverInit"},
)
controller.servers.init_all_servers() controller.servers.init_all_servers()
def tasks_starter(): # start up our tasks handler in tasks.py
# start stats logging
tasks_manager.start_stats_recording()
# once the controller is up and stats are logging, we can kick off
# the scheduler officially
tasks_manager.start_scheduler()
# refresh our cache and schedule for every 12 hoursour cache refresh
# for serverjars.com
tasks_manager.serverjar_cache_refresher()
tasks_starter_thread = Thread(target=tasks_starter, name="tasks_starter") tasks_starter_thread = Thread(target=tasks_starter, name="tasks_starter")
def internet_check(): # check to see if instance has internet
print()
logger.info("Checking Internet. This may take a minute.")
Console.info("Checking Internet. This may take a minute.")
if not helper.check_internet():
logger.warning(
"We have detected the machine running Crafty has no "
"connection to the internet. Client connections to "
"the server may be limited."
)
Console.warning(
"We have detected the machine running Crafty has no "
"connection to the internet. Client connections to "
"the server may be limited."
)
internet_check_thread = Thread(target=internet_check, name="internet_check") internet_check_thread = Thread(target=internet_check, name="internet_check")
def controller_setup(): # start the Crafty console.
if not controller.check_system_user():
controller.add_system_user()
if getattr(sys, "frozen", False):
application_path = os.path.dirname(sys.executable)
running_mode = "Frozen/executable"
else:
try:
app_full_path = os.path.realpath(__file__)
application_path = os.path.dirname(app_full_path)
running_mode = "Non-interactive (e.g. 'python main.py')"
except NameError:
application_path = os.getcwd()
running_mode = "Interactive"
controller.set_project_root(application_path)
master_server_dir = controller.management.get_master_server_dir()
if master_server_dir == "":
logger.debug("Could not find master server path. Setting default")
controller.set_master_server_dir(
os.path.join(controller.project_root, "servers")
)
else:
helper.servers_dir = master_server_dir
Console.debug(f"Execution Mode: {running_mode}")
Console.debug(f"Application path : '{application_path}'")
controller.clear_support_status()
crafty_prompt = MainPrompt( crafty_prompt = MainPrompt(
helper, tasks_manager, migration_manager, controller, import3, migrate_uuid helper, tasks_manager, migration_manager, controller, import3, migrate_uuid
) )
# set up all controllers
controller_setup_thread = Thread(target=controller_setup, name="controller_setup") controller_setup_thread = Thread(target=controller_setup, name="controller_setup")
def setup_starter(): setup_starter_thread = Thread(target=setup_starter, name="setup_starter")
if not args.daemon:
time.sleep(0.01) # Wait for the prompt to start
print() # Make a newline after the prompt so logs are on an empty line
else:
time.sleep(0.01) # Wait for the daemon info message
Console.info("Setting up Crafty's internal components...") setup_starter_thread.start()
# Start the setup threads
tasks_starter_thread.start()
internet_check_thread.start()
controller_setup_thread.start()
# Wait for the setup threads to finish
tasks_starter_thread.join()
internet_check_thread.join()
controller_setup_thread.join()
Console.info("Crafty has fully started and is now ready for use!")
# Check if new version available
remote_ver = helper.check_remote_version()
if remote_ver:
notice = f"""
A new version of Crafty is available!
{'/' * 37}
New version available: {remote_ver}
Current version: {pkg_version.parse(helper.get_version_string())}
{'/' * 37}
"""
Console.yellow(notice)
crafty_prompt.prompt = f"Crafty Controller v{helper.get_version_string()} > "
try:
logger.info("Removing old temp dirs")
FileHelpers.del_dirs(os.path.join(controller.project_root, "temp"))
except:
logger.info("Did not find old temp dir.")
os.mkdir(os.path.join(controller.project_root, "temp"))
if not args.daemon:
# Put the prompt under the cursor
crafty_prompt.print_prompt()
Thread(target=setup_starter, name="setup_starter").start()
if not args.daemon: if not args.daemon:
# Start the Crafty prompt # Start the Crafty prompt

View File

@ -4,13 +4,13 @@ argon2-cffi==23.1.0
cached_property==1.5.2 cached_property==1.5.2
colorama==0.4.6 colorama==0.4.6
croniter==1.4.1 croniter==1.4.1
cryptography==41.0.4 cryptography==41.0.7
libgravatar==1.0.4 libgravatar==1.0.4
nh3==0.2.14 nh3==0.2.14
packaging==23.2 packaging==23.2
peewee==3.13 peewee==3.13
psutil==5.9.5 psutil==5.9.5
pyOpenSSL==23.2.0 pyOpenSSL==23.3.0
pyjwt==2.8.0 pyjwt==2.8.0
PyYAML==6.0.1 PyYAML==6.0.1
requests==2.31.0 requests==2.31.0

View File

@ -3,7 +3,7 @@ sonar.organization=crafty-controller
# This is the name and version displayed in the SonarCloud UI. # This is the name and version displayed in the SonarCloud UI.
sonar.projectName=Crafty 4 sonar.projectName=Crafty 4
sonar.projectVersion=4.2.1 sonar.projectVersion=4.2.3
sonar.python.version=3.9, 3.10, 3.11 sonar.python.version=3.9, 3.10, 3.11
sonar.exclusions=app/migrations/**, app/frontend/static/assets/vendors/** sonar.exclusions=app/migrations/**, app/frontend/static/assets/vendors/**