restructure the API and update the admin script
This commit is contained in:
429
admin/admin.py
429
admin/admin.py
@ -1,176 +1,331 @@
|
||||
#!/bin/python3
|
||||
|
||||
"""
|
||||
Administration script for my website (ngn.tf)
|
||||
#############################################
|
||||
I really enjoy doing stuff from the terminal,
|
||||
so I wrote this simple python script that interacts
|
||||
with the API and lets me add/remove new posts/services
|
||||
from the terminal
|
||||
|
||||
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/>.
|
||||
|
||||
"""
|
||||
|
||||
from os import remove, getenv
|
||||
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
|
||||
from getpass import getpass
|
||||
import requests as req
|
||||
from os import getenv
|
||||
from sys import argv
|
||||
|
||||
URL = ""
|
||||
API_URL_ENV = "API_URL"
|
||||
|
||||
def join(pth: str) -> str:
|
||||
if URL == None:
|
||||
return ""
|
||||
|
||||
if URL.endswith("/"):
|
||||
return URL+pth
|
||||
return URL+"/"+pth
|
||||
# logger used by the script
|
||||
class Log:
|
||||
def __init__(self) -> None:
|
||||
self.reset = Fore.RESET + Style.RESET_ALL
|
||||
|
||||
def get_token() -> str:
|
||||
try:
|
||||
f = open("/tmp/wa", "r")
|
||||
token = f.read()
|
||||
f.close()
|
||||
return token
|
||||
except:
|
||||
print("[-] You are not authenticated")
|
||||
exit(1)
|
||||
def info(self, m: str) -> None:
|
||||
print(Fore.BLUE + Style.BRIGHT + "[*]" + self.reset + " " + m)
|
||||
|
||||
def login() -> None:
|
||||
pwd = getpass("[>] Enter your password: ")
|
||||
res = req.get(join("admin/login")+f"?pass={pwd}").json()
|
||||
if res["error"] != "":
|
||||
print(f"[-] Error logging in: {res['error']}")
|
||||
return
|
||||
def error(self, m: str) -> None:
|
||||
print(Fore.RED + Style.BRIGHT + "[-]" + self.reset + " " + m)
|
||||
|
||||
token = res["token"]
|
||||
f = open("/tmp/wa", "w")
|
||||
f.write(token)
|
||||
f.close()
|
||||
def input(self, m: str) -> str:
|
||||
return input(Fore.CYAN + Style.BRIGHT + "[?]" + self.reset + " " + m + ": ")
|
||||
|
||||
def logout() -> None:
|
||||
token = get_token()
|
||||
res = req.get(join("admin/logout"), headers={
|
||||
"Authorization": token
|
||||
}).json()
|
||||
if res["error"] != "":
|
||||
print(f"[-] Error logging out: {res['error']}")
|
||||
return
|
||||
def password(self, m: str) -> str:
|
||||
return getpass(Fore.CYAN + Style.BRIGHT + "[?]" + self.reset + " " + m + ": ")
|
||||
|
||||
remove("/tmp/wa")
|
||||
print("[+] Logged out")
|
||||
|
||||
def add_post() -> None:
|
||||
token = get_token()
|
||||
title = input("[>] Post title: ")
|
||||
author = input("[>] Post author: ")
|
||||
content_file = input("[>] Post content file: ")
|
||||
public = input("[>] Should post be public? (y/n): ")
|
||||
# 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
|
||||
|
||||
try:
|
||||
f = open(content_file, "r")
|
||||
content = f.read()
|
||||
f.close()
|
||||
except:
|
||||
print("[-] Content file not found")
|
||||
return
|
||||
def _title_to_id(self, title: str) -> str:
|
||||
return title.lower().replace(" ", "_")
|
||||
|
||||
res = req.put(join("admin/blog/add"), json={
|
||||
"title": title,
|
||||
"author": author,
|
||||
"content": content,
|
||||
"public": 1 if public == "y" else 0
|
||||
}, headers={
|
||||
"Authorization": token
|
||||
}).json()
|
||||
def _check_multilang_field(self, ml: Dict[str, str]) -> bool:
|
||||
for l in self.languages:
|
||||
if l in ml and ml[l] != "":
|
||||
return True
|
||||
return False
|
||||
|
||||
if res["error"] != "":
|
||||
print(f"[-] Error adding post: {res['error']}")
|
||||
return
|
||||
def _api_url_join(self, path: str) -> str:
|
||||
api_has_slash = self.api_url.endswith("/")
|
||||
path_has_slash = path.startswith("/")
|
||||
|
||||
print("[+] Post has been added")
|
||||
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 remove_post() -> None:
|
||||
token = get_token()
|
||||
id = input("[>] Post ID: ")
|
||||
res = req.delete(join("admin/blog/remove")+f"?id={id}", headers={
|
||||
"Authorization": token
|
||||
}).json()
|
||||
def _to_json(self, res: req.Response) -> dict:
|
||||
if res.status_code == 403:
|
||||
raise Exception("Authentication failed")
|
||||
|
||||
if res["error"] != "":
|
||||
print(f"[-] Error removing post: {res['error']}")
|
||||
return
|
||||
json = res.json()
|
||||
|
||||
print("[-] Post has been removed")
|
||||
if json["error"] != "":
|
||||
raise Exception("API error: %s" % json["error"])
|
||||
|
||||
def add_service() -> None:
|
||||
token = get_token()
|
||||
name = input("[>] Serivce name: ")
|
||||
desc = input("[>] Serivce desc: ")
|
||||
link = input("[>] Serivce URL: ")
|
||||
return json
|
||||
|
||||
res = req.put(join("admin/service/add"), json={
|
||||
"name": name,
|
||||
"desc": desc,
|
||||
"url": link
|
||||
}, headers={
|
||||
"Authorization": token
|
||||
}).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},
|
||||
)
|
||||
)
|
||||
|
||||
if res["error"] != "":
|
||||
print(f"[-] Error adding service: {res['error']}")
|
||||
return
|
||||
def DELETE(self, url: str) -> req.Response:
|
||||
return self._to_json(
|
||||
req.delete(
|
||||
self._api_url_join(url), headers={"Authorization": self.password}
|
||||
)
|
||||
)
|
||||
|
||||
print("[+] Service has been added")
|
||||
def GET(self, url: str) -> req.Response:
|
||||
return self._to_json(
|
||||
req.get(self._api_url_join(url), headers={"Authorization": self.password})
|
||||
)
|
||||
|
||||
def remove_service() -> None:
|
||||
token = get_token()
|
||||
name = input("[>] Service name: ")
|
||||
res = req.delete(join("admin/service/remove")+f"?name={name}", headers={
|
||||
"Authorization": token
|
||||
}).json()
|
||||
def add_service(self, service: Dict[str, str]):
|
||||
if not "name" in service or service["name"] == "":
|
||||
raise Exception('Service structure is missing required "name" field')
|
||||
|
||||
if res["error"] != "":
|
||||
print(f"[-] Error removing service: {res['error']}")
|
||||
return
|
||||
if not "desc" in service:
|
||||
raise Exception('Service structure is missing required "desc" field')
|
||||
|
||||
print("[+] Serivce has been removed")
|
||||
if (
|
||||
(not "clear" in service or service["clear"] == "")
|
||||
and (not "onion" in service or service["onion"] == "")
|
||||
and (not "i2p" in service or service["i2p"] == "")
|
||||
):
|
||||
raise Exception(
|
||||
'Service structure is missing "clear", "onion" and "i2p" field, at least one needed'
|
||||
)
|
||||
|
||||
cmds = {
|
||||
"login": login,
|
||||
"logout": logout,
|
||||
"add_post": add_post,
|
||||
"remove_post": remove_post,
|
||||
"add_service": add_service,
|
||||
"remove_service": remove_service,
|
||||
}
|
||||
if not self._check_multilang_field(service["desc"]):
|
||||
raise Exception(
|
||||
'Service structure field "desc" needs at least one supported language entry'
|
||||
)
|
||||
|
||||
def main():
|
||||
global URL
|
||||
URL = getenv("API")
|
||||
if URL == None or URL == "":
|
||||
print("[-] API enviroment variable not set")
|
||||
exit(1)
|
||||
self.PUT("/v1/admin/service/add", service)
|
||||
|
||||
if len(argv) != 2:
|
||||
print(f"[-] Usage: admin_script <command>")
|
||||
print(f"[+] Run \"admin_script help\" to get all commands")
|
||||
exit(1)
|
||||
def del_service(self, service: str) -> None:
|
||||
if service == "":
|
||||
raise Exception("Service name cannot be empty")
|
||||
|
||||
if argv[1] == "help":
|
||||
print("Avaliable commands:")
|
||||
for k in cmds.keys():
|
||||
print(f" {k}")
|
||||
exit()
|
||||
self.DELETE("/v1/admin/service/del?name=%s" % quote_plus(service))
|
||||
|
||||
for k in cmds.keys():
|
||||
if k != argv[1]:
|
||||
continue
|
||||
try:
|
||||
cmds[k]()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
exit()
|
||||
def check_services(self) -> None:
|
||||
self.GET("/v1/admin/service/check")
|
||||
|
||||
def add_news(self, news: Dict[str, str]):
|
||||
if not "id" in news or news["id"] == "":
|
||||
raise Exception('News structure is missing required "id" field')
|
||||
|
||||
if not "author" in news or news["author"] == "":
|
||||
raise Exception('News structure is missing required "author" field')
|
||||
|
||||
if not "title" in news:
|
||||
raise Exception('News structure is missing required "title" field')
|
||||
|
||||
if not "content" in news:
|
||||
raise Exception('News structure is missing required "content" field')
|
||||
|
||||
if not self._check_multilang_field(news["title"]):
|
||||
raise Exception(
|
||||
'News structure field "title" needs at least one supported language entry'
|
||||
)
|
||||
|
||||
if not self._check_multilang_field(news["content"]):
|
||||
raise Exception(
|
||||
'News structure field "content" needs at least one supported language entry'
|
||||
)
|
||||
|
||||
self.PUT("/v1/admin/news/add", news)
|
||||
|
||||
def del_news(self, news: str) -> None:
|
||||
if news == "":
|
||||
raise Exception("News ID cannot be empty")
|
||||
|
||||
self.DELETE("/v1/admin/news/del?id=%s" % quote_plus(news))
|
||||
|
||||
def logs(self) -> List[Dict[str, Any]]:
|
||||
return self.GET("/v1/admin/logs")
|
||||
|
||||
|
||||
# local helper functions used by the script
|
||||
def __format_time(ts: int) -> str:
|
||||
return datetime.fromtimestamp(ts, UTC).strftime("%H:%M:%S %d/%m/%Y")
|
||||
|
||||
|
||||
def __load_json_file(file: str) -> Dict[str, Any]:
|
||||
with open(file, "r") as f:
|
||||
data = loads(f.read())
|
||||
return data
|
||||
|
||||
|
||||
def __dump_json_file(data: Dict[str, Any], file: str) -> None:
|
||||
with open(file, "w") as f:
|
||||
data = dumps(data, indent=2)
|
||||
f.write(data)
|
||||
|
||||
|
||||
# command handlers
|
||||
def __handle_command(log: Log, api: AdminAPI, cmd: str) -> None:
|
||||
match cmd:
|
||||
case "add_service":
|
||||
data: Dict[str, str] = {}
|
||||
data["desc"] = {}
|
||||
|
||||
data["name"] = log.input("Serivce name")
|
||||
for l in api.languages:
|
||||
data["desc"][l] = log.input("Serivce desc (%s)" % l)
|
||||
data["check_url"] = log.input("Serivce status check URL")
|
||||
data["clear"] = log.input("Serivce clearnet URL")
|
||||
data["onion"] = log.input("Serivce onion URL")
|
||||
data["i2p"] = log.input("Serivce I2P URL")
|
||||
|
||||
api.add_service(data)
|
||||
log.info("Service has been added")
|
||||
|
||||
case "del_service":
|
||||
api.del_service(self.log.input("Serivce name"))
|
||||
log.info("Service has been deleted")
|
||||
|
||||
case "check_services":
|
||||
api.check_services()
|
||||
log.info("Requested status check for all the services")
|
||||
|
||||
case "add_news":
|
||||
news: Dict[str, str] = {}
|
||||
news["title"] = {}
|
||||
news["content"] = {}
|
||||
|
||||
data["id"] = log.input("News ID")
|
||||
for l in api.languages:
|
||||
data["title"][l] = log.input("News title (%s)" % l)
|
||||
data["author"] = log.input("News author")
|
||||
for l in api.languages:
|
||||
data["content"][l] = log.input("News content (%s)" % l)
|
||||
|
||||
api.add_news(data)
|
||||
log.info("News has been added")
|
||||
|
||||
case "del_news":
|
||||
api.del_news(log.input("News ID"))
|
||||
log.info("News has been deleted")
|
||||
|
||||
case "logs":
|
||||
logs = api.logs()
|
||||
|
||||
if None == logs["result"] or len(logs["result"]) == 0:
|
||||
return log.info("No available logs")
|
||||
|
||||
for l in logs["result"]:
|
||||
log.info(
|
||||
"Time: %s | Action: %s" % (__format_time(l["time"]), l["action"])
|
||||
)
|
||||
|
||||
|
||||
def __handle_command_with_file(log: Log, api: AdminAPI, cmd: str, file: str) -> None:
|
||||
match cmd:
|
||||
case "add_service":
|
||||
data = __load_json_file(file)
|
||||
api.add_service(data)
|
||||
log.info("Service has been added")
|
||||
|
||||
case "del_service":
|
||||
data = __load_json_file(file)
|
||||
api.del_service(data["name"])
|
||||
log.info("Service has been deleted")
|
||||
|
||||
case "check_services":
|
||||
api.check_services()
|
||||
log.info("Requested status check for all the services")
|
||||
|
||||
case "add_news":
|
||||
data = __load_json_file(file)
|
||||
api.add_news(data)
|
||||
log.info("News has been added")
|
||||
|
||||
case "del_news":
|
||||
data = __load_json_file(file)
|
||||
api.del_news(data["id"])
|
||||
log.info("News has been deleted")
|
||||
|
||||
case "logs":
|
||||
logs = api.logs()
|
||||
|
||||
if None == logs["result"] or len(logs["result"]) == 0:
|
||||
return log.info("No available logs")
|
||||
|
||||
__dump_json_file(logs["result"], file)
|
||||
log.info("Logs has been saved")
|
||||
|
||||
print("[-] Command not found")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
log = Log()
|
||||
|
||||
if len(argv) < 2 or len(argv) > 3:
|
||||
log.error("Usage: %s [command] <file>" % argv[0])
|
||||
log.info("Here is a list of available commands:")
|
||||
print("\tadd_service")
|
||||
print("\tdel_service")
|
||||
print("\tcheck_services")
|
||||
print("\tadd_news")
|
||||
print("\tdel_news")
|
||||
print("\tlogs")
|
||||
exit(1)
|
||||
|
||||
url = getenv(API_URL_ENV)
|
||||
|
||||
if url == None:
|
||||
log.error(
|
||||
"Please specify the API URL using %s environment variable" % API_URL_ENV
|
||||
)
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
password = log.password("Please enter the admin password")
|
||||
api = AdminAPI(url, password)
|
||||
|
||||
if len(argv) == 2:
|
||||
__handle_command(log, api, argv[1])
|
||||
elif len(argv) == 3:
|
||||
__handle_command_with_file(log, api, argv[1], argv[2])
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print()
|
||||
log.error("Command cancelled")
|
||||
exit(1)
|
||||
|
||||
except Exception as e:
|
||||
log.error("Command failed: %s" % e)
|
||||
exit(1)
|
||||
|
Reference in New Issue
Block a user