diff --git a/backend/README.md b/backend/BACKEND.md similarity index 100% rename from backend/README.md rename to backend/BACKEND.md diff --git a/backend/api b/backend/api new file mode 100755 index 0000000..823bbf8 Binary files /dev/null and b/backend/api differ diff --git a/backend/cmd/manual_migrate/main.go b/backend/cmd/manual_migrate/main.go index e2102e2..eafa9bf 100644 --- a/backend/cmd/manual_migrate/main.go +++ b/backend/cmd/manual_migrate/main.go @@ -44,50 +44,62 @@ func main() { } defer db.Close() - // Try multiple paths - paths := []string{ - "migrations/023_ensure_seeded_admins_roles.sql", - "backend/migrations/023_ensure_seeded_admins_roles.sql", - "../migrations/023_ensure_seeded_admins_roles.sql", - "/home/yamamoto/lab/gohorsejobs/backend/migrations/023_ensure_seeded_admins_roles.sql", + // List of migrations to run (in order) + migrations := []string{ + "024_create_external_services_credentials.sql", + "025_create_chat_tables.sql", + "026_create_system_settings.sql", + "027_create_email_system.sql", + "028_add_avatar_url_to_users.sql", } - var content []byte - var readErr error + for _, migFile := range migrations { + log.Printf("Processing migration: %s", migFile) - for _, p := range paths { - content, readErr = os.ReadFile(p) - if readErr == nil { - log.Printf("Found migration at: %s", p) - break + // Try multiple paths + paths := []string{ + "migrations/" + migFile, + "backend/migrations/" + migFile, + "../migrations/" + migFile, + "/home/yamamoto/lab/gohorsejobs/backend/migrations/" + migFile, } - } - if content == nil { - log.Fatalf("Could not find migration file. Last error: %v", readErr) - } + var content []byte + var readErr error - statements := strings.Split(string(content), ";") - for _, stmt := range statements { - trimmed := strings.TrimSpace(stmt) - if trimmed == "" { - continue - } - log.Printf("Executing: %s", trimmed) - _, err = db.Exec(trimmed) - if err != nil { - // Log but maybe don't fail if it's just "already exists"? - // But we want to be strict. - // If it's "relation already exists", we might ignore. - if strings.Contains(err.Error(), "already exists") { - log.Printf("Warning (ignored): %v", err) - } else { - log.Printf("FAILED executing: %s\nError: %v", trimmed, err) - // Fail? - // log.Fatal(err) + for _, p := range paths { + content, readErr = os.ReadFile(p) + if readErr == nil { + log.Printf("Found migration at: %s", p) + break } } + + if content == nil { + log.Fatalf("Could not find migration file %s. Last error: %v", migFile, readErr) + } + + statements := strings.Split(string(content), ";") + for _, stmt := range statements { + trimmed := strings.TrimSpace(stmt) + if trimmed == "" { + continue + } + log.Printf("Executing: %s", trimmed) + _, err = db.Exec(trimmed) + if err != nil { + if strings.Contains(err.Error(), "already exists") { + log.Printf("Warning (ignored): %v", err) + } else { + log.Printf("FAILED executing: %s\nError: %v", trimmed, err) + // Verify if we should stop. For now, continue best effort or fail? + // Use fatal for critical schema errors not "already exists" + log.Fatal(err) + } + } + } + log.Printf("Migration %s applied successfully", migFile) } - fmt.Println("Migration 017 applied successfully") + fmt.Println("All requested migrations applied.") } diff --git a/backend/go.mod b/backend/go.mod index fa7ea14..10f0697 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -3,7 +3,9 @@ module github.com/rede5/gohorsejobs/backend go 1.24.0 require ( + firebase.google.com/go/v4 v4.18.0 github.com/DATA-DOG/go-sqlmock v1.5.2 + github.com/appwrite/sdk-for-go v0.16.0 github.com/aws/aws-sdk-go-v2 v1.41.0 github.com/aws/aws-sdk-go-v2/config v1.32.5 github.com/aws/aws-sdk-go-v2/credentials v1.19.5 @@ -11,14 +13,29 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.0 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.11.1 github.com/swaggo/http-swagger/v2 v2.0.2 github.com/swaggo/swag v1.16.6 - golang.org/x/crypto v0.45.0 + golang.org/x/crypto v0.46.0 + google.golang.org/api v0.258.0 ) require ( + cel.dev/expr v0.24.0 // indirect + cloud.google.com/go v0.121.0 // indirect + cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.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 + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.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/KyleBanks/depth v1.2.1 // indirect + github.com/MicahParks/keyfunc v1.9.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect @@ -34,7 +51,15 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect github.com/aws/smithy-go v1.24.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.22.3 // indirect github.com/go-openapi/jsonreference v0.21.3 // indirect github.com/go-openapi/spec v0.22.1 // indirect @@ -45,12 +70,41 @@ require ( github.com/go-openapi/swag/stringutils v0.25.4 // indirect github.com/go-openapi/swag/typeutils v0.25.4 // indirect github.com/go-openapi/swag/yamlutils v0.25.4 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/objx v0.1.0 // 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/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rabbitmq/amqp091-go v1.10.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/swaggo/files/v2 v2.0.2 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/mod v0.30.0 // indirect - golang.org/x/sync v0.18.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.39.0 // indirect - gopkg.in/yaml.v3 v3.0.0 // indirect + google.golang.org/appengine/v2 v2.0.6 // indirect + google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/grpc v1.77.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index bddc1bc..d6c0f0b 100755 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,7 +1,45 @@ +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/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.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= +cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +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.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +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/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= +cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= +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= +cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= +cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= +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/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +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/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= +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/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +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/appwrite/sdk-for-go v0.16.0 h1:btKIB+4+bEYutUBaYdHPsBGupI6rio25CzJgI2wt2B0= +github.com/appwrite/sdk-for-go v0.16.0/go.mod h1:aFiOAbfOzGS3811eMCt3T9WDBvjvPVAfOjw10Vghi4E= github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= @@ -40,9 +78,29 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= -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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +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/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/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-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8= github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo= github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= @@ -70,39 +128,141 @@ github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxE github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +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.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -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/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU= github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0= github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg= github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +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.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +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-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.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +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.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +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/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +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= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc= +google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww= +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-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +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.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +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/backend/internal/api/handlers/admin_handlers.go b/backend/internal/api/handlers/admin_handlers.go index ea23491..4a5c0a0 100644 --- a/backend/internal/api/handlers/admin_handlers.go +++ b/backend/internal/api/handlers/admin_handlers.go @@ -11,9 +11,10 @@ import ( ) type AdminHandlers struct { - adminService *services.AdminService - auditService *services.AuditService - jobService *services.JobService + adminService *services.AdminService + auditService *services.AuditService + jobService *services.JobService + cloudflareService *services.CloudflareService } type RoleAccess struct { @@ -41,11 +42,12 @@ type UpdateTagRequest struct { Active *bool `json:"active,omitempty"` } -func NewAdminHandlers(adminService *services.AdminService, auditService *services.AuditService, jobService *services.JobService) *AdminHandlers { +func NewAdminHandlers(adminService *services.AdminService, auditService *services.AuditService, jobService *services.JobService, cloudflareService *services.CloudflareService) *AdminHandlers { return &AdminHandlers{ - adminService: adminService, - auditService: auditService, - jobService: jobService, + adminService: adminService, + auditService: auditService, + jobService: jobService, + cloudflareService: cloudflareService, } } @@ -380,3 +382,140 @@ func (h *AdminHandlers) DeleteCompany(w http.ResponseWriter, r *http.Request) { } w.WriteHeader(http.StatusNoContent) } + +func (h *AdminHandlers) PurgeCache(w http.ResponseWriter, r *http.Request) { + if err := h.cloudflareService.PurgeCache(r.Context()); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"message": "Cache purge requested"}) +} + +// ============================================================================ +// Email Templates CRUD Handlers +// ============================================================================ + +func (h *AdminHandlers) ListEmailTemplates(w http.ResponseWriter, r *http.Request) { + templates, err := h.adminService.ListEmailTemplates(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(templates) +} + +func (h *AdminHandlers) GetEmailTemplate(w http.ResponseWriter, r *http.Request) { + slug := r.PathValue("slug") + tmpl, err := h.adminService.GetEmailTemplate(r.Context(), slug) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if tmpl == nil { + http.Error(w, "Template not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(tmpl) +} + +func (h *AdminHandlers) CreateEmailTemplate(w http.ResponseWriter, r *http.Request) { + var req dto.CreateEmailTemplateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + if req.Slug == "" || req.Subject == "" { + http.Error(w, "Slug and subject are required", http.StatusBadRequest) + return + } + + tmpl, err := h.adminService.CreateEmailTemplate(r.Context(), req) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(tmpl) +} + +func (h *AdminHandlers) UpdateEmailTemplate(w http.ResponseWriter, r *http.Request) { + slug := r.PathValue("slug") + var req dto.UpdateEmailTemplateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + tmpl, err := h.adminService.UpdateEmailTemplate(r.Context(), slug, req) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(tmpl) +} + +func (h *AdminHandlers) DeleteEmailTemplate(w http.ResponseWriter, r *http.Request) { + slug := r.PathValue("slug") + if err := h.adminService.DeleteEmailTemplate(r.Context(), slug); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) +} + +// ============================================================================ +// Email Settings Handlers +// ============================================================================ + +func (h *AdminHandlers) GetEmailSettings(w http.ResponseWriter, r *http.Request) { + settings, err := h.adminService.GetEmailSettings(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if settings == nil { + // Return empty object with defaults + settings = &dto.EmailSettingsDTO{ + Provider: "smtp", + SMTPSecure: true, + SenderName: "GoHorse Jobs", + SenderEmail: "no-reply@gohorsejobs.com", + } + } + // Mask password in response + if settings.SMTPPass != nil { + masked := "********" + settings.SMTPPass = &masked + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(settings) +} + +func (h *AdminHandlers) UpdateEmailSettings(w http.ResponseWriter, r *http.Request) { + var req dto.UpdateEmailSettingsRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + settings, err := h.adminService.UpdateEmailSettings(r.Context(), req) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Mask password in response + if settings.SMTPPass != nil { + masked := "********" + settings.SMTPPass = &masked + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(settings) +} diff --git a/backend/internal/api/handlers/chat_handlers.go b/backend/internal/api/handlers/chat_handlers.go new file mode 100644 index 0000000..c72935b --- /dev/null +++ b/backend/internal/api/handlers/chat_handlers.go @@ -0,0 +1,146 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "github.com/rede5/gohorsejobs/backend/internal/api/middleware" + "github.com/rede5/gohorsejobs/backend/internal/services" +) + +type ChatHandlers struct { + chatService *services.ChatService +} + +func NewChatHandlers(s *services.ChatService) *ChatHandlers { + return &ChatHandlers{chatService: s} +} + +// ListConversations lists all conversations for the authenticated user +// @Summary List Conversations +// @Description List chat conversations for candidate or company +// @Tags Chat +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {array} services.Conversation +// @Router /api/v1/conversations [get] +func (h *ChatHandlers) ListConversations(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + userID, _ := ctx.Value(middleware.ContextUserID).(string) + tenantID, _ := ctx.Value(middleware.ContextTenantID).(string) + roles := middleware.ExtractRoles(ctx.Value(middleware.ContextRoles)) + + if userID == "" { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + isCandidate := false + for _, r := range roles { + if r == "candidate" || r == "CANDIDATE" { + isCandidate = true + break + } + } + + // Logic: If user has company (tenantID), prefer company view? + // But a user might be both candidate and admin (unlikely in this domain model). + // If tenantID is present, assume acting as Company. + if tenantID != "" { + isCandidate = false + } else { + isCandidate = true + } + + convs, err := h.chatService.ListConversations(ctx, userID, tenantID, isCandidate) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(convs) +} + +// ListMessages lists messages in a conversation +// @Summary List Messages +// @Description Get message history for a conversation +// @Tags Chat +// @Accept json +// @Produce json +// @Param id path string true "Conversation ID" +// @Security BearerAuth +// @Success 200 {array} services.Message +// @Router /api/v1/conversations/{id}/messages [get] +func (h *ChatHandlers) ListMessages(w http.ResponseWriter, r *http.Request) { + conversationID := r.PathValue("id") + if conversationID == "" { + http.Error(w, "Conversation ID required", http.StatusBadRequest) + return + } + + msgs, err := h.chatService.ListMessages(r.Context(), conversationID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Mark "IsMine" + userID, _ := r.Context().Value(middleware.ContextUserID).(string) + for i := range msgs { + if msgs[i].SenderID == userID { + msgs[i].IsMine = true + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(msgs) +} + +// SendMessage sends a new message +// @Summary Send Message +// @Description Send a message to a conversation +// @Tags Chat +// @Accept json +// @Produce json +// @Param id path string true "Conversation ID" +// @Param request body map[string]string true "Message Content" +// @Security BearerAuth +// @Success 200 {object} services.Message +// @Router /api/v1/conversations/{id}/messages [post] +func (h *ChatHandlers) SendMessage(w http.ResponseWriter, r *http.Request) { + conversationID := r.PathValue("id") + if conversationID == "" { + http.Error(w, "Conversation ID required", http.StatusBadRequest) + return + } + + userID, _ := r.Context().Value(middleware.ContextUserID).(string) + if userID == "" { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + var req struct { + Content string `json:"content"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid body", http.StatusBadRequest) + return + } + if req.Content == "" { + http.Error(w, "Content required", http.StatusBadRequest) + return + } + + msg, err := h.chatService.SendMessage(r.Context(), userID, conversationID, req.Content) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + msg.IsMine = true + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(msg) +} diff --git a/backend/internal/api/handlers/settings_handler.go b/backend/internal/api/handlers/settings_handler.go new file mode 100644 index 0000000..af3a27b --- /dev/null +++ b/backend/internal/api/handlers/settings_handler.go @@ -0,0 +1,86 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "github.com/rede5/gohorsejobs/backend/internal/api/middleware" + "github.com/rede5/gohorsejobs/backend/internal/services" +) + +type SettingsHandler struct { + settingsService *services.SettingsService +} + +func NewSettingsHandler(s *services.SettingsService) *SettingsHandler { + return &SettingsHandler{settingsService: s} +} + +// GetSettings retrieves a setting by key. +// Public for 'theme', restricted for others. +func (h *SettingsHandler) GetSettings(w http.ResponseWriter, r *http.Request) { + key := r.PathValue("key") + if key == "" { + http.Error(w, "Key is required", http.StatusBadRequest) + return + } + + // Security Check + if key != "theme" { + // Require Admin + roles := middleware.ExtractRoles(r.Context().Value(middleware.ContextRoles)) + isAdmin := false + for _, role := range roles { + if role == "ADMIN" || role == "SUPERADMIN" || role == "admin" || role == "superadmin" { + isAdmin = true + break + } + } + if !isAdmin { + http.Error(w, "Forbidden: Access restricted", http.StatusForbidden) + return + } + } + + val, err := h.settingsService.GetSettings(r.Context(), key) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if val == nil { + // Return empty object for theme instead of 404 to avoid errors in frontend + if key == "theme" { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{}")) + return + } + http.Error(w, "Setting not found", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(val) +} + +// SaveSettings saves a setting. Admin only. +func (h *SettingsHandler) SaveSettings(w http.ResponseWriter, r *http.Request) { + key := r.PathValue("key") + if key == "" { + http.Error(w, "Key is required", http.StatusBadRequest) + return + } + + var req interface{} + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid value", http.StatusBadRequest) + return + } + + if err := h.settingsService.SaveSettings(r.Context(), key, req); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"message": "Setting saved"}) +} diff --git a/backend/internal/api/handlers/storage_handler.go b/backend/internal/api/handlers/storage_handler.go new file mode 100644 index 0000000..7cef46d --- /dev/null +++ b/backend/internal/api/handlers/storage_handler.go @@ -0,0 +1,83 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "net/http" + "path/filepath" + "strings" + "time" + + "github.com/rede5/gohorsejobs/backend/internal/api/middleware" + "github.com/rede5/gohorsejobs/backend/internal/services" +) + +type StorageHandler struct { + storageService *services.StorageService +} + +func NewStorageHandler(s *services.StorageService) *StorageHandler { + return &StorageHandler{storageService: s} +} + +// GetUploadURL returns a pre-signed URL for uploading a file. +// Clients upload directly to this URL. +// Query Params: +// - filename: Original filename +// - contentType: MIME type +// - folder: Optional folder (e.g. 'avatars', 'resumes') +func (h *StorageHandler) GetUploadURL(w http.ResponseWriter, r *http.Request) { + // Authentication required + userIDVal := r.Context().Value(middleware.ContextUserID) + userID, ok := userIDVal.(string) + if !ok || userID == "" { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + filename := r.URL.Query().Get("filename") + contentType := r.URL.Query().Get("contentType") + folder := r.URL.Query().Get("folder") + + if filename == "" { + http.Error(w, "Filename is required", http.StatusBadRequest) + return + } + if folder == "" { + folder = "uploads" // Default + } + + // Validate folder + validFolders := map[string]bool{"avatars": true, "resumes": true, "logos": true, "uploads": true} + if !validFolders[folder] { + http.Error(w, "Invalid folder", http.StatusBadRequest) + return + } + + // Generate a unique key + ext := filepath.Ext(filename) + if ext == "" { + // Attempt to guess from contentType if needed, or just allow no ext + } + + // Key format: {folder}/{userID}/{timestamp}_{random}{ext} + // Using user ID ensures isolation if needed, or use a UUID. + key := fmt.Sprintf("%s/%s/%d_%s", folder, userID, time.Now().Unix(), strings.ReplaceAll(filename, " ", "_")) + + url, err := h.storageService.GetPresignedUploadURL(r.Context(), key, contentType) + if err != nil { + // If credentials missing, log error and return 500 + // "storage credentials incomplete" might mean admin needs to configure them. + http.Error(w, "Failed to generate upload URL: "+err.Error(), http.StatusInternalServerError) + return + } + + // Return simple JSON + resp := map[string]string{ + "url": url, + "key": key, // Client needs key to save to DB profile + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) +} diff --git a/backend/internal/core/domain/entity/email.go b/backend/internal/core/domain/entity/email.go new file mode 100644 index 0000000..24aba01 --- /dev/null +++ b/backend/internal/core/domain/entity/email.go @@ -0,0 +1,28 @@ +package entity + +import "time" + +type EmailTemplate struct { + ID string `json:"id"` + Slug string `json:"slug"` + Subject string `json:"subject"` + BodyHTML string `json:"body_html"` + Variables []string `json:"variables"` // From JSONB + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type EmailSettings struct { + ID string `json:"id"` + Provider string `json:"provider"` + SMTPHost *string `json:"smtp_host,omitempty"` + SMTPPort *int `json:"smtp_port,omitempty"` + SMTPUser *string `json:"smtp_user,omitempty"` + SMTPPass *string `json:"smtp_pass,omitempty"` + SMTPSecure bool `json:"smtp_secure"` + SenderName string `json:"sender_name"` + SenderEmail string `json:"sender_email"` + AMQPURL *string `json:"amqp_url,omitempty"` + IsActive bool `json:"is_active"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/backend/internal/core/domain/entity/user.go b/backend/internal/core/domain/entity/user.go index 50c4f3e..580c52c 100644 --- a/backend/internal/core/domain/entity/user.go +++ b/backend/internal/core/domain/entity/user.go @@ -27,6 +27,7 @@ type User struct { Name string `json:"name"` Email string `json:"email"` PasswordHash string `json:"-"` + AvatarUrl string `json:"avatar_url"` Roles []Role `json:"roles"` Status string `json:"status"` // "ACTIVE", "INACTIVE" Metadata map[string]interface{} `json:"metadata"` diff --git a/backend/internal/core/dto/user_auth.go b/backend/internal/core/dto/user_auth.go index 6b446e8..d43b9a0 100644 --- a/backend/internal/core/dto/user_auth.go +++ b/backend/internal/core/dto/user_auth.go @@ -22,11 +22,12 @@ type CreateUserRequest struct { } type UpdateUserRequest struct { - Name *string `json:"name,omitempty"` - Email *string `json:"email,omitempty"` - Active *bool `json:"active,omitempty"` - Status *string `json:"status,omitempty"` - Roles *[]string `json:"roles,omitempty"` + Name *string `json:"name,omitempty"` + Email *string `json:"email,omitempty"` + Active *bool `json:"active,omitempty"` + Status *string `json:"status,omitempty"` + Roles *[]string `json:"roles,omitempty"` + AvatarUrl *string `json:"avatarUrl,omitempty"` } type UserResponse struct { @@ -35,6 +36,7 @@ type UserResponse struct { Email string `json:"email"` Roles []string `json:"roles"` Status string `json:"status"` + AvatarUrl string `json:"avatar_url"` CreatedAt time.Time `json:"created_at"` } diff --git a/backend/internal/core/ports/repositories.go b/backend/internal/core/ports/repositories.go index 8e2026e..82c21bc 100644 --- a/backend/internal/core/ports/repositories.go +++ b/backend/internal/core/ports/repositories.go @@ -23,3 +23,16 @@ type UserRepository interface { Update(ctx context.Context, user *entity.User) (*entity.User, error) Delete(ctx context.Context, id string) error } + +type EmailRepository interface { + // Templates + ListTemplates(ctx context.Context) ([]*entity.EmailTemplate, error) + GetTemplate(ctx context.Context, slug string) (*entity.EmailTemplate, error) + CreateTemplate(ctx context.Context, tmpl *entity.EmailTemplate) error + UpdateTemplate(ctx context.Context, tmpl *entity.EmailTemplate) error + DeleteTemplate(ctx context.Context, slug string) error + + // Settings + GetSettings(ctx context.Context) (*entity.EmailSettings, error) + UpdateSettings(ctx context.Context, settings *entity.EmailSettings) error +} diff --git a/backend/internal/core/ports/services.go b/backend/internal/core/ports/services.go index bddac45..c22ed10 100644 --- a/backend/internal/core/ports/services.go +++ b/backend/internal/core/ports/services.go @@ -1,5 +1,7 @@ package ports +import "context" + // AuthService defines the interface for authentication logic. type AuthService interface { HashPassword(password string) (string, error) @@ -7,3 +9,8 @@ type AuthService interface { GenerateToken(userID, tenantID string, roles []string) (string, error) ValidateToken(token string) (map[string]interface{}, error) } + +// EmailService defines the interface for sending emails. +type EmailService interface { + SendTemplateEmail(ctx context.Context, to, templateSlug string, variables map[string]interface{}) error +} diff --git a/backend/internal/core/usecases/auth/register_candidate.go b/backend/internal/core/usecases/auth/register_candidate.go index 4504a24..6f5614e 100644 --- a/backend/internal/core/usecases/auth/register_candidate.go +++ b/backend/internal/core/usecases/auth/register_candidate.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "log" "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" "github.com/rede5/gohorsejobs/backend/internal/core/dto" @@ -11,16 +12,18 @@ import ( ) type RegisterCandidateUseCase struct { - userRepo ports.UserRepository - companyRepo ports.CompanyRepository - authService ports.AuthService + userRepo ports.UserRepository + companyRepo ports.CompanyRepository + authService ports.AuthService + emailService ports.EmailService } -func NewRegisterCandidateUseCase(uRepo ports.UserRepository, cRepo ports.CompanyRepository, auth ports.AuthService) *RegisterCandidateUseCase { +func NewRegisterCandidateUseCase(uRepo ports.UserRepository, cRepo ports.CompanyRepository, auth ports.AuthService, email ports.EmailService) *RegisterCandidateUseCase { return &RegisterCandidateUseCase{ - userRepo: uRepo, - companyRepo: cRepo, - authService: auth, + userRepo: uRepo, + companyRepo: cRepo, + authService: auth, + emailService: email, } } @@ -80,6 +83,19 @@ func (uc *RegisterCandidateUseCase) Execute(ctx context.Context, input dto.Regis return nil, err } + // 6. Send Welcome Email (Async) + if uc.emailService != nil { + // Non-blocking call or handled by service internally (RabbitMQ pub is fast) + vars := map[string]interface{}{ + "name": saved.Name, + "link": "https://gohorsejobs.com/dashboard", // TODO: make dynamic + } + if err := uc.emailService.SendTemplateEmail(ctx, saved.Email, "welcome-candidate", vars); err != nil { + log.Printf("[RegisterCandidate] Failed to queue welcome email: %v", err) + // Don't fail registration + } + } + return &dto.AuthResponse{ Token: token, User: dto.UserResponse{ diff --git a/backend/internal/core/usecases/auth/register_candidate_test.go b/backend/internal/core/usecases/auth/register_candidate_test.go index dd95582..8b0ef35 100644 --- a/backend/internal/core/usecases/auth/register_candidate_test.go +++ b/backend/internal/core/usecases/auth/register_candidate_test.go @@ -98,7 +98,7 @@ func TestRegisterCandidateUseCase_Execute(t *testing.T) { companyRepo := &MockCompanyRepo{} authSvc := &MockAuthService{} - uc := auth.NewRegisterCandidateUseCase(userRepo, companyRepo, authSvc) + uc := auth.NewRegisterCandidateUseCase(userRepo, companyRepo, authSvc, nil) input := dto.RegisterCandidateRequest{ Name: "John Doe", @@ -132,7 +132,7 @@ func TestRegisterCandidateUseCase_Execute(t *testing.T) { } companyRepo := &MockCompanyRepo{} authSvc := &MockAuthService{} - uc := auth.NewRegisterCandidateUseCase(userRepo, companyRepo, authSvc) + uc := auth.NewRegisterCandidateUseCase(userRepo, companyRepo, authSvc, nil) _, err := uc.Execute(context.Background(), dto.RegisterCandidateRequest{Email: "exists@example.com", Password: "123"}) @@ -155,7 +155,7 @@ func TestRegisterCandidateUseCase_Execute(t *testing.T) { } companyRepo := &MockCompanyRepo{} authSvc := &MockAuthService{} - uc := auth.NewRegisterCandidateUseCase(userRepo, companyRepo, authSvc) + uc := auth.NewRegisterCandidateUseCase(userRepo, companyRepo, authSvc, nil) uc.Execute(context.Background(), dto.RegisterCandidateRequest{Username: "coder", Phone: "999"}) @@ -180,7 +180,7 @@ func TestRegisterCandidateUseCase_Execute(t *testing.T) { }, } authSvc := &MockAuthService{} - uc := auth.NewRegisterCandidateUseCase(userRepo, companyRepo, authSvc) + uc := auth.NewRegisterCandidateUseCase(userRepo, companyRepo, authSvc, nil) uc.Execute(context.Background(), dto.RegisterCandidateRequest{Name: "Test User", Email: "test@test.com", Password: "123"}) diff --git a/backend/internal/core/usecases/user/update_user.go b/backend/internal/core/usecases/user/update_user.go index 5d8ba88..fab42be 100644 --- a/backend/internal/core/usecases/user/update_user.go +++ b/backend/internal/core/usecases/user/update_user.go @@ -58,6 +58,9 @@ func (uc *UpdateUserUseCase) Execute(ctx context.Context, id, tenantID string, i user.Roles = append(user.Roles, entity.Role{Name: r}) } } + if input.AvatarUrl != nil { + user.AvatarUrl = *input.AvatarUrl + } // 4. Save updated, err := uc.userRepo.Update(ctx, user) @@ -77,6 +80,7 @@ func (uc *UpdateUserUseCase) Execute(ctx context.Context, id, tenantID string, i Email: updated.Email, Roles: roles, Status: updated.Status, + AvatarUrl: updated.AvatarUrl, CreatedAt: updated.CreatedAt, }, nil } diff --git a/backend/internal/dto/email.go b/backend/internal/dto/email.go new file mode 100644 index 0000000..6adf60b --- /dev/null +++ b/backend/internal/dto/email.go @@ -0,0 +1,58 @@ +package dto + +import "time" + +// EmailTemplateDTO represents an email template for API responses +type EmailTemplateDTO struct { + ID string `json:"id"` + Slug string `json:"slug"` + Subject string `json:"subject"` + BodyHTML string `json:"body_html"` + Variables []string `json:"variables"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// CreateEmailTemplateRequest for creating new templates +type CreateEmailTemplateRequest struct { + Slug string `json:"slug"` + Subject string `json:"subject"` + BodyHTML string `json:"body_html"` + Variables []string `json:"variables"` +} + +// UpdateEmailTemplateRequest for updating existing templates +type UpdateEmailTemplateRequest struct { + Subject *string `json:"subject,omitempty"` + BodyHTML *string `json:"body_html,omitempty"` + Variables *[]string `json:"variables,omitempty"` +} + +// EmailSettingsDTO represents email settings for API responses +type EmailSettingsDTO struct { + ID string `json:"id"` + Provider string `json:"provider"` + SMTPHost *string `json:"smtp_host,omitempty"` + SMTPPort *int `json:"smtp_port,omitempty"` + SMTPUser *string `json:"smtp_user,omitempty"` + SMTPPass *string `json:"smtp_pass,omitempty"` // Should be masked in responses + SMTPSecure bool `json:"smtp_secure"` + SenderName string `json:"sender_name"` + SenderEmail string `json:"sender_email"` + AMQPURL *string `json:"amqp_url,omitempty"` // LavinMQ connection + IsActive bool `json:"is_active"` + UpdatedAt time.Time `json:"updated_at"` +} + +// UpdateEmailSettingsRequest for updating email settings +type UpdateEmailSettingsRequest struct { + Provider *string `json:"provider,omitempty"` + SMTPHost *string `json:"smtp_host,omitempty"` + SMTPPort *int `json:"smtp_port,omitempty"` + SMTPUser *string `json:"smtp_user,omitempty"` + SMTPPass *string `json:"smtp_pass,omitempty"` + SMTPSecure *bool `json:"smtp_secure,omitempty"` + SenderName *string `json:"sender_name,omitempty"` + SenderEmail *string `json:"sender_email,omitempty"` + AMQPURL *string `json:"amqp_url,omitempty"` +} diff --git a/backend/internal/infrastructure/persistence/postgres/email_repository.go b/backend/internal/infrastructure/persistence/postgres/email_repository.go new file mode 100644 index 0000000..e02674b --- /dev/null +++ b/backend/internal/infrastructure/persistence/postgres/email_repository.go @@ -0,0 +1,154 @@ +package postgres + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "time" + + "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" +) + +type EmailRepository struct { + db *sql.DB +} + +func NewEmailRepository(db *sql.DB) *EmailRepository { + return &EmailRepository{db: db} +} + +// Templates + +func (r *EmailRepository) ListTemplates(ctx context.Context) ([]*entity.EmailTemplate, error) { + query := `SELECT id, slug, subject, body_html, variables, created_at, updated_at FROM email_templates ORDER BY slug` + rows, err := r.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var templates []*entity.EmailTemplate + for rows.Next() { + t := &entity.EmailTemplate{} + var varsJSON []byte + if err := rows.Scan(&t.ID, &t.Slug, &t.Subject, &t.BodyHTML, &varsJSON, &t.CreatedAt, &t.UpdatedAt); err != nil { + return nil, err + } + if len(varsJSON) > 0 { + json.Unmarshal(varsJSON, &t.Variables) + } + templates = append(templates, t) + } + return templates, nil +} + +func (r *EmailRepository) GetTemplate(ctx context.Context, slug string) (*entity.EmailTemplate, error) { + query := `SELECT id, slug, subject, body_html, variables, created_at, updated_at FROM email_templates WHERE slug = $1` + row := r.db.QueryRowContext(ctx, query, slug) + + t := &entity.EmailTemplate{} + var varsJSON []byte + err := row.Scan(&t.ID, &t.Slug, &t.Subject, &t.BodyHTML, &varsJSON, &t.CreatedAt, &t.UpdatedAt) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + if len(varsJSON) > 0 { + json.Unmarshal(varsJSON, &t.Variables) + } + return t, nil +} + +func (r *EmailRepository) CreateTemplate(ctx context.Context, tmpl *entity.EmailTemplate) error { + query := `INSERT INTO email_templates (slug, subject, body_html, variables, updated_at) VALUES ($1, $2, $3, $4, $5) RETURNING id, created_at` + + varsJSON, _ := json.Marshal(tmpl.Variables) + if varsJSON == nil { + varsJSON = []byte("[]") + } + + return r.db.QueryRowContext(ctx, query, tmpl.Slug, tmpl.Subject, tmpl.BodyHTML, varsJSON, time.Now()).Scan(&tmpl.ID, &tmpl.CreatedAt) +} + +func (r *EmailRepository) UpdateTemplate(ctx context.Context, tmpl *entity.EmailTemplate) error { + query := `UPDATE email_templates SET subject=$1, body_html=$2, variables=$3, updated_at=$4 WHERE slug=$5` + + varsJSON, _ := json.Marshal(tmpl.Variables) + if varsJSON == nil { + varsJSON = []byte("[]") + } + + res, err := r.db.ExecContext(ctx, query, tmpl.Subject, tmpl.BodyHTML, varsJSON, time.Now(), tmpl.Slug) + if err != nil { + return err + } + affected, _ := res.RowsAffected() + if affected == 0 { + return errors.New("template not found") + } + return nil +} + +func (r *EmailRepository) DeleteTemplate(ctx context.Context, slug string) error { + _, err := r.db.ExecContext(ctx, "DELETE FROM email_templates WHERE slug=$1", slug) + return err +} + +// Settings + +func (r *EmailRepository) GetSettings(ctx context.Context) (*entity.EmailSettings, error) { + // We assume there's mostly one active setting, order by updated_at desc + query := `SELECT id, provider, smtp_host, smtp_port, smtp_user, smtp_pass, smtp_secure, sender_name, sender_email, amqp_url, is_active, updated_at + FROM email_settings WHERE is_active = true ORDER BY updated_at DESC LIMIT 1` + + row := r.db.QueryRowContext(ctx, query) + s := &entity.EmailSettings{} + + err := row.Scan( + &s.ID, &s.Provider, &s.SMTPHost, &s.SMTPPort, &s.SMTPUser, &s.SMTPPass, + &s.SMTPSecure, &s.SenderName, &s.SenderEmail, &s.AMQPURL, &s.IsActive, &s.UpdatedAt, + ) + if err != nil { + if err == sql.ErrNoRows { + // Return default empty struct or nil + return nil, nil + } + return nil, err + } + return s, nil +} + +func (r *EmailRepository) UpdateSettings(ctx context.Context, s *entity.EmailSettings) error { + // We can either update the existing row or Insert a new one (audit history). + // Let's Insert a new one and set others to inactive if we want history, OR just update for simplicity as per requirement. + // Migration 027 says "id UUID PRIMARY KEY". Let's try to update if ID exists, else insert. + // Actually, GetSettings fetches the latest active. + // Let's implement logical "Upsert active settings". + + // If ID is provided, update. If not, insert. + if s.ID != "" { + query := `UPDATE email_settings SET + provider=$1, smtp_host=$2, smtp_port=$3, smtp_user=$4, smtp_pass=$5, smtp_secure=$6, + sender_name=$7, sender_email=$8, amqp_url=$9, is_active=$10, updated_at=$11 + WHERE id=$12` + _, err := r.db.ExecContext(ctx, query, + s.Provider, s.SMTPHost, s.SMTPPort, s.SMTPUser, s.SMTPPass, s.SMTPSecure, + s.SenderName, s.SenderEmail, s.AMQPURL, s.IsActive, time.Now(), s.ID, + ) + return err + } + + // Insert new + query := `INSERT INTO email_settings ( + provider, smtp_host, smtp_port, smtp_user, smtp_pass, smtp_secure, + sender_name, sender_email, amqp_url, is_active) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id` + + return r.db.QueryRowContext(ctx, query, + s.Provider, s.SMTPHost, s.SMTPPort, s.SMTPUser, s.SMTPPass, s.SMTPSecure, + s.SenderName, s.SenderEmail, s.AMQPURL, true, + ).Scan(&s.ID) +} diff --git a/backend/internal/infrastructure/persistence/postgres/user_repository.go b/backend/internal/infrastructure/persistence/postgres/user_repository.go index 851d871..8857182 100644 --- a/backend/internal/infrastructure/persistence/postgres/user_repository.go +++ b/backend/internal/infrastructure/persistence/postgres/user_repository.go @@ -31,8 +31,8 @@ func (r *UserRepository) Save(ctx context.Context, user *entity.User) (*entity.U // 1. Insert User - users table has UUID id query := ` - INSERT INTO users (identifier, password_hash, role, full_name, email, name, tenant_id, status, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + INSERT INTO users (identifier, password_hash, role, full_name, email, name, tenant_id, status, created_at, updated_at, avatar_url) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id ` @@ -54,6 +54,7 @@ func (r *UserRepository) Save(ctx context.Context, user *entity.User) (*entity.U user.Status, user.CreatedAt, user.UpdatedAt, + user.AvatarUrl, ).Scan(&id) if err != nil { @@ -81,13 +82,13 @@ func (r *UserRepository) Save(ctx context.Context, user *entity.User) (*entity.U func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*entity.User, error) { query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''), - COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at + COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at, COALESCE(avatar_url, '') FROM users WHERE email = $1 OR identifier = $1` row := r.db.QueryRowContext(ctx, query, email) u := &entity.User{} var dbID string - err := row.Scan(&dbID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt) + err := row.Scan(&dbID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt, &u.AvatarUrl) if err != nil { if err == sql.ErrNoRows { return nil, nil // Return nil if not found @@ -101,13 +102,13 @@ func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*entity func (r *UserRepository) FindByID(ctx context.Context, id string) (*entity.User, error) { query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''), - COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at + COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at, COALESCE(avatar_url, '') FROM users WHERE id = $1` row := r.db.QueryRowContext(ctx, query, id) u := &entity.User{} var dbID string - err := row.Scan(&dbID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt) + err := row.Scan(&dbID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt, &u.AvatarUrl) if err != nil { return nil, err } @@ -124,7 +125,7 @@ func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string, l } query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''), - COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at + COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at, COALESCE(avatar_url, '') FROM users WHERE tenant_id = $1 ORDER BY created_at DESC @@ -139,7 +140,7 @@ func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string, l for rows.Next() { u := &entity.User{} var dbID string - if err := rows.Scan(&dbID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt); err != nil { + if err := rows.Scan(&dbID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt, &u.AvatarUrl); err != nil { return nil, 0, err } u.ID = dbID @@ -165,8 +166,8 @@ func (r *UserRepository) Update(ctx context.Context, user *entity.User) (*entity primaryRole = user.Roles[0].Name } - query := `UPDATE users SET name=$1, email=$2, status=$3, role=$4, updated_at=$5 WHERE id=$6` - _, err = tx.ExecContext(ctx, query, user.Name, user.Email, user.Status, primaryRole, user.UpdatedAt, user.ID) + query := `UPDATE users SET name=$1, email=$2, status=$3, role=$4, updated_at=$5, avatar_url=$6 WHERE id=$7` + _, err = tx.ExecContext(ctx, query, user.Name, user.Email, user.Status, primaryRole, user.UpdatedAt, user.AvatarUrl, user.ID) if err != nil { return nil, err } diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 171eb38..f945b1d 100755 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -2,7 +2,6 @@ package router import ( "encoding/json" - "log" "net/http" "os" "time" @@ -11,7 +10,6 @@ import ( "github.com/rede5/gohorsejobs/backend/internal/database" "github.com/rede5/gohorsejobs/backend/internal/handlers" "github.com/rede5/gohorsejobs/backend/internal/infrastructure/persistence/postgres" - "github.com/rede5/gohorsejobs/backend/internal/infrastructure/storage" "github.com/rede5/gohorsejobs/backend/internal/services" // Core Imports @@ -37,10 +35,19 @@ func NewRouter() http.Handler { // --- CORE ARCHITECTURE INITIALIZATION --- // Infrastructure - // Infrastructure, userRepo := postgres.NewUserRepository(database.DB) companyRepo := postgres.NewCompanyRepository(database.DB) + // Utils Services (Moved up for dependency injection) + credentialsService := services.NewCredentialsService(database.DB) + settingsService := services.NewSettingsService(database.DB) + storageService := services.NewStorageService(credentialsService) + fcmService := services.NewFCMService(credentialsService) + cloudflareService := services.NewCloudflareService(credentialsService) + emailService := services.NewEmailService(database.DB, credentialsService) + + adminService := services.NewAdminService(database.DB) + jwtSecret := os.Getenv("JWT_SECRET") if jwtSecret == "" { // Fallback for dev, but really should be in env @@ -51,22 +58,22 @@ func NewRouter() http.Handler { // UseCases loginUC := authUC.NewLoginUseCase(userRepo, authService) - registerCandidateUC := authUC.NewRegisterCandidateUseCase(userRepo, companyRepo, authService) + registerCandidateUC := authUC.NewRegisterCandidateUseCase(userRepo, companyRepo, authService, emailService) createCompanyUC := tenantUC.NewCreateCompanyUseCase(companyRepo, userRepo, authService) listCompaniesUC := tenantUC.NewListCompaniesUseCase(companyRepo) createUserUC := userUC.NewCreateUserUseCase(userRepo, authService) listUsersUC := userUC.NewListUsersUseCase(userRepo) deleteUserUC := userUC.NewDeleteUserUseCase(userRepo) updateUserUC := userUC.NewUpdateUserUseCase(userRepo) - - // Handlers & Middleware auditService := services.NewAuditService(database.DB) - notificationService := services.NewNotificationService(database.DB) + notificationService := services.NewNotificationService(database.DB, fcmService) ticketService := services.NewTicketService(database.DB) - authMiddleware := middleware.NewMiddleware(authService) - adminService := services.NewAdminService(database.DB) - credentialsService := services.NewCredentialsService(database.DB) + + // Chat Services + appwriteService := services.NewAppwriteService(credentialsService) + chatService := services.NewChatService(database.DB, appwriteService) + chatHandlers := apiHandlers.NewChatHandlers(chatService) coreHandlers := apiHandlers.NewCoreHandlers( loginUC, @@ -84,7 +91,9 @@ func NewRouter() http.Handler { credentialsService, // Added for Encrypted Credentials ) - adminHandlers := apiHandlers.NewAdminHandlers(adminService, auditService, jobService) + settingsHandler := apiHandlers.NewSettingsHandler(settingsService) + storageHandler := apiHandlers.NewStorageHandler(storageService) + adminHandlers := apiHandlers.NewAdminHandlers(adminService, auditService, jobService, cloudflareService) // Initialize Legacy Handlers jobHandler := handlers.NewJobHandler(jobService) @@ -209,8 +218,30 @@ func NewRouter() http.Handler { mux.Handle("GET /api/v1/support/tickets/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.GetTicket))) mux.Handle("POST /api/v1/support/tickets/{id}/messages", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.AddMessage))) + // System Settings + mux.Handle("GET /api/v1/system/settings/{key}", authMiddleware.OptionalHeaderAuthGuard(http.HandlerFunc(settingsHandler.GetSettings))) + mux.Handle("POST /api/v1/system/settings/{key}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(settingsHandler.SaveSettings)))) + + // Storage (Presigned URL) + mux.Handle("GET /api/v1/storage/upload-url", authMiddleware.HeaderAuthGuard(http.HandlerFunc(storageHandler.GetUploadURL))) + // System Credentials Route mux.Handle("POST /api/v1/system/credentials", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(coreHandlers.SaveCredentials)))) + mux.Handle("POST /api/v1/system/cloudflare/purge", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.PurgeCache)))) + + // Email Templates & Settings (Admin Only) + mux.Handle("GET /api/v1/admin/email-templates", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.ListEmailTemplates)))) + mux.Handle("POST /api/v1/admin/email-templates", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.CreateEmailTemplate)))) + mux.Handle("GET /api/v1/admin/email-templates/{slug}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.GetEmailTemplate)))) + mux.Handle("PUT /api/v1/admin/email-templates/{slug}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.UpdateEmailTemplate)))) + mux.Handle("DELETE /api/v1/admin/email-templates/{slug}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.DeleteEmailTemplate)))) + mux.Handle("GET /api/v1/admin/email-settings", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.GetEmailSettings)))) + mux.Handle("PUT /api/v1/admin/email-settings", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.UpdateEmailSettings)))) + + // Chat Routes + mux.Handle("GET /api/v1/conversations", authMiddleware.HeaderAuthGuard(http.HandlerFunc(chatHandlers.ListConversations))) + mux.Handle("GET /api/v1/conversations/{id}/messages", authMiddleware.HeaderAuthGuard(http.HandlerFunc(chatHandlers.ListMessages))) + mux.Handle("POST /api/v1/conversations/{id}/messages", authMiddleware.HeaderAuthGuard(http.HandlerFunc(chatHandlers.SendMessage))) // Application Routes mux.HandleFunc("POST /api/v1/applications", applicationHandler.CreateApplication) @@ -224,18 +255,7 @@ func NewRouter() http.Handler { mux.HandleFunc("POST /api/v1/payments/webhook", paymentHandler.HandleWebhook) mux.HandleFunc("GET /api/v1/payments/status/{id}", paymentHandler.GetPaymentStatus) - // --- STORAGE ROUTES --- - // Initialize S3 Storage (optional - graceful degradation if not configured) - s3Storage, err := storage.NewS3Storage() - if err != nil { - log.Printf("Warning: S3 storage not available: %v", err) - } else { - storageHandler := handlers.NewStorageHandler(s3Storage) - mux.Handle("POST /api/v1/storage/upload-url", authMiddleware.HeaderAuthGuard(http.HandlerFunc(storageHandler.GenerateUploadURL))) - mux.Handle("POST /api/v1/storage/download-url", authMiddleware.HeaderAuthGuard(http.HandlerFunc(storageHandler.GenerateDownloadURL))) - mux.Handle("DELETE /api/v1/storage/files", authMiddleware.HeaderAuthGuard(http.HandlerFunc(storageHandler.DeleteFile))) - log.Println("S3 storage routes registered successfully") - } + // --- STORAGE ROUTES (Legacy Removed) --- // Swagger Route - available at /docs mux.HandleFunc("/docs/", httpSwagger.WrapHandler) diff --git a/backend/internal/services/admin_service.go b/backend/internal/services/admin_service.go index 1ca452b..b2bd595 100644 --- a/backend/internal/services/admin_service.go +++ b/backend/internal/services/admin_service.go @@ -3,6 +3,7 @@ package services import ( "context" "database/sql" + "encoding/json" "fmt" "strings" "time" @@ -709,3 +710,189 @@ func (s *AdminService) DeleteCompany(ctx context.Context, id string) error { _, err = s.DB.ExecContext(ctx, `DELETE FROM companies WHERE id=$1`, id) return err } + +// ============================================================================ +// Email Templates & Settings CRUD +// ============================================================================ + +func (s *AdminService) ListEmailTemplates(ctx context.Context) ([]dto.EmailTemplateDTO, error) { + query := `SELECT id, slug, subject, body_html, variables, created_at, updated_at FROM email_templates ORDER BY slug` + rows, err := s.DB.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + templates := []dto.EmailTemplateDTO{} + for rows.Next() { + var t dto.EmailTemplateDTO + var varsJSON []byte + if err := rows.Scan(&t.ID, &t.Slug, &t.Subject, &t.BodyHTML, &varsJSON, &t.CreatedAt, &t.UpdatedAt); err != nil { + return nil, err + } + if len(varsJSON) > 0 { + json.Unmarshal(varsJSON, &t.Variables) + } + templates = append(templates, t) + } + return templates, nil +} + +func (s *AdminService) GetEmailTemplate(ctx context.Context, slug string) (*dto.EmailTemplateDTO, error) { + query := `SELECT id, slug, subject, body_html, variables, created_at, updated_at FROM email_templates WHERE slug = $1` + row := s.DB.QueryRowContext(ctx, query, slug) + + t := &dto.EmailTemplateDTO{} + var varsJSON []byte + err := row.Scan(&t.ID, &t.Slug, &t.Subject, &t.BodyHTML, &varsJSON, &t.CreatedAt, &t.UpdatedAt) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + if len(varsJSON) > 0 { + json.Unmarshal(varsJSON, &t.Variables) + } + return t, nil +} + +func (s *AdminService) CreateEmailTemplate(ctx context.Context, req dto.CreateEmailTemplateRequest) (*dto.EmailTemplateDTO, error) { + query := `INSERT INTO email_templates (slug, subject, body_html, variables, updated_at) VALUES ($1, $2, $3, $4, $5) RETURNING id, created_at` + + varsJSON, _ := json.Marshal(req.Variables) + if varsJSON == nil { + varsJSON = []byte("[]") + } + + t := &dto.EmailTemplateDTO{ + Slug: req.Slug, + Subject: req.Subject, + BodyHTML: req.BodyHTML, + Variables: req.Variables, + } + err := s.DB.QueryRowContext(ctx, query, req.Slug, req.Subject, req.BodyHTML, varsJSON, time.Now()).Scan(&t.ID, &t.CreatedAt) + if err != nil { + return nil, err + } + t.UpdatedAt = t.CreatedAt + return t, nil +} + +func (s *AdminService) UpdateEmailTemplate(ctx context.Context, slug string, req dto.UpdateEmailTemplateRequest) (*dto.EmailTemplateDTO, error) { + // Fetch existing + existing, err := s.GetEmailTemplate(ctx, slug) + if err != nil { + return nil, err + } + if existing == nil { + return nil, fmt.Errorf("template not found") + } + + // Apply updates + if req.Subject != nil { + existing.Subject = *req.Subject + } + if req.BodyHTML != nil { + existing.BodyHTML = *req.BodyHTML + } + if req.Variables != nil { + existing.Variables = *req.Variables + } + + varsJSON, _ := json.Marshal(existing.Variables) + query := `UPDATE email_templates SET subject=$1, body_html=$2, variables=$3, updated_at=$4 WHERE slug=$5` + _, err = s.DB.ExecContext(ctx, query, existing.Subject, existing.BodyHTML, varsJSON, time.Now(), slug) + if err != nil { + return nil, err + } + + return s.GetEmailTemplate(ctx, slug) +} + +func (s *AdminService) DeleteEmailTemplate(ctx context.Context, slug string) error { + _, err := s.DB.ExecContext(ctx, "DELETE FROM email_templates WHERE slug=$1", slug) + return err +} + +func (s *AdminService) GetEmailSettings(ctx context.Context) (*dto.EmailSettingsDTO, error) { + query := `SELECT id, provider, smtp_host, smtp_port, smtp_user, smtp_pass, smtp_secure, sender_name, sender_email, amqp_url, is_active, updated_at + FROM email_settings WHERE is_active = true ORDER BY updated_at DESC LIMIT 1` + + row := s.DB.QueryRowContext(ctx, query) + var s_ dto.EmailSettingsDTO + + err := row.Scan( + &s_.ID, &s_.Provider, &s_.SMTPHost, &s_.SMTPPort, &s_.SMTPUser, &s_.SMTPPass, + &s_.SMTPSecure, &s_.SenderName, &s_.SenderEmail, &s_.AMQPURL, &s_.IsActive, &s_.UpdatedAt, + ) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + return &s_, nil +} + +func (s *AdminService) UpdateEmailSettings(ctx context.Context, req dto.UpdateEmailSettingsRequest) (*dto.EmailSettingsDTO, error) { + existing, err := s.GetEmailSettings(ctx) + if err != nil { + return nil, err + } + + if existing == nil { + // Insert new + query := `INSERT INTO email_settings (provider, smtp_host, smtp_port, smtp_user, smtp_pass, smtp_secure, sender_name, sender_email, amqp_url, is_active) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, true) RETURNING id, updated_at` + newS := &dto.EmailSettingsDTO{ + Provider: "smtp", + SMTPSecure: true, + SenderName: "GoHorse Jobs", + SenderEmail: "no-reply@gohorsejobs.com", + IsActive: true, + } + applyEmailSettingsUpdate(newS, req) + err = s.DB.QueryRowContext(ctx, query, newS.Provider, newS.SMTPHost, newS.SMTPPort, newS.SMTPUser, newS.SMTPPass, newS.SMTPSecure, newS.SenderName, newS.SenderEmail, newS.AMQPURL).Scan(&newS.ID, &newS.UpdatedAt) + return newS, err + } + + // Update existing + applyEmailSettingsUpdate(existing, req) + query := `UPDATE email_settings SET provider=$1, smtp_host=$2, smtp_port=$3, smtp_user=$4, smtp_pass=$5, smtp_secure=$6, sender_name=$7, sender_email=$8, amqp_url=$9, updated_at=$10 WHERE id=$11` + _, err = s.DB.ExecContext(ctx, query, existing.Provider, existing.SMTPHost, existing.SMTPPort, existing.SMTPUser, existing.SMTPPass, existing.SMTPSecure, existing.SenderName, existing.SenderEmail, existing.AMQPURL, time.Now(), existing.ID) + if err != nil { + return nil, err + } + return s.GetEmailSettings(ctx) +} + +func applyEmailSettingsUpdate(s *dto.EmailSettingsDTO, req dto.UpdateEmailSettingsRequest) { + if req.Provider != nil { + s.Provider = *req.Provider + } + if req.SMTPHost != nil { + s.SMTPHost = req.SMTPHost + } + if req.SMTPPort != nil { + s.SMTPPort = req.SMTPPort + } + if req.SMTPUser != nil { + s.SMTPUser = req.SMTPUser + } + if req.SMTPPass != nil { + s.SMTPPass = req.SMTPPass + } + if req.SMTPSecure != nil { + s.SMTPSecure = *req.SMTPSecure + } + if req.SenderName != nil { + s.SenderName = *req.SenderName + } + if req.SenderEmail != nil { + s.SenderEmail = *req.SenderEmail + } + if req.AMQPURL != nil { + s.AMQPURL = req.AMQPURL + } +} diff --git a/backend/internal/services/appwrite_service.go b/backend/internal/services/appwrite_service.go new file mode 100644 index 0000000..7dd9a69 --- /dev/null +++ b/backend/internal/services/appwrite_service.go @@ -0,0 +1,90 @@ +package services + +import ( + "context" + "encoding/json" + "fmt" + "os" + "time" + + "github.com/appwrite/sdk-for-go/client" + "github.com/appwrite/sdk-for-go/databases" +) + +type AppwriteConfig struct { + Endpoint string `json:"endpoint"` + ProjectID string `json:"projectId"` + APIKey string `json:"apiKey"` + DatabaseID string `json:"databaseId"` + CollectionID string `json:"collectionId"` +} + +type AppwriteService struct { + credentialsService *CredentialsService +} + +func NewAppwriteService(creds *CredentialsService) *AppwriteService { + return &AppwriteService{ + credentialsService: creds, + } +} + +// PushMessage pushes a message to Appwrite Database for Realtime sync +func (s *AppwriteService) PushMessage(ctx context.Context, msgID, convoID, senderID, content string) error { + config, err := s.getConfig(ctx) + if err != nil { + return fmt.Errorf("appwrite config error: %w", err) + } + + if config.Endpoint == "" || config.ProjectID == "" { + return fmt.Errorf("appwrite not configured") + } + + // Initialize Client + // sdk-for-go v0.16.x uses direct field assignment or AddHeader for config + cl := client.New() + cl.Endpoint = config.Endpoint + cl.AddHeader("X-Appwrite-Project", config.ProjectID) + cl.AddHeader("X-Appwrite-Key", config.APIKey) + + db := databases.New(cl) + + // Create Document + _, err = db.CreateDocument( + config.DatabaseID, + config.CollectionID, + msgID, + map[string]interface{}{ + "conversation_id": convoID, + "sender_id": senderID, + "content": content, + "timestamp": time.Now().Format(time.RFC3339), + }, + db.WithCreateDocumentPermissions([]string{}), // Optional permissions + ) + if err != nil { + return fmt.Errorf("failed to push to appwrite: %w", err) + } + + return nil +} + +func (s *AppwriteService) getConfig(ctx context.Context) (*AppwriteConfig, error) { + // Try DB first + payload, err := s.credentialsService.GetDecryptedKey(ctx, "appwrite") + if err == nil && payload != "" { + var cfg AppwriteConfig + if err := json.Unmarshal([]byte(payload), &cfg); err == nil { + return &cfg, nil + } + } + + // Fallback to Env + return &AppwriteConfig{ + Endpoint: os.Getenv("APPWRITE_ENDPOINT"), + ProjectID: os.Getenv("APPWRITE_PROJECT_ID"), + APIKey: os.Getenv("APPWRITE_API_KEY"), + DatabaseID: os.Getenv("APPWRITE_DATABASE_ID"), + CollectionID: "messages", // Default if not set + }, nil +} diff --git a/backend/internal/services/chat_service.go b/backend/internal/services/chat_service.go new file mode 100644 index 0000000..a70cf62 --- /dev/null +++ b/backend/internal/services/chat_service.go @@ -0,0 +1,156 @@ +package services + +import ( + "context" + "database/sql" + "fmt" + "time" +) + +type ChatService struct { + DB *sql.DB + Appwrite *AppwriteService +} + +func NewChatService(db *sql.DB, appwrite *AppwriteService) *ChatService { + return &ChatService{ + DB: db, + Appwrite: appwrite, + } +} + +type Message struct { + ID string `json:"id"` + ConversationID string `json:"conversationId"` + SenderID string `json:"senderId"` + Content string `json:"content"` + CreatedAt time.Time `json:"createdAt"` + IsMine bool `json:"isMine"` // Populated by handler/frontend logic +} + +type Conversation struct { + ID string `json:"id"` + CandidateID string `json:"candidateId"` + CompanyID string `json:"companyId"` + JobID *string `json:"jobId"` + LastMessage *string `json:"lastMessage"` + LastMessageAt *time.Time `json:"lastMessageAt"` + ParticipantName string `json:"participantName"` + ParticipantAvatar string `json:"participantAvatar"` + UnreadCount int `json:"unreadCount"` +} + +func (s *ChatService) SendMessage(ctx context.Context, senderID, conversationID, content string) (*Message, error) { + // 1. Insert into Postgres + var msgID string + var createdAt time.Time + query := ` + INSERT INTO messages (conversation_id, sender_id, content) + VALUES ($1, $2, $3) + RETURNING id, created_at + ` + err := s.DB.QueryRowContext(ctx, query, conversationID, senderID, content).Scan(&msgID, &createdAt) + if err != nil { + return nil, fmt.Errorf("failed to insert message: %w", err) + } + + // 2. Update Conversation (async-ish if needed, but important for sorting) + updateQuery := ` + UPDATE conversations + SET last_message = $1, last_message_at = $2, updated_at = $2 + WHERE id = $3 + ` + if _, err := s.DB.ExecContext(ctx, updateQuery, content, createdAt, conversationID); err != nil { + // Log error but assume message is sent + fmt.Printf("Failed to update conversation: %v\n", err) + } + + // 3. Push to Appwrite + go func() { + // Fire and forget for realtime + err := s.Appwrite.PushMessage(context.Background(), msgID, conversationID, senderID, content) + if err != nil { + fmt.Printf("Appwrite push failed: %v\n", err) + } + }() + + return &Message{ + ID: msgID, + ConversationID: conversationID, + SenderID: senderID, + Content: content, + CreatedAt: createdAt, + }, nil +} + +func (s *ChatService) ListMessages(ctx context.Context, conversationID string) ([]Message, error) { + query := ` + SELECT id, conversation_id, sender_id, content, created_at + FROM messages + WHERE conversation_id = $1 + ORDER BY created_at ASC + ` + rows, err := s.DB.QueryContext(ctx, query, conversationID) + if err != nil { + return nil, err + } + defer rows.Close() + + var msgs []Message + for rows.Next() { + var m Message + if err := rows.Scan(&m.ID, &m.ConversationID, &m.SenderID, &m.Content, &m.CreatedAt); err != nil { + return nil, err + } + msgs = append(msgs, m) + } + return msgs, nil +} + +// ListConversations lists conversations for a user (candidate or company admin) +func (s *ChatService) ListConversations(ctx context.Context, userID, tenantID string, isCandidate bool) ([]Conversation, error) { + var query string + var args []interface{} + + if isCandidate { + // User is candidate -> Fetch company info + query = ` + SELECT c.id, c.candidate_id, c.company_id, c.job_id, c.last_message, c.last_message_at, + comp.name as participant_name + FROM conversations c + JOIN companies comp ON c.company_id = comp.id + WHERE c.candidate_id = $1 + ORDER BY c.last_message_at DESC NULLS LAST + ` + args = append(args, userID) + } else if tenantID != "" { + // User is company admin -> Fetch candidate info + query = ` + SELECT c.id, c.candidate_id, c.company_id, c.job_id, c.last_message, c.last_message_at, + COALESCE(u.name, u.full_name, u.identifier) as participant_name + FROM conversations c + JOIN users u ON c.candidate_id = u.id + WHERE c.company_id = $1 + ORDER BY c.last_message_at DESC NULLS LAST + ` + args = append(args, tenantID) + } else { + return nil, fmt.Errorf("invalid context for listing conversations") + } + + rows, err := s.DB.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var convs []Conversation + for rows.Next() { + var c Conversation + if err := rows.Scan(&c.ID, &c.CandidateID, &c.CompanyID, &c.JobID, &c.LastMessage, &c.LastMessageAt, &c.ParticipantName); err != nil { + return nil, err + } + convs = append(convs, c) + } + return convs, nil +} diff --git a/backend/internal/services/cloudflare_service.go b/backend/internal/services/cloudflare_service.go new file mode 100644 index 0000000..9c4faa5 --- /dev/null +++ b/backend/internal/services/cloudflare_service.go @@ -0,0 +1,64 @@ +package services + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" +) + +type CloudflareConfig struct { + APIToken string `json:"apiToken"` + ZoneID string `json:"zoneId"` +} + +type CloudflareService struct { + credentials *CredentialsService +} + +func NewCloudflareService(c *CredentialsService) *CloudflareService { + return &CloudflareService{credentials: c} +} + +// PurgeCache clears the entire Cloudflare cache for the configured zone +func (s *CloudflareService) PurgeCache(ctx context.Context) error { + payload, err := s.credentials.GetDecryptedKey(ctx, "cloudflare_config") + if err != nil { + return fmt.Errorf("cloudflare credentials missing: %w", err) + } + + var cfg CloudflareConfig + if err := json.Unmarshal([]byte(payload), &cfg); err != nil { + return fmt.Errorf("invalid cloudflare config: %w", err) + } + + if cfg.APIToken == "" || cfg.ZoneID == "" { + return fmt.Errorf("cloudflare not configured") + } + + url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/purge_cache", cfg.ZoneID) + body := []byte(`{"purge_everything":true}`) + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(body)) + if err != nil { + return err + } + + req.Header.Set("Authorization", "Bearer "+cfg.APIToken) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("cloudflare api request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + respBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("cloudflare api returned %d: %s", resp.StatusCode, string(respBody)) + } + + return nil +} diff --git a/backend/internal/services/email_service.go b/backend/internal/services/email_service.go new file mode 100644 index 0000000..eb0aab6 --- /dev/null +++ b/backend/internal/services/email_service.go @@ -0,0 +1,94 @@ +package services + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "log" + + amqp "github.com/rabbitmq/amqp091-go" +) + +type EmailService struct { + db *sql.DB + credentialsService *CredentialsService +} + +func NewEmailService(db *sql.DB, cs *CredentialsService) *EmailService { + return &EmailService{ + db: db, + credentialsService: cs, + } // Ensure return pointer matches +} + +type EmailJob struct { + To string `json:"to"` + Template string `json:"template"` // slug + Variables map[string]interface{} `json:"variables"` +} + +// SendTemplateEmail queues an email via RabbitMQ +func (s *EmailService) SendTemplateEmail(ctx context.Context, to, templateSlug string, variables map[string]interface{}) error { + // 1. Get AMQP URL from email_settings + var amqpURL sql.NullString + err := s.db.QueryRowContext(ctx, "SELECT amqp_url FROM email_settings LIMIT 1").Scan(&amqpURL) + if err != nil && err != sql.ErrNoRows { + log.Printf("[EmailService] Failed to fetch AMQP URL: %v", err) + } + + url := "" + if amqpURL.Valid { + url = amqpURL.String + } + + if url == "" { + // Log but don't error hard if just testing, but for "system" we need IT. + // Return error so caller knows. + return fmt.Errorf("AMQP URL not configured in email_settings") + } + + // 2. Connect & Publish + conn, err := amqp.Dial(url) + if err != nil { + return fmt.Errorf("failed to connect to RabbitMQ: %w", err) + } + defer conn.Close() + + ch, err := conn.Channel() + if err != nil { + return fmt.Errorf("failed to open channel: %w", err) + } + defer ch.Close() + + q, err := ch.QueueDeclare( + "mail_queue", // name + true, // durable + false, // delete when unused + false, // exclusive + false, // no-wait + nil, // arguments + ) + if err != nil { + return fmt.Errorf("failed to declare queue: %w", err) + } + + job := EmailJob{To: to, Template: templateSlug, Variables: variables} + body, _ := json.Marshal(job) + + err = ch.PublishWithContext(ctx, + "", // exchange + q.Name, // routing key + false, // mandatory + false, // immediate + amqp.Publishing{ + ContentType: "application/json", + Body: body, + }) + if err != nil { + return fmt.Errorf("failed to publish message: %w", err) + } + + log.Printf("[EmailService] Queued email to %s (Template: %s)", to, templateSlug) + return nil +} diff --git a/backend/internal/services/fcm_service.go b/backend/internal/services/fcm_service.go new file mode 100644 index 0000000..963fa05 --- /dev/null +++ b/backend/internal/services/fcm_service.go @@ -0,0 +1,85 @@ +package services + +import ( + "context" + "fmt" + "sync" + + firebase "firebase.google.com/go/v4" + "firebase.google.com/go/v4/messaging" + "google.golang.org/api/option" +) + +type FCMService struct { + credentials *CredentialsService + client *messaging.Client + currentKey string + mu sync.RWMutex +} + +func NewFCMService(c *CredentialsService) *FCMService { + return &FCMService{credentials: c} +} + +func (s *FCMService) getClient(ctx context.Context) (*messaging.Client, error) { + s.mu.Lock() + defer s.mu.Unlock() + + // Get Service Account JSON from DB + jsonKey, err := s.credentials.GetDecryptedKey(ctx, "fcm_service_account") + if err != nil { + return nil, fmt.Errorf("fcm credentials missing: %w", err) + } + + // If initialized and key hasn't changed, return cached client + if s.client != nil && jsonKey == s.currentKey { + return s.client, nil + } + + // Initialize new client + opt := option.WithCredentialsJSON([]byte(jsonKey)) + app, err := firebase.NewApp(ctx, nil, opt) + if err != nil { + return nil, fmt.Errorf("error initializing firebase app: %w", err) + } + + client, err := app.Messaging(ctx) + if err != nil { + return nil, fmt.Errorf("error getting messaging client: %w", err) + } + + s.client = client + s.currentKey = jsonKey + return client, nil +} + +// SendPush sends a push notification to a specific token +func (s *FCMService) SendPush(ctx context.Context, token, title, body string, data map[string]string) error { + client, err := s.getClient(ctx) + if err != nil { + return err + } + + msg := &messaging.Message{ + Token: token, + Notification: &messaging.Notification{ + Title: title, + Body: body, + }, + Data: data, + } + + _, err = client.Send(ctx, msg) + return err +} + +// SubscribeToTopic subscribes a token to a topic +func (s *FCMService) SubscribeToTopic(ctx context.Context, tokens []string, topic string) error { + client, err := s.getClient(ctx) + if err != nil { + return err + } + + _, err = client.SubscribeToTopic(ctx, tokens, topic) + return err +} diff --git a/backend/internal/services/notification_service.go b/backend/internal/services/notification_service.go index 7bbb65c..007fd7d 100644 --- a/backend/internal/services/notification_service.go +++ b/backend/internal/services/notification_service.go @@ -8,11 +8,12 @@ import ( ) type NotificationService struct { - DB *sql.DB + DB *sql.DB + FCM *FCMService } -func NewNotificationService(db *sql.DB) *NotificationService { - return &NotificationService{DB: db} +func NewNotificationService(db *sql.DB, fcm *FCMService) *NotificationService { + return &NotificationService{DB: db, FCM: fcm} } func (s *NotificationService) CreateNotification(ctx context.Context, userID string, nType, title, message string, link *string) error { diff --git a/backend/internal/services/notification_service_test.go b/backend/internal/services/notification_service_test.go index af85ce2..8b91f4a 100644 --- a/backend/internal/services/notification_service_test.go +++ b/backend/internal/services/notification_service_test.go @@ -14,7 +14,7 @@ func TestNotificationService_ListNotifications(t *testing.T) { } defer db.Close() - service := NewNotificationService(db) + service := NewNotificationService(db, nil) ctx := context.Background() userID := "019b5290-9680-7c06-9ee3-c9e0e117251b" @@ -44,7 +44,7 @@ func TestNotificationService_CreateNotification(t *testing.T) { } defer db.Close() - service := NewNotificationService(db) + service := NewNotificationService(db, nil) ctx := context.Background() userID := "019b5290-9680-7c06-9ee3-c9e0e117251b" @@ -71,7 +71,7 @@ func TestNotificationService_MarkAsRead(t *testing.T) { } defer db.Close() - service := NewNotificationService(db) + service := NewNotificationService(db, nil) ctx := context.Background() userID := "019b5290-9680-7c06-9ee3-c9e0e117251b" @@ -98,7 +98,7 @@ func TestNotificationService_MarkAllAsRead(t *testing.T) { } defer db.Close() - service := NewNotificationService(db) + service := NewNotificationService(db, nil) ctx := context.Background() userID := "019b5290-9680-7c06-9ee3-c9e0e117251b" diff --git a/backend/internal/services/settings_service.go b/backend/internal/services/settings_service.go new file mode 100644 index 0000000..5f58cfb --- /dev/null +++ b/backend/internal/services/settings_service.go @@ -0,0 +1,51 @@ +package services + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" +) + +type SettingsService struct { + db *sql.DB +} + +func NewSettingsService(db *sql.DB) *SettingsService { + return &SettingsService{db: db} +} + +// GetSettings retrieves a JSON setting by key. Returns nil if not found. +func (s *SettingsService) GetSettings(ctx context.Context, key string) (json.RawMessage, error) { + var value []byte + err := s.db.QueryRowContext(ctx, "SELECT value FROM system_settings WHERE key = $1", key).Scan(&value) + if err == sql.ErrNoRows { + return nil, nil // Not found + } + if err != nil { + return nil, fmt.Errorf("failed to get setting %s: %w", key, err) + } + + return json.RawMessage(value), nil +} + +// SaveSettings saves a JSON setting. Updates if exists. +func (s *SettingsService) SaveSettings(ctx context.Context, key string, value interface{}) error { + jsonBytes, err := json.Marshal(value) + if err != nil { + return fmt.Errorf("failed to marshal setting value: %w", err) + } + + query := ` + INSERT INTO system_settings (key, value, updated_at) + VALUES ($1, $2, NOW()) + ON CONFLICT (key) DO UPDATE + SET value = EXCLUDED.value, updated_at = NOW() + ` + _, err = s.db.ExecContext(ctx, query, key, jsonBytes) + if err != nil { + return fmt.Errorf("failed to save setting %s: %w", key, err) + } + + return nil +} diff --git a/backend/internal/services/storage_service.go b/backend/internal/services/storage_service.go new file mode 100644 index 0000000..54c8a75 --- /dev/null +++ b/backend/internal/services/storage_service.go @@ -0,0 +1,91 @@ +package services + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +type StorageService struct { + credentialsService *CredentialsService +} + +func NewStorageService(cs *CredentialsService) *StorageService { + return &StorageService{credentialsService: cs} +} + +// UploadConfig holds the necessary keys. +type UploadConfig struct { + Endpoint string `json:"endpoint"` + AccessKey string `json:"accessKey"` + SecretKey string `json:"secretKey"` + Bucket string `json:"bucket"` + Region string `json:"region"` +} + +func (s *StorageService) getClient(ctx context.Context) (*s3.PresignClient, string, error) { + // 1. Fetch Credentials from DB (Encrypted Payload) + payload, err := s.credentialsService.GetDecryptedKey(ctx, "storage") + if err != nil { + return nil, "", fmt.Errorf("failed to get storage credentials: %w", err) + } + + var uCfg UploadConfig + if err := json.Unmarshal([]byte(payload), &uCfg); err != nil { + return nil, "", fmt.Errorf("failed to parse storage credentials: %w", err) + } + + if uCfg.Endpoint == "" || uCfg.AccessKey == "" || uCfg.SecretKey == "" || uCfg.Bucket == "" { + return nil, "", fmt.Errorf("storage credentials incomplete (all fields required)") + } + + if uCfg.Region == "" { + uCfg.Region = "auto" + } + + // 2. Setup S3 V2 Client + cfg, err := config.LoadDefaultConfig(ctx, + config.WithRegion(uCfg.Region), + config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(uCfg.AccessKey, uCfg.SecretKey, "")), + ) + if err != nil { + return nil, "", err + } + + // R2/S3 specific endpoint + client := s3.NewFromConfig(cfg, func(o *s3.Options) { + o.BaseEndpoint = aws.String(uCfg.Endpoint) + o.UsePathStyle = true // Often needed for R2/MinIO + }) + + psClient := s3.NewPresignClient(client) + return psClient, uCfg.Bucket, nil +} + +// GetPresignedUploadURL generates a URL for PUT requests +func (s *StorageService) GetPresignedUploadURL(ctx context.Context, key string, contentType string) (string, error) { + psClient, bucket, err := s.getClient(ctx) + if err != nil { + return "", err + } + + req, err := psClient.PresignPutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(key), + ContentType: aws.String(contentType), + }, func(o *s3.PresignOptions) { + o.Expires = 15 * time.Minute + }) + + if err != nil { + return "", fmt.Errorf("failed to presign upload: %w", err) + } + + return req.URL, nil +} diff --git a/backend/migrations/025_create_chat_tables.sql b/backend/migrations/025_create_chat_tables.sql new file mode 100644 index 0000000..6ade6bb --- /dev/null +++ b/backend/migrations/025_create_chat_tables.sql @@ -0,0 +1,32 @@ +-- Migration: Create chat tables (conversations, messages) +-- Description: Stores chat history for candidate-company communication + +CREATE TABLE IF NOT EXISTS conversations ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), + candidate_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, + job_id UUID REFERENCES jobs(id) ON DELETE SET NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + last_message TEXT, + last_message_at TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX idx_conversations_candidate ON conversations(candidate_id); +CREATE INDEX idx_conversations_company ON conversations(company_id); +CREATE INDEX idx_conversations_updated ON conversations(updated_at DESC); + +CREATE TABLE IF NOT EXISTS messages ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), + conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, + sender_id UUID NOT NULL REFERENCES users(id), + content TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + appwrite_id VARCHAR(255) +); + +CREATE INDEX idx_messages_conversation ON messages(conversation_id); +CREATE INDEX idx_messages_created ON messages(created_at); + +COMMENT ON TABLE conversations IS 'Chat conversations between candidates and companies'; +COMMENT ON TABLE messages IS 'Individual chat messages synchronized with Appwrite'; diff --git a/backend/migrations/026_create_system_settings.sql b/backend/migrations/026_create_system_settings.sql new file mode 100644 index 0000000..21f9c3c --- /dev/null +++ b/backend/migrations/026_create_system_settings.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS system_settings ( + key VARCHAR(255) PRIMARY KEY, + value JSONB NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); diff --git a/backend/migrations/027_create_email_system.sql b/backend/migrations/027_create_email_system.sql new file mode 100644 index 0000000..80a036e --- /dev/null +++ b/backend/migrations/027_create_email_system.sql @@ -0,0 +1,53 @@ +-- Migration: Create Email System Tables (Settings, Templates) +-- Description: Stores configuration for Email Service (SMTP, LavinMQ) and Templates. + +-- 1. Email Settings (Singleton or Key-Value) +-- We can reuse `system_settings` or `external_services_credentials` for the actual secrets. +-- But requirements said "Criar tabela email_settings". Let's create it for specific non-secret config or reference. +-- Actually, the user asked for `email_settings` to store SMTP (Host, Port, User, Pass) and LavinMQ. +-- Since we have `external_services_credentials` for encrypted data, we should probably stick to that for passwords. +-- However, for the sake of the requirement "Criar tabela email_settings", let's create it but maybe keep passwords encrypted or reference them. +-- User Requirement: "Armazenar SMTP (Host, Port, User, Pass) e as credenciais do LavinMQ." +-- We will create a table that holds this. For security, we should encrypt 'pass' fields, but for this exercise we might store them plaintext or assume app handles encryption. +-- Given `external_services_credentials` exists, we can use it. But the user explicitly asked for `email_settings`. +-- Let's make `email_settings` a single row table or key-value. +-- Let's use a structured table for simplicity as requested. + +CREATE TABLE IF NOT EXISTS email_settings ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), + provider VARCHAR(50) NOT NULL DEFAULT 'smtp', -- smtp, ses, sendgrid + + -- SMTP Config + smtp_host VARCHAR(255), + smtp_port INTEGER, + smtp_user VARCHAR(255), + smtp_pass VARCHAR(255), -- Ideally encrypted, but we'll store as is or encrypted string + smtp_secure BOOLEAN DEFAULT true, + sender_name VARCHAR(255) DEFAULT 'GoHorse Jobs', + sender_email VARCHAR(255) DEFAULT 'no-reply@gohorsejobs.com', + + -- LavinMQ Config + amqp_url VARCHAR(255), -- amqp://user:pass@host:port/vhost + + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT true +); + +-- 2. Email Templates +CREATE TABLE IF NOT EXISTS email_templates ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), + slug VARCHAR(100) UNIQUE NOT NULL, -- e.g. 'welcome-user', 'reset-password' + subject VARCHAR(255) NOT NULL, + body_html TEXT NOT NULL, + variables JSONB DEFAULT '[]', -- List of expected variables e.g. ["name", "link"] + + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Seed some initial templates +INSERT INTO email_templates (slug, subject, body_html, variables) +VALUES +('welcome-candidate', 'Welcome to GoHorse Jobs!', '

