diff --git a/.gitlab/merge_request_templates/Default.md b/.gitlab/merge_request_templates/Default.md index 2acf3b1e..a82cb3f8 100644 --- a/.gitlab/merge_request_templates/Default.md +++ b/.gitlab/merge_request_templates/Default.md @@ -27,4 +27,3 @@ This checklist encourages us to confirm any changes have been analyzed to reduce * [ ] Have you resolved any lint issues? * [ ] Have you assigned a reviewer? * [ ] Have you applied correct labels? -* [ ] Have you updated CHANGELOG.md? diff --git a/CHANGELOG.md b/CHANGELOG.md index 60bee829..642fc76f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,20 @@ # Changelog -## --- [4.0.6] - 2022/07/06 +## --- [4.0.7] - 2022/07/18 ### New features +- Task toggle ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/398)) +- Basic API for modifying tasks ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/398)) +### Bug fixes None +### Tweaks +None +### Lang +None + +[*Full Changelog*](../compare/v4.0.6...v4.0.7) +

+ +## --- [4.0.6] - 2022/07/06 ### Bug fixes - Remove redundant path check on backup restore ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/390)) - Fix issue with stats pinging on slow starting servers ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/391)) @@ -16,6 +28,8 @@ None - Check for passwords matching on client side ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/395)) ### Lang - Add string "cloneConfirm" to german translation ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/389)) + +[*Full Changelog*](../compare/v4.0.5...v4.0.6)

## --- [4.0.5] - 2022/06/24 @@ -31,6 +45,8 @@ None - Add clone server confirmation ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/384)) ### Lang - German translation review, fixed some spelling issues and added some missing strings ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/385)) + +[*Full Changelog*](../compare/v4.0.4...v4.0.5)

## --- [4.0.4-hotfix2] - 2022/06/21 @@ -58,6 +74,8 @@ None - - ([Merge Request 2](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/377)) - Rework server list on dashboard display for use on small screens ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/372)) - File handling enhancements ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/362)) + +[*Full Changelog*](../compare/v4.0.3...v4.0.4)

## --- [4.0.3] - 2022/06/18 @@ -68,6 +86,8 @@ None - API Token authentication hardening ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/364)) ### Tweaks - Add better error logging for statistic collection ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/359)) + +[*Full Changelog*](../compare/v4.0.2...v4.0.3)

## --- [4.0.2-hotfix1] - 2022/06/17 @@ -84,6 +104,8 @@ None - - ([Merge Request 2](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/351)) - Add version inheretence & config check ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/353)) - Fix support log temp file deletion issue/hang ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/354)) + +[*Full Changelog*](../compare/v4.0.1...v4.0.2)

