From 0f327c814a2c41188ca6a5f052b359d9a74631d5 Mon Sep 17 00:00:00 2001 From: Ruidy Date: Sat, 21 Mar 2026 10:19:33 +0100 Subject: [PATCH] fix: normalize minio endpoints --- AGENTS.md | 43 --------------------------- internal/driver/storage/minio.go | 34 +++++++++++++++++++-- internal/driver/storage/minio_test.go | 31 +++++++++++++++++++ internal/view/booking_by_id_templ.go | 6 ++-- internal/view/booking_form_templ.go | 2 +- internal/view/booking_lines_templ.go | 2 +- internal/view/bookings_list_templ.go | 2 +- internal/view/bookings_new_templ.go | 2 +- internal/view/item_list_templ.go | 2 +- internal/view/layout/base_templ.go | 4 +-- internal/view/line_item_templ.go | 2 +- internal/view/login_form_templ.go | 2 +- internal/view/login_templ.go | 2 +- internal/view/payment_templ.go | 2 +- internal/view/report_section_templ.go | 2 +- internal/view/reports_templ.go | 4 +-- internal/view/success_templ.go | 2 +- 17 files changed, 81 insertions(+), 63 deletions(-) delete mode 100644 AGENTS.md create mode 100644 internal/driver/storage/minio_test.go diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index c51dc36..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,43 +0,0 @@ -# Repository Guidelines - -## Project Structure & Module Organization -- `main.go`: application entrypoint (HTTP server). -- `internal/`: core app code by layer: - - `server/` (HTTP handlers/routes), `service/` (business logic), `repository/` (data access), `driver/` (external integrations), `config/` (env config), `view/` (templ views/viewmodels). -- `internal/view/*.templ` are source templates; generated files are `internal/view/*_templ.go`. -- `cmd/cron/`: cron entrypoint. -- `assets/`: static assets. `docs/`: documentation/images. `scripts/` and `tmp/`: tooling/artifacts. -- Tests live next to code as `*_test.go`. - -## Build, Test, and Development Commands -- `make dev`: run local dev stack with Docker Compose and hot reload. -- `make run`: build and run production image locally. -- `make test`: run `go test ./...`. -- `make format`: run `templ generate`, `templ fmt`, and `go fmt`. -- `make lint`: run `golangci-lint run ./...`. -- `make stop`: stop dev containers. -- Local (without Docker): `go run .`, `go test ./...`. - -## Coding Style & Naming Conventions -- Go style is standard `go fmt` (tabs, canonical imports). -- Package names: lowercase, no underscores. -- File names: lowercase with underscores when needed. -- Exported identifiers: `PascalCase`; unexported: `camelCase`. -- Keep handler/controller code thin; place business rules in `internal/service`. - -## Testing Guidelines -- Use Go `testing` with table-driven tests where practical. -- Name files `*_test.go`; tests `TestXxx`; benchmarks `BenchmarkXxx`. -- Cover success and failure paths for changed logic. -- Run `make test` (or `go test ./...`) before opening a PR. - -## Commit & Pull Request Guidelines -- Prefer Conventional Commits: `feat(scope): ...`, `fix(scope): ...`, `chore: ...`. -- Keep commits focused and atomic. -- PRs should include: clear summary, linked issue (e.g., `#51`), and screenshots for UI changes. -- Call out config/env changes and any migration or deployment impact. - -## Security & Configuration Tips -- Config is environment-driven with `APP_` prefix (see `internal/config/config.go`). -- Example parser model override: `APP_OPENAI_MODEL=gpt-5-nano`. -- Never commit secrets; keep them in local `.env` / deployment secret manager. diff --git a/internal/driver/storage/minio.go b/internal/driver/storage/minio.go index 141b1a3..b89c764 100644 --- a/internal/driver/storage/minio.go +++ b/internal/driver/storage/minio.go @@ -4,6 +4,8 @@ import ( "bytes" "context" "fmt" + "net/url" + "strings" "time" "github.com/minio/minio-go/v7" @@ -34,9 +36,14 @@ func NewMinioInvoiceStorage(cfg *config.Config) (*MinioInvoiceStorage, error) { return nil, fmt.Errorf("minio bucket is required") } - client, err := minio.New(cfg.MinIOEndpoint, &minio.Options{ + endpoint, secure, err := normalizeEndpoint(cfg.MinIOEndpoint, cfg.MinIOUseSSL) + if err != nil { + return nil, fmt.Errorf("normalize minio endpoint: %w", err) + } + + client, err := minio.New(endpoint, &minio.Options{ Creds: credentials.NewStaticV4(cfg.MinIOAccessKey, cfg.MinIOSecretKey, ""), - Secure: cfg.MinIOUseSSL, + Secure: secure, }) if err != nil { return nil, fmt.Errorf("create minio client: %w", err) @@ -48,6 +55,29 @@ func NewMinioInvoiceStorage(cfg *config.Config) (*MinioInvoiceStorage, error) { }, nil } +func normalizeEndpoint(raw string, secure bool) (string, bool, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return "", secure, fmt.Errorf("endpoint is required") + } + + if strings.HasPrefix(raw, "http://") || strings.HasPrefix(raw, "https://") { + u, err := url.Parse(raw) + if err != nil { + return "", secure, fmt.Errorf("parse endpoint: %w", err) + } + if u.Host == "" { + return "", secure, fmt.Errorf("endpoint host is required") + } + if u.Path != "" && u.Path != "/" { + return "", secure, fmt.Errorf("endpoint path is not supported") + } + return u.Host, u.Scheme == "https", nil + } + + return raw, secure, nil +} + func (s *MinioInvoiceStorage) StoreInvoice( ctx context.Context, objectKey string, diff --git a/internal/driver/storage/minio_test.go b/internal/driver/storage/minio_test.go new file mode 100644 index 0000000..392695f --- /dev/null +++ b/internal/driver/storage/minio_test.go @@ -0,0 +1,31 @@ +package storage + +import "testing" + +func TestNormalizeEndpoint_WithSchemeOverridesSecureFlag(t *testing.T) { + endpoint, secure, err := normalizeEndpoint("https://s3api.nemausat.com", false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if endpoint != "s3api.nemausat.com" { + t.Fatalf("expected normalized host, got %q", endpoint) + } + if !secure { + t.Fatal("expected https endpoint to force secure=true") + } +} + +func TestNormalizeEndpoint_WithoutSchemePreservesSecureFlag(t *testing.T) { + endpoint, secure, err := normalizeEndpoint("s3api.nemausat.com", true) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if endpoint != "s3api.nemausat.com" { + t.Fatalf("expected endpoint to be preserved, got %q", endpoint) + } + if !secure { + t.Fatal("expected secure flag to be preserved") + } +} diff --git a/internal/view/booking_by_id_templ.go b/internal/view/booking_by_id_templ.go index cbeb22a..c5a249f 100644 --- a/internal/view/booking_by_id_templ.go +++ b/internal/view/booking_by_id_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.977 +// templ: version: v0.3.1001 package view //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -123,7 +123,7 @@ func BookingById(booking *BookingViewModel) templ.Component { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/view/success_templ.go b/internal/view/success_templ.go index 4dc26b7..e3fd471 100644 --- a/internal/view/success_templ.go +++ b/internal/view/success_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.977 +// templ: version: v0.3.1001 package view //lint:file-ignore SA4006 This context is only used if a nested component is present.