Phase 1: Go backend scaffold

- Project structure: cmd/boosapi, internal/, pkg/
- Config loader (all env vars from Node.js original)
- Database layer with SQLite/MySQL/Postgres support
- Migration runner (sequential SQL files)
- Health check endpoint (GET /healthz)
- HTTP middleware: logger, auth, CORS, panic recovery
- chi router with /admin, /v1, /api route groups
- Graceful shutdown (SIGINT/SIGTERM)
- Multi-stage Dockerfile (alpine, ~20MB image)
- Initial schema migration (users, api_keys, sites, usage_logs)

Signed-off-by: Boos4721 <boos4721@icloud.com>
This commit is contained in:
boos
2026-05-16 01:07:02 +08:00
parent f0188d3f68
commit b6ecd613d4
9 changed files with 686 additions and 88 deletions
+131
View File
@@ -0,0 +1,131 @@
// Package config loads application configuration from environment variables.
package config
import (
"os"
"strconv"
"time"
)
// Config holds all application configuration.
type Config struct {
// Server
Port int `env:"PORT" default:"4000"`
Host string `env:"HOST" default:"0.0.0.0"`
// Auth
AuthToken string `env:"AUTH_TOKEN" required:"true"`
ProxyToken string `env:"PROXY_TOKEN" required:"true"`
// Database
DBType string `env:"DB_TYPE" default:"sqlite"`
DBURL string `env:"DB_URL" default:""`
DBSSL bool `env:"DB_SSL" default:"false"`
// Storage
DataDir string `env:"DATA_DIR" default:"/app/data"`
// Redis (optional)
RedisURL string `env:"REDIS_URL" default:""`
// Runtime
TZ string `env:"TZ" default:"UTC"`
LogLevel string `env:"LOG_LEVEL" default:"info"`
JWTSecret string `env:"JWT_SECRET" default:""`
// Desktop (set by Electron wrapper, not user)
Desktop bool `env:"BOOSAPI_DESKTOP" default:"false"`
DesktopPort int `env:"BOOSAPI_DESKTOP_SERVER_PORT" default:"0"`
DesktopDev string `env:"BOOSAPI_DESKTOP_DEV_SERVER_URL" default:""`
DesktopExt string `env:"BOOSAPI_DESKTOP_EXTERNAL_SERVER_URL" default:""`
// Derived
location *time.Location
}
// Load reads configuration from environment variables.
func Load() (*Config, error) {
cfg := &Config{}
if v := os.Getenv("PORT"); v != "" {
p, err := strconv.Atoi(v)
if err != nil {
return nil, err
}
cfg.Port = p
}
if v := os.Getenv("HOST"); v != "" {
cfg.Host = v
}
cfg.AuthToken = os.Getenv("AUTH_TOKEN")
if cfg.AuthToken == "" {
cfg.AuthToken = "change-me-admin-token"
}
cfg.ProxyToken = os.Getenv("PROXY_TOKEN")
if v := os.Getenv("DB_TYPE"); v != "" {
cfg.DBType = v
}
if v := os.Getenv("DB_URL"); v != "" {
cfg.DBURL = v
}
if v := os.Getenv("DB_SSL"); v == "true" || v == "1" {
cfg.DBSSL = true
}
if v := os.Getenv("DATA_DIR"); v != "" {
cfg.DataDir = v
}
if v := os.Getenv("REDIS_URL"); v != "" {
cfg.RedisURL = v
}
if v := os.Getenv("TZ"); v != "" {
cfg.TZ = v
}
if v := os.Getenv("LOG_LEVEL"); v != "" {
cfg.LogLevel = v
}
cfg.JWTSecret = os.Getenv("JWT_SECRET")
if v := os.Getenv("BOOSAPI_DESKTOP"); v == "1" {
cfg.Desktop = true
}
if v := os.Getenv("BOOSAPI_DESKTOP_SERVER_PORT"); v != "" {
p, _ := strconv.Atoi(v)
cfg.DesktopPort = p
}
cfg.DesktopDev = os.Getenv("BOOSAPI_DESKTOP_DEV_SERVER_URL")
cfg.DesktopExt = os.Getenv("BOOSAPI_DESKTOP_EXTERNAL_SERVER_URL")
loc, err := time.LoadLocation(cfg.TZ)
if err != nil {
loc = time.UTC
}
cfg.location = loc
return cfg, nil
}
// Location returns the configured timezone location.
func (c *Config) Location() *time.Location {
return c.location
}
// ListenAddr returns the address string for net/http to listen on.
func (c *Config) ListenAddr() string {
return c.Host + ":" + strconv.Itoa(c.Port)
}
// DataPath returns the path under DataDir for a given subdirectory.
func (c *Config) DataPath(sub string) string {
return c.DataDir + "/" + sub
}