add projects and metrics routes
This commit is contained in:
@ -24,8 +24,8 @@ 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())
|
||||
if db.rows, err = db.sql.Query("SELECT * FROM " + TABLE_ADMIN_LOG); err != nil {
|
||||
util.Fail("failed to query table: %s", err.Error())
|
||||
goto fail
|
||||
}
|
||||
}
|
||||
@ -35,7 +35,7 @@ func (db *Type) AdminLogNext(l *AdminLog) bool {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ fail:
|
||||
|
||||
func (db *Type) AdminLogAdd(l *AdminLog) error {
|
||||
_, err := db.sql.Exec(
|
||||
`INSERT INTO admin_log(
|
||||
"INSERT INTO "+TABLE_ADMIN_LOG+`(
|
||||
action, time
|
||||
) values(?, ?)`,
|
||||
&l.Action, &l.Time,
|
@ -2,76 +2,62 @@ package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"database/sql"
|
||||
|
||||
_ "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 {
|
||||
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())
|
||||
func (db *Type) create_table(table string) error {
|
||||
var (
|
||||
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
|
||||
_, err = db.sql.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS visitor_count(
|
||||
id TEXT NOT NULL UNIQUE,
|
||||
count INTEGER NOT NULL
|
||||
);
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create the visitor_count table: %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())
|
||||
if _, err = db.sql.Exec(string(query)); err != nil {
|
||||
return fmt.Errorf("failed to create the %s table: %s", table, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Type) Load() (err error) {
|
||||
if db.sql, err = sql.Open("sqlite3", "data.db"); err != nil {
|
||||
return fmt.Errorf("failed access the database: %s", err.Error())
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
if err = db.create_table(table); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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())
|
||||
if db.rows, err = db.sql.Query("SELECT * FROM " + TABLE_NEWS); err != nil {
|
||||
util.Fail("failed to query table: %s", err.Error())
|
||||
goto fail
|
||||
}
|
||||
}
|
||||
@ -75,7 +75,7 @@ func (db *Type) NewsNext(n *News) bool {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -92,7 +92,7 @@ fail:
|
||||
|
||||
func (db *Type) NewsRemove(id string) error {
|
||||
_, err := db.sql.Exec(
|
||||
"DELETE FROM news WHERE id = ?",
|
||||
"DELETE FROM "+TABLE_NEWS+" WHERE id = ?",
|
||||
id,
|
||||
)
|
||||
|
||||
@ -105,7 +105,7 @@ func (db *Type) NewsAdd(n *News) (err error) {
|
||||
}
|
||||
|
||||
_, err = db.sql.Exec(
|
||||
`INSERT OR REPLACE INTO news(
|
||||
"INSERT OR REPLACE INTO "+TABLE_NEWS+`(
|
||||
id, title, author, time, content
|
||||
) values(?, ?, ?, ?, ?)`,
|
||||
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
|
||||
|
||||
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())
|
||||
if db.rows, err = db.sql.Query("SELECT * FROM " + TABLE_SERVICES); err != nil {
|
||||
util.Fail("failed to query table: %s", err.Error())
|
||||
goto fail
|
||||
}
|
||||
}
|
||||
@ -69,7 +69,7 @@ func (db *Type) ServiceNext(s *Service) bool {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -91,7 +91,7 @@ func (db *Type) ServiceFind(name string) (*Service, 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
|
||||
}
|
||||
|
||||
@ -104,7 +104,7 @@ func (db *Type) ServiceFind(name string) (*Service, error) {
|
||||
|
||||
func (db *Type) ServiceRemove(name string) error {
|
||||
_, err := db.sql.Exec(
|
||||
"DELETE FROM services WHERE name = ?",
|
||||
"DELETE FROM "+TABLE_SERVICES+" WHERE name = ?",
|
||||
name,
|
||||
)
|
||||
|
||||
@ -117,7 +117,7 @@ func (db *Type) ServiceUpdate(s *Service) (err error) {
|
||||
}
|
||||
|
||||
_, 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
|
||||
) values(?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
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.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 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/project/add", routes.PUT_AddProject)
|
||||
v1.Delete("/admin/project/del", routes.DEL_DelProject)
|
||||
|
||||
v1.Put("/admin/news/add", routes.PUT_AddNews)
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
var (
|
||||
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
|
||||
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
|
||||
Returns a list of administrator logs. Each log has the following JSON format:
|
||||
```
|
||||
|
Reference in New Issue
Block a user