Merge branch 'dev' into dev-StatsDBImprovement

This commit is contained in:
Silversthorn 2022-05-26 15:39:52 +02:00
commit 99b28efd33
41 changed files with 1079 additions and 381 deletions

View File

@ -5,6 +5,7 @@
---
stages:
- lint
- test
- prod-deployment
- dev-deployment
@ -16,7 +17,7 @@ yamllint:
stage: lint
image: registry.gitlab.com/pipeline-components/yamllint:latest
tags:
- "docker"
- docker
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: "$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS"
@ -28,7 +29,7 @@ jsonlint:
stage: lint
image: registry.gitlab.com/pipeline-components/jsonlint:latest
tags:
- "docker"
- docker
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: "$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS"
@ -42,7 +43,7 @@ black:
stage: lint
image: registry.gitlab.com/pipeline-components/black:latest
tags:
- "docker"
- docker
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: "$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS"
@ -54,7 +55,7 @@ pylint:
stage: lint
image: registry.gitlab.com/pipeline-components/pylint:latest
tags:
- "docker"
- docker
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: "$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS"
@ -84,7 +85,7 @@ docker-build-dev:
- name: docker:dind
stage: dev-deployment
tags:
- "docker_priv"
- docker_priv
rules:
- if: $CI_COMMIT_BRANCH == 'dev'
environment:
@ -139,7 +140,7 @@ docker-build-prod:
- name: docker:dind
stage: prod-deployment
tags:
- "docker_priv"
- docker_priv
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
environment:
@ -269,3 +270,31 @@ win-prod-build:
- .\crafty_commander.exe
exclude:
- app\classes\**\*
sast:
variables:
SAST_EXCLUDED_PATHS: spec, test, tests, tmp, migrations, vendors
SAST_BANDIT_EXCLUDED_PATHS: "'*/migrations/*, */vendors/*'"
SAST_EXCLUDED_ANALYZERS: semgrep
stage: test
tags:
- docker
secret_detection:
variables:
SECRET_DETECTION_EXCLUDED_PATHS: migrations, vendors
tags:
- docker
gemnasium-dependency_scanning:
tags:
- docker
gemnasium-python-dependency_scanning:
tags:
- docker
include:
- template: Security/Dependency-Scanning.gitlab-ci.yml
- template: Security/SAST.gitlab-ci.yml
- template: Security/Secret-Detection.gitlab-ci.yml

View File

@ -443,7 +443,7 @@ ignored-classes=optparse.Values,thread._local,_thread._local
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
ignored-modules=jsonschema,orjson
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.

View File

@ -328,7 +328,7 @@ class ServersController(metaclass=Singleton):
server_data.append(
{
"server_data": server,
"stats": DatabaseShortcuts.return_rows(latest)[0],
"stats": latest,
"user_command_permission": user_command_permission,
}
)

View File

@ -11,6 +11,11 @@ logger = logging.getLogger(__name__)
class UsersController:
class ApiPermissionDict(t.TypedDict):
name: str
quantity: int
enabled: bool
def __init__(self, helper, users_helper, authentication):
self.helper = helper
self.users_helper = users_helper

View File

@ -1,4 +1,5 @@
import logging
import typing as t
from enum import Enum
from peewee import (
ForeignKeyField,
@ -99,7 +100,7 @@ class PermissionsCrafty:
try:
user_crafty = UserCrafty.get(UserCrafty.user_id == user_id)
except DoesNotExist:
user_crafty = UserCrafty.insert(
UserCrafty.insert(
{
UserCrafty.user_id: user_id,
UserCrafty.permissions: "000",
@ -114,6 +115,13 @@ class PermissionsCrafty:
user_crafty = PermissionsCrafty.get_user_crafty(user_id)
return user_crafty
@staticmethod
def get_user_crafty_optional(user_id) -> t.Optional[UserCrafty]:
try:
return UserCrafty.get(UserCrafty.user_id == user_id)
except DoesNotExist:
return None
@staticmethod
def add_user_crafty(user_id, uc_permissions):
user_crafty = UserCrafty.insert(

View File

@ -66,10 +66,9 @@ class HelperRoles:
@staticmethod
def get_role_column(role_id: t.Union[str, int], column_name: str) -> t.Any:
column = getattr(Roles, column_name)
return model_to_dict(
Roles.select(column).where(Roles.role_id == role_id).get(),
only=[column],
)[column_name]
return getattr(
Roles.select(column).where(Roles.role_id == role_id).get(), column_name
)
@staticmethod
def add_role(role_name):

View File

@ -205,7 +205,7 @@ class PermissionsServers:
@staticmethod
def get_user_permissions_mask(user: Users, server_id: str):
if user.superuser:
permissions_mask = "1" * len(PermissionsServers.get_permissions_list())
permissions_mask = "1" * len(EnumPermissionsServer)
else:
roles_list = HelperUsers.get_user_roles_id(user.user_id)
role_server = (
@ -217,7 +217,7 @@ class PermissionsServers:
try:
permissions_mask = role_server[0].permissions
except IndexError:
permissions_mask = "0" * len(PermissionsServers.get_permissions_list())
permissions_mask = "0" * len(EnumPermissionsServer)
return permissions_mask
@staticmethod

View File

@ -1,6 +1,9 @@
import os
import logging
import datetime
import typing as t
from playhouse.shortcuts import model_to_dict
import typing as t
@ -10,14 +13,12 @@ from playhouse.shortcuts import model_to_dict
from app.classes.models.servers import Servers, HelperServers
from app.classes.shared.helpers import Helpers
from app.classes.shared.main_models import DatabaseShortcuts
from app.classes.shared.migration import MigrationManager
try:
from peewee import (
SqliteDatabase,
Model,
DatabaseProxy,
ForeignKeyField,
CharField,
AutoField,
@ -25,6 +26,7 @@ try:
BooleanField,
IntegerField,
FloatField,
DoesNotExist,
)
except ModuleNotFoundError as e:
@ -33,8 +35,6 @@ except ModuleNotFoundError as e:
logger = logging.getLogger(__name__)
peewee_logger = logging.getLogger("peewee")
peewee_logger.setLevel(logging.INFO)
# database_stats_proxy = DatabaseProxy()
# **********************************************************************************
# Servers Stats Class
@ -65,7 +65,6 @@ class ServerStats(Model):
class Meta:
table_name = "server_stats"
# database = database_stats_proxy
# **********************************************************************************
@ -102,14 +101,13 @@ class HelperServerStats:
f"{helper_stats.migration_dir}", "stats"
)
helper_stats.db_path = db_file
# database_stats_proxy.initialize(self.database)
migration_manager = MigrationManager(self.database, helper_stats)
migration_manager.up() # Automatically runs migrations
# database_stats_proxy.initialize(self.database)
except Exception as ex:
logger.warning(
f"Error try to look for the db_stats files for server : {ex}"
)
return None
def select_database(self):
try:
@ -122,29 +120,28 @@ class HelperServerStats:
self.database = SqliteDatabase(
db_file, pragmas={"journal_mode": "wal", "cache_size": -1024 * 10}
)
# database_stats_proxy.initialize(self.database)
except Exception as ex:
logger.warning(
f"Error try to look for the db_stats files for server : {ex}"
)
return None
def get_all_servers_stats(self):
servers = HelperServers.get_all_defined_servers()
server_data = []
try:
for s in servers:
# self.select_database(s.get("server_id"))
for server in servers:
latest = (
ServerStats.select()
.where(ServerStats.server_id == s.get("server_id"))
.where(ServerStats.server_id == server.get("server_id"))
.order_by(ServerStats.created.desc())
.limit(1)
)
latest._database = self.database
server_data.append(
{
"server_data": s,
"stats": DatabaseShortcuts.return_rows(latest)[0],
"server_data": server,
"stats": stats,
"user_command_permission": True,
}
)
@ -156,7 +153,6 @@ class HelperServerStats:
def insert_server_stats(self, server):
server_id = server.get("id", 0)
# self.select_database(server_id)
if server_id == 0:
logger.warning("Stats saving failed with error: Server unknown (id = 0)")
@ -172,7 +168,7 @@ class HelperServerStats:
ServerStats.mem_percent: server.get("mem_percent", 0),
ServerStats.world_name: server.get("world_name", ""),
ServerStats.world_size: server.get("world_size", ""),
ServerStats.server_port: server.get("server_port", ""),
ServerStats.server_port: server.get("server_port", 0),
ServerStats.int_ping_results: server.get("int_ping_results", False),
ServerStats.online: server.get("online", False),
ServerStats.max: server.get("max", False),
@ -202,13 +198,12 @@ class HelperServerStats:
return {}
def get_server_stats(self):
# self.select_database(self.server_id)
stats = (
ServerStats.select()
.where(ServerStats.server_id == self.server_id)
.order_by(ServerStats.created.desc())
.limit(1)
.get(self.database)
.first(self.database)
)
return model_to_dict(stats)
@ -261,11 +256,7 @@ class HelperServerStats:
.where(ServerStats.server_id == self.server_id)
.get(self.database)
)
# pylint: disable=singleton-comparison
if svr.crashed == True:
return True
else:
return False
return svr.crashed
def set_update(self, value):
if self.server_id is None:
@ -277,8 +268,9 @@ class HelperServerStats:
ServerStats.select().where(ServerStats.server_id == self.server_id).execute(
self.database
)
except Exception as ex:
except DoesNotExist as ex:
logger.error(f"Database entry not found! {ex}")
return
ServerStats.update(updating=value).where(
ServerStats.server_id == self.server_id
).execute(self.database)
@ -334,26 +326,24 @@ class HelperServerStats:
return last_stat.created - last_stat_with_player.created
def can_stop_no_players(self, time_limit):
# self.select_database(self.server_id)
can = False
ttl_no_players = self.get_ttl_without_player()
if (time_limit == -1) or (ttl_no_players > time_limit):
can = True
return can
return (time_limit == -1) or (ttl_no_players > time_limit)
def set_waiting_start(self, value):
# self.select_database(self.server_id)
try:
# Checks if server even exists
ServerStats.select().where(ServerStats.server_id == self.server_id)
except Exception as ex:
ServerStats.select().where(ServerStats.server_id == self.server_id).execute(
self.database
)
except DoesNotExist as ex:
logger.error(f"Database entry not found! {ex}")
return
ServerStats.update(waiting_start=value).where(
ServerStats.server_id == self.server_id
).execute(self.database)
def get_waiting_start(self):
# self.select_database(self.server_id)
waiting_start = (
ServerStats.select()
.where(ServerStats.server_id == self.server_id)

View File

@ -143,10 +143,10 @@ class HelperServers:
@staticmethod
def get_server_column(server_id: t.Union[str, int], column_name: str) -> t.Any:
column = getattr(Servers, column_name)
return model_to_dict(
return getattr(
Servers.select(column).where(Servers.server_id == server_id).get(),
only=[column],
)[column_name]
column_name,
)
# **********************************************************************************
# Servers Methods

View File

@ -165,19 +165,10 @@ class HelperUsers:
@staticmethod
def get_user_column(user_id: t.Union[str, int], column_name: str) -> t.Any:
column = getattr(Users, column_name)
return model_to_dict(
return getattr(
Users.select(column).where(Users.user_id == user_id).get(),
only=[column],
)[column_name]
@staticmethod
def check_system_user(user_id):
try:
result = Users.get(Users.user_id == user_id).user_id == user_id
if result:
return True
except:
return False
column_name,
)
@staticmethod
def get_user_model(user_id: str) -> Users:

View File

@ -65,10 +65,7 @@ class Controller:
@staticmethod
def check_system_user():
if HelperUsers.get_user_id_by_name("system") is not None:
return True
else:
return False
return HelperUsers.get_user_id_by_name("system") is not None
def set_project_root(self, root_dir):
self.project_root = root_dir
@ -218,12 +215,12 @@ class Controller:
+ ("" if empty else f"\nserver-port={port}")
)
server_file = "server.jar" # HACK: Throw this horrible default out of here
root_create_data = data[data["create_type"] + "_create_data"]
create_data = root_create_data[root_create_data["create_type"] + "_create_data"]
if data["create_type"] == "minecraft_java":
if root_create_data["create_type"] == "download_jar":
server_file = f"{create_data['type']}-{create_data['version']}.jar"
full_jar_path = os.path.join(new_server_path, server_file)
# Create an EULA file
with open(
@ -234,16 +231,18 @@ class Controller:
)
elif root_create_data["create_type"] == "import_server":
_copy_import_dir_files(create_data["existing_server_path"])
full_jar_path = os.path.join(new_server_path, create_data["jarfile"])
server_file = create_data["jarfile"]
elif root_create_data["create_type"] == "import_zip":
# TODO: Copy files from the zip file to the new server directory
full_jar_path = os.path.join(new_server_path, create_data["jarfile"])
server_file = create_data["jarfile"]
raise Exception("Not yet implemented")
_create_server_properties_if_needed(create_data["server_properties_port"])
min_mem = create_data["mem_min"]
max_mem = create_data["mem_max"]
full_jar_path = os.path.join(new_server_path, server_file)
def _gibs_to_mibs(gibs: float) -> str:
return str(int(gibs * 1024))
@ -273,7 +272,9 @@ class Controller:
_create_server_properties_if_needed(0, True)
server_command = create_data["command"]
server_file = ""
server_file = (
"./bedrock_server" # HACK: This is a hack to make the server start
)
elif data["create_type"] == "custom":
# TODO: working_directory, executable_update
if root_create_data["create_type"] == "raw_exec":
@ -293,7 +294,11 @@ class Controller:
_create_server_properties_if_needed(0, True)
server_command = create_data["command"]
server_file = root_create_data["executable_update"].get("file", "")
server_file_new = root_create_data["executable_update"].get("file", "")
if server_file_new != "":
# HACK: Horrible hack to make the server start
server_file = server_file_new
stop_command = data.get("stop_command", "")
if stop_command == "":
@ -303,7 +308,7 @@ class Controller:
log_location = data.get("log_location", "")
if log_location == "":
# TODO: different default log locations for server creation types
log_location = "/logs/latest.log"
log_location = "./logs/latest.log"
if data["monitoring_type"] == "minecraft_java":
monitoring_port = data["minecraft_java_monitoring_data"]["port"]
@ -315,7 +320,7 @@ class Controller:
monitoring_type = "minecraft-bedrock"
elif data["monitoring_type"] == "none":
# TODO: this needs to be NUKED..
# There shouldn't be anything set if there are nothing to monitor
# There shouldn't be anything set if there is nothing to monitor
monitoring_port = 25565
monitoring_host = "127.0.0.1"
monitoring_type = "minecraft-java"

View File

@ -714,6 +714,7 @@ class Server:
)
# cancel the watcher task
self.server_scheduler.remove_job("c_" + str(self.server_id))
self.server_scheduler.remove_job("stats_" + str(self.server_id))
return
self.stats_helper.sever_crashed()
@ -1143,6 +1144,7 @@ class Server:
"desc": raw_ping_result.get("desc"),
"version": raw_ping_result.get("version"),
"icon": raw_ping_result.get("icon"),
"crashed": self.is_crashed,
}
)
if len(self.helper.websocket_helper.clients) > 0:
@ -1167,6 +1169,7 @@ class Server:
"desc": raw_ping_result.get("desc"),
"version": raw_ping_result.get("version"),
"icon": raw_ping_result.get("icon"),
"crashed": self.is_crashed,
},
)
total_players += int(raw_ping_result.get("online"))

