@@ -0,0 +1,177 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user