add projects and metrics routes
This commit is contained in:
parent
dee3ef4d85
commit
ac307de76c
335
admin/admin.py
335
admin/admin.py
@ -30,8 +30,6 @@ import requests as req
|
|||||||
from os import getenv
|
from os import getenv
|
||||||
from sys import argv
|
from sys import argv
|
||||||
|
|
||||||
API_URL_ENV = "API_URL"
|
|
||||||
|
|
||||||
|
|
||||||
# logger used by the script
|
# logger used by the script
|
||||||
class Log:
|
class Log:
|
||||||
@ -138,11 +136,32 @@ class AdminAPI:
|
|||||||
|
|
||||||
self.PUT("/v1/admin/service/add", service)
|
self.PUT("/v1/admin/service/add", service)
|
||||||
|
|
||||||
def del_service(self, service: str) -> None:
|
def del_service(self, name: str) -> None:
|
||||||
if service == "":
|
if name == "":
|
||||||
raise Exception("Service name cannot be empty")
|
raise Exception("Service name cannot be empty")
|
||||||
|
|
||||||
self.DELETE("/v1/admin/service/del?name=%s" % quote_plus(service))
|
self.DELETE("/v1/admin/service/del?name=%s" % quote_plus(name))
|
||||||
|
|
||||||
|
def add_project(self, project: Dict[str, str]):
|
||||||
|
if "name" not in project or project["name"] == "":
|
||||||
|
raise Exception('Project structure is missing required "name" field')
|
||||||
|
|
||||||
|
if "desc" not in project:
|
||||||
|
raise Exception('Project structure is missing required "desc" field')
|
||||||
|
|
||||||
|
if not self._check_multilang_field(project["desc"]):
|
||||||
|
raise Exception(
|
||||||
|
'Project structure field "desc" needs at least '
|
||||||
|
+ "one supported language entry"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.PUT("/v1/admin/project/add", project)
|
||||||
|
|
||||||
|
def del_project(self, name: str) -> None:
|
||||||
|
if name == "":
|
||||||
|
raise Exception("Project name cannot be empty")
|
||||||
|
|
||||||
|
self.DELETE("/v1/admin/project/del?name=%s" % quote_plus(name))
|
||||||
|
|
||||||
def check_services(self) -> None:
|
def check_services(self) -> None:
|
||||||
self.GET("/v1/admin/service/check")
|
self.GET("/v1/admin/service/check")
|
||||||
@ -174,184 +193,212 @@ class AdminAPI:
|
|||||||
|
|
||||||
self.PUT("/v1/admin/news/add", news)
|
self.PUT("/v1/admin/news/add", news)
|
||||||
|
|
||||||
def del_news(self, news: str) -> None:
|
def del_news(self, id: str) -> None:
|
||||||
if news == "":
|
if id == "":
|
||||||
raise Exception("News ID cannot be empty")
|
raise Exception("News ID cannot be empty")
|
||||||
|
|
||||||
self.DELETE("/v1/admin/news/del?id=%s" % quote_plus(news))
|
self.DELETE("/v1/admin/news/del?id=%s" % quote_plus(id))
|
||||||
|
|
||||||
def logs(self) -> List[Dict[str, Any]]:
|
def logs(self) -> List[Dict[str, Any]]:
|
||||||
return self.GET("/v1/admin/logs")
|
return self.GET("/v1/admin/logs")
|
||||||
|
|
||||||
|
|
||||||
# local helper functions used by the script
|
class AdminScript:
|
||||||
def __format_time(ts: int) -> str:
|
def __init__(self):
|
||||||
return datetime.fromtimestamp(ts, UTC).strftime("%H:%M:%S %d/%m/%Y")
|
self.log: Log = Log()
|
||||||
|
self.api: AdminAPI = None
|
||||||
|
self.commands = {
|
||||||
|
"add_service": self.add_service,
|
||||||
|
"del_service": self.del_service,
|
||||||
|
"add_project": self.add_project,
|
||||||
|
"del_project": self.del_project,
|
||||||
|
"add_news": self.add_news,
|
||||||
|
"del_news": self.del_news,
|
||||||
|
"check_services": self.check_services,
|
||||||
|
"logs": self.get_logs,
|
||||||
|
}
|
||||||
|
self.api_url_env = "API_URL"
|
||||||
|
|
||||||
|
def __format_time(self, ts: int) -> str:
|
||||||
|
return datetime.fromtimestamp(ts, UTC).strftime("%H:%M:%S %d/%m/%Y")
|
||||||
|
|
||||||
def __load_json_file(file: str) -> Dict[str, Any]:
|
def __load_json_file(self, file: str) -> Dict[str, Any]:
|
||||||
with open(file, "r") as f:
|
with open(file, "r") as f:
|
||||||
data = loads(f.read())
|
data = loads(f.read())
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def __dump_json_file(self, data: Dict[str, Any], file: str) -> None:
|
||||||
|
with open(file, "w") as f:
|
||||||
|
data = dumps(data, indent=2)
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
def __dump_json_file(data: Dict[str, Any], file: str) -> None:
|
def run(self) -> bool:
|
||||||
with open(file, "w") as f:
|
if len(argv) < 2 or len(argv) > 3:
|
||||||
data = dumps(data, indent=2)
|
self.log.error("Usage: %s [command] <file>" % argv[0])
|
||||||
f.write(data)
|
self.log.info("Here is a list of available commands:")
|
||||||
|
|
||||||
|
for command in self.commands.keys():
|
||||||
|
print("\t%s" % command)
|
||||||
|
|
||||||
# command handlers
|
return False
|
||||||
def __handle_command(log: Log, api: AdminAPI, cmd: str) -> None:
|
|
||||||
match cmd:
|
url = getenv(self.api_url_env)
|
||||||
case "add_service":
|
valid_cmd = False
|
||||||
|
|
||||||
|
if url is None:
|
||||||
|
self.log.error(
|
||||||
|
"Please specify the API URL using %s environment variable"
|
||||||
|
% self.api_url_env
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
for cmd in self.commands:
|
||||||
|
if argv[1] == cmd:
|
||||||
|
valid_cmd = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not valid_cmd:
|
||||||
|
self.log.error(
|
||||||
|
"Invalid command, run the script with no commands to list the available commands"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
password = self.log.password("Please enter the admin password")
|
||||||
|
self.api = AdminAPI(url, password)
|
||||||
|
|
||||||
|
if len(argv) == 2:
|
||||||
|
self.handle_command(argv[1])
|
||||||
|
|
||||||
|
elif len(argv) == 3:
|
||||||
|
self.handle_command(argv[1], argv[2])
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self.log.error("Command cancelled")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error("Command failed: %s" % e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# service commands
|
||||||
|
def add_service(self, data: Dict[str, Any] = None) -> None:
|
||||||
|
if data is None:
|
||||||
data: Dict[str, str] = {}
|
data: Dict[str, str] = {}
|
||||||
data["desc"] = {}
|
data["desc"] = {}
|
||||||
|
|
||||||
data["name"] = log.input("Serivce name")
|
data["name"] = self.log.input("Serivce name")
|
||||||
for lang in api.languages:
|
|
||||||
data["desc"][lang] = log.input("Serivce desc (%s)" % lang)
|
|
||||||
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)
|
for lang in self.api.languages:
|
||||||
log.info("Service has been added")
|
data["desc"][lang] = self.log.input("Serivce desc (%s)" % lang)
|
||||||
|
|
||||||
case "del_service":
|
data["check_url"] = self.log.input("Serivce status check URL")
|
||||||
api.del_service(log.input("Serivce name"))
|
data["clear"] = self.log.input("Serivce clearnet URL")
|
||||||
log.info("Service has been deleted")
|
data["onion"] = self.log.input("Serivce onion URL")
|
||||||
|
data["i2p"] = self.log.input("Serivce I2P URL")
|
||||||
|
|
||||||
case "check_services":
|
self.api.add_service(data)
|
||||||
api.check_services()
|
self.log.info("Service has been added")
|
||||||
log.info("Requested status check for all the services")
|
|
||||||
|
|
||||||
case "add_news":
|
def del_service(self, data: Dict[str, Any] = None) -> None:
|
||||||
|
if data is None:
|
||||||
|
data: Dict[str, str] = {}
|
||||||
|
data["name"] = self.log.input("Service name")
|
||||||
|
|
||||||
|
self.api.del_service(data["name"])
|
||||||
|
self.log.info("Service has been deleted")
|
||||||
|
|
||||||
|
# project commands
|
||||||
|
def add_project(self, data: Dict[str, Any] = None) -> None:
|
||||||
|
if data is None:
|
||||||
|
data: Dict[str, str] = {}
|
||||||
|
data["desc"] = {}
|
||||||
|
|
||||||
|
data["name"] = self.log.input("Project name")
|
||||||
|
|
||||||
|
for lang in self.api.languages:
|
||||||
|
data["desc"][lang] = self.log.input("Project desc (%s)" % lang)
|
||||||
|
|
||||||
|
data["url"] = self.log.input("Project URL")
|
||||||
|
data["license"] = self.log.input("Project license")
|
||||||
|
|
||||||
|
self.api.add_project(data)
|
||||||
|
self.log.info("Project has been added")
|
||||||
|
|
||||||
|
def del_project(self, data: Dict[str, Any] = None) -> None:
|
||||||
|
if data is None:
|
||||||
|
data: Dict[str, str] = {}
|
||||||
|
data["name"] = self.log.input("Project name")
|
||||||
|
|
||||||
|
self.api.del_project(data["name"])
|
||||||
|
self.log.info("Project has been deleted")
|
||||||
|
|
||||||
|
# news command
|
||||||
|
def add_news(self, data: Dict[str, Any] = None) -> None:
|
||||||
|
if data is None:
|
||||||
news: Dict[str, str] = {}
|
news: Dict[str, str] = {}
|
||||||
news["title"] = {}
|
news["title"] = {}
|
||||||
news["content"] = {}
|
news["content"] = {}
|
||||||
|
|
||||||
data["id"] = log.input("News ID")
|
data["id"] = self.log.input("News ID")
|
||||||
for lang in api.languages:
|
|
||||||
data["title"][lang] = log.input("News title (%s)" % lang)
|
|
||||||
data["author"] = log.input("News author")
|
|
||||||
for lang in api.languages:
|
|
||||||
data["content"][lang] = log.input("News content (%s)" % lang)
|
|
||||||
|
|
||||||
api.add_news(data)
|
for lang in self.api.languages:
|
||||||
log.info("News has been added")
|
data["title"][lang] = self.log.input("News title (%s)" % lang)
|
||||||
|
|
||||||
case "del_news":
|
data["author"] = self.log.input("News author")
|
||||||
api.del_news(log.input("News ID"))
|
|
||||||
log.info("News has been deleted")
|
|
||||||
|
|
||||||
case "logs":
|
for lang in self.api.languages:
|
||||||
logs = api.logs()
|
data["content"][lang] = self.log.input("News content (%s)" % lang)
|
||||||
|
|
||||||
if logs["result"] is None or len(logs["result"]) == 0:
|
self.api.add_news(data)
|
||||||
return log.info("No available logs")
|
self.log.info("News has been added")
|
||||||
|
|
||||||
for log in logs["result"]:
|
def del_news(self, data: Dict[str, Any] = None) -> None:
|
||||||
log.info(
|
if data is None:
|
||||||
"Time: %s | Action: %s"
|
data: Dict[str, str] = {}
|
||||||
% (__format_time(log["time"]), log["action"])
|
data["id"] = self.log.input("News ID")
|
||||||
)
|
|
||||||
|
|
||||||
|
self.api.del_project(data["id"])
|
||||||
|
self.log.info("News has been deleted")
|
||||||
|
|
||||||
def __handle_command_with_file(log: Log, api: AdminAPI, cmd: str, file: str) -> None:
|
def check_services(self, data: Dict[str, Any] = None) -> None:
|
||||||
match cmd:
|
self.api.check_services()
|
||||||
case "add_service":
|
self.log.info("Requested status check for all the services")
|
||||||
data = __load_json_file(file)
|
|
||||||
api.add_service(data)
|
|
||||||
log.info("Service has been added")
|
|
||||||
|
|
||||||
case "del_service":
|
def get_logs(self, data: Dict[str, Any] = None) -> None:
|
||||||
data = __load_json_file(file)
|
logs = self.api.logs()
|
||||||
api.del_service(data["name"])
|
|
||||||
log.info("Service has been deleted")
|
|
||||||
|
|
||||||
case "check_services":
|
if logs["result"] is None or len(logs["result"]) == 0:
|
||||||
api.check_services()
|
return self.log.info("No available logs")
|
||||||
log.info("Requested status check for all the services")
|
|
||||||
|
|
||||||
case "add_news":
|
for log in logs["result"]:
|
||||||
data = __load_json_file(file)
|
self.log.info(
|
||||||
api.add_news(data)
|
"Time: %s | Action: %s"
|
||||||
log.info("News has been added")
|
% (self.__format_time(log["time"]), log["action"])
|
||||||
|
)
|
||||||
|
|
||||||
case "del_news":
|
def handle_command(self, cmd: str, file: str = None) -> bool:
|
||||||
data = __load_json_file(file)
|
for command in self.commands.keys():
|
||||||
api.del_news(data["id"])
|
if command != cmd:
|
||||||
log.info("News has been deleted")
|
continue
|
||||||
|
|
||||||
case "logs":
|
data = None
|
||||||
logs = api.logs()
|
|
||||||
|
|
||||||
if logs["result"] is None or len(logs["result"]) == 0:
|
try:
|
||||||
return log.info("No available logs")
|
if file != "" and file is not None:
|
||||||
|
data = self.__load_json_file(file)
|
||||||
|
|
||||||
__dump_json_file(logs["result"], file)
|
self.commands[cmd](data)
|
||||||
log.info("Logs has been saved")
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error("Command failed: %s" % e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.log.error("Invalid command: %s", cmd)
|
||||||
|
return False
|
||||||
|
|
||||||
commands = [
|
|
||||||
"add_service",
|
|
||||||
"del_service",
|
|
||||||
"check_services",
|
|
||||||
"add_news",
|
|
||||||
"del_news",
|
|
||||||
"logs",
|
|
||||||
]
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
log = Log()
|
script = AdminScript()
|
||||||
|
exit(script.run() if 1 else 0)
|
||||||
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)
|
|
||||||
valid_cmd = False
|
|
||||||
|
|
||||||
for cmd in commands:
|
|
||||||
if argv[1] == cmd:
|
|
||||||
valid_cmd = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not valid_cmd:
|
|
||||||
log.error(
|
|
||||||
"Invalid command, run the script with no commands to list the available commands"
|
|
||||||
)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
if url is 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)
|
|
||||||
|
9
admin/tests/test_project.json
Normal file
9
admin/tests/test_project.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "test",
|
||||||
|
"desc": {
|
||||||
|
"en": "A non-existent project used to test the API",
|
||||||
|
"tr": "API'ı test etmek için kullanılan varolmayan bir proje"
|
||||||
|
},
|
||||||
|
"url": "https://github.com/ngn13/test",
|
||||||
|
"license": "GPL-3.0"
|
||||||
|
}
|
@ -24,8 +24,8 @@ func (db *Type) AdminLogNext(l *AdminLog) bool {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if nil == db.rows {
|
if nil == db.rows {
|
||||||
if db.rows, err = db.sql.Query("SELECT * FROM admin_log"); err != nil {
|
if db.rows, err = db.sql.Query("SELECT * FROM " + TABLE_ADMIN_LOG); err != nil {
|
||||||
util.Fail("failed to query admin_log table: %s", err.Error())
|
util.Fail("failed to query table: %s", err.Error())
|
||||||
goto fail
|
goto fail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ func (db *Type) AdminLogNext(l *AdminLog) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = l.Scan(db.rows); err != nil {
|
if err = l.Scan(db.rows); err != nil {
|
||||||
util.Fail("failed to scan the admin_log table: %s", err.Error())
|
util.Fail("failed to scan the table: %s", err.Error())
|
||||||
goto fail
|
goto fail
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ fail:
|
|||||||
|
|
||||||
func (db *Type) AdminLogAdd(l *AdminLog) error {
|
func (db *Type) AdminLogAdd(l *AdminLog) error {
|
||||||
_, err := db.sql.Exec(
|
_, err := db.sql.Exec(
|
||||||
`INSERT INTO admin_log(
|
"INSERT INTO "+TABLE_ADMIN_LOG+`(
|
||||||
action, time
|
action, time
|
||||||
) values(?, ?)`,
|
) values(?, ?)`,
|
||||||
&l.Action, &l.Time,
|
&l.Action, &l.Time,
|
@ -2,76 +2,62 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SQL_PATH = "sql"
|
||||||
|
|
||||||
|
TABLE_ADMIN_LOG = "admin_log" // stores administrator logs
|
||||||
|
TABLE_METRICS = "metrics" // stores API usage metrcis
|
||||||
|
TABLE_NEWS = "news" // stores news posts
|
||||||
|
TABLE_SERVICES = "services" // stores services
|
||||||
|
TABLE_PROJECTS = "projects" // stores projects
|
||||||
|
)
|
||||||
|
|
||||||
|
var tables []string = []string{
|
||||||
|
TABLE_ADMIN_LOG, TABLE_METRICS, TABLE_NEWS,
|
||||||
|
TABLE_SERVICES, TABLE_PROJECTS,
|
||||||
|
}
|
||||||
|
|
||||||
type Type struct {
|
type Type struct {
|
||||||
sql *sql.DB
|
sql *sql.DB
|
||||||
rows *sql.Rows
|
rows *sql.Rows
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Type) Load() (err error) {
|
func (db *Type) create_table(table string) error {
|
||||||
if db.sql, err = sql.Open("sqlite3", "data.db"); err != nil {
|
var (
|
||||||
return fmt.Errorf("cannot access the database: %s", err.Error())
|
err error
|
||||||
|
query []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
query_path := path.Join(SQL_PATH, table+".sql")
|
||||||
|
|
||||||
|
if query, err = os.ReadFile(query_path); err != nil {
|
||||||
|
return fmt.Errorf("failed to read %s for table %s: %", query_path, table, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// see database/visitor.go
|
if _, err = db.sql.Exec(string(query)); err != nil {
|
||||||
_, err = db.sql.Exec(`
|
return fmt.Errorf("failed to create the %s table: %s", table, err.Error())
|
||||||
CREATE TABLE IF NOT EXISTS visitor_count(
|
}
|
||||||
id TEXT NOT NULL UNIQUE,
|
|
||||||
count INTEGER NOT NULL
|
return nil
|
||||||
);
|
}
|
||||||
`)
|
|
||||||
|
func (db *Type) Load() (err error) {
|
||||||
if err != nil {
|
if db.sql, err = sql.Open("sqlite3", "data.db"); err != nil {
|
||||||
return fmt.Errorf("failed to create the visitor_count table: %s", err.Error())
|
return fmt.Errorf("failed access the database: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// see database/service.go
|
for _, table := range tables {
|
||||||
_, err = db.sql.Exec(`
|
if err = db.create_table(table); err != nil {
|
||||||
CREATE TABLE IF NOT EXISTS services(
|
return err
|
||||||
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
|
return nil
|
||||||
|
57
api/database/metrics.go
Normal file
57
api/database/metrics.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (db *Type) MetricsGet(key string) (uint64, error) {
|
||||||
|
var (
|
||||||
|
row *sql.Row
|
||||||
|
count uint64
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if row = db.sql.QueryRow("SELECT value FROM "+TABLE_METRICS+" WHERE key = ?", key); row == nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = row.Scan(&count); err != nil && err != sql.ErrNoRows {
|
||||||
|
util.Fail("failed to scan the table: %s", err.Error())
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) MetricsSet(key string, value uint64) error {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
res sql.Result
|
||||||
|
)
|
||||||
|
|
||||||
|
if res, err = db.sql.Exec("UPDATE "+TABLE_METRICS+" SET value = ? WHERE key = ?", value, key); err != nil && err != sql.ErrNoRows {
|
||||||
|
util.Fail("failed to query table: %s", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if effected, err := res.RowsAffected(); err != nil {
|
||||||
|
return err
|
||||||
|
} else if effected < 1 {
|
||||||
|
_, err = db.sql.Exec(
|
||||||
|
`INSERT INTO "+TABLE_METRICS+"(
|
||||||
|
key, value
|
||||||
|
) values(?, ?)`,
|
||||||
|
key, value,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -64,8 +64,8 @@ func (db *Type) NewsNext(n *News) bool {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if nil == db.rows {
|
if nil == db.rows {
|
||||||
if db.rows, err = db.sql.Query("SELECT * FROM news"); err != nil {
|
if db.rows, err = db.sql.Query("SELECT * FROM " + TABLE_NEWS); err != nil {
|
||||||
util.Fail("failed to query news table: %s", err.Error())
|
util.Fail("failed to query table: %s", err.Error())
|
||||||
goto fail
|
goto fail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,7 +75,7 @@ func (db *Type) NewsNext(n *News) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = n.Scan(db.rows); err != nil {
|
if err = n.Scan(db.rows); err != nil {
|
||||||
util.Fail("failed to scan the news table: %s", err.Error())
|
util.Fail("failed to scan the table: %s", err.Error())
|
||||||
goto fail
|
goto fail
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ fail:
|
|||||||
|
|
||||||
func (db *Type) NewsRemove(id string) error {
|
func (db *Type) NewsRemove(id string) error {
|
||||||
_, err := db.sql.Exec(
|
_, err := db.sql.Exec(
|
||||||
"DELETE FROM news WHERE id = ?",
|
"DELETE FROM "+TABLE_NEWS+" WHERE id = ?",
|
||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -105,7 +105,7 @@ func (db *Type) NewsAdd(n *News) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.sql.Exec(
|
_, err = db.sql.Exec(
|
||||||
`INSERT OR REPLACE INTO news(
|
"INSERT OR REPLACE INTO "+TABLE_NEWS+`(
|
||||||
id, title, author, time, content
|
id, title, author, time, content
|
||||||
) values(?, ?, ?, ?, ?)`,
|
) values(?, ?, ?, ?, ?)`,
|
||||||
n.ID, n.title,
|
n.ID, n.title,
|
||||||
|
92
api/database/project.go
Normal file
92
api/database/project.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Project struct {
|
||||||
|
Name string `json:"name"` // name of the project
|
||||||
|
desc string `json:"-"` // description of the project (string)
|
||||||
|
Desc Multilang `json:"desc"` // description of the project
|
||||||
|
URL string `json:"url"` // URL of the project's homepage/source
|
||||||
|
License string `json:"license"` // name of project's license
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Project) Load() error {
|
||||||
|
return p.Desc.Load(p.desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Project) Dump() (err error) {
|
||||||
|
p.desc, err = p.Desc.Dump()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Project) Scan(rows *sql.Rows) (err error) {
|
||||||
|
if err = rows.Scan(
|
||||||
|
&p.Name, &p.desc,
|
||||||
|
&p.URL, &p.License); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Project) IsValid() bool {
|
||||||
|
return p.Name != "" && p.URL != "" && !p.Desc.Empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) ProjectNext(p *Project) bool {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if nil == db.rows {
|
||||||
|
if db.rows, err = db.sql.Query("SELECT * FROM " + TABLE_PROJECTS); err != nil {
|
||||||
|
util.Fail("failed to query table: %s", err.Error())
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !db.rows.Next() {
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = p.Scan(db.rows); err != nil {
|
||||||
|
util.Fail("failed to scan the table: %s", err.Error())
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
fail:
|
||||||
|
if db.rows != nil {
|
||||||
|
db.rows.Close()
|
||||||
|
}
|
||||||
|
db.rows = nil
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) ProjectRemove(name string) error {
|
||||||
|
_, err := db.sql.Exec(
|
||||||
|
"DELETE FROM "+TABLE_PROJECTS+" WHERE name = ?",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) ProjectAdd(p *Project) (err error) {
|
||||||
|
if err = p.Dump(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.sql.Exec(
|
||||||
|
"INSERT OR REPLACE INTO "+TABLE_PROJECTS+`(
|
||||||
|
name, desc, url, license
|
||||||
|
) values(?, ?, ?, ?)`,
|
||||||
|
p.Name, p.desc, p.URL, p.License,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
@ -58,8 +58,8 @@ func (db *Type) ServiceNext(s *Service) bool {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if nil == db.rows {
|
if nil == db.rows {
|
||||||
if db.rows, err = db.sql.Query("SELECT * FROM services"); err != nil {
|
if db.rows, err = db.sql.Query("SELECT * FROM " + TABLE_SERVICES); err != nil {
|
||||||
util.Fail("failed to query services table: %s", err.Error())
|
util.Fail("failed to query table: %s", err.Error())
|
||||||
goto fail
|
goto fail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,7 +69,7 @@ func (db *Type) ServiceNext(s *Service) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = s.Scan(db.rows, nil); err != nil {
|
if err = s.Scan(db.rows, nil); err != nil {
|
||||||
util.Fail("failed to scan the services table: %s", err.Error())
|
util.Fail("failed to scan the table: %s", err.Error())
|
||||||
goto fail
|
goto fail
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,7 +91,7 @@ func (db *Type) ServiceFind(name string) (*Service, error) {
|
|||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if row = db.sql.QueryRow("SELECT * FROM services WHERE name = ?", name); row == nil || row.Err() == sql.ErrNoRows {
|
if row = db.sql.QueryRow("SELECT * FROM "+TABLE_SERVICES+" WHERE name = ?", name); row == nil || row.Err() == sql.ErrNoRows {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +104,7 @@ func (db *Type) ServiceFind(name string) (*Service, error) {
|
|||||||
|
|
||||||
func (db *Type) ServiceRemove(name string) error {
|
func (db *Type) ServiceRemove(name string) error {
|
||||||
_, err := db.sql.Exec(
|
_, err := db.sql.Exec(
|
||||||
"DELETE FROM services WHERE name = ?",
|
"DELETE FROM "+TABLE_SERVICES+" WHERE name = ?",
|
||||||
name,
|
name,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ func (db *Type) ServiceUpdate(s *Service) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.sql.Exec(
|
_, err = db.sql.Exec(
|
||||||
`INSERT OR REPLACE INTO services(
|
"INSERT OR REPLACE INTO "+TABLE_SERVICES+`(
|
||||||
name, desc, check_time, check_res, check_url, clear, onion, i2p
|
name, desc, check_time, check_res, check_url, clear, onion, i2p
|
||||||
) values(?, ?, ?, ?, ?, ?, ?, ?)`,
|
) values(?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
s.Name, s.desc,
|
s.Name, s.desc,
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (db *Type) VisitorGet() (uint64, error) {
|
|
||||||
var (
|
|
||||||
row *sql.Row
|
|
||||||
count uint64
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if row = db.sql.QueryRow("SELECT count FROM visitor_count WHERE id = 0"); row == nil {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = row.Scan(&count); err != nil && err != sql.ErrNoRows {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return count, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *Type) VisitorIncrement() (err error) {
|
|
||||||
if _, err = db.sql.Exec("UPDATE visitor_count SET count = count + 1 WHERE id = 0"); err != nil && err != sql.ErrNoRows {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: err is always nil even if there is no rows for some reason, check sql.Result instead
|
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
_, err = db.sql.Exec(
|
|
||||||
`INSERT INTO visitor_count(
|
|
||||||
id, count
|
|
||||||
) values(?, ?)`,
|
|
||||||
0, 0,
|
|
||||||
)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -89,15 +89,21 @@ func main() {
|
|||||||
|
|
||||||
// v1 user routes
|
// v1 user routes
|
||||||
v1.Get("/services", routes.GET_Services)
|
v1.Get("/services", routes.GET_Services)
|
||||||
v1.Get("/visitor", routes.GET_Visitor)
|
v1.Get("/projects", routes.GET_Projects)
|
||||||
|
v1.Get("/metrics", routes.GET_Metrics)
|
||||||
v1.Get("/news/:lang", routes.GET_News)
|
v1.Get("/news/:lang", routes.GET_News)
|
||||||
|
|
||||||
// v1 admin routes
|
// v1 admin routes
|
||||||
v1.Use("/admin", routes.AuthMiddleware)
|
v1.Use("/admin", routes.AuthMiddleware)
|
||||||
v1.Get("/admin/logs", routes.GET_AdminLogs)
|
v1.Get("/admin/logs", routes.GET_AdminLogs)
|
||||||
|
|
||||||
v1.Get("/admin/service/check", routes.GET_CheckService)
|
v1.Get("/admin/service/check", routes.GET_CheckService)
|
||||||
v1.Put("/admin/service/add", routes.PUT_AddService)
|
v1.Put("/admin/service/add", routes.PUT_AddService)
|
||||||
v1.Delete("/admin/service/del", routes.DEL_DelService)
|
v1.Delete("/admin/service/del", routes.DEL_DelService)
|
||||||
|
|
||||||
|
v1.Put("/admin/project/add", routes.PUT_AddProject)
|
||||||
|
v1.Delete("/admin/project/del", routes.DEL_DelProject)
|
||||||
|
|
||||||
v1.Put("/admin/news/add", routes.PUT_AddNews)
|
v1.Put("/admin/news/add", routes.PUT_AddNews)
|
||||||
v1.Delete("/admin/news/del", routes.DEL_DelNews)
|
v1.Delete("/admin/news/del", routes.DEL_DelNews)
|
||||||
|
|
||||||
|
@ -103,6 +103,56 @@ func GET_CheckService(c *fiber.Ctx) error {
|
|||||||
return util.JSON(c, 200, nil)
|
return util.JSON(c, 200, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PUT_AddProject(c *fiber.Ctx) error {
|
||||||
|
var (
|
||||||
|
project database.Project
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
db := c.Locals("database").(*database.Type)
|
||||||
|
|
||||||
|
if c.BodyParser(&project) != nil {
|
||||||
|
return util.ErrBadJSON(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !project.IsValid() {
|
||||||
|
return util.ErrBadReq(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = admin_log(c, fmt.Sprintf("Added project \"%s\"", project.Name)); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.ProjectAdd(&project); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSON(c, 200, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DEL_DelProject(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 err = admin_log(c, fmt.Sprintf("Removed project \"%s\"", name)); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.ProjectRemove(name); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSON(c, 200, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func DEL_DelNews(c *fiber.Ctx) error {
|
func DEL_DelNews(c *fiber.Ctx) error {
|
||||||
var (
|
var (
|
||||||
id string
|
id string
|
||||||
|
77
api/routes/metrics.go
Normal file
77
api/routes/metrics.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/ngn13/website/api/database"
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type visitor_cache_entry struct {
|
||||||
|
Addr string // SHA1 hash of visitor's IP
|
||||||
|
Number uint64 // number of the visitor
|
||||||
|
}
|
||||||
|
|
||||||
|
const VISITOR_CACHE_MAX = 30 // store 30 visitor data at most
|
||||||
|
var visitor_cache []visitor_cache_entry // in memory cache for the visitor
|
||||||
|
|
||||||
|
func GET_Metrics(c *fiber.Ctx) error {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
result map[string]uint64 = map[string]uint64{
|
||||||
|
"number": 0, // visitor number of the current visitor
|
||||||
|
"total": 0, // total number of visitors
|
||||||
|
"since": 0, // metric collection start date (UNIX timestamp)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
db := c.Locals("database").(*database.Type)
|
||||||
|
new_addr := util.GetSHA1(util.IP(c))
|
||||||
|
|
||||||
|
for i := range visitor_cache {
|
||||||
|
if new_addr == visitor_cache[i].Addr {
|
||||||
|
result["number"] = visitor_cache[i].Number
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result["total"], err = db.MetricsGet("visitor_count"); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result["number"] == 0 {
|
||||||
|
result["total"]++
|
||||||
|
result["number"] = result["total"]
|
||||||
|
|
||||||
|
if len(visitor_cache) > VISITOR_CACHE_MAX {
|
||||||
|
util.Debg("visitor cache is full, removing the oldest entry")
|
||||||
|
visitor_cache = visitor_cache[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
visitor_cache = append(visitor_cache, visitor_cache_entry{
|
||||||
|
Addr: new_addr,
|
||||||
|
Number: result["number"],
|
||||||
|
})
|
||||||
|
|
||||||
|
if err = db.MetricsSet("visitor_count", result["total"]); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result["since"], err = db.MetricsGet("start_date"); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result["since"] == 0 {
|
||||||
|
result["since"] = uint64(time.Now().Truncate(24 * time.Hour).Unix())
|
||||||
|
|
||||||
|
if err = db.MetricsSet("since", result["since"]); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSON(c, 200, fiber.Map{
|
||||||
|
"result": result,
|
||||||
|
})
|
||||||
|
}
|
24
api/routes/projects.go
Normal file
24
api/routes/projects.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/ngn13/website/api/database"
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GET_Projects(c *fiber.Ctx) error {
|
||||||
|
var (
|
||||||
|
projects []database.Project
|
||||||
|
project database.Project
|
||||||
|
)
|
||||||
|
|
||||||
|
db := c.Locals("database").(*database.Type)
|
||||||
|
|
||||||
|
for db.ProjectNext(&project) {
|
||||||
|
projects = append(projects, project)
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSON(c, 200, fiber.Map{
|
||||||
|
"result": projects,
|
||||||
|
})
|
||||||
|
}
|
@ -1,50 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/ngn13/website/api/database"
|
|
||||||
"github.com/ngn13/website/api/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
const LAST_ADDRS_MAX = 30
|
|
||||||
|
|
||||||
var last_addrs []string
|
|
||||||
|
|
||||||
func GET_Visitor(c *fiber.Ctx) error {
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
count uint64
|
|
||||||
)
|
|
||||||
|
|
||||||
db := c.Locals("database").(*database.Type)
|
|
||||||
new_addr := util.GetSHA1(util.IP(c))
|
|
||||||
|
|
||||||
for _, addr := range last_addrs {
|
|
||||||
if new_addr == addr {
|
|
||||||
if count, err = db.VisitorGet(); err != nil {
|
|
||||||
return util.ErrInternal(c, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.JSON(c, 200, fiber.Map{
|
|
||||||
"result": count,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = db.VisitorIncrement(); err != nil {
|
|
||||||
return util.ErrInternal(c, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if count, err = db.VisitorGet(); err != nil {
|
|
||||||
return util.ErrInternal(c, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(last_addrs) > LAST_ADDRS_MAX {
|
|
||||||
last_addrs = append(last_addrs[:0], last_addrs[1:]...)
|
|
||||||
last_addrs = append(last_addrs, new_addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.JSON(c, 200, fiber.Map{
|
|
||||||
"result": count,
|
|
||||||
})
|
|
||||||
}
|
|
4
api/sql/admin_log.sql
Normal file
4
api/sql/admin_log.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS admin_log(
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
time INTEGER NOT NULL
|
||||||
|
);
|
4
api/sql/metrics.sql
Normal file
4
api/sql/metrics.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS metrics(
|
||||||
|
key TEXT NOT NULL UNIQUE,
|
||||||
|
value INTEGER NOT NULL
|
||||||
|
);
|
7
api/sql/news.sql
Normal file
7
api/sql/news.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
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
|
||||||
|
);
|
6
api/sql/projects.sql
Normal file
6
api/sql/projects.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS projects(
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
desc TEXT NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
license TEXT
|
||||||
|
);
|
10
api/sql/services.sql
Normal file
10
api/sql/services.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
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
|
||||||
|
);
|
@ -97,6 +97,23 @@ a URL query named "name".
|
|||||||
Returns a Atom feed of news for the given language. Supports languages that are supported
|
Returns a Atom feed of news for the given language. Supports languages that are supported
|
||||||
by Multilang.
|
by Multilang.
|
||||||
|
|
||||||
|
### GET /v1/metrics
|
||||||
|
Returns metrics about the API usage. The metric data has the following format:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"number":8,
|
||||||
|
"since":1736294400,
|
||||||
|
"total":8
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Where:
|
||||||
|
- `number`: Visitor number of the the current visitor (integer)
|
||||||
|
- `since`: Metric collection start date (integer, UNIX timestamp)
|
||||||
|
- `total`: Total number of visitors (integer)
|
||||||
|
|
||||||
|
Note that visitor number may change after a certain amount of requests by other clients,
|
||||||
|
if the client wants to preserve it's visitor number, it should save it somewhere.
|
||||||
|
|
||||||
### GET /v1/admin/logs
|
### GET /v1/admin/logs
|
||||||
Returns a list of administrator logs. Each log has the following JSON format:
|
Returns a list of administrator logs. Each log has the following JSON format:
|
||||||
```
|
```
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import { locale } from "svelte-i18n";
|
|
||||||
|
|
||||||
export const handle = async ({ event, resolve }) => {
|
|
||||||
const lang = event.request.headers.get("accept-language")?.split(",")[0];
|
|
||||||
if (lang) locale.set(lang);
|
|
||||||
return resolve(event);
|
|
||||||
};
|
|
@ -22,12 +22,16 @@ async function GET(fetch, url) {
|
|||||||
return json["result"];
|
return json["result"];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function visitor(fetch) {
|
async function get_metrics(fetch) {
|
||||||
return GET(fetch, api_url("/visitor"));
|
return GET(fetch, api_url("/metrics"));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function services(fetch) {
|
async function get_services(fetch) {
|
||||||
return GET(fetch, api_url("/services"));
|
return GET(fetch, api_url("/services"));
|
||||||
}
|
}
|
||||||
|
|
||||||
export { version, api_url, visitor, services };
|
async function get_projects(fetch) {
|
||||||
|
return GET(fetch, api_url("/projects"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export { version, api_url, get_metrics, get_services, get_projects };
|
||||||
|
@ -1,74 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { click } from "$lib/util.js";
|
|
||||||
export let title;
|
|
||||||
export let url;
|
|
||||||
|
|
||||||
let current = "";
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
while (title.length > i) {
|
|
||||||
let c = title[i];
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
current += c;
|
|
||||||
},
|
|
||||||
100 * (i + 1)
|
|
||||||
);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<a on:click={click} data-sveltekit-preload-data href={url}>
|
|
||||||
<div class="title">
|
|
||||||
{current}
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
a {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
background: var(--dark-three);
|
|
||||||
box-shadow: var(--box-shadow);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: 0.4s;
|
|
||||||
text-decoration: none;
|
|
||||||
border: solid 1px var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover > .title {
|
|
||||||
text-shadow: var(--text-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
border: solid 1px var(--dark-two);
|
|
||||||
background: var(--dark-two);
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: 7px 7px 0px 0px;
|
|
||||||
font-size: 20px;
|
|
||||||
font-family:
|
|
||||||
Consolas,
|
|
||||||
Monaco,
|
|
||||||
Lucida Console,
|
|
||||||
Liberation Mono,
|
|
||||||
DejaVu Sans Mono,
|
|
||||||
Bitstream Vera Sans Mono,
|
|
||||||
Courier New,
|
|
||||||
monospace;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
background: var(--dark-three);
|
|
||||||
padding: 30px;
|
|
||||||
padding-top: 30px;
|
|
||||||
color: white;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 25px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,14 +1,15 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { color, date_from_ts } from "$lib/util.js";
|
||||||
|
import { get_metrics } from "$lib/api.js";
|
||||||
import Link from "$lib/link.svelte";
|
import Link from "$lib/link.svelte";
|
||||||
|
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { visitor } from "$lib/api.js";
|
|
||||||
import { color } from "$lib/util.js";
|
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
let visitor_count = 0;
|
let data = {};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
visitor_count = await visitor(fetch);
|
data = await get_metrics(fetch);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -33,8 +34,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="useless">
|
<div class="useless">
|
||||||
<span>
|
<span>
|
||||||
{$_("footer.number", { values: { count: visitor_count } })}
|
{$_("footer.number", {
|
||||||
{#if visitor_count % 1000 == 0}
|
values: {
|
||||||
|
number: data.number,
|
||||||
|
since: date_from_ts(data.since),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
{#if data.number % 1000 == 0}
|
||||||
<span style="color: var(--{color()})">({$_("footer.congrat")})</span>
|
<span style="color: var(--{color()})">({$_("footer.congrat")})</span>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { locale } from "svelte-i18n";
|
import { language, set_lang } from "$lib/util.js";
|
||||||
import languages from "$lib/lang.js";
|
import languages from "$lib/lang.js";
|
||||||
|
|
||||||
let icon = "",
|
let icon = "",
|
||||||
indx = 0,
|
indx = 0,
|
||||||
len = languages.length;
|
len = languages.length;
|
||||||
@ -9,18 +10,20 @@
|
|||||||
if (indx >= languages.length) indx = 0;
|
if (indx >= languages.length) indx = 0;
|
||||||
|
|
||||||
icon = languages[indx].icon;
|
icon = languages[indx].icon;
|
||||||
locale.set(languages[indx++].code);
|
set_lang(languages[indx++].code);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (indx = 0; indx < len; indx++) {
|
for (indx = 0; indx < len; indx++) {
|
||||||
if (languages[indx].code == $locale.slice(0, 2)) {
|
if (languages[indx].code == $language) {
|
||||||
icon = languages[indx++].icon;
|
icon = languages[indx++].icon;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button on:click={next}>{icon}</button>
|
<button on:click={next}>
|
||||||
|
{icon}
|
||||||
|
</button>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
button {
|
button {
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
import Icon from "$lib/icon.svelte";
|
import Icon from "$lib/icon.svelte";
|
||||||
import Link from "$lib/link.svelte";
|
import Link from "$lib/link.svelte";
|
||||||
|
|
||||||
import { color, time_from_ts } from "$lib/util.js";
|
import { color, time_from_ts, language } from "$lib/util.js";
|
||||||
import { _, locale } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
export let service = {};
|
export let service = {};
|
||||||
let style = "";
|
let style = "";
|
||||||
@ -15,7 +15,7 @@
|
|||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<h1>{service.name}</h1>
|
<h1>{service.name}</h1>
|
||||||
<p>{service.desc[$locale.slice(0, 2)]}</p>
|
<p>{service.desc[$language]}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<Link highlight={false} link={service.clear}><Icon icon="nf-oct-link" /></Link>
|
<Link highlight={false} link={service.clear}><Icon icon="nf-oct-link" /></Link>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
|
import { locale } from "svelte-i18n";
|
||||||
|
import languages from "$lib/lang.js";
|
||||||
|
import { writable, get } from "svelte/store";
|
||||||
|
|
||||||
const default_lang = "en";
|
const default_language = languages[0].code;
|
||||||
const colors = [
|
const colors = [
|
||||||
"yellow",
|
"yellow",
|
||||||
"cyan",
|
"cyan",
|
||||||
@ -10,8 +13,33 @@ const colors = [
|
|||||||
// "blue" (looks kinda ass)
|
// "blue" (looks kinda ass)
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let language = writable(default_language);
|
||||||
let colors_pos = -1;
|
let colors_pos = -1;
|
||||||
|
|
||||||
|
function browser_lang() {
|
||||||
|
if (browser) return window.navigator.language.slice(0, 2).toLowerCase();
|
||||||
|
else return get(language);
|
||||||
|
}
|
||||||
|
|
||||||
|
function set_lang(lang) {
|
||||||
|
language.set(default_language);
|
||||||
|
|
||||||
|
if (lang === null || lang === undefined) {
|
||||||
|
if (browser) set_lang(browser_lang());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lang = lang.slice(0, 2);
|
||||||
|
|
||||||
|
for (let i = 0; i < languages.length; i++) {
|
||||||
|
if (lang === languages[i].code) {
|
||||||
|
language.set(lang);
|
||||||
|
locale.set(lang);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function urljoin(url, path = null, query = {}) {
|
function urljoin(url, path = null, query = {}) {
|
||||||
let url_len = url.length;
|
let url_len = url.length;
|
||||||
|
|
||||||
@ -42,13 +70,40 @@ function click() {
|
|||||||
audio.play();
|
audio.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
function browser_lang() {
|
|
||||||
if (browser) return window.navigator.language.slice(0, 2).toLowerCase();
|
|
||||||
return default_lang;
|
|
||||||
}
|
|
||||||
|
|
||||||
function time_from_ts(ts) {
|
function time_from_ts(ts) {
|
||||||
return new Date(ts * 1000).toLocaleTimeString();
|
if (ts === 0 || ts === undefined) return;
|
||||||
|
|
||||||
|
let ts_date = new Date(ts * 1000);
|
||||||
|
let ts_zone = ts_date.toString().match(/([A-Z]+[\+-][0-9]+)/)[1];
|
||||||
|
|
||||||
|
return (
|
||||||
|
new Intl.DateTimeFormat(browser_lang(), {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
}).format(ts_date) + ` (${ts_zone})`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { urljoin, frontend_url, browser_lang, click, color, time_from_ts };
|
function date_from_ts(ts) {
|
||||||
|
if (ts === 0 || ts === undefined) return;
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat(browser_lang(), {
|
||||||
|
month: "2-digit",
|
||||||
|
year: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
}).format(new Date(ts * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
default_language,
|
||||||
|
browser_lang,
|
||||||
|
language,
|
||||||
|
set_lang,
|
||||||
|
urljoin,
|
||||||
|
frontend_url,
|
||||||
|
click,
|
||||||
|
color,
|
||||||
|
time_from_ts,
|
||||||
|
date_from_ts,
|
||||||
|
};
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"title": "work",
|
"title": "work",
|
||||||
"desc": "I don't currently have a job, so I spend most of my time...",
|
"desc": "I don't currently have a job, so I spend most of my time...",
|
||||||
"build": "building stupid shit",
|
"build": "building stupid shit",
|
||||||
|
"fix": "fixing stupid shit",
|
||||||
"ctf": "solving CTF challenges",
|
"ctf": "solving CTF challenges",
|
||||||
"contribute": "contributing to random projects",
|
"contribute": "contributing to random projects",
|
||||||
"wiki": "expanding my wiki"
|
"wiki": "expanding my wiki"
|
||||||
@ -25,16 +26,20 @@
|
|||||||
"links": {
|
"links": {
|
||||||
"title": "contact",
|
"title": "contact",
|
||||||
"desc": "Here are some useful links if you want to get in contact with me",
|
"desc": "Here are some useful links if you want to get in contact with me",
|
||||||
"prefer": "preferred"
|
"prefer": "I highly prefer email, you can send encrypted emails using my PGP key"
|
||||||
},
|
},
|
||||||
"info": {
|
"services": {
|
||||||
"title": "services",
|
"title": "services",
|
||||||
"desc": "A part from working on stupid shit, I host free (as in freedom, and price) services available for all",
|
"desc": "A part from working on stupid shit, I host free (as in freedom, and price) services available for all",
|
||||||
"speed": "All of these services are available over a 600 Mbit/s interface",
|
"speed": "All of these services are available over a 600 Mbit/s interface",
|
||||||
"security": "All use SSL encrypted connection and they are all privacy-respecting",
|
"security": "All use SSL encrypted connection and they are all privacy-respecting",
|
||||||
"privacy": "Accessible from clearnet, TOR and I2P, no region or network blocks",
|
"privacy": "Accessible from clearnet, TOR and I2P, no region or network blocks",
|
||||||
"bullshit": "No CDNs, no cloudflare, no CAPTCHA, no analytics, no bullshit",
|
"bullshit": "No CDNs, no cloudflare, no CAPTCHA, no analytics, no bullshit",
|
||||||
"link": "see all the services"
|
"link": "Check them out!"
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"title": "projects",
|
||||||
|
"desc": "I mostly work on free software projects, here are some of projects that you might find interesting"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
@ -68,7 +73,7 @@
|
|||||||
"license": "License",
|
"license": "License",
|
||||||
"privacy": "Privacy",
|
"privacy": "Privacy",
|
||||||
"powered": "Powered by Svelte, Go, SQLite and donations",
|
"powered": "Powered by Svelte, Go, SQLite and donations",
|
||||||
"number": "You are the visitor number {count}",
|
"number": "You are the visitor number {number} since {since}",
|
||||||
"congrat": "congrats!!",
|
"congrat": "congrats!!",
|
||||||
"version": "Using API version {api_version}, frontend version {frontend_version}"
|
"version": "Using API version {api_version}, frontend version {frontend_version}"
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
"license": "Lisans",
|
"license": "Lisans",
|
||||||
"privacy": "Gizlilik",
|
"privacy": "Gizlilik",
|
||||||
"powered": "Svelte, Go, SQLite ve yemek param tarafından destekleniyor",
|
"powered": "Svelte, Go, SQLite ve yemek param tarafından destekleniyor",
|
||||||
"number": "{count}. ziyaretçisiniz",
|
"number": "{since} tarihinden beri {number}. ziyaretçisiniz",
|
||||||
"congrat": "tebrikler!!",
|
"congrat": "tebrikler!!",
|
||||||
"version": "Kullan API versiyonu {api_version}, arayüz versiyonu {frontend_version}"
|
"version": "Kullan API versiyonu {api_version}, arayüz versiyonu {frontend_version}"
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
import { init, register, waitLocale } from "svelte-i18n";
|
import { default_language, language, set_lang } from "$lib/util.js";
|
||||||
import { browser_lang } from "$lib/util.js";
|
import { get_services, get_projects } from "$lib/api.js";
|
||||||
import { services } from "$lib/api.js";
|
|
||||||
import languages from "$lib/lang.js";
|
import languages from "$lib/lang.js";
|
||||||
|
|
||||||
|
import { init, register, waitLocale } from "svelte-i18n";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
// setup the locale
|
// setup the locale
|
||||||
for (let i = 0; i < languages.length; i++)
|
for (let i = 0; i < languages.length; i++)
|
||||||
register(languages[i].code, () => import(/* @vite-ignore */ languages[i].path));
|
register(languages[i].code, () => import(/* @vite-ignore */ languages[i].path));
|
||||||
|
|
||||||
|
// set the language
|
||||||
|
set_lang();
|
||||||
|
|
||||||
init({
|
init({
|
||||||
fallbackLocale: languages[0].code,
|
fallbackLocale: default_language,
|
||||||
initialLocale: browser_lang(),
|
initialLocale: get(language),
|
||||||
});
|
});
|
||||||
|
|
||||||
// load locales & load data from the API
|
// load locales & load data from the API
|
||||||
@ -18,7 +23,8 @@ export async function load({ fetch }) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return {
|
return {
|
||||||
services: await services(fetch),
|
services: await get_services(fetch),
|
||||||
|
projects: await get_projects(fetch),
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -4,8 +4,11 @@
|
|||||||
import Card from "$lib/card.svelte";
|
import Card from "$lib/card.svelte";
|
||||||
import Link from "$lib/link.svelte";
|
import Link from "$lib/link.svelte";
|
||||||
|
|
||||||
import { color } from "$lib/util.js";
|
import { color, language } from "$lib/util.js";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
|
const { data } = $props();
|
||||||
|
let projects = $state(data.projects);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Head title="home" desc="home page of my personal website" />
|
<Head title="home" desc="home page of my personal website" />
|
||||||
@ -24,6 +27,7 @@
|
|||||||
<span>{$_("home.work.desc")}</span>
|
<span>{$_("home.work.desc")}</span>
|
||||||
<ul>
|
<ul>
|
||||||
<li>⌨️ {$_("home.work.build")}</li>
|
<li>⌨️ {$_("home.work.build")}</li>
|
||||||
|
<li>🤦 {$_("home.work.fix")}</li>
|
||||||
<li>🚩 {$_("home.work.ctf")}</li>
|
<li>🚩 {$_("home.work.ctf")}</li>
|
||||||
<li>👥 {$_("home.work.contribute")}</li>
|
<li>👥 {$_("home.work.contribute")}</li>
|
||||||
<li>📑 {$_("home.work.wiki")}</li>
|
<li>📑 {$_("home.work.wiki")}</li>
|
||||||
@ -37,44 +41,53 @@
|
|||||||
PGP
|
PGP
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link icon="nf-md-email" link="mailto:ngn@ngn.tf">Email</Link>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link icon="nf-md-mastodon" link="https://defcon.social/@ngn">Mastodon</Link>
|
<Link icon="nf-md-mastodon" link="https://defcon.social/@ngn">Mastodon</Link>
|
||||||
</li>
|
</li>
|
||||||
|
</ul>
|
||||||
|
<span>
|
||||||
|
{$_("home.links.prefer")}
|
||||||
|
</span>
|
||||||
|
</Card>
|
||||||
|
<Card title={$_("home.services.title")}>
|
||||||
|
<span>
|
||||||
|
{$_("home.services.desc")}
|
||||||
|
</span>
|
||||||
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<Link icon="nf-cod-github" link="https://github.com/ngn13">Github</Link>
|
<i style="color: var(--{color()});" class="nf nf-md-speedometer_slow"></i>
|
||||||
|
{$_("home.services.speed")}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link icon="nf-md-email" link="mailto:ngn@ngn.tf">Email</Link>
|
<i style="color: var(--{color()});" class="nf nf-fa-lock"></i>
|
||||||
<span class="prefer">({$_("home.links.prefer")})</span>
|
{$_("home.services.security")}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i style="color: var(--{color()});" class="nf nf-fa-network_wired"></i>
|
||||||
|
{$_("home.services.privacy")}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i style="color: var(--{color()});" class="nf nf-md-eye_off"></i>
|
||||||
|
{$_("home.services.bullshit")}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<Link linK="/services">{$_("home.services.link")}</Link>
|
||||||
</Card>
|
</Card>
|
||||||
<Card title={$_("home.info.title")}>
|
<Card title={$_("home.projects.title")}>
|
||||||
<div class="services">
|
<span>
|
||||||
<div class="info">
|
{$_("home.projects.desc")}:
|
||||||
<span>
|
</span>
|
||||||
{$_("home.info.desc")}
|
<ul>
|
||||||
</span>
|
{#each projects as project}
|
||||||
<ul>
|
<li>
|
||||||
<li>
|
<Link active={true} link={project.url}>{project.name}</Link>:
|
||||||
<i style="color: var(--{color()});" class="nf nf-md-speedometer_slow"></i>
|
{project.desc[$language]}
|
||||||
{$_("home.info.speed")}
|
</li>
|
||||||
</li>
|
{/each}
|
||||||
<li>
|
</ul>
|
||||||
<i style="color: var(--{color()});" class="nf nf-fa-lock"></i>
|
|
||||||
{$_("home.info.security")}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<i style="color: var(--{color()});" class="nf nf-fa-network_wired"></i>
|
|
||||||
{$_("home.info.privacy")}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<i style="color: var(--{color()});" class="nf nf-md-eye_off"></i>
|
|
||||||
{$_("home.info.bullshit")}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@ -89,25 +102,6 @@
|
|||||||
gap: 28px;
|
gap: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prefer {
|
|
||||||
color: var(--white-2);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services .info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 900px) {
|
@media only screen and (max-width: 900px) {
|
||||||
main {
|
main {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -4,24 +4,25 @@
|
|||||||
import Link from "$lib/link.svelte";
|
import Link from "$lib/link.svelte";
|
||||||
import Head from "$lib/head.svelte";
|
import Head from "$lib/head.svelte";
|
||||||
|
|
||||||
import { _, locale } from "svelte-i18n";
|
import { language } from "$lib/util.js";
|
||||||
import { api_url } from "$lib/api.js";
|
import { api_url } from "$lib/api.js";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
let list = $state(data.services);
|
let services = $state(data.services);
|
||||||
|
|
||||||
function change(input) {
|
function change(input) {
|
||||||
let value = input.target.value.toLowerCase();
|
let value = input.target.value.toLowerCase();
|
||||||
list = [];
|
services = [];
|
||||||
|
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
list = data.services;
|
services = data.services;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
data.services.forEach((s) => {
|
data.services.forEach((s) => {
|
||||||
if (s.name.toLowerCase().includes(value)) list.push(s);
|
if (s.name.toLowerCase().includes(value)) services.push(s);
|
||||||
else if (s.desc[$locale.slice(0, 2)].toLowerCase().includes(value)) list.push(s);
|
else if (s.desc[$language].toLowerCase().includes(value)) services.push(s);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -33,13 +34,11 @@
|
|||||||
<div class="title">
|
<div class="title">
|
||||||
<input oninput={change} type="text" placeholder={$_("services.search")} />
|
<input oninput={change} type="text" placeholder={$_("services.search")} />
|
||||||
<div>
|
<div>
|
||||||
<Link icon="nf-fa-feed" link={api_url("/news/" + $locale.slice(0, 2))}
|
<Link icon="nf-fa-feed" link={api_url("/news/" + $language)}>{$_("services.feed")}</Link>
|
||||||
>{$_("services.feed")}</Link
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="services">
|
<div class="services">
|
||||||
{#each list as service}
|
{#each services as service}
|
||||||
<Service {service} />
|
<Service {service} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user