v0.12.0 migrate sqlite3 to psql18, updated auth, deploy docs, account and replay updates
test / test (push) Canceled after 0s

This commit is contained in:
lixen committed 2026-09-26 20:58:19 -04:00
1 parent 5d2be4abb2
commit d0326b059e
66 files changed
+4016 -2433

No files matched your search

+58 -19
View File
@@ -41,12 +41,17 @@ func main() {
apiHost = flag.String("api-host", "localhost", "API server host")
apiPort = flag.Int("api-port", 8080, "API server port")
dev = flag.Bool("dev", false, "Development mode (relaxed rate limits)")
storagePath = flag.String("storage-path", "", "Path to SQLite database file (disables persistence if empty)")
dsn = flag.String("dsn", "", "PostgreSQL connection string (default $CHESS_DSN; persistence is disabled if empty)")
pidPath = flag.String("pid", "", "Optional path to write PID file")
pidLock = flag.Bool("pid-lock", false, "Lock PID file to allow only one instance (requires -pid)")
logLevel = flag.String("log-level", "info", "Log level: debug, info, warn, or error")
logHTTP = flag.Bool("log-http", true, "Log HTTP requests")
proxies = flag.String("trusted-proxies", "", "Comma-separated reverse-proxy IPs/CIDRs whose -proxy-header is trusted for client IPs")
proxyHeader = flag.String("proxy-header", "X-Real-IP", "Header carrying the client IP from a trusted proxy")
finishedTTL = flag.Duration("finished-game-ttl", service.FinishedGameTTL, "How long completed games remain in memory (0 disables eviction)")
anonTTL = flag.Duration("anonymous-game-ttl", service.AnonymousGameTTL, "How long games without a registered player survive after their last activity, in memory and in the database (0 keeps them)")
maxUsers = flag.Int("max-users", service.DefaultMaxUsers, "Accounts at which public registration closes (0 = no limit; CLI-created accounts are not limited)")
jwtFile = flag.String("jwt-secret-file", "", "File holding a stable JWT signing key, mode 0600, at least 32 bytes (default $CHESS_JWT_SECRET_FILE)")
// Web UI server flags
serve = flag.Bool("serve", false, "Enable web UI server")
@@ -55,6 +60,14 @@ func main() {
webAPIURL = flag.String("web-api-url", "", "Browser-visible API base URL (defaults to the API listen address)")
)
flag.Parse()
// Environment fallbacks are applied after parsing so -h never prints a
// DSN, which may contain a password.
if *dsn == "" {
*dsn = os.Getenv("CHESS_DSN")
}
if *jwtFile == "" {
*jwtFile = os.Getenv("CHESS_JWT_SECRET_FILE")
}
var level slog.Level
if err := level.UnmarshalText([]byte(*logLevel)); err != nil {
@@ -73,6 +86,11 @@ func main() {
// handler. Avoid embedding a second timestamp inside its message.
log.SetFlags(0)
trustedProxies, err := parseTrustedProxies(*proxies)
if err != nil {
log.Fatalf("Invalid -trusted-proxies: %v", err)
}
// Validate PID flags
if *pidLock && *pidPath == "" {
log.Fatal("Error: -pid-lock flag requires the -pid flag to be set")
@@ -88,12 +106,13 @@ func main() {
log.Printf("PID file created at: %s (lock: %v)", *pidPath, *pidLock)
}
// 1. Initialize Storage (optional)
// 1. Initialize Storage (optional). The DSN is never logged: it may carry a
// password when peer or .pgpass authentication is not used.
var store *storage.Store
if *storagePath != "" {
log.Printf("Initializing persistent storage at: %s", *storagePath)
if *dsn != "" {
log.Printf("Initializing PostgreSQL storage")
var err error
store, err = storage.NewStore(*storagePath, *dev)
store, err = storage.NewStore(*dsn)
if err != nil {
log.Fatalf("Failed to initialize storage: %v", err)
}
@@ -101,27 +120,37 @@ func main() {
log.Fatalf("Failed to initialize schema: %v", err)
}
} else {
log.Printf("Persistent storage disabled (use -storage-path to enable)")
log.Printf("Persistent storage disabled (use -dsn or CHESS_DSN to enable)")
}
// JWT secret management
// JWT signing key: a key file keeps sessions valid across restarts; without
// one, dev mode uses a fixed key and production generates a per-process key.
var jwtSecret []byte
if *dev {
// Fixed secret in dev mode for testing consistency
switch {
case *jwtFile != "":
var err error
if jwtSecret, err = loadJWTSecret(*jwtFile); err != nil {
log.Fatalf("Failed to load JWT secret: %v", err)
}
log.Printf("JWT secret loaded from file (sessions survive restarts)")
case *dev:
jwtSecret = []byte("dev-secret-minimum-32-characters-long")
log.Printf("Using fixed JWT secret (dev mode)")
} else {
// Generate cryptographically secure secret
default:
jwtSecret = make([]byte, 32)
if _, err := rand.Read(jwtSecret); err != nil {
log.Fatalf("Failed to generate JWT secret: %v", err)
}
log.Printf("JWT secret generated (sessions valid until restart)")
rand.Read(jwtSecret)
log.Printf("JWT secret generated (sessions valid until restart; use -jwt-secret-file to persist)")
}
// 2. Initialize the Service with optional storage and auth
svc := service.New(store, jwtSecret)
svc, err := service.New(store, jwtSecret)
clear(jwtSecret) // the JWT manager keeps its own copy
if err != nil {
log.Fatalf("Failed to initialize service: %v", err)
}
svc.SetFinishedGameTTL(*finishedTTL)
svc.SetAnonymousGameTTL(*anonTTL)
svc.SetMaxUsers(*maxUsers)
// Start cleanup job for expired users/sessions
cleanupCtx, cleanupCancel := context.WithCancel(context.Background())
@@ -141,7 +170,12 @@ func main() {
}
// 4. Initialize the Fiber App/HTTP Handler, injecting processor and service
app := http.NewFiberApp(proc, svc, *dev, *logHTTP)
app := http.NewFiberApp(proc, svc, http.Options{
DevMode: *dev,
LogRequests: *logHTTP,
TrustedProxies: trustedProxies,
ProxyHeader: *proxyHeader,
})
// API Server configuration
apiAddr := fmt.Sprintf("%s:%d", *apiHost, *apiPort)
@@ -157,8 +191,13 @@ func main() {
} else {
log.Printf("Rate Limit: 10 requests/second per IP")
}
if *storagePath != "" {
log.Printf("Storage: Enabled (%s)", *storagePath)
if len(trustedProxies) > 0 {
log.Printf("Client IP: %s from trusted proxies %v", *proxyHeader, trustedProxies)
} else {
log.Printf("Client IP: TCP peer (behind a reverse proxy, set -trusted-proxies or all clients share one rate limit)")
}
if store != nil {
log.Printf("Storage: Enabled (PostgreSQL)")
} else {
log.Printf("Storage: Disabled (auth features unavailable)")
}