Files
Gardomatic/internal/api/api.go
T
kleiax 904d14b64c
CI / test (push) Canceled after 0s
Initial commit
2026-09-12 22:22:17 +02:00

178 lines
3.8 KiB
Go

package api
import (
"context"
"database/sql"
"expvar"
"fmt"
"log/slog"
"net/http"
"os"
"runtime"
"sync"
"time"
"gardomatic.kleiax.de/internal/mailer"
"gardomatic.kleiax.de/internal/storage"
"gardomatic.kleiax.de/internal/storage/postgres"
"gardomatic.kleiax.de/internal/vcs"
"github.com/alexedwards/scs/postgresstore"
"github.com/alexedwards/scs/v2"
_ "github.com/lib/pq"
)
var (
version = vcs.Version()
)
// Config contains API server, database, session, mail, and security settings.
type Config struct {
Host string
Port int
Env string
WebBaseURL string
DB DatabaseConfig
Limiter LimiterConfig
Session SessionConfig
Mail mailer.Config
Cors CORSConfig
}
// DatabaseConfig controls the PostgreSQL connection pool.
type DatabaseConfig struct {
Dsn string
MaxOpenConns int
MaxIdleConns int
MaxIdleTime time.Duration
}
// LimiterConfig controls per-client HTTP rate limiting.
type LimiterConfig struct {
Enabled bool
Rps float64
Burst int
}
// SessionConfig controls the session cookie and server-side session lifetime.
type SessionConfig struct {
Lifetime time.Duration
IdleTimeout time.Duration
CookieName string
CookieSecure bool
}
// CORSConfig lists origins allowed to make cross-origin API requests.
type CORSConfig struct {
TrustedOrigins []string
}
type application struct {
config Config
logger *slog.Logger
models storage.Models
mailer *mailer.Mailer
sessions *scs.SessionManager
wg sync.WaitGroup
// db is retained by the application so Run can close the connection pool.
db *sql.DB
}
// New initializes the API application and its database-backed dependencies.
func New(cfg Config) *application {
var handler slog.Handler = slog.NewTextHandler(os.Stdout, nil)
if cfg.Env == "production" {
handler = slog.NewJSONHandler(os.Stdout, nil)
}
logger := slog.New(handler)
db, err := openDB(cfg)
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
logger.Info("database connection pool established")
expvar.NewString("version").Set(version)
expvar.Publish("goroutines", expvar.Func(func() any {
return runtime.NumGoroutine()
}))
expvar.Publish("database", expvar.Func(func() any {
return db.Stats()
}))
expvar.Publish("timestamp", expvar.Func(func() any {
return time.Now().Unix()
}))
mailer, err := mailer.New(cfg.Mail)
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
sessions := scs.New()
sessions.Store = postgresstore.New(db)
sessions.Lifetime = cfg.Session.Lifetime
sessions.IdleTimeout = cfg.Session.IdleTimeout
sessions.HashTokenInStore = true
sessions.Cookie.Name = cfg.Session.CookieName
sessions.Cookie.HttpOnly = true
sessions.Cookie.SameSite = http.SameSiteLaxMode
sessions.Cookie.Secure = cfg.Session.CookieSecure
app := &application{
config: cfg,
logger: logger,
models: postgres.New(db),
mailer: mailer,
sessions: sessions,
db: db,
}
sessions.ErrorFunc = app.serverErrorResponse
return app
}
// Run serves API requests until shutdown and then releases application resources.
func (app *application) Run() {
defer app.db.Close()
err := app.serve()
if err != nil {
app.logger.Error(err.Error())
os.Exit(1)
}
}
// Version returns the version embedded in the API binary.
func (app *application) Version() string {
return fmt.Sprintf("Version:\t%s\n", version)
}
func openDB(cfg Config) (*sql.DB, error) {
db, err := sql.Open("postgres", cfg.DB.Dsn)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(cfg.DB.MaxOpenConns)
db.SetMaxIdleConns(cfg.DB.MaxIdleConns)
db.SetConnMaxIdleTime(cfg.DB.MaxIdleTime)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = db.PingContext(ctx)
if err != nil {
db.Close()
return nil, err
}
return db, nil
}