Cleanup
Some checks are pending
check / check (push) Waiting to run

This commit is contained in:
KS Jannette
2026-02-28 20:42:57 -05:00
parent 90a338d46c
commit 7357315810
11 changed files with 69 additions and 27 deletions

View File

@@ -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)

View File

@@ -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 := "?"

View File

@@ -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"

View File

@@ -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, ", "))

View File

@@ -1,3 +1,4 @@
// Package middleware provides HTTP middleware for authentication and context injection.
package middleware
import (

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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