178 lines
3.8 KiB
Go
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
|
|
}
|