Files
plumber/internal/store/postgres.go
codegirl007 35c8c9f391 Fix migrate lock scope and stop RealIP from bypassing proxy trust.
Hold a session advisory lock for the full migration apply path, and remove Chi RealIP so clientIP can validate the TCP peer before walking X-Forwarded-For.
2026-08-22 12:25:16 -07:00

74 lines
1.7 KiB
Go

package store
import (
"context"
"database/sql"
"fmt"
"net/url"
"strings"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
)
// applySchema runs semicolon-separated DDL statements.
func applySchema(ctx context.Context, exec execContext, schema string) error {
for _, stmt := range strings.Split(schema, ";") {
stmt = strings.TrimSpace(stmt)
if stmt == "" {
continue
}
if _, err := exec.ExecContext(ctx, stmt); err != nil {
return fmt.Errorf("%w: %s", err, stmt)
}
}
return nil
}
// postgresDSN normalizes DATABASE_URL for pgx (sslmode default, strip unsupported params).
func postgresDSN(raw string) (string, error) {
u, err := url.Parse(raw)
if err != nil {
return "", fmt.Errorf("DATABASE_URL: %w", err)
}
switch u.Scheme {
case "postgres", "postgresql":
default:
return "", fmt.Errorf("DATABASE_URL must be a postgres URL")
}
q := u.Query()
if strings.EqualFold(q.Get("sslrootcert"), "system") {
q.Del("sslrootcert")
}
q.Del("sslnegotiation")
if q.Get("sslmode") == "" {
q.Set("sslmode", "verify-full")
}
u.RawQuery = q.Encode()
return u.String(), nil
}
// OpenPostgres connects to Postgres, applies schema/migrations, and starts session cleanup.
func OpenPostgres(databaseURL, schema string) (*sql.DB, *SessionStore, error) {
dsn, err := postgresDSN(databaseURL)
if err != nil {
return nil, nil, err
}
db, err := sql.Open("pgx", dsn)
if err != nil {
return nil, nil, err
}
db.SetMaxOpenConns(20)
db.SetMaxIdleConns(5)
if err := db.Ping(); err != nil {
_ = db.Close()
return nil, nil, fmt.Errorf("postgres ping: %w", err)
}
if err := applyMigrations(db, schema); err != nil {
_ = db.Close()
return nil, nil, fmt.Errorf("migrate: %w", err)
}
sessions := NewSessionStore(db, 5*time.Minute)
return db, sessions, nil
}