View File

@ -238,4 +238,4 @@ class BaseHandler(tornado.web.RequestHandler):
def finish_json(self, status: int, data: t.Dict[str, t.Any]):
self.set_status(status)
self.set_header("Content-Type", "application/json")
self.finish(orjson.dumps(data)) # pylint: disable=no-member
self.finish(orjson.dumps(data))

View File

@ -16,12 +16,9 @@ from tornado import iostream
# TZLocal is set as a hidden import on win pipeline
from tzlocal import get_localzone
from croniter import croniter
from app.classes.controllers.roles_controller import RolesController
from app.classes.models.roles import HelperRoles
from app.classes.models.server_permissions import (
EnumPermissionsServer,
PermissionsServers,
)
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.models.management import HelpersManagement
from app.classes.shared.helpers import Helpers
@ -40,8 +37,8 @@ class PanelHandler(BaseHandler):
user_roles[user_id] = user_roles_list
return user_roles
def get_role_servers(self) -> t.Set[int]:
servers = set()
def get_role_servers(self) -> t.List[RolesController.RoleServerJsonType]:
servers = []
for server in self.controller.servers.list_defined_servers():
argument = self.get_argument(f"server_{server['server_id']}_access", "0")
if argument == "0":
@ -57,7 +54,9 @@ class PanelHandler(BaseHandler):
permission_mask, permission, "1"
)
servers.add((server["server_id"], permission_mask))
servers.append(
{"server_id": server["server_id"], "permissions": permission_mask}
)
return servers
def get_perms_quantity(self) -> t.Tuple[str, dict]:
@ -2016,35 +2015,7 @@ class PanelHandler(BaseHandler):
servers = self.get_role_servers()
# TODO: use update_role_advanced when API v2 gets merged
base_data = self.controller.roles.get_role_with_servers(role_id)
server_ids = {server[0] for server in servers}
server_permissions_map = {server[0]: server[1] for server in servers}
added_servers = server_ids.difference(set(base_data["servers"]))
removed_servers = set(base_data["servers"]).difference(server_ids)
same_servers = server_ids.intersection(set(base_data["servers"]))
logger.debug(
f"role: {role_id} +server:{added_servers} -server{removed_servers}"
)
for server_id in added_servers:
PermissionsServers.get_or_create(
role_id, server_id, server_permissions_map[server_id]
)
for server_id in same_servers:
PermissionsServers.update_role_permission(
role_id, server_id, server_permissions_map[server_id]
)
if len(removed_servers) != 0:
PermissionsServers.delete_roles_permissions(role_id, removed_servers)
up_data = {
"role_name": role_name,
"last_update": Helpers.get_time_as_string(),
}
# TODO: do the last_update on the db side
HelperRoles.update_role(role_id, up_data)
self.controller.roles.update_role_advanced(role_id, role_name, servers)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
@ -2081,10 +2052,7 @@ class PanelHandler(BaseHandler):
servers = self.get_role_servers()
role_id = self.controller.roles.add_role(role_name)
# TODO: use add_role_advanced when API v2 gets merged
for server in servers:
PermissionsServers.get_or_create(role_id, server[0], server[1])
role_id = self.controller.roles.add_role_advanced(role_name, servers)
self.controller.management.add_to_audit_log(
exec_user["user_id"],

View File

@ -25,6 +25,9 @@ from app.classes.web.routes.api.servers.server.stats import ApiServersServerStat
from app.classes.web.routes.api.servers.server.users import ApiServersServerUsersHandler
from app.classes.web.routes.api.users.index import ApiUsersIndexHandler
from app.classes.web.routes.api.users.user.index import ApiUsersUserIndexHandler
from app.classes.web.routes.api.users.user.permissions import (
ApiUsersUserPermissionsHandler,
)
from app.classes.web.routes.api.users.user.pfp import ApiUsersUserPfpHandler
from app.classes.web.routes.api.users.user.public import ApiUsersUserPublicHandler
@ -58,6 +61,16 @@ def api_handlers(handler_args):
ApiUsersUserIndexHandler,
handler_args,
),
(
r"/api/v2/users/([0-9]+)/permissions/?",
ApiUsersUserPermissionsHandler,
handler_args,
),
(
r"/api/v2/users/(@me)/permissions/?",
ApiUsersUserPermissionsHandler,
handler_args,
),
(
r"/api/v2/users/([0-9]+)/pfp/?",
ApiUsersUserPfpHandler,

View File

@ -1,6 +1,5 @@
import datetime
import logging
from app.classes.shared.console import Console
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
@ -12,8 +11,7 @@ class ApiAuthInvalidateTokensHandler(BaseApiHandler):
if not auth_data:
return
# TODO: Invalidate tokens
Console.info("invalidate_tokens")
logger.debug(f"Invalidate tokens for user {auth_data[4]['user_id']}")
self.controller.users.raw_update_user(
auth_data[4]["user_id"], {"valid_tokens_from": datetime.datetime.now()}
)

View File

@ -79,8 +79,8 @@ class ApiRolesIndexHandler(BaseApiHandler):
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = orjson.loads(self.request.body) # pylint: disable=no-member
except orjson.decoder.JSONDecodeError as e: # pylint: disable=no-member
data = orjson.loads(self.request.body)
except orjson.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)

View File

@ -105,8 +105,8 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = orjson.loads(self.request.body) # pylint: disable=no-member
except orjson.decoder.JSONDecodeError as e: # pylint: disable=no-member
data = orjson.loads(self.request.body)
except orjson.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)

View File

