Merge branch 'dev' into refactor/backups

This commit is contained in:
--unset 2024-05-15 21:01:16 -04:00
commit 6ca396854d
84 changed files with 1019 additions and 972 deletions

View File

@ -5,7 +5,7 @@ yamllint:
stage: lint
image: registry.gitlab.com/pipeline-components/yamllint:latest
tags:
- docker
- saas-linux-medium-amd64
rules:
- if: "$CODE_QUALITY_DISABLED"
when: never
@ -18,7 +18,7 @@ jsonlint:
stage: lint
image: registry.gitlab.com/pipeline-components/jsonlint:latest
tags:
- docker
- saas-linux-medium-amd64
rules:
- if: "$CODE_QUALITY_DISABLED"
when: never
@ -33,7 +33,7 @@ black:
stage: lint
image: registry.gitlab.com/pipeline-components/black:latest
tags:
- docker
- saas-linux-medium-amd64
rules:
- if: "$CODE_QUALITY_DISABLED"
when: never
@ -46,7 +46,7 @@ pylint:
stage: lint
image: registry.gitlab.com/pipeline-components/pylint:latest
tags:
- docker
- saas-linux-medium-amd64
rules:
- if: "$CODE_QUALITY_DISABLED"
when: never
@ -69,7 +69,7 @@ sonarcloud-check:
name: sonarsource/sonar-scanner-cli:latest
entrypoint: [""]
tags:
- docker
- saas-linux-medium-amd64
rules:
- if: "$SONAR_TOKEN == null"
when: never
@ -91,7 +91,7 @@ lang-check:
stage: lint
image: alpine:latest
tags:
- docker
- saas-linux-medium-amd64
rules:
- if: "$CODE_QUALITY_DISABLED"
when: never

View File

@ -1,5 +1,5 @@
# Changelog
## --- [4.3.3] - 2024/TBD
## --- [4.4.1] - 2024/TBD
### New features
TBD
### Bug fixes
@ -10,6 +10,20 @@ TBD
TBD
<br><br>
## --- [4.4.0] - 2024/05/11
### Refactor
- Refactor API keys "super user" to "full access" ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/731) | [Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/759))
- Refactor SBuilder to use Big Bucket Svc ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/755))
### Bug fixes
- Reset query arguments on login if `?next` is not available ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/750))
- Fix child schedule failing to load after del parent ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/753))
### Tweaks
- Add link to go back to dashboard on error page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/743))
- Set audit logging to logfile instead of DB ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/751))
### Lang
- Changes of phrase in `cs_CS` translation ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/749))
<br><br>
## --- [4.3.2] - 2024/04/07
### Refactor
- Refactor ServerJars caching and move to api.serverjars.com ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/744) | [Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/746))

View File

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

View File

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

View File

@ -17,6 +17,10 @@ class ServerPermsController:
def get_server_user_list(server_id):
return PermissionsServers.get_server_user_list(server_id)
@staticmethod
def get_permissions(permissions_mask):
return PermissionsServers.get_permissions(permissions_mask)
@staticmethod
def list_defined_permissions():
permissions_list = PermissionsServers.get_permissions_list()
@ -61,6 +65,22 @@ class ServerPermsController:
def get_permissions_mask(role_id, server_id):
return PermissionsServers.get_permissions_mask(role_id, server_id)
@staticmethod
def get_lowest_api_perm_mask(user_server_permissions_mask, api_key_permssions_mask):
mask = ""
# If this isn't an API key we'll know the request came from basic
# authentication and ignore the API key permissions mask.
if not api_key_permssions_mask:
return user_server_permissions_mask
for _index, (user_perm, api_perm) in enumerate(
zip(user_server_permissions_mask, api_key_permssions_mask)
):
if user_perm == "1" and api_perm == "1":
mask += "1"
else:
mask += "0"
return mask
@staticmethod
def set_permission(
permission_mask, permission_tested: EnumPermissionsServer, value
@ -82,6 +102,11 @@ class ServerPermsController:
def get_api_key_permissions_list(key: ApiKeys, server_id: str):
return PermissionsServers.get_api_key_permissions_list(key, server_id)
@staticmethod
def get_user_permissions_mask(user_id: str, server_id: str):
user = HelperUsers.get_user_model(user_id)
return PermissionsServers.get_user_permissions_mask(user, server_id)
@staticmethod
def get_authorized_servers_stats_from_roles(user_id):
user_roles = HelperUsers.get_user_roles_id(user_id)

View File

@ -0,0 +1,53 @@
import logging
import logging.config
import json
from datetime import datetime
class JsonEncoderStrFallback(json.JSONEncoder):
def default(self, o):
try:
return super().default(o)
except TypeError as exc:
if "not JSON serializable" in str(exc):
return str(o)
raise
class JsonEncoderDatetime(JsonEncoderStrFallback):
def default(self, o):
if isinstance(o, datetime):
return o.strftime("%Y-%m-%dT%H:%M:%S%z")
return super().default(o)
class JsonFormatter(logging.Formatter):
def formatTime(self, record, datefmt=None):
"""
Override formatTime to customize the time format.
"""
timestamp = datetime.fromtimestamp(record.created)
if datefmt:
# Use the specified date format
return timestamp.strftime(datefmt)
# Default date format: YYYY-MM-DD HH:MM:SS,mmm
secs = int(record.msecs)
return f"{timestamp.strftime('%Y-%m-%d %H:%M:%S')},{secs:03d}"
def format(self, record):
log_data = {
"level": record.levelname,
"time": self.formatTime(record),
"log_msg": record.getMessage(),
}
# Filter out standard log record attributes and include only custom ones
custom_attrs = ["user_name", "user_id", "server_id", "source_ip"]
extra_attrs = {
key: value for key, value in record.__dict__.items() if key in custom_attrs
}
# Merge extra attributes with log data
log_data.update(extra_attrs)
return json.dumps(log_data)

View File

@ -0,0 +1,236 @@
import os
import json
import threading
import time
import logging
from datetime import datetime
import requests
from app.classes.controllers.servers_controller import ServersController
from app.classes.models.server_permissions import PermissionsServers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger(__name__)
# Temp type var until sjars restores generic fetchTypes0
class BigBucket:
def __init__(self, helper):
self.helper = helper
# remove any trailing slash from config.json
# url since we add it on all the calls
self.base_url = str(
self.helper.get_setting("big_bucket_repo", "https://jars.arcadiatech.org")
).rstrip("/")
def _read_cache(self) -> dict:
cache_file = self.helper.big_bucket_cache
cache = {}
try:
with open(cache_file, "r", encoding="utf-8") as f:
cache = json.load(f)
except Exception as e:
logger.error(f"Unable to read big_bucket cache file: {e}")
return cache
def get_bucket_data(self):
data = self._read_cache()
return data.get("categories")
def _check_bucket_alive(self) -> bool:
logger.info("Checking Big Bucket status")
check_url = f"{self.base_url}/healthcheck"
try:
response = requests.get(check_url, timeout=2)
response_json = response.json()
if (
response.status_code in [200, 201]
and response_json.get("status") == "ok"
):
logger.info("Big bucket is alive and responding as expected")
return True
except Exception as e:
logger.error(f"Unable to connect to big bucket due to error: {e}")
return False
logger.error(
"Big bucket manifest is not available as expected or unable to contact"
)
return False
def _get_big_bucket(self) -> dict:
logger.debug("Calling for big bucket manifest.")
try:
response = requests.get(f"{self.base_url}/manifest.json", timeout=5)
if response.status_code in [200, 201]:
data = response.json()
del data["manifest_version"]
return data
return {}
except TimeoutError as e:
logger.error(f"Unable to get jars from remote with error {e}")
return {}
def _refresh_cache(self):
"""
Contains the shared logic for refreshing the cache.
This method is called by both manual_refresh_cache and refresh_cache methods.
"""
if not self._check_bucket_alive():
logger.error("big bucket API is not available.")
return False
cache_data = {
"last_refreshed": datetime.now().strftime("%m/%d/%Y, %H:%M:%S"),
"categories": self._get_big_bucket(),
}
try:
with open(
self.helper.big_bucket_cache, "w", encoding="utf-8"
) as cache_file:
json.dump(cache_data, cache_file, indent=4)
logger.info("Cache file successfully refreshed manually.")
except Exception as e:
logger.error(f"Failed to update cache file manually: {e}")
def manual_refresh_cache(self):
"""
Manually triggers the cache refresh process.
"""
logger.info("Manual bucket cache refresh initiated.")
self._refresh_cache()
logger.info("Manual refresh completed.")
def refresh_cache(self):
"""
Automatically trigger cache refresh process based age.
This method checks if the cache file is older than a specified number of days
before deciding to refresh.
"""
cache_file_path = self.helper.big_bucket_cache
# Determine if the cache is old and needs refreshing
cache_old = self.helper.is_file_older_than_x_days(cache_file_path)
# debug override
# cache_old = True
if not self._check_bucket_alive():
logger.error("big bucket API is not available.")
return False
if not cache_old:
logger.info("Cache file is not old enough to require automatic refresh.")
return False
logger.info("Automatic cache refresh initiated due to old cache.")
self._refresh_cache()
def get_fetch_url(self, jar, server, version) -> str:
"""
Constructs the URL for downloading a server JAR file based on the server type.
Parameters:
jar (str): The category of the JAR file to download.
server (str): Server software name (e.g., "paper").
version (str): Server version.
Returns:
str or None: URL for downloading the JAR file, or None if URL cannot be
constructed or an error occurs.
"""
try:
# Read cache file for URL that is in a list of one item
return self.get_bucket_data()[jar]["types"][server]["versions"][version][
"url"
][0]
except Exception as e:
logger.error(f"An error occurred while constructing fetch URL: {e}")
return None
def download_jar(self, jar, server, version, path, server_id):
update_thread = threading.Thread(
name=f"server_download-{server_id}-{server}-{version}",
target=self.a_download_jar,
daemon=True,
args=(jar, server, version, path, server_id),
)
update_thread.start()
def a_download_jar(self, jar, server, version, path, server_id):
"""
Downloads a server JAR file and performs post-download actions including
notifying users and setting import status.
This method waits for the server registration to complete, retrieves the
download URL for the specified server JAR file.
Upon successful download, it either runs the installer for
Forge servers or simply finishes the import process for other types. It
notifies server users about the completion of the download.
Parameters:
- jar (str): The category of the JAR file to download.
- server (str): The type of server software (e.g., 'forge', 'paper').
- version (str): The version of the server software.
- path (str): The local filesystem path where the JAR file will be saved.
- server_id (str): The unique identifier for the server being updated or
imported, used for notifying users and setting the import status.
Returns:
- bool: True if the JAR file was successfully downloaded and saved;
False otherwise.
The method ensures that the server is properly registered before proceeding
with the download and handles exceptions by logging errors and reverting
the import status if necessary.
"""
# delaying download for server register to finish
time.sleep(3)
fetch_url = self.get_fetch_url(jar, server, version)
if not fetch_url:
return False
server_users = PermissionsServers.get_server_user_list(server_id)
# Make sure the server is registered before updating its stats
while True:
try:
ServersController.set_import(server_id)
for user in server_users:
WebSocketManager().broadcast_user(user, "send_start_reload", {})
break
except Exception as ex:
logger.debug(f"Server not registered yet. Delaying download - {ex}")
# Initiate Download
jar_dir = os.path.dirname(path)
jar_name = os.path.basename(path)
logger.info(fetch_url)
success = FileHelpers.ssl_get_file(fetch_url, jar_dir, jar_name)
# Post-download actions
if success:
if server == "forge-installer":
# If this is the newer Forge version, run the installer
ServersController.finish_import(server_id, True)
else:
ServersController.finish_import(server_id)
# Notify users
for user in server_users:
WebSocketManager().broadcast_user(
user, "notification", "Executable download finished"
)
time.sleep(3) # Delay for user notification
WebSocketManager().broadcast_user(user, "send_start_reload", {})
else:
logger.error(f"Unable to save jar to {path} due to download failure.")
ServersController.finish_import(server_id)
return success

View File

@ -1,395 +0,0 @@
import os
import json
import threading
import time
import logging
from datetime import datetime
import requests
from app.classes.controllers.servers_controller import ServersController
from app.classes.models.server_permissions import PermissionsServers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger(__name__)
# Temp type var until sjars restores generic fetchTypes0
SERVERJARS_TYPES = ["modded", "proxies", "servers", "vanilla"]
PAPERJARS = ["paper", "folia"]
class ServerJars:
def __init__(self, helper):
self.helper = helper
self.base_url = "https://api.serverjars.com"
self.paper_base = "https://api.papermc.io"
@staticmethod
def get_paper_jars():
return PAPERJARS
def get_paper_versions(self, project):
"""
Retrieves a list of versions for a specified project from the PaperMC API.
Parameters:
project (str): The project name to query for available versions.
Returns:
list: A list of version strings available for the project. Returns an empty
list if the API call fails or if no versions are found.
This function makes a GET request to the PaperMC API to fetch available project
versions, The versions are returned in reverse order, with the most recent
version first.
"""
try:
response = requests.get(
f"{self.paper_base}/v2/projects/{project}/", timeout=2
)
response.raise_for_status()
api_data = response.json()
except Exception as e:
logger.error(f"Error loading project versions for {project}: {e}")
return []
versions = api_data.get("versions", [])
versions.reverse() # Ensure the most recent version comes first
return versions
def get_paper_build(self, project, version):
"""
Fetches the latest build for a specified project and version from PaperMC API.
Parameters:
project (str): Project name, typically a server software like 'paper'.
version (str): Project version to fetch the build number for.
Returns:
int or None: Latest build number if successful, None if not or on error.
This method attempts to query the PaperMC API for the latest build and
handles exceptions by logging errors and returning None.
"""
try:
response = requests.get(
f"{self.paper_base}/v2/projects/{project}/versions/{version}/builds/",
timeout=2,
)
response.raise_for_status()
api_data = response.json()
except Exception as e:
logger.error(f"Error fetching build for {project} {version}: {e}")
return None
builds = api_data.get("builds", [])
return builds[-1] if builds else None
def _read_cache(self):
cache_file = self.helper.serverjar_cache
cache = {}
try:
with open(cache_file, "r", encoding="utf-8") as f:
cache = json.load(f)
except Exception as e:
logger.error(f"Unable to read serverjars.com cache file: {e}")
return cache
def get_serverjar_data(self):
data = self._read_cache()
return data.get("types")
def _check_sjars_api_alive(self):
logger.info("Checking serverjars.com API status")
check_url = f"{self.base_url}"
try:
response = requests.get(check_url, timeout=2)
response_json = response.json()
if (
response.status_code in [200, 201]
and response_json.get("status") == "success"
and response_json.get("response", {}).get("status") == "ok"
):
logger.info("Serverjars.com API is alive and responding as expected")
return True
except Exception as e:
logger.error(f"Unable to connect to serverjar.com API due to error: {e}")
return False
logger.error(
"Serverjars.com API is not responding as expected or unable to contact"
)
return False
def _fetch_projects_for_type(self, server_type):
"""
Fetches projects for a given server type from the ServerJars API.
"""
try:
response = requests.get(
f"{self.base_url}/api/fetchTypes/{server_type}", timeout=5
)
response.raise_for_status() # Ensure HTTP errors are caught
data = response.json()
if data.get("status") == "success":
return data["response"].get("servers", [])
except requests.RequestException as e:
print(f"Error fetching projects for type {server_type}: {e}")
return []
def _get_server_type_list(self):
"""
Builds the type structure with projects fetched for each type.
"""
type_structure = {}
for server_type in SERVERJARS_TYPES:
projects = self._fetch_projects_for_type(server_type)
type_structure[server_type] = {project: [] for project in projects}
return type_structure
def _get_jar_versions(self, server_type, project_name, max_ver=50):
"""
Grabs available versions for specified project
Args:
server_type (str): Server Type Category (modded, servers, etc)
project_name (str): Target project (paper, forge, magma, etc)
max (int, optional): Max versions returned. Defaults to 50.
Returns:
list: An array of versions
"""
url = f"{self.base_url}/api/fetchAll/{server_type}/{project_name}?max={max_ver}"
try:
response = requests.get(url, timeout=5)
response.raise_for_status() # Ensure HTTP errors are caught
data = response.json()
logger.debug(f"Received data for {server_type}/{project_name}: {data}")
if data.get("status") == "success":
versions = [
item.get("version")
for item in data.get("response", [])
if "version" in item
]
versions.reverse() # Reverse so versions are newest -> oldest
logger.debug(f"Versions extracted: {versions}")
return versions
except requests.RequestException as e:
logger.error(
f"Error fetching jar versions for {server_type}/{project_name}: {e}"
)
return []
def _refresh_cache(self):
"""
Contains the shared logic for refreshing the cache.
This method is called by both manual_refresh_cache and refresh_cache methods.
"""
now = datetime.now()
cache_data = {
"last_refreshed": now.strftime("%m/%d/%Y, %H:%M:%S"),
"types": self._get_server_type_list(),
}
for server_type, projects in cache_data["types"].items():
for project_name in projects:
versions = self._get_jar_versions(server_type, project_name)
cache_data["types"][server_type][project_name] = versions
for paper_project in PAPERJARS:
cache_data["types"]["servers"][paper_project] = self.get_paper_versions(
paper_project
)
return cache_data
def manual_refresh_cache(self):
"""
Manually triggers the cache refresh process.
"""
if not self._check_sjars_api_alive():
logger.error("ServerJars API is not available.")
return False
logger.info("Manual cache refresh requested.")
cache_data = self._refresh_cache()
# Save the updated cache data
try:
with open(self.helper.serverjar_cache, "w", encoding="utf-8") as cache_file:
json.dump(cache_data, cache_file, indent=4)
logger.info("Cache file successfully refreshed manually.")
except Exception as e:
logger.error(f"Failed to update cache file manually: {e}")
def refresh_cache(self):
"""
Automatically trigger cache refresh process based age.
This method checks if the cache file is older than a specified number of days
before deciding to refresh.
"""
cache_file_path = self.helper.serverjar_cache
# Determine if the cache is old and needs refreshing
cache_old = self.helper.is_file_older_than_x_days(cache_file_path)
# debug override
# cache_old = True
if not self._check_sjars_api_alive():
logger.error("ServerJars API is not available.")
return False
if not cache_old:
logger.info("Cache file is not old enough to require automatic refresh.")
return False
logger.info("Automatic cache refresh initiated due to old cache.")
cache_data = self._refresh_cache()
# Save the updated cache data
try:
with open(cache_file_path, "w", encoding="utf-8") as cache_file:
json.dump(cache_data, cache_file, indent=4)
logger.info("Cache file successfully refreshed automatically.")
except Exception as e:
logger.error(f"Failed to update cache file automatically: {e}")
def get_fetch_url(self, jar, server, version):
"""
Constructs the URL for downloading a server JAR file based on the server type.
Supports two main types of server JAR sources:
- ServerJars API for servers not in PAPERJARS.
- Paper API for servers available through the Paper project.
Parameters:
jar (str): Name of the JAR file.
server (str): Server software name (e.g., "paper").
version (str): Server version.
Returns:
str or None: URL for downloading the JAR file, or None if URL cannot be
constructed or an error occurs.
"""
try:
# Check if the server type is not specifically handled by Paper.
if server not in PAPERJARS:
return f"{self.base_url}/api/fetchJar/{jar}/{server}/{version}"
# For Paper servers, attempt to get the build for the specified version.
paper_build_info = self.get_paper_build(server, version)
if paper_build_info is None:
# Log an error or handle the case where paper_build_info is None
logger.error(
"Error: Unable to get build information for server:"
f" {server}, version: {version}"
)
return None
build = paper_build_info.get("build")
if not build:
# Log an error or handle the case where build is None or not found
logger.error(
f"Error: Build number not found for server:"
f" {server}, version: {version}"
)
return None
# Construct and return the URL for downloading the Paper server JAR.
return (
f"{self.paper_base}/v2/projects/{server}/versions/{version}/"
f"builds/{build}/downloads/{server}-{version}-{build}.jar"
)
except Exception as e:
logger.error(f"An error occurred while constructing fetch URL: {e}")
return None
def download_jar(self, jar, server, version, path, server_id):
update_thread = threading.Thread(
name=f"server_download-{server_id}-{server}-{version}",
target=self.a_download_jar,
daemon=True,
args=(jar, server, version, path, server_id),
)
update_thread.start()
def a_download_jar(self, jar, server, version, path, server_id):
"""
Downloads a server JAR file and performs post-download actions including
notifying users and setting import status.
This method waits for the server registration to complete, retrieves the
download URL for the specified server JAR file.
Upon successful download, it either runs the installer for
Forge servers or simply finishes the import process for other types. It
notifies server users about the completion of the download.
Parameters:
- jar (str): The name of the JAR file to download.
- server (str): The type of server software (e.g., 'forge', 'paper').
- version (str): The version of the server software.
- path (str): The local filesystem path where the JAR file will be saved.
- server_id (str): The unique identifier for the server being updated or
imported, used for notifying users and setting the import status.
Returns:
- bool: True if the JAR file was successfully downloaded and saved;
False otherwise.
The method ensures that the server is properly registered before proceeding
with the download and handles exceptions by logging errors and reverting
the import status if necessary.
"""
# delaying download for server register to finish
time.sleep(3)
fetch_url = self.get_fetch_url(jar, server, version)
if not fetch_url:
return False
server_users = PermissionsServers.get_server_user_list(server_id)
# Make sure the server is registered before updating its stats
while True:
try:
ServersController.set_import(server_id)
for user in server_users:
WebSocketManager().broadcast_user(user, "send_start_reload", {})
break
except Exception as ex:
logger.debug(f"Server not registered yet. Delaying download - {ex}")
# Initiate Download
jar_dir = os.path.dirname(path)
jar_name = os.path.basename(path)
logger.info(fetch_url)
success = FileHelpers.ssl_get_file(fetch_url, jar_dir, jar_name)
# Post-download actions
if success:
if server == "forge":
# If this is the newer Forge version, run the installer
ServersController.finish_import(server_id, True)
else:
ServersController.finish_import(server_id)
# Notify users
for user in server_users:
WebSocketManager().broadcast_user(
user, "notification", "Executable download finished"
)
time.sleep(3) # Delay for user notification
WebSocketManager().broadcast_user(user, "send_start_reload", {})
else:
logger.error(f"Unable to save jar to {path} due to download failure.")
ServersController.finish_import(server_id)
return success

View File

@ -187,7 +187,7 @@ class PermissionsCrafty:
@staticmethod
def get_api_key_permissions_list(key: ApiKeys):
user = HelperUsers.get_user(key.user_id)
if user["superuser"] and key.superuser:
if user["superuser"] and key.full_access:
return PermissionsCrafty.get_permissions_list()
if user["superuser"]:
# User is superuser but API key isn't

View File

@ -18,28 +18,10 @@ from app.classes.models.base_model import BaseModel
from app.classes.models.users import HelperUsers
from app.classes.models.servers import Servers
from app.classes.models.server_permissions import PermissionsServers
from app.classes.shared.main_models import DatabaseShortcuts
from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger(__name__)
# **********************************************************************************
# Audit_Log Class
# **********************************************************************************
class AuditLog(BaseModel):
audit_id = AutoField()
created = DateTimeField(default=datetime.datetime.now)
user_name = CharField(default="")
user_id = IntegerField(default=0, index=True)
source_ip = CharField(default="127.0.0.1")
server_id = ForeignKeyField(
Servers, backref="audit_server", null=True
) # When auditing global events, use server ID null
log_msg = TextField(default="")
class Meta:
table_name = "audit_log"
auth_logger = logging.getLogger("audit_log")
# **********************************************************************************
@ -155,10 +137,6 @@ class HelpersManagement:
# **********************************************************************************
# Audit_Log Methods
# **********************************************************************************
@staticmethod
def get_activity_log():
query = AuditLog.select()
return DatabaseShortcuts.return_db_rows(query)
def add_to_audit_log(self, user_id, log_msg, server_id=None, source_ip=None):
logger.debug(f"Adding to audit log User:{user_id} - Message: {log_msg} ")
@ -172,50 +150,28 @@ class HelpersManagement:
WebSocketManager().broadcast_user(user, "notification", audit_msg)
except Exception as e:
logger.error(f"Error broadcasting to user {user} - {e}")
AuditLog.insert(
{
AuditLog.user_name: user_data["username"],
AuditLog.user_id: user_id,
AuditLog.server_id: server_id,
AuditLog.log_msg: audit_msg,
AuditLog.source_ip: source_ip,
}
).execute()
# deletes records when there's more than 300
ordered = AuditLog.select().order_by(+AuditLog.created)
for item in ordered:
if not self.helper.get_setting("max_audit_entries"):
max_entries = 300
else:
max_entries = self.helper.get_setting("max_audit_entries")
if AuditLog.select().count() > max_entries:
AuditLog.delete().where(AuditLog.audit_id == item.audit_id).execute()
else:
return
auth_logger.info(
str(log_msg),
extra={
"user_name": user_data["username"],
"user_id": user_id,
"server_id": server_id,
"source_ip": source_ip,
},
)
def add_to_audit_log_raw(self, user_name, user_id, server_id, log_msg, source_ip):
AuditLog.insert(
{
AuditLog.user_name: user_name,
AuditLog.user_id: user_id,
AuditLog.server_id: server_id,
AuditLog.log_msg: log_msg,
AuditLog.source_ip: source_ip,
}
).execute()
# deletes records when there's more than 300
ordered = AuditLog.select().order_by(+AuditLog.created)
for item in ordered:
# configurable through app/config/config.json
if not self.helper.get_setting("max_audit_entries"):
max_entries = 300
else:
max_entries = self.helper.get_setting("max_audit_entries")
if AuditLog.select().count() > max_entries:
AuditLog.delete().where(AuditLog.audit_id == item.audit_id).execute()
else:
return
if isinstance(server_id, Servers) and server_id is not None:
server_id = server_id.server_id
auth_logger.info(
str(log_msg),
extra={
"user_name": user_name,
"user_id": user_id,
"server_id": server_id,
"source_ip": source_ip,
},
)
@staticmethod
def create_crafty_row():

View File

@ -264,7 +264,7 @@ class PermissionsServers:
@staticmethod
def get_api_key_permissions_list(key: ApiKeys, server_id: str):
user = HelperUsers.get_user(key.user_id)
if user["superuser"] and key.superuser:
if user["superuser"] and key.full_access:
return PermissionsServers.get_permissions_list()
roles_list = HelperUsers.get_user_roles_id(user["user_id"])
role_server = (

View File

@ -71,7 +71,7 @@ class ApiKeys(BaseModel):
user_id = ForeignKeyField(Users, backref="api_token", index=True)
server_permissions = CharField(default="00000000")
crafty_permissions = CharField(default="000")
superuser = BooleanField(default=False)
full_access = BooleanField(default=False)
class Meta:
table_name = "api_keys"
@ -407,7 +407,7 @@ class HelperUsers:
def add_user_api_key(
name: str,
user_id: str,
superuser: bool = False,
full_access: bool = False,
server_permissions_mask: t.Optional[str] = None,
crafty_permissions_mask: t.Optional[str] = None,
):
@ -425,7 +425,7 @@ class HelperUsers:
if crafty_permissions_mask is not None
else {}
),
ApiKeys.superuser: superuser,
ApiKeys.full_access: full_access,
}
).execute()

