mirror of
https://gitlab.com/crafty-controller/crafty-4.git
synced 2024-08-30 18:23:09 +00:00
Merge branch 'refactor/backups' into refactor/upload-api
This commit is contained in:
commit
d9b9f00e9a
@ -56,8 +56,8 @@ get_keys "${DIR}/en_EN.json" | sort > "${ref_keys}"
|
|||||||
|
|
||||||
# Iterate over each .json file in the directory
|
# Iterate over each .json file in the directory
|
||||||
for file in "${DIR}"/*.json; do
|
for file in "${DIR}"/*.json; do
|
||||||
# Check if file is a regular file and not en_EN.json, and does not contain "_incomplete" in its name
|
# Check if file is a regular file and not en_EN.json, humanized index and does not contain "_incomplete" in its name
|
||||||
if [[ -f "${file}" && "${file}" != "${DIR}/en_EN.json" && ! "${file}" =~ _incomplete ]]; then
|
if [[ -f "${file}" && "${file}" != "${DIR}/en_EN.json" && "${file}" != "${DIR}/humanized_index.json" && ! "${file}" =~ _incomplete ]]; then
|
||||||
|
|
||||||
# Get keys and subkeys from the current file
|
# Get keys and subkeys from the current file
|
||||||
current_keys=$(mktemp)
|
current_keys=$(mktemp)
|
||||||
|
13
CHANGELOG.md
13
CHANGELOG.md
@ -2,17 +2,30 @@
|
|||||||
## --- [4.4.1] - 2024/TBD
|
## --- [4.4.1] - 2024/TBD
|
||||||
### New features
|
### New features
|
||||||
TBD
|
TBD
|
||||||
|
### Refactor
|
||||||
|
- Backups | Allow multiple backup configurations ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/711))
|
||||||
### Bug fixes
|
### Bug fixes
|
||||||
- Fix zip imports so the root dir selection is functional ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/764))
|
- Fix zip imports so the root dir selection is functional ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/764))
|
||||||
- Fix bug where full access gives minimal access ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/768))
|
- Fix bug where full access gives minimal access ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/768))
|
||||||
|
- Bump tornado & requests for sec advisories ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/774))
|
||||||
- Ensure audit.log exists or create it on Crafty startup ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/771))
|
- Ensure audit.log exists or create it on Crafty startup ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/771))
|
||||||
|
- Fix typing issue on ID comparison causing general users to not be able to delete their own API keys ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/775))
|
||||||
|
- Fix user creation bug where it would fail when a role was selected ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/763))
|
||||||
|
- Security improvements for general user creations on roles page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/763))
|
||||||
|
- Security improvements for general user creations on user page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/763))
|
||||||
|
- Use UTC for tokens_valid_from in user config, to resolve token invalidation on instance TZ change ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/765))
|
||||||
### Tweaks
|
### Tweaks
|
||||||
- Add info note to default creds file ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/760))
|
- Add info note to default creds file ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/760))
|
||||||
- Remove navigation label from sidebar ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/766))
|
- Remove navigation label from sidebar ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/766))
|
||||||
|
- Do not allow slashes in server names ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/767))
|
||||||
- Add a thread dump to support logs ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/769))
|
- Add a thread dump to support logs ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/769))
|
||||||
- Remove text from status page and use symbols ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/770))
|
- Remove text from status page and use symbols ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/770))
|
||||||
|
- Add better feedback on when errors appear on user creation ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/763))
|
||||||
|
- Workaround cpu_freq call catching on obscure cpu architectures ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/776))
|
||||||
### Lang
|
### Lang
|
||||||
|
- Show natural language name instead of country code in User Config Lang select list ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/773))
|
||||||
- Add remaining `he_IL`, `th_TH` translations for 4.4.0 Release ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/761))
|
- Add remaining `he_IL`, `th_TH` translations for 4.4.0 Release ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/761))
|
||||||
|
- Mark `he_IL` incomplete ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/763))
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
## --- [4.4.0] - 2024/05/11
|
## --- [4.4.0] - 2024/05/11
|
||||||
|
@ -55,6 +55,7 @@ class UsersController:
|
|||||||
"minLength": self.helper.minimum_password_length,
|
"minLength": self.helper.minimum_password_length,
|
||||||
"examples": ["crafty"],
|
"examples": ["crafty"],
|
||||||
"title": "Password",
|
"title": "Password",
|
||||||
|
"error": "passLength",
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -86,7 +86,7 @@ class Stats:
|
|||||||
def get_node_stats(self) -> NodeStatsReturnDict:
|
def get_node_stats(self) -> NodeStatsReturnDict:
|
||||||
try:
|
try:
|
||||||
cpu_freq = psutil.cpu_freq()
|
cpu_freq = psutil.cpu_freq()
|
||||||
except (NotImplementedError, FileNotFoundError):
|
except (NotImplementedError, AttributeError, FileNotFoundError):
|
||||||
cpu_freq = None
|
cpu_freq = None
|
||||||
if cpu_freq is None:
|
if cpu_freq is None:
|
||||||
cpu_freq = psutil._common.scpufreq(current=-1, min=-1, max=-1)
|
cpu_freq = psutil._common.scpufreq(current=-1, min=-1, max=-1)
|
||||||
|
@ -38,7 +38,7 @@ class Users(BaseModel):
|
|||||||
superuser = BooleanField(default=False)
|
superuser = BooleanField(default=False)
|
||||||
lang = CharField(default="en_EN")
|
lang = CharField(default="en_EN")
|
||||||
support_logs = CharField(default="")
|
support_logs = CharField(default="")
|
||||||
valid_tokens_from = DateTimeField(default=datetime.datetime.now)
|
valid_tokens_from = DateTimeField(default=Helpers.get_utc_now)
|
||||||
server_order = CharField(default="")
|
server_order = CharField(default="")
|
||||||
preparing = BooleanField(default=False)
|
preparing = BooleanField(default=False)
|
||||||
hints = BooleanField(default=True)
|
hints = BooleanField(default=True)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
from datetime import datetime
|
||||||
from typing import Optional, Dict, Any, Tuple
|
from typing import Optional, Dict, Any, Tuple
|
||||||
import jwt
|
import jwt
|
||||||
from jwt import PyJWTError
|
from jwt import PyJWTError
|
||||||
@ -62,7 +63,17 @@ class Authentication:
|
|||||||
user = HelperUsers.get_user(user_id)
|
user = HelperUsers.get_user(user_id)
|
||||||
# TODO: Have a cache or something so we don't constantly
|
# TODO: Have a cache or something so we don't constantly
|
||||||
# have to query the database
|
# have to query the database
|
||||||
if int(user.get("valid_tokens_from").timestamp()) < iat:
|
valid_tokens_from_str = user.get("valid_tokens_from")
|
||||||
|
# It's possible this will be a string or a dt coming from the DB
|
||||||
|
# We need to account for that
|
||||||
|
try:
|
||||||
|
valid_tokens_from_dt = datetime.strptime(
|
||||||
|
valid_tokens_from_str, "%Y-%m-%d %H:%M:%S.%f%z"
|
||||||
|
)
|
||||||
|
except TypeError:
|
||||||
|
valid_tokens_from_dt = valid_tokens_from_str
|
||||||
|
# Convert the string to a datetime object
|
||||||
|
if int(valid_tokens_from_dt.timestamp()) < iat:
|
||||||
# Success!
|
# Success!
|
||||||
return key, data, user
|
return key, data, user
|
||||||
return None
|
return None
|
||||||
|
@ -19,7 +19,7 @@ import shutil
|
|||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import itertools
|
import itertools
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from socket import gethostname
|
from socket import gethostname
|
||||||
from contextlib import redirect_stderr, suppress
|
from contextlib import redirect_stderr, suppress
|
||||||
import libgravatar
|
import libgravatar
|
||||||
@ -639,6 +639,10 @@ class Helpers:
|
|||||||
version = f"{major}.{minor}.{sub}"
|
version = f"{major}.{minor}.{sub}"
|
||||||
return str(version)
|
return str(version)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_utc_now() -> datetime:
|
||||||
|
return datetime.fromtimestamp(time.time(), tz=timezone.utc)
|
||||||
|
|
||||||
def encode_pass(self, password):
|
def encode_pass(self, password):
|
||||||
return self.passhasher.hash(password)
|
return self.passhasher.hash(password)
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ class Translation:
|
|||||||
def get_language_file(self, language: str):
|
def get_language_file(self, language: str):
|
||||||
return os.path.join(self.translations_path, str(language) + ".json")
|
return os.path.join(self.translations_path, str(language) + ".json")
|
||||||
|
|
||||||
def translate(self, page, word, language):
|
def translate(self, page, word, language, error=True):
|
||||||
fallback_language = "en_EN"
|
fallback_language = "en_EN"
|
||||||
|
|
||||||
translated_word = self.translate_inner(page, word, language)
|
translated_word = self.translate_inner(page, word, language)
|
||||||
@ -37,7 +37,9 @@ class Translation:
|
|||||||
if hasattr(translated_word, "__iter__"):
|
if hasattr(translated_word, "__iter__"):
|
||||||
# Multiline strings
|
# Multiline strings
|
||||||
return "\n".join(translated_word)
|
return "\n".join(translated_word)
|
||||||
return "Error while getting translation"
|
if error:
|
||||||
|
return "Error while getting translation"
|
||||||
|
return word
|
||||||
|
|
||||||
def translate_inner(self, page, word, language) -> t.Union[t.Any, None]:
|
def translate_inner(self, page, word, language) -> t.Union[t.Any, None]:
|
||||||
language_file = self.get_language_file(language)
|
language_file = self.get_language_file(language)
|
||||||
|
@ -879,6 +879,8 @@ class PanelHandler(BaseHandler):
|
|||||||
os.path.join(self.helper.root_dir, "app", "translations")
|
os.path.join(self.helper.root_dir, "app", "translations")
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
|
if file == "humanized_index.json":
|
||||||
|
continue
|
||||||
if file.endswith(".json"):
|
if file.endswith(".json"):
|
||||||
if file.split(".")[0] not in self.helper.get_setting(
|
if file.split(".")[0] not in self.helper.get_setting(
|
||||||
"disabled_language_files"
|
"disabled_language_files"
|
||||||
@ -1432,6 +1434,8 @@ class PanelHandler(BaseHandler):
|
|||||||
for file in sorted(
|
for file in sorted(
|
||||||
os.listdir(os.path.join(self.helper.root_dir, "app", "translations"))
|
os.listdir(os.path.join(self.helper.root_dir, "app", "translations"))
|
||||||
):
|
):
|
||||||
|
if file == "humanized_index.json":
|
||||||
|
continue
|
||||||
if file.endswith(".json"):
|
if file.endswith(".json"):
|
||||||
if file.split(".")[0] not in self.helper.get_setting(
|
if file.split(".")[0] not in self.helper.get_setting(
|
||||||
"disabled_language_files"
|
"disabled_language_files"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import datetime
|
|
||||||
import logging
|
import logging
|
||||||
from app.classes.web.base_api_handler import BaseApiHandler
|
from app.classes.web.base_api_handler import BaseApiHandler
|
||||||
|
from app.classes.shared.helpers import Helpers
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ class ApiAuthInvalidateTokensHandler(BaseApiHandler):
|
|||||||
|
|
||||||
logger.debug(f"Invalidate tokens for user {auth_data[4]['user_id']}")
|
logger.debug(f"Invalidate tokens for user {auth_data[4]['user_id']}")
|
||||||
self.controller.users.raw_update_user(
|
self.controller.users.raw_update_user(
|
||||||
auth_data[4]["user_id"], {"valid_tokens_from": datetime.datetime.now()}
|
auth_data[4]["user_id"], {"valid_tokens_from": Helpers.get_utc_now()}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.finish_json(200, {"status": "ok"})
|
self.finish_json(200, {"status": "ok"})
|
||||||
|
@ -2,6 +2,7 @@ import typing as t
|
|||||||
from jsonschema import ValidationError, validate
|
from jsonschema import ValidationError, validate
|
||||||
import orjson
|
import orjson
|
||||||
from playhouse.shortcuts import model_to_dict
|
from playhouse.shortcuts import model_to_dict
|
||||||
|
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
|
||||||
from app.classes.web.base_api_handler import BaseApiHandler
|
from app.classes.web.base_api_handler import BaseApiHandler
|
||||||
|
|
||||||
create_role_schema = {
|
create_role_schema = {
|
||||||
@ -71,7 +72,7 @@ class ApiRolesIndexHandler(BaseApiHandler):
|
|||||||
return
|
return
|
||||||
(
|
(
|
||||||
_,
|
_,
|
||||||
_,
|
exec_user_permissions_crafty,
|
||||||
_,
|
_,
|
||||||
superuser,
|
superuser,
|
||||||
_,
|
_,
|
||||||
@ -81,7 +82,10 @@ class ApiRolesIndexHandler(BaseApiHandler):
|
|||||||
# GET /api/v2/roles?ids=true
|
# GET /api/v2/roles?ids=true
|
||||||
get_only_ids = self.get_query_argument("ids", None) == "true"
|
get_only_ids = self.get_query_argument("ids", None) == "true"
|
||||||
|
|
||||||
if not superuser:
|
if (
|
||||||
|
not superuser
|
||||||
|
and EnumPermissionsCrafty.ROLES_CONFIG not in exec_user_permissions_crafty
|
||||||
|
):
|
||||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||||
|
|
||||||
self.finish_json(
|
self.finish_json(
|
||||||
@ -104,14 +108,17 @@ class ApiRolesIndexHandler(BaseApiHandler):
|
|||||||
return
|
return
|
||||||
(
|
(
|
||||||
_,
|
_,
|
||||||
_,
|
exec_user_permissions_crafty,
|
||||||
_,
|
_,
|
||||||
superuser,
|
superuser,
|
||||||
user,
|
user,
|
||||||
_,
|
_,
|
||||||
) = auth_data
|
) = auth_data
|
||||||
|
|
||||||
if not superuser:
|
if (
|
||||||
|
not superuser
|
||||||
|
and EnumPermissionsCrafty.ROLES_CONFIG not in exec_user_permissions_crafty
|
||||||
|
):
|
||||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -138,6 +145,8 @@ class ApiRolesIndexHandler(BaseApiHandler):
|
|||||||
|
|
||||||
role_name = data["name"]
|
role_name = data["name"]
|
||||||
manager = data.get("manager", None)
|
manager = data.get("manager", None)
|
||||||
|
if not superuser and not manager:
|
||||||
|
manager = auth_data[4]["user_id"]
|
||||||
if manager == self.controller.users.get_id_by_name("SYSTEM") or manager == 0:
|
if manager == self.controller.users.get_id_by_name("SYSTEM") or manager == 0:
|
||||||
manager = None
|
manager = None
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from jsonschema import ValidationError, validate
|
from jsonschema import ValidationError, validate
|
||||||
import orjson
|
import orjson
|
||||||
from peewee import DoesNotExist
|
from peewee import DoesNotExist, IntegrityError
|
||||||
|
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
|
||||||
from app.classes.web.base_api_handler import BaseApiHandler
|
from app.classes.web.base_api_handler import BaseApiHandler
|
||||||
|
|
||||||
modify_role_schema = {
|
modify_role_schema = {
|
||||||
@ -70,14 +71,17 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
|
|||||||
return
|
return
|
||||||
(
|
(
|
||||||
_,
|
_,
|
||||||
_,
|
exec_user_permissions_crafty,
|
||||||
_,
|
_,
|
||||||
superuser,
|
superuser,
|
||||||
_,
|
_,
|
||||||
_,
|
_,
|
||||||
) = auth_data
|
) = auth_data
|
||||||
|
|
||||||
if not superuser:
|
if (
|
||||||
|
not superuser
|
||||||
|
and EnumPermissionsCrafty.ROLES_CONFIG not in exec_user_permissions_crafty
|
||||||
|
):
|
||||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -100,8 +104,11 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
|
|||||||
user,
|
user,
|
||||||
_,
|
_,
|
||||||
) = auth_data
|
) = auth_data
|
||||||
|
role = self.controller.roles.get_role(role_id)
|
||||||
if not superuser:
|
if (
|
||||||
|
str(role.get("manager", "no manager found")) != str(auth_data[4]["user_id"])
|
||||||
|
and not superuser
|
||||||
|
):
|
||||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||||
|
|
||||||
self.controller.roles.remove_role(role_id)
|
self.controller.roles.remove_role(role_id)
|
||||||
@ -124,7 +131,7 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
|
|||||||
return
|
return
|
||||||
(
|
(
|
||||||
_,
|
_,
|
||||||
_,
|
exec_user_permissions_crafty,
|
||||||
_,
|
_,
|
||||||
superuser,
|
superuser,
|
||||||
user,
|
user,
|
||||||
@ -132,7 +139,10 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
|
|||||||
) = auth_data
|
) = auth_data
|
||||||
|
|
||||||
role = self.controller.roles.get_role(role_id)
|
role = self.controller.roles.get_role(role_id)
|
||||||
if not superuser and user["user_id"] != role["manager"]:
|
if not superuser and (
|
||||||
|
user["user_id"] != role["manager"]
|
||||||
|
or EnumPermissionsCrafty.ROLES_CONFIG not in exec_user_permissions_crafty
|
||||||
|
):
|
||||||
return self.finish_json(
|
return self.finish_json(
|
||||||
400,
|
400,
|
||||||
{
|
{
|
||||||
@ -179,7 +189,10 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
|
|||||||
)
|
)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
return self.finish_json(404, {"status": "error", "error": "ROLE_NOT_FOUND"})
|
return self.finish_json(404, {"status": "error", "error": "ROLE_NOT_FOUND"})
|
||||||
|
except IntegrityError:
|
||||||
|
return self.finish_json(
|
||||||
|
404, {"status": "error", "error": "ROLE_NAME_EXISTS"}
|
||||||
|
)
|
||||||
self.controller.management.add_to_audit_log(
|
self.controller.management.add_to_audit_log(
|
||||||
user["user_id"],
|
user["user_id"],
|
||||||
f"modified role with ID {role_id}",
|
f"modified role with ID {role_id}",
|
||||||
|
@ -23,6 +23,7 @@ new_server_schema = {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"examples": ["My Server"],
|
"examples": ["My Server"],
|
||||||
"minLength": 2,
|
"minLength": 2,
|
||||||
|
"pattern": "^[^/\\\\]*$",
|
||||||
},
|
},
|
||||||
"roles": {"title": "Roles to add", "type": "array", "examples": [1, 2, 3]},
|
"roles": {"title": "Roles to add", "type": "array", "examples": [1, 2, 3]},
|
||||||
"stop_command": {
|
"stop_command": {
|
||||||
|
@ -414,10 +414,13 @@ class ApiServersServerBackupsBackupFilesIndexHandler(BaseApiHandler):
|
|||||||
"error_data": str(e),
|
"error_data": str(e),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
FileHelpers.del_file(
|
FileHelpers.del_file(
|
||||||
os.path.join(backup_conf["backup_location"], data["filename"])
|
os.path.join(
|
||||||
|
backup_conf["backup_location"],
|
||||||
|
backup_conf["backup_id"],
|
||||||
|
data["filename"],
|
||||||
|
)
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return self.finish_json(
|
return self.finish_json(
|
||||||
|
@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
|
|||||||
server_patch_schema = {
|
server_patch_schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"server_name": {"type": "string", "minLength": 1},
|
"server_name": {"type": "string", "minLength": 2, "pattern": "^[^/\\\\]*$"},
|
||||||
"backup_path": {"type": "string"},
|
"backup_path": {"type": "string"},
|
||||||
"executable": {"type": "string"},
|
"executable": {"type": "string"},
|
||||||
"log_path": {"type": "string", "minLength": 1},
|
"log_path": {"type": "string", "minLength": 1},
|
||||||
|
@ -2,6 +2,7 @@ import logging
|
|||||||
import json
|
import json
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
from jsonschema.exceptions import ValidationError
|
from jsonschema.exceptions import ValidationError
|
||||||
|
from app.classes.shared.translation import Translation
|
||||||
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
|
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
|
||||||
from app.classes.models.roles import Roles, HelperRoles
|
from app.classes.models.roles import Roles, HelperRoles
|
||||||
from app.classes.models.users import PUBLIC_USER_ATTRS
|
from app.classes.models.users import PUBLIC_USER_ATTRS
|
||||||
@ -54,6 +55,7 @@ class ApiUsersIndexHandler(BaseApiHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
|
self.translator = Translation(self.helper)
|
||||||
new_user_schema = {
|
new_user_schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -87,12 +89,17 @@ class ApiUsersIndexHandler(BaseApiHandler):
|
|||||||
try:
|
try:
|
||||||
validate(data, new_user_schema)
|
validate(data, new_user_schema)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
|
err = self.translator.translate(
|
||||||
|
"validators",
|
||||||
|
e.schema["error"],
|
||||||
|
self.controller.users.get_user_lang_by_id(auth_data[4]["user_id"]),
|
||||||
|
)
|
||||||
return self.finish_json(
|
return self.finish_json(
|
||||||
400,
|
400,
|
||||||
{
|
{
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"error": "INVALID_JSON_SCHEMA",
|
"error": "INVALID_JSON_SCHEMA",
|
||||||
"error_data": str(e),
|
"error_data": f"{str(err)}",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
username = data["username"]
|
username = data["username"]
|
||||||
@ -153,7 +160,11 @@ class ApiUsersIndexHandler(BaseApiHandler):
|
|||||||
|
|
||||||
for role in roles:
|
for role in roles:
|
||||||
role = self.controller.roles.get_role(role)
|
role = self.controller.roles.get_role(role)
|
||||||
if int(role["manager"]) != int(auth_data[4]["user_id"]) and not superuser:
|
if (
|
||||||
|
str(role.get("manager", "no manager found"))
|
||||||
|
!= str(auth_data[4]["user_id"])
|
||||||
|
and not superuser
|
||||||
|
):
|
||||||
return self.finish_json(
|
return self.finish_json(
|
||||||
400, {"status": "error", "error": "INVALID_ROLES_CREATE"}
|
400, {"status": "error", "error": "INVALID_ROLES_CREATE"}
|
||||||
)
|
)
|
||||||
|
@ -217,7 +217,7 @@ class ApiUsersUserKeyHandler(BaseApiHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
target_key.user_id != auth_data[4]["user_id"]
|
str(target_key.user_id) != str(auth_data[4]["user_id"])
|
||||||
and not auth_data[4]["superuser"]
|
and not auth_data[4]["superuser"]
|
||||||
):
|
):
|
||||||
return self.finish_json(
|
return self.finish_json(
|
||||||
|
@ -132,7 +132,6 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
|
|||||||
return self.finish_json(
|
return self.finish_json(
|
||||||
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
|
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
validate(data, user_patch_schema)
|
validate(data, user_patch_schema)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
@ -144,10 +143,8 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
|
|||||||
"error_data": str(e),
|
"error_data": str(e),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if user_id == "@me":
|
if user_id == "@me":
|
||||||
user_id = user["user_id"]
|
user_id = user["user_id"]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
EnumPermissionsCrafty.USER_CONFIG not in exec_user_crafty_permissions
|
EnumPermissionsCrafty.USER_CONFIG not in exec_user_crafty_permissions
|
||||||
and str(user["user_id"]) != str(user_id)
|
and str(user["user_id"]) != str(user_id)
|
||||||
@ -215,6 +212,25 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
|
|||||||
return self.finish_json(
|
return self.finish_json(
|
||||||
400, {"status": "error", "error": "INVALID_ROLES_MODIFY"}
|
400, {"status": "error", "error": "INVALID_ROLES_MODIFY"}
|
||||||
)
|
)
|
||||||
|
user_modify = self.controller.users.get_user_roles_id(user_id)
|
||||||
|
|
||||||
|
for role in data["roles"]:
|
||||||
|
# Check if user is not a super user and that the exec user is the role
|
||||||
|
# manager or that the role already exists in the user's list
|
||||||
|
if not superuser and (
|
||||||
|
str(
|
||||||
|
self.controller.roles.get_role(role).get(
|
||||||
|
"manager", "no manager found"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
!= str(auth_data[4]["user_id"])
|
||||||
|
and role not in user_modify
|
||||||
|
):
|
||||||
|
for item in user_modify:
|
||||||
|
print(type(role), type(item))
|
||||||
|
return self.finish_json(
|
||||||
|
400, {"status": "error", "error": "INVALID_ROLES_MODIFY"}
|
||||||
|
)
|
||||||
|
|
||||||
user_obj = HelperUsers.get_user_model(user_id)
|
user_obj = HelperUsers.get_user_model(user_id)
|
||||||
if "password" in data and str(user["user_id"]) != str(user_id):
|
if "password" in data and str(user["user_id"]) != str(user_id):
|
||||||
|
@ -428,10 +428,13 @@
|
|||||||
if (responseData.status === "ok") {
|
if (responseData.status === "ok") {
|
||||||
window.location.href = "/panel/panel_config";
|
window.location.href = "/panel/panel_config";
|
||||||
} else {
|
} else {
|
||||||
|
let errordata = responseData.error;
|
||||||
|
if (responseData.error_data){
|
||||||
|
errordata = responseData.error
|
||||||
|
}
|
||||||
bootbox.alert({
|
bootbox.alert({
|
||||||
title: responseData.error,
|
title: responseData.error,
|
||||||
message: responseData.error_data
|
message: errordata
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -122,7 +122,7 @@ data['lang']) }}{% end %}
|
|||||||
name="lang" form="user_form">
|
name="lang" form="user_form">
|
||||||
{% for lang in data['languages'] %}
|
{% for lang in data['languages'] %}
|
||||||
{% if not 'incomplete' in lang %}
|
{% if not 'incomplete' in lang %}
|
||||||
<option value="{{lang}}">{{lang}}</option>
|
<option value="{{lang}}" >{{translate('language', lang, 'humanized_index')}}</option>
|
||||||
{% else %}
|
{% else %}
|
||||||
<option value="{{lang}}" disabled>{{lang}}</option>
|
<option value="{{lang}}" disabled>{{lang}}</option>
|
||||||
{% end %}
|
{% end %}
|
||||||
@ -393,6 +393,7 @@ data['lang']) }}{% end %}
|
|||||||
}
|
}
|
||||||
function replacer(key, value) {
|
function replacer(key, value) {
|
||||||
if (typeof value == "boolean" || key === "email" || key === "permissions" || key === "roles") {
|
if (typeof value == "boolean" || key === "email" || key === "permissions" || key === "roles") {
|
||||||
|
console.log(key)
|
||||||
return value
|
return value
|
||||||
} else {
|
} else {
|
||||||
console.log(key, value)
|
console.log(key, value)
|
||||||
@ -433,6 +434,7 @@ data['lang']) }}{% end %}
|
|||||||
let disabled_flag = false;
|
let disabled_flag = false;
|
||||||
let roles = null;
|
let roles = null;
|
||||||
if (superuser || userId != edit_id){
|
if (superuser || userId != edit_id){
|
||||||
|
console.log("ROLES")
|
||||||
roles = $('.role_check').map(function() {
|
roles = $('.role_check').map(function() {
|
||||||
if ($(this).attr("disabled")){
|
if ($(this).attr("disabled")){
|
||||||
disabled_flag = true;
|
disabled_flag = true;
|
||||||
@ -457,9 +459,7 @@ data['lang']) }}{% end %}
|
|||||||
delete formDataObject.username
|
delete formDataObject.username
|
||||||
}
|
}
|
||||||
if (superuser || userId != edit_id){
|
if (superuser || userId != edit_id){
|
||||||
if (!disabled_flag){
|
|
||||||
formDataObject.roles = roles;
|
formDataObject.roles = roles;
|
||||||
}
|
|
||||||
if ($("#permissions").length){
|
if ($("#permissions").length){
|
||||||
formDataObject.permissions = permissions;
|
formDataObject.permissions = permissions;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import peewee
|
import peewee
|
||||||
import datetime
|
import datetime
|
||||||
|
from app.classes.shared.helpers import Helpers
|
||||||
|
|
||||||
|
|
||||||
def migrate(migrator, database, **kwargs):
|
def migrate(migrator, database, **kwargs):
|
||||||
migrator.add_columns(
|
migrator.add_columns(
|
||||||
"users", valid_tokens_from=peewee.DateTimeField(default=datetime.datetime.now)
|
"users", valid_tokens_from=peewee.DateTimeField(default=Helpers.get_utc_now)
|
||||||
)
|
)
|
||||||
migrator.drop_columns("users", ["api_token"])
|
migrator.drop_columns("users", ["api_token"])
|
||||||
|
|
||||||
|
@ -688,6 +688,9 @@
|
|||||||
"userTheme": "Motiv UI",
|
"userTheme": "Motiv UI",
|
||||||
"uses": "Počet povolených použití (-1==bez omezení)"
|
"uses": "Počet povolených použití (-1==bez omezení)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Heslo je příliš krátké. Minimální délka je 8 znaků"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Seš si jistý že chceš smazat tento webhook?",
|
"areYouSureDel": "Seš si jistý že chceš smazat tento webhook?",
|
||||||
"areYouSureRun": "Seš si jistý že chceš otestovat tento webhook?",
|
"areYouSureRun": "Seš si jistý že chceš otestovat tento webhook?",
|
||||||
|
@ -669,6 +669,9 @@
|
|||||||
"userTheme": "Design für die Benutzeroberfläche",
|
"userTheme": "Design für die Benutzeroberfläche",
|
||||||
"uses": "Anzahl der erlaubten Verwendungen (-1==Keine Begrenzung)"
|
"uses": "Anzahl der erlaubten Verwendungen (-1==Keine Begrenzung)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Passwort zu kurz. Mindestlänge: 8"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Sind Sie sicher, dass Sie diesen Webhook löschen möchten?",
|
"areYouSureDel": "Sind Sie sicher, dass Sie diesen Webhook löschen möchten?",
|
||||||
"areYouSureRun": "Sind Sie sicher, dass Sie diesen Webhook testen möchten?",
|
"areYouSureRun": "Sind Sie sicher, dass Sie diesen Webhook testen möchten?",
|
||||||
|
@ -665,6 +665,9 @@
|
|||||||
"userTheme": "UI Theme",
|
"userTheme": "UI Theme",
|
||||||
"uses": "Number of uses allowed (-1==No Limit)"
|
"uses": "Number of uses allowed (-1==No Limit)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Password Too Short. Minimum Length: 8"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Are you sure you want to delete this webhook?",
|
"areYouSureDel": "Are you sure you want to delete this webhook?",
|
||||||
"areYouSureRun": "Are you sure you want to test this webhook?",
|
"areYouSureRun": "Are you sure you want to test this webhook?",
|
||||||
|
@ -669,6 +669,9 @@
|
|||||||
"userTheme": "Tema de Interfaz",
|
"userTheme": "Tema de Interfaz",
|
||||||
"uses": "Número de usos permitidos. (Sin límite: -1)"
|
"uses": "Número de usos permitidos. (Sin límite: -1)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Contraseña demasiado corta. Longitud mínima: 8"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "¿Estás seguro de que quieres eliminar este webhook?",
|
"areYouSureDel": "¿Estás seguro de que quieres eliminar este webhook?",
|
||||||
"areYouSureRun": "¿Estás seguro de que quieres probar este webhook?",
|
"areYouSureRun": "¿Estás seguro de que quieres probar este webhook?",
|
||||||
|
@ -669,6 +669,9 @@
|
|||||||
"userTheme": "Theme d'Interface Utilisateur",
|
"userTheme": "Theme d'Interface Utilisateur",
|
||||||
"uses": "Nombre d'utilisation Authorisé (-1 == Illimité)"
|
"uses": "Nombre d'utilisation Authorisé (-1 == Illimité)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Mot de passe trop court. Longueur minimum : 8"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Es-tu sûr de vouloir supprimer ce webhook ?",
|
"areYouSureDel": "Es-tu sûr de vouloir supprimer ce webhook ?",
|
||||||
"areYouSureRun": "Es-tu sûr de vouloir tester ce webhook ?",
|
"areYouSureRun": "Es-tu sûr de vouloir tester ce webhook ?",
|
||||||
|
19
app/translations/humanized_index.json
Normal file
19
app/translations/humanized_index.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"language": {
|
||||||
|
"cs_CS": "Čeština",
|
||||||
|
"de_DE": "Deutsch",
|
||||||
|
"en_EN": "English (US)",
|
||||||
|
"es_ES": "Español",
|
||||||
|
"fr_FR": "Français (France)",
|
||||||
|
"he_IL": "he_IL",
|
||||||
|
"it_IT": "Italiano",
|
||||||
|
"lol_EN": "Lolcatz",
|
||||||
|
"lv_LV": "Latviešu",
|
||||||
|
"nl_BE": "nl_BE",
|
||||||
|
"pl_PL": "Polski",
|
||||||
|
"th_TH": "ไทย",
|
||||||
|
"tr_TR": "Türkçe",
|
||||||
|
"uk_UA": "Українська",
|
||||||
|
"zh_CN": "中文(中国)"
|
||||||
|
}
|
||||||
|
}
|
@ -669,6 +669,9 @@
|
|||||||
"userTheme": "Tema IU",
|
"userTheme": "Tema IU",
|
||||||
"uses": "Numero di usi permessi (-1==Nessun limite)"
|
"uses": "Numero di usi permessi (-1==Nessun limite)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "La password è troppo corta. Lunghezza minima: 8"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Sei sicuro di voler eliminare questo webhook?",
|
"areYouSureDel": "Sei sicuro di voler eliminare questo webhook?",
|
||||||
"areYouSureRun": "Sei sicuro di voler testare questo webhook?",
|
"areYouSureRun": "Sei sicuro di voler testare questo webhook?",
|
||||||
|
@ -669,6 +669,9 @@
|
|||||||
"userTheme": "THEMEZ",
|
"userTheme": "THEMEZ",
|
||||||
"uses": "NUMBER OV USES ALLOWED (-1==NO LIMIT)"
|
"uses": "NUMBER OV USES ALLOWED (-1==NO LIMIT)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "PASSWRD TOO SMOL. NEEDZ 8 CATZ PLZ"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "U SURE U WANTZ TO EATZ DIS WEBHOOK?",
|
"areYouSureDel": "U SURE U WANTZ TO EATZ DIS WEBHOOK?",
|
||||||
"areYouSureRun": "U SURE U WANTZ TO TESTZ DIS WEBHOOK?",
|
"areYouSureRun": "U SURE U WANTZ TO TESTZ DIS WEBHOOK?",
|
||||||
|
@ -670,6 +670,9 @@
|
|||||||
"userTheme": "UI Tēma",
|
"userTheme": "UI Tēma",
|
||||||
"uses": "Dauzums, cik reizes lietot (-1==Bez Limita)"
|
"uses": "Dauzums, cik reizes lietot (-1==Bez Limita)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Parole pārāk īsa. Minimālais Garums: 8"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Vai tiešām vēlies noņemt šo webhook?",
|
"areYouSureDel": "Vai tiešām vēlies noņemt šo webhook?",
|
||||||
"areYouSureRun": "Vai tiešām vēlies testēt šo webhook?",
|
"areYouSureRun": "Vai tiešām vēlies testēt šo webhook?",
|
||||||
|
@ -669,6 +669,9 @@
|
|||||||
"userTheme": "UI-thema",
|
"userTheme": "UI-thema",
|
||||||
"uses": "Aantal toegestane gebruiken (-1==Geen Limiet)"
|
"uses": "Aantal toegestane gebruiken (-1==Geen Limiet)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Wachtwoord te kort. Minimumlengte: 8 tekens"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Weet u zeker dat u deze webhook wilt verwijderen?",
|
"areYouSureDel": "Weet u zeker dat u deze webhook wilt verwijderen?",
|
||||||
"areYouSureRun": "Weet u zeker dat u deze webhook wilt testen?",
|
"areYouSureRun": "Weet u zeker dat u deze webhook wilt testen?",
|
||||||
|
@ -668,6 +668,9 @@
|
|||||||
"userTheme": "Wygląd interfejsu",
|
"userTheme": "Wygląd interfejsu",
|
||||||
"uses": "Ilość użyć (-1==Bez limitu)"
|
"uses": "Ilość użyć (-1==Bez limitu)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Hasło jest zbyt krótkie. Hasło musi posiadać minimum 8 znaków."
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Usunąć ten webhook?",
|
"areYouSureDel": "Usunąć ten webhook?",
|
||||||
"areYouSureRun": "Przetestować ten webhook?",
|
"areYouSureRun": "Przetestować ten webhook?",
|
||||||
|
@ -668,6 +668,9 @@
|
|||||||
"userTheme": "ธีม UI",
|
"userTheme": "ธีม UI",
|
||||||
"uses": "จำนวนการใช้งานที่อนุญาต (-1==ไม่มีขีดจำกัด)"
|
"uses": "จำนวนการใช้งานที่อนุญาต (-1==ไม่มีขีดจำกัด)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "รหัสผ่านสั้นเกินไป จำนวนตัวอักขระขั้นต่ำ: 8"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "คุณแน่ใจหรือไม่ว่าต้องการลบ Webhook นี้?",
|
"areYouSureDel": "คุณแน่ใจหรือไม่ว่าต้องการลบ Webhook นี้?",
|
||||||
"areYouSureRun": "คุณแน่ใจหรือไม่ว่าต้องการทดสอบ Webhook นี้?",
|
"areYouSureRun": "คุณแน่ใจหรือไม่ว่าต้องการทดสอบ Webhook นี้?",
|
||||||
|
@ -668,6 +668,9 @@
|
|||||||
"userTheme": "UI Teması",
|
"userTheme": "UI Teması",
|
||||||
"uses": "İzin verilen kullanım sayısı (-1==Sınır Yok)"
|
"uses": "İzin verilen kullanım sayısı (-1==Sınır Yok)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Şifre çok kısa. Şifre en az 8 karakter olmalı."
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Bu webhooku silmek istediğinizden emin misiniz?",
|
"areYouSureDel": "Bu webhooku silmek istediğinizden emin misiniz?",
|
||||||
"areYouSureRun": "Bu webhooku test etmek istediğinizden emin misiniz?",
|
"areYouSureRun": "Bu webhooku test etmek istediğinizden emin misiniz?",
|
||||||
|
@ -668,6 +668,9 @@
|
|||||||
"userTheme": "Тема інтерфейсу",
|
"userTheme": "Тема інтерфейсу",
|
||||||
"uses": "Дозволена кількість використань(-1==Без ліміту)"
|
"uses": "Дозволена кількість використань(-1==Без ліміту)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Пароль, надто короткий. Мінімальна довжина: 8 символів"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Ви впевнені, що хочете видалити цей Вебхук?",
|
"areYouSureDel": "Ви впевнені, що хочете видалити цей Вебхук?",
|
||||||
"areYouSureRun": "Ви впевнені, що хочете перевірити цей Вебхук?",
|
"areYouSureRun": "Ви впевнені, що хочете перевірити цей Вебхук?",
|
||||||
|
@ -669,6 +669,9 @@
|
|||||||
"userTheme": "UI 主题",
|
"userTheme": "UI 主题",
|
||||||
"uses": "使用次数限制(-1==无限制)"
|
"uses": "使用次数限制(-1==无限制)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "密码过短。最短长度:8"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "您确定要删除此 webhook 吗?",
|
"areYouSureDel": "您确定要删除此 webhook 吗?",
|
||||||
"areYouSureRun": "您确定要测试此 webhook 吗?",
|
"areYouSureRun": "您确定要测试此 webhook 吗?",
|
||||||
|
@ -13,7 +13,7 @@ psutil==5.9.5
|
|||||||
pyOpenSSL==24.0.0
|
pyOpenSSL==24.0.0
|
||||||
pyjwt==2.8.0
|
pyjwt==2.8.0
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
requests==2.31.0
|
requests==2.32.0
|
||||||
termcolor==1.1
|
termcolor==1.1
|
||||||
tornado==6.4.1
|
tornado==6.4.1
|
||||||
tzlocal==5.1
|
tzlocal==5.1
|
||||||
|
Loading…
Reference in New Issue
Block a user