@ -17,24 +17,6 @@ new_server_schema = {
"monitoring_type",
"create_type",
],
"examples": [
{
"name": "My Server",
"monitoring_type": "minecraft_java",
"minecraft_java_monitoring_data": {"host": "127.0.0.1", "port": 25565},
"create_type": "minecraft_java",
"minecraft_java_create_data": {
"create_type": "download_jar",
"download_jar_create_data": {
"type": "Paper",
"version": "1.18.2",
"mem_min": 1,
"mem_max": 2,
"server_properties_port": 25565,
},
},
}
],
"properties": {
"name": {
"title": "Name",
@ -665,8 +647,8 @@ class ApiServersIndexHandler(BaseApiHandler):
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = orjson.loads(self.request.body) # pylint: disable=no-member
except orjson.decoder.JSONDecodeError as e: # pylint: disable=no-member
data = orjson.loads(self.request.body)
except orjson.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)

View File

@ -110,7 +110,7 @@ class ApiServersServerIndexHandler(BaseApiHandler):
server_obj = self.controller.servers.get_server_obj(server_id)
for key in data:
# If we don't validate the input there could be security issues
setattr(self, key, data[key])
setattr(server_obj, key, data[key])
self.controller.servers.update_server(server_obj)
self.controller.management.add_to_audit_log(

View File

@ -99,7 +99,7 @@ class ApiUsersIndexHandler(BaseApiHandler):
email = data.get("email", "default@example.com")
enabled = data.get("enabled", True)
lang = data.get("lang", self.helper.get_setting("language"))
superuser = data.get("superuser", False)
new_superuser = data.get("superuser", False)
permissions = data.get("permissions", None)
roles = data.get("roles", None)
hints = data.get("hints", True)
@ -134,13 +134,24 @@ class ApiUsersIndexHandler(BaseApiHandler):
)
permissions_mask = "".join(permissions_mask)
if new_superuser and not superuser:
return self.finish_json(
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"}
)
# TODO: do this in the most efficient way
user_id = self.controller.users.add_user(
username,
password,
email,
enabled,
superuser,
new_superuser,
)
self.controller.users.update_user(
user_id,

View File

@ -1,8 +1,13 @@
import json
import logging
import typing as t
from jsonschema import ValidationError, validate
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.controllers.users_controller import UsersController
from app.classes.models.crafty_permissions import (
EnumPermissionsCrafty,
PermissionsCrafty,
)
from app.classes.models.roles import HelperRoles
from app.classes.models.users import HelperUsers
from app.classes.web.base_api_handler import BaseApiHandler
@ -170,7 +175,7 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
},
)
if data.get("username", None) is not None:
if "username" in data:
if data["username"].lower() in ["system", ""]:
return self.finish_json(
400, {"status": "error", "error": "INVALID_USERNAME"}
@ -180,10 +185,10 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
400, {"status": "error", "error": "USER_EXISTS"}
)
if data.get("superuser", None) is not None:
if str(user["user_id"]) == str(user_id):
# Checks if user is trying to change super user status of self.
# We don't want that.
if "superuser" in data:
if str(user["user_id"]) == str(user_id) and not superuser:
# Checks if user is trying to change super user status
# of self without superuser. We don't want that.
return self.finish_json(
400, {"status": "error", "error": "INVALID_SUPERUSER_MODIFY"}
)
@ -191,10 +196,10 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
# The user is not superuser so they can't change the superuser status
data.pop("superuser")
if data.get("permissions", None) is not None:
if str(user["user_id"]) == str(user_id):
# Checks if user is trying to change permissions of self.
# We don't want that.
if "permissions" in data:
if str(user["user_id"]) == str(user_id) and not superuser:
# Checks if user is trying to change permissions
# of self without superuser. We don't want that.
return self.finish_json(
400, {"status": "error", "error": "INVALID_PERMISSIONS_MODIFY"}
)
@ -205,10 +210,10 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
400, {"status": "error", "error": "INVALID_PERMISSIONS_MODIFY"}
)
if data.get("roles", None) is not None:
if str(user["user_id"]) == str(user_id):
# Checks if user is trying to change roles of self.
# We don't want that.
if "roles" in data:
if str(user["user_id"]) == str(user_id) and not superuser:
# Checks if user is trying to change roles of
# self without superuser. We don't want that.
return self.finish_json(
400, {"status": "error", "error": "INVALID_ROLES_MODIFY"}
)
@ -219,10 +224,66 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
400, {"status": "error", "error": "INVALID_ROLES_MODIFY"}
)
# TODO: make this more efficient
# TODO: add permissions and roles because I forgot
if "password" in data and str(user["user_id"] == str(user_id)):
# TODO: edit your own password
return self.finish_json(
400, {"status": "error", "error": "INVALID_PASSWORD_MODIFY"}
)
user_obj = HelperUsers.get_user_model(user_id)
if "roles" in data:
roles: t.Set[str] = set(data.pop("roles"))
base_roles: t.Set[str] = set(user_obj.roles)
added_roles = roles.difference(base_roles)
removed_roles = base_roles.difference(roles)
logger.debug(
f"updating user {user_id}'s roles: "
f"+role:{added_roles} -role:{removed_roles}"
)
for role_id in added_roles:
HelperUsers.get_or_create(user_id, role_id)
if len(removed_roles) != 0:
self.controller.users.users_helper.delete_user_roles(
user_id, removed_roles
)
if "permissions" in data:
permissions: t.List[UsersController.ApiPermissionDict] = data.pop(
"permissions"
)
permissions_mask = "0" * len(EnumPermissionsCrafty)
limit_server_creation = 0
limit_user_creation = 0
limit_role_creation = 0
for permission in permissions:
self.controller.crafty_perms.set_permission(
permissions_mask,
EnumPermissionsCrafty.__members__[permission["name"]],
"1" if permission["enabled"] else "0",
)
PermissionsCrafty.add_or_update_user(
user_id,
permissions_mask,
limit_server_creation,
limit_user_creation,
limit_role_creation,
)
# TODO: make this more efficient
if len(data) != 0:
for key in data:
# If we don't validate the input there could be security issues
value = data[key]
if key == "password":
value = self.helper.encode_pass(value)
setattr(user_obj, key, value)
user_obj.save()
self.controller.management.add_to_audit_log(
user["user_id"],
(
@ -233,9 +294,4 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
source_ip=self.get_remote_ip(),
)
for key in data:
# If we don't validate the input there could be security issues
setattr(user_obj, key, data[key])
user_obj.save()
return self.finish_json(200, {"status": "ok"})

View File

@ -0,0 +1,73 @@
import logging
import typing as t
from app.classes.models.crafty_permissions import (
EnumPermissionsCrafty,
PermissionsCrafty,
)
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
SERVER_CREATION: t.Final[str] = EnumPermissionsCrafty.SERVER_CREATION.name
USER_CONFIG: t.Final[str] = EnumPermissionsCrafty.USER_CONFIG.name
ROLES_CONFIG: t.Final[str] = EnumPermissionsCrafty.ROLES_CONFIG.name
class ApiUsersUserPermissionsHandler(BaseApiHandler):
def get(self, user_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
exec_user_crafty_permissions,
_,
_,
user,
) = auth_data
if user_id in ["@me", user["user_id"]]:
user_id = user["user_id"]
res_data = PermissionsCrafty.get_user_crafty(user_id)
elif EnumPermissionsCrafty.USER_CONFIG not in exec_user_crafty_permissions:
return self.finish_json(
400,
{
"status": "error",
"error": "NOT_AUTHORIZED",
},
)
else:
# has User_Config permission and isn't viewing self
res_data = PermissionsCrafty.get_user_crafty_optional(user_id)
if res_data is None:
return self.finish_json(
404,
{
"status": "error",
"error": "USER_NOT_FOUND",
},
)
self.finish_json(
200,
{
"status": "ok",
"data": {
"permissions": res_data.permissions,
"counters": {
SERVER_CREATION: res_data.created_server,
USER_CONFIG: res_data.created_user,
ROLES_CONFIG: res_data.created_role,
},
"limits": {
SERVER_CREATION: res_data.limit_server_creation,
USER_CONFIG: res_data.limit_user_creation,
ROLES_CONFIG: res_data.limit_role_creation,
},
},
},
)

View File

@ -52,12 +52,13 @@
</div>
<div class="navbar-menu-wrapper d-flex align-items-center">
<style>
body:not(.sidebar-icon-only) .navbar-toggler .mdi-chevron-double-right {
body:not(.sidebar-icon-only) .navbar-toggler .mdi-chevron-double-right {
display: none;
}
body.sidebar-icon-only .navbar-toggler .mdi-chevron-double-left {
}
body.sidebar-icon-only .navbar-toggler .mdi-chevron-double-left {
display: none;
}
}
</style>
<button class="navbar-toggler navbar-toggler align-self-center" type="button" data-toggle="minimize">
<span class="mdi mdi-chevron-double-left"></span>
@ -157,7 +158,8 @@
<script src="/static/assets/js/shared/off-canvas.js"></script>
<script src="/static/assets/js/shared/hoverable-collapse.js"></script>
<script src="/static/assets/js/shared/misc.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/v/bs4/dt-1.10.22/fh-3.1.7/r-2.2.6/sc-2.0.3/sp-1.2.2/datatables.min.js"></script>
<script type="text/javascript"
src="https://cdn.datatables.net/v/bs4/dt-1.10.22/fh-3.1.7/r-2.2.6/sc-2.0.3/sp-1.2.2/datatables.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/5.4.0/bootbox.min.js"></script>
<script type="text/javascript" src="/static/assets/js/motd.js"></script>
@ -214,14 +216,12 @@
};
wsInternal.onerror = function (errorEvent) {
console.error('WebSocket Error', errorEvent);
warn('WebSockets are required for Crafty to work. This websocket connection has been closed. Are you using a reverse proxy? See our'+
' documentation for details')
warn('WebSockets are required for Crafty to work. This websocket connection has been closed. Are you using a reverse proxy?', 'https://wiki.craftycontrol.com/en/4/docs/Reverse%20Proxy%20Examples')
};
wsInternal.onclose = function (closeEvent) {
console.log('Closed WebSocket', closeEvent);
warn('WebSockets are required for Crafty to work. This websocket connection has been closed. Are you using a reverse proxy? See our'+
' documentation for details')
warn('WebSockets are required for Crafty to work. This websocket connection has been closed. Are you using a reverse proxy?', 'https://wiki.craftycontrol.com/en/4/docs/Reverse%20Proxy%20Examples')
};
@ -269,11 +269,11 @@
}
if (webSocket) {
webSocket.on('support_status_update', function (logs) {
if(logs.percent >= 100){
if (logs.percent >= 100) {
document.getElementById('logs_progress_bar').innerHTML = '100%';
document.getElementById('logs_progress_bar').style.width = '100%';
}else{
document.getElementById('logs_progress_bar').innerHTML = logs.percent +'%';
} else {
document.getElementById('logs_progress_bar').innerHTML = logs.percent + '%';
document.getElementById('logs_progress_bar').style.width = logs.percent + '%';
}
});
@ -341,7 +341,7 @@
}
function eulaAgree(server_id, command) {
//< !--this getCookie function is in base.html-- >
//< !--this getCookie function is in base.html-- >
var token = getCookie("_xsrf");
$.ajax({
@ -357,7 +357,7 @@
}
function warn(message) {
function warn(message, link = null) {
var closeEl = document.createElement('span');
var strongEL = document.createElement('strong');
var msgEl = document.createElement('div');
@ -383,6 +383,16 @@
parentEl.appendChild(closeEl);
parentEl.appendChild(msgEl);
if (link) {
let linkEl = document.createElement('a')
linkEl.href = link;
linkEl.innerHTML = "See our documentation for details.";
linkEl.style.color = 'white';
linkEl.style.textDecoration = 'underline';
linkEl.target = "_blank";
parentEl.appendChild(linkEl);
}
document.querySelector('.warnings').appendChild(parentEl);
}
@ -460,4 +470,4 @@
</body>
</html>
</html>

View File

@ -24,7 +24,8 @@
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="far fa-code"></i> &nbsp;{{ translate('credits', 'developmentTeam', data['lang']) }}</h4>
<h4 class="card-title"><i class="far fa-code"></i> &nbsp;{{ translate('credits', 'developmentTeam', data['lang'])
}}</h4>
</div>
<div class="card-body">
<div class="row">
@ -40,7 +41,8 @@
<img src="{{ person['pic'] }}" alt="profile image" class="profile-img img-lg rounded-circle">
{% else %}
<div alt="profil image" class="profile-img img-lg rounded-circle">
<img src="/static/assets/images/credits/user-circle-solid.svg" alt="profile image" class="profile-img img-lg rounded-circle">
<img src="/static/assets/images/credits/user-circle-solid.svg" alt="profile image"
class="profile-img img-lg rounded-circle">
</div>
{% end %}
</div>
@ -66,14 +68,16 @@
{% end %}
{% if person['tags'][1] %}
{% if type(person['tags'][1]) is list %}
<a href="{{ person['tags'][1][1] }}" class="btn btn-sm btn-primary mr-2">{{ person['tags'][1][0] }}</a>
<a href="{{ person['tags'][1][1] }}" class="btn btn-sm btn-primary mr-2">{{ person['tags'][1][0]
}}</a>
{% else %}
<span class="btn btn-sm btn-primary mr-2">{{ person['tags'][1] }}</span>
{% end %}
{% end %}
{% if person['tags'][2] %}
{% if type(person['tags'][2]) is list %}
<a href="{{ person['tags'][2][1] }}" class="btn btn-sm btn-inverse-success mr-2">{{ person['tags'][2][0] }}</a>
<a href="{{ person['tags'][2][1] }}" class="btn btn-sm btn-inverse-success mr-2">{{
person['tags'][2][0] }}</a>
{% else %}
<span class="btn btn-sm btn-inverse-success mr-2">{{ person['tags'][2] }}</span>
{% end %}
@ -117,7 +121,8 @@
<img src="{{ person['pic'] }}" alt="profile image" class="profile-img img-lg rounded-circle">
{% else %}
<div alt="profil image" class="profile-img img-lg rounded-circle">
<img src="/static/assets/images/credits/user-circle-solid.svg" alt="profile image" class="profile-img img-lg rounded-circle">
<img src="/static/assets/images/credits/user-circle-solid.svg" alt="profile image"
class="profile-img img-lg rounded-circle">
</div>
{% end %}
</div>
@ -143,14 +148,16 @@
{% end %}
{% if person['tags'][1] %}
{% if type(person['tags'][1]) is list %}
<a href="{{ person['tags'][1][1] }}" class="btn btn-sm btn-primary mr-2">{{ person['tags'][1][0] }}</a>
<a href="{{ person['tags'][1][1] }}" class="btn btn-sm btn-primary mr-2">{{ person['tags'][1][0]
}}</a>
{% else %}
<span class="btn btn-sm btn-primary mr-2">{{ person['tags'][1] }}</span>
{% end %}
{% end %}
{% if person['tags'][2] %}
{% if type(person['tags'][2]) is list %}
<a href="{{ person['tags'][2][1] }}" class="btn btn-sm btn-inverse-success mr-2">{{ person['tags'][2][0] }}</a>
<a href="{{ person['tags'][2][1] }}" class="btn btn-sm btn-inverse-success mr-2">{{
person['tags'][2][0] }}</a>
{% else %}
<span class="btn btn-sm btn-inverse-success mr-2">{{ person['tags'][2] }}</span>
{% end %}
@ -177,7 +184,8 @@
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="far fa-server"></i> &nbsp;{{ translate('credits', 'retiredStaff', data['lang']) }}</h4>
<h4 class="card-title"><i class="far fa-server"></i> &nbsp;{{ translate('credits', 'retiredStaff', data['lang'])
}}</h4>
</div>
<div class="card-body">
@ -185,163 +193,180 @@
{% for person in data['staff']['retired'] %}
<div class="col-lg-6 mb-5">
<div class="card rounded shadow-none">
<div class="row">
<div class="col-md-4" style="max-width: fit-content;">
<div class="card-img-top user-avatar mb-auto">
{% if person['pic'] %}
<img src="{{ person['pic'] }}" alt="profile image" class="profile-img img-lg rounded-circle">
{% else %}
<div alt="profil image" class="profile-img img-lg rounded-circle">
<img src="/static/assets/images/credits/user-circle-solid.svg" alt="profile image">
</div>
{% end %}
</div>
<div class="wrapper">
<div class="wrapper d-flex align-items-center">
<h4 class="mb-0 font-weight-medium">{{ person['name'] }}</h4>
</div>
<div class="wrapper d-flex align-items-center font-weight-medium text-muted">
{% if person['loc'] %}
<i class="mdi mdi-map-marker-outline mr-2"></i>
<p class="mb-0 text-muted">{{ person['loc'] }}</p>
{% end %}
</div>
<div class="row">
<div class="col-md-4" style="max-width: fit-content;">
<div class="card-img-top user-avatar mb-auto">
{% if person['pic'] %}
<img src="{{ person['pic'] }}" alt="profile image" class="profile-img img-lg rounded-circle">
{% else %}
<div alt="profil image" class="profile-img img-lg rounded-circle">
<img src="/static/assets/images/credits/user-circle-solid.svg" alt="profile image">
</div>
{% end %}
</div>
<div class="col-md-8">
<div class="wrapper d-flex align-items-start">
{% if person['tags'][0] %}
<span class="btn btn-sm btn-info mr-2">{{ person['tags'][0] }}</span>
{% end %}
{% if person['tags'][1] %}
{% if type(person['tags'][1]) is list %}
<a href="{{ person['tags'][1][1] }}" class="btn btn-sm btn-primary mr-2">{{ person['tags'][1][0] }}</a>
{% else %}
<span class="btn btn-sm btn-primary mr-2">{{ person['tags'][1] }}</span>
{% end %}
{% end %}
{% if person['tags'][2] %}
{% if type(person['tags'][2]) is list %}
<a href="{{ person['tags'][2][1] }}" class="btn btn-sm btn-inverse-success mr-2">{{ person['tags'][2][0] }}</a>
{% else %}
<span class="btn btn-sm btn-inverse-success mr-2">{{ person['tags'][2] }}</span>
{% end %}
{% end %}
<div class="wrapper">
<div class="wrapper d-flex align-items-center">
<h4 class="mb-0 font-weight-medium">{{ person['name'] }}</h4>
</div>
<div class="wrapper align-items-start pt-3">
{% if person['title'] %}
<h5><strong>{{ person['title'] }}</strong></h5>
<div class="wrapper d-flex align-items-center font-weight-medium text-muted">
{% if person['loc'] %}
<i class="mdi mdi-map-marker-outline mr-2"></i>
<p class="mb-0 text-muted">{{ person['loc'] }}</p>
{% end %}
<p>{{ person['blurb'] }}</p>
</div>
</div>
</div>
<div class="col-md-8">
<div class="wrapper d-flex align-items-start">
{% if person['tags'][0] %}
<span class="btn btn-sm btn-info mr-2">{{ person['tags'][0] }}</span>
{% end %}
{% if person['tags'][1] %}
{% if type(person['tags'][1]) is list %}
<a href="{{ person['tags'][1][1] }}" class="btn btn-sm btn-primary mr-2">{{ person['tags'][1][0]
}}</a>
{% else %}
<span class="btn btn-sm btn-primary mr-2">{{ person['tags'][1] }}</span>
{% end %}
{% end %}
{% if person['tags'][2] %}
{% if type(person['tags'][2]) is list %}
<a href="{{ person['tags'][2][1] }}" class="btn btn-sm btn-inverse-success mr-2">{{
person['tags'][2][0] }}</a>
{% else %}
<span class="btn btn-sm btn-inverse-success mr-2">{{ person['tags'][2] }}</span>
{% end %}
{% end %}
</div>
<div class="wrapper align-items-start pt-3">
{% if person['title'] %}
<h5><strong>{{ person['title'] }}</strong></h5>
{% end %}
<p>{{ person['blurb'] }}</p>
</div>
</div>
</div>
</div>
{% end %}
</div> <!-- end user row-->
</div>
</div>
<br />
<div class="row">
<div class="col-lg-6 grid-margin stretch-card">
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fab fa-patreon"></i> {{ translate('credits', 'patreonSupporter',
data['lang'])
}}</h4>
</div>
<div class="card-body">
<p class="card-description"> {{ translate('credits', 'hugeDesc', data['lang']) }}
<code>{{ translate('credits', 'thankYou', data['lang']) }}</code>&nbsp; {{ translate('credits', 'patreonDesc', data['lang']) }} | <span style="color: #9365B8">{{ translate('credits', 'patreonUpdate', data['lang']) }} {{ data["lastUpdate"] }}</span>
</p>
<table class="table table-hover">
<thead>
<tr>
<th>{{ translate('credits', 'patreonName', data['lang']) }}</th>
<th>{{ translate('credits', 'patreonLevel', data['lang']) }}</th>
</tr>
</thead>
<tbody>
{% for pat in data["patrons"] %}
<tr>
<td>{{ pat["name"] }}</td>
<td>
{% if pat["level"] == "Crafty Sustainer" %}
<span class="btn btn-sm btn-info mr-2">Sustainer</span>
{% elif pat["level"] == "Crafty Advocate" %}
<span class="btn btn-sm btn-primary mr-2">Advocate</span>
{% elif pat["level"] == "Crafty Supporter" %}
<span class="btn btn-sm btn-inverse-success mr-2">Supporter</span>
{% else %}
<span class="btn btn-sm btn-secondary mr-2">{{ translate('credits', 'patreonOther', data['lang']) }}</span>
{% end %}
</td>
</tr>
{% end %}
</tbody>
</table>
</div>
</div>
</div>
<div class="col-lg-6 grid-margin stretch-card">
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="far fa-language"></i> {{ translate('credits', 'translationTitle', data['lang']) }}</h4>
</div>
<div class="card-body">
<p class="card-text"> {{ translate('credits', 'hugeDesc', data['lang']) }}
<code>{{ translate('credits', 'thankYou', data['lang']) }}</code>&nbsp; {{ translate('credits', 'translationDesc', data['lang']) }}
</p>
<table class="table table-hover">
<thead>
<tr>
<th>{{ translate('credits', 'translationName', data['lang']) }}</th>
<th>{{ translate('credits', 'translator', data['lang']) }}</th>
</tr>
</thead>
<tbody>
{% for person in data['translations'] %}
<tr>
<td>{{ person }}</td>
<td class="pb-0">
<div class="row">
{% for language in data['translations'][person] %}
<span class="btn btn-sm btn-inverse-success mr-2" style="margin-bottom: 12px;">{{ language }}</span>
{% end %}
</div>
</td>
</tr>
{% end %}
</tbody>
</table>
</div>
</div>
</div>
{% end %}
</div> <!-- end user row-->
</div>
</div>
<!-- content-wrapper ends -->
{% end %}
<br />
{% block js %}
<script>
<div class="row">
$(document).ready(function () {
console.log('ready for JS!')
<div class="col-lg-6 grid-margin stretch-card">
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fab fa-patreon"></i> {{ translate('credits', 'patreonSupporter',
data['lang'])
}} <i class="fa fa-coffee"></i></h4>
</div>
<div class="card-body">
<p class="card-description"> {{ translate('credits', 'hugeDesc', data['lang']) }}
<code>{{ translate('credits', 'thankYou', data['lang']) }}</code>&nbsp; {{ translate('credits',
'patreonDesc', data['lang']) }} | <span style="color: #9365B8">{{ translate('credits', 'patreonUpdate',
data['lang']) }} {{ data["lastUpdate"] }}</span>
</p>
<table class="table table-hover">
<thead>
<tr>
<th>{{ translate('credits', 'subscriberName', data['lang']) }}</th>
<th>{{ translate('credits', 'subscriptionLevel', data['lang']) }}</th>
</tr>
</thead>
<tbody>
{% for pat in data["patrons"] %}
<tr>
<td>{{ pat["name"] }}</td>
<td>
{% if pat["level"] == "Crafty Sustainer" %}
<span class="btn btn-sm btn-info mr-2">Sustainer</span>
{% elif pat["level"] == "Crafty Advocate" %}
<span class="btn btn-sm btn-primary mr-2">Advocate</span>
{% elif pat["level"] == "Crafty Supporter" %}
<span class="btn btn-sm btn-inverse-success mr-2">Supporter</span>
{% else %}
<span class="btn btn-sm btn-secondary mr-2">{{ translate('credits', 'patreonOther', data['lang'])
}}</span>
{% end %}
{% if pat["source"] == "Patreon" %}
<span class="badge badge-pill badge-info"><i class="fab fa-patreon"></i> Patreon</span>
{% elif pat["source"] == "Ko-fi" %}
<span class="badge badge-pill badge-primary"><i class="fa fa-coffee"></i> Ko-fi</span>
{% else %}
<span class="badge badge-pill badge-dark"><i class="fa fa-question"></i> {{ translate('credits',
'patreonOther',
data['lang'])
}}</span>
{% end %}
</td>
</tr>
{% end %}
});
</script>
</tbody>
</table>
</div>
</div>
</div>
{% end %}
<div class="col-lg-6 grid-margin stretch-card">
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="far fa-language"></i> {{ translate('credits', 'translationTitle',
data['lang']) }}</h4>
</div>
<div class="card-body">
<p class="card-text"> {{ translate('credits', 'hugeDesc', data['lang']) }}
<code>{{ translate('credits', 'thankYou', data['lang']) }}</code>&nbsp; {{ translate('credits',
'translationDesc', data['lang']) }}
</p>
<table class="table table-hover">
<thead>
<tr>
<th>{{ translate('credits', 'translationName', data['lang']) }}</th>
<th>{{ translate('credits', 'translator', data['lang']) }}</th>
</tr>
</thead>
<tbody>
{% for person in data['translations'] %}
<tr>
<td>{{ person }}</td>
<td class="pb-0">
<div class="row">
{% for language in data['translations'][person] %}
<span class="btn btn-sm btn-inverse-success mr-2" style="margin-bottom: 12px;">{{ language }}</span>
{% end %}
</div>
</td>
</tr>
{% end %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- content-wrapper ends -->
{% end %}
{% block js %}
<script>
$(document).ready(function () {
console.log('ready for JS!')
});
</script>
{% end %}

View File

@ -39,11 +39,11 @@
"hugeDesc": "Ein riesiges",
"pageDescription": "Ohne diese Menschen würde Crafty nicht existieren",
"pageTitle": "Mitwirkende",
"patreonDesc": "an unsere Patreon-Unterstützer!",
"patreonLevel": "Unterstützer-Level",
"patreonName": "Name",
"patreonDesc": "an unsere Patreon/Ko-fi-Unterstützer!",
"subscriptionLevel": "Unterstützer-Level",
"subscriberName": "Name",
"patreonOther": "Andere",
"patreonSupporter": "Patreon Unterstützer",
"patreonSupporter": "Patreon / Ko-fi Unterstützer",
"patreonUpdate": "Letzte Aktualisierung:",
"retiredStaff": "Ehemalige Helfer",
"supportTeam": "Support- und Dokumentationsteam",

View File

@ -39,11 +39,11 @@
"hugeDesc": "A huge",
"pageDescription": "Without these people, you wouldn't have Crafty",
"pageTitle": "Credits",
"patreonDesc": "to our Patreon supporters!",
"patreonLevel": "Level",
"patreonName": "Name",
"patreonDesc": "to our Patreon / Ko-fi supporters!",
"subscriptionLevel": "Level",
"subscriberName": "Name",
"patreonOther": "Other",
"patreonSupporter": "Patreon Supporters",
"patreonSupporter": "Patreon / Ko-fi Supporters",
"patreonUpdate": "Last Update:",
"retiredStaff": "Retired Staff",
"supportTeam": "Support and Documentation Team",

View File

@ -39,11 +39,11 @@
"hugeDesc": "Valtava",
"pageDescription": "Ilman näitä ihmisiä sinulla ei olisi Craftya",
"pageTitle": "Hyvitykset",
"patreonDesc": "Patreon-tukijoillemme!",
"patreonLevel": "Taso",
"patreonName": "Nimi",
"patreonDesc": "Patreon / Ko-fi-tukijoillemme!",
"subscriptionLevel": "Taso",
"subscriberName": "Nimi",
"patreonOther": "Muu",
"patreonSupporter": "Patreon-tukijat",
"patreonSupporter": "Patreon / Ko-fi-tukijat",
"patreonUpdate": "Viimeisin päivitys:",
"retiredStaff": "Entinen henkilökunta",
"supportTeam": "Tuki- ja dokumentointitiimi",

View File

@ -39,11 +39,11 @@
"hugeDesc": "Un Enorme",
"pageDescription": "Sans ces personnes, vous n'auriez pas Crafty",
"pageTitle": "Crédits",
"patreonDesc": "à nos Soutiens Patreon !",
"patreonLevel": "Niveau",
"patreonName": "Nom",
"patreonDesc": "à nos Soutiens Patreon / Ko-fi !",
"subscriptionLevel": "Niveau",
"subscriberName": "Nom",
"patreonOther": "Autre",
"patreonSupporter": "Soutiens Patreon",
"patreonSupporter": "Soutiens Patreon / Ko-fi",
"patreonUpdate": "Dernière mise à Jour :",
"retiredStaff": "Retraités de Crafty",
"supportTeam": "Equipe de Support et de Documentation",

View File

@ -39,11 +39,11 @@
"hugeDesc": "In enoarm",
"pageDescription": "Sûnder dizze minsken soene jo Crafty net hawwe",
"pageTitle": "Credits",
"patreonDesc": "oan ús Patreon-supporters!",
"patreonLevel": "Nivel",
"patreonName": "Namme",
"patreonDesc": "oan ús Patreon / Ko-fi-supporters!",
"subscriptionLevel": "Nivel",
"subscriberName": "Namme",
"patreonOther": "Oare",
"patreonSupporter": "Patreon Supporters",
"patreonSupporter": "Patreon / Ko-fi Supporters",
"patreonUpdate": "Lêste update:",
"retiredStaff": "Pensionearre staff",
"supportTeam": "Support- en dokumintaasjeteam",

532
app/translations/he_IL.json Normal file
View File

@ -0,0 +1,532 @@
{
"404": {
"contact": "בבקשה צרו קשר עם תמיכת Crafty Control באמצעות דיסקורד",
"notFound": "דף לא נימצא",
"unableToFind": "לא הצלחנו למצוא את הדף שאתם מחפשים. בבקשה נסו שוב, או בצעו ריענון."
},
"accessDenied": {
"accessDenied": "גישה נדחתה",
"contact": "בבקשה צרו קשר עם תמיכת Crafty Control באמצעות דיסקורד",
"contactAdmin": "צרו קשר עם מנהל השרת שלכם לקבלת גישה למשאב זה, או אם אתם חושב שיש לכם גישה למשאב זה, פנו לתמיכה.",
"noAccess": "אין לך גישה למשאב זה"
},
"apiKeys": {
"apiKeys": "מפתחות API",
"auth": "Authorized? ",
"buttons": "כפתורים",
"config": "הגדרה",
"crafty": "Crafty: ",
"created": "Created",
"createNew": "צור מפתח API חדש",
"deleteKeyConfirmation": "האם ברצונך למחוק מפתח API זה? אי אפשר לבטל את זה.",
"deleteKeyConfirmationTitle": "להסיר את מפתח ה-API ${keyId}?",
"getToken": "קבלת אסימון",
"name": "שם",
"nameDesc": "איך תרצו לקרוא לאסימון ה-API הזה? ",
"no": "לא",
"pageTitle": "ערוך מפתחות API של משתמש",
"permName": "שם הגישה",
"perms": "גישות",
"server": "שרת: ",
"superUser": "סופר מתשמש",
"yes": "כן"
},
"base": {
"doesNotWorkWithoutJavascript": "<strong>אזהרה: </strong>Crafty לא עובד כשורה כאשר JavaScript אינו מופעל!"
},
"credits": {
"developmentTeam": "צוות פיתוח",
"hugeDesc": "תודה ענקית",
"pageDescription": "בלי האנשים האלה, לא היה לכם את Crafty",
"pageTitle": "קרדיטים",
"patreonDesc": "לתומכי הפטראון שלנו!",
"subscriptionLevel": "רמה",
"subscriberName": "שם",
"patreonOther": "אחר",
"patreonSupporter": "תומכי פטראון",
"patreonUpdate": "העדכון אחרון:",
"retiredStaff": "צוות לשעבר",
"supportTeam": "צוות תמיכה וכותבי ויקי",
"thankYou": "תודה רבה",
"translationDesc": "לקהילה שלנו שמתרגמים!",
"translationName": "שם",
"translationTitle": "שפת התרגום",
"translator": "מתרגמים"
},
"dashboard": {
"actions": "פעולות",
"allServers": "כל השרתים",
"avg": "ממוצע",
"backups": "גיבויים",
"bePatientClone": "בבקשה חכו בסבלנות בזמן שאנו משכפלים את השרת.<br /> מסך זה יתרענן בעוד רגע",
"bePatientRestart": "בבקשה חכו בסבלנות בזמן שאנו מפעילים מחדש את השרת.<br /> מסך זה יתרענן בעוד רגע",
"bePatientStart": "בבקשה חכו בסבלנות בזמן שאנו מפעילים את השרת.<br /> מסך זה יתרענן בעוד רגע",
"bePatientStop": "בבקשה חכו בסבלנות בזמן שאנו עוצרים את השרת.<br /> מסך זה יתרענן בעוד רגע",
"cannotSee": "לא רואים הכל?",
"cannotSeeOnMobile": "לא רואים הכל בנייד?",
"cannotSeeOnMobile2": "נסו לגלול את הטבלה הצידה.",
"clone": "העתק",
"cpuCores": "ליבות מעבד",
"cpuCurFreq": "שעון נוכחי של מעבד",
"cpuMaxFreq": "שעון מרבי של מעבד",
"cpuUsage": "שימוש במעבד",
"crashed": "קריסות",
"dashboard": "פאנל",
"delay-explained": "השירות/סוכן התחיל לאחרונה והוא מעכב את הדלקת שרת המיינקראפט",
"host": "אחסון",
"kill": "כיבוי מידי",
"killing": "מכבה מידית...",
"lastBackup": "אחרון:",
"max": "מקסימום",
"memUsage": "שימוש בזיכרון",
"motd": "MOTD",
"newServer": "צרו שרת חדש",
"nextBackup": "הבא:",
"no-servers": "כרגע אין שרתים. כדי להתחיל, לחצו",
"offline": "סגור",
"online": "פעיל",
"players": "שחקנים",
"restart": "הפעלה מחדש",
"sendingCommand": "שולח את הפקודה שלך",
"server": "שרת",
"servers": "שרתים",
"size": "גודל השרת",
"start": "התחלה",
"starting": "התחלה בעיכוב",
"status": "סטאטוס",
"stop": "עצור",
"version": "גרסה",
"welcome": "ברוכים הבאים ל-Crafty Controller"
},
"datatables": {
"i18n": {
"aria": {
"sortAscending": ": הפעילו כדי למיין עמודות בסדר עולה",
"sortDescending": ": הפעילו כדי למיין עמודות בסדר יורד"
},
"buttons": {
"collection": "אוסף <span class='ui-button-icon-primary ui-icon ui-icon-triangle-1-s'/>",
"colvis": "נראות עמודה",
"colvisRestore": "שיחזור נראות",
"copy": "העתק",
"copyKeys": "הקש ctrl או u2318 + C כדי להעתיק את נתוני הטבלה ללוח המערכת שלך.<br><br>כדי לבטל, לחצו על הודעה זו או והקישו על escape.",
"copySuccess": {
"1": "הועתקה שורה אחת ללוח",
"_": "הועתקו %d שורות ללוח"
},
"copyTitle": "העתקה ללוח",
"csv": "CSV",
"excel": "Excel",
"pageLength": {
"1": "הצגת שורה אחת",
"-1": "הצגת כל השורות",
"_": "הצג %d שורות"
},
"pdf": "PDF",
"print": "הדפסה"
},
"decimal": "",
"emptyTable": "אין נתונים זמינים בטבלה",
"info": "Showing _START_ to _END_ of _TOTAL_ entries",
"infoEmpty": "מציג 0 עד 0 מתוך 0 ערכים",
"infoFiltered": "(filtered from _MAX_ total entries)",
"infoPostFix": "",
"lengthMenu": "Show _MENU_ entries",
"loadingRecords": "טוען...",
"paginate": {
"first": "ראשון",
"last": "אחרון",
"next": "הבא",
"previous": "הקודם"
},
"processing": "מעבד...",
"search": "לחפש:",
"select": {
"cells": {
"0": "לחצו על תא כדי לבחור אותו",
"1": "תא %d נבחר",
"_": "נבחרו %d תאים"
},
"columns": {
"0": "לחצו על עמודה כדי לבחור בה",
"1": "עמודה %d נבחרה",
"_": "נבחרו %d עמודות"
},
"rows": {
"0": "לחצו על שורה כדי לבחור בה",
"1": "נבחרה שורה %d",
"_": "נבחרו %d שורות"
}
},
"thousands": ",",
"zeroRecords": "לא נמצאו תוצאות תואמות"
}
},
"error": {
"contact": "בבקשה צרו קשר עם תמיכת Crafty Control באמצעות דיסקורד",
"embarassing": "אוי, טוב, זה מביך.",
"error": "שגיאה!",
"eulaAgree": "אתם מסכימים?",
"eulaMsg": "עליכם להסכים להסכם הרישיון למשתמש הקצה. עותק של הסכם הרישיון למשתמש הקצה של מוג'אנג מקושר תחת הודעה זו.",
"eulaTitle": "להסכים להסכם רישיון משתמש קצה של מוג'אנג",
"hereIsTheError": "הנה השגיאה",
"internet": "גילינו שמכונה שמריצה את Crafty אין חיבור לאינטרנט. חיבורי לקוחות לשרת עשויים להיות מוגבלים.",
"no-file": "נראה שאיננו מצליחים לאתר את הקובץ המבוקש. בדוק שוב את הנתיב. האם ל-Crafty יש הרשאות מתאימות?",
"noJava": "השרת {} לא הצליח להתחיל עם קוד השגיאה: גילינו ש-Java אינו מותקן. אנא התקינו את Java ואז הפעילו את השרת.",
"not-downloaded": "לא הצלחנו למצוא את קובץ ההפעלה שלך. האם זה סיים להוריד? האם ההרשאות מוגדרות בשביל הפעלה?",
"portReminder": "זיהינו שזו הפעם הראשונה ש-{} מופעל. הקפידו להעביר את היציאה {} דרך הנתב/חומת האש שלכם כדי להפוך אותה לנגישה מרחוק מהאינטרנט.",
"start-error": "השרת {} לא הצליח להתחיל עם קוד שגיאה: {}",
"terribleFailure": "איזה כישלון נורא!"
},
"footer": {
"allRightsReserved": "כל הזכויות שמורות",
"copyright": "זכויות יוצרים",
"version": "גרסה"
},
"login": {
"forgotPassword": "שכחתי סיסמה",
"login": "התחברות",
"password": "סיסמה",
"username": "שם משתמש"
},
"notify": {
"activityLog": "יומני פעילות",
"backupComplete": "הגיבוי הושלם בהצלחה עבור השרת {}",
"backupStarted": "התחיל גיבוי לשרת {}",
"downloadLogs": "להוריד יומני תמיכה?",
"finishedPreparing": "סיימנו להכין את יומני התמיכה שלך. אנא לחצו על הורדה כדי להוריד",
"logout": "להתנתק",
"preparingLogs": "אנא המתינו בזמן שאנו מכינים את היומנים שלכם... נשלח הודעה כשהם יהיו מוכנים. זה עשוי להימשך זמן מה לפריסות שרתים גדולות.",
"supportLogs": "יומני תמיכה"
},
"panelConfig": {
"adminControls": "בקרות מנהל",
"allowedServers": "שרתים מורשים",
"assignedRoles": "תפקידים שהוקצו",
"cancel": "ביטול",
"clearComms": "ניקוי פקודות שלא בוצעו",
"delete": "מחיקה",
"edit": "עריכה",
"enabled": "מופעל",
"newRole": "הוספת תפקיד חדש",
"newUser": "הוספת משתמש חדש",
"pageTitle": "הגדרת פאנל",
"role": "תפקיד",
"roles": "תפקידים",
"roleUsers": "תפקידי משתמשים",
"save": "שמירה",
"superConfirm": "המשיכו רק אם אתם רוצים שלמשתמש זה תהיה גישה להכל (כל חשבונות המשתמש, השרתים, הגדרות הפאנל וכו'). הם יכולים אפילו למחוק את זכויות משתמש העל שלך.",
"superConfirmTitle": "להפעיל משתמש-על? האם אתם בטוחים?",
"user": "משתמש",
"users": "משתמשים"
},
"rolesConfig": {
"config": "הגדרת תפקיד",
"configDesc": "כאן תוכלו לשנות את הגדרות התפקיד הזה",
"configUpdate": "עודכן לאחרונה: ",
"created": "נוצר: ",
"delRole": "מחק תפקיד",
"doesNotExist": "אתה לא יכול למחוק משהו שעדיין לא קיים",
"pageTitle": "ערוך תפקיד",
"pageTitleNew": "תפקיד חדש",
"permAccess": "גישה?",
"permName": "שם הרשאה",
"permsServer": "הרשאות לתפקיד זה עבור השרתים שצוינו",
"roleConfigArea": "אזור הגדרת תפקידים",
"roleDesc": "איך הייתם רוצים לקרוא לתפקיד הזה?",
"roleName": "שם התפקיד: ",
"rolePerms": "הרשאות תפקיד",
"roleServers": "שרתים מורשים",
"roleTitle": "הגדרות תפקידים",
"roleUserName": "שם משתמש",
"roleUsers": "תפקידי המשתמשים: ",
"serverAccess": "גישה?",
"serverName": "שם שרת",
"serversDesc": "לשרתים מותר לגשת לתפקיד זה"
},
"serverBackups": {
"backupAtMidnight": "גיבוי אוטומטי בחצות?",
"backupNow": "גיבוי עכשיו!",
"backupTask": "החלה משימת גיבוי.",
"cancel": "לבטל",
"clickExclude": "לחצו כדי לבחור מה לא יהיה בגיבוי",
"compress": "דחוס גיבוי",
"confirm": "אישור",
"confirmDelete": "האם ברצונכם למחוק את הגיבוי הזה? אי אפשר לבטל את זה.",
"confirmRestore": "האם אתם בטוחים שברצונכם לשחזר מגיבוי זה. כל קבצי השרת הנוכחיים ישתנו למצב גיבוי ולא יהיה אפשר לשחזר.",
"currentBackups": "גיבויים נוכחיים",
"delete": "למחוק",
"destroyBackup": "Destroy backup \" + file_to_del + \"?",
"download": "הורדה",
"excludedBackups": "נתיבים שלא נכללו: ",
"excludedChoose": "בחרו את הנתיבים שברצונכם לא לכלול בגיבויים",
"exclusionsTitle": "אי הכללות גיבוי",
"maxBackups": "מקסימום גיבויים",
"maxBackupsDesc": "Crafty לא יאחסן יותר מ-N גיבויים, ימחק את הישן ביותר (הזן 0 כדי לשמור את כולם)",
"options": "אפשרויות",
"path": "נתיב",
"restore": "לשחזר",
"restoring": "שחזור גיבוי. זה עשוי לקחת זמן. אנא חכו בסבלנות.",
"save": "שמירה",
"size": "גודל",
"storageLocation": "מקום איחסון",
"storageLocationDesc": "איפו אתם רוצים לאחסן גיבויים?"
},
"serverConfig": {
"bePatientDelete": "אנא חכו בסבלנות בזמן שאנו מסירים את השרת שלכם מלוח Crafty. מסך זה ייסגר בעוד מספר רגעים.",
"bePatientDeleteFiles": "אנא חכו בסבלנות בזמן שאנו מסירים את השרת שלך מהחלונית Crafty ומוחקים את כל הקבצים. מסך זה ייסגר בעוד מספר רגעים.",
"bePatientUpdate": "אנא חכו בסבלנות בזמן שאנו מעדכנים את השרת. זמני ההורדה עשויים להשתנות בהתאם למהירויות האינטרנט שלך.<br /> מסך זה יתרענן בעוד רגע",
"cancel": "ביטול",
"crashTime": "פסק זמן לקריסה",
"crashTimeDesc": "כמה זמן עלינו להמתין לפני שנראה שהשרת שלך קרס?",
"deleteFilesQuestion": "למחוק קבצי שרת מהמחשב?",
"deleteFilesQuestionMessage": "האם תרצו ש-Crafty תמחק את כל קבצי השרת מהמחשב המארח? <br><br><strong>זה כולל גיבויים של שרתים.</strong>",
"deleteServer": "מחיקת שרת",
"deleteServerQuestion": "מחיקת שרת?",
"deleteServerQuestionMessage": "האם אתם בטוחים שברצונכם למחוק את השרת הזה? אחרי זה אין דרך חזרה...",
"exeUpdateURL": "כתובת ה-URL של עדכון השרת הניתן להפעלה",
"exeUpdateURLDesc": "כתובת אתר להורדה ישירה לקבלת עדכונים.",
"noDelete": "לא, חזרה אחורה",
"noDeleteFiles": "לא, פשוט הסר מהפאנל",
"removeOldLogsAfter": "הסר יומנים ישנים לאחר",
"removeOldLogsAfterDesc": "כמה ימים קובץ יומן צריך להיות ישן כדי להימחק (0 כבוי)",
"save": "שמירה",
"sendingDelete": "מוחק שרת",
"sendingRequest": "שולח את בקשתכם...",
"serverAutoStart": "הפעלה אוטומטית של שרת",
"serverAutostartDelay": "השהיית הפעלה אוטומטית של השרת",
"serverAutostartDelayDesc": "עיכוב לפני הפעלה אוטומטית (אם מופעל למטה)",
"serverCrashDetection": "זיהוי קריסת שרת",
"serverExecutable": "שרת להפעלה",
"serverExecutableDesc": "קובץ ההפעלה של השרת",
"serverExecutionCommand": "פקודת ביצוע שרת",
"serverExecutionCommandDesc": "מה יושק בטרמינל נסתר",
"serverIP": "אייפי של השרת",
"serverIPDesc": "Crafty צריך כתובת אייפי בשביל להתחבר לסטטיסטיקה (נסה IP אמיתי במקום 127.0.0.1 אם יש לך בעיות)",
"serverLogLocation": "מיקום יומן שרת",
"serverLogLocationDesc": "נתיב מלא לקובץ היומן",
"serverName": "שם שרת",
"serverNameDesc": "איך אתם רוצים לקרוא לשרת הזה",
"serverPath": "ספריית עבודה של שרת",
"serverPathDesc": "נתיב מלא מוחלט (לא כולל קובץ הפעלה)",
"serverPort": "פורט שרת",
"serverPortDesc": "Crafty צריך פורט בשביל להתחבר לנתונים סטטיסטיים",
"serverStopCommand": "פקודת עצירת שרת",
"serverStopCommandDesc": "פקודה לשלוח את התוכנית כדי לעצור אותה",
"stopBeforeDeleting": "בבקשה לעצור את השרת לפני מחיקתו",
"update": "עדכנו את קובץ ההפעלה",
"yesDelete": "כן, למחוק",
"yesDeleteFiles": "כן, מחק קבצים"
},
"serverConfigHelp": {
"desc": "כאן אתם יכולים לשנות את הגדרות השרת שלכם",
"perms": [
"מומלץ <code>לא</code> לשנות את הנתיבים של שרת המנוהל על ידי Crafty.",
"שינוי נתיבים <code>יכול</code> לשבור דברים, במיוחד במערכות הפעלה מסוג לינוקס שבהן הרשאות הקבצים נעולות יותר.",
"<br /><br/>",
"אם אתם מרגישים צורך לשנות את מיקום השרת, אתם רשאים לעשות זאת כל עוד אתם נותנים למשתמש \"crafty\" הרשאת קריאה / כתיבה לנתיב השרת.",
"<br />",
"<br />",
"בלינוקס זה נעשה בצורה הטובה ביותר על ידי ביצוע הפעולות הבאות:<br />",
"<code>",
" sudo chown crafty:crafty /path/to/your/server -R<br />",
" sudo chmod 2775 /path/to/your/server -R<br />",
"</code>"
],
"title": "אזור הגדרת השרת"
},
"serverDetails": {
"backup": "גיבוי",
"config": "הגדרות",
"files": "קבצים",
"logs": "לוג",
"playerControls": "ניהול שחקנים",
"schedule": "לוח זמנים",
"serverDetails": "פרטי שרת",
"terminal": "מסוף פקודות"
},
"serverFiles": {
"clickUpload": "לחץ כאן כדי לבחור את הקבצים שלך",
"close": "סגור",
"createDir": "ליצור תיקייה",
"createDirQuestion": "איזה שם אתם רוצים לספרייה החדשה?",
"createFile": "צור קובץ",
"createFileQuestion": "איזה שם אתם רוצים לקובץ החדש?",
"default": "ברירת מחדל",
"delete": "למחוק",
"deleteItemQuestion": "Are you sure you want to delete \" + name + \"?",
"deleteItemQuestionMessage": "You are deleting \\\"\" + path + \"\\\"!<br/><br/>This action will be irreversible and it'll be lost forever!",
"download": "הורדה",
"editingFile": "עריכת קובץ",
"error": "שגיאה בעת קבלת קבצים",
"fileReadError": "שגיאת קריאת קובץ",
"files": "קבצים",
"keybindings": "מקשי קיצור",
"loadingRecords": "טוען קבצים...",
"noDelete": "לא",
"noscript": "מנהל הקבצים לא עובד ללא JavaScript",
"rename": "שנה שם",
"renameItemQuestion": "מה צריך להיות השם החדש?",
"save": "שמור",
"stayHere": "אל תצאומדף הזה!!",
"unsupportedLanguage": "אזהרה: סוג קובץ נתמך",
"unzip": "פתיחת קובץ מכווץ",
"upload": "העלה",
"uploadTitle": "העלת קבצים אל: ",
"waitUpload": "אנא המתן בזמן שאנו מעלים את הקבצים שלך... זה עשוי לקחת זמן מה.",
"yesDelete": "כן, אני מבין.ה את ההשלכות"
},
"serverPlayerManagement": {
"bannedPlayers": "שחקנים בבאן",
"loadingBannedPlayers": "טוען שחקנים שהם בבאן",
"players": "שחקנים"
},
"serverScheduleConfig": {
"backup": "גיבוי שרת",
"basic": "בסיסי",
"children": "משימות מקושרות: ",
"command": "פקודה",
"command-explain": "איזו פקודה אתם רוצים שנבצע? אל תכללו את ה-'/'",
"cron": "קרון",
"cron-explain": "הזן את מחרוזת הקרון שלך",
"custom": "פקודה מותאמת אישית",
"days": "ימים",
"enabled": "מופעל",
"hours": "שעות",
"interval": "הפסקה",
"interval-explain": "באיזו תדירות אתם רוצים לשלוח זמנים אם זה יתבצע?",
"minutes": "דקות",
"offset": "קיזוז עיכוב",
"offset-explain": "כמה זמן עלינו לחכות כדי לשגר את זה לאחר שיגור המשימה הראשונה? (שניות)",
"one-time": "מחק לאחר ביצוע",
"parent": "בחר לוח זמנים להורים",
"parent-explain": "איזה לוח זמנים צריך להפעיל את זה?",
"reaction": "תגובה",
"restart": "הפעלה מחדש",
"start": "הדלקת שרת",
"stop": "כיבוי שרת",
"time": "זמן",
"time-explain": "באיזו שעה אתה רוצה שהלוח שלך יתבצע?"
},
"serverSchedules": {
"areYouSure": "למחוק משימה מתוזמנת?",
"cancel": "לבטל",
"cannotSee": "לא רואים הכל?",
"cannotSeeOnMobile": "נסה ללחוץ על משימה מתוזמנת לפרטים מלאים.",
"confirm": "אישור",
"confirmDelete": "האם ברצונך למחוק את המשימה המתוזמנת הזו? אי אפשר לבטל את זה."
},
"serverStats": {
"cpuUsage": "שימוש במעבד",
"description": "תיאור",
"errorCalculatingUptime": "שגיאה בחישוב זמן פעולה",
"memUsage": "שימוש בזיכרון",
"offline": "לא מקוון",
"online": "פועל",
"players": "שחקנים",
"serverStarted": "השרת התחיל",
"serverStatus": "סטטוס שרת",
"serverTime": "איזור זמן",
"serverTimeZone": "אזור זמן של שרת",
"serverUptime": "זמן פעילות שרת",
"starting": "התחלה מאוחרת",
"unableToConnect": "לא מצליח להתחבר",
"version": "גרסה"
},
"serverTerm": {
"commandInput": "הקלידו את הפקודה שלכם",
"delay-explained": "The service/agent has recently started and is delaying the start of the minecraft server instance",
"downloading": "מוריד...",
"restart": "הפעלה מחדש",
"sendCommand": "שליחת פקודה",
"start": "התחלה",
"starting": "התחלה מאוחרת",
"stop": "עצור",
"stopScroll": "לעצור את הגלילה האוטומטית",
"updating": "מתעדכן..."
},
"serverWizard": {
"absoluteServerPath": "נתיב מוחלט לשרת שלך",
"absoluteZipPath": "נתיב מוחלט לשרת שלך",
"addRole": "הוסף שרת לתפקידים קיימים",
"autoCreate": "אם אף אחד לא נבחר, Crafty תעשה אחד!",
"bePatient": "אנא התאזר בסבלנות בזמן שאנו ' + (מייבאים? 'ייבוא': 'מורידים') + ' השרת",
"buildServer": "בניית שרת!",
"clickRoot": "לחץ כאן כדי לבחור Root Dir",
"close": "סגור",
"defaultPort": "25565 ברירת מחדל",
"downloading": "מוריד שרת...",
"explainRoot": "אנא לחץ על הלחצן למטה כדי לבחור את ה-root dir של השרת שלך בתוך הארכיון",
"importing": "מייבא שרת...",
"importServer": "ייבוא שרת קיים",
"importServerButton": "ייבוא שרת!",
"importZip": "ייבוא מקובץ Zip",
"maxMem": "מקסימום זיכרון",
"minMem": "מינימום זיכרון",
"myNewServer": "השרת החדש שלי",
"newServer": "צור שרת חדש",
"quickSettings": "הגדרות מהירות",
"quickSettingsDescription": "אל תדאג, אתה יכול לשנות את אלה מאוחר יותר",
"resetForm": "אפס טופס",
"save": "שמור",
"selectRole": "בחר תפקידים",
"selectRoot": "בחר ארכיון שורש Dir",
"selectType": "בחר סוג",
"selectVersion": "בחר גרסה",
"selectZipDir": "בחר את הספרייה בארכיון שממנו אתה רוצה שנפתח קבצים",
"serverJar": "קובץ שרת להפעלה",
"serverName": "שם השרת",
"serverPath": "נתיב שרת",
"serverPort": "פורט שרת",
"serverType": "סוג השרת",
"serverVersion": "גרסת השרת",
"sizeInGB": "גודל ב-GB",
"zipPath": "נתיב שרת"
},
"sidebar": {
"contribute": "לתרום",
"credits": "קרדיט",
"dashboard": "פאנל",
"documentation": "ויקיפדייה",
"navigation": "ניווט",
"newServer": "צור שרת חדש",
"servers": "שרתים"
},
"userConfig": {
"apiKey": "מפתחות API",
"auth": "מורשה? ",
"config": "הגדרות",
"configArea": "אזור הגדרות משתמש",
"configAreaDesc": "כאן אתה משנה את כל הגדרות המשתמש שלך",
"confirmDelete": "האם אתה בטוח שברצונך למחוק משתמש זה? פעולה זו היא בלתי הפיכה.",
"craftyPermDesc": "הרשאות Crafty יש למשתמש הזה ",
"craftyPerms": "Crafty הרשאות: ",
"created": "נוצר: ",
"deleteUser": "מחק משתמש: ",
"deleteUserB": "מחק משתמש",
"delSuper": "אינך יכול למחוק משתמש-על",
"enabled": "מופעל",
"gravDesc": "אימייל זה מיועד אך ורק לשימוש עם Gravatar™. Crafty לא תשתמש בשום מקרה באימייל זה לשום דבר מלבד חיפוש Gravatar™ שלך",
"gravEmail": "אימייל Gravatar™",
"lastIP": "אייפי אחרון: ",
"lastLogin": "כניסה אחרונה: ",
"lastUpdate": "עדכון אחרון: ",
"leaveBlank": "כדי לערוך משתמש מבלי לשנות סיסמה השאר אותו ריק.",
"member": "חבר?",
"notExist": "אתה לא יכול למחוק משהו שלא קיים!",
"pageTitle": "ערוך משתמש",
"pageTitleNew": "צור משתמש",
"password": "סיסמה",
"permName": "שם הרשאה",
"repeat": "חזור על הסיסמה",
"roleName": "שם התפקיד",
"super": "משתמש על",
"userLang": "שפת משתמש",
"userName": "שם משתמש",
"userNameDesc": "איך אתה רוצה לקרוא למשתמש הזה?",
"userRoles": "תפקידי משתמש",
"userRolesDesc": "תפקידים שמשתמש זה חבר בהם",
"userSettings": "הגדרות משתמש",
"uses": "מספר השימושים המותרים (-1==ללא הגבלה)"
}
}

View File

@ -39,11 +39,11 @@
"hugeDesc": "Veliko",
"pageDescription": "Bez ovih ljudi ne biste imali Crafty",
"pageTitle": "Zasluge",
"patreonDesc": "našim Patreon donorima!",
"patreonLevel": "Razina",
"patreonName": "Ime",
"patreonDesc": "našim Patreon / Ko-fi donorima!",
"subscriptionLevel": "Razina",
"subscriberName": "Ime",
"patreonOther": "Ostalo",
"patreonSupporter": "Patreon donorima",
"patreonSupporter": "Patreon / Ko-fi donorima",
"patreonUpdate": "Zadnje ažuriranje:",
"retiredStaff": "Umirovljeno osoblje",
"supportTeam": "Tim za podršku i dokumentaciju",

View File

@ -39,11 +39,11 @@
"hugeDesc": "Besar",
"pageDescription": "Tanpa orang-orang ini, Anda tidak akan memiliki Crafty",
"pageTitle": "Kredit",
"patreonDesc": "untuk pendukung Patreon kami!",
"patreonLevel": "Level",
"patreonName": "Nama",
"patreonDesc": "untuk pendukung Patreon / Ko-fi kami!",
"subscriptionLevel": "Level",
"subscriberName": "Nama",
"patreonOther": "Lainnya",
"patreonSupporter": "Suporter Patreon",
"patreonSupporter": "Suporter Patreon / Ko-fi",
"patreonUpdate": "Pembaharuan Terakhir:",
"retiredStaff": "Pensiunan Staf",
"supportTeam": "Tim Dukungan dan Dokumentasi",

View File

@ -39,11 +39,11 @@
"hugeDesc": "Un enorme",
"pageDescription": "Senza queste persone, non avremmo Crafty",
"pageTitle": "Crediti",
"patreonDesc": "ai nostri supporter di Patreon!",
"patreonLevel": "Livello",
"patreonName": "Nome",
"patreonDesc": "ai nostri supporter di Patreon / Ko-fi!",
"subscriptionLevel": "Livello",
"subscriberName": "Nome",
"patreonOther": "Altro",
"patreonSupporter": "Supporter Patreon",
"patreonSupporter": "Supporter Patreon / Ko-fi",
"patreonUpdate": "Ultimo aggiornamento:",
"retiredStaff": "Staff ritirato",
"supportTeam": "Squadra di supporto e documentazione",

View File

@ -39,11 +39,11 @@
"hugeDesc": "A HOOJ",
"pageDescription": "WITHOUT THEES PEEPS, U WOULDNT HAS CWAFTY",
"pageTitle": "GUD HOOMANZ",
"patreonDesc": "2 DA PATREON SUPPORTERS!",
"patreonLevel": "LVLZ",
"patreonName": "NAMEZ",
"patreonDesc": "2 DA PATREUN UN KOFEE SUPPORTERS!",
"subscriptionLevel": "LVLZ",
"subscriberName": "NAMEZ",
"patreonOther": "OTHERZ",
"patreonSupporter": "PATREON SUPPORTERS",
"patreonSupporter": "PATREUN UN KOFEE SUPPORTERS",
"patreonUpdate": "LAST UPDATE:",
"retiredStaff": "DISTANT PPLZ, NEVR FORGOTTEN",
"supportTeam": "THEES PEEOPLE FED AN CARE 4 ME, THEY VRY VRY GUD PPL",

View File

@ -39,11 +39,11 @@
"hugeDesc": "Liels",
"pageDescription": "Bez šiem cilvēkiem, jums nebūtu Crafty",
"pageTitle": "Pateicības",
"patreonDesc": "mūsu Patreon atbalstītājiem!",
"patreonLevel": "Līmenis",
"patreonName": "Vārds",
"patreonDesc": "mūsu Patreon / Ko-fi atbalstītājiem!",
"subscriptionLevel": "Līmenis",
"subscriberName": "Vārds",
"patreonOther": "Cits",
"patreonSupporter": "Patreon Atbalstītāji",
"patreonSupporter": "Patreon / Ko-fi Atbalstītāji",
"patreonUpdate": "Pēdējais Atjaunojums:",
"retiredStaff": "Atvaļinātie Komandas Biedri",
"supportTeam": "Atbalsta un Dokumentācijas Komanda",

View File

@ -39,11 +39,11 @@
"hugeDesc": "Een enorme",
"pageDescription": "Zonder deze mensen, zou je Crafty niet hebben",
"pageTitle": "Credits",
"patreonDesc": "aan onze Patreon supporters!",
"patreonLevel": "Niveau",
"patreonName": "Naam",
"patreonDesc": "aan onze Patreon / Ko-fi supporters!",
"subscriptionLevel": "Niveau",
"subscriberName": "Naam",
"patreonOther": "Andere",
"patreonSupporter": "Patreon supporters",
"patreonSupporter": "Patreon / Ko-fi supporters",
"patreonUpdate": "Laatste Update:",
"retiredStaff": "Gepensioneerd personeel",
"supportTeam": "Ondersteunings- en documentatieteam",

View File

@ -39,11 +39,11 @@
"hugeDesc": "Een gigantische",
"pageDescription": "Zonder deze mensen zou er geen Crafty zijn",
"pageTitle": "Credits",
"patreonDesc": "aan onze Patreon supporters!",
"patreonLevel": "Niveau",
"patreonName": "Naam",
"patreonDesc": "aan onze Patreon / Ko-fi supporters!",
"subscriptionLevel": "Niveau",
"subscriberName": "Naam",
"patreonOther": "Overig",
"patreonSupporter": "Patreon Supporters",
"patreonSupporter": "Patreon / Ko-fi Supporters",
"patreonUpdate": "Laatste update:",
"retiredStaff": "Gepensioneerde staff",
"supportTeam": "Support- en documentatieteam",

View File

@ -39,11 +39,11 @@
"hugeDesc": "非常",
"pageDescription": "没有这些人,就没有 Crafty",
"pageTitle": "鸣谢",
"patreonDesc": "我们的 Patreon 支持者!",
"patreonLevel": "等级",
"patreonName": "名称",
"patreonDesc": "我们的 Patreon / Ko-fi 支持者!",
"subscriptionLevel": "等级",
"subscriberName": "名称",
"patreonOther": "其他",
"patreonSupporter": "Patreon 支持者",
"patreonSupporter": "Patreon / Ko-fi 支持者",
"patreonUpdate": "上次更新:",
"retiredStaff": "退休员工",
"supportTeam": "支持与文档团队",

View File

@ -1,5 +1,5 @@
#Base config made by Justman10000 and Zedifus (https://gitlab.com/Zedifus)
#Adapted for WSS by Andrew McManus https://gitlab.com/amcmanu3
#Adapted for WSS by pretzelDewey https://gitlab.com/amcmanu3
#For this config you need to add the following mods
#mod_ssl
#mod_rewrite

View File

@ -17,5 +17,5 @@ requests==2.26
termcolor==1.1
tornado==6.0
tzlocal==4.0
jsonschema==4.4.0
jsonschema==4.5.1
orjson==3.6.7