restructure the API and update the admin script

This commit is contained in:
ngn
2025-01-04 00:00:10 +03:00
parent 03586da8df
commit 26e8909998
34 changed files with 1699 additions and 983 deletions

12
admin/Makefile Normal file
View File

@ -0,0 +1,12 @@
SRCS = $(wildcard *.py)
PREFIX = /usr
all:
format:
black $(SRCS)
install:
install -Dm755 admin.py $(PREFIX)/bin/admin_script
.PHONY: format install

View File

@ -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)

View File

@ -1,8 +0,0 @@
#!/bin/bash
echo -n "Enter API URL: "
read url
cat > /usr/bin/admin_script << EOF
#!/bin/sh
API=$url python3 $(pwd)/admin.py \$1
EOF
chmod +x /usr/bin/admin_script

View File

@ -0,0 +1,12 @@
{
"id": "test_news",
"title": {
"en": "Very important news",
"tr": "Çok önemli haber"
},
"author": "ngn",
"content": {
"en": "Just letting you know that I'm testing the API",
"tr": "Sadece API'ı test ettiğimi bilmenizi istedim"
}
}

View File

@ -0,0 +1,9 @@
{
"name": "Test Service",
"desc": {
"en": "Service used for testing the API",
"tr": "API'ı test etmek için kullanılan servis"
},
"check_url": "http://localhost:7001",
"clear": "http://localhost:7001"
}