## --- [4.0.1] - 2022/06/15 diff --git a/app/classes/shared/tasks.py b/app/classes/shared/tasks.py index e9646fca..9876abc2 100644 --- a/app/classes/shared/tasks.py +++ b/app/classes/shared/tasks.py @@ -288,11 +288,13 @@ class TasksManager: 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): HelpersManagement.update_scheduled_task(sch_id, {"parent": None}) - # Check to see if it's enabled and is not a chain reaction. + + # 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: @@ -389,11 +391,21 @@ class TasksManager: ) def update_job(self, sch_id, job_data): - HelpersManagement.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): - HelpersManagement.update_scheduled_task(sch_id, {"parent": None}) + if str(job_data.get("parent")) == str(sch_id): + job_data["parent"] = None + + HelpersManagement.update_scheduled_task(sch_id, job_data) + + if not ( + "interval" in job_data + and "enabled" in job_data + and "cron_string" in job_data + and "interval_type" in job_data + ): + return + try: if job_data["interval"] != "reaction": self.scheduler.remove_job(str(sch_id)) @@ -403,71 +415,70 @@ class TasksManager: "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( - HelpersManagement.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.") - self.controller.management_helper.delete_scheduled_task(sch_id) - else: - if job_data["interval_type"] == "hours": - self.scheduler.add_job( - HelpersManagement.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( - HelpersManagement.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( - HelpersManagement.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"], - ], - ) + if job_data["enabled"] and job_data["interval"] != "reaction": + if job_data["cron_string"] != "": + try: + self.scheduler.add_job( + HelpersManagement.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.") + self.controller.management_helper.delete_scheduled_task(sch_id) + else: + if job_data["interval_type"] == "hours": + self.scheduler.add_job( + HelpersManagement.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( + HelpersManagement.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( + HelpersManagement.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)) diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py index df01f60d..53c45986 100644 --- a/app/classes/web/panel_handler.py +++ b/app/classes/web/panel_handler.py @@ -531,6 +531,7 @@ class PanelHandler(BaseHandler): page_data["downloading"] = self.controller.servers.get_download_status( server_id ) + page_data["server_id"] = server_id try: page_data["waiting_start"] = self.controller.servers.get_waiting_start( server_id diff --git a/app/classes/web/routes/api/api_handlers.py b/app/classes/web/routes/api/api_handlers.py index e5f72b48..29ee02c5 100644 --- a/app/classes/web/routes/api/api_handlers.py +++ b/app/classes/web/routes/api/api_handlers.py @@ -23,6 +23,15 @@ from app.classes.web.routes.api.servers.server.public import ( ) from app.classes.web.routes.api.servers.server.stats import ApiServersServerStatsHandler from app.classes.web.routes.api.servers.server.stdin import ApiServersServerStdinHandler +from app.classes.web.routes.api.servers.server.tasks.index import ( + ApiServersServerTasksIndexHandler, +) +from app.classes.web.routes.api.servers.server.tasks.task.children import ( + ApiServersServerTasksTaskChildrenHandler, +) +from app.classes.web.routes.api.servers.server.tasks.task.index import ( + ApiServersServerTasksTaskIndexHandler, +) from app.classes.web.routes.api.servers.server.users import ApiServersServerUsersHandler from app.classes.web.routes.api.users.index import ApiUsersIndexHandler from app.classes.web.routes.api.users.user.index import ApiUsersUserIndexHandler @@ -103,6 +112,21 @@ def api_handlers(handler_args): ApiServersServerIndexHandler, handler_args, ), + ( + r"/api/v2/servers/([0-9]+)/tasks/?", + ApiServersServerTasksIndexHandler, + handler_args, + ), + ( + r"/api/v2/servers/([0-9]+)/tasks/([0-9]+)/?", + ApiServersServerTasksTaskIndexHandler, + handler_args, + ), + ( + r"/api/v2/servers/([0-9]+)/tasks/([0-9]+)/children/?", + ApiServersServerTasksTaskChildrenHandler, + handler_args, + ), ( r"/api/v2/servers/([0-9]+)/stats/?", ApiServersServerStatsHandler, diff --git a/app/classes/web/routes/api/jsonschema.py b/app/classes/web/routes/api/jsonschema.py index 3497f052..70b19fa7 100644 --- a/app/classes/web/routes/api/jsonschema.py +++ b/app/classes/web/routes/api/jsonschema.py @@ -5,6 +5,7 @@ from app.classes.web.routes.api.roles.role.index import modify_role_schema from app.classes.web.routes.api.roles.index import create_role_schema from app.classes.web.routes.api.servers.server.index import server_patch_schema from app.classes.web.routes.api.servers.index import new_server_schema +from app.classes.web.routes.api.servers.server.tasks.task.index import task_patch_schema SCHEMA_LIST: t.Final = [ "login", @@ -14,6 +15,7 @@ SCHEMA_LIST: t.Final = [ "new_server", "user_patch", "new_user", + "task_patch", ] @@ -59,22 +61,8 @@ class ApiJsonSchemaHandler(BaseApiHandler): "properties": { **self.controller.users.user_jsonschema_props, }, - "anyOf": [ - # Require at least one property - {"required": [name]} - for name in [ - "username", - "password", - "email", - "enabled", - "lang", - "superuser", - "permissions", - "roles", - "hints", - ] - ], "additionalProperties": False, + "minProperties": 1, }, }, ) @@ -93,6 +81,11 @@ class ApiJsonSchemaHandler(BaseApiHandler): }, }, ) + elif schema_name == "task_patch": + self.finish_json( + 200, + {"status": "ok", "data": task_patch_schema}, + ) else: self.finish_json( 404, diff --git a/app/classes/web/routes/api/roles/role/index.py b/app/classes/web/routes/api/roles/role/index.py index 43abbd55..c0601b5e 100644 --- a/app/classes/web/routes/api/roles/role/index.py +++ b/app/classes/web/routes/api/roles/role/index.py @@ -28,11 +28,8 @@ modify_role_schema = { }, }, }, - "anyOf": [ - {"required": ["name"]}, - {"required": ["servers"]}, - ], "additionalProperties": False, + "minProperties": 1, } diff --git a/app/classes/web/routes/api/servers/server/index.py b/app/classes/web/routes/api/servers/server/index.py index ea8c71de..11f8620b 100644 --- a/app/classes/web/routes/api/servers/server/index.py +++ b/app/classes/web/routes/api/servers/server/index.py @@ -28,28 +28,8 @@ server_patch_schema = { "logs_delete_after": {"type": "integer"}, "type": {"type": "string", "minLength": 1}, }, - "anyOf": [ - # Require at least one property - {"required": [name]} - for name in [ - "server_name", - "path", - "backup_path", - "executable", - "log_path", - "execution_command", - "auto_start", - "auto_start_delay", - "crash_detection", - "stop_command", - "executable_update_url", - "server_ip", - "server_port", - "logs_delete_after", - "type", - ] - ], "additionalProperties": False, + "minProperties": 1, } diff --git a/app/classes/web/routes/api/servers/server/tasks/index.py b/app/classes/web/routes/api/servers/server/tasks/index.py new file mode 100644 index 00000000..64ca3bef --- /dev/null +++ b/app/classes/web/routes/api/servers/server/tasks/index.py @@ -0,0 +1,16 @@ +# TODO: create and read + +import logging + +from app.classes.web.base_api_handler import BaseApiHandler + + +logger = logging.getLogger(__name__) + + +class ApiServersServerTasksIndexHandler(BaseApiHandler): + def get(self, server_id: str, task_id: str): + pass + + def post(self, server_id: str, task_id: str): + pass diff --git a/app/classes/web/routes/api/servers/server/tasks/task/children.py b/app/classes/web/routes/api/servers/server/tasks/task/children.py new file mode 100644 index 00000000..d92a42f7 --- /dev/null +++ b/app/classes/web/routes/api/servers/server/tasks/task/children.py @@ -0,0 +1,13 @@ +# TODO: read + +import logging + +from app.classes.web.base_api_handler import BaseApiHandler + + +logger = logging.getLogger(__name__) + + +class ApiServersServerTasksTaskChildrenHandler(BaseApiHandler): + def get(self, server_id: str, task_id: str): + pass diff --git a/app/classes/web/routes/api/servers/server/tasks/task/index.py b/app/classes/web/routes/api/servers/server/tasks/task/index.py new file mode 100644 index 00000000..3c567fdd --- /dev/null +++ b/app/classes/web/routes/api/servers/server/tasks/task/index.py @@ -0,0 +1,110 @@ +# TODO: read and delete + +import json +import logging + +from jsonschema import ValidationError, validate +from app.classes.models.management import HelpersManagement +from app.classes.models.server_permissions import EnumPermissionsServer + +from app.classes.web.base_api_handler import BaseApiHandler + + +logger = logging.getLogger(__name__) + +task_patch_schema = { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": True, + }, + "action": { + "type": "string", + }, + "interval": {"type": "integer"}, + "interval_type": { + "type": "string", + "enum": [ + # Basic tasks + "hours", + "minutes", + "days", + # Chain reaction tasks: + "reaction", + # CRON tasks: + "", + ], + }, + "start_time": {"type": "string", "pattern": r"\d{1,2}:\d{1,2}"}, + "command": {"type": ["string", "null"]}, + "one_time": {"type": "boolean", "default": False}, + "cron_string": {"type": "string", "default": ""}, + "parent": {"type": ["integer", "null"]}, + "delay": {"type": "integer", "default": 0}, + }, + "additionalProperties": False, + "minProperties": 1, +} + + +class ApiServersServerTasksTaskIndexHandler(BaseApiHandler): + def get(self, server_id: str, task_id: str): + pass + + def delete(self, server_id: str, task_id: str): + pass + + def patch(self, server_id: str, task_id: str): + auth_data = self.authenticate_user() + if not auth_data: + return + + try: + data = json.loads(self.request.body) + except json.decoder.JSONDecodeError as e: + return self.finish_json( + 400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)} + ) + + try: + validate(data, task_patch_schema) + except ValidationError as e: + return self.finish_json( + 400, + { + "status": "error", + "error": "INVALID_JSON_SCHEMA", + "error_data": str(e), + }, + ) + + if server_id not in [str(x["server_id"]) for x in auth_data[0]]: + # if the user doesn't have access to the server, return an error + return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) + + if ( + EnumPermissionsServer.SCHEDULE + not in self.controller.server_perms.get_user_id_permissions_list( + auth_data[4]["user_id"], server_id + ) + ): + # if the user doesn't have Schedule permission, return an error + return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) + + # Checks to make sure some doofus didn't actually make the newly + # created task a child of itself. + if str(data.get("parent")) == str(task_id) and data.get("parent") is not None: + data["parent"] = None + + HelpersManagement.update_scheduled_task(task_id, data) + + self.controller.management.add_to_audit_log( + auth_data[4]["user_id"], + f"Edited server {server_id}: updated schedule", + server_id, + self.get_remote_ip(), + ) + self.tasks_manager.reload_schedule_from_db() + + self.finish_json(200, {"status": "ok"}) diff --git a/app/classes/web/routes/api/users/user/index.py b/app/classes/web/routes/api/users/user/index.py index 7274611a..47d8dd68 100644 --- a/app/classes/web/routes/api/users/user/index.py +++ b/app/classes/web/routes/api/users/user/index.py @@ -112,22 +112,8 @@ class ApiUsersUserIndexHandler(BaseApiHandler): "properties": { **self.controller.users.user_jsonschema_props, }, - "anyOf": [ - # Require at least one property - {"required": [name]} - for name in [ - "username", - "password", - "email", - "enabled", - "lang", - "superuser", - "permissions", - "roles", - "hints", - ] - ], "additionalProperties": False, + "minProperties": 1, } auth_data = self.authenticate_user() if not auth_data: diff --git a/app/frontend/templates/base.html b/app/frontend/templates/base.html index eba6c2e8..4133dfac 100755 --- a/app/frontend/templates/base.html +++ b/app/frontend/templates/base.html @@ -37,6 +37,11 @@ + + + + + diff --git a/app/frontend/templates/panel/server_schedules.html b/app/frontend/templates/panel/server_schedules.html index 49282814..2d965ad7 100644 --- a/app/frontend/templates/panel/server_schedules.html +++ b/app/frontend/templates/panel/server_schedules.html @@ -24,7 +24,7 @@ - {% include "parts/details_stats.html %} + {% include "parts/details_stats.html" %}
@@ -33,10 +33,10 @@
- {% include "parts/server_controls_list.html %} + {% include "parts/server_controls_list.html" %} - {% include "parts/m_server_controls_list.html %} + {% include "parts/m_server_controls_list.html" %}
@@ -94,15 +94,7 @@

{{schedule.start_time}}

- {% if schedule.enabled %} - - Yes - - {% else %} - - No - - {% end %} +
@@ -230,6 +215,15 @@ color: white !important; ; } + .toggle-handle { + background-color: white !important; + } + .toggle-on { + color: black !important; + } + .toggle { + height: 0px !important; + } @@ -256,6 +250,39 @@ {% block js %} -{% end %} \ No newline at end of file +{% end %}