Merge branch 'dev' into 'master'

Alpha 3.5

See merge request crafty-controller/crafty-commander!150
This commit is contained in:
Iain Powrie 2022-03-08 05:54:26 +00:00
commit 9db35445bc
134 changed files with 19395 additions and 6618 deletions

View File

@ -1,8 +1,19 @@
# docker related
docker/
.dockerignore
Dockerfile
docker-compose.yml
# git & gitlab related
.git/
.gitignore
.gitlab-ci.yml
# root
.editorconfig
.pylintrc
.venv
.vscode
crafty_commander.exe
DBCHANGES.md
docker-compose.yml.example

16
.editorconfig Normal file
View File

@ -0,0 +1,16 @@
# https://editorconfig.org
root = true
[*.{js,py,html}]
charset = utf-8
insert_final_newline = true
# end_of_line = lf
[*.py]
indent_style = space
indent_size = 4
[*.{js,html}]
indent_style = space
indent_size = 2

View File

@ -1,15 +1,47 @@
stages:
- win-dev
- win-prod
- docker-dev
- docker-prod
- test
- prod-deployment
- dev-deployment
variables:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
pylint:
stage: test
image: python:3.7-slim
services:
- name: docker:dind
tags:
- 'docker_testers'
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS'
when: never
before_script:
- mkdir -p public/badges public/lint
- echo undefined > public/badges/$CI_JOB_NAME.score
- pip install pylint-gitlab
script:
- pylint --exit-zero --output-format=text $(find -type f -name "*.py" ! -path "**/.venv/**" ! -path "**/app/migrations/**") | tee /tmp/pylint.txt
- sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' /tmp/pylint.txt > public/badges/$CI_JOB_NAME.score
- pylint --exit-zero --output-format=pylint_gitlab.GitlabCodeClimateReporter $(find -type f -name "*.py" ! -path "**/.venv/**" ! -path "**/app/migrations/**") > codeclimate.json
after_script:
- anybadge --overwrite --label $CI_JOB_NAME --value=$(cat public/badges/$CI_JOB_NAME.score) --file=public/badges/$CI_JOB_NAME.svg 4=red 6=orange 8=yellow 10=green
- |
echo "Your score is: $(cat public/badges/$CI_JOB_NAME.score)"
artifacts:
paths:
- public
reports:
codequality: codeclimate.json
when: always
docker-build-dev:
image: docker:latest
services:
- name: docker:dind
command: ["--experimental"]
stage: docker-dev
stage: dev-deployment
tags:
- docker
rules:
@ -26,13 +58,15 @@ docker-build-dev:
mkdir -p ~/.docker/cli-plugins
mv docker-buildx ~/.docker/cli-plugins/docker-buildx
docker version
- docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
- docker run --rm --privileged aptman/qus -- -r
- docker run --rm --privileged aptman/qus -s -- -p aarch64 x86_64
- echo $CI_BUILD_TOKEN | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
script:
- |
tag=":$CI_COMMIT_REF_SLUG"
echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag"
- docker buildx create --use --name zedBuilder
- docker context create tls-environment
- docker buildx create --name zedBuilder --use tls-environment
- docker buildx build
--cache-from type=registry,ref="$CI_REGISTRY_IMAGE${tag}"
--build-arg BUILDKIT_INLINE_CACHE=1
@ -42,6 +76,7 @@ docker-build-dev:
after_script:
- |
docker buildx rm zedBuilder && echo "Successfully Stopped builder instance" || echo "Failed to stop builder instance."
docker context rm tls-environment || true
echo "Please review multi-arch manifests are present:"
docker buildx imagetools inspect "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
@ -49,8 +84,7 @@ docker-build-prod:
image: docker:latest
services:
- name: docker:dind
command: ["--experimental"]
stage: docker-prod
stage: prod-deployment
tags:
- docker
rules:
@ -67,13 +101,15 @@ docker-build-prod:
mkdir -p ~/.docker/cli-plugins
mv docker-buildx ~/.docker/cli-plugins/docker-buildx
docker version
- docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
- docker run --rm --privileged aptman/qus -- -r
- docker run --rm --privileged aptman/qus -s -- -p aarch64 x86_64
- echo $CI_BUILD_TOKEN | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
script:
- |
tag=""
echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'"
- docker buildx create --use --name zedBuilder
- docker context create tls-environment
- docker buildx create --name zedBuilder --use tls-environment
- docker buildx build
--cache-from type=registry,ref="$CI_REGISTRY_IMAGE${tag}"
--build-arg BUILDKIT_INLINE_CACHE=1
@ -83,11 +119,12 @@ docker-build-prod:
after_script:
- |
docker buildx rm zedBuilder && echo "Successfully Stopped builder instance" || echo "Failed to stop builder instance."
docker context rm tls-environment || true
echo "Please review multi-arch manifests are present:"
docker buildx imagetools inspect "$CI_REGISTRY_IMAGE${tag}"
win-dev-build:
stage: win-dev
stage: dev-deployment
tags:
- win64
cache:
@ -111,7 +148,14 @@ win-dev-build:
--paths .venv\Lib\site-packages
--hidden-import cryptography
--hidden-import cffi
--hidden-import apscheduler
--collect-all tzlocal
--collect-all tzdata
--collect-all pytz
--collect-all six
artifacts:
name: "crafty-${CI_RUNNER_TAGS}-${CI_COMMIT_BRANCH}_${CI_COMMIT_SHORT_SHA}"
paths:
- app\
- .\crafty_commander.exe
@ -121,7 +165,7 @@ win-dev-build:
# | https://gitlab.com/crafty-controller/crafty-commander/-/jobs/artifacts/dev/download?job=win-dev-build
win-prod-build:
stage: win-prod
stage: prod-deployment
tags:
- win64
cache:
@ -145,7 +189,14 @@ win-prod-build:
--paths .venv\Lib\site-packages
--hidden-import cryptography
--hidden-import cffi
--hidden-import apscheduler
--collect-all tzlocal
--collect-all tzdata
--collect-all pytz
--collect-all six
artifacts:
name: "crafty-${CI_RUNNER_TAGS}-${CI_COMMIT_BRANCH}_${CI_COMMIT_SHORT_SHA}"
paths:
- app\
- .\crafty_commander.exe

603
.pylintrc Normal file
View File

@ -0,0 +1,603 @@
[MASTER]
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-allow-list=
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
# for backward compatibility.)
extension-pkg-whitelist=
# Return non-zero exit code if any of these messages/categories are detected,
# even if score is above --fail-under value. Syntax same as enable. Messages
# specified are enabled, while categories only check already-enabled messages.
fail-on=
# Specify a score threshold to be exceeded before program exits with error.
fail-under=10.0
# Files or directories to be skipped. They should be base names, not paths.
ignore=
# Add files or directories matching the regex patterns to the ignore-list. The
# regex matches against paths and can be in Posix or Windows format.
ignore-paths=app/migrations, app/classes/shared/migration.py
# Files or directories matching the regex patterns are skipped. The regex
# matches against base names, not paths.
ignore-patterns=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use.
jobs=0
# Control the amount of potential inferred values when inferring a single
# object. This can help the performance when dealing with large functions or
# complex, nested conditions.
limit-inference-results=100
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# Minimum Python version to use for version dependent checks. Will default to
# the version used to run pylint.
py-version=3.9
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
confidence=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=abstract-method,
attribute-defined-outside-init,
bad-inline-option,
bare-except,
broad-except,
cell-var-from-loop,
consider-iterating-dictionary,
consider-using-with,
deprecated-pragma,
duplicate-code,
file-ignored,
fixme,
import-error,
inconsistent-return-statements,
invalid-name,
locally-disabled,
logging-format-interpolation,
logging-fstring-interpolation,
logging-not-lazy,
missing-docstring,
no-else-break,
no-else-continue,
no-else-return,
no-self-use,
no-value-for-parameter,
not-an-iterable,
protected-access,
simplifiable-condition,
simplifiable-if-statement,
suppressed-message,
too-few-public-methods,
too-many-arguments,
too-many-branches,
too-many-instance-attributes,
too-many-locals,
too-many-nested-blocks,
too-many-public-methods,
too-many-return-statements,
too-many-statements,
use-symbolic-message-instead,
useless-suppression,
raw-checker-failed
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=c-extension-no-member
[REPORTS]
# Python expression which should return a score less than or equal to 10. You
# have access to the variables 'error', 'warning', 'refactor', and 'convention'
# which contain the number of messages in each category, as well as 'statement'
# which is the total number of statements analyzed. This score is used by the
# global evaluation report (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details.
#msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio). You can also give a reporter class, e.g.
# mypackage.mymodule.MyReporterClass.
output-format=text
# Tells whether to display a full report or only the messages.
reports=no
# Activate the evaluation score.
score=yes
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit,argparse.parse_error
[BASIC]
# Naming style matching correct argument names.
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style.
#argument-rgx=
# Naming style matching correct attribute names.
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style.
#attr-rgx=
# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Bad variable names regexes, separated by a comma. If names match any regex,
# they will always be refused
bad-names-rgxs=
# Naming style matching correct class attribute names.
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style.
#class-attribute-rgx=
# Naming style matching correct class constant names.
class-const-naming-style=UPPER_CASE
# Regular expression matching correct class constant names. Overrides class-
# const-naming-style.
#class-const-rgx=
# Naming style matching correct class names.
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-
# style.
#class-rgx=
# Naming style matching correct constant names.
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style.
#const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names.
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style.
#function-rgx=
# Good variable names which should always be accepted, separated by a comma.
good-names=i,
j,
k,
ex,
Run,
_
# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
good-names-rgxs=
# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style.
#inlinevar-rgx=
# Naming style matching correct method names.
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style.
#method-rgx=
# Naming style matching correct module names.
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style.
#module-rgx=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty
# Naming style matching correct variable names.
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style.
#variable-rgx=
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=150
# Maximum number of lines in a module.
max-module-lines=2000
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[LOGGING]
# The type of string formatting that logging methods do. `old` means using %
# formatting, `new` is for `{}` formatting.
logging-format-style=old
# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules=logging
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
# Regular expression of note tags to take in consideration.
#notes-rgx=
[SIMILARITIES]
# Comments are removed from the similarity computation
ignore-comments=yes
# Docstrings are removed from the similarity computation
ignore-docstrings=yes
# Imports are removed from the similarity computation
ignore-imports=no
# Signatures are removed from the similarity computation
ignore-signatures=no
# Minimum lines number of a similarity.
min-similarity-lines=4
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4
# Spelling dictionary name. Available dictionaries: none. To make it work,
# install the 'python-enchant' package.
spelling-dict=
# List of comma separated words that should be considered directives if they
# appear and the beginning of a comment and should not be checked.
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains the private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to the private dictionary (see the
# --spelling-private-dict-file option) instead of raising a message.
spelling-store-unknown-words=no
[STRING]
# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=no
# This flag controls whether the implicit-str-concat should generate a warning
# on implicit string concatenation in sequences defined over several lines.
check-str-concat-over-line-jumps=no
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=os.*
# Tells whether missing members accessed in mixin class should be ignored. A
# class is considered mixin if its name matches the mixin-class-rgx option.
ignore-mixin-members=yes
# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local
# List of module names for which member attributes should not be checked
# (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=
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
# Regex pattern to define which classes are considered mixins ignore-mixin-
# members is set to 'yes'
mixin-class-rgx=.*[Mm]ixin
# List of decorators that change the signature of a decorated function.
signature-mutators=
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of names allowed to shadow builtins
allowed-redefined-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expected to
# not be used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored. Default to name
# with leading underscore.
ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
[CLASSES]
# Warn about protected attribute access inside special methods
check-protected-access-in-special-methods=no
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp,
__post_init__
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=cls
[DESIGN]
# List of regular expressions of class ancestor names to ignore when counting
# public methods (see R0903)
exclude-too-few-public-methods=
# List of qualified class names to ignore when counting class parents (see
# R0901)
ignored-parents=
# Maximum number of arguments for function / method.
max-args=8
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Maximum number of boolean expressions in an if statement (see R0916).
max-bool-expr=5
# Maximum number of branch for function / method body.
max-branches=12
# Maximum number of locals for function / method body.
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body.
max-returns=6
# Maximum number of statements in function / method body.
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[IMPORTS]
# List of modules that can be imported at any level, not just the top level
# one.
allow-any-import-level=
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=
# Output a graph (.gv or any supported image format) of external dependencies
# to the given file (report RP0402 must not be disabled).
ext-import-graph=
# Output a graph (.gv or any supported image format) of all (i.e. internal and
# external) dependencies to the given file (report RP0402 must not be
# disabled).
import-graph=
# Output a graph (.gv or any supported image format) of internal dependencies
# to the given file (report RP0402 must not be disabled).
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
# Couples of modules and preferred modules, separated by a comma.
preferred-modules=
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "BaseException, Exception".
overgeneral-exceptions=BaseException,
Exception

View File

@ -1,25 +1,48 @@
FROM python:alpine
FROM ubuntu:20.04
ENV DEBIAN_FRONTEND="noninteractive"
LABEL maintainer="Dockerfile created by Zedifus <https://gitlab.com/zedifus>"
# Security Patch for CVE-2021-44228
ENV LOG4J_FORMAT_MSG_NO_LOOKUPS=true
# Install Packages, Build Dependencies & Garbage Collect & Harden
# (Alpine Edge repo is needed because jre16 is new)
COPY requirements.txt /commander/requirements.txt
RUN apk add --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/community llvm11-libs openssl-dev rust cargo gcc musl-dev libffi-dev make openjdk8-jre-base openjdk11-jre-headless openjdk16-jre-headless mariadb-dev \
&& pip3 install --no-cache-dir -r /commander/requirements.txt \
&& apk del --no-cache gcc musl-dev libffi-dev make rust cargo openssl-dev llvm11-libs \
&& rm -rf /sbin/apk \
&& rm -rf /etc/apk \
&& rm -rf /lib/apk \
&& rm -rf /usr/share/apk \
&& rm -rf /var/lib/apk
# Create non-root user & required dirs
RUN useradd -g root -M crafty \
&& mkdir /commander \
&& chown -R crafty:root /commander
# Copy Source & copy default config from image
COPY ./ /commander
# Install required system packages
RUN apt-get update \
&& apt-get -y --no-install-recommends install \
sudo \
gcc \
python3 \
python3-dev \
python3-pip \
python3-venv \
libmariadb-dev \
default-jre \
openjdk-8-jre-headless \
openjdk-11-jre-headless \
openjdk-16-jre-headless \
openjdk-17-jre-headless \
&& apt-get autoremove \
&& apt-get clean
# Switch to service user for installing crafty deps
USER crafty
WORKDIR /commander
COPY --chown=crafty:root requirements.txt ./
RUN python3 -m venv ./.venv \
&& . .venv/bin/activate \
&& pip3 install --no-cache-dir --upgrade setuptools==50.3.2 pip==22.0.3 \
&& pip3 install --no-cache-dir -r requirements.txt \
&& deactivate
USER root
# Copy Source w/ perms & prepare default config from example
COPY --chown=crafty:root ./ ./
RUN mv ./app/config ./app/config_original \
&& mv ./app/config_original/default.json.example ./app/config_original/default.json \
&& chmod +x ./docker_launcher.sh

View File

@ -8,7 +8,7 @@ a web interface for the server administrators to interact with their servers. Cr
is compatible with Docker, Linux, Windows 7, Windows 8 and Windows 10.
## Documentation
Temporary documentation available on [GitLab](https://gitlab.com/crafty-controller/crafty-commander/wikis/home)
Documentation available on [wiki.craftycontrol.com](https://craftycontrol.com)
## Meta
Project Homepage - https://craftycontrol.com
@ -17,15 +17,35 @@ Discord Server - https://discord.gg/9VJPhCE
Git Repository - https://gitlab.com/crafty-controller/crafty-web
## Basic Docker Usage
<br>
**To get started with docker**, all you need to do is pull the image from this git repository's registry.
This is done by using `docker-compose` or `docker run`(You don't need to clone the Repository and build, like in 3.x ).
## Basic Docker Usage 🐳
If you have a config folder already from previous local installation or docker setup, the image should mount this volume, if none is present then it will populate its own config folder for you.
With `Crafty Controller 4.0` we have focused on building our DevOps Principles, implementing build automation, and securing our containers, with the hopes of making our Container user's lives abit easier.
### Using the registry image:
The provided image supports both `arm64` and `amd64` out the box, if you have issues though you can build it yourself.
### - Two big changes you will notice is:
- We now provide pre-built images for you guys.
- Containers now run as non-root, using practices used by OpenShift & Kubernetes (root group perms).
> __**⚠ 🔻WARNING: [WSL/WSL2 | WINDOWS 11 | DOCKER DESKTOP]🔻**__ <br>
BE ADVISED! Upstream is currently broken for Minecraft running on **Docker under WSL/WSL2, Windows 11 / DOCKER DESKTOP!** <br>
On '**Stop**' or '**Restart**' of the MC Server, there is a 90% chance the World's Chunks will be shredded irreparably! <br>
Please only run Docker on Linux, If you are using Windows we have a portable installs found here: [Latest-Stable](https://gitlab.com/crafty-controller/crafty-commander/-/jobs/artifacts/master/download?job=win-prod-build), [Latest-Development](https://gitlab.com/crafty-controller/crafty-commander/-/jobs/artifacts/dev/download?job=win-dev-build)
----
### - To get started with docker 🛫
All you need to do is pull the image from this git repository's registry.
This is done by using `'docker-compose'` or `'docker run'` (You don't need to clone the Repository and build, like in 3.x ).
If you have a config folder already from previous local installation or _docker setup_*, the image should mount this volume and fix the permission as required, if no config present then it will populate its own config folder for you. <br> <br>
As the Dockerfile uses the permission structure of `crafty:root` **internally** there is no need to worry about matching the `UID` or `GID` on the host system :)
<br>
### - Using the registry image 🌎
The provided image supports both `arm64` and `amd64` out the box, if you have issues though you can build it yourself with the `compose` file in `docker/`.
The image is located at: `registry.gitlab.com/crafty-controller/crafty-commander:latest`
| Branch | Status |
@ -56,7 +76,11 @@ $ cat ~/my_password.txt | docker login registry.gitlab.com -u <username> --passw
```
Then use one of the following methods:
#### docker-compose.yml
### **docker-compose.yml:**
```sh
# Make your compose file
$ vim docker-compose.yml
```
```yml
version: '3'
@ -64,20 +88,27 @@ services:
crafty:
container_name: crafty_commander
image: registry.gitlab.com/crafty-controller/crafty-commander:latest
environment:
- TZ=Etc/UTC
ports:
- "8000:8000" # HTTP
- "8443:8443" # HTTPS
- "8123:8123" # DYNMAP
- "19132:19132/udp" # BEDROCK
- "24000-25600:24000-25600" # MC SERV PORT RANGE
- "25500-25600:25500-25600" # MC SERV PORT RANGE
volumes:
- ./docker/backups:/commander/backups
- ./docker/logs:/commander/logs
- ./docker/servers:/commander/servers
- ./docker/config:/commander/app/config
- ./docker/import:/commander/import
```
```sh
$ docker-compose up -d && docker-compose logs -f
```
<br>
#### docker run
### **docker run:**
```sh
$ docker run \
--name crafty_commander \
@ -85,15 +116,17 @@ $ docker run \
-p 8443:8443 \
-p 8123:8123 \
-p 19132:19132/udp \
-p 24000-25600:24000-25600 \
-p 25500-25600:25500-25600 \
-e TZ=Etc/UTC \
-v "/$(pwd)/docker/backups:/commander/backups" \
-v "/$(pwd)/docker/logs:/commander/logs" \
-v "/$(pwd)/docker/servers:/commander/servers" \
-v "/$(pwd)/docker/config:/commander/app/config" \
-v "/$(pwd)/docker/import:/commander/import" \
registry.gitlab.com/crafty-controller/crafty-commander:latest
```
### Building from the cloned repository:
### **Building from the cloned repository:**
If you are building from `docker-compose` you can find the compose file in `./docker/docker-compose.yml` just `cd` to the docker directory and `docker-compose up -d`
@ -108,11 +141,13 @@ $ docker run \
-p 8443:8443 \
-p 8123:8123 \
-p 19132:19132/udp \
-p 24000-25600:24000-25600 \
-p 25500-25600:25500-25600 \
-e TZ=Etc/UTC \
-v "/$(pwd)/docker/backups:/commander/backups" \
-v "/$(pwd)/docker/logs:/commander/logs" \
-v "/$(pwd)/docker/servers:/commander/servers" \
-v "/$(pwd)/docker/config:/commander/app/config" \
-v "/$(pwd)/docker/import:/commander/import" \
crafty
```
A fresh build will take several minutes depending on your system, but will be rapid thereafter.

View File

@ -1,23 +1,7 @@
import os
import time
import logging
import sys
import yaml
import asyncio
import shutil
import tempfile
import zipfile
from distutils import dir_util
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
from app.classes.models.crafty_permissions import crafty_permissions, Enum_Permissions_Crafty
from app.classes.shared.server import Server
from app.classes.minecraft.server_props import ServerProps
from app.classes.minecraft.serverjars import server_jar_obj
from app.classes.minecraft.stats import Stats
from app.classes.models.users import ApiKeys
logger = logging.getLogger(__name__)
@ -42,13 +26,13 @@ class Crafty_Perms_Controller:
return crafty_permissions.can_add_in_crafty(user_id, Enum_Permissions_Crafty.Server_Creation)
@staticmethod
def can_add_user(user_id):
def can_add_user(): # Add back argument 'user_id' when you work on this
#TODO: Complete if we need a User Addition limit
#return crafty_permissions.can_add_in_crafty(user_id, Enum_Permissions_Crafty.User_Config)
return True
@staticmethod
def can_add_role(user_id):
def can_add_role(): # Add back argument 'user_id' when you work on this
#TODO: Complete if we need a Role Addition limit
#return crafty_permissions.can_add_in_crafty(user_id, Enum_Permissions_Crafty.Roles_Config)
return True
@ -70,3 +54,7 @@ class Crafty_Perms_Controller:
@staticmethod
def add_server_creation(user_id):
return crafty_permissions.add_server_creation(user_id)
@staticmethod
def get_api_key_permissions_list(key: ApiKeys):
return crafty_permissions.get_api_key_permissions_list(key)

View File

@ -1,25 +1,8 @@
import os
import time
import logging
import sys
import yaml
import asyncio
import shutil
import tempfile
import zipfile
from distutils import dir_util
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
from app.classes.models.management import management_helper
from app.classes.models.servers import servers_helper
from app.classes.shared.server import Server
from app.classes.minecraft.server_props import ServerProps
from app.classes.minecraft.serverjars import server_jar_obj
from app.classes.minecraft.stats import Stats
logger = logging.getLogger(__name__)
class Management_Controller:
@ -31,10 +14,6 @@ class Management_Controller:
def get_latest_hosts_stats():
return management_helper.get_latest_hosts_stats()
@staticmethod
def new_api_token():
return management_helper.new_api_token()
#************************************************************************************************
# Commands Methods
#************************************************************************************************
@ -44,13 +23,10 @@ class Management_Controller:
@staticmethod
def send_command(user_id, server_id, remote_ip, command):
server_name = servers_helper.get_server_friendly_name(server_id)
# Example: Admin issued command start_server for server Survival
management_helper.add_to_audit_log(user_id, "issued command {} for server {}".format(command, server_name),
server_id, remote_ip)
management_helper.add_to_audit_log(user_id, f"issued command {command} for server {server_name}", server_id, remote_ip)
management_helper.add_command(server_id, user_id, remote_ip, command)
@staticmethod
@ -77,7 +53,16 @@ class Management_Controller:
#************************************************************************************************
@staticmethod
def create_scheduled_task(server_id, action, interval, interval_type, start_time, command, comment=None, enabled=True):
return management_helper.create_scheduled_task(server_id, action, interval, interval_type, start_time, command, comment, enabled)
return management_helper.create_scheduled_task(
server_id,
action,
interval,
interval_type,
start_time,
command,
comment,
enabled
)
@staticmethod
def delete_scheduled_task(schedule_id):
@ -91,6 +76,14 @@ class Management_Controller:
def get_scheduled_task(schedule_id):
return management_helper.get_scheduled_task(schedule_id)
@staticmethod
def get_scheduled_task_model(schedule_id):
return management_helper.get_scheduled_task_model(schedule_id)
@staticmethod
def get_child_schedules(sch_id):
return management_helper.get_child_schedules(sch_id)
@staticmethod
def get_schedules_by_server(server_id):
return management_helper.get_schedules_by_server(server_id)
@ -111,5 +104,17 @@ class Management_Controller:
return management_helper.get_backup_config(server_id)
@staticmethod
def set_backup_config(server_id: int, backup_path: str = None, max_backups: int = None, auto_enabled: bool = True):
return management_helper.set_backup_config(server_id, backup_path, max_backups, auto_enabled)
def set_backup_config(server_id: int, backup_path: str = None, max_backups: int = None, excluded_dirs: list = None, compress: bool = False,):
return management_helper.set_backup_config(server_id, backup_path, max_backups, excluded_dirs, compress)
@staticmethod
def get_excluded_backup_dirs(server_id: int):
return management_helper.get_excluded_backup_dirs(server_id)
@staticmethod
def add_excluded_backup_dir(server_id: int, dir_to_add: str):
management_helper.add_excluded_backup_dir(server_id, dir_to_add)
@staticmethod
def del_excluded_backup_dir(server_id: int, dir_to_del: str):
management_helper.del_excluded_backup_dir(server_id, dir_to_del)

View File

@ -1,25 +1,9 @@
import os
import time
import logging
import sys
import yaml
import asyncio
import shutil
import tempfile
import zipfile
from distutils import dir_util
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
from app.classes.models.roles import roles_helper
from app.classes.models.server_permissions import server_permissions
from app.classes.models.users import users_helper
from app.classes.shared.server import Server
from app.classes.minecraft.server_props import ServerProps
from app.classes.minecraft.serverjars import server_jar_obj
from app.classes.minecraft.stats import Stats
from app.classes.shared.helpers import helper
logger = logging.getLogger(__name__)
@ -39,11 +23,12 @@ class Roles_Controller:
@staticmethod
def update_role(role_id, role_data={}, permissions_mask="00000000"):
def update_role(role_id: str, role_data = None, permissions_mask: str = "00000000"):
if role_data is None:
role_data = {}
base_data = Roles_Controller.get_role_with_servers(role_id)
up_data = {}
added_servers = set()
edited_servers = set()
removed_servers = set()
for key in role_data:
if key == "role_id":
@ -54,7 +39,7 @@ class Roles_Controller:
elif base_data[key] != role_data[key]:
up_data[key] = role_data[key]
up_data['last_update'] = helper.get_time_as_string()
logger.debug("role: {} +server:{} -server{}".format(role_data, added_servers, removed_servers))
logger.debug(f"role: {role_data} +server:{added_servers} -server{removed_servers}")
for server in added_servers:
server_permissions.get_or_create(role_id, server, permissions_mask)
for server in base_data['servers']:

View File

@ -1,32 +1,19 @@
import os
import time
import logging
import sys
import yaml
import asyncio
import shutil
import tempfile
import zipfile
from distutils import dir_util
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
from app.classes.shared.main_models import db_helper
from app.classes.models.server_permissions import server_permissions, Enum_Permissions_Server
from app.classes.models.users import users_helper
from app.classes.models.users import users_helper, ApiKeys
from app.classes.models.roles import roles_helper
from app.classes.models.servers import servers_helper
from app.classes.shared.server import Server
from app.classes.minecraft.server_props import ServerProps
from app.classes.minecraft.serverjars import server_jar_obj
from app.classes.minecraft.stats import Stats
from app.classes.shared.main_models import db_helper
logger = logging.getLogger(__name__)
class Server_Perms_Controller:
@staticmethod
def get_server_user_list(server_id):
return server_permissions.get_server_user_list(server_id)
@staticmethod
def list_defined_permissions():
permissions_list = server_permissions.get_permissions_list()
@ -42,15 +29,23 @@ class Server_Perms_Controller:
permissions_list = server_permissions.get_role_permissions_list(role_id)
return permissions_list
@staticmethod
def get_server_permissions_foruser(user_id, server_id):
permissions_list = server_permissions.get_user_permissions_list(user_id, server_id)
return permissions_list
@staticmethod
def add_role_server(server_id, role_id, rs_permissions="00000000"):
return server_permissions.add_role_server(server_id, role_id, rs_permissions)
@staticmethod
def get_server_roles(server_id):
return server_permissions.get_server_roles(server_id)
@staticmethod
def backup_role_swap(old_server_id, new_server_id):
role_list = server_permissions.get_server_roles(old_server_id)
for role in role_list:
server_permissions.add_role_server(
new_server_id, role.role_id,
server_permissions.get_permissions_mask(int(role.role_id), int(old_server_id)))
#server_permissions.add_role_server(new_server_id, role.role_id, '00001000')
#************************************************************************************************
# Servers Permissions Methods
#************************************************************************************************
@ -67,8 +62,17 @@ class Server_Perms_Controller:
return server_permissions.get_role_permissions_list(role_id)
@staticmethod
def get_user_permissions_list(user_id, server_id):
return server_permissions.get_user_permissions_list(user_id, server_id)
def get_user_id_permissions_list(user_id: str, server_id: str):
return server_permissions.get_user_id_permissions_list(user_id, server_id)
@staticmethod
def get_api_key_id_permissions_list(key_id: str, server_id: str):
key = users_helper.get_user_api_key(key_id)
return server_permissions.get_api_key_permissions_list(key, server_id)
@staticmethod
def get_api_key_permissions_list(key: ApiKeys, server_id: str):
return server_permissions.get_api_key_permissions_list(key, server_id)
@staticmethod
def get_authorized_servers_stats_from_roles(user_id):

View File

@ -1,29 +1,13 @@
from app.classes.controllers.roles_controller import Roles_Controller
import os
import time
import logging
import json
import sys
import yaml
import asyncio
import shutil
import tempfile
import zipfile
from distutils import dir_util
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
from app.classes.shared.main_models import db_helper
from app.classes.controllers.roles_controller import Roles_Controller
from app.classes.models.servers import servers_helper
from app.classes.models.roles import roles_helper
from app.classes.models.users import users_helper
from app.classes.models.users import users_helper, ApiKeys
from app.classes.models.server_permissions import server_permissions, Enum_Permissions_Server
from app.classes.shared.server import Server
from app.classes.minecraft.server_props import ServerProps
from app.classes.minecraft.serverjars import server_jar_obj
from app.classes.minecraft.stats import Stats
from app.classes.shared.helpers import helper
from app.classes.shared.main_models import db_helper
logger = logging.getLogger(__name__)
@ -33,8 +17,48 @@ class Servers_Controller:
# Generic Servers Methods
#************************************************************************************************
@staticmethod
def create_server(name: str, server_uuid: str, server_dir: str, backup_path: str, server_command: str, server_file: str, server_log_file: str, server_stop: str, server_port=25565):
return servers_helper.create_server(name, server_uuid, server_dir, backup_path, server_command, server_file, server_log_file, server_stop, server_port)
def create_server(
name: str,
server_uuid: str,
server_dir: str,
backup_path: str,
server_command: str,
server_file: str,
server_log_file: str,
server_stop: str,
server_type: str,
server_port=25565):
return servers_helper.create_server(
name,
server_uuid,
server_dir,
backup_path,
server_command,
server_file,
server_log_file,
server_stop,
server_type,
server_port)
@staticmethod
def get_server_obj(server_id):
return servers_helper.get_server_obj(server_id)
@staticmethod
def update_server(server_obj):
return servers_helper.update_server(server_obj)
@staticmethod
def set_download(server_id):
return servers_helper.set_download(server_id)
@staticmethod
def finish_download(server_id):
return servers_helper.finish_download(server_id)
@staticmethod
def get_download_status(server_id):
return servers_helper.get_download_status(server_id)
@staticmethod
def remove_server(server_id):
@ -73,6 +97,22 @@ class Servers_Controller:
def get_all_servers_stats():
return servers_helper.get_all_servers_stats()
@staticmethod
def get_authorized_servers_stats_api_key(api_key: ApiKeys):
server_data = []
authorized_servers = Servers_Controller.get_authorized_servers(api_key.user.user_id)
for s in authorized_servers:
latest = servers_helper.get_latest_server_stats(s.get('server_id'))
key_permissions = server_permissions.get_api_key_permissions_list(api_key, s.get('server_id'))
if Enum_Permissions_Server.Commands in key_permissions:
user_command_permission = True
else:
user_command_permission = False
server_data.append({'server_data': s, "stats": db_helper.return_rows(latest)[0],
"user_command_permission": user_command_permission})
return server_data
@staticmethod
def get_authorized_servers_stats(user_id):
server_data = []
@ -80,12 +120,18 @@ class Servers_Controller:
for s in authorized_servers:
latest = servers_helper.get_latest_server_stats(s.get('server_id'))
user_permissions = server_permissions.get_user_permissions_list(user_id, s.get('server_id'))
# TODO
user_permissions = server_permissions.get_user_id_permissions_list(user_id, s.get('server_id'))
if Enum_Permissions_Server.Commands in user_permissions:
user_command_permission = True
else:
user_command_permission = False
server_data.append({'server_data': s, "stats": db_helper.return_rows(latest)[0], "user_command_permission":user_command_permission})
server_data.append({
'server_data': s,
'stats': db_helper.return_rows(latest)[0],
'user_command_permission': user_command_permission
})
return server_data
@staticmethod
@ -104,17 +150,28 @@ class Servers_Controller:
return servers_helper.server_id_exists(server_id)
@staticmethod
def server_id_authorized(serverId, user_id):
authorized = 0
def get_server_type_by_id(server_id):
return servers_helper.get_server_type_by_id(server_id)
@staticmethod
def server_id_authorized(server_id_a, user_id):
user_roles = users_helper.user_role_query(user_id)
for role in user_roles:
authorized = server_permissions.get_role_servers_from_role_id(role.role_id)
#authorized = db_helper.return_rows(authorized)
if authorized.count() == 0:
return False
for server_id_b in server_permissions.get_role_servers_from_role_id(role.role_id):
if str(server_id_a) == str(server_id_b.server_id):
return True
return False
@staticmethod
def is_crashed(server_id):
return servers_helper.is_crashed(server_id)
@staticmethod
def server_id_authorized_api_key(server_id: str, api_key: ApiKeys) -> bool:
# TODO
return Servers_Controller.server_id_authorized(server_id, api_key.user.user_id)
# There is no view server permission
# permission_helper.both_have_perm(api_key)
@staticmethod
def set_update(server_id, value):
@ -136,6 +193,10 @@ class Servers_Controller:
def get_waiting_start(server_id):
return servers_helper.get_waiting_start(server_id)
@staticmethod
def get_update_status(server_id):
return servers_helper.get_update_status(server_id)
#************************************************************************************************
# Servers Helpers Methods
#************************************************************************************************
@ -146,7 +207,7 @@ class Servers_Controller:
path = os.path.join(server_path, 'banned-players.json')
try:
with open(path) as file:
with open(helper.get_os_understandable_path(path), encoding='utf-8') as file:
content = file.read()
file.close()
except Exception as ex:
@ -170,7 +231,6 @@ class Servers_Controller:
))
for log_file in log_files:
log_file_path = os.path.join(logs_path, log_file)
if self.check_file_exists(log_file_path) and \
self.is_file_older_than_x_days(log_file_path, logs_delete_after):
if helper.check_file_exists(log_file_path) and \
helper.is_file_older_than_x_days(log_file_path, logs_delete_after):
os.remove(log_file_path)

View File

@ -1,20 +1,10 @@
import os
import time
import logging
import sys
import yaml
import asyncio
import shutil
import tempfile
import zipfile
from distutils import dir_util
from typing import Optional
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
from app.classes.models.users import Users, users_helper
from app.classes.models.users import users_helper
from app.classes.models.crafty_permissions import crafty_permissions, Enum_Permissions_Crafty
from app.classes.models.management import management_helper
from app.classes.shared.helpers import helper
from app.classes.shared.authentication import authentication
logger = logging.getLogger(__name__)
@ -31,10 +21,6 @@ class Users_Controller:
def get_id_by_name(username):
return users_helper.get_user_id_by_name(username)
@staticmethod
def get_user_by_api_token(token: str):
return users_helper.get_user_by_api_token(token)
@staticmethod
def get_user_lang_by_id(user_id):
return users_helper.get_user_lang_by_id(user_id)
@ -43,26 +29,38 @@ class Users_Controller:
def get_user_by_id(user_id):
return users_helper.get_user(user_id)
@staticmethod
def update_server_order(user_id, user_server_order):
users_helper.update_server_order(user_id, user_server_order)
@staticmethod
def get_server_order(user_id):
return users_helper.get_server_order(user_id)
@staticmethod
def user_query(user_id):
return users_helper.user_query(user_id)
@staticmethod
def update_user(user_id, user_data={}, user_crafty_data={}):
def set_support_path(user_id, support_path):
users_helper.set_support_path(user_id, support_path)
@staticmethod
def update_user(user_id: str, user_data=None, user_crafty_data=None):
if user_crafty_data is None:
user_crafty_data = {}
if user_data is None:
user_data = {}
base_data = users_helper.get_user(user_id)
up_data = {}
added_roles = set()
removed_roles = set()
removed_servers = set()
for key in user_data:
if key == "user_id":
continue
elif key == "roles":
added_roles = user_data['roles'].difference(base_data['roles'])
removed_roles = base_data['roles'].difference(user_data['roles'])
elif key == "regen_api":
if user_data['regen_api']:
up_data['api_token'] = management_helper.new_api_token()
elif key == "password":
if user_data['password'] is not None and user_data['password'] != "":
up_data['password'] = helper.encode_pass(user_data['password'])
@ -70,16 +68,15 @@ class Users_Controller:
up_data[key] = user_data[key]
up_data['last_update'] = helper.get_time_as_string()
up_data['lang'] = user_data['lang']
logger.debug("user: {} +role:{} -role:{}".format(user_data, added_roles, removed_roles))
logger.debug(f"user: {user_data} +role:{added_roles} -role:{removed_roles}")
for role in added_roles:
users_helper.get_or_create(user_id=user_id, role_id=role)
# TODO: This is horribly inefficient and we should be using bulk queries but im going for functionality at this point
permissions_mask = user_crafty_data.get('permissions_mask', '000')
if 'server_quantity' in user_crafty_data:
limit_server_creation = user_crafty_data['server_quantity'][
Enum_Permissions_Crafty.Server_Creation.name]
for key in user_crafty_data:
if key == "permissions_mask":
permissions_mask = user_crafty_data['permissions_mask']
if key == "server_quantity":
limit_server_creation = user_crafty_data['server_quantity'][Enum_Permissions_Crafty.Server_Creation.name]
limit_user_creation = user_crafty_data['server_quantity'][Enum_Permissions_Crafty.User_Config.name]
limit_role_creation = user_crafty_data['server_quantity'][Enum_Permissions_Crafty.Roles_Config.name]
else:
@ -87,15 +84,20 @@ class Users_Controller:
limit_user_creation = 0
limit_role_creation = 0
crafty_permissions.add_or_update_user(user_id, permissions_mask, limit_server_creation, limit_user_creation, limit_role_creation)
crafty_permissions.add_or_update_user(
user_id,
permissions_mask,
limit_server_creation,
limit_user_creation,
limit_role_creation)
users_helper.delete_user_roles(user_id, removed_roles)
users_helper.update_user(user_id, up_data)
@staticmethod
def add_user(username, password=None, api_token=None, enabled=True, superuser=False):
return users_helper.add_user(username, password=password, api_token=api_token, enabled=enabled, superuser=superuser)
def add_user(username, password=None, email="default@example.com", enabled: bool = True, superuser: bool = False):
return users_helper.add_user(username, password=password, email=email, enabled=enabled, superuser=superuser)
@staticmethod
def remove_user(user_id):
@ -105,6 +107,16 @@ class Users_Controller:
def user_id_exists(user_id):
return users_helper.user_id_exists(user_id)
@staticmethod
def get_user_id_by_api_token(token: str) -> str:
token_data = authentication.check_no_iat(token)
return token_data['user_id']
@staticmethod
def get_user_by_api_token(token: str):
_, user = authentication.check(token)
return user
# ************************************************************************************************
# User Roles Methods
# ************************************************************************************************
@ -128,3 +140,29 @@ class Users_Controller:
@staticmethod
def user_role_query(user_id):
return users_helper.user_role_query(user_id)
# ************************************************************************************************
# Api Keys Methods
# ************************************************************************************************
@staticmethod
def get_user_api_keys(user_id: str):
return users_helper.get_user_api_keys(user_id)
@staticmethod
def get_user_api_key(key_id: str):
return users_helper.get_user_api_key(key_id)
@staticmethod
def add_user_api_key(name: str, user_id: str, superuser: bool = False,
server_permissions_mask: Optional[str] = None,
crafty_permissions_mask: Optional[str] = None):
return users_helper.add_user_api_key(name, user_id, superuser, server_permissions_mask, crafty_permissions_mask)
@staticmethod
def delete_user_api_keys(user_id: str):
return users_helper.delete_user_api_keys(user_id)
@staticmethod
def delete_user_api_key(key_id: str):
return users_helper.delete_user_api_key(key_id)

View File

@ -0,0 +1,107 @@
import os
import socket
import time
import psutil
class BedrockPing:
magic = b'\x00\xff\xff\x00\xfe\xfe\xfe\xfe\xfd\xfd\xfd\xfd\x12\x34\x56\x78'
fields = { # (len, signed)
"byte": (1, False),
"long": (8, True),
"ulong": (8, False),
"magic": (16, False),
"short": (2, True),
"ushort": (2, False), #unsigned short
"string": (2, False), #strlen is ushort
"bool": (1, False),
"address": (7, False),
"uint24le": (3, False)
}
byte_order = 'big'
def __init__(self, bedrock_addr, bedrock_port, client_guid=0, timeout=5):
self.addr = bedrock_addr
self.port = bedrock_port
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.settimeout(timeout)
self.proc = psutil.Process(os.getpid())
self.guid = client_guid
self.guid_bytes = self.guid.to_bytes(8, BedrockPing.byte_order)
@staticmethod
def __byter(in_val, to_type):
f = BedrockPing.fields[to_type]
return in_val.to_bytes(f[0], BedrockPing.byte_order, signed=f[1])
@staticmethod
def __slice(in_bytes, pattern):
ret = []
bi = 0 # bytes index
pi = 0 # pattern index
while bi < len(in_bytes):
try:
f = BedrockPing.fields[pattern[pi]]
except IndexError as index_error:
raise IndexError("Ran out of pattern with additional bytes remaining") from index_error
if pattern[pi] == "string":
shl = f[0] # string header length
sl = int.from_bytes(in_bytes[bi:bi+shl], BedrockPing.byte_order, signed=f[1]) # string length
l = shl+sl
ret.append(in_bytes[bi+shl:bi+shl+sl].decode('ascii'))
elif pattern[pi] == "magic":
l = f[0] # length of field
ret.append(in_bytes[bi:bi+l])
else:
l = f[0] # length of field
ret.append(int.from_bytes(in_bytes[bi:bi+l], BedrockPing.byte_order, signed=f[1]))
bi+=l
pi+=1
return ret
@staticmethod
def __get_time():
#return time.time_ns() // 1000000
return time.perf_counter_ns() // 1000000
def __sendping(self):
pack_id = BedrockPing.__byter(0x01, 'byte')
now = BedrockPing.__byter(BedrockPing.__get_time(), 'ulong')
guid = self.guid_bytes
d2s = pack_id+now+BedrockPing.magic+guid
#print("S:", d2s)
self.sock.sendto(d2s, (self.addr, self.port))
def __recvpong(self):
data = self.sock.recv(4096)
if data[0] == 0x1c:
ret = {}
sliced = BedrockPing.__slice(data,["byte","ulong","ulong","magic","string"])
if sliced[3] != BedrockPing.magic:
raise ValueError(f"Incorrect magic received ({sliced[3]})")
ret["server_guid"] = sliced[2]
ret["server_string_raw"] = sliced[4]
server_info = sliced[4].split(';')
ret["server_edition"] = server_info[0]
ret["server_motd"] = (server_info[1], server_info[7])
ret["server_protocol_version"] = server_info[2]
ret["server_version_name"] = server_info[3]
ret["server_player_count"] = server_info[4]
ret["server_player_max"] = server_info[5]
ret["server_uuid"] = server_info[6]
ret["server_game_mode"] = server_info[8]
ret["server_game_mode_num"] = server_info[9]
ret["server_port_ipv4"] = server_info[10]
ret["server_port_ipv6"] = server_info[11]
return ret
else:
raise ValueError(f"Incorrect packet type ({data[0]} detected")
def ping(self, retries=3):
rtr = retries
while rtr > 0:
try:
self.__sendping()
return self.__recvpong()
except ValueError as e:
print(f"E: {e}, checking next packet. Retries remaining: {rtr}/{retries}")
rtr -= 1

View File

@ -1,16 +1,18 @@
from app.classes.shared.helpers import Helpers
import struct
import socket
import base64
import json
import sys
import os
import re
import logging.config
import uuid
import random
from app.classes.minecraft.bedrock_ping import BedrockPing
from app.classes.shared.console import console
logger = logging.getLogger(__name__)
class Server:
def __init__(self, data):
self.description = data.get('description')
@ -57,7 +59,11 @@ class Server:
self.description = self.description['text']
self.icon = base64.b64decode(data.get('favicon', '')[22:])
try:
self.players = Players(data['players']).report()
except KeyError:
logger.error("Error geting player information key error")
self.players = []
self.version = data['version']['name']
self.protocol = data['version']['protocol']
@ -101,13 +107,13 @@ def get_code_format(format_name):
if format_name in data.keys():
return data.get(format_name)
else:
logger.error("Format MOTD Error: format name {} does not exist".format(format_name))
console.error("Format MOTD Error: format name {} does not exist".format(format_name))
logger.error(f"Format MOTD Error: format name {format_name} does not exist")
console.error(f"Format MOTD Error: format name {format_name} does not exist")
return ""
except Exception as e:
logger.critical("Config File Error: Unable to read {} due to {}".format(format_file, e))
console.critical("Config File Error: Unable to read {} due to {}".format(format_file, e))
logger.critical(f"Config File Error: Unable to read {format_file} due to {e}")
console.critical(f"Config File Error: Unable to read {format_file} due to {e}")
return ""
@ -126,15 +132,14 @@ def ping(ip, port):
j += 1
if j > 5:
raise ValueError('var_int too big')
if not (k & 0x80):
if not k & 0x80:
return i
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.connect((ip, port))
except socket.error as err:
pass
except socket.error:
return False
try:
@ -163,7 +168,25 @@ def ping(ip, port):
return False
data += chunk
logger.debug("Server reports this data on ping: {}".format(data))
logger.debug(f"Server reports this data on ping: {data}")
try:
return Server(json.loads(data))
except KeyError:
return {}
finally:
sock.close()
# For the rest of requests see wiki.vg/Protocol
def ping_bedrock(ip, port):
rd = random.Random()
try:
#pylint: disable=consider-using-f-string
rd.seed(''.join(re.findall('..', '%012x' % uuid.getnode())))
client_guid = uuid.UUID(int=rd.getrandbits(32)).int
except:
client_guid = 0
try:
brp = BedrockPing(ip, port, client_guid)
return brp.ping()
except:
logger.debug("Unable to get RakNet stats")

View File

@ -8,8 +8,8 @@ class ServerProps:
self.props = self._parse()
def _parse(self):
"""Loads and parses the file speified in self.filepath"""
with open(self.filepath) as fp:
"""Loads and parses the file specified in self.filepath"""
with open(self.filepath, encoding='utf-8') as fp:
line = fp.readline()
d = {}
if os.path.exists(".header"):
@ -24,7 +24,7 @@ class ServerProps:
s2 = s[s.find('=')+1:]
d[s1] = s2
else:
with open(".header", "a+") as h:
with open(".header", "a+", encoding='utf-8') as h:
h.write(line)
line = fp.readline()
return d
@ -47,9 +47,9 @@ class ServerProps:
def save(self):
"""Writes to the new file"""
with open(self.filepath, "a+") as f:
with open(self.filepath, "a+", encoding='utf-8') as f:
f.truncate(0)
with open(".header") as header:
with open(".header", encoding='utf-8') as header:
line = header.readline()
while line:
f.write(line)

View File

@ -1,5 +1,3 @@
import os
import sys
import json
import threading
import time
@ -7,10 +5,9 @@ import shutil
import logging
from datetime import datetime
from app.classes.controllers.servers_controller import Servers_Controller
from app.classes.models.server_permissions import server_permissions
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
from app.classes.models.servers import Servers
from app.classes.minecraft.server_props import ServerProps
from app.classes.web.websocket_helper import websocket_helper
logger = logging.getLogger(__name__)
@ -18,11 +15,8 @@ logger = logging.getLogger(__name__)
try:
import requests
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True)
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
except ModuleNotFoundError as err:
helper.auto_installer_fix(err)
class ServerJars:
@ -30,7 +24,7 @@ class ServerJars:
self.base_url = "https://serverjars.com"
def _get_api_result(self, call_url: str):
full_url = "{base}{call_url}".format(base=self.base_url, call_url=call_url)
full_url = f"{self.base_url}{call_url}"
try:
r = requests.get(full_url, timeout=2)
@ -38,20 +32,20 @@ class ServerJars:
if r.status_code not in [200, 201]:
return {}
except Exception as e:
logger.error("Unable to connect to serverjar.com api due to error: {}".format(e))
logger.error(f"Unable to connect to serverjar.com api due to error: {e}")
return {}
try:
api_data = json.loads(r.content)
except Exception as e:
logger.error("Unable to parse serverjar.com api result due to error: {}".format(e))
logger.error(f"Unable to parse serverjar.com api result due to error: {e}")
return {}
api_result = api_data.get('status')
api_response = api_data.get('response', {})
if api_result != "success":
logger.error("Api returned a failed status: {}".format(api_result))
logger.error(f"Api returned a failed status: {api_result}")
return {}
return api_response
@ -61,11 +55,11 @@ class ServerJars:
cache_file = helper.serverjar_cache
cache = {}
try:
with open(cache_file, "r") as f:
with open(cache_file, "r", encoding='utf-8') as f:
cache = json.load(f)
except Exception as e:
logger.error("Unable to read serverjars.com cache file: {}".format(e))
logger.error(f"Unable to read serverjars.com cache file: {e}")
return cache
@ -99,7 +93,7 @@ class ServerJars:
def _check_api_alive(self):
logger.info("Checking serverjars.com API status")
check_url = "{base}/api/fetchTypes".format(base=self.base_url)
check_url = f"{self.base_url}/api/fetchTypes"
try:
r = requests.get(check_url, timeout=2)
@ -107,7 +101,7 @@ class ServerJars:
logger.info("Serverjars.com API is alive")
return True
except Exception as e:
logger.error("Unable to connect to serverjar.com api due to error: {}".format(e))
logger.error(f"Unable to connect to serverjar.com api due to error: {e}")
return {}
logger.error("unable to contact serverjars.com api")
@ -153,15 +147,15 @@ class ServerJars:
# save our cache
try:
with open(cache_file, "w") as f:
with open(cache_file, "w", encoding='utf-8') as f:
f.write(json.dumps(data, indent=4))
logger.info("Cache file refreshed")
except Exception as e:
logger.error("Unable to update serverjars.com cache file: {}".format(e))
logger.error(f"Unable to update serverjars.com cache file: {e}")
def _get_jar_details(self, jar_type='servers'):
url = '/api/fetchAll/{type}'.format(type=jar_type)
url = f'/api/fetchAll/{jar_type}'
response = self._get_api_result(url)
temp = []
for v in response:
@ -174,24 +168,51 @@ class ServerJars:
response = self._get_api_result(url)
return response
def download_jar(self, server, version, path, name):
update_thread = threading.Thread(target=self.a_download_jar, daemon=True, name="exe_download", args=(server, version, path, name))
def download_jar(self, server, version, path, server_id):
update_thread = threading.Thread(target=self.a_download_jar, daemon=True, args=(server, version, path, server_id))
update_thread.start()
def a_download_jar(self, server, version, path, name):
fetch_url = "{base}/api/fetchJar/{server}/{version}".format(base=self.base_url, server=server, version=version)
def a_download_jar(self, server, version, path, server_id):
#delaying download for server register to finish
time.sleep(3)
fetch_url = f"{self.base_url}/api/fetchJar/{server}/{version}"
server_users = server_permissions.get_server_user_list(server_id)
#We need to make sure the server is registered before we submit a db update for it's stats.
while True:
try:
Servers_Controller.set_download(server_id)
for user in server_users:
websocket_helper.broadcast_user(user, 'send_start_reload', {
})
break
except:
logger.debug("server not registered yet. Delaying download.")
# open a file stream
with requests.get(fetch_url, timeout=2, stream=True) as r:
try:
with open(path, 'wb') as output:
shutil.copyfileobj(r.raw, output)
Servers_Controller.finish_download(server_id)
for user in server_users:
websocket_helper.broadcast_user(user, 'notification', "Executable download finished")
time.sleep(3)
websocket_helper.broadcast_user(user, 'send_start_reload', {
})
return True
except Exception as e:
logger.error("Unable to save jar to {path} due to error:{error}".format(path=path, error=e))
pass
websocket_helper.broadcast('notification', "Executable download finished for server named: " + name)
logger.error(f"Unable to save jar to {path} due to error:{e}")
Servers_Controller.finish_download(server_id)
server_users = server_permissions.get_server_user_list(server_id)
for user in server_users:
websocket_helper.broadcast_user(user, 'notification', "Executable download finished")
time.sleep(3)
websocket_helper.broadcast_user(user, 'send_start_reload', {
})
return False

View File

@ -1,20 +1,16 @@
import os
import json
import time
import psutil
import logging
import datetime
import base64
import psutil
from app.classes.shared.helpers import helper
from app.classes.minecraft.mc_ping import ping
from app.classes.models.management import Host_Stats
from app.classes.models.servers import Server_Stats, servers_helper
from app.classes.models.servers import servers_helper
from app.classes.shared.helpers import helper
logger = logging.getLogger(__name__)
class Stats:
def __init__(self, controller):
@ -38,8 +34,8 @@ class Stats:
'mem_total': helper.human_readable_file_size(psutil.virtual_memory()[0]),
'disk_data': self._all_disk_usage()
}
server_stats = self.get_servers_stats()
data['servers'] = server_stats
#server_stats = self.get_servers_stats()
#data['servers'] = server_stats
data['node_stats'] = node_stats
return data
@ -64,8 +60,6 @@ class Stats:
real_cpu = round(p.cpu_percent(interval=0.5) / psutil.cpu_count(), 2)
process_start_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(p.create_time()))
# this is a faster way of getting data for a process
with p.oneshot():
process_stats = {
@ -76,7 +70,7 @@ class Stats:
return process_stats
except Exception as e:
logger.error("Unable to get process details for pid: {} due to error: {}".format(process_pid, e))
logger.error(f"Unable to get process details for pid: {process_pid} due to error: {e}")
# Dummy Data
process_stats = {
@ -92,7 +86,7 @@ class Stats:
# print(templ % ("Device", "Total", "Used", "Free", "Use ", "Type","Mount"))
for part in psutil.disk_partitions(all=False):
if os.name == 'nt':
if helper.is_os_windows():
if 'cdrom' in part.opts or part.fstype == '':
# skip cd-rom drives with no disk in it; they may raise
# ENOENT, pop-up a Windows GUI error for a non-ready
@ -114,28 +108,44 @@ class Stats:
return disk_data
@staticmethod
def get_world_size(world_path):
def get_world_size(server_path):
total_size = 0
# do a scan of the directories in the server path.
for root, dirs, files in os.walk(world_path, topdown=False):
# for each directory we find
for name in dirs:
# if the directory name is "region"
if name == "region":
# log it!
logger.debug("Path %s is called region. Getting directory size", os.path.join(root, name))
# get this directory size, and add it to the total we have running.
total_size += helper.get_dir_size(os.path.join(root, name))
total_size = helper.get_dir_size(server_path)
level_total_size = helper.human_readable_file_size(total_size)
return level_total_size
def get_server_players(self, server_id):
server = servers_helper.get_server_data_by_id(server_id)
logger.info(f"Getting players for server {server}")
# get our settings and data dictionaries
# server_settings = server.get('server_settings', {})
# server_data = server.get('server_data_obj', {})
# TODO: search server properties file for possible override of 127.0.0.1
internal_ip = server['server_ip']
server_port = server['server_port']
logger.debug("Pinging {internal_ip} on port {server_port}")
if servers_helper.get_server_type_by_id(server_id) != 'minecraft-bedrock':
int_mc_ping = ping(internal_ip, int(server_port))
ping_data = {}
# if we got a good ping return, let's parse it
if int_mc_ping:
ping_data = Stats.parse_server_ping(int_mc_ping)
return ping_data['players']
return []
@staticmethod
def parse_server_ping(ping_obj: object):
online_stats = {}
@ -144,14 +154,15 @@ class Stats:
online_stats = json.loads(ping_obj.players)
except Exception as e:
logger.info("Unable to read json from ping_obj: {}".format(e))
pass
logger.info(f"Unable to read json from ping_obj: {e}")
try:
server_icon = base64.encodebytes(ping_obj.icon)
server_icon = server_icon.decode('utf-8')
except Exception as e:
server_icon = False
logger.info("Unable to read the server icon : {}".format(e))
logger.info(f"Unable to read the server icon : {e}")
ping_data = {
'online': online_stats.get("online", 0),
@ -164,158 +175,26 @@ class Stats:
return ping_data
def get_server_players(self, server_id):
@staticmethod
def parse_server_RakNet_ping(ping_obj: object):
server = servers_helper.get_server_data_by_id(server_id)
logger.info("Getting players for server {}".format(server))
# get our settings and data dictionaries
server_settings = server.get('server_settings', {})
server_data = server.get('server_data_obj', {})
# TODO: search server properties file for possible override of 127.0.0.1
internal_ip = server['server_ip']
server_port = server['server_port']
logger.debug("Pinging {} on port {}".format(internal_ip, server_port))
int_mc_ping = ping(internal_ip, int(server_port))
ping_data = {}
# if we got a good ping return, let's parse it
if int_mc_ping:
ping_data = self.parse_server_ping(int_mc_ping)
return ping_data['players']
return []
def get_servers_stats(self):
server_stats_list = []
server_stats = {}
servers = self.controller.servers_list
logger.info("Getting Stats for all servers...")
for s in servers:
server_id = s.get('server_id', None)
server = servers_helper.get_server_data_by_id(server_id)
logger.debug('Getting stats for server: {}'.format(server_id))
# get our server object, settings and data dictionaries
server_obj = s.get('server_obj', None)
server_obj.reload_server_settings()
server_settings = s.get('server_settings', {})
server_data = self.controller.get_server_data(server_id)
# world data
world_name = server_settings.get('level-name', 'Unknown')
world_path = os.path.join(server_data.get('path', None), world_name)
# process stats
p_stats = self._get_process_stats(server_obj.process)
# TODO: search server properties file for possible override of 127.0.0.1
internal_ip = server['server_ip']
server_port = server['server_port']
logger.debug("Pinging server '{}' on {}:{}".format(s.get('server_name', "ID#{}".format(server_id)), internal_ip, server_port))
int_mc_ping = ping(internal_ip, int(server_port))
int_data = False
ping_data = {}
# if we got a good ping return, let's parse it
if int_mc_ping:
int_data = True
ping_data = self.parse_server_ping(int_mc_ping)
server_stats = {
'id': server_id,
'started': server_obj.get_start_time(),
'running': server_obj.check_running(),
'cpu': p_stats.get('cpu_usage', 0),
'mem': p_stats.get('memory_usage', 0),
"mem_percent": p_stats.get('mem_percentage', 0),
'world_name': world_name,
'world_size': self.get_world_size(world_path),
'server_port': server_port,
'int_ping_results': int_data,
'online': ping_data.get("online", False),
"max": ping_data.get("max", False),
'players': ping_data.get("players", False),
'desc': ping_data.get("server_description", False),
'version': ping_data.get("server_version", False)
try:
server_icon = base64.encodebytes(ping_obj['icon'])
except Exception as e:
server_icon = False
logger.info(f"Unable to read the server icon : {e}")
ping_data = {
'online': ping_obj['server_player_count'],
'max': ping_obj['server_player_max'],
'players': [],
'server_description': ping_obj['server_edition'],
'server_version': ping_obj['server_version_name'],
'server_icon': server_icon
}
# add this servers data to the stack
server_stats_list.append(server_stats)
return server_stats_list
return ping_data
def get_raw_server_stats(self, server_id):
server_stats = {}
server = self.controller.get_server_obj(server_id)
logger.debug('Getting stats for server: {}'.format(server_id))
# get our server object, settings and data dictionaries
server_obj = self.controller.get_server_obj(server_id)
server_obj.reload_server_settings()
server_settings = self.controller.get_server_settings(server_id)
server_data = self.controller.get_server_data(server_id)
# world data
world_name = server_settings.get('level-name', 'Unknown')
world_path = os.path.join(server_data.get('path', None), world_name)
# process stats
p_stats = self._get_process_stats(server_obj.process)
# TODO: search server properties file for possible override of 127.0.0.1
#internal_ip = server['server_ip']
#server_port = server['server_port']
internal_ip = server_data.get('server_ip', "127.0.0.1")
server_port = server_settings.get('server-port', "25565")
logger.debug("Pinging server '{}' on {}:{}".format(server.name, internal_ip, server_port))
int_mc_ping = ping(internal_ip, int(server_port))
int_data = False
ping_data = {}
# if we got a good ping return, let's parse it
if int_mc_ping:
int_data = True
ping_data = self.parse_server_ping(int_mc_ping)
server_stats = {
'id': server_id,
'started': server_obj.get_start_time(),
'running': server_obj.check_running(),
'cpu': p_stats.get('cpu_usage', 0),
'mem': p_stats.get('memory_usage', 0),
"mem_percent": p_stats.get('mem_percentage', 0),
'world_name': world_name,
'world_size': self.get_world_size(world_path),
'server_port': server_port,
'int_ping_results': int_data,
'online': ping_data.get("online", False),
"max": ping_data.get("max", False),
'players': ping_data.get("players", False),
'desc': ping_data.get("server_description", False),
'version': ping_data.get("server_version", False),
'icon': ping_data.get("server_icon", False)
}
return server_stats
def record_stats(self):
stats_to_send = self.get_node_stats()
@ -333,26 +212,26 @@ class Stats:
Host_Stats.disk_json: node_stats.get('disk_data', '{}')
}).execute()
server_stats = stats_to_send.get('servers')
for server in server_stats:
Server_Stats.insert({
Server_Stats.server_id: server.get('id', 0),
Server_Stats.started: server.get('started', ""),
Server_Stats.running: server.get('running', False),
Server_Stats.cpu: server.get('cpu', 0),
Server_Stats.mem: server.get('mem', 0),
Server_Stats.mem_percent: server.get('mem_percent', 0),
Server_Stats.world_name: server.get('world_name', ""),
Server_Stats.world_size: server.get('world_size', ""),
Server_Stats.server_port: server.get('server_port', ""),
Server_Stats.int_ping_results: server.get('int_ping_results', False),
Server_Stats.online: server.get("online", False),
Server_Stats.max: server.get("max", False),
Server_Stats.players: server.get("players", False),
Server_Stats.desc: server.get("desc", False),
Server_Stats.version: server.get("version", False)
}).execute()
# server_stats = stats_to_send.get('servers')#
#
# for server in server_stats:
# Server_Stats.insert({
# Server_Stats.server_id: server.get('id', 0),
# Server_Stats.started: server.get('started', ""),
# Server_Stats.running: server.get('running', False),
# Server_Stats.cpu: server.get('cpu', 0),
# Server_Stats.mem: server.get('mem', 0),
# Server_Stats.mem_percent: server.get('mem_percent', 0),
# Server_Stats.world_name: server.get('world_name', ""),
# Server_Stats.world_size: server.get('world_size', ""),
# Server_Stats.server_port: server.get('server_port', ""),
# Server_Stats.int_ping_results: server.get('int_ping_results', False),
# Server_Stats.online: server.get("online", False),
# Server_Stats.max: server.get("max", False),
# Server_Stats.players: server.get("players", False),
# Server_Stats.desc: server.get("desc", False),
# Server_Stats.version: server.get("version", False)
# }).execute()
# delete old data
max_age = helper.get_setting("history_max_age")
@ -360,4 +239,4 @@ class Stats:
last_week = now.day - max_age
Host_Stats.delete().where(Host_Stats.time < last_week).execute()
Server_Stats.delete().where(Server_Stats.created < last_week).execute()
# Server_Stats.delete().where(Server_Stats.created < last_week).execute()

View File

@ -1,27 +1,19 @@
import os
import sys
import logging
import datetime
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
from app.classes.models.users import Users
from app.classes.shared.permission_helper import permission_helper
from app.classes.models.users import Users, ApiKeys
try:
from peewee import SqliteDatabase, Model, ForeignKeyField, CharField, IntegerField, DoesNotExist
from enum import Enum
except ModuleNotFoundError as e:
helper.auto_installer_fix(e)
logger = logging.getLogger(__name__)
peewee_logger = logging.getLogger('peewee')
peewee_logger.setLevel(logging.INFO)
try:
from peewee import *
from playhouse.shortcuts import model_to_dict
from enum import Enum
import yaml
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True)
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
database = SqliteDatabase(helper.db_path, pragmas = {
'journal_mode': 'wal',
'cache_size': -1024 * 10})
@ -123,7 +115,7 @@ class Permissions_Crafty:
def get_User_Crafty(user_id):
try:
user_crafty = User_Crafty.select().where(User_Crafty.user_id == user_id).get()
except User_Crafty.DoesNotExist:
except DoesNotExist:
user_crafty = User_Crafty.insert({
User_Crafty.user_id: user_id,
User_Crafty.permissions: "000",
@ -172,7 +164,6 @@ class Permissions_Crafty:
@staticmethod
def get_crafty_limit_value(user_id, permission):
user_crafty = crafty_permissions.get_User_Crafty(user_id)
quantity_list = crafty_permissions.get_permission_quantity_list(user_id)
return quantity_list[permission]
@ -191,4 +182,18 @@ class Permissions_Crafty:
User_Crafty.save(user_crafty)
return user_crafty.created_server
@staticmethod
def get_api_key_permissions_list(key: ApiKeys):
user = key.user
if user.superuser and key.superuser:
return crafty_permissions.get_permissions_list()
else:
user_permissions_mask = crafty_permissions.get_crafty_permissions_mask(user.user_id)
key_permissions_mask: str = key.crafty_permissions
permissions_mask = permission_helper.combine_masks(user_permissions_mask, key_permissions_mask)
permissions_list = crafty_permissions.get_permissions(permissions_mask)
return permissions_list
crafty_permissions = Permissions_Crafty()

View File

@ -1,31 +1,23 @@
import os
import sys
import logging
import datetime
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
from app.classes.shared.main_models import db_helper
from app.classes.models.users import Users, users_helper
from app.classes.models.servers import Servers, servers_helper
from app.classes.models.servers import Servers
from app.classes.models.server_permissions import server_permissions
from app.classes.shared.helpers import helper
from app.classes.shared.main_models import db_helper
from app.classes.web.websocket_helper import websocket_helper
try:
from peewee import SqliteDatabase, Model, ForeignKeyField, CharField, IntegerField, DateTimeField, FloatField, TextField, AutoField, BooleanField
from playhouse.shortcuts import model_to_dict
except ModuleNotFoundError as e:
helper.auto_installer_fix(e)
logger = logging.getLogger(__name__)
peewee_logger = logging.getLogger('peewee')
peewee_logger.setLevel(logging.INFO)
try:
from peewee import *
from playhouse.shortcuts import model_to_dict
from enum import Enum
import yaml
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True)
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
database = SqliteDatabase(helper.db_path, pragmas = {
'journal_mode': 'wal',
'cache_size': -1024 * 10})
@ -112,6 +104,10 @@ class Schedules(Model):
start_time = CharField(null=True)
command = CharField(null=True)
comment = CharField()
one_time = BooleanField(default=False)
cron_string = CharField(default="")
parent = IntegerField(null=True)
delay = IntegerField(default=0)
class Meta:
table_name = 'schedules'
@ -122,11 +118,10 @@ class Schedules(Model):
# Backups Class
#************************************************************************************************
class Backups(Model):
directories = CharField(null=True)
excluded_dirs = CharField(null=True)
max_backups = IntegerField()
server_id = ForeignKeyField(Servers, backref='backups_server')
schedule_id = ForeignKeyField(Schedules, backref='backups_schedule')
compress = BooleanField(default=False)
class Meta:
table_name = 'backups'
database = database
@ -138,6 +133,7 @@ class helpers_management:
#************************************************************************************************
@staticmethod
def get_latest_hosts_stats():
#pylint: disable=no-member
query = Host_Stats.select().order_by(Host_Stats.id.desc()).get()
return model_to_dict(query)
@ -156,12 +152,12 @@ class helpers_management:
@staticmethod
def get_unactioned_commands():
query = Commands.select().where(Commands.executed == 0)
return db_helper.return_rows(query)
return query
@staticmethod
def mark_command_complete(command_id=None):
if command_id is not None:
logger.debug("Marking Command {} completed".format(command_id))
logger.debug(f"Marking Command {command_id} completed")
Commands.update({
Commands.executed: True
}).where(Commands.command_id == command_id).execute()
@ -176,12 +172,14 @@ class helpers_management:
@staticmethod
def add_to_audit_log(user_id, log_msg, server_id=None, source_ip=None):
logger.debug("Adding to audit log User:{} - Message: {} ".format(user_id, log_msg))
logger.debug(f"Adding to audit log User:{user_id} - Message: {log_msg} ")
user_data = users_helper.get_user(user_id)
audit_msg = "{} {}".format(str(user_data['username']).capitalize(), log_msg)
audit_msg = f"{str(user_data['username']).capitalize()} {log_msg}"
websocket_helper.broadcast('notification', audit_msg)
server_users = server_permissions.get_server_user_list(server_id)
for user in server_users:
websocket_helper.broadcast_user(user,'notification', audit_msg)
Audit_Log.insert({
Audit_Log.user_name: user_data['username'],
@ -190,6 +188,17 @@ class helpers_management:
Audit_Log.log_msg: audit_msg,
Audit_Log.source_ip: source_ip
}).execute()
#deletes records when they're more than 100
ordered = Audit_Log.select().order_by(+Audit_Log.created)
for item in ordered:
if not helper.get_setting('max_audit_entries'):
max_entries = 300
else:
max_entries = helper.get_setting('max_audit_entries')
if Audit_Log.select().count() > max_entries:
Audit_Log.delete().where(Audit_Log.audit_id == item.audit_id).execute()
else:
return
@staticmethod
def add_to_audit_log_raw(user_name, user_id, server_id, log_msg, source_ip):
@ -200,12 +209,35 @@ class helpers_management:
Audit_Log.log_msg: log_msg,
Audit_Log.source_ip: source_ip
}).execute()
#deletes records when they're more than 100
ordered = Audit_Log.select().order_by(+Audit_Log.created)
for item in ordered:
#configurable through app/config/config.json
if not helper.get_setting('max_audit_entries'):
max_entries = 300
else:
max_entries = helper.get_setting('max_audit_entries')
if Audit_Log.select().count() > max_entries:
Audit_Log.delete().where(Audit_Log.audit_id == item.audit_id).execute()
else:
return
#************************************************************************************************
# Schedules Methods
#************************************************************************************************
@staticmethod
def create_scheduled_task(server_id, action, interval, interval_type, start_time, command, comment=None, enabled=True):
def create_scheduled_task(
server_id,
action,
interval,
interval_type,
start_time,
command,
comment=None,
enabled=True,
one_time=False,
cron_string='* * * * *',
parent=None,
delay=0):
sch_id = Schedules.insert({
Schedules.server_id: server_id,
Schedules.action: action,
@ -214,7 +246,12 @@ class helpers_management:
Schedules.interval_type: interval_type,
Schedules.start_time: start_time,
Schedules.command: command,
Schedules.comment: comment
Schedules.comment: comment,
Schedules.one_time: one_time,
Schedules.cron_string: cron_string,
Schedules.parent: parent,
Schedules.delay: delay
}).execute()
return sch_id
@ -227,20 +264,37 @@ class helpers_management:
def update_scheduled_task(schedule_id, updates):
Schedules.update(updates).where(Schedules.schedule_id == schedule_id).execute()
@staticmethod
def delete_scheduled_task_by_server(server_id):
Schedules.delete().where(Schedules.server_id == int(server_id)).execute()
@staticmethod
def get_scheduled_task(schedule_id):
return model_to_dict(Schedules.get(Schedules.schedule_id == schedule_id)).execute()
return model_to_dict(Schedules.get(Schedules.schedule_id == schedule_id))
@staticmethod
def get_scheduled_task_model(schedule_id):
return Schedules.select().where(Schedules.schedule_id == schedule_id).get()
@staticmethod
def get_schedules_by_server(server_id):
return Schedules.select().where(Schedules.server_id == server_id).execute()
@staticmethod
def get_child_schedules_by_server(schedule_id, server_id):
return Schedules.select().where(Schedules.server_id == server_id, Schedules.parent == schedule_id).execute()
@staticmethod
def get_child_schedules(schedule_id):
return Schedules.select().where(Schedules.parent == schedule_id)
@staticmethod
def get_schedules_all():
return Schedules.select().execute()
@staticmethod
def get_schedules_enabled():
#pylint: disable=singleton-comparison
return Schedules.select().where(Schedules.enabled == True).execute()
#************************************************************************************************
@ -249,51 +303,44 @@ class helpers_management:
@staticmethod
def get_backup_config(server_id):
try:
row = Backups.select().where(Backups.server_id == server_id).join(Schedules).join(Servers)[0]
row = Backups.select().where(Backups.server_id == server_id).join(Servers)[0]
conf = {
"backup_path": row.server_id.backup_path,
"directories": row.directories,
"excluded_dirs": row.excluded_dirs,
"max_backups": row.max_backups,
"auto_enabled": row.schedule_id.enabled,
"server_id": row.server_id.server_id
"server_id": row.server_id.server_id,
"compress": row.compress
}
except IndexError:
conf = {
"backup_path": None,
"directories": None,
"excluded_dirs": None,
"max_backups": 0,
"auto_enabled": True,
"server_id": server_id
"server_id": server_id,
"compress": False,
}
return conf
@staticmethod
def set_backup_config(server_id: int, backup_path: str = None, max_backups: int = None, auto_enabled: bool = True):
logger.debug("Updating server {} backup config with {}".format(server_id, locals()))
try:
row = Backups.select().where(Backups.server_id == server_id).join(Schedules).join(Servers)[0]
def set_backup_config(server_id: int, backup_path: str = None, max_backups: int = None, excluded_dirs: list = None, compress: bool = False):
logger.debug(f"Updating server {server_id} backup config with {locals()}")
if Backups.select().where(Backups.server_id == server_id).count() != 0:
new_row = False
conf = {}
schd = {}
except IndexError:
else:
conf = {
"directories": None,
"excluded_dirs": None,
"max_backups": 0,
"server_id": server_id
}
schd = {
"enabled": True,
"action": "backup_server",
"interval_type": "days",
"interval": 1,
"start_time": "00:00",
"server_id": server_id,
"comment": "Default backup job"
"compress": False
}
new_row = True
if max_backups is not None:
conf['max_backups'] = max_backups
schd['enabled'] = bool(auto_enabled)
if excluded_dirs is not None:
dirs_to_exclude = ",".join(excluded_dirs)
conf['excluded_dirs'] = dirs_to_exclude
conf['compress'] = compress
if not new_row:
with database.atomic():
if backup_path is not None:
@ -301,17 +348,47 @@ class helpers_management:
else:
u1 = 0
u2 = Backups.update(conf).where(Backups.server_id == server_id).execute()
u3 = Schedules.update(schd).where(Schedules.schedule_id == row.schedule_id).execute()
logger.debug("Updating existing backup record. {}+{}+{} rows affected".format(u1, u2, u3))
logger.debug(f"Updating existing backup record. {u1}+{u2} rows affected")
else:
with database.atomic():
conf["server_id"] = server_id
if backup_path is not None:
u = Servers.update(backup_path=backup_path).where(Servers.server_id == server_id)
s = Schedules.create(**schd)
conf['schedule_id'] = s.schedule_id
b = Backups.create(**conf)
Servers.update(backup_path=backup_path).where(Servers.server_id == server_id)
Backups.create(**conf)
logger.debug("Creating new backup record.")
def get_excluded_backup_dirs(self, server_id: int):
excluded_dirs = self.get_backup_config(server_id)['excluded_dirs']
if excluded_dirs is not None and excluded_dirs != "":
dir_list = excluded_dirs.split(",")
else:
dir_list = []
return dir_list
def add_excluded_backup_dir(self, server_id: int, dir_to_add: str):
dir_list = self.get_excluded_backup_dirs()
if dir_to_add not in dir_list:
dir_list.append(dir_to_add)
excluded_dirs = ",".join(dir_list)
self.set_backup_config(server_id=server_id, excluded_dirs=excluded_dirs)
else:
logger.debug(f"Not adding {dir_to_add} to excluded directories - already in the excluded directory list for server ID {server_id}")
def del_excluded_backup_dir(self, server_id: int, dir_to_del: str):
dir_list = self.get_excluded_backup_dirs()
if dir_to_del in dir_list:
dir_list.remove(dir_to_del)
excluded_dirs = ",".join(dir_list)
self.set_backup_config(server_id=server_id, excluded_dirs=excluded_dirs)
else:
logger.debug(f"Not removing {dir_to_del} from excluded directories - not in the excluded directory list for server ID {server_id}")
@staticmethod
def clear_unexecuted_commands():
Commands.update({
Commands.executed: True
#pylint: disable=singleton-comparison
}).where(Commands.executed == False).execute()
management_helper = helpers_management()

View File

@ -1,26 +1,18 @@
import os
import sys
import logging
import datetime
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
try:
from peewee import SqliteDatabase, Model, CharField, DoesNotExist, AutoField, DateTimeField
from playhouse.shortcuts import model_to_dict
except ModuleNotFoundError as e:
helper.auto_installer_fix(e)
logger = logging.getLogger(__name__)
peewee_logger = logging.getLogger('peewee')
peewee_logger.setLevel(logging.INFO)
try:
from peewee import *
from playhouse.shortcuts import model_to_dict
from enum import Enum
import yaml
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True)
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
database = SqliteDatabase(helper.db_path, pragmas = {
'journal_mode': 'wal',
'cache_size': -1024 * 10})

View File

@ -1,34 +1,25 @@
import os
import sys
import logging
import datetime
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
from app.classes.models.servers import Servers
from app.classes.models.roles import Roles
from app.classes.models.users import users_helper
from app.classes.models.users import User_Roles, users_helper, ApiKeys, Users
from app.classes.shared.helpers import helper
from app.classes.shared.permission_helper import permission_helper
try:
from peewee import SqliteDatabase, Model, ForeignKeyField, CharField, CompositeKey, JOIN
from enum import Enum
except ModuleNotFoundError as e:
helper.auto_installer_fix(e)
logger = logging.getLogger(__name__)
peewee_logger = logging.getLogger('peewee')
peewee_logger.setLevel(logging.INFO)
try:
from peewee import *
from playhouse.shortcuts import model_to_dict
from enum import Enum
import yaml
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True)
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
database = SqliteDatabase(helper.db_path, pragmas = {
'journal_mode': 'wal',
'cache_size': -1024 * 10})
#************************************************************************************************
# Role Servers Class
#************************************************************************************************
@ -78,10 +69,7 @@ class Permissions_Servers:
@staticmethod
def has_permission(permission_mask, permission_tested: Enum_Permissions_Server):
result = False
if permission_mask[permission_tested.value] == '1':
result = True
return result
return permission_mask[permission_tested.value] == '1'
@staticmethod
def set_permission(permission_mask, permission_tested: Enum_Permissions_Server, value):
@ -94,6 +82,14 @@ class Permissions_Servers:
def get_permission(permission_mask, permission_tested: Enum_Permissions_Server):
return permission_mask[permission_tested.value]
@staticmethod
def get_token_permissions(permissions_mask, api_permissions_mask):
permissions_list = []
for member in Enum_Permissions_Server.__members__.items():
if permission_helper.both_have_perm(permissions_mask, api_permissions_mask, member[1]):
permissions_list.append(member[1])
return permissions_list
#************************************************************************************************
# Role_Servers Methods
@ -112,20 +108,29 @@ class Permissions_Servers:
@staticmethod
def add_role_server(server_id, role_id, rs_permissions="00000000"):
servers = Role_Servers.insert({Role_Servers.server_id: server_id, Role_Servers.role_id: role_id, Role_Servers.permissions: rs_permissions}).execute()
servers = Role_Servers.insert({Role_Servers.server_id: server_id, Role_Servers.role_id: role_id,
Role_Servers.permissions: rs_permissions}).execute()
return servers
@staticmethod
def get_permissions_mask(role_id, server_id):
permissions_mask = ''
role_server = Role_Servers.select().where(Role_Servers.role_id == role_id).where(Role_Servers.server_id == server_id).execute()
role_server = Role_Servers.select().where(Role_Servers.role_id == role_id).where(Role_Servers.server_id == server_id).get()
permissions_mask = role_server.permissions
return permissions_mask
@staticmethod
def get_server_roles(server_id):
role_list = []
roles = Role_Servers.select().where(Role_Servers.server_id == server_id).execute()
for role in roles:
role_list.append(role.role_id)
return role_list
@staticmethod
def get_role_permissions_list(role_id):
permissions_mask = '00000000'
role_server = Role_Servers.get_or_none(role_id)
role_server = Role_Servers.get_or_none(Role_Servers.role_id == role_id)
if role_server is not None:
permissions_mask = role_server.permissions
permissions_list = server_permissions.get_permissions(permissions_mask)
@ -138,7 +143,9 @@ class Permissions_Servers:
Role_Servers.save(role_server)
@staticmethod
def delete_roles_permissions(role_id, removed_servers={}):
def delete_roles_permissions(role_id, removed_servers=None):
if removed_servers is None:
removed_servers = {}
return Role_Servers.delete().where(Role_Servers.role_id == role_id).where(Role_Servers.server_id.in_(removed_servers)).execute()
@staticmethod
@ -147,18 +154,71 @@ class Permissions_Servers:
return Role_Servers.delete().where(Role_Servers.server_id == server_id).execute()
@staticmethod
def get_user_permissions_list(user_id, server_id):
permissions_mask = ''
permissions_list = []
def get_user_id_permissions_mask(user_id, server_id: str):
user = users_helper.get_user_model(user_id)
return server_permissions.get_user_permissions_mask(user, server_id)
user = users_helper.get_user(user_id)
if user['superuser'] == True:
@staticmethod
def get_user_permissions_mask(user: Users, server_id: str):
if user.superuser:
permissions_mask = '1' * len(server_permissions.get_permissions_list())
else:
roles_list = users_helper.get_user_roles_id(user.user_id)
role_server = Role_Servers.select().where(Role_Servers.role_id.in_(roles_list)).where(Role_Servers.server_id == server_id).execute()
try:
permissions_mask = role_server[0].permissions
except IndexError:
permissions_mask = '0' * len(server_permissions.get_permissions_list())
return permissions_mask
@staticmethod
def get_server_user_list(server_id):
final_users = []
server_roles = Role_Servers.select().where(Role_Servers.server_id == server_id)
# pylint: disable=singleton-comparison
super_users = Users.select().where(Users.superuser == True)
for role in server_roles:
users = User_Roles.select().where(User_Roles.role_id == role.role_id)
for user in users:
if user.user_id.user_id not in final_users:
final_users.append(user.user_id.user_id)
for suser in super_users:
if suser.user_id not in final_users:
final_users.append(suser.user_id)
return final_users
@staticmethod
def get_user_id_permissions_list(user_id, server_id: str):
user = users_helper.get_user_model(user_id)
return server_permissions.get_user_permissions_list(user, server_id)
@staticmethod
def get_user_permissions_list(user: Users, server_id: str):
if user.superuser:
permissions_list = server_permissions.get_permissions_list()
else:
roles_list = users_helper.get_user_roles_id(user_id)
role_server = Role_Servers.select().where(Role_Servers.role_id.in_(roles_list)).where(Role_Servers.server_id == int(server_id)).execute()
permissions_mask = role_server[0].permissions
permissions_mask = server_permissions.get_user_permissions_mask(user, server_id)
permissions_list = server_permissions.get_permissions(permissions_mask)
return permissions_list
@staticmethod
def get_api_key_id_permissions_list(key_id, server_id: str):
key = ApiKeys.get(ApiKeys.token_id == key_id)
return server_permissions.get_api_key_permissions_list(key, server_id)
@staticmethod
def get_api_key_permissions_list(key: ApiKeys, server_id: str):
user = key.user
if user.superuser and key.superuser:
return server_permissions.get_permissions_list()
else:
roles_list = users_helper.get_user_roles_id(user['user_id'])
role_server = Role_Servers.select().where(Role_Servers.role_id.in_(roles_list)).where(Role_Servers.server_id == server_id).execute()
user_permissions_mask = role_server[0].permissions
key_permissions_mask = key.server_permissions
permissions_mask = permission_helper.combine_masks(user_permissions_mask, key_permissions_mask)
permissions_list = server_permissions.get_permissions(permissions_mask)
return permissions_list
server_permissions = Permissions_Servers()

View File

@ -1,28 +1,18 @@
import os
import sys
import logging
import datetime
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
from app.classes.shared.main_models import db_helper
try:
from peewee import SqliteDatabase, Model, ForeignKeyField, CharField, AutoField, DateTimeField, BooleanField, IntegerField, FloatField
except ModuleNotFoundError as e:
helper.auto_installer_fix(e)
logger = logging.getLogger(__name__)
peewee_logger = logging.getLogger('peewee')
peewee_logger.setLevel(logging.INFO)
try:
from peewee import *
from playhouse.shortcuts import model_to_dict
from enum import Enum
import yaml
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True)
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
database = SqliteDatabase(helper.db_path, pragmas = {
'journal_mode': 'wal',
'cache_size': -1024 * 10})
@ -48,6 +38,7 @@ class Servers(Model):
server_ip = CharField(default="127.0.0.1")
server_port = IntegerField(default=25565)
logs_delete_after = IntegerField(default=0)
type = CharField(default="minecraft-java")
class Meta:
table_name = "servers"
@ -77,6 +68,9 @@ class Server_Stats(Model):
version = CharField(default="")
updating = BooleanField(default=False)
waiting_start = BooleanField(default=False)
first_run = BooleanField(default=True)
crashed = BooleanField(default=False)
downloading = BooleanField(default=False)
class Meta:
@ -93,7 +87,17 @@ class helper_servers:
# Generic Servers Methods
#************************************************************************************************
@staticmethod
def create_server(name: str, server_uuid: str, server_dir: str, backup_path: str, server_command: str, server_file: str, server_log_file: str, server_stop: str, server_port=25565):
def create_server(
name: str,
server_uuid: str,
server_dir: str,
backup_path: str,
server_command: str,
server_file: str,
server_log_file: str,
server_stop: str,
server_type: str,
server_port=25565):
return Servers.insert({
Servers.server_name: name,
Servers.server_uuid: server_uuid,
@ -106,9 +110,24 @@ class helper_servers:
Servers.log_path: server_log_file,
Servers.server_port: server_port,
Servers.stop_command: server_stop,
Servers.backup_path: backup_path
Servers.backup_path: backup_path,
Servers.type: server_type
}).execute()
@staticmethod
def get_server_obj(server_id):
return Servers.get_by_id(server_id)
@staticmethod
def get_server_type_by_id(server_id):
server_type = Servers.select().where(Servers.server_id == server_id).get()
return server_type.type
@staticmethod
def update_server(server_obj):
return server_obj.save()
@staticmethod
def remove_server(server_id):
with database.atomic():
@ -134,16 +153,18 @@ class helper_servers:
def get_all_servers_stats():
servers = servers_helper.get_all_defined_servers()
server_data = []
try:
for s in servers:
latest = Server_Stats.select().where(Server_Stats.server_id == s.get('server_id')).order_by(Server_Stats.created.desc()).limit(1)
server_data.append({'server_data': s, "stats": db_helper.return_rows(latest)[0], "user_command_permission":True})
except IndexError as ex:
logger.error(f"Stats collection failed with error: {ex}. Was a server just created?")
return server_data
@staticmethod
def get_server_friendly_name(server_id):
server_data = servers_helper.get_server_data_by_id(server_id)
friendly_name = "{} with ID: {}".format(server_data.get('server_name', None), server_data.get('server_id', 0))
friendly_name = f"{server_data.get('server_name', None)} with ID: {server_data.get('server_id', 0)}"
return friendly_name
#************************************************************************************************
@ -164,19 +185,82 @@ class helper_servers:
return False
return True
@staticmethod
def sever_crashed(server_id):
with database.atomic():
Server_Stats.update(crashed=True).where(Server_Stats.server_id == server_id).execute()
@staticmethod
def set_download(server_id):
with database.atomic():
Server_Stats.update(downloading=True).where(Server_Stats.server_id == server_id).execute()
@staticmethod
def finish_download(server_id):
with database.atomic():
Server_Stats.update(downloading=False).where(Server_Stats.server_id == server_id).execute()
@staticmethod
def get_download_status(server_id):
download_status = Server_Stats.select().where(Server_Stats.server_id == server_id).get()
return download_status.downloading
@staticmethod
def server_crash_reset(server_id):
with database.atomic():
Server_Stats.update(crashed=False).where(Server_Stats.server_id == server_id).execute()
@staticmethod
def is_crashed(server_id):
svr = Server_Stats.select().where(Server_Stats.server_id == server_id).get()
#pylint: disable=singleton-comparison
if svr.crashed == True:
return True
else:
return False
@staticmethod
def set_update(server_id, value):
try:
row = Server_Stats.select().where(Server_Stats.server_id == server_id)
#Checks if server even exists
Server_Stats.select().where(Server_Stats.server_id == server_id)
except Exception as ex:
logger.error("Database entry not found. ".format(ex))
logger.error(f"Database entry not found! {ex}")
with database.atomic():
Server_Stats.update(updating=value).where(Server_Stats.server_id == server_id).execute()
@staticmethod
def get_update_status(server_id):
update_status = Server_Stats.select().where(Server_Stats.server_id == server_id).get()
return update_status.updating
@staticmethod
def set_first_run(server_id):
#Sets first run to false
try:
#Checks if server even exists
Server_Stats.select().where(Server_Stats.server_id == server_id)
except Exception as ex:
logger.error(f"Database entry not found! {ex}")
return
with database.atomic():
Server_Stats.update(first_run=False).where(Server_Stats.server_id == server_id).execute()
@staticmethod
def get_first_run(server_id):
first_run = Server_Stats.select().where(Server_Stats.server_id == server_id).get()
return first_run.first_run
@staticmethod
def get_TTL_without_player(server_id):
last_stat = Server_Stats.select().where(Server_Stats.server_id == server_id).order_by(Server_Stats.created.desc()).first()
last_stat_with_player = Server_Stats.select().where(Server_Stats.server_id == server_id).where(Server_Stats.online > 0).order_by(Server_Stats.created.desc()).first()
last_stat_with_player = (Server_Stats
.select()
.where(Server_Stats.server_id == server_id)
.where(Server_Stats.online > 0)
.order_by(Server_Stats.created.desc())
.first())
return last_stat.created - last_stat_with_player.created
@staticmethod
@ -190,9 +274,10 @@ class helper_servers:
@staticmethod
def set_waiting_start(server_id, value):
try:
row = Server_Stats.select().where(Server_Stats.server_id == server_id)
# Checks if server even exists
Server_Stats.select().where(Server_Stats.server_id == server_id)
except Exception as ex:
logger.error("Database entry not found. ".format(ex))
logger.error(f"Database entry not found! {ex}")
with database.atomic():
Server_Stats.update(waiting_start=value).where(Server_Stats.server_id == server_id).execute()

View File

@ -1,28 +1,20 @@
import os
import sys
import logging
import datetime
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
from typing import Optional, Union
from app.classes.models.roles import Roles, roles_helper
from app.classes.shared.helpers import helper
try:
from peewee import SqliteDatabase, Model, ForeignKeyField, CharField, AutoField, DateTimeField, BooleanField, CompositeKey, DoesNotExist, JOIN
from playhouse.shortcuts import model_to_dict
except ModuleNotFoundError as e:
helper.auto_installer_fix(e)
logger = logging.getLogger(__name__)
peewee_logger = logging.getLogger('peewee')
peewee_logger.setLevel(logging.INFO)
try:
from peewee import *
from playhouse.shortcuts import model_to_dict
from enum import Enum
import yaml
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True)
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
database = SqliteDatabase(helper.db_path, pragmas = {
'journal_mode': 'wal',
'cache_size': -1024 * 10})
@ -38,15 +30,36 @@ class Users(Model):
last_ip = CharField(default="")
username = CharField(default="", unique=True, index=True)
password = CharField(default="")
email = CharField(default="default@example.com")
enabled = BooleanField(default=True)
superuser = BooleanField(default=False)
api_token = CharField(default="", unique=True, index=True) # we may need to revisit this
lang = CharField(default="en_EN")
support_logs = CharField(default = '')
valid_tokens_from = DateTimeField(default=datetime.datetime.now)
server_order = CharField(default="")
class Meta:
table_name = "users"
database = database
# ************************************************************************************************
# API Keys Class
# ************************************************************************************************
class ApiKeys(Model):
token_id = AutoField()
name = CharField(default='', unique=True, index=True)
created = DateTimeField(default=datetime.datetime.now)
user_id = ForeignKeyField(Users, backref='api_token', index=True)
server_permissions = CharField(default='00000000')
crafty_permissions = CharField(default='000')
superuser = BooleanField(default=False)
class Meta:
table_name = 'api_keys'
database = database
#************************************************************************************************
# User Roles Class
#************************************************************************************************
@ -70,7 +83,7 @@ class helper_users:
@staticmethod
def get_all_users():
query = Users.select()
query = Users.select().where(Users.username != "system")
return query
@staticmethod
@ -79,25 +92,11 @@ class helper_users:
@staticmethod
def get_user_id_by_name(username):
if username == "SYSTEM":
return 0
try:
return (Users.get(Users.username == username)).user_id
except DoesNotExist:
return None
@staticmethod
def get_user_by_api_token(token: str):
query = Users.select().where(Users.api_token == token)
if query.exists():
user = model_to_dict(Users.get(Users.api_token == token))
# I know it should apply it without setting it but I'm just making sure
user = users_helper.add_user_roles(user)
return user
else:
return {}
@staticmethod
def user_query(user_id):
user_query = Users.select().where(Users.user_id == user_id)
@ -108,17 +107,18 @@ class helper_users:
if user_id == 0:
return {
'user_id': 0,
'created': None,
'last_login': None,
'last_update': None,
'created': '10/24/2019, 11:34:00',
'last_login': '10/24/2019, 11:34:00',
'last_update': '10/24/2019, 11:34:00',
'last_ip': "127.27.23.89",
'username': "SYSTEM",
'password': None,
'email': "default@example.com",
'enabled': True,
'superuser': False,
'api_token': None,
'superuser': True,
'roles': [],
'servers': [],
'support_logs': '',
}
user = model_to_dict(Users.get(Users.user_id == user_id))
@ -131,20 +131,30 @@ class helper_users:
return {}
@staticmethod
def add_user(username, password=None, api_token=None, enabled=True, superuser=False):
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
@staticmethod
def get_user_model(user_id: str) -> Users:
user = Users.get(Users.user_id == user_id)
user = users_helper.add_user_roles(user)
return user
@staticmethod
def add_user(username: str, password: Optional[str] = None, email: Optional[str] = None, enabled: bool = True, superuser: bool = False) -> str:
if password is not None:
pw_enc = helper.encode_pass(password)
else:
pw_enc = None
if api_token is None:
api_token = users_helper.new_api_token()
else:
if type(api_token) is not str and len(api_token) != 32:
raise ValueError("API token must be a 32 character string")
user_id = Users.insert({
Users.username: username.lower(),
Users.password: pw_enc,
Users.api_token: api_token,
Users.email: email,
Users.enabled: enabled,
Users.superuser: superuser,
Users.created: helper.get_time_as_string()
@ -152,10 +162,30 @@ class helper_users:
return user_id
@staticmethod
def update_user(user_id, up_data={}):
def update_user(user_id, up_data=None):
if up_data is None:
up_data = {}
if up_data:
Users.update(up_data).where(Users.user_id == user_id).execute()
@staticmethod
def update_server_order(user_id, user_server_order):
Users.update(server_order = user_server_order).where(Users.user_id == user_id).execute()
@staticmethod
def get_server_order(user_id):
return Users.select().where(Users.user_id == user_id)
@staticmethod
def get_super_user_list():
final_users = []
# pylint: disable=singleton-comparison
super_users = Users.select().where(Users.superuser == True)
for suser in super_users:
if suser.user_id not in final_users:
final_users.append(suser.user_id)
return final_users
@staticmethod
def remove_user(user_id):
with database.atomic():
@ -163,20 +193,16 @@ class helper_users:
user = Users.get(Users.user_id == user_id)
return user.delete_instance()
@staticmethod
def set_support_path(user_id, support_path):
Users.update(support_logs = support_path).where(Users.user_id == user_id).execute()
@staticmethod
def user_id_exists(user_id):
if not users_helper.get_user(user_id):
return False
return True
@staticmethod
def new_api_token():
while True:
token = helper.random_string_generator(32)
test = list(Users.select(Users.user_id).where(Users.api_token == token))
if len(test) == 0:
return token
#************************************************************************************************
# User_Roles Methods
#************************************************************************************************
@ -209,8 +235,8 @@ class helper_users:
}).execute()
@staticmethod
def add_user_roles(user):
if type(user) == dict:
def add_user_roles(user: Union[dict, Users]):
if isinstance(user, dict):
user_id = user['user_id']
else:
user_id = user.user_id
@ -223,7 +249,11 @@ class helper_users:
for r in roles_query:
roles.add(r.role_id.role_id)
if isinstance(user, dict):
user['roles'] = roles
else:
user.roles = roles
#logger.debug("user: ({}) {}".format(user_id, user))
return user
@ -243,5 +273,41 @@ class helper_users:
def remove_roles_from_role_id(role_id):
User_Roles.delete().where(User_Roles.role_id == role_id).execute()
# ************************************************************************************************
# ApiKeys Methods
# ************************************************************************************************
@staticmethod
def get_user_api_keys(user_id: str):
return ApiKeys.select().where(ApiKeys.user_id == user_id).execute()
@staticmethod
def get_user_api_key(key_id: str) -> ApiKeys:
return ApiKeys.get(ApiKeys.token_id == key_id)
@staticmethod
def add_user_api_key(
name: str,
user_id: str,
superuser: bool = False,
server_permissions_mask: Optional[str] = None,
crafty_permissions_mask: Optional[str] = None):
return ApiKeys.insert({
ApiKeys.name: name,
ApiKeys.user_id: user_id,
**({ApiKeys.server_permissions: server_permissions_mask} if server_permissions_mask is not None else {}),
**({ApiKeys.crafty_permissions: crafty_permissions_mask} if crafty_permissions_mask is not None else {}),
ApiKeys.superuser: superuser
}).execute()
@staticmethod
def delete_user_api_keys(user_id: str):
ApiKeys.delete().where(ApiKeys.user_id == user_id).execute()
@staticmethod
def delete_user_api_key(key_id: str):
ApiKeys.delete().where(ApiKeys.token_id == key_id).execute()
users_helper = helper_users()

View File

@ -0,0 +1,79 @@
import logging
import time
from typing import Optional, Dict, Any, Tuple
from app.classes.models.users import users_helper, ApiKeys
from app.classes.shared.helpers import helper
try:
import jwt
from jwt import PyJWTError
except ModuleNotFoundError as e:
helper.auto_installer_fix(e)
logger = logging.getLogger(__name__)
class Authentication:
def __init__(self):
self.secret = "my secret"
self.secret = helper.get_setting('apikey_secret', None)
if self.secret is None or self.secret == 'random':
self.secret = helper.random_string_generator(64)
@staticmethod
def generate(user_id, extra=None):
if extra is None:
extra = {}
return jwt.encode(
{
'user_id': user_id,
'iat': int(time.time()),
**extra
},
authentication.secret,
algorithm="HS256"
)
@staticmethod
def read(token):
return jwt.decode(token, authentication.secret, algorithms=["HS256"])
@staticmethod
def check_no_iat(token) -> Optional[Dict[str, Any]]:
try:
return jwt.decode(token, authentication.secret, algorithms=["HS256"])
except PyJWTError as error:
logger.debug("Error while checking JWT token: ", exc_info=error)
return None
@staticmethod
def check(token) -> Optional[Tuple[Optional[ApiKeys], Dict[str, Any], Dict[str, Any]]]:
try:
data = jwt.decode(token, authentication.secret, algorithms=["HS256"])
except PyJWTError as error:
logger.debug("Error while checking JWT token: ", exc_info=error)
return None
iat: int = data['iat']
key: Optional[ApiKeys] = None
if 'token_id' in data:
key_id = data['token_id']
key = users_helper.get_user_api_key(key_id)
if key is None:
return None
user_id: str = data['user_id']
user = users_helper.get_user(user_id)
# TODO: Have a cache or something so we don't constantly have to query the database
if int(user.get('valid_tokens_from').timestamp()) < iat:
# Success!
return key, data, user
else:
return None
@staticmethod
def check_bool(token) -> bool:
return authentication.check(token) is not None
authentication = Authentication()

View File

@ -1,26 +1,16 @@
import os
import sys
import cmd
import time
import threading
import logging
logger = logging.getLogger(__name__)
from app.classes.shared.console import console
from app.classes.shared.helpers import helper
from app.classes.web.websocket_helper import websocket_helper
try:
import requests
logger = logging.getLogger(__name__)
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True)
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
class MainPrompt(cmd.Cmd, object):
class MainPrompt(cmd.Cmd):
def __init__(self, tasks_manager, migration_manager):
super().__init__()
@ -28,63 +18,56 @@ class MainPrompt(cmd.Cmd, object):
self.migration_manager = migration_manager
# overrides the default Prompt
prompt = "Crafty Controller v{} > ".format(helper.get_version_string())
prompt = f"Crafty Controller v{helper.get_version_string()} > "
@staticmethod
def emptyline():
pass
@staticmethod
def _clean_shutdown():
exit_file = os.path.join(helper.root_dir, "exit.txt")
try:
with open(exit_file, 'w') as f:
f.write("exit")
except Exception as e:
logger.critical("Unable to write exit file due to error: {}".format(e))
console.critical("Unable to write exit file due to error: {}".format(e))
#pylint: disable=unused-argument
def do_exit(self, line):
self.tasks_manager._main_graceful_exit()
self.universal_exit()
def do_migrations(self, line):
if (line == 'up'):
if line == 'up':
self.migration_manager.up()
elif (line == 'down'):
elif line == 'down':
self.migration_manager.down()
elif (line == 'done'):
elif line == 'done':
console.info(self.migration_manager.done)
elif (line == 'todo'):
elif line == 'todo':
console.info(self.migration_manager.todo)
elif (line == 'diff'):
elif line == 'diff':
console.info(self.migration_manager.diff)
elif (line == 'info'):
console.info('Done: {}'.format(self.migration_manager.done))
console.info('FS: {}'.format(self.migration_manager.todo))
console.info('Todo: {}'.format(self.migration_manager.diff))
elif (line.startswith('add ')):
elif line == 'info':
console.info(f'Done: {self.migration_manager.done}')
console.info(f'FS: {self.migration_manager.todo}')
console.info(f'Todo: {self.migration_manager.diff}')
elif line.startswith('add '):
migration_name = line[len('add '):]
self.migration_manager.create(migration_name, False)
else:
console.info('Unknown migration command')
def do_threads(self, line):
@staticmethod
def do_threads(_line):
for thread in threading.enumerate():
print(f'Name: {thread.name} IDENT: {thread.ident}')
if sys.version_info >= (3, 8):
print(f'Name: {thread.name} Identifier: {thread.ident} TID/PID: {thread.native_id}')
else:
print(f'Name: {thread.name} Identifier: {thread.ident}')
def universal_exit(self):
logger.info("Stopping all server daemons / threads")
console.info("Stopping all server daemons / threads - This may take a few seconds")
websocket_helper.disconnect_all()
self._clean_shutdown()
console.info('Waiting for main thread to stop')
while True:
if self.tasks_manager.get_main_thread_run_status():
sys.exit(0)
time.sleep(1)
@staticmethod
def help_exit():
console.help("Stops the server if running, Exits the program")

View File

@ -8,14 +8,11 @@ try:
from colorama import init
from termcolor import colored
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True)
print("Import Error: Unable to load {} module".format(e.name))
except ModuleNotFoundError as ex:
logger.critical(f"Import Error: Unable to load {ex.name} module", exc_info=True)
print(f"Import Error: Unable to load {ex.name} module")
from app.classes.shared.installer import installer
installer.do_install()
sys.exit(1)
class Console:
def __init__(self):
@ -49,28 +46,27 @@ class Console:
def debug(self, message):
dt = datetime.datetime.now().strftime("%Y-%m-%d %I:%M:%S %p")
self.magenta("[+] Crafty: {} - DEBUG:\t{}".format(dt, message))
self.magenta(f"[+] Crafty: {dt} - DEBUG:\t{message}")
def info(self, message):
dt = datetime.datetime.now().strftime("%Y-%m-%d %I:%M:%S %p")
self.white("[+] Crafty: {} - INFO:\t{}".format(dt, message))
self.white(f"[+] Crafty: {dt} - INFO:\t{message}")
def warning(self, message):
dt = datetime.datetime.now().strftime("%Y-%m-%d %I:%M:%S %p")
self.cyan("[+] Crafty: {} - WARNING:\t{}".format(dt, message))
self.cyan(f"[+] Crafty: {dt} - WARNING:\t{message}")
def error(self, message):
dt = datetime.datetime.now().strftime("%Y-%m-%d %I:%M:%S %p")
self.yellow("[+] Crafty: {} - ERROR:\t{}".format(dt, message))
self.yellow(f"[+] Crafty: {dt} - ERROR:\t{message}")
def critical(self, message):
dt = datetime.datetime.now().strftime("%Y-%m-%d %I:%M:%S %p")
self.red("[+] Crafty: {} - CRITICAL:\t{}".format(dt, message))
self.red(f"[+] Crafty: {dt} - CRITICAL:\t{message}")
def help(self, message):
dt = datetime.datetime.now().strftime("%Y-%m-%d %I:%M:%S %p")
self.green("[+] Crafty: {} - HELP:\t{}".format(dt, message))
self.green(f"[+] Crafty: {dt} - HELP:\t{message}")
console = Console()

View File

@ -0,0 +1,101 @@
import os
import shutil
import logging
import pathlib
from zipfile import ZipFile, ZIP_DEFLATED
logger = logging.getLogger(__name__)
class FileHelpers:
allowed_quotes = [
"\"",
"'",
"`"
]
def del_dirs(self, path):
path = pathlib.Path(path)
for sub in path.iterdir():
if sub.is_dir():
# Delete folder if it is a folder
self.del_dirs(sub)
else:
# Delete file if it is a file:
sub.unlink()
# This removes the top-level folder:
path.rmdir()
return True
@staticmethod
def del_file(path):
path = pathlib.Path(path)
try:
logger.debug(f"Deleting file: {path}")
#Remove the file
os.remove(path)
return True
except FileNotFoundError:
logger.error(f"Path specified is not a file or does not exist. {path}")
return False
@staticmethod
def copy_dir(src_path, dest_path, dirs_exist_ok=False):
# pylint: disable=unexpected-keyword-arg
shutil.copytree(src_path, dest_path, dirs_exist_ok=dirs_exist_ok)
@staticmethod
def copy_file(src_path, dest_path):
shutil.copy(src_path, dest_path)
def move_dir(self, src_path, dest_path):
self.copy_dir(src_path, dest_path)
self.del_dirs(src_path)
def move_file(self, src_path, dest_path):
self.copy_file(src_path, dest_path)
self.del_file(src_path)
@staticmethod
def make_archive(path_to_destination, path_to_zip):
# create a ZipFile object
path_to_destination += '.zip'
with ZipFile(path_to_destination, 'w') as z:
for root, _dirs, files in os.walk(path_to_zip, topdown=True):
ziproot = path_to_zip
for file in files:
try:
logger.info(f"backing up: {os.path.join(root, file)}")
if os.name == "nt":
z.write(os.path.join(root, file), os.path.join(root.replace(ziproot, ""), file))
else:
z.write(os.path.join(root, file), os.path.join(root.replace(ziproot, "/"), file))
except Exception as e:
logger.warning(f"Error backing up: {os.path.join(root, file)}! - Error was: {e}")
return True
@staticmethod
def make_compressed_archive(path_to_destination, path_to_zip):
# create a ZipFile object
path_to_destination += '.zip'
with ZipFile(path_to_destination, 'w', ZIP_DEFLATED) as z:
for root, _dirs, files in os.walk(path_to_zip, topdown=True):
ziproot = path_to_zip
for file in files:
try:
logger.info(f"backing up: {os.path.join(root, file)}")
if os.name == "nt":
z.write(os.path.join(root, file), os.path.join(root.replace(ziproot, ""), file))
else:
z.write(os.path.join(root, file), os.path.join(root.replace(ziproot, "/"), file))
except Exception as e:
logger.warning(f"Error backing up: {os.path.join(root, file)}! - Error was: {e}")
return True
file_helper = FileHelpers()

View File

@ -13,25 +13,29 @@ import logging
import html
import zipfile
import pathlib
import shutil
from requests import get
import ctypes
from datetime import datetime
from socket import gethostname
from contextlib import suppress
import psutil
from app.classes.shared.console import console
from app.classes.shared.installer import installer
from app.classes.shared.file_helpers import file_helper
from app.classes.web.websocket_helper import websocket_helper
logger = logging.getLogger(__name__)
try:
import requests
from requests import get
from OpenSSL import crypto
from argon2 import PasswordHasher
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True)
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
except ModuleNotFoundError as err:
logger.critical(f"Import Error: Unable to load {err.name} module", exc_info=True)
print(f"Import Error: Unable to load {err.name} module")
installer.do_install()
class Helpers:
allowed_quotes = [
@ -58,14 +62,20 @@ class Helpers:
self.passhasher = PasswordHasher()
self.exiting = False
@staticmethod
def auto_installer_fix(ex):
logger.critical(f"Import Error: Unable to load {ex.name} module", exc_info=True)
print(f"Import Error: Unable to load {ex.name} module")
installer.do_install()
def float_to_string(self, gbs: int):
s = str(float(gbs) * 1000).rstrip("0").rstrip(".")
return s
def check_file_perms(self, path):
try:
fp = open(path, "r").close()
logger.info("{} is readable".format(path))
open(path, "r", encoding='utf-8').close()
logger.info(f"{path} is readable")
return True
except PermissionError:
return False
@ -78,30 +88,52 @@ class Helpers:
return True
else:
return False
logger.error("{} does not exist".format(file))
logger.error(f"{file} does not exist")
return True
def get_servers_root_dir(self):
return self.servers_dir
@staticmethod
def check_internet():
try:
requests.get('https://google.com', timeout=1)
return True
except Exception as err:
except Exception:
return False
@staticmethod
def check_port(server_port):
try:
host_public = get('https://api.ipify.org').text
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(10.0)
result = sock.connect_ex((host_public ,server_port))
sock.close()
if result == 0:
ip = get('https://api.ipify.org').content.decode('utf8')
except:
ip = 'google.com'
a_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
a_socket.settimeout(20.0)
location = (ip, server_port)
result_of_check = a_socket.connect_ex(location)
a_socket.close()
if result_of_check == 0:
return True
else:
return False
except Exception as err:
@staticmethod
def check_server_conn(server_port):
a_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
a_socket.settimeout(10.0)
ip = '127.0.0.1'
location = (ip, server_port)
result_of_check = a_socket.connect_ex(location)
a_socket.close()
if result_of_check == 0:
return True
else:
return False
@staticmethod
@ -113,7 +145,7 @@ class Helpers:
esc = False # whether an escape character was encountered
stch = None # if we're dealing with a quote, save the quote type here. Nested quotes to be dealt with by the command
for c in cmd_in: # for character in string
if np == True: # if set, begin a new argument and increment the command index. Continue the loop.
if np: # if set, begin a new argument and increment the command index. Continue the loop.
if c == ' ':
continue
else:
@ -128,52 +160,35 @@ class Helpers:
else:
if c == '\\': # if the current character is an escape character, set the esc flag and continue to next loop
esc = True
elif c == ' ' and stch is None: # if we encounter a space and are not dealing with a quote, set the new argument flag and continue to next loop
elif c == ' ' and stch is None: # if we encounter a space and are not dealing with a quote,
# set the new argument flag and continue to next loop
np = True
elif c == stch: # if we encounter the character that matches our start quote, end the quote and continue to next loop
stch = None
elif stch is None and (c in Helpers.allowed_quotes): # if we're not in the middle of a quote and we get a quotable character, start a quote and proceed to the next loop
elif stch is None and (c in Helpers.allowed_quotes): # if we're not in the middle of a quote and we get a quotable character,
# start a quote and proceed to the next loop
stch = c
else: # else, just store the character in the current arg
cmd_out[ci] += c
return cmd_out
def check_for_old_logs(self, db_helper):
servers = db_helper.get_all_defined_servers()
for server in servers:
logs_path = os.path.split(server['log_path'])[0]
latest_log_file = os.path.split(server['log_path'])[1]
logs_delete_after = int(server['logs_delete_after'])
if logs_delete_after == 0:
continue
log_files = list(filter(
lambda val: val != latest_log_file,
os.listdir(logs_path)
))
for log_file in log_files:
log_file_path = os.path.join(logs_path, log_file)
if self.check_file_exists(log_file_path) and \
self.is_file_older_than_x_days(log_file_path, logs_delete_after):
os.remove(log_file_path)
def get_setting(self, key, default_return=False):
try:
with open(self.settings_file, "r") as f:
with open(self.settings_file, "r", encoding='utf-8') as f:
data = json.load(f)
if key in data.keys():
return data.get(key)
else:
logger.error("Config File Error: setting {} does not exist".format(key))
console.error("Config File Error: setting {} does not exist".format(key))
logger.error(f"Config File Error: setting {key} does not exist")
console.error(f"Config File Error: setting {key} does not exist")
return default_return
except Exception as e:
logger.critical("Config File Error: Unable to read {} due to {}".format(self.settings_file, e))
console.critical("Config File Error: Unable to read {} due to {}".format(self.settings_file, e))
logger.critical(f"Config File Error: Unable to read {self.settings_file} due to {e}")
console.critical(f"Config File Error: Unable to read {self.settings_file} due to {e}")
return default_return
@ -192,11 +207,11 @@ class Helpers:
def get_version(self):
version_data = {}
try:
with open(os.path.join(self.config_dir, 'version.json'), 'r') as f:
with open(os.path.join(self.config_dir, 'version.json'), 'r', encoding='utf-8') as f:
version_data = json.load(f)
except Exception as e:
console.critical("Unable to get version data!")
console.critical(f"Unable to get version data! \n{e}")
return version_data
@ -210,7 +225,7 @@ class Helpers:
try:
data = json.loads(r.content)
except Exception as e:
logger.error("Failed to load json content with error: {} ".format(e))
logger.error(f"Failed to load json content with error: {e}")
return data
@ -218,23 +233,15 @@ class Helpers:
def get_version_string(self):
version_data = self.get_version()
major = version_data.get('major', '?')
minor = version_data.get('minor', '?')
sub = version_data.get('sub', '?')
meta = version_data.get('meta', '?')
# set some defaults if we don't get version_data from our helper
version = "{}.{}.{}-{}".format(version_data.get('major', '?'),
version_data.get('minor', '?'),
version_data.get('sub', '?'),
version_data.get('meta', '?'))
version = f"{major}.{minor}.{sub}-{meta}"
return str(version)
def do_exit(self):
exit_file = os.path.join(self.root_dir, 'exit.txt')
try:
open(exit_file, 'a').close()
except Exception as e:
logger.critical("Unable to create exit file!")
console.critical("Unable to create exit file!")
sys.exit(1)
def encode_pass(self, password):
return self.passhasher.hash(password)
@ -243,7 +250,6 @@ class Helpers:
self.passhasher.verify(currenthash, password)
return True
except:
pass
return False
def log_colors(self, line):
@ -256,15 +262,18 @@ class Helpers:
(r'(\[.+?/INFO\])', r'<span class="mc-log-info">\1</span>'),
(r'(\[.+?/WARN\])', r'<span class="mc-log-warn">\1</span>'),
(r'(\[.+?/ERROR\])', r'<span class="mc-log-error">\1</span>'),
(r'(\[.+?/FATAL\])', r'<span class="mc-log-fatal">\1</span>'),
(r'(\w+?\[/\d+?\.\d+?\.\d+?\.\d+?\:\d+?\])', r'<span class="mc-log-keyword">\1</span>'),
(r'\[(\d\d:\d\d:\d\d)\]', r'<span class="mc-log-time">[\1]</span>'),
(r'(\[.+? INFO\])', r'<span class="mc-log-info">\1</span>'),
(r'(\[.+? WARN\])', r'<span class="mc-log-warn">\1</span>'),
(r'(\[.+? ERROR\])', r'<span class="mc-log-error">\1</span>')
(r'(\[.+? ERROR\])', r'<span class="mc-log-error">\1</span>'),
(r'(\[.+? FATAL\])', r'<span class="mc-log-fatal">\1</span>')
]
# highlight users keywords
for keyword in user_keywords:
# pylint: disable=consider-using-f-string
search_replace = (r'({})'.format(keyword), r'<span class="mc-log-keyword">\1</span>')
replacements.append(search_replace)
@ -273,10 +282,23 @@ class Helpers:
return line
def validate_traversal(self, base_path, filename):
logger.debug(f"Validating traversal (\"{base_path}\", \"{filename}\")")
base = pathlib.Path(base_path).resolve()
file = pathlib.Path(filename)
fileabs = base.joinpath(file).resolve()
cp = pathlib.Path(os.path.commonpath([base, fileabs]))
if base == cp:
return fileabs
else:
raise ValueError("Path traversal detected")
def tail_file(self, file_name, number_lines=20):
if not self.check_file_exists(file_name):
logger.warning("Unable to find file to tail: {}".format(file_name))
return ["Unable to find file to tail: {}".format(file_name)]
logger.warning(f"Unable to find file to tail: {file_name}")
return [f"Unable to find file to tail: {file_name}"]
# length of lines is X char here
avg_line_length = 255
@ -285,7 +307,7 @@ class Helpers:
line_buffer = number_lines * avg_line_length
# open our file
with open(file_name, 'r') as f:
with open(file_name, 'r', encoding='utf-8') as f:
# seek
f.seek(0, 2)
@ -301,8 +323,7 @@ class Helpers:
lines = f.readlines()
except Exception as e:
logger.warning('Unable to read a line in the file:{} - due to error: {}'.format(file_name, e))
pass
logger.warning(f'Unable to read a line in the file:{file_name} - due to error: {e}')
# now we are done getting the lines, let's return it
return lines
@ -311,14 +332,26 @@ class Helpers:
def check_writeable(path: str):
filename = os.path.join(path, "tempfile.txt")
try:
fp = open(filename, "w").close()
open(filename, "w", encoding='utf-8').close()
os.remove(filename)
logger.info("{} is writable".format(filename))
logger.info(f"{filename} is writable")
return True
except Exception as e:
logger.critical("Unable to write to {} - Error: {}".format(path, e))
logger.critical(f"Unable to write to {path} - Error: {e}")
return False
def checkRoot(self):
if self.is_os_windows():
if ctypes.windll.shell32.IsUserAnAdmin() == 1:
return True
else:
return False
else:
if os.geteuid() == 0:
return True
else:
return False
def unzipFile(self, zip_path):
@ -336,24 +369,17 @@ class Helpers:
try:
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(tempDir)
for i in range(len(zip_ref.filelist)):
for i in enumerate(zip_ref.filelist):
if len(zip_ref.filelist) > 1 or not zip_ref.filelist[i].filename.endswith('/'):
test = zip_ref.filelist[i].filename
break
path_list = test.split('/')
root_path = path_list[0]
'''
if len(path_list) > 1:
for i in range(len(path_list) - 2):
root_path = os.path.join(root_path, path_list[i + 1])
'''
full_root_path = tempDir
for item in os.listdir(full_root_path):
try:
shutil.move(os.path.join(full_root_path, item), os.path.join(new_dir, item))
file_helper.move_dir(os.path.join(full_root_path, item), os.path.join(new_dir, item))
except Exception as ex:
logger.error('ERROR IN ZIP IMPORT: {}'.format(ex))
logger.error(f'ERROR IN ZIP IMPORT: {ex}')
except Exception as ex:
print(ex)
else:
@ -370,27 +396,28 @@ class Helpers:
# if not writeable, let's bomb out
if not writeable:
logger.critical("Unable to write to {} directory!".format(self.root_dir))
logger.critical(f"Unable to write to {self.root_dir} directory!")
sys.exit(1)
# ensure the log directory is there
try:
with suppress(FileExistsError):
os.makedirs(os.path.join(self.root_dir, 'logs'))
except Exception as e:
console.error("Failed to make logs directory with error: {} ".format(e))
console.error(f"Failed to make logs directory with error: {e} ")
# ensure the log file is there
try:
open(log_file, 'a').close()
open(log_file, 'a', encoding='utf-8').close()
except Exception as e:
console.critical("Unable to open log file!")
console.critical(f"Unable to open log file! {e}")
sys.exit(1)
# del any old session.lock file as this is a new session
try:
os.remove(session_log_file)
except Exception as e:
logger.error("Deleting Session.lock failed with error: {} ".format(e))
logger.error(f"Deleting Session.lock failed with error: {e}")
@staticmethod
def get_time_as_string():
@ -399,10 +426,10 @@ class Helpers:
@staticmethod
def check_file_exists(path: str):
logger.debug('Looking for path: {}'.format(path))
logger.debug(f'Looking for path: {path}')
if os.path.exists(path) and os.path.isfile(path):
logger.debug('Found path: {}'.format(path))
logger.debug(f'Found path: {path}')
return True
else:
return False
@ -411,18 +438,20 @@ class Helpers:
def human_readable_file_size(num: int, suffix='B'):
for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']:
if abs(num) < 1024.0:
# pylint: disable=consider-using-f-string
return "%3.1f%s%s" % (num, unit, suffix)
num /= 1024.0
# pylint: disable=consider-using-f-string
return "%.1f%s%s" % (num, 'Y', suffix)
@staticmethod
def check_path_exists(path: str):
if not path:
return False
logger.debug('Looking for path: {}'.format(path))
logger.debug(f'Looking for path: {path}')
if os.path.exists(path):
logger.debug('Found path: {}'.format(path))
logger.debug(f'Found path: {path}')
return True
else:
return False
@ -434,17 +463,17 @@ class Helpers:
if os.path.exists(path) and os.path.isfile(path):
try:
with open(path, 'r') as f:
with open(path, 'r', encoding='utf-8') as f:
for line in (f.readlines() [-lines:]):
contents = contents + line
return contents
except Exception as e:
logger.error("Unable to read file: {}. \n Error: ".format(path, e))
logger.error(f"Unable to read file: {path}. \n Error: {e}")
return False
else:
logger.error("Unable to read file: {}. File not found, or isn't a file.".format(path))
logger.error(f"Unable to read file: {path}. File not found, or isn't a file.")
return False
def create_session_file(self, ignore=False):
@ -459,11 +488,17 @@ class Helpers:
data = json.loads(file_data)
pid = data.get('pid')
started = data.get('started')
console.critical("Another Crafty Controller agent seems to be running...\npid: {} \nstarted on: {}".format(pid, started))
except Exception as e:
logger.error("Failed to locate existing session.lock with error: {} ".format(e))
console.error("Failed to locate existing session.lock with error: {} ".format(e))
if psutil.pid_exists(pid):
console.critical(f"Another Crafty Controller agent seems to be running...\npid: {pid} \nstarted on: {started}")
logger.critical("Found running crafty process. Exiting.")
sys.exit(1)
else:
logger.info("No process found for pid. Assuming crafty crashed. Deleting stale session.lock")
os.remove(self.session_file)
except Exception as e:
logger.error(f"Failed to locate existing session.lock with error: {e} ")
console.error(f"Failed to locate existing session.lock with error: {e} ")
sys.exit(1)
@ -474,7 +509,7 @@ class Helpers:
'pid': pid,
'started': now.strftime("%d-%m-%Y, %H:%M:%S")
}
with open(self.session_file, 'w') as f:
with open(self.session_file, 'w', encoding='utf-8') as f:
json.dump(session_data, f, indent=True)
# because this is a recursive function, we will return bytes, and set human readable later
@ -501,14 +536,14 @@ class Helpers:
return sizes
@staticmethod
def base64_encode_string(string: str):
s_bytes = str(string).encode('utf-8')
def base64_encode_string(fun_str: str):
s_bytes = str(fun_str).encode('utf-8')
b64_bytes = base64.encodebytes(s_bytes)
return b64_bytes.decode('utf-8')
@staticmethod
def base64_decode_string(string: str):
s_bytes = str(string).encode('utf-8')
def base64_decode_string(fun_str: str):
s_bytes = str(fun_str).encode('utf-8')
b64_bytes = base64.decodebytes(s_bytes)
return b64_bytes.decode("utf-8")
@ -528,7 +563,7 @@ class Helpers:
try:
os.makedirs(path)
logger.debug("Created Directory : {}".format(path))
logger.debug(f"Created Directory : {path}")
# directory already exists - non-blocking error
except FileExistsError:
@ -545,8 +580,8 @@ class Helpers:
cert_file = os.path.join(cert_dir, 'commander.cert.pem')
key_file = os.path.join(cert_dir, 'commander.key.pem')
logger.info("SSL Cert File is set to: {}".format(cert_file))
logger.info("SSL Key File is set to: {}".format(key_file))
logger.info(f"SSL Cert File is set to: {cert_file}")
logger.info(f"SSL Key File is set to: {key_file}")
# don't create new files if we already have them.
if self.check_file_exists(cert_file) and self.check_file_exists(key_file):
@ -577,11 +612,11 @@ class Helpers:
cert.set_pubkey(k)
cert.sign(k, 'sha256')
f = open(cert_file, "w")
f = open(cert_file, "w", encoding='utf-8')
f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode())
f.close()
f = open(key_file, "w")
f = open(key_file, "w", encoding='utf-8')
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k).decode())
f.close()
@ -601,12 +636,26 @@ class Helpers:
else:
return False
@staticmethod
def wtol_path(w_path):
l_path = w_path.replace('\\', '/')
return l_path
@staticmethod
def ltow_path(l_path):
w_path = l_path.replace('/', '\\')
return w_path
@staticmethod
def get_os_understandable_path(path):
return os.path.normpath(path)
def find_default_password(self):
default_file = os.path.join(self.root_dir, "default.json")
data = {}
if self.check_file_exists(default_file):
with open(default_file, 'r') as f:
with open(default_file, 'r', encoding='utf-8') as f:
data = json.load(f)
del_json = helper.get_setting('delete_default_json')
@ -618,37 +667,169 @@ class Helpers:
@staticmethod
def generate_tree(folder, output=""):
for raw_filename in os.listdir(folder):
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
for item in file_list:
if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item)
else:
unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted(unsorted_files, key=str.casefold)
for raw_filename in file_list:
filename = html.escape(raw_filename)
rel = os.path.join(folder, raw_filename)
dpath = os.path.join(folder, filename)
if os.path.isdir(rel):
output += \
f"""<li class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
<i class="far fa-folder"></i>
<i class="far fa-folder-open"></i>
{filename}
</span>
</div><li>
\n"""\
else:
if filename != "crafty_managed.txt":
output += f"""<li
class="tree-nested d-block tree-ctx-item tree-file tree-item"
data-path="{dpath}"
data-name="{filename}"
onclick="clickOnFile(event)"><span style="margin-right: 6px;"><i class="far fa-file"></i></span>{filename}</li>"""
return output
@staticmethod
def generate_dir(folder, output=""):
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
for item in file_list:
if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item)
else:
unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted(unsorted_files, key=str.casefold)
output += \
f"""<ul class="tree-nested d-block" id="{folder}ul">"""\
for raw_filename in file_list:
filename = html.escape(raw_filename)
dpath = os.path.join(folder, filename)
rel = os.path.join(folder, raw_filename)
if os.path.isdir(rel):
output += \
"""<li class="tree-item" data-path="{}">
\n<div data-path="{}" data-name="{}" class="tree-caret tree-ctx-item tree-folder">
f"""<li class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
<i class="far fa-folder"></i>
<i class="far fa-folder-open"></i>
{}
</div>
\n<ul class="tree-nested">"""\
.format(os.path.join(folder, filename), os.path.join(folder, filename), filename, filename)
{filename}
</span>
</div><li>"""\
output += helper.generate_tree(rel)
output += '</ul>\n</li>'
else:
output += """<li
class="tree-item tree-ctx-item tree-file"
data-path="{}"
data-name="{}"
onclick="clickOnFile(event)"><span style="margin-right: 6px;"><i class="far fa-file"></i></span>{}</li>""".format(os.path.join(folder, filename), filename, filename)
if filename != "crafty_managed.txt":
output += f"""<li
class="tree-nested d-block tree-ctx-item tree-file tree-item"
data-path="{dpath}"
data-name="{filename}"
onclick="clickOnFile(event)"><span style="margin-right: 6px;"><i class="far fa-file"></i></span>{filename}</li>"""
output += '</ul>\n'
return output
@staticmethod
def generate_zip_tree(folder, output=""):
file_list = os.listdir(folder)
file_list = sorted(file_list, key=str.casefold)
output += \
f"""<ul class="tree-nested d-block" id="{folder}ul">"""\
for raw_filename in file_list:
filename = html.escape(raw_filename)
rel = os.path.join(folder, raw_filename)
dpath = os.path.join(folder, filename)
if os.path.isdir(rel):
output += \
f"""<li class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="radio" name="root_path" value="{dpath}">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
<i class="far fa-folder"></i>
<i class="far fa-folder-open"></i>
{filename}
</span>
</input></div><li>
\n"""\
return output
@staticmethod
def generate_zip_dir(folder, output=""):
file_list = os.listdir(folder)
file_list = sorted(file_list, key=str.casefold)
output += \
f"""<ul class="tree-nested d-block" id="{folder}ul">"""\
for raw_filename in file_list:
filename = html.escape(raw_filename)
rel = os.path.join(folder, raw_filename)
dpath = os.path.join(folder, filename)
if os.path.isdir(rel):
output += \
f"""<li class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="radio" name="root_path" value="{dpath}">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
<i class="far fa-folder"></i>
<i class="far fa-folder-open"></i>
{filename}
</span>
</input></div><li>"""\
return output
@staticmethod
def unzipServer(zip_path, user_id):
if helper.check_file_perms(zip_path):
tempDir = tempfile.mkdtemp()
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
#extracts archive to temp directory
zip_ref.extractall(tempDir)
if user_id:
websocket_helper.broadcast_user(user_id, 'send_temp_path',{
'path': tempDir
})
@staticmethod
def backup_select(path, user_id):
if user_id:
websocket_helper.broadcast_user(user_id, 'send_temp_path',{
'path': path
})
@staticmethod
def unzip_backup_archive(backup_path, zip_name):
zip_path = os.path.join(backup_path, zip_name)
if helper.check_file_perms(zip_path):
tempDir = tempfile.mkdtemp()
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
#extracts archive to temp directory
zip_ref.extractall(tempDir)
return tempDir
else:
return False
@staticmethod
def in_path(parent_path, child_path):
# Smooth out relative path names, note: if you are concerned about symbolic links, you should use os.path.realpath too
parent_path = os.path.abspath(parent_path)
child_path = os.path.abspath(child_path)
# Compare the common path of the parent and child path with the common path of just the parent path. Using the commonpath method on just the parent path will regularise the path name in the same way as the comparison that deals with both paths, removing any trailing path separator
# Compare the common path of the parent and child path with the common path of just the parent path.
# Using the commonpath method on just the parent path will regularise the path name in the same way
# as the comparison that deals with both paths, removing any trailing path separator
return os.path.commonpath([parent_path]) == os.path.commonpath([parent_path, child_path])
@staticmethod
@ -658,7 +839,7 @@ class Helpers:
@staticmethod
def copy_files(source, dest):
if os.path.isfile(source):
shutil.copyfile(source, dest)
file_helper.copy_file(source, dest)
logger.info("Copying jar %s to %s", source, dest)
else:
logger.info("Source jar does not exist.")
@ -688,4 +869,13 @@ class Helpers:
return text[len(prefix):]
return text
@staticmethod
def getLangPage(text):
lang = text.split("_")[0]
region = text.split("_")[1]
if region == 'EN':
return 'en'
else:
return lang+"-"+region
helper = Helpers()

View File

@ -1,7 +1,6 @@
import sys
import subprocess
class install:
@staticmethod
@ -21,5 +20,4 @@ class install:
print("Crafty has installed it's dependencies, please restart Crafty")
sys.exit(0)
installer = install()

View File

@ -1,32 +1,36 @@
import os
import pathlib
from pathlib import Path
import shutil
import time
import logging
import sys
import yaml
import asyncio
import shutil
import tempfile
import zipfile
from distutils import dir_util
from typing import Union
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
#Importing Models
from app.classes.models.crafty_permissions import crafty_permissions, Enum_Permissions_Crafty
from app.classes.models.servers import servers_helper
#Importing Controllers
from app.classes.controllers.crafty_perms_controller import Crafty_Perms_Controller
from app.classes.controllers.management_controller import Management_Controller
from app.classes.controllers.users_controller import Users_Controller
from app.classes.controllers.roles_controller import Roles_Controller
from app.classes.controllers.server_perms_controller import Server_Perms_Controller
from app.classes.controllers.servers_controller import Servers_Controller
from app.classes.models.server_permissions import Enum_Permissions_Server
from app.classes.models.users import helper_users
from app.classes.models.management import helpers_management
from app.classes.models.servers import servers_helper
from app.classes.shared.console import console
from app.classes.shared.helpers import helper
from app.classes.shared.server import Server
from app.classes.shared.file_helpers import file_helper
from app.classes.minecraft.server_props import ServerProps
from app.classes.minecraft.serverjars import server_jar_obj
from app.classes.minecraft.stats import Stats
from app.classes.web.websocket_helper import websocket_helper
try:
from peewee import DoesNotExist
except ModuleNotFoundError as err:
helper.auto_installer_fix(err)
logger = logging.getLogger(__name__)
@ -44,7 +48,7 @@ class Controller:
def check_server_loaded(self, server_id_to_check: int):
logger.info("Checking to see if we already registered {}".format(server_id_to_check))
logger.info(f"Checking to see if we already registered {server_id_to_check}")
for s in self.servers_list:
known_server = s.get('server_id')
@ -52,7 +56,7 @@ class Controller:
return False
if known_server == server_id_to_check:
logger.info('skipping initialization of server {} because it is already loaded'.format(server_id_to_check))
logger.info(f'skipping initialization of server {server_id_to_check} because it is already loaded')
return True
return False
@ -69,20 +73,18 @@ class Controller:
continue
# if this server path no longer exists - let's warn and bomb out
if not helper.check_path_exists(s['path']):
logger.warning("Unable to find server {} at path {}. Skipping this server".format(s['server_name'],
s['path']))
if not helper.check_path_exists(helper.get_os_understandable_path(s['path'])):
logger.warning(f"Unable to find server {s['server_name']} at path {s['path']}. Skipping this server")
console.warning("Unable to find server {} at path {}. Skipping this server".format(s['server_name'],
s['path']))
console.warning(f"Unable to find server {s['server_name']} at path {s['path']}. Skipping this server")
continue
settings_file = os.path.join(s['path'], 'server.properties')
settings_file = os.path.join(helper.get_os_understandable_path(s['path']), 'server.properties')
# if the properties file isn't there, let's warn
if not helper.check_file_exists(settings_file):
logger.error("Unable to find {}. Skipping this server.".format(settings_file))
console.error("Unable to find {}. Skipping this server.".format(settings_file))
logger.error(f"Unable to find {settings_file}. Skipping this server.")
console.error(f"Unable to find {settings_file}. Skipping this server.")
continue
settings = ServerProps(settings_file)
@ -105,39 +107,103 @@ class Controller:
self.refresh_server_settings(s['server_id'])
console.info("Loaded Server: ID {} | Name: {} | Autostart: {} | Delay: {} ".format(
s['server_id'],
s['server_name'],
s['auto_start'],
s['auto_start_delay']
))
console.info(f"Loaded Server: ID {s['server_id']}" +
f" | Name: {s['server_name']}" +
f" | Autostart: {s['auto_start']}" +
f" | Delay: {s['auto_start_delay']} ")
def refresh_server_settings(self, server_id: int):
server_obj = self.get_server_obj(server_id)
server_obj.reload_server_settings()
@staticmethod
def check_system_user():
if helper_users.get_user_id_by_name("system") is not None:
return True
else:
return False
def set_project_root(self, root_dir):
self.project_root = root_dir
def package_support_logs(self, exec_user):
#pausing so on screen notifications can run for user
time.sleep(7)
websocket_helper.broadcast_user(exec_user['user_id'], 'notification', 'Preparing your support logs')
tempDir = tempfile.mkdtemp()
tempZipStorage = tempfile.mkdtemp()
full_temp = os.path.join(tempDir, 'support_logs')
os.mkdir(full_temp)
tempZipStorage = os.path.join(tempZipStorage, "support_logs")
crafty_path = os.path.join(full_temp, "crafty")
os.mkdir(crafty_path)
server_path = os.path.join(full_temp, "server")
os.mkdir(server_path)
if exec_user['superuser']:
auth_servers = self.servers.get_all_defined_servers()
else:
user_servers = self.servers.get_authorized_servers(int(exec_user['user_id']))
auth_servers = []
for server in user_servers:
if Enum_Permissions_Server.Logs in self.server_perms.get_user_id_permissions_list(exec_user['user_id'], server["server_id"]):
auth_servers.append(server)
else:
logger.info(f"Logs permission not available for server {server['server_name']}. Skipping.")
#we'll iterate through our list of log paths from auth servers.
for server in auth_servers:
final_path = os.path.join(server_path, str(server['server_name']))
os.mkdir(final_path)
try:
file_helper.copy_file(server['log_path'], final_path)
except Exception as e:
logger.warning(f"Failed to copy file with error: {e}")
#Copy crafty logs to archive dir
full_log_name = os.path.join(crafty_path, 'logs')
file_helper.copy_dir(os.path.join(self.project_root, 'logs'), full_log_name)
file_helper.make_archive(tempZipStorage, tempDir)
tempZipStorage += '.zip'
websocket_helper.broadcast_user(exec_user['user_id'], 'send_logs_bootbox', {
})
self.users.set_support_path(exec_user['user_id'], tempZipStorage)
@staticmethod
def add_system_user():
helper_users.add_user("system", helper.random_string_generator(64), "default@example.com", False, False)
def get_server_settings(self, server_id):
for s in self.servers_list:
if int(s['server_id']) == int(server_id):
return s['server_settings']
logger.warning("Unable to find server object for server id {}".format(server_id))
logger.warning(f"Unable to find server object for server id {server_id}")
return False
def get_server_obj(self, server_id):
def crash_detection(self, server_obj):
svr = self.get_server_obj(server_obj.server_id)
#start or stop crash detection depending upon user preference
#The below functions check to see if the server is running. They only execute if it's running.
if server_obj.crash_detection == 1:
svr.start_crash_detection()
else:
svr.stop_crash_detection()
def get_server_obj(self, server_id: Union[str, int]) -> Union[bool, Server]:
for s in self.servers_list:
if int(s['server_id']) == int(server_id):
if str(s['server_id']) == str(server_id):
return s['server_obj']
logger.warning("Unable to find server object for server id {}".format(server_id))
return False
logger.warning(f"Unable to find server object for server id {server_id}")
return False # TODO: Change to None
def get_server_data(self, server_id):
def get_server_data(self, server_id: str):
for s in self.servers_list:
if int(s['server_id']) == int(server_id):
if str(s['server_id']) == str(server_id):
return s['server_data_obj']
logger.warning("Unable to find server object for server id {}".format(server_id))
logger.warning(f"Unable to find server object for server id {server_id}")
return False
@staticmethod
@ -165,15 +231,15 @@ class Controller:
def stop_all_servers(self):
servers = self.list_running_servers()
logger.info("Found {} running server(s)".format(len(servers)))
console.info("Found {} running server(s)".format(len(servers)))
logger.info(f"Found {len(servers)} running server(s)")
console.info(f"Found {len(servers)} running server(s)")
logger.info("Stopping All Servers")
console.info("Stopping All Servers")
for s in servers:
logger.info("Stopping Server ID {} - {}".format(s['id'], s['name']))
console.info("Stopping Server ID {} - {}".format(s['id'], s['name']))
logger.info(f"Stopping Server ID {s['id']} - {s['name']}")
console.info(f"Stopping Server ID {s['id']} - {s['name']}")
self.stop_server(s['id'])
@ -185,14 +251,20 @@ class Controller:
def stop_server(self, server_id):
# issue the stop command
svr_obj = self.get_server_obj(server_id)
svr_obj.stop_threaded_server()
def create_jar_server(self, server: str, version: str, name: str, min_mem: int, max_mem: int, port: int):
server_id = helper.create_uuid()
server_dir = os.path.join(helper.servers_dir, server_id)
backup_path = os.path.join(helper.backup_path, server_id)
if helper.is_os_windows():
server_dir = helper.wtol_path(server_dir)
backup_path = helper.wtol_path(backup_path)
server_dir.replace(' ', '^ ')
backup_path.replace(' ', '^ ')
server_file = "{server}-{version}.jar".format(server=server, version=version)
server_file = f"{server}-{version}.jar"
full_jar_path = os.path.join(server_dir, server_file)
# make the dir - perhaps a UUID?
@ -201,33 +273,38 @@ class Controller:
try:
# do a eula.txt
with open(os.path.join(server_dir, "eula.txt"), 'w') as f:
with open(os.path.join(server_dir, "eula.txt"), 'w', encoding='utf-8') as f:
f.write("eula=false")
f.close()
# setup server.properties with the port
with open(os.path.join(server_dir, "server.properties"), "w") as f:
f.write("server-port={}".format(port))
with open(os.path.join(server_dir, "server.properties"), "w", encoding='utf-8') as f:
f.write(f"server-port={port}")
f.close()
except Exception as e:
logger.error("Unable to create required server files due to :{}".format(e))
logger.error(f"Unable to create required server files due to :{e}")
return False
server_command = 'java -Xms{}M -Xmx{}M -jar {} nogui'.format(helper.float_to_string(min_mem),
helper.float_to_string(max_mem),
full_jar_path)
server_log_file = "{}/logs/latest.log".format(server_dir)
#must remain non-fstring due to string addtion
if helper.is_os_windows():
server_command = f'java -Xms{helper.float_to_string(min_mem)}M -Xmx{helper.float_to_string(max_mem)}M -jar "{full_jar_path}" nogui'
else:
server_command = f'java -Xms{helper.float_to_string(min_mem)}M -Xmx{helper.float_to_string(max_mem)}M -jar {full_jar_path} nogui'
server_log_file = f"{server_dir}/logs/latest.log"
server_stop = "stop"
# download the jar
server_jar_obj.download_jar(server, version, full_jar_path, name)
new_id = self.register_server(name, server_id, server_dir, backup_path, server_command, server_file, server_log_file, server_stop,
port, server_type='minecraft-java')
# download the jar
server_jar_obj.download_jar(server, version, full_jar_path, new_id)
new_id = self.register_server(name, server_id, server_dir, backup_path, server_command, server_file, server_log_file, server_stop)
return new_id
@staticmethod
def verify_jar_server( server_path: str, server_jar: str):
server_path = helper.get_os_understandable_path(server_path)
path_check = helper.check_path_exists(server_path)
jar_check = helper.check_file_exists(os.path.join(server_path, server_jar))
if not path_check or not jar_check:
@ -236,6 +313,7 @@ class Controller:
@staticmethod
def verify_zip_server(zip_path: str):
zip_path = helper.get_os_understandable_path(zip_path)
zip_check = helper.check_file_exists(zip_path)
if not zip_check:
return False
@ -245,87 +323,238 @@ class Controller:
server_id = helper.create_uuid()
new_server_dir = os.path.join(helper.servers_dir, server_id)
backup_path = os.path.join(helper.backup_path, server_id)
if helper.is_os_windows():
new_server_dir = helper.wtol_path(new_server_dir)
backup_path = helper.wtol_path(backup_path)
new_server_dir.replace(' ', '^ ')
backup_path.replace(' ', '^ ')
helper.ensure_dir_exists(new_server_dir)
helper.ensure_dir_exists(backup_path)
dir_util.copy_tree(server_path, new_server_dir)
server_path = helper.get_os_understandable_path(server_path)
try:
file_helper.copy_dir(server_path, new_server_dir, True)
except shutil.Error as ex:
logger.error(f"Server import failed with error: {ex}")
has_properties = False
for item in os.listdir(new_server_dir):
if str(item) == 'server.properties':
has_properties = True
if not has_properties:
logger.info(f"No server.properties found on zip file import. Creating one with port selection of {str(port)}")
with open(os.path.join(new_server_dir, "server.properties"), "w", encoding='utf-8') as f:
f.write(f"server-port={port}")
f.close()
full_jar_path = os.path.join(new_server_dir, server_jar)
server_command = 'java -Xms{}M -Xmx{}M -jar {} nogui'.format(helper.float_to_string(min_mem),
helper.float_to_string(max_mem),
full_jar_path)
server_log_file = "{}/logs/latest.log".format(new_server_dir)
#due to adding strings this must not be an fstring
if helper.is_os_windows():
server_command = f'java -Xms{helper.float_to_string(min_mem)}M -Xmx{helper.float_to_string(max_mem)}M -jar "{full_jar_path}" nogui'
else:
server_command = f'java -Xms{helper.float_to_string(min_mem)}M -Xmx{helper.float_to_string(max_mem)}M -jar {full_jar_path} nogui'
server_log_file = f"{new_server_dir}/logs/latest.log"
server_stop = "stop"
new_id = self.register_server(server_name, server_id, new_server_dir, backup_path, server_command, server_jar,
server_log_file, server_stop, port)
server_log_file, server_stop, port, server_type='minecraft-java')
return new_id
def import_zip_server(self, server_name: str, zip_path: str, server_jar: str, min_mem: int, max_mem: int, port: int):
server_id = helper.create_uuid()
new_server_dir = os.path.join(helper.servers_dir, server_id)
backup_path = os.path.join(helper.backup_path, server_id)
if helper.is_os_windows():
new_server_dir = helper.wtol_path(new_server_dir)
backup_path = helper.wtol_path(backup_path)
new_server_dir.replace(' ', '^ ')
backup_path.replace(' ', '^ ')
if helper.check_file_perms(zip_path):
tempDir = helper.get_os_understandable_path(zip_path)
helper.ensure_dir_exists(new_server_dir)
helper.ensure_dir_exists(backup_path)
tempDir = tempfile.mkdtemp()
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(tempDir)
for i in range(len(zip_ref.filelist)):
if len(zip_ref.filelist) > 1 or not zip_ref.filelist[i].filename.endswith('/'):
test = zip_ref.filelist[i].filename
break
path_list = test.split('/')
root_path = path_list[0]
if len(path_list) > 1:
for i in range(len(path_list)-2):
root_path = os.path.join(root_path, path_list[i+1])
full_root_path = os.path.join(tempDir, root_path)
has_properties = False
for item in os.listdir(full_root_path):
#extracts archive to temp directory
for item in os.listdir(tempDir):
if str(item) == 'server.properties':
has_properties = True
try:
shutil.move(os.path.join(full_root_path, item), os.path.join(new_server_dir, item))
except Exception as ex:
logger.error('ERROR IN ZIP IMPORT: {}'.format(ex))
if not has_properties:
logger.info("No server.properties found on zip file import. Creating one with port selection of {}".format(str(port)))
with open(os.path.join(new_server_dir, "server.properties"), "w") as f:
f.write("server-port={}".format(port))
f.close()
zip_ref.close()
if not os.path.isdir(os.path.join(tempDir, item)):
file_helper.move_file(os.path.join(tempDir, item), os.path.join(new_server_dir, item))
else:
return "false"
file_helper.move_dir(os.path.join(tempDir, item), os.path.join(new_server_dir, item))
except Exception as ex:
logger.error(f'ERROR IN ZIP IMPORT: {ex}')
if not has_properties:
logger.info(f"No server.properties found on zip file import. Creating one with port selection of {str(port)}")
with open(os.path.join(new_server_dir, "server.properties"), "w", encoding='utf-8') as f:
f.write(f"server-port={port}")
f.close()
full_jar_path = os.path.join(new_server_dir, server_jar)
server_command = 'java -Xms{}M -Xmx{}M -jar {} nogui'.format(helper.float_to_string(min_mem),
helper.float_to_string(max_mem),
full_jar_path)
#due to strings being added we need to leave this as not an fstring
if helper.is_os_windows():
server_command = f'java -Xms{helper.float_to_string(min_mem)}M -Xmx{helper.float_to_string(max_mem)}M -jar "{full_jar_path}" nogui'
else:
server_command = f'java -Xms{helper.float_to_string(min_mem)}M -Xmx{helper.float_to_string(max_mem)}M -jar {full_jar_path} nogui'
logger.debug('command: ' + server_command)
server_log_file = "{}/logs/latest.log".format(new_server_dir)
server_log_file = f"{new_server_dir}/logs/latest.log"
server_stop = "stop"
new_id = self.register_server(server_name, server_id, new_server_dir, backup_path, server_command, server_jar,
server_log_file, server_stop, port)
server_log_file, server_stop, port, server_type='minecraft-java')
return new_id
def register_server(self, name: str, server_uuid: str, server_dir: str, backup_path: str, server_command: str, server_file: str, server_log_file: str, server_stop: str, server_port=25565):
# put data in the db
new_id = self.servers.create_server(name, server_uuid, server_dir, backup_path, server_command, server_file, server_log_file, server_stop, server_port)
#************************************************************************************************
# BEDROCK IMPORTS
#************************************************************************************************
def import_bedrock_server(self, server_name: str, server_path: str, server_exe: str, port: int):
server_id = helper.create_uuid()
new_server_dir = os.path.join(helper.servers_dir, server_id)
backup_path = os.path.join(helper.backup_path, server_id)
if helper.is_os_windows():
new_server_dir = helper.wtol_path(new_server_dir)
backup_path = helper.wtol_path(backup_path)
new_server_dir.replace(' ', '^ ')
backup_path.replace(' ', '^ ')
helper.ensure_dir_exists(new_server_dir)
helper.ensure_dir_exists(backup_path)
server_path = helper.get_os_understandable_path(server_path)
try:
file_helper.copy_dir(server_path, new_server_dir, True)
except shutil.Error as ex:
logger.error(f"Server import failed with error: {ex}")
has_properties = False
for item in os.listdir(new_server_dir):
if str(item) == 'server.properties':
has_properties = True
if not has_properties:
logger.info(f"No server.properties found on zip file import. Creating one with port selection of {str(port)}")
with open(os.path.join(new_server_dir, "server.properties"), "w", encoding='utf-8') as f:
f.write(f"server-port={port}")
f.close()
full_jar_path = os.path.join(new_server_dir, server_exe)
#due to adding strings this must not be an fstring
if helper.is_os_windows():
server_command = f'"{full_jar_path}"'
else:
server_command = f'./{server_exe}'
logger.debug('command: ' + server_command)
server_log_file = "N/A"
server_stop = "stop"
new_id = self.register_server(server_name, server_id, new_server_dir, backup_path, server_command, server_exe,
server_log_file, server_stop, port, server_type='minecraft-bedrock')
if os.name != "nt":
if helper.check_file_exists(full_jar_path):
os.chmod(full_jar_path, 0o2775)
return new_id
def import_bedrock_zip_server(self, server_name: str, zip_path: str, server_exe: str, port: int):
server_id = helper.create_uuid()
new_server_dir = os.path.join(helper.servers_dir, server_id)
backup_path = os.path.join(helper.backup_path, server_id)
if helper.is_os_windows():
new_server_dir = helper.wtol_path(new_server_dir)
backup_path = helper.wtol_path(backup_path)
new_server_dir.replace(' ', '^ ')
backup_path.replace(' ', '^ ')
tempDir = helper.get_os_understandable_path(zip_path)
helper.ensure_dir_exists(new_server_dir)
helper.ensure_dir_exists(backup_path)
has_properties = False
#extracts archive to temp directory
for item in os.listdir(tempDir):
if str(item) == 'server.properties':
has_properties = True
try:
if not os.path.isdir(os.path.join(tempDir, item)):
file_helper.move_file(os.path.join(tempDir, item), os.path.join(new_server_dir, item))
else:
file_helper.move_dir(os.path.join(tempDir, item), os.path.join(new_server_dir, item))
except Exception as ex:
logger.error(f'ERROR IN ZIP IMPORT: {ex}')
if not has_properties:
logger.info(f"No server.properties found on zip file import. Creating one with port selection of {str(port)}")
with open(os.path.join(new_server_dir, "server.properties"), "w", encoding='utf-8') as f:
f.write(f"server-port={port}")
f.close()
full_jar_path = os.path.join(new_server_dir, server_exe)
#due to strings being added we need to leave this as not an fstring
if helper.is_os_windows():
server_command = f'"{full_jar_path}"'
else:
server_command = f'./{server_exe}'
logger.debug('command: ' + server_command)
server_log_file = "N/A"
server_stop = "stop"
new_id = self.register_server(server_name, server_id, new_server_dir, backup_path, server_command, server_exe,
server_log_file, server_stop, port, server_type='minecraft-bedrock')
if os.name != "nt":
if helper.check_file_exists(full_jar_path):
os.chmod(full_jar_path, 0o2775)
return new_id
#************************************************************************************************
# BEDROCK IMPORTS END
#************************************************************************************************
def rename_backup_dir(self, old_server_id, new_server_id, new_uuid):
server_data = self.servers.get_server_data_by_id(old_server_id)
old_bu_path = server_data['backup_path']
Server_Perms_Controller.backup_role_swap(old_server_id, new_server_id)
if not helper.is_os_windows():
backup_path = helper.validate_traversal(helper.backup_path, old_bu_path)
if helper.is_os_windows():
backup_path = helper.validate_traversal(helper.wtol_path(helper.backup_path), helper.wtol_path(old_bu_path))
backup_path = helper.wtol_path(str(backup_path))
backup_path.replace(' ', '^ ')
backup_path = Path(backup_path)
backup_path_components = list(backup_path.parts)
backup_path_components[-1] = new_uuid
new_bu_path = pathlib.PurePath(os.path.join(*backup_path_components))
if os.path.isdir(new_bu_path):
if helper.validate_traversal(helper.backup_path, new_bu_path):
os.rmdir(new_bu_path)
backup_path.rename(new_bu_path)
def register_server(self, name: str,
server_uuid: str,
server_dir: str,
backup_path: str,
server_command: str,
server_file: str,
server_log_file: str,
server_stop: str,
server_port: int,
server_type: str):
# put data in the db
new_id = self.servers.create_server(
name, server_uuid, server_dir, backup_path, server_command, server_file, server_log_file, server_stop, server_type, server_port)
if not helper.check_file_exists(os.path.join(server_dir, "crafty_managed.txt")):
try:
# place a file in the dir saying it's owned by crafty
with open(os.path.join(server_dir, "crafty_managed.txt"), 'w') as f:
with open(os.path.join(server_dir, "crafty_managed.txt"), 'w', encoding='utf-8') as f:
f.write(
"The server is managed by Crafty Controller.\n Leave this directory/files alone please")
f.close()
except Exception as e:
logger.error("Unable to create required server files due to :{}".format(e))
logger.error(f"Unable to create required server files due to :{e}")
return False
# let's re-init all servers
@ -338,12 +567,12 @@ class Controller:
for s in self.servers_list:
# if this is the droid... im mean server we are looking for...
if int(s['server_id']) == int(server_id):
if str(s['server_id']) == str(server_id):
server_data = self.get_server_data(server_id)
server_name = server_data['server_name']
logger.info("Deleting Server: ID {} | Name: {} ".format(server_id, server_name))
console.info("Deleting Server: ID {} | Name: {} ".format(server_id, server_name))
logger.info(f"Deleting Server: ID {server_id} | Name: {server_name} ")
console.info(f"Deleting Server: ID {server_id} | Name: {server_name} ")
srv_obj = s['server_obj']
running = srv_obj.check_running()
@ -351,7 +580,19 @@ class Controller:
if running:
self.stop_server(server_id)
if files:
shutil.rmtree(self.servers.get_server_data_by_id(server_id)['path'])
try:
file_helper.del_dirs(helper.get_os_understandable_path(self.servers.get_server_data_by_id(server_id)['path']))
except Exception as e:
logger.error(f"Unable to delete server files for server with ID: {server_id} with error logged: {e}")
if helper.check_path_exists(self.servers.get_server_data_by_id(server_id)['backup_path']):
file_helper.del_dirs(helper.get_os_understandable_path(self.servers.get_server_data_by_id(server_id)['backup_path']))
#Cleanup scheduled tasks
try:
helpers_management.delete_scheduled_task_by_server(server_id)
except DoesNotExist:
logger.info("No scheduled jobs exist. Continuing.")
# remove the server from the DB
self.servers.remove_server(server_id)
@ -359,3 +600,6 @@ class Controller:
self.servers_list.pop(counter)
counter += 1
@staticmethod
def clear_unexecuted_commands():
helpers_management.clear_unexecuted_commands()

View File

@ -1,30 +1,24 @@
import os
import sys
import logging
import datetime
from app.classes.models.users import Users, users_helper
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
from app.classes.models.users import Users, users_helper
from app.classes.minecraft.server_props import ServerProps
from app.classes.web.websocket_helper import websocket_helper
# To disable warning about unused import ; Users is imported from here in other places
# pylint: disable=self-assigning-variable
Users = Users
try:
# pylint: disable=unused-import
from peewee import SqliteDatabase, fn
from playhouse.shortcuts import model_to_dict
except ModuleNotFoundError as err:
helper.auto_installer_fix(err)
logger = logging.getLogger(__name__)
peewee_logger = logging.getLogger('peewee')
peewee_logger.setLevel(logging.INFO)
try:
from peewee import *
from playhouse.shortcuts import model_to_dict
from enum import Enum
import yaml
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True)
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
database = SqliteDatabase(helper.db_path, pragmas = {
'journal_mode': 'wal',
'cache_size': -1024 * 10})
@ -39,28 +33,17 @@ class db_builder:
username = default_data.get("username", 'admin')
password = default_data.get("password", 'crafty')
#api_token = helper.random_string_generator(32)
#
#Users.insert({
# Users.username: username.lower(),
# Users.password: helper.encode_pass(password),
# Users.api_token: api_token,
# Users.enabled: True,
# Users.superuser: True
#}).execute()
user_id = users_helper.add_user(username=username, password=password, superuser=True)
#users_helper.update_user(user_id, user_crafty_data={"permissions_mask":"111", "server_quantity":[-1,-1,-1]} )
#console.info("API token is {}".format(api_token))
users_helper.add_user(username=username, password=password, email="default@example.com", superuser=True)
@staticmethod
def is_fresh_install():
try:
user = users_helper.get_by_id(1)
if user:
return False
except:
return True
pass
class db_shortcuts:
@ -76,8 +59,7 @@ class db_shortcuts:
for s in query:
rows.append(model_to_dict(s))
except Exception as e:
logger.warning("Database Error: {}".format(e))
pass
logger.warning(f"Database Error: {e}")
return rows

View File

@ -1,36 +1,44 @@
# pylint: skip-file
from datetime import datetime
import logging
import typing as t
import sys
import os
import re
from importlib import import_module
from functools import wraps
try:
from functools import cached_property
except ImportError:
from cached_property import cached_property
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
logger = logging.getLogger(__name__)
try:
import peewee
from playhouse.migrate import (
SchemaMigrator as ScM,
SqliteMigrator as SqM,
Operation, SQL, operation, SqliteDatabase,
make_index_name, Context
SqliteMigrator,
Operation, SQL, SqliteDatabase,
make_index_name
)
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(
e.name), exc_info=True)
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
helper.auto_installer_fix(e)
logger = logging.getLogger(__name__)
MIGRATE_TABLE = 'migratehistory'
MIGRATE_TEMPLATE = '''# Generated by database migrator
import peewee
def migrate(migrator, db):
"""
Write your migrations here.
"""
{migrate}
def rollback(migrator, db):
"""
Write your rollback migrations here.
"""
{rollback}'''
class MigrateHistory(peewee.Model):
@ -41,30 +49,15 @@ class MigrateHistory(peewee.Model):
name = peewee.CharField(unique=True)
migrated_at = peewee.DateTimeField(default=datetime.utcnow)
# noinspection PyTypeChecker
def __unicode__(self) -> str:
"""
String representation of this migration
"""
return self.name
MIGRATE_TABLE = 'migratehistory'
MIGRATE_TEMPLATE = '''# Generated by database migrator
def migrate(migrator, database, **kwargs):
"""
Write your migrations here.
"""
{migrate}
def rollback(migrator, database, **kwargs):
"""
Write your rollback migrations here.
"""
{rollback}'''
VOID: t.Callable = lambda m, d: None
class Meta:
table_name = MIGRATE_TABLE
def get_model(method):
@ -75,11 +68,12 @@ def get_model(method):
@wraps(method)
def wrapper(migrator, model, *args, **kwargs):
if isinstance(model, str):
return method(migrator, migrator.orm[model], *args, **kwargs)
return method(migrator, migrator.table_dict[model], *args, **kwargs)
return method(migrator, model, *args, **kwargs)
return wrapper
# noinspection PyProtectedMember
class Migrator(object):
def __init__(self, database: t.Union[peewee.Database, peewee.Proxy]):
"""
@ -88,8 +82,8 @@ class Migrator(object):
if isinstance(database, peewee.Proxy):
database = database.obj
self.database: SqliteDatabase = database
self.orm: t.Dict[str, peewee.Model] = {}
self.operations: t.List[Operation] = []
self.table_dict: t.Dict[str, peewee.Model] = {}
self.operations: t.List[t.Union[Operation, callable]] = []
self.migrator = SqliteMigrator(database)
def run(self):
@ -113,13 +107,13 @@ class Migrator(object):
"""
Executes raw SQL.
"""
self.operations.append(self.migrator.sql(sql, *params))
self.operations.append(SQL(sql, *params))
def create_table(self, model: peewee.Model) -> peewee.Model:
"""
Creates model and table in database.
"""
self.orm[model._meta.table_name] = model
self.table_dict[model._meta.table_name] = model
model._meta.database = self.database
self.operations.append(model.create_table)
return model
@ -129,8 +123,8 @@ class Migrator(object):
"""
Drops model and table from database.
"""
del self.orm[model._meta.table_name]
self.operations.append(self.migrator.drop_table(model))
del self.table_dict[model._meta.table_name]
self.operations.append(lambda: model.drop_table(cascade=False))
@get_model
def add_columns(self, model: peewee.Model, **fields: peewee.Field) -> peewee.Model:
@ -147,64 +141,16 @@ class Migrator(object):
return model
@get_model
def change_columns(self, model: peewee.Model, **fields: peewee.Field) -> peewee.Model:
"""
Changes fields.
"""
for name, field in fields.items():
old_field = model._meta.fields.get(name, field)
old_column_name = old_field and old_field.column_name
model._meta.add_field(name, field)
if isinstance(old_field, peewee.ForeignKeyField):
self.operations.append(self.migrator.drop_foreign_key_constraint(
model._meta.table_name, old_column_name))
if old_column_name != field.column_name:
self.operations.append(
self.migrator.rename_column(
model._meta.table_name, old_column_name, field.column_name))
if isinstance(field, peewee.ForeignKeyField):
on_delete = field.on_delete if field.on_delete else 'RESTRICT'
on_update = field.on_update if field.on_update else 'RESTRICT'
self.operations.append(self.migrator.add_foreign_key_constraint(
model._meta.table_name, field.column_name,
field.rel_model._meta.table_name, field.rel_field.name,
on_delete, on_update))
continue
self.operations.append(self.migrator.change_column(
model._meta.table_name, field.column_name, field))
if field.unique == old_field.unique:
continue
if field.unique:
index = (field.column_name,), field.unique
self.operations.append(self.migrator.add_index(
model._meta.table_name, *index))
model._meta.indexes.append(index)
else:
index = (field.column_name,), old_field.unique
self.operations.append(self.migrator.drop_index(
model._meta.table_name, *index))
model._meta.indexes.remove(index)
return model
@get_model
def drop_columns(self, model: peewee.Model, names: str, **kwargs) -> peewee.Model:
def drop_columns(self, model: peewee.Model, names: str) -> peewee.Model:
"""
Removes fields from model.
"""
fields = [field for field in model._meta.fields.values()
if field.name in names]
cascade = kwargs.pop('cascade', True)
for field in fields:
self.__del_field__(model, field)
if field.unique:
# Drop unique index
index_name = make_index_name(
model._meta.table_name, [field.column_name])
self.operations.append(self.migrator.drop_index(
@ -250,16 +196,15 @@ class Migrator(object):
Renames table in database.
"""
old_name = model._meta.table_name
del self.orm[model._meta.table_name]
del self.table_dict[model._meta.table_name]
model._meta.table_name = new_name
self.orm[model._meta.table_name] = model
self.table_dict[model._meta.table_name] = model
self.operations.append(self.migrator.rename_table(old_name, new_name))
return model
@get_model
def add_index(self, model: peewee.Model, *columns: str, **kwargs) -> peewee.Model:
def add_index(self, model: peewee.Model, *columns: str, unique=False) -> peewee.Model:
"""Create indexes."""
unique = kwargs.pop('unique', False)
model._meta.indexes.append((columns, unique))
columns_ = []
for col in columns:
@ -329,42 +274,8 @@ class Migrator(object):
return model
class SqliteMigrator(SqM):
def drop_table(self, model):
return lambda: model.drop_table(cascade=False)
@operation
def change_column(self, table: str, column_name: str, field: peewee.Field):
operations = [self.alter_change_column(table, column_name, field)]
if not field.null:
operations.extend([self.add_not_null(table, column_name)])
return operations
def alter_change_column(self, table: str, column_name: str, field: peewee.Field) -> Operation:
return self._update_column(table, column_name, lambda x, y: y)
@operation
def sql(self, sql: str, *params) -> SQL:
"""
Executes raw SQL.
"""
return SQL(sql, *params)
def alter_add_column(
self, table: str, column_name: str, field: peewee.Field, **kwargs) -> Operation:
"""
Fixes field name for ForeignKeys.
"""
name = field.name
op = super().alter_add_column(
table, column_name, field, **kwargs)
if isinstance(field, peewee.ForeignKeyField):
field.name = name
return op
# noinspection PyProtectedMember
class MigrationManager(object):
filemask = re.compile(r"[\d]+_[^\.]+\.py$")
def __init__(self, database: t.Union[peewee.Database, peewee.Proxy]):
@ -376,7 +287,7 @@ class MigrationManager(object):
self.database = database
@cached_property
def model(self) -> peewee.Model:
def model(self) -> t.Type[MigrateHistory]:
"""
Initialize and cache the MigrationHistory model.
"""
@ -479,7 +390,7 @@ class MigrationManager(object):
Reads a migration from a file.
"""
call_params = dict()
if os.name == 'nt' and sys.version_info >= (3, 0):
if helper.is_os_windows() and sys.version_info >= (3, 0):
# if system is windows - force utf-8 encoding
call_params['encoding'] = 'utf-8'
with open(os.path.join(helper.migration_dir, name + '.py'), **call_params) as f:
@ -487,7 +398,7 @@ class MigrationManager(object):
scope = {}
code = compile(code, '<string>', 'exec', dont_inherit=True)
exec(code, scope, None)
return scope.get('migrate', VOID), scope.get('rollback', VOID)
return scope.get('migrate', lambda m, d: None), scope.get('rollback', lambda m, d: None)
def up_one(self, name: str, migrator: Migrator,
fake: bool = False, rollback: bool = False) -> str:
@ -518,11 +429,11 @@ class MigrationManager(object):
except Exception:
self.database.rollback()
operation = 'Rollback' if rollback else 'Migration'
logger.exception('{} failed: {}'.format(operation, name))
operation_name = 'Rollback' if rollback else 'Migration'
logger.exception('{} failed: {}'.format(operation_name, name))
raise
def down(self, name: t.Optional[str] = None):
def down(self):
"""
Rolls back migrations.
"""

View File

@ -0,0 +1,22 @@
from enum import Enum
class PermissionHelper:
@staticmethod
def both_have_perm(a: str, b: str, permission_tested: Enum):
return permission_helper.combine_perm_bool(a[permission_tested.value], b[permission_tested.value])
@staticmethod
def combine_perm(a: str, b: str) -> str:
return '1' if (a == '1' and b == '1') else '0'
@staticmethod
def combine_perm_bool(a: str, b: str) -> bool:
return a == '1' and b == '1'
@staticmethod
def combine_masks(permission_mask_a: str, permission_mask_b: str) -> str:
both_masks = zip(list(permission_mask_a), list(permission_mask_b))
return ''.join(map(lambda x: permission_helper.combine_perm(x[0], x[1]), both_masks))
permission_helper = PermissionHelper()

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +1,29 @@
import os
import sys
import json
import time
import logging
import threading
import asyncio
import shutil
import datetime
from app.classes.controllers.users_controller import Users_Controller
from app.classes.minecraft.serverjars import server_jar_obj
from app.classes.models.management import management_helper
from app.classes.models.users import users_helper
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
from app.classes.web.tornado import Webserver
from app.classes.web.tornado_handler import Webserver
from app.classes.web.websocket_helper import websocket_helper
from app.classes.minecraft.serverjars import server_jar_obj
from app.classes.models.servers import servers_helper
from app.classes.models.management import management_helper
logger = logging.getLogger(__name__)
try:
import schedule
from tzlocal import get_localzone
from apscheduler.events import EVENT_JOB_EXECUTED
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True)
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
except ModuleNotFoundError as err:
helper.auto_installer_fix(err)
logger = logging.getLogger('apscheduler')
scheduler_intervals = { 'seconds',
'minutes',
'hours',
@ -46,9 +44,13 @@ class TasksManager:
self.controller = controller
self.tornado = Webserver(controller, self)
self.tz = get_localzone()
self.scheduler = BackgroundScheduler(timezone=str(self.tz))
self.users_controller = Users_Controller()
self.webserver_thread = threading.Thread(target=self.tornado.run_tornado, daemon=True, name='tornado_thread')
self.main_kill_switch_thread = threading.Thread(target=self.main_kill_switch, daemon=True, name="main_loop")
self.main_thread_exiting = False
self.schedule_thread = threading.Thread(target=self.scheduler_thread, daemon=True, name="scheduler")
@ -65,65 +67,49 @@ class TasksManager:
def get_main_thread_run_status(self):
return self.main_thread_exiting
def start_main_kill_switch_watcher(self):
self.main_kill_switch_thread.start()
def main_kill_switch(self):
while True:
if os.path.exists(os.path.join(helper.root_dir, 'exit.txt')):
logger.info("Found Exit File, stopping everything")
self._main_graceful_exit()
time.sleep(5)
def reload_schedule_from_db(self):
jobs = management_helper.get_schedules_enabled()
schedule.clear(tag='backup')
schedule.clear(tag='db')
for j in jobs:
if j.interval_type in scheduler_intervals:
logger.info("Loading schedule ID#{i}: '{a}' every {n} {t} at {s}".format(
i=j.schedule_id, a=j.action, n=j.interval, t=j.interval_type, s=j.start_time))
try:
getattr(schedule.every(j.interval), j.interval_type).at(j.start_time).do(
self.controller.management.send_command, 0, j.server_id, "127.27.23.89", j.action)
except schedule.ScheduleValueError as e:
logger.critical("Scheduler value error occurred: {} on ID#{}".format(e, j.schedule_id))
else:
logger.critical("Unknown schedule job type '{}' at id {}, skipping".format(j.interval_type, j.schedule_id))
logger.info("Reload from DB called. Current enabled schedules: ")
for item in jobs:
logger.info(f"JOB: {item}")
def command_watcher(self):
while True:
# select any commands waiting to be processed
commands = management_helper.get_unactioned_commands()
for c in commands:
try:
svr = self.controller.get_server_obj(c.server_id)
except:
logger.error("Server value requested does note exist purging item from waiting commands.")
management_helper.mark_command_complete(c.command_id)
svr = self.controller.get_server_obj(c['server_id']['server_id'])
user_lang = c.get('user')['lang']
command = c.get('command', None)
user_id = c.user_id
command = c.command
if command == 'start_server':
svr.run_threaded_server(user_lang)
svr.run_threaded_server(user_id)
elif command == 'stop_server':
svr.stop_threaded_server()
elif command == "restart_server":
svr.restart_threaded_server(user_lang)
svr.restart_threaded_server(user_id)
elif command == "backup_server":
svr.backup_server()
elif command == "update_executable":
svr.jar_update()
management_helper.mark_command_complete(c.get('command_id', None))
else:
svr.send_command(command)
management_helper.mark_command_complete(c.command_id)
time.sleep(1)
def _main_graceful_exit(self):
try:
os.remove(helper.session_file)
os.remove(os.path.join(helper.root_dir, 'exit.txt'))
os.remove(os.path.join(helper.root_dir, '.header'))
self.controller.stop_all_servers()
except:
logger.info("Caught error during shutdown", exc_info=True)
@ -159,33 +145,283 @@ class TasksManager:
console.info("Launching realtime thread...")
self.realtime_thread.start()
@staticmethod
def scheduler_thread():
while True:
schedule.run_pending()
time.sleep(1)
def scheduler_thread(self):
schedules = management_helper.get_schedules_enabled()
self.scheduler.add_listener(self.schedule_watcher, mask=EVENT_JOB_EXECUTED)
#self.scheduler.add_job(self.scheduler.print_jobs, 'interval', seconds=10, id='-1')
#load schedules from DB
for schedule in schedules:
if schedule.interval != 'reaction':
if schedule.cron_string != "":
try:
self.scheduler.add_job(management_helper.add_command,
CronTrigger.from_crontab(schedule.cron_string,
timezone=str(self.tz)),
id = str(schedule.schedule_id),
args = [schedule.server_id,
self.users_controller.get_id_by_name('system'),
'127.0.0.1',
schedule.command]
)
except Exception as e:
console.error(f"Failed to schedule task with error: {e}.")
console.warning("Removing failed task from DB.")
logger.error(f"Failed to schedule task with error: {e}.")
logger.warning("Removing failed task from DB.")
#remove items from DB if task fails to add to apscheduler
management_helper.delete_scheduled_task(schedule.schedule_id)
else:
if schedule.interval_type == 'hours':
self.scheduler.add_job(management_helper.add_command,
'cron',
minute = 0,
hour = '*/'+str(schedule.interval),
id = str(schedule.schedule_id),
args = [schedule.server_id,
self.users_controller.get_id_by_name('system'),
'127.0.0.1',
schedule.command]
)
elif schedule.interval_type == 'minutes':
self.scheduler.add_job(management_helper.add_command,
'cron',
minute = '*/'+str(schedule.interval),
id = str(schedule.schedule_id),
args = [schedule.server_id,
self.users_controller.get_id_by_name('system'),
'127.0.0.1',
schedule.command]
)
elif schedule.interval_type == 'days':
curr_time = schedule.start_time.split(':')
self.scheduler.add_job(management_helper.add_command,
'cron',
day = '*/'+str(schedule.interval),
hour=curr_time[0],
minute=curr_time[1],
id=str(schedule.schedule_id),
args=[schedule.server_id,
self.users_controller.get_id_by_name('system'),
'127.0.0.1',
schedule.command]
)
self.scheduler.start()
jobs = self.scheduler.get_jobs()
logger.info("Loaded schedules. Current enabled schedules: ")
for item in jobs:
logger.info(f"JOB: {item}")
def schedule_job(self, job_data):
sch_id = management_helper.create_scheduled_task(
job_data['server_id'],
job_data['action'],
job_data['interval'],
job_data['interval_type'],
job_data['start_time'],
job_data['command'],
"None",
job_data['enabled'],
job_data['one_time'],
job_data['cron_string'],
job_data['parent'],
job_data['delay'])
#Checks to make sure some doofus didn't actually make the newly created task a child of itself.
if str(job_data['parent']) == str(sch_id):
management_helper.update_scheduled_task(sch_id, {'parent':None})
#Check to see if it's enabled and is not a chain reaction.
if job_data['enabled'] and job_data['interval_type'] != 'reaction':
if job_data['cron_string'] != "":
try:
self.scheduler.add_job(management_helper.add_command,
CronTrigger.from_crontab(job_data['cron_string'],
timezone=str(self.tz)),
id=str(sch_id),
args=[job_data['server_id'],
self.users_controller.get_id_by_name('system'),
'127.0.0.1',
job_data['command']]
)
except Exception as e:
console.error(f"Failed to schedule task with error: {e}.")
console.warning("Removing failed task from DB.")
logger.error(f"Failed to schedule task with error: {e}.")
logger.warning("Removing failed task from DB.")
#remove items from DB if task fails to add to apscheduler
management_helper.delete_scheduled_task(sch_id)
else:
if job_data['interval_type'] == 'hours':
self.scheduler.add_job(management_helper.add_command,
'cron',
minute = 0,
hour = '*/'+str(job_data['interval']),
id=str(sch_id),
args=[job_data['server_id'],
self.users_controller.get_id_by_name('system'),
'127.0.0.1',
job_data['command']]
)
elif job_data['interval_type'] == 'minutes':
self.scheduler.add_job(management_helper.add_command,
'cron',
minute = '*/'+str(job_data['interval']),
id=str(sch_id),
args=[job_data['server_id'],
self.users_controller.get_id_by_name('system'),
'127.0.0.1',
job_data['command']]
)
elif job_data['interval_type'] == 'days':
curr_time = job_data['start_time'].split(':')
self.scheduler.add_job(management_helper.add_command,
'cron',
day = '*/'+str(job_data['interval']),
hour = curr_time[0],
minute = curr_time[1],
id=str(sch_id),
args=[job_data['server_id'],
self.users_controller.get_id_by_name('system'),
'127.0.0.1',
job_data['command']],
)
logger.info("Added job. Current enabled schedules: ")
jobs = self.scheduler.get_jobs()
for item in jobs:
logger.info(f"JOB: {item}")
def remove_all_server_tasks(self, server_id):
schedules = management_helper.get_schedules_by_server(server_id)
for schedule in schedules:
if schedule.interval != 'reaction':
self.remove_job(schedule.schedule_id)
def remove_job(self, sch_id):
job = management_helper.get_scheduled_task_model(sch_id)
for schedule in management_helper.get_child_schedules(sch_id):
management_helper.update_scheduled_task(schedule.schedule_id, {'parent':None})
management_helper.delete_scheduled_task(sch_id)
if job.enabled and job.interval_type != 'reaction':
self.scheduler.remove_job(str(sch_id))
logger.info(f"Job with ID {sch_id} was deleted.")
else:
logger.info(f"Job with ID {sch_id} was deleted from DB, but was not enabled."
+ "Not going to try removing something that doesn't exist from active schedules.")
def update_job(self, sch_id, job_data):
management_helper.update_scheduled_task(sch_id, job_data)
#Checks to make sure some doofus didn't actually make the newly created task a child of itself.
if str(job_data['parent']) == str(sch_id):
management_helper.update_scheduled_task(sch_id, {'parent':None})
try:
if job_data['interval'] != 'reaction':
self.scheduler.remove_job(str(sch_id))
except:
logger.info("No job found in update job. Assuming it was previously disabled. Starting new job.")
if job_data['enabled']:
if job_data['interval'] != 'reaction':
if job_data['cron_string'] != "":
try:
self.scheduler.add_job(management_helper.add_command,
CronTrigger.from_crontab(job_data['cron_string'],
timezone=str(self.tz)),
id=str(sch_id),
args=[job_data['server_id'],
self.users_controller.get_id_by_name('system'),
'127.0.0.1',
job_data['command']]
)
except Exception as e:
console.error(f"Failed to schedule task with error: {e}.")
console.info("Removing failed task from DB.")
management_helper.delete_scheduled_task(sch_id)
else:
if job_data['interval_type'] == 'hours':
self.scheduler.add_job(management_helper.add_command,
'cron',
minute = 0,
hour = '*/'+str(job_data['interval']),
id=str(sch_id),
args=[job_data['server_id'],
self.users_controller.get_id_by_name('system'),
'127.0.0.1',
job_data['command']]
)
elif job_data['interval_type'] == 'minutes':
self.scheduler.add_job(management_helper.add_command,
'cron',
minute = '*/'+str(job_data['interval']),
id=str(sch_id),
args=[job_data['server_id'],
self.users_controller.get_id_by_name('system'),
'127.0.0.1',
job_data['command']]
)
elif job_data['interval_type'] == 'days':
curr_time = job_data['start_time'].split(':')
self.scheduler.add_job(management_helper.add_command,
'cron',
day = '*/'+str(job_data['interval']),
hour = curr_time[0],
minute = curr_time[1],
id=str(sch_id),
args=[job_data['server_id'],
self.users_controller.get_id_by_name('system'),
'127.0.0.1',
job_data['command']]
)
else:
try:
self.scheduler.get_job(str(sch_id))
self.scheduler.remove_job(str(sch_id))
except:
logger.info(f"APScheduler found no scheduled job on schedule update for schedule with id: {sch_id} Assuming it was already disabled.")
def schedule_watcher(self, event):
if not event.exception:
if str(event.job_id).isnumeric():
task = management_helper.get_scheduled_task_model(int(event.job_id))
management_helper.add_to_audit_log_raw('system', users_helper.get_user_id_by_name('system'), task.server_id,
f"Task with id {task.schedule_id} completed successfully", '127.0.0.1')
#check if the task is a single run.
if task.one_time:
self.remove_job(task.schedule_id)
logger.info("one time task detected. Deleting...")
#check for any child tasks for this. It's kind of backward, but this makes DB management a lot easier. One to one instead of one to many.
for schedule in management_helper.get_child_schedules_by_server(task.schedule_id, task.server_id):
#event job ID's are strings so we need to look at this as the same data type.
if str(schedule.parent) == str(event.job_id):
if schedule.enabled:
delaytime = datetime.datetime.now() + datetime.timedelta(seconds=schedule.delay)
self.scheduler.add_job(management_helper.add_command, 'date', run_date=delaytime, id=str(schedule.schedule_id),
args=[schedule.server_id,
self.users_controller.get_id_by_name('system'),
'127.0.0.1',
schedule.command])
else:
logger.info("Event job ID is not numerical. Assuming it's stats - not stored in DB. Moving on.")
else:
logger.error(f"Task failed with error: {event.exception}")
def start_stats_recording(self):
stats_update_frequency = helper.get_setting('stats_update_frequency')
logger.info("Stats collection frequency set to {stats} seconds".format(stats=stats_update_frequency))
console.info("Stats collection frequency set to {stats} seconds".format(stats=stats_update_frequency))
logger.info(f"Stats collection frequency set to {stats_update_frequency} seconds")
console.info(f"Stats collection frequency set to {stats_update_frequency} seconds")
# one for now,
self.controller.stats.record_stats()
# one for later
schedule.every(stats_update_frequency).seconds.do(self.controller.stats.record_stats).tag('stats-recording')
self.scheduler.add_job(self.controller.stats.record_stats, 'interval', seconds=stats_update_frequency, id="stats")
@staticmethod
def serverjar_cache_refresher():
def serverjar_cache_refresher(self):
logger.info("Refreshing serverjars.com cache on start")
server_jar_obj.refresh_cache()
logger.info("Scheduling Serverjars.com cache refresh service every 12 hours")
schedule.every(12).hours.do(server_jar_obj.refresh_cache).tag('serverjars')
self.scheduler.add_job(server_jar_obj.refresh_cache, 'interval', hours=12, id="serverjars")
@staticmethod
def realtime():
def realtime(self):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
@ -210,9 +446,7 @@ class TasksManager:
'mem_percent': host_stats.get('mem_percent'),
'mem_usage': host_stats.get('mem_usage')
})
time.sleep(4)
def log_watcher(self):
self.controller.servers.check_for_old_logs()
schedule.every(6).hours.do(lambda: self.controller.servers.check_for_old_logs()).tag('log-mgmt')
self.scheduler.add_job(self.controller.servers.check_for_old_logs, 'interval', hours=6, id="log-mgmt")

View File

@ -1,71 +1,75 @@
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
import os
import json
import logging
import os
import typing as t
from app.classes.shared.console import console
from app.classes.shared.helpers import helper
logger = logging.getLogger(__name__)
class Translation():
class Translation:
def __init__(self):
self.translations_path = os.path.join(helper.root_dir, 'app', 'translations')
self.cached_translation = None
self.cached_translation_lang = None
self.lang_file_exists = []
def translate(self, page, word, lang):
translated_word = None
fallback_lang = 'en_EN'
if lang not in self.lang_file_exists and \
helper.check_file_exists(os.path.join(self.translations_path, str(lang) + '.json')):
self.lang_file_exists.append(lang)
def get_language_file(self, language: str):
return os.path.join(self.translations_path, str(language) + '.json')
translated_word = self.translate_inner(page, word, lang) \
if lang in self.lang_file_exists else self.translate_inner(page, word, fallback_lang)
def translate(self, page, word, language):
fallback_language = 'en_EN'
translated_word = self.translate_inner(page, word, language)
if translated_word is None:
translated_word = self.translate_inner(page, word, fallback_language)
if translated_word:
if isinstance(translated_word, dict): return json.dumps(translated_word)
elif iter(translated_word) and not isinstance(translated_word, str): return '\n'.join(translated_word)
if isinstance(translated_word, dict):
# JSON objects
return json.dumps(translated_word)
elif isinstance(translated_word, str):
# Basic strings
return translated_word
elif hasattr(translated_word, '__iter__'):
# Multiline strings
return '\n'.join(translated_word)
return 'Error while getting translation'
def translate_inner(self, page, word, lang):
lang_file = os.path.join(
self.translations_path,
lang + '.json'
)
def translate_inner(self, page, word, language) -> t.Union[t.Any, None]:
language_file = self.get_language_file(language)
try:
if not self.cached_translation:
with open(lang_file, 'r', encoding='utf-8') as f:
with open(language_file, 'r', encoding='utf-8') as f:
data = json.load(f)
self.cached_translation = data
elif self.cached_translation_lang != lang:
with open(lang_file, 'r', encoding='utf-8') as f:
elif self.cached_translation_lang != language:
with open(language_file, 'r', encoding='utf-8') as f:
data = json.load(f)
self.cached_translation = data
self.cached_translation_lang = lang
self.cached_translation_lang = language
else:
data = self.cached_translation
try:
translated_page = data[page]
except KeyError:
logger.error('Translation File Error: page {} does not exist for lang {}'.format(page, lang))
console.error('Translation File Error: page {} does not exist for lang {}'.format(page, lang))
logger.error(f'Translation File Error: page {page} does not exist for lang {language}')
console.error(f'Translation File Error: page {page} does not exist for lang {language}')
return None
try:
translated_word = translated_page[word]
return translated_word
except KeyError:
logger.error('Translation File Error: word {} does not exist on page {} for lang {}'.format(word, page, lang))
console.error('Translation File Error: word {} does not exist on page {} for lang {}'.format(word, page, lang))
logger.error(f'Translation File Error: word {word} does not exist on page {page} for lang {language}')
console.error(f'Translation File Error: word {word} does not exist on page {page} for lang {language}')
return None
except Exception as e:
logger.critical('Translation File Error: Unable to read {} due to {}'.format(lang_file, e))
console.critical('Translation File Error: Unable to read {} due to {}'.format(lang_file, e))
logger.critical(f'Translation File Error: Unable to read {language_file} due to {e}')
console.critical(f'Translation File Error: Unable to read {language_file} due to {e}')
return None
translation = Translation()

View File

@ -1,27 +1,27 @@
import json
import logging
import tempfile
import threading
from typing import Container
import zipfile
import tornado.web
import tornado.escape
import bleach
import os
import shutil
import html
import re
import logging
import time
from app.classes.models.server_permissions import Enum_Permissions_Server
from app.classes.shared.console import console
from app.classes.shared.main_models import Users, installer
from app.classes.web.base_handler import BaseHandler
from app.classes.shared.helpers import helper
from app.classes.shared.translation import translation
from app.classes.shared.server import ServerOutBuf
from app.classes.web.websocket_helper import websocket_helper
from app.classes.web.base_handler import BaseHandler
try:
import bleach
import tornado.web
import tornado.escape
except ModuleNotFoundError as ex:
helper.auto_installer_fix(ex)
logger = logging.getLogger(__name__)
class AjaxHandler(BaseHandler):
def render_page(self, template, page_data):
@ -33,13 +33,13 @@ class AjaxHandler(BaseHandler):
@tornado.web.authenticated
def get(self, page):
user_data = json.loads(self.get_secure_cookie("user_data"))
_, _, exec_user = self.current_user
error = bleach.clean(self.get_argument('error', "WTF Error!"))
template = "panel/denied.html"
page_data = {
'user_data': user_data,
'user_data': exec_user,
'error': error
}
@ -65,11 +65,11 @@ class AjaxHandler(BaseHandler):
return
if not server_data['log_path']:
logger.warning("Log path not found in server_log ajax call ({})".format(server_id))
logger.warning(f"Log path not found in server_log ajax call ({server_id})")
if full_log:
log_lines = helper.get_setting('max_log_lines')
data = helper.tail_file(server_data['log_path'], log_lines)
data = helper.tail_file(helper.get_os_understandable_path(server_data['log_path']), log_lines)
else:
data = ServerOutBuf.lines.get(server_id, [])
@ -79,70 +79,206 @@ class AjaxHandler(BaseHandler):
d = re.sub('(\033\\[(0;)?[0-9]*[A-z]?(;[0-9])?m?)|(> )', '', d)
d = re.sub('[A-z]{2}\b\b', '', d)
line = helper.log_colors(html.escape(d))
self.write('{}<br />'.format(line))
self.write(f'{line}<br />')
# self.write(d.encode("utf-8"))
except Exception as e:
logger.warning("Skipping Log Line due to error: {}".format(e))
pass
logger.warning(f"Skipping Log Line due to error: {e}")
elif page == "announcements":
data = helper.get_announcements()
page_data['notify_data'] = data
self.render_page('ajax/notify.html', page_data)
elif page == "get_file":
file_path = self.get_argument('file_path', None)
server_id = self.get_argument('id', None)
if not self.check_server_id(server_id, 'get_file'): return
else: server_id = bleach.clean(server_id)
elif page == "get_zip_tree":
path = self.get_argument('path', None)
if not helper.in_path(self.controller.servers.get_server_data_by_id(server_id)['path'], file_path)\
or not helper.check_file_exists(os.path.abspath(file_path)):
logger.warning("Invalid path in get_file ajax call ({})".format(file_path))
console.warning("Invalid path in get_file ajax call ({})".format(file_path))
return
error = None
try:
with open(file_path) as file:
file_contents = file.read()
except UnicodeDecodeError:
file_contents = ''
error = 'UnicodeDecodeError'
self.write({
'content': file_contents,
'error': error
})
self.write(helper.get_os_understandable_path(path) + '\n' +
helper.generate_zip_tree(path))
self.finish()
elif page == "get_tree":
elif page == "get_zip_dir":
path = self.get_argument('path', None)
self.write(helper.get_os_understandable_path(path) + '\n' +
helper.generate_zip_dir(path))
self.finish()
elif page == "get_backup_tree":
server_id = self.get_argument('id', None)
folder = self.get_argument('path', None)
if not self.check_server_id(server_id, 'get_tree'): return
else: server_id = bleach.clean(server_id)
output = ""
self.write(self.controller.servers.get_server_data_by_id(server_id)['path'] + '\n' +
helper.generate_tree(self.controller.servers.get_server_data_by_id(server_id)['path']))
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
for item in file_list:
if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item)
else:
unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted(unsorted_files, key=str.casefold)
output += \
f"""<ul class="tree-nested d-block" id="{folder}ul">"""\
for raw_filename in file_list:
filename = html.escape(raw_filename)
rel = os.path.join(folder, raw_filename)
dpath = os.path.join(folder, filename)
if str(dpath) in self.controller.management.get_excluded_backup_dirs(server_id):
if os.path.isdir(rel):
output += \
f"""<li class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" class="checkBoxClass" name="root_path" value="{dpath}" checked>
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
<i class="far fa-folder"></i>
<i class="far fa-folder-open"></i>
<strong>{filename}</strong>
</span>
</input></div><li>
\n"""\
else:
output += f"""<li
class="tree-nested d-block tree-ctx-item tree-file"
data-path="{dpath}"
data-name="{filename}"
onclick=""><input type='checkbox' class="checkBoxClass" name='root_path' value="{dpath}" checked><span style="margin-right: 6px;">
<i class="far fa-file"></i></span></input>{filename}</li>"""
else:
if os.path.isdir(rel):
output += \
f"""<li class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" class="checkBoxClass" name="root_path" value="{dpath}">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
<i class="far fa-folder"></i>
<i class="far fa-folder-open"></i>
<strong>{filename}</strong>
</span>
</input></div><li>
\n"""\
else:
output += f"""<li
class="tree-nested d-block tree-ctx-item tree-file"
data-path="{dpath}"
data-name="{filename}"
onclick=""><input type='checkbox' class="checkBoxClass" name='root_path' value="{dpath}">
<span style="margin-right: 6px;"><i class="far fa-file"></i></span></input>{filename}</li>"""
self.write(helper.get_os_understandable_path(folder) + '\n' +
output)
self.finish()
elif page == "get_backup_dir":
server_id = self.get_argument('id', None)
folder = self.get_argument('path', None)
output = ""
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
for item in file_list:
if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item)
else:
unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted(unsorted_files, key=str.casefold)
output += \
f"""<ul class="tree-nested d-block" id="{folder}ul">"""\
for raw_filename in file_list:
filename = html.escape(raw_filename)
rel = os.path.join(folder, raw_filename)
dpath = os.path.join(folder, filename)
if str(dpath) in self.controller.management.get_excluded_backup_dirs(server_id):
if os.path.isdir(rel):
output += \
f"""<li class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" name="root_path" value="{dpath}">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
<i class="far fa-folder"></i>
<i class="far fa-folder-open"></i>
<strong>{filename}</strong>
</span>
</input></div><li>"""\
else:
output += f"""<li
class="tree-item tree-nested d-block tree-ctx-item tree-file"
data-path="{dpath}"
data-name="{filename}"
onclick=""><input type='checkbox' name='root_path' value='{dpath}'><span style="margin-right: 6px;">
<i class="far fa-file"></i></span></input>{filename}</li>"""
else:
if os.path.isdir(rel):
output += \
f"""<li class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" name="root_path" value="{dpath}">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
<i class="far fa-folder"></i>
<i class="far fa-folder-open"></i>
<strong>{filename}</strong>
</span>
</input></div><li>"""\
else:
output += f"""<li
class="tree-item tree-nested d-block tree-ctx-item tree-file"
data-path="{dpath}"
data-name="{filename}"
onclick=""><input type='checkbox' name='root_path' value='{dpath}'>
<span style="margin-right: 6px;"><i class="far fa-file"></i></span></input>{filename}</li>"""
self.write(helper.get_os_understandable_path(folder) + '\n' +
output)
self.finish()
elif page == "get_dir":
server_id = self.get_argument('id', None)
path = self.get_argument('path', None)
if not self.check_server_id(server_id, 'get_tree'):
return
else:
server_id = bleach.clean(server_id)
if helper.validate_traversal(self.controller.servers.get_server_data_by_id(server_id)['path'], path):
self.write(helper.get_os_understandable_path(path) + '\n' +
helper.generate_dir(path))
self.finish()
@tornado.web.authenticated
def post(self, page):
user_data = json.loads(self.get_secure_cookie("user_data"))
error = bleach.clean(self.get_argument('error', "WTF Error!"))
api_key, _, exec_user = self.current_user
superuser = exec_user['superuser']
if api_key is not None:
superuser = superuser and api_key.superuser
page_data = {
'user_data': user_data,
'error': error
server_id = self.get_argument('id', None)
permissions = {
'Commands': Enum_Permissions_Server.Commands,
'Terminal': Enum_Permissions_Server.Terminal,
'Logs': Enum_Permissions_Server.Logs,
'Schedule': Enum_Permissions_Server.Schedule,
'Backup': Enum_Permissions_Server.Backup,
'Files': Enum_Permissions_Server.Files,
'Config': Enum_Permissions_Server.Config,
'Players': Enum_Permissions_Server.Players,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(exec_user['user_id'], server_id)
if page == "send_command":
command = self.get_body_argument('command', default=None, strip=True)
server_id = self.get_argument('id')
server_id = self.get_argument('id', None)
if server_id is None:
logger.warning("Server ID not found in send_command ajax call")
@ -150,182 +286,213 @@ class AjaxHandler(BaseHandler):
srv_obj = self.controller.get_server_obj(server_id)
if command == srv_obj.settings['stop_command']:
logger.info("Stop command detected as terminal input - intercepting." +
f"Starting Crafty's stop process for server with id: {server_id}")
self.controller.management.send_command(exec_user['user_id'], server_id, self.get_remote_ip(), 'stop_server')
command = None
elif command == 'restart':
logger.info("Restart command detected as terminal input - intercepting." +
f"Starting Crafty's stop process for server with id: {server_id}")
self.controller.management.send_command(exec_user['user_id'], server_id, self.get_remote_ip(), 'restart_server')
command = None
if command:
if srv_obj.check_running():
srv_obj.send_command(command)
self.controller.management.add_to_audit_log(user_data['user_id'], "Sent command to {} terminal: {}".format(self.controller.servers.get_server_friendly_name(server_id), command), server_id, self.get_remote_ip())
self.controller.management.add_to_audit_log(exec_user['user_id'],
f"Sent command to {self.controller.servers.get_server_friendly_name(server_id)} terminal: {command}",
server_id,
self.get_remote_ip())
elif page == "create_file":
file_parent = self.get_body_argument('file_parent', default=None, strip=True)
file_name = self.get_body_argument('file_name', default=None, strip=True)
file_path = os.path.join(file_parent, file_name)
server_id = self.get_argument('id', None)
if not self.check_server_id(server_id, 'create_file'): return
else: server_id = bleach.clean(server_id)
if not helper.in_path(self.controller.servers.get_server_data_by_id(server_id)['path'], file_path) \
or helper.check_file_exists(os.path.abspath(file_path)):
logger.warning("Invalid path in create_file ajax call ({})".format(file_path))
console.warning("Invalid path in create_file ajax call ({})".format(file_path))
elif page == "send_order":
self.controller.users.update_server_order(exec_user['user_id'], bleach.clean(self.get_argument('order')))
return
# Create the file by opening it
with open(file_path, 'w') as file_object:
file_object.close()
elif page == "create_dir":
dir_parent = self.get_body_argument('dir_parent', default=None, strip=True)
dir_name = self.get_body_argument('dir_name', default=None, strip=True)
dir_path = os.path.join(dir_parent, dir_name)
server_id = self.get_argument('id', None)
if not self.check_server_id(server_id, 'create_dir'): return
else: server_id = bleach.clean(server_id)
if not helper.in_path(self.controller.servers.get_server_data_by_id(server_id)['path'], dir_path) \
or helper.check_path_exists(os.path.abspath(dir_path)):
logger.warning("Invalid path in create_dir ajax call ({})".format(dir_path))
console.warning("Invalid path in create_dir ajax call ({})".format(dir_path))
return
# Create the directory
os.mkdir(dir_path)
elif page == "unzip_file":
server_id = self.get_argument('id', None)
path = self.get_argument('path', None)
helper.unzipFile(path)
self.redirect("/panel/server_detail?id={}&subpage=files".format(server_id))
elif page == "clear_comms":
if exec_user['superuser']:
self.controller.clear_unexecuted_commands()
return
elif page == "kill":
if not permissions['Commands'] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Commands")
return
server_id = self.get_argument('id', None)
svr = self.controller.get_server_obj(server_id)
try:
svr.kill()
except Exception as e:
logger.error("Could not find PID for requested termsig. Full error: {}".format(e))
logger.error(f"Could not find PID for requested termsig. Full error: {e}")
return
elif page == "eula":
server_id = self.get_argument('id', None)
svr = self.controller.get_server_obj(server_id)
svr.agree_eula(exec_user['user_id'])
elif page == "restore_backup":
if not permissions['Backup'] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Backups")
return
server_id = bleach.clean(self.get_argument('id', None))
zip_name = bleach.clean(self.get_argument('zip_file', None))
svr_obj = self.controller.servers.get_server_obj(server_id)
server_data = self.controller.servers.get_server_data_by_id(server_id)
if server_data['type'] == 'minecraft-java':
backup_path = svr_obj.backup_path
if helper.validate_traversal(backup_path, zip_name):
tempDir = helper.unzip_backup_archive(backup_path, zip_name)
new_server = self.controller.import_zip_server(svr_obj.server_name,
tempDir,
server_data['executable'],
'1', '2',
server_data['server_port'])
new_server_id = new_server
new_server = self.controller.get_server_data(new_server)
self.controller.rename_backup_dir(server_id, new_server_id, new_server['server_uuid'])
self.controller.remove_server(server_id, True)
self.redirect('/panel/dashboard')
else:
backup_path = svr_obj.backup_path
if helper.validate_traversal(backup_path, zip_name):
tempDir = helper.unzip_backup_archive(backup_path, zip_name)
new_server = self.controller.import_bedrock_zip_server(svr_obj.server_name,
tempDir,
server_data['executable'],
server_data['server_port'])
new_server_id = new_server
new_server = self.controller.get_server_data(new_server)
self.controller.rename_backup_dir(server_id, new_server_id, new_server['server_uuid'])
self.controller.remove_server(server_id, True)
self.redirect('/panel/dashboard')
elif page == "unzip_server":
path = self.get_argument('path', None)
if helper.check_file_exists(path):
helper.unzipServer(path, exec_user['user_id'])
else:
user_id = exec_user['user_id']
if user_id:
time.sleep(5)
user_lang = self.controller.users.get_user_lang_by_id(user_id)
websocket_helper.broadcast_user(user_id, 'send_start_error',{
'error': translation.translate('error', 'no-file', user_lang)
})
return
elif page == "backup_select":
path = self.get_argument('path', None)
helper.backup_select(path, exec_user['user_id'])
return
@tornado.web.authenticated
def delete(self, page):
if page == "del_file":
file_path = self.get_body_argument('file_path', default=None, strip=True)
api_key, _, exec_user = self.current_user
superuser = exec_user['superuser']
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument('id', None)
if os.name == "nt":
file_path = file_path.replace('/', "\\")
console.warning("delete {} for server {}".format(file_path, server_id))
if not self.check_server_id(server_id, 'del_file'):
permissions = {
'Commands': Enum_Permissions_Server.Commands,
'Terminal': Enum_Permissions_Server.Terminal,
'Logs': Enum_Permissions_Server.Logs,
'Schedule': Enum_Permissions_Server.Schedule,
'Backup': Enum_Permissions_Server.Backup,
'Files': Enum_Permissions_Server.Files,
'Config': Enum_Permissions_Server.Config,
'Players': Enum_Permissions_Server.Players,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(exec_user['user_id'], server_id)
if page == "del_task":
if not permissions['Schedule'] in user_perms:
self.redirect("/panel/error?error=Unauthorized access to Tasks")
else:
sch_id = self.get_argument('schedule_id', '-404')
self.tasks_manager.remove_job(sch_id)
if page == "del_backup":
if not permissions['Backup'] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Backups")
return
file_path = helper.get_os_understandable_path(self.get_body_argument('file_path', default=None, strip=True))
server_id = self.get_argument('id', None)
console.warning(f"Delete {file_path} for server {server_id}")
if not self.check_server_id(server_id, 'del_backup'):
return
else: server_id = bleach.clean(server_id)
server_info = self.controller.servers.get_server_data_by_id(server_id)
if not (helper.in_path(server_info['path'], file_path) \
or helper.in_path(server_info['backup_path'], file_path)) \
if not (helper.in_path(helper.get_os_understandable_path(server_info['path']), file_path) \
or helper.in_path(helper.get_os_understandable_path(server_info['backup_path']), file_path)) \
or not helper.check_file_exists(os.path.abspath(file_path)):
logger.warning("Invalid path in del_file ajax call ({})".format(file_path))
console.warning("Invalid path in del_file ajax call ({})".format(file_path))
logger.warning(f"Invalid path in del_backup ajax call ({file_path})")
console.warning(f"Invalid path in del_backup ajax call ({file_path})")
return
# Delete the file
if helper.validate_traversal(helper.get_os_understandable_path(server_info['backup_path']), file_path):
os.remove(file_path)
elif page == "del_dir":
dir_path = self.get_body_argument('dir_path', default=None, strip=True)
server_id = self.get_argument('id', None)
console.warning("delete {} for server {}".format(dir_path, server_id))
if not self.check_server_id(server_id, 'del_dir'): return
else: server_id = bleach.clean(server_id)
server_info = self.controller.servers.get_server_data_by_id(server_id)
if not helper.in_path(server_info['path'], dir_path) \
or not helper.check_path_exists(os.path.abspath(dir_path)):
logger.warning("Invalid path in del_file ajax call ({})".format(dir_path))
console.warning("Invalid path in del_file ajax call ({})".format(dir_path))
return
# Delete the directory
# os.rmdir(dir_path) # Would only remove empty directories
shutil.rmtree(dir_path) # Removes also when there are contents
elif page == "delete_server":
if not permissions['Config'] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Config")
return
server_id = self.get_argument('id', None)
logger.info(
"Removing server from panel for server: {}".format(self.controller.servers.get_server_friendly_name(server_id)))
logger.info(f"Removing server from panel for server: {self.controller.servers.get_server_friendly_name(server_id)}")
server_data = self.controller.get_server_data(server_id)
server_name = server_data['server_name']
self.controller.management.add_to_audit_log(exec_user['user_id'],
f"Deleted server {server_id} named {server_name}",
server_id,
self.get_remote_ip())
self.tasks_manager.remove_all_server_tasks(server_id)
self.controller.remove_server(server_id, False)
elif page == "delete_server_files":
if not permissions['Config'] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Config")
return
server_id = self.get_argument('id', None)
logger.info(
"Removing server and all associated files for server: {}".format(self.controller.servers.get_server_friendly_name(server_id)))
logger.info(f"Removing server and all associated files for server: {self.controller.servers.get_server_friendly_name(server_id)}")
server_data = self.controller.get_server_data(server_id)
server_name = server_data['server_name']
self.controller.management.add_to_audit_log(exec_user['user_id'],
f"Deleted server {server_id} named {server_name}",
server_id,
self.get_remote_ip())
self.tasks_manager.remove_all_server_tasks(server_id)
self.controller.remove_server(server_id, True)
@tornado.web.authenticated
def put(self, page):
if page == "save_file":
file_contents = self.get_body_argument('file_contents', default=None, strip=True)
file_path = self.get_body_argument('file_path', default=None, strip=True)
server_id = self.get_argument('id', None)
if not self.check_server_id(server_id, 'save_file'): return
else: server_id = bleach.clean(server_id)
if not helper.in_path(self.controller.servers.get_server_data_by_id(server_id)['path'], file_path)\
or not helper.check_file_exists(os.path.abspath(file_path)):
logger.warning("Invalid path in save_file ajax call ({})".format(file_path))
console.warning("Invalid path in save_file ajax call ({})".format(file_path))
return
# Open the file in write mode and store the content in file_object
with open(file_path, 'w') as file_object:
file_object.write(file_contents)
elif page == "rename_item":
item_path = self.get_body_argument('item_path', default=None, strip=True)
new_item_name = self.get_body_argument('new_item_name', default=None, strip=True)
server_id = self.get_argument('id', None)
if not self.check_server_id(server_id, 'rename_item'): return
else: server_id = bleach.clean(server_id)
if item_path is None or new_item_name is None:
logger.warning("Invalid path(s) in rename_item ajax call")
console.warning("Invalid path(s) in rename_item ajax call")
return
if not helper.in_path(self.controller.servers.get_server_data_by_id(server_id)['path'], item_path) \
or not helper.check_path_exists(os.path.abspath(item_path)):
logger.warning("Invalid old name path in rename_item ajax call ({})".format(server_id))
console.warning("Invalid old name path in rename_item ajax call ({})".format(server_id))
return
new_item_path = os.path.join(os.path.split(item_path)[0], new_item_name)
if not helper.in_path(self.controller.servers.get_server_data_by_id(server_id)['path'], new_item_path) \
or helper.check_path_exists(os.path.abspath(new_item_path)):
logger.warning("Invalid new name path in rename_item ajax call ({})".format(server_id))
console.warning("Invalid new name path in rename_item ajax call ({})".format(server_id))
return
# RENAME
os.rename(item_path, new_item_path)
def check_server_id(self, server_id, page_name):
if server_id is None:
logger.warning("Server ID not defined in {} ajax call ({})".format(page_name, server_id))
console.warning("Server ID not defined in {} ajax call ({})".format(page_name, server_id))
logger.warning(f"Server ID not defined in {page_name} ajax call ({server_id})")
console.warning(f"Server ID not defined in {page_name} ajax call ({server_id})")
return
else:
server_id = bleach.clean(server_id)
# does this server id exist?
if not self.controller.servers.server_id_exists(server_id):
logger.warning("Server ID not found in {} ajax call ({})".format(page_name, server_id))
console.warning("Server ID not found in {} ajax call ({})".format(page_name, server_id))
logger.warning(f"Server ID not found in {page_name} ajax call ({server_id})")
console.warning(f"Server ID not found in {page_name} ajax call ({server_id})")
return
return True

View File

@ -1,14 +1,10 @@
import os
import secrets
import threading
import tornado.web
import tornado.escape
import logging
import re
from app.classes.web.base_handler import BaseHandler
log = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
bearer_pattern = re.compile(r'^Bearer', flags=re.IGNORECASE)
class ApiHandler(BaseHandler):
@ -18,8 +14,9 @@ class ApiHandler(BaseHandler):
self.write(data)
def access_denied(self, user, reason=''):
if reason: reason = ' because ' + reason
log.info("User %s from IP %s was denied access to the API route " + self.request.path + reason, user, self.get_remote_ip())
if reason:
reason = ' because ' + reason
logger.info("User %s from IP %s was denied access to the API route " + self.request.path + reason, user, self.get_remote_ip())
self.finish(self.return_response(403, {
'error':'ACCESS_DENIED',
'info':'You were denied access to the requested resource'
@ -27,31 +24,42 @@ class ApiHandler(BaseHandler):
def authenticate_user(self) -> bool:
try:
log.debug("Searching for specified token")
# TODO: YEET THIS
user_data = self.controller.users.get_user_by_api_token(self.get_argument('token'))
log.debug("Checking results")
logger.debug("Searching for specified token")
api_token = self.get_argument('token', '')
if api_token is None and self.request.headers.get('Authorization'):
api_token = bearer_pattern.sub('', self.request.headers.get('Authorization'))
elif api_token is None:
api_token = self.get_cookie('token')
user_data = self.controller.users.get_user_by_api_token(api_token)
logger.debug("Checking results")
if user_data:
# Login successful! Check perms
log.info("User {} has authenticated to API".format(user_data['username']))
logger.info(f"User {user_data['username']} has authenticated to API")
# TODO: Role check
return True # This is to set the "authenticated"
else:
logging.debug("Auth unsuccessful")
self.access_denied("unknown", "the user provided an invalid token")
return
return False
except Exception as e:
log.warning("An error occured while authenticating an API user: %s", e)
self.access_denied("unknown"), "an error occured while authenticating the user"
return
logger.warning("An error occured while authenticating an API user: %s", e)
self.finish(self.return_response(403, {
'error':'ACCESS_DENIED',
'info':'An error occured while authenticating the user'
}))
return False
class ServersStats(ApiHandler):
def get(self):
"""Get details about all servers"""
authenticated = self.authenticate_user()
if not authenticated: return
if not authenticated:
return
# Get server stats
# TODO Check perms
self.finish(self.write({"servers": self.controller.stats.get_servers_stats()}))
@ -61,7 +69,9 @@ class NodeStats(ApiHandler):
def get(self):
"""Get stats for particular node"""
authenticated = self.authenticate_user()
if not authenticated: return
if not authenticated:
return
# Get node stats
node_stats = self.controller.stats.get_node_stats()
node_stats.pop("servers")

View File

@ -1,22 +1,30 @@
import logging
import tornado.web
import bleach
from typing import (
Union,
List,
Optional
Optional, Tuple, Dict, Any
)
from app.classes.models.users import ApiKeys
from app.classes.shared.authentication import authentication
from app.classes.shared.main_controller import Controller
from app.classes.shared.helpers import helper
try:
import tornado.web
import bleach
except ModuleNotFoundError as e:
helper.auto_installer_fix(e)
logger = logging.getLogger(__name__)
class BaseHandler(tornado.web.RequestHandler):
nobleach = {bool, type(None)}
redactables = ("pass", "api")
# noinspection PyAttributeOutsideInit
def initialize(self, controller: Controller = None, tasks_manager=None, translator=None):
self.controller = controller
self.tasks_manager = tasks_manager
@ -28,16 +36,17 @@ class BaseHandler(tornado.web.RequestHandler):
self.request.remote_ip
return remote_ip
def get_current_user(self):
return self.get_secure_cookie("user", max_age_days=1)
current_user: Optional[Tuple[Optional[ApiKeys], Dict[str, Any], Dict[str, Any]]]
def get_current_user(self) -> Optional[Tuple[Optional[ApiKeys], Dict[str, Any], Dict[str, Any]]]:
return authentication.check(self.get_cookie("token"))
def autobleach(self, name, text):
for r in self.redactables:
if r in name:
logger.debug("Auto-bleaching {}: {}".format(name, "[**REDACTED**]"))
logger.debug(f"Auto-bleaching {name}: [**REDACTED**]")
break
else:
logger.debug("Auto-bleaching {}: {}".format(name, text))
logger.debug(f"Auto-bleaching {name}: {text}")
if type(text) in self.nobleach:
logger.debug("Auto-bleaching - bypass type")
return text
@ -54,7 +63,8 @@ class BaseHandler(tornado.web.RequestHandler):
return self.autobleach(name, arg)
def get_arguments(self, name: str, strip: bool = True) -> List[str]:
assert isinstance(strip, bool)
if not isinstance(strip, bool):
raise AssertionError
args = self._get_arguments(name, self.request.arguments, strip)
args_ret = []
for arg in args:

View File

@ -4,10 +4,10 @@ from app.classes.web.base_handler import BaseHandler
logger = logging.getLogger(__name__)
class DefaultHandler(BaseHandler):
# Override prepare() instead of get() to cover all possible HTTP methods.
# pylint: disable=arguments-differ
def prepare(self, page=None):
if page is not None:
self.set_status(404)
@ -20,4 +20,3 @@ class DefaultHandler(BaseHandler):
"/public/login",
#translate=self.translator.translate,
)

View File

@ -0,0 +1,414 @@
import os
import logging
from app.classes.models.server_permissions import Enum_Permissions_Server
from app.classes.shared.console import console
from app.classes.shared.helpers import helper
from app.classes.shared.file_helpers import file_helper
from app.classes.web.base_handler import BaseHandler
try:
import bleach
import tornado.web
import tornado.escape
except ModuleNotFoundError as e:
helper.auto_installer_fix(e)
logger = logging.getLogger(__name__)
class FileHandler(BaseHandler):
def render_page(self, template, page_data):
self.render(
template,
data=page_data,
translate=self.translator.translate,
)
@tornado.web.authenticated
def get(self, page):
api_key, _, exec_user = self.current_user
superuser = exec_user['superuser']
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument('id', None)
permissions = {
'Commands': Enum_Permissions_Server.Commands,
'Terminal': Enum_Permissions_Server.Terminal,
'Logs': Enum_Permissions_Server.Logs,
'Schedule': Enum_Permissions_Server.Schedule,
'Backup': Enum_Permissions_Server.Backup,
'Files': Enum_Permissions_Server.Files,
'Config': Enum_Permissions_Server.Config,
'Players': Enum_Permissions_Server.Players,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(exec_user['user_id'], server_id)
if page == "get_file":
if not permissions['Files'] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
file_path = helper.get_os_understandable_path(self.get_argument('file_path', None))
if not self.check_server_id(server_id, 'get_file'):
return
else:
server_id = bleach.clean(server_id)
if not helper.in_path(helper.get_os_understandable_path(self.controller.servers.get_server_data_by_id(server_id)['path']), file_path)\
or not helper.check_file_exists(os.path.abspath(file_path)):
logger.warning(f"Invalid path in get_file file file ajax call ({file_path})")
console.warning(f"Invalid path in get_file file file ajax call ({file_path})")
return
error = None
try:
with open(file_path, encoding='utf-8') as file:
file_contents = file.read()
except UnicodeDecodeError:
file_contents = ''
error = 'UnicodeDecodeError'
self.write({
'content': file_contents,
'error': error
})
self.finish()
elif page == "get_tree":
if not permissions['Files'] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
path = self.get_argument('path', None)
if not self.check_server_id(server_id, 'get_tree'):
return
else:
server_id = bleach.clean(server_id)
if helper.validate_traversal(self.controller.servers.get_server_data_by_id(server_id)['path'], path):
self.write(helper.get_os_understandable_path(path) + '\n' +
helper.generate_tree(path))
self.finish()
elif page == "get_dir":
if not permissions['Files'] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
path = self.get_argument('path', None)
if not self.check_server_id(server_id, 'get_tree'):
return
else:
server_id = bleach.clean(server_id)
if helper.validate_traversal(self.controller.servers.get_server_data_by_id(server_id)['path'], path):
self.write(helper.get_os_understandable_path(path) + '\n' +
helper.generate_dir(path))
self.finish()
@tornado.web.authenticated
def post(self, page):
api_key, _, exec_user = self.current_user
superuser = exec_user['superuser']
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument('id', None)
permissions = {
'Commands': Enum_Permissions_Server.Commands,
'Terminal': Enum_Permissions_Server.Terminal,
'Logs': Enum_Permissions_Server.Logs,
'Schedule': Enum_Permissions_Server.Schedule,
'Backup': Enum_Permissions_Server.Backup,
'Files': Enum_Permissions_Server.Files,
'Config': Enum_Permissions_Server.Config,
'Players': Enum_Permissions_Server.Players,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(exec_user['user_id'], server_id)
if page == "create_file":
if not permissions['Files'] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
file_parent = helper.get_os_understandable_path(self.get_body_argument('file_parent', default=None, strip=True))
file_name = self.get_body_argument('file_name', default=None, strip=True)
file_path = os.path.join(file_parent, file_name)
if not self.check_server_id(server_id, 'create_file'):
return
else:
server_id = bleach.clean(server_id)
if not helper.in_path(helper.get_os_understandable_path(self.controller.servers.get_server_data_by_id(server_id)['path']), file_path) \
or helper.check_file_exists(os.path.abspath(file_path)):
logger.warning(f"Invalid path in create_file file ajax call ({file_path})")
console.warning(f"Invalid path in create_file file ajax call ({file_path})")
return
# Create the file by opening it
with open(file_path, 'w', encoding='utf-8') as file_object:
file_object.close()
elif page == "create_dir":
if not permissions['Files'] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
dir_parent = helper.get_os_understandable_path(self.get_body_argument('dir_parent', default=None, strip=True))
dir_name = self.get_body_argument('dir_name', default=None, strip=True)
dir_path = os.path.join(dir_parent, dir_name)
if not self.check_server_id(server_id, 'create_dir'):
return
else:
server_id = bleach.clean(server_id)
if not helper.in_path(helper.get_os_understandable_path(self.controller.servers.get_server_data_by_id(server_id)['path']), dir_path) \
or helper.check_path_exists(os.path.abspath(dir_path)):
logger.warning(f"Invalid path in create_dir file ajax call ({dir_path})")
console.warning(f"Invalid path in create_dir file ajax call ({dir_path})")
return
# Create the directory
os.mkdir(dir_path)
elif page == "unzip_file":
if not permissions['Files'] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
path = helper.get_os_understandable_path(self.get_argument('path', None))
helper.unzipFile(path)
self.redirect(f"/panel/server_detail?id={server_id}&subpage=files")
return
@tornado.web.authenticated
def delete(self, page):
api_key, _, exec_user = self.current_user
superuser = exec_user['superuser']
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument('id', None)
permissions = {
'Commands': Enum_Permissions_Server.Commands,
'Terminal': Enum_Permissions_Server.Terminal,
'Logs': Enum_Permissions_Server.Logs,
'Schedule': Enum_Permissions_Server.Schedule,
'Backup': Enum_Permissions_Server.Backup,
'Files': Enum_Permissions_Server.Files,
'Config': Enum_Permissions_Server.Config,
'Players': Enum_Permissions_Server.Players,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(exec_user['user_id'], server_id)
if page == "del_file":
if not permissions['Files'] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
file_path = helper.get_os_understandable_path(self.get_body_argument('file_path', default=None, strip=True))
console.warning(f"Delete {file_path} for server {server_id}")
if not self.check_server_id(server_id, 'del_file'):
return
else: server_id = bleach.clean(server_id)
server_info = self.controller.servers.get_server_data_by_id(server_id)
if not (helper.in_path(helper.get_os_understandable_path(server_info['path']), file_path) \
or helper.in_path(helper.get_os_understandable_path(server_info['backup_path']), file_path)) \
or not helper.check_file_exists(os.path.abspath(file_path)):
logger.warning(f"Invalid path in del_file file ajax call ({file_path})")
console.warning(f"Invalid path in del_file file ajax call ({file_path})")
return
# Delete the file
file_helper.del_file(file_path)
elif page == "del_dir":
if not permissions['Files'] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
dir_path = helper.get_os_understandable_path(self.get_body_argument('dir_path', default=None, strip=True))
console.warning(f"Delete {dir_path} for server {server_id}")
if not self.check_server_id(server_id, 'del_dir'):
return
else:
server_id = bleach.clean(server_id)
server_info = self.controller.servers.get_server_data_by_id(server_id)
if not helper.in_path(helper.get_os_understandable_path(server_info['path']), dir_path) \
or not helper.check_path_exists(os.path.abspath(dir_path)):
logger.warning(f"Invalid path in del_file file ajax call ({dir_path})")
console.warning(f"Invalid path in del_file file ajax call ({dir_path})")
return
# Delete the directory
# os.rmdir(dir_path) # Would only remove empty directories
if helper.validate_traversal(helper.get_os_understandable_path(server_info['path']), dir_path):
# Removes also when there are contents
file_helper.del_dirs(dir_path)
@tornado.web.authenticated
def put(self, page):
api_key, _, exec_user = self.current_user
superuser = exec_user['superuser']
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument('id', None)
permissions = {
'Commands': Enum_Permissions_Server.Commands,
'Terminal': Enum_Permissions_Server.Terminal,
'Logs': Enum_Permissions_Server.Logs,
'Schedule': Enum_Permissions_Server.Schedule,
'Backup': Enum_Permissions_Server.Backup,
'Files': Enum_Permissions_Server.Files,
'Config': Enum_Permissions_Server.Config,
'Players': Enum_Permissions_Server.Players,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(exec_user['user_id'], server_id)
if page == "save_file":
if not permissions['Files'] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
file_contents = self.get_body_argument('file_contents', default=None, strip=True)
file_path = helper.get_os_understandable_path(self.get_body_argument('file_path', default=None, strip=True))
if not self.check_server_id(server_id, 'save_file'):
return
else:
server_id = bleach.clean(server_id)
if not helper.in_path(helper.get_os_understandable_path(self.controller.servers.get_server_data_by_id(server_id)['path']), file_path)\
or not helper.check_file_exists(os.path.abspath(file_path)):
logger.warning(f"Invalid path in save_file file ajax call ({file_path})")
console.warning(f"Invalid path in save_file file ajax call ({file_path})")
return
# Open the file in write mode and store the content in file_object
with open(file_path, 'w', encoding='utf-8') as file_object:
file_object.write(file_contents)
elif page == "rename_file":
if not permissions['Files'] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
item_path = helper.get_os_understandable_path(self.get_body_argument('item_path', default=None, strip=True))
new_item_name = self.get_body_argument('new_item_name', default=None, strip=True)
if not self.check_server_id(server_id, 'rename_file'):
return
else:
server_id = bleach.clean(server_id)
if item_path is None or new_item_name is None:
logger.warning("Invalid path(s) in rename_file file ajax call")
console.warning("Invalid path(s) in rename_file file ajax call")
return
if not helper.in_path(helper.get_os_understandable_path(self.controller.servers.get_server_data_by_id(server_id)['path']), item_path) \
or not helper.check_path_exists(os.path.abspath(item_path)):
logger.warning(f"Invalid old name path in rename_file file ajax call ({server_id})")
console.warning(f"Invalid old name path in rename_file file ajax call ({server_id})")
return
new_item_path = os.path.join(os.path.split(item_path)[0], new_item_name)
if not helper.in_path(helper.get_os_understandable_path(self.controller.servers.get_server_data_by_id(server_id)['path']),
new_item_path) \
or helper.check_path_exists(os.path.abspath(new_item_path)):
logger.warning(f"Invalid new name path in rename_file file ajax call ({server_id})")
console.warning(f"Invalid new name path in rename_file file ajax call ({server_id})")
return
# RENAME
os.rename(item_path, new_item_path)
@tornado.web.authenticated
def patch(self, page):
api_key, _, exec_user = self.current_user
superuser = exec_user['superuser']
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument('id', None)
permissions = {
'Commands': Enum_Permissions_Server.Commands,
'Terminal': Enum_Permissions_Server.Terminal,
'Logs': Enum_Permissions_Server.Logs,
'Schedule': Enum_Permissions_Server.Schedule,
'Backup': Enum_Permissions_Server.Backup,
'Files': Enum_Permissions_Server.Files,
'Config': Enum_Permissions_Server.Config,
'Players': Enum_Permissions_Server.Players,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(exec_user['user_id'], server_id)
if page == "rename_file":
if not permissions['Files'] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
item_path = helper.get_os_understandable_path(self.get_body_argument('item_path', default=None, strip=True))
new_item_name = self.get_body_argument('new_item_name', default=None, strip=True)
if not self.check_server_id(server_id, 'rename_file'):
return
else:
server_id = bleach.clean(server_id)
if item_path is None or new_item_name is None:
logger.warning("Invalid path(s) in rename_file file ajax call")
console.warning("Invalid path(s) in rename_file file ajax call")
return
if not helper.in_path(helper.get_os_understandable_path(self.controller.servers.get_server_data_by_id(server_id)['path']), item_path) \
or not helper.check_path_exists(os.path.abspath(item_path)):
logger.warning(f"Invalid old name path in rename_file file ajax call ({server_id})")
console.warning(f"Invalid old name path in rename_file file ajax call ({server_id})")
return
new_item_path = os.path.join(os.path.split(item_path)[0], new_item_name)
if not helper.in_path(helper.get_os_understandable_path(self.controller.servers.get_server_data_by_id(server_id)['path']),
new_item_path) \
or helper.check_path_exists(os.path.abspath(new_item_path)):
logger.warning(f"Invalid new name path in rename_file file ajax call ({server_id})")
console.warning(f"Invalid new name path in rename_file file ajax call ({server_id})")
return
# RENAME
os.rename(item_path, new_item_path)
def check_server_id(self, server_id, page_name):
if server_id is None:
logger.warning(f"Server ID not defined in {page_name} file ajax call ({server_id})")
console.warning(f"Server ID not defined in {page_name} file ajax call ({server_id})")
return
else:
server_id = bleach.clean(server_id)
# does this server id exist?
if not self.controller.servers.server_id_exists(server_id):
logger.warning(f"Server ID not found in {page_name} file ajax call ({server_id})")
console.warning(f"Server ID not found in {page_name} file ajax call ({server_id})")
return
return True

View File

@ -1,25 +1,15 @@
import sys
import json
import logging
import tornado.web
import tornado.escape
import requests
from app.classes.shared.helpers import helper
from app.classes.web.base_handler import BaseHandler
from app.classes.shared.console import console
from app.classes.shared.main_models import Users, fn
logger = logging.getLogger(__name__)
try:
import bleach
import requests
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True)
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
helper.auto_installer_fix(e)
logger = logging.getLogger(__name__)
class HTTPHandler(BaseHandler):
def get(self):
@ -34,13 +24,13 @@ class HTTPHandler(BaseHandler):
try:
resp = requests.get(url + ":" + str(port))
resp.raise_for_status()
except Exception as err:
except Exception:
port = db_port
self.redirect(url+":"+str(port))
class HTTPHandlerPage(BaseHandler):
def get(self, page):
def get(self):
url = str(self.request.host)
port = 443
url_list = url.split(":")
@ -52,6 +42,6 @@ class HTTPHandlerPage(BaseHandler):
try:
resp = requests.get(url + ":" + str(port))
resp.raise_for_status()
except Exception as err:
except Exception:
port = db_port
self.redirect(url+":"+str(port))

View File

@ -1,49 +1,31 @@
import sys
import json
import logging
import tornado.web
import tornado.escape
import requests
from app.classes.shared.helpers import helper
from app.classes.web.base_handler import BaseHandler
from app.classes.shared.console import console
from app.classes.shared.main_models import Users, fn
logger = logging.getLogger(__name__)
try:
import bleach
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True)
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
class HTTPHandlerPage(BaseHandler):
def get(self, page):
def get(self):
url = self.request.full_url
port = 443
print(url)
if url[len(url)-1] == '/':
url = url.strip(url[len(url)-1])
url_list = url.split('/')
print(url_list)
if url_list[0] != "":
primary_url = url_list[0] + ":"+str(port)+"/"
backup_url = url_list[0] + ":" +str(helper.get_setting["https_port"]) +"/"
backup_url = url_list[0] + ":" +str(helper.get_setting("https_port")) +"/"
for i in range(len(url_list)-1):
primary_url += url_list[i+1]
backup_url += url_list[i+1]
else:
primary_url = url + str(port)
backup_url = url + str(helper.get_setting['https_port'])
backup_url = url + str(helper.get_setting('https_port'))
try:
resp = requests.get(primary_url)
resp.raise_for_status()
url = primary_url
except Exception as err:
except Exception:
url = backup_url
self.redirect('https://'+url+':'+ str(port))

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +1,22 @@
import sys
import json
import logging
import tornado.web
import tornado.escape
from app.classes.shared.helpers import helper
from app.classes.web.base_handler import BaseHandler
from app.classes.shared.console import console
from app.classes.shared.main_models import fn
from app.classes.models.users import Users
logger = logging.getLogger(__name__)
from app.classes.shared.authentication import authentication
from app.classes.shared.helpers import helper
from app.classes.shared.main_models import fn
from app.classes.web.base_handler import BaseHandler
try:
import bleach
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True)
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
helper.auto_installer_fix(e)
logger = logging.getLogger(__name__)
class PublicHandler(BaseHandler):
def set_current_user(self, user):
def set_current_user(self, user_id: str = None):
expire_days = helper.get_setting('cookie_expire')
@ -32,22 +24,22 @@ class PublicHandler(BaseHandler):
if not expire_days:
expire_days = "5"
if user:
self.set_secure_cookie("user", tornado.escape.json_encode(user), expires_days=int(expire_days))
if user_id is not None:
self.set_cookie("token", authentication.generate(user_id), expires_days=int(expire_days))
else:
self.clear_cookie("user")
def get(self, page=None):
error = bleach.clean(self.get_argument('error', "Invalid Login!"))
error_msg = bleach.clean(self.get_argument('error_msg', ''))
page_data = {
'version': helper.get_version_string(),
'error': error
'error': error, 'lang': helper.get_setting('language'),
'lang_page': helper.getLangPage(helper.get_setting('language'))
}
page_data['lang'] = tornado.locale.get("en_EN")
# sensible defaults
template = "public/404.html"
@ -75,6 +67,7 @@ class PublicHandler(BaseHandler):
template,
data=page_data,
translate=self.translator.translate,
error_msg = error_msg
)
def post(self, page=None):
@ -85,30 +78,32 @@ class PublicHandler(BaseHandler):
entered_username = bleach.clean(self.get_argument('username'))
entered_password = bleach.clean(self.get_argument('password'))
# pylint: disable=no-member
user_data = Users.get_or_none(fn.Lower(Users.username) == entered_username.lower())
# if we don't have a user
if not user_data:
next_page = "/public/error?error=Login Failed"
error_msg = "Incorrect username or password. Please try again."
self.clear_cookie("user")
self.clear_cookie("user_data")
self.redirect(next_page)
self.redirect(f'/public/login?error_msg={error_msg}')
return
# if they are disabled
if not user_data.enabled:
next_page = "/public/error?error=Login Failed"
error_msg = "User account disabled. Please contact your system administrator for more info."
self.clear_cookie("user")
self.clear_cookie("user_data")
self.redirect(next_page)
self.redirect(f'/public/login?error_msg={error_msg}')
return
login_result = helper.verify_pass(entered_password, user_data.password)
# Valid Login
if login_result:
self.set_current_user(entered_username)
logger.info("User: {} Logged in from IP: {}".format(user_data, self.get_remote_ip()))
self.set_current_user(user_data.user_id)
logger.info(f"User: {user_data} Logged in from IP: {self.get_remote_ip()}")
# record this login
q = Users.select().where(Users.username == entered_username.lower()).get()
@ -119,22 +114,14 @@ class PublicHandler(BaseHandler):
# log this login
self.controller.management.add_to_audit_log(user_data.user_id, "Logged in", 0, self.get_remote_ip())
cookie_data = {
"username": user_data.username,
"user_id": user_data.user_id,
"account_type": user_data.superuser,
}
self.set_secure_cookie('user_data', json.dumps(cookie_data))
next_page = "/panel/dashboard"
self.redirect(next_page)
else:
self.clear_cookie("user")
self.clear_cookie("user_data")
error_msg = "Inncorrect username or password. Please try again."
# log this failed login attempt
self.controller.management.add_to_audit_log(user_data.user_id, "Tried to log in", 0, self.get_remote_ip())
self.redirect('/public/error?error=Login Failed')
self.redirect(f'/public/login?error_msg={error_msg}')
else:
self.redirect("/public/login")

View File

@ -1,40 +1,37 @@
import sys
import json
import logging
import os
import shutil
from app.classes.shared.console import console
from app.classes.web.base_handler import BaseHandler
from app.classes.models.crafty_permissions import Enum_Permissions_Crafty
from app.classes.minecraft.serverjars import server_jar_obj
from app.classes.models.crafty_permissions import Enum_Permissions_Crafty
from app.classes.shared.helpers import helper
logger = logging.getLogger(__name__)
from app.classes.shared.file_helpers import file_helper
from app.classes.web.base_handler import BaseHandler
try:
import tornado.web
import tornado.escape
import bleach
import libgravatar
import requests
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True)
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
helper.auto_installer_fix(e)
logger = logging.getLogger(__name__)
class ServerHandler(BaseHandler):
@tornado.web.authenticated
def get(self, page):
# name = tornado.escape.json_decode(self.current_user)
exec_user_data = json.loads(self.get_secure_cookie("user_data"))
exec_user_id = exec_user_data['user_id']
exec_user = self.controller.users.get_user_by_id(exec_user_id)
# pylint: disable=unused-variable
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
exec_user_role = set()
if exec_user['superuser'] == 1:
if superuser:
defined_servers = self.controller.list_defined_servers()
exec_user_role.add("Super User")
exec_user_crafty_permissions = self.controller.crafty_perms.list_defined_crafty_permissions()
@ -42,8 +39,8 @@ class ServerHandler(BaseHandler):
for role in self.controller.roles.get_all_roles():
list_roles.append(self.controller.roles.get_role(role.role_id))
else:
exec_user_crafty_permissions = self.controller.crafty_perms.get_crafty_permissions_list(exec_user_id)
defined_servers = self.controller.servers.get_authorized_servers(exec_user_id)
exec_user_crafty_permissions = self.controller.crafty_perms.get_crafty_permissions_list(exec_user["user_id"])
defined_servers = self.controller.servers.get_authorized_servers(exec_user["user_id"])
list_roles = []
for r in exec_user['roles']:
role = self.controller.roles.get_role(r)
@ -54,7 +51,7 @@ class ServerHandler(BaseHandler):
page_data = {
'version_data': helper.get_version_string(),
'user_data': exec_user_data,
'user_data': exec_user,
'user_role' : exec_user_role,
'roles' : list_roles,
'user_crafty_permissions' : exec_user_crafty_permissions,
@ -71,20 +68,54 @@ class ServerHandler(BaseHandler):
'hosts_data': self.controller.management.get_latest_hosts_stats(),
'menu_servers': defined_servers,
'show_contribute': helper.get_setting("show_contribute_link", True),
'lang': self.controller.users.get_user_lang_by_id(exec_user_id)
'lang': self.controller.users.get_user_lang_by_id(exec_user["user_id"]),
'lang_page': helper.getLangPage(self.controller.users.get_user_lang_by_id(exec_user["user_id"])),
'api_key': {
'name': api_key.name,
'created': api_key.created,
'server_permissions': api_key.server_permissions,
'crafty_permissions': api_key.crafty_permissions,
'superuser': api_key.superuser
} if api_key is not None else None,
'superuser': superuser
}
if exec_user['superuser'] == 1:
if helper.get_setting("allow_nsfw_profile_pictures"):
rating = "x"
else:
rating = "g"
if exec_user['email'] != 'default@example.com' or "":
g = libgravatar.Gravatar(libgravatar.sanitize_email(exec_user['email']))
url = g.get_image(size=80, default="404", force_default=False, rating=rating, filetype_extension=False, use_ssl=True) # + "?d=404"
if requests.head(url).status_code != 404:
profile_url = url
else:
profile_url = "/static/assets/images/faces-clipart/pic-3.png"
else:
profile_url = "/static/assets/images/faces-clipart/pic-3.png"
page_data['user_image'] = profile_url
if superuser:
page_data['roles'] = list_roles
if page == "step1":
if not exec_user['superuser'] and not self.controller.crafty_perms.can_create_server(exec_user_id):
if not superuser and not self.controller.crafty_perms.can_create_server(exec_user["user_id"]):
self.redirect("/panel/error?error=Unauthorized access: not a server creator or server limit reached")
return
page_data['server_types'] = server_jar_obj.get_serverjar_data_sorted()
page_data['js_server_types'] = json.dumps(server_jar_obj.get_serverjar_data_sorted())
page_data['server_types'] = server_jar_obj.get_serverjar_data()
page_data['js_server_types'] = json.dumps(server_jar_obj.get_serverjar_data())
template = "server/wizard.html"
if page == "bedrock_step1":
if not superuser and not self.controller.crafty_perms.can_create_server(exec_user["user_id"]):
self.redirect("/panel/error?error=Unauthorized access: not a server creator or server limit reached")
return
template = "server/bedrock_wizard.html"
self.render(
template,
data=page_data,
@ -93,17 +124,19 @@ class ServerHandler(BaseHandler):
@tornado.web.authenticated
def post(self, page):
exec_user_data = json.loads(self.get_secure_cookie("user_data"))
exec_user_id = exec_user_data['user_id']
exec_user = self.controller.users.get_user_by_id(exec_user_id)
# pylint: disable=unused-variable
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
template = "public/404.html"
page_data = {
'version_data': "version_data_here",
'user_data': exec_user_data,
'version_data': "version_data_here", # TODO
'user_data': exec_user,
'show_contribute': helper.get_setting("show_contribute_link", True),
'lang': self.controller.users.get_user_lang_by_id(exec_user_id)
'lang': self.controller.users.get_user_lang_by_id(exec_user["user_id"]),
'lang_page': helper.getLangPage(self.controller.users.get_user_lang_by_id(exec_user["user_id"]))
}
if page == "command":
@ -125,7 +158,7 @@ class ServerHandler(BaseHandler):
name_counter = 1
while is_name_used(new_server_name):
name_counter += 1
new_server_name = server_data.get('server_name') + " (Copy {})".format(name_counter)
new_server_name = server_data.get('server_name') + f" (Copy {name_counter})"
new_server_uuid = helper.create_uuid()
while os.path.exists(os.path.join(helper.servers_dir, new_server_uuid)):
@ -133,29 +166,36 @@ class ServerHandler(BaseHandler):
new_server_path = os.path.join(helper.servers_dir, new_server_uuid)
# copy the old server
shutil.copytree(server_data.get('path'), new_server_path)
file_helper.copy_dir(server_data.get('path'), new_server_path)
# TODO get old server DB data to individual variables
stop_command = server_data.get('stop_command')
new_server_command = str(server_data.get('execution_command')).replace(server_uuid, new_server_uuid)
new_executable = server_data.get('executable')
new_server_log_file = str(server_data.get('log_path')).replace(server_uuid, new_server_uuid)
auto_start = server_data.get('auto_start')
auto_start_delay = server_data.get('auto_start_delay')
crash_detection = server_data.get('crash_detection')
new_server_log_file = str(helper.get_os_understandable_path(server_data.get('log_path'))).replace(server_uuid, new_server_uuid)
server_port = server_data.get('server_port')
server_type = server_data.get('type')
self.controller.servers.create_server(new_server_name, new_server_uuid, new_server_path, "", new_server_command, new_executable, new_server_log_file, stop_command, server_port)
self.controller.servers.create_server(new_server_name,
new_server_uuid,
new_server_path,
"",
new_server_command,
new_executable,
new_server_log_file,
stop_command,
server_type,
server_port)
self.controller.init_all_servers()
return
self.controller.management.send_command(exec_user_data['user_id'], server_id, self.get_remote_ip(), command)
self.controller.management.send_command(exec_user['user_id'], server_id, self.get_remote_ip(), command)
if page == "step1":
if not exec_user['superuser']:
if not superuser:
user_roles = self.controller.roles.get_all_roles()
else:
user_roles = self.controller.roles.get_all_roles()
@ -185,46 +225,51 @@ class ServerHandler(BaseHandler):
return
new_server_id = self.controller.import_jar_server(server_name, import_server_path,import_server_jar, min_mem, max_mem, port)
self.controller.management.add_to_audit_log(exec_user_data['user_id'],
"imported a jar server named \"{}\"".format(server_name), # Example: Admin imported a server named "old creative"
self.controller.management.add_to_audit_log(exec_user['user_id'],
f"imported a jar server named \"{server_name}\"", # Example: Admin imported a server named "old creative"
new_server_id,
self.get_remote_ip())
elif import_type == 'import_zip':
# here import_server_path means the zip path
good_path = self.controller.verify_zip_server(import_server_path)
zip_path = bleach.clean(self.get_argument('root_path'))
good_path = helper.check_path_exists(zip_path)
if not good_path:
self.redirect("/panel/error?error=Zip file not found!")
self.redirect("/panel/error?error=Temp path not found!")
return
new_server_id = self.controller.import_zip_server(server_name, import_server_path,import_server_jar, min_mem, max_mem, port)
new_server_id = self.controller.import_zip_server(server_name, zip_path, import_server_jar, min_mem, max_mem, port)
if new_server_id == "false":
self.redirect("/panel/error?error=Zip file not accessible! You can fix this permissions issue with sudo chown -R crafty:crafty {} And sudo chmod 2775 -R {}".format(import_server_path, import_server_path))
self.redirect("/panel/error?error=Zip file not accessible! You can fix this permissions issue with" +
f"sudo chown -R crafty:crafty {import_server_path} And sudo chmod 2775 -R {import_server_path}")
return
self.controller.management.add_to_audit_log(exec_user_data['user_id'],
"imported a zip server named \"{}\"".format(server_name), # Example: Admin imported a server named "old creative"
self.controller.management.add_to_audit_log(exec_user['user_id'],
f"imported a zip server named \"{server_name}\"", # Example: Admin imported a server named "old creative"
new_server_id,
self.get_remote_ip())
#deletes temp dir
file_helper.del_dirs(zip_path)
else:
if len(server_parts) != 2:
self.redirect("/panel/error?error=Invalid server data")
return
server_type, server_version = server_parts
# TODO: add server type check here and call the correct server add functions if not a jar
role_ids = self.controller.users.get_user_roles_id(exec_user_id)
role_ids = self.controller.users.get_user_roles_id(exec_user["user_id"])
new_server_id = self.controller.create_jar_server(server_type, server_version, server_name, min_mem, max_mem, port)
self.controller.management.add_to_audit_log(exec_user_data['user_id'],
"created a {} {} server named \"{}\"".format(server_version, str(server_type).capitalize(), server_name), # Example: Admin created a 1.16.5 Bukkit server named "survival"
self.controller.management.add_to_audit_log(exec_user['user_id'],
f"created a {server_version} {str(server_type).capitalize()} server named \"{server_name}\"",
# Example: Admin created a 1.16.5 Bukkit server named "survival"
new_server_id,
self.get_remote_ip())
# These lines create a new Role for the Server with full permissions and add the user to it if he's not a superuser
if len(captured_roles) == 0:
if not exec_user['superuser']:
if not superuser:
new_server_uuid = self.controller.servers.get_server_data_by_id(new_server_id).get("server_uuid")
role_id = self.controller.roles.add_role("Creator of Server with uuid={}".format(new_server_uuid))
role_id = self.controller.roles.add_role(f"Creator of Server with uuid={new_server_uuid}")
self.controller.server_perms.add_role_server(new_server_id, role_id, "11111111")
self.controller.users.add_role_to_user(exec_user_id, role_id)
self.controller.crafty_perms.add_server_creation(exec_user_id)
self.controller.users.add_role_to_user(exec_user["user_id"], role_id)
self.controller.crafty_perms.add_server_creation(exec_user["user_id"])
else:
for role in captured_roles:
@ -234,8 +279,94 @@ class ServerHandler(BaseHandler):
self.controller.stats.record_stats()
self.redirect("/panel/dashboard")
if page == "bedrock_step1":
if not superuser:
user_roles = self.controller.roles.get_all_roles()
else:
user_roles = self.controller.roles.get_all_roles()
server = bleach.clean(self.get_argument('server', ''))
server_name = bleach.clean(self.get_argument('server_name', ''))
port = bleach.clean(self.get_argument('port', ''))
import_type = bleach.clean(self.get_argument('create_type', ''))
import_server_path = bleach.clean(self.get_argument('server_path', ''))
import_server_exe = bleach.clean(self.get_argument('server_jar', ''))
server_parts = server.split("|")
captured_roles = []
for role in user_roles:
if bleach.clean(self.get_argument(str(role), '')) == "on":
captured_roles.append(role)
if not server_name:
self.redirect("/panel/error?error=Server name cannot be empty!")
return
if import_type == 'import_jar':
good_path = self.controller.verify_jar_server(import_server_path, import_server_exe)
if not good_path:
self.redirect("/panel/error?error=Server path or Server Jar not found!")
return
new_server_id = self.controller.import_bedrock_server(server_name, import_server_path,import_server_exe, port)
self.controller.management.add_to_audit_log(exec_user['user_id'],
f"imported a jar server named \"{server_name}\"", # Example: Admin imported a server named "old creative"
new_server_id,
self.get_remote_ip())
elif import_type == 'import_zip':
# here import_server_path means the zip path
zip_path = bleach.clean(self.get_argument('root_path'))
good_path = helper.check_path_exists(zip_path)
if not good_path:
self.redirect("/panel/error?error=Temp path not found!")
return
new_server_id = self.controller.import_bedrock_zip_server(server_name, zip_path, import_server_exe, port)
if new_server_id == "false":
self.redirect("/panel/error?error=Zip file not accessible! You can fix this permissions issue with" +
f"sudo chown -R crafty:crafty {import_server_path} And sudo chmod 2775 -R {import_server_path}")
return
self.controller.management.add_to_audit_log(exec_user['user_id'],
f"imported a zip server named \"{server_name}\"", # Example: Admin imported a server named "old creative"
new_server_id,
self.get_remote_ip())
#deletes temp dir
file_helper.del_dirs(zip_path)
else:
if len(server_parts) != 2:
self.redirect("/panel/error?error=Invalid server data")
return
server_type, server_version = server_parts
# TODO: add server type check here and call the correct server add functions if not a jar
role_ids = self.controller.users.get_user_roles_id(exec_user["user_id"])
new_server_id = self.controller.create_jar_server(server_type, server_version, server_name, min_mem, max_mem, port)
self.controller.management.add_to_audit_log(exec_user['user_id'],
f"created a {server_version} {str(server_type).capitalize()} server named \"{server_name}\"",
# Example: Admin created a 1.16.5 Bukkit server named "survival"
new_server_id,
self.get_remote_ip())
# These lines create a new Role for the Server with full permissions and add the user to it if he's not a superuser
if len(captured_roles) == 0:
if not superuser:
new_server_uuid = self.controller.servers.get_server_data_by_id(new_server_id).get("server_uuid")
role_id = self.controller.roles.add_role(f"Creator of Server with uuid={new_server_uuid}")
self.controller.server_perms.add_role_server(new_server_id, role_id, "11111111")
self.controller.users.add_role_to_user(exec_user["user_id"], role_id)
self.controller.crafty_perms.add_server_creation(exec_user["user_id"])
else:
for role in captured_roles:
role_id = role
self.controller.server_perms.add_role_server(new_server_id, role_id, "11111111")
self.controller.stats.record_stats()
self.redirect("/panel/dashboard")
try:
self.render(
template,
data=page_data,
translate=self.translator.translate,
)
except RuntimeError:
self.redirect('/panel/dashboard')

View File

@ -1,9 +1,11 @@
import tornado.web
from typing import (
Optional
)
from typing import ( Optional )
from app.classes.shared.console import console
try:
import tornado.web
except ModuleNotFoundError as e:
from app.classes.shared.helpers import helper
helper.auto_installer_fix(e)
class CustomStaticHandler(tornado.web.StaticFileHandler):
def validate_absolute_path(self, root: str, absolute_path: str) -> Optional[str]:

View File

@ -1,36 +1,27 @@
from re import template
import sys
import json
import logging
import tornado.web
import tornado.escape
import requests
from app.classes.shared.helpers import helper
from app.classes.web.base_handler import BaseHandler
from app.classes.shared.console import console
from app.classes.shared.main_models import fn
logger = logging.getLogger(__name__)
try:
import bleach
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True)
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
class StatusHandler(BaseHandler):
def get(self):
page_data = {}
page_data['lang'] = tornado.locale.get("en_EN")
page_data['lang'] = helper.get_setting('language')
page_data['lang_page'] = helper.getLangPage(helper.get_setting('language'))
page_data['servers'] = self.controller.servers.get_all_servers_stats()
running = 0
for srv in page_data['servers']:
if srv['stats']['running']:
running += 1
server_data = srv.get('server_data', False)
server_id = server_data.get('server_id', False)
srv['raw_ping_result'] = self.controller.stats.get_raw_server_stats(server_id)
srv['raw_ping_result'] = self.controller.servers.get_server_stats_by_id(server_id)
if 'icon' not in srv['raw_ping_result']:
srv['raw_ping_result']['icon'] = False
page_data['running'] = running
template = 'public/status.html'
@ -45,8 +36,7 @@ class StatusHandler(BaseHandler):
for srv in page_data['servers']:
server_data = srv.get('server_data', False)
server_id = server_data.get('server_id', False)
srv['raw_ping_result'] = self.controller.stats.get_raw_server_stats(server_id)
srv['raw_ping_result'] = self.controller.servers.get_server_stats_by_id(server_id)
template = 'public/status.html'
self.render(

View File

@ -3,12 +3,22 @@ import sys
import json
import asyncio
import logging
import threading
from app.classes.shared.translation import translation
from app.classes.shared.console import console
from app.classes.shared.helpers import helper
logger = logging.getLogger(__name__)
from app.classes.web.file_handler import FileHandler
from app.classes.web.public_handler import PublicHandler
from app.classes.web.panel_handler import PanelHandler
from app.classes.web.default_handler import DefaultHandler
from app.classes.web.server_handler import ServerHandler
from app.classes.web.ajax_handler import AjaxHandler
from app.classes.web.api_handler import ServersStats, NodeStats
from app.classes.web.websocket_handler import SocketHandler
from app.classes.web.static_handler import CustomStaticHandler
from app.classes.web.upload_handler import UploadHandler
from app.classes.web.http_handler import HTTPHandler, HTTPHandlerPage
from app.classes.web.status_handler import StatusHandler
try:
import tornado.web
@ -18,25 +28,11 @@ try:
import tornado.escape
import tornado.locale
import tornado.httpserver
from app.classes.web.public_handler import PublicHandler
from app.classes.web.panel_handler import PanelHandler
from app.classes.web.default_handler import DefaultHandler
from app.classes.web.server_handler import ServerHandler
from app.classes.web.ajax_handler import AjaxHandler
from app.classes.web.api_handler import ServersStats, NodeStats
from app.classes.web.websocket_handler import SocketHandler
from app.classes.web.static_handler import CustomStaticHandler
from app.classes.shared.translation import translation
from app.classes.web.upload_handler import UploadHandler
from app.classes.web.http_handler import HTTPHandler, HTTPHandlerPage
from app.classes.web.status_handler import StatusHandler
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e, e.name))
console.critical("Import Error: Unable to load {} module".format(e, e.name))
sys.exit(1)
helper.auto_installer_fix(e)
logger = logging.getLogger(__name__)
class Webserver:
@ -48,7 +44,6 @@ class Webserver:
self.tasks_manager = tasks_manager
self._asyncio_patch()
@staticmethod
def log_function(handler):
@ -57,6 +52,7 @@ class Webserver:
'Method': handler.request.method,
'URL': handler.request.uri,
'Remote_IP': handler.request.remote_ip,
# pylint: disable=consider-using-f-string
'Elapsed_Time': '%.2fms' % (handler.request.request_time() * 1000)
}
@ -74,12 +70,12 @@ class Webserver:
"""
logger.debug("Checking if asyncio patch is required")
if sys.platform.startswith("win") and sys.version_info >= (3, 8):
# pylint: disable=reimported,import-outside-toplevel,redefined-outer-name
import asyncio
try:
from asyncio import WindowsSelectorEventLoopPolicy
except ImportError:
logger.debug("asyncio patch isn't required")
pass # Can't assign a policy which doesn't exist.
logger.debug("asyncio patch isn't required") # Can't assign a policy which doesn't exist.
else:
if not isinstance(asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy):
asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())
@ -110,12 +106,13 @@ class Webserver:
'keyfile': os.path.join(helper.config_dir, 'web', 'certs', 'commander.key.pem'),
}
logger.info("Starting Web Server on ports http:{} https:{}".format(http_port, https_port))
logger.info(f"Starting Web Server on ports http:{http_port} https:{https_port}")
asyncio.set_event_loop(asyncio.new_event_loop())
tornado.template.Loader('.')
# TODO: Remove because we don't and won't use
tornado.locale.set_default_locale('en_EN')
handler_args = {"controller": self.controller, "tasks_manager": self.tasks_manager, "translator": translation}
@ -125,6 +122,7 @@ class Webserver:
(r'/panel/(.*)', PanelHandler, handler_args),
(r'/server/(.*)', ServerHandler, handler_args),
(r'/ajax/(.*)', AjaxHandler, handler_args),
(r'/files/(.*)', FileHandler, handler_args),
(r'/api/stats/servers', ServersStats, handler_args),
(r'/api/stats/node', NodeStats, handler_args),
(r'/ws', SocketHandler, handler_args),
@ -175,10 +173,8 @@ class Webserver:
self.HTTPS_Server = tornado.httpserver.HTTPServer(app, ssl_options=cert_objects)
self.HTTPS_Server.listen(https_port)
logger.info("http://{}:{} is up and ready for connections.".format(helper.get_local_ip(), http_port))
logger.info("https://{}:{} is up and ready for connections.".format(helper.get_local_ip(), https_port))
console.info("http://{}:{} is up and ready for connections.".format(helper.get_local_ip(), http_port))
console.info("https://{}:{} is up and ready for connections.".format(helper.get_local_ip(), https_port))
logger.info(f"https://{helper.get_local_ip()}:{https_port} is up and ready for connections.")
console.info(f"https://{helper.get_local_ip()}:{https_port} is up and ready for connections.")
console.info("Server Init Complete: Listening For Connections:")

View File

@ -1,25 +1,31 @@
from app.classes.shared.main_controller import Controller
import tornado.options
import tornado.web
import tornado.httpserver
from tornado.options import options
from app.classes.models.server_permissions import Enum_Permissions_Server
from app.classes.shared.helpers import helper
from app.classes.web.websocket_helper import websocket_helper
from app.classes.shared.console import console
import logging
import os
import json
import time
from app.classes.models.server_permissions import Enum_Permissions_Server
from app.classes.shared.helpers import helper
from app.classes.shared.console import console
from app.classes.shared.main_controller import Controller
from app.classes.web.websocket_helper import websocket_helper
from app.classes.web.base_handler import BaseHandler
try:
import tornado.web
import tornado.options
import tornado.httpserver
except ModuleNotFoundError as ex:
helper.auto_installer_fix(ex)
logger = logging.getLogger(__name__)
# Class & Function Defination
MAX_STREAMED_SIZE = 1024 * 1024 * 1024
@tornado.web.stream_request_body
class UploadHandler(tornado.web.RequestHandler):
class UploadHandler(BaseHandler):
# noinspection PyAttributeOutsideInit
def initialize(self, controller: Controller=None, tasks_manager=None, translator=None):
self.controller = controller
self.tasks_manager = tasks_manager
@ -27,8 +33,21 @@ class UploadHandler(tornado.web.RequestHandler):
def prepare(self):
self.do_upload = True
user_data = json.loads(self.get_secure_cookie('user_data'))
user_id = user_data['user_id']
# pylint: disable=unused-variable
api_key, token_data, exec_user = self.current_user
server_id = self.get_argument('server_id', None)
superuser = exec_user['superuser']
if api_key is not None:
superuser = superuser and api_key.superuser
user_id = exec_user['user_id']
if superuser:
exec_user_server_permissions = self.controller.server_perms.list_defined_permissions()
elif api_key is not None:
exec_user_server_permissions = self.controller.server_perms.get_api_key_permissions_list(api_key, server_id)
else:
exec_user_server_permissions = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id)
server_id = self.request.headers.get('X-ServerId', None)
@ -42,8 +61,7 @@ class UploadHandler(tornado.web.RequestHandler):
console.warning('Server ID not found in upload handler call')
self.do_upload = False
user_permissions = self.controller.server_perms.get_user_permissions_list(user_id, server_id)
if Enum_Permissions_Server.Files not in user_permissions:
if Enum_Permissions_Server.Files not in exec_user_server_permissions:
logger.warning(f'User {user_id} tried to upload a file to {server_id} without permissions!')
console.warning(f'User {user_id} tried to upload a file to {server_id} without permissions!')
self.do_upload = False
@ -52,8 +70,8 @@ class UploadHandler(tornado.web.RequestHandler):
filename = self.request.headers.get('X-FileName', None)
full_path = os.path.join(path, filename)
if not helper.in_path(self.controller.servers.get_server_data_by_id(server_id)['path'], full_path):
print(user_id, server_id, self.controller.servers.get_server_data_by_id(server_id)['path'], full_path)
if not helper.in_path(helper.get_os_understandable_path(self.controller.servers.get_server_data_by_id(server_id)['path']), full_path):
print(user_id, server_id, helper.get_os_understandable_path(self.controller.servers.get_server_data_by_id(server_id)['path']), full_path)
logger.warning(f'User {user_id} tried to upload a file to {server_id} but the path is not inside of the server!')
console.warning(f'User {user_id} tried to upload a file to {server_id} but the path is not inside of the server!')
self.do_upload = False
@ -62,7 +80,7 @@ class UploadHandler(tornado.web.RequestHandler):
try:
self.f = open(full_path, "wb")
except Exception as e:
logger.error("Upload failed with error: {}".format(e))
logger.error(f"Upload failed with error: {e}")
self.do_upload = False
# If max_body_size is not set, you cannot upload files > 100MB
self.request.connection.set_max_body_size(MAX_STREAMED_SIZE)
@ -83,6 +101,6 @@ class UploadHandler(tornado.web.RequestHandler):
websocket_helper.broadcast('close_upload_box', 'error')
self.finish('error')
def data_received(self, data):
def data_received(self, chunk):
if self.do_upload:
self.f.write(data)
self.f.write(chunk)

View File

@ -1,23 +1,27 @@
import json
import logging
import asyncio
from urllib.parse import parse_qsl
from app.classes.models.users import Users
from app.classes.shared.authentication import authentication
from app.classes.shared.helpers import helper
from app.classes.web.websocket_helper import websocket_helper
logger = logging.getLogger(__name__)
try:
import tornado.websocket
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e, e.name))
console.critical("Import Error: Unable to load {} module".format(e, e.name))
sys.exit(1)
helper.auto_installer_fix(e)
logger = logging.getLogger(__name__)
class SocketHandler(tornado.websocket.WebSocketHandler):
page = None
page_query_params = None
controller = None
tasks_manager = None
translator = None
io_loop = None
def initialize(self, controller=None, tasks_manager=None, translator=None):
self.controller = controller
@ -31,18 +35,14 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
self.request.remote_ip
return remote_ip
def get_user_id(self):
_, _, user = authentication.check(self.get_cookie('token'))
return user['user_id']
def check_auth(self):
user_data_cookie_raw = self.get_secure_cookie('user_data')
if user_data_cookie_raw and user_data_cookie_raw.decode('utf-8'):
user_data_cookie = user_data_cookie_raw.decode('utf-8')
user_id = json.loads(user_data_cookie)['user_id']
query = Users.select().where(Users.user_id == user_id)
if query.exists():
return True
return False
return authentication.check_bool(self.get_cookie('token'))
# pylint: disable=arguments-differ
def open(self):
logger.debug('Checking WebSocket authentication')
if self.check_auth():
@ -50,7 +50,10 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
else:
websocket_helper.send_message(self, 'notification', 'Not authenticated for WebSocket connection')
self.close()
self.controller.management.add_to_audit_log_raw('unknown', 0, 0, 'Someone tried to connect via WebSocket without proper authentication', self.get_remote_ip())
self.controller.management.add_to_audit_log_raw('unknown',
0, 0,
'Someone tried to connect via WebSocket without proper authentication',
self.get_remote_ip())
websocket_helper.broadcast('notification', 'Someone tried to connect via WebSocket without proper authentication')
logger.warning('Someone tried to connect via WebSocket without proper authentication')
@ -62,22 +65,21 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
)))
websocket_helper.add_client(self)
logger.debug('Opened WebSocket connection')
# websocket_helper.broadcast('notification', 'New client connected')
def on_message(self, rawMessage):
# pylint: disable=arguments-renamed
@staticmethod
def on_message(raw_message):
logger.debug('Got message from WebSocket connection {}'.format(rawMessage))
message = json.loads(rawMessage)
logger.debug('Event Type: {}, Data: {}'.format(message['event'], message['data']))
logger.debug(f'Got message from WebSocket connection {raw_message}')
message = json.loads(raw_message)
logger.debug(f"Event Type: {message['event']}, Data: {message['data']}")
def on_close(self):
websocket_helper.remove_client(self)
logger.debug('Closed WebSocket connection')
# websocket_helper.broadcast('notification', 'Client disconnected')
async def write_message_int(self, message):
self.write_message(message)
def write_message_helper(self, message):
asyncio.run_coroutine_threadsafe(self.write_message_int(message), self.io_loop.asyncio_loop)

View File

@ -1,20 +1,10 @@
import json
import logging
import sys, threading, asyncio
from app.classes.shared.console import console
logger = logging.getLogger(__name__)
try:
import tornado.ioloop
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e, e.name))
console.critical("Import Error: Unable to load {} module".format(e, e.name))
sys.exit(1)
class WebSocketHelper:
def __init__(self):
self.clients = set()
@ -25,32 +15,54 @@ class WebSocketHelper:
def remove_client(self, client):
self.clients.remove(client)
# pylint: disable=no-self-use
def send_message(self, client, event_type: str, data):
if client.check_auth():
message = str(json.dumps({'event': event_type, 'data': data}))
client.write_message_helper(message)
def broadcast(self, event_type: str, data):
logger.debug('Sending to {} clients: {}'.format(len(self.clients), json.dumps({'event': event_type, 'data': data})))
logger.debug(f"Sending to {len(self.clients)} clients: {json.dumps({'event': event_type, 'data': data})}")
for client in self.clients:
try:
self.send_message(client, event_type, data)
except Exception as e:
logger.exception('Error catched while sending WebSocket message to {}'.format(client.get_remote_ip()))
logger.exception(f'Error caught while sending WebSocket message to {client.get_remote_ip()} {e}')
def broadcast_page(self, page: str, event_type: str, data):
def filter_fn(client):
return client.page == page
clients = list(filter(filter_fn, self.clients))
self.broadcast_with_fn(filter_fn, event_type, data)
logger.debug('Sending to {} out of {} clients: {}'.format(len(clients), len(self.clients), json.dumps({'event': event_type, 'data': data})))
def broadcast_user(self, user_id: str, event_type: str, data):
def filter_fn(client):
return client.get_user_id() == user_id
for client in clients:
try:
self.send_message(client, event_type, data)
except Exception as e:
logger.exception('Error catched while sending WebSocket message to {}'.format(client.get_remote_ip()))
self.broadcast_with_fn(filter_fn, event_type, data)
def broadcast_user_page(self, page: str, user_id: str, event_type: str, data):
def filter_fn(client):
if client.get_user_id() != user_id:
return False
if client.page != page:
return False
return True
self.broadcast_with_fn(filter_fn, event_type, data)
def broadcast_user_page_params(self, page: str, params: dict, user_id: str, event_type: str, data):
def filter_fn(client):
if client.get_user_id() != user_id:
return False
if client.page != page:
return False
for key, param in params.items():
if param != client.page_query_params.get(key, None):
return False
return True
self.broadcast_with_fn(filter_fn, event_type, data)
def broadcast_page_params(self, page: str, params: dict, event_type: str, data):
def filter_fn(client):
@ -61,15 +73,17 @@ class WebSocketHelper:
return False
return True
clients = list(filter(filter_fn, self.clients))
self.broadcast_with_fn(filter_fn, event_type, data)
logger.debug('Sending to {} out of {} clients: {}'.format(len(clients), len(self.clients), json.dumps({'event': event_type, 'data': data})))
def broadcast_with_fn(self, filter_fn, event_type: str, data):
clients = list(filter(filter_fn, self.clients))
logger.debug(f"Sending to {len(clients)} out of {len(self.clients)} clients: {json.dumps({'event': event_type, 'data': data})}")
for client in clients:
try:
self.send_message(client, event_type, data)
except Exception as e:
logger.exception('Error catched while sending WebSocket message to {}'.format(client.get_remote_ip()))
logger.exception(f'Error catched while sending WebSocket message to {client.get_remote_ip()} {e}')
def disconnect_all(self):
console.info('Disconnecting WebSocket clients')

View File

@ -5,6 +5,7 @@
"language": "en_EN",
"cookie_expire": 30,
"cookie_secret": "random",
"apikey_secret": "random",
"show_errors": true,
"history_max_age": 7,
"stats_update_frequency": 30,
@ -12,5 +13,8 @@
"show_contribute_link": true,
"virtual_terminal_lines": 70,
"max_log_lines": 700,
"keywords": ["help", "chunk"]
"max_audit_entries": 300,
"disabled_language_files": ["lol_EN.json", ""],
"keywords": ["help", "chunk"],
"allow_nsfw_profile_pictures": false
}

View File

@ -1,138 +1,67 @@
{
"staff": {
"development": [
{
"name": "Phil Tarrant",
"title": "Creator",
"loc": "Southeast, USA",
"tags": [
"Staff",
[
"Developer",
"https://gitlab.com/Ptarrant1"
],
"Creator"
],
"blurb": "For best results, apply a thin layer of Phillip to code, cyber security, and parenthood for maximum effectiveness. Phillip often spends too much time with his chickens.",
"pic": "/static/assets/images/credits/ptarrant.png"
},
{
"name": "Pita Bread",
"title": "Leadership Team",
"title": "CEO",
"loc": "Midwest, USA",
"tags": [
"Staff",
[
"Developer",
"https://gitlab.com/craftbreadth"
],
"Community Leader"
],
"tags": [ "Staff", [ "Developer", "https://gitlab.com/craftbreadth" ], "Crafty Leadership" ],
"blurb": "Baked goods enthusiast who dabbles in Linux and sets up networks for fun. Can be found writing long-winded emails, debugging strange wifi issues, or taking care of his extensive houseplant collection.",
"pic": "/static/assets/images/credits/pita.png"
},
{
"name": "macgeek",
"title": "Leadership Team",
"name": "Xithical",
"title": "CFO/COO",
"loc": "Midwest, USA",
"tags": [
"Staff",
[
"Developer",
"https://gitlab.com/computergeek125"
],
"Project Manager"
],
"tags": [ "Staff", [ "Developer", "https://gitlab.com/xithical" ], "Crafty Leadership" ],
"blurb": "Having sold his soul to the IT administration gods many eons ago, you can usually find him working, going home to do work, or continuing to work in the support Discord.",
"pic": "/static/assets/images/credits/xithical.png"
},
{
"name": "Phil Tarrant",
"title": "Creator",
"loc": "Southeast, USA",
"tags": [ "Staff", [ "Developer", "https://gitlab.com/Ptarrant1" ], "Crafty Advisor" ],
"blurb": "For best results, apply a thin layer of Phillip to code, cyber security, and parenthood for maximum effectiveness. Phillip often spends too much time with his chickens.",
"pic": "/static/assets/images/credits/ptarrant.png"
},
{
"name": "macgeek",
"title": "Lead Software Engineer",
"loc": "Midwest, USA",
"tags": [ "Staff", [ "Developer", "https://gitlab.com/computergeek125" ], "Crafty Advisor" ],
"blurb": "Sysadmin for work and sysadmin for fun (avid homelabber). He enjoys a good tech tangent and gaming.",
"pic": "/static/assets/images/credits/macgeek.png"
},
{
"name": "parzivaldewey",
"title": null,
"title": "Support Manager",
"loc": "East Coast, USA",
"tags": [
"Staff",
[
"Developer",
"https://gitlab.com/amcmanu3"
],
"Support Manager"
],
"tags": [ "Staff", [ "Developer", "https://gitlab.com/amcmanu3" ], "Support Manager" ],
"blurb": "His interests include Linux, gaming, and helping others. When he's able to unplug he enjoys biking, hiking, and playing soccer.",
"pic": "/static/assets/images/credits/parzivaldewey.png"
},
{
"name": "Xithical",
"title": null,
"loc": "Midwest, USA",
"tags": [
"Staff",
[
"Developer",
"https://gitlab.com/xithical"
],
null
],
"blurb": "Having sold his soul to the IT administration gods many eons ago, you can usually find him working, going home to do work, or continuing to work in the support Discord.",
"pic": "/static/assets/images/credits/xithical.png"
},
{
"name": "MC Gaming",
"title": null,
"loc": "Central, UK",
"tags": [
"Staff",
[
"Developer",
"https://gitlab.com/MCgamin1738"
],
null
],
"blurb": "His interests include learning, Linux, and programming. He loves pentesting apps and gaming.",
"pic": "/static/assets/images/credits/mcgaming.png"
},
{
"name": "Silversthorn",
"title": null,
"title": "Software Engineer",
"loc": "Provence, FR",
"tags": [
"Staff",
[
"Developer",
"https://gitlab.com/Silversthorn"
],
null
],
"blurb": "Developper at work and at home, testing his own code is a pain, so his coding precept is \"Testing is Doubting\"",
"tags": [ "Staff", [ "Developer", "https://gitlab.com/Silversthorn" ], null ],
"blurb": "Developer at work and at home, testing his own code is a pain, so his coding precept is \"Testing is Doubting\"",
"pic": "/static/assets/images/credits/silversthorn.png"
},
{
"name": "ThatOneLukas",
"title": null,
"title": "Software Engineer",
"loc": "Helsinki, FI",
"tags": [
"Staff",
[
"Developer",
"https://gitlab.com/LukasDoesDev"
],
null
],
"blurb": "Lukas enjoys bashing his head at the table while his code does not work",
"tags": [ "Staff", [ "Developer", "https://gitlab.com/LukasDoesDev" ], null ],
"blurb": "Arch Linux enthusiast who likes 80s/90s music, Rust (The programming language) and light distros like Arch Linux btw. Dislikes C, C++, Go, and Microsoft. Also doesn't like Finnish weather that freezes molten snow to (deadly) ice.",
"pic": "/static/assets/images/credits/lukas.png"
},
{
"name": "Zedifus",
"title": null,
"title": "DevOps Engineer",
"loc": "Scotland, UK",
"tags": [
"Staff",
[
"Developer",
"https://gitlab.com/Zedifus"
],
"DevOps"
],
"tags": [ "Staff", [ "Developer", "https://gitlab.com/Zedifus" ], "DevOps" ],
"blurb": "A Streamer, who is currently working in the insurance industry, self-teaching software development & DevOps, with the hopes of a career change! Enjoys playing games and has a sassy cat called Eve.",
"pic": "/static/assets/images/credits/zedifus.jpg"
}
@ -140,27 +69,27 @@
"support": [
{
"name": "iSilverfyre",
"title": null,
"loc": null,
"tags": [
"Staff",
"Wiki",
null
],
"title": "Community Manager",
"loc": "South Central, US",
"tags": [ "Staff", null, "Wiki" ],
"blurb": "Silver enjoys helping others with their computer needs, writing documentation, and loving her cat.",
"pic": "/static/assets/images/credits/isilverfyre.png"
},
{
"name": "Quentin",
"title": null,
"title": "Document Curator",
"loc": null,
"tags": [
"Staff",
"Developer",
null
],
"tags": [ "Staff", [ "Developer", "https://gitlab.com/qub3d" ], "Wiki" ],
"blurb": "Hosts Minecraft servers for his weird friends, works for an IoT company as his dayjob. The 's' in IoT stands for 'secure'.",
"pic": "/static/assets/images/credits/qub3d.png"
},
{
"name": "DarthLeo",
"title": "Support Engineer",
"loc": "East Coast, US",
"tags": [ "Staff", null, "Discord" ],
"blurb": "Just a simple gamer.",
"pic": "/static/assets/images/credits/darthLeo.png"
}
],
"retired": [
@ -168,14 +97,7 @@
"name": "Kev Dagoat",
"title": "Head of Development",
"loc": "East Coast, AU",
"tags": [
"Staff",
[
"Developer",
"https://gitlab.com/kevdagoat"
],
"HOD"
],
"tags": [ "Staff", [ "Developer", "https://gitlab.com/kevdagoat" ], "HOD" ],
"blurb": "His interests include Linux, programming, and goats of course. He enjoys building APIs, K8s, and geeking over video cards.",
"pic": "/static/assets/images/credits/kevdagoat.jpeg"
},
@ -183,11 +105,7 @@
"name": "Manu",
"title": null,
"loc": "Eastern, CA",
"tags": [
"Staff",
"Developer",
"Project Manager"
],
"tags": [ "Staff", "Developer", "Project Manager" ],
"blurb": "His interests include learning, Linux, and programming. He enjoys speaking French, doing 6 things at once, and testing software.",
"pic": "/static/assets/images/credits/manu.png"
},
@ -195,107 +113,28 @@
"name": "UltraBlack",
"title": null,
"loc": "Bavaria, DE",
"tags": [
"Staff",
null,
"Idea Manager"
],
"tags": [ "Staff", null, "Idea Manager" ],
"blurb": "Hi, my name is Tim, and I'm a huge fan of linux. I'm often gaming and occasionally coding.",
"pic": "/static/assets/images/credits/ultrablack.png"
},
{
"name": "MC Gaming",
"title": null,
"loc": "Central, UK",
"tags": [ "Staff", [ "Developer", "https://gitlab.com/MCgamin1738" ], null ],
"blurb": "His interests include learning, Linux, and programming. He loves pentesting apps and gaming.",
"pic": "/static/assets/images/credits/mcgaming.png"
}
]
},
"translations": {
"ptarrant": [
"Sarcasm",
"Wit"
],
"Lukas": [
"Finnish"
],
"Manu": [
"French"
],
"Silversthorn": [
"French"
],
"UltraBlack": [
"German"
]
"Data Unavailable": ["🌎🪣🔗👎😭"]
},
"patrons": [
{
"name": "Richard B",
"level": "Crafty Sustainer"
},
{
"name": "JOHN C",
"level": "Crafty Advocate"
},
{
"name": "iSilverfyre",
"level": "Crafty Sustainer"
},
{
"name": "Dean R",
"level": "Crafty Sustainer"
},
{
"name": "scott m",
"level": "Crafty Sustainer"
},
{
"name": "zedifus",
"level": "Crafty Sustainer"
},
{
"name": "Lino",
"level": "Crafty Sustainer"
},
{
"name": "PeterPorker3",
"level": "Crafty Supporter"
},
{
"name": "Ewari",
"level": "Crafty Supporter"
},
{
"name": "Thyodas",
"level": "Crafty Sustainer"
},
{
"name": "AngryPanda",
"level": "Crafty Sustainer"
},
{
"name": "Chris T",
"level": "Crafty Sustainer"
},
{
"name": "Adam B",
"level": "Crafty Sustainer"
},
{
"name": "Eric G",
"level": "Crafty Sustainer"
},
{
"name": "Chris B",
"level": "Crafty Sustainer"
},
{
"name": "Emmet d",
"level": "Crafty Sustainer"
},
{
"name": "Steven T",
"level": "Crafty Sustainer"
},
{
"name": "Row H",
"level": "Crafty Supporter"
"name": "Data Unavailable 🌎🪣🔗👎😭",
"level": ""
}
],
"lastUpdate": 1637281075498
"lastUpdate": 0
}

View File

@ -64,7 +64,7 @@
"handlers": ["tornado_access_file_handler"],
"propagate": false
},
"schedule": {
"apscheduler": {
"level": "INFO",
"handlers": ["schedule_file_handler"],
"propagate": false

View File

@ -2,5 +2,5 @@
"major": 4,
"minor": 0,
"sub": 0,
"meta": "alpha.3"
"meta": "alpha.3.5"
}

View File

@ -50,7 +50,11 @@
}
.mc-log-error{
color:#ff6258;
color:#af463f;
}
.mc-log-fatal{
color:#da0f00;
}
.mc-log-keyword{

View File

@ -10449,10 +10449,10 @@ pre {
padding-right: 80px; }
.sidebar > .nav:not(.sub-menu) > .nav-item:hover:not(.nav-profile):not(.hover-open) > .nav-link:not([aria-expanded="true"]):before {
border-color: #0b0b0b; }
.sidebar > .nav:not(.sub-menu) > .nav-item:hover:not(.nav-profile):not(.hover-open) > .nav-link:not([aria-expanded="true"]) .menu-title {
.sidebar > .nav:not(.sub-menu) > .nav-item:hover:not(.nav-profile):not(.hover-open) > .nav-link:not([aria-expanded="true"]) {
color: #0b0b0b; }
.sidebar > .nav:not(.sub-menu) > .nav-item:hover:not(.nav-profile):not(.hover-open) > .nav-link:not([aria-expanded="true"]) .menu-arrow::before {
color: #0b0b0b; }
.sidebar > .nav:not(.sub-menu) > .nav-item:hover:not(.nav-profile):not(.hover-open) > .nav-link:not([aria-expanded="true"]) .menu-arrow:before {
color: #b9c0d3; }
/* style for off-canvas menu*/
@media screen and (max-width: 991px) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 96c48.6 0 88 39.4 88 88s-39.4 88-88 88-88-39.4-88-88 39.4-88 88-88zm0 344c-58.7 0-111.3-26.6-146.5-68.2 18.8-35.4 55.6-59.8 98.5-59.8 2.4 0 4.8.4 7.1 1.1 13 4.2 26.6 6.9 40.9 6.9 14.3 0 28-2.7 40.9-6.9 2.3-.7 4.7-1.1 7.1-1.1 42.9 0 79.7 24.4 98.5 59.8C359.3 421.4 306.7 448 248 448z" style="fill: #b9c0d3;"/></svg>

After

Width:  |  Height:  |  Size: 594 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1,81 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 614 412">
<!--<rect
x="0"
y="0"
width="100%"
height="100%"
class="crafty-svg-bg"
fill="hsl(234, 24%, 17%)"
/>-->
<defs>
<mask id="mask-rect">
<rect width="100%" height="100%" fill="white"/>
<!--
307 - 200 / 2 = 207
206 - 200 / 2 = 106
-->
<rect
width="200"
height="200"
x="207"
y="106"
fill="black"
transform-origin="center"
transform="rotate(45)"
/>
<!-- <circle r="50" cx="307" cy="206" fill="black"/> -->
<circle r="55" cx="150" cy="206" fill="black"/>
</mask>
<mask id="mask-rect2">
<rect width="100%" height="100%" fill="white"/>
<!--
307 - 200 / 2 = 207
206 - 200 / 2 = 106
-->
<rect
width="200"
height="200"
x="207"
y="106"
fill="black"
transform-origin="center"
transform="rotate(45)"
/>
<!-- <circle r="50" cx="307" cy="206" fill="black"/> -->
<circle r="55" cx="464" cy="206" fill="black"/>
</mask>
</defs>
<!--
purple: hsl(266, 71%, 57%)
green: hsl(160, 59%, 45%)
blue: hsl(192, 99%, 45%)
-->
<!-- center of image: 307 206 -->
<!-- left circle offset: -157 0 -->
<!-- right circle offset: 157 0 -->
<circle r="120" cx="150" cy="206" fill="hsl(160, 59%, 45%)" mask="url(#mask-rect)" />
<circle r="120" cx="464" cy="206" fill="hsl(192, 99%, 45%)" mask="url(#mask-rect2)"/>
<!-- the purple thing in the middle -->
<g fill="hsl(266, 71%, 57%)">
<circle r="35" cx="150" cy="206"/>
<circle r="35" cx="464" cy="206"/>
<path
d="
M 307 206
m -157 -32
q 157 30 314 0
l 0 65
q -157 -30 -314 0
Z
"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -207,6 +207,10 @@ if ($('canvas').length) {
body.toggleClass('sidebar-hidden');
} else {
body.toggleClass('sidebar-icon-only');
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
if (vw >= 1200) {
localStorage.setItem('crafty-sidebar-expanded', !body.hasClass('sidebar-icon-only'));
}
}
});

View File

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<html lang="{{ data['lang_page'] }}">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
@ -13,7 +14,8 @@
<link rel="stylesheet" href="/static/assets/vendors/ti-icons/css/themify-icons.css">
<link rel="stylesheet" href="/static/assets/vendors/typicons/typicons.css">
<link rel="stylesheet" href="/static/assets/vendors/fontawesome5/css/all.css">
<link rel="stylesheet" type="text/css" href="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.css"/>
<link rel="stylesheet" type="text/css"
href="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.css" />
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css">
<link rel="stylesheet" href="/static/assets/css/crafty.css">
@ -21,14 +23,21 @@
<!-- Plugin css for this page -->
<link rel="stylesheet" href="/static/assets/vendors/jvectormap/jquery-jvectormap.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<!-- End Plugin css for this page -->
<!-- Layout styles -->
<link rel="stylesheet" href="/static/assets/css/dark/style.css">
<!-- End Layout styles -->
<link rel="shortcut icon" href="/static/assets/images/favicon.png" />
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/images/logo_small.svg">
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
<link rel="stylesheet" href="/static/assets/css/crafty.css">
<!-- Alpine.js - The modern jQuery alternative -->
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<!-- End Alpine.js -->
</head>
<body class="dark-theme">
@ -37,18 +46,28 @@
<nav class="navbar default-layout col-lg-12 col-12 p-0 fixed-top d-flex flex-row">
<div class="text-center navbar-brand-wrapper d-flex align-items-top justify-content-center">
<a class="navbar-brand brand-logo" href="/panel/dashboard">
<img src="/static/assets/images/logo_long.jpg" alt="logo" /> </a>
<img src="/static/assets/images/logo_long.svg" alt="logo" /> </a>
<a class="navbar-brand brand-logo-mini" href="/panel/dashboard">
<img src="/static/assets/images/logo_square.jpg" alt="logo" /> </a>
<img src="/static/assets/images/logo_small.svg" alt="logo" /> </a>
</div>
<div class="navbar-menu-wrapper d-flex align-items-center">
<style>
body:not(.sidebar-icon-only) .navbar-toggler .mdi-chevron-double-right {
display: none;
}
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-menu"></span>
<span class="mdi mdi-chevron-double-left"></span>
<span class="mdi mdi-chevron-double-right"></span>
</button>
{% include notify.html %}
<button class="navbar-toggler navbar-toggler-right d-lg-none align-self-center" type="button" data-toggle="offcanvas">
<button class="navbar-toggler navbar-toggler-right d-lg-none align-self-center" type="button"
data-toggle="offcanvas">
<span class="mdi mdi-menu"></span>
</button>
</div>
@ -84,6 +103,7 @@
top: 70px;
right: 0px;
}
.notification {
position: relative;
box-sizing: border-box;
@ -100,20 +120,24 @@
z-index: 999;
top: 0px;
}
.notification.active {
right: 0rem;
opacity: 1;
}
.notification.remove {
right: 0rem;
opacity: 0.1;
top: -2rem;
}
.notification p {
margin: 0px;
width: calc(160.8px - 16px);
z-index: inherit;
}
.notification span {
position: absolute;
right: 0.5rem;
@ -135,9 +159,9 @@
<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 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>
<script>
$.extend($.fn.dataTable.defaults, {
language: {% raw translate('datatables', 'i18n', data['lang']) %}
})
@ -224,10 +248,12 @@ if (webSocket) {
webSocket.on('send_start_error', function (start_error) {
var x = document.querySelector('.bootbox');
if (x) {
x.remove()}
x.remove()
}
var x = document.querySelector('.modal-backdrop');
if (x) {
x.remove()}
x.remove()
}
bootbox.alert({
message: start_error.error,
callback: function () {
@ -236,6 +262,83 @@ if (webSocket) {
})
});
}
if (webSocket) {
webSocket.on('send_logs_bootbox', function (server_id) {
var x = document.querySelector('.bootbox');
if (x) {
x.remove()
}
var x = document.querySelector('.modal-backdrop');
if (x) {
x.remove()
}
bootbox.alert({
title: "{{ translate('notify', 'downloadLogs', data['lang']) }}",
message: "{{ translate('notify', 'finishedPreparing', data['lang']) }}",
buttons: {
ok: {
label: 'Download',
className: 'btn-info'
}
},
callback: function () {
console.log("in callback")
location.href = "/panel/download_support_package";
}
});
});
}
if (webSocket) {
webSocket.on('send_eula_bootbox', function (server_id) {
var x = document.querySelector('.bootbox');
if (x) {
x.remove()
}
var x = document.querySelector('.modal-backdrop');
if (x) {
x.remove()
}
bootbox.confirm({
title: '{% raw translate("error", "eulaTitle", data['lang']) %}',
message: '{% raw translate("error", "eulaMsg", data['lang']) %} <br><br><a href="https://account.mojang.com/documents/minecraft_eula" target="_blank">EULA</a><br><br>{% raw translate("error", "eulaAgree", data['lang']) %}',
buttons: {
confirm: {
label: 'Yes',
className: 'btn-info'
},
cancel: {
label: 'No',
className: 'btn-secondary'
}
},
callback: function (result) {
if (result == true) {
eulaAgree(server_id.id)
}
else {
location.reload()
}
}
})
});
}
function eulaAgree(server_id, command) {
//< !--this getCookie function is in base.html-- >
var token = getCookie("_xsrf");
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/eula?id=' + server_id,
success: function (data) {
console.log("got response:");
console.log(data);
location.reload();
}
});
}
function warn(message) {
@ -276,7 +379,7 @@ if (webSocket) {
}
function notify(message) {
console.log(`notify(${message}})`);
console.log(`notify(${message})`);
var paragraphEl = document.createElement('p');
var closeEl = document.createElement('span');
@ -311,12 +414,34 @@ if (webSocket) {
}
webSocket.on('notification', notify);
document.addEventListener('alpine:init', () => {
console.log('%c[Crafty Controller] %cAlpine.js pre-initialization!', 'font-weight: 900; color: #800080;', 'color: #eee;');
})
document.addEventListener('alpine:initialized', () => {
console.log('%c[Crafty Controller] %cAlpine.js initialized!', 'font-weight: 900; color: #800080;', 'color: #eee;');
})
$(document).ready(function () {
console.log('%c[Crafty Controller] %cReady for JS!', 'font-weight: 900; color: #800080;', 'font-weight: 900; color: #eee;');
$('#support_logs').click(function () {
var dialog = bootbox.dialog({
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i>{{ translate('notify', 'preparingLogs', data['lang']) }}</p>',
closeButton: false
});
setTimeout(function () {
location.href = "/panel/support_logs";
}, 6000);
});
});
</script>
{% block js %}
<!-- Custom js for this page -->
<!-- End custom js for this page -->
<!-- Custom js for base.html page partial pages -->
<!-- End custom js for base.html page -->
{% end %}
</body>
</html>

View File

@ -19,7 +19,8 @@
<!-- Layout styles -->
<link rel="stylesheet" href="/static/assets/css/dark/style.css">
<!-- End Layout styles -->
<link rel="shortcut icon" href="/static/assets/images/favicon.png" />
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/images/logo_small.svg">
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
</head>
<body class="dark-theme">
<div class="container-scroller">

View File

@ -1,7 +1,6 @@
{% extends ../base.html %}
{% block meta %}
<!-- <meta http-equiv="refresh" content="60">-->
{% end %}
{% block title %}Crafty Controller - Blank Page{% end %}

View File

@ -2,7 +2,8 @@
<footer class="footer">
<div class="container-fluid ">
<span class="text-muted d-block text-center text-sm-left d-sm-inline-block">{{ translate('footer', 'copyright', data['lang']) }} © 2021 <a href="http://www.craftycontrol.com/" target="_blank">Crafty Controller</a>. {{ translate('footer', 'allRightsReserved', data['lang']) }}.</span>
<span class="text-muted d-block text-center text-sm-left d-sm-inline-block">{{ translate('footer', 'copyright', data['lang']) }} © 2021 - <span x-data x-text="new Date().getFullYear()"></span> <a href="https://craftycontrol.com/" target="_blank">Crafty Controller</a>. {{ translate('footer', 'allRightsReserved', data['lang']) }}.</span>
<span class="float-none float-sm-right d-block mt-1 mt-sm-0 text-center">{{ translate('footer', 'version', data['lang']) }}: {{ data['version_data'] }}
</span>
</div>

View File

@ -1,6 +1,68 @@
<!-- partial -->
<div class="container-fluid page-body-wrapper">
<!-- partial:partials/_sidebar.html -->
<style>
@media screen and (max-width: 991px) {
.sidebar-offcanvas {
-webkit-transition: all 0.25s cubic-bezier(.22,.61,.36,1);
transition: all 0.25s cubic-bezier(.22,.61,.36,1);
box-shadow: 0px 8px 17px 2px rgba(0,0,0,0.14) , 0px 3px 14px 2px rgba(0,0,0,0.12) , 0px 5px 5px -3px rgba(0,0,0,0.2);
}
}
</style>
<script>
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
function isExtraLargeBreakpoint() {
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
return vw >= 1200;
}
function isLargeBreakpoint() {
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
return vw >= 992;
}
$(document).ready(function() {
sidebarResizeHandler(null);
$(window).on(
'resize',
debounce(sidebarResizeHandler, 25, true)
);
});
function sidebarResizeHandler(e) {
/*
Viewport sizes: Extra large (vw >= 1200px), large (vw >= 992px), medium (vw >= 768px)
- A localstorage item is set to remember a user's preference between collapsed or expanded.
- For extra large viewports, the sidebar is the user's preference (by default expanded). When
expanded or collapsed manually, it doesn't overlap the page content and the preference
gets saved to a localstorage item.
- For large viewports, the sidebar is collapsed. When expanded manually, it doesn't overlap
the page content. The user's localstorage preference is not overridden during this state.
- For medium and below viewports, the sidebar is hidden behing a hamburger icon. When expanded, the sidebar
overlaps the page content. The user's localstorage preference is not overridden during this state.
More code in `app/frontend/static/assets/js/shared/misc.js` and `app/frontend/templates/base.html`
*/
if (isExtraLargeBreakpoint()) {
let value = localStorage.getItem('crafty-sidebar-expanded') !== 'false';
$('body').toggleClass('sidebar-icon-only', !value);
localStorage.setItem('crafty-sidebar-expanded', value);
} else if (isLargeBreakpoint()) {
$('body').toggleClass('sidebar-icon-only', true);
}
}
</script>
<nav class="sidebar sidebar-offcanvas" id="sidebar">
<ul class="nav">
@ -37,7 +99,7 @@
</li>
<li class="nav-item">
<a class="nav-link" href="https://gitlab.com/crafty-controller/crafty-web/-/wikis/home" target="_blank">
<a class="nav-link" href="https://wiki.craftycontrol.com" target="_blank">
<i class="fas fa-book"></i> &nbsp;
<span class="menu-title">{{ translate('sidebar', 'documentation', data['lang']) }}</span>
</a>

View File

@ -18,20 +18,25 @@
<li class="nav-item dropdown user-dropdown">
<a class="nav-link dropdown-toggle" id="UserDropdown" href="#" data-toggle="dropdown" aria-expanded="false">
<img class="img-xs rounded-circle" src="/static/assets/images/faces-clipart/pic-1.png" alt="Profile image"> </a>
<img class="img-xs rounded-circle profile-picture" src="{{ data['user_image'] }}" alt="Profile image"> </a>
<div class="dropdown-menu dropdown-menu-right navbar-dropdown" aria-labelledby="UserDropdown">
<div class="dropdown-header text-center">
<img class="img-md rounded-circle" src="/static/assets/images/faces-clipart/pic-1.png" alt="Profile image">
<img class="img-md rounded-circle profile-picture" src="{{ data['user_image'] }}" alt="Profile image">
<p class="mb-1 mt-3 font-weight-semibold">{{ data['user_data']['username'] }}</p>
<p class="font-weight-light text-muted mb-0">Roles: </p>
{% for r in data['user_role'] %}
<p class="font-weight-light text-muted mb-0">{{ r }}</p>
{% end %}
</div>
{% if "Super User" in data['user_role'] %}
<a class="dropdown-item" href="/panel/activity_logs"><i class="dropdown-item-icon mdi mdi-calendar-check-outline text-primary"></i> Activity</a>
{% if data.get('api_key') %}
<p class="mt-3">Logged in as API key "{{ data['api_key']['name'] }}"</p>
{% end %}
<a class="dropdown-item" href="/public/logout"><i class="dropdown-item-icon mdi mdi-power text-primary"></i>Sign Out</a>
<p class="font-weight-light text-muted mb-0">Email: {{ data['user_data']['email'] }}</p>
</div>
<a class="dropdown-item" id="support_logs" ><i class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs', data['lang']) }}</i></a>
{% if data['superuser'] %}
<a class="dropdown-item" href="/panel/activity_logs"><i class="dropdown-item-icon mdi mdi-calendar-check-outline text-primary"></i>{{ translate('notify', 'activityLog', data['lang']) }}</a>
{% end %}
<a class="dropdown-item" href="/public/logout"><i class="dropdown-item-icon mdi mdi-power text-primary"></i>{{ translate('notify', 'logout', data['lang']) }}</a>
</div>
</li>
</ul>

View File

@ -1,8 +1,6 @@
{% extends ../base.html %}
{% block meta %}
<!-- <meta http-equiv="refresh" content="60">-->
{% end %}
{% block title %}Crafty Controller - Activity Logs{% end %}
@ -21,14 +19,19 @@
</div>
<!-- Page Title Header Ends-->
<div class="row">
<div class="col-md-12 grid-margin">
<div class="col-md-12 col-lg-12 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="fas fa-history"></i> &nbsp;Audit Logs</h4>
<span class="too_small" title="{{ translate('dashboard', 'cannotSeeOnMobile', data['lang']) }}", data-content="{{ translate('dashboard', 'cannotSeeOnMobile2', data['lang']) }}", data-placement="top"></span>
</div>
<div class="card-body">
<table class="table" id="audit_table">
<div class="table-responsive">
<table class="table table-hover" id="audit_table" style="overflow: scroll;" width="100%">
<thead>
<tr>
<tr class="rounded">
<td>Username</td>
<td>Time</td>
<td>Action</td>
@ -41,7 +44,7 @@
<tr>
<td>{{ row['user_name'] }}</td>
<td>
{{ row['created'].strftime('%m-%d-%Y %H:%M:%S') }}
{{ row['created'].strftime('%Y-%m-%d %H:%M:%S') }}
</td>
<td>{{ row['log_msg'] }}</td>
<td>{{ row['server_id'] }}</td>
@ -50,11 +53,17 @@
{% end %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.popover-body{
color: white !important;;
}
</style>
@ -69,7 +78,34 @@
$( document ).ready(function() {
console.log('ready for JS!')
$('#audit_table').DataTable();
$('#audit_table').DataTable({
'order': [1, 'desc']
}
);
});
</script>
<script>
$(document).ready(function(){
$('[data-toggle="popover"]').popover();
if($(window).width() < 1000){
$('.too_small').popover("show");
}
});
$(window).ready(function(){
$('body').click(function(){
$('.too_small').popover("hide");
});
});
$(window).resize(function() {
// This will execute whenever the window is resized
if($(window).width() < 1000){
$('.too_small').popover("show");
}
else{
$('.too_small').popover("hide");
} // New width
});
</script>

View File

@ -1,7 +1,6 @@
{% extends ../base.html %}
{% block meta %}
<!-- <meta http-equiv="refresh" content="60">-->
{% end %}
{% block title %}Crafty Controller - Contribute{% end %}
@ -23,7 +22,7 @@
<div class="row">
<div class="col-md-3 grid-margin">
<div class="col-md-4 grid-margin">
<div class="card">
<div class="card-body">
@ -31,8 +30,8 @@
<div class="media-body">
<p class="card-text">
Patrons get access to several perks, such as behind the scenes videos, posts, and updates.
Patrons also get access to a Crafty Controller PE (Patreon Edition) with additional functions not in other versions of Crafty.
Patrons get access to several perks, such as behind the scenes videos, posts, and updates.<br>
Patrons also get early access to new software!
</p>
<br />
<div class="text-center">
@ -44,35 +43,7 @@
</div>
</div>
<div class="col-md-3 grid-margin">
<div class="card">
<div class="card-body">
<h4 class="card-title">One Time Support</h4>
<div class="media-body">
<p class="card-text">
Contribute via Paypal, you can contribute any amount, as often or as little as you want.
Please understand that while PayPal calls this a "Donation"; this is not a charitable donation and can not be claimed
as a charitable donation for tax purposes
</p>
<br />
<div class="text-center">
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
<input type="hidden" name="cmd" value="_donations" />
<input type="hidden" name="business" value="H2HNTLFZAJRXG" />
<input type="hidden" name="currency_code" value="USD" />
<input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif" border="0" name="submit" title="PayPal - The safer, easier way to pay online!" alt="Donate with PayPal button" />
<img alt="" border="0" src="https://www.paypal.com/en_US/i/scr/pixel.gif" width="1" height="1" />
</form>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 grid-margin">
<div class="col-md-4 grid-margin">
<div class="card">
<div class="card-body">
@ -93,6 +64,28 @@
</div>
</div>
<div class="col-md-4 grid-margin">
<div class="card">
<div class="card-body">
<h4 class="card-title">More Ways Coming Soon...</h4>
<div class="media-body">
<p class="card-text">
Thank you for your interest in contributing to Aracdia Technology's Crafty Controller.
We are always thinking of new ways for our community to contribute to this awesome project. <br><br> If you don't see
a contribution method that peaks your interest now please check back soon.
</p>
<br />
<div class="text-center">
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,10 +1,9 @@
{% extends ../base.html %}
{% block meta %}
<!-- <meta http-equiv="refresh" content="60">-->
{% end %}
{% block title %}Crafty Controller - Credits{% end %}
{% block title %}Crafty Controller - {{ translate('credits', 'pageTitle', data['lang']) }}{% end %}
{% block content %}
@ -14,8 +13,8 @@
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<h4 class="page-title">Credits
<small>Without these people, you wouldn't have Crafty</small>
<h4 class="page-title">{{ translate('credits', 'pageTitle', data['lang']) }}
<small>{{ translate('credits', 'pageDescription', data['lang']) }}</small>
</h4>
</div>
</div>
@ -23,25 +22,27 @@
</div>
<!-- Page Title Header Ends-->
<div class="row">
<div class="col-md-12 grid-margin">
<div class="card">
<div class="card-body">
<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;Development Team</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">
{% for person in data['staff']['development'] %}
<div class="col-md-6 mb-5">
<div class="col-lg-6 mb-5">
<div class="card rounded shadow-none">
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="col-md-4" style="max-width: fit-content;">
<div class="user-avatar mb-auto">
<img src="{{ person['pic'] }}"
alt="profile image" class="profile-img img-lg rounded-circle">
{% 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" class="profile-img img-lg rounded-circle">
</div>
{% end %}
</div>
<div class="wrapper">
@ -67,7 +68,7 @@
{% 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-inverse-success mr-2">{{ person['tags'][1] }}</span>
<span class="btn btn-sm btn-primary mr-2">{{ person['tags'][1] }}</span>
{% end %}
{% end %}
{% if person['tags'][2] %}
@ -90,27 +91,35 @@
</div>
</div>
</div>
{% end %}
</div>
</div> <!-- end of user row -->
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fa fa-book"></i> &nbsp;Support and Documentation Team</h4>
</div>
<br />
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fa fa-book"></i> &nbsp;{{ translate('credits', 'supportTeam', data['lang'])
}}</h4>
</div>
<div class="card-body">
<div class="row">
{% for person in data['staff']['support'] %}
<div class="col-md-6 mb-5">
<div class="col-lg-6 mb-5">
<div class="card rounded shadow-none">
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="col-md-4" style="max-width: fit-content;">
<div class="user-avatar mb-auto">
<img src="{{ person['pic'] }}"
alt="profile image" class="profile-img img-lg rounded-circle">
{% 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" class="profile-img img-lg rounded-circle">
</div>
{% end %}
</div>
<div class="wrapper">
@ -136,7 +145,7 @@
{% 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-inverse-success mr-2">{{ person['tags'][1] }}</span>
<span class="btn btn-sm btn-primary mr-2">{{ person['tags'][1] }}</span>
{% end %}
{% end %}
{% if person['tags'][2] %}
@ -150,7 +159,7 @@
<div class="wrapper align-items-start pt-3">
{% if person['title'] %}
<h5><strong>Crafty's {{ person['title'] }}</strong></h5>
<h5><strong>{{ person['title'] }}</strong></h5>
{% end %}
<p>{{ person['blurb'] }}</p>
</div>
@ -159,28 +168,33 @@
</div>
</div>
</div>
{% end %}
</div>
</div> <!-- end user row-->
</div>
<br />
<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;Retired Staff</h4>
<h4 class="card-title"><i class="far fa-server"></i> &nbsp;{{ translate('credits', 'retiredStaff', data['lang']) }}</h4>
</div>
<div class="row">
{% for person in data['staff']['retired'] %}
<div class="col-md-6 mb-5">
<div class="card rounded shadow-none">
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="user-avatar mb-auto">
<img src="{{ person['pic'] }}"
alt="profile image" class="profile-img img-lg rounded-circle">
{% 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">
@ -206,7 +220,7 @@
{% 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-inverse-success mr-2">{{ person['tags'][1] }}</span>
<span class="btn btn-sm btn-primary mr-2">{{ person['tags'][1] }}</span>
{% end %}
{% end %}
{% if person['tags'][2] %}
@ -220,7 +234,7 @@
<div class="wrapper align-items-start pt-3">
{% if person['title'] %}
<h5><strong>Crafty's {{ person['title'] }}</strong></h5>
<h5><strong>{{ person['title'] }}</strong></h5>
{% end %}
<p>{{ person['blurb'] }}</p>
</div>
@ -229,27 +243,31 @@
</div>
</div>
</div>
{% end %}
</div> <!-- end user row-->
</div>
</div>
</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">
<h4 class="card-title">Patreon Supporters</h4>
<p class="card-description"> A huge <code>thank you</code>&nbsp; to our Patreon supporters! | <span style="color: #9365B8">Last Update: {{ data["lastUpdate"] }}</span></p>
<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>Name</th>
<th>Level</th>
<th>{{ translate('credits', 'patreonName', data['lang']) }}</th>
<th>{{ translate('credits', 'patreonLevel', data['lang']) }}</th>
</tr>
</thead>
<tbody>
@ -264,7 +282,7 @@
{% 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">Other</span>
<span class="btn btn-sm btn-secondary mr-2">{{ translate('credits', 'patreonOther', data['lang']) }}</span>
{% end %}
</td>
</tr>
@ -278,24 +296,30 @@
<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">
<h4 class="card-title">Language Translation</h4>
<p class="card-description"> A huge <code>thank you</code>&nbsp; to our community who translate! </p>
<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>Name</th>
<th>Status</th>
<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>
<td class="pb-0">
<div class="row">
{% for language in data['translations'][person] %}
<span class="btn btn-sm btn-inverse-success mr-2">{{ language }}</span>
<span class="btn btn-sm btn-inverse-success mr-2" style="margin-bottom: 12px;">{{ language }}</span>
{% end %}
</div>
</td>
</tr>
{% end %}
@ -305,8 +329,6 @@
</div>
</div>
</div>
</div>
<!-- content-wrapper ends -->

View File

@ -1,7 +1,6 @@
{% extends ../base.html %}
{% block meta %}
<meta http-equiv="refresh" content="60">
{% end %}
{% block title %}Crafty Controller - {{ translate('dashboard', 'dashboard', data['lang']) }}{% end %}
@ -14,7 +13,10 @@
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<h4 class="page-title">{{ translate('dashboard', 'dashboard', data['lang']) }}</h4>
<h4 class="page-title">{{ translate('dashboard', 'dashboard', data['lang']) }}
{% if data['server_stats']['running'] != 0 %}
<span id="sync" style="margin-left: 5px;"><i class="fas fa-sync fa-spin"></i></span></h4>
{% end %}
</div>
</div>
@ -29,16 +31,21 @@
<div class="col-lg-4 col-md-6">
<div class="d-flex">
<div class="wrapper">
<h5 class="mb-1 font-weight-medium text-primary"> {{ translate('dashboard', 'host', data['lang']) }}</h5>
<h5 class="mb-1 font-weight-medium text-primary"> {{ translate('dashboard', 'host', data['lang']) }}
</h5>
<h3 class="mb-0 font-weight-semibold"> <i class="fas fa-chart-line"></i></h3>
</div>
<div class="wrapper my-auto ml-auto ml-lg-4">
<p id="cpu_data" class="mb-0 text-success" data-toggle="tooltip" data-placement="top" data-html="true" title="{% raw translate('dashboard', 'cpuCores', data['lang']) %}: {{ data.get('hosts_data').get('cpu_cores') }} <br /> {% raw translate('dashboard', 'cpuCurFreq', data['lang']) %}: {{ data.get('hosts_data').get('cpu_cur_freq') }} <br /> {% raw translate('dashboard', 'cpuMaxFreq', data['lang']) %}: {{ data.get('hosts_data').get('cpu_max_freq') }}" >
{{ translate('dashboard', 'cpuUsage', data['lang']) }}: <span id="cpu_usage">{{ data.get('hosts_data').get('cpu_usage') }}</span>
<p id="cpu_data" class="mb-0 text-success" data-toggle="tooltip" data-placement="top" data-html="true"
title="{% raw translate('dashboard', 'cpuCores', data['lang']) %}: {{ data.get('hosts_data').get('cpu_cores') }} <br /> {% raw translate('dashboard', 'cpuCurFreq', data['lang']) %}: {{ data.get('hosts_data').get('cpu_cur_freq') }} <br /> {% raw translate('dashboard', 'cpuMaxFreq', data['lang']) %}: {{ data.get('hosts_data').get('cpu_max_freq') }}">
{{ translate('dashboard', 'cpuUsage', data['lang']) }}: <span id="cpu_usage">{{
data.get('hosts_data').get('cpu_usage') }}</span>
</p>
<p id="mem_usage" class="mb-0 text-danger" data-toggle="tooltip" data-placement="top" title="{{ translate('dashboard', 'memUsage', data['lang']) }}: {{ data.get('hosts_data').get('mem_usage') }}" >
{{ translate('dashboard', 'memUsage', data['lang']) }}: <span id="mem_percent">{{ data.get('hosts_data').get('mem_percent') }}%</span>
<p id="mem_usage" class="mb-0 text-danger" data-toggle="tooltip" data-placement="top"
title="{{ translate('dashboard', 'memUsage', data['lang']) }}: {{ data.get('hosts_data').get('mem_usage') }}">
{{ translate('dashboard', 'memUsage', data['lang']) }}: <span id="mem_percent">{{
data.get('hosts_data').get('mem_percent') }}%</span>
</p>
</div>
</div>
@ -46,26 +53,28 @@
<div class="col-lg-4 col-md-6 mt-md-0 mt-4">
<div class="d-flex">
<div class="wrapper">
<h5 class="mb-1 font-weight-medium text-primary">{{ translate('dashboard', 'servers', data['lang']) }}</h5>
<h5 class="mb-1 font-weight-medium text-primary">{{ translate('dashboard', 'servers', data['lang']) }}
</h5>
<h3 class="mb-0 font-weight-semibold">{{ data['server_stats']['total'] }}</h3>
</div>
<div class="wrapper my-auto ml-auto ml-lg-4">
<p class="mb-0 text-success">{{ data['server_stats']['running'] }} {{ translate('dashboard', 'online', data['lang']).lower() }}</p>
<p class="mb-0 text-warning"> {{ data['server_stats']['stopped'] }} {{ translate('dashboard', 'offline', data['lang']).lower() }}</p>
<p class="mb-0 text-success">{{ data['server_stats']['running'] }} {{ translate('dashboard', 'online',
data['lang']).lower() }}</p>
<p class="mb-0 text-warning"> {{ data['server_stats']['stopped'] }} {{ translate('dashboard',
'offline', data['lang']).lower() }}</p>
</div>
</div>
</div>
<div class="col-lg-4 col-md-6 mt-md-0 mt-4">
<div class="d-flex">
<div class="wrapper">
<h5 class="mb-1 font-weight-medium text-primary">{{ translate('dashboard', 'players', data['lang']) }}</h5>
<h3 class="mb-0 font-weight-semibold">{{ data['num_players'] }}</h3>
<h5 class="mb-1 font-weight-medium text-primary">{{ translate('dashboard', 'players', data['lang']) }}
</h5>
<h3 class="mb-0 font-weight-semibold" id="total_players">{{ data['num_players'] }}</h3>
</div>
<div class="wrapper my-auto ml-auto ml-lg-4">
<p class="mb-0 text-success">35 {{ translate('dashboard', 'max', data['lang']) }}</p>
<p class="mb-0 text-warning">10 {{ translate('dashboard', 'avg', data['lang']) }}</p>
<p class="mb-0 text-warning"><span id="max_players">0</span> {{ translate('dashboard', 'max', data['lang']) }}</p>
</div>
</div>
</div>
@ -79,11 +88,15 @@
<div class="col-md-12 col-lg-12 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="fas fa-server"></i> &nbsp;{{ translate('dashboard', 'allServers', data['lang']) }}</h4>
<h4 class="card-title"><i class="fas fa-server"></i> &nbsp;{{ translate('dashboard', 'allServers',
data['lang']) }}</h4>
{% if len(data['servers']) > 0 %}
<span class="too_small" title="{{ translate('dashboard', 'cannotSeeOnMobile', data['lang']) }}", data-content="{{ translate('dashboard', 'cannotSeeOnMobile2', data['lang']) }}", data-placement="top"></span>
<span class="too_small" title="{{ translate('dashboard', 'cannotSeeOnMobile', data['lang']) }}" ,
data-content="{{ translate('dashboard', 'cannotSeeOnMobile2', data['lang']) }}" ,
data-placement="top"></span>
{% end %}
<div><a class="nav-link" href="/server/step1"><i class="fas fa-plus-circle"></i> &nbsp; {{ translate('dashboard', 'newServer', data['lang']) }}</a></div>
<div><a class="nav-link" href="/server/step1"><i class="fas fa-plus-circle"></i> &nbsp; {{
translate('dashboard', 'newServer', data['lang']) }}</a></div>
</div>
<div class="card-body">
@ -92,7 +105,8 @@
<div style="text-align: center; color: grey;">
<h1>{{ translate('dashboard', 'welcome', data['lang']) }}</h1>
<br>
<h7>{{ translate('dashboard', 'no-servers', data['lang']) }} {{ translate('dashboard', 'newServer', data['lang']) }}.</h7>
<h7>{{ translate('dashboard', 'no-servers', data['lang']) }} {{ translate('dashboard', 'newServer',
data['lang']) }}.</h7>
</div>
{% end %}
@ -104,17 +118,17 @@
<th>{{ translate('dashboard', 'actions', data['lang']) }}</th>
<th>{{ translate('dashboard', 'cpuUsage', data['lang']) }}</th>
<th>{{ translate('dashboard', 'memUsage', data['lang']) }}</th>
<th>{{ translate('dashboard', 'world', data['lang']) }}</th>
<th>{{ translate('dashboard', 'size', data['lang']) }}</th>
<th>{{ translate('dashboard', 'players', data['lang']) }}</th>
<th>{{ translate('dashboard', 'status', data['lang']) }}</th>
</tr>
</thead>
<tbody>
{% for server in data['servers'] %}
<tr>
<td>
<tr id="{{server['server_data']['server_id']}}" draggable="true" ondragstart="start()" ondragover="dragover()" ondragend="dragend()">
<td draggable="false">
<i class="fas fa-server"></i>
<a href="/panel/server_detail?id={{server['server_data']['server_id']}}">
<a draggable="false" href="/panel/server_detail?id={{server['server_data']['server_id']}}">
{{ server['server_data']['server_name'] }}
</a>
</td>
@ -122,23 +136,53 @@
<td id="controls{{server['server_data']['server_id']}}" class="actions_serverlist">
{% if server['user_command_permission'] %}
{% if server['stats']['running'] %}
<a class="stop_button" data-id="{{server['server_data']['server_id']}}" data-toggle="tooltip" title={{ translate('dashboard', 'stop', data['lang']) }}> <i class="fas fa-stop"></i></a> &nbsp;
<a class="restart_button" data-id="{{server['server_data']['server_id']}}" data-toggle="tooltip" title={{ translate('dashboard', 'restart', data['lang']) }}> <i class="fas fa-sync"></i></a> &nbsp;
<a class="kill_button" data-id="{{server['server_data']['server_id']}}" class="kill_button" data-toggle="tooltip" title={{ translate('dashboard', 'kill', data['lang']) }}> <i class="fas fa-skull"></i></a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="stop_button"
data-toggle="tooltip" title="{{ translate('dashboard', 'stop' , data['lang']) }}">
<i class="fas fa-stop"></i>
</a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="restart_button"
data-toggle="tooltip" title="{{ translate('dashboard', 'restart' , data['lang']) }}">
<i class="fas fa-sync"></i>
</a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="kill_button"
data-toggle="tooltip" title="{{ translate('dashboard', 'kill' , data['lang']) }}">
<i class="fas fa-skull"></i>
</a> &nbsp;
{% elif server['stats']['updating']%}
<a data-id="{{server['server_data']['server_id']}}" class="">{{ translate('serverTerm', 'updating', data['lang']) }}</i></a>
<!-- WHAT HAPPENED HERE -->
<a data-id="{{server['server_data']['server_id']}}" class="">{{ translate('serverTerm', 'updating',
data['lang']) }}</i></a>
{% elif server['stats']['waiting_start']%}
<a data-id="{{server['server_data']['server_id']}}" class="" title={{ translate('dashboard', 'delay-explained', data['lang'])}}>{{ translate('dashboard', 'starting', data['lang']) }}</i></a>
<!-- WHAT HAPPENED HERE -->
<a data-id="{{server['server_data']['server_id']}}" class="" title="{{
translate('dashboard', 'delay-explained' , data['lang'])}}">{{ translate('dashboard', 'starting',
data['lang']) }}</i></a>
{% elif server['stats']['downloading']%}
<a data-id="{{server['server_data']['server_id']}}" class=""><i class="fa fa-spinner fa-spin"></i> {{ translate('serverTerm', 'downloading',
data['lang']) }}</a>
{% else %}
<a data-id="{{server['server_data']['server_id']}}" class="play_button"><i class="fas fa-play" data-toggle="tooltip" title={{ translate('dashboard', 'start', data['lang']) }}></i></a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="clone_button"> <i class="fas fa-clone" data-toggle="tooltip" title={{ translate('dashboard', 'clone', data['lang']) }}></i></a>&nbsp;
<a class="kill_button" data-id="{{server['server_data']['server_id']}}" class="kill_button" data-toggle="tooltip" title={{ translate('dashboard', 'kill', data['lang']) }}> <i class="fas fa-skull"></i></a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="play_button"
data-toggle="tooltip" title="{{ translate('dashboard', 'start' , data['lang']) }}">
<i class="fas fa-play"></i>
</a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="clone_button"
data-toggle="tooltip" title="{{ translate('dashboard', 'clone' , data['lang']) }}">
<i class="fas fa-clone"></i>
</a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="kill_button"
data-toggle="tooltip" title="{{ translate('dashboard', 'kill' , data['lang']) }}">
<i class="fas fa-skull"></i>
</a> &nbsp;
{% end %}
{% end %}
</td>
<td>
<div class="progress mb-1" data-toggle="tooltip" data-placement="top" title="{{server['stats']['cpu']}}">
<td id="server_cpu_{{server['server_data']['server_id']}}">
<div class="progress mb-1" data-toggle="tooltip" data-placement="top"
title="{{server['stats']['cpu']}}">
<div class="progress-bar
{% if server['stats']['cpu'] <= 33 %}
bg-success
@ -147,13 +191,15 @@
{% else %}
bg-danger
{% end %}
" role="progressbar" style="width: {{server['stats']['cpu']}}%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
" role="progressbar" style="width: {{server['stats']['cpu']}}%" aria-valuenow="0"
aria-valuemin="0" aria-valuemax="100"></div>
</div>
{{server['stats']['cpu']}}%
</td>
<td>
<div class="progress mb-1" data-toggle="tooltip" data-placement="top" title="{{server['stats']['mem']}}">
<td id="server_mem_{{server['server_data']['server_id']}}">
<div class="progress mb-1" data-toggle="tooltip" data-placement="top"
title="{{server['stats']['mem']}}">
<div class="progress-bar
{% if server['stats']['mem_percent'] <= 33 %}
bg-success
@ -162,7 +208,8 @@
{% else %}
bg-danger
{% end %}
" role="progressbar" style="width: {{server['stats']['mem_percent']}}%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
" role="progressbar" style="width: {{server['stats']['mem_percent']}}%" aria-valuenow="0"
aria-valuemin="0" aria-valuemax="100"></div>
</div>
{{server['stats']['mem_percent']}}% -
@ -172,15 +219,16 @@
{{server['stats']['mem']}}
{% end %}
</td>
<td>
{{ server['stats']['world_name'] }} : {{ server['stats']['world_size'] }}
<td id="server_world_{{server['server_data']['server_id']}}">
{{ server['stats']['world_size'] }}
</td>
<td>
<td id="server_desc_{{server['server_data']['server_id']}}">
{% if server['stats']['int_ping_results'] %}
{{ server['stats']['online'] }} / {{ server['stats']['max'] }} {{ translate('dashboard', 'max', data['lang']) }}<br />
{{ server['stats']['online'] }} / {{ server['stats']['max'] }} {{ translate('dashboard', 'max',
data['lang']) }} <br />
{% if server['stats']['desc'] != 'False' %}
<span id="input_motd_{{ server['stats']['server_id']['server_id'] }}" class="input_motd">{{ server['stats']['desc'] }}</span> <br />
{{ server['stats']['desc'] }} <br />
{% end %}
{% if server['stats']['version'] != 'False' %}
@ -189,13 +237,19 @@
{% end %}
</td>
<td>
<td id="server_running_status_{{server['server_data']['server_id']}}">
{% if server['stats']['running'] %}
<i class="fas fa-thumbs-up"></i> <span class="text-success">{{ translate('dashboard', 'online', data['lang']) }}</span>
<span class="text-success"><i class="fas fa-signal"></i> {{ translate('dashboard', 'online',
data['lang']) }}</span>
{% elif server['stats']['crashed'] %}
<span class="text-danger"><i class="fas fa-exclamation-triangle"></i> {{ translate('dashboard', 'crashed',
data['lang']) }}</span>
{% else %}
<i class="fas fa-thumbs-down"></i> <span class="text-danger">{{ translate('dashboard', 'offline', data['lang']) }}</span>
<span class="text-warning"><i class="fas fa-ban"></i> {{ translate('dashboard', 'offline',
data['lang']) }}</span>
{% end %}
</td>
<span class="server-player-totals" id="server_players_{{server['server_data']['server_id']}}" data-players="{{ server['stats']['online']}}" data-max="{{ server['stats']['max'] }}"></span>
</tr>
{% end %}
@ -213,7 +267,8 @@
<!-- content-wrapper ends -->
<style>
.popover-body {
color: white !important;;
color: white !important;
;
}
</style>
@ -221,18 +276,22 @@
{% end %}
{% block js %}
<script src="/static/assets/js/motd.js"></script>
<script>
function display_motd() {
var all_motds = Array.from(document.getElementsByClassName('input_motd'));
for (element of all_motds) {
initParser(element.id, element.id);
};
}
$(document).ready(function () {
$('[data-toggle="popover"]').popover();
if ($(window).width() < 1000) {
$('.too_small').popover("show");
}
var all_motds = Array.from(document.getElementsByClassName('input_motd'));
for (element of all_motds) {
initParser(element.id, element.id);
};
});
$(window).ready(function () {
$('body').click(function () {
@ -262,12 +321,11 @@ function send_command (server_id, command){
success: function (data) {
console.log("got response:");
console.log(data);
setTimeout(function(){
/*setTimeout(function () {
if (command != 'start_server') {
location.reload();
}
}, 10000);
}, 10000);*/
}
});
}
@ -283,14 +341,133 @@ function send_kill (server_id){
success: function (data) {
console.log("got response:");
console.log(data);
setTimeout(function(){
/*setTimeout(function () {
location.reload();
}, 10000);
}, 10000);*/
}
});
}
function update_one_server_status(server) {
server_cpu = document.getElementById('server_cpu_' + server.id);
server_mem = document.getElementById('server_mem_' + server.id);
server_world = document.getElementById('server_world_' + server.id);
server_desc = document.getElementById('server_desc_' + server.id);
server_online_status = document.getElementById('server_running_status_' + server.id);
server_players = document.getElementById('server_players_' + server.id);
total_players = document.getElementById('total_players');
console.log("Received Data : " + server.id + ": " + server);
/* TODO Update each element */
/* Update CPU */
cpu_status = "";
if (server.cpu <= 33)
{
cpu_status = "bg-success";
}
else if (server.cpu > 33 && server.cpu <= 66)
{
cpu_status = "bg-warning";
}
else
{
cpu_status = "bg-danger";
}
server_cpu.innerHTML = `<div class="progress mb-1" data-toggle="tooltip" data-placement="top" title="`+ server.cpu +`"><div class="progress-bar `+ cpu_status + `" role="progressbar" style="width: `+ server.cpu + `%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div></div>`+ server.cpu +`%`;
/* Update Memory */
mem_status = "";
total_mem = "";
if (server.mem_percent <= 33)
{
mem_status = "bg-success";
} else if (server.mem_percent > 33 && server.mem_percent <= 66)
{
mem_status = "bg-warning";
}
else
{
mem_status = "bg-danger";
}
if (server.mem == 0)
{
total_mem = "0 MB";
}
else
{
total_mem = server.mem;
}
server_mem.innerHTML = `<div class="progress mb-1" data-toggle="tooltip" data-placement="top" title="`+ server_mem +`"><div class="progress-bar `+ mem_status + `" role="progressbar" style="width: `+ server.mem_percent + `%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div></div>`+ server.mem_percent +`% - ` + total_mem;
/* Update World Infos */
server_world.innerHTML = server.world_size
/* Update Server Infos */
if (server.int_ping_results) {
/* Update Players */
if (server.players) {
server_desc.innerHTML = server.online + ` / ` + server.max + ` {{ translate('dashboard', 'max', data['lang']) }}<br />`
server_players.setAttribute('data-players', server.online);
server_players.setAttribute('data-max', server.max);
let servers = document.getElementsByClassName("server-player-totals");
let all_total_players = 0;
let all_total_max_players = 0;
for(var i = 0; i < servers.length; i++){
try{
all_total_players += parseInt(servers[i].getAttribute('data-players'));
all_total_max_players += parseInt(servers[i].getAttribute('data-max'));
}catch{
console.log("Player totals are not of type int");
}
}
total_players.innerHTML = all_total_players;
document.getElementById('max_players').innerHTML = all_total_max_players;
document.getElementById('sync').innerHTML = '';
server_infos = "";
server_infos = server.online + " / " + server.max + " {{ translate('dashboard', 'max', data['lang']) }}<br />"
}
/* Update Motd */
var motd = "";
if (server.desc) {
motd = `<span id="input_motd_` + server.id + `" class="input_motd">` + server.desc + `</span>`;
server_infos = server_infos + motd + "<br />";
}
/* Version */
if (server.version) {
server_infos = server_infos + server.version
}
server_desc.innerHTML = server_infos;
}
/* Update Online Status */
var online_status = "";
if (server.running) {
online_status = `<span class="text-success"><i class="fas fa-signal"></i> {{ translate('dashboard', 'online', data['lang'])}}</span>`;
}
else {
if (server.crashed){
online_status = `<span class="text-danger"><i class="fas fa-exclamation-triangle"></i> {{ translate('dashboard', 'crashed', data['lang'])}}</span>`
}else{
online_status = `<span class="text-warning"><i class="fas fa-ban"></i> {{ translate('dashboard', 'offline', data['lang'])}}</span>`;
}
}
server_online_status.innerHTML = online_status;
}
function update_servers_status(data) {
update_one_server_status(data[0]);
display_motd();
}
$(document).ready(function () {
console.log('ready for JS!')
@ -299,8 +476,8 @@ $( document ).ready(function() {
send_command(server_id, 'start_server');
bootbox.alert({
backdrop: true,
title: '{% raw translate("dashboard", "sendingCommand", data['lang']) %}',
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("dashboard", "bePatientStart", data['lang']) %} </div>'
title: '{% raw translate("dashboard", "sendingCommand", data["lang"]) %}',
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("dashboard", "bePatientStart", data["lang"]) %} </div>'
});
});
@ -310,8 +487,8 @@ $( document ).ready(function() {
send_command(server_id, 'stop_server');
bootbox.alert({
backdrop: true,
title: '{% raw translate("dashboard", "sendingCommand", data['lang']) %}',
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("dashboard", "bePatientStop", data['lang']) %} </div>'
title: '{% raw translate("dashboard", "sendingCommand", data["lang"]) %}',
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("dashboard", "bePatientStop", data["lang"]) %} </div>'
});
});
@ -320,8 +497,8 @@ $( document ).ready(function() {
send_command(server_id, 'restart_server');
bootbox.alert({
backdrop: true,
title: '{% raw translate("dashboard", "sendingCommand", data['lang']) %}',
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("dashboard", "bePatientRestart", data['lang']) %} </div>'
title: '{% raw translate("dashboard", "sendingCommand", data["lang"]) %}',
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("dashboard", "bePatientRestart", data["lang"]) %} </div>'
});
});
$(".kill_button").click(function () {
@ -330,11 +507,11 @@ $( document ).ready(function() {
message: "This will kill the server process and all it's subprocesses. Killing a process can potentially corrupt files. Only do this in extreme circumstances. Are you sure you would like to continue?",
buttons: {
confirm: {
label: '{% raw translate("dashboard", "kill", data['lang']) %}',
label: '{% raw translate("dashboard", "kill", data["lang"]) %}',
className: 'btn-danger'
},
cancel: {
label: '{% raw translate("panelConfig", "cancel", data['lang']) %}',
label: '{% raw translate("panelConfig", "cancel", data["lang"]) %}',
className: 'btn-secondary'
}
},
@ -342,7 +519,7 @@ $( document ).ready(function() {
if (result) {
send_kill(server_id);
var dialog = bootbox.dialog({
title: '{% raw translate("dashboard", "killing", data['lang']) %}',
title: '{% raw translate("dashboard", "killing", data["lang"]) %}',
message: '<p><i class="fa fa-spin fa-spinner"></i> Loading...</p>'
});
@ -361,6 +538,7 @@ dialog.init(function(){
mem_usage = document.getElementById('mem_usage');
mem_percent = document.getElementById('mem_percent');
webSocket.on('update_host_stats', function (hostStats) {
var cpuDataTitle = `{% raw translate('dashboard', 'cpuCores', data['lang']) %}: ${hostStats.cpu_cores} <br /> {% raw translate("dashboard", "cpuCurFreq", data['lang']) %}: ${hostStats.cpu_cur_freq} <br /> {% raw translate("dashboard", "cpuMaxFreq", data['lang']) %}: ${hostStats.cpu_max_freq}`;
cpu_data.setAttribute('data-original-title', cpuDataTitle);
@ -371,20 +549,20 @@ dialog.init(function(){
}
if (webSocket) {
webSocket.on('send_start_reload', function (start_error) {
webSocket.on('send_start_reload', function () {
location.reload()
});
}
if (webSocket) {
webSocket.on('update_button_status', function (updateButton) {
var id = 'controls';
var dataId = updateButton.server_id;
var string = updateButton.string
var id = id.concat(updateButton.server_id);
if (updateButton.isUpdating){
console.log(updateButton.isUpdating)
document.getElementById(id).innerHTML = string;
let serverId = updateButton.server_id;
let message = updateButton.string;
let updating = updateButton.isUpdating;
let id = 'controls' + serverId;
if (updating) {
console.log(updating)
document.getElementById(id).innerHTML = message;
}
else {
window.location.reload()
@ -392,17 +570,71 @@ dialog.init(function(){
});
}
if (webSocket) {
webSocket.on('update_server_status', update_servers_status);
}
$(".clone_button").click(function () {
server_id = $(this).attr("data-id");
send_command(server_id, 'clone_server');
bootbox.alert({
backdrop: true,
title: '{% raw translate("dashboard", "sendingCommand", data['lang']) %}',
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("dashboard", "bePatientClone", data['lang']) %} </div>'
title: '{% raw translate("dashboard", "sendingCommand", data["lang"]) %}',
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("dashboard", "bePatientClone", data["lang"]) %} </div>'
});
});
});
</script>
<script>
var row;
function start(){
row = event.target;
}
function dragover(){
var e = event;
e.preventDefault();
let children= Array.from(e.target.parentNode.parentNode.children);
if(children.indexOf(e.target.parentNode)>children.indexOf(row))
e.target.parentNode.after(row);
else
e.target.parentNode.before(row);
}
function dragend(){
var id_string = '';
const table = document.querySelector("table");
for (const row of table.rows) {
if (row.getAttribute('id') != null){
if (id_string != ''){
id_string += ',' + String(row.getAttribute('id'));
}else{
id_string +=String(row.getAttribute('id'));
}
}
}
console.log(id_string)
sendOrder(id_string)
}
function sendOrder(id_string) {
var token = getCookie("_xsrf")
$.ajax({
type: "POST",
headers: {'X-XSRFToken': token},
url: '/ajax/send_order?order='+id_string,
data: {
order: id_string,
},
success: function(data){
console.log("got response:");
console.log(data);
},
});
}
</script>
{% end %}

View File

@ -17,7 +17,8 @@
<!-- Layout styles -->
<link rel="stylesheet" href="/static/assets/css/dark/style.css">
<!-- End Layout styles -->
<link rel="shortcut icon" href="/static/assets/images/favicon.png" />
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/images/logo_small.svg">
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
</head>
<body class="dark-theme">
<div class="container-scroller">
@ -28,7 +29,7 @@
<div class="auto-form-wrapper">
<div class="text-center">
<img src="/static/assets/images/logo_long.jpg"><br /><br />
<img src="/static/assets/images/logo_long.svg"><br /><br />
<div class="col-sm-12 grid-margin stretch-card">
<div class="card card-statistics social-card google-card card-colored">
<div class="card-body">

View File

@ -1,10 +1,9 @@
{% extends ../base.html %}
{% block meta %}
<!-- <meta http-equiv="refresh" content="60">-->
{% end %}
{% block title %}Crafty Controller - Panel Config{% end %}
{% block title %}Crafty Controller - {{ translate('panelConfig', 'pageTitle', data['lang']) }}{% end %}
{% block content %}
@ -14,7 +13,8 @@
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<h4 class="page-title">Panel Config</h4>
<!-- TODO: Translate the following -->
<h4 class="page-title">{{ translate('panelConfig', 'pageTitle', data['lang']) }}</h4>
</div>
</div>
@ -30,22 +30,23 @@
<div class="col-md-12 col-lg-12 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="fas fa-users"></i> Users</h4>
<h4 class="card-title"><i class="fas fa-users"></i> {{ translate('panelConfig', 'users', data['lang']) }}</h4>
<span class="too_small" title="{{ translate('dashboard', 'cannotSee', data['lang']) }}", data-content="{{ translate('dashboard', 'cannotSeeOnMobile2', data['lang']) }}", data-placement="top"></span>
<div><a class="nav-link" href="/panel/add_user"><i class="fas fa-plus-circle"></i> &nbsp; Add New User</a></div>
<!-- TODO: Translate the following -->
<div><a class="nav-link" href="/panel/add_user"><i class="fas fa-plus-circle"></i> &nbsp; {{ translate('panelConfig', 'newUser', data['lang']) }}</a></div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<!-- TODO: Translate the following -->
<tr class="rounded">
<th>User</th>
<th>Enabled</th>
<th>API Token</th>
<th>Allowed Servers</th>
<th>Assigned Roles</th>
<th>Edit</th>
<th>{{ translate('panelConfig', 'user', data['lang']) }}</th>
<th>{{ translate('panelConfig', 'enabled', data['lang']) }}</th>
<th>{{ translate('panelConfig', 'allowedServers', data['lang']) }}</th>
<th>{{ translate('panelConfig', 'assignedRoles', data['lang']) }}</th>
<th>{{ translate('panelConfig', 'edit', data['lang']) }}</th>
</tr>
</thead>
<tbody>
@ -64,9 +65,6 @@
{% end %}
</td>
<td>
<button data-toggle="tooltip" title="Show API Key" data-id="{{ user.api_token }}" type="button" class="btn btn-info show_button">Show</button>
</td>
<td id="server_list_{{user.user_id}}">
<ul id="{{user.user_id}}">
{% for item in data['auth-servers'][user.user_id] %}
@ -95,19 +93,20 @@
<div class="col-md-12 col-lg-12 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="fas fa-user-tag"></i> Roles</h4>
<h4 class="card-title"><i class="fas fa-user-tag"></i> {{ translate('panelConfig', 'roles', data['lang']) }}</h4>
<span class="too_small2" title="{{ translate('dashboard', 'cannotSee', data['lang']) }}", data-content="{{ translate('dashboard', 'cannotSeeOnMobile2', data['lang']) }}", data-placement="top"></span>
<div><a class="nav-link" href="/panel/add_role"><i class="fas fa-plus-circle"></i> &nbsp; Add New Role</a></div>
<div><a class="nav-link" href="/panel/add_role"><i class="fas fa-plus-circle"></i> &nbsp; {{ translate('panelConfig', 'newRole', data['lang']) }}</a></div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<!-- TODO: Translate the following -->
<tr class="rounded">
<th>Role</th>
<th>Allowed Servers</th>
<th>Role Users</th>
<th>Edit</th>
<th>{{ translate('panelConfig', 'role', data['lang']) }}</th>
<th>{{ translate('panelConfig', 'allowedServers', data['lang']) }}</th>
<th>{{ translate('panelConfig', 'roleUsers', data['lang']) }}</th>
<th>{{ translate('panelConfig', 'edit', data['lang']) }}</th>
</tr>
</thead>
<tbody>
@ -142,6 +141,21 @@
</div>
</div>
</div>
{% if data['superuser'] %}
<div class="row">
<div class="col-md-12 col-lg-12 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="fas fa-user-tag"></i> {{ translate('panelConfig', 'adminControls', data['lang']) }}</h4>
</div>
<div class="card-body">
<button type="button" class="btn btn-outline-danger clear-comm">{{ translate('panelConfig', 'clearComms', data['lang']) }}</button>
</div>
</div>
</div>
</div>
{% end %}
</div>
</div>
</div>
@ -207,6 +221,17 @@ $( document ).ready(function() {
message: api_key,
});
});
$('.clear-comm').click(function () {
var token = getCookie("_xsrf")
$.ajax({
type: "POST",
headers: {'X-XSRFToken': token},
url: '/ajax/clear_comm',
success: function (data) {
},
});
})
</script>
{% end %}

View File

@ -1,10 +1,9 @@
{% extends ../base.html %}
{% block meta %}
<!-- <meta http-equiv="refresh" content="60">-->
{% end %}
{% block title %}Crafty Controller - Edit Role{% end %}
{% block title %}Crafty Controller - {{ translate('rolesConfig', 'pageTitle', data['lang']) }}{% end %}
{% block content %}
@ -16,13 +15,13 @@
<div class="page-header">
{% if data['new_role'] %}
<h4 class="page-title">
New Role
{{ translate('rolesConfig', 'pageTitleNew', data['lang']) }}
<br />
<small>RID: N/A</small>
</h4>
{% else %}
<h4 class="page-title">
Edit Role - {{ data['role']['role_name'] }}
{{ translate('rolesConfig', 'pageTitle', data['lang']) }} - {{ data['role']['role_name'] }}
<br />
<small>RID: {{ data['role']['role_id'] }}</small>
</h4>
@ -41,7 +40,7 @@
<ul class="nav nav-tabs col-md-12 tab-simple-styled " role="tablist">
<li class="nav-item">
<a class="nav-link active" href="/panel/edit_role?id={{ data['role']['role_id'] }}&subpage=config" role="tab" aria-selected="true">
<i class="fas fa-cogs"></i>Config</a>
<i class="fas fa-cogs"></i>{{ translate('rolesConfig', 'config', data['lang']) }}</a>
</li>
<!-- <li class="nav-item">
<a class="nav-link" href="/panel/edit_role?id={{ data['role']['role_id'] }}&subpage=other" role="tab" aria-selected="false">
@ -61,11 +60,11 @@
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fas fa-user-tag"></i> Role Settings</h4>
<h4 class="card-title"><i class="fas fa-user-tag"></i> {{ translate('rolesConfig', 'roleTitle', data['lang']) }}</h4>
</div>
<div class="card-body">
<div class="form-group">
<label for="role_name">Role Name <small class="text-muted ml-1"> - What you wish to call this role</small> </label>
<label for="role_name">{{ translate('rolesConfig', 'roleName', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('rolesConfig', 'roleDesc', data['lang']) }}</small> </label>
<input type="text" class="form-control" name="role_name" id="role_name" value="{{ data['role']['role_name'] }}" placeholder="Role Name" >
</div>
</div>
@ -73,7 +72,7 @@
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fas fa-server"></i> Allowed Servers <small class="text-muted ml-1"> - servers this role is allowed to access </small> </h4>
<h4 class="card-title"><i class="fas fa-server"></i> {{ translate('rolesConfig', 'roleServers', data['lang']) }} <small class="text-muted ml-1"> {{ translate('rolesConfig', 'serversDesc', data['lang']) }}</small> </h4>
</div>
<div class="card-body">
<div class="form-group">
@ -81,8 +80,8 @@
<table class="table table-hover">
<thead>
<tr class="rounded">
<th>Server Name</th>
<th>Access?</th>
<th>{{ translate('rolesConfig', 'serverName', data['lang']) }}</th>
<th>{{ translate('rolesConfig', 'serverAccess', data['lang']) }}</th>
</tr>
</thead>
<tbody>
@ -108,7 +107,7 @@
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fas fa-user-lock"></i> Roles Permissions <small class="text-muted ml-1"> - permissions this role has on this/these servers </small></h4>
<h4 class="card-title"><i class="fas fa-user-lock"></i> {{ translate('rolesConfig', 'rolePerms', data['lang']) }}<small class="text-muted ml-1"> - {{ translate('rolesConfig', 'permsServer', data['lang']) }} </small></h4>
</div>
<div class="card-body">
<div class="form-group">
@ -116,8 +115,8 @@
<table class="table table-hover">
<thead>
<tr class="rounded">
<th>Permission Name</th>
<th>Authorized ?</th>
<th>{{ translate('rolesConfig', 'permName', data['lang']) }}</th>
<th>{{ translate('rolesConfig', 'permAccess', data['lang']) }}</th>
</tr>
</thead>
<tbody>
@ -149,14 +148,14 @@
<div class="col-md-3 col-sm-12">
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fas fa-users"></i> Users Assigned to Role:</h4>
<h4 class="card-title"><i class="fas fa-users"></i> {{ translate('rolesConfig', 'roleUsers', data['lang']) }}</h4>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr class="rounded">
<th>User Name</th>
<th>{{ translate('rolesConfig', 'roleUserName', data['lang']) }}</th>
<th></th>
</tr>
</thead>
@ -183,22 +182,22 @@
<div class="col-md-3 col-sm-12">
<div class="card">
<div class="card-body">
<h4 class="card-title">Role Config Area</h4>
<p class="card-description"> Here is where you can change the configuration of your role</p>
<h4 class="card-title">{{ translate('rolesConfig', 'roleConfigArea', data['lang']) }}</h4>
<p class="card-description"> {{ translate('rolesConfig', 'configDesc', data['lang']) }}</p>
<blockquote class="blockquote">
<p class="mb-0">
Created: {{ str(data['role']['created']) }}
{{ translate('rolesConfig', 'created', data['lang']) }} {{ str(data['role']['created']) }}
<br />
Last updated: {{ str(data['role']['last_update']) }}
{{ translate('rolesConfig', 'configUpdate', data['lang']) }} {{ str(data['role']['last_update']) }}
<br />
</p>
</blockquote>
<div class="text-center">
{% if data['new_role'] %}
<a class="btn btn-sm btn-danger disabled"><i class="fas fa-trash"></i> Delete Role</a><br />
<small>You cannot delete something that does not yet exist</small>
<a class="btn btn-sm btn-danger disabled"><i class="fas fa-trash"></i>{{ translate('rolesConfig', 'delRole', data['lang']) }}</a><br />
<small>{{ translate('rolesConfig', 'doesNotExist', data['lang']) }}</small>
{% else %}
<a href="/panel/remove_role?id={{ data['role']['role_id'] }}" class="btn btn-sm btn-danger"><i class="fas fa-trash"></i> Delete Role</a>
<a href="/panel/remove_role?id={{ data['role']['role_id'] }}" class="btn btn-sm btn-danger"><i class="fas fa-trash"></i>{{ translate('rolesConfig', 'delRole', data['lang']) }}</a>
{% end %}
</div>
</div>
@ -229,7 +228,6 @@
$( document ).ready(function() {
console.log( "ready!" );
});

View File

@ -1,7 +1,6 @@
{% extends ../base.html %}
{% block meta %}
<!-- <meta http-equiv="refresh" content="60">-->
{% end %}
{% block title %}Crafty Controller - Edit User{% end %}
@ -16,13 +15,13 @@
<div class="page-header">
{% if data['new_user'] %}
<h4 class="page-title">
New User
{{ translate('userConfig', 'pageTitleNew', data['lang']) }}
<br />
<small>UID: N/A</small>
</h4>
{% else %}
<h4 class="page-title">
Edit User - {{ data['user']['user_id'] }}
{{ translate('userConfig', 'pageTitle', data['lang']) }} - {{ data['user']['user_id'] }}
<br />
<small>UID: {{ data['user']['user_id'] }}</small>
</h4>
@ -41,12 +40,12 @@
<ul class="nav nav-tabs col-md-12 tab-simple-styled " role="tablist">
<li class="nav-item">
<a class="nav-link active" href="/panel/{{ 'add_user' if data['new_user'] else 'edit_user' }}?id={{ data['user']['user_id'] }}&subpage=config" role="tab" aria-selected="true">
<i class="fas fa-cogs"></i>Config</a>
<i class="fas fa-cogs"></i> {{ translate('userConfig', 'config', data['lang']) }} - {{ data['user']['user_id'] }}</a>
</li>
{% if not data['new_user'] %}
<li class="nav-item">
<a class="nav-link" href="/panel/add_user?id={{ data['user']['user_id'] }}&subpage=other" role="tab" aria-selected="false">
<i class="fas fa-folder-tree"></i>Other</a>
<a class="nav-link" href="/panel/edit_user_apikeys?id={{ data['user']['user_id'] }}" role="tab" aria-selected="false">
<i class="fas fa-key"></i>{{ translate('userConfig', 'apiKey', data['lang']) }} - {{ data['user']['user_id'] }}</a>
</li>
{% end %}
</ul>
@ -65,26 +64,34 @@
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fas fa-user"></i> User Settings</h4>
<h4 class="card-title"><i class="fas fa-user"></i> {{ translate('userConfig', 'userSettings', data['lang']) }} - {{ data['user']['user_id'] }}</h4>
</div>
<div class="card-body">
<div class="form-group">
<label class="form-label" for="username">User Name <small class="text-muted ml-1"> - What you wish to call this user</small> </label>
<label class="form-label" for="username">{{ translate('userConfig', 'userName', data['lang']) }} - {{ data['user']['user_id'] }}<small class="text-muted ml-1"> - {{ translate('userConfig', 'userNameDesc', data['lang']) }} - {{ data['user']['user_id'] }}</small> </label>
<input type="text" class="form-control" name="username" id="username" value="{{ data['user']['username'] }}" placeholder="User Name" >
</div>
<div class="form-group">
<label class="form-label" for="password0">Password <small class="text-muted ml-1"> - leave blank to don't change</small> </label>
<label class="form-label" for="password0">{{ translate('userConfig', 'password', data['lang']) }} - {{ data['user']['user_id'] }}<small class="text-muted ml-1"> - {{ translate('userConfig', 'leaveBlank', data['lang']) }} - {{ data['user']['user_id'] }}</small> </label>
<input type="password" class="form-control" name="password0" id="password0" value="" placeholder="Password" >
</div>
<div class="form-group">
<label class="form-label" for="password1">Repeat Password <small class="text-muted ml-1"> - leave blank to don't change</small> </label>
<label class="form-label" for="password1">{{ translate('userConfig', 'repeat', data['lang']) }} - {{ data['user']['user_id'] }} <small class="text-muted ml-1"> - {{ translate('userConfig', 'leaveBlank', data['lang']) }} - {{ data['user']['user_id'] }}</small> </label>
<input type="password" class="form-control" name="password1" id="password1" value="" placeholder="Repeat Password" >
</div>
<div class="form-group">
<label class="form-label" for="language">User Language:</label>
<select class="form-select" id="language" name="language" form="user_form">
<label class="form-label" for="email">{{ translate('userConfig', 'gravEmail', data['lang']) }} - {{ data['user']['user_id'] }}<small class="text-muted ml-1"> - {{ translate('userConfig', 'gravDesc', data['lang']) }} - {{ data['user']['user_id'] }}</small> </label>
<input type="email" class="form-control" name="email" id="email" value="{{ data['user']['email'] }}" placeholder="Gravatar Email" >
</div>
<div class="form-group">
<label class="form-label" for="language">{{ translate('userConfig', 'userLang', data['lang']) }}</label>
<select class="form-select form-control form-control-lg select-css" id="language" name="language" form="user_form">
{% for lang in data['languages'] %}
{% if not 'incomplete' in lang %}
<option value="{{lang}}">{{lang}}</option>
{% else %}
<option value="{{lang}}" disabled>{{lang}}</option>
{% end %}
{% end %}
</select>
</div>
@ -93,7 +100,7 @@
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fas fa-user-tag"></i> Roles <small class="text-muted ml-1"> - the roles this user is a member of</small></h4>
<h4 class="card-title"><i class="fas fa-user-tag"></i> {{ translate('userConfig', 'userRoles', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('userConfig', 'userRolesDesc', data['lang']) }}</small></h4>
</div>
<div class="card-body">
<div class="form-group">
@ -101,8 +108,8 @@
<table class="table table-hover">
<thead>
<tr class="rounded">
<th>Role Name</th>
<th>Member?</th>
<th>{{ translate('userConfig', 'roleName', data['lang']) }}</th>
<th>{{ translate('userConfig', 'member', data['lang']) }}</th>
</tr>
</thead>
<tbody>
@ -130,7 +137,7 @@
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fas fa-user-lock"></i> Crafty Permissions <small class="text-muted ml-1"> - permissions this user has on Crafty Controller </small></h4>
<h4 class="card-title"><i class="fas fa-user-lock"></i> {{ translate('userConfig', 'craftyPerms', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('userConfig', 'craftyPermDesc', data['lang']) }}</small></h4>
</div>
<div class="card-body">
<div class="form-group">
@ -138,9 +145,9 @@
<table class="table table-hover">
<thead>
<tr class="rounded">
<th>Permission Name</th>
<th>Authorized ?</th>
<th>Quantity</th>
<th>{{ translate('userConfig', 'permName', data['lang']) }}</th>
<th>{{ translate('userConfig', 'auth', data['lang']) }}</th>
<th>{{ translate('userConfig', 'uses', data['lang']) }}</th>
</tr>
</thead>
<tbody>
@ -167,25 +174,17 @@
<div class="form-check-flat">
<label for="enabled" class="form-check-label ml-4 mb-4">
{% if data['user']['enabled'] %}
<input type="checkbox" class="form-check-input" id="enabled" name="enabled" checked="" value="1">Enabled
<input type="checkbox" class="form-check-input" id="enabled" name="enabled" checked="" value="1">{{ translate('userConfig', 'enabled', data['lang']) }}
{% else %}
<input type="checkbox" class="form-check-input" id="enabled" name="enabled" value="1">Enabled
{% end %}
</label>
<label for="regen_api" class="form-check-label ml-4 mb-4">
{% if data['new_user'] %}
<input type="checkbox" class="form-check-input" id="regen_api" name="regen_api" checked="" value="1" disabled >Regenerate API Key
{% else %}
<input type="checkbox" class="form-check-input" id="regen_api" name="regen_api" value="1">Regenerate API Key
<input type="checkbox" class="form-check-input" id="enabled" name="enabled" value="1">{{ translate('userConfig', 'enabled', data['lang']) }}
{% end %}
</label>
<label for="superuser" class="form-check-label ml-4 mb-4">
{% if data['user']['superuser'] %}
<input type="checkbox" class="form-check-input" id="superuser" name="superuser" checked="" value="1" disabled >Super User
<input type="checkbox" onclick="superConfirm()" class="form-check-input" id="superuser" name="superuser" checked="" value="1" {{ data['super-disabled'] }} >{{ translate('userConfig', 'super', data['lang']) }}
{% else %}
<input type="checkbox" class="form-check-input" id="superuser" name="superuser" value="1" disabled >Super User
<input type="checkbox" onclick="superConfirm()" class="form-check-input" id="superuser" name="superuser" {{ data['super-disabled'] }} value="1" >{{ translate('userConfig', 'super', data['lang']) }}
{% end %}
</label>
@ -199,19 +198,17 @@
<div class="col-md-6 col-sm-12">
<div class="card">
<div class="card-body">
<h4 class="card-title"><i class="fas fa-user-cog"></i> User Config Area</h4>
<p class="card-description"> Here is where you can change the configuration of your user</p>
<h4 class="card-title"><i class="fas fa-user-cog"></i> {{ translate('userConfig', 'configArea', data['lang']) }}</h4>
<p class="card-description"> {{ translate('userConfig', 'configAreaDesc', data['lang']) }}</p>
<blockquote class="blockquote">
<p class="mb-0">
Created: {{ str(data['user']['created']) }}
{{ translate('userConfig', 'created', data['lang']) }} {{ str(data['user']['created']) }}
<br />
Last login: {{ str(data['user']['last_login']) }}
{{ translate('userConfig', 'lastLogin', data['lang']) }} {{ str(data['user']['last_login']) }}
<br />
Last update: {{ str(data['user']['last_update']) }}
{{ translate('userConfig', 'lastUpdate', data['lang']) }} {{ str(data['user']['last_update']) }}
<br />
Last IP: {{ data['user']['last_ip'] }}
<br />
API Key: {{ data['user']['api_token'] }}
{{ translate('userConfig', 'lastIP', data['lang']) }} {{ data['user']['last_ip'] }}
<br />
</p>
</blockquote>
@ -219,13 +216,13 @@
</div>
<div class="text-center">
{% if data['new_user'] %}
<a class="btn btn-sm btn-danger disabled"><i class="fas fa-trash"></i> Delete User</a><br />
<small>You cannot delete something that does not yet exist</small>
<a class="btn btn-sm btn-danger disabled"><i class="fas fa-trash"></i>{{ translate('userConfig', 'deleteUserB', data['lang']) }}</a><br />
<small>{{ translate('userConfig', 'notExist', data['lang']) }}</small>
{% elif data['user']['superuser'] %}
<a class="btn btn-sm btn-danger disabled"><i class="fas fa-trash"></i> Delete User</a><br />
<small>You cannot delete a superuser</small>
<a class="btn btn-sm btn-danger disabled"><i class="fas fa-trash"></i> {{ translate('userConfig', 'deleteUserB', data['lang']) }}</a><br />
<small>{{ translate('userConfig', 'delSuper', data['lang']) }}</small>
{% else %}
<a href="/panel/remove_user?id={{ data['user']['user_id'] }}" class="btn btn-sm btn-danger"><i class="fas fa-trash"></i> Delete User</a>
<button class="btn btn-sm btn-danger delete-user"><i class="fas fa-trash"></i> {{ translate('userConfig', 'deleteUserB', data['lang']) }}</a>
{% end %}
</div>
@ -246,6 +243,60 @@
{% block js %}
<script>
const userId = new URLSearchParams(document.location.search).get('id')
$( ".delete-user" ).click(function() {
var file_to_del = $(this).data("file");
console.log("User to delete is "+userId);
bootbox.confirm({
title: "{% raw translate('userConfig', 'deleteUser', data['lang']) %} "+userId,
message: "{{ translate('userConfig', 'confirmDelete', data['lang']) }}",
buttons: {
cancel: {
label: '<i class="fas fa-times"></i> {{ translate("serverBackups", "cancel", data['lang']) }}'
},
confirm: {
className: 'btn-outline-danger',
label: '<i class="fas fa-check"></i> {{ translate("serverBackups", "confirm", data['lang']) }}'
}
},
callback: function (result) {
console.log(result);
if (result == true) {
location.href="/panel/remove_user?id="+userId;
}
}
});
});
function superConfirm() {
if (document.getElementById('superuser').checked){
bootbox.confirm({
title: "{{ translate('panelConfig', 'superConfirmTitle', data['lang']) }}",
message: "{{ translate('panelConfig', 'superConfirm', data['lang']) }}",
buttons: {
cancel: {
label: '<i class="fa fa-times"></i> {{ translate('panelConfig', 'cancel', data['lang']) }}'
},
confirm: {
className: 'btn-outline-warning',
label: '<i class="fa fa-check"></i> {{ translate('serverBackups', 'confirm', data['lang']) }}'
}
},
callback: function (result) {
if (result == true){
return;
}else{
document.getElementById('superuser').checked = false;
}
}
});
}else{
return
}
}
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security

View File

@ -0,0 +1,252 @@
{% extends ../base.html %}
{% block meta %}
{% end %}
{% block title %}Crafty Controller - Edit User API Keys{% end %}
{% block content %}
<div class="content-wrapper">
<!-- Page Title Header Starts-->
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<h4 class="page-title">
{{ translate('apiKeys', 'pageTitle', data['lang']) }} - {{ data['user']['user_id'] }}
<br/>
<small>UID: {{ data['user']['user_id'] }}</small>
</h4>
</div>
</div>
</div>
<!-- Page Title Header Ends-->
<div class="row">
<div class="col-sm-12 grid-margin">
<div class="card">
<div class="card-body pt-0">
<ul class="nav nav-tabs col-md-12 tab-simple-styled " role="tablist">
<li class="nav-item">
<a class="nav-link" href="/panel/edit_user?id={{ data['user']['user_id'] }}&subpage=config"
role="tab"
aria-selected="false">
<i class="fas fa-cogs"></i>{{ translate('apiKeys', 'config', data['lang']) }}</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/panel/edit_user_apikeys?id={{ data['user']['user_id'] }}"
role="tab"
aria-selected="true">
<i class="fas fa-key"></i>{{ translate('apiKeys', 'apiKeys', data['lang']) }}</a>
</li>
</ul>
<div class="row">
<div class="col-md-7 col-sm-12">
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fas fa-key"></i>{{ translate('apiKeys', 'apiKeys', data['lang']) }}</h4>
</div>
<div class="card-body">
<div class="form-group">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr class="rounded">
<!--<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', 'perms', data['lang']) }}</th>
<th>{{ translate('apiKeys', 'buttons', data['lang']) }}</th>
</tr>
</thead>
<tbody>
{% for apikey in data['api_keys'] %}
<tr>
<!--<td>{-{ apikey.token_id }-}</td>-->
<td>{{ apikey.name }}</td>
<td>{{ apikey.created.strftime('%d/%m/%Y %H:%M:%S') }}</td>
<td>
{% if apikey.superuser %}
<span class="text-success">
<i class="fas fa-check-square"></i> {{ translate('apiKeys', 'yes', data['lang']) }}
</span>
{% else %}
<span class="text-danger">
<i class="far fa-times-square"></i> {{ translate('apiKeys', 'no', data['lang']) }}
</span>
{% end %}
</td>
<td>{{ translate('apiKeys', 'server', data['lang']) }} {{ apikey.server_permissions }}
{{ translate('apiKeys', 'crafty', data['lang']) }} {{ apikey.crafty_permissions }}</td>
<td>
<button
class="btn btn-danger delete-api-key"
data-key-id="{{ apikey.token_id }}"
data-key-name="{{ apikey.name }}"
>{{ translate('panelConfig', 'delete', data['lang']) }}</button>
<button
class="btn btn-outline-primary get-a-token"
data-key-id="{{ apikey.token_id }}"
data-key-name="{{ apikey.name }}"
>{{ translate('apiKeys', 'getToken', data['lang']) }}
</button>
</td>
</tr>
{% end %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-5 col-sm-12">
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fas fa-plus"></i> {{ translate('apiKeys', 'createNew', data['lang']) }}</h4>
</div>
<div class="card-body">
<form id="user_form" class="forms-sample" method="post"
action="/panel/edit_user_apikeys">
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['user']['user_id'] }}">
<div class="form-group">
<label class="form-label" for="username">{{ translate('apiKeys', 'name', data['lang']) }}<small
class="text-muted ml-1"> - {{ translate('apiKeys', 'nameDesc', data['lang']) }}</small> </label>
<input type="text" class="form-control" name="name" id="name"
placeholder="API Key">
</div>
<table class="table table-hover mb-3">
<thead>
<tr class="rounded">
<th>{{ translate('apiKeys', 'permName', data['lang']) }}</th>
<th>{{ translate('apiKeys', 'auth', data['lang']) }}</th>
</tr>
</thead>
<tbody>
{% for permission in data['server_permissions_all'] %}
<tr>
<td><label
for="permission_{{ permission.name }}">{{ permission.name }}</label>
</td>
<td>
<input type="checkbox" class=""
id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" value="1">
</td>
</tr>
{% end %}
{% for permission in data['crafty_permissions_all'] %}
<tr>
<td><label
for="permission_{{ permission.name }}">{{ permission.name }}</label>
</td>
<td>
<input type="checkbox" class=""
id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" value="1">
</td>
</tr>
{% end %}
</tbody>
</table>
<label for="superuser">Superuser</label>
<input type="checkbox" class="" id="superuser"
name="superuser" value="1">
<br/>
<button type="submit" class="btn btn-success mr-2"><i class="fas fa-plus"></i>
Create
</button>
<button type="reset" class="btn btn-light"><i
class="fas fa-undo-alt"></i> {{ translate('panelConfig', 'cancel', data['lang']) }}
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- content-wrapper ends -->
{% end %}
{% block js %}
<script>
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
$(document).ready(function () {
console.log("ready!");
$('.delete-api-key').click(function () {
var keyId = $(this).data("key-id");
var keyName = $(this).data("key-name");
bootbox.confirm({
title: `Remove API key ${keyName}?`,
message: "Do you want to delete this API key? This cannot be undone.",
buttons: {
cancel: {
label: '<i class="fas fa-times"></i> {{ translate("panelConfig", "cancel", data['lang']) }}'
},
confirm: {
label: '<i class="fas fa-check"></i> {{ translate("serverBackups", "confirm", data['lang']) }}'
}
},
callback: function (result) {
var token = getCookie("_xsrf")
$.ajax({
type: "DELETE",
headers: {'X-XSRFToken': token},
url: '/panel/remove_apikey?id=' + keyId,
success: function (data) {
location.reload();
},
});
}
});
})
$('.get-a-token').click(function () {
var keyId = $(this).data("key-id");
var keyName = $(this).data("key-name");
var token = getCookie("_xsrf")
$.ajax({
type: "POST",
headers: {'X-XSRFToken': token},
url: '/panel/get_token?id=' + keyId,
success: function (data) {
bootbox.alert({
title: `API token for ${keyName}`,
message: `Here is an API token for ${keyName}:\n<pre style="white-space: pre-wrap;color:white;word-break:break-all;background: grey;border-radius: 5px;">${data}</pre>`
});
},
});
})
});
</script>
{% end %}

View File

@ -3,51 +3,55 @@
<div class="card">
<div class="card-body pt-3 pb-3">
<div class="row">
<div class="col-sm-3 mr-2">
<div class="col-sm-4 mr-2">
{% if data['server_stats']['running'] %}
<b>{{ translate('serverStats', 'serverStatus', data['lang']) }}:</b> <span class="text-success">{{ translate('serverStats', 'online', data['lang']) }}</span><br />
<b>{{ translate('serverStats', 'serverStarted', data['lang']) }}:</b> <span id="started">{{ data['server_stats']['started'] }} ({{ translate('serverStats', 'serverTime', data['lang']) }})</span><br />
<b>{{ translate('serverStats', 'serverStatus', data['lang']) }}:</b> <span id="status" class="text-success">{{ translate('serverStats', 'online', data['lang']) }}</span><br />
<b>{{ translate('serverStats', 'serverStarted', data['lang']) }}:</b> <span id="started">{{ data['server_stats']['started'] }}</span><br />
<b>{{ translate('serverStats', 'serverUptime', data['lang']) }}:</b> <span id="uptime">{{ translate('serverStats', 'errorCalculatingUptime', data['lang']) }}</span>
{% elif data['server_stats']['crashed'] %}
<b>{{ translate('serverStats', 'serverStatus', data['lang']) }}:</b> <span id="status" class="text-danger"> <i class="fas fa-exclamation-triangle"></i> {{ translate('dashboard', 'crashed', data['lang']) }}</span><br />
<b>{{ translate('serverStats', 'serverStarted', data['lang']) }}:</b> <span id="started" class="text-danger"> <i class="fas fa-exclamation-triangle"></i> {{ translate('dashboard', 'crashed', data['lang']) }}</span><br />
<b>{{ translate('serverStats', 'serverUptime', data['lang']) }}:</b> <span id="uptime" class="text-danger"> <i class="fas fa-exclamation-triangle"></i> {{ translate('dashboard', 'crashed', data['lang']) }}</span>
{% else %}
<b>{{ translate('serverStats', 'serverStatus', data['lang']) }}:</b> <span class="text-danger">{{ translate('serverStats', 'offline', data['lang']) }}</span><br />
<b>{{ translate('serverStats', 'serverStarted', data['lang']) }}:</b> <span class="text-danger">{{ translate('serverStats', 'offline', data['lang']) }}</span><br />
<b>{{ translate('serverStats', 'serverUptime', data['lang']) }}:</b> <span class="text-danger">{{ translate('serverStats', 'offline', data['lang']) }}</span>
<b>{{ translate('serverStats', 'serverStatus', data['lang']) }}:</b> <span id="status" class="text-warning">{{ translate('serverStats', 'offline', data['lang']) }}</span><br />
<b>{{ translate('serverStats', 'serverStarted', data['lang']) }}:</b> <span id="started" class="text-warning">{{ translate('serverStats', 'offline', data['lang']) }}</span><br />
<b>{{ translate('serverStats', 'serverUptime', data['lang']) }}:</b> <span id="uptime" class="text-warning">{{ translate('serverStats', 'offline', data['lang']) }}</span>
{% end %}
<br>
<b>{{ translate('serverStats', 'serverTimeZone', data['lang']) }}:</b> <span class="text-info">{{ data['serverTZ'] }}</span>
</div>
<div class="col-sm-3 mr-2">
<b>{{ translate('serverStats', 'cpuUsage', data['lang']) }}:</b> {{ data['server_stats']['cpu'] }}% <br />
<b>{{ translate('serverStats', 'memUsage', data['lang']) }}:</b> {{ data['server_stats']['mem'] }} <br />
<b>{{ translate('serverStats', 'cpuUsage', data['lang']) }}:</b> <span id="cpu">{{ data['server_stats']['cpu'] }}%</span> <br />
<b>{{ translate('serverStats', 'memUsage', data['lang']) }}:</b> <span id="mem" >{{ data['server_stats']['mem'] }}</span> <br />
{% if data['server_stats']['int_ping_results'] %}
<b>{{ translate('serverStats', 'players', data['lang']) }}:</b> {{ data['server_stats']['online'] }} / {{ data['server_stats']['max'] }}<br />
<b>{{ translate('serverStats', 'players', data['lang']) }}:</b> <span id="players" >{{ data['server_stats']['online'] }} / {{ data['server_stats']['max'] }}</span><br />
{% else %}
<b>{{ translate('serverStats', 'players', data['lang']) }}:</b> 0/0<br />
<b>{{ translate('serverStats', 'players', data['lang']) }}:</b> <span id="players" >0/0</span><br />
{% end %}
</div>
<div class="col-sm-3 mr-2">
{% if data['server_stats']['version'] != 'False' %}
<b>{{ translate('serverStats', 'version', data['lang']) }}:</b> {{ data['server_stats']['version'] }} <br />
<b>{{ translate('serverStats', 'version', data['lang']) }}:</b> <span id="version">{{ data['server_stats']['version'] }}</span><br />
<b>{{ translate('serverStats', 'description', data['lang']) }}:</b> <span id="input_motd" class="input_motd">{{ data['server_stats']['desc'] }}</span> <br />
{% else %}
<b>{{ translate('serverStats', 'version', data['lang']) }}:</b> {{ translate('serverStats', 'unableToConnect', data['lang']) }} <br />
<b>{{ translate('serverStats', 'description', data['lang']) }}:</b> {{ translate('serverStats', 'unableToConnect', data['lang']) }} <br />
<b>{{ translate('serverStats', 'version', data['lang']) }}:</b> <span id="version">{{ translate('serverStats', 'unableToConnect', data['lang']) }}</span> <br />
<b>{{ translate('serverStats', 'description', data['lang']) }}:</b> <span id="input_motd" class="input_motd">{{ translate('serverStats', 'unableToConnect', data['lang']) }}</span> <br />
{% end %}
</div>
<b>Server Type: <span class="text-info">{{data['server_stats']['server_type']}}</span></b>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="/static/assets/vendors/moment/moment.min.js" type="text/javascript" charset="utf-8"></script>
<script src="/static/assets/js/motd.js"></script>
<script>
function durationToHumanizedString(duration) {
duration._data.months += duration._data.years * 12;
// 30.45833333333 = average month length, calculate with (31+28.5+31+30+31+30+31+31+30+31+30+31) / 12
@ -78,20 +82,21 @@
return output;
}
document.body.onload = (() => {
console.log('calculateTime');
let uptime = document.querySelector('#uptime');
let started = document.querySelector('#started');
let startedUTC;
let startedLocal;
let uptimeLoop;
if (started != null) {
startedUTC = '{{ data['server_stats']['started'] }}';
document.body.onload = (() => {
console.log('calculateTime');
startedUTC = "{{ data['server_stats']['started'] }}";
if (startedUTC != 'False') {
console.log('started utc:', startedUTC);
startedUTC = moment.utc(startedUTC, 'YYYY-MM-DD HH:mm:ss');
let browserUTCOffset = moment().utcOffset(); // This is in minutes
var browserUTCOffset = moment().utcOffset(); // This is in minutes
startedLocal = startedUTC.utcOffset(browserUTCOffset);
startedLocalFormatted = startedLocal.format('YYYY-MM-DD HH:mm:ss');
@ -102,23 +107,110 @@
}
var calculateUptime = () => {
var msdiff = moment()
.diff(startedLocal);
var msdiff = moment().diff(startedLocal);
var diff = moment.duration(msdiff);
uptime.textContent = durationToHumanizedString(diff);
}
if (uptime != null && started != null) {
console.log('startedLocal', startedLocal)
if (startedLocal) {
calculateUptime()
var uptimeLoop = setInterval(calculateUptime, 1000)
calculateUptime();
uptimeLoop = setInterval(calculateUptime, 1000);
}
}
initParser('input_motd', 'input_motd');
});
function update_server_details(server) {
server_status = document.getElementById('status');
server_started = document.getElementById('started');
server_uptime = document.getElementById('uptime');
server_cpu = document.getElementById('cpu');
server_mem = document.getElementById('mem');
server_players = document.getElementById('players');
server_version = document.getElementById('version');
server_input_motd = document.getElementById('input_motd');
/* TODO Update each element */
if (server.running){
server_status.setAttribute("class", "text-success");
server_status.innerHTML = `{{ translate('serverStats', 'online', data['lang']) }}`;
startedUTC = server.started;
startedUTC = moment.utc(startedUTC, 'YYYY-MM-DD HH:mm:ss');
var browserUTCOffset = moment().utcOffset(); // This is in minutes
startedLocal = startedUTC.utcOffset(browserUTCOffset);
startedLocalFormatted = startedLocal.format('YYYY-MM-DD HH:mm:ss');
server_started.setAttribute("class", "");
server_started.innerHTML = startedLocalFormatted;
server_uptime.setAttribute("class", "");
if (!uptimeLoop) {
var calculateUptime = () => {
var msdiff = moment().diff(startedLocal);
var diff = moment.duration(msdiff);
uptime.textContent = durationToHumanizedString(diff);
}
uptimeLoop = setInterval(calculateUptime, 1000);
}
}
else
{
if (server.crashed){
server_status.setAttribute("class", "text-danger");
server_status.innerHTML = `<i class="fas fa-exclamation-triangle"></i> {{ translate('dashboard', 'crashed', data['lang']) }}`;
server_started.setAttribute("class", "text-danger");
server_started.innerHTML = `<i class="fas fa-exclamation-triangle"></i> {{ translate('dashboard', 'crashed', data['lang']) }}`;
clearInterval(uptimeLoop);
uptimeLoop = null;
server_uptime.setAttribute("class", "text-danger");
server_uptime.innerHTML = `<i class="fas fa-exclamation-triangle"></i> {{ translate('dashboard', 'crashed', data['lang']) }}`;
}else{
server_status.setAttribute("class", "text-warning");
server_status.innerHTML = `{{ translate('serverStats', 'offline', data['lang']) }}`;
server_started.setAttribute("class", "text-warning");
server_started.innerHTML = `{{ translate('serverStats', 'offline', data['lang']) }}`;
clearInterval(uptimeLoop);
uptimeLoop = null;
server_uptime.setAttribute("class", "text-warning");
server_uptime.innerHTML = `{{ translate('serverStats', 'offline', data['lang']) }}`;
}
}
server_cpu.innerHTML = server.cpu + ` %`;
server_mem.innerHTML = server.mem;
if (server.int_ping_results)
{
server_players.innerHTML = server.online + `/` + server.max;
}
else
{
server_players.innerHTML = `0/0`;
}
if (server.version)
{
server_version.innerHTML = server.version;
server_input_motd.innerHTML = server.desc;
}
else
{
server_version.innerHTML = `{{ translate('serverStats', 'unableToConnect', data['lang']) }}`;
server_input_motd.innerHTML = `{{ translate('serverStats', 'unableToConnect', data['lang']) }}`;
}
initParser('input_motd', 'input_motd');
}
$(window).ready(function () {
console.log("ready!");
//if (webSocket) {
webSocket.on('update_server_details', update_server_details);
//}
});
</script>

View File

@ -5,7 +5,8 @@
<i class="fas fa-file-signature"></i>{{ translate('serverDetails', 'terminal', data['lang']) }}</a>
</li>
{% end %}
{% if data['permissions']['Logs'] in data['user_permissions'] %}
<!--Bedrock servers don't have logs so we'll only show it if we know it's not a bedrock server.-->
{% if data['permissions']['Logs'] in data['user_permissions'] and data['server_data']['type'] != 'minecraft-bedrock'%}
<li class="nav-item term-nav-item">
<a class="nav-link {% if data['active_link'] == 'logs' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=logs" role="tab" aria-selected="false">
<i class="fas fa-file-signature"></i>{{ translate('serverDetails', 'logs', data['lang']) }}</a>
@ -13,7 +14,7 @@
{% end %}
{% if data['permissions']['Schedule'] in data['user_permissions'] %}
<li class="nav-item term-nav-item">
<a class="nav-link {% if data['active_link'] == 'schedule' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=tasks" role="tab" aria-selected="false">
<a class="nav-link {% if data['active_link'] == 'schedules' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=schedules" role="tab" aria-selected="false">
<i class="fas fa-clock"></i>{{ translate('serverDetails', 'schedule', data['lang']) }}</a>
</li>
{% end %}
@ -35,7 +36,7 @@
<i class="fas fa-cogs"></i>{{ translate('serverDetails', 'config', data['lang']) }}</a>
</li>
{% end %}
{% if data['permissions']['Players'] in data['user_permissions'] %}
{% if data['permissions']['Players'] in data['user_permissions'] and data['server_data']['type'] != 'minecraft-bedrock' %}
<li class="nav-item term-nav-item">
<a class="nav-link {% if data['active_link'] == 'admin_controls' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=admin_controls" role="tab" aria-selected="true">
<i class="fas fa-users"></i>{{ translate('serverDetails', 'playerControls', data['lang']) }}</a>

View File

@ -1,7 +1,6 @@
{% extends ../base.html %}
{% block meta %}
<!-- <meta http-equiv="refresh" content="60">-->
{% end %}
{% block title %}Crafty Controller - {{ translate('serverDetails', 'serverDetails', data['lang']) }}{% end %}

View File

@ -1,7 +1,6 @@
{% extends ../base.html %}
{% block meta %}
<!-- <meta http-equiv="refresh" content="60">-->
{% end %}
{% block title %}Crafty Controller - {{ translate('serverDetails', 'serverDetails', data['lang']) }}{% end %}
@ -36,6 +35,8 @@
<div class="row">
<div class="col-md-6 col-sm-12">
<br>
<br>
<form class="forms-sample" method="post" action="/panel/server_backup">
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['server_stats']['server_id']['server_id'] }}">
@ -45,23 +46,58 @@
<a href="/panel/backup_now?id={{ data['server_stats']['server_id']['server_id'] }}" class="btn btn-primary" onclick="backup_started()">{{ translate('serverBackups', 'backupNow', data['lang']) }}</a>
</div>
<div class="form-group">
{% if data['super_user'] %}
<label for="server_name">{{ translate('serverBackups', 'storageLocation', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverBackups', 'storageLocationDesc', data['lang']) }}</small> </label>
<input type="text" class="form-control" name="backup_path" id="backup_path" value="{{ data['server_stats']['server_id']['backup_path'] }}" placeholder="{{ translate('serverBackups', 'storageLocation', data['lang']) }}" >
{% end %}
</div>
<div class="form-group">
<label for="server_path">{{ translate('serverBackups', 'maxBackups', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverBackups', 'maxBackupsDesc', data['lang']) }}</small> </label>
<input type="text" class="form-control" name="max_backups" id="max_backups" value="{{ data['backup_config']['max_backups'] }}" placeholder="{{ translate('serverBackups', 'maxBackups', data['lang']) }}" >
</div>
<div class="form-group">
<label for="superuser" class="form-check-label ml-4 mb-4">
{% if data['backup_config']['auto_enabled'] %}
<input type="checkbox" class="form-check-input" id="auto_enabled" name="auto_enabled" checked="" value="1" >{{ translate('serverBackups', 'backupAtMidnight', data['lang']) }}
<label for="compress" class="form-check-label ml-4 mb-4"></label>
{% if data['backup_config']['compress'] %}
<input type="checkbox" class="form-check-input" id="compress" name="compress"
checked="" value="True">{{ translate('serverBackups', 'compress', data['lang']) }}
{% else %}
<input type="checkbox" class="form-check-input" id="auto_enabled" name="auto_enabled" value="1" >{{ translate('serverBackups', 'backupAtMidnight', data['lang']) }}
<input type="checkbox" class="form-check-input" id="compress" name="compress"
value="True">{{ translate('serverBackups', 'compress', data['lang']) }}
{% end %}
</label>
</div>
<div class="form-group">
<label for="server">{{ translate('serverBackups', 'exclusionsTitle', data['lang']) }} <small> - {{ translate('serverBackups', 'excludedChoose', data['lang']) }}</small></label>
<br>
<button class="btn btn-primary mr-2" id="root_files_button" data-server_path="{{ data['server_stats']['server_id']['path']}}" type="button">{{ translate('serverBackups', 'clickExclude', data['lang']) }}</button>
</div>
<input type="number" class="form-control" name="changed" id="changed" value="0" style="visibility: hidden;"></input>
<div class="modal fade" id="dir_select" tabindex="-1" role="dialog" aria-labelledby="dir_select" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">{{ translate('serverBackups', 'excludedChoose', data['lang']) }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="tree-ctx-item" id="main-tree-div" data-path="" style="overflow: scroll; max-height:75%;">
<input type="checkbox" id="main-tree-input" name="root_path" value="" disabled>
<span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path="">
<i class="far fa-folder"></i>
<i class="far fa-folder-open"></i>
{{ translate('serverFiles', 'files', data['lang']) }}
</span>
</input>
</div>
</div>
<div class="modal-footer">
<button type="button" id="modal-cancel" class="btn btn-secondary" data-dismiss="modal">{{ translate('serverBackups', 'cancel', data['lang']) }}</button>
<button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary">{{ translate('serverWizard', 'save', data['lang']) }}</button>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-success mr-2">{{ translate('serverBackups', 'save', data['lang']) }}</button>
@ -70,15 +106,10 @@
</div>
<div class="col-md-6 col-sm-12">
<div class="card">
<div class="card-body">
<h4 class="card-title">{{ translate('serverBackups', 'currentBackups', data['lang']) }}</h4>
</div>
</div>
<div class="text-center">
<table class="table table-responsive dataTable" id="backup_table">
<h4 class="card-title">{{ translate('serverBackups', 'currentBackups', data['lang']) }}</h4>
<thead>
<tr>
<th width="10%">{{ translate('serverBackups', 'options', data['lang']) }}</th>
@ -96,10 +127,14 @@
</a>
<br>
<br>
<button data-file="{{ backup['path'] }}" class="btn btn-danger del_button">
<button data-file="{{ backup['path'] }}" data-backup_path="{{ data['backup_path'] }}" class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i>
{{ translate('serverBackups', 'delete', data['lang']) }}
</button>
<button data-file="{{ backup['path'] }}" class="btn btn-warning restore_button">
<i class="fas fa-undo-alt" aria-hidden="true"></i>
{{ translate('serverBackups', 'restore', data['lang']) }}
</button>
</td>
<td>{{ backup['path'] }}</td>
<td>{{ backup['size'] }}</td>
@ -112,6 +147,20 @@
</div>
</div>
</div>
<div class="col-md-12 col-sm-12">
<br>
<br>
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fas fa-server"></i> {{ translate('serverBackups', 'excludedBackups', data['lang']) }} <small class="text-muted ml-1"></small> </h4>
</div>
<br>
<ul>
{% for item in data['exclusions'] %}
<li>{{item}}</li>
<br>
{% end %}
</ul>
</div>
</div>
</div>
@ -121,6 +170,44 @@
</div>
<style>
/* Remove default bullets */
.tree-view,
.tree-nested {
list-style-type: none;
margin: 0;
padding: 0;
margin-left: 10px;
}
/* Style the items */
.tree-item,
.files-tree-title {
cursor: pointer;
user-select: none; /* Prevent text selection */
}
/* Create the caret/arrow with a unicode, and style it */
.tree-caret .fa-folder {
display: inline-block;
}
.tree-caret .fa-folder-open {
display: none;
}
/* Rotate the caret/arrow icon when clicked on (using JavaScript) */
.tree-caret-down .fa-folder {
display: none;
}
.tree-caret-down .fa-folder-open {
display: inline-block;
}
/* Hide the nested list */
.tree-nested {
display: none;
}
</style>
<!-- content-wrapper ends -->
{% end %}
@ -128,6 +215,8 @@
{% block js %}
<script>
const server_id = new URLSearchParams(document.location.search).get('id')
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
function getCookie(name) {
@ -151,7 +240,7 @@
$.ajax({
type: "DELETE",
headers: {'X-XSRFToken': token},
url: '/ajax/del_file?server_id='+id,
url: '/ajax/del_backup?server_id='+id,
data: {
file_path: filename,
id: id
@ -162,6 +251,31 @@
});
}
function restore_backup(filename, id){
var token = getCookie("_xsrf")
var dialog = bootbox.dialog({
message: '<i class="fa fa-spin fa-spinner"></i> {{ translate('serverBackups', 'restoring', data['lang']) }}',
closeButton: false
});
console.log('Sending Command to restore backup: ' + filename)
$.ajax({
type: "POST",
headers: {'X-XSRFToken': token},
url: '/ajax/restore_backup?server_id='+id,
data: {
zip_file: filename,
id: id
},
success: function(data) {
setTimeout(function(){
location.href=('/panel/dashboard');
}, 15000);
},
});
}
$( document ).ready(function() {
console.log( "ready!" );
$("#backup_config_box").hide();
@ -187,6 +301,7 @@
$( ".del_button" ).click(function() {
var file_to_del = $(this).data("file");
var backup_path = $(this).data('backup_path');
console.log("file to delete is" + file_to_del);
@ -204,8 +319,33 @@
callback: function (result) {
console.log(result);
if (result == true) {
var full_path = '{{ data['backup_path'] }}' + '/' + file_to_del;
del_backup(full_path, {{ data['server_stats']['server_id']['server_id'] }} );
var full_path = backup_path + '/' + file_to_del;
del_backup(full_path, server_id);
}
}
});
});
$( ".restore_button" ).click(function() {
var file_to_restore = $(this).data("file");
bootbox.confirm({
title: "{{ translate('serverBackups', 'restore', data['lang']) }} "+file_to_restore,
message: "{{ translate('serverBackups', 'confirmRestore', data['lang']) }}",
buttons: {
cancel: {
label: '<i class="fas fa-times"></i> {{ translate("serverBackups", "cancel", data['lang']) }}'
},
confirm: {
label: '<i class="fas fa-check"></i> {{ translate("serverBackups", "restore", data['lang']) }}',
className: 'btn-outline-danger'
}
},
callback: function (result) {
console.log(result);
if (result == true) {
restore_backup(file_to_restore, server_id);
}
}
});
@ -213,6 +353,140 @@
});
document.getElementById("modal-cancel").addEventListener("click", function(){
document.getElementById("root_files_button").classList.remove('clicked');
document.getElementById("main-tree-div").innerHTML = '<input type="checkbox" id="main-tree-input" name="root_path" value="" disabled><span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path=""><i class="far fa-folder"></i><i class="far fa-folder-open"></i>{{ translate("serverFiles", "files", data["lang"]) }}</span></input>'
})
document.getElementById("root_files_button").addEventListener("click", function(){
if($("#root_files_button").data('server_path') != ""){
if(document.getElementById('root_files_button').classList.contains('clicked')){
show_file_tree();
return;
}else{
document.getElementById('root_files_button').classList.add('clicked');
document.getElementById("changed").value = 1;
}
path = $("#root_files_button").data('server_path')
console.log($("#root_files_button").data('server_path'))
var token = getCookie("_xsrf");
var dialog = bootbox.dialog({
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
});
$.ajax({
type: "POST",
headers: {'X-XSRFToken': token},
url: '/ajax/backup_select?id='+server_id+'&path='+path,
});
}else{
bootbox.alert("You must input a path before selecting this button");
}
});
if (webSocket) {
webSocket.on('send_temp_path', function (data) {
setTimeout(function(){
var x = document.querySelector('.bootbox');
if (x) {
x.remove()
}
var x = document.querySelector('.modal-backdrop');
if (x) {
x.remove()
}
document.getElementById('main-tree-input').setAttribute('value', data.path)
getTreeView(data.path);
show_file_tree();
}, 5000);
});
}
function getTreeView(path) {
path = path
$.ajax({
type: "GET",
url: '/ajax/get_backup_tree?id='+server_id+'&path='+path,
dataType: 'text',
success: function(data){
console.log("got response:");
console.log(data);
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
try{
document.getElementById('main-tree-div').innerHTML += text;
document.getElementById('main-tree').parentElement.classList.add("clicked");
}catch{
document.getElementById('files-tree').innerHTML = text;
}
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-path', serverDir);
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-name', 'Files');
},
});
}
function getToggleMain(event) {
path = event.target.parentElement.getAttribute('data-path');
document.getElementById("files-tree").classList.toggle("d-block");
document.getElementById(path+"span").classList.toggle("tree-caret-down");
document.getElementById(path+"span").classList.toggle("tree-caret");
}
function getDirView(event) {
path = event.target.parentElement.getAttribute('data-path');
if (document.getElementById(path).classList.contains('clicked')){
var toggler = document.getElementById(path+"span");
if (toggler.classList.contains('files-tree-title')){
document.getElementById(path+"ul").classList.toggle("d-block");
document.getElementById(path+"span").classList.toggle("tree-caret-down");
}
return;
}else{
$.ajax({
type: "GET",
url: '/ajax/get_backup_dir?id='+server_id+'&path='+path,
dataType: 'text',
success: function(data){
console.log("got response:");
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
try{
document.getElementById(path+"span").classList.add('tree-caret-down');
document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked");
}catch{
console.log("Bad")
}
var toggler = document.getElementById(path);
if (toggler.classList.contains('files-tree-title')){
document.getElementById(path+"span").addEventListener("click", function caretListener() {
document.getElementById(path+"ul").classList.toggle("d-block");
document.getElementById(path+"span").classList.toggle("tree-caret-down");
});
}
},
});
}
}
function show_file_tree(){
$("#dir_select").modal();
}
</script>

View File

@ -1,7 +1,6 @@
{% extends ../base.html %}
{% block meta %}
<!-- <meta http-equiv="refresh" content="60">-->
{% end %}
{% block title %}Crafty Controller - {{ translate('serverDetails', 'serverDetails', data['lang']) }}{% end %}
@ -15,7 +14,8 @@
<div class="col-12">
<div class="page-header">
<h4 class="page-title">
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{ data['server_stats']['server_id']['server_name'] }}
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
</h4>
@ -25,14 +25,14 @@
</div>
<!-- Page Title Header Ends-->
{% include "parts/details_stats.html %}
{% include "parts/details_stats.html" %}
<div class="row">
<div class="col-sm-12 grid-margin">
<div class="card">
<div class="card-body pt-0">
{% include "parts/server_controls_list.html %}
{% include "parts/server_controls_list.html" %}
<div class="row">
<div class="col-md-6 col-sm-12">
@ -42,81 +42,135 @@
<input type="hidden" name="subpage" value="config">
<div class="form-group">
<label for="server_name">{{ translate('serverConfig', 'serverName', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverNameDesc', data['lang']) }}</small> </label>
<input type="text" class="form-control" name="server_name" id="server_name" value="{{ data['server_stats']['server_id']['server_name'] }}" placeholder="{{ translate('serverConfig', 'serverName', data['lang']) }}" >
<label for="server_name">{{ translate('serverConfig', 'serverName', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverNameDesc', data['lang']) }}</small>
</label>
<input type="text" class="form-control" name="server_name" id="server_name"
value="{{ data['server_stats']['server_id']['server_name'] }}"
placeholder="{{ translate('serverConfig', 'serverName', data['lang']) }}" required>
</div>
<div class="form-group">
<label for="server_path">{{ translate('serverConfig', 'serverPath', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPathDesc', data['lang']) }}</small> </label>
<input type="text" class="form-control" name="server_path" id="server_path" value="{{ data['server_stats']['server_id']['path'] }}" placeholder="{{ translate('serverConfig', 'serverPath', data['lang']) }}" >
{% if data['super_user'] %}
<label for="server_path">{{ translate('serverConfig', 'serverPath', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPathDesc', data['lang']) }}</small>
</label>
<input type="text" class="form-control" name="server_path" id="server_path"
value="{{ data['server_stats']['server_id']['path'] }}"
placeholder="{{ translate('serverConfig', 'serverPath', data['lang']) }}" required>
</div>
<div class="form-group">
<label for="log_path">{{ translate('serverConfig', 'serverLogLocation', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverLogLocationDesc', data['lang']) }}</small> </label>
<input type="text" class="form-control" name="log_path" id="log_path" value="{{ data['server_stats']['server_id']['log_path'] }}" placeholder="{{ translate('serverConfig', 'serverLogLocation', data['lang']) }}" >
<label for="log_path">{{ translate('serverConfig', 'serverLogLocation', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverLogLocationDesc', data['lang'])
}}</small> </label>
<input type="text" class="form-control" name="log_path" id="log_path"
value="{{ data['server_stats']['server_id']['log_path'] }}"
placeholder="{{ translate('serverConfig', 'serverLogLocation', data['lang']) }}" required>
</div>
<div class="form-group">
<label for="executable">{{ translate('serverConfig', 'serverExecutable', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverExecutableDesc', data['lang']) }}</small> </label>
<input type="text" class="form-control" name="executable" id="executable" value="{{ data['server_stats']['server_id']['executable'] }}" placeholder="{{ translate('serverConfig', 'serverExecutable', data['lang']) }}" >
<label for="executable">{{ translate('serverConfig', 'serverExecutable', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverExecutableDesc', data['lang'])
}}</small> </label>
<input type="text" class="form-control" name="executable" id="executable"
value="{{ data['server_stats']['server_id']['executable'] }}"
placeholder="{{ translate('serverConfig', 'serverExecutable', data['lang']) }}" required>
</div>
<div class="form-group">
<label for="execution_command">{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverExecutionCommandDesc', data['lang']) }}</small> </label>
<input type="text" class="form-control" name="execution_command" id="execution_command" value="{{ data['server_stats']['server_id']['execution_command'] }}" placeholder="{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }}" >
<label for="execution_command">{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }}
<small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverExecutionCommandDesc',
data['lang']) }}</small> </label>
<input type="text" class="form-control" name="execution_command" id="execution_command"
value="{{ data['server_stats']['server_id']['execution_command'] }}"
placeholder="{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }}" required>
{% end %}
</div>
<div class="form-group">
<label for="stop_command">{{ translate('serverConfig', 'serverStopCommand', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverStopCommandDesc', data['lang']) }}</small> </label>
<input type="text" class="form-control" name="stop_command" id="stop_command" value="{{ data['server_stats']['server_id']['stop_command'] }}" placeholder="{{ translate('serverConfig', 'serverStopCommand', data['lang']) }}" >
<label for="stop_command">{{ translate('serverConfig', 'serverStopCommand', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverStopCommandDesc', data['lang'])
}}</small> </label>
<input type="text" class="form-control" name="stop_command" id="stop_command"
value="{{ data['server_stats']['server_id']['stop_command'] }}"
placeholder="{{ translate('serverConfig', 'serverStopCommand', data['lang']) }}" required>
</div>
<div class="form-group">
<label for="auto_start_delay">{{ translate('serverConfig', 'serverAutostartDelay', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverAutostartDelayDesc', data['lang']) }}</small> </label>
<input type="number" class="form-control" name="auto_start_delay" id="auto_start_delay" value="{{ data['server_stats']['server_id']['auto_start_delay'] }}" step="1" max="999" min="10" >
<label for="auto_start_delay">{{ translate('serverConfig', 'serverAutostartDelay', data['lang']) }}
<small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverAutostartDelayDesc',
data['lang']) }}</small> </label>
<input type="number" class="form-control" name="auto_start_delay" id="auto_start_delay"
value="{{ data['server_stats']['server_id']['auto_start_delay'] }}" step="1" max="999" min="10"
required>
</div>
{% if data['super_user'] %}
<div class="form-group">
<label for="executable_update_url">{{ translate('serverConfig', 'exeUpdateURL', data['lang']) }}
<small class="text-muted ml-1"> - {{ translate('serverConfig', 'exeUpdateURLDesc', data['lang'])
}}</small> </label>
<input type="text" class="form-control" name="executable_update_url" id="executable_update_url"
value="{{ data['server_stats']['server_id']['executable_update_url'] }}"
placeholder="{{ translate('serverConfig', 'exeUpdateURL', data['lang']) }}">
</div>
<div class="form-group">
<label for="executable_update_url">{{ translate('serverConfig', 'exeUpdateURL', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'exeUpdateURLDesc', data['lang']) }}</small> </label>
<input type="text" class="form-control" name="executable_update_url" id="executable_update_url" value="{{ data['server_stats']['server_id']['executable_update_url'] }}" placeholder="{{ translate('serverConfig', 'exeUpdateURL', data['lang']) }}" >
<label for="server_ip">{{ translate('serverConfig', 'serverIP', data['lang']) }} <small
class="text-muted ml-1">- {{ translate('serverConfig', 'serverIPDesc', data['lang']) }}</small>
</label>
<input type="text" class="form-control" name="server_ip" id="server_ip"
value="{{ data['server_stats']['server_id']['server_ip'] }}" required>
</div>
<div class="form-group">
<label for="server_ip">{{ translate('serverConfig', 'serverIP', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPortDesc', data['lang']) }}</small> </label>
<input type="text" class="form-control" name="server_ip" id="server_ip" value="{{ data['server_stats']['server_id']['server_ip'] }}">
<label for="server_port">{{ translate('serverConfig', 'serverPort', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPortDesc', data['lang']) }}
</small> </label>
<input type="number" class="form-control" name="server_port" id="server_port"
value="{{ data['server_stats']['server_id']['server_port'] }}" step="1" max="65566" min="1"
required>
</div>
{% end %}
<div class="form-group">
<label for="server_port">{{ translate('serverConfig', 'serverPort', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverIPDesc', data['lang']) }}</small> </label>
<input type="number" class="form-control" name="server_port" id="server_port" value="{{ data['server_stats']['server_id']['server_port'] }}" step="1" max="65566" min="1" >
</div>
<div class="form-group">
<label for="logs_delete_after">{{ translate('serverConfig', 'removeOldLogsAfter', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'removeOldLogsAfterDesc', data['lang']) }}</small> </label>
<input type="number" class="form-control" name="logs_delete_after" id="logs_delete_after" value="{{ data['server_stats']['server_id']['logs_delete_after'] }}" step="1" max="365" min="0" >
<label for="logs_delete_after">{{ translate('serverConfig', 'removeOldLogsAfter', data['lang']) }}
<small class="text-muted ml-1"> - {{ translate('serverConfig', 'removeOldLogsAfterDesc',
data['lang']) }}</small> </label>
<input type="number" class="form-control" name="logs_delete_after" id="logs_delete_after"
value="{{ data['server_stats']['server_id']['logs_delete_after'] }}" step="1" max="365" min="0"
required>
</div>
<div class="form-check-flat">
<label for="auto_start" class="form-check-label ml-4 mb-4">
{% if data['server_stats']['server_id']['auto_start'] %}
<input type="checkbox" class="form-check-input" id="auto_start" name="auto_start" checked="" value="1">{{ translate('serverConfig', 'serverAutoStart', data['lang']) }}
<input type="checkbox" class="form-check-input" id="auto_start" name="auto_start" checked=""
value="1">{{ translate('serverConfig', 'serverAutoStart', data['lang']) }}
{% else %}
<input type="checkbox" class="form-check-input" id="auto_start" name="auto_start" value="1">{{ translate('serverConfig', 'serverAutoStart', data['lang']) }}
<input type="checkbox" class="form-check-input" id="auto_start" name="auto_start" value="1">{{
translate('serverConfig', 'serverAutoStart', data['lang']) }}
{% end %}
</label>
<label for="crash_detection" class="form-check-label ml-4 mb-4">
{% if data['server_stats']['server_id']['crash_detection'] %}
<input type="checkbox" class="form-check-input" id="crash_detection" name="crash_detection" checked="" value="1">{{ translate('serverConfig', 'serverCrashDetection', data['lang']) }}
<input type="checkbox" class="form-check-input" id="crash_detection" name="crash_detection"
checked="" value="1">{{ translate('serverConfig', 'serverCrashDetection', data['lang']) }}
{% else %}
<input type="checkbox" class="form-check-input" id="crash_detection" name="crash_detection" value="1" >{{ translate('serverConfig', 'serverCrashDetection', data['lang']) }}
<input type="checkbox" class="form-check-input" id="crash_detection" name="crash_detection"
value="1">{{ translate('serverConfig', 'serverCrashDetection', data['lang']) }}
{% end %}
</label>
</div>
<button type="submit" class="btn btn-success mr-2"><i class="fas fa-save"></i> {{ translate('serverConfig', 'save', data['lang']) }}</button>
<button type="reset" class="btn btn-light"><i class="fas fa-times"></i> {{ translate('serverConfig', 'cancel', data['lang']) }}</button>
<button type="submit" class="btn btn-success mr-2"><i class="fas fa-save"></i> {{
translate('serverConfig', 'save', data['lang']) }}</button>
<button type="reset" class="btn btn-light"><i class="fas fa-times"></i> {{ translate('serverConfig',
'cancel', data['lang']) }}</button>
</form>
</div>
@ -134,12 +188,18 @@
</div>
<div class="text-center">
{% if data['server_stats']['running'] %}
<button onclick="send_command(server_id, 'update_executable');" id="update_executable" style="max-width: 7rem;" class="btn btn-warning m-1 flex-grow-1 disabled">{{ translate('serverConfig', 'update', data['lang']) }}</button>
<a class="btn btn-sm btn-danger disabled">{{ translate('serverConfig', 'deleteServer', data['lang']) }}</a><br />
<button onclick="send_command(serverId, 'update_executable');" id="update_executable"
style="max-width: 7rem;" class="btn btn-warning m-1 flex-grow-1 disabled">{{ translate('serverConfig',
'update', data['lang']) }}</button>
<a class="btn btn-sm btn-danger disabled">{{ translate('serverConfig', 'deleteServer', data['lang'])
}}</a><br />
<small>{{ translate('serverConfig', 'stopBeforeDeleting', data['lang']) }}</small>
{% else %}
<button onclick="send_command(server_id, 'update_executable');" id="update_executable" style="max-width: 7rem;" class="btn btn-warning m-1 flex-grow-1">{{ translate('serverConfig', 'update', data['lang']) }}</button>
<button onclick="deleteConfirm()" class="btn btn-sm btn-danger">{{ translate('serverConfig', 'deleteServer', data['lang']) }}</button>
<button onclick="send_command(serverId, 'update_executable');" id="update_executable"
style="max-width: 7rem;" class="btn btn-warning m-1 flex-grow-1">{{ translate('serverConfig',
'update', data['lang']) }}</button>
<button onclick="deleteConfirm()" class="btn btn-sm btn-danger">{{ translate('serverConfig',
'deleteServer', data['lang']) }}</button>
{% end %}
</div>
@ -161,6 +221,8 @@
{% block js %}
<script>
const serverId = new URLSearchParams(document.location.search).get('id')
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
function getCookie(name) {
@ -178,7 +240,7 @@
$.ajax({
type: "DELETE",
headers: { 'X-XSRFToken': token },
url: '/ajax/delete_server?id={{ data['server_stats']['server_id']['server_id'] }}',
url: '/ajax/delete_server?id=' + serverId,
data: {
},
success: function (data) {
@ -192,7 +254,7 @@
$.ajax({
type: "DELETE",
headers: { 'X-XSRFToken': token },
url: '/ajax/delete_server_files?id={{ data['server_stats']['server_id']['server_id'] }}',
url: '/ajax/delete_server_files?id=' + serverId,
data: {
},
success: function (data) {
@ -202,21 +264,18 @@
});
}
let server_id = '{{ data['server_stats']['server_id']['server_id'] }}';
function send_command (server_id, command){
<!-- this getCookie function is in base.html-->
function send_command(serverId, command) {
//<!-- this getCookie function is in base.html-->
var token = getCookie("_xsrf");
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/server/command?command=' + command + '&id=' + server_id,
url: '/server/command?command=' + command + '&id=' + serverId,
success: function (data) {
console.log("got response:");
console.log(data);
setTimeout(function () { location.reload(); }, 10000);
}
});
if (command != "delete_server" && command != "delete_server_files") {
@ -231,34 +290,16 @@ let server_id = '{{ data['server_stats']['server_id']['server_id'] }}';
function deleteServer() {
path = "{{data['server_stats']['server_id']['path']}}";
name = "{{data['server_stats']['server_id']['server_name']}}";
bootbox.confirm({
bootbox.dialog({
size: "",
title: "{% raw translate('serverConfig', 'deleteFilesQuestion', data['lang']) %}",
closeButton: false,
message: "{% raw translate('serverConfig', 'deleteFilesQuestionMessage', data['lang']) %}",
buttons: {
confirm: {
files: {
label: "{{ translate('serverConfig', 'yesDeleteFiles', data['lang']) }}",
className: 'btn-danger',
},
cancel: {
label: "{{ translate('serverConfig', 'noDeleteFiles', data['lang']) }}",
className: 'btn-link',
}
},
callback: function(result) {
if (!result){
deleteServerE()
setTimeout(function(){ window.location = '/panel/dashboard'; }, 5000);
bootbox.dialog({
backdrop: true,
title: '{% raw translate("serverConfig", "sendingDelete", data['lang']) %}',
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("serverConfig", "bePatientDelete", data['lang']) %} </div>',
closeButton: false
})
return;}
else{
callback: function () {
deleteServerFilesE();
setTimeout(function () { window.location = '/panel/dashboard'; }, 5000);
bootbox.dialog({
@ -267,7 +308,34 @@ let server_id = '{{ data['server_stats']['server_id']['server_id'] }}';
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("serverConfig", "bePatientDeleteFiles", data['lang']) %} </div>',
closeButton: false
})
return;
}
},
noFiles: {
label: "{{ translate('serverConfig', 'noDeleteFiles', data['lang']) }}",
className: 'btn-outline-danger',
callback: function () {
deleteServerE()
setTimeout(function () { window.location = '/panel/dashboard'; }, 5000);
bootbox.dialog({
backdrop: true,
title: '{% raw translate("serverConfig", "sendingDelete", data['lang']) %}',
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> &nbsp; {% raw translate("serverConfig", "bePatientDelete", data['lang']) %} </div>',
closeButton: false
})
return;
}
},
cancel: {
label: "{{ translate('serverConfig', 'cancel', data['lang']) }}",
className: 'btn-secondary',
callback: function () {
return;
}
}
},
callback: function (result) {
}
});
@ -293,7 +361,8 @@ let server_id = '{{ data['server_stats']['server_id']['server_id'] }}';
callback: function (result) {
if (!result) {
return;
return;}
return;
}
else {
deleteServer();
}

View File

@ -1,7 +1,6 @@
{% extends ../base.html %}
{% block meta %}
<!-- <meta http-equiv="refresh" content="60">-->
{% end %}
{% block title %}Crafty Controller - {{ translate('serverDetails', 'serverDetails', data['lang']) }}{% end %}
@ -25,14 +24,14 @@
</div>
<!-- Page Title Header Ends-->
{% include "parts/details_stats.html %}
{% include "parts/details_stats.html" %}
<div class="row">
<div class="col-sm-12 grid-margin">
<div class="card">
<div class="card-body pt-0">
{% include "parts/server_controls_list.html %}
{% include "parts/server_controls_list.html" %}
<div class="row">
<div class="col-md-6 col-sm-12">
@ -134,13 +133,15 @@
</style>
<ul class="tree-view">
<li>
<div class="tree-caret tree-ctx-item files-tree-title">
<div id="root_dir" class="tree-ctx-item" data-path="{{ data['server_stats']['server_id']['path'] }}">
<span id="{{ data['server_stats']['server_id']['path'] }}span" class="files-tree-title tree-caret-down root-dir" data-path="{{ data['server_stats']['server_id']['path'] }}" onclick="getToggleMain(event)">
<i class="far fa-folder"></i>
<i class="far fa-folder-open"></i>
{{ translate('serverFiles', 'files', data['lang']) }}
</span>
</div>
<ul class="tree-nested" id="files-tree">
<li>{{ translate('serverFiles', 'error', data['lang']) }}</li>
<ul class="tree-nested d-block" id="files-tree">
<li><i class="fa fa-spin fa-spinner"></i>{{ translate('serverFiles', 'loadingRecords', data['lang']) }}</li>
</ul>
</li>
@ -220,6 +221,8 @@
<script>
const serverId = new URLSearchParams(document.location.search).get('id')
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
function getCookie(name) {
@ -334,7 +337,7 @@
filePath = event.target.getAttribute('data-path');
$.ajax({
type: 'GET',
url: '/ajax/get_file?id={{ data['server_stats']['server_id']['server_id'] }}&file_path=' + encodeURIComponent(filePath),
url: "/files/get_file?id=" + serverId + "&file_path=" + encodeURIComponent(filePath),
dataType: 'text',
success: function (data) {
console.log('Got File Contents From Server');
@ -412,7 +415,7 @@
$.ajax({
type: "PUT",
headers: {'X-XSRFToken': token},
url: '/ajax/save_file?id={{ data['server_stats']['server_id']['server_id'] }}',
url: "/files/save_file?id=" + serverId,
data: {
file_contents: text,
file_path: filePath
@ -429,7 +432,7 @@
$.ajax({
type: "POST",
headers: {'X-XSRFToken': token},
url: '/ajax/create_file?id={{ data['server_stats']['server_id']['server_id'] }}',
url: "/files/create_file?id=" + serverId,
data: {
file_parent: parent,
file_name: name
@ -447,7 +450,7 @@
$.ajax({
type: "POST",
headers: {'X-XSRFToken': token},
url: '/ajax/create_dir?id={{ data['server_stats']['server_id']['server_id'] }}',
url: "/files/create_dir?id=" + serverId,
data: {
dir_parent: parent,
dir_name: name
@ -463,9 +466,9 @@
function renameItem(path, name, callback) {
var token = getCookie("_xsrf")
$.ajax({
type: "PUT",
type: "PATCH",
headers: {'X-XSRFToken': token},
url: '/ajax/rename_item?id={{ data['server_stats']['server_id']['server_id'] }}',
url: "/files/rename_file?id=" + serverId,
data: {
item_path: path,
new_item_name: name
@ -484,7 +487,7 @@
$.ajax({
type: "DELETE",
headers: {'X-XSRFToken': token},
url: '/ajax/del_file?id={{ data['server_stats']['server_id']['server_id'] }}',
url: "/files/del_file?id=" + serverId,
data: {
file_path: path
},
@ -501,7 +504,7 @@
$.ajax({
type: "DELETE",
headers: {'X-XSRFToken': token},
url: '/ajax/del_dir?id={{ data['server_stats']['server_id']['server_id'] }}',
url: "/files/del_dir?id=" + serverId,
data: {
dir_path: path
},
@ -518,19 +521,19 @@
$.ajax({
type: "POST",
headers: {'X-XSRFToken': token},
url: '/ajax/unzip_file?id={{ data['server_stats']['server_id']['server_id'] }}',
url: "/files/unzip_file?id=" + serverId,
data: {
path: path
},
});
window.location.href = "/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=files"
window.location.href = "/panel/server_detail?id=" + serverId + "&subpage=files"
}
function sendFile(file, path, server_id, left, onProgress){
function sendFile(file, path, serverId, left, onProgress){
var xmlHttpRequest = new XMLHttpRequest();
var token = getCookie("_xsrf")
var fileName = file.name
var target = '/upload?server_id=' + server_id
var target = '/upload?server_id=' + serverId
var mimeType = file.type
xmlHttpRequest.open('POST', target, true);
@ -540,7 +543,7 @@
xmlHttpRequest.setRequestHeader('X-Path', path);
xmlHttpRequest.setRequestHeader('X-Files-Left', left);
xmlHttpRequest.setRequestHeader('X-FileName', fileName);
xmlHttpRequest.setRequestHeader('X-ServerId', "{{ data['server_stats']['server_id']['server_id'] }}");
xmlHttpRequest.setRequestHeader('X-ServerId', serverId);
xmlHttpRequest.upload.addEventListener('progress', (event) =>
onProgress(Math.floor(event.loaded / event.total * 100)), false);
xmlHttpRequest.addEventListener('load', (event) => {
@ -566,7 +569,6 @@
path = event.target.parentElement.getAttribute('data-path');
console.log("PATH: " + path);
$(function () {
server_id = {{ data['server_stats']['server_id']['server_id'] }};
var uploadHtml = "<div>" +
'<form id="upload_file" enctype="multipart/form-data">'+"<label class='upload-area' style='width:100%;text-align:center;' for='files'>" +
"<input id='files' name='files' type='file' style='display:none;' multiple='true'>" +
@ -623,7 +625,7 @@
`;
$('#upload-progress-bar-parent').append(progressHtml);
console.log(files.files.length)
sendFile(files.files[i], path, server_id, files.files.length - i - 1, (progress) => {
sendFile(files.files[i], path, serverId, files.files.length - i - 1, (progress) => {
$(`#upload-progress-bar-${i + 1}`).attr('aria-valuenow', progress)
$(`#upload-progress-bar-${i + 1}`).css('width', progress + '%')
});
@ -635,8 +637,6 @@
}
});
var fileList = document.getElementById("files");
fileList.addEventListener("change", function (e) {
var list = "";
@ -648,10 +648,13 @@
}, false);
});
}
function getTreeView() {
function getTreeView(event) {
const path = $('#root_dir').data('path');;
$.ajax({
type: "GET",
url: '/ajax/get_tree?id={{ data['server_stats']['server_id']['server_id'] }}',
url: "/files/get_tree?id=" + serverId + "&path=" + path,
dataType: 'text',
success: function(data){
console.log("got response:");
@ -661,26 +664,75 @@
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
try{
document.getElementById(path).innerHTML += text;
event.target.parentElement.classList.add("clicked");
}catch{
document.getElementById('files-tree').innerHTML = text;
}
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-path', serverDir);
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-name', 'Files');
setTimeout(function () {setTreeViewContext()}, 1000);
},
});
}
var toggler = document.getElementsByClassName("tree-caret");
var i;
function getToggleMain(event) {
path = event.target.parentElement.getAttribute('data-path');
document.getElementById("files-tree").classList.toggle("d-block");
document.getElementById(path+"span").classList.toggle("tree-caret-down");
document.getElementById(path+"span").classList.toggle("tree-caret");
}
for (i = 0; i < toggler.length; i++) {
if (toggler[i].classList.contains('files-tree-title')) continue;
toggler[i].addEventListener("click", function caretListener() {
this.parentElement.querySelector(".tree-nested").classList.toggle("d-block");
this.classList.toggle("tree-caret-down");
function getDirView(event) {
let path = event.target.parentElement.getAttribute('data-path');
if (document.getElementById(path).classList.contains('clicked')) {
var toggler = document.getElementById(path+"span");
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path+"ul").classList.toggle("d-block");
document.getElementById(path+"span").classList.toggle("tree-caret-down");
}
return;
} else {
$.ajax({
type: "GET",
url: "/files/get_dir?id=" + serverId + "&path=" + path,
dataType: 'text',
success: function(data) {
console.log("got response:");
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
try {
document.getElementById(path+"span").classList.add('tree-caret-down');
document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked");
} catch {
console.log("Bad")
}
setTimeout(function () {setTreeViewContext()}, 1000);
var toggler = document.getElementById(path);
if (toggler.classList.contains('files-tree-title')){
document.getElementById(path+"span").addEventListener("click", function caretListener() {
document.getElementById(path+"ul").classList.toggle("d-block");
document.getElementById(path+"span").classList.toggle("tree-caret-down");
});
}
},
});
}
}
function setTreeViewContext() {
var treeItems = document.getElementsByClassName('tree-ctx-item');
@ -697,7 +749,7 @@
}
$('#renameItem').show();
var isDir = event.target.classList.contains('tree-folder');
var isDir = event.target.classList.contains('files-tree-title');
$('#createFile').toggle(isDir);
$('#createDir').toggle(isDir);
$('#deleteDir').toggle(isDir);
@ -708,7 +760,7 @@
$('#downloadFile').toggle(isFile);
console.log({ 'event.target': event.target, isDir, isFile });
if(event.target.classList.contains('files-tree-title')) {
if (event.target.classList.contains('root-dir')) {
$('#createFile').show();
$('#createDir').show();
$('#renameItem').hide();
@ -721,7 +773,8 @@
$('#unzip').show();
console.log(event.target.textContent)
} else {
$('#unzip').hide();}
$('#unzip').hide();
}
var clientX = event.clientX;
var clientY = event.clientY;
@ -796,7 +849,7 @@
function downloadFileE(event) {
path = event.target.parentElement.getAttribute('data-path');
name = event.target.parentElement.getAttribute('data-name');
window.location.href = '/panel/download_file?id={{ data['server_stats']['server_id']['server_id'] }}&path='+path+'&name='+name;
window.location.href = `/panel/download_file?id=${serverId}&path=${path}&name=${name}`;
}
function renameItemE(event) {
@ -875,11 +928,6 @@
});
}
document.getElementsByClassName('files-tree-title')[0].addEventListener("click", function caretListener() {
this.parentElement.querySelector(".tree-nested").classList.toggle("d-block");
this.classList.toggle("tree-caret-down");
});
getTreeView();
setTreeViewContext();
@ -898,6 +946,8 @@
target.classList.add('btn-primary');
}
</script>
{% end %}

View File

@ -1,7 +1,6 @@
{% extends ../base.html %}
{% block meta %}
<!-- <meta http-equiv="refresh" content="60">-->
{% end %}
{% block title %}Crafty Controller - {{ translate('serverDetails', 'serverDetails', data['lang']) }}{% end %}
@ -25,14 +24,14 @@
</div>
<!-- Page Title Header Ends-->
{% include "parts/details_stats.html %}
{% include "parts/details_stats.html" %}
<div class="row">
<div class="col-sm-12 grid-margin">
<div class="card">
<div class="card-body pt-0">
{% include "parts/server_controls_list.html %}
{% include "parts/server_controls_list.html" %}
<div class="col-md-12">
<div class="input-group">
@ -56,11 +55,13 @@
{% block js %}
<script>
const serverId = new URLSearchParams(document.location.search).get('id')
function get_server_log(){
if( !$("#stop_scroll").is(':checked')){
$.ajax({
type: 'GET',
url: '/ajax/server_log?id={{ data['server_stats']['server_id']['server_id'] }}&full=1',
url: '/ajax/server_log?id=' + serverId + '&full=1',
dataType: 'text',
success: function (data) {
console.log('Got Log From Server')

View File

@ -0,0 +1,267 @@
{% extends ../base.html %}
{% block meta %}
{% end %}
{% block title %}Crafty Controller - {{ translate('serverDetails', 'serverDetails', data['lang']) }}{% end %}
{% block content %}
<div class="content-wrapper">
<!-- Page Title Header Starts-->
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<h4 class="page-title">
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{ data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
</h4>
</div>
</div>
</div>
<!-- Page Title Header Ends-->
{% include "parts/details_stats.html" %}
<div class="row">
<div class="col-sm-12 grid-margin">
<div class="card">
<div class="card-body pt-0">
{% include "parts/server_controls_list.html" %}
<div class="row">
<div class="col-md-8 col-sm-8">
{% if data['new_schedule'] == True %}
<form class="forms-sample" method="post" action="/panel/new_schedule?id={{ data['server_stats']['server_id']['server_id'] }}">
{% else %}
<form class="forms-sample" method="post" action="/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{ data['schedule']['schedule_id'] }}">
{% end %}
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['server_stats']['server_id']['server_id'] }}">
<input type="hidden" name="subpage" value="config">
<div class="form-group">
<label for="difficulty">Basic / Cron / Chain-Reaction Select<small class="text-muted ml-1"></small> </label><br>
<select id="difficulty" name="difficulty" onchange="basicAdvanced(this);" class="form-control form-control-lg select-css" value="{{ data['schedule']['difficulty'] }}">
<option id="basic" value="basic">{{ translate('serverScheduleConfig', 'basic' , data['lang']) }}</option>
<option id="advanced" value="advanced">{{ translate('serverScheduleConfig', 'cron' , data['lang']) }}</option>
<option id="reaction" value="reaction">{{ translate('serverScheduleConfig', 'reaction' , data['lang']) }}</option>
</select>
</div>
<div class="form-group">
<label for="server_name">Action<small class="text-muted ml-1"></small> </label><br>
<select id="action" name="action" onchange="yesnoCheck(this);" class="form-control form-control-lg select-css" value="{{ data['schedule']['action'] }}">
<option id="start" value="start">{{ translate('serverScheduleConfig', 'start' , data['lang']) }}</option>
<option id="restart" value="restart">{{ translate('serverScheduleConfig', 'restart' , data['lang']) }}</option>
<option id="stop" value="stop">{{ translate('serverScheduleConfig', 'stop' , data['lang']) }}</option>
<option id="backup" value="backup">{{ translate('serverScheduleConfig', 'backup' , data['lang']) }}</option>
<option id="command" value="command">{{ translate('serverScheduleConfig', 'custom' , data['lang']) }}</option>
</select>
</div>
<div id="ifBasic">
<div class="form-group">
<label for="server_path">{{ translate('serverScheduleConfig', 'interval' , data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverScheduleConfig', 'interval-explain' , data['lang']) }}</small> </label>
<input type="number" class="form-control" name="interval" id="interval" value="{{ data['schedule']['interval'] }}" placeholder="Interval" required>
<br>
<br>
<select id="interval_type" onchange="ifDays(this);" name="interval_type" class="form-control form-control-lg select-css" value="{{ data['schedule']['interval_type'] }}">
<option id = "days" value="days">{{ translate('serverScheduleConfig', 'days' , data['lang']) }}</option>
<option id = "hours" value="hours">{{ translate('serverScheduleConfig', 'hours' , data['lang']) }}</option>
<option id = "minutes" value="minutes">{{ translate('serverScheduleConfig', 'minutes' , data['lang']) }}</option>
</select>
</div>
<div id="ifDays" style="display: block;">
<div class="form-group">
<label for="time">{{ translate('serverScheduleConfig', 'time' , data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverScheduleConfig', 'time-explain' , data['lang']) }}</small> </label>
<input type="time" class="form-control" name="time" id="time" value="{{ data['schedule']['time'] }}" placeholder="Time" required>
</div>
</div>
</div>
<div id="ifYes" style="display: none;">
<div class="form-group">
<label for="command">{{ translate('serverScheduleConfig', 'command' , data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverScheduleConfig', 'command-explain' , data['lang']) }}</small> </label>
<input type="input" class="form-control" name="command" id="command_input" value="{{ data['schedule']['command'] }}" placeholder="Command" required>
</div>
</div>
<div id="ifAdvanced" style="display: none;">
<div class="form-group">
<label for="cron">{{ translate('serverScheduleConfig', 'cron' , data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverScheduleConfig', 'cron-explain' , data['lang']) }}</small> </label>
<input type="input" class="form-control" name="cron" id="cron" value="{{ data['schedule']['cron_string'] }}" placeholder="* * * * *">
</div>
</div>
<div id="ifReaction" style="display: none;">
<div class="form-group">
<label for="delay">{{ translate('serverScheduleConfig', 'offset' , data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverScheduleConfig', 'offset-explain' , data['lang']) }}</small> </label>
<input type="number" class="form-control" name="delay" id="delay" value="0">
<br>
<br>
<label for="parent">{{ translate('serverScheduleConfig', 'parent' , data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverScheduleConfig', 'parent-explain' , data['lang']) }}</small> </label>
<select id="parent" name="parent" class="form-control form-control-lg select-css" value="{{ data['schedule']['action'] }}">
{% for schedule in data['schedules'] %}
{% if schedule.schedule_id != data['schedule']['schedule_id'] %}
{% if schedule.interval != '' %}
<option id="{{schedule.schedule_id}}" value="{{schedule.schedule_id}}">ID: {{schedule.schedule_id}} | {{schedule.command}} | {{schedule.interval}} {{ schedule.interval_type}}</option>
{% else %}
<option id="{{schedule.schedule_id}}" value="{{schedule.schedule_id}}">ID: {{schedule.schedule_id}} {{schedule.command}} {{schedule.cron_string}}</option>
{% end %}
{% end %}
{% end %}
</select>
</div>
</div>
<div class="form-check-flat">
<label for="enabled" class="form-check-label ml-4 mb-4">
<input type="checkbox" class="form-check-input" id="enabled" name="enabled" checked="" value="1">{{ translate('serverScheduleConfig', 'enabled' , data['lang']) }}
</label>
</div>
<div class="form-check-flat">
<label for="one_time" class="form-check-label ml-4 mb-4">
<input type="checkbox" class="form-check-input" id="one_time" name="one_time" value="1">{{ translate('serverScheduleConfig', 'one-time' , data['lang']) }}
</label>
</div>
<button type="submit" class="btn btn-success mr-2"><i class="fas fa-save"></i> {{ translate('serverConfig', 'save', data['lang']) }}</button>
<button type="reset" onclick="location.href=`/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=schedules`" class="btn btn-light"><i class="fas fa-times"></i> {{ translate('serverConfig', 'cancel', data['lang']) }}</button>
</form>
</div>
<div class="col-sm-4 grid-margin">
<h4>{{ translate('serverScheduleConfig', 'children' , data['lang']) }}</h4>
<ul>
{% for schedule in data['schedule']['children'] %}
<li style="overflow: auto;"><a href="/panel/edit_schedule?id={{schedule.server_id}}&sch_id={{schedule.schedule_id}}">{{schedule.schedule_id}} | {{schedule.command}}</a></li>
{% end %}
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- content-wrapper ends -->
{% end %}
{% block js %}
<script>
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
$( document ).ready(function() {
console.log( "ready!" );
});
function yesnoCheck() {
if (document.getElementById('action').value == "command") {
document.getElementById("ifYes").style.display = "block";
document.getElementById("command_input").required = true;
} else {
document.getElementById("ifYes").style.display = "none";
document.getElementById("command_input").required = false;
}
}
function basicAdvanced() {
if (document.getElementById('difficulty').value == "advanced") {
document.getElementById("ifAdvanced").style.display = "block";
document.getElementById("ifReaction").style.display = "none";
document.getElementById("ifBasic").style.display = "none";
document.getElementById("delay").required = false;
document.getElementById("parent").required = false;
document.getElementById("interval").required = false;
document.getElementById("time").required = false;
} else if(document.getElementById('difficulty').value == "reaction"){
document.getElementById("ifReaction").style.display = "block";
document.getElementById("ifBasic").style.display = "none";
document.getElementById("ifAdvanced").style.display = "none";
document.getElementById("delay").required = true;
document.getElementById("parent").required = true;
document.getElementById("interval").required = false;
document.getElementById("time").required = false;
}
else {
document.getElementById("ifAdvanced").style.display = "none";
document.getElementById("ifReaction").style.display = "none";
document.getElementById("ifBasic").style.display = "block";
document.getElementById("delay").required = false;
document.getElementById("parent").required = false;
document.getElementById("interval").required = true;
document.getElementById("time").required = true;
}
}
function ifDays() {
if (document.getElementById('interval_type').value == "days") {
document.getElementById("ifDays").style.display = "block";
document.getElementById("time").required = true;
} else {
document.getElementById("ifDays").style.display = "none";
document.getElementById("time").required = false;
}
}
function del_task(sch_id, id){
var token = getCookie("_xsrf")
$.ajax({
type: "DELETE",
headers: {'X-XSRFToken': token},
url: '/ajax/del_task?server_id='+id+'&schedule_id='+sch_id,
data: {
schedule_id: sch_id,
id: id
},
success: function(data) {
location.reload();
},
});
}
function startup(){
try{
document.getElementById("{{ data['schedule']['interval_type'] }}").setAttribute('selected', true);
}catch{
console.log("no element named")
}
try{
document.getElementById("{{ data['schedule']['difficulty'] }}").setAttribute('selected', true);
}catch{
console.log("no element named")
}
try{
document.getElementById("{{ data['schedule']['action'] }}").setAttribute('selected', true);
}catch{
console.log("no element named")
}
ifDays();
yesnoCheck();
basicAdvanced();
if("{{ data['schedule']['enabled'] }}" == 'True'){
document.getElementById('enabled').checked = true;
}else{
document.getElementById('enabled').checked = false;
}
if("{{ data['schedule']['one_time'] }}" == 'True'){
document.getElementById('one_time').checked = true;
}else{
document.getElementById('one_time').checked = false;
}
}
window.onload(startup())
</script>
{% end %}

View File

@ -0,0 +1,374 @@
{% extends ../base.html %}
{% block meta %}
{% end %}
{% block title %}Crafty Controller - {{ translate('serverDetails', 'serverDetails', data['lang']) }}{% end %}
{% block content %}
<div class="content-wrapper">
<!-- Page Title Header Starts-->
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<h4 class="page-title">
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{ data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
</h4>
</div>
</div>
</div>
<!-- Page Title Header Ends-->
{% include "parts/details_stats.html %}
<div class="row">
<div class="col-sm-12 grid-margin">
<div class="card">
<div class="card-body pt-0">
{% include "parts/server_controls_list.html %}
<div class="row">
<div class="col-md-12 col-sm-12" style="overflow-x:auto;">
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fas fa-calendar"></i> Scheduled Tasks</h4>
<span class="too_small" title="{{ translate('serverSchedules', 'cannotSee', data['lang']) }}", data-content="{{ translate('serverSchedules', 'cannotSeeOnMobile', data['lang']) }}", data-placement="bottom"></span>
<div><button onclick="location.href=`/panel/add_schedule?id={{ data['server_stats']['server_id']['server_id'] }}`" class="btn btn-info">Create New Schedule <i class="fas fa-pencil-alt"></i></button></div>
</div>
<div class="card-body">
<table class="table table-hover d-none d-lg-block responsive-table" id="schedule_table" width="100%" style="table-layout:fixed;">
<thead>
<tr class="rounded">
<th style="width: 2%; min-width: 10px;">ID</th>
<th style="width: 23%; min-width: 50px;">Action</th>
<th style="width: 40%; min-width: 50px;">Command</th>
<th style="width: 10%; min-width: 50px;">Interval</th>
<th style="width: 10%; min-width: 50px;">Start Time</th>
<th style="width: 10%; min-width: 50px;">Enabled</th>
<th style="width: 10%; min-width: 50px;">Edit</th>
</tr>
</thead>
<tbody>
{% for schedule in data['schedules'] %}
<tr>
<td id="{{schedule.schedule_id}}" class="id">
<p>{{schedule.schedule_id}}</p>
</td>
<td id="{{schedule.action}}" class="action">
<p>{{schedule.action}}</p>
</td>
<td id="{{schedule.command}}" class="action" style="overflow: scroll; max-width: 30px;">
<p>{{schedule.command}}</p>
</td>
<td id="{{schedule.interval}}" class="action">
{% if schedule.interval != '' %}
<p>Every</p>
<p>{{schedule.interval}} {{schedule.interval_type}}</p>
{% elif schedule.interval_type == 'reaction' %}
<p>{{schedule.interval_type}}<br><br>child of ID: {{ schedule.parent }}</p>
{% else %}
<p>Cron String:</p>
<p>{{schedule.cron_string}}</p>
{% end %}
</td>
<td id="{{schedule.start_time}}" class="action">
<p>{{schedule.start_time}}</p>
</td>
<td id="{{schedule.enabled}}" class="action">
{% if schedule.enabled %}
<span class="text-success">
<i class="fas fa-check-square"></i> Yes
</span>
{% else %}
<span class="text-danger">
<i class="far fa-times-square"></i> No
</span>
{% end %}
</td>
<td id="{{schedule.action}}" class="action">
<button onclick="window.location.href='/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{schedule.schedule_id}}'" class="btn btn-info">
<i class="fas fa-pencil-alt"></i>
</button>
<br>
<br>
<button data-sch={{ schedule.schedule_id }} class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i>
</button>
</td>
</tr>
{% end %}
</tbody>
</table>
<hr />
<table class="table table-hover d-block d-lg-none" id="mini_schedule_table" width="100%" style="table-layout:fixed;">
<thead>
<tr class="rounded">
<th style="width: 25%; min-width: 50px;">Action</th>
<th style="max-width: 40%; min-width: 50px;">Command</th>
<th style="width: 10%; min-width: 50px;">Enabled</th>
</tr>
</thead>
<tbody>
{% for schedule in data['schedules'] %}
<tr data-toggle="modal" data-target="#task_details_{{schedule.schedule_id}}">
<td id="{{schedule.action}}" class="action">
<p>{{schedule.action}}</p>
</td>
<td id="{{schedule.command}}" class="action" style="overflow: scroll; max-width: 30px;">
<p>{{schedule.command}}</p>
</td>
<td id="{{schedule.enabled}}" class="action">
{% if schedule.enabled %}
<span class="text-success">
<i class="fas fa-check-square"></i> Yes
</span>
{% else %}
<span class="text-danger">
<i class="far fa-times-square"></i> No
</span>
{% end %}
</td>
</tr>
<!-- Modal -->
<div class="modal fade" id="task_details_{{schedule.schedule_id}}" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Task Details</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<ul style="list-style: none;">
<li id="{{schedule.schedule_id}}" class="id" style="border-top: .1em solid gray;">
<h4>ID</h4><p>{{schedule.schedule_id}}</p>
</li>
<li id="{{schedule.action}}" class="action" style="border-top: .1em solid gray;">
<h4>Action</h4><p>{{schedule.action}}</p>
</li>
<li id="{{schedule.command}}" class="action" style="border-top: .1em solid gray;">
<h4>Command</h4><p>{{schedule.command}}</p>
</li>
<li id="{{schedule.interval}}" class="action" style="border-top: .1em solid gray;">
{% if schedule.interval != '' %}
<h4>Interval</h4> <p>Every {{schedule.interval}} {{schedule.interval_type}}</p>
{% elif schedule.interval_type == 'reaction' %}
<h4>Interval</h4> <p>{{schedule.interval_type}}<br><br>child of ID: {{ schedule.parent }}</p>
{% else %}
<h4>Interval</h4> <p>Cron String: {{schedule.cron_string}}</p>
{% end %}
</li>
<li id="{{schedule.start_time}}" class="action" style="border-top: .1em solid gray;">
<h4>Start Time</h4> <p>{{schedule.start_time}}</p>
</li>
<li id="{{schedule.enabled}}" class="action" style="border-top: .1em solid gray; border-bottom: .1em solid gray">
{% if schedule.enabled %}
<h4>Enabled</h4> <span class="text-success">
<i class="fas fa-check-square"></i> Yes
</span>
{% else %}
<h4>Enabled</h4> <span class="text-danger">
<i class="far fa-times-square"></i> No
</span>
{% end %}
</li>
</ul>
</div>
<div class="modal-footer">
<button onclick="window.location.href='/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{schedule.schedule_id}}'" class="btn btn-info">
<i class="fas fa-pencil-alt"></i> Edit
</button>
<button data-sch={{ schedule.schedule_id }} class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i> Delete
</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% end %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.popover-body{
color: white !important;;
}
</style>
</div>
<style>
/* Hide scrollbar for Chrome, Safari and Opera */
td::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
td {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
</style>
<!-- content-wrapper ends -->
{% end %}
{% block js %}
<script>
const serverId = new URLSearchParams(document.location.search).get('id')
$( document ).ready(function() {
console.log('ready for JS!')
$('#schedule_table').DataTable({
'order': [4, 'desc']
}
);
});
$( document ).ready(function() {
console.log('ready for JS!')
$('#mini_schedule_table').DataTable({
'order': [2, 'desc']
}
);
document.getElementById('mini_schedule_table_wrapper').hidden = true;
});
$(document).ready(function(){
$('[data-toggle="popover"]').popover();
if($(window).width() < 1000){
$('.too_small').popover("show");
document.getElementById('schedule_table_wrapper').hidden = true;
document.getElementById('mini_schedule_table_wrapper').hidden = false;
}
});
$(window).ready(function(){
$('body').click(function(){
$('.too_small').popover("hide");
});
});
$(window).resize(function() {
// This will execute whenever the window is resized
if($(window).width() < 1000){
$('.too_small').popover("show");
document.getElementById('schedule_table_wrapper').hidden = true;
document.getElementById('mini_schedule_table_wrapper').hidden = false;
}
else{
$('.too_small').popover("hide");
document.getElementById('schedule_table_wrapper').hidden = false;
document.getElementById('mini_schedule_table_wrapper').hidden = true;
} // New width
});
</script>
<script>
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
$( document ).ready(function() {
console.log( "ready!" );
});
function yesnoCheck(that) {
if (that.value == "command") {
document.getElementById("ifYes").style.display = "block";
document.getElementById("command").required = true;
} else {
document.getElementById("ifYes").style.display = "none";
document.getElementById("command").required = false;
}
}
function basicAdvanced(that) {
if (that.value == "advanced") {
document.getElementById("ifAdvanced").style.display = "block";
document.getElementById("ifBasic").style.display = "none";
document.getElementById("interval").required = false;
document.getElementById("time").required = false;
} else {
document.getElementById("ifAdvanced").style.display = "none";
document.getElementById("ifBasic").style.display = "block";
document.getElementById("interval").required = true;
document.getElementById("time").required = true;
}
}
function ifDays(that) {
if (that.value == "days") {
document.getElementById("ifDays").style.display = "block";
document.getElementById("time").required = true;
} else {
document.getElementById("ifDays").style.display = "none";
document.getElementById("time").required = false;
}
}
$( ".del_button" ).click(function() {
var sch_id = $(this).data('sch');
console.log(sch_id)
bootbox.confirm({
title: "{{ translate('serverSchedules', 'areYouSure', data['lang']) }}",
message: "{{ translate('serverSchedules', 'confirmDelete', data['lang']) }}",
buttons: {
cancel: {
label: '<i class="fas fa-times"></i> {{ translate("serverSchedules", "cancel", data['lang']) }}'
},
confirm: {
className: 'btn-outline-danger',
label: '<i class="fas fa-check"></i> {{ translate("serverSchedules", "confirm", data['lang']) }}'
}
},
callback: function (result) {
console.log(result);
if (result == true) {
del_task(sch_id, serverId);
}
}
});
});
function del_task(sch_id, id){
var token = getCookie("_xsrf")
$.ajax({
type: "DELETE",
headers: {'X-XSRFToken': token},
url: '/ajax/del_task?server_id='+id+'&schedule_id='+sch_id,
data: {
schedule_id: sch_id,
id: id
},
success: function(data) {
location.reload();
},
});
}
</script>
{% end %}

View File

@ -1,7 +1,6 @@
{% extends ../base.html %}
{% block meta %}
<!-- <meta http-equiv="refresh" content="60">-->
{% end %}
{% block title %}Crafty Controller - {{ translate('serverDetails', 'serverDetails', data['lang']) }}{% end %}
@ -43,8 +42,8 @@
<div style="gap: 0.5rem;" class="input-group flex-wrap">
<input style="min-width: 10rem;" type="text" class="form-control" id="server_command" name="server_command" placeholder="{{ translate('serverTerm', 'commandInput', data['lang']) }}" autofocus="">
<span class="input-group-btn ml-5">
<input type="hidden" value="" id="last_command"/>
<button id="submit" class="btn btn-sm btn-info" type="button">{{ translate('serverTerm', 'sendCommand', data['lang']) }}</button>
<button id="submit" class="btn btn-sm btn-info" type="button">{{ translate('serverTerm', 'sendCommand',
data['lang']) }}</button>
</span>
</div>
{% if data['permissions']['Commands'] in data['user_permissions'] %}
@ -60,16 +59,22 @@
<button onclick="" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1 disabled">{% raw translate('serverTerm', 'restart', data['lang']) %}</button>
<button onclick="" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate('serverTerm', 'stop', data['lang']) }}</button>
</div>
{% elif data['downloading'] %}
<div id="control_buttons" class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0" style="visibility: visible">
<button onclick="" id="start-btn" style="max-width: 12rem; white-space: nowrap;" class="btn btn-secondary m-1 flex-grow-1 disabled"><i class="fa fa-spinner fa-spin"></i> {{ translate('serverTerm', 'downloading',
data['lang']) }}</button>
<button onclick="" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1 disabled">{% raw translate('serverTerm', 'restart', data['lang']) %}</button>
<button onclick="" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate('serverTerm', 'stop', data['lang']) }}</button>
</div>
{% else %}
<div id="control_buttons" class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0" style="visibility: visible">
<button onclick="send_command(server_id, 'start_server');" id="start-btn" style="max-width: 7rem;" class="btn btn-primary m-1 flex-grow-1">{{ translate('serverTerm', 'start', data['lang']) }}</button>
<button onclick="send_command(server_id, 'restart_server');" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate('serverTerm', 'restart', data['lang']) %}</button>
<button onclick="send_command(server_id, 'stop_server');" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1">{{ translate('serverTerm', 'stop', data['lang']) }}</button>
<button onclick="send_command(serverId, 'start_server');" id="start-btn" style="max-width: 7rem;" class="btn btn-primary m-1 flex-grow-1">{{ translate('serverTerm', 'start', data['lang']) }}</button>
<button onclick="send_command(serverId, 'restart_server');" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate('serverTerm', 'restart', data['lang']) %}</button>
<button onclick="send_command(serverId, 'stop_server');" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1">{{ translate('serverTerm', 'stop', data['lang']) }}</button>
</div>
{% end %}
{% end %}
</div>
</div>
</div>
</div>
@ -82,10 +87,11 @@
/* Hide scrollbar for IE, Edge and Firefox */
#virt_console {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none;
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
}
</style>
<!-- content-wrapper ends -->
@ -94,7 +100,9 @@
{% block js %}
<script>
function send_command (server_id, command){
const serverId = new URLSearchParams(document.location.search).get('id')
function send_command(serverId, command) {
if (command == 'start_server') {
startBtn.setAttribute('disabled', 'disabled');
restartBtn.removeAttribute('disabled');
@ -105,46 +113,44 @@
restartBtn.setAttribute('disabled', 'disabled');
stopBtn.setAttribute('disabled', 'disabled');
}
<!-- this getCookie function is in base.html-->
//<!-- this getCookie function is in base.html-->
var token = getCookie("_xsrf");
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/server/command?command=' + command + '&id=' + server_id,
url: '/server/command?command=' + command + '&id=' + serverId,
success: function (data) {
console.log("got response:");
console.log(data);
setTimeout(function(){
if (command != 'start_server'){
location.reload();
}
}, 10000);
}
});
}
if (webSocket) {
webSocket.on('update_button_status', function (updateButton) {
if (updateButton.isUpdating) {
if (updateButton.server_id == serverId) {
console.log(updateButton.isUpdating)
document.getElementById('control_buttons').innerHTML = '<button onclick="" id="start-btn" style="max-width: 7rem;" class="btn btn-primary m-1 flex-grow-1">{{ translate("serverTerm", "updating", data['lang']) }}</button><button onclick="" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate("serverTerm", "restart", data['lang']) %}</button><button onclick="" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate("serverTerm", "stop", data['lang']) }}</button>';
document.getElementById('control_buttons').innerHTML = '<button onclick="" id="start-btn" style="max-width: 7rem;" class="btn btn-primary m-1 flex-grow-1">{{ translate("serverTerm", "updating", data["lang"]) }}</button><button onclick="" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate("serverTerm", "restart", data["lang"]) %}</button><button onclick="" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate("serverTerm", "stop", data["lang"]) }}</button>';
}
}
else {
if (updateButton.server_id == serverId) {
window.location.reload()
document.getElementById('update_control_buttons').innerHTML = '<button onclick="send_command(server_id, "start_server");" id="start-btn" style="max-width: 7rem;" class="btn btn-primary m-1 flex-grow-1">{{ translate("serverTerm", "start", data['lang']) }}</button><button onclick="send_command(server_id, "restart_server");" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate("serverTerm", "restart", data['lang']) %}</button><button onclick="" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate("serverTerm", "stop", data['lang']) }}</button>';
document.getElementById('update_control_buttons').innerHTML = '<button onclick="send_command(serverId, "start_server");" id="start-btn" style="max-width: 7rem;" class="btn btn-primary m-1 flex-grow-1">{{ translate("serverTerm", "start", data["lang"]) }}</button><button onclick="send_command(serverId, "restart_server");" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate("serverTerm", "restart", data["lang"]) %}</button><button onclick="" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate("serverTerm", "stop", data["lang"]) }}</button>';
}
}
});
}
// Convert running to lower case (example: 'True' converts to 'true') and
// then to boolean via JSON.parse()
let online = JSON.parse('{{ data['server_stats']['running'] }}'.toLowerCase());
let online = JSON.parse("{{ data['server_stats']['running'] }}".toLowerCase());
let startBtn = document.querySelector('#start-btn');
let restartBtn = document.querySelector('#restart-btn');
let stopBtn = document.querySelector('#stop-btn');
{% if data['permissions']['Commands'] in data['user_permissions'] %}
//{% if data['permissions']['Commands'] in data['user_permissions'] %}
if (online) {
startBtn.setAttribute('disabled', 'disabled');
restartBtn.removeAttribute('disabled');
@ -154,14 +160,18 @@
restartBtn.setAttribute('disabled', 'disabled');
stopBtn.setAttribute('disabled', 'disabled');
}
{% end %}
let server_id = '{{ data['server_stats']['server_id']['server_id'] }}';
if (webSocket) {
webSocket.on('send_start_reload', function () {
location.reload()
});
}
//{% end %}
function get_server_log() {
$.ajax({
type: 'GET',
url: '/ajax/server_log?id={{ data['server_stats']['server_id']['server_id'] }}',
url: '/ajax/server_log?id=' + serverId,
dataType: 'text',
success: function (data) {
console.log('Got Log From Server')
@ -198,20 +208,23 @@
$('#server_command').on('keydown', function (e) {
if (e.which == 13) {
$(this).attr("disabled", "disabled"); //Disable textbox to prevent multiple submit
send_command_to_server()
sendConsoleCommand()
$(this).removeAttr("disabled"); //Enable the textbox again if needed.
$(this).focus();
}
else if (e.which == 38) {
last_command = $('#last_command').val()
$("#server_command").val(last_command)
e.preventDefault();
$('#server_command').val(cmdHistory.getPrev());
} else if (e.which == 40) {
e.preventDefault();
$('#server_command').val(cmdHistory.getNext());
}
});
$("#submit").click(function (e) {
e.preventDefault();
send_command_to_server();
sendConsoleCommand();
});
@ -222,27 +235,56 @@
}
function send_command_to_server(){
var server_command = $("#server_command").val()
console.log(server_command)
$("#last_command").val(server_command)
async function sendConsoleCommand() {
let serverCommand = $("#server_command").val()
console.log(serverCommand)
var token = getCookie("_xsrf")
cmdHistory.push(serverCommand);
data_to_send = { command :server_command, }
let token = getCookie("_xsrf")
console.log('sending command: ' + server_command)
$.ajax({
type: "POST",
headers: {'X-XSRFToken': token},
url: '/ajax/send_command?id={{ data['server_stats']['server_id']['server_id'] }}',
data: data_to_send,
success: function(data){
console.log("got response:");
console.log(data);
$("#server_command").val('')
let formdata = new FormData();
formdata.append('command', serverCommand)
console.log('sending command: ' + serverCommand)
let res = await fetch("/ajax/send_command?id=" + serverId, {
method: 'POST',
headers: {
'X-XSRFToken': token
},
body: formdata,
});
let responseData = await res.text();
console.log("got response:");
console.log(responseData);
$("#server_command").val('')
}
const cmdHistory = {
history: [],
current: 0,
push: function (cmd) {
this.history.push(cmd);
this.current = this.history.length - 1;
},
getPrev: function () {
const prevCommand = this.history[this.current];
this.current--;
if (this.current < 0) this.current = 0;
return prevCommand;
},
getNext: function () {
this.current++;
if (this.current > (this.history.length - 1)) {
this.current = (this.history.length - 1);
return '';
}
const nextCommand = this.history[this.current];
return nextCommand;
}
}
</script>

View File

@ -17,7 +17,8 @@
<!-- Layout styles -->
<link rel="stylesheet" href="/static/assets/css/dark/style.css">
<!-- End Layout styles -->
<link rel="shortcut icon" href="/static/assets/images/favicon.png" />
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/images/logo_small.svg">
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
</head>
<body class="dark-theme">
<div class="container-scroller">
@ -28,7 +29,7 @@
<div class="auto-form-wrapper">
<div class="text-center">
<img src="/static/assets/images/logo_long.jpg"><br /><br />
<img src="/static/assets/images/logo_long.svg"><br /><br />
<div class="col-sm-12 grid-margin stretch-card">
<div class="card card-statistics social-card facebook-card card-colored">
<div class="card-body">

View File

@ -17,7 +17,8 @@
<!-- Layout styles -->
<link rel="stylesheet" href="/static/assets/css/dark/style.css">
<!-- End Layout styles -->
<link rel="shortcut icon" href="/static/assets/images/favicon.png" />
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/images/logo_small.svg">
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
</head>
<body class="dark-theme">
<div class="container-scroller">
@ -28,7 +29,7 @@
<div class="auto-form-wrapper">
<div class="text-center">
<img src="/static/assets/images/logo_long.jpg"><br /><br />
<img src="/static/assets/images/logo_long.svg"><br /><br />
<div class="col-sm-12 grid-margin stretch-card">
<div class="card card-statistics social-card google-card card-colored">
<div class="card-body">

View File

@ -17,7 +17,8 @@
<!-- Layout styles -->
<link rel="stylesheet" href="/static/assets/css/dark/style.css">
<!-- End Layout styles -->
<link rel="shortcut icon" href="/static/assets/images/favicon.png" />
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/images/logo_small.svg">
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
</head>
<body class="dark-theme">
<div class="container-scroller">
@ -28,7 +29,7 @@
<div class="auto-form-wrapper login-modal">
<div class="text-center">
<img src="/static/assets/images/logo_long.jpg">
<img src="/static/assets/images/logo_long.svg">
</div>
<style>
.login-modal {
@ -71,6 +72,11 @@
<div class="form-group">
<button class="login-input btn btn-primary submit-btn btn-block">{{ translate('login', 'login', data['lang']) }}</button>
</div>
{% if error_msg is not None %}
<fieldset style="color: red; text-align: center;">
<span>{{error_msg}}</span>
</fieldset>
{% end %}
<div class="form-group d-flex justify-content-between">
<div class="form-check form-check-flat mt-0">
&nbsp;

View File

@ -1,7 +1,6 @@
{% extends ../public_base.html %}
{% block meta %}
<meta http-equiv="refresh" content="30">
{% end %}
{% block title %}Crafty Controller - {{ translate('dashboard', 'dashboard', data['lang']) }}{% end %}
@ -22,41 +21,50 @@
</tr>
</thead>
<tbody>
{% if data['running'] != 0 %}
<span id="sync" style="margin-left: 5px;"><i class="fas fa-sync fa-spin"></i></span></h4>
{% end %}
{% for server in data['servers'] %}
<tr>
<td>
<td id="server_name_{{ server['stats']['server_id']['server_id'] }}">
<i class="fas fa-server"></i>
{{ server['server_data']['server_name'] }}
</td>
{% if server['stats']['int_ping_results'] != 'False' %}
<td>
{{ server['stats']['online'] }} / {{ server['stats']['max'] }} {{ translate('dashboard', 'max', data['lang']) }}<br />
<td id="server_players_{{ server['stats']['server_id']['server_id'] }}">
{{ server['stats']['online'] }} / {{ server['stats']['max'] }} {{ translate('dashboard', 'max',
data['lang']) }}<br />
</td>
<td>
<td id="server_motd_{{ server['stats']['server_id']['server_id'] }}">
{% if server['stats']['desc'] != 'False' %}
{% if server['raw_ping_result']['icon'] %}
<img src="data:image/png;base64,{% raw server['raw_ping_result']['icon'] %}" alt="icon"/>
{% else %}
<img src="/static/assets/images/pack.png" alt="icon" />
{% end %}
<span id="input_motd_{{ server['stats']['server_id']['server_id'] }}" class="input_motd">{{ server['stats']['desc'] }}</span> <br />
<img src="/static/assets/images/pack.png" alt="icon" style="-webkit-filter:grayscale(100%); filter:grayscale(100%)"/>
<span id="input_motd_{{ server['stats']['server_id']['server_id'] }}" class="input_motd">{{
server['stats']['desc'] }}</span> <br />
{% end %}
</td>
<td>
<td id="server_version_{{ server['stats']['server_id']['server_id'] }}">
{% if server['stats']['version'] != 'False' %}
{{ server['stats']['version'] }}
{% end %}
</td>
{% else %}
<td colspan="3">
<span class="text-warning"><i class="fas fa-exclamation-triangle"></i> Crafty can't get infos from this Server </span>
<td id="server_players_{{ server['stats']['server_id']['server_id'] }}">
<span class="text-warning"><i class="fas fa-exclamation-triangle"></i></span>
</td>
<td id="server_motd_{{ server['stats']['server_id']['server_id'] }}">
<span class="text-warning">Crafty can't get infos from this Server </span>
</td>
<td id="server_version_{{ server['stats']['server_id']['server_id'] }}">
<span class="text-warning"><i class="fas fa-question"></i></i></span>
</td>
{% end %}
<td>
<td id="server_online_status_{{ server['stats']['server_id']['server_id'] }}">
{% if server['stats']['running'] %}
<span class="text-success"><i class="fas fa-signal"></i> {{ translate('dashboard', 'online', data['lang']) }}</span>
<span class="text-success"><i class="fas fa-signal"></i> {{ translate('dashboard', 'online', data['lang'])
}}</span>
{% else %}
<span class="text-danger"><i class="fas fa-ban"></i> {{ translate('dashboard', 'offline', data['lang']) }}</span>
<span class="text-danger"><i class="fas fa-ban"></i> {{ translate('dashboard', 'offline', data['lang'])
}}</span>
{% end %}
</td>
</tr>
@ -74,11 +82,78 @@
<script src="/static/assets/js/motd.js"></script>
<script>
$(document).ready(function () {
function display_motd() {
var all_motds = Array.from(document.getElementsByClassName('input_motd'));
for (element of all_motds) {
initParser(element.id, element.id);
};
}
function update_one_server_status(server) {
server_players = document.getElementById('server_players_' + server.id);
server_motd = document.getElementById('server_motd_' + server.id);
server_version = document.getElementById('server_version_' + server.id);
server_online_status = document.getElementById('server_online_status_' + server.id);
/* TODO Update each element */
if (server.int_ping_results) {
document.getElementById('sync').innerHTML='';
/* Update Players */
if (server.players)
{
server_players.innerHTML = server.online + ` / ` + server.max + ` {{ translate('dashboard', 'max', data['lang']) }}<br />`
}
/* Update Motd */
var motd = "";
if (server.desc) {
if (server.icon) {
motd = `<img src="data:image/png;base64,` + server.icon + `" alt="icon" /> `;
}
else {
motd = `<img src="/static/assets/images/pack.png" alt="icon" /> `;
}
motd = motd + `<span id="input_motd_` + server.id + `" class="input_motd">` + server.desc + `</span> <br />`;
server_motd.innerHTML = motd;
}
/* Version */
if (server.version)
{
server_version.innerHTML = server.version
}
}
else {
server_players.innerHTML = `<span class="text-warning"><i class="fas fa-exclamation-triangle"></i></span>`;
server_motd.innerHTML = `<span class="text-warning">Crafty can't get infos from this Server </span>`;
server_version.innerHTML = `<span class="text-warning"><i class="fas fa-question"></i></i></span>`
}
/* Update Online Status */
var online_status = "";
if (server.running) {
online_status = `<span class="text-success"><i class="fas fa-signal"></i> {{ translate('dashboard', 'online', data['lang'])}}</span>`;
}
else {
online_status = `<span class="text-danger"><i class="fas fa-ban"></i> {{ translate('dashboard', 'offline', data['lang'])}}</span>`;
}
server_online_status.innerHTML = online_status;
}
function update_servers_status(data) {
console.log(data);
update_one_server_status(data[0]);
display_motd();
}
$(document).ready(function () {
console.log("ready!");
if (webSocket)
{
webSocket.on('update_server_status', update_servers_status);
}
}());
</script>

View File

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<html lang="{{ data['lang_page'] }}">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
@ -19,8 +20,10 @@
<!-- Layout styles -->
<link rel="stylesheet" href="/static/assets/css/dark/style.css">
<!-- End Layout styles -->
<link rel="shortcut icon" href="/static/assets/images/favicon.png" />
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/images/logo_small.svg">
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
</head>
<body class="dark-theme">
<div class="container-scroller">
<div class="container-fluid page-body-wrapper full-page-wrapper">
@ -49,11 +52,67 @@
<script src="/static/assets/js/shared/settings.js"></script>
<script src="/static/assets/js/shared/todolist.js"></script>
<!-- endinject -->
<script>
// {% if request.protocol == 'https' %}
let usingWebSockets = true;
let listenEvents = [];
try {
pageQueryParams = 'page_query_params=' + encodeURIComponent(location.search)
page = 'page=' + encodeURIComponent(location.pathname)
var wsInternal = new WebSocket('wss://' + location.host + '/ws?' + page + '&' + pageQueryParams);
wsInternal.onopen = function () {
console.log('opened WebSocket connection:', wsInternal)
};
wsInternal.onmessage = function (rawMessage) {
var message = JSON.parse(rawMessage.data);
console.log('got message: ', message)
listenEvents
.filter(listenedEvent => listenedEvent.event == message.event)
.forEach(listenedEvent => listenedEvent.callback(message.data))
};
wsInternal.onerror = function (errorEvent) {
console.error('WebSocket Error', errorEvent);
};
wsInternal.onclose = function (closeEvent) {
console.log('Closed WebSocket', closeEvent);
};
webSocket = {
on: function (event, callback) {
console.log('registered ' + event + ' event');
listenEvents.push({ event: event, callback: callback })
},
emit: function (event, data) {
var message = {
event: event,
data: data
}
wsInternal.send(JSON.stringify(message));
}
}
} catch (error) {
console.error('Error while making websocket helpers', error);
usingWebSockets = false;
}
// {% else %}
let usingWebSockets = false;
warn('WebSockets are not supported in Crafty if not using the https protocol')
var webSocket;
// {% end%}
</script>
{% block js %}
<!-- Custom js for this page -->
<!-- End custom js for this page -->
{% end %}
</body>
</html>

View File

@ -0,0 +1,468 @@
{% extends ../base.html %}
{% block title %}Crafty Controller - {{ translate('serverWizard', 'newServer', data['lang']) }}{% end %}
{% block content %}
<div class="content-wrapper">
<ul class="nav nav-tabs col-md-12 tab-simple-styled " role="tablist">
<li class="nav-item term-nav-item">
<a class="nav-link" href="/server/step1" role="tab" aria-selected="false">
<i class="fas fa-file-signature"></i>Minecraft-Java</a>
</li>
<li class="nav-item term-nav-item">
<a class="nav-link active" href="/server/bedrock_step1" role="tab" aria-selected="false">
<i class="fas fa-file-signature"></i>Minecraft-Bedrock</a>
</li>
</ul>
<br>
<div class="d-none" id="overlay" onclick="hide(event)"></div>
<div class="row">
<div class="col-sm-6 grid-margin stretch-card">
<div class="card">
<div class="card-body">
<h4>{{ translate('serverWizard', 'importServer', data['lang']) }}</h4>
<br />
<p class="card-description">
<form method="post" class="server-wizard" onSubmit="wait_msg(true)">
{% raw xsrf_form_html() %}
<input type="hidden" value="import_jar" name="create_type">
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label for="server_name">{{ translate('serverWizard', 'serverName', data['lang']) }}</label>
<input type="text" class="form-control" id="server_name" name="server_name" value="" placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}" required>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label for="server">{{ translate('serverWizard', 'serverPath', data['lang']) }} <small>{{ translate('serverWizard', 'absoluteServerPath', data['lang']) }}</small></label>
<input type="text" class="form-control" id="server_path" name="server_path" placeholder="/var/opt/server" required>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label for="server_jar">{{ translate('serverWizard', 'serverJar', data['lang']) }}</label>
<input type="text" class="form-control" id="server_jar" name="server_jar" value="" placeholder="bedrock_server" required>
</div>
</div>
</div>
<br />
<h4 class="card-title">{{ translate('serverWizard', 'quickSettings', data['lang']) }} <small style="text-transform: none;"> - {{ translate('serverWizard', 'quickSettingsDescription', data['lang']) }}</small></h4>
<hr>
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label for="port2">{{ translate('serverWizard', 'serverPort', data['lang']) }} <small></small></label>
<input type="number" class="form-control" id="port2" name="port" value="19132" step="1" min="1" required>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<div id="accordion-2">
<div class="card">
<div class="card-header p-2" id="Role-2">
<p class="mb-0 p-0" data-toggle="collapse" data-target="#collapseRole-2" aria-expanded="true" aria-controls="collapseRole-2">
<i class="fas fa-chevron-down"></i> {{ translate('serverWizard', 'addRole', data['lang']) }} <small style="text-transform: none;"> - {{ translate('serverWizard', 'autoCreate', data['lang']) }}</small>
</p>
</div>
<div id="collapseRole-2" class="collapse" aria-labelledby="Role-2" data-parent="">
<div class="card-body scroll">
<div class="form-group">
{% for r in data['roles'] %}
<span class="d-block menu-option"><label><input name="{{ r['role_id'] }}" type="checkbox">&nbsp;
{{ r['role_name'].capitalize() }}</label></span>
{% end %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary mr-2">{{ translate('serverWizard', 'importServerButton', data['lang']) }}</button>
<button type="reset" class="btn btn-danger mr-2">{{ translate('serverWizard', 'resetForm', data['lang']) }}</button>
</form>
</p>
</div>
</div>
</div>
<div class="col-sm-6 grid-margin stretch-card">
<div class="card">
<div class="card-body">
<h4>{{ translate('serverWizard', 'importZip', data['lang']) }}</h4>
<br />
<p class="card-description">
<form name="zip" method="post" class="server-wizard" onSubmit="wait_msg(true)">
{% raw xsrf_form_html() %}
<input type="hidden" value="import_zip" name="create_type">
<div class="row">
<div class="col-sm-9">
<div class="col-sm-12">
<div class="form-group">
<label for="server_name">{{ translate('serverWizard', 'serverName', data['lang']) }}</label>
<input type="text" class="form-control" id="server_name" name="server_name" value="" placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}" required>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label for="server">{{ translate('serverWizard', 'zipPath', data['lang']) }} <small>{{ translate('serverWizard', 'absoluteZipPath', data['lang']) }}</small></label>
<input type="text" class="form-control" id="server_path" name="server_path" placeholder="/var/opt/server.zip" required>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label for="server">{{ translate('serverWizard', 'selectRoot', data['lang']) }} <small>{{ translate('serverWizard', 'explainRoot', data['lang']) }}</small></label>
<br>
<button class="btn btn-primary mr-2" id="root_files_button" type="button">{{ translate('serverWizard', 'clickRoot', data['lang']) }}</button>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label for="server_jar">{{ translate('serverWizard', 'serverJar', data['lang']) }}</label>
<input type="text" class="form-control" id="server_jar" name="server_jar" value="" placeholder="bedrock_server" required>
</div>
</div>
</div>
</div>
<div class="col-sm-12">
<h4 class="card-title">{{ translate('serverWizard', 'quickSettings', data['lang']) }} <small style="text-transform: none;"> - {{ translate('serverWizard', 'quickSettingsDescription', data['lang']) }}</small></h4>
<hr>
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label for="port3">{{ translate('serverWizard', 'serverPort', data['lang']) }} <small></small></label>
<input type="number" class="form-control" id="port3" name="port" value="19132" step="1" min="1" required>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<div id="accordion-3">
<div class="card">
<div class="card-header p-2" id="Role-3">
<p class="mb-0 p-0" data-toggle="collapse" data-target="#collapseRole-3" aria-expanded="true" aria-controls="collapseRole-3">
<i class="fas fa-chevron-down"></i> {{ translate('serverWizard', 'addRole', data['lang']) }} <small style="text-transform: none;"> - {{ translate('serverWizard', 'autoCreate', data['lang']) }}</small>
</p>
</div>
<div id="collapseRole-3" class="collapse" aria-labelledby="Role-3" data-parent="">
<div class="card-body scroll">
<div class="form-group">
{% for r in data['roles'] %}
<span class="d-block menu-option"><label><input name="{{ r['role_id'] }}" type="checkbox">&nbsp;
{{ r['role_name'].capitalize() }}</label></span>
{% end %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-12" style="visibility: hidden;">
<div class="form-group">
<input type="text" class="form-control" id="zip_root_path" name="zip_root_path">
</div>
</div>
<div class="modal fade" id="dir_select" tabindex="-1" role="dialog" aria-labelledby="dir_select" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">{{ translate('serverWizard', 'selectZipDir', data['lang']) }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="tree-ctx-item" id="main-tree-div" data-path="" style="overflow: scroll; max-height:75%;">
<input type="radio" id="main-tree-input" name="root_path" value="" checked>
<span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path="">
<i class="far fa-folder"></i>
<i class="far fa-folder-open"></i>
{{ translate('serverFiles', 'files', data['lang']) }}
</span>
</input>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ translate('serverWizard', 'close', data['lang']) }}</button>
<button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary">{{ translate('serverWizard', 'save', data['lang']) }}</button>
</div>
</div>
</div>
</div>
</div>
<button id="zip_submit" type="submit" title="You must select server root dir first" disabled class="btn btn-primary mr-2">{{ translate('serverWizard', 'importServerButton', data['lang']) }}</button>
<button type="reset" class="btn btn-danger mr-2">{{ translate('serverWizard', 'resetForm', data['lang']) }}</button>
</div>
</div>
</form>
</p>
</div>
</div>
</div>
</div>
<style>
.scroll {
max-height: 12em;
overflow-y: auto;
}
.menu-btn {
font-size: 0.9em;
padding: 2px 10px;
}
.menu {
padding-top: 10px;
z-index: 200;
margin-top: 4px;
position: absolute;
background-color: #2a2c44;
}
.menu-option {
padding: 6px 20px 6px;
color: white;
}
#overlay {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
z-index: 100;
}
</style>
<style>
/* Remove default bullets */
.tree-view,
.tree-nested {
list-style-type: none;
margin: 0;
padding: 0;
margin-left: 10px;
}
/* Style the items */
.tree-item,
.files-tree-title {
cursor: pointer;
user-select: none; /* Prevent text selection */
}
/* Create the caret/arrow with a unicode, and style it */
.tree-caret .fa-folder {
display: inline-block;
}
.tree-caret .fa-folder-open {
display: none;
}
/* Rotate the caret/arrow icon when clicked on (using JavaScript) */
.tree-caret-down .fa-folder {
display: none;
}
.tree-caret-down .fa-folder-open {
display: inline-block;
}
/* Hide the nested list */
.tree-nested {
display: none;
}
</style>
{% end %}
{% block js%}
<script>
document.getElementById("root_files_button").addEventListener("click", function(){
if(document.forms["zip"]["server_path"].value != ""){
if(document.getElementById('root_files_button').classList.contains('clicked')){
document.getElementById('main-tree-div').innerHTML = '<input type="radio" id="main-tree-input" name="root_path" value="" checked><span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path=""><i class="far fa-folder"></i><i class="far fa-folder-open"></i>{{ translate('serverFiles', 'files', data['lang']) }}</span></input>'
}else{
document.getElementById('root_files_button').classList.add('clicked')
}
path = document.forms["zip"]["server_path"].value;
console.log(document.forms["zip"]["server_path"].value)
var token = getCookie("_xsrf");
var dialog = bootbox.dialog({
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
});
$.ajax({
type: "POST",
headers: {'X-XSRFToken': token},
url: '/ajax/unzip_server?id=-1&path='+path,
});
}else{
bootbox.alert("You must input a path before selecting this button");
}
});
</script>
<script>
function dropDown(event) {
event.target.parentElement.children[1].classList.remove("d-none");
document.getElementById("overlay").classList.remove("d-none");
}
function hide(event) {
var items = document.getElementsByClassName('menu');
for (let i = 0; i < items.length; i++) {
items[i].classList.add("d-none");
}
document.getElementById("overlay").classList.add("d-none");
}
function wait_msg(importing){
bootbox.alert({
title: importing ? '{% raw translate("serverWizard", "importing", data['lang']) %}' : '{% raw translate("serverWizard", "downloading", data['lang']) %}',
message: '<i class="fas fa-cloud-download"></i> {% raw translate("serverWizard", "bePatient", data['lang']) %}',
});
}
function show_file_tree(){
$("#dir_select").modal();
}
function getTreeView(path) {
document.getElementById('zip_submit').disabled = false;
path = path
$.ajax({
type: "GET",
url: '/ajax/get_zip_tree?id=-1&path='+path,
dataType: 'text',
success: function(data){
console.log("got response:");
console.log(data);
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
try{
document.getElementById('main-tree-div').innerHTML += text;
document.getElementById('main-tree').parentElement.classList.add("clicked");
}catch{
document.getElementById('files-tree').innerHTML = text;
}
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-path', serverDir);
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-name', 'Files');
},
});
}
function getToggleMain(event) {
path = event.target.parentElement.getAttribute('data-path');
document.getElementById("files-tree").classList.toggle("d-block");
document.getElementById(path+"span").classList.toggle("tree-caret-down");
document.getElementById(path+"span").classList.toggle("tree-caret");
}
function getDirView(event) {
path = event.target.parentElement.getAttribute('data-path');
if (document.getElementById(path).classList.contains('clicked')){
var toggler = document.getElementById(path+"span");
if (toggler.classList.contains('files-tree-title')){
document.getElementById(path+"ul").classList.toggle("d-block");
document.getElementById(path+"span").classList.toggle("tree-caret-down");
}
return;
}else{
$.ajax({
type: "GET",
url: '/ajax/get_zip_dir?id=-1&path='+path,
dataType: 'text',
success: function(data){
console.log("got response:");
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
try{
document.getElementById(path+"span").classList.add('tree-caret-down');
document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked");
}catch{
console.log("Bad")
}
var toggler = document.getElementById(path);
if (toggler.classList.contains('files-tree-title')){
document.getElementById(path+"span").addEventListener("click", function caretListener() {
document.getElementById(path+"ul").classList.toggle("d-block");
document.getElementById(path+"span").classList.toggle("tree-caret-down");
});
}
},
});
}
}
if (webSocket) {
webSocket.on('send_temp_path', function (data) {
setTimeout(function(){
var x = document.querySelector('.bootbox');
if (x) {
x.remove()
}
var x = document.querySelector('.modal-backdrop');
if (x) {
x.remove()
}
document.getElementById('main-tree-input').setAttribute('value', data.path)
getTreeView(data.path);
show_file_tree();
}, 5000);
});
}
</script>
<script type="text/javascript">
//<![CDATA[
// array of possible countries in the same order as they appear in the country selection list
function decodeHtmlCharCodes(str) {
return str.replace("&quot;", "\"");
}
function convertHtmlJsonToJavacriptArray(str) {
var result = []
str = decodeHtmlCharCodes(str)
for(var i in str)
result.push([i, str [i]]);
return result
}
//]]>
</script>
{% end %}

View File

@ -5,6 +5,17 @@
{% block content %}
<div class="content-wrapper">
<div class="content-wrapper">
<ul class="nav nav-tabs col-md-12 tab-simple-styled " role="tablist">
<li class="nav-item term-nav-item">
<a class="nav-link active" href="/server/step1" role="tab" aria-selected="false">
<i class="fas fa-file-signature"></i>Minecraft-Java</a>
</li>
<li class="nav-item term-nav-item">
<a class="nav-link" href="/server/bedrock_step1" role="tab" aria-selected="false">
<i class="fas fa-file-signature"></i>Minecraft-Bedrock</a>
</li>
</ul>
<div class="d-none" id="overlay" onclick="hide(event)"></div>
<div class="row">
<div class="col-sm-6 grid-margin stretch-card">
@ -15,14 +26,14 @@
<br />
<p class="card-description">
<form method="post" class="server-wizard">
<form method="post" class="server-wizard" onSubmit="wait_msg()">
{% raw xsrf_form_html() %}
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label for="server_type">{{ translate('serverWizard', 'serverType', data['lang']) }}</label>
<select class="form-control form-control-lg select-css" id="server_type" name="server_type" onchange="serverTypeChange(this)">
<option value="empty">{{ translate('serverWizard', 'selectType', data['lang']) }}</option>
<select required class="form-control form-control-lg select-css" id="server_type" name="server_type" onchange="serverTypeChange(this)">
<option value="">{{ translate('serverWizard', 'selectType', data['lang']) }}</option>
{% for s in data['server_types'] %}
<option value="{{ s }}">{{ s.capitalize() }}</option>
{% end %}
@ -33,8 +44,8 @@
<div class="col-sm-12">
<div class="form-group">
<label for="server_version">{{ translate('serverWizard', 'serverVersion', data['lang']) }}</label>
<select class="form-control form-control-lg select-css" id="server" name="server">
<option value="0">{{ translate('serverWizard', 'selectVersion', data['lang']) }}</option>
<select required class="form-control form-control-lg select-css" id="server" name="server">
<option value="">{{ translate('serverWizard', 'selectVersion', data['lang']) }}</option>
</select>
</div>
</div>
@ -42,7 +53,7 @@
<div class="col-sm-12">
<div class="form-group">
<label for="server_name">{{ translate('serverWizard', 'serverName', data['lang']) }}</label>
<input type="text" class="form-control" id="server_name" name="server_name" placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}">
<input type="text" class="form-control" id="server_name" name="server_name" placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}" required>
</div>
</div>
@ -55,21 +66,21 @@
<div class="col-sm-3">
<div class="form-group">
<label for="min_memory1">{{ translate('serverWizard', 'minMem', data['lang']) }} <small> - {{ translate('serverWizard', 'sizeInGB', data['lang']) }}</small></label>
<input type="number" class="form-control" id="min_memory1" name="min_memory" value="1" step="0.5" min="0.5">
<input type="number" class="form-control" id="min_memory1" name="min_memory" value="1" step="0.5" min="0.5" required>
</div>
</div>
<div class="col-sm-3 offset-1">
<div class="form-group">
<label for="max_memory1">{{ translate('serverWizard', 'maxMem', data['lang']) }} <small> - {{ translate('serverWizard', 'sizeInGB', data['lang']) }}</small></label>
<input type="number" class="form-control" id="max_memory1" name="max_memory" value="2" step="0.5" min="0.5">
<input type="number" class="form-control" id="max_memory1" name="max_memory" value="2" step="0.5" min="0.5" required>
</div>
</div>
<div class="col-sm-3 offset-1">
<div class="form-group">
<label for="port1">{{ translate('serverWizard', 'serverPort', data['lang']) }} <small> - {{ translate('serverWizard', 'defaultPort', data['lang']) }}</small></label>
<input type="number" class="form-control" id="port1" name="port" value="25565" step="1" min="1">
<input type="number" class="form-control" id="port1" name="port" value="25565" step="1" min="1" required>
</div>
</div>
<div class="col-sm-12">
@ -97,7 +108,7 @@
</div>
</div>
<button type="submit" class="btn btn-primary mr-2" onclick="wait_msg()">{{ translate('serverWizard', 'buildServer', data['lang']) }}</button>
<button type="submit" class="btn btn-primary mr-2">{{ translate('serverWizard', 'buildServer', data['lang']) }}</button>
<button type="reset" class="btn btn-danger mr-2">{{ translate('serverWizard', 'resetForm', data['lang']) }}</button>
</form>
@ -114,7 +125,7 @@
<br />
<p class="card-description">
<form method="post" class="server-wizard">
<form method="post" class="server-wizard" onSubmit="wait_msg(true)">
{% raw xsrf_form_html() %}
<input type="hidden" value="import_jar" name="create_type">
<div class="row">
@ -122,21 +133,21 @@
<div class="col-sm-12">
<div class="form-group">
<label for="server_name">{{ translate('serverWizard', 'serverName', data['lang']) }}</label>
<input type="text" class="form-control" id="server_name" name="server_name" value="" placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}">
<input type="text" class="form-control" id="server_name" name="server_name" value="" placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}" required>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label for="server">{{ translate('serverWizard', 'serverPath', data['lang']) }} <small>{{ translate('serverWizard', 'absoluteServerPath', data['lang']) }}</small></label>
<input type="text" class="form-control" id="server_path" name="server_path" placeholder="/var/opt/server">
<input type="text" class="form-control" id="server_path" name="server_path" placeholder="/var/opt/server" required>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label for="server_jar">{{ translate('serverWizard', 'serverJar', data['lang']) }}</label>
<input type="text" class="form-control" id="server_jar" name="server_jar" value="" placeholder="paper.jar">
<input type="text" class="form-control" id="server_jar" name="server_jar" value="" placeholder="paper.jar" required>
</div>
</div>
@ -151,21 +162,21 @@
<div class="col-sm-3">
<div class="form-group">
<label for="min_memory2">{{ translate('serverWizard', 'minMem', data['lang']) }} <small> - {{ translate('serverWizard', 'sizeInGB', data['lang']) }}</small></label>
<input type="number" class="form-control" id="min_memory2" name="min_memory" value="1" step="0.5" min="0.5">
<input type="number" class="form-control" id="min_memory2" name="min_memory" value="1" step="0.5" min="0.5" required>
</div>
</div>
<div class="col-sm-3 offset-1">
<div class="form-group">
<label for="max_memory2">{{ translate('serverWizard', 'maxMem', data['lang']) }} <small> - {{ translate('serverWizard', 'sizeInGB', data['lang']) }}</small></label>
<input type="number" class="form-control" id="max_memory2" name="max_memory" value="2" step="0.5" min="0.5">
<input type="number" class="form-control" id="max_memory2" name="max_memory" value="2" step="0.5" min="0.5" required>
</div>
</div>
<div class="col-sm-3 offset-1">
<div class="form-group">
<label for="port2">{{ translate('serverWizard', 'serverPort', data['lang']) }} <small> - {{ translate('serverWizard', 'defaultPort', data['lang']) }}</small></label>
<input type="number" class="form-control" id="port2" name="port" value="25565" step="1" min="1">
<input type="number" class="form-control" id="port2" name="port" value="25565" step="1" min="1" required>
</div>
</div>
<div class="col-sm-12">
@ -192,7 +203,7 @@
</div>
</div>
</div>
<button type="submit" class="btn btn-primary mr-2" onclick="wait_msg(true)">{{ translate('serverWizard', 'importServerButton', data['lang']) }}</button>
<button type="submit" class="btn btn-primary mr-2">{{ translate('serverWizard', 'importServerButton', data['lang']) }}</button>
<button type="reset" class="btn btn-danger mr-2">{{ translate('serverWizard', 'resetForm', data['lang']) }}</button>
</form>
@ -209,7 +220,7 @@
<br />
<p class="card-description">
<form method="post" class="server-wizard">
<form name="zip" method="post" class="server-wizard" onSubmit="wait_msg(true)">
{% raw xsrf_form_html() %}
<input type="hidden" value="import_zip" name="create_type">
@ -218,21 +229,30 @@
<div class="col-sm-12">
<div class="form-group">
<label for="server_name">{{ translate('serverWizard', 'serverName', data['lang']) }}</label>
<input type="text" class="form-control" id="server_name" name="server_name" value="" placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}">
<input type="text" class="form-control" id="server_name" name="server_name" value="" placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}" required>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label for="server">{{ translate('serverWizard', 'zipPath', data['lang']) }} <small>{{ translate('serverWizard', 'absoluteZipPath', data['lang']) }}</small></label>
<input type="text" class="form-control" id="server_path" name="server_path" placeholder="/var/opt/server.zip">
<input type="text" class="form-control" id="server_path" name="server_path" placeholder="/var/opt/server.zip" required>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label for="server">{{ translate('serverWizard', 'selectRoot', data['lang']) }} <small>{{ translate('serverWizard', 'explainRoot', data['lang']) }}</small></label>
<br>
<button class="btn btn-primary mr-2" id="root_files_button" type="button">{{ translate('serverWizard', 'clickRoot', data['lang']) }}</button>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label for="server_jar">{{ translate('serverWizard', 'serverJar', data['lang']) }}</label>
<input type="text" class="form-control" id="server_jar" name="server_jar" value="" placeholder="paper.jar">
<input type="text" class="form-control" id="server_jar" name="server_jar" value="" placeholder="paper.jar" required>
</div>
</div>
</div>
@ -247,21 +267,21 @@
<div class="col-sm-12">
<div class="form-group">
<label for="min_memory3">{{ translate('serverWizard', 'minMem', data['lang']) }} <small> - {{ translate('serverWizard', 'sizeInGB', data['lang']) }}</small></label>
<input type="number" class="form-control" id="min_memory3" name="min_memory" value="1" step="0.5" min="0.5">
<input type="number" class="form-control" id="min_memory3" name="min_memory" value="1" step="0.5" min="0.5" required>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label for="max_memory3">{{ translate('serverWizard', 'maxMem', data['lang']) }} <small> - {{ translate('serverWizard', 'sizeInGB', data['lang']) }}</small></label>
<input type="number" class="form-control" id="max_memory3" name="max_memory" value="2" step="0.5" min="0.5">
<input type="number" class="form-control" id="max_memory3" name="max_memory" value="2" step="0.5" min="0.5" required>
</div>
</div>
<div class="col-sm-12">
<div class="form-group">
<label for="port3">{{ translate('serverWizard', 'serverPort', data['lang']) }} <small> - {{ translate('serverWizard', 'defaultPort', data['lang']) }}</small></label>
<input type="number" class="form-control" id="port3" name="port" value="25565" step="1" min="1">
<input type="number" class="form-control" id="port3" name="port" value="25565" step="1" min="1" required>
</div>
</div>
@ -288,9 +308,40 @@
</div>
</div>
</div>
<div class="col-sm-12" style="visibility: hidden;">
<div class="form-group">
<input type="text" class="form-control" id="zip_root_path" name="zip_root_path">
</div>
<button type="submit" class="btn btn-primary mr-2" onclick="wait_msg(true)">{{ translate('serverWizard', 'importServerButton', data['lang']) }}</button>
</div>
<div class="modal fade" id="dir_select" tabindex="-1" role="dialog" aria-labelledby="dir_select" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">{{ translate('serverWizard', 'selectZipDir', data['lang']) }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="tree-ctx-item" id="main-tree-div" data-path="" style="overflow: scroll; max-height:75%;">
<input type="radio" id="main-tree-input" name="root_path" value="" checked>
<span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path="">
<i class="far fa-folder"></i>
<i class="far fa-folder-open"></i>
{{ translate('serverFiles', 'files', data['lang']) }}
</span>
</input>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ translate('serverWizard', 'close', data['lang']) }}</button>
<button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary">{{ translate('serverWizard', 'save', data['lang']) }}</button>
</div>
</div>
</div>
</div>
</div>
<button id="zip_submit" type="submit" title="You must select server root dir first" disabled class="btn btn-primary mr-2">{{ translate('serverWizard', 'importServerButton', data['lang']) }}</button>
<button type="reset" class="btn btn-danger mr-2">{{ translate('serverWizard', 'resetForm', data['lang']) }}</button>
</div>
</div>
@ -329,10 +380,73 @@
z-index: 100;
}
</style>
<style>
/* Remove default bullets */
.tree-view,
.tree-nested {
list-style-type: none;
margin: 0;
padding: 0;
margin-left: 10px;
}
/* Style the items */
.tree-item,
.files-tree-title {
cursor: pointer;
user-select: none; /* Prevent text selection */
}
/* Create the caret/arrow with a unicode, and style it */
.tree-caret .fa-folder {
display: inline-block;
}
.tree-caret .fa-folder-open {
display: none;
}
/* Rotate the caret/arrow icon when clicked on (using JavaScript) */
.tree-caret-down .fa-folder {
display: none;
}
.tree-caret-down .fa-folder-open {
display: inline-block;
}
/* Hide the nested list */
.tree-nested {
display: none;
}
</style>
{% end %}
{% block js%}
<script>
document.getElementById("root_files_button").addEventListener("click", function(){
if(document.forms["zip"]["server_path"].value != ""){
if(document.getElementById('root_files_button').classList.contains('clicked')){
document.getElementById('main-tree-div').innerHTML = '<input type="radio" id="main-tree-input" name="root_path" value="" checked><span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path=""><i class="far fa-folder"></i><i class="far fa-folder-open"></i>{{ translate("serverFiles", "files", data["lang"]) }}</span></input>'
}else{
document.getElementById('root_files_button').classList.add('clicked')
}
path = document.forms["zip"]["server_path"].value;
console.log(document.forms["zip"]["server_path"].value)
var token = getCookie("_xsrf");
var dialog = bootbox.dialog({
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
});
$.ajax({
type: "POST",
headers: {'X-XSRFToken': token},
url: '/ajax/unzip_server?id=-1&path='+path,
});
}else{
bootbox.alert("You must input a path before selecting this button");
}
});
</script>
<script>
function dropDown(event) {
@ -365,11 +479,15 @@ function hide(event) {
function wait_msg(importing){
bootbox.alert({
title: importing ? '{% raw translate("serverWizard", "importing", data['lang']) %}' : '{% raw translate("serverWizard", "downloading", data['lang']) %}',
message: '<i class="fas fa-cloud-download"></i> {% raw translate("serverWizard", "bePatient", data['lang']) %}'
title: importing ? '{% raw translate("serverWizard", "importing", data["lang"]) %}' : '{% raw translate("serverWizard", "downloading", data["lang"]) %}',
message: '<i class="fas fa-cloud-download"></i> {% raw translate("serverWizard", "bePatient", data["lang"]) %}',
});
}
function show_file_tree(){
$("#dir_select").modal();
}
function check_sizes(a, b, changed){
max_mem = parseFloat(a.val());
min_mem = parseFloat(b.val());
@ -381,6 +499,108 @@ function hide(event) {
}
}
function getTreeView(path) {
document.getElementById('zip_submit').disabled = false;
path = path
$.ajax({
type: "GET",
url: '/ajax/get_zip_tree?id=-1&path='+path,
dataType: 'text',
success: function(data){
console.log("got response:");
console.log(data);
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
try{
document.getElementById('main-tree-div').innerHTML += text;
document.getElementById('main-tree').parentElement.classList.add("clicked");
}catch{
document.getElementById('files-tree').innerHTML = text;
}
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-path', serverDir);
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-name', 'Files');
},
});
}
function getToggleMain(event) {
path = event.target.parentElement.getAttribute('data-path');
document.getElementById("files-tree").classList.toggle("d-block");
document.getElementById(path+"span").classList.toggle("tree-caret-down");
document.getElementById(path+"span").classList.toggle("tree-caret");
}
function getDirView(event) {
path = event.target.parentElement.getAttribute('data-path');
if (document.getElementById(path).classList.contains('clicked')){
var toggler = document.getElementById(path+"span");
if (toggler.classList.contains('files-tree-title')){
document.getElementById(path+"ul").classList.toggle("d-block");
document.getElementById(path+"span").classList.toggle("tree-caret-down");
}
return;
}else{
$.ajax({
type: "GET",
url: '/ajax/get_zip_dir?id=-1&path='+path,
dataType: 'text',
success: function(data){
console.log("got response:");
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
try{
document.getElementById(path+"span").classList.add('tree-caret-down');
document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked");
}catch{
console.log("Bad")
}
var toggler = document.getElementById(path);
if (toggler.classList.contains('files-tree-title')){
document.getElementById(path+"span").addEventListener("click", function caretListener() {
document.getElementById(path+"ul").classList.toggle("d-block");
document.getElementById(path+"span").classList.toggle("tree-caret-down");
});
}
},
});
}
}
if (webSocket) {
webSocket.on('send_temp_path', function (data) {
setTimeout(function(){
var x = document.querySelector('.bootbox');
if (x) {
x.remove()
}
var x = document.querySelector('.modal-backdrop');
if (x) {
x.remove()
}
document.getElementById('main-tree-input').setAttribute('value', data.path)
getTreeView(data.path);
show_file_tree();
}, 5000);
});
}
</script>
<script type="text/javascript">
//<![CDATA[
@ -419,8 +639,8 @@ function hide(event) {
cSelect.remove(0);
}
var newOption;
// create new options ordered by descending
for (var i=(cList.length)-1; i>=0; i--) {
// create new options ordered by ascending
for (var i=0; i < (cList.length); i++) {
newOption = document.createElement("option");
newOption.value = which+"|"+cList[i]; // assumes option string and value are the same
newOption.text=cList[i];

View File

@ -6,7 +6,7 @@
<div class="auto-form-wrapper">
<div class="text-center">
<!-- <img src="/static/assets/images/logo_long.jpg">-->
<!-- <img src="/static/assets/images/logo_long.svg">-->
{{ _('Configure Your Existing Server') }}<br /><br />
</div>
<form action="/public/login" method="post">

View File

@ -0,0 +1,17 @@
# Generated by database migrator
import peewee
from app.classes.models.management import Schedules
def migrate(migrator, database, **kwargs):
migrator.drop_columns('backups', ['schedule_id'])
"""
Write your migrations here.
"""
def rollback(migrator, database, **kwargs):
migrator.add_columns('backups', schedule_id=peewee.ForeignKeyField(Schedules, backref='backups_schedule'))
"""
Write your rollback migrations here.
"""

View File

@ -0,0 +1,16 @@
# Generated by database migrator
import peewee
def migrate(migrator, database, **kwargs):
migrator.add_columns('server_stats', crashed=peewee.BooleanField(default=False))
"""
Write your migrations here.
"""
def rollback(migrator, database, **kwargs):
migrator.drop_columns('server_stats', ['crashed'])
"""
Write your rollback migrations here.
"""

View File

@ -0,0 +1,16 @@
# Generated by database migrator
import peewee
def migrate(migrator, database, **kwargs):
migrator.add_columns('schedules', cron_string=peewee.CharField(default=""))
"""
Write your migrations here.
"""
def rollback(migrator, database, **kwargs):
migrator.drop_columns('schedules', ['cron_string'])
"""
Write your rollback migrations here.
"""

View File

@ -0,0 +1,16 @@
# Generated by database migrator
import peewee
def migrate(migrator, database, **kwargs):
migrator.add_columns('server_stats', first_run=peewee.BooleanField(default=True))
"""
Write your migrations here.
"""
def rollback(migrator, database, **kwargs):
migrator.drop_columns('server_stats', ['first_run'])
"""
Write your rollback migrations here.
"""

Some files were not shown because too many files have changed in this diff Show More