v0.12.0 migrate sqlite3 to psql18, updated auth, deploy docs, account and replay updates
test / test (push) Canceled after 0s
test / test (push) Canceled after 0s
This commit is contained in:
1 parent
5d2be4abb2
commit
d0326b059e
66 files changed
+4016
-2433
No files matched your search
+58
-19
@@ -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)")
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user