Compare commits

..

No commits in common. "dc43bdb3afca3f3b34dec2e0d12b3c91c4f91877" and "322d8d5c399408331f43e3ab25b954fc3f0e448e" have entirely different histories.

11 changed files with 188 additions and 60 deletions

View File

@ -3,47 +3,107 @@ package config
import (
"fmt"
"net/url"
"github.com/ngn13/ortam"
"os"
)
type Type struct {
Debug bool // should display debug messgaes?
AppUrl *url.URL // frontend application URL for the website
Password string // admin password
Host string // host the server should listen on
IPHeader string // header that should be checked for obtaining the client IP
Interval string // service status check interval
Timeout string // timeout for the service status check
Limit string // if the service responds slower than this limit, it will be marked as "slow"
Options []Option
Count int
}
func Load() (*Type, error) {
var conf = Type{
Debug: false,
Password: "",
Host: "0.0.0.0:7002",
IPHeader: "X-Real-IP",
Interval: "1h",
Timeout: "15s",
Limit: "5s",
func (c *Type) Find(name string, typ uint8) (*Option, error) {
for i := 0; i < c.Count; i++ {
if c.Options[i].Name != name {
continue
}
if c.Options[i].Type != typ {
return nil, fmt.Errorf("bad option type")
}
return &c.Options[i], nil
}
if err := ortam.Load(&conf, "WEBSITE"); err != nil {
return nil, err
}
if conf.AppUrl == nil {
conf.AppUrl, _ = url.Parse("http://localhost:7001/")
}
if conf.Password == "" {
return nil, fmt.Errorf("password is not specified")
}
if conf.Host == "" {
return nil, fmt.Errorf("host address is not specified")
}
return &conf, nil
return nil, fmt.Errorf("option not found")
}
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: "app_url", Value: "http://localhost:7001/", 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:7002", 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"
}
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
View 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("WEBSITE_%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
}

View File

@ -1,6 +1,6 @@
module github.com/ngn13/website/api
go 1.24.0
go 1.21.3
require (
github.com/gofiber/fiber/v2 v2.52.6
@ -14,7 +14,6 @@ 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.16 // indirect
github.com/ngn13/ortam v0.0.0-20250412195317-e76e62a7a305 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect

View File

@ -1,9 +1,17 @@
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -11,12 +19,14 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU=
github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/ngn13/ortam v0.0.0-20250412195317-e76e62a7a305 h1:1YxtSMwR14PklXNlZxIqcmfpiq2+G98YNmhSuz7GKCQ=
github.com/ngn13/ortam v0.0.0-20250412195317-e76e62a7a305/go.mod h1:MSJZ4ZstrLvVEvivbp9hhup+iL8rvtpgKcYaF3DSOKk=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@ -27,5 +37,7 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View File

@ -36,18 +36,18 @@ func main() {
app *fiber.App
stat status.Type
conf *config.Type
conf config.Type
db database.Type
err error
)
if conf, err = config.Load(); err != nil {
if err = conf.Load(); err != nil {
util.Fail("failed to load the configuration: %s", err.Error())
return
}
if !conf.Debug {
if !conf.GetBool("debug") {
util.Debg = func(m string, v ...any) {}
}
@ -56,7 +56,7 @@ func main() {
return
}
if err = stat.Setup(conf, &db); err != nil {
if err = stat.Setup(&conf, &db); err != nil {
util.Fail("failed to setup the status checker: %s", err.Error())
return
}
@ -75,7 +75,7 @@ func main() {
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("config", &conf)
c.Locals("database", &db)
return c.Next()
@ -121,9 +121,9 @@ func main() {
}
// start the app
util.Info("starting web server on %s", conf.Host)
util.Info("starting web server on %s", conf.GetStr("host"))
if err = app.Listen(conf.Host); err != nil {
if err = app.Listen(conf.GetStr("host")); err != nil {
util.Fail("failed to start the web server: %s", err.Error())
}

View File

@ -21,7 +21,7 @@ func admin_log(c *fiber.Ctx, m string) error {
func AuthMiddleware(c *fiber.Ctx) error {
conf := c.Locals("config").(*config.Type)
if c.Get("Authorization") != conf.Password {
if c.Get("Authorization") != conf.GetStr("password") {
return util.ErrAuth(c)
}

View File

@ -7,7 +7,8 @@ import (
func GET_Index(c *fiber.Ctx) error {
conf := c.Locals("config").(*config.Type)
app := conf.GetURL("app_url")
// redirect to the API documentation
return c.Redirect(conf.AppUrl.JoinPath("/doc/api").String())
return c.Redirect(app.JoinPath("/doc/api").String())
}

View File

@ -40,6 +40,7 @@ func GET_News(c *fiber.Ctx) error {
db := c.Locals("database").(*database.Type)
conf := c.Locals("config").(*config.Type)
app := conf.GetURL("app_url")
lang := c.Params("lang")
if lang == "" || len(lang) != 2 {
@ -62,10 +63,10 @@ func GET_News(c *fiber.Ctx) error {
})
if feed, err = util.Render("views/news.xml", fiber.Map{
"app_url": conf.AppUrl,
"updated": time.Now().Format(time.RFC3339),
"entries": entries,
"lang": lang,
"app": app,
}); err != nil {
return util.ErrInternal(c, err)
}

View File

@ -66,24 +66,29 @@ func (s *Type) loop() {
func (s *Type) Setup(conf *config.Type, db *database.Type) error {
var (
dur time.Duration
err error
dur time.Duration
iv, to, lm string
err error
)
if conf.Interval == "" || conf.Timeout == "" || conf.Limit == "" {
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(conf.Interval); err != nil {
if dur, err = util.GetDuration(iv); err != nil {
return err
}
if s.timeout, err = util.GetDuration(conf.Timeout); err != nil {
if s.timeout, err = util.GetDuration(iv); err != nil {
return err
}
if s.limit, err = util.GetDuration(conf.Limit); err != nil {
if s.limit, err = util.GetDuration(iv); err != nil {
return err
}

View File

@ -10,9 +10,10 @@ import (
func IP(c *fiber.Ctx) string {
conf := c.Locals("config").(*config.Type)
ip_header := conf.GetStr("ip_header")
if conf.IPHeader != "" && c.Get(conf.IPHeader) != "" {
return strings.Clone(c.Get(conf.IPHeader))
if ip_header != "" && c.Get(ip_header) != "" {
return strings.Clone(c.Get(ip_header))
}
return c.IP()

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom">
<title>{{.app_url.Host}} news</title>
<title>{{.app.Host}} news</title>
<updated>{{.updated}}</updated>
<subtitle>News and updates about my projects and self-hosted services</subtitle>
<link href="{{.app_url.JoinPath "/news"}}"></link>
<link href="{{.app.JoinPath "/news"}}"></link>
{{ range .entries }}
<entry>
<title>{{.Title}}</title>