website/admin/admin.py

409 lines
13 KiB
Python
Raw Normal View History

2023-11-12 17:43:23 +03:00
#!/bin/python3
"""
website/admin | Administration script for my personal website
written by ngn (https://ngn.tf) (2025)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
2023-11-12 17:43:23 +03:00
"""
from urllib.parse import quote_plus
from typing import Dict, List, Any
from datetime import datetime, UTC
from colorama import Fore, Style
from json import dumps, loads
2023-11-12 17:43:23 +03:00
from getpass import getpass
import requests as req
from os import getenv
2023-11-12 17:43:23 +03:00
from sys import argv
# logger used by the script
class Log:
def __init__(self) -> None:
self.reset = Fore.RESET + Style.RESET_ALL
def info(self, m: str) -> None:
print(Fore.BLUE + Style.BRIGHT + "[*]" + self.reset + " " + m)
def error(self, m: str) -> None:
print(Fore.RED + Style.BRIGHT + "[-]" + self.reset + " " + m)
2023-11-12 17:43:23 +03:00
def input(self, m: str) -> str:
return input(Fore.CYAN + Style.BRIGHT + "[?]" + self.reset + " " + m + ": ")
2023-11-12 17:43:23 +03:00
def password(self, m: str) -> str:
return getpass(Fore.CYAN + Style.BRIGHT + "[?]" + self.reset + " " + m + ": ")
2023-11-12 17:43:23 +03:00
# API interface for the admin endpoints
class AdminAPI:
def __init__(self, url: str, password: str) -> None:
self.languages: List[str] = [
"en",
"tr",
] # languages supported by multilang fields
self.password = password
self.api_url = url
2023-11-12 17:43:23 +03:00
def _title_to_id(self, title: str) -> str:
return title.lower().replace(" ", "_")
def _check_multilang_field(self, ml: Dict[str, str]) -> bool:
2025-01-04 19:59:44 +03:00
for lang in self.languages:
if lang in ml and ml[lang] != "":
return True
return False
def _api_url_join(self, path: str) -> str:
api_has_slash = self.api_url.endswith("/")
path_has_slash = path.startswith("/")
if api_has_slash or path_has_slash:
return self.api_url + path
elif api_has_slash and path_has_slash:
return self.api_url + path[1:]
else:
return self.api_url + "/" + path
def _to_json(self, res: req.Response) -> dict:
if res.status_code == 403:
raise Exception("Authentication failed")
json = res.json()
if json["error"] != "":
raise Exception("API error: %s" % json["error"])
return json
def PUT(self, url: str, data: dict) -> req.Response:
return self._to_json(
req.put(
self._api_url_join(url),
json=data,
headers={"Authorization": self.password},
)
)
def DELETE(self, url: str) -> req.Response:
return self._to_json(
req.delete(
self._api_url_join(url), headers={"Authorization": self.password}
)
)
def GET(self, url: str) -> req.Response:
return self._to_json(
req.get(self._api_url_join(url), headers={"Authorization": self.password})
)
def add_service(self, service: Dict[str, str]):
2025-01-04 19:59:44 +03:00
if "name" not in service or service["name"] == "":
raise Exception('Service structure is missing required "name" field')
2025-01-04 19:59:44 +03:00
if "desc" not in service:
raise Exception('Service structure is missing required "desc" field')
if (
2025-01-04 19:59:44 +03:00
("clear" not in service or service["clear"] == "")
and ("onion" not in service or service["onion"] == "")
and ("i2p" not in service or service["i2p"] == "")
):
raise Exception(
2025-01-04 19:59:44 +03:00
'Service structure is missing "clear", "onion" '
+ 'and "i2p" field, at least one needed'
)
if not self._check_multilang_field(service["desc"]):
raise Exception(
2025-01-04 19:59:44 +03:00
'Service structure field "desc" needs at least '
+ "one supported language entry"
)
self.PUT("/v1/admin/service/add", service)
2025-01-09 00:30:59 +03:00
def del_service(self, name: str) -> None:
if name == "":
raise Exception("Service name cannot be empty")
2025-01-09 00:30:59 +03:00
self.DELETE("/v1/admin/service/del?name=%s" % quote_plus(name))
def add_project(self, project: Dict[str, str]):
if "name" not in project or project["name"] == "":
raise Exception('Project structure is missing required "name" field')
if "desc" not in project:
raise Exception('Project structure is missing required "desc" field')
if not self._check_multilang_field(project["desc"]):
raise Exception(
'Project structure field "desc" needs at least '
+ "one supported language entry"
)
self.PUT("/v1/admin/project/add", project)
def del_project(self, name: str) -> None:
if name == "":
raise Exception("Project name cannot be empty")
self.DELETE("/v1/admin/project/del?name=%s" % quote_plus(name))
def check_services(self) -> None:
self.GET("/v1/admin/service/check")
def add_news(self, news: Dict[str, str]):
2025-01-04 19:59:44 +03:00
if "id" not in news or news["id"] == "":
raise Exception('News structure is missing required "id" field')
2025-01-04 19:59:44 +03:00
if "author" not in news or news["author"] == "":
raise Exception('News structure is missing required "author" field')
2025-01-04 19:59:44 +03:00
if "title" not in news:
raise Exception('News structure is missing required "title" field')
2025-01-04 19:59:44 +03:00
if "content" not in news:
raise Exception('News structure is missing required "content" field')
if not self._check_multilang_field(news["title"]):
raise Exception(
2025-01-04 19:59:44 +03:00
'News structure field "title" needs at least '
+ "one supported language entry"
)
if not self._check_multilang_field(news["content"]):
raise Exception(
2025-01-04 19:59:44 +03:00
'News structure field "content" needs at least '
+ "one supported language entry"
)
self.PUT("/v1/admin/news/add", news)
2025-01-09 00:30:59 +03:00
def del_news(self, id: str) -> None:
if id == "":
raise Exception("News ID cannot be empty")
2025-01-09 00:30:59 +03:00
self.DELETE("/v1/admin/news/del?id=%s" % quote_plus(id))
def logs(self) -> List[Dict[str, Any]]:
return self.GET("/v1/admin/logs")
2025-01-09 00:30:59 +03:00
class AdminScript:
def __init__(self):
self.log: Log = Log()
self.api: AdminAPI = None
self.commands = {
"add_service": self.add_service,
"del_service": self.del_service,
"add_project": self.add_project,
"del_project": self.del_project,
"add_news": self.add_news,
"del_news": self.del_news,
"check_services": self.check_services,
"logs": self.get_logs,
}
self.api_url_env = "API_URL"
self.password_env = "API_PASSWORD"
2025-01-09 00:30:59 +03:00
def __format_time(self, ts: int) -> str:
return datetime.fromtimestamp(ts, UTC).strftime("%H:%M:%S %d/%m/%Y")
def __load_json_file(self, file: str) -> Dict[str, Any]:
with open(file, "r") as f:
data = loads(f.read())
return data
def __dump_json_file(self, data: Dict[str, Any], file: str) -> None:
with open(file, "w") as f:
data = dumps(data, indent=2)
f.write(data)
def run(self) -> bool:
if len(argv) < 2 or len(argv) > 3:
self.log.error("Usage: %s [command] <file>" % argv[0])
self.log.info("Here is a list of available commands:")
for command in self.commands.keys():
print("\t%s" % command)
return False
url = getenv(self.api_url_env)
valid_cmd = False
if url is None:
self.log.error(
"Please specify the API URL using %s environment variable"
% self.api_url_env
)
return False
for cmd in self.commands:
if argv[1] == cmd:
valid_cmd = True
break
2025-01-09 00:30:59 +03:00
if not valid_cmd:
self.log.error(
"Invalid command, run the script with no commands to list the available commands"
)
return False
2025-01-09 00:30:59 +03:00
try:
password = getenv(self.password_env)
if password is None:
password = self.log.password("Please enter the admin password")
2025-01-09 00:30:59 +03:00
self.api = AdminAPI(url, password)
2025-01-09 00:30:59 +03:00
if len(argv) == 2:
self.handle_command(argv[1])
2025-01-09 00:30:59 +03:00
elif len(argv) == 3:
self.handle_command(argv[1], argv[2])
2025-01-09 00:30:59 +03:00
except KeyboardInterrupt:
self.log.error("Command cancelled")
return False
2025-01-09 00:30:59 +03:00
except Exception as e:
self.log.error("Command failed: %s" % e)
return False
# service commands
def add_service(self, data: Dict[str, Any] = None) -> None:
if data is None:
data: Dict[str, str] = {}
data["desc"] = {}
2025-01-09 00:30:59 +03:00
data["name"] = self.log.input("Serivce name")
2025-01-09 00:30:59 +03:00
for lang in self.api.languages:
data["desc"][lang] = self.log.input("Serivce desc (%s)" % lang)
2025-01-09 00:30:59 +03:00
data["check_url"] = self.log.input("Serivce status check URL")
data["clear"] = self.log.input("Serivce clearnet URL")
data["onion"] = self.log.input("Serivce onion URL")
data["i2p"] = self.log.input("Serivce I2P URL")
2025-01-09 00:30:59 +03:00
self.api.add_service(data)
self.log.info("Service has been added")
2025-01-09 00:30:59 +03:00
def del_service(self, data: Dict[str, Any] = None) -> None:
if data is None:
data: Dict[str, str] = {}
data["name"] = self.log.input("Service name")
2025-01-09 00:30:59 +03:00
self.api.del_service(data["name"])
self.log.info("Service has been deleted")
2025-01-09 00:30:59 +03:00
# project commands
def add_project(self, data: Dict[str, Any] = None) -> None:
if data is None:
data: Dict[str, str] = {}
data["desc"] = {}
2025-01-09 00:30:59 +03:00
data["name"] = self.log.input("Project name")
2025-01-09 00:30:59 +03:00
for lang in self.api.languages:
data["desc"][lang] = self.log.input("Project desc (%s)" % lang)
2025-01-09 00:30:59 +03:00
data["url"] = self.log.input("Project URL")
data["license"] = self.log.input("Project license")
2025-01-09 00:30:59 +03:00
self.api.add_project(data)
self.log.info("Project has been added")
2025-01-09 00:30:59 +03:00
def del_project(self, data: Dict[str, Any] = None) -> None:
if data is None:
data: Dict[str, str] = {}
data["name"] = self.log.input("Project name")
2025-01-09 00:30:59 +03:00
self.api.del_project(data["name"])
self.log.info("Project has been deleted")
2025-01-09 00:30:59 +03:00
# news command
def add_news(self, data: Dict[str, Any] = None) -> None:
if data is None:
data: Dict[str, str] = {}
data["title"] = {}
data["content"] = {}
2025-01-09 00:30:59 +03:00
data["id"] = self.log.input("News ID")
2025-01-09 00:30:59 +03:00
for lang in self.api.languages:
data["title"][lang] = self.log.input("News title (%s)" % lang)
2025-01-09 00:30:59 +03:00
data["author"] = self.log.input("News author")
2025-01-09 00:30:59 +03:00
for lang in self.api.languages:
data["content"][lang] = self.log.input("News content (%s)" % lang)
2025-01-09 00:30:59 +03:00
self.api.add_news(data)
self.log.info("News has been added")
2025-01-09 00:30:59 +03:00
def del_news(self, data: Dict[str, Any] = None) -> None:
if data is None:
data: Dict[str, str] = {}
data["id"] = self.log.input("News ID")
2025-01-09 00:30:59 +03:00
self.api.del_project(data["id"])
self.log.info("News has been deleted")
2025-01-09 00:30:59 +03:00
def check_services(self, data: Dict[str, Any] = None) -> None:
self.api.check_services()
self.log.info("Requested status check for all the services")
2025-01-07 00:13:11 +03:00
2025-01-09 00:30:59 +03:00
def get_logs(self, data: Dict[str, Any] = None) -> None:
logs = self.api.logs()
2025-01-09 00:30:59 +03:00
if logs["result"] is None or len(logs["result"]) == 0:
return self.log.info("No available logs")
2023-11-12 17:43:23 +03:00
2025-01-09 00:30:59 +03:00
for log in logs["result"]:
self.log.info(
"Time: %s | Action: %s"
% (self.__format_time(log["time"]), log["action"])
)
def handle_command(self, cmd: str, file: str = None) -> bool:
for command in self.commands.keys():
if command != cmd:
continue
data = None
2023-11-12 17:43:23 +03:00
2025-01-09 00:30:59 +03:00
try:
if file != "" and file is not None:
data = self.__load_json_file(file)
self.commands[cmd](data)
return True
2023-11-12 17:43:23 +03:00
2025-01-09 00:30:59 +03:00
except Exception as e:
self.log.error("Command failed: %s" % e)
return False
2023-11-12 17:43:23 +03:00
2025-01-09 00:30:59 +03:00
self.log.error("Invalid command: %s", cmd)
return False
if __name__ == "__main__":
script = AdminScript()
exit(0 if script.run() else 1)