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
|
||||
|
||||
"""
|
||||
Administration script for my website (ngn.tf)
|
||||
#############################################
|
||||
I really enjoy doing stuff from the terminal,
|
||||
so I wrote this simple python script that interacts
|
||||
with the API and lets me add/remove new posts/services
|
||||
from the terminal
|
||||
|
||||
website/admin | Administration script for my personal website
|
||||
written by ngn (https://ngn.tf) (2025)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
|
||||
from os import remove, getenv
|
||||
from urllib.parse import quote_plus
|
||||
from typing import Dict, List, Any
|
||||
from datetime import datetime, UTC
|
||||
from colorama import Fore, Style
|
||||
from json import dumps, loads
|
||||
from getpass import getpass
|
||||
import requests as req
|
||||
from os import getenv
|
||||
from sys import argv
|
||||
|
||||
URL = ""
|
||||
API_URL_ENV = "API_URL"
|
||||
|
||||
def join(pth: str) -> str:
|
||||
if URL == None:
|
||||
return ""
|
||||
|
||||
if URL.endswith("/"):
|
||||
return URL+pth
|
||||
return URL+"/"+pth
|
||||
# logger used by the script
|
||||
class Log:
|
||||
def __init__(self) -> None:
|
||||
self.reset = Fore.RESET + Style.RESET_ALL
|
||||
|
||||
def get_token() -> str:
|
||||
try:
|
||||
f = open("/tmp/wa", "r")
|
||||
token = f.read()
|
||||
f.close()
|
||||
return token
|
||||
except:
|
||||
print("[-] You are not authenticated")
|
||||
exit(1)
|
||||
def info(self, m: str) -> None:
|
||||
print(Fore.BLUE + Style.BRIGHT + "[*]" + self.reset + " " + m)
|
||||
|
||||
def login() -> None:
|
||||
pwd = getpass("[>] Enter your password: ")
|
||||
res = req.get(join("admin/login")+f"?pass={pwd}").json()
|
||||
if res["error"] != "":
|
||||
print(f"[-] Error logging in: {res['error']}")
|
||||
return
|
||||
def error(self, m: str) -> None:
|
||||
print(Fore.RED + Style.BRIGHT + "[-]" + self.reset + " " + m)
|
||||
|
||||
token = res["token"]
|
||||
f = open("/tmp/wa", "w")
|
||||
f.write(token)
|
||||
f.close()
|
||||
def input(self, m: str) -> str:
|
||||
return input(Fore.CYAN + Style.BRIGHT + "[?]" + self.reset + " " + m + ": ")
|
||||
|
||||
def logout() -> None:
|
||||
token = get_token()
|
||||
res = req.get(join("admin/logout"), headers={
|
||||
"Authorization": token
|
||||
}).json()
|
||||
if res["error"] != "":
|
||||
print(f"[-] Error logging out: {res['error']}")
|
||||
return
|
||||
def password(self, m: str) -> str:
|
||||
return getpass(Fore.CYAN + Style.BRIGHT + "[?]" + self.reset + " " + m + ": ")
|
||||
|
||||
remove("/tmp/wa")
|
||||
print("[+] Logged out")
|
||||
|
||||
def add_post() -> None:
|
||||
token = get_token()
|
||||
title = input("[>] Post title: ")
|
||||
author = input("[>] Post author: ")
|
||||
content_file = input("[>] Post content file: ")
|
||||
public = input("[>] Should post be public? (y/n): ")
|
||||
# API interface for the admin endpoints
|
||||
class AdminAPI:
|
||||
def __init__(self, url: str, password: str) -> None:
|
||||
self.languages: List[str] = [
|
||||
"en",
|
||||
"tr",
|
||||
] # languages supported by multilang fields
|
||||
self.password = password
|
||||
self.api_url = url
|
||||
|
||||
try:
|
||||
f = open(content_file, "r")
|
||||
content = f.read()
|
||||
f.close()
|
||||
except:
|
||||
print("[-] Content file not found")
|
||||
return
|
||||
def _title_to_id(self, title: str) -> str:
|
||||
return title.lower().replace(" ", "_")
|
||||
|
||||
res = req.put(join("admin/blog/add"), json={
|
||||
"title": title,
|
||||
"author": author,
|
||||
"content": content,
|
||||
"public": 1 if public == "y" else 0
|
||||
}, headers={
|
||||
"Authorization": token
|
||||
}).json()
|
||||
def _check_multilang_field(self, ml: Dict[str, str]) -> bool:
|
||||
for l in self.languages:
|
||||
if l in ml and ml[l] != "":
|
||||
return True
|
||||
return False
|
||||
|
||||
if res["error"] != "":
|
||||
print(f"[-] Error adding post: {res['error']}")
|
||||
return
|
||||
def _api_url_join(self, path: str) -> str:
|
||||
api_has_slash = self.api_url.endswith("/")
|
||||
path_has_slash = path.startswith("/")
|
||||
|
||||
print("[+] Post has been added")
|
||||
if api_has_slash or path_has_slash:
|
||||
return self.api_url + path
|
||||
elif api_has_slash and path_has_slash:
|
||||
return self.api_url + path[1:]
|
||||
else:
|
||||
return self.api_url + "/" + path
|
||||
|
||||
def remove_post() -> None:
|
||||
token = get_token()
|
||||
id = input("[>] Post ID: ")
|
||||
res = req.delete(join("admin/blog/remove")+f"?id={id}", headers={
|
||||
"Authorization": token
|
||||
}).json()
|
||||
def _to_json(self, res: req.Response) -> dict:
|
||||
if res.status_code == 403:
|
||||
raise Exception("Authentication failed")
|
||||
|
||||
if res["error"] != "":
|
||||
print(f"[-] Error removing post: {res['error']}")
|
||||
return
|
||||
json = res.json()
|
||||
|
||||
print("[-] Post has been removed")
|
||||
if json["error"] != "":
|
||||
raise Exception("API error: %s" % json["error"])
|
||||
|
||||
def add_service() -> None:
|
||||
token = get_token()
|
||||
name = input("[>] Serivce name: ")
|
||||
desc = input("[>] Serivce desc: ")
|
||||
link = input("[>] Serivce URL: ")
|
||||
return json
|
||||
|
||||
res = req.put(join("admin/service/add"), json={
|
||||
"name": name,
|
||||
"desc": desc,
|
||||
"url": link
|
||||
}, headers={
|
||||
"Authorization": token
|
||||
}).json()
|
||||
def PUT(self, url: str, data: dict) -> req.Response:
|
||||
return self._to_json(
|
||||
req.put(
|
||||
self._api_url_join(url),
|
||||
json=data,
|
||||
headers={"Authorization": self.password},
|
||||
)
|
||||
)
|
||||
|
||||
if res["error"] != "":
|
||||
print(f"[-] Error adding service: {res['error']}")
|
||||
return
|
||||
def DELETE(self, url: str) -> req.Response:
|
||||
return self._to_json(
|
||||
req.delete(
|
||||
self._api_url_join(url), headers={"Authorization": self.password}
|
||||
)
|
||||
)
|
||||
|
||||
print("[+] Service has been added")
|
||||
def GET(self, url: str) -> req.Response:
|
||||
return self._to_json(
|
||||
req.get(self._api_url_join(url), headers={"Authorization": self.password})
|
||||
)
|
||||
|
||||
def remove_service() -> None:
|
||||
token = get_token()
|
||||
name = input("[>] Service name: ")
|
||||
res = req.delete(join("admin/service/remove")+f"?name={name}", headers={
|
||||
"Authorization": token
|
||||
}).json()
|
||||
def add_service(self, service: Dict[str, str]):
|
||||
if not "name" in service or service["name"] == "":
|
||||
raise Exception('Service structure is missing required "name" field')
|
||||
|
||||
if res["error"] != "":
|
||||
print(f"[-] Error removing service: {res['error']}")
|
||||
return
|
||||
if not "desc" in service:
|
||||
raise Exception('Service structure is missing required "desc" field')
|
||||
|
||||
print("[+] Serivce has been removed")
|
||||
if (
|
||||
(not "clear" in service or service["clear"] == "")
|
||||
and (not "onion" in service or service["onion"] == "")
|
||||
and (not "i2p" in service or service["i2p"] == "")
|
||||
):
|
||||
raise Exception(
|
||||
'Service structure is missing "clear", "onion" and "i2p" field, at least one needed'
|
||||
)
|
||||
|
||||
cmds = {
|
||||
"login": login,
|
||||
"logout": logout,
|
||||
"add_post": add_post,
|
||||
"remove_post": remove_post,
|
||||
"add_service": add_service,
|
||||
"remove_service": remove_service,
|
||||
}
|
||||
if not self._check_multilang_field(service["desc"]):
|
||||
raise Exception(
|
||||
'Service structure field "desc" needs at least one supported language entry'
|
||||
)
|
||||
|
||||
def main():
|
||||
global URL
|
||||
URL = getenv("API")
|
||||
if URL == None or URL == "":
|
||||
print("[-] API enviroment variable not set")
|
||||
exit(1)
|
||||
self.PUT("/v1/admin/service/add", service)
|
||||
|
||||
if len(argv) != 2:
|
||||
print(f"[-] Usage: admin_script <command>")
|
||||
print(f"[+] Run \"admin_script help\" to get all commands")
|
||||
exit(1)
|
||||
def del_service(self, service: str) -> None:
|
||||
if service == "":
|
||||
raise Exception("Service name cannot be empty")
|
||||
|
||||
if argv[1] == "help":
|
||||
print("Avaliable commands:")
|
||||
for k in cmds.keys():
|
||||
print(f" {k}")
|
||||
exit()
|
||||
self.DELETE("/v1/admin/service/del?name=%s" % quote_plus(service))
|
||||
|
||||
for k in cmds.keys():
|
||||
if k != argv[1]:
|
||||
continue
|
||||
try:
|
||||
cmds[k]()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
exit()
|
||||
def check_services(self) -> None:
|
||||
self.GET("/v1/admin/service/check")
|
||||
|
||||
def add_news(self, news: Dict[str, str]):
|
||||
if not "id" in news or news["id"] == "":
|
||||
raise Exception('News structure is missing required "id" field')
|
||||
|
||||
if not "author" in news or news["author"] == "":
|
||||
raise Exception('News structure is missing required "author" field')
|
||||
|
||||
if not "title" in news:
|
||||
raise Exception('News structure is missing required "title" field')
|
||||
|
||||
if not "content" in news:
|
||||
raise Exception('News structure is missing required "content" field')
|
||||
|
||||
if not self._check_multilang_field(news["title"]):
|
||||
raise Exception(
|
||||
'News structure field "title" needs at least one supported language entry'
|
||||
)
|
||||
|
||||
if not self._check_multilang_field(news["content"]):
|
||||
raise Exception(
|
||||
'News structure field "content" needs at least one supported language entry'
|
||||
)
|
||||
|
||||
self.PUT("/v1/admin/news/add", news)
|
||||
|
||||
def del_news(self, news: str) -> None:
|
||||
if news == "":
|
||||
raise Exception("News ID cannot be empty")
|
||||
|
||||
self.DELETE("/v1/admin/news/del?id=%s" % quote_plus(news))
|
||||
|
||||
def logs(self) -> List[Dict[str, Any]]:
|
||||
return self.GET("/v1/admin/logs")
|
||||
|
||||
|
||||
# local helper functions used by the script
|
||||
def __format_time(ts: int) -> str:
|
||||
return datetime.fromtimestamp(ts, UTC).strftime("%H:%M:%S %d/%m/%Y")
|
||||
|
||||
|
||||
def __load_json_file(file: str) -> Dict[str, Any]:
|
||||
with open(file, "r") as f:
|
||||
data = loads(f.read())
|
||||
return data
|
||||
|
||||
|
||||
def __dump_json_file(data: Dict[str, Any], file: str) -> None:
|
||||
with open(file, "w") as f:
|
||||
data = dumps(data, indent=2)
|
||||
f.write(data)
|
||||
|
||||
|
||||
# command handlers
|
||||
def __handle_command(log: Log, api: AdminAPI, cmd: str) -> None:
|
||||
match cmd:
|
||||
case "add_service":
|
||||
data: Dict[str, str] = {}
|
||||
data["desc"] = {}
|
||||
|
||||
data["name"] = log.input("Serivce name")
|
||||
for l in api.languages:
|
||||
data["desc"][l] = log.input("Serivce desc (%s)" % l)
|
||||
data["check_url"] = log.input("Serivce status check URL")
|
||||
data["clear"] = log.input("Serivce clearnet URL")
|
||||
data["onion"] = log.input("Serivce onion URL")
|
||||
data["i2p"] = log.input("Serivce I2P URL")
|
||||
|
||||
api.add_service(data)
|
||||
log.info("Service has been added")
|
||||
|
||||
case "del_service":
|
||||
api.del_service(self.log.input("Serivce name"))
|
||||
log.info("Service has been deleted")
|
||||
|
||||
case "check_services":
|
||||
api.check_services()
|
||||
log.info("Requested status check for all the services")
|
||||
|
||||
case "add_news":
|
||||
news: Dict[str, str] = {}
|
||||
news["title"] = {}
|
||||
news["content"] = {}
|
||||
|
||||
data["id"] = log.input("News ID")
|
||||
for l in api.languages:
|
||||
data["title"][l] = log.input("News title (%s)" % l)
|
||||
data["author"] = log.input("News author")
|
||||
for l in api.languages:
|
||||
data["content"][l] = log.input("News content (%s)" % l)
|
||||
|
||||
api.add_news(data)
|
||||
log.info("News has been added")
|
||||
|
||||
case "del_news":
|
||||
api.del_news(log.input("News ID"))
|
||||
log.info("News has been deleted")
|
||||
|
||||
case "logs":
|
||||
logs = api.logs()
|
||||
|
||||
if None == logs["result"] or len(logs["result"]) == 0:
|
||||
return log.info("No available logs")
|
||||
|
||||
for l in logs["result"]:
|
||||
log.info(
|
||||
"Time: %s | Action: %s" % (__format_time(l["time"]), l["action"])
|
||||
)
|
||||
|
||||
|
||||
def __handle_command_with_file(log: Log, api: AdminAPI, cmd: str, file: str) -> None:
|
||||
match cmd:
|
||||
case "add_service":
|
||||
data = __load_json_file(file)
|
||||
api.add_service(data)
|
||||
log.info("Service has been added")
|
||||
|
||||
case "del_service":
|
||||
data = __load_json_file(file)
|
||||
api.del_service(data["name"])
|
||||
log.info("Service has been deleted")
|
||||
|
||||
case "check_services":
|
||||
api.check_services()
|
||||
log.info("Requested status check for all the services")
|
||||
|
||||
case "add_news":
|
||||
data = __load_json_file(file)
|
||||
api.add_news(data)
|
||||
log.info("News has been added")
|
||||
|
||||
case "del_news":
|
||||
data = __load_json_file(file)
|
||||
api.del_news(data["id"])
|
||||
log.info("News has been deleted")
|
||||
|
||||
case "logs":
|
||||
logs = api.logs()
|
||||
|
||||
if None == logs["result"] or len(logs["result"]) == 0:
|
||||
return log.info("No available logs")
|
||||
|
||||
__dump_json_file(logs["result"], file)
|
||||
log.info("Logs has been saved")
|
||||
|
||||
print("[-] Command not found")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
log = Log()
|
||||
|
||||
if len(argv) < 2 or len(argv) > 3:
|
||||
log.error("Usage: %s [command] <file>" % argv[0])
|
||||
log.info("Here is a list of available commands:")
|
||||
print("\tadd_service")
|
||||
print("\tdel_service")
|
||||
print("\tcheck_services")
|
||||
print("\tadd_news")
|
||||
print("\tdel_news")
|
||||
print("\tlogs")
|
||||
exit(1)
|
||||
|
||||
url = getenv(API_URL_ENV)
|
||||
|
||||
if url == None:
|
||||
log.error(
|
||||
"Please specify the API URL using %s environment variable" % API_URL_ENV
|
||||
)
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
password = log.password("Please enter the admin password")
|
||||
api = AdminAPI(url, password)
|
||||
|
||||
if len(argv) == 2:
|
||||
__handle_command(log, api, argv[1])
|
||||
elif len(argv) == 3:
|
||||
__handle_command_with_file(log, api, argv[1], argv[2])
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print()
|
||||
log.error("Command cancelled")
|
||||
exit(1)
|
||||
|
||||
except Exception as e:
|
||||
log.error("Command failed: %s" % e)
|
||||
exit(1)
|
||||
|
@ -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
|
||||
|
@ -1,17 +1,19 @@
|
||||
FROM golang:1.23.4
|
||||
|
||||
WORKDIR /app
|
||||
WORKDIR /api
|
||||
|
||||
COPY *.go ./
|
||||
COPY *.mod ./
|
||||
COPY *.sum ./
|
||||
COPY Makefile ./
|
||||
COPY util ./util
|
||||
COPY views ./views
|
||||
COPY routes ./routes
|
||||
COPY config ./config
|
||||
COPY status ./status
|
||||
COPY database ./database
|
||||
COPY util ./util
|
||||
|
||||
EXPOSE 7001
|
||||
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
|
||||
go build -o $@ .
|
||||
all: api.elf
|
||||
|
||||
test:
|
||||
API_FRONTEND_URL=http://localhost:5173/ API_PASSWORD=test ./server
|
||||
api.elf: $(GOSRCS)
|
||||
go build -o $@
|
||||
|
||||
run:
|
||||
API_DEBUG=true API_FRONTEND_URL=http://localhost:5173/ API_PASSWORD=test ./api.elf
|
||||
|
||||
format:
|
||||
gofmt -s -w .
|
||||
|
@ -2,59 +2,112 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ngn13/website/api/util"
|
||||
)
|
||||
|
||||
type Option struct {
|
||||
Name string
|
||||
Value string
|
||||
Required bool
|
||||
type Type struct {
|
||||
Options []Option
|
||||
Count int
|
||||
}
|
||||
|
||||
func (o *Option) Env() string {
|
||||
return strings.ToUpper(fmt.Sprintf("API_%s", o.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 == "" {
|
||||
func (c *Type) Find(name string, typ uint8) (*Option, error) {
|
||||
for i := 0; i < c.Count; i++ {
|
||||
if c.Options[i].Name != name {
|
||||
continue
|
||||
}
|
||||
|
||||
options[i].Value = val
|
||||
options[i].Required = false
|
||||
}
|
||||
|
||||
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 c.Options[i].Type != typ {
|
||||
return nil, fmt.Errorf("bad option type")
|
||||
}
|
||||
|
||||
if options[i].Required && options[i].Value != "" {
|
||||
util.Fail("using the default value \"%s\" for required config option \"%s\" (\"%s\")", options[i].Value, options[i].Name, options[i].Env())
|
||||
}
|
||||
return &c.Options[i], nil
|
||||
}
|
||||
|
||||
return true
|
||||
return nil, fmt.Errorf("option not found")
|
||||
}
|
||||
|
||||
func Get(name string) string {
|
||||
for i := range options {
|
||||
if options[i].Name != name {
|
||||
continue
|
||||
}
|
||||
return options[i].Value
|
||||
func (c *Type) Load() (err error) {
|
||||
var (
|
||||
env_val string
|
||||
env_name string
|
||||
opt *Option
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"database/sql"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func Setup(db *sql.DB) error {
|
||||
_, err := db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS posts(
|
||||
id TEXT NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
author TEXT NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
public INTEGER NOT NULL,
|
||||
vote INTEGER NOT NULL
|
||||
);
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS services(
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
desc TEXT NOT NULL,
|
||||
url TEXT NOT NULL
|
||||
);
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS votes(
|
||||
hash TEXT NOT NULL UNIQUE,
|
||||
is_upvote INTEGER NOT NULL
|
||||
);
|
||||
`)
|
||||
|
||||
return err
|
||||
type Type struct {
|
||||
sql *sql.DB
|
||||
rows *sql.Rows
|
||||
}
|
||||
|
||||
func (db *Type) Load() (err error) {
|
||||
if db.sql, err = sql.Open("sqlite3", "data.db"); err != nil {
|
||||
return fmt.Errorf("cannot access the database: %s", err.Error())
|
||||
}
|
||||
|
||||
// see database/service.go
|
||||
_, err = db.sql.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS services(
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
desc TEXT NOT NULL,
|
||||
check_time INTEGER NOT NULL,
|
||||
check_res INTEGER NOT NULL,
|
||||
check_url TEXT NOT NULL,
|
||||
clear TEXT,
|
||||
onion TEXT,
|
||||
i2p TEXT
|
||||
);
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create the services table: %s", err.Error())
|
||||
}
|
||||
|
||||
// see database/news.go
|
||||
_, err = db.sql.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS news(
|
||||
id TEXT NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
author TEXT NOT NULL,
|
||||
time INTEGER NOT NULL,
|
||||
content TEXT NOT NULL
|
||||
);
|
||||
`)
|
||||
|
||||
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 (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/ngn13/website/api/util"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"desc"`
|
||||
Url string `json:"url"`
|
||||
Name string `json:"name"` // name of the service
|
||||
desc string `json:"-"` // description of the service (string)
|
||||
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 {
|
||||
return rows.Scan(&s.Name, &s.Desc, &s.Url)
|
||||
func (s *Service) Load() error {
|
||||
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 (
|
||||
success bool
|
||||
rows *sql.Rows
|
||||
err error
|
||||
row *sql.Row
|
||||
s Service
|
||||
err error
|
||||
)
|
||||
|
||||
if rows, err = db.Query("SELECT * FROM services WHERE name = ?", name); err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
if success = rows.Next(); !success {
|
||||
return false, nil
|
||||
if row = db.sql.QueryRow("SELECT * FROM services WHERE name = ?", name); row == nil || row.Err() == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err = s.Load(rows); err != nil {
|
||||
return false, err
|
||||
if err = s.Scan(nil, row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func (s *Service) Remove(db *sql.DB) error {
|
||||
_, err := db.Exec(
|
||||
func (db *Type) ServiceRemove(name string) error {
|
||||
_, err := db.sql.Exec(
|
||||
"DELETE FROM services WHERE name = ?",
|
||||
s.Name,
|
||||
name,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) Save(db *sql.DB) error {
|
||||
_, err := db.Exec(
|
||||
"INSERT INTO services(name, desc, url) values(?, ?, ?)",
|
||||
s.Name, s.Desc, s.Url,
|
||||
func (db *Type) ServiceUpdate(s *Service) (err error) {
|
||||
if err = s.Dump(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, 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
|
||||
|
@ -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
|
||||
|
||||
require (
|
||||
github.com/gofiber/fiber/v2 v2.52.5
|
||||
github.com/gorilla/feeds v1.2.0
|
||||
github.com/mattn/go-sqlite3 v1.14.24
|
||||
)
|
||||
require github.com/gofiber/fiber/v2 v2.52.5
|
||||
|
||||
require (
|
||||
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-isatty v0.0.20 // 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/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.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/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
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/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/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
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/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
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/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
|
126
api/main.go
126
api/main.go
@ -1,102 +1,124 @@
|
||||
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 (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/ngn13/website/api/config"
|
||||
"github.com/ngn13/website/api/database"
|
||||
"github.com/ngn13/website/api/routes"
|
||||
"github.com/ngn13/website/api/status"
|
||||
"github.com/ngn13/website/api/util"
|
||||
)
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
func main() {
|
||||
var (
|
||||
app *fiber.App
|
||||
//db *sql.DB
|
||||
app *fiber.App
|
||||
stat status.Type
|
||||
|
||||
conf config.Type
|
||||
db database.Type
|
||||
|
||||
err error
|
||||
)
|
||||
|
||||
if !config.Load() {
|
||||
util.Fail("failed to load the configuration")
|
||||
if err = conf.Load(); err != nil {
|
||||
util.Fail("failed to load the configuration: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if db, err = sql.Open("sqlite3", "data.db"); err != nil {
|
||||
util.Fail("cannot access the database: %s", err.Error())
|
||||
if !conf.GetBool("debug") {
|
||||
util.Debg = func(m string, v ...any) {}
|
||||
}
|
||||
|
||||
if err = db.Load(); err != nil {
|
||||
util.Fail("failed to load the database: %s", err.Error())
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if err = database.Setup(db); err != nil {
|
||||
util.Fail("cannot setup the database: %s", err.Error())
|
||||
if err = stat.Setup(&conf, &db); err != nil {
|
||||
util.Fail("failed to setup the status checker: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
app = fiber.New(fiber.Config{
|
||||
AppName: "ngn's website",
|
||||
DisableStartupMessage: true,
|
||||
ServerHeader: "",
|
||||
})
|
||||
|
||||
app.Use("*", func(c *fiber.Ctx) error {
|
||||
// CORS stuff
|
||||
c.Set("Access-Control-Allow-Origin", "*")
|
||||
c.Set("Access-Control-Allow-Headers",
|
||||
"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)
|
||||
|
||||
return c.Next()
|
||||
})
|
||||
|
||||
// index route
|
||||
app.Get("/", func(c *fiber.Ctx) error {
|
||||
return c.Send([]byte("o/"))
|
||||
})
|
||||
app.Get("/", routes.GET_Index)
|
||||
|
||||
// blog routes
|
||||
blog_routes := app.Group("/blog")
|
||||
// version groups
|
||||
v1 := app.Group("v1")
|
||||
|
||||
// blog feed routes
|
||||
blog_routes.Get("/feed.*", routes.GET_Feed)
|
||||
// v1 user routes
|
||||
v1.Get("/services", routes.GET_Services)
|
||||
v1.Get("/news/:lang", routes.GET_News)
|
||||
|
||||
// blog post routes
|
||||
blog_routes.Get("/sum", routes.GET_PostSum)
|
||||
blog_routes.Get("/get", routes.GET_Post)
|
||||
|
||||
// blog post vote routes
|
||||
blog_routes.Get("/vote/set", routes.GET_VoteSet)
|
||||
blog_routes.Get("/vote/get", routes.GET_VoteGet)
|
||||
|
||||
// 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)
|
||||
// v1 admin routes
|
||||
v1.Use("/admin", routes.AuthMiddleware)
|
||||
v1.Get("/admin/logs", routes.GET_AdminLogs)
|
||||
v1.Get("/admin/service/check", routes.GET_CheckService)
|
||||
v1.Put("/admin/service/add", routes.PUT_AddService)
|
||||
v1.Delete("/admin/service/del", routes.DEL_DelService)
|
||||
v1.Put("/admin/news/add", routes.PUT_AddNews)
|
||||
v1.Delete("/admin/news/del", routes.DEL_DelNews)
|
||||
|
||||
// 404 route
|
||||
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")
|
||||
|
||||
if err = app.Listen("0.0.0.0:7001"); err != nil {
|
||||
util.Fail("error starting the webserver: %s", err.Error())
|
||||
// start the status checker
|
||||
if err = stat.Run(); err != nil {
|
||||
util.Fail("failed to start the status checker: %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
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"strings"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/mattn/go-sqlite3"
|
||||
"github.com/ngn13/website/api/config"
|
||||
"github.com/ngn13/website/api/database"
|
||||
"github.com/ngn13/website/api/status"
|
||||
"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 {
|
||||
if c.Path() == "/admin/login" {
|
||||
return c.Next()
|
||||
}
|
||||
conf := c.Locals("config").(*config.Type)
|
||||
|
||||
if c.Get("Authorization") != Token {
|
||||
if c.Get("Authorization") != conf.GetStr("password") {
|
||||
return util.ErrAuth(c)
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
func GET_Login(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 {
|
||||
func GET_AdminLogs(c *fiber.Ctx) error {
|
||||
var (
|
||||
db *sql.DB
|
||||
service database.Service
|
||||
name string
|
||||
found bool
|
||||
err error
|
||||
list []database.AdminLog
|
||||
log database.AdminLog
|
||||
)
|
||||
|
||||
db = *(c.Locals("database").(**sql.DB))
|
||||
name = c.Query("name")
|
||||
db := c.Locals("database").(*database.Type)
|
||||
|
||||
if name == "" {
|
||||
util.ErrBadData(c)
|
||||
for db.AdminLogNext(&log) {
|
||||
list = append(list, log)
|
||||
}
|
||||
|
||||
if found, err = service.Get(db, name); err != nil {
|
||||
util.Fail("error while searching for a service (\"%s\"): %s", name, err.Error())
|
||||
return util.ErrServer(c)
|
||||
return util.JSON(c, 200, fiber.Map{
|
||||
"result": list,
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
return util.ErrEntryNotExists(c)
|
||||
if err = admin_log(c, fmt.Sprintf("Removed service \"%s\"", name)); err != nil {
|
||||
return util.ErrInternal(c, err)
|
||||
}
|
||||
|
||||
if err = service.Remove(db); err != nil {
|
||||
util.Fail("error while removing a service (\"%s\"): %s", service.Name, err.Error())
|
||||
return util.ErrServer(c)
|
||||
if err = db.ServiceRemove(name); err != nil {
|
||||
return util.ErrInternal(c, err)
|
||||
}
|
||||
|
||||
return util.NoError(c)
|
||||
return util.JSON(c, 200, nil)
|
||||
}
|
||||
|
||||
func PUT_AddService(c *fiber.Ctx) error {
|
||||
var (
|
||||
service database.Service
|
||||
db *sql.DB
|
||||
found bool
|
||||
err error
|
||||
)
|
||||
|
||||
db = *(c.Locals("database").(**sql.DB))
|
||||
db := c.Locals("database").(*database.Type)
|
||||
|
||||
if c.BodyParser(&service) != nil {
|
||||
return util.ErrBadJSON(c)
|
||||
}
|
||||
|
||||
if service.Name == "" || service.Desc == "" || service.Url == "" {
|
||||
return util.ErrBadData(c)
|
||||
if !service.IsValid() {
|
||||
return util.ErrBadReq(c)
|
||||
}
|
||||
|
||||
if found, err = service.Get(db, service.Name); err != nil {
|
||||
util.Fail("error while searching for a service (\"%s\"): %s", service.Name, err.Error())
|
||||
return util.ErrServer(c)
|
||||
if err = admin_log(c, fmt.Sprintf("Added service \"%s\"", service.Name)); err != nil {
|
||||
return util.ErrInternal(c, err)
|
||||
}
|
||||
|
||||
if found {
|
||||
return util.ErrEntryExists(c)
|
||||
if err = db.ServiceUpdate(&service); err != nil {
|
||||
return util.ErrInternal(c, err)
|
||||
}
|
||||
|
||||
if err = service.Save(db); err != nil {
|
||||
util.Fail("error while saving a new service (\"%s\"): %s", service.Name, err.Error())
|
||||
return util.ErrServer(c)
|
||||
}
|
||||
// force a status check so we can get the status of the new service
|
||||
c.Locals("status").(*status.Type).Check()
|
||||
|
||||
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 (
|
||||
db *sql.DB
|
||||
id string
|
||||
found bool
|
||||
err error
|
||||
post database.Post
|
||||
id string
|
||||
err error
|
||||
)
|
||||
|
||||
db = *(c.Locals("database").(**sql.DB))
|
||||
db := c.Locals("database").(*database.Type)
|
||||
|
||||
if id = c.Query("id"); id == "" {
|
||||
return util.ErrBadData(c)
|
||||
util.ErrBadReq(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 err = admin_log(c, fmt.Sprintf("Removed news \"%s\"", id)); err != nil {
|
||||
return util.ErrInternal(c, err)
|
||||
}
|
||||
|
||||
if !found {
|
||||
return util.ErrEntryNotExists(c)
|
||||
if err = db.NewsRemove(id); err != nil {
|
||||
return util.ErrInternal(c, err)
|
||||
}
|
||||
|
||||
if err = post.Remove(db); err != nil {
|
||||
util.Fail("error while removing a post (\"%s\"): %s", post.ID, err.Error())
|
||||
return util.ErrServer(c)
|
||||
}
|
||||
|
||||
return util.NoError(c)
|
||||
return util.JSON(c, 200, nil)
|
||||
}
|
||||
|
||||
func PUT_AddPost(c *fiber.Ctx) error {
|
||||
func PUT_AddNews(c *fiber.Ctx) error {
|
||||
var (
|
||||
db *sql.DB
|
||||
post database.Post
|
||||
news database.News
|
||||
err error
|
||||
)
|
||||
|
||||
db = *(c.Locals("database").(**sql.DB))
|
||||
post.Public = 1
|
||||
db := c.Locals("database").(*database.Type)
|
||||
|
||||
if c.BodyParser(&post) != nil {
|
||||
if c.BodyParser(&news) != nil {
|
||||
return util.ErrBadJSON(c)
|
||||
}
|
||||
|
||||
if post.Title == "" || post.Author == "" || post.Content == "" {
|
||||
return util.ErrBadData(c)
|
||||
if !news.IsValid() {
|
||||
return util.ErrBadReq(c)
|
||||
}
|
||||
|
||||
post.Date = time.Now().Format("02/01/06")
|
||||
|
||||
if err = post.Save(db); err != nil && strings.Contains(err.Error(), sqlite3.ErrConstraintUnique.Error()) {
|
||||
return util.ErrEntryExists(c)
|
||||
if err = admin_log(c, fmt.Sprintf("Added news \"%s\"", news.ID)); err != nil {
|
||||
return util.ErrInternal(c, err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
util.Fail("error while saving a new post (\"%s\"): %s", post.ID, err.Error())
|
||||
return util.ErrServer(c)
|
||||
if err = db.NewsAdd(&news); err != nil {
|
||||
return util.ErrInternal(c, err)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/ngn13/website/api/database"
|
||||
"github.com/ngn13/website/api/util"
|
||||
@ -11,32 +9,29 @@ import (
|
||||
func GET_Services(c *fiber.Ctx) error {
|
||||
var (
|
||||
services []database.Service
|
||||
rows *sql.Rows
|
||||
db *sql.DB
|
||||
err error
|
||||
service database.Service
|
||||
)
|
||||
|
||||
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 {
|
||||
util.Fail("cannot load services: %s", err.Error())
|
||||
return util.ErrServer(c)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
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)
|
||||
if name != "" {
|
||||
if s, err := db.ServiceFind(name); err != nil {
|
||||
return util.ErrInternal(c, err)
|
||||
} else if s != nil {
|
||||
return util.JSON(c, 200, fiber.Map{
|
||||
"result": s,
|
||||
})
|
||||
}
|
||||
|
||||
return util.ErrNotExist(c)
|
||||
}
|
||||
|
||||
for db.ServiceNext(&service) {
|
||||
services = append(services, service)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"error": "",
|
||||
return util.JSON(c, 200, fiber.Map{
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
Info = log.New(os.Stdout, "\033[34m[info]\033[0m ", log.Ltime|log.Lshortfile).Printf
|
||||
Warn = log.New(os.Stderr, "\033[33m[warn]\033[0m ", log.Ltime|log.Lshortfile).Printf
|
||||
Fail = log.New(os.Stderr, "\033[31m[fail]\033[0m ", log.Ltime|log.Lshortfile).Printf
|
||||
const (
|
||||
COLOR_BLUE = "\033[34m"
|
||||
COLOR_YELLOW = "\033[33m"
|
||||
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