View File

@ -72,7 +72,7 @@ class Helpers:
self.db_path = os.path.join(
self.root_dir, "app", "config", "db", "crafty.sqlite"
)
self.serverjar_cache = os.path.join(self.config_dir, "serverjars.json")
self.big_bucket_cache = os.path.join(self.config_dir, "bigbucket.json")
self.credits_cache = os.path.join(self.config_dir, "credits.json")
self.passhasher = PasswordHasher()
self.exiting = False
@ -516,6 +516,7 @@ class Helpers:
"monitored_mounts": mounts,
"dir_size_poll_freq_minutes": 5,
"crafty_logs_delete_after_days": 0,
"big_bucket_repo": "https://jars.arcadiatech.org",
}
def get_all_settings(self):

View File

@ -32,7 +32,7 @@ from app.classes.shared.console import Console
from app.classes.shared.helpers import Helpers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.import_helper import ImportHelpers
from app.classes.minecraft.serverjars import ServerJars
from app.classes.minecraft.bigbucket import BigBucket
from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger(__name__)
@ -43,7 +43,7 @@ class Controller:
self.helper: Helpers = helper
self.file_helper: FileHelpers = file_helper
self.import_helper: ImportHelpers = import_helper
self.server_jars: ServerJars = ServerJars(helper)
self.big_bucket: BigBucket = BigBucket(helper)
self.users_helper: HelperUsers = HelperUsers(database, self.helper)
self.roles_helper: HelperRoles = HelperRoles(database)
self.servers_helper: HelperServers = HelperServers(database)
@ -436,7 +436,7 @@ class Controller:
if root_create_data["create_type"] == "download_jar":
if Helpers.is_os_windows():
# Let's check for and setup for install server commands
if create_data["type"] == "forge":
if create_data["type"] == "forge-installer":
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
@ -449,7 +449,7 @@ class Controller:
f'-jar "{server_file}" nogui'
)
else:
if create_data["type"] == "forge":
if create_data["type"] == "forge-installer":
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
@ -568,19 +568,16 @@ class Controller:
if data["create_type"] == "minecraft_java":
if root_create_data["create_type"] == "download_jar":
# modded update urls from server jars will only update the installer
if (
create_data["category"] != "modded"
and create_data["type"] not in ServerJars.get_paper_jars()
):
if create_data["type"] != "forge-installer":
server_obj = self.servers.get_server_obj(new_server_id)
url = (
"https://api.serverjars.com/api/fetchJar/"
f"{create_data['category']}"
f"/{create_data['type']}/{create_data['version']}"
url = self.big_bucket.get_fetch_url(
create_data["category"],
create_data["type"],
create_data["version"],
)
server_obj.executable_update_url = url
self.servers.update_server(server_obj)
self.server_jars.download_jar(
self.big_bucket.download_jar(
create_data["category"],
create_data["type"],
create_data["version"],

View File

@ -690,7 +690,8 @@ class ServerInstance:
try:
# Getting the forge version from the executable command
version = re.findall(
r"forge-([0-9\.]+)((?:)|(?:-([0-9\.]+)-[a-zA-Z]+)).jar",
r"forge-installer-([0-9\.]+)((?:)|"
r"(?:-([0-9\.]+)-[a-zA-Z]+)).jar",
server_obj.execution_command,
)
version_param = version[0][0].split(".")

View File

@ -685,16 +685,16 @@ class TasksManager:
id="stats",
)
def serverjar_cache_refresher(self):
logger.info("Refreshing serverjars.com cache on start")
self.controller.server_jars.refresh_cache()
def big_bucket_cache_refresher(self):
logger.info("Refreshing big bucket cache on start")
self.controller.big_bucket.refresh_cache()
logger.info("Scheduling Serverjars.com cache refresh service every 12 hours")
logger.info("Scheduling big bucket cache refresh service every 12 hours")
self.scheduler.add_job(
self.controller.server_jars.refresh_cache,
self.controller.big_bucket.refresh_cache,
"interval",
hours=12,
id="serverjars",
id="big_bucket",
)
def realtime(self):

View File

@ -182,6 +182,7 @@ class BaseHandler(tornado.web.RequestHandler):
t.List[str],
bool,
t.Dict[str, t.Any],
str,
]
]:
try:
@ -190,9 +191,10 @@ class BaseHandler(tornado.web.RequestHandler):
)
superuser = user["superuser"]
server_permissions_api_mask = ""
if api_key is not None:
superuser = superuser and api_key.superuser
superuser = superuser and api_key.full_access
server_permissions_api_mask = api_key.server_permissions
exec_user_role = set()
if superuser:
authorized_servers = self.controller.servers.get_all_defined_servers()
@ -214,6 +216,7 @@ class BaseHandler(tornado.web.RequestHandler):
user["user_id"]
)
)
logger.debug(user["roles"])
for r in user["roles"]:
role = self.controller.roles.get_role(r)
@ -234,6 +237,7 @@ class BaseHandler(tornado.web.RequestHandler):
exec_user_role,
superuser,
user,
server_permissions_api_mask,
)
logging.debug("Auth unsuccessful")
auth_log.error(

View File

@ -168,7 +168,7 @@ class PanelHandler(BaseHandler):
# Commented out because there is no server access control for API keys,
# they just inherit from the host user
# if api_key is not None:
# superuser = superuser and api_key.superuser
# superuser = superuser and api_key.full_access
if server_id is None:
self.redirect("/panel/error?error=Invalid Server ID")
@ -242,7 +242,7 @@ class PanelHandler(BaseHandler):
api_key, _token_data, exec_user = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
superuser = superuser and api_key.full_access
if superuser: # TODO: Figure out a better solution
defined_servers = self.controller.servers.list_defined_servers()
@ -351,7 +351,7 @@ class PanelHandler(BaseHandler):
"created": api_key.created,
"server_permissions": api_key.server_permissions,
"crafty_permissions": api_key.crafty_permissions,
"superuser": api_key.superuser,
"full_access": api_key.full_access,
}
if api_key is not None
else None
@ -1224,9 +1224,11 @@ class PanelHandler(BaseHandler):
page_data["schedule"]["interval_type"] = schedule.interval_type
if schedule.interval_type == "reaction":
difficulty = "reaction"
page_data["parent"] = self.controller.management.get_scheduled_task(
schedule.parent
)
page_data["parent"] = None
if schedule.parent:
page_data["parent"] = self.controller.management.get_scheduled_task(
schedule.parent
)
elif schedule.cron_string == "":
difficulty = "basic"
page_data["parent"] = None
@ -1417,6 +1419,9 @@ class PanelHandler(BaseHandler):
page_data["crafty_permissions_all"] = (
self.controller.crafty_perms.list_defined_crafty_permissions()
)
page_data["user_crafty_permissions"] = (
self.controller.crafty_perms.get_crafty_permissions_list(user_id)
)
if user_id is None:
self.redirect("/panel/error?error=Invalid User ID")
@ -1564,8 +1569,6 @@ class PanelHandler(BaseHandler):
template = "panel/panel_edit_role.html"
elif page == "activity_logs":
page_data["audit_logs"] = self.controller.management.get_activity_log()
template = "panel/activity_logs.html"
elif page == "download_file":

