Initial commit
CI / test (push) Canceled after 0s

This commit is contained in:
2026-09-12 22:22:17 +02:00
commit 904d14b64c
314 changed files with 31884 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
package main
import (
"errors"
"fmt"
"net/url"
"strings"
"time"
"gardomatic.kleiax.de/internal/api"
"gardomatic.kleiax.de/internal/mailer"
"gardomatic.kleiax.de/internal/platform/environment"
)
func configFromEnvironment() (api.Config, error) {
var errs []error
required := func(name string) string {
value, err := environment.Required(name)
if err != nil {
errs = append(errs, err)
}
return value
}
integer := func(name string, fallback int) int {
value, err := environment.Int(name, fallback)
if err != nil {
errs = append(errs, err)
}
return value
}
boolean := func(name string, fallback bool) bool {
value, err := environment.Bool(name, fallback)
if err != nil {
errs = append(errs, err)
}
return value
}
duration := func(name string, fallback time.Duration) time.Duration {
value, err := environment.Duration(name, fallback)
if err != nil {
errs = append(errs, err)
}
return value
}
floating := func(name string, fallback float64) float64 {
value, err := environment.Float(name, fallback)
if err != nil {
errs = append(errs, err)
}
return value
}
cfg := api.Config{
Host: environment.String("GARDOMATIC_API_HOST", ""),
Port: integer("GARDOMATIC_API_PORT", 4000),
Env: environment.String("GARDOMATIC_ENV", "development"),
WebBaseURL: environment.String("GARDOMATIC_WEB_BASE_URL", "http://localhost:4040"),
DB: api.DatabaseConfig{
Dsn: required("GARDOMATIC_DB_DSN"), MaxOpenConns: integer("GARDOMATIC_DB_MAX_OPEN_CONNS", 25),
MaxIdleConns: integer("GARDOMATIC_DB_MAX_IDLE_CONNS", 25), MaxIdleTime: duration("GARDOMATIC_DB_MAX_IDLE_TIME", 15*time.Minute),
},
Limiter: api.LimiterConfig{Enabled: boolean("GARDOMATIC_RATE_LIMIT_ENABLED", true), Rps: floating("GARDOMATIC_RATE_LIMIT_RPS", 10), Burst: integer("GARDOMATIC_RATE_LIMIT_BURST", 40)},
Session: api.SessionConfig{Lifetime: duration("GARDOMATIC_SESSION_LIFETIME", 12*time.Hour), IdleTimeout: duration("GARDOMATIC_SESSION_IDLE_TIMEOUT", 30*time.Minute), CookieName: environment.String("GARDOMATIC_SESSION_COOKIE_NAME", "gardomatic_session"), CookieSecure: boolean("GARDOMATIC_COOKIE_SECURE", false)},
Mail: mailer.Config{Mode: mailer.Mode(environment.String("GARDOMATIC_SMTP_MODE", string(mailer.ModeFile))), Host: environment.String("GARDOMATIC_SMTP_HOST", ""), Port: integer("GARDOMATIC_SMTP_PORT", 25), Username: environment.String("GARDOMATIC_SMTP_USERNAME", ""), Password: environment.String("GARDOMATIC_SMTP_PASSWORD", ""), Sender: environment.String("GARDOMATIC_SMTP_SENDER", "gardomatic@localhost"), FilePath: environment.String("GARDOMATIC_SMTP_FILE_PATH", "/tmp/gardomatic-mails.log")},
Cors: api.CORSConfig{TrustedOrigins: environment.CSV("GARDOMATIC_CORS_TRUSTED_ORIGINS")},
}
if cfg.Port < 1 || cfg.Port > 65535 {
errs = append(errs, errors.New("GARDOMATIC_API_PORT must be between 1 and 65535"))
}
if cfg.Env != "development" && cfg.Env != "test" && cfg.Env != "production" {
errs = append(errs, fmt.Errorf("GARDOMATIC_ENV has unsupported value %q", cfg.Env))
}
if cfg.DB.MaxOpenConns < 1 || cfg.DB.MaxIdleConns < 0 || cfg.DB.MaxIdleConns > cfg.DB.MaxOpenConns {
errs = append(errs, errors.New("database pool limits are invalid"))
}
if cfg.Limiter.Rps <= 0 || cfg.Limiter.Burst < 1 {
errs = append(errs, errors.New("rate limit values must be positive"))
}
if strings.TrimSpace(cfg.Session.CookieName) == "" {
errs = append(errs, errors.New("GARDOMATIC_SESSION_COOKIE_NAME must not be empty"))
}
for _, origin := range cfg.Cors.TrustedOrigins {
parsed, err := url.ParseRequestURI(origin)
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
errs = append(errs, fmt.Errorf("invalid trusted origin %q", origin))
}
}
if parsed, err := url.ParseRequestURI(cfg.WebBaseURL); err != nil || parsed.Scheme == "" || parsed.Host == "" {
errs = append(errs, errors.New("GARDOMATIC_WEB_BASE_URL must be an absolute URL"))
}
if cfg.Env == "production" && !cfg.Session.CookieSecure {
errs = append(errs, errors.New("GARDOMATIC_COOKIE_SECURE must be true in production"))
}
if _, err := mailer.New(cfg.Mail); err != nil {
errs = append(errs, fmt.Errorf("mail configuration: %w", err))
}
return cfg, errors.Join(errs...)
}
+24
View File
@@ -0,0 +1,24 @@
package main
import "testing"
func TestConfigFromEnvironment(t *testing.T) {
t.Setenv("GARDOMATIC_DB_DSN", "postgres://example")
t.Setenv("GARDOMATIC_API_HOST", "127.0.0.1")
t.Setenv("GARDOMATIC_API_PORT", "4100")
t.Setenv("GARDOMATIC_CORS_TRUSTED_ORIGINS", "https://example.com, https://app.example.com")
cfg, err := configFromEnvironment()
if err != nil {
t.Fatal(err)
}
if cfg.Host != "127.0.0.1" || cfg.Port != 4100 || len(cfg.Cors.TrustedOrigins) != 2 {
t.Fatalf("unexpected config: %#v", cfg)
}
}
func TestConfigRequiresDSN(t *testing.T) {
t.Setenv("GARDOMATIC_DB_DSN", "")
if _, err := configFromEnvironment(); err == nil {
t.Fatal("expected missing DSN error")
}
}
+2
View File
@@ -0,0 +1,2 @@
// Package main starts the Gardomatic JSON API service.
package main
+16
View File
@@ -0,0 +1,16 @@
package main
import (
"log"
"gardomatic.kleiax.de/internal/api"
)
func main() {
cfg, err := configFromEnvironment()
if err != nil {
log.Fatal(err)
}
server := api.New(cfg)
server.Run()
}