Hello {{name}},

Welcome to GoHorse Jobs. We are excited to have you.

', '["name"]'), +('reset-password', 'Reset your password', '

Click here to reset your password.

', '["link"]') +ON CONFLICT (slug) DO NOTHING; diff --git a/backend/migrations/028_add_avatar_url_to_users.sql b/backend/migrations/028_add_avatar_url_to_users.sql new file mode 100644 index 0000000..c724386 --- /dev/null +++ b/backend/migrations/028_add_avatar_url_to_users.sql @@ -0,0 +1,4 @@ +-- Migration: Add avatar_url to users table +-- Description: Stores the URL/Key of the user's profile picture + +ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_url VARCHAR(512); diff --git a/backoffice/README.md b/backoffice/BACKOFFICE.md similarity index 100% rename from backoffice/README.md rename to backoffice/BACKOFFICE.md diff --git a/backoffice/package.json b/backoffice/package.json index 0ac3cf5..75bf68e 100644 --- a/backoffice/package.json +++ b/backoffice/package.json @@ -22,8 +22,8 @@ }, "dependencies": { "@fastify/compress": "^8.0.1", - "@fastify/cors": "^10.0.2", "@fastify/cookie": "^11.0.0", + "@fastify/cors": "^10.0.2", "@fastify/helmet": "^13.0.1", "@fastify/static": "^8.3.0", "@nestjs/axios": "^4.0.1", @@ -35,6 +35,7 @@ "axios": "^1.13.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.3", + "firebase-admin": "^13.6.0", "jsonwebtoken": "^9.0.2", "pino": "^9.6.0", "pino-pretty": "^13.0.0", @@ -48,9 +49,11 @@ "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.1", + "@types/amqplib": "^0.10.8", "@types/jest": "^30.0.0", "@types/jsonwebtoken": "^9.0.6", "@types/node": "^22.10.7", + "@types/nodemailer": "^7.0.4", "@types/supertest": "^6.0.2", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", diff --git a/backoffice/pnpm-lock.yaml b/backoffice/pnpm-lock.yaml index e9ef216..1086332 100644 --- a/backoffice/pnpm-lock.yaml +++ b/backoffice/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^8.0.1 version: 8.3.1 '@fastify/cookie': - specifier: ^9.3.1 - version: 9.4.0 + specifier: ^11.0.0 + version: 11.0.2 '@fastify/cors': specifier: ^10.0.2 version: 10.1.0 @@ -50,6 +50,9 @@ importers: class-validator: specifier: ^0.14.3 version: 0.14.3 + firebase-admin: + specifier: ^13.6.0 + version: 13.6.0 jsonwebtoken: specifier: ^9.0.2 version: 9.0.3 @@ -84,6 +87,9 @@ importers: '@nestjs/testing': specifier: ^11.0.1 version: 11.1.10(@nestjs/common@11.1.10(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.10(@nestjs/common@11.1.10(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@types/amqplib': + specifier: ^0.10.8 + version: 0.10.8 '@types/jest': specifier: ^30.0.0 version: 30.0.0 @@ -93,6 +99,9 @@ importers: '@types/node': specifier: ^22.10.7 version: 22.19.3 + '@types/nodemailer': + specifier: ^7.0.4 + version: 7.0.4 '@types/supertest': specifier: ^6.0.2 version: 6.0.3 @@ -172,6 +181,135 @@ packages: resolution: {integrity: sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-sesv2@3.958.0': + resolution: {integrity: sha512-3x3n8IIxIMAkdpt9wy9zS7MO2lqTcJwQTdHMn6BlD7YUohb+r5Q4KCOEQ2uHWd4WIJv2tlbXnfypHaXReO/WXA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/client-sso@3.958.0': + resolution: {integrity: sha512-6qNCIeaMzKzfqasy2nNRuYnMuaMebCcCPP4J2CVGkA8QYMbIVKPlkn9bpB20Vxe6H/r3jtCCLQaOJjVTx/6dXg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/core@3.957.0': + resolution: {integrity: sha512-DrZgDnF1lQZv75a52nFWs6MExihJF2GZB6ETZRqr6jMwhrk2kbJPUtvgbifwcL7AYmVqHQDJBrR/MqkwwFCpiw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-env@3.957.0': + resolution: {integrity: sha512-475mkhGaWCr+Z52fOOVb/q2VHuNvqEDixlYIkeaO6xJ6t9qR0wpLt4hOQaR6zR1wfZV0SlE7d8RErdYq/PByog==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-http@3.957.0': + resolution: {integrity: sha512-8dS55QHRxXgJlHkEYaCGZIhieCs9NU1HU1BcqQ4RfUdSsfRdxxktqUKgCnBnOOn0oD3PPA8cQOCAVgIyRb3Rfw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-ini@3.958.0': + resolution: {integrity: sha512-u7twvZa1/6GWmPBZs6DbjlegCoNzNjBsMS/6fvh5quByYrcJr/uLd8YEr7S3UIq4kR/gSnHqcae7y2nL2bqZdg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-login@3.958.0': + resolution: {integrity: sha512-sDwtDnBSszUIbzbOORGh5gmXGl9aK25+BHb4gb1aVlqB+nNL2+IUEJA62+CE55lXSH8qXF90paivjK8tOHTwPA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-node@3.958.0': + resolution: {integrity: sha512-vdoZbNG2dt66I7EpN3fKCzi6fp9xjIiwEA/vVVgqO4wXCGw8rKPIdDUus4e13VvTr330uQs2W0UNg/7AgtquEQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-process@3.957.0': + resolution: {integrity: sha512-/KIz9kadwbeLy6SKvT79W81Y+hb/8LMDyeloA2zhouE28hmne+hLn0wNCQXAAupFFlYOAtZR2NTBs7HBAReJlg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-sso@3.958.0': + resolution: {integrity: sha512-CBYHJ5ufp8HC4q+o7IJejCUctJXWaksgpmoFpXerbjAso7/Fg7LLUu9inXVOxlHKLlvYekDXjIUBXDJS2WYdgg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.958.0': + resolution: {integrity: sha512-dgnvwjMq5Y66WozzUzxNkCFap+umHUtqMMKlr8z/vl9NYMLem/WUbWNpFFOVFWquXikc+ewtpBMR4KEDXfZ+KA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-host-header@3.957.0': + resolution: {integrity: sha512-BBgKawVyfQZglEkNTuBBdC3azlyqNXsvvN4jPkWAiNYcY0x1BasaJFl+7u/HisfULstryweJq/dAvIZIxzlZaA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-logger@3.957.0': + resolution: {integrity: sha512-w1qfKrSKHf9b5a8O76yQ1t69u6NWuBjr5kBX+jRWFx/5mu6RLpqERXRpVJxfosbep7k3B+DSB5tZMZ82GKcJtQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.957.0': + resolution: {integrity: sha512-D2H/WoxhAZNYX+IjkKTdOhOkWQaK0jjJrDBj56hKjU5c9ltQiaX/1PqJ4dfjHntEshJfu0w+E6XJ+/6A6ILBBA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.957.0': + resolution: {integrity: sha512-5B2qY2nR2LYpxoQP0xUum5A1UNvH2JQpLHDH1nWFNF/XetV7ipFHksMxPNhtJJ6ARaWhQIDXfOUj0jcnkJxXUg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-user-agent@3.957.0': + resolution: {integrity: sha512-50vcHu96XakQnIvlKJ1UoltrFODjsq2KvtTgHiPFteUS884lQnK5VC/8xd1Msz/1ONpLMzdCVproCQqhDTtMPQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/nested-clients@3.958.0': + resolution: {integrity: sha512-/KuCcS8b5TpQXkYOrPLYytrgxBhv81+5pChkOlhegbeHttjM69pyUpQVJqyfDM/A7wPLnDrzCAnk4zaAOkY0Nw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/region-config-resolver@3.957.0': + resolution: {integrity: sha512-V8iY3blh8l2iaOqXWW88HbkY5jDoWjH56jonprG/cpyqqCnprvpMUZWPWYJoI8rHRf2bqzZeql1slxG6EnKI7A==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.957.0': + resolution: {integrity: sha512-t6UfP1xMUigMMzHcb7vaZcjv7dA2DQkk9C/OAP1dKyrE0vb4lFGDaTApi17GN6Km9zFxJthEMUbBc7DL0hq1Bg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/token-providers@3.958.0': + resolution: {integrity: sha512-UCj7lQXODduD1myNJQkV+LYcGYJ9iiMggR8ow8Hva1g3A/Na5imNXzz6O67k7DAee0TYpy+gkNw+SizC6min8Q==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/types@3.957.0': + resolution: {integrity: sha512-wzWC2Nrt859ABk6UCAVY/WYEbAd7FjkdrQL6m24+tfmWYDNRByTJ9uOgU/kw9zqLCAwb//CPvrJdhqjTznWXAg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-arn-parser@3.957.0': + resolution: {integrity: sha512-Aj6m+AyrhWyg8YQ4LDPg2/gIfGHCEcoQdBt5DeSFogN5k9mmJPOJ+IAmNSWmWRjpOxEy6eY813RNDI6qS97M0g==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-endpoints@3.957.0': + resolution: {integrity: sha512-xwF9K24mZSxcxKS3UKQFeX/dPYkEps9wF1b+MGON7EvnbcucrJGyQyK1v1xFPn1aqXkBTFi+SZaMRx5E5YCVFw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-locate-window@3.957.0': + resolution: {integrity: sha512-nhmgKHnNV9K+i9daumaIz8JTLsIIML9PE/HUks5liyrjUzenjW/aHoc7WJ9/Td/gPZtayxFnXQSJRb/fDlBuJw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-user-agent-browser@3.957.0': + resolution: {integrity: sha512-exueuwxef0lUJRnGaVkNSC674eAiWU07ORhxBnevFFZEKisln+09Qrtw823iyv5I1N8T+wKfh95xvtWQrNKNQw==} + + '@aws-sdk/util-user-agent-node@3.957.0': + resolution: {integrity: sha512-ycbYCwqXk4gJGp0Oxkzf2KBeeGBdTxz559D41NJP8FlzSej1Gh7Rk40Zo6AyTfsNWkrl/kVi1t937OIzC5t+9Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.957.0': + resolution: {integrity: sha512-Ai5iiQqS8kJ5PjzMhWcLKN0G2yasAkvpnPlq2EnqlIMdB48HsizElt62qcktdxp4neRMyGkFq4NzgmDbXnhRiA==} + engines: {node: '>=18.0.0'} + + '@aws/lambda-invoke-store@0.2.2': + resolution: {integrity: sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -401,11 +539,14 @@ packages: '@fastify/ajv-compiler@4.0.5': resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + '@fastify/busboy@3.2.0': + resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} + '@fastify/compress@8.3.1': resolution: {integrity: sha512-BUpItLr6MUX9e9ukg5Y6xekyA/7pBFG8QWtFCrUDm9ctoBc3R2/nA16yOaOWtVoccpXGjdDEYA/MxAb5+8cxag==} - '@fastify/cookie@9.4.0': - resolution: {integrity: sha512-Th+pt3kEkh4MQD/Q2q1bMuJIB5NX/D5SwSpOKu3G/tjoGbwfpurIMJsWSPS0SJJ4eyjtmQ8OipDQspf8RbUOlg==} + '@fastify/cookie@11.0.2': + resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==} '@fastify/cors@10.1.0': resolution: {integrity: sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==} @@ -443,6 +584,72 @@ packages: '@fastify/static@8.3.0': resolution: {integrity: sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==} + '@firebase/app-check-interop-types@0.3.3': + resolution: {integrity: sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==} + + '@firebase/app-types@0.9.3': + resolution: {integrity: sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==} + + '@firebase/auth-interop-types@0.2.4': + resolution: {integrity: sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==} + + '@firebase/component@0.7.0': + resolution: {integrity: sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==} + engines: {node: '>=20.0.0'} + + '@firebase/database-compat@2.1.0': + resolution: {integrity: sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==} + engines: {node: '>=20.0.0'} + + '@firebase/database-types@1.0.16': + resolution: {integrity: sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==} + + '@firebase/database@1.1.0': + resolution: {integrity: sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==} + engines: {node: '>=20.0.0'} + + '@firebase/logger@0.5.0': + resolution: {integrity: sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==} + engines: {node: '>=20.0.0'} + + '@firebase/util@1.13.0': + resolution: {integrity: sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==} + engines: {node: '>=20.0.0'} + + '@google-cloud/firestore@7.11.6': + resolution: {integrity: sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw==} + engines: {node: '>=14.0.0'} + + '@google-cloud/paginator@5.0.2': + resolution: {integrity: sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==} + engines: {node: '>=14.0.0'} + + '@google-cloud/projectify@4.0.0': + resolution: {integrity: sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==} + engines: {node: '>=14.0.0'} + + '@google-cloud/promisify@4.0.0': + resolution: {integrity: sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==} + engines: {node: '>=14'} + + '@google-cloud/storage@7.18.0': + resolution: {integrity: sha512-r3ZwDMiz4nwW6R922Z1pwpePxyRwE5GdevYX63hRmAQUkUQJcBH/79EnQPDv5cOv1mFBgevdNWQfi3tie3dHrQ==} + engines: {node: '>=14'} + + '@grpc/grpc-js@1.14.3': + resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + + '@grpc/proto-loader@0.8.0': + resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} + engines: {node: '>=6'} + hasBin: true + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -726,6 +933,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} @@ -867,6 +1077,10 @@ packages: engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'} hasBin: true + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} @@ -881,6 +1095,36 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} @@ -893,6 +1137,178 @@ packages: '@sinonjs/fake-timers@13.0.5': resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} + '@smithy/abort-controller@4.2.7': + resolution: {integrity: sha512-rzMY6CaKx2qxrbYbqjXWS0plqEy7LOdKHS0bg4ixJ6aoGDPNUcLWk/FRNuCILh7GKLG9TFUXYYeQQldMBBwuyw==} + engines: {node: '>=18.0.0'} + + '@smithy/config-resolver@4.4.5': + resolution: {integrity: sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.20.0': + resolution: {integrity: sha512-WsSHCPq/neD5G/MkK4csLI5Y5Pkd9c1NMfpYEKeghSGaD4Ja1qLIohRQf2D5c1Uy5aXp76DeKHkzWZ9KAlHroQ==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.2.7': + resolution: {integrity: sha512-CmduWdCiILCRNbQWFR0OcZlUPVtyE49Sr8yYL0rZQ4D/wKxiNzBNS/YHemvnbkIWj623fplgkexUd/c9CAKdoA==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.3.8': + resolution: {integrity: sha512-h/Fi+o7mti4n8wx1SR6UHWLaakwHRx29sizvp8OOm7iqwKGFneT06GCSFhml6Bha5BT6ot5pj3CYZnCHhGC2Rg==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.2.7': + resolution: {integrity: sha512-PU/JWLTBCV1c8FtB8tEFnY4eV1tSfBc7bDBADHfn1K+uRbPgSJ9jnJp0hyjiFN2PMdPzxsf1Fdu0eo9fJ760Xw==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.2.7': + resolution: {integrity: sha512-ncvgCr9a15nPlkhIUx3CU4d7E7WEuVJOV7fS7nnK2hLtPK9tYRBkMHQbhXU1VvvKeBm/O0x26OEoBq+ngFpOEQ==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.2.0': + resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.7': + resolution: {integrity: sha512-GszfBfCcvt7kIbJ41LuNa5f0wvQCHhnGx/aDaZJCCT05Ld6x6U2s0xsc/0mBFONBZjQJp2U/0uSJ178OXOwbhg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.4.1': + resolution: {integrity: sha512-gpLspUAoe6f1M6H0u4cVuFzxZBrsGZmjx2O9SigurTx4PbntYa4AJ+o0G0oGm1L2oSX6oBhcGHwrfJHup2JnJg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.4.17': + resolution: {integrity: sha512-MqbXK6Y9uq17h+4r0ogu/sBT6V/rdV+5NvYL7ZV444BKfQygYe8wAhDrVXagVebN6w2RE0Fm245l69mOsPGZzg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.2.8': + resolution: {integrity: sha512-8rDGYen5m5+NV9eHv9ry0sqm2gI6W7mc1VSFMtn6Igo25S507/HaOX9LTHAS2/J32VXD0xSzrY0H5FJtOMS4/w==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.2.7': + resolution: {integrity: sha512-bsOT0rJ+HHlZd9crHoS37mt8qRRN/h9jRve1SXUhVbkRzu0QaNYZp1i1jha4n098tsvROjcwfLlfvcFuJSXEsw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.3.7': + resolution: {integrity: sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.4.7': + resolution: {integrity: sha512-NELpdmBOO6EpZtWgQiHjoShs1kmweaiNuETUpuup+cmm/xJYjT4eUjfhrXRP4jCOaAsS3c3yPsP3B+K+/fyPCQ==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.2.7': + resolution: {integrity: sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.3.7': + resolution: {integrity: sha512-1r07pb994I20dD/c2seaZhoCuNYm0rWrvBxhCQ70brNh11M5Ml2ew6qJVo0lclB3jMIXirD4s2XRXRe7QEi0xA==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.2.7': + resolution: {integrity: sha512-eKONSywHZxK4tBxe2lXEysh8wbBdvDWiA+RIuaxZSgCMmA0zMgoDpGLJhnyj+c0leOQprVnXOmcB4m+W9Rw7sg==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.2.7': + resolution: {integrity: sha512-3X5ZvzUHmlSTHAXFlswrS6EGt8fMSIxX/c3Rm1Pni3+wYWB6cjGocmRIoqcQF9nU5OgGmL0u7l9m44tSUpfj9w==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.2.7': + resolution: {integrity: sha512-YB7oCbukqEb2Dlh3340/8g8vNGbs/QsNNRms+gv3N2AtZz9/1vSBx6/6tpwQpZMEJFs7Uq8h4mmOn48ZZ72MkA==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.4.2': + resolution: {integrity: sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.3.7': + resolution: {integrity: sha512-9oNUlqBlFZFOSdxgImA6X5GFuzE7V2H7VG/7E70cdLhidFbdtvxxt81EHgykGK5vq5D3FafH//X+Oy31j3CKOg==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.10.2': + resolution: {integrity: sha512-D5z79xQWpgrGpAHb054Fn2CCTQZpog7JELbVQ6XAvXs5MNKWf28U9gzSBlJkOyMl9LA1TZEjRtwvGXfP0Sl90g==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.11.0': + resolution: {integrity: sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.2.7': + resolution: {integrity: sha512-/RLtVsRV4uY3qPWhBDsjwahAtt3x2IsMGnP5W1b2VZIe+qgCqkLxI1UOHDZp1Q1QSOrdOR32MF3Ph2JfWT1VHg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.3.0': + resolution: {integrity: sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.2.0': + resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.2.1': + resolution: {integrity: sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.2.0': + resolution: {integrity: sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.2.0': + resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.3.16': + resolution: {integrity: sha512-/eiSP3mzY3TsvUOYMeL4EqUX6fgUOj2eUOU4rMMgVbq67TiRLyxT7Xsjxq0bW3OwuzK009qOwF0L2OgJqperAQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.19': + resolution: {integrity: sha512-3a4+4mhf6VycEJyHIQLypRbiwG6aJvbQAeRAVXydMmfweEPnLLabRbdyo/Pjw8Rew9vjsh5WCdhmDaHkQnhhhA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.2.7': + resolution: {integrity: sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.2.0': + resolution: {integrity: sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.2.7': + resolution: {integrity: sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.2.7': + resolution: {integrity: sha512-SvDdsQyF5CIASa4EYVT02LukPHVzAgUA4kMAuZ97QJc2BpAqZfA4PINB8/KOoCXEw9tsuv/jQjMeaHFvxdLNGg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.5.8': + resolution: {integrity: sha512-ZnnBhTapjM0YPGUSmOs0Mcg/Gg87k503qG4zU2v/+Js2Gu+daKOJMeqcQns8ajepY8tgzzfYxl6kQyZKml6O2w==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.2.0': + resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.2.0': + resolution: {integrity: sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==} + engines: {node: '>=18.0.0'} + + '@smithy/uuid@1.1.0': + resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} + engines: {node: '>=18.0.0'} + '@tokenizer/inflate@0.4.1': resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} @@ -900,6 +1316,10 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + '@tsconfig/node10@1.0.12': resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} @@ -915,6 +1335,9 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/amqplib@0.10.8': + resolution: {integrity: sha512-vtDp8Pk1wsE/AuQ8/Rgtm6KUZYqcnTgNvEHwzCkX8rL7AGsC6zqAfKAAJhUZXFhM/Pp++tbnUHiam/8vVpPztA==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -927,6 +1350,15 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/caseless@0.12.5': + resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/cookiejar@2.1.5': resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} @@ -939,6 +1371,15 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@4.19.7': + resolution: {integrity: sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==} + + '@types/express@4.17.25': + resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -957,15 +1398,42 @@ packages: '@types/jsonwebtoken@9.0.10': resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/long@4.0.2': + resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} '@types/node@22.19.3': resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==} + '@types/nodemailer@7.0.4': + resolution: {integrity: sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/request@2.48.13': + resolution: {integrity: sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==} + + '@types/send@0.17.6': + resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@1.15.10': + resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -975,6 +1443,9 @@ packages: '@types/supertest@6.0.3': resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/validator@13.15.10': resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} @@ -1219,6 +1690,14 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -1299,9 +1778,16 @@ packages: array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + arrify@2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -1350,9 +1836,15 @@ packages: resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} hasBin: true + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bowser@2.13.1: + resolution: {integrity: sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -1515,10 +2007,6 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} @@ -1774,6 +2262,13 @@ packages: resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + farmhash-modern@1.1.0: + resolution: {integrity: sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==} + engines: {node: '>=18.0.0'} + fast-copy@4.0.2: resolution: {integrity: sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==} @@ -1804,8 +2299,13 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fastify-plugin@4.5.1: - resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} + fast-xml-parser@4.5.3: + resolution: {integrity: sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==} + hasBin: true + + fast-xml-parser@5.2.5: + resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} + hasBin: true fastify-plugin@5.1.0: resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} @@ -1816,6 +2316,10 @@ packages: fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} @@ -1852,6 +2356,10 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + firebase-admin@13.6.0: + resolution: {integrity: sha512-GdPA/t0+Cq8p1JnjFRBmxRxAGvF/kl2yfdhALl38PrRp325YxyQ5aNaHui0XmaKcKiGRFIJ/EgBNWFoDP0onjw==} + engines: {node: '>=18'} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -1879,6 +2387,10 @@ packages: typescript: '>3.6.0' webpack: ^5.11.0 + form-data@2.5.5: + resolution: {integrity: sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==} + engines: {node: '>= 0.12'} + form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} @@ -1905,6 +2417,17 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + functional-red-black-tree@1.0.1: + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1961,6 +2484,18 @@ packages: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} + google-auth-library@9.15.1: + resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} + engines: {node: '>=14'} + + google-gax@4.6.1: + resolution: {integrity: sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==} + engines: {node: '>=14'} + + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -1968,6 +2503,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -1996,6 +2535,9 @@ packages: help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -2003,6 +2545,21 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + http-parser-js@0.5.10: + resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -2250,6 +2807,9 @@ packages: node-notifier: optional: true + jose@4.15.9: + resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -2270,6 +2830,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -2306,6 +2869,10 @@ packages: jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + jwks-rsa@3.2.0: + resolution: {integrity: sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==} + engines: {node: '>=14'} + jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} @@ -2326,6 +2893,9 @@ packages: light-my-request@6.6.0: resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + limiter@1.1.5: + resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -2345,6 +2915,12 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -2379,6 +2955,9 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2389,6 +2968,13 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru-memoizer@2.3.0: + resolution: {integrity: sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -2492,6 +3078,19 @@ packages: node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-forge@1.3.3: + resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==} + engines: {node: '>= 6.13.0'} + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -2506,6 +3105,10 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -2671,6 +3274,14 @@ packages: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} + proto3-json-serializer@2.0.2: + resolution: {integrity: sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==} + engines: {node: '>=14.0.0'} + + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -2750,6 +3361,14 @@ packages: resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} engines: {node: '>=10'} + retry-request@7.0.2: + resolution: {integrity: sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==} + engines: {node: '>=14'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -2879,6 +3498,9 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + stream-events@1.0.5: + resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} + stream-shift@1.0.3: resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} @@ -2937,10 +3559,19 @@ packages: '@types/node': optional: true + strnum@1.1.2: + resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} + + strnum@2.1.2: + resolution: {integrity: sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==} + strtok3@10.3.4: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} engines: {node: '>=18'} + stubs@3.0.0: + resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} + superagent@10.2.3: resolution: {integrity: sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==} engines: {node: '>=14.18.0'} @@ -2972,6 +3603,10 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + teeny-request@9.0.0: + resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==} + engines: {node: '>=14'} + terser-webpack-plugin@5.3.16: resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} engines: {node: '>= 10.13.0'} @@ -3026,6 +3661,9 @@ packages: resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==} engines: {node: '>=14.16'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -3154,6 +3792,18 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -3175,6 +3825,9 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webpack-node-externals@3.0.0: resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} engines: {node: '>=6'} @@ -3193,6 +3846,17 @@ packages: webpack-cli: optional: true + websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3235,6 +3899,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -3311,6 +3978,404 @@ snapshots: transitivePeerDependencies: - chokidar + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.957.0 + '@aws-sdk/util-locate-window': 3.957.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.957.0 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.957.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-sesv2@3.958.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.957.0 + '@aws-sdk/credential-provider-node': 3.958.0 + '@aws-sdk/middleware-host-header': 3.957.0 + '@aws-sdk/middleware-logger': 3.957.0 + '@aws-sdk/middleware-recursion-detection': 3.957.0 + '@aws-sdk/middleware-user-agent': 3.957.0 + '@aws-sdk/region-config-resolver': 3.957.0 + '@aws-sdk/signature-v4-multi-region': 3.957.0 + '@aws-sdk/types': 3.957.0 + '@aws-sdk/util-endpoints': 3.957.0 + '@aws-sdk/util-user-agent-browser': 3.957.0 + '@aws-sdk/util-user-agent-node': 3.957.0 + '@smithy/config-resolver': 4.4.5 + '@smithy/core': 3.20.0 + '@smithy/fetch-http-handler': 5.3.8 + '@smithy/hash-node': 4.2.7 + '@smithy/invalid-dependency': 4.2.7 + '@smithy/middleware-content-length': 4.2.7 + '@smithy/middleware-endpoint': 4.4.1 + '@smithy/middleware-retry': 4.4.17 + '@smithy/middleware-serde': 4.2.8 + '@smithy/middleware-stack': 4.2.7 + '@smithy/node-config-provider': 4.3.7 + '@smithy/node-http-handler': 4.4.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/smithy-client': 4.10.2 + '@smithy/types': 4.11.0 + '@smithy/url-parser': 4.2.7 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.16 + '@smithy/util-defaults-mode-node': 4.2.19 + '@smithy/util-endpoints': 3.2.7 + '@smithy/util-middleware': 4.2.7 + '@smithy/util-retry': 4.2.7 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sso@3.958.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.957.0 + '@aws-sdk/middleware-host-header': 3.957.0 + '@aws-sdk/middleware-logger': 3.957.0 + '@aws-sdk/middleware-recursion-detection': 3.957.0 + '@aws-sdk/middleware-user-agent': 3.957.0 + '@aws-sdk/region-config-resolver': 3.957.0 + '@aws-sdk/types': 3.957.0 + '@aws-sdk/util-endpoints': 3.957.0 + '@aws-sdk/util-user-agent-browser': 3.957.0 + '@aws-sdk/util-user-agent-node': 3.957.0 + '@smithy/config-resolver': 4.4.5 + '@smithy/core': 3.20.0 + '@smithy/fetch-http-handler': 5.3.8 + '@smithy/hash-node': 4.2.7 + '@smithy/invalid-dependency': 4.2.7 + '@smithy/middleware-content-length': 4.2.7 + '@smithy/middleware-endpoint': 4.4.1 + '@smithy/middleware-retry': 4.4.17 + '@smithy/middleware-serde': 4.2.8 + '@smithy/middleware-stack': 4.2.7 + '@smithy/node-config-provider': 4.3.7 + '@smithy/node-http-handler': 4.4.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/smithy-client': 4.10.2 + '@smithy/types': 4.11.0 + '@smithy/url-parser': 4.2.7 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.16 + '@smithy/util-defaults-mode-node': 4.2.19 + '@smithy/util-endpoints': 3.2.7 + '@smithy/util-middleware': 4.2.7 + '@smithy/util-retry': 4.2.7 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.957.0': + dependencies: + '@aws-sdk/types': 3.957.0 + '@aws-sdk/xml-builder': 3.957.0 + '@smithy/core': 3.20.0 + '@smithy/node-config-provider': 4.3.7 + '@smithy/property-provider': 4.2.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/signature-v4': 5.3.7 + '@smithy/smithy-client': 4.10.2 + '@smithy/types': 4.11.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-middleware': 4.2.7 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.957.0': + dependencies: + '@aws-sdk/core': 3.957.0 + '@aws-sdk/types': 3.957.0 + '@smithy/property-provider': 4.2.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.957.0': + dependencies: + '@aws-sdk/core': 3.957.0 + '@aws-sdk/types': 3.957.0 + '@smithy/fetch-http-handler': 5.3.8 + '@smithy/node-http-handler': 4.4.7 + '@smithy/property-provider': 4.2.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/smithy-client': 4.10.2 + '@smithy/types': 4.11.0 + '@smithy/util-stream': 4.5.8 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.958.0': + dependencies: + '@aws-sdk/core': 3.957.0 + '@aws-sdk/credential-provider-env': 3.957.0 + '@aws-sdk/credential-provider-http': 3.957.0 + '@aws-sdk/credential-provider-login': 3.958.0 + '@aws-sdk/credential-provider-process': 3.957.0 + '@aws-sdk/credential-provider-sso': 3.958.0 + '@aws-sdk/credential-provider-web-identity': 3.958.0 + '@aws-sdk/nested-clients': 3.958.0 + '@aws-sdk/types': 3.957.0 + '@smithy/credential-provider-imds': 4.2.7 + '@smithy/property-provider': 4.2.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.958.0': + dependencies: + '@aws-sdk/core': 3.957.0 + '@aws-sdk/nested-clients': 3.958.0 + '@aws-sdk/types': 3.957.0 + '@smithy/property-provider': 4.2.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.958.0': + dependencies: + '@aws-sdk/credential-provider-env': 3.957.0 + '@aws-sdk/credential-provider-http': 3.957.0 + '@aws-sdk/credential-provider-ini': 3.958.0 + '@aws-sdk/credential-provider-process': 3.957.0 + '@aws-sdk/credential-provider-sso': 3.958.0 + '@aws-sdk/credential-provider-web-identity': 3.958.0 + '@aws-sdk/types': 3.957.0 + '@smithy/credential-provider-imds': 4.2.7 + '@smithy/property-provider': 4.2.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.957.0': + dependencies: + '@aws-sdk/core': 3.957.0 + '@aws-sdk/types': 3.957.0 + '@smithy/property-provider': 4.2.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.958.0': + dependencies: + '@aws-sdk/client-sso': 3.958.0 + '@aws-sdk/core': 3.957.0 + '@aws-sdk/token-providers': 3.958.0 + '@aws-sdk/types': 3.957.0 + '@smithy/property-provider': 4.2.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.958.0': + dependencies: + '@aws-sdk/core': 3.957.0 + '@aws-sdk/nested-clients': 3.958.0 + '@aws-sdk/types': 3.957.0 + '@smithy/property-provider': 4.2.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/middleware-host-header@3.957.0': + dependencies: + '@aws-sdk/types': 3.957.0 + '@smithy/protocol-http': 5.3.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.957.0': + dependencies: + '@aws-sdk/types': 3.957.0 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.957.0': + dependencies: + '@aws-sdk/types': 3.957.0 + '@aws/lambda-invoke-store': 0.2.2 + '@smithy/protocol-http': 5.3.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.957.0': + dependencies: + '@aws-sdk/core': 3.957.0 + '@aws-sdk/types': 3.957.0 + '@aws-sdk/util-arn-parser': 3.957.0 + '@smithy/core': 3.20.0 + '@smithy/node-config-provider': 4.3.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/signature-v4': 5.3.7 + '@smithy/smithy-client': 4.10.2 + '@smithy/types': 4.11.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-middleware': 4.2.7 + '@smithy/util-stream': 4.5.8 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.957.0': + dependencies: + '@aws-sdk/core': 3.957.0 + '@aws-sdk/types': 3.957.0 + '@aws-sdk/util-endpoints': 3.957.0 + '@smithy/core': 3.20.0 + '@smithy/protocol-http': 5.3.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.958.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.957.0 + '@aws-sdk/middleware-host-header': 3.957.0 + '@aws-sdk/middleware-logger': 3.957.0 + '@aws-sdk/middleware-recursion-detection': 3.957.0 + '@aws-sdk/middleware-user-agent': 3.957.0 + '@aws-sdk/region-config-resolver': 3.957.0 + '@aws-sdk/types': 3.957.0 + '@aws-sdk/util-endpoints': 3.957.0 + '@aws-sdk/util-user-agent-browser': 3.957.0 + '@aws-sdk/util-user-agent-node': 3.957.0 + '@smithy/config-resolver': 4.4.5 + '@smithy/core': 3.20.0 + '@smithy/fetch-http-handler': 5.3.8 + '@smithy/hash-node': 4.2.7 + '@smithy/invalid-dependency': 4.2.7 + '@smithy/middleware-content-length': 4.2.7 + '@smithy/middleware-endpoint': 4.4.1 + '@smithy/middleware-retry': 4.4.17 + '@smithy/middleware-serde': 4.2.8 + '@smithy/middleware-stack': 4.2.7 + '@smithy/node-config-provider': 4.3.7 + '@smithy/node-http-handler': 4.4.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/smithy-client': 4.10.2 + '@smithy/types': 4.11.0 + '@smithy/url-parser': 4.2.7 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.16 + '@smithy/util-defaults-mode-node': 4.2.19 + '@smithy/util-endpoints': 3.2.7 + '@smithy/util-middleware': 4.2.7 + '@smithy/util-retry': 4.2.7 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.957.0': + dependencies: + '@aws-sdk/types': 3.957.0 + '@smithy/config-resolver': 4.4.5 + '@smithy/node-config-provider': 4.3.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.957.0': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.957.0 + '@aws-sdk/types': 3.957.0 + '@smithy/protocol-http': 5.3.7 + '@smithy/signature-v4': 5.3.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.958.0': + dependencies: + '@aws-sdk/core': 3.957.0 + '@aws-sdk/nested-clients': 3.958.0 + '@aws-sdk/types': 3.957.0 + '@smithy/property-provider': 4.2.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.957.0': + dependencies: + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.957.0': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.957.0': + dependencies: + '@aws-sdk/types': 3.957.0 + '@smithy/types': 4.11.0 + '@smithy/url-parser': 4.2.7 + '@smithy/util-endpoints': 3.2.7 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.957.0': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.957.0': + dependencies: + '@aws-sdk/types': 3.957.0 + '@smithy/types': 4.11.0 + bowser: 2.13.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.957.0': + dependencies: + '@aws-sdk/middleware-user-agent': 3.957.0 + '@aws-sdk/types': 3.957.0 + '@smithy/node-config-provider': 4.3.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.957.0': + dependencies: + '@smithy/types': 4.11.0 + fast-xml-parser: 5.2.5 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.2': {} + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -3579,6 +4644,8 @@ snapshots: ajv-formats: 3.0.1(ajv@8.17.1) fast-uri: 3.1.0 + '@fastify/busboy@3.2.0': {} + '@fastify/compress@8.3.1': dependencies: '@fastify/accept-negotiator': 2.0.1 @@ -3590,10 +4657,10 @@ snapshots: pumpify: 2.0.1 readable-stream: 4.7.0 - '@fastify/cookie@9.4.0': + '@fastify/cookie@11.0.2': dependencies: - cookie-signature: 1.2.2 - fastify-plugin: 4.5.1 + cookie: 1.1.1 + fastify-plugin: 5.1.0 '@fastify/cors@10.1.0': dependencies: @@ -3656,6 +4723,117 @@ snapshots: fastq: 1.20.1 glob: 11.1.0 + '@firebase/app-check-interop-types@0.3.3': {} + + '@firebase/app-types@0.9.3': {} + + '@firebase/auth-interop-types@0.2.4': {} + + '@firebase/component@0.7.0': + dependencies: + '@firebase/util': 1.13.0 + tslib: 2.8.1 + + '@firebase/database-compat@2.1.0': + dependencies: + '@firebase/component': 0.7.0 + '@firebase/database': 1.1.0 + '@firebase/database-types': 1.0.16 + '@firebase/logger': 0.5.0 + '@firebase/util': 1.13.0 + tslib: 2.8.1 + + '@firebase/database-types@1.0.16': + dependencies: + '@firebase/app-types': 0.9.3 + '@firebase/util': 1.13.0 + + '@firebase/database@1.1.0': + dependencies: + '@firebase/app-check-interop-types': 0.3.3 + '@firebase/auth-interop-types': 0.2.4 + '@firebase/component': 0.7.0 + '@firebase/logger': 0.5.0 + '@firebase/util': 1.13.0 + faye-websocket: 0.11.4 + tslib: 2.8.1 + + '@firebase/logger@0.5.0': + dependencies: + tslib: 2.8.1 + + '@firebase/util@1.13.0': + dependencies: + tslib: 2.8.1 + + '@google-cloud/firestore@7.11.6': + dependencies: + '@opentelemetry/api': 1.9.0 + fast-deep-equal: 3.1.3 + functional-red-black-tree: 1.0.1 + google-gax: 4.6.1 + protobufjs: 7.5.4 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@google-cloud/paginator@5.0.2': + dependencies: + arrify: 2.0.1 + extend: 3.0.2 + optional: true + + '@google-cloud/projectify@4.0.0': + optional: true + + '@google-cloud/promisify@4.0.0': + optional: true + + '@google-cloud/storage@7.18.0': + dependencies: + '@google-cloud/paginator': 5.0.2 + '@google-cloud/projectify': 4.0.0 + '@google-cloud/promisify': 4.0.0 + abort-controller: 3.0.0 + async-retry: 1.3.3 + duplexify: 4.1.3 + fast-xml-parser: 4.5.3 + gaxios: 6.7.1 + google-auth-library: 9.15.1 + html-entities: 2.6.0 + mime: 3.0.0 + p-limit: 3.1.0 + retry-request: 7.0.2 + teeny-request: 9.0.0 + uuid: 8.3.2 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@grpc/grpc-js@1.14.3': + dependencies: + '@grpc/proto-loader': 0.8.0 + '@js-sdsl/ordered-map': 4.4.2 + optional: true + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 + optional: true + + '@grpc/proto-loader@0.8.0': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 + optional: true + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -4040,6 +5218,9 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@js-sdsl/ordered-map@4.4.2': + optional: true + '@lukeed/csprng@1.1.0': {} '@lukeed/ms@2.0.2': {} @@ -4182,6 +5363,9 @@ snapshots: dependencies: consola: 3.4.2 + '@opentelemetry/api@1.9.0': + optional: true + '@paralleldrive/cuid2@2.3.1': dependencies: '@noble/hashes': 1.8.0 @@ -4193,6 +5377,39 @@ snapshots: '@pkgr/core@0.2.9': {} + '@protobufjs/aspromise@1.1.2': + optional: true + + '@protobufjs/base64@1.1.2': + optional: true + + '@protobufjs/codegen@2.0.4': + optional: true + + '@protobufjs/eventemitter@1.1.0': + optional: true + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + optional: true + + '@protobufjs/float@1.0.2': + optional: true + + '@protobufjs/inquire@1.1.0': + optional: true + + '@protobufjs/path@1.1.2': + optional: true + + '@protobufjs/pool@1.1.0': + optional: true + + '@protobufjs/utf8@1.1.0': + optional: true + '@scarf/scarf@1.4.0': {} '@sinclair/typebox@0.34.41': {} @@ -4205,6 +5422,280 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@smithy/abort-controller@4.2.7': + dependencies: + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/config-resolver@4.4.5': + dependencies: + '@smithy/node-config-provider': 4.3.7 + '@smithy/types': 4.11.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-endpoints': 3.2.7 + '@smithy/util-middleware': 4.2.7 + tslib: 2.8.1 + + '@smithy/core@3.20.0': + dependencies: + '@smithy/middleware-serde': 4.2.8 + '@smithy/protocol-http': 5.3.7 + '@smithy/types': 4.11.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-middleware': 4.2.7 + '@smithy/util-stream': 4.5.8 + '@smithy/util-utf8': 4.2.0 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.2.7': + dependencies: + '@smithy/node-config-provider': 4.3.7 + '@smithy/property-provider': 4.2.7 + '@smithy/types': 4.11.0 + '@smithy/url-parser': 4.2.7 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.3.8': + dependencies: + '@smithy/protocol-http': 5.3.7 + '@smithy/querystring-builder': 4.2.7 + '@smithy/types': 4.11.0 + '@smithy/util-base64': 4.3.0 + tslib: 2.8.1 + + '@smithy/hash-node@4.2.7': + dependencies: + '@smithy/types': 4.11.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.2.7': + dependencies: + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.2.7': + dependencies: + '@smithy/protocol-http': 5.3.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.4.1': + dependencies: + '@smithy/core': 3.20.0 + '@smithy/middleware-serde': 4.2.8 + '@smithy/node-config-provider': 4.3.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + '@smithy/url-parser': 4.2.7 + '@smithy/util-middleware': 4.2.7 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.4.17': + dependencies: + '@smithy/node-config-provider': 4.3.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/service-error-classification': 4.2.7 + '@smithy/smithy-client': 4.10.2 + '@smithy/types': 4.11.0 + '@smithy/util-middleware': 4.2.7 + '@smithy/util-retry': 4.2.7 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.2.8': + dependencies: + '@smithy/protocol-http': 5.3.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.2.7': + dependencies: + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.3.7': + dependencies: + '@smithy/property-provider': 4.2.7 + '@smithy/shared-ini-file-loader': 4.4.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.4.7': + dependencies: + '@smithy/abort-controller': 4.2.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/querystring-builder': 4.2.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/property-provider@4.2.7': + dependencies: + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/protocol-http@5.3.7': + dependencies: + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.2.7': + dependencies: + '@smithy/types': 4.11.0 + '@smithy/util-uri-escape': 4.2.0 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.2.7': + dependencies: + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/service-error-classification@4.2.7': + dependencies: + '@smithy/types': 4.11.0 + + '@smithy/shared-ini-file-loader@4.4.2': + dependencies: + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.3.7': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + '@smithy/protocol-http': 5.3.7 + '@smithy/types': 4.11.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-middleware': 4.2.7 + '@smithy/util-uri-escape': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/smithy-client@4.10.2': + dependencies: + '@smithy/core': 3.20.0 + '@smithy/middleware-endpoint': 4.4.1 + '@smithy/middleware-stack': 4.2.7 + '@smithy/protocol-http': 5.3.7 + '@smithy/types': 4.11.0 + '@smithy/util-stream': 4.5.8 + tslib: 2.8.1 + + '@smithy/types@4.11.0': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.2.7': + dependencies: + '@smithy/querystring-parser': 4.2.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/util-base64@4.3.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.2.1': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.2.0': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-config-provider@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.3.16': + dependencies: + '@smithy/property-provider': 4.2.7 + '@smithy/smithy-client': 4.10.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.19': + dependencies: + '@smithy/config-resolver': 4.4.5 + '@smithy/credential-provider-imds': 4.2.7 + '@smithy/node-config-provider': 4.3.7 + '@smithy/property-provider': 4.2.7 + '@smithy/smithy-client': 4.10.2 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.2.7': + dependencies: + '@smithy/node-config-provider': 4.3.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.2.7': + dependencies: + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/util-retry@4.2.7': + dependencies: + '@smithy/service-error-classification': 4.2.7 + '@smithy/types': 4.11.0 + tslib: 2.8.1 + + '@smithy/util-stream@4.5.8': + dependencies: + '@smithy/fetch-http-handler': 5.3.8 + '@smithy/node-http-handler': 4.4.7 + '@smithy/types': 4.11.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-uri-escape@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.2.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + tslib: 2.8.1 + + '@smithy/uuid@1.1.0': + dependencies: + tslib: 2.8.1 + '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3 @@ -4214,6 +5705,9 @@ snapshots: '@tokenizer/token@0.3.0': {} + '@tootallnate/once@2.0.0': + optional: true + '@tsconfig/node10@1.0.12': {} '@tsconfig/node12@1.0.11': {} @@ -4227,6 +5721,10 @@ snapshots: tslib: 2.8.1 optional: true + '@types/amqplib@0.10.8': + dependencies: + '@types/node': 22.19.3 + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.5 @@ -4248,6 +5746,18 @@ snapshots: dependencies: '@babel/types': 7.28.5 + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.19.3 + + '@types/caseless@0.12.5': + optional: true + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.19.3 + '@types/cookiejar@2.1.5': {} '@types/eslint-scope@3.7.7': @@ -4262,6 +5772,22 @@ snapshots: '@types/estree@1.0.8': {} + '@types/express-serve-static-core@4.19.7': + dependencies: + '@types/node': 22.19.3 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@4.17.25': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.7 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.10 + + '@types/http-errors@2.0.5': {} + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -4284,14 +5810,53 @@ snapshots: '@types/ms': 2.1.0 '@types/node': 22.19.3 + '@types/long@4.0.2': + optional: true + '@types/methods@1.1.4': {} + '@types/mime@1.3.5': {} + '@types/ms@2.1.0': {} '@types/node@22.19.3': dependencies: undici-types: 6.21.0 + '@types/nodemailer@7.0.4': + dependencies: + '@aws-sdk/client-sesv2': 3.958.0 + '@types/node': 22.19.3 + transitivePeerDependencies: + - aws-crt + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/request@2.48.13': + dependencies: + '@types/caseless': 0.12.5 + '@types/node': 22.19.3 + '@types/tough-cookie': 4.0.5 + form-data: 2.5.5 + optional: true + + '@types/send@0.17.6': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 22.19.3 + + '@types/send@1.2.1': + dependencies: + '@types/node': 22.19.3 + + '@types/serve-static@1.15.10': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 22.19.3 + '@types/send': 0.17.6 + '@types/stack-utils@2.0.3': {} '@types/superagent@8.1.9': @@ -4306,6 +5871,9 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 + '@types/tough-cookie@4.0.5': + optional: true + '@types/validator@13.15.10': {} '@types/yargs-parser@21.0.3': {} @@ -4566,6 +6134,15 @@ snapshots: acorn@8.15.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + + agent-base@7.1.4: {} + ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -4632,8 +6209,16 @@ snapshots: array-timsort@1.0.3: {} + arrify@2.0.1: + optional: true + asap@2.0.6: {} + async-retry@1.3.3: + dependencies: + retry: 0.13.1 + optional: true + asynckit@0.4.0: {} atomic-sleep@1.0.0: {} @@ -4709,12 +6294,16 @@ snapshots: baseline-browser-mapping@2.9.11: {} + bignumber.js@9.3.1: {} + bl@4.1.0: dependencies: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 + bowser@2.13.1: {} + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -4863,8 +6452,6 @@ snapshots: convert-source-map@2.0.0: {} - cookie-signature@1.2.2: {} - cookie@1.1.1: {} cookiejar@2.1.4: {} @@ -5113,6 +6700,10 @@ snapshots: jest-mock: 30.2.0 jest-util: 30.2.0 + extend@3.0.2: {} + + farmhash-modern@1.1.0: {} + fast-copy@4.0.2: {} fast-decode-uri-component@1.0.1: {} @@ -5142,7 +6733,14 @@ snapshots: fast-uri@3.1.0: {} - fastify-plugin@4.5.1: {} + fast-xml-parser@4.5.3: + dependencies: + strnum: 1.1.2 + optional: true + + fast-xml-parser@5.2.5: + dependencies: + strnum: 2.1.2 fastify-plugin@5.1.0: {} @@ -5168,6 +6766,10 @@ snapshots: dependencies: reusify: 1.1.0 + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.4 + fb-watchman@2.0.2: dependencies: bser: 2.1.1 @@ -5209,6 +6811,26 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + firebase-admin@13.6.0: + dependencies: + '@fastify/busboy': 3.2.0 + '@firebase/database-compat': 2.1.0 + '@firebase/database-types': 1.0.16 + '@types/node': 22.19.3 + farmhash-modern: 1.1.0 + fast-deep-equal: 3.1.3 + google-auth-library: 9.15.1 + jsonwebtoken: 9.0.3 + jwks-rsa: 3.2.0 + node-forge: 1.3.3 + uuid: 11.1.0 + optionalDependencies: + '@google-cloud/firestore': 7.11.6 + '@google-cloud/storage': 7.18.0 + transitivePeerDependencies: + - encoding + - supports-color + flat-cache@4.0.1: dependencies: flatted: 3.3.3 @@ -5240,6 +6862,16 @@ snapshots: typescript: 5.9.3 webpack: 5.103.0 + form-data@2.5.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + safe-buffer: 5.2.1 + optional: true + form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -5269,6 +6901,29 @@ snapshots: function-bind@1.1.2: {} + functional-red-black-tree@1.0.1: + optional: true + + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gcp-metadata@6.1.1: + dependencies: + gaxios: 6.7.1 + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -5338,10 +6993,51 @@ snapshots: globals@16.5.0: {} + google-auth-library@9.15.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1 + gcp-metadata: 6.1.1 + gtoken: 7.1.0 + jws: 4.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + google-gax@4.6.1: + dependencies: + '@grpc/grpc-js': 1.14.3 + '@grpc/proto-loader': 0.7.15 + '@types/long': 4.0.2 + abort-controller: 3.0.0 + duplexify: 4.1.3 + google-auth-library: 9.15.1 + node-fetch: 2.7.0 + object-hash: 3.0.0 + proto3-json-serializer: 2.0.2 + protobufjs: 7.5.4 + retry-request: 7.0.2 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + google-logging-utils@0.0.2: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} + gtoken@7.1.0: + dependencies: + gaxios: 6.7.1 + jws: 4.0.1 + transitivePeerDependencies: + - encoding + - supports-color + handlebars@4.7.8: dependencies: minimist: 1.2.8 @@ -5367,6 +7063,9 @@ snapshots: help-me@5.0.0: {} + html-entities@2.6.0: + optional: true + html-escaper@2.0.2: {} http-errors@2.0.1: @@ -5377,6 +7076,32 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + http-parser-js@0.5.10: {} + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + human-signals@2.1.0: {} iconv-lite@0.7.1: @@ -5795,6 +7520,8 @@ snapshots: - supports-color - ts-node + jose@4.15.9: {} + joycon@3.1.1: {} js-tokens@4.0.0: {} @@ -5810,6 +7537,10 @@ snapshots: jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + json-buffer@3.0.1: {} json-parse-even-better-errors@2.3.1: {} @@ -5853,6 +7584,17 @@ snapshots: ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 + jwks-rsa@3.2.0: + dependencies: + '@types/express': 4.17.25 + '@types/jsonwebtoken': 9.0.10 + debug: 4.4.3 + jose: 4.15.9 + limiter: 1.1.5 + lru-memoizer: 2.3.0 + transitivePeerDependencies: + - supports-color + jws@4.0.1: dependencies: jwa: 2.0.1 @@ -5877,6 +7619,8 @@ snapshots: process-warning: 4.0.1 set-cookie-parser: 2.7.2 + limiter@1.1.5: {} + lines-and-columns@1.2.4: {} load-esm@1.0.3: {} @@ -5891,6 +7635,11 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.camelcase@4.3.0: + optional: true + + lodash.clonedeep@4.5.0: {} + lodash.includes@4.3.0: {} lodash.isboolean@3.0.3: {} @@ -5916,6 +7665,9 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + long@5.3.2: + optional: true + lru-cache@10.4.3: {} lru-cache@11.2.4: {} @@ -5924,6 +7676,15 @@ snapshots: dependencies: yallist: 3.1.1 + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + lru-memoizer@2.3.0: + dependencies: + lodash.clonedeep: 4.5.0 + lru-cache: 6.0.0 + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -6003,6 +7764,12 @@ snapshots: dependencies: lodash: 4.17.21 + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-forge@1.3.3: {} + node-int64@0.4.0: {} node-releases@2.0.27: {} @@ -6013,6 +7780,9 @@ snapshots: dependencies: path-key: 3.1.1 + object-hash@3.0.0: + optional: true + object-inspect@1.13.4: {} obliterator@2.0.5: {} @@ -6197,6 +7967,27 @@ snapshots: process@0.11.10: {} + proto3-json-serializer@2.0.2: + dependencies: + protobufjs: 7.5.4 + optional: true + + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 22.19.3 + long: 5.3.2 + optional: true + proxy-from-env@1.1.0: {} pump@3.0.3: @@ -6275,6 +8066,19 @@ snapshots: ret@0.5.0: {} + retry-request@7.0.2: + dependencies: + '@types/request': 2.48.13 + extend: 3.0.2 + teeny-request: 9.0.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + retry@0.13.1: + optional: true + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -6396,6 +8200,11 @@ snapshots: statuses@2.0.2: {} + stream-events@1.0.5: + dependencies: + stubs: 3.0.0 + optional: true + stream-shift@1.0.3: {} string-length@4.0.2: @@ -6447,10 +8256,18 @@ snapshots: optionalDependencies: '@types/node': 22.19.3 + strnum@1.1.2: + optional: true + + strnum@2.1.2: {} + strtok3@10.3.4: dependencies: '@tokenizer/token': 0.3.0 + stubs@3.0.0: + optional: true + superagent@10.2.3: dependencies: component-emitter: 1.3.1 @@ -6492,6 +8309,18 @@ snapshots: tapable@2.3.0: {} + teeny-request@9.0.0: + dependencies: + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0 + stream-events: 1.0.5 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + terser-webpack-plugin@5.3.16(webpack@5.103.0): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -6544,6 +8373,8 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + tr46@0.0.3: {} + ts-api-utils@2.1.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -6683,6 +8514,13 @@ snapshots: util-deprecate@1.0.2: {} + uuid@11.1.0: {} + + uuid@8.3.2: + optional: true + + uuid@9.0.1: {} + v8-compile-cache-lib@3.0.1: {} v8-to-istanbul@9.3.0: @@ -6706,6 +8544,8 @@ snapshots: dependencies: defaults: 1.0.4 + webidl-conversions@3.0.1: {} + webpack-node-externals@3.0.0: {} webpack-sources@3.3.3: {} @@ -6742,6 +8582,19 @@ snapshots: - esbuild - uglify-js + websocket-driver@0.7.4: + dependencies: + http-parser-js: 0.5.10 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + websocket-extensions@0.1.4: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -6781,6 +8634,8 @@ snapshots: yallist@3.1.1: {} + yallist@4.0.0: {} + yargs-parser@21.1.1: {} yargs@17.7.2: diff --git a/backoffice/src/app.module.ts b/backoffice/src/app.module.ts index 8e18a5e..ad70846 100644 --- a/backoffice/src/app.module.ts +++ b/backoffice/src/app.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { StripeModule } from './stripe'; @@ -8,16 +8,30 @@ import { AdminModule } from './admin'; import { AuthModule } from './auth'; import { FcmTokensModule } from './fcm-tokens/fcm-tokens.module'; import { ExternalServicesModule } from './external-services/external-services.module'; +import { EmailModule } from './email/email.module'; + +import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' }), + TypeOrmModule.forRootAsync({ + imports: [ConfigModule], + useFactory: (configService: ConfigService) => ({ + type: 'postgres', + url: configService.get('DATABASE_URL'), + autoLoadEntities: true, + synchronize: false, // Managed by Go migrations + }), + inject: [ConfigService], + }), AuthModule, StripeModule, PlansModule, AdminModule, FcmTokensModule, ExternalServicesModule, + EmailModule, // Register Email Module ], controllers: [AppController], providers: [AppService], diff --git a/backoffice/src/email/email.module.ts b/backoffice/src/email/email.module.ts new file mode 100644 index 0000000..2899f6e --- /dev/null +++ b/backoffice/src/email/email.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { EmailService } from './email.service'; +import { EmailSettings } from './entities/email-setting.entity'; +import { EmailTemplate } from './entities/email-template.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([EmailSettings, EmailTemplate])], + providers: [EmailService], + exports: [EmailService], +}) +export class EmailModule { } diff --git a/backoffice/src/email/email.service.ts b/backoffice/src/email/email.service.ts new file mode 100644 index 0000000..1b48049 --- /dev/null +++ b/backoffice/src/email/email.service.ts @@ -0,0 +1,102 @@ +import { Injectable, OnModuleInit, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { EmailSettings } from './entities/email-setting.entity'; +import { EmailTemplate } from './entities/email-template.entity'; +import * as amqp from 'amqplib'; +import * as nodemailer from 'nodemailer'; +import * as handlebars from 'handlebars'; + +@Injectable() +export class EmailService implements OnModuleInit { + private readonly logger = new Logger(EmailService.name); + private connection: any; // Use any to bypass strict type check on amqplib if wrapper types are elusive + private channel: any; + + constructor( + @InjectRepository(EmailSettings) + private settingsRepo: Repository, + @InjectRepository(EmailTemplate) + private templateRepo: Repository, + ) { } + + async onModuleInit() { + this.connectLavinMQ(); + } + + async connectLavinMQ() { + try { + const settings = await this.settingsRepo.findOne({ where: { isActive: true }, order: { updatedAt: 'DESC' } }); + if (!settings || !settings.amqpUrl) { + this.logger.warn('LavinMQ URL not configured in email_settings. Worker disabled.'); + return; + } + + this.logger.log(`Connecting to LavinMQ...`); + this.connection = await amqp.connect(settings.amqpUrl); + this.channel = await this.connection.createChannel(); + + const queue = 'mail_queue'; + await this.channel.assertQueue(queue, { durable: true }); + this.channel.prefetch(1); + + this.logger.log(`Waiting for messages in ${queue}...`); + + this.channel.consume(queue, async (msg) => { + if (msg !== null) { + this.logger.log(`Received message: ${msg.content.toString()}`); + try { + const job = JSON.parse(msg.content.toString()); + await this.processJob(job); + this.channel.ack(msg); + } catch (error) { + this.logger.error(`Failed to process message: ${error.message}`); + // Nack? Or Ack to discard? Discard for now to verify. + this.channel.nack(msg, false, false); // requeue=false + } + } + }); + } catch (error) { + this.logger.error(`Failed to connect to LavinMQ: ${error.message}`); + setTimeout(() => this.connectLavinMQ(), 5000); // Retry + } + } + + async processJob(job: { to: string; template: string; variables: any }) { + const { to, template: slug, variables } = job; + + // 1. Get Template + const tmpl = await this.templateRepo.findOne({ where: { slug } }); + if (!tmpl) { + throw new Error(`Template '${slug}' not found`); + } + + // 2. Compile + const compiledSubject = handlebars.compile(tmpl.subject)(variables); + const compiledBody = handlebars.compile(tmpl.bodyHtml)(variables); + + // 3. Get Settings + const settings = await this.settingsRepo.findOne({ where: { isActive: true } }); + if (!settings) throw new Error('Email settings not found'); + + // 4. Send + const transporter = nodemailer.createTransport({ + host: settings.smtpHost, + port: settings.smtpPort, + secure: settings.smtpSecure, + auth: { + user: settings.smtpUser, + pass: settings.smtpPass, // Assumption: Plaintext or handled by app logic + }, + }); + + await transporter.sendMail({ + from: `"${settings.senderName}" <${settings.senderEmail}>`, + to, + subject: compiledSubject, + html: compiledBody, + }); + + this.logger.log(`Email sent to ${to} (Template: ${slug})`); + } +} diff --git a/backoffice/src/email/entities/email-setting.entity.ts b/backoffice/src/email/entities/email-setting.entity.ts new file mode 100644 index 0000000..36d9f12 --- /dev/null +++ b/backoffice/src/email/entities/email-setting.entity.ts @@ -0,0 +1,40 @@ +import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('email_settings') +export class EmailSettings { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ default: 'smtp' }) + provider: string; + + @Column({ name: 'smtp_host', nullable: true }) + smtpHost: string; + + @Column({ name: 'smtp_port', nullable: true }) + smtpPort: number; + + @Column({ name: 'smtp_user', nullable: true }) + smtpUser: string; + + @Column({ name: 'smtp_pass', nullable: true }) + smtpPass: string; // Stored as is for now + + @Column({ name: 'smtp_secure', default: true }) + smtpSecure: boolean; + + @Column({ name: 'sender_name', default: 'GoHorse Jobs' }) + senderName: string; + + @Column({ name: 'sender_email', default: 'no-reply@gohorsejobs.com' }) + senderEmail: string; + + @Column({ name: 'amqp_url', nullable: true }) + amqpUrl: string; + + @Column({ name: 'is_active', default: true }) + isActive: boolean; + + @CreateDateColumn({ name: 'updated_at' }) + updatedAt: Date; +} diff --git a/backoffice/src/email/entities/email-template.entity.ts b/backoffice/src/email/entities/email-template.entity.ts new file mode 100644 index 0000000..e6a2821 --- /dev/null +++ b/backoffice/src/email/entities/email-template.entity.ts @@ -0,0 +1,25 @@ +import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('email_templates') +export class EmailTemplate { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true }) + slug: string; + + @Column() + subject: string; + + @Column({ name: 'body_html', type: 'text' }) + bodyHtml: string; + + @Column({ type: 'jsonb', default: [] }) + variables: string[]; // JSONB array + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; +} diff --git a/backoffice/src/fcm-tokens/fcm-tokens.service.ts b/backoffice/src/fcm-tokens/fcm-tokens.service.ts index a657088..87489f4 100644 --- a/backoffice/src/fcm-tokens/fcm-tokens.service.ts +++ b/backoffice/src/fcm-tokens/fcm-tokens.service.ts @@ -32,11 +32,12 @@ export class FcmTokensService implements OnModuleInit { return; } - if (serviceAccountPath || process.env.FIREBASE_SERVICE_ACCOUNT) { + const certPath = serviceAccountPath || process.env.FIREBASE_SERVICE_ACCOUNT || ''; + if (certPath) { try { // Simplified init - in prod use cert() with service account object // This is "showing how to configure" - const cert = require(serviceAccountPath || process.env.FIREBASE_SERVICE_ACCOUNT); + const cert = require(certPath); admin.initializeApp({ credential: admin.credential.cert(cert), }); diff --git a/docs/DEVOPS.md b/docs/DEVOPS.md new file mode 100644 index 0000000..cffd9e5 --- /dev/null +++ b/docs/DEVOPS.md @@ -0,0 +1,257 @@ +# DevOps - GoHorseJobs + +Documentação de infraestrutura, CI/CD e deploy do projeto GoHorseJobs. + +--- + +## 📁 Estrutura + +``` +. +├── .drone.yml # Pipeline CI/CD (Drone) +├── k8s/ +│ ├── dev/ # Manifests Kubernetes - Desenvolvimento +│ │ ├── backend-deployment.yaml +│ │ └── backend-service.yaml +│ ├── hml/ # Manifests Kubernetes - Homologação +│ │ ├── backend-deployment.yaml +│ │ └── backend-service.yaml +│ └── prd/ # Manifests Kubernetes - Produção +│ ├── backend-deployment.yaml +│ └── backend-service.yaml +├── backend/ +│ ├── Dockerfile # Build da API Go +│ └── .env.example # Variáveis de ambiente +├── frontend/ # Next.js App +└── seeder-api/ # Seeder Node.js para popular DB +``` + +--- + +## 🌍 Ambientes + +| Ambiente | Branch | Namespace K8s | Registry Harbor | Réplicas | +|----------|--------|---------------|-----------------|----------| +| **DEV** | `dev` | `gohorsejobsdev` | `gohorsejobsdev/gohorsejobs-backend` | 1 | +| **HML** | `hml` | `gohorsejobshml` | `gohorsejobshml/gohorsejobs-backend` | 2 | +| **PRD** | `main` | `gohorsejobs` | `gohorsejobs/gohorsejobs-backend` | 3 | + +--- + +## 🔄 Pipeline CI/CD (Drone) + +### Fluxo de Deploy + +``` +dev branch → build → push (Harbor) → deploy (K8s gohorsejobsdev) + ↓ +hml branch → build → push (Harbor) → deploy (K8s gohorsejobshml) + ↓ +main branch → build → push (Harbor) → deploy (K8s gohorsejobs) +``` + +### Triggers + +- Push na branch `dev` → executa pipeline `deploy-backend-dev` +- Push na branch `hml` → executa pipeline `deploy-backend-hml` +- Push na branch `main` → executa pipeline `deploy-backend-prd` + +### Etapas do Pipeline + +1. **build-and-push-backend** - Builda imagem Docker e envia para Harbor +2. **export-envs-to-k8s** - Cria secret `backend-secrets` no namespace +3. **deploy-backend** - Aplica manifests K8s e reinicia deployment + +--- + +## 🔐 Secrets (Drone CI) + +Secrets que precisam estar configurados no Drone: + +### Registry +| Secret | Descrição | +|--------|-----------| +| `HARBOR_USERNAME` | Usuário do Harbor | +| `HARBOR_PASSWORD` | Senha do Harbor | + +### Database +| Secret | Ambiente | Descrição | +|--------|----------|-----------| +| `DB_HOST` | Todos | Host do PostgreSQL | +| `DB_PORT` | Todos | Porta do PostgreSQL | +| `DB_USER` | Todos | Usuário do PostgreSQL | +| `DB_PASSWORD` | Todos | Senha do PostgreSQL | +| `DB_SSLMODE` | Todos | `require` ou `disable` | +| `DB_NAME_DEV` | DEV | Nome do banco dev | +| `DB_NAME_HML` | HML | Nome do banco hml | +| `DB_NAME` | PRD | Nome do banco produção | + +### S3/Object Storage +| Secret | Descrição | +|--------|-----------| +| `AWS_ACCESS_KEY_ID` | Access Key | +| `AWS_SECRET_ACCESS_KEY` | Secret Key | +| `AWS_ENDPOINT` | Endpoint S3-compatible | +| `AWS_REGION` | Região | +| `S3_BUCKET` | Nome do bucket | + +### Aplicação +| Secret | Descrição | +|--------|-----------| +| `JWT_SECRET` | Secret para tokens JWT (min. 32 chars) | +| `PORT` | Porta da API (8521) | +| `CORS_ORIGINS_DEV` | URLs permitidas CORS (dev) | +| `CORS_ORIGINS_HML` | URLs permitidas CORS (hml) | +| `CORS_ORIGINS` | URLs permitidas CORS (prd) | + +--- + +## ☸️ Kubernetes + +### Namespaces + +```bash +# Criar namespaces +kubectl create namespace gohorsejobsdev +kubectl create namespace gohorsejobshml +kubectl create namespace gohorsejobs +``` + +### Registry Secret + +Criar secret para pull de imagens do Harbor em cada namespace: + +```bash +kubectl create secret docker-registry harbor-registry \ + --docker-server=in.gohorsejobs.com \ + --docker-username= \ + --docker-password= \ + -n gohorsejobsdev + +# Repetir para gohorsejobshml e gohorsejobs +``` + +### Deploy Manual + +```bash +# DEV +kubectl apply -f k8s/dev/backend-deployment.yaml +kubectl apply -f k8s/dev/backend-service.yaml + +# HML +kubectl apply -f k8s/hml/backend-deployment.yaml +kubectl apply -f k8s/hml/backend-service.yaml + +# PRD +kubectl apply -f k8s/prd/backend-deployment.yaml +kubectl apply -f k8s/prd/backend-service.yaml +``` + +### Comandos Úteis + +```bash +# Ver pods +kubectl get pods -n gohorsejobsdev + +# Ver logs +kubectl logs -f deployment/gohorse-backend -n gohorsejobsdev + +# Restart deployment +kubectl rollout restart deployment/gohorse-backend -n gohorsejobsdev + +# Ver secrets +kubectl get secrets -n gohorsejobsdev + +# Descrever deployment +kubectl describe deployment gohorse-backend -n gohorsejobsdev +``` + +--- + +## 🐳 Docker + +### Build Local + +```bash +cd backend +docker build -t gohorsejobs-backend:local . +``` + +### Variáveis de Ambiente + +Ver `.env.example` para lista completa. Principais: + +| Variável | Descrição | Exemplo | +|----------|-----------|---------| +| `PORT` | Porta da API | `8521` | +| `DB_HOST` | Host PostgreSQL | `db.example.com` | +| `DB_NAME` | Nome do banco | `gohorsejobs_dev` | +| `DB_SSLMODE` | Modo SSL | `require` | +| `JWT_SECRET` | Secret JWT | `sua-chave-secreta-32-chars` | + +--- + +## 🗄️ Banco de Dados + +### Conexão + +``` +Host: db-60059.dc-sp-1.absamcloud.com +Port: 26868 +SSL: require +``` + +### Bancos por Ambiente + +| Ambiente | Database | +|----------|----------| +| DEV | `gohorsejobs_dev` | +| HML | `gohorsejobs_hml` | +| PRD | `gohorsejobs` | + +### Seeder + +```bash +cd seeder-api +npm install +npm run seed # Popular banco +npm run seed:reset # Limpar banco +``` + +--- + +## 🧑‍💻 Usuários de Teste + +### SuperAdmin +- **Login:** `superadmin` +- **Senha:** `Admin@2025!` + +### Company Admins +| Login | Senha | Empresa | +|-------|-------|---------| +| `takeshi_yamamoto` | `Takeshi@2025` | TechCorp | +| `kenji@appmakers.mobile` | `Takeshi@2025` | AppMakers | + +### Recrutadores +| Login | Senha | Empresa | +|-------|-------|---------| +| `maria_santos` | `User@2025` | DesignHub | + +### Candidatos +| Login | Senha | +|-------|-------| +| `paulo_santos` | `User@2025` | +| `maria@email.com` | `User@2025` | + +--- + +## 📋 Checklist Deploy Novo Ambiente + +- [ ] Criar namespace no K8s +- [ ] Criar secret `harbor-registry` no namespace +- [ ] Adicionar secrets no Drone CI +- [ ] Criar banco de dados +- [ ] Executar seeder (opcional) +- [ ] Fazer push na branch correspondente +- [ ] Verificar logs do pipeline +- [ ] Testar endpoint `/health` diff --git a/frontend/README.md b/frontend/FRONTEND.md similarity index 100% rename from frontend/README.md rename to frontend/FRONTEND.md diff --git a/frontend/src/app/dashboard/admin/email-templates/[slug]/page.tsx b/frontend/src/app/dashboard/admin/email-templates/[slug]/page.tsx new file mode 100644 index 0000000..614ca0d --- /dev/null +++ b/frontend/src/app/dashboard/admin/email-templates/[slug]/page.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter, useParams } from "next/navigation"; +import { toast } from "sonner"; +import { emailTemplatesApi, EmailTemplate } from "@/lib/api"; + +export default function EditEmailTemplatePage() { + const router = useRouter(); + const params = useParams(); + const slug = params.slug as string; + + const [template, setTemplate] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + useEffect(() => { + fetchTemplate(); + }, [slug]); + + const fetchTemplate = async () => { + try { + const data = await emailTemplatesApi.get(slug); + setTemplate(data); + } catch (err: any) { + toast.error(err.message || "Failed to load template"); + } finally { + setLoading(false); + } + }; + + const handleSave = async () => { + if (!template) return; + setSaving(true); + try { + await emailTemplatesApi.update(slug, { + subject: template.subject, + body_html: template.body_html, + variables: template.variables, + }); + toast.success("Template saved!"); + } catch (err: any) { + toast.error(err.message || "Failed to save template"); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( +
+
+
+ ); + } + + if (!template) { + return ( +
+

Template not found

+ +
+ ); + } + + return ( +
+ + +

Edit Template: {slug}

+ +
+
+ + setTemplate({ ...template, subject: e.target.value })} + className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary" + /> +
+ +
+ +