View File

@ -48,7 +48,10 @@ class PublicHandler(BaseHandler):
}
if self.request.query:
page_data["query"] = self.request.query_arguments.get("next")[0].decode()
request_query = self.request.query_arguments.get("next")
if not request_query:
self.redirect("/login")
page_data["query"] = request_query[0].decode()
# sensible defaults
template = "public/404.html"

View File

@ -26,6 +26,7 @@ class ApiAnnounceIndexHandler(BaseApiHandler):
_,
_,
_user,
_,
) = auth_data
data = self.helper.get_announcements()
@ -72,6 +73,7 @@ class ApiAnnounceIndexHandler(BaseApiHandler):
_,
_,
_user,
_,
) = auth_data
try:
data = json.loads(self.request.body)

View File

@ -1,3 +1,5 @@
import os
import json
from app.classes.web.base_api_handler import BaseApiHandler
@ -12,6 +14,7 @@ class ApiCraftyLogIndexHandler(BaseApiHandler):
_,
superuser,
_,
_,
) = auth_data
if not superuser:
@ -22,9 +25,17 @@ class ApiCraftyLogIndexHandler(BaseApiHandler):
raise NotImplementedError
if log_type == "audit":
with open(
os.path.join(self.controller.project_root, "logs", "audit.log"),
"r",
encoding="utf-8",
) as f:
log_lines = [json.loads(line) for line in f]
rev_log_lines = log_lines[::-1]
return self.finish_json(
200,
{"status": "ok", "data": self.controller.management.get_activity_log()},
{"status": "ok", "data": rev_log_lines},
)
if log_type == "session":

View File

@ -31,6 +31,7 @@ config_json_schema = {
"monitored_mounts": {"type": "array"},
"dir_size_poll_freq_minutes": {"type": "integer"},
"crafty_logs_delete_after_days": {"type": "integer"},
"big_bucket_repo": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
@ -67,6 +68,7 @@ class ApiCraftyConfigIndexHandler(BaseApiHandler):
_,
superuser,
_,
_,
) = auth_data
# GET /api/v2/roles?ids=true
@ -93,13 +95,7 @@ class ApiCraftyConfigIndexHandler(BaseApiHandler):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
superuser,
user,
) = auth_data
(_, _, _, superuser, user, _) = auth_data
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
@ -149,6 +145,7 @@ class ApiCraftyCustomizeIndexHandler(BaseApiHandler):
_,
superuser,
_,
_,
) = auth_data
# GET /api/v2/roles?ids=true
@ -181,6 +178,7 @@ class ApiCraftyCustomizeIndexHandler(BaseApiHandler):
_,
superuser,
user,
_,
) = auth_data
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})

View File

@ -24,6 +24,7 @@ class ApiCraftyConfigServerDirHandler(BaseApiHandler):
_,
superuser,
_,
_,
) = auth_data
# GET /api/v2/roles?ids=true
@ -56,6 +57,7 @@ class ApiCraftyConfigServerDirHandler(BaseApiHandler):
_,
_,
_,
_,
) = auth_data
if not auth_data:

View File

@ -12,16 +12,17 @@ class ApiCraftyJarCacheIndexHandler(BaseApiHandler):
_,
_,
_,
_,
) = auth_data
if not auth_data[4]["superuser"]:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.controller.server_jars.manual_refresh_cache()
self.controller.big_bucket.manual_refresh_cache()
self.finish_json(
200,
{
"status": "ok",
"data": self.controller.server_jars.get_serverjar_data(),
"data": self.controller.big_bucket.get_bucket_data(),
},
)

View File

@ -75,6 +75,7 @@ class ApiRolesIndexHandler(BaseApiHandler):
_,
superuser,
_,
_,
) = auth_data
# GET /api/v2/roles?ids=true
@ -107,6 +108,7 @@ class ApiRolesIndexHandler(BaseApiHandler):
_,
superuser,
user,
_,
) = auth_data
if not superuser:

View File

@ -74,6 +74,7 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
_,
superuser,
_,
_,
) = auth_data
if not superuser:
@ -97,6 +98,7 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
_,
superuser,
user,
_,
) = auth_data
if not superuser:
@ -126,10 +128,19 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
_,
superuser,
user,
_,
) = auth_data
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
role = self.controller.roles.get_role(role_id)
if not superuser and user["user_id"] != role["manager"]:
return self.finish_json(
400,
{
"status": "error",
"error": "NOT_AUTHORIZED",
"error_data": "Not Authorized",
},
)
try:
data = orjson.loads(self.request.body)

View File

@ -13,6 +13,7 @@ class ApiRolesRoleServersHandler(BaseApiHandler):
_,
superuser,
_,
_,
) = auth_data
# GET /api/v2/roles/role/servers?ids=true

View File

@ -12,6 +12,7 @@ class ApiRolesRoleUsersHandler(BaseApiHandler):
_,
superuser,
_,
_,
) = auth_data
if not superuser:

View File

@ -139,7 +139,7 @@ new_server_schema = {
"category": {
"title": "Jar Category",
"type": "string",
"examples": ["modded", "vanilla"],
"examples": ["Mc_java_servers", "Mc_java_proxies"],
},
"properties": {
"type": {
@ -685,6 +685,7 @@ class ApiServersIndexHandler(BaseApiHandler):
_,
_superuser,
user,
_,
) = auth_data
if EnumPermissionsCrafty.SERVER_CREATION not in exec_user_crafty_permissions:

View File

@ -18,13 +18,14 @@ class ApiServersServerActionHandler(BaseApiHandler):
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.COMMANDS
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.COMMANDS not in server_permissions:
# if the user doesn't have Commands permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})

View File

@ -26,12 +26,14 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsServer.BACKUP
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.BACKUP not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.finish_json(200, self.controller.management.get_backup_config(server_id))
@ -41,12 +43,14 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
backup_conf = self.controller.management.get_backup_config(server_id)
if not auth_data:
return
if (
EnumPermissionsServer.BACKUP
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.BACKUP not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
@ -89,12 +93,14 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsServer.BACKUP
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.BACKUP not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})

View File

@ -42,12 +42,14 @@ class ApiServersServerBackupsIndexHandler(BaseApiHandler):
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsServer.BACKUP
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.BACKUP not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.finish_json(200, self.controller.management.get_backup_config(server_id))
@ -83,13 +85,14 @@ class ApiServersServerBackupsIndexHandler(BaseApiHandler):
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.BACKUP
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.BACKUP not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})

View File

@ -80,16 +80,16 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler):
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
and EnumPermissionsServer.BACKUP
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
EnumPermissionsServer.FILES not in server_permissions
and EnumPermissionsServer.BACKUP not in server_permissions
):
# if the user doesn't have Files or Backup permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
@ -197,13 +197,14 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler):
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.FILES not in server_permissions:
# if the user doesn't have Files permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
@ -254,13 +255,14 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler):
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.FILES not in server_permissions:
# if the user doesn't have Files permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
@ -307,13 +309,14 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler):
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.FILES not in server_permissions:
# if the user doesn't have Files permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
@ -373,13 +376,14 @@ class ApiServersServerFilesCreateHandler(BaseApiHandler):
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.FILES not in server_permissions:
# if the user doesn't have Files permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
@ -438,13 +442,14 @@ class ApiServersServerFilesCreateHandler(BaseApiHandler):
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.FILES not in server_permissions:
# if the user doesn't have Files permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
@ -504,13 +509,14 @@ class ApiServersServerFilesZipHandler(BaseApiHandler):
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.FILES not in server_permissions:
# if the user doesn't have Files permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:

View File

@ -102,13 +102,14 @@ class ApiServersServerIndexHandler(BaseApiHandler):
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.CONFIG
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.CONFIG not in server_permissions:
# if the user doesn't have Config permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
@ -154,13 +155,14 @@ class ApiServersServerIndexHandler(BaseApiHandler):
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.CONFIG
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.CONFIG not in server_permissions:
# if the user doesn't have Config permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})

View File

@ -30,13 +30,14 @@ class ApiServersServerLogsHandler(BaseApiHandler):
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.LOGS
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.LOGS not in server_permissions:
# if the user doesn't have Logs permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})

View File

@ -16,13 +16,14 @@ class ApiServersServerStdinHandler(BaseApiHandler):
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.COMMANDS
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.COMMANDS not in server_permissions:
# if the user doesn't have Commands permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})

View File

@ -78,13 +78,14 @@ class ApiServersServerTasksIndexHandler(BaseApiHandler):
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.SCHEDULE
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.SCHEDULE not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
data["server_id"] = server_id

View File

@ -54,12 +54,14 @@ class ApiServersServerTasksTaskIndexHandler(BaseApiHandler):
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsServer.SCHEDULE
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.SCHEDULE not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.finish_json(200, self.controller.management.get_scheduled_task(task_id))
@ -68,12 +70,14 @@ class ApiServersServerTasksTaskIndexHandler(BaseApiHandler):
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsServer.SCHEDULE
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.SCHEDULE not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
@ -120,13 +124,14 @@ class ApiServersServerTasksTaskIndexHandler(BaseApiHandler):
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.SCHEDULE
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.SCHEDULE not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})

View File

@ -38,12 +38,14 @@ class ApiServersServerWebhooksIndexHandler(BaseApiHandler):
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsServer.CONFIG
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.CONFIG not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.finish_json(
@ -81,13 +83,14 @@ class ApiServersServerWebhooksIndexHandler(BaseApiHandler):
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.CONFIG
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.CONFIG not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
data["server_id"] = server_id

View File

@ -39,12 +39,14 @@ class ApiServersServerWebhooksManagementIndexHandler(BaseApiHandler):
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsServer.CONFIG
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.CONFIG not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
@ -66,12 +68,14 @@ class ApiServersServerWebhooksManagementIndexHandler(BaseApiHandler):
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsServer.CONFIG
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.CONFIG not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
@ -117,13 +121,14 @@ class ApiServersServerWebhooksManagementIndexHandler(BaseApiHandler):
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.CONFIG
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.CONFIG not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
@ -159,13 +164,14 @@ class ApiServersServerWebhooksManagementIndexHandler(BaseApiHandler):
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.CONFIG
not in self.controller.server_perms.get_user_id_permissions_list(
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
)
):
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.CONFIG not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
webhook = self.controller.management.get_webhook_by_id(webhook_id)

View File

@ -21,6 +21,7 @@ class ApiUsersIndexHandler(BaseApiHandler):
_,
_,
user,
_,
) = auth_data
# GET /api/v2/users?ids=true
@ -70,6 +71,7 @@ class ApiUsersIndexHandler(BaseApiHandler):
_,
superuser,
user,
_,
) = auth_data
if EnumPermissionsCrafty.USER_CONFIG not in exec_user_crafty_permissions:
@ -149,11 +151,12 @@ class ApiUsersIndexHandler(BaseApiHandler):
400, {"status": "error", "error": "INVALID_SUPERUSER_CREATE"}
)
if len(roles) != 0 and not superuser:
# HACK: This should check if the user has the roles or something
return self.finish_json(
400, {"status": "error", "error": "INVALID_ROLES_CREATE"}
)
for role in roles:
role = self.controller.roles.get_role(role)
if int(role["manager"]) != int(auth_data[4]["user_id"]) and not superuser:
return self.finish_json(
400, {"status": "error", "error": "INVALID_ROLES_CREATE"}
)
# TODO: do this in the most efficient way
user_id = self.controller.users.add_user(

View File

@ -75,7 +75,7 @@ class ApiUsersUserKeyHandler(BaseApiHandler):
"name": key.name,
"server_permissions": key.server_permissions,
"crafty_permissions": key.crafty_permissions,
"superuser": key.superuser,
"full_access": key.full_access,
}
)
self.finish_json(
@ -99,7 +99,7 @@ class ApiUsersUserKeyHandler(BaseApiHandler):
"type": "string",
"pattern": "^[01]{3}$", # 8 bits, see EnumPermissionsCrafty
},
"superuser": {"type": "boolean"},
"full_access": {"type": "boolean"},
},
"additionalProperties": False,
"minProperties": 1,
@ -113,6 +113,7 @@ class ApiUsersUserKeyHandler(BaseApiHandler):
_,
_superuser,
user,
_,
) = auth_data
try:
@ -163,7 +164,7 @@ class ApiUsersUserKeyHandler(BaseApiHandler):
key_id = self.controller.users.add_user_api_key(
data["name"],
user_id,
data["superuser"],
data["full_access"],
data["server_permissions_mask"],
data["crafty_permissions_mask"],
)
@ -188,6 +189,7 @@ class ApiUsersUserKeyHandler(BaseApiHandler):
_,
_,
_user,
_,
) = auth_data
if key_id:
key = self.controller.users.get_user_api_key(key_id)

