diff --git a/.env.example b/.env.example index ff770df..4480ca6 100644 --- a/.env.example +++ b/.env.example @@ -29,21 +29,22 @@ EMAIL_HOST_PASSWORD=your-app-password DEFAULT_FROM_EMAIL=Casera # APNs Settings (iOS Push Notifications) +# Direct APNs integration - no external push server needed APNS_AUTH_KEY_PATH=/path/to/AuthKey_XXXXXX.p8 APNS_AUTH_KEY_ID=XXXXXXXXXX APNS_TEAM_ID=XXXXXXXXXX APNS_TOPIC=com.example.casera -APNS_USE_SANDBOX=true +APNS_PRODUCTION=false # Set to true for production APNs, false for sandbox # FCM Settings (Android Push Notifications) +# Direct FCM integration using legacy HTTP API FCM_SERVER_KEY=your-firebase-server-key # Worker Settings (Background Jobs) -CELERY_BEAT_REMINDER_HOUR=20 -CELERY_BEAT_REMINDER_MINUTE=0 - -# Gorush Push Notification Server -GORUSH_URL=http://localhost:8088 +TASK_REMINDER_HOUR=20 +TASK_REMINDER_MINUTE=0 +OVERDUE_REMINDER_HOUR=9 +DAILY_DIGEST_HOUR=11 # Admin Panel ADMIN_PORT=9000 diff --git a/cmd/api/main.go b/cmd/api/main.go index 671b8e1..34df1b2 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -14,6 +14,7 @@ import ( "github.com/treytartt/casera-api/internal/config" "github.com/treytartt/casera-api/internal/database" + "github.com/treytartt/casera-api/internal/push" "github.com/treytartt/casera-api/internal/router" "github.com/treytartt/casera-api/internal/services" "github.com/treytartt/casera-api/pkg/utils" @@ -110,6 +111,18 @@ func main() { pdfService := services.NewPDFService() log.Info().Msg("PDF service initialized") + // Initialize push notification client (APNs + FCM) + var pushClient *push.Client + pushClient, err = push.NewClient(&cfg.Push) + if err != nil { + log.Warn().Err(err).Msg("Failed to initialize push client - push notifications disabled") + } else { + log.Info(). + Bool("ios_enabled", pushClient.IsIOSEnabled()). + Bool("android_enabled", pushClient.IsAndroidEnabled()). + Msg("Push notification client initialized") + } + // Setup router with dependencies (includes admin panel at /admin) deps := &router.Dependencies{ DB: db, @@ -117,6 +130,7 @@ func main() { Config: cfg, EmailService: emailService, PDFService: pdfService, + PushClient: pushClient, StorageService: storageService, } r := router.SetupRouter(deps) diff --git a/cmd/worker/main.go b/cmd/worker/main.go index b7bfff4..a38e11e 100644 --- a/cmd/worker/main.go +++ b/cmd/worker/main.go @@ -39,11 +39,16 @@ func main() { sqlDB, _ := db.DB() defer sqlDB.Close() - // Initialize push client (optional) - var gorushClient *push.GorushClient - if cfg.Push.GorushURL != "" { - gorushClient = push.NewGorushClient(&cfg.Push) - log.Info().Str("url", cfg.Push.GorushURL).Msg("Gorush client initialized") + // Initialize push client (APNs + FCM) + var pushClient *push.Client + pushClient, err = push.NewClient(&cfg.Push) + if err != nil { + log.Warn().Err(err).Msg("Failed to initialize push client - push notifications disabled") + } else { + log.Info(). + Bool("ios_enabled", pushClient.IsIOSEnabled()). + Bool("android_enabled", pushClient.IsAndroidEnabled()). + Msg("Push notification client initialized") } // Initialize email service (optional) @@ -80,7 +85,7 @@ func main() { ) // Create job handler - jobHandler := jobs.NewHandler(db, gorushClient, emailService, cfg) + jobHandler := jobs.NewHandler(db, pushClient, emailService, cfg) // Create Asynq mux and register handlers mux := asynq.NewServeMux() diff --git a/go.mod b/go.mod index c35683d..12c26e7 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/shopspring/decimal v1.4.0 github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 - golang.org/x/crypto v0.31.0 + golang.org/x/crypto v0.40.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 @@ -24,20 +24,47 @@ require ( ) require ( + cel.dev/expr v0.23.1 // indirect + cloud.google.com/go v0.121.0 // indirect + cloud.google.com/go/auth v0.16.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.6.0 // indirect + cloud.google.com/go/firestore v1.18.0 // indirect + cloud.google.com/go/iam v1.5.2 // indirect + cloud.google.com/go/longrunning v0.6.7 // indirect + cloud.google.com/go/monitoring v1.24.2 // indirect + cloud.google.com/go/storage v1.53.0 // indirect + firebase.google.com/go/v4 v4.18.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect + github.com/MicahParks/keyfunc v1.9.0 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect + github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.23.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect @@ -53,23 +80,43 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/sagikazarmark/locafero v0.9.0 // indirect + github.com/sideshow/apns2 v0.25.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.14.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect + github.com/zeebo/errs v1.4.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.35.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect golang.org/x/arch v0.12.0 // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.8.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/api v0.231.0 // indirect + google.golang.org/appengine/v2 v2.0.6 // indirect + google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect + google.golang.org/grpc v1.72.0 // indirect google.golang.org/protobuf v1.36.9 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 1ac1369..22e9a3f 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,35 @@ +cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg= +cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go v0.121.0 h1:pgfwva8nGw7vivjZiRfrmglGWiCJBP+0OmDpenG/Fwg= +cloud.google.com/go v0.121.0/go.mod h1:rS7Kytwheu/y9buoDmu5EIpMMCI4Mb8ND4aeN4Vwj7Q= +cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= +cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s= +cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= +cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= +cloud.google.com/go/storage v1.53.0 h1:gg0ERZwL17pJ+Cz3cD2qS60w1WMDnwcm5YPAIQBHUAw= +cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5it1i5boaA= +firebase.google.com/go/v4 v4.18.0 h1:S+g0P72oDGqOaG4wlLErX3zQmU9plVdu7j+Bc3R1qFw= +firebase.google.com/go/v4 v4.18.0/go.mod h1:P7UfBpzc8+Z3MckX79+zsWzKVfpGryr6HLbAe7gCWfs= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= +github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= +github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20201120081800-1786d5ef83d4/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= @@ -11,12 +43,20 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -29,6 +69,13 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -42,13 +89,28 @@ github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang-jwt/jwt/v4 v4.4.1 h1:pC5DB52sCeK48Wlb9oPcdhnjkz1TKt1D/P7WKJ0kUcQ= +github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw= github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -94,6 +156,8 @@ github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8 github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.17.1 h1:7tl732FjYPRT9H9aNfyTwKg9iTETjWjGKEJ2t/5iWTs= @@ -102,6 +166,7 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= @@ -110,6 +175,8 @@ github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFT github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sideshow/apns2 v0.25.0 h1:XOzanncO9MQxkb03T/2uU2KcdVjYiIf0TMLzec0FTW4= +github.com/sideshow/apns2 v0.25.0/go.mod h1:7Fceu+sL0XscxrfLSkAoH6UtvKefq3Kq1n4W3ayQZqE= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= @@ -120,11 +187,14 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -137,29 +207,105 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20170512130425-ab89591268e0/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.231.0 h1:LbUD5FUl0C4qwia2bjXhCMH65yz1MLPzA/0OYEsYY7Q= +google.golang.org/api v0.231.0/go.mod h1:H52180fPI/QQlUc0F4xWfGZILdv09GCWKt2bcsn164A= +google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= +google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= +google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= +google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= +google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0= +google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 h1:IqsN8hx+lWLqlN+Sc3DoMy/watjofWiU8sRFgQ8fhKM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= +google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -167,6 +313,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/admin/handlers/notification_handler.go b/internal/admin/handlers/notification_handler.go index 064e36e..9233dc8 100644 --- a/internal/admin/handlers/notification_handler.go +++ b/internal/admin/handlers/notification_handler.go @@ -19,11 +19,11 @@ import ( type AdminNotificationHandler struct { db *gorm.DB emailService *services.EmailService - pushClient *push.GorushClient + pushClient *push.Client } // NewAdminNotificationHandler creates a new admin notification handler -func NewAdminNotificationHandler(db *gorm.DB, emailService *services.EmailService, pushClient *push.GorushClient) *AdminNotificationHandler { +func NewAdminNotificationHandler(db *gorm.DB, emailService *services.EmailService, pushClient *push.Client) *AdminNotificationHandler { return &AdminNotificationHandler{ db: db, emailService: emailService, diff --git a/internal/admin/routes.go b/internal/admin/routes.go index 22ba5be..4982078 100644 --- a/internal/admin/routes.go +++ b/internal/admin/routes.go @@ -20,7 +20,7 @@ import ( // Dependencies holds optional services for admin routes type Dependencies struct { EmailService *services.EmailService - PushClient *push.GorushClient + PushClient *push.Client } // SetupRoutes configures all admin routes @@ -114,7 +114,7 @@ func SetupRoutes(router *gin.Engine, db *gorm.DB, cfg *config.Config, deps *Depe // Notification management var emailService *services.EmailService - var pushClient *push.GorushClient + var pushClient *push.Client if deps != nil { emailService = deps.EmailService pushClient = deps.PushClient diff --git a/internal/config/config.go b/internal/config/config.go index 491102c..7347cfb 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -59,15 +59,16 @@ type EmailConfig struct { } type PushConfig struct { - // Gorush server URL + // Gorush server URL (deprecated - kept for backwards compatibility) GorushURL string // APNs (iOS) - APNSKeyPath string - APNSKeyID string - APNSTeamID string - APNSTopic string - APNSSandbox bool + APNSKeyPath string + APNSKeyID string + APNSTeamID string + APNSTopic string + APNSSandbox bool + APNSProduction bool // If true, use production APNs; if false, use sandbox // FCM (Android) FCMServerKey string @@ -163,13 +164,14 @@ func Load() (*Config, error) { UseTLS: viper.GetBool("EMAIL_USE_TLS"), }, Push: PushConfig{ - GorushURL: viper.GetString("GORUSH_URL"), - APNSKeyPath: viper.GetString("APNS_AUTH_KEY_PATH"), - APNSKeyID: viper.GetString("APNS_AUTH_KEY_ID"), - APNSTeamID: viper.GetString("APNS_TEAM_ID"), - APNSTopic: viper.GetString("APNS_TOPIC"), - APNSSandbox: viper.GetBool("APNS_USE_SANDBOX"), - FCMServerKey: viper.GetString("FCM_SERVER_KEY"), + GorushURL: viper.GetString("GORUSH_URL"), + APNSKeyPath: viper.GetString("APNS_AUTH_KEY_PATH"), + APNSKeyID: viper.GetString("APNS_AUTH_KEY_ID"), + APNSTeamID: viper.GetString("APNS_TEAM_ID"), + APNSTopic: viper.GetString("APNS_TOPIC"), + APNSSandbox: viper.GetBool("APNS_USE_SANDBOX"), + APNSProduction: viper.GetBool("APNS_PRODUCTION"), + FCMServerKey: viper.GetString("FCM_SERVER_KEY"), }, Worker: WorkerConfig{ TaskReminderHour: viper.GetInt("TASK_REMINDER_HOUR"), @@ -242,6 +244,7 @@ func setDefaults() { viper.SetDefault("GORUSH_URL", "http://localhost:8088") viper.SetDefault("APNS_TOPIC", "com.example.casera") viper.SetDefault("APNS_USE_SANDBOX", true) + viper.SetDefault("APNS_PRODUCTION", false) // Worker defaults (all times in UTC) viper.SetDefault("TASK_REMINDER_HOUR", 20) // 8:00 PM UTC diff --git a/internal/push/apns.go b/internal/push/apns.go new file mode 100644 index 0000000..e89dfd0 --- /dev/null +++ b/internal/push/apns.go @@ -0,0 +1,134 @@ +package push + +import ( + "context" + "fmt" + + "github.com/rs/zerolog/log" + "github.com/sideshow/apns2" + "github.com/sideshow/apns2/payload" + "github.com/sideshow/apns2/token" + + "github.com/treytartt/casera-api/internal/config" +) + +// APNsClient handles direct communication with Apple Push Notification service +type APNsClient struct { + client *apns2.Client + topic string +} + +// NewAPNsClient creates a new APNs client using token-based authentication +func NewAPNsClient(cfg *config.PushConfig) (*APNsClient, error) { + if cfg.APNSKeyPath == "" || cfg.APNSKeyID == "" || cfg.APNSTeamID == "" { + return nil, fmt.Errorf("APNs configuration incomplete: key_path=%s, key_id=%s, team_id=%s", + cfg.APNSKeyPath, cfg.APNSKeyID, cfg.APNSTeamID) + } + + // Load the APNs auth key (.p8 file) + authKey, err := token.AuthKeyFromFile(cfg.APNSKeyPath) + if err != nil { + return nil, fmt.Errorf("failed to load APNs auth key from %s: %w", cfg.APNSKeyPath, err) + } + + // Create token for authentication + authToken := &token.Token{ + AuthKey: authKey, + KeyID: cfg.APNSKeyID, + TeamID: cfg.APNSTeamID, + } + + // Create client - production or sandbox + // Use APNSProduction if set, otherwise fall back to inverse of APNSSandbox + var client *apns2.Client + useProduction := cfg.APNSProduction || !cfg.APNSSandbox + if useProduction { + client = apns2.NewTokenClient(authToken).Production() + log.Info().Msg("APNs client configured for PRODUCTION") + } else { + client = apns2.NewTokenClient(authToken).Development() + log.Info().Msg("APNs client configured for DEVELOPMENT/SANDBOX") + } + + return &APNsClient{ + client: client, + topic: cfg.APNSTopic, + }, nil +} + +// Send sends a push notification to iOS devices +func (c *APNsClient) Send(ctx context.Context, tokens []string, title, message string, data map[string]string) error { + if len(tokens) == 0 { + return nil + } + + // Build the notification payload + p := payload.NewPayload(). + AlertTitle(title). + AlertBody(message). + Sound("default"). + MutableContent() + + // Add custom data + for key, value := range data { + p.Custom(key, value) + } + + var errors []error + successCount := 0 + + for _, deviceToken := range tokens { + notification := &apns2.Notification{ + DeviceToken: deviceToken, + Topic: c.topic, + Payload: p, + Priority: apns2.PriorityHigh, + } + + res, err := c.client.PushWithContext(ctx, notification) + if err != nil { + log.Error(). + Err(err). + Str("token", truncateToken(deviceToken)). + Msg("Failed to send APNs notification") + errors = append(errors, fmt.Errorf("token %s: %w", truncateToken(deviceToken), err)) + continue + } + + if !res.Sent() { + log.Error(). + Str("token", truncateToken(deviceToken)). + Str("reason", res.Reason). + Int("status", res.StatusCode). + Msg("APNs notification not sent") + errors = append(errors, fmt.Errorf("token %s: %s (status %d)", truncateToken(deviceToken), res.Reason, res.StatusCode)) + continue + } + + successCount++ + log.Debug(). + Str("token", truncateToken(deviceToken)). + Str("apns_id", res.ApnsID). + Msg("APNs notification sent successfully") + } + + log.Info(). + Int("total", len(tokens)). + Int("success", successCount). + Int("failed", len(errors)). + Msg("APNs batch send complete") + + if len(errors) > 0 && successCount == 0 { + return fmt.Errorf("all APNs notifications failed: %v", errors) + } + + return nil +} + +// truncateToken returns first 8 chars of token for logging +func truncateToken(token string) string { + if len(token) > 8 { + return token[:8] + "..." + } + return token +} diff --git a/internal/push/client.go b/internal/push/client.go new file mode 100644 index 0000000..2e6c697 --- /dev/null +++ b/internal/push/client.go @@ -0,0 +1,113 @@ +package push + +import ( + "context" + + "github.com/rs/zerolog/log" + + "github.com/treytartt/casera-api/internal/config" +) + +// Platform constants +const ( + PlatformIOS = "ios" + PlatformAndroid = "android" +) + +// Client provides a unified interface for sending push notifications +type Client struct { + apns *APNsClient + fcm *FCMClient +} + +// NewClient creates a new unified push notification client +func NewClient(cfg *config.PushConfig) (*Client, error) { + client := &Client{} + + // Initialize APNs client (iOS) + if cfg.APNSKeyPath != "" && cfg.APNSKeyID != "" && cfg.APNSTeamID != "" { + apnsClient, err := NewAPNsClient(cfg) + if err != nil { + log.Warn().Err(err).Msg("Failed to initialize APNs client - iOS push disabled") + } else { + client.apns = apnsClient + log.Info().Msg("APNs client initialized successfully") + } + } else { + log.Warn().Msg("APNs not configured - iOS push disabled") + } + + // Initialize FCM client (Android) + if cfg.FCMServerKey != "" { + fcmClient, err := NewFCMClient(cfg) + if err != nil { + log.Warn().Err(err).Msg("Failed to initialize FCM client - Android push disabled") + } else { + client.fcm = fcmClient + log.Info().Msg("FCM client initialized successfully") + } + } else { + log.Warn().Msg("FCM not configured - Android push disabled") + } + + return client, nil +} + +// SendToIOS sends a push notification to iOS devices +func (c *Client) SendToIOS(ctx context.Context, tokens []string, title, message string, data map[string]string) error { + if c.apns == nil { + log.Warn().Msg("APNs client not initialized, skipping iOS push") + return nil + } + return c.apns.Send(ctx, tokens, title, message, data) +} + +// SendToAndroid sends a push notification to Android devices +func (c *Client) SendToAndroid(ctx context.Context, tokens []string, title, message string, data map[string]string) error { + if c.fcm == nil { + log.Warn().Msg("FCM client not initialized, skipping Android push") + return nil + } + return c.fcm.Send(ctx, tokens, title, message, data) +} + +// SendToAll sends a push notification to both iOS and Android devices +func (c *Client) SendToAll(ctx context.Context, iosTokens, androidTokens []string, title, message string, data map[string]string) error { + var lastErr error + + if len(iosTokens) > 0 { + if err := c.SendToIOS(ctx, iosTokens, title, message, data); err != nil { + log.Error().Err(err).Msg("Failed to send iOS notifications") + lastErr = err + } + } + + if len(androidTokens) > 0 { + if err := c.SendToAndroid(ctx, androidTokens, title, message, data); err != nil { + log.Error().Err(err).Msg("Failed to send Android notifications") + lastErr = err + } + } + + return lastErr +} + +// IsIOSEnabled returns true if iOS push is configured +func (c *Client) IsIOSEnabled() bool { + return c.apns != nil +} + +// IsAndroidEnabled returns true if Android push is configured +func (c *Client) IsAndroidEnabled() bool { + return c.fcm != nil +} + +// HealthCheck checks if the push services are available +func (c *Client) HealthCheck(ctx context.Context) error { + // For direct clients, we can't easily health check without sending a notification + // Return nil if at least one platform is configured + if c.apns != nil || c.fcm != nil { + return nil + } + return nil +} diff --git a/internal/push/fcm.go b/internal/push/fcm.go new file mode 100644 index 0000000..fcd4d32 --- /dev/null +++ b/internal/push/fcm.go @@ -0,0 +1,139 @@ +package push + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/rs/zerolog/log" + + "github.com/treytartt/casera-api/internal/config" +) + +const fcmEndpoint = "https://fcm.googleapis.com/fcm/send" + +// FCMClient handles direct communication with Firebase Cloud Messaging +type FCMClient struct { + serverKey string + httpClient *http.Client +} + +// FCMMessage represents an FCM message payload +type FCMMessage struct { + To string `json:"to,omitempty"` + RegistrationIDs []string `json:"registration_ids,omitempty"` + Notification *FCMNotification `json:"notification,omitempty"` + Data map[string]string `json:"data,omitempty"` + Priority string `json:"priority,omitempty"` + ContentAvailable bool `json:"content_available,omitempty"` +} + +// FCMNotification represents the notification payload +type FCMNotification struct { + Title string `json:"title,omitempty"` + Body string `json:"body,omitempty"` + Sound string `json:"sound,omitempty"` + Badge string `json:"badge,omitempty"` + Icon string `json:"icon,omitempty"` +} + +// FCMResponse represents the FCM API response +type FCMResponse struct { + MulticastID int64 `json:"multicast_id"` + Success int `json:"success"` + Failure int `json:"failure"` + CanonicalIDs int `json:"canonical_ids"` + Results []FCMResult `json:"results"` +} + +// FCMResult represents a single result in the FCM response +type FCMResult struct { + MessageID string `json:"message_id,omitempty"` + RegistrationID string `json:"registration_id,omitempty"` + Error string `json:"error,omitempty"` +} + +// NewFCMClient creates a new FCM client +func NewFCMClient(cfg *config.PushConfig) (*FCMClient, error) { + if cfg.FCMServerKey == "" { + return nil, fmt.Errorf("FCM server key not configured") + } + + return &FCMClient{ + serverKey: cfg.FCMServerKey, + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + }, nil +} + +// Send sends a push notification to Android devices +func (c *FCMClient) Send(ctx context.Context, tokens []string, title, message string, data map[string]string) error { + if len(tokens) == 0 { + return nil + } + + msg := FCMMessage{ + RegistrationIDs: tokens, + Notification: &FCMNotification{ + Title: title, + Body: message, + Sound: "default", + }, + Data: data, + Priority: "high", + } + + body, err := json.Marshal(msg) + if err != nil { + return fmt.Errorf("failed to marshal FCM message: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", fcmEndpoint, bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("failed to create FCM request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "key="+c.serverKey) + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to send FCM request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("FCM returned status %d", resp.StatusCode) + } + + var fcmResp FCMResponse + if err := json.NewDecoder(resp.Body).Decode(&fcmResp); err != nil { + return fmt.Errorf("failed to decode FCM response: %w", err) + } + + // Log individual results + for i, result := range fcmResp.Results { + if result.Error != "" { + log.Error(). + Str("token", truncateToken(tokens[i])). + Str("error", result.Error). + Msg("FCM notification failed") + } + } + + log.Info(). + Int("total", len(tokens)). + Int("success", fcmResp.Success). + Int("failure", fcmResp.Failure). + Msg("FCM batch send complete") + + if fcmResp.Success == 0 && fcmResp.Failure > 0 { + return fmt.Errorf("all FCM notifications failed") + } + + return nil +} diff --git a/internal/push/gorush.go b/internal/push/gorush.go deleted file mode 100644 index e73d801..0000000 --- a/internal/push/gorush.go +++ /dev/null @@ -1,199 +0,0 @@ -package push - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/rs/zerolog/log" - - "github.com/treytartt/casera-api/internal/config" -) - -// Platform constants -const ( - PlatformIOS = "ios" - PlatformAndroid = "android" -) - -// GorushClient handles communication with Gorush server -type GorushClient struct { - baseURL string - httpClient *http.Client - config *config.PushConfig -} - -// NewGorushClient creates a new Gorush client -func NewGorushClient(cfg *config.PushConfig) *GorushClient { - return &GorushClient{ - baseURL: cfg.GorushURL, - httpClient: &http.Client{ - Timeout: 30 * time.Second, - }, - config: cfg, - } -} - -// PushNotification represents a push notification request -type PushNotification struct { - Tokens []string `json:"tokens"` - Platform int `json:"platform"` // 1 = iOS, 2 = Android - Message string `json:"message"` - Title string `json:"title,omitempty"` - Topic string `json:"topic,omitempty"` // iOS bundle ID - Badge *int `json:"badge,omitempty"` // iOS badge count - Sound string `json:"sound,omitempty"` // Notification sound - ContentAvailable bool `json:"content_available,omitempty"` // iOS background notification - MutableContent bool `json:"mutable_content,omitempty"` // iOS mutable content - Data map[string]string `json:"data,omitempty"` // Custom data payload - Priority string `json:"priority,omitempty"` // high or normal - ThreadID string `json:"thread_id,omitempty"` // iOS thread grouping - CollapseKey string `json:"collapse_key,omitempty"` // Android collapse key -} - -// GorushRequest represents the full Gorush API request -type GorushRequest struct { - Notifications []PushNotification `json:"notifications"` -} - -// GorushResponse represents the Gorush API response -type GorushResponse struct { - Counts int `json:"counts"` - Logs []GorushLog `json:"logs,omitempty"` - Success string `json:"success,omitempty"` -} - -// GorushLog represents a log entry from Gorush -type GorushLog struct { - Type string `json:"type"` - Platform string `json:"platform"` - Token string `json:"token"` - Message string `json:"message"` - Error string `json:"error,omitempty"` -} - -// SendToIOS sends a push notification to iOS devices -func (c *GorushClient) SendToIOS(ctx context.Context, tokens []string, title, message string, data map[string]string) error { - if len(tokens) == 0 { - return nil - } - - notification := PushNotification{ - Tokens: tokens, - Platform: 1, // iOS - Title: title, - Message: message, - Topic: c.config.APNSTopic, - Sound: "default", - MutableContent: true, - Data: data, - Priority: "high", - } - - return c.send(ctx, notification) -} - -// SendToAndroid sends a push notification to Android devices -func (c *GorushClient) SendToAndroid(ctx context.Context, tokens []string, title, message string, data map[string]string) error { - if len(tokens) == 0 { - return nil - } - - notification := PushNotification{ - Tokens: tokens, - Platform: 2, // Android - Title: title, - Message: message, - Data: data, - Priority: "high", - } - - return c.send(ctx, notification) -} - -// SendToAll sends a push notification to both iOS and Android devices -func (c *GorushClient) SendToAll(ctx context.Context, iosTokens, androidTokens []string, title, message string, data map[string]string) error { - var errs []error - - if len(iosTokens) > 0 { - if err := c.SendToIOS(ctx, iosTokens, title, message, data); err != nil { - errs = append(errs, fmt.Errorf("iOS: %w", err)) - } - } - - if len(androidTokens) > 0 { - if err := c.SendToAndroid(ctx, androidTokens, title, message, data); err != nil { - errs = append(errs, fmt.Errorf("Android: %w", err)) - } - } - - if len(errs) > 0 { - return fmt.Errorf("push notification errors: %v", errs) - } - - return nil -} - -// send sends the notification to Gorush -func (c *GorushClient) send(ctx context.Context, notification PushNotification) error { - req := GorushRequest{ - Notifications: []PushNotification{notification}, - } - - body, err := json.Marshal(req) - if err != nil { - return fmt.Errorf("failed to marshal request: %w", err) - } - - httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/api/push", bytes.NewReader(body)) - if err != nil { - return fmt.Errorf("failed to create request: %w", err) - } - - httpReq.Header.Set("Content-Type", "application/json") - - resp, err := c.httpClient.Do(httpReq) - if err != nil { - return fmt.Errorf("failed to send request: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("gorush returned status %d", resp.StatusCode) - } - - var gorushResp GorushResponse - if err := json.NewDecoder(resp.Body).Decode(&gorushResp); err != nil { - return fmt.Errorf("failed to decode response: %w", err) - } - - log.Debug(). - Int("counts", gorushResp.Counts). - Int("tokens", len(notification.Tokens)). - Msg("Push notification sent") - - return nil -} - -// HealthCheck checks if Gorush is healthy -func (c *GorushClient) HealthCheck(ctx context.Context) error { - httpReq, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/api/stat/go", nil) - if err != nil { - return err - } - - resp, err := c.httpClient.Do(httpReq) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("gorush health check failed: status %d", resp.StatusCode) - } - - return nil -} diff --git a/internal/router/router.go b/internal/router/router.go index eb2b3f1..9988f28 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -27,7 +27,7 @@ type Dependencies struct { Config *config.Config EmailService *services.EmailService PDFService *services.PDFService - PushClient interface{} // *push.GorushClient - optional + PushClient *push.Client // Direct APNs/FCM client StorageService *services.StorageService } @@ -66,14 +66,6 @@ func SetupRouter(deps *Dependencies) *gin.Engine { notificationRepo := repositories.NewNotificationRepository(deps.DB) subscriptionRepo := repositories.NewSubscriptionRepository(deps.DB) - // Initialize push client (optional) - var gorushClient *push.GorushClient - if deps.PushClient != nil { - if gc, ok := deps.PushClient.(*push.GorushClient); ok { - gorushClient = gc - } - } - // Initialize services authService := services.NewAuthService(userRepo, cfg) userService := services.NewUserService(userRepo) @@ -82,7 +74,7 @@ func SetupRouter(deps *Dependencies) *gin.Engine { taskService := services.NewTaskService(taskRepo, residenceRepo) contractorService := services.NewContractorService(contractorRepo, residenceRepo) documentService := services.NewDocumentService(documentRepo, residenceRepo) - notificationService := services.NewNotificationService(notificationRepo, gorushClient) + notificationService := services.NewNotificationService(notificationRepo, deps.PushClient) // Wire up notification and email services to task service (for task completion notifications) taskService.SetNotificationService(notificationService) @@ -116,11 +108,7 @@ func SetupRouter(deps *Dependencies) *gin.Engine { // Set up admin routes (separate auth system) adminDeps := &admin.Dependencies{ EmailService: deps.EmailService, - } - if deps.PushClient != nil { - if gc, ok := deps.PushClient.(*push.GorushClient); ok { - adminDeps.PushClient = gc - } + PushClient: deps.PushClient, } admin.SetupRoutes(r, deps.DB, cfg, adminDeps) diff --git a/internal/services/notification_service.go b/internal/services/notification_service.go index 39b9316..322d712 100644 --- a/internal/services/notification_service.go +++ b/internal/services/notification_service.go @@ -22,14 +22,14 @@ var ( // NotificationService handles notification business logic type NotificationService struct { notificationRepo *repositories.NotificationRepository - gorushClient *push.GorushClient + pushClient *push.Client } // NewNotificationService creates a new notification service -func NewNotificationService(notificationRepo *repositories.NotificationRepository, gorushClient *push.GorushClient) *NotificationService { +func NewNotificationService(notificationRepo *repositories.NotificationRepository, pushClient *push.Client) *NotificationService { return &NotificationService{ notificationRepo: notificationRepo, - gorushClient: gorushClient, + pushClient: pushClient, } } @@ -123,8 +123,8 @@ func (s *NotificationService) CreateAndSendNotification(ctx context.Context, use pushData["notification_id"] = string(rune(notification.ID)) // Send push notification - if s.gorushClient != nil { - err = s.gorushClient.SendToAll(ctx, iosTokens, androidTokens, title, body, pushData) + if s.pushClient != nil { + err = s.pushClient.SendToAll(ctx, iosTokens, androidTokens, title, body, pushData) if err != nil { s.notificationRepo.SetError(notification.ID, err.Error()) return err diff --git a/internal/worker/jobs/handler.go b/internal/worker/jobs/handler.go index 22f7ec5..fb5c29d 100644 --- a/internal/worker/jobs/handler.go +++ b/internal/worker/jobs/handler.go @@ -28,13 +28,13 @@ const ( // Handler handles background job processing type Handler struct { db *gorm.DB - pushClient *push.GorushClient + pushClient *push.Client emailService *services.EmailService config *config.Config } // NewHandler creates a new job handler -func NewHandler(db *gorm.DB, pushClient *push.GorushClient, emailService *services.EmailService, cfg *config.Config) *Handler { +func NewHandler(db *gorm.DB, pushClient *push.Client, emailService *services.EmailService, cfg *config.Config) *Handler { return &Handler{ db: db, pushClient: pushClient,