From 7357315810f5964380ae8c8d3df535a1f76c050c Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sat, 28 Feb 2026 20:42:57 -0500 Subject: [PATCH] Cleanup --- backend-go/cmd/api/main.go | 14 ++++++-- backend-go/internal/config/config.go | 9 +---- backend-go/internal/domain/types.go | 10 ++++-- backend-go/internal/handlers/alert_rule.go | 2 +- backend-go/internal/middleware/auth.go | 1 + backend-go/internal/models/address.go | 4 ++- backend-go/internal/models/alert_rule.go | 8 +++-- backend-go/internal/models/checkpoint.go | 4 ++- .../internal/models/notification_config.go | 4 ++- backend-go/internal/notifications/discord.go | 34 +++++++++++++++---- backend-go/internal/services/evaluator.go | 6 +++- 11 files changed, 69 insertions(+), 27 deletions(-) diff --git a/backend-go/cmd/api/main.go b/backend-go/cmd/api/main.go index d9a553b..7610e23 100644 --- a/backend-go/cmd/api/main.go +++ b/backend-go/cmd/api/main.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "net/http" + "time" "github.com/joho/godotenv" "github.com/kjannette/koin-ping/backend-go/internal/config" @@ -15,6 +16,13 @@ import ( "github.com/kjannette/koin-ping/backend-go/internal/models" ) +const ( + // serverReadTimeoutSeconds is the maximum duration to read a request. + serverReadTimeoutSeconds = 5 + // serverWriteTimeoutSeconds is the maximum duration to write a response. + serverWriteTimeoutSeconds = 10 +) + //nolint:funlen func main() { _ = godotenv.Load() // .env is optional; env vars can also be set externally @@ -90,8 +98,10 @@ func main() { log.Printf("Environment: %s", cfg.NodeEnv) server := &http.Server{ - Addr: addr, - Handler: handler, + Addr: addr, + Handler: handler, + ReadTimeout: serverReadTimeoutSeconds * time.Second, + WriteTimeout: serverWriteTimeoutSeconds * time.Second, } if err := server.ListenAndServe(); err != nil { log.Fatalf("Server failed: %v", err) diff --git a/backend-go/internal/config/config.go b/backend-go/internal/config/config.go index 119c755..8097158 100644 --- a/backend-go/internal/config/config.go +++ b/backend-go/internal/config/config.go @@ -1,4 +1,4 @@ -// Package config loads environment-based configuration. +//loads environment-based configuration. package config import ( @@ -9,17 +9,12 @@ import ( ) const ( - // defaultPort is the default HTTP server port. defaultPort = 3001 - // defaultDBPort is the default PostgreSQL port. defaultDBPort = 5432 - // defaultPollIntervalMS is the default poller interval in milliseconds. defaultPollIntervalMS = 60000 - // minPollIntervalMS is the minimum allowed poller interval. minPollIntervalMS = 1000 ) -// Config holds application configuration. type Config struct { Port int APIBasePath string @@ -59,10 +54,8 @@ func Load() (*Config, error) { return cfg, nil } -// DSN returns the PostgreSQL data source name for the configured database. func (c *Config) DSN() string { if c.DatabaseURL != "" { - // pgx defaults to sslmode=prefer, which fails against local Postgres. // Append sslmode=disable if not already specified. if !strings.Contains(c.DatabaseURL, "sslmode=") { sep := "?" diff --git a/backend-go/internal/domain/types.go b/backend-go/internal/domain/types.go index f98816e..8b10330 100644 --- a/backend-go/internal/domain/types.go +++ b/backend-go/internal/domain/types.go @@ -15,6 +15,9 @@ type Address struct { // AlertType identifies the kind of alert rule. type AlertType string +// String implements fmt.Stringer. +func (a AlertType) String() string { return string(a) } + // Alert type constants define the supported alert triggers. const ( AlertIncomingTx AlertType = "incoming_tx" @@ -24,7 +27,7 @@ const ( ) // ValidAlertTypes lists all alert types accepted by the API. -var ValidAlertTypes = []AlertType{ +var ValidAlertTypes = []AlertType{ //nolint:gochecknoglobals AlertIncomingTx, AlertOutgoingTx, AlertLargeTransfer, @@ -32,7 +35,7 @@ var ValidAlertTypes = []AlertType{ } // ThresholdRequiredTypes lists alert types that require a threshold value. -var ThresholdRequiredTypes = []AlertType{ +var ThresholdRequiredTypes = []AlertType{ //nolint:gochecknoglobals AlertLargeTransfer, AlertBalanceBelow, } @@ -120,6 +123,9 @@ type NormalizedTx struct { // Direction indicates whether a transaction is incoming or outgoing. type Direction string +// String implements fmt.Stringer. +func (d Direction) String() string { return string(d) } + // Direction constants indicate the flow of a transaction relative to a watched address. const ( DirectionIncoming Direction = "incoming" diff --git a/backend-go/internal/handlers/alert_rule.go b/backend-go/internal/handlers/alert_rule.go index c6ebb22..eb2888b 100644 --- a/backend-go/internal/handlers/alert_rule.go +++ b/backend-go/internal/handlers/alert_rule.go @@ -68,7 +68,7 @@ func (h *AlertRuleHandler) Create(w http.ResponseWriter, r *http.Request) { if !domain.IsValidAlertType(body.Type) { types := make([]string, len(domain.ValidAlertTypes)) for i, t := range domain.ValidAlertTypes { - types[i] = string(t) + types[i] = t.String() } writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Invalid alert type. Must be one of: "+strings.Join(types, ", ")) diff --git a/backend-go/internal/middleware/auth.go b/backend-go/internal/middleware/auth.go index 2bce577..c06784c 100644 --- a/backend-go/internal/middleware/auth.go +++ b/backend-go/internal/middleware/auth.go @@ -1,3 +1,4 @@ +// Package middleware provides HTTP middleware for authentication and context injection. package middleware import ( diff --git a/backend-go/internal/models/address.go b/backend-go/internal/models/address.go index ef4125d..fd1c8f9 100644 --- a/backend-go/internal/models/address.go +++ b/backend-go/internal/models/address.go @@ -2,7 +2,9 @@ package models import ( "context" + "errors" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/kjannette/koin-ping/backend-go/internal/domain" ) @@ -97,7 +99,7 @@ func (m *AddressModel) FindByID(ctx context.Context, id int, userID *string) (*d } if err != nil { - if err.Error() == "no rows in result set" { + if errors.Is(err, pgx.ErrNoRows) { return nil, nil } return nil, err diff --git a/backend-go/internal/models/alert_rule.go b/backend-go/internal/models/alert_rule.go index ea45e45..809009b 100644 --- a/backend-go/internal/models/alert_rule.go +++ b/backend-go/internal/models/alert_rule.go @@ -2,7 +2,9 @@ package models import ( "context" + "errors" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/kjannette/koin-ping/backend-go/internal/domain" ) @@ -21,7 +23,7 @@ func (m *AlertRuleModel) Create(ctx context.Context, addressID int, alertType do `INSERT INTO alert_rules (address_id, type, threshold, enabled) VALUES ($1, $2, $3, TRUE) RETURNING id, address_id, type, threshold, enabled, created_at`, - addressID, string(alertType), threshold, + addressID, alertType.String(), threshold, ).Scan(&r.ID, &r.AddressID, &r.Type, &r.Threshold, &r.Enabled, &r.CreatedAt) if err != nil { return nil, err @@ -75,7 +77,7 @@ func (m *AlertRuleModel) FindByID(ctx context.Context, id int, userID *string) ( } if err != nil { - if err.Error() == "no rows in result set" { + if errors.Is(err, pgx.ErrNoRows) { return nil, nil } return nil, err @@ -93,7 +95,7 @@ func (m *AlertRuleModel) UpdateEnabled(ctx context.Context, id int, enabled bool id, enabled, ).Scan(&r.ID, &r.AddressID, &r.Type, &r.Threshold, &r.Enabled, &r.CreatedAt) if err != nil { - if err.Error() == "no rows in result set" { + if errors.Is(err, pgx.ErrNoRows) { return nil, nil } return nil, err diff --git a/backend-go/internal/models/checkpoint.go b/backend-go/internal/models/checkpoint.go index 94008b9..c6c60d6 100644 --- a/backend-go/internal/models/checkpoint.go +++ b/backend-go/internal/models/checkpoint.go @@ -2,7 +2,9 @@ package models import ( "context" + "errors" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/kjannette/koin-ping/backend-go/internal/domain" ) @@ -23,7 +25,7 @@ func (m *CheckpointModel) GetLastCheckedBlock(ctx context.Context, addressID int addressID, ).Scan(&block) if err != nil { - if err.Error() == "no rows in result set" { + if errors.Is(err, pgx.ErrNoRows) { return 0, false, nil } return 0, false, err diff --git a/backend-go/internal/models/notification_config.go b/backend-go/internal/models/notification_config.go index 7e6b175..4a43a63 100644 --- a/backend-go/internal/models/notification_config.go +++ b/backend-go/internal/models/notification_config.go @@ -2,7 +2,9 @@ package models import ( "context" + "errors" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/kjannette/koin-ping/backend-go/internal/domain" ) @@ -26,7 +28,7 @@ func (m *NotificationConfigModel) GetConfig(ctx context.Context, userID string) ).Scan(&c.UserID, &c.DiscordWebhookURL, &c.TelegramChatID, &c.TelegramBotToken, &c.Email, &c.NotificationEnabled, &c.CreatedAt, &c.UpdatedAt) if err != nil { - if err.Error() == "no rows in result set" { + if errors.Is(err, pgx.ErrNoRows) { return nil, nil } return nil, err diff --git a/backend-go/internal/notifications/discord.go b/backend-go/internal/notifications/discord.go index 128a8a3..b1bb788 100644 --- a/backend-go/internal/notifications/discord.go +++ b/backend-go/internal/notifications/discord.go @@ -9,6 +9,22 @@ import ( "time" ) +const ( + // discordHTTPTimeoutSeconds is the timeout for Discord webhook requests. + discordHTTPTimeoutSeconds = 10 + + // Alert color codes for Discord embeds. + colorGreen = 0x00ff00 + colorOrange = 0xff9900 + colorRed = 0xff0000 + colorBlue = 0x0099ff +) + +// discordHTTPClient is a shared HTTP client with a timeout for Discord requests. +var discordHTTPClient = &http.Client{ //nolint:gochecknoglobals + Timeout: discordHTTPTimeoutSeconds * time.Second, +} + type AlertMetadata struct { TxHash string AddressLabel string @@ -73,7 +89,9 @@ func SendDiscordNotification(webhookURL, message string, meta AlertMetadata) (bo return false, fmt.Errorf("marshal discord payload: %w", err) } - resp, err := http.Post(webhookURL, "application/json", bytes.NewReader(body)) + resp, err := discordHTTPClient.Post( + webhookURL, "application/json", bytes.NewReader(body), + ) if err != nil { log.Printf("Failed to send Discord notification: %v", err) return false, err @@ -98,7 +116,9 @@ func TestDiscordWebhook(webhookURL string) (bool, error) { return false, err } - resp, err := http.Post(webhookURL, "application/json", bytes.NewReader(body)) + resp, err := discordHTTPClient.Post( + webhookURL, "application/json", bytes.NewReader(body), + ) if err != nil { log.Printf("Discord webhook test failed: %v", err) return false, err @@ -111,14 +131,14 @@ func TestDiscordWebhook(webhookURL string) (bool, error) { func colorForAlertType(alertType string) int { switch alertType { case "incoming_tx": - return 0x00ff00 // Green + return colorGreen case "outgoing_tx": - return 0xff9900 // Orange + return colorOrange case "large_transfer": - return 0xff0000 // Red + return colorRed case "balance_below": - return 0xff0000 // Red + return colorRed default: - return 0x0099ff // Blue + return colorBlue } } diff --git a/backend-go/internal/services/evaluator.go b/backend-go/internal/services/evaluator.go index 9dfc9b2..0e30d35 100644 --- a/backend-go/internal/services/evaluator.go +++ b/backend-go/internal/services/evaluator.go @@ -170,7 +170,11 @@ func (s *EvaluatorService) fireAlert(ctx context.Context, rule domain.AlertRule, // Send Discord notification (non-fatal on failure) if addr != nil { - go s.sendNotification(ctx, addr.UserID, message, obs, addressLabel, rule, addr.Address) + go func() { + s.sendNotification( + ctx, addr.UserID, message, obs, addressLabel, rule, addr.Address, + ) + }() } return nil