View File

@ -24,6 +24,7 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
_,
_,
user,
_,
) = auth_data
if user_id in ["@me", user["user_id"]]:
@ -72,6 +73,7 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
_,
_,
user,
_,
) = auth_data
if (user_id in ["@me", user["user_id"]]) and self.helper.get_setting(
@ -121,6 +123,7 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
_,
superuser,
user,
_,
) = auth_data
try:

View File

@ -27,6 +27,7 @@ class ApiUsersUserPermissionsHandler(BaseApiHandler):
_,
_,
user,
_,
) = auth_data
if user_id in ["@me", user["user_id"]]:

View File

@ -17,6 +17,7 @@ class ApiUsersUserPublicHandler(BaseApiHandler):
_,
_,
user,
_,
) = auth_data
if user_id == "@me":

View File

@ -30,7 +30,7 @@ class ServerHandler(BaseHandler):
) = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
superuser = superuser and api_key.full_access
if superuser:
defined_servers = self.controller.servers.list_defined_servers()
@ -124,7 +124,7 @@ class ServerHandler(BaseHandler):
"created": api_key.created,
"server_permissions": api_key.server_permissions,
"crafty_permissions": api_key.crafty_permissions,
"superuser": api_key.superuser,
"full_access": api_key.full_access,
}
if api_key is not None
else None
@ -146,12 +146,12 @@ class ServerHandler(BaseHandler):
return
page_data["server_api"] = False
if page_data["online"]:
page_data["server_api"] = self.helper.check_address_status(
"https://api.serverjars.com"
page_data["server_api"] = (
self.controller.big_bucket._check_bucket_alive()
)
page_data["server_types"] = self.controller.server_jars.get_serverjar_data()
page_data["server_types"] = self.controller.big_bucket.get_bucket_data()
page_data["js_server_types"] = json.dumps(
self.controller.server_jars.get_serverjar_data()
self.controller.big_bucket.get_bucket_data()
)
if page_data["server_types"] is None:
page_data["server_types"] = []

View File

@ -42,7 +42,7 @@ class UploadHandler(BaseHandler):
if self.upload_type == "server_import":
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
superuser = superuser and api_key.full_access
user_id = exec_user["user_id"]
stream_size_value = self.helper.get_setting("stream_size_GB")
@ -133,7 +133,7 @@ class UploadHandler(BaseHandler):
elif self.upload_type == "background":
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
superuser = superuser and api_key.full_access
user_id = exec_user["user_id"]
stream_size_value = self.helper.get_setting("stream_size_GB")
@ -212,7 +212,7 @@ class UploadHandler(BaseHandler):
server_id = self.get_argument("server_id", None)
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
superuser = superuser and api_key.full_access
user_id = exec_user["user_id"]
stream_size_value = self.helper.get_setting("stream_size_GB")

View File

@ -14,6 +14,9 @@
"auth": {
"format": "%(asctime)s - [AUTH] - %(levelname)s - %(message)s"
},
"audit": {
"()": "app.classes.logging.log_formatter.JsonFormatter"
},
"cmd_queue": {
"format": "%(asctime)s - [CMD_QUEUE] - %(levelname)s - %(message)s"
}
@ -70,6 +73,14 @@
"maxBytes": 10485760,
"backupCount": 20,
"encoding": "utf8"
},
"audit_log_handler": {
"class": "logging.handlers.RotatingFileHandler",
"formatter": "audit",
"filename": "logs/audit.log",
"maxBytes": 10485760,
"backupCount": 20,
"encoding": "utf8"
}
},
"loggers": {
@ -108,6 +119,12 @@
"cmd_queue_file_handler"
],
"propagate": false
},
"audit_log": {
"level": "INFO",
"handlers": [
"audit_log_handler"
]
}
}
}

View File

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

View File

