restructure the API and update the admin script
This commit is contained in:
parent
03586da8df
commit
26e8909998
12
admin/Makefile
Normal file
12
admin/Makefile
Normal 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
|
429
admin/admin.py
429
admin/admin.py
@ -1,176 +1,331 @@
|
|||||||
#!/bin/python3
|
#!/bin/python3
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Administration script for my website (ngn.tf)
|
|
||||||
#############################################
|
website/admin | Administration script for my personal website
|
||||||
I really enjoy doing stuff from the terminal,
|
written by ngn (https://ngn.tf) (2025)
|
||||||
so I wrote this simple python script that interacts
|
|
||||||
with the API and lets me add/remove new posts/services
|
This program is free software: you can redistribute it and/or modify
|
||||||
from the terminal
|
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
|
from getpass import getpass
|
||||||
import requests as req
|
import requests as req
|
||||||
|
from os import getenv
|
||||||
from sys import argv
|
from sys import argv
|
||||||
|
|
||||||
URL = ""
|
API_URL_ENV = "API_URL"
|
||||||
|
|
||||||
def join(pth: str) -> str:
|
|
||||||
if URL == None:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
if URL.endswith("/"):
|
# logger used by the script
|
||||||
return URL+pth
|
class Log:
|
||||||
return URL+"/"+pth
|
def __init__(self) -> None:
|
||||||
|
self.reset = Fore.RESET + Style.RESET_ALL
|
||||||
|
|
||||||
def get_token() -> str:
|
def info(self, m: str) -> None:
|
||||||
try:
|
print(Fore.BLUE + Style.BRIGHT + "[*]" + self.reset + " " + m)
|
||||||
f = open("/tmp/wa", "r")
|
|
||||||
token = f.read()
|
|
||||||
f.close()
|
|
||||||
return token
|
|
||||||
except:
|
|
||||||
print("[-] You are not authenticated")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
def login() -> None:
|
def error(self, m: str) -> None:
|
||||||
pwd = getpass("[>] Enter your password: ")
|
print(Fore.RED + Style.BRIGHT + "[-]" + self.reset + " " + m)
|
||||||
res = req.get(join("admin/login")+f"?pass={pwd}").json()
|
|
||||||
if res["error"] != "":
|
|
||||||
print(f"[-] Error logging in: {res['error']}")
|
|
||||||
return
|
|
||||||
|
|
||||||
token = res["token"]
|
def input(self, m: str) -> str:
|
||||||
f = open("/tmp/wa", "w")
|
return input(Fore.CYAN + Style.BRIGHT + "[?]" + self.reset + " " + m + ": ")
|
||||||
f.write(token)
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
def logout() -> None:
|
def password(self, m: str) -> str:
|
||||||
token = get_token()
|
return getpass(Fore.CYAN + Style.BRIGHT + "[?]" + self.reset + " " + m + ": ")
|
||||||
res = req.get(join("admin/logout"), headers={
|
|
||||||
"Authorization": token
|
|
||||||
}).json()
|
|
||||||
if res["error"] != "":
|
|
||||||
print(f"[-] Error logging out: {res['error']}")
|
|
||||||
return
|
|
||||||
|
|
||||||
remove("/tmp/wa")
|
|
||||||
print("[+] Logged out")
|
|
||||||
|
|
||||||
def add_post() -> None:
|
# API interface for the admin endpoints
|
||||||
token = get_token()
|
class AdminAPI:
|
||||||
title = input("[>] Post title: ")
|
def __init__(self, url: str, password: str) -> None:
|
||||||
author = input("[>] Post author: ")
|
self.languages: List[str] = [
|
||||||
content_file = input("[>] Post content file: ")
|
"en",
|
||||||
public = input("[>] Should post be public? (y/n): ")
|
"tr",
|
||||||
|
] # languages supported by multilang fields
|
||||||
|
self.password = password
|
||||||
|
self.api_url = url
|
||||||
|
|
||||||
try:
|
def _title_to_id(self, title: str) -> str:
|
||||||
f = open(content_file, "r")
|
return title.lower().replace(" ", "_")
|
||||||
content = f.read()
|
|
||||||
f.close()
|
|
||||||
except:
|
|
||||||
print("[-] Content file not found")
|
|
||||||
return
|
|
||||||
|
|
||||||
res = req.put(join("admin/blog/add"), json={
|
def _check_multilang_field(self, ml: Dict[str, str]) -> bool:
|
||||||
"title": title,
|
for l in self.languages:
|
||||||
"author": author,
|
if l in ml and ml[l] != "":
|
||||||
"content": content,
|
return True
|
||||||
"public": 1 if public == "y" else 0
|
return False
|
||||||
}, headers={
|
|
||||||
"Authorization": token
|
|
||||||
}).json()
|
|
||||||
|
|
||||||
if res["error"] != "":
|
def _api_url_join(self, path: str) -> str:
|
||||||
print(f"[-] Error adding post: {res['error']}")
|
api_has_slash = self.api_url.endswith("/")
|
||||||
return
|
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:
|
def _to_json(self, res: req.Response) -> dict:
|
||||||
token = get_token()
|
if res.status_code == 403:
|
||||||
id = input("[>] Post ID: ")
|
raise Exception("Authentication failed")
|
||||||
res = req.delete(join("admin/blog/remove")+f"?id={id}", headers={
|
|
||||||
"Authorization": token
|
|
||||||
}).json()
|
|
||||||
|
|
||||||
if res["error"] != "":
|
json = res.json()
|
||||||
print(f"[-] Error removing post: {res['error']}")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("[-] Post has been removed")
|
if json["error"] != "":
|
||||||
|
raise Exception("API error: %s" % json["error"])
|
||||||
|
|
||||||
def add_service() -> None:
|
return json
|
||||||
token = get_token()
|
|
||||||
name = input("[>] Serivce name: ")
|
|
||||||
desc = input("[>] Serivce desc: ")
|
|
||||||
link = input("[>] Serivce URL: ")
|
|
||||||
|
|
||||||
res = req.put(join("admin/service/add"), json={
|
def PUT(self, url: str, data: dict) -> req.Response:
|
||||||
"name": name,
|
return self._to_json(
|
||||||
"desc": desc,
|
req.put(
|
||||||
"url": link
|
self._api_url_join(url),
|
||||||
}, headers={
|
json=data,
|
||||||
"Authorization": token
|
headers={"Authorization": self.password},
|
||||||
}).json()
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if res["error"] != "":
|
def DELETE(self, url: str) -> req.Response:
|
||||||
print(f"[-] Error adding service: {res['error']}")
|
return self._to_json(
|
||||||
return
|
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:
|
def add_service(self, service: Dict[str, str]):
|
||||||
token = get_token()
|
if not "name" in service or service["name"] == "":
|
||||||
name = input("[>] Service name: ")
|
raise Exception('Service structure is missing required "name" field')
|
||||||
res = req.delete(join("admin/service/remove")+f"?name={name}", headers={
|
|
||||||
"Authorization": token
|
|
||||||
}).json()
|
|
||||||
|
|
||||||
if res["error"] != "":
|
if not "desc" in service:
|
||||||
print(f"[-] Error removing service: {res['error']}")
|
raise Exception('Service structure is missing required "desc" field')
|
||||||
return
|
|
||||||
|
|
||||||
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 = {
|
if not self._check_multilang_field(service["desc"]):
|
||||||
"login": login,
|
raise Exception(
|
||||||
"logout": logout,
|
'Service structure field "desc" needs at least one supported language entry'
|
||||||
"add_post": add_post,
|
)
|
||||||
"remove_post": remove_post,
|
|
||||||
"add_service": add_service,
|
|
||||||
"remove_service": remove_service,
|
|
||||||
}
|
|
||||||
|
|
||||||
def main():
|
self.PUT("/v1/admin/service/add", service)
|
||||||
global URL
|
|
||||||
URL = getenv("API")
|
|
||||||
if URL == None or URL == "":
|
|
||||||
print("[-] API enviroment variable not set")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
if len(argv) != 2:
|
def del_service(self, service: str) -> None:
|
||||||
print(f"[-] Usage: admin_script <command>")
|
if service == "":
|
||||||
print(f"[+] Run \"admin_script help\" to get all commands")
|
raise Exception("Service name cannot be empty")
|
||||||
exit(1)
|
|
||||||
|
|
||||||
if argv[1] == "help":
|
self.DELETE("/v1/admin/service/del?name=%s" % quote_plus(service))
|
||||||
print("Avaliable commands:")
|
|
||||||
for k in cmds.keys():
|
|
||||||
print(f" {k}")
|
|
||||||
exit()
|
|
||||||
|
|
||||||
for k in cmds.keys():
|
def check_services(self) -> None:
|
||||||
if k != argv[1]:
|
self.GET("/v1/admin/service/check")
|
||||||
continue
|
|
||||||
try:
|
def add_news(self, news: Dict[str, str]):
|
||||||
cmds[k]()
|
if not "id" in news or news["id"] == "":
|
||||||
except KeyboardInterrupt:
|
raise Exception('News structure is missing required "id" field')
|
||||||
pass
|
|
||||||
exit()
|
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__":
|
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)
|
||||||
|
@ -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
|
|
12
admin/tests/test_news.json
Normal file
12
admin/tests/test_news.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
9
admin/tests/test_service.json
Normal file
9
admin/tests/test_service.json
Normal 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"
|
||||||
|
}
|
2
api/.gitignore
vendored
2
api/.gitignore
vendored
@ -1,2 +1,2 @@
|
|||||||
server
|
*.elf
|
||||||
*.db
|
*.db
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
FROM golang:1.23.4
|
FROM golang:1.23.4
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /api
|
||||||
|
|
||||||
COPY *.go ./
|
COPY *.go ./
|
||||||
COPY *.mod ./
|
COPY *.mod ./
|
||||||
COPY *.sum ./
|
COPY *.sum ./
|
||||||
COPY Makefile ./
|
COPY Makefile ./
|
||||||
|
COPY util ./util
|
||||||
|
COPY views ./views
|
||||||
COPY routes ./routes
|
COPY routes ./routes
|
||||||
COPY config ./config
|
COPY config ./config
|
||||||
|
COPY status ./status
|
||||||
COPY database ./database
|
COPY database ./database
|
||||||
COPY util ./util
|
|
||||||
|
|
||||||
EXPOSE 7001
|
EXPOSE 7001
|
||||||
RUN make
|
RUN make
|
||||||
|
|
||||||
ENTRYPOINT ["/app/server"]
|
ENTRYPOINT ["/api/api.elf"]
|
||||||
|
12
api/Makefile
12
api/Makefile
@ -1,10 +1,12 @@
|
|||||||
all: server
|
GOSRCS = $(wildcard *.go) $(wildcard */*.go)
|
||||||
|
|
||||||
server: *.go routes/*.go database/*.go util/*.go config/*.go
|
all: api.elf
|
||||||
go build -o $@ .
|
|
||||||
|
|
||||||
test:
|
api.elf: $(GOSRCS)
|
||||||
API_FRONTEND_URL=http://localhost:5173/ API_PASSWORD=test ./server
|
go build -o $@
|
||||||
|
|
||||||
|
run:
|
||||||
|
API_DEBUG=true API_FRONTEND_URL=http://localhost:5173/ API_PASSWORD=test ./api.elf
|
||||||
|
|
||||||
format:
|
format:
|
||||||
gofmt -s -w .
|
gofmt -s -w .
|
||||||
|
@ -2,59 +2,112 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ngn13/website/api/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Option struct {
|
type Type struct {
|
||||||
Name string
|
Options []Option
|
||||||
Value string
|
Count int
|
||||||
Required bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Option) Env() string {
|
func (c *Type) Find(name string, typ uint8) (*Option, error) {
|
||||||
return strings.ToUpper(fmt.Sprintf("API_%s", o.Name))
|
for i := 0; i < c.Count; i++ {
|
||||||
}
|
if c.Options[i].Name != name {
|
||||||
|
|
||||||
var options []Option = []Option{
|
|
||||||
{Name: "password", Value: "", Required: true},
|
|
||||||
{Name: "frontend_url", Value: "http://localhost:5173/", Required: true},
|
|
||||||
}
|
|
||||||
|
|
||||||
func Load() bool {
|
|
||||||
var val string
|
|
||||||
|
|
||||||
for i := range options {
|
|
||||||
if val = os.Getenv(options[i].Env()); val == "" {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
options[i].Value = val
|
if c.Options[i].Type != typ {
|
||||||
options[i].Required = false
|
return nil, fmt.Errorf("bad option type")
|
||||||
}
|
|
||||||
|
|
||||||
for i := range options {
|
|
||||||
if options[i].Required && options[i].Value == "" {
|
|
||||||
util.Fail("please specify the required config option \"%s\" (\"%s\")", options[i].Name, options[i].Env())
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if options[i].Required && options[i].Value != "" {
|
return &c.Options[i], nil
|
||||||
util.Fail("using the default value \"%s\" for required config option \"%s\" (\"%s\")", options[i].Value, options[i].Name, options[i].Env())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return nil, fmt.Errorf("option not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func Get(name string) string {
|
func (c *Type) Load() (err error) {
|
||||||
for i := range options {
|
var (
|
||||||
if options[i].Name != name {
|
env_val string
|
||||||
continue
|
env_name string
|
||||||
}
|
opt *Option
|
||||||
return options[i].Value
|
exists bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// default options
|
||||||
|
c.Options = []Option{
|
||||||
|
{Name: "debug", Value: "false", Type: OPTION_TYPE_BOOL, Required: true}, // should display debug messgaes?
|
||||||
|
{Name: "index", Value: "true", Type: OPTION_TYPE_BOOL, Required: false}, // should display the index page (view/index.md)?
|
||||||
|
|
||||||
|
{Name: "api_url", Value: "http://localhost:7001/", Type: OPTION_TYPE_URL, Required: true}, // API URL for the website
|
||||||
|
{Name: "frontend_url", Value: "http://localhost:5173/", Type: OPTION_TYPE_URL, Required: true}, // frontend application URL for the website
|
||||||
|
|
||||||
|
{Name: "password", Value: "", Type: OPTION_TYPE_STR, Required: true}, // admin password
|
||||||
|
{Name: "host", Value: "0.0.0.0:7001", Type: OPTION_TYPE_STR, Required: true}, // host the server should listen on
|
||||||
|
{Name: "ip_header", Value: "X-Real-IP", Type: OPTION_TYPE_STR, Required: false}, // header that should be checked for obtaining the client IP
|
||||||
|
{Name: "interval", Value: "1h", Type: OPTION_TYPE_STR, Required: false}, // service status check interval
|
||||||
|
{Name: "timeout", Value: "15s", Type: OPTION_TYPE_STR, Required: false}, // timeout for the service status check
|
||||||
|
{Name: "limit", Value: "5s", Type: OPTION_TYPE_STR, Required: false}, // if the service responds slower than this limit, it will be marked as "slow"
|
||||||
}
|
}
|
||||||
return ""
|
c.Count = len(c.Options)
|
||||||
|
|
||||||
|
for i := 0; i < c.Count; i++ {
|
||||||
|
opt = &c.Options[i]
|
||||||
|
|
||||||
|
env_name = opt.Env()
|
||||||
|
|
||||||
|
if env_val, exists = os.LookupEnv(env_name); exists {
|
||||||
|
opt.Value = env_val
|
||||||
|
}
|
||||||
|
|
||||||
|
if opt.Value == "" && opt.Required {
|
||||||
|
return fmt.Errorf("please specify a value for the config option \"%s\" (\"%s\")", opt.Name, env_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = opt.Load(); err != nil {
|
||||||
|
return fmt.Errorf("failed to load option \"%s\" (\"%s\"): %s", opt.Name, env_name, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Type) GetStr(name string) string {
|
||||||
|
var (
|
||||||
|
opt *Option
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if opt, err = c.Find(name, OPTION_TYPE_STR); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return opt.TypeValue.Str
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Type) GetBool(name string) bool {
|
||||||
|
var (
|
||||||
|
opt *Option
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if opt, err = c.Find(name, OPTION_TYPE_BOOL); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return opt.TypeValue.Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Type) GetURL(name string) *url.URL {
|
||||||
|
var (
|
||||||
|
opt *Option
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if opt, err = c.Find(name, OPTION_TYPE_URL); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return opt.TypeValue.URL
|
||||||
}
|
}
|
||||||
|
49
api/config/option.go
Normal file
49
api/config/option.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
OPTION_TYPE_STR = 0
|
||||||
|
OPTION_TYPE_BOOL = 1
|
||||||
|
OPTION_TYPE_URL = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type Option struct {
|
||||||
|
Name string
|
||||||
|
Value string
|
||||||
|
Required bool
|
||||||
|
Type uint8
|
||||||
|
TypeValue struct {
|
||||||
|
URL *url.URL
|
||||||
|
Str string
|
||||||
|
Bool bool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Option) Env() string {
|
||||||
|
return strings.ToUpper(fmt.Sprintf("API_%s", o.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Option) Load() (err error) {
|
||||||
|
err = nil
|
||||||
|
|
||||||
|
switch o.Type {
|
||||||
|
case OPTION_TYPE_STR:
|
||||||
|
o.TypeValue.Str = o.Value
|
||||||
|
|
||||||
|
case OPTION_TYPE_BOOL:
|
||||||
|
o.TypeValue.Bool = "1" == o.Value || "true" == strings.ToLower(o.Value)
|
||||||
|
|
||||||
|
case OPTION_TYPE_URL:
|
||||||
|
o.TypeValue.URL, err = url.Parse(o.Value)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid option type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
62
api/database/admin.go
Normal file
62
api/database/admin.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AdminLog struct {
|
||||||
|
Action string `json:"action"` // action that was performed (service removal, service addition etc.)
|
||||||
|
Time int64 `json:"time"` // time when the action was performed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *AdminLog) Scan(rows *sql.Rows) (err error) {
|
||||||
|
if rows != nil {
|
||||||
|
return rows.Scan(&l.Action, &l.Time)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("no row/rows specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) AdminLogNext(l *AdminLog) bool {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if nil == db.rows {
|
||||||
|
if db.rows, err = db.sql.Query("SELECT * FROM admin_log"); err != nil {
|
||||||
|
util.Fail("failed to query admin_log table: %s", err.Error())
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !db.rows.Next() {
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = l.Scan(db.rows); err != nil {
|
||||||
|
util.Fail("failed to scan the admin_log table: %s", err.Error())
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
fail:
|
||||||
|
if db.rows != nil {
|
||||||
|
db.rows.Close()
|
||||||
|
}
|
||||||
|
db.rows = nil
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) AdminLogAdd(l *AdminLog) error {
|
||||||
|
_, err := db.sql.Exec(
|
||||||
|
`INSERT INTO admin_log(
|
||||||
|
action, time
|
||||||
|
) values(?, ?)`,
|
||||||
|
&l.Action, &l.Time,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
@ -1,44 +1,66 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Setup(db *sql.DB) error {
|
type Type struct {
|
||||||
_, err := db.Exec(`
|
sql *sql.DB
|
||||||
CREATE TABLE IF NOT EXISTS posts(
|
rows *sql.Rows
|
||||||
id TEXT NOT NULL UNIQUE,
|
}
|
||||||
title TEXT NOT NULL,
|
|
||||||
author TEXT NOT NULL,
|
func (db *Type) Load() (err error) {
|
||||||
date TEXT NOT NULL,
|
if db.sql, err = sql.Open("sqlite3", "data.db"); err != nil {
|
||||||
content TEXT NOT NULL,
|
return fmt.Errorf("cannot access the database: %s", err.Error())
|
||||||
public INTEGER NOT NULL,
|
}
|
||||||
vote INTEGER NOT NULL
|
|
||||||
);
|
// see database/service.go
|
||||||
`)
|
_, err = db.sql.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS services(
|
||||||
if err != nil {
|
name TEXT NOT NULL UNIQUE,
|
||||||
return err
|
desc TEXT NOT NULL,
|
||||||
}
|
check_time INTEGER NOT NULL,
|
||||||
|
check_res INTEGER NOT NULL,
|
||||||
_, err = db.Exec(`
|
check_url TEXT NOT NULL,
|
||||||
CREATE TABLE IF NOT EXISTS services(
|
clear TEXT,
|
||||||
name TEXT NOT NULL UNIQUE,
|
onion TEXT,
|
||||||
desc TEXT NOT NULL,
|
i2p TEXT
|
||||||
url TEXT NOT NULL
|
);
|
||||||
);
|
`)
|
||||||
`)
|
|
||||||
|
if err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("failed to create the services table: %s", err.Error())
|
||||||
return err
|
}
|
||||||
}
|
|
||||||
|
// see database/news.go
|
||||||
_, err = db.Exec(`
|
_, err = db.sql.Exec(`
|
||||||
CREATE TABLE IF NOT EXISTS votes(
|
CREATE TABLE IF NOT EXISTS news(
|
||||||
hash TEXT NOT NULL UNIQUE,
|
id TEXT NOT NULL UNIQUE,
|
||||||
is_upvote INTEGER NOT NULL
|
title TEXT NOT NULL,
|
||||||
);
|
author TEXT NOT NULL,
|
||||||
`)
|
time INTEGER NOT NULL,
|
||||||
|
content TEXT NOT NULL
|
||||||
return err
|
);
|
||||||
|
`)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create the news table: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// see database/admin.go
|
||||||
|
_, err = db.sql.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS admin_log(
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
time INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create the admin_log table: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
58
api/database/multilang.go
Normal file
58
api/database/multilang.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Multilang struct {
|
||||||
|
En string `json:"en"` // english
|
||||||
|
Tr string `json:"tr"` // turkish
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *Multilang) Supports(lang string) bool {
|
||||||
|
ml_ref := reflect.ValueOf(ml).Elem()
|
||||||
|
|
||||||
|
for i := 0; i < reflect.Indirect(ml_ref).NumField(); i++ {
|
||||||
|
if name := reflect.Indirect(ml_ref).Field(i).Type().Name(); strings.ToLower(name) == lang {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *Multilang) Get(lang string) string {
|
||||||
|
r := []rune(lang)
|
||||||
|
r[0] = unicode.ToUpper(r[0])
|
||||||
|
l := string(r)
|
||||||
|
|
||||||
|
ml_ref := reflect.ValueOf(ml)
|
||||||
|
return reflect.Indirect(ml_ref).FieldByName(l).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *Multilang) Empty() bool {
|
||||||
|
ml_ref := reflect.ValueOf(ml)
|
||||||
|
|
||||||
|
for i := 0; i < reflect.Indirect(ml_ref).NumField(); i++ {
|
||||||
|
if field := reflect.Indirect(ml_ref).Field(i); field.String() != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *Multilang) Dump() (string, error) {
|
||||||
|
if data, err := json.Marshal(ml); err != nil {
|
||||||
|
return "", err
|
||||||
|
} else {
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *Multilang) Load(s string) error {
|
||||||
|
return json.Unmarshal([]byte(s), ml)
|
||||||
|
}
|
116
api/database/news.go
Normal file
116
api/database/news.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type News struct {
|
||||||
|
ID string `json:"id"` // ID of the news
|
||||||
|
title string `json:"-"` // title of the news (string)
|
||||||
|
Title Multilang `json:"title"` // title of the news
|
||||||
|
Author string `json:"author"` // author of the news
|
||||||
|
Time uint64 `json:"time"` // when the new was published
|
||||||
|
content string `json:"-"` // content of the news (string)
|
||||||
|
Content Multilang `json:"content"` // content of the news
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *News) Supports(lang string) bool {
|
||||||
|
return n.Content.Supports(lang) && n.Title.Supports(lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *News) Load() (err error) {
|
||||||
|
if err = n.Title.Load(n.title); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = n.Content.Load(n.content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *News) Dump() (err error) {
|
||||||
|
if n.title, err = n.Title.Dump(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.content, err = n.Content.Dump(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *News) Scan(rows *sql.Rows) (err error) {
|
||||||
|
err = rows.Scan(
|
||||||
|
&n.ID, &n.title, &n.Author,
|
||||||
|
&n.Time, &n.content)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return n.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *News) IsValid() bool {
|
||||||
|
return n.Author != "" && n.ID != "" && !n.Title.Empty() && !n.Content.Empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) NewsNext(n *News) bool {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if nil == db.rows {
|
||||||
|
if db.rows, err = db.sql.Query("SELECT * FROM news"); err != nil {
|
||||||
|
util.Fail("failed to query news table: %s", err.Error())
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !db.rows.Next() {
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = n.Scan(db.rows); err != nil {
|
||||||
|
util.Fail("failed to scan the news table: %s", err.Error())
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
fail:
|
||||||
|
if db.rows != nil {
|
||||||
|
db.rows.Close()
|
||||||
|
}
|
||||||
|
db.rows = nil
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) NewsRemove(id string) error {
|
||||||
|
_, err := db.sql.Exec(
|
||||||
|
"DELETE FROM news WHERE id = ?",
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) NewsAdd(n *News) (err error) {
|
||||||
|
if err = n.Dump(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.sql.Exec(
|
||||||
|
`INSERT OR REPLACE INTO news(
|
||||||
|
id, title, author, time, content
|
||||||
|
) values(?, ?, ?, ?, ?)`,
|
||||||
|
n.ID, n.title,
|
||||||
|
n.Author, n.Time, n.content,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
@ -1,71 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"github.com/ngn13/website/api/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Post struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Author string `json:"author"`
|
|
||||||
Date string `json:"date"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
Public int `json:"public"`
|
|
||||||
Vote int `json:"vote"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Post) Load(rows *sql.Rows) error {
|
|
||||||
return rows.Scan(&p.ID, &p.Title, &p.Author, &p.Date, &p.Content, &p.Public, &p.Vote)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Post) Get(db *sql.DB, id string) (bool, error) {
|
|
||||||
var (
|
|
||||||
success bool
|
|
||||||
rows *sql.Rows
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if rows, err = db.Query("SELECT * FROM posts WHERE id = ?", id); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
if success = rows.Next(); !success {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = p.Load(rows); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Post) Remove(db *sql.DB) error {
|
|
||||||
_, err := db.Exec("DELETE FROM posts WHERE id = ?", p.ID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Post) Save(db *sql.DB) error {
|
|
||||||
p.ID = util.TitleToID(p.Title)
|
|
||||||
|
|
||||||
_, err := db.Exec(
|
|
||||||
"INSERT INTO posts(id, title, author, date, content, public, vote) values(?, ?, ?, ?, ?, ?, ?)",
|
|
||||||
p.ID, p.Title, p.Author, p.Date, p.Content, p.Public, p.Vote,
|
|
||||||
)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Post) Update(db *sql.DB) error {
|
|
||||||
p.ID = util.TitleToID(p.Title)
|
|
||||||
|
|
||||||
_, err := db.Exec(
|
|
||||||
"UPDATE posts SET title = ?, author = ?, date = ?, content = ?, public = ?, vote = ? WHERE id = ?",
|
|
||||||
p.Title, p.Author, p.Date, p.Content, p.Public, p.Vote, p.ID,
|
|
||||||
)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
@ -2,54 +2,127 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"` // name of the service
|
||||||
Desc string `json:"desc"`
|
desc string `json:"-"` // description of the service (string)
|
||||||
Url string `json:"url"`
|
Desc Multilang `json:"desc"` // description of the service
|
||||||
|
CheckTime uint64 `json:"check_time"` // last status check time
|
||||||
|
CheckRes uint8 `json:"check_res"` // result of the status check
|
||||||
|
CheckURL string `json:"check_url"` // URL used for status check
|
||||||
|
Clear string `json:"clear"` // Clearnet (cringe) URL for the service
|
||||||
|
Onion string `json:"onion"` // Onion (TOR) URL for the service
|
||||||
|
I2P string `json:"i2p"` // I2P URL for the service
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Load(rows *sql.Rows) error {
|
func (s *Service) Load() error {
|
||||||
return rows.Scan(&s.Name, &s.Desc, &s.Url)
|
return s.Desc.Load(s.desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Get(db *sql.DB, name string) (bool, error) {
|
func (s *Service) Dump() (err error) {
|
||||||
|
s.desc, err = s.Desc.Dump()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Scan(rows *sql.Rows, row *sql.Row) (err error) {
|
||||||
|
if rows != nil {
|
||||||
|
err = rows.Scan(
|
||||||
|
&s.Name, &s.desc,
|
||||||
|
&s.CheckTime, &s.CheckRes, &s.CheckURL,
|
||||||
|
&s.Clear, &s.Onion, &s.I2P)
|
||||||
|
} else if row != nil {
|
||||||
|
err = row.Scan(
|
||||||
|
&s.Name, &s.desc,
|
||||||
|
&s.CheckTime, &s.CheckRes, &s.CheckURL,
|
||||||
|
&s.Clear, &s.Onion, &s.I2P)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("no row/rows specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) IsValid() bool {
|
||||||
|
return s.Name != "" && (s.Clear != "" || s.Onion != "" || s.I2P != "") && !s.Desc.Empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) ServiceNext(s *Service) bool {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if nil == db.rows {
|
||||||
|
if db.rows, err = db.sql.Query("SELECT * FROM services"); err != nil {
|
||||||
|
util.Fail("failed to query services table: %s", err.Error())
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !db.rows.Next() {
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = s.Scan(db.rows, nil); err != nil {
|
||||||
|
util.Fail("failed to scan the services table: %s", err.Error())
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
fail:
|
||||||
|
if db.rows != nil {
|
||||||
|
db.rows.Close()
|
||||||
|
}
|
||||||
|
db.rows = nil
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) ServiceFind(name string) (*Service, error) {
|
||||||
var (
|
var (
|
||||||
success bool
|
row *sql.Row
|
||||||
rows *sql.Rows
|
s Service
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if rows, err = db.Query("SELECT * FROM services WHERE name = ?", name); err != nil {
|
if row = db.sql.QueryRow("SELECT * FROM services WHERE name = ?", name); row == nil || row.Err() == sql.ErrNoRows {
|
||||||
return false, err
|
return nil, nil
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
if success = rows.Next(); !success {
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = s.Load(rows); err != nil {
|
if err = s.Scan(nil, row); err != nil {
|
||||||
return false, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, nil
|
return &s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Remove(db *sql.DB) error {
|
func (db *Type) ServiceRemove(name string) error {
|
||||||
_, err := db.Exec(
|
_, err := db.sql.Exec(
|
||||||
"DELETE FROM services WHERE name = ?",
|
"DELETE FROM services WHERE name = ?",
|
||||||
s.Name,
|
name,
|
||||||
)
|
)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Save(db *sql.DB) error {
|
func (db *Type) ServiceUpdate(s *Service) (err error) {
|
||||||
_, err := db.Exec(
|
if err = s.Dump(); err != nil {
|
||||||
"INSERT INTO services(name, desc, url) values(?, ?, ?)",
|
return err
|
||||||
s.Name, s.Desc, s.Url,
|
}
|
||||||
|
|
||||||
|
_, err = db.sql.Exec(
|
||||||
|
`INSERT OR REPLACE INTO services(
|
||||||
|
name, desc, check_time, check_res, check_url, clear, onion, i2p
|
||||||
|
) values(?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
s.Name, s.desc,
|
||||||
|
s.CheckTime, s.CheckRes, s.CheckURL,
|
||||||
|
s.Clear, s.Onion, s.I2P,
|
||||||
)
|
)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import "database/sql"
|
|
||||||
|
|
||||||
type Vote struct {
|
|
||||||
Hash string
|
|
||||||
IsUpvote bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Vote) Load(rows *sql.Rows) error {
|
|
||||||
return rows.Scan(&v.Hash, &v.IsUpvote)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Vote) Get(db *sql.DB, hash string) (bool, error) {
|
|
||||||
var (
|
|
||||||
success bool
|
|
||||||
rows *sql.Rows
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if rows, err = db.Query("SELECT * FROM votes WHERE hash = ?", hash); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
if success = rows.Next(); !success {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = v.Load(rows); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Vote) Update(db *sql.DB) error {
|
|
||||||
_, err := db.Exec("UPDATE votes SET is_upvote = ? WHERE hash = ?", v.IsUpvote, v.Hash)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Vote) Save(db *sql.DB) error {
|
|
||||||
_, err := db.Exec(
|
|
||||||
"INSERT INTO votes(hash, is_upvote) values(?, ?)",
|
|
||||||
v.Hash, v.IsUpvote,
|
|
||||||
)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
@ -2,11 +2,7 @@ module github.com/ngn13/website/api
|
|||||||
|
|
||||||
go 1.21.3
|
go 1.21.3
|
||||||
|
|
||||||
require (
|
require github.com/gofiber/fiber/v2 v2.52.5
|
||||||
github.com/gofiber/fiber/v2 v2.52.5
|
|
||||||
github.com/gorilla/feeds v1.2.0
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.24
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||||
@ -15,7 +11,9 @@ require (
|
|||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
10
api/go.sum
10
api/go.sum
@ -4,14 +4,8 @@ github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yG
|
|||||||
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc=
|
|
||||||
github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
|
|
||||||
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
@ -23,8 +17,8 @@ github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBW
|
|||||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||||
|
126
api/main.go
126
api/main.go
@ -1,102 +1,124 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
* website/api | API server 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/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/ngn13/website/api/config"
|
"github.com/ngn13/website/api/config"
|
||||||
"github.com/ngn13/website/api/database"
|
"github.com/ngn13/website/api/database"
|
||||||
"github.com/ngn13/website/api/routes"
|
"github.com/ngn13/website/api/routes"
|
||||||
|
"github.com/ngn13/website/api/status"
|
||||||
"github.com/ngn13/website/api/util"
|
"github.com/ngn13/website/api/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var db *sql.DB
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var (
|
var (
|
||||||
app *fiber.App
|
app *fiber.App
|
||||||
//db *sql.DB
|
stat status.Type
|
||||||
|
|
||||||
|
conf config.Type
|
||||||
|
db database.Type
|
||||||
|
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if !config.Load() {
|
if err = conf.Load(); err != nil {
|
||||||
util.Fail("failed to load the configuration")
|
util.Fail("failed to load the configuration: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if db, err = sql.Open("sqlite3", "data.db"); err != nil {
|
if !conf.GetBool("debug") {
|
||||||
util.Fail("cannot access the database: %s", err.Error())
|
util.Debg = func(m string, v ...any) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.Load(); err != nil {
|
||||||
|
util.Fail("failed to load the database: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
if err = database.Setup(db); err != nil {
|
if err = stat.Setup(&conf, &db); err != nil {
|
||||||
util.Fail("cannot setup the database: %s", err.Error())
|
util.Fail("failed to setup the status checker: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app = fiber.New(fiber.Config{
|
app = fiber.New(fiber.Config{
|
||||||
|
AppName: "ngn's website",
|
||||||
DisableStartupMessage: true,
|
DisableStartupMessage: true,
|
||||||
|
ServerHeader: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
app.Use("*", func(c *fiber.Ctx) error {
|
app.Use("*", func(c *fiber.Ctx) error {
|
||||||
|
// CORS stuff
|
||||||
c.Set("Access-Control-Allow-Origin", "*")
|
c.Set("Access-Control-Allow-Origin", "*")
|
||||||
c.Set("Access-Control-Allow-Headers",
|
c.Set("Access-Control-Allow-Headers",
|
||||||
"Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
"Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||||
c.Set("Access-Control-Allow-Methods", "PUT, DELETE, GET")
|
c.Set("Access-Control-Allow-Methods", "PUT, DELETE, GET") // POST can be sent from HTML forms, so I prefer PUT for API endpoints
|
||||||
|
|
||||||
|
c.Locals("status", &stat)
|
||||||
|
c.Locals("config", &conf)
|
||||||
c.Locals("database", &db)
|
c.Locals("database", &db)
|
||||||
|
|
||||||
return c.Next()
|
return c.Next()
|
||||||
})
|
})
|
||||||
|
|
||||||
// index route
|
// index route
|
||||||
app.Get("/", func(c *fiber.Ctx) error {
|
app.Get("/", routes.GET_Index)
|
||||||
return c.Send([]byte("o/"))
|
|
||||||
})
|
|
||||||
|
|
||||||
// blog routes
|
// version groups
|
||||||
blog_routes := app.Group("/blog")
|
v1 := app.Group("v1")
|
||||||
|
|
||||||
// blog feed routes
|
// v1 user routes
|
||||||
blog_routes.Get("/feed.*", routes.GET_Feed)
|
v1.Get("/services", routes.GET_Services)
|
||||||
|
v1.Get("/news/:lang", routes.GET_News)
|
||||||
|
|
||||||
// blog post routes
|
// v1 admin routes
|
||||||
blog_routes.Get("/sum", routes.GET_PostSum)
|
v1.Use("/admin", routes.AuthMiddleware)
|
||||||
blog_routes.Get("/get", routes.GET_Post)
|
v1.Get("/admin/logs", routes.GET_AdminLogs)
|
||||||
|
v1.Get("/admin/service/check", routes.GET_CheckService)
|
||||||
// blog post vote routes
|
v1.Put("/admin/service/add", routes.PUT_AddService)
|
||||||
blog_routes.Get("/vote/set", routes.GET_VoteSet)
|
v1.Delete("/admin/service/del", routes.DEL_DelService)
|
||||||
blog_routes.Get("/vote/get", routes.GET_VoteGet)
|
v1.Put("/admin/news/add", routes.PUT_AddNews)
|
||||||
|
v1.Delete("/admin/news/del", routes.DEL_DelNews)
|
||||||
// service routes
|
|
||||||
service_routes := app.Group("services")
|
|
||||||
service_routes.Get("/all", routes.GET_Services)
|
|
||||||
|
|
||||||
// admin routes
|
|
||||||
admin_routes := app.Group("admin")
|
|
||||||
admin_routes.Use("*", routes.AuthMiddleware)
|
|
||||||
|
|
||||||
// admin auth routes
|
|
||||||
admin_routes.Get("/login", routes.GET_Login)
|
|
||||||
admin_routes.Get("/logout", routes.GET_Logout)
|
|
||||||
|
|
||||||
// admin service managment routes
|
|
||||||
admin_routes.Put("/service/add", routes.PUT_AddService)
|
|
||||||
admin_routes.Delete("/service/remove", routes.DEL_RemoveService)
|
|
||||||
|
|
||||||
// admin blog managment routes
|
|
||||||
admin_routes.Put("/blog/add", routes.PUT_AddPost)
|
|
||||||
admin_routes.Delete("/blog/remove", routes.DEL_RemovePost)
|
|
||||||
|
|
||||||
// 404 route
|
// 404 route
|
||||||
app.All("*", func(c *fiber.Ctx) error {
|
app.All("*", func(c *fiber.Ctx) error {
|
||||||
return util.ErrNotFound(c)
|
return util.JSON(c, http.StatusNotFound, fiber.Map{
|
||||||
|
"error": "Endpoint not found",
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
util.Info("starting web server at port 7001")
|
// start the status checker
|
||||||
|
if err = stat.Run(); err != nil {
|
||||||
if err = app.Listen("0.0.0.0:7001"); err != nil {
|
util.Fail("failed to start the status checker: %s", err.Error())
|
||||||
util.Fail("error starting the webserver: %s", err.Error())
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// start the app
|
||||||
|
util.Info("starting web server on %s", conf.GetStr("host"))
|
||||||
|
|
||||||
|
if err = app.Listen(conf.GetStr("host")); err != nil {
|
||||||
|
util.Fail("failed to start the web server: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
stat.Stop()
|
||||||
}
|
}
|
||||||
|
@ -1,183 +1,154 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/mattn/go-sqlite3"
|
|
||||||
"github.com/ngn13/website/api/config"
|
"github.com/ngn13/website/api/config"
|
||||||
"github.com/ngn13/website/api/database"
|
"github.com/ngn13/website/api/database"
|
||||||
|
"github.com/ngn13/website/api/status"
|
||||||
"github.com/ngn13/website/api/util"
|
"github.com/ngn13/website/api/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Token string = util.CreateToken()
|
func admin_log(c *fiber.Ctx, m string) error {
|
||||||
|
return c.Locals("database").(*database.Type).AdminLogAdd(&database.AdminLog{
|
||||||
|
Action: m, // action that the admin peformed
|
||||||
|
Time: time.Now().Unix(), // current time
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func AuthMiddleware(c *fiber.Ctx) error {
|
func AuthMiddleware(c *fiber.Ctx) error {
|
||||||
if c.Path() == "/admin/login" {
|
conf := c.Locals("config").(*config.Type)
|
||||||
return c.Next()
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Get("Authorization") != Token {
|
if c.Get("Authorization") != conf.GetStr("password") {
|
||||||
return util.ErrAuth(c)
|
return util.ErrAuth(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
func GET_Login(c *fiber.Ctx) error {
|
func GET_AdminLogs(c *fiber.Ctx) error {
|
||||||
if c.Query("pass") != config.Get("password") {
|
|
||||||
return util.ErrAuth(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
util.Info("new login from %s", util.GetIP(c))
|
|
||||||
|
|
||||||
return c.Status(http.StatusOK).JSON(fiber.Map{
|
|
||||||
"error": "",
|
|
||||||
"token": Token,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func GET_Logout(c *fiber.Ctx) error {
|
|
||||||
Token = util.CreateToken()
|
|
||||||
|
|
||||||
util.Info("logout from %s", util.GetIP(c))
|
|
||||||
|
|
||||||
return c.Status(http.StatusOK).JSON(fiber.Map{
|
|
||||||
"error": "",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func DEL_RemoveService(c *fiber.Ctx) error {
|
|
||||||
var (
|
var (
|
||||||
db *sql.DB
|
list []database.AdminLog
|
||||||
service database.Service
|
log database.AdminLog
|
||||||
name string
|
|
||||||
found bool
|
|
||||||
err error
|
|
||||||
)
|
)
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
db := c.Locals("database").(*database.Type)
|
||||||
name = c.Query("name")
|
|
||||||
|
|
||||||
if name == "" {
|
for db.AdminLogNext(&log) {
|
||||||
util.ErrBadData(c)
|
list = append(list, log)
|
||||||
}
|
}
|
||||||
|
|
||||||
if found, err = service.Get(db, name); err != nil {
|
return util.JSON(c, 200, fiber.Map{
|
||||||
util.Fail("error while searching for a service (\"%s\"): %s", name, err.Error())
|
"result": list,
|
||||||
return util.ErrServer(c)
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DEL_DelService(c *fiber.Ctx) error {
|
||||||
|
var (
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
db := c.Locals("database").(*database.Type)
|
||||||
|
|
||||||
|
if name = c.Query("name"); name == "" {
|
||||||
|
util.ErrBadReq(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if err = admin_log(c, fmt.Sprintf("Removed service \"%s\"", name)); err != nil {
|
||||||
return util.ErrEntryNotExists(c)
|
return util.ErrInternal(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = service.Remove(db); err != nil {
|
if err = db.ServiceRemove(name); err != nil {
|
||||||
util.Fail("error while removing a service (\"%s\"): %s", service.Name, err.Error())
|
return util.ErrInternal(c, err)
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.NoError(c)
|
return util.JSON(c, 200, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func PUT_AddService(c *fiber.Ctx) error {
|
func PUT_AddService(c *fiber.Ctx) error {
|
||||||
var (
|
var (
|
||||||
service database.Service
|
service database.Service
|
||||||
db *sql.DB
|
|
||||||
found bool
|
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
db := c.Locals("database").(*database.Type)
|
||||||
|
|
||||||
if c.BodyParser(&service) != nil {
|
if c.BodyParser(&service) != nil {
|
||||||
return util.ErrBadJSON(c)
|
return util.ErrBadJSON(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
if service.Name == "" || service.Desc == "" || service.Url == "" {
|
if !service.IsValid() {
|
||||||
return util.ErrBadData(c)
|
return util.ErrBadReq(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
if found, err = service.Get(db, service.Name); err != nil {
|
if err = admin_log(c, fmt.Sprintf("Added service \"%s\"", service.Name)); err != nil {
|
||||||
util.Fail("error while searching for a service (\"%s\"): %s", service.Name, err.Error())
|
return util.ErrInternal(c, err)
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if found {
|
if err = db.ServiceUpdate(&service); err != nil {
|
||||||
return util.ErrEntryExists(c)
|
return util.ErrInternal(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = service.Save(db); err != nil {
|
// force a status check so we can get the status of the new service
|
||||||
util.Fail("error while saving a new service (\"%s\"): %s", service.Name, err.Error())
|
c.Locals("status").(*status.Type).Check()
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.NoError(c)
|
return util.JSON(c, 200, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DEL_RemovePost(c *fiber.Ctx) error {
|
func GET_CheckService(c *fiber.Ctx) error {
|
||||||
|
c.Locals("status").(*status.Type).Check()
|
||||||
|
return util.JSON(c, 200, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DEL_DelNews(c *fiber.Ctx) error {
|
||||||
var (
|
var (
|
||||||
db *sql.DB
|
id string
|
||||||
id string
|
err error
|
||||||
found bool
|
|
||||||
err error
|
|
||||||
post database.Post
|
|
||||||
)
|
)
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
db := c.Locals("database").(*database.Type)
|
||||||
|
|
||||||
if id = c.Query("id"); id == "" {
|
if id = c.Query("id"); id == "" {
|
||||||
return util.ErrBadData(c)
|
util.ErrBadReq(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
if found, err = post.Get(db, id); err != nil {
|
if err = admin_log(c, fmt.Sprintf("Removed news \"%s\"", id)); err != nil {
|
||||||
util.Fail("error while searching for a post (\"%s\"): %s", id, err.Error())
|
return util.ErrInternal(c, err)
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if err = db.NewsRemove(id); err != nil {
|
||||||
return util.ErrEntryNotExists(c)
|
return util.ErrInternal(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = post.Remove(db); err != nil {
|
return util.JSON(c, 200, nil)
|
||||||
util.Fail("error while removing a post (\"%s\"): %s", post.ID, err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.NoError(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func PUT_AddPost(c *fiber.Ctx) error {
|
func PUT_AddNews(c *fiber.Ctx) error {
|
||||||
var (
|
var (
|
||||||
db *sql.DB
|
news database.News
|
||||||
post database.Post
|
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
db := c.Locals("database").(*database.Type)
|
||||||
post.Public = 1
|
|
||||||
|
|
||||||
if c.BodyParser(&post) != nil {
|
if c.BodyParser(&news) != nil {
|
||||||
return util.ErrBadJSON(c)
|
return util.ErrBadJSON(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
if post.Title == "" || post.Author == "" || post.Content == "" {
|
if !news.IsValid() {
|
||||||
return util.ErrBadData(c)
|
return util.ErrBadReq(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
post.Date = time.Now().Format("02/01/06")
|
if err = admin_log(c, fmt.Sprintf("Added news \"%s\"", news.ID)); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
if err = post.Save(db); err != nil && strings.Contains(err.Error(), sqlite3.ErrConstraintUnique.Error()) {
|
|
||||||
return util.ErrEntryExists(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err = db.NewsAdd(&news); err != nil {
|
||||||
util.Fail("error while saving a new post (\"%s\"): %s", post.ID, err.Error())
|
return util.ErrInternal(c, err)
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.NoError(c)
|
return util.JSON(c, 200, nil)
|
||||||
}
|
}
|
||||||
|
@ -1,203 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/gorilla/feeds"
|
|
||||||
"github.com/ngn13/website/api/config"
|
|
||||||
"github.com/ngn13/website/api/database"
|
|
||||||
"github.com/ngn13/website/api/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GET_Post(c *fiber.Ctx) error {
|
|
||||||
var (
|
|
||||||
post database.Post
|
|
||||||
id string
|
|
||||||
db *sql.DB
|
|
||||||
found bool
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
|
||||||
|
|
||||||
if id = c.Query("id"); id == "" {
|
|
||||||
return util.ErrBadData(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if found, err = post.Get(db, id); err != nil {
|
|
||||||
util.Fail("error while search for a post (\"%s\"): %s", id, err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return util.ErrEntryNotExists(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(fiber.Map{
|
|
||||||
"error": "",
|
|
||||||
"result": post,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func GET_PostSum(c *fiber.Ctx) error {
|
|
||||||
var (
|
|
||||||
posts []database.Post
|
|
||||||
rows *sql.Rows
|
|
||||||
db *sql.DB
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
|
||||||
|
|
||||||
if rows, err = db.Query("SELECT * FROM posts"); err != nil {
|
|
||||||
util.Fail("cannot load posts: %s", err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var post database.Post
|
|
||||||
|
|
||||||
if err = post.Load(rows); err != nil {
|
|
||||||
util.Fail("error while loading post: %s", err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if post.Public == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(post.Content) > 255 {
|
|
||||||
post.Content = post.Content[0:250]
|
|
||||||
}
|
|
||||||
|
|
||||||
posts = append(posts, post)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(fiber.Map{
|
|
||||||
"error": "",
|
|
||||||
"result": posts,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFeed(db *sql.DB) (*feeds.Feed, error) {
|
|
||||||
var (
|
|
||||||
posts []database.Post
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
rows, err := db.Query("SELECT * FROM posts")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var post database.Post
|
|
||||||
|
|
||||||
if err = post.Load(rows); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if post.Public == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
posts = append(posts, post)
|
|
||||||
}
|
|
||||||
rows.Close()
|
|
||||||
|
|
||||||
blogurl, err := url.JoinPath(
|
|
||||||
config.Get("frontend_url"), "/blog",
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create the blog URL: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
feed := &feeds.Feed{
|
|
||||||
Title: "[ngn.tf] | blog",
|
|
||||||
Link: &feeds.Link{Href: blogurl},
|
|
||||||
Description: "ngn's personal blog",
|
|
||||||
Author: &feeds.Author{Name: "ngn", Email: "ngn@ngn.tf"},
|
|
||||||
Created: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
feed.Items = []*feeds.Item{}
|
|
||||||
for _, p := range posts {
|
|
||||||
purl, err := url.JoinPath(blogurl, p.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create URL for '%s': %s\n", p.ID, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed, err := time.Parse("02/01/06", p.Date)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse time for '%s': %s\n", p.ID, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
feed.Items = append(feed.Items, &feeds.Item{
|
|
||||||
Id: p.ID,
|
|
||||||
Title: p.Title,
|
|
||||||
Link: &feeds.Link{Href: purl},
|
|
||||||
Author: &feeds.Author{Name: p.Author},
|
|
||||||
Created: parsed,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return feed, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GET_Feed(c *fiber.Ctx) error {
|
|
||||||
var (
|
|
||||||
db *sql.DB
|
|
||||||
err error
|
|
||||||
feed *feeds.Feed
|
|
||||||
name []string
|
|
||||||
res string
|
|
||||||
ext string
|
|
||||||
)
|
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
|
||||||
|
|
||||||
if name = strings.Split(path.Base(c.Path()), "."); len(name) != 2 {
|
|
||||||
return util.ErrNotFound(c)
|
|
||||||
}
|
|
||||||
ext = name[1]
|
|
||||||
|
|
||||||
if feed, err = getFeed(db); err != nil {
|
|
||||||
util.Fail("cannot obtain the feed: %s", err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ext {
|
|
||||||
case "atom":
|
|
||||||
res, err = feed.ToAtom()
|
|
||||||
c.Set("Content-Type", "application/atom+xml")
|
|
||||||
break
|
|
||||||
|
|
||||||
case "json":
|
|
||||||
res, err = feed.ToJSON()
|
|
||||||
c.Set("Content-Type", "application/feed+json")
|
|
||||||
break
|
|
||||||
|
|
||||||
case "rss":
|
|
||||||
res, err = feed.ToRss()
|
|
||||||
c.Set("Content-Type", "application/rss+xml")
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
return util.ErrNotFound(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
util.Fail("cannot obtain the feed as the specified format: %s", err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Send([]byte(res))
|
|
||||||
}
|
|
32
api/routes/index.go
Normal file
32
api/routes/index.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/ngn13/website/api/config"
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GET_Index(c *fiber.Ctx) error {
|
||||||
|
var (
|
||||||
|
md []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
conf := c.Locals("config").(*config.Type)
|
||||||
|
|
||||||
|
if !conf.GetBool("index") {
|
||||||
|
return util.ErrNotFound(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
frontend := conf.GetURL("frontend_url")
|
||||||
|
api := conf.GetURL("api_url")
|
||||||
|
|
||||||
|
if md, err = util.Render("views/index.md", fiber.Map{
|
||||||
|
"frontend": frontend,
|
||||||
|
"api": api,
|
||||||
|
}); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.Markdown(c, md)
|
||||||
|
}
|
47
api/routes/news.go
Normal file
47
api/routes/news.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/ngn13/website/api/config"
|
||||||
|
"github.com/ngn13/website/api/database"
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GET_News(c *fiber.Ctx) error {
|
||||||
|
var (
|
||||||
|
news []database.News
|
||||||
|
n database.News
|
||||||
|
feed []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
db := c.Locals("database").(*database.Type)
|
||||||
|
conf := c.Locals("config").(*config.Type)
|
||||||
|
frontend := conf.GetURL("frontend_url")
|
||||||
|
lang := c.Params("lang")
|
||||||
|
|
||||||
|
if lang == "" || len(lang) != 2 {
|
||||||
|
return util.ErrBadReq(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
lang = strings.ToLower(lang)
|
||||||
|
|
||||||
|
for db.NewsNext(&n) {
|
||||||
|
if n.Supports(lang) {
|
||||||
|
news = append(news, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if feed, err = util.Render("views/news.xml", fiber.Map{
|
||||||
|
"frontend": frontend,
|
||||||
|
"lang": lang,
|
||||||
|
"news": news,
|
||||||
|
}); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("Content-Type", "application/atom+xml; charset=utf-8")
|
||||||
|
return c.Send(feed)
|
||||||
|
}
|
@ -1,8 +1,6 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/ngn13/website/api/database"
|
"github.com/ngn13/website/api/database"
|
||||||
"github.com/ngn13/website/api/util"
|
"github.com/ngn13/website/api/util"
|
||||||
@ -11,32 +9,29 @@ import (
|
|||||||
func GET_Services(c *fiber.Ctx) error {
|
func GET_Services(c *fiber.Ctx) error {
|
||||||
var (
|
var (
|
||||||
services []database.Service
|
services []database.Service
|
||||||
rows *sql.Rows
|
service database.Service
|
||||||
db *sql.DB
|
|
||||||
err error
|
|
||||||
)
|
)
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
db := c.Locals("database").(*database.Type)
|
||||||
|
name := c.Query("name")
|
||||||
|
|
||||||
if rows, err = db.Query("SELECT * FROM services"); err != nil {
|
if name != "" {
|
||||||
util.Fail("cannot load services: %s", err.Error())
|
if s, err := db.ServiceFind(name); err != nil {
|
||||||
return util.ErrServer(c)
|
return util.ErrInternal(c, err)
|
||||||
}
|
} else if s != nil {
|
||||||
defer rows.Close()
|
return util.JSON(c, 200, fiber.Map{
|
||||||
|
"result": s,
|
||||||
for rows.Next() {
|
})
|
||||||
var service database.Service
|
|
||||||
|
|
||||||
if err = service.Load(rows); err != nil {
|
|
||||||
util.Fail("error while loading service: %s", err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return util.ErrNotExist(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
for db.ServiceNext(&service) {
|
||||||
services = append(services, service)
|
services = append(services, service)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(fiber.Map{
|
return util.JSON(c, 200, fiber.Map{
|
||||||
"error": "",
|
|
||||||
"result": services,
|
"result": services,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,139 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/ngn13/website/api/database"
|
|
||||||
"github.com/ngn13/website/api/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getVoteHash(id string, ip string) string {
|
|
||||||
return util.GetSHA512(id + "_" + ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GET_VoteGet(c *fiber.Ctx) error {
|
|
||||||
var (
|
|
||||||
db *sql.DB
|
|
||||||
id string
|
|
||||||
hash string
|
|
||||||
vote database.Vote
|
|
||||||
found bool
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
|
||||||
|
|
||||||
if id = c.Query("id"); id == "" {
|
|
||||||
return util.ErrBadData(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
hash = getVoteHash(id, util.GetIP(c))
|
|
||||||
|
|
||||||
if found, err = vote.Get(db, hash); err != nil {
|
|
||||||
util.Fail("error while searchig for a vote (\"%s\"): %s", hash, err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return util.ErrEntryNotExists(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if vote.IsUpvote {
|
|
||||||
return c.JSON(fiber.Map{
|
|
||||||
"error": "",
|
|
||||||
"result": "upvote",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(fiber.Map{
|
|
||||||
"error": "",
|
|
||||||
"result": "downvote",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func GET_VoteSet(c *fiber.Ctx) error {
|
|
||||||
var (
|
|
||||||
db *sql.DB
|
|
||||||
id string
|
|
||||||
is_upvote bool
|
|
||||||
hash string
|
|
||||||
vote database.Vote
|
|
||||||
post database.Post
|
|
||||||
found bool
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
|
||||||
id = c.Query("id")
|
|
||||||
|
|
||||||
if c.Query("to") == "" || id == "" {
|
|
||||||
return util.ErrBadData(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if found, err = post.Get(db, id); err != nil {
|
|
||||||
util.Fail("error while searching for a post (\"%s\"): %s", id, err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return util.ErrEntryNotExists(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
is_upvote = c.Query("to") == "upvote"
|
|
||||||
hash = getVoteHash(id, util.GetIP(c))
|
|
||||||
|
|
||||||
if found, err = vote.Get(db, hash); err != nil {
|
|
||||||
util.Fail("error while searching for a vote (\"%s\"): %s", hash, err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if found {
|
|
||||||
if vote.IsUpvote == is_upvote {
|
|
||||||
return util.ErrEntryExists(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if vote.IsUpvote && !is_upvote {
|
|
||||||
post.Vote -= 2
|
|
||||||
}
|
|
||||||
|
|
||||||
if !vote.IsUpvote && is_upvote {
|
|
||||||
post.Vote += 2
|
|
||||||
}
|
|
||||||
|
|
||||||
vote.IsUpvote = is_upvote
|
|
||||||
|
|
||||||
if err = post.Update(db); err != nil {
|
|
||||||
util.Fail("error while updating post (\"%s\"): %s", post.ID, err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = vote.Update(db); err != nil {
|
|
||||||
util.Fail("error while updating vote (\"%s\"): %s", vote.Hash, err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.NoError(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
vote.Hash = hash
|
|
||||||
vote.IsUpvote = is_upvote
|
|
||||||
|
|
||||||
if is_upvote {
|
|
||||||
post.Vote++
|
|
||||||
} else {
|
|
||||||
post.Vote--
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = post.Update(db); err != nil {
|
|
||||||
util.Fail("error while updating post (\"%s\"): %s", post.ID, err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = vote.Save(db); err != nil {
|
|
||||||
util.Fail("error while updating vote (\"%s\"): %s", vote.Hash, err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.NoError(c)
|
|
||||||
}
|
|
105
api/status/service.go
Normal file
105
api/status/service.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptrace"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ngn13/website/api/database"
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
STATUS_RES_DOWN = 0 // service is down
|
||||||
|
STATUS_RES_OK = 1 // service is up
|
||||||
|
STATUS_RES_SLOW = 2 // service is up, but slow
|
||||||
|
STATUS_RES_NONE = 3 // service doesn't support status checking/status checking is disabled
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Type) check_http_service(service *database.Service) (r uint8, err error) {
|
||||||
|
var (
|
||||||
|
req *http.Request
|
||||||
|
res *http.Response
|
||||||
|
|
||||||
|
start time.Time
|
||||||
|
elapsed time.Duration
|
||||||
|
)
|
||||||
|
|
||||||
|
r = STATUS_RES_NONE
|
||||||
|
|
||||||
|
if req, err = http.NewRequest("GET", service.CheckURL, nil); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
trace := &httptrace.ClientTrace{
|
||||||
|
GetConn: func(_ string) { start = time.Now() },
|
||||||
|
GotFirstResponseByte: func() { elapsed = time.Since(start) },
|
||||||
|
}
|
||||||
|
|
||||||
|
http.DefaultClient.Timeout = s.timeout
|
||||||
|
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
|
||||||
|
res, err = http.DefaultClient.Do(req)
|
||||||
|
|
||||||
|
if res != nil {
|
||||||
|
defer res.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
util.Debg("marking service \"%s\" as down (%s)", service.Name, err.Error())
|
||||||
|
err = nil
|
||||||
|
r = STATUS_RES_DOWN
|
||||||
|
} else if res.StatusCode != 200 {
|
||||||
|
util.Debg("marking service \"%s\" as down (status code %d)", service.Name, res.StatusCode)
|
||||||
|
r = STATUS_RES_DOWN
|
||||||
|
} else if elapsed.Microseconds() > s.limit.Microseconds() {
|
||||||
|
r = STATUS_RES_SLOW
|
||||||
|
} else {
|
||||||
|
r = STATUS_RES_OK
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Type) check_service(service *database.Service) error {
|
||||||
|
var (
|
||||||
|
res uint8
|
||||||
|
url *url.URL
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if s.disabled || service.CheckURL == "" {
|
||||||
|
err = nil
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
if url, err = url.Parse(service.CheckURL); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch url.Scheme {
|
||||||
|
case "https":
|
||||||
|
if res, err = s.check_http_service(service); err != nil {
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
case "http":
|
||||||
|
if res, err = s.check_http_service(service); err != nil {
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// unsupported protocol
|
||||||
|
err = nil
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
service.CheckTime = uint64(time.Now().Unix())
|
||||||
|
service.CheckRes = res
|
||||||
|
return nil
|
||||||
|
|
||||||
|
fail:
|
||||||
|
service.CheckTime = 0
|
||||||
|
service.CheckRes = STATUS_RES_NONE
|
||||||
|
return err
|
||||||
|
}
|
139
api/status/status.go
Normal file
139
api/status/status.go
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ngn13/website/api/config"
|
||||||
|
"github.com/ngn13/website/api/database"
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Type struct {
|
||||||
|
conf *config.Type
|
||||||
|
db *database.Type
|
||||||
|
|
||||||
|
ticker *time.Ticker
|
||||||
|
updateChan chan int
|
||||||
|
closeChan chan int
|
||||||
|
|
||||||
|
disabled bool
|
||||||
|
timeout time.Duration
|
||||||
|
limit time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Type) check() {
|
||||||
|
var (
|
||||||
|
services []database.Service
|
||||||
|
service database.Service
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
for s.db.ServiceNext(&service) {
|
||||||
|
services = append(services, service)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range services {
|
||||||
|
if err = s.check_service(&services[i]); err != nil {
|
||||||
|
util.Fail("failed to check the service status for \"%s\": %s", services[i].Name, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = s.db.ServiceUpdate(&services[i]); err != nil {
|
||||||
|
util.Fail("failed to update service status for \"%s\": %s", services[i].Name, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Type) loop() {
|
||||||
|
s.check()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.closeChan:
|
||||||
|
close(s.updateChan)
|
||||||
|
s.ticker.Stop()
|
||||||
|
s.closeChan <- 0
|
||||||
|
return
|
||||||
|
|
||||||
|
case <-s.updateChan:
|
||||||
|
s.check()
|
||||||
|
|
||||||
|
case <-s.ticker.C:
|
||||||
|
s.check()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Type) Setup(conf *config.Type, db *database.Type) error {
|
||||||
|
var (
|
||||||
|
dur time.Duration
|
||||||
|
iv, to, lm string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
iv = conf.GetStr("interval")
|
||||||
|
to = conf.GetStr("timeout")
|
||||||
|
lm = conf.GetStr("limit")
|
||||||
|
|
||||||
|
if iv == "" || to == "" || lm == "" {
|
||||||
|
s.disabled = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if dur, err = util.GetDuration(iv); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.timeout, err = util.GetDuration(iv); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.limit, err = util.GetDuration(iv); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.conf = conf
|
||||||
|
s.db = db
|
||||||
|
|
||||||
|
s.ticker = time.NewTicker(dur)
|
||||||
|
s.updateChan = make(chan int)
|
||||||
|
s.closeChan = make(chan int)
|
||||||
|
|
||||||
|
s.disabled = false
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Type) Run() error {
|
||||||
|
if s.ticker == nil || s.updateChan == nil || s.closeChan == nil {
|
||||||
|
return fmt.Errorf("you either didn't call Setup() or you called it and it failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.disabled {
|
||||||
|
go s.check()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
go s.loop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Type) Check() {
|
||||||
|
if !s.disabled {
|
||||||
|
s.updateChan <- 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Type) Stop() {
|
||||||
|
// tell loop() to stop
|
||||||
|
s.closeChan <- 0
|
||||||
|
|
||||||
|
// wait till loop() stops
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.closeChan:
|
||||||
|
close(s.closeChan)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,8 +5,17 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
const (
|
||||||
Info = log.New(os.Stdout, "\033[34m[info]\033[0m ", log.Ltime|log.Lshortfile).Printf
|
COLOR_BLUE = "\033[34m"
|
||||||
Warn = log.New(os.Stderr, "\033[33m[warn]\033[0m ", log.Ltime|log.Lshortfile).Printf
|
COLOR_YELLOW = "\033[33m"
|
||||||
Fail = log.New(os.Stderr, "\033[31m[fail]\033[0m ", log.Ltime|log.Lshortfile).Printf
|
COLOR_RED = "\033[31m"
|
||||||
|
COLOR_CYAN = "\033[36m"
|
||||||
|
COLOR_RESET = "\033[0m"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Debg = log.New(os.Stdout, COLOR_CYAN+"[debg]"+COLOR_RESET+" ", log.Ltime|log.Lshortfile).Printf
|
||||||
|
Info = log.New(os.Stdout, COLOR_BLUE+"[info]"+COLOR_RESET+" ", log.Ltime|log.Lshortfile).Printf
|
||||||
|
Warn = log.New(os.Stderr, COLOR_YELLOW+"[warn]"+COLOR_RESET+" ", log.Ltime|log.Lshortfile).Printf
|
||||||
|
Fail = log.New(os.Stderr, COLOR_RED+"[fail]"+COLOR_RESET+" ", log.Ltime|log.Lshortfile).Printf
|
||||||
)
|
)
|
||||||
|
89
api/util/res.go
Normal file
89
api/util/res.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/ngn13/website/api/config"
|
||||||
|
"github.com/russross/blackfriday/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IP(c *fiber.Ctx) string {
|
||||||
|
conf := c.Locals("config").(*config.Type)
|
||||||
|
ip_header := conf.GetStr("ip_header")
|
||||||
|
|
||||||
|
if ip_header != "" && c.Get(ip_header) != "" {
|
||||||
|
return strings.Clone(c.Get(ip_header))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.IP()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Markdown(c *fiber.Ctx, raw []byte) error {
|
||||||
|
exts := blackfriday.FencedCode
|
||||||
|
exts |= blackfriday.NoEmptyLineBeforeBlock
|
||||||
|
exts |= blackfriday.HardLineBreak
|
||||||
|
|
||||||
|
c.Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
return c.Send(blackfriday.Run(raw, blackfriday.WithExtensions(exts)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func JSON(c *fiber.Ctx, code int, data fiber.Map) error {
|
||||||
|
if data == nil {
|
||||||
|
data = fiber.Map{}
|
||||||
|
data["error"] = ""
|
||||||
|
} else if _, ok := data["error"]; !ok {
|
||||||
|
data["error"] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if data["error"] == 200 {
|
||||||
|
Warn("200 response with an error at %s", c.Path())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(code).JSON(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrInternal(c *fiber.Ctx, err error) error {
|
||||||
|
Warn("Internal server error at %s: %s", c.Path(), err.Error())
|
||||||
|
|
||||||
|
return JSON(c, http.StatusInternalServerError, fiber.Map{
|
||||||
|
"error": "Server error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrExists(c *fiber.Ctx) error {
|
||||||
|
return JSON(c, http.StatusConflict, fiber.Map{
|
||||||
|
"error": "Entry already exists",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrNotExist(c *fiber.Ctx) error {
|
||||||
|
return JSON(c, http.StatusNotFound, fiber.Map{
|
||||||
|
"error": "Entry does not exist",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrBadReq(c *fiber.Ctx) error {
|
||||||
|
return JSON(c, http.StatusBadRequest, fiber.Map{
|
||||||
|
"error": "Provided data is invalid",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrNotFound(c *fiber.Ctx) error {
|
||||||
|
return JSON(c, http.StatusNotFound, fiber.Map{
|
||||||
|
"error": "Endpoint not found",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrBadJSON(c *fiber.Ctx) error {
|
||||||
|
return JSON(c, http.StatusBadRequest, fiber.Map{
|
||||||
|
"error": "Invalid JSON data",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrAuth(c *fiber.Ctx) error {
|
||||||
|
return JSON(c, http.StatusUnauthorized, fiber.Map{
|
||||||
|
"error": "Authentication failed",
|
||||||
|
})
|
||||||
|
}
|
67
api/util/util.go
Normal file
67
api/util/util.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Render(file string, data interface{}) ([]byte, error) {
|
||||||
|
var (
|
||||||
|
rendered *bytes.Buffer
|
||||||
|
tmpl *template.Template
|
||||||
|
content []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if content, err = os.ReadFile(file); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tmpl, err = template.New("template").Parse(string(content)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rendered = bytes.NewBuffer(nil)
|
||||||
|
err = tmpl.Execute(rendered, data)
|
||||||
|
|
||||||
|
return rendered.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDuration(d string) (time.Duration, error) {
|
||||||
|
var (
|
||||||
|
d_num uint64
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
d_num_end := d[len(d)-1]
|
||||||
|
d_num_str := strings.TrimSuffix(d, string(d_num_end))
|
||||||
|
|
||||||
|
if d_num, err = strconv.ParseUint(d_num_str, 10, 64); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch d_num_end {
|
||||||
|
case 's':
|
||||||
|
return time.Duration(d_num) * (time.Second), nil
|
||||||
|
|
||||||
|
case 'm':
|
||||||
|
return time.Duration(d_num) * (time.Second * 60), nil
|
||||||
|
|
||||||
|
case 'h':
|
||||||
|
return time.Duration(d_num) * ((time.Second * 60) * 60), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, fmt.Errorf("invalid time duration format")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSHA1(s string) string {
|
||||||
|
hasher := sha1.New()
|
||||||
|
return hex.EncodeToString(hasher.Sum([]byte(s)))
|
||||||
|
}
|
@ -1,74 +0,0 @@
|
|||||||
package util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha512"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetSHA512(s string) string {
|
|
||||||
hasher := sha512.New()
|
|
||||||
return fmt.Sprintf("%x", hasher.Sum([]byte(s)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TitleToID(name string) string {
|
|
||||||
return strings.ToLower(strings.ReplaceAll(name, " ", ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateToken() string {
|
|
||||||
s := make([]byte, 32)
|
|
||||||
for i := 0; i < 32; i++ {
|
|
||||||
s[i] = byte(65 + rand.Intn(25))
|
|
||||||
}
|
|
||||||
return string(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrorJSON(error string) fiber.Map {
|
|
||||||
return fiber.Map{
|
|
||||||
"error": error,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetIP(c *fiber.Ctx) string {
|
|
||||||
if c.Get("X-Real-IP") != "" {
|
|
||||||
return strings.Clone(c.Get("X-Real-IP"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.IP()
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrServer(c *fiber.Ctx) error {
|
|
||||||
return c.Status(http.StatusInternalServerError).JSON(ErrorJSON("Server error"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrEntryExists(c *fiber.Ctx) error {
|
|
||||||
return c.Status(http.StatusConflict).JSON(ErrorJSON("Entry already exists"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrEntryNotExists(c *fiber.Ctx) error {
|
|
||||||
return c.Status(http.StatusNotFound).JSON(ErrorJSON("Entry does not exist"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrBadData(c *fiber.Ctx) error {
|
|
||||||
return c.Status(http.StatusBadRequest).JSON(ErrorJSON("Provided data is invalid"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrBadJSON(c *fiber.Ctx) error {
|
|
||||||
return c.Status(http.StatusBadRequest).JSON(ErrorJSON("Bad JSON data"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrAuth(c *fiber.Ctx) error {
|
|
||||||
return c.Status(http.StatusUnauthorized).JSON(ErrorJSON("Authentication failed"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrNotFound(c *fiber.Ctx) error {
|
|
||||||
return c.Status(http.StatusNotFound).JSON(ErrorJSON("Requested endpoint not found"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func NoError(c *fiber.Ctx) error {
|
|
||||||
return c.Status(http.StatusOK).JSON(ErrorJSON(""))
|
|
||||||
}
|
|
161
api/views/index.md
Normal file
161
api/views/index.md
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
<!-- This is the markdown file that will be served by the index route -->
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
# [{{.api.Host}}]({{.api.String}})
|
||||||
|
This is the API for my personal website, [{{.frontend.Host}}]({{.frontend.String}}).
|
||||||
|
|
||||||
|
It stores information about the self-hosted services I provide and it also allows me
|
||||||
|
to publish news and updates about these services using an Atom feed. It's written in
|
||||||
|
Go and uses SQLite for storage. Licensed under GNU GPL version 3.
|
||||||
|
|
||||||
|
**Source code and the license is available at**: [https://github.com/ngn13/website](https://github.com/ngn13/website)
|
||||||
|
**You can report issues to**: [https://github.com/ngn13/website/issues](https://github.com/ngn13/website/issues)
|
||||||
|
|
||||||
|
The rest of this document contains documentation for all the available API endpoints.
|
||||||
|
|
||||||
|
## Version 1 Endpoints
|
||||||
|
Each version 1 endpoint, can be accessed using the /v1 route.
|
||||||
|
|
||||||
|
All the endpoints return JSON formatted data.
|
||||||
|
|
||||||
|
### Errors
|
||||||
|
If any error occurs, you will get a non-200 response. And the JSON data will have an
|
||||||
|
"error" key, which will contain information about the error that occured, in the
|
||||||
|
string format. This is the only JSON key that will be set in non-200 responses.
|
||||||
|
|
||||||
|
### Results
|
||||||
|
If no error occurs, "error" key will be set to an emtpy string (""). If the endpoint
|
||||||
|
returns any data, this will be stored using the "result" key. The "result" have a
|
||||||
|
different expected type and a format for each endpoint.
|
||||||
|
|
||||||
|
### Multilang
|
||||||
|
Some "result" formats may use a structure called "Multilang". This is a simple JSON
|
||||||
|
structure that includes one key for each supported language. The key is named after
|
||||||
|
the language it represents. Currently only supported languages are:
|
||||||
|
- English (`en`)
|
||||||
|
- Turkish (`tr`)
|
||||||
|
|
||||||
|
So each multilang structure, will have **at least** one of these keys.
|
||||||
|
|
||||||
|
Here is an example multilang structure:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"en": "Hello, world!",
|
||||||
|
"tr": "Merhaba, dünya!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
If a "result" field is using a multilang structure, it will be specified as "Multilang"
|
||||||
|
in the rest of the documentation.
|
||||||
|
|
||||||
|
### Administrator routes
|
||||||
|
The endpoints under the "/v1/admin" route, are administrator-only routes. To access
|
||||||
|
these routes you'll need to specfiy and password using the "Authorization" header.
|
||||||
|
If the password you specify, matches with the password specified using the
|
||||||
|
`API_PASSWORD` environment variable, you will be able to access the route.
|
||||||
|
|
||||||
|
### GET /v1/services
|
||||||
|
Returns a list of available services. Each service has the following JSON format:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"name": "Test Service",
|
||||||
|
"desc": {
|
||||||
|
"en": "Service used for testing the API",
|
||||||
|
"tr": "API'ı test etmek için kullanılan servis"
|
||||||
|
},
|
||||||
|
"check_time": 1735861944,
|
||||||
|
"check_res": 1,
|
||||||
|
"check_url": "http://localhost:7001",
|
||||||
|
"clear": "http://localhost:7001",
|
||||||
|
"onion": "",
|
||||||
|
"i2p": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Where:
|
||||||
|
- `name`: Service name (string)
|
||||||
|
- `desc`: Service description (Multilang)
|
||||||
|
- `check_time`: Last time status check time for the service, set 0 if status checking is
|
||||||
|
not supported for this service/status checking is disabled (integer, UNIX timestamp)
|
||||||
|
- `check_res`: Last service status check result (integer)
|
||||||
|
* 0 if the service is down
|
||||||
|
* 1 if the service is up
|
||||||
|
* 2 if the service is up, but slow
|
||||||
|
* 3 if the service doesn't support status checking/status checking is disabled
|
||||||
|
- `check_url`: URL used for service's status check (string, empty if none)
|
||||||
|
- `clear`: Clearnet URL for the service (string, empty string if none)
|
||||||
|
- `onion`: Onion (TOR) URL for the service (string, empty string if none)
|
||||||
|
- `i2p`: I2P URL for the service (string, empty string if none)
|
||||||
|
|
||||||
|
You can also get information about a specific service by specifying it's name using
|
||||||
|
a URL query named "name".
|
||||||
|
|
||||||
|
### GET /v1/news/:language
|
||||||
|
Returns a Atom feed of news for the given language. Supports languages that are supported
|
||||||
|
by Multilang.
|
||||||
|
|
||||||
|
### GET /v1/admin/logs
|
||||||
|
Returns a list of administrator logs. Each log has the following JSON format:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"action": "Added service \"Test Service\"",
|
||||||
|
"time": 1735861794
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Where:
|
||||||
|
- `action`: Action that the administrator performed (string)
|
||||||
|
- `time`: Time when the administrator action was performed (integer, UNIX timestamp)
|
||||||
|
|
||||||
|
Client can get the logs for only a single address, by setting the URL query "addr".
|
||||||
|
|
||||||
|
### PUT /v1/admin/service/add
|
||||||
|
Creates a new service. The request body needs to contain JSON data, and it needs to
|
||||||
|
have the JSON format used to represent a service. See "/v1/services/all" route to
|
||||||
|
see this format.
|
||||||
|
|
||||||
|
Returns no data on success.
|
||||||
|
|
||||||
|
### DELETE /v1/admin/service/del
|
||||||
|
Deletes a service. The client needs to specify the name of the service to delete, by
|
||||||
|
setting the URL query "name".
|
||||||
|
|
||||||
|
Returns no data on success.
|
||||||
|
|
||||||
|
### GET /v1/admin/service/check
|
||||||
|
Forces a status check for all the services.
|
||||||
|
|
||||||
|
Returns no data on success.
|
||||||
|
|
||||||
|
### PUT /v1/admin/news/add
|
||||||
|
Creates a news post. The request body needs to contain JSON data, and it needs
|
||||||
|
to use the following JSON format:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Where:
|
||||||
|
- `id`: Unique ID for the news post (string)
|
||||||
|
- `title`: Title for the news post (Multilang)
|
||||||
|
- `author`: Author of the news post (string)
|
||||||
|
- `content`: Contents of the news post (Multilang)
|
||||||
|
|
||||||
|
Returns no data on success.
|
||||||
|
|
||||||
|
### DELETE /v1/admin/news/del
|
||||||
|
Deletes a news post. The client needs to specify the ID of the news post to delete,
|
||||||
|
by setting the URL query "id".
|
||||||
|
|
||||||
|
Returns no data on success.
|
6
api/views/news.xml
Normal file
6
api/views/news.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<title>{{.frontend.Host}} news</title>
|
||||||
|
<updated>2025-01-02T20:46:24Z</updated>
|
||||||
|
<subtitle>News and updates about my self-hosted services and projects</subtitle>
|
||||||
|
<link href="{{.frontend.String}}/news"></link>
|
||||||
|
</feed>
|
Loading…
x
Reference in New Issue
Block a user