@ -1,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 683.6 143.8" style="enable-background:new 0 0 683.6 143.8;" xml:space="preserve">
<style type="text/css">
.st0{opacity:0.85;fill:#FFFFFF;enable-background:new ;}
.st1{opacity:0.85;}
.st2{fill:#FFFFFF;}
.st3{fill:none;}
.st4{fill:url(#SVGID_1_);}
.st5{fill:url(#SVGID_00000137122815686618769650000009047437546445953421_);}
.st6{fill:url(#SVGID_00000170963539203169094570000007184871682409824703_);}
.st7{fill:url(#SVGID_00000169549353698428389090000007910489870824235905_);}
.st8{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_00000029754379306852418700000008865188217784465572_);}
</style>
<path class="st0" d="M175.8,111.5h17.6v3.8h-13.2v8.9h12.1v3.7h-12.1v11.8h-4.4V111.5z"/>
<path class="st0" d="M196.3,119.1h4.2v3.5h0.1c0.4-2.3,2.4-3.9,4.7-3.9c0.5,0,1,0.1,1.5,0.2v3.9c-0.6-0.2-1.3-0.3-1.9-0.2
c-2.7,0-4.4,1.8-4.4,4.8v12.3h-4.2L196.3,119.1z"/>
<path class="st0" d="M207.2,129.4L207.2,129.4c0-6.6,3.9-10.6,9.7-10.6s9.8,4,9.8,10.6l0,0c0,6.6-3.9,10.7-9.8,10.7
S207.2,136,207.2,129.4z M222.4,129.4L222.4,129.4c0-4.5-2.2-7.1-5.5-7.1s-5.4,2.6-5.4,7.1l0,0c0,4.5,2.2,7.2,5.5,7.2
S222.4,133.9,222.4,129.4L222.4,129.4z"/>
<path class="st0" d="M229.6,119.1h4.2v3.2h0.1c1-2.3,3.2-3.7,5.7-3.6c2.6-0.2,5,1.5,5.7,4h0.1c1.1-2.5,3.6-4.1,6.4-4
c4.1,0,6.7,2.7,6.7,6.8v14.1h-4.2v-13.1c0-2.7-1.4-4.2-3.9-4.2c-2.3,0-4.2,1.8-4.3,4.2c0,0.1,0,0.2,0,0.3v12.9H242v-13.4
c0.2-2-1.3-3.8-3.3-3.9c-0.2,0-0.4,0-0.5,0c-2.4,0-4.3,2-4.3,4.3c0,0.1,0,0.2,0,0.3v12.7h-4.2L229.6,119.1z"/>
<g id="Layer_2_00000138553854520646606810000012156271018779627156_" class="st1">
<g id="Layer_1-2">
<path class="st2" d="M343.7,139.9c-6.9,0-12.5-5.6-12.5-12.5s5.6-12.5,12.5-12.5c2.1,0,4.2,0.5,6,1.5c1.8,1,3.3,2.4,4.3,4.1
l-4.1,2.4c-0.6-1.1-1.5-1.9-2.5-2.5c-3.1-1.6-6.8-1.1-9.4,1.3c-1.5,1.5-2.2,3.6-2.1,5.7c-0.1,2.1,0.7,4.1,2.1,5.7
c1.5,1.5,3.5,2.3,5.7,2.2c1.3,0,2.6-0.3,3.7-0.9c1.1-0.6,2-1.4,2.5-2.5l4.1,2.4c-1,1.7-2.5,3.2-4.3,4.1
C347.8,139.4,345.8,139.9,343.7,139.9z"/>
<path class="st2" d="M361.4,122.3v3c0.3-1,1.1-1.9,2-2.5c1-0.6,2.1-0.9,3.2-0.8v4.9c-1.3-0.2-2.6,0.1-3.6,0.8
c-1.1,0.8-1.7,2.2-1.6,3.5v8.2H357v-17.2H361.4z"/>
<path class="st2" d="M381.6,124.3v-2h4.4v17.2h-4.4v-2c-1.4,1.7-3.4,2.6-5.6,2.5c-2.2,0-4.4-0.9-5.9-2.6c-1.6-1.8-2.5-4.1-2.4-6.5
c-0.1-2.4,0.8-4.7,2.4-6.4c1.5-1.7,3.6-2.7,5.9-2.7C378.1,121.7,380.2,122.6,381.6,124.3z M373.4,134.3c1.9,1.8,4.9,1.8,6.8,0
c0.9-0.9,1.4-2.2,1.4-3.5c0.1-1.3-0.4-2.6-1.4-3.5c-1.9-1.8-4.9-1.8-6.8,0c-0.9,0.9-1.4,2.2-1.3,3.5
C372,132.1,372.5,133.4,373.4,134.3z"/>
<path class="st2" d="M399.2,115v4.2c-2.4-0.2-3.6,0.8-3.7,2.9v0.2h3.6v4.3h-3.6v12.9h-4.4v-12.9h-2.5v-4.2h2.5v-0.2
c-0.2-2,0.6-4.1,2-5.5C394.5,115.3,396.6,114.8,399.2,115z"/>
<path class="st2" d="M411.6,122.3v4.2h-3.9v7.1c0,0.5,0.1,1,0.5,1.3c0.4,0.3,0.8,0.5,1.3,0.5c0.7,0,1.4,0,2.1,0v4
c-3,0.3-5.1,0.1-6.4-0.8s-1.9-2.5-1.9-4.9v-7.1h-3v-4.2h3v-3.5l4.4-1.3v4.8L411.6,122.3z"/>
<path class="st2" d="M427.2,124.3v-2h4.4v17.2h-4.4v-2c-1.4,1.7-3.4,2.6-5.6,2.5c-2.2,0-4.4-0.9-5.9-2.6c-1.6-1.8-2.5-4.1-2.4-6.5
c-0.1-2.4,0.8-4.7,2.4-6.4c1.5-1.7,3.6-2.7,5.9-2.7C423.8,121.7,425.9,122.6,427.2,124.3z M419.1,134.3c1.9,1.8,4.9,1.8,6.8,0
c0.9-0.9,1.4-2.2,1.4-3.5c0-1.3-0.4-2.5-1.4-3.5c-1.9-1.8-4.9-1.8-6.8,0c-0.9,0.9-1.4,2.2-1.3,3.5
C417.7,132.1,418.2,133.4,419.1,134.3L419.1,134.3z"/>
<path class="st2" d="M440.1,122.3v3c0.4-1,1.1-1.9,2-2.5c1-0.6,2.1-0.9,3.2-0.8v4.9c-1.3-0.2-2.6,0.1-3.6,0.8
c-1.1,0.8-1.7,2.2-1.6,3.5v8.2h-4.4v-17.2H440.1z"/>
<path class="st2" d="M461.9,137.3c-3.6,3.6-9.3,3.6-12.9,0s-3.6-9.3,0-12.9l0,0c3.6-3.5,9.3-3.5,12.9,0.1c1.7,1.7,2.6,4,2.6,6.4
C464.5,133.3,463.6,135.6,461.9,137.3z M452.1,134.3c1.9,1.8,4.8,1.8,6.7,0c1.8-1.9,1.8-4.9,0-6.8c-1.9-1.8-4.8-1.8-6.7,0
C450.3,129.4,450.3,132.3,452.1,134.3L452.1,134.3z"/>
<path class="st2" d="M320,137.6l-2.9-20.3c-0.4-2.7-2.7-4.7-5.5-4.7h-9c-0.3,0-0.5,0.2-0.7,0.4l-0.9,2H292l-0.9-2
c-0.1-0.3-0.4-0.4-0.7-0.4h-9c-2.7,0-5.1,2-5.5,4.7l-2.9,20.3c-0.4,3,1.7,5.8,4.7,6.2c0,0,0,0,0,0l0,0c0.3,0,0.5,0.1,0.8,0.1h36
c3,0,5.5-2.5,5.5-5.5l0,0C320,138.1,320,137.8,320,137.6z M287.1,130c-2.7,0-4.9-2.2-4.9-4.9c0-2.7,2.2-4.9,4.9-4.9
c2.7,0,4.9,2.2,4.9,4.9c0,0,0,0,0,0l0,0C292,127.8,289.8,130,287.1,130z M296.5,138c-2.7,0-4.9-2.2-4.9-4.9h9.8
C301.4,135.8,299.3,138,296.5,138L296.5,138L296.5,138z M305.9,130c-2.7,0-4.9-2.2-4.9-4.9c0-2.7,2.2-4.9,4.9-4.9
c2.7,0,4.9,2.2,4.9,4.9c0,0,0,0,0,0l0,0C310.8,127.8,308.6,130,305.9,130L305.9,130z"/>
</g>
</g>
<path class="st2" d="M133.1,19.2H9.7c-1.8,0-3.2-1.4-3.2-3.2V3.2C6.5,1.5,7.9,0,9.7,0h123.4c1.8,0,3.2,1.4,3.2,3.2V16
C136.3,17.8,134.9,19.2,133.1,19.2"/>
<path class="st2" d="M23.6,36.7c-3.4,0-6.7,1.6-8.8,4.3c-2.9,3.6-4.1,8.3-3.2,12.8l9.2,51.9c1.2,6.6,6.2,11.4,12.1,11.4H110
c5.8,0,10.9-4.8,12.1-11.4l9.2-51.9c0.8-4.5-0.4-9.2-3.3-12.8c-2.1-2.7-5.4-4.3-8.8-4.3H23.6z M110,128.3H32.8
c-11.3,0-21-8.7-23.1-20.7L0.5,55.8c-1.5-7.8,0.6-15.9,5.7-22c4.3-5.2,10.7-8.3,17.4-8.3h95.6c6.8,0.1,13.1,3.1,17.4,8.3
c5.1,6.1,7.2,14.2,5.7,22l-9.2,51.9C130.9,119.7,121.2,128.4,110,128.3"/>
<path class="st2" d="M120.8,23.8v-2.2c2,0,3.5-1.6,3.5-3.6c0-1.8-1.5-3.4-3.3-3.5H21.6c-2,0.1-3.5,1.8-3.4,3.7
c0.1,1.8,1.5,3.3,3.4,3.4v2.2c-3.2-0.1-5.7-2.8-5.6-6c0.1-3,2.5-5.4,5.6-5.6h99.2c3.2-0.1,5.9,2.4,6,5.6s-2.4,5.9-5.6,6
C121.1,23.8,121,23.8,120.8,23.8"/>
<path class="st2" d="M120.8,33.1H21.6c-3.2,0-5.8-2.6-5.8-5.8c0-3.2,2.6-5.8,5.8-5.8v2.2c-2,0.1-3.5,1.8-3.4,3.7
c0.1,1.8,1.5,3.3,3.4,3.4h99.2c2,0.1,3.7-1.3,3.8-3.3c0.1-2-1.3-3.7-3.3-3.8c-0.1,0-0.2,0-0.3,0h-0.2v-2.2c3.2-0.1,5.9,2.4,6,5.6
s-2.4,5.9-5.6,6C121.1,33.1,121,33.1,120.8,33.1"/>
<path class="st2" d="M21.6,21.5l36.1,1.1l-36.1,1.1V21.5z"/>
<path class="st2" d="M125.5,23.8l-45.1-1.1l45.1-1.1V23.8z"/>
<rect x="-2.5" y="-1.1" class="st3" width="571.3" height="131.4"/>
<path class="st2" d="M163.8,91.7l7.3-10.9c5.8,5.5,14.3,9.3,22.3,9.3c7.1,0,13.1-3.3,13.1-8.3c0-6-8.1-7.9-15.4-9.6
c-13.7-3.2-24.8-9.8-24.8-22.3c0-12.7,11.1-21,27.1-21c10.7,0,19.4,3.7,24.7,8.9l-6.6,10.8c-4-3.9-11.2-6.9-18.3-6.9
s-12.2,3.2-12.2,7.7c0,5.5,7.4,7.9,14.1,9.3s26.2,6.2,26.2,22.5c0,12.8-12.2,21.6-27.8,21.6C182.6,102.8,171.1,98.4,163.8,91.7z"/>
<path class="st2" d="M281.7,80.1h-40.9c1.9,6.6,7.5,10.9,15.1,10.9c5.6,0.1,10.9-2.3,14.5-6.5l9,7.9c-5.5,6.5-14,10.5-23.9,10.5
c-16.8,0-29.3-12-29.3-27.8c0-15.6,12.1-27.4,28-27.4S282,59.4,282,75.3C282,76.9,281.9,78.5,281.7,80.1z M240.8,70.3h26.9
c-1.7-6.6-6.9-10.9-13.4-10.9C247.7,59.4,242.5,63.8,240.8,70.3L240.8,70.3z"/>
<path class="st2" d="M321.3,48v13.9h-2.3c-9.6,0-15.2,5.7-15.2,14.7v25h-13.4V48.9h13.5v6.8c3.6-4.8,9.2-7.7,15.2-7.7L321.3,48z"/>
<path class="st2" d="M381.9,48.9L360,101.6h-13.9l-21.9-52.8h15.3l13.8,35.9L367,48.9H381.9z"/>
<path class="st2" d="M437.1,80.1h-40.9c1.9,6.6,7.5,10.9,15.1,10.9c5.6,0.1,10.9-2.3,14.5-6.5l9,7.9c-5.5,6.5-14,10.5-23.9,10.5
c-16.8,0-29.3-12-29.3-27.8c0-15.6,12.1-27.4,28-27.4s27.7,11.8,27.7,27.7C437.4,76.9,437.3,78.5,437.1,80.1z M396.1,70.3H423
c-1.7-6.6-6.9-10.9-13.4-10.9S397.7,63.8,396.1,70.3L396.1,70.3z"/>
<path class="st2" d="M476.7,48v13.9h-2.2c-9.6,0-15.2,5.7-15.2,14.7v25h-13.5V48.9h13.5v6.8c3.6-4.8,9.2-7.7,15.2-7.7L476.7,48z"/>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="870.0443" y1="434.2369" x2="907.1767" y2="465.2789" gradientTransform="matrix(1 0 0 1 -374.6 -381.3801)">
<stop offset="0" style="stop-color:#FEAF6F"/>
<stop offset="1" style="stop-color:#FD5E83"/>
</linearGradient>
<path class="st4" d="M492.5,100.6V87c3.2,1.4,6.6,2.1,10,2.2c7.3,0,11.8-3.9,11.8-10.9v-48h14.3V79c0,15-9.8,23.9-24.5,23.9
C500,102.9,496.1,102.1,492.5,100.6z"/>
<linearGradient id="SVGID_00000162328622213414588160000008200821717462734513_" gradientUnits="userSpaceOnUse" x1="920.7661" y1="434.5518" x2="972.3098" y2="477.6348" gradientTransform="matrix(1 0 0 1 -374.6 -381.3801)">
<stop offset="0" style="stop-color:#FEAF6F"/>
<stop offset="1" style="stop-color:#FD5E83"/>
</linearGradient>
<path style="fill:url(#SVGID_00000162328622213414588160000008200821717462734513_);" d="M593.2,48.9v52.8h-13.5v-6.3
c-4.4,4.9-10.6,7.6-17.2,7.5c-14.7,0-25.8-11.9-25.8-27.6s11.1-27.6,25.8-27.6c6.5-0.1,12.8,2.7,17.2,7.5v-6.3L593.2,48.9z
M579.8,75.2c0-8-6.6-14.5-14.6-14.5c-8,0-14.5,6.6-14.5,14.6c0,8,6.5,14.4,14.5,14.5c7.9,0.2,14.4-6,14.6-13.9
C579.8,75.7,579.8,75.5,579.8,75.2z"/>
<linearGradient id="SVGID_00000026849485640012965730000014957007722205225107_" gradientUnits="userSpaceOnUse" x1="973.2171" y1="437.9167" x2="1007.0711" y2="466.2133" gradientTransform="matrix(1 0 0 1 -374.6 -381.3801)">
<stop offset="0" style="stop-color:#FEAF6F"/>
<stop offset="1" style="stop-color:#FD5E83"/>
</linearGradient>
<path style="fill:url(#SVGID_00000026849485640012965730000014957007722205225107_);" d="M635.9,48v13.9h-2.3
c-9.6,0-15.2,5.7-15.2,14.7v25H605V48.9h13.4v6.8c3.6-4.8,9.2-7.7,15.2-7.7L635.9,48z"/>
<linearGradient id="SVGID_00000011000279650532451330000005619277557075874698_" gradientUnits="userSpaceOnUse" x1="1015.3561" y1="439.477" x2="1056.9301" y2="474.2302" gradientTransform="matrix(1 0 0 1 -374.6 -381.3801)">
<stop offset="0" style="stop-color:#FEAF6F"/>
<stop offset="1" style="stop-color:#FD5E83"/>
</linearGradient>
<path style="fill:url(#SVGID_00000011000279650532451330000005619277557075874698_);" d="M638.7,94.8l6.5-8.9
c4.2,3.8,9.7,5.9,15.4,5.9c5.4,0,9.3-1.8,9.3-5c0-3.5-4.6-4.8-10.3-6.1c-8.4-1.9-19.2-4.5-19.2-16.5c0-11.2,9.8-16.7,21.5-16.7
c7.4-0.1,14.6,2.3,20.5,6.9l-6.5,9c-3.9-3.1-8.7-4.8-13.7-4.9c-4.6,0-8.3,1.5-8.3,4.5c0,3.5,4.4,4.7,10.3,5.9
c8.4,1.9,19.2,4.5,19.2,16.4c0,11.2-9.9,17.3-22.6,17.3C652.9,102.9,644.9,100.1,638.7,94.8z"/>
<linearGradient id="SVGID_00000176732902084481618460000012775063734620060048_" gradientUnits="userSpaceOnUse" x1="408.7259" y1="431.5905" x2="485.4144" y2="495.6844" gradientTransform="matrix(1 0 0 1 -374.6 -381.3801)">
<stop offset="0" style="stop-color:#FEAF6F"/>
<stop offset="1" style="stop-color:#FD5E83"/>
</linearGradient>
<path style="fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_00000176732902084481618460000012775063734620060048_);" d="
M124.5,62c-12.7,0.9-27,5.5-35.7,12.3c-38.7,30.3-69.2-6.6-69.3-6.6l6.8,36.8c0.8,4.3,4.6,7.5,9,7.5l73,0.2c4.5,0,8.3-3.2,9.1-7.6
L124.5,62z"/>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 142.71 128.36"><defs><style>.cls-1{fill:#fff;}.cls-2{fill-rule:evenodd;fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" x1="408.73" y1="431.59" x2="485.41" y2="495.68" gradientTransform="translate(-374.6 -381.38)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#feaf6f"/><stop offset="1" stop-color="#fd5e83"/></linearGradient></defs><path class="cls-1" d="M133.09,19.17H9.67A3.24,3.24,0,0,1,6.46,16V3.24A3.24,3.24,0,0,1,9.7,0H133.09a3.25,3.25,0,0,1,3.25,3.24V16a3.25,3.25,0,0,1-3.25,3.24"/><path class="cls-1" d="M23.61,36.67A11.41,11.41,0,0,0,14.8,41a15.79,15.79,0,0,0-3.25,12.8l9.18,51.92c1.17,6.62,6.25,11.42,12.06,11.42H110c5.82,0,10.89-4.8,12.06-11.42l9.18-51.91A15.86,15.86,0,0,0,128,41a11.5,11.5,0,0,0-8.82-4.33ZM110,128.35H32.8c-11.27,0-21-8.7-23.12-20.69L.46,55.75a26.72,26.72,0,0,1,5.71-22,22.77,22.77,0,0,1,17.41-8.34h95.56a22.8,22.8,0,0,1,17.41,8.34,26.79,26.79,0,0,1,5.71,22l-9.19,51.91c-2.12,12-11.84,20.7-23.12,20.7"/><path class="cls-1" d="M120.8,23.76V21.51A3.56,3.56,0,0,0,121,14.4H21.59a3.56,3.56,0,0,0,0,7.11v2.25a5.81,5.81,0,0,1,0-11.61H120.8a5.81,5.81,0,0,1,.48,11.61h-.48"/><path class="cls-1" d="M120.8,33.11H21.59a5.8,5.8,0,0,1,0-11.6v2.24a3.56,3.56,0,0,0,0,7.11H120.8a3.56,3.56,0,0,0,.52-7.1h-.52V21.51a5.81,5.81,0,0,1,.48,11.61,3.84,3.84,0,0,1-.48,0"/><path class="cls-1" d="M21.59,21.51l36.13,1.13L21.59,23.76Z"/><path class="cls-1" d="M125.46,23.76,80.35,22.64l45.11-1.13Z"/><path class="cls-2" d="M124.46,62c-12.72.93-27,5.55-35.7,12.34-38.69,30.34-69.25-6.6-69.28-6.58l6.75,36.83a9.16,9.16,0,0,0,9,7.52l73,.16a9.17,9.17,0,0,0,9.06-7.64Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -36,25 +36,21 @@
<table class="table table-hover" id="audit_table" style="overflow: scroll;" width="100%">
<thead>
<tr class="rounded">
<td>Username</td>
<td>Time</td>
<td>Action</td>
<td>Server ID</td>
<td>IP</td>
<th>Time</th>
<th>Username</th>
<th>Action</th>
<th>Server ID</th>
<th>IP</th>
</tr>
</thead>
<tbody>
{% for row in data['audit_logs'] %}
<tr>
<td>{{ row['user_name'] }}</td>
<td>
{{ row['created'].strftime('%Y-%m-%d %H:%M:%S') }}
<td colspan="5" id="image-div" class="text-center"> <!-- Center image within table -->
<img class="img-center" id="logo-animate" src="../static/assets/images/crafty-logo-square-1024.png"
alt="Crafty Logo, Crafty is loading" width="20%"><br><br>{{ translate('datatables',
'loadingRecords', data['lang'])}}
</td>
<td>{{ row['log_msg'] }}</td>
<td>{{ row['server_id'] }}</td>
<td>{{ row['source_ip'] }}</td>
</tr>
{% end %}
</tbody>
</table>
@ -79,17 +75,6 @@
{% end %}
{% block js %}
<script>
$(document).ready(function () {
console.log('ready for JS!')
$('#audit_table').DataTable({
'order': [1, 'desc']
}
);
});
</script>
<script>
$(document).ready(function () {
$('[data-toggle="popover"]').popover();
@ -112,6 +97,74 @@
$('.too_small').popover("hide");
} // New width
});
$(document).ready(function () {
console.log('ready for JS!')
// Initialize DataTables
// Load initial data
getActivity();
});
function updateActivity(data) {
let tbody = $('#audit_table tbody');
tbody.empty(); // Clear existing rows
$.each(data, function (index, value) {
let row = $('<tr>');
row.append(`<td>${value.time}</td>`);
if (value.user_name != "system" && value.user_id != "-1") {
row.append(`<td><a href="/panel/edit_user?id=${value.user_id}">${value.user_name}</a></td>`);
} else {
row.append(`<td>${value.user_name}</td>`);
}
row.append(`<td>${value.log_msg}</td>`);
row.append(`<td>${value.server_id}</td>`);
row.append(`<td>${value.source_ip}</td>`);
tbody.append(row);
});
$('#audit_table').DataTable({
'order': [[0, 'desc']], // Sort by the first column in descending order
filter: true,
"searching": true,
})
}
async function getActivity() {
var token = getCookie("_xsrf");
let res = await fetch(`/api/v2/crafty/logs/audit`, {
method: 'GET',
headers: {
'X-XSRFToken': token
},
});
let responseData = await res.json();
console.log(responseData);
if (responseData.status === "ok") {
updateActivity(responseData.data);
console.log("activity update")
} else {
bootbox.alert(responseData.error)
}
}
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);
});
</script>
{% end %}

View File

@ -20,7 +20,8 @@
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']) }}">
data-almost="{{ translate('startup', 'almost', data['lang']) }}"
data-cache="{{ translate('startup', 'cache', data['lang'])}}">
{{ translate('startup', 'starting', data['lang']) }}</h2>
</div>

View File

@ -58,7 +58,7 @@
<!--<th>ID</th>-->
<th>{{ translate('apiKeys', 'name', data['lang']) }}</th>
<th>{{ translate('apiKeys', 'created', data['lang']) }}</th>
<th>{{ translate('apiKeys', 'superUser', data['lang']) }}</th>
<th>{{ translate('apiKeys', 'fullAccess', data['lang']) }}</th>
<th>{{ translate('apiKeys', 'perms', data['lang']) }}</th>
<th>{{ translate('apiKeys', 'buttons', data['lang']) }}</th>
</tr>
@ -70,7 +70,7 @@
<td>{{ apikey.name }}</td>
<td>{{ apikey.created.strftime('%d/%m/%Y %H:%M:%S') }}</td>
<td>
{% if apikey.superuser %}
{% if apikey.full_access %}
<span class="text-success">
<i class="fas fa-check-square"></i> {{
translate('apiKeys', 'yes', data['lang']) }}
@ -148,9 +148,15 @@
}}</label>
</td>
<td>
{% if permission in data['user_crafty_permissions'] %}
<input type="checkbox" class="crafty_perm"
id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" value="1">
{% else %}
<input type="checkbox" class="crafty_perm"
id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" value="1" disabled>
{% end %}
</td>
</tr>
{% end %}
@ -158,8 +164,8 @@
</tbody>
</table>
<label for="superuser">Superuser</label>
<input type="checkbox" class="" id="superuser" name="superuser" value="1">
<label for="full_access">{{translate('apiKeys', 'fullAccess', data['lang'])}}</label>
<input type="checkbox" class="" id="full_access" name="full_access" value="1">
<br />
@ -240,7 +246,7 @@
"name": formDataObject.name,
"server_permissions_mask": server_permissions,
"crafty_permissions_mask": crafty_permissions,
"superuser": $("#superuser").prop('checked'),
"full_access": $("#full_access").prop('checked'),
});
console.log(formDataJsonString);

View File

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<html lang="{{ data.get('lang_page', 'en') }}" class="{{data['user_data'].get('theme', 'default')}}"
data-username="{{data['user_data'].get('username', None)}}">
<head>
<!-- Required meta tags -->
@ -60,6 +61,11 @@
<b>{{ translate('error', 'hereIsTheError', data['lang']) }}: {{data['error']}}</b><br /><br />
That's all the help I can give you - Godspeed
<br /><br />
<a class="d-inline font-weight-medium" href="/panel/dashboard"><button class="btn btn-info">{{
translate('error', 'return',
data['lang'])}}</button></a>
<br>
<br>
<a class="d-inline font-weight-medium" href="https://discord.gg/9VJPhCE"> {{ translate('error',
'contact', data['lang']) }}</a>
</p>

View File

@ -171,7 +171,6 @@
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
console.log(formDataObject)
let res = await fetch(`/login`, {
method: 'POST',
headers: {

View File

@ -20,11 +20,6 @@
<div class="col-sm-6 grid-margin stretch-card">
<div class="card" id="creation_wizard">
<div class="card-body">
{% if data["server_api"] and data["online"] %}
<a href="https://serverjars.com/" target="_blank" alt="serverjars icon"><img
src="../../static/assets/images/serverjars/ICON.svg"
style="float: right; width: 40px; position: relative;"></a>
{% end %}
<h4>{{ translate('serverWizard', 'newServer', data['lang']) }}</h4>
<br />
@ -67,16 +62,19 @@
{% end %}
{% raw xsrf_form_html() %}
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label for="server_jar">{{ translate('serverWizard', 'serverType', data['lang'])
}}</label>
<label for="server_jar">{{ translate('serverWizard', 'serverType', data['lang']) }}</label>
<div class="input-group">
<select required class="form-control form-control-lg select-css" id="server_jar"
name="server_jar" onchange="serverJarChange(this)">
<option value="None">{{ translate('serverWizard', 'selectType', data['lang']) }}</option>
<select required class="form-control form-control-lg select-css" id="server_jar" name="type"
onchange="serverJarChange(this)">
<option value="">{{ translate('serverWizard', 'selectServer', data['lang']) }}</option>
{% for s in data['server_types'] %}
<option value="{{ s }}">{{ s.capitalize() }}</option>
{% if data['server_types'][s].get("enabled", False) %}
<option value="{{ s }}">{{ data["server_types"][s].get("friendly_name", s).capitalize() }}
{% end %}
</option>
{% end %}
</select>
{% if data['super_user'] %}
@ -108,7 +106,8 @@
</div>
</div>
<span data-html="true" class="version-hint text-center"
data-content="⚠️ {{ translate('serverWizard', 'unsupported', data['lang']) }} ⚠️" , data-placement="right"></span>
data-content="⚠️ {{ translate('serverWizard', 'unsupported', data['lang']) }} ⚠️" ,
data-placement="right"></span>
<div class="col-sm-12">
<div class="form-group">
<label for="server_name">{{ translate('serverWizard', 'serverName', data['lang']) }}</label>
@ -191,10 +190,12 @@
{% if not data["server_api"] and data["online"] %}
<div class="api-alert" style="position: absolute; top: -5px; z-index: 100; opacity: .99;">
<p style="color: white !important;"><i class="fas fa-exclamation-triangle" style="color: red;"></i>&nbsp;{{
translate('error', 'serverJars1', data['lang']) }}<a style="color: red;" ;
href="https://status.craftycontrol.com/status/craftycontrol" target="_blank" rel="noopener">&nbsp;{{ translate('error',
translate('error', 'bigBucket1', data['lang']) }}<a style="color: red;" ;
href="https://status.craftycontrol.com/status/craftycontrol" target="_blank" rel="noopener">&nbsp;{{
translate('error',
'craftyStatus', data['lang']) }}</a>
&nbsp;{{ translate('error', 'serverJars2', data['lang']) }}</p>
&nbsp;{{ translate('error', 'bigBucket2', data['lang']) }}</br></br><small>{{ translate('error', 'selfHost',
data['lang'])}}</small></p>
</div>
{% end %}
{% if not data["online"] %}
@ -827,7 +828,7 @@
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
closeButton: false
});
setTimeout(function(){
setTimeout(function () {
getDirView();
}, 2000);
} else {
@ -845,9 +846,9 @@
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
closeButton: false
});
setTimeout(function(){
getDirView();
}, 2000);
setTimeout(function () {
getDirView();
}, 2000);
});
var upload = false;
var file;
@ -1137,7 +1138,7 @@
function wait_msg(importing) {
bootbox.alert({
title: importing ? '{% raw translate("serverWizard", "importing", data["lang"]) %}' : '{% raw translate("serverWizard", "downloading", data["lang"]) %}',
message: importing ? '<i class="fas fa-cloud-download"></i> {% raw translate("serverWizard", "bePatient", data["lang"]) %}' : '<i class="fas fa-cloud-download"></i> {% raw translate("serverWizard", "bePatient", data["lang"]) %}<br><br><a href="https://serverjars.com" target="_blank" style="text-align: center;"><img src="../../static/assets/images/serverjars/FULL-WHITE.svg" alt="Powered by serverjars.com" width="40%"></a>',
message: importing ? '<i class="fas fa-cloud-download"></i> {% raw translate("serverWizard", "bePatient", data["lang"]) %}' : '<i class="fas fa-cloud-download"></i> {% raw translate("serverWizard", "bePatient", data["lang"]) %}',
});
}
@ -1197,33 +1198,28 @@
*/
function serverTypeChange(selectObj) {
// get the index of the selected option
let idx = document.getElementById('server_type').selectedIndex;
let idx = document.getElementById('server_type').value;
let idx_list = idx.split("|");
// get the value of the selected option
let cSelect = document.getElementById("server");
let which = {};
try {
which = document.getElementById('server_type').options[idx].value;
versions = Object.keys(serverTypesLists[idx_list[0]]["types"][idx_list[1]]["versions"]);
} catch {
while (cSelect.options.length > 0) {
cSelect.remove(0);
}
return;
}
let server_type = which.split('|')[0];
let server = which.split('|')[1];
// use the selected option value to retrieve the list of items from the serverTypesLists array
let cList = serverTypesLists[server_type];
// get the country select element via its known id
cSelect = document.getElementById("server");
// remove the current options from the country select
while (cSelect.options.length > 0) {
cSelect.remove(0);
}
let newOption;
$("#server option").each(function () {
$(this).remove()
})
// create new options ordered by ascending
cList[server].forEach(type => {
versions.forEach(type => {
newOption = document.createElement("option");
newOption.value = which + "|" + type; // assumes option string and value are the same
newOption.value = idx + "|" + type; // assumes option string and value are the same
newOption.text = type;
// add the new option
try {
@ -1234,17 +1230,18 @@
}
})
}
$("#server").change(function (){
$("#server").change(function () {
let selected_version = $("#server :selected").text().split(".");
if(parseInt(selected_version[0]) === 1 && parseInt(selected_version[1]) < 8 ){
if (parseInt(selected_version[0]) === 1 && parseInt(selected_version[1]) < 8) {
$('[data-toggle="popover"]').popover();
if ($(window).width() < 1000) {
$('.version-hint').attr("data-placement", "top")
if ($(window).width() < 1000) {
$('.version-hint').attr("data-placement", "top")
} else {
$('.version-hint').attr("data-placement", "right")
}
$('.version-hint').popover("show");
} else {
$('.version-hint').attr("data-placement", "right")
}
$('.version-hint').popover("show");
}else{
$('.version-hint').popover("hide");
}
});
@ -1253,6 +1250,15 @@
const type_select = document.getElementById('server_jar')
const tidx = type_select.selectedIndex;
const val = type_select.options[tidx].value;
if (!val) {
$("#server_type option").each(function () {
$(this).remove()
})
$("#server option").each(function () {
$(this).remove()
})
return;
}
let jcSelect = {};
if (val == 'None') {
jcSelect = document.getElementById("server_type");
@ -1267,7 +1273,7 @@
// get the value of the selected option
let jwhich = selectObj.options[jidx].value;
// use the selected option value to retrieve the list of items from the serverTypesLists array
let jcList = Object.keys(serverTypesLists[jwhich]);
let jcList = Object.keys(serverTypesLists[jwhich]["types"]);
// get the country select element via its known id
jcSelect = document.getElementById("server_type");
// remove the current options from the country select

View File

@ -6,7 +6,6 @@ import logging
from app.classes.shared.console import Console
from app.classes.shared.migration import Migrator, MigrateHistory
from app.classes.models.management import (
AuditLog,
Webhooks,
Schedules,
Backups,
@ -61,17 +60,6 @@ def migrate(migrator: Migrator, database, **kwargs):
peewee.CharField(primary_key=True, default=str(uuid.uuid4())),
)
# Changes on Audit Log Table
migrator.alter_column_type(
AuditLog,
"server_id",
peewee.ForeignKeyField(
Servers,
backref="audit_server",
null=True,
field=peewee.CharField(primary_key=True, default=str(uuid.uuid4())),
),
)
# Changes on Webhook Table
migrator.alter_column_type(
Webhooks,
@ -109,13 +97,6 @@ def rollback(migrator: Migrator, database, **kwargs):
peewee.AutoField(),
)
# Changes on Audit Log Table
migrator.alter_column_type(
AuditLog,
"server_id",
peewee.IntegerField(default=None, index=True),
)
# Changes on Webhook Table
migrator.alter_column_type(
Webhooks,

View File

@ -6,7 +6,6 @@ import logging
from app.classes.shared.console import Console
from app.classes.shared.migration import Migrator, MigrateHistory
from app.classes.models.management import (
AuditLog,
Webhooks,
Schedules,
Backups,
@ -73,20 +72,6 @@ def migrate(migrator: Migrator, database, **kwargs):
try:
logger.info("Migrating Data from Int to UUID (Foreign Keys)")
Console.info("Migrating Data from Int to UUID (Foreign Keys)")
# Changes on Audit Log Table
for audit_log in AuditLog.select():
old_server_id = audit_log.server_id_id
if old_server_id == "0" or old_server_id is None:
server_uuid = None
else:
try:
server = Servers.get_by_id(old_server_id)
server_uuid = server.server_uuid
except:
server_uuid = old_server_id
AuditLog.update(server_id=server_uuid).where(
AuditLog.audit_id == audit_log.audit_id
).execute()
# Changes on Webhooks Log Table
for webhook in Webhooks.select():
@ -247,21 +232,6 @@ def rollback(migrator: Migrator, database, **kwargs):
try:
logger.info("Migrating Data from UUID to Int (Foreign Keys)")
Console.info("Migrating Data from UUID to Int (Foreign Keys)")
# Changes on Audit Log Table
for audit_log in AuditLog.select():
old_server_id = audit_log.server_id_id
if old_server_id is None:
new_server_id = 0
else:
try:
server = Servers.get_or_none(Servers.server_uuid == old_server_id)
new_server_id = server.server_id
except:
new_server_id = old_server_id
AuditLog.update(server_id=new_server_id).where(
AuditLog.audit_id == audit_log.audit_id
).execute()
# Changes on Webhooks Log Table
for webhook in Webhooks.select():
old_server_id = webhook.server_id_id

View File

@ -0,0 +1,17 @@
# Generated by database migrator
import peewee
def migrate(migrator, database, **kwargs):
migrator.rename_column("api_keys", "superuser", "full_access")
"""
Write your migrations here.
"""
def rollback(migrator, database, **kwargs):
migrator.rename_column("api_keys", "full_access", "superuser")
"""
Write your rollback migrations here.
"""

View File

@ -0,0 +1,34 @@
import peewee
import datetime
from peewee import (
AutoField,
DateTimeField,
CharField,
IntegerField,
ForeignKeyField,
TextField,
)
from app.classes.shared.server import Servers
def migrate(migrator, db):
migrator.drop_table("audit_log")
def rollback(migrator, db):
class AuditLog(peewee.Model):
audit_id = AutoField()
created = DateTimeField(default=datetime.datetime.now)
user_name = CharField(default="")
user_id = IntegerField(default=0, index=True)
source_ip = CharField(default="127.0.0.1")
server_id = ForeignKeyField(
Servers, backref="audit_server", null=True
) # When auditing global events, use server ID null
log_msg = TextField(default="")
class Meta:
table_name = "audit_log"
migrator.create_table(AuditLog)

View File

@ -20,6 +20,7 @@
"created": "Vytvořen",
"deleteKeyConfirmation": "Chcete tento API klíč odstranit? Tuto akci nelze vrátit zpět.",
"deleteKeyConfirmationTitle": "Odstranit klíč API ${keyId}?",
"fullAccess": "všechno",
"getToken": "Získat token",
"name": "Jméno",
"nameDesc": "Jak chcete nazvat tento token API? ",
@ -192,11 +193,14 @@
},
"thousands": " ",
"zeroRecords": "Nebyly nalezeny žádné odpovídající záznamy"
}
},
"loadingRecords": "Načítání..."
},
"error": {
"agree": "Souhlasím",
"bedrockError": "Stažení Bedrocku není dostupné. Prosím zkontrolujte",
"bigBucket1": "Big Bucket Kontrola stavu selhala. Prosím zkontrolujte jej",
"bigBucket2": "pro nejnovější informace.",
"cancel": "Zrušit",
"contact": "Kontaktujte podporu Crafty přes Discord",
"craftyStatus": "Crafty stav systémů",
@ -218,6 +222,8 @@
"not-downloaded": "Zdá se, že nemůžeme najít váš spustitelný soubor. Bylo jeho stahování dokončeno? Jsou oprávnění nastavena na spustitelný soubor?",
"portReminder": "Zjistili jsme, že server {} byl spuštěn poprvé. Ujistěte se, že jste přesměrovali port {} přes váš směrovač/firewall, aby byl tento port vzdáleně přístupný z internetu.",
"privMsg": "a ",
"return": "vrátit se na hlavní stránku",
"selfHost": "Pokud Hostujete sami toto uložiště prosím zkontrolujte adresu nebo si přečtěte náš průvodce odstraňováním problémů.",
"serverJars1": "Server JAR api je nepřístupná. Prosím zkontrolujte",
"serverJars2": "pro aktualní informace.",
"start-error": "Server {} se nepodařilo spustit s kódem chyby: {}",
@ -612,13 +618,14 @@
"credits": "Zásluhy",
"dashboard": "Ovládací panel",
"documentation": "Dokumentace",
"inApp": "V app dokumentaci",
"inApp": "V lokalní dokumentaci",
"navigation": "Navigace",
"newServer": "Vytvořit nový server",
"servers": "Servery"
},
"startup": {
"almost": "Dokončuji. Držte se...",
"cache": "Znovu načítam mezipaměť Big Bucket",
"internals": "Nastavuji a startuji Crafty interní komponenty",
"internet": "Kontroluju připojení k internetu",
"server": "Konfigurace ",

View File

@ -20,6 +20,7 @@
"created": "Erstellt",
"deleteKeyConfirmation": "Möchten Sie diesen API Schlüssel löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
"deleteKeyConfirmationTitle": "Folgenden API Schlüssel löschen: ${keyId}?",
"fullAccess": "Vollzugriff",
"getToken": "Schlüssel erhalten",
"name": "Name",
"nameDesc": "Wie soll der API Schlüssel genannt werden? ",
@ -177,11 +178,14 @@
},
"thousands": ".",
"zeroRecords": "Keine passenden Einträge gefunden"
}
},
"loadingRecords": "Laden..."
},
"error": {
"agree": "Zustimmen",
"bedrockError": "Bedrock-Downloads sind nicht verfügbar. Bitte überprüfen Sie",
"bigBucket1": "Big Bucket Zustandsprüfung fehlgeschlagen. Bitte Überprüfen",
"bigBucket2": "für die aktuellsten Informationen.",
"cancel": "Abbrechen",
"contact": "Kontaktieren Sie den Crafty Control Support über Discord",
"craftyStatus": "Crafty-Statusseite",
@ -203,6 +207,8 @@
"not-downloaded": "Crafty kann die auszuführende Datei nicht finden. Ist der Download abgeschlossen? Sind die Berechtigungen für Crafty korrekt?",
"portReminder": "Wir haben festgestellt, dass dies das erste Mal ist, dass {} ausgeführt wurde. Stellen Sie sicher, dass Sie Port {} durch Ihren Router/Firewall weiterleiten, um den Fernzugriff aus dem Internet zu ermöglichen.",
"privMsg": "und der/die/das ",
"return": "Zurück zum Dashboard",
"selfHost": "Wenn Sie dieses Repo selbst hosten, überprüfen Sie bitte Ihre Adresse oder konsultieren Sie unsere Anleitung zur Fehlerbehebung.",
"serverJars1": "Server-JAR-API nicht erreichbar. Bitte überprüfen Sie ",
"serverJars2": "um die aktuellsten Informationen zu erhalten.",
"start-error": "Der Server {} konnte wegen dem Fehlercode: {} nicht gestartet werden",
@ -600,6 +606,7 @@
},
"startup": {
"almost": "Nur noch einen Moment, fast geschafft",
"cache": "Aktualisieren der Big Bucket-Cache-Datei",
"internals": "Crafty's interne Komponneten initialisieren und starten",
"internet": "Verbindung zum Internet überprüfen",
"server": "initialisieren ",

View File

@ -20,6 +20,7 @@
"created": "Created",
"deleteKeyConfirmation": "Do you want to delete this API key? This cannot be undone.",
"deleteKeyConfirmationTitle": "Remove API key ${keyId}?",
"fullAccess": "Full Access",
"getToken": "Get A Token",
"name": "Name",
"nameDesc": "What would you like to call this API token? ",
@ -28,7 +29,6 @@
"permName": "Permission Name",
"perms": "Permissions",
"server": "Server: ",
"superUser": "Super User",
"yes": "Yes"
},
"base": {
@ -177,11 +177,14 @@
},
"thousands": ",",
"zeroRecords": "No matching records found"
}
},
"loadingRecords": "Loading..."
},
"error": {
"agree": "Agree",
"bedrockError": "Bedrock downloads unavailable. Please check",
"bigBucket1": "Big Bucket Health Check Failed. Please check",
"bigBucket2": "for the most up to date information.",
"cancel": "Cancel",
"contact": "Contact Crafty Control Support via Discord",
"craftyStatus": "Crafty's status page",
@ -203,8 +206,8 @@
"not-downloaded": "We can't seem to find your executable file. Has it finished downloading? Are the permissions set to executable?",
"portReminder": "We have detected this is the first time {} has been run. Make sure to forward port {} through your router/firewall to make this remotely accessible from the internet.",
"privMsg": "and the ",
"serverJars1": "Server JARs API unreachable. Please check",
"serverJars2": "for the most up to date information.",
"return": "Return to Dashboard",
"selfHost": "If you are self-hosting this repo please check your address or consult our troubleshooting guide.",
"start-error": "Server {} failed to start with error code: {}",
"superError": "You must be a super user to complete this action.",
"terribleFailure": "What a Terrible Failure!"
@ -607,6 +610,7 @@
},
"startup": {
"almost": "Finishing up. Hang on tight...",
"cache": "Refreshing Big Bucket cache file",
"internals": "Configuring and starting Crafty's internal components",
"internet": "Checking for internet connection",
"server": "Initializing ",

View File

@ -20,6 +20,7 @@
"created": "Creado",
"deleteKeyConfirmation": "¿Quieres eliminar esta clave de API? Esto no se puede deshacer.",
"deleteKeyConfirmationTitle": "¿Eliminar la clave API ${keyId}?",
"fullAccess": "Acceso completo",
"getToken": "Conseguir un Token",
"name": "Nombre",
"nameDesc": "¿Como te gustaría llamar a este Token de API? ",
@ -177,11 +178,14 @@
},
"thousands": ",",
"zeroRecords": "No se encontraron registros que coincidan"
}
},
"loadingRecords": "Cargando..."
},
"error": {
"agree": "Aceptar",
"bedrockError": "Descargas de Bedrock no disponibles. por favor, compruebe",
"bigBucket1": "La verificación de estado de Big Bucket ha fallado. Por favor, verifica",
"bigBucket2": "para obtener la información más actualizada.",
"cancel": "Cancelar",
"contact": "Contacta el soporte de Crafty Control desde Discord",
"craftyStatus": "Página de estados de Crafty",
@ -203,6 +207,8 @@
"not-downloaded": "No podemos encontrar el archivo ejecutable. ¿Ha terminado de descargarse? ¿Están los permisos puestos como ejecutable?",
"portReminder": "Detectamos que es la primera vez que se inicia {}. Asegúrese de configurar el puerto {} a través de su router/firewall para hacer el servidor accesible por Internet.",
"privMsg": "y el ",
"return": "Volver al panel de control",
"selfHost": "Si estás autoalojando este repositorio, revisa tu dirección o consulta nuestra guía de solución de problemas.",
"serverJars1": "API de Servidor JAR no disponible. por favor, compruebe",
"serverJars2": "para la información más actualizada.",
"start-error": "Servidor {} fallo al iniciar con código de error: {}",
@ -600,6 +606,7 @@
},
"startup": {
"almost": "Terminando. Espera un momento...",
"cache": "Actualizando el archivo de caché de Big Bucket",
"internals": "Configurando e inicializando los componentes internos de Crafty",
"internet": "Verificando conexion a internet",
"server": "Inicializando ",

View File

@ -100,6 +100,7 @@
"welcome": "Tervetuloa Crafty Controller"
},
"datatables": {
"loadingRecords": "Ladataan...",
"i18n": {
"aria": {
"sortAscending": ": lajittele sarake nousevasti",

View File

@ -20,6 +20,7 @@
"created": "Crée",
"deleteKeyConfirmation": "Es-tu sûr de vouloir supprimer cette clé API? Tu ne pourras plus revenir en arrière.",
"deleteKeyConfirmationTitle": "Supprimer la clé API ${keyId}?",
"fullAccess": "Accès Complet",
"getToken": "Obtenir un Jeton",
"name": "Nom",
"nameDesc": "Comment appeler ce Jeton d'API ? ",
@ -177,11 +178,14 @@
},
"thousands": ",",
"zeroRecords": "Aucun enregistrement correspondant trouvcé"
}
},
"loadingRecords": "Chargement ..."
},
"error": {
"agree": "Agree",
"bedrockError": "Téléchargement Bedrock non disponible. Merci de vérifier",
"bigBucket1": "Echec de vérification de l'état de Big Bucket. Veuillez vérifier",
"bigBucket2": " pour l'information la plus à jour.",
"cancel": "Annuler",
"contact": "Contacter le Support de Crafty Control via Discord",
"craftyStatus": "Page de statut de Crafty",
@ -203,6 +207,8 @@
"not-downloaded": "Nous ne parvenons pas à trouver le fichier exécutable. A-t-il fini de Télécharger ? Les permissions permettent elles l'exécution ?",
"portReminder": "Nous avons détecté que c'est la première fois que {} est exécuté. Assurez-vous de transférer le port {} via votre routeur/pare-feu pour le rendre accessible à distance depuis Internet.",
"privMsg": "et le ",
"return": "Revenir au Tableau de Bord",
"selfHost": "Si vous hébergez vous-même ce repo, veuillez vérifier votre adresse et votre guide de dépannage.",
"serverJars1": "l'API Server JARs est inaccessible. Merci de vérifier",
"serverJars2": "pour les informations les plus à jour.",
"start-error": "Le serveur {} n'a pas pu démarrer avec le code d'erreur : {}",
@ -600,6 +606,7 @@
},
"startup": {
"almost": "Finalisation. Patienter ...",
"cache": "Mise à jour du fichier cache de Big Bucket",
"internals": "Configuration et Démarrage des composants internes de Crafty",
"internet": "Vérification de la connexion à Internet",
"server": "Initialisation ",

View File

@ -99,6 +99,7 @@
"welcome": "Wolkom by Crafty Controller"
},
"datatables": {
"loadingRecords": "Laden...",
"i18n": {
"aria": {
"sortAscending": ": aktivearje om kolom oprinnend te sortearjen",

View File

@ -20,6 +20,7 @@
"created": "נוצר",
"deleteKeyConfirmation": "האם ברצונך למחוק מפתח API זה? אי אפשר לבטל את זה.",
"deleteKeyConfirmationTitle": "? ${keyId} API-להסיר את מפתח ה",
"fullAccess": "גישה מלאה להכל",
"getToken": "קבלת אסימון",
"name": "שם",
"nameDesc": "הזה API-איך תרצו לקרוא לאסימון ה",
@ -177,7 +178,8 @@
},
"thousands": ",",
"zeroRecords": "לא נמצאו תוצאות תואמות"
}
},
"loadingRecords": "...טוען"
},
"error": {
"agree": "מסכים",
@ -203,6 +205,7 @@
"not-downloaded": "לא הצלחנו למצוא את קובץ ההפעלה שלך. האם זה סיים להוריד? האם ההרשאות מוגדרות בשביל הפעלה?",
"portReminder": "זיהינו שזו הפעם הראשונה ש-{} מופעל. הקפידו להעביר את היציאה {} דרך הנתב/חומת האש שלכם כדי להפוך אותה לנגישה מרחוק מהאינטרנט.",
"privMsg": "וה",
"return": "חזרה לפאנל",
"serverJars1": "API של צנצנות השרת אינו נגיש. אנא בדוק",
"serverJars2": "למידע מעודכן ביותר.",
"start-error": "השרת {} לא הצליח להתחיל עם קוד שגיאה: {}",

View File

@ -99,6 +99,7 @@
"welcome": "Dobrodošli u Crafty Controller"
},
"datatables": {
"loadingRecords": "Učitavanje...",
"i18n": {
"aria": {
"sortAscending": ": aktiviraj za sortiranje stupca uzlazno",

View File

@ -100,6 +100,7 @@
"welcome": "Selamat Datang Di Crafty Controller"
},
"datatables": {
"loadingRecords": "Loading...",
"i18n": {
"aria": {
"sortAscending": ": aktifkan untuk mengurutkan kolom menaik",

View File

@ -20,6 +20,7 @@
"created": "Creato",
"deleteKeyConfirmation": "Vuoi cancellare questa chiave API? Non puoi tornare indietro.",
"deleteKeyConfirmationTitle": "Rimuovere la chiave API ${keyId}?",
"fullAccess": " Accesso completo",
"getToken": "Prendi un Token",
"name": "Nome",
"nameDesc": "Come desideri chiamare questo Token API? ",
@ -177,11 +178,14 @@
},
"thousands": ",",
"zeroRecords": "Nessun record corrispondente trovato"
}
},
"loadingRecords": "Carico..."
},
"error": {
"agree": "Conferma",
"bedrockError": "I download di Bedrock non sono disponibili. Si prega di controllare",
"bigBucket1": "Controllo integrità di Big Bucket fallito. Controlla nuovamente",
"bigBucket2": "per informazioni più aggiornate.",
"cancel": "Annulla",
"contact": "Contact Crafty Control Support via Discord",
"craftyStatus": "Stato di Crafty",
@ -203,6 +207,8 @@
"not-downloaded": "We can't seem to find your executable file. Has it finished downloading? Are the permissions set to executable?",
"portReminder": "We have detected this is the first time {} has been run. Make sure to forward port {} through your router/firewall to make this remotely accessible from the internet.",
"privMsg": "e il ",
"return": "Torna alla pagina iniziale",
"selfHost": "se stai ospitando te questo repo, controlla il tuo indirizzo o consulta la nostra guida di risoluzione dei problemi.",
"serverJars1": "API JAR del server non raggiungibile. Si prega di controllare",
"serverJars2": "per informazioni più aggiornate.",
"start-error": "Server {} failed to start with error code: {}",
@ -600,6 +606,7 @@
},
"startup": {
"almost": "Finalizzazione. Tieniti forte...",
"cache": "Aggiornamento cache di Big Bucket",
"internals": "Configurazione e avvio dei componenti interni di Crafty",
"internet": "Controllo connessione a internet",
"server": "Inizializzazione ",

View File

@ -20,6 +20,7 @@
"created": "CREATED",
"deleteKeyConfirmation": "U SURE U WANTZ TO DELETE DIS? CAN'T UNDO!",
"deleteKeyConfirmationTitle": "I CAN EATZ IT??? : ${keyId}?",
"fullAccess": "All da Doors Open",
"getToken": "GIT TOKEN",
"name": "NAME",
"nameDesc": "WUT WUD U LIEK 2 CALL DIS API TOKEN? ",
@ -177,11 +178,14 @@
},
"thousands": ",",
"zeroRecords": "No matching records found"
}
},
"loadingRecords": "Loading..."
},
"error": {
"agree": "YESH PLS",
"bedrockError": "BEDROCKZ DOWNLOADZ NO WORKY. CHECK PLZ.",
"bigBucket1": "Big Bucket No Worky. Plz check",
"bigBucket2": "for da freshest nooz.",
"cancel": "NOPEZ",
"contact": "CONTACK CWAFTY CONTROLLR SUPORT ON DA DIZORD",
"craftyStatus": "CWAFTY'S STATUSZ",
@ -203,6 +207,8 @@
"not-downloaded": "SOZ BUT I FAILDZ CAN'T SEEM TO FINDZ YOUR FISH. PLZ GIB MEZ IT. I HUNGRY.",
"portReminder": "WE HAS DETECTD DIS AR TEH FURST TIEM {} IZ BEAN RUN. IF U WANTS IT ACESIBLE TO NEIGHBORHOOD CATS PLZ UNLOCK CAT_FLAP, {}, THRU UR ROUTR IF U HAS NOT DUN SO.",
"privMsg": "AND THEEZ ",
"return": "Go Bak to Dashbored",
"selfHost": "If u iz self-hostin' dis repo, check ur addy or peep our fix-it guide.",
"serverJars1": "CAN'T TALK TO SERVER JARS API. CHECKZ",
"serverJars2": "TO SEE NEWZ STUFFZ.",
"start-error": "CHAIR {} FAILD 2 START WIF OOF CODE: {}",
@ -600,6 +606,7 @@
},
"startup": {
"almost": "ALMOST DUN. HOLD ON TO YER WHISKERS...",
"cache": "Fightin' for a Big Bucket o Fish",
"internals": "SETTIN' UP AN' STARTIN' CWAFTY'S INSIDE BITZ",
"internet": "LOOKIN' FOR OUTER SPACE TALKY",
"server": "WAKIN' UPZ ",

View File

@ -20,6 +20,7 @@
"created": "Izveidots",
"deleteKeyConfirmation": "Vai vēlies dzēst šo API atslēgu? Šo nevar atdarīt.",
"deleteKeyConfirmationTitle": "Noņemt API atslēgu ${keyId}?",
"fullAccess": "Pilna piekļuve",
"getToken": "Saņemt Pilnvaru (Token)",
"name": "Nosaukums",
"nameDesc": "Kā jūs vēlaties nosaukt šo Pilnvaru (Token)? ",
@ -178,11 +179,14 @@
},
"thousands": ",",
"zeroRecords": "Nav atrasti atbilstoši ieraksti"
}
},
"loadingRecords": "Ielādē..."
},
"error": {
"agree": "Piekrītu",
"bedrockError": "Bedrock lejupielādes nav pieejamas. Lūdzu pārbaudi",
"bigBucket1": "Big Bucket stāvokļa pārbaude neizdevās. Lūdzu izpētiet",
"bigBucket2": "priekš jaunākās informācijas.",
"cancel": "Atcelt",
"contact": "Sazinies ar Crafty Control Atbalstu izmantojot Discord",
"craftyStatus": "Crafty statusa lapa",
@ -204,6 +208,8 @@
"not-downloaded": "Mēs nevaram atrast jūsu izpildāmo failu. Vai tas ir beidzis lejupielādēties? Vai tā peikļuves ir uzstādītas kā palaižamas?",
"portReminder": "Mēs noteicām ka šī ir pirmā reize, kad {} ir ticis palaists. Pārliecinies izlaist portu {} cauri savam rūterim/ugunsmūrim lai padarītu šo attāli pieejamu no interneta.",
"privMsg": "un ",
"return": "Atgriezties uz pārskatu",
"selfHost": "Ja jūs paši uzturat šo repozitoriju, pārbaudiet savu adresi vai apskatiet mūsu kļūdu novēršanas dokumentāciju.",
"serverJars1": "Serveru JAR API nav sasniedzams. Lūdzu pārbaudiet",
"serverJars2": "priekš jaunākās informācijas.",
"start-error": "Serveris {} neveiskmīgi startējās ar kļūdas kodu: {}",
@ -601,6 +607,7 @@
},
"startup": {
"almost": "Pabeidz. Vēl tik nedaudz...",
"cache": "Atjauno Big Bucket keša failu",
"internals": "Konfigurē un Startē Crafty iekšējās komponenetes",
"internet": "Pārbauda interneta savienojumu",
"server": "Inicializē ",

View File

@ -20,6 +20,7 @@
"created": "Gecreëerd",
"deleteKeyConfirmation": "Wilt u deze API sleutel verwijderen? Dit kan niet ongedaan gemaakt worden.",
"deleteKeyConfirmationTitle": "API sleutel verwijderen ${keyId}?",
"fullAccess": "Volledige toegang",
"getToken": "Verkrijg een Token",
"name": "Naam",
"nameDesc": "Hoe wilt u dit API token noemen? ",
@ -177,11 +178,14 @@
},
"thousands": ",",
"zeroRecords": "Geen overeenkomende records gevonden"
}
},
"loadingRecords": "Bezig met laden..."
},
"error": {
"agree": "Akkoord",
"bedrockError": "Bedrock-downloads niet beschikbaar. Controleer alstublieft",
"bigBucket1": "Big Bucket-gezondheidscontrole mislukt. Controleer alstublieft",
"bigBucket2": "voor de meest recente informatie.",
"cancel": "Annuleren",
"contact": "Neem contact op met Crafty Control ondersteuning via Discord",
"craftyStatus": "Crafty's statuspagina",
@ -203,6 +207,8 @@
"not-downloaded": "We kunnen uw uitvoerbare bestand niet vinden. Is het klaar met downloaden? Zijn de rechten ingesteld op uitvoerbaar?",
"portReminder": "We hebben ontdekt dat dit de eerste keer is dat {} wordt uitgevoerd. Zorg ervoor dat u poort {} doorstuurt via uw router/firewall om deze op afstand toegankelijk te maken vanaf het internet.",
"privMsg": "en de ",
"return": "Terug naar Dashboard",
"selfHost": "Als u deze repository zelf host, controleer dan uw adres of raadpleeg onze handleiding voor probleemoplossing.",
"serverJars1": "Server JARs API niet bereikbaar. Controleer alstublieft",
"serverJars2": "voor de meest recente informatie.",
"start-error": "Server {} kan niet starten met foutcode: {}",
@ -600,6 +606,7 @@
},
"startup": {
"almost": "De laatste hand leggen. Houd je vast...",
"cache": "Big Bucket-cachebestand vernieuwen",
"internals": "Crafty's interne componenten configureren en starten",
"internet": "Controleren op internetverbinding",
"server": "Initialiseren ",

View File

@ -99,6 +99,7 @@
"welcome": "Welkom bij Crafty Controller"
},
"datatables": {
"loadingRecords": "Laden...",
"i18n": {
"aria": {
"sortAscending": ": activeer om kolom oplopend te sorteren",

View File

@ -20,6 +20,7 @@
"created": "Stworzono",
"deleteKeyConfirmation": "Czy chcesz usunąć ten klucz API? Nie można tego cofnąć.",
"deleteKeyConfirmationTitle": "Usunąć Klucz API ${keyId}?",
"fullAccess": "Pełny dostęp",
"getToken": "Zdobądź token",
"name": "Nazwa",
"nameDesc": "Jak chcesz nazwać ten klucz API? ",
@ -177,11 +178,14 @@
},
"thousands": ",",
"zeroRecords": "Nie znaleziono pasujacego wyniku"
}
},
"loadingRecords": "Wczytywanie..."
},
"error": {
"agree": "Zgadzam się",
"bedrockError": "Pobieranie serwera bedrock jest teraz niedostępne. Proszę sprawdź",
"bigBucket1": "Odświeżanie Big Bucket nie powiodło się. Sprawdź",
"bigBucket2": "dla najnowszych informacji.",
"cancel": "Anuluj",
"contact": "Podrzebujesz pomocy? Zapraszamy na serwer discord Crafty Controler",
"craftyStatus": "Status strony Craftyiego",
@ -203,6 +207,8 @@
"not-downloaded": "Nie możemy znaleść twojego pliku serwera. Czy skończył się pobierać? Czy permisje są ustawione na wykonywanle?",
"portReminder": "Zauważyliśmy że to jest pierwszy raz {} kiedy był włączony. Upewnij się że otworzyłeś port {} na swoim routerze/firewallu aby korzystać z tego poza domem.",
"privMsg": "i także ",
"return": "Powrót do panelu",
"selfHost": "Jeśli zarządasz tą repozytorią upewnij się że adres hest poprawny, w innym przypadku odwiedź strone rozwiązywania problemów.",
"serverJars1": "API Server Jars jest niedostępne. Proszę sprawdź",
"serverJars2": "dla najnowzsych informacji.",
"start-error": "Serwer {} nie mógł się odpalić z powodu: {}",
@ -599,6 +605,7 @@
},
"startup": {
"almost": "Prawie gotowe! Jeszcze tylko chwilka...",
"cache": "Odświeżanie pamięci podręcznej Big Bucket",
"internals": "Konfigurowanie i włączanie backendu...",
"internet": "Sprawdzam połączenie z internetem",
"server": "Włączanie ",

View File

@ -100,6 +100,7 @@
"welcome": "Bem-vindo ao Crafty Controller"
},
"datatables": {
"loadingRecords": "Carregando...",
"i18n": {
"aria": {
"sortAscending": ": ative para ordenar a coluna de forma ascendente",

View File

@ -20,6 +20,7 @@
"created": "สร้างเมื่อ",
"deleteKeyConfirmation": "คุณต้องการลบคีย์ API นี้หรือไม่ สิ่งนี้ไม่สามารถยกเลิกได้",
"deleteKeyConfirmationTitle": "ลบคีย์ API นี้ ${keyId} หรือไม่?",
"fullAccess": "เข้าถึงได้ทั้งหมด",
"getToken": "แสดงโทเค็น",
"name": "ชื่อ",
"nameDesc": "คุณต้องการเรียกโทเค็น API นี้ว่าอะไร ? ",
@ -177,7 +178,8 @@
},
"thousands": ",",
"zeroRecords": "ไม่พบรายการที่ตรงกัน"
}
},
"loadingRecords": "กำลังโหลด..."
},
"error": {
"agree": "ยอมรับ",
@ -203,6 +205,7 @@
"not-downloaded": "ดูเหมือนว่าเราจะไม่พบแฟ้มกระทำการของคุณ (.jar) ตรวจสอบให้แน่ใจว่าการดาวโหลดน์เสร็จสิ้นแล้ว, การอนุญาตถูกตั้งไปยังแฟ้มกระทำการหรือไม่?",
"portReminder": "เราตรวจพบว่านี่เป็นครั้งแรกที่มีการเรียกใช้ {} ตรวจสอบให้แน่ใจว่าได้ Forward port {} ผ่านเราเตอร์/ไฟร์วอลล์ของคุณเพื่อให้สามารถเข้าถึงได้จากอินเทอร์เน็ตจากระยะไกล",
"privMsg": "และ ",
"return": "ย้อนกลับไปยังแผงควบคุม",
"serverJars1": "ไม่สามารถเข้าถึงเซิร์ฟเวอร์ JARs API กรุณาตรวจสอบ",
"serverJars2": "เพื่อข้อมูลที่ทันสมัยที่สุด",
"start-error": "เซิร์ฟเวอร์ {} ไม่สามารถเริ่มต้นได้เนื่องจากรหัสข้อผิดพลาด: {}",

View File

@ -20,6 +20,7 @@
"created": "Oluşturuldu",
"deleteKeyConfirmation": "Bu API anahtarını silmek istediğine emin misin? Bu geri alınamaz.",
"deleteKeyConfirmationTitle": "${keyId} API anahtarını kaldırma işlemi.",
"fullAccess": "Tam Erişim",
"getToken": "Bir Token Al",
"name": "Ad",
"nameDesc": "Bu API tokeninin adı ne olsun?",
@ -177,11 +178,14 @@
},
"thousands": ".",
"zeroRecords": "Eşleşen kayıt bulunamadı"
}
},
"loadingRecords": "Yükleniyor..."
},
"error": {
"agree": "Kabul Et",
"bedrockError": "Bedrock indirmeleri kullanılamıyor. Lütfen kontrol edin",
"bigBucket1": "Big Bucket sağlık kontrolü yapılamadı. En güncel bilgileri burada bulabilirsiniz:",
"bigBucket2": ".",
"cancel": "İptal",
"contact": "Crafty Control Destek Discord Sunucusu",
"craftyStatus": "Crafty'nin durum sayfası",
@ -203,6 +207,8 @@
"not-downloaded": "Çalıştırılabilir dosyanızı bulamıyoruz. İndirme işlemi tamamlandı mı? İzinler çalıştırılabilir olarak ayarlandı mı?",
"portReminder": "{} ilk kez çalıştırılıyor olduğunu tespit ettik. Bunu internetten uzaktan erişilebilir kılmak için {} bağlantı noktasını yönlendiriciniz/güvenlik duvarınız üzerinden ilettiğinizden emin olun.",
"privMsg": "ve ",
"return": "Arayüze Geri Dön",
"selfHost": "Bu depoyu kendiniz barındırıyorsanız lütfen adresinizi kontrol ediniz veya sorun giderme kılavuzumuza bakınız.",
"serverJars1": "Sunucu JARs API'ına erişilemiyor.",
"serverJars2": "en güncel bilgilere sahiptir",
"start-error": "{} sunucusu başlamatılamadı. Hata kodu: {}",
@ -599,6 +605,7 @@
},
"startup": {
"almost": "Bitiriliyor. Sıkı tutun...",
"cache": "Big Bucket önbellek dosyası yenileniyor",
"internals": "Crafty'nin dahili bileşenlerini konfigüre etme ve başlatma",
"internet": "İnternet bağlantısı kontrol ediliyor",
"server": "Başlatılıyor ",

View File

@ -20,6 +20,7 @@
"created": "Створений",
"deleteKeyConfirmation": "Ви дійсно бажаєте видалити API ключ? Це незворотня дія.",
"deleteKeyConfirmationTitle": "Видалення API ключ ${keyId}?",
"fullAccess": "Повний доступ",
"getToken": "Отримати Токен",
"name": "Ім'я",
"nameDesc": "Як ви хочете назвати даний API токен?",
@ -177,11 +178,14 @@
},
"thousands": ",",
"zeroRecords": "Не знайдено збігів в базі"
}
},
"loadingRecords": "Завантаження..."
},
"error": {
"agree": "Згодний",
"bedrockError": "Bedrock недоступний для скачування. Перевірте будь ласка",
"bigBucket1": "Перевірка Big Bucket не пройдено. Будь ласка, перевірте",
"bigBucket2": "для найактуальнішої інформації.",
"cancel": "Відміна",
"contact": "Зв'язатись з Crafty Control підтримкою через Discord",
"craftyStatus": "Crafty's статус",
@ -203,6 +207,8 @@
"not-downloaded": "Здається, ми не можемо знайти ваш виконуваний файл. Чи завершилось завантаження? Чи встановлено дозволи на виконуваний файл?",
"portReminder": "Ми виявили це вперше {} був запущений. Обов’язково перенаправте порт {} через ваш маршрутизатор/брандмауер, щоб зробити це доступним з Інтернету.",
"privMsg": "і ",
"return": "Повернутись до панелі",
"selfHost": "Якщо ви самостійно розміщуєте цей репозеторій, перевірте свою адресу або зверніться до нашого посібника з усунення несправностей.",
"serverJars1": "API сервера JAR недоступний. Будь ласка, перевірте",
"serverJars2": "для найактуальнішої інформації.",
"start-error": "Сервер {} не запустився через помилку: {}",
@ -599,6 +605,7 @@
},
"startup": {
"almost": "Закінчуємо. Тримайся міцніше...",
"cache": "Оновлення файлу кешу Big Bucket",
"internals": "Налаштування та запуск внутрішніх компонентів Crafty ",
"internet": "Перевірка доступу до інтернету",
"server": "Ініціалізація ",

View File

@ -20,6 +20,7 @@
"created": "创建时间",
"deleteKeyConfirmation": "您想要删除这个 API 密钥吗?此操作不能撤销。",
"deleteKeyConfirmationTitle": "删除 API 密钥 ${keyId}",
"fullAccess": "完全访问",
"getToken": "获得一个令牌",
"name": "名称",
"nameDesc": "你想把这个 API 令牌叫做什么?",
@ -177,11 +178,14 @@
},
"thousands": ",",
"zeroRecords": "没有找到匹配的记录"
}
},
"loadingRecords": "正在加载……"
},
"error": {
"agree": "同意",
"bedrockError": "基岩版下载不可用。请检查",
"bigBucket1": "Big Bucket 查活失败。请检查",
"bigBucket2": "以获取最新信息。",
"cancel": "取消",
"contact": "通过 Discord 联系 Crafty Control 支持",
"craftyStatus": "Crafty 的状态页面",
@ -203,6 +207,8 @@
"not-downloaded": "我们似乎找不到您的可执行文件。它下载完成了吗?可执行文件的权限设置正确了吗?",
"portReminder": "我们检测到这是你首次运行 {}。请确保从您的路由器/防火墙转发 {} 端口,以使程序可以从公网远程访问。",
"privMsg": "以及",
"return": "返回仪表板",
"selfHost": "如果您自托管此仓库,请检查您的地址或参考我们的故障排除指南。",
"serverJars1": "无法访问服务器 JAR API。请检查",
"serverJars2": "以获取最新信息。",
"start-error": "服务器 {} 启动失败,错误代码为:{}",
@ -600,6 +606,7 @@
},
"startup": {
"almost": "即将完成。请稍候……",
"cache": "正在刷新 Big Bucket 缓存文件",
"internals": "正在配置并启动 Crafty 的内部组件",
"internet": "正在检查网络连接",
"server": "正在初始化 ",

14
main.py
View File

@ -17,6 +17,7 @@ from app.classes.models.users import HelperUsers
from app.classes.models.management import HelpersManagement
from app.classes.shared.import_helper import ImportHelpers
from app.classes.shared.websocket_manager import WebSocketManager
from app.classes.logging.log_formatter import JsonFormatter
console = Console()
helper = Helpers()
@ -117,7 +118,7 @@ def controller_setup():
def tasks_starter():
"""
Method starts stats recording, app scheduler, and
serverjars/steamCMD cache refreshers
big bucket/steamCMD cache refreshers
"""
# start stats logging
tasks_manager.start_stats_recording()
@ -127,8 +128,8 @@ def tasks_starter():
tasks_manager.start_scheduler()
# refresh our cache and schedule for every 12 hoursour cache refresh
# for serverjars.com
tasks_manager.serverjar_cache_refresher()
# for big bucket.com
tasks_manager.big_bucket_cache_refresher()
def signal_handler(signum, _frame):
@ -212,6 +213,8 @@ def setup_starter():
time.sleep(2)
controller_setup_thread.start()
web_sock.broadcast("update", {"section": "cache"})
controller.big_bucket.manual_refresh_cache()
# Wait for the setup threads to finish
web_sock.broadcast(
"update",
@ -284,6 +287,11 @@ def setup_logging(debug=True):
logging.config.dictConfig(logging_config)
# Apply JSON formatting to the "audit" handler
for handler in logging.getLogger().handlers:
if handler.name == "audit_log_handler":
handler.setFormatter(JsonFormatter())
else:
logging.basicConfig(level=logging.DEBUG)
logging.warning(f"Unable to read logging config from {logging_config_file}")

View File

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