fx build fix
Some checks failed
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx/1 Pipeline failed
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/fx/2 Pipeline failed

This commit is contained in:
Stephan D
2025-11-08 00:30:29 +01:00
parent 590fad0071
commit 49b86efecb
165 changed files with 9466 additions and 0 deletions

57
api/server/.air.toml Normal file
View File

@@ -0,0 +1,57 @@
# Config file for [Air](https://github.com/air-verse/air) in TOML format
# Working directory
# . or absolute path, please note that the directories following must be under root.
root = "./.."
tmp_dir = "tmp"
[build]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o app -ldflags \"-X 'github.com/tech/sendico/server/internal/appversion.BuildUser=$(whoami)' -X 'github.com/tech/sendico/server/internal/appversion.Version=$APP_V' -X 'github.com/tech/sendico/server/internal/appversion.Branch=$BUILD_BRANCH' -X 'github.com/tech/sendico/server/internal/appversion.Revision=$GIT_REV' -X 'github.com/tech/sendico/server/internal/appversion.BuildDate=$(date)' -X 'github.com/tech/sendico/server/internal/mutil/ampli.Version=$APP_V'\""
# Binary file yields from `cmd`.
bin = "./app"
# Customize binary, can setup environment variables when run your app.
full_bin = "./app --debug"
# Watch these filename extensions.
include_ext = ["go"]
# Ignore these filename extensions or directories.
exclude_dir = ["server/.git", "pkg/.git", "server/tmp", "server/storage", "server/resources", "server/env"]
# Watch these directories if you specified.
include_dir = []
# Watch these files.
include_file = []
# Exclude files.
exclude_file = []
# Exclude specific regular expressions.
exclude_regex = ["_test\\.go"]
# Exclude unchanged files.
exclude_unchanged = true
# Follow symlink for directories
follow_symlink = true
# This log file places in your tmp_dir.
log = "air.log"
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 0 # ms
# Stop running old binary when build errors occur.
stop_on_error = true
# Send Interrupt signal before killing process (windows does not support this feature)
send_interrupt = true
# Delay after sending Interrupt signal
kill_delay = 500 # ms
# Add additional arguments when running binary (bin/full_bin). Will run './tmp/main hello world'.
args_bin = []
[log]
# Show log time
time = false
[color]
# Customize each part's color. If no color found, use the raw app log.
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"
[misc]
# Delete tmp directory on exit
clean_on_exit = true

1
api/server/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
storage

14
api/server/ampli.json Normal file
View File

@@ -0,0 +1,14 @@
{
"Zone": "eu",
"OrgId": "100001828",
"WorkspaceId": "c75043a3-1fad-45ec-bd71-c807a99c650d",
"SourceId": "81b24ac7-e285-4519-9e82-bb575601120c",
"Branch": "main",
"Version": "2.0.0",
"VersionId": "4fa6851a-4ff0-42f1-b440-8b39f07870e4",
"Runtime": "go:go-ampli",
"Platform": "Go",
"Language": "Go",
"SDK": "analytics-go",
"Path": "./internal/ampli"
}

BIN
api/server/app Executable file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
package assets
import _ "embed"
//go:embed resources/logo.png
var MailLogo []byte

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

97
api/server/config.yml Executable file
View File

@@ -0,0 +1,97 @@
http_server:
listen_address: :8081
read_header_timeout: 60
shutdown_timeout: 5
api:
amplitude:
ampli_environment_env: AMPLI_ENVIRONMENT
middleware:
api_protocol_env: API_PROTOCOL
domain_env: SERVICE_HOST
api_endpoint_env: API_ENDPOINT
signature:
secret_key_env: API_ENDPOINT_SECRET
algorithm: HS256
CORS:
max_age: 300
allowed_origins:
- "http://*"
- "https://*"
allowed_methods:
- "GET"
- "POST"
- "PUT"
- "PATCH"
- "DELETE"
- "OPTIONS"
allowed_headers:
- "Accept"
- "Authorization"
- "Content-Type"
- "X-Requested-With"
exposed_headers:
allow_credentials: false
websocket:
endpoint_env: WS_ENDPOINT
timeout: 60
message_broker:
driver: NATS
settings:
host_env: NATS_HOST
port_env: NATS_PORT
username_env: NATS_USER
password_env: NATS_PASSWORD
broker_name: Sendico Backend server
max_reconnects: 10
reconnect_wait: 5
# type: in-process
# settings:
# buffer_size: 10
token:
expiration_hours:
account: 24
refresh: 720
length: 32
password:
token_length: 32
checks:
min_length: 8
digit: true
upper: true
lower: true
special: true
storage:
# driver: aws_s3
# settings:
# access_key_id_env: S3_ACCESS_KEY_ID
# secret_access_key_env: S3_ACCESS_KEY_SECRET
# region_env: S3_REGION
# bucket_name_env: S3_BUCKET_NAME
driver: local_fs
settings:
root_path: ./storage
app:
database:
driver: mongodb
settings:
host_env: MONGO_HOST
port_env: MONGO_PORT
database_env: MONGO_DATABASE
user_env: MONGO_USER
password_env: MONGO_PASSWORD
auth_source_env: MONGO_AUTH_SOURCE
replica_set_env: MONGO_REPLICA_SET
enforcer:
driver: native
settings:
model_path_env: PERMISSION_MODEL
adapter:
collection_name_env: PERMISSION_COLLECTION
database_name_env: MONGO_DATABASE
timeout_seconds_env: PERMISSION_TIMEOUT
is_filtered_env: PERMISSION_IS_FILTERED

1
api/server/env/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.env.api

133
api/server/go.mod Normal file
View File

@@ -0,0 +1,133 @@
module github.com/tech/sendico/server
go 1.25.3
replace github.com/tech/sendico/pkg => ../pkg
require (
github.com/aws/aws-sdk-go-v2 v1.39.6
github.com/aws/aws-sdk-go-v2/config v1.31.17
github.com/aws/aws-sdk-go-v2/credentials v1.18.21
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0
github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2
github.com/go-chi/jwtauth/v5 v5.3.3
github.com/go-chi/metrics v0.1.1
github.com/mitchellh/mapstructure v1.5.0
github.com/stretchr/testify v1.11.1
github.com/tech/sendico/pkg v0.1.0
github.com/testcontainers/testcontainers-go v0.33.0
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0
go.mongodb.org/mongo-driver v1.17.6
go.uber.org/zap v1.27.0
golang.org/x/net v0.46.0
gopkg.in/yaml.v3 v3.0.1
moul.io/chizap v1.0.3
)
require (
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
github.com/casbin/casbin/v2 v2.132.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
)
require (
dario.cat/mergo v1.0.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect
github.com/aws/smithy-go v1.23.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v27.3.1+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-chi/chi v1.5.5 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.6 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/jwx/v2 v2.1.6 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.3.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.47.0 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.2 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)

379
api/server/go.sum Normal file
View File

@@ -0,0 +1,379 @@
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk=
github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y=
github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y=
github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c=
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA=
github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 h1:eg/WYAa12vqTphzIdWMzqYRVKKnCboVPRlvaybNCqPA=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13/go.mod h1:/FDdxWhz1486obGrKKC1HONd7krpk38LBt+dutLcN9k=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 h1:NvMjwvv8hpGUILarKw7Z4Q0w1H9anXKsesMxtw++MA4=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4/go.mod h1:455WPHSwaGj2waRSpQp7TsnpOnBfw8iDfPfbwl7KPJE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 h1:zhBJXdhWIFZ1acfDYIhu4+LCzdUS2Vbcum7D01dXlHQ=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 h1:ef6gIJR+xv/JQWwpa5FYirzoQctfSJm7tuDe3SZsUf8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs=
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk=
github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/mongodb-adapter/v3 v3.7.0 h1:w9c3bea1BGK4eZTAmk17JkY52yv/xSZDSHKji8q+z6E=
github.com/casbin/mongodb-adapter/v3 v3.7.0/go.mod h1:F1mu4ojoJVE/8VhIMxMedhjfwRDdIXgANYs6Sd0MgVA=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
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/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
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/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/jwtauth/v5 v5.3.3 h1:50Uzmacu35/ZP9ER2Ht6SazwPsnLQ9LRJy6zTZJpHEo=
github.com/go-chi/jwtauth/v5 v5.3.3/go.mod h1:O4QvPRuZLZghl9WvfVaON+ARfGzpD2PBX/QY5vUz7aQ=
github.com/go-chi/metrics v0.1.1 h1:CXhbnkAVVjb0k73EBRQ6Z2YdWFnbXZgNtg1Mboguibk=
github.com/go-chi/metrics v0.1.1/go.mod h1:mcGTM1pPalP7WCtb+akNYFO/lwNwBBLCuedepqjoPn4=
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-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k=
github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA=
github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw=
github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8=
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 h1:iXVA84s5hKMS5gn01GWOYHE3ymy/2b+0YkpFeTxB2XY=
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0/go.mod h1:R6tMjTojRiaoo89fh/hf7tOmfzohdqSU17R9DwSVSog=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.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/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
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.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
moul.io/chizap v1.0.3 h1:mliXvvuS5HVo3QP8qPXczWtRM5dQ9UmK3bBVIkZo6ek=
moul.io/chizap v1.0.3/go.mod h1:pq4R9kGLwz4XjBc4hodQYuoE7Yc9RUabLBFyyi2uErk=

View File

@@ -0,0 +1,409 @@
package accountserviceimp
import (
"context"
"errors"
"fmt"
"slices"
"unicode"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/auth/management"
"github.com/tech/sendico/pkg/db"
"github.com/tech/sendico/pkg/db/account"
"github.com/tech/sendico/pkg/db/organization"
"github.com/tech/sendico/pkg/db/policy"
"github.com/tech/sendico/pkg/db/transaction"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/server/interface/middleware"
"github.com/tech/sendico/server/internal/mutil/flrstring"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
type service struct {
logger mlogger.Logger
accountDB account.DB
orgDB organization.DB
enforcer auth.Enforcer
roleManager management.Role
config *middleware.PasswordConfig
tf transaction.Factory
policyDB policy.DB
}
func validateUserRequest(u *model.Account) error {
if u.Name == "" {
return merrors.InvalidArgument("Name must not be empty")
}
if u.Login == "" {
return merrors.InvalidArgument("Login must not be empty")
}
if u.Password == "" {
return merrors.InvalidArgument("Password must not be empty")
}
return nil
}
func (s *service) ValidatePassword(
password string,
oldPassword *string,
) error {
var hasDigit, hasUpper, hasLower, hasSpecial bool
if oldPassword != nil {
if *oldPassword == password {
return merrors.InvalidArgument("New password cannot be the same as the old password")
}
}
if len(password) < s.config.Check.MinLength {
return merrors.InvalidArgument(fmt.Sprintf("Password must be at least %d characters long", s.config.Check.MinLength))
}
// Check for digit, uppercase, lowercase, and special character
for _, char := range password {
switch {
case unicode.IsDigit(char):
hasDigit = true
case unicode.IsUpper(char):
hasUpper = true
case unicode.IsLower(char):
hasLower = true
case unicode.IsPunct(char) || unicode.IsSymbol(char):
hasSpecial = true
}
}
if s.config.Check.Digit && !hasDigit {
return merrors.InvalidArgument("Password must contain at least one digit")
}
if s.config.Check.Upper && !hasUpper {
return merrors.InvalidArgument("Password must contain at least one uppercase letter")
}
if s.config.Check.Lower && !hasLower {
return merrors.InvalidArgument("Password must contain at least one lowercase letter")
}
if s.config.Check.Special && !hasSpecial {
return merrors.InvalidArgument("Password must contain at least one special character")
}
// If all checks pass, return nil (no error)
return nil
}
func (s *service) ValidateAccount(acct *model.Account) error {
if err := validateUserRequest(acct); err != nil {
s.logger.Warn("Invalid signup acccount received", zap.Error(err), zap.String("account", acct.Login))
return err
}
if err := s.ValidatePassword(acct.Password, nil); err != nil {
s.logger.Warn("Password validation failed", zap.Error(err), zap.String("account", acct.Login))
return err
}
if err := acct.HashPassword(); err != nil {
s.logger.Warn("Failed to hash password", zap.Error(err), zap.String("account", acct.Login))
return err
}
acct.VerifyToken = flrstring.CreateRandString(s.config.TokenLength)
return nil
}
func (s *service) CreateAccount(
ctx context.Context,
org *model.Organization,
acct *model.Account,
roleDescID primitive.ObjectID,
) error {
if org == nil {
return merrors.InvalidArgument("Organization must not be nil")
}
if acct == nil || len(acct.Login) == 0 {
return merrors.InvalidArgument("Account must have a non-empty login")
}
if roleDescID == primitive.NilObjectID {
return merrors.InvalidArgument("Role description must be provided")
}
// 1) Create the account
if err := s.accountDB.Create(ctx, acct); err != nil {
if errors.Is(err, merrors.ErrDataConflict) {
s.logger.Info("Username is already taken", zap.String("login", acct.Login))
} else {
s.logger.Warn("Failed to signup a user", zap.Error(err), zap.String("login", acct.Login))
}
return err
}
// 2) Add to organization
if err := s.JoinOrganization(ctx, org, acct, roleDescID); err != nil {
s.logger.Warn("Failed to register new organization member", zap.Error(err), mzap.StorableRef(acct))
return err
}
return nil
}
func (s *service) DeleteAccount(
ctx context.Context,
org *model.Organization,
accountRef primitive.ObjectID,
) error {
// Check if this is the only member in the organization
if len(org.Members) <= 1 {
s.logger.Warn("Cannot delete account - it's the only member in the organization",
mzap.ObjRef("account_ref", accountRef), mzap.StorableRef(org))
return merrors.InvalidArgument("Cannot delete the only member of an organization")
}
// 1) Remove from organization
if err := s.RemoveAccountFromOrganization(ctx, org, accountRef); err != nil {
s.logger.Warn("Failed to revoke account role", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
return err
}
// 2) Delete the account document
if err := s.accountDB.Delete(ctx, accountRef); err != nil {
s.logger.Warn("Failed to delete account", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
return err
}
return nil
}
func (s *service) RemoveAccountFromOrganization(
ctx context.Context,
org *model.Organization,
accountRef primitive.ObjectID,
) error {
if org == nil {
return merrors.InvalidArgument("Organization must not be nil")
}
roles, err := s.enforcer.GetRoles(ctx, accountRef, org.ID)
if err != nil {
s.logger.Warn("Failed to fetch account permissions", zap.Error(err), mzap.StorableRef(org),
mzap.ObjRef("account_ref", accountRef))
return err
}
for _, role := range roles {
if err := s.roleManager.Revoke(ctx, role.DescriptionRef, accountRef, org.ID); err != nil {
s.logger.Warn("Failed to revoke account role", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("role_ref", role.DescriptionRef))
return err
}
}
for i, member := range org.Members {
if member == accountRef {
// Remove the member by slicing it out
org.Members = append(org.Members[:i], org.Members[i+1:]...)
if err := s.orgDB.Update(ctx, accountRef, org); err != nil {
s.logger.Warn("Failed to remove member from organization", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
return err
}
break
}
}
return nil
}
func (s *service) ResetPassword(
ctx context.Context,
acct *model.Account,
) error {
acct.ResetPasswordToken = flrstring.CreateRandString(s.config.TokenLength)
return s.accountDB.Update(ctx, acct)
}
func (s *service) UpdateLogin(
ctx context.Context,
acct *model.Account,
newLogin string,
) error {
acct.EmailBackup = acct.Login
acct.Login = newLogin
acct.VerifyToken = flrstring.CreateRandString(s.config.TokenLength)
return s.accountDB.Update(ctx, acct)
}
func (s *service) JoinOrganization(
ctx context.Context,
org *model.Organization,
account *model.Account,
roleDescID primitive.ObjectID,
) error {
if slices.Contains(org.Members, account.ID) {
s.logger.Debug("Account is already a member", mzap.StorableRef(org), mzap.StorableRef(account))
return nil
}
org.Members = append(org.Members, account.ID)
if err := s.orgDB.Update(ctx, *account.GetID(), org); err != nil {
s.logger.Warn("Failed to update organization members list", zap.Error(err), mzap.StorableRef(account))
return err
}
role := &model.Role{
DescriptionRef: roleDescID,
OrganizationRef: org.ID,
AccountRef: account.ID,
}
if err := s.roleManager.Assign(ctx, role); err != nil {
s.logger.Warn("Failed to assign role to account", zap.Error(err), mzap.StorableRef(account))
return err
}
return nil
}
func (s *service) deleteOrganizationRoles(ctx context.Context, orgRef primitive.ObjectID) error {
s.logger.Info("Deleting roles for organization", mzap.ObjRef("organization_ref", orgRef))
// Get all roles for the organization
roles, err := s.roleManager.List(ctx, orgRef)
if err != nil && !errors.Is(err, merrors.ErrNoData) {
s.logger.Warn("Failed to fetch roles for deletion", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
return err
}
// Delete each role
for _, role := range roles {
if err := s.roleManager.Delete(ctx, role.ID); err != nil {
s.logger.Warn("Failed to delete role", zap.Error(err), mzap.ObjRef("role_ref", role.ID))
return err
}
}
s.logger.Info("Successfully deleted roles", zap.Int("count", len(roles)), mzap.ObjRef("organization_ref", orgRef))
return nil
}
func (s *service) deleteOrganizationPolicies(ctx context.Context, orgRef primitive.ObjectID) error {
s.logger.Info("Deleting policies for organization", mzap.ObjRef("organization_ref", orgRef))
// Get all policies for the organization
policies, err := s.policyDB.All(ctx, orgRef)
if err != nil && !errors.Is(err, merrors.ErrNoData) {
s.logger.Warn("Failed to fetch policies for deletion", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
return err
}
// Delete each policy
for _, policy := range policies {
if err := s.policyDB.Delete(ctx, policy.ID); err != nil {
s.logger.Warn("Failed to delete policy", zap.Error(err), mzap.ObjRef("policy_ref", policy.ID))
return err
}
}
s.logger.Info("Successfully deleted policies", zap.Int("count", len(policies)), mzap.ObjRef("organization_ref", orgRef))
return nil
}
func (s *service) DeleteOrganization(
ctx context.Context,
org *model.Organization,
) error {
s.logger.Info("Starting organization deletion", mzap.StorableRef(org))
// Use transaction to ensure atomicity
_, err := s.tf.CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) {
// 8. Delete all roles and role descriptions in the organization
if err := s.deleteOrganizationRoles(ctx, org.ID); err != nil {
return nil, err
}
// 9. Delete all policies in the organization
if err := s.deleteOrganizationPolicies(ctx, org.ID); err != nil {
return nil, err
}
// 10. Finally, delete the organization itself
if err := s.orgDB.Delete(ctx, primitive.NilObjectID, org.ID); err != nil {
s.logger.Warn("Failed to delete organization", zap.Error(err), mzap.StorableRef(org))
return nil, err
}
return nil, nil
})
if err != nil {
s.logger.Error("Failed to delete organization", zap.Error(err), mzap.StorableRef(org))
return err
}
s.logger.Info("Organization deleted successfully", mzap.StorableRef(org))
return nil
}
func (s *service) DeleteAll(
ctx context.Context,
org *model.Organization,
accountRef primitive.ObjectID,
) error {
s.logger.Info("Starting complete deletion (organization + account)",
mzap.StorableRef(org), mzap.ObjRef("account_ref", accountRef))
// Use transaction to ensure atomicity
_, err := s.tf.CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) {
// 1. First delete the organization and all its data
if err := s.DeleteOrganization(ctx, org); err != nil {
return nil, err
}
// 2. Then delete the account
if err := s.accountDB.Delete(ctx, accountRef); err != nil {
s.logger.Warn("Failed to delete account", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
return nil, err
}
return nil, nil
})
if err != nil {
s.logger.Error("Failed to delete all data", zap.Error(err), mzap.StorableRef(org), mzap.ObjRef("account_ref", accountRef))
return err
}
s.logger.Info("Complete deletion successful", mzap.StorableRef(org), mzap.ObjRef("account_ref", accountRef))
return nil
}
// NewAccountService wires in your logger plus the three dependencies.
func NewAccountService(
l mlogger.Logger,
dbf db.Factory,
enforcer auth.Enforcer,
ra management.Role,
config *middleware.PasswordConfig,
) (*service, error) {
logger := l.Named("account_service")
if config == nil {
return nil, merrors.Internal("Invalid account service configuration provides")
}
res := &service{
logger: logger,
enforcer: enforcer,
roleManager: ra,
config: config,
tf: dbf.TransactionFactory(),
}
var err error
if res.accountDB, err = dbf.NewAccountDB(); err != nil {
logger.Warn("Failed to create accounts database", zap.Error(err))
return nil, err
}
if res.orgDB, err = dbf.NewOrganizationDB(); err != nil {
logger.Warn("Failed to create organizations database", zap.Error(err))
return nil, err
}
// Initialize database dependencies for cascade deletion
if res.policyDB, err = dbf.NewPoliciesDB(); err != nil {
logger.Warn("Failed to create policies database", zap.Error(err))
return nil, err
}
return res, nil
}

View File

@@ -0,0 +1,156 @@
package accountserviceimp
import (
"testing"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson/primitive"
)
func TestDeleteAccount_Validation(t *testing.T) {
t.Run("DeleteAccount_LastMemberFails", func(t *testing.T) {
orgID := primitive.NewObjectID()
accountID := primitive.NewObjectID()
org := &model.Organization{
OrganizationBase: model.OrganizationBase{
Describable: model.Describable{Name: "Single Member Org"},
},
Members: []primitive.ObjectID{accountID}, // Only one member
}
org.ID = orgID
// This should fail because it's the only member
err := validateDeleteAccount(org)
require.Error(t, err)
assert.Contains(t, err.Error(), "Cannot delete the only member")
})
t.Run("DeleteAccount_MultipleMembersSuccess", func(t *testing.T) {
orgID := primitive.NewObjectID()
accountID := primitive.NewObjectID()
otherAccountID := primitive.NewObjectID()
org := &model.Organization{
OrganizationBase: model.OrganizationBase{
Describable: model.Describable{Name: "Multi Member Org"},
},
Members: []primitive.ObjectID{accountID, otherAccountID}, // Multiple members
}
org.ID = orgID
// This should succeed because there are multiple members
err := validateDeleteAccount(org)
require.NoError(t, err)
})
t.Run("DeleteAccount_EmptyMembersList", func(t *testing.T) {
orgID := primitive.NewObjectID()
org := &model.Organization{
OrganizationBase: model.OrganizationBase{
Describable: model.Describable{Name: "Empty Org"},
},
Members: []primitive.ObjectID{}, // No members
}
org.ID = orgID
// This should fail because there are no members
err := validateDeleteAccount(org)
require.Error(t, err)
assert.Contains(t, err.Error(), "Cannot delete the only member")
})
}
func TestDeleteOrganization_Validation(t *testing.T) {
t.Run("DeleteOrganization_NilOrganization", func(t *testing.T) {
err := validateDeleteOrganization(nil)
require.Error(t, err)
assert.Contains(t, err.Error(), "organization cannot be nil")
})
t.Run("DeleteOrganization_EmptyOrganization", func(t *testing.T) {
org := &model.Organization{}
err := validateDeleteOrganization(org)
require.Error(t, err)
assert.Contains(t, err.Error(), "organization ID cannot be empty")
})
t.Run("DeleteOrganization_ValidOrganization", func(t *testing.T) {
org := &model.Organization{
OrganizationBase: model.OrganizationBase{
Describable: model.Describable{Name: "Valid Organization"},
},
}
org.ID = primitive.NewObjectID()
err := validateDeleteOrganization(org)
require.NoError(t, err)
})
}
func TestDeleteAll_Validation(t *testing.T) {
t.Run("DeleteAll_NilOrganization", func(t *testing.T) {
accountID := primitive.NewObjectID()
err := validateDeleteAll(nil, accountID)
require.Error(t, err)
assert.Contains(t, err.Error(), "organization cannot be nil")
})
t.Run("DeleteAll_EmptyAccountID", func(t *testing.T) {
org := &model.Organization{
OrganizationBase: model.OrganizationBase{
Describable: model.Describable{Name: "Valid Organization"},
},
}
org.ID = primitive.NewObjectID()
err := validateDeleteAll(org, primitive.NilObjectID)
require.Error(t, err)
assert.Contains(t, err.Error(), "account ID cannot be empty")
})
t.Run("DeleteAll_ValidInput", func(t *testing.T) {
org := &model.Organization{
OrganizationBase: model.OrganizationBase{
Describable: model.Describable{Name: "Valid Organization"},
},
}
org.ID = primitive.NewObjectID()
accountID := primitive.NewObjectID()
err := validateDeleteAll(org, accountID)
require.NoError(t, err)
})
}
// Helper functions that implement the validation logic from the service
func validateDeleteAccount(org *model.Organization) error {
if len(org.Members) <= 1 {
return merrors.InvalidArgument("Cannot delete the only member of an organization")
}
return nil
}
func validateDeleteOrganization(org *model.Organization) error {
if org == nil {
return merrors.InvalidArgument("organization cannot be nil")
}
if org.ID == primitive.NilObjectID {
return merrors.InvalidArgument("organization ID cannot be empty")
}
return nil
}
func validateDeleteAll(org *model.Organization, accountRef primitive.ObjectID) error {
if org == nil {
return merrors.InvalidArgument("organization cannot be nil")
}
if accountRef == primitive.NilObjectID {
return merrors.InvalidArgument("account ID cannot be empty")
}
return nil
}

View File

@@ -0,0 +1,298 @@
package accountserviceimp
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tech/sendico/pkg/model"
apiconfig "github.com/tech/sendico/server/internal/api/config"
"go.uber.org/zap"
)
// TestValidatePassword tests the password validation logic directly
func TestValidatePassword(t *testing.T) {
config := &apiconfig.PasswordConfig{
Check: apiconfig.PasswordChecks{
MinLength: 8,
Digit: true,
Upper: true,
Lower: true,
Special: true,
},
TokenLength: 32,
}
// Create a minimal service for testing password validation
logger := zap.NewNop() // Use no-op logger for tests
service := &service{
config: config,
logger: logger,
}
t.Run("ValidPassword", func(t *testing.T) {
err := service.ValidatePassword("TestPassword123!", nil)
assert.NoError(t, err)
})
t.Run("PasswordTooShort", func(t *testing.T) {
err := service.ValidatePassword("Test1!", nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "at least 8 characters")
})
t.Run("PasswordMissingDigit", func(t *testing.T) {
err := service.ValidatePassword("TestPassword!", nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "at least one digit")
})
t.Run("PasswordMissingUppercase", func(t *testing.T) {
err := service.ValidatePassword("testpassword123!", nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "at least one uppercase")
})
t.Run("PasswordMissingLowercase", func(t *testing.T) {
err := service.ValidatePassword("TESTPASSWORD123!", nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "at least one lowercase")
})
t.Run("PasswordMissingSpecialCharacter", func(t *testing.T) {
err := service.ValidatePassword("TestPassword123", nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "at least one special character")
})
t.Run("PasswordSameAsOld", func(t *testing.T) {
oldPassword := "TestPassword123!"
err := service.ValidatePassword("TestPassword123!", &oldPassword)
assert.Error(t, err)
assert.Contains(t, err.Error(), "cannot be the same as the old password")
})
}
// TestValidateAccount tests the account validation logic directly
func TestValidateAccount(t *testing.T) {
config := &apiconfig.PasswordConfig{
Check: apiconfig.PasswordChecks{
MinLength: 8,
Digit: true,
Upper: true,
Lower: true,
Special: true,
},
TokenLength: 32,
}
logger := zap.NewNop() // Use no-op logger for tests
service := &service{
config: config,
logger: logger,
}
t.Run("ValidAccount", func(t *testing.T) {
account := &model.Account{
AccountPublic: model.AccountPublic{
AccountBase: model.AccountBase{
Describable: model.Describable{
Name: "Test User",
},
},
UserDataBase: model.UserDataBase{
Login: "test@example.com",
},
},
Password: "TestPassword123!",
}
originalPassword := account.Password
err := service.ValidateAccount(account)
require.NoError(t, err)
// Password should be hashed after validation
assert.NotEqual(t, originalPassword, account.Password)
assert.NotEmpty(t, account.VerifyToken)
assert.Equal(t, config.TokenLength, len(account.VerifyToken))
})
t.Run("AccountMissingName", func(t *testing.T) {
account := &model.Account{
AccountPublic: model.AccountPublic{
UserDataBase: model.UserDataBase{
Login: "test@example.com",
},
},
Password: "TestPassword123!",
}
err := service.ValidateAccount(account)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Name must not be empty")
})
t.Run("AccountMissingLogin", func(t *testing.T) {
account := &model.Account{
AccountPublic: model.AccountPublic{
AccountBase: model.AccountBase{
Describable: model.Describable{
Name: "Test User",
},
},
},
Password: "TestPassword123!",
}
err := service.ValidateAccount(account)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Login must not be empty")
})
t.Run("AccountMissingPassword", func(t *testing.T) {
account := &model.Account{
AccountPublic: model.AccountPublic{
AccountBase: model.AccountBase{
Describable: model.Describable{
Name: "Test User",
},
},
UserDataBase: model.UserDataBase{
Login: "test@example.com",
},
},
Password: "",
}
err := service.ValidateAccount(account)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Password must not be empty")
})
t.Run("AccountInvalidPassword", func(t *testing.T) {
account := &model.Account{
AccountPublic: model.AccountPublic{
AccountBase: model.AccountBase{
Describable: model.Describable{
Name: "Test User",
},
},
UserDataBase: model.UserDataBase{
Login: "test@example.com",
},
},
Password: "weak", // Should fail validation
}
err := service.ValidateAccount(account)
assert.Error(t, err)
// Should fail on password validation
assert.Contains(t, err.Error(), "at least 8 characters")
})
}
// TestPasswordConfiguration verifies different password rule configurations
func TestPasswordConfiguration(t *testing.T) {
t.Run("MinimalRequirements", func(t *testing.T) {
config := &apiconfig.PasswordConfig{
Check: apiconfig.PasswordChecks{
MinLength: 4,
Digit: false,
Upper: false,
Lower: false,
Special: false,
},
TokenLength: 16,
}
logger := zap.NewNop() // Use no-op logger for tests
service := &service{
config: config,
logger: logger,
}
// Should pass with minimal requirements
err := service.ValidatePassword("test", nil)
assert.NoError(t, err)
})
t.Run("StrictRequirements", func(t *testing.T) {
config := &apiconfig.PasswordConfig{
Check: apiconfig.PasswordChecks{
MinLength: 12,
Digit: true,
Upper: true,
Lower: true,
Special: true,
},
TokenLength: 64,
}
logger := zap.NewNop() // Use no-op logger for tests
service := &service{
config: config,
logger: logger,
}
// Should fail with shorter password
err := service.ValidatePassword("Test123!", nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "at least 12 characters")
// Should pass with longer password
err = service.ValidatePassword("TestPassword123!", nil)
assert.NoError(t, err)
})
}
// TestTokenGeneration verifies that verification tokens are generated with correct length
func TestTokenGeneration(t *testing.T) {
testCases := []struct {
name string
tokenLength int
}{
{"Short", 8},
{"Medium", 32},
{"Long", 64},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
config := &apiconfig.PasswordConfig{
Check: apiconfig.PasswordChecks{
MinLength: 8,
Digit: true,
Upper: true,
Lower: true,
Special: true,
},
TokenLength: tc.tokenLength,
}
logger := zap.NewNop() // Use no-op logger for tests
service := &service{
config: config,
logger: logger,
}
account := &model.Account{
AccountPublic: model.AccountPublic{
AccountBase: model.AccountBase{
Describable: model.Describable{
Name: "Test User",
},
},
UserDataBase: model.UserDataBase{
Login: "test@example.com",
},
},
Password: "TestPassword123!",
}
err := service.ValidateAccount(account)
require.NoError(t, err)
assert.Equal(t, tc.tokenLength, len(account.VerifyToken))
})
}
}

View File

@@ -0,0 +1,99 @@
package accountservice
import (
"context"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/auth/management"
"github.com/tech/sendico/pkg/db"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
accountserviceimp "github.com/tech/sendico/server/interface/accountservice/internal"
"github.com/tech/sendico/server/interface/middleware"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// AccountService defines all account-related workflows.
type AccountService interface {
// ValidateAccount will:
// 1) check it's completeness
// 2) hash password
// 3) prepare verification token
ValidateAccount(
acct *model.Account,
) error
// ValidatePassword will:
// 1) check passsword conformance
ValidatePassword(
password string,
oldPassword *string,
) error
// ResetPassword will:
// 1) generate reset password token
ResetPassword(
ctx context.Context,
acct *model.Account,
) error
// CreateAccount will:
// 1) create the account
// 2) add it to the orgs member list
// 3) assign the given role description to it
CreateAccount(
ctx context.Context,
org *model.Organization,
acct *model.Account,
roleDescID primitive.ObjectID,
) error
JoinOrganization(
ctx context.Context,
org *model.Organization,
acct *model.Account,
roleDescID primitive.ObjectID,
) error
UpdateLogin(
ctx context.Context,
acct *model.Account,
newLogin string,
) error
// DeleteAccount deletes the account and removes it from the org.
DeleteAccount(
ctx context.Context,
org *model.Organization,
accountRef primitive.ObjectID,
) error
// RemoveAccountFromOrganization just drops it from the member slice.
RemoveAccountFromOrganization(
ctx context.Context,
org *model.Organization,
accountRef primitive.ObjectID,
) error
DeleteOrganization(
ctx context.Context,
org *model.Organization,
) error
// DeleteAll deletes both the organization and the account.
DeleteAll(
ctx context.Context,
org *model.Organization,
accountRef primitive.ObjectID,
) error
}
func NewAccountService(
logger mlogger.Logger,
dbf db.Factory,
enforcer auth.Enforcer,
roleManeger management.Role,
config *middleware.PasswordConfig,
) (AccountService, error) {
return accountserviceimp.NewAccountService(logger, dbf, enforcer, roleManeger, config)
}

View File

@@ -0,0 +1,20 @@
package api
import (
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/db"
"github.com/tech/sendico/pkg/domainprovider"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
)
type API interface {
Logger() mlogger.Logger
DomainProvider() domainprovider.DomainProvider
Config() *Config
DBFactory() db.Factory
Permissions() auth.Provider
Register() Register
}
type MicroServiceFactoryT = func(API) (mservice.MicroService, error)

View File

@@ -0,0 +1,11 @@
package api
import (
mwa "github.com/tech/sendico/server/interface/middleware"
fsc "github.com/tech/sendico/server/interface/services/fileservice/config"
)
type Config struct {
Mw *mwa.Config `yaml:"middleware"`
Storage *fsc.Config `yaml:"storage"`
}

View File

@@ -0,0 +1,10 @@
package permissions
import (
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/model"
)
func Deny(_ *model.Account, _ *auth.Enforcer) (bool, error) {
return true, nil
}

View File

@@ -0,0 +1,10 @@
package permissions
import (
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/model"
)
func DoNotCheck(_ *model.Account, _ *auth.Enforcer) (bool, error) {
return true, nil
}

View File

@@ -0,0 +1,17 @@
package api
import (
api "github.com/tech/sendico/pkg/api/http"
"github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api/sresponse"
"github.com/tech/sendico/server/interface/api/ws"
)
type Register interface {
Handler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.HandlerFunc)
AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc)
WSHandler(messageType string, handler ws.HandlerFunc)
Messaging() messaging.Register
}

View File

@@ -0,0 +1,7 @@
package srequest
import "github.com/tech/sendico/pkg/model"
type AcceptInvitation struct {
Account *model.AccountData `json:"account,omitempty"`
}

View File

@@ -0,0 +1,12 @@
package srequest
import (
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type ChangePolicies struct {
RoleRef primitive.ObjectID `json:"roleRef"`
Add *[]model.RolePolicy `json:"add,omitempty"`
Remove *[]model.RolePolicy `json:"remove,omitempty"`
}

View File

@@ -0,0 +1,8 @@
package srequest
import "go.mongodb.org/mongo-driver/bson/primitive"
type ChangeRole struct {
AccountRef primitive.ObjectID `json:"accountRef"`
NewRoleDescriptionRef primitive.ObjectID `json:"newRoleDescriptionRef"`
}

View File

@@ -0,0 +1,7 @@
package srequest
import "go.mongodb.org/mongo-driver/bson/primitive"
type FileUpload struct {
ObjRef primitive.ObjectID `json:"objRef"`
}

View File

@@ -0,0 +1,7 @@
package srequest
import (
"github.com/tech/sendico/pkg/model"
)
type CreateInvitation = model.Invitation

View File

@@ -0,0 +1,8 @@
package srequest
import "github.com/tech/sendico/pkg/model"
type Login struct {
model.SessionIdentifier `json:",inline"`
model.LoginData `json:"login"`
}

View File

@@ -0,0 +1,15 @@
package srequest
type ChangePassword struct {
Old string `json:"old"`
New string `json:"new"`
DeviceID string `json:"deviceId"`
}
type ResetPassword struct {
Password string `json:"password"`
}
type ForgotPassword struct {
Login string `json:"login"`
}

View File

@@ -0,0 +1,8 @@
package srequest
import "github.com/tech/sendico/pkg/model"
type CreatePriorityGroup struct {
Description model.Describable `json:"description"`
Priorities []model.Colorable `json:"priorities"`
}

View File

@@ -0,0 +1,31 @@
package srequest
import (
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type CreateProject struct {
Project model.Describable `json:"project"`
LogoURI *string `json:"logoUrl,omitempty"`
PrioriyGroupRef primitive.ObjectID `json:"priorityGroupRef"`
StatusGroupRef primitive.ObjectID `json:"statusGroupRef"`
Mnemonic string `json:"mnemonic"`
}
type ProjectPreview struct {
Projects []primitive.ObjectID `json:"projects"`
}
type TagFilterMode string
const (
TagFilterModeNone TagFilterMode = "none"
TagFilterModePresent TagFilterMode = "present"
TagFilterModeMissing TagFilterMode = "missing"
TagFilterModeIncludeAny TagFilterMode = "includeAny"
TagFilterModeIncludeAll TagFilterMode = "includeAll"
TagFilterModeExcludeAny TagFilterMode = "excludeAny"
)
type ProjectsFilter = model.ProjectFilterBase

View File

@@ -0,0 +1,11 @@
package srequest
import (
"go.mongodb.org/mongo-driver/bson/primitive"
)
// DeleteProject represents a request to delete a project
type DeleteProject struct {
OrganizationRef primitive.ObjectID `json:"organizationRef"` // If provided, move tasks to this project. If null, delete all tasks
MoveTasksToProjectRef *primitive.ObjectID `json:"moveTasksToProjectRef,omitempty"` // If provided, move tasks to this project. If null, delete all tasks
}

View File

@@ -0,0 +1,5 @@
package srequest
import "github.com/tech/sendico/pkg/model"
type AccessTokenRefresh = model.ClientRefreshToken

View File

@@ -0,0 +1,19 @@
package srequest
import "go.mongodb.org/mongo-driver/bson/primitive"
type Reorder struct {
ParentRef primitive.ObjectID `json:"parentRef"`
From int `json:"from"`
To int `json:"to"`
}
type ReorderX struct {
ObjectRef primitive.ObjectID `json:"objectRef"`
To int `json:"to"`
}
type ReorderXDefault struct {
ReorderX `json:",inline"`
ParentRef primitive.ObjectID `json:"parentRef"`
}

View File

@@ -0,0 +1,5 @@
package srequest
import "github.com/tech/sendico/pkg/model"
type TokenRefreshRotate = model.ClientRefreshToken

View File

@@ -0,0 +1,13 @@
package srequest
import "go.mongodb.org/mongo-driver/bson/primitive"
type GroupItemChange struct {
GroupRef primitive.ObjectID `json:"groupRef"`
ItemRef primitive.ObjectID `json:"itemRef"`
}
type RemoveItemFromGroup struct {
GroupItemChange `json:",inline"`
TargetItemRef primitive.ObjectID `json:"targetItemRef"`
}

View File

@@ -0,0 +1,14 @@
package srequest
import "github.com/tech/sendico/pkg/model"
type Signup struct {
Account model.AccountData `json:"account"`
OrganizationName string `json:"organizationName"`
OrganizationTimeZone string `json:"organizationTimeZone"`
DefaultPriorityGroup CreatePriorityGroup `json:"defaultPriorityGroup"`
DefaultStatusGroup CreateStatusGroup `json:"defaultStatusGroup"`
AnonymousUser model.Describable `json:"anonymousUser"`
OwnerRole model.Describable `json:"ownerRole"`
AnonymousRole model.Describable `json:"anonymousRole"`
}

View File

@@ -0,0 +1,312 @@
package srequest_test
import (
"encoding/json"
"fmt"
"testing"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/server/interface/api/srequest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Helper function to create string pointers
func stringPtr(s string) *string {
return &s
}
func TestSignupRequest_JSONSerialization(t *testing.T) {
signup := srequest.Signup{
Account: model.AccountData{
LoginData: model.LoginData{
UserDataBase: model.UserDataBase{
Login: "test@example.com",
},
Password: "TestPassword123!",
},
Name: "Test User",
},
OrganizationName: "Test Organization",
OrganizationTimeZone: "UTC",
DefaultPriorityGroup: srequest.CreatePriorityGroup{
Description: model.Describable{
Name: "Default Priority Group",
},
Priorities: []model.Colorable{
{
Describable: model.Describable{Name: "High"},
Color: stringPtr("#FF0000"),
},
{
Describable: model.Describable{Name: "Medium"},
Color: stringPtr("#FFFF00"),
},
{
Describable: model.Describable{Name: "Low"},
Color: stringPtr("#00FF00"),
},
},
},
AnonymousUser: model.Describable{
Name: "Anonymous User",
},
OwnerRole: model.Describable{
Name: "Owner",
},
AnonymousRole: model.Describable{
Name: "Anonymous",
},
}
// Test JSON marshaling
jsonData, err := json.Marshal(signup)
require.NoError(t, err)
assert.NotEmpty(t, jsonData)
// Test JSON unmarshaling
var unmarshaled srequest.Signup
err = json.Unmarshal(jsonData, &unmarshaled)
require.NoError(t, err)
// Verify all fields are properly serialized/deserialized
assert.Equal(t, signup.Account.Name, unmarshaled.Account.Name)
assert.Equal(t, signup.Account.Login, unmarshaled.Account.Login)
assert.Equal(t, signup.Account.Password, unmarshaled.Account.Password)
assert.Equal(t, signup.OrganizationName, unmarshaled.OrganizationName)
assert.Equal(t, signup.OrganizationTimeZone, unmarshaled.OrganizationTimeZone)
assert.Equal(t, signup.DefaultPriorityGroup.Description.Name, unmarshaled.DefaultPriorityGroup.Description.Name)
assert.Equal(t, len(signup.DefaultPriorityGroup.Priorities), len(unmarshaled.DefaultPriorityGroup.Priorities))
assert.Equal(t, signup.AnonymousUser.Name, unmarshaled.AnonymousUser.Name)
assert.Equal(t, signup.OwnerRole.Name, unmarshaled.OwnerRole.Name)
assert.Equal(t, signup.AnonymousRole.Name, unmarshaled.AnonymousRole.Name)
// Verify priorities
for i, priority := range signup.DefaultPriorityGroup.Priorities {
assert.Equal(t, priority.Name, unmarshaled.DefaultPriorityGroup.Priorities[i].Name)
if priority.Color != nil && unmarshaled.DefaultPriorityGroup.Priorities[i].Color != nil {
assert.Equal(t, *priority.Color, *unmarshaled.DefaultPriorityGroup.Priorities[i].Color)
}
}
}
func TestSignupRequest_MinimalValidRequest(t *testing.T) {
signup := srequest.Signup{
Account: model.AccountData{
LoginData: model.LoginData{
UserDataBase: model.UserDataBase{
Login: "test@example.com",
},
Password: "TestPassword123!",
},
Name: "Test User",
},
OrganizationName: "Test Organization",
OrganizationTimeZone: "UTC",
DefaultPriorityGroup: srequest.CreatePriorityGroup{
Description: model.Describable{
Name: "Default",
},
Priorities: []model.Colorable{
{
Describable: model.Describable{Name: "Normal"},
Color: stringPtr("#000000"),
},
},
},
AnonymousUser: model.Describable{
Name: "Anonymous",
},
OwnerRole: model.Describable{
Name: "Owner",
},
AnonymousRole: model.Describable{
Name: "Anonymous",
},
}
// Test JSON marshaling
jsonData, err := json.Marshal(signup)
require.NoError(t, err)
assert.NotEmpty(t, jsonData)
// Test JSON unmarshaling
var unmarshaled srequest.Signup
err = json.Unmarshal(jsonData, &unmarshaled)
require.NoError(t, err)
// Verify minimal request is valid
assert.Equal(t, signup.Account.Name, unmarshaled.Account.Name)
assert.Equal(t, signup.Account.Login, unmarshaled.Account.Login)
assert.Equal(t, signup.OrganizationName, unmarshaled.OrganizationName)
assert.Len(t, unmarshaled.DefaultPriorityGroup.Priorities, 1)
}
func TestSignupRequest_InvalidJSON(t *testing.T) {
invalidJSONs := []string{
`{"account": invalid}`,
`{"organizationName": 123}`,
`{"organizationTimeZone": true}`,
`{"defaultPriorityGroup": "not_an_object"}`,
`{"anonymousUser": []}`,
`{"anonymousRole": 456}`,
`{invalid json}`,
}
for i, invalidJSON := range invalidJSONs {
t.Run(fmt.Sprintf("Invalid JSON %d", i), func(t *testing.T) {
var signup srequest.Signup
err := json.Unmarshal([]byte(invalidJSON), &signup)
require.Error(t, err)
})
}
}
func TestSignupRequest_UnicodeCharacters(t *testing.T) {
signup := srequest.Signup{
Account: model.AccountData{
LoginData: model.LoginData{
UserDataBase: model.UserDataBase{
Login: "测试@example.com",
},
Password: "TestPassword123!",
},
Name: "Test 用户 Üser",
},
OrganizationName: "测试 Organization",
OrganizationTimeZone: "UTC",
DefaultPriorityGroup: srequest.CreatePriorityGroup{
Description: model.Describable{
Name: "默认 Priority Group",
},
Priorities: []model.Colorable{
{
Describable: model.Describable{Name: "高"},
Color: stringPtr("#FF0000"),
},
},
},
AnonymousUser: model.Describable{
Name: "匿名 User",
},
OwnerRole: model.Describable{
Name: "所有者",
},
AnonymousRole: model.Describable{
Name: "匿名",
},
}
// Test JSON marshaling
jsonData, err := json.Marshal(signup)
require.NoError(t, err)
assert.NotEmpty(t, jsonData)
// Test JSON unmarshaling
var unmarshaled srequest.Signup
err = json.Unmarshal(jsonData, &unmarshaled)
require.NoError(t, err)
// Verify unicode characters are properly handled
assert.Equal(t, "测试@example.com", unmarshaled.Account.Login)
assert.Equal(t, "Test 用户 Üser", unmarshaled.Account.Name)
assert.Equal(t, "测试 Organization", unmarshaled.OrganizationName)
assert.Equal(t, "默认 Priority Group", unmarshaled.DefaultPriorityGroup.Description.Name)
assert.Equal(t, "高", unmarshaled.DefaultPriorityGroup.Priorities[0].Name)
assert.Equal(t, "匿名 User", unmarshaled.AnonymousUser.Name)
assert.Equal(t, "所有者", unmarshaled.OwnerRole.Name)
assert.Equal(t, "匿名", unmarshaled.AnonymousRole.Name)
}
func TestCreatePriorityGroup_JSONSerialization(t *testing.T) {
priorityGroup := srequest.CreatePriorityGroup{
Description: model.Describable{
Name: "Test Priority Group",
},
Priorities: []model.Colorable{
{
Describable: model.Describable{Name: "Critical"},
Color: stringPtr("#FF0000"),
},
{
Describable: model.Describable{Name: "High"},
Color: stringPtr("#FF8000"),
},
{
Describable: model.Describable{Name: "Medium"},
Color: stringPtr("#FFFF00"),
},
{
Describable: model.Describable{Name: "Low"},
Color: stringPtr("#00FF00"),
},
},
}
// Test JSON marshaling
jsonData, err := json.Marshal(priorityGroup)
require.NoError(t, err)
assert.NotEmpty(t, jsonData)
// Test JSON unmarshaling
var unmarshaled srequest.CreatePriorityGroup
err = json.Unmarshal(jsonData, &unmarshaled)
require.NoError(t, err)
// Verify all fields are properly serialized/deserialized
assert.Equal(t, priorityGroup.Description.Name, unmarshaled.Description.Name)
assert.Equal(t, len(priorityGroup.Priorities), len(unmarshaled.Priorities))
for i, priority := range priorityGroup.Priorities {
assert.Equal(t, priority.Name, unmarshaled.Priorities[i].Name)
if priority.Color != nil && unmarshaled.Priorities[i].Color != nil {
assert.Equal(t, *priority.Color, *unmarshaled.Priorities[i].Color)
}
}
}
func TestCreatePriorityGroup_EmptyPriorities(t *testing.T) {
priorityGroup := srequest.CreatePriorityGroup{
Description: model.Describable{
Name: "Empty Priority Group",
},
Priorities: []model.Colorable{},
}
// Test JSON marshaling
jsonData, err := json.Marshal(priorityGroup)
require.NoError(t, err)
assert.NotEmpty(t, jsonData)
// Test JSON unmarshaling
var unmarshaled srequest.CreatePriorityGroup
err = json.Unmarshal(jsonData, &unmarshaled)
require.NoError(t, err)
// Verify empty priorities array is handled correctly
assert.Equal(t, priorityGroup.Description.Name, unmarshaled.Description.Name)
assert.Empty(t, unmarshaled.Priorities)
}
func TestCreatePriorityGroup_NilPriorities(t *testing.T) {
priorityGroup := srequest.CreatePriorityGroup{
Description: model.Describable{
Name: "Nil Priority Group",
},
Priorities: nil,
}
// Test JSON marshaling
jsonData, err := json.Marshal(priorityGroup)
require.NoError(t, err)
assert.NotEmpty(t, jsonData)
// Test JSON unmarshaling
var unmarshaled srequest.CreatePriorityGroup
err = json.Unmarshal(jsonData, &unmarshaled)
require.NoError(t, err)
// Verify nil priorities is handled correctly
assert.Equal(t, priorityGroup.Description.Name, unmarshaled.Description.Name)
assert.Nil(t, unmarshaled.Priorities)
}

View File

@@ -0,0 +1,16 @@
package srequest
import (
"github.com/tech/sendico/pkg/model"
)
type CreateStatus struct {
model.Colorable `json:"description"`
Icon string `json:"icon"`
IsFinal bool `json:"isFinal"`
}
type CreateStatusGroup struct {
Description model.Describable `json:"description"`
Statuses []CreateStatus `json:"statuses"`
}

View File

@@ -0,0 +1,20 @@
package srequest
import "go.mongodb.org/mongo-driver/bson/primitive"
// TaggableSingle is used for single tag operations (add/remove tag)
type TaggableSingle struct {
ObjectRef primitive.ObjectID `json:"objectRef"`
TagRef primitive.ObjectID `json:"tagRef"`
}
// TaggableMultiple is used for multiple tag operations (add tags, set tags)
type TaggableMultiple struct {
ObjectRef primitive.ObjectID `json:"objectRef"`
TagRefs []primitive.ObjectID `json:"tagRefs"`
}
// TaggableObject is used for object-only operations (remove all tags, get tags)
type TaggableObject struct {
ObjectRef primitive.ObjectID `json:"objectRef"`
}

View File

@@ -0,0 +1,62 @@
package sresponse
import (
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type accountData struct {
model.AccountPublic `json:",inline"`
IsAnonymous bool `json:"isAnonymous"`
}
type accountResponse struct {
authResponse `json:",inline"`
Account accountData `json:"account"`
}
func _createAccount(account *model.Account, isAnonymous bool) *accountData {
return &accountData{
AccountPublic: account.AccountPublic,
IsAnonymous: isAnonymous,
}
}
func _toAccount(account *model.Account, orgRef primitive.ObjectID) *accountData {
return _createAccount(account, model.AccountIsAnonymous(&account.UserDataBase, orgRef))
}
func Account(logger mlogger.Logger, account *model.Account, accessToken *TokenData) http.HandlerFunc {
return response.Ok(
logger,
&accountResponse{
Account: *_createAccount(account, false),
authResponse: authResponse{AccessToken: *accessToken},
},
)
}
type accountsResponse struct {
authResponse `json:",inline"`
Accounts []accountData `json:"accounts"`
}
func Accounts(logger mlogger.Logger, accounts []model.Account, orgRef primitive.ObjectID, accessToken *TokenData) http.HandlerFunc {
// Convert each account to its public representation.
publicAccounts := make([]accountData, len(accounts))
for i, a := range accounts {
publicAccounts[i] = *_toAccount(&a, orgRef)
}
return response.Ok(
logger,
&accountsResponse{
Accounts: publicAccounts,
authResponse: authResponse{AccessToken: *accessToken},
},
)
}

View File

@@ -0,0 +1,5 @@
package sresponse
type authResponse struct {
AccessToken TokenData `json:"accessToken"`
}

View File

@@ -0,0 +1,15 @@
package sresponse
import (
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
"go.uber.org/zap"
)
func BadRPassword(logger mlogger.Logger, source mservice.Type, err error) http.HandlerFunc {
logger.Info("Failed password validation check", zap.Error(err))
return response.BadRequest(logger, source, "invalid_request", err.Error())
}

View File

@@ -0,0 +1,24 @@
package sresponse
import (
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
)
type commentPreviewResponse struct {
authResponse `json:",inline"`
Comments []model.CommentPreview `json:"comments"`
}
func CommentPreview(logger mlogger.Logger, accessToken *TokenData, comments []model.CommentPreview) http.HandlerFunc {
return response.Ok(
logger,
&commentPreviewResponse{
Comments: comments,
authResponse: authResponse{AccessToken: *accessToken},
},
)
}

View File

@@ -0,0 +1,24 @@
package sresponse
import (
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
)
type dzoneResponse struct {
authResponse `json:",inline"`
DZone model.DZone `json:"dzone"`
}
func DZone(logger mlogger.Logger, dzone *model.DZone, accessToken *TokenData) http.HandlerFunc {
return response.Ok(
logger,
&dzoneResponse{
DZone: *dzone,
authResponse: authResponse{AccessToken: *accessToken},
},
)
}

View File

@@ -0,0 +1,16 @@
package sresponse
import (
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
)
type fileUpladed struct {
URL string `json:"url"`
}
func FileUploaded(logger mlogger.Logger, url string) http.HandlerFunc {
return response.Ok(logger, &fileUpladed{URL: url})
}

View File

@@ -0,0 +1,21 @@
package sresponse
import (
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
)
type invitationResp struct {
Invitation model.PublicInvitation `json:"invitation"`
}
func Invitation(logger mlogger.Logger, invitation *model.PublicInvitation) http.HandlerFunc {
return response.Ok(logger, &invitationResp{Invitation: *invitation})
}
func Invitations(logger mlogger.Logger, invitations []model.Invitation) http.HandlerFunc {
return response.Ok(logger, invitations)
}

View File

@@ -0,0 +1,27 @@
package sresponse
import (
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
)
type loginResponse struct {
accountResponse
RefreshToken TokenData `json:"refreshToken"`
}
func Login(logger mlogger.Logger, account *model.Account, accessToken, refreshToken *TokenData) http.HandlerFunc {
return response.Ok(
logger,
&loginResponse{
accountResponse: accountResponse{
Account: *_createAccount(account, false),
authResponse: authResponse{AccessToken: *accessToken},
},
RefreshToken: *refreshToken,
},
)
}

View File

@@ -0,0 +1,49 @@
package sresponse
import (
"encoding/json"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
)
type DynamicResponse[T any] struct {
authResponse `json:",inline"`
Items []T
// FieldName is the JSON key to use for the items.
FieldName string
}
func (dr DynamicResponse[T]) MarshalJSON() ([]byte, error) {
// Create a temporary map to hold the keys and values.
m := map[string]any{
dr.FieldName: dr.Items,
"accessToken": dr.AccessToken,
}
return json.Marshal(m)
}
type handler = func(logger mlogger.Logger, data any) http.HandlerFunc
func objectsAuth[T any](logger mlogger.Logger, items []T, accessToken *TokenData, resource mservice.Type, handler handler) http.HandlerFunc {
resp := &DynamicResponse[T]{
Items: items,
authResponse: authResponse{AccessToken: *accessToken},
FieldName: resource,
}
return handler(logger, resp)
}
func ObjectsAuth[T any](logger mlogger.Logger, items []T, accessToken *TokenData, resource mservice.Type) http.HandlerFunc {
return objectsAuth(logger, items, accessToken, resource, response.Ok)
}
func ObjectAuth[T any](logger mlogger.Logger, item *T, accessToken *TokenData, resource mservice.Type) http.HandlerFunc {
return ObjectsAuth(logger, []T{*item}, accessToken, resource)
}
func ObjectAuthCreated[T any](logger mlogger.Logger, item *T, accessToken *TokenData, resource mservice.Type) http.HandlerFunc {
return objectsAuth(logger, []T{*item}, accessToken, resource, response.Created)
}

View File

@@ -0,0 +1,35 @@
package sresponse
import (
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
)
type organizationsResponse struct {
authResponse `json:",inline"`
Organizations []model.Organization `json:"organizations"`
}
func Organization(logger mlogger.Logger, organization *model.Organization, accessToken *TokenData) http.HandlerFunc {
return Organizations(logger, []model.Organization{*organization}, accessToken)
}
func Organizations(logger mlogger.Logger, organizations []model.Organization, accessToken *TokenData) http.HandlerFunc {
return response.Ok(logger, organizationsResponse{
Organizations: organizations,
authResponse: authResponse{AccessToken: *accessToken},
})
}
type organizationPublicResponse struct {
Organizations []model.OrganizationBase `json:"organizations"`
}
func OrganizationPublic(logger mlogger.Logger, organization *model.OrganizationBase) http.HandlerFunc {
return response.Ok(logger, organizationPublicResponse{
[]model.OrganizationBase{*organization},
})
}

View File

@@ -0,0 +1,45 @@
package sresponse
import (
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
)
type permissionsDescription struct {
Roles []model.RoleDescription `json:"roles"`
Policies []model.PolicyDescription `json:"policies"`
}
type permissionsData struct {
Roles []model.Role `json:"roles"`
Policies []model.RolePolicy `json:"policies"`
Permissions []model.Permission `json:"permissions"`
}
type permissionsResponse struct {
authResponse `json:",inline"`
Descriptions permissionsDescription `json:"descriptions"`
Permissions permissionsData `json:"permissions"`
}
func Permisssions(logger mlogger.Logger,
rolesDescs []model.RoleDescription, policiesDescs []model.PolicyDescription,
roles []model.Role, policies []model.RolePolicy, permissions []model.Permission,
accessToken *TokenData,
) http.HandlerFunc {
return response.Ok(logger, permissionsResponse{
Descriptions: permissionsDescription{
Roles: rolesDescs,
Policies: policiesDescs,
},
Permissions: permissionsData{
Roles: roles,
Policies: policies,
Permissions: permissions,
},
authResponse: authResponse{AccessToken: *accessToken},
})
}

View File

@@ -0,0 +1,37 @@
package sresponse
import (
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
)
type projectsResponse struct {
authResponse `json:",inline"`
Projects []model.Project `json:"projects"`
}
func Projects(logger mlogger.Logger, projects []model.Project, accessToken *TokenData) http.HandlerFunc {
return response.Ok(logger, projectsResponse{
Projects: projects,
authResponse: authResponse{AccessToken: *accessToken},
})
}
func Project(logger mlogger.Logger, project *model.Project, accessToken *TokenData) http.HandlerFunc {
return Projects(logger, []model.Project{*project}, accessToken)
}
type projectPreviewsResponse struct {
authResponse `json:",inline"`
Previews []model.ProjectPreview `json:"previews"`
}
func ProjectsPreviews(logger mlogger.Logger, previews []model.ProjectPreview, accessToken *TokenData) http.HandlerFunc {
return response.Ok(logger, &projectPreviewsResponse{
authResponse: authResponse{AccessToken: *accessToken},
Previews: previews,
})
}

View File

@@ -0,0 +1,12 @@
package sresponse
import (
"net/http"
"github.com/tech/sendico/pkg/model"
)
type (
HandlerFunc = func(r *http.Request) http.HandlerFunc
AccountHandlerFunc = func(r *http.Request, account *model.Account, accessToken *TokenData) http.HandlerFunc
)

View File

@@ -0,0 +1,27 @@
package sresponse
import (
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
)
type resultAuth struct {
authResponse `json:",inline"`
response.Result `json:",inline"`
}
func Success(logger mlogger.Logger, accessToken *TokenData) http.HandlerFunc {
return response.Ok(logger, &resultAuth{
Result: response.Result{Result: true},
authResponse: authResponse{AccessToken: *accessToken},
})
}
func Failed(logger mlogger.Logger, accessToken *TokenData) http.HandlerFunc {
return response.Accepted(logger, &resultAuth{
Result: response.Result{Result: false},
authResponse: authResponse{AccessToken: *accessToken},
})
}

View File

@@ -0,0 +1,16 @@
package sresponse
import (
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
)
func SignUp(logger mlogger.Logger, account *model.Account) http.HandlerFunc {
return response.Ok(
logger,
&account.AccountBase,
)
}

View File

@@ -0,0 +1,25 @@
package sresponse
import (
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
)
type statusesResponse struct {
authResponse `json:",inline"`
Statuses []model.Status `json:"statuses"`
}
func Statuses(logger mlogger.Logger, statuses []model.Status, accessToken *TokenData) http.HandlerFunc {
return response.Ok(logger, statusesResponse{
Statuses: statuses,
authResponse: authResponse{AccessToken: *accessToken},
})
}
func Status(logger mlogger.Logger, status *model.Status, accessToken *TokenData) http.HandlerFunc {
return Statuses(logger, []model.Status{*status}, accessToken)
}

View File

@@ -0,0 +1,8 @@
package sresponse
import "time"
type TokenData struct {
Token string `json:"token"`
Expiration time.Time `json:"expiration"`
}

View File

@@ -0,0 +1,57 @@
package ws
import (
"net/http"
api "github.com/tech/sendico/pkg/api/http"
r "github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/server/interface/api/ws"
"go.uber.org/zap"
"golang.org/x/net/websocket"
)
func respond(logger mlogger.Logger, conn *websocket.Conn, messageType, apiStatus, requestID string, data any) {
message := ws.Message{
BaseResponse: r.BaseResponse{
Status: apiStatus,
Data: data,
},
ID: requestID,
MessageType: messageType,
}
if err := websocket.JSON.Send(conn, message); err != nil {
logger.Warn("Failed to send error message", zap.Error(err), zap.Any("message", message))
}
}
func errorf(logger mlogger.Logger, messageType, requestID string, conn *websocket.Conn, resp r.ErrorResponse) {
logger.Debug(
"Writing error sresponse",
zap.String("error", resp.Error),
zap.String("details", resp.Details),
zap.Int("code", resp.Code),
)
respond(logger, conn, messageType, api.MSError, requestID, &resp)
}
func Ok(logger mlogger.Logger, requestID string, data any) ws.ResponseHandler {
res := func(messageType string, conn *websocket.Conn) {
logger.Debug("Successfully executed request", zap.Any("sresponse", data))
respond(logger, conn, messageType, api.MSSuccess, requestID, data)
}
return res
}
func Internal(logger mlogger.Logger, requestID string, err error) ws.ResponseHandler {
res := func(messageType string, conn *websocket.Conn) {
errorf(logger, messageType, requestID, conn,
r.ErrorResponse{
Error: "internal_error",
Details: err.Error(),
Code: http.StatusInternalServerError,
})
}
return res
}

View File

@@ -0,0 +1,9 @@
package ws
import (
ac "github.com/tech/sendico/server/internal/api/config"
)
type (
Config = ac.WebSocketConfig
)

View File

@@ -0,0 +1,12 @@
package ws
import (
"context"
"golang.org/x/net/websocket"
)
type (
ResponseHandler func(messageType string, conn *websocket.Conn)
HandlerFunc func(ctx context.Context, msg Message) ResponseHandler
)

View File

@@ -0,0 +1,9 @@
package ws
import "github.com/tech/sendico/pkg/api/http/response"
type Message struct {
response.BaseResponse
ID string `json:"id"`
MessageType string `json:"messageType"`
}

View File

@@ -0,0 +1,31 @@
package middleware
import (
"os"
ai "github.com/tech/sendico/server/internal/api/config"
)
type (
TokenConfig = ai.TokenConfig
Config = ai.Config
Signature = ai.SignatureConf
PasswordConfig = ai.PasswordConfig
)
type MapClaims = ai.MapClaims
func getKey(osEnv string) any {
if len(osEnv) == 0 {
return nil
}
return []byte(os.Getenv(osEnv))
}
func SignatureConf(conf *Config) Signature {
return Signature{
PrivateKey: []byte(os.Getenv(conf.Signature.PrivateKeyEnv)),
PublicKey: getKey(conf.Signature.PublicKeyEnv),
Algorithm: conf.Signature.Algorithm,
}
}

View File

@@ -0,0 +1,94 @@
package model
import (
"fmt"
"time"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
mduration "github.com/tech/sendico/pkg/mutil/duration"
"github.com/tech/sendico/server/interface/middleware"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type AccountToken struct {
AccountRef primitive.ObjectID
Login string
Name string
Locale string
Expiration time.Time
}
func createAccountToken(a *model.Account, expiration int) AccountToken {
return AccountToken{
AccountRef: *a.GetID(),
Login: a.Login,
Name: a.Name,
Locale: a.Locale,
Expiration: time.Now().Add(mduration.Param2Duration(expiration, time.Hour)),
}
}
func getTokenParam(claims middleware.MapClaims, param string) (string, error) {
id, ok := claims[param].(string)
if !ok {
return "", merrors.NoData(fmt.Sprintf("param '%s' not found", param))
}
return id, nil
}
const (
paramNameID = "id"
paramNameName = "name"
paramNameLocale = "locale"
paramNameLogin = "login"
paramNameExpiration = "exp"
)
func Claims2Token(claims middleware.MapClaims) (*AccountToken, error) {
var at AccountToken
var err error
var account string
if account, err = getTokenParam(claims, paramNameID); err != nil {
return nil, err
}
if at.AccountRef, err = primitive.ObjectIDFromHex(account); err != nil {
return nil, err
}
if at.Login, err = getTokenParam(claims, paramNameLogin); err != nil {
return nil, err
}
if at.Name, err = getTokenParam(claims, paramNameName); err != nil {
return nil, err
}
if at.Locale, err = getTokenParam(claims, paramNameLocale); err != nil {
return nil, err
}
if expValue, ok := claims[paramNameExpiration]; ok {
switch exp := expValue.(type) {
case time.Time:
at.Expiration = exp
case float64:
at.Expiration = time.Unix(int64(exp), 0)
case int64:
at.Expiration = time.Unix(exp, 0)
default:
return nil, merrors.InvalidDataType(fmt.Sprintf("expiration param is of invalid type: %T", expValue))
}
} else {
return nil, merrors.InvalidDataType(fmt.Sprintf("expiration param is of invalid type: %T", expValue))
}
return &at, nil
}
func Account2Claims(a *model.Account, expiration int) middleware.MapClaims {
t := createAccountToken(a, expiration)
return middleware.MapClaims{
paramNameID: t.AccountRef.Hex(),
paramNameLogin: t.Login,
paramNameName: t.Name,
paramNameLocale: t.Locale,
paramNameExpiration: int64(t.Expiration.Unix()),
}
}

View File

@@ -0,0 +1,11 @@
package account
import (
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api"
"github.com/tech/sendico/server/internal/server/accountapiimp"
)
func Create(a api.API) (mservice.MicroService, error) {
return accountapiimp.CreateAPI(a)
}

View File

@@ -0,0 +1,12 @@
package fileservice
import "github.com/tech/sendico/pkg/model"
type StorageType string
const (
LocalFS StorageType = "local_fs"
AwsS3 StorageType = "aws_s3"
)
type Config = model.DriverConfig[StorageType]

View File

@@ -0,0 +1,11 @@
package fileservice
import (
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api"
"github.com/tech/sendico/server/internal/server/fileserviceimp"
)
func CreateAPI(a api.API, directory string) (mservice.MicroService, error) {
return fileserviceimp.CreateAPI(a, directory)
}

View File

@@ -0,0 +1,11 @@
package invitation
import (
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api"
"github.com/tech/sendico/server/internal/server/invitationimp"
)
func Create(a api.API) (mservice.MicroService, error) {
return invitationimp.CreateAPI(a)
}

View File

@@ -0,0 +1,11 @@
package logo
import (
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api"
"github.com/tech/sendico/server/internal/server/logoimp"
)
func Create(a api.API) (mservice.MicroService, error) {
return logoimp.CreateAPI(a)
}

View File

@@ -0,0 +1,11 @@
package organization
import (
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api"
"github.com/tech/sendico/server/internal/server/organizationimp"
)
func Create(a api.API) (mservice.MicroService, error) {
return organizationimp.CreateAPI(a)
}

View File

@@ -0,0 +1,11 @@
package permission
import (
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api"
"github.com/tech/sendico/server/internal/server/permissionsimp"
)
func Create(a api.API) (mservice.MicroService, error) {
return permissionsimp.CreateAPI(a)
}

View File

@@ -0,0 +1,141 @@
package apiimp
import (
"context"
"github.com/go-chi/chi/v5"
"github.com/tech/sendico/pkg/api/routers/health"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/db"
"github.com/tech/sendico/pkg/domainprovider"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api"
"github.com/tech/sendico/server/interface/services/account"
"github.com/tech/sendico/server/interface/services/invitation"
"github.com/tech/sendico/server/interface/services/logo"
"github.com/tech/sendico/server/interface/services/organization"
"github.com/tech/sendico/server/interface/services/permission"
"go.uber.org/zap"
)
type Microservices = []mservice.MicroService
// APIImp represents the structure of the APIImp
type APIImp struct {
logger mlogger.Logger
db db.Factory
domain domainprovider.DomainProvider
config *api.Config
services Microservices
mw *Middleware
}
func (a *APIImp) installMicroservice(srv mservice.MicroService) {
a.services = append(a.services, srv)
a.logger.Info("Microservice installed", zap.String("service", srv.Name()))
}
func (a *APIImp) addMicroservice(srvf api.MicroServiceFactoryT) error {
srv, err := srvf(a)
if err != nil {
a.logger.Error("Failed to install a microservice", zap.Error(err))
return err
}
a.installMicroservice(srv)
return nil
}
func (a *APIImp) Logger() mlogger.Logger {
return a.logger
}
func (a *APIImp) Config() *api.Config {
return a.config
}
func (a *APIImp) DBFactory() db.Factory {
return a.db
}
func (a *APIImp) DomainProvider() domainprovider.DomainProvider {
return a.domain
}
func (a *APIImp) Register() api.Register {
return a.mw
}
func (a *APIImp) Permissions() auth.Provider {
return a.db.Permissions()
}
func (a *APIImp) installServices() error {
srvf := make([]api.MicroServiceFactoryT, 0)
srvf = append(srvf, account.Create)
srvf = append(srvf, organization.Create)
srvf = append(srvf, invitation.Create)
srvf = append(srvf, logo.Create)
srvf = append(srvf, permission.Create)
for _, v := range srvf {
if err := a.addMicroservice(v); err != nil {
return err
}
}
a.mw.SetStatus(health.SSRunning)
return nil
}
func (a *APIImp) Finish(ctx context.Context) error {
a.mw.SetStatus(health.SSTerminating)
a.mw.Finish()
var lastError error
for i := len(a.services) - 1; i >= 0; i-- {
if err := (a.services[i]).Finish(ctx); err != nil {
lastError = err
a.logger.Warn("Error occurred when finishing service",
zap.Error(err), zap.String("service_name", (a.services[i]).Name()))
} else {
a.logger.Info("Microservice is down", zap.String("service_name", (a.services[i]).Name()))
}
}
return lastError
}
func (a *APIImp) Name() string {
return "api"
}
func CreateAPI(logger mlogger.Logger, config *api.Config, db db.Factory, router *chi.Mux, debug bool) (mservice.MicroService, error) {
p := &APIImp{
logger: logger.Named("api"),
config: config,
db: db,
}
var err error
if p.domain, err = domainprovider.CreateDomainProvider(p.logger, config.Mw.DomainEnv, config.Mw.APIProtocolEnv, config.Mw.EndPointEnv); err != nil {
p.logger.Error("Failed to initizlize domain provider")
return nil, err
}
p.logger.Info("Domain provider installed")
if p.mw, err = CreateMiddleware(logger, db, p.db.Permissions().Enforcer(), router, config.Mw, debug); err != nil {
p.logger.Error("Failed to create middleware", zap.Error(err))
return nil, err
}
p.logger.Info("Middleware installed", zap.Bool("debug_mode", debug))
p.logger.Info("Installing microservices...")
if err := p.installServices(); err != nil {
p.logger.Error("Failed to install a microservice", zap.Error(err))
return nil, err
}
p.logger.Info("Microservices installation complete", zap.Int("microservices", len(p.services)))
return p, nil
}

View File

@@ -0,0 +1,66 @@
package apiimp
import "github.com/tech/sendico/pkg/messaging"
type CORSSettings struct {
MaxAge int `yaml:"max_age"`
AllowedOrigins []string `yaml:"allowed_origins"`
AllowedMethods []string `yaml:"allowed_methods"`
AllowedHeaders []string `yaml:"allowed_headers"`
ExposedHeaders []string `yaml:"exposed_headers"`
AllowCredentials bool `yaml:"allow_credentials"`
}
type SignatureConf struct {
PublicKey any
PrivateKey []byte
Algorithm string
}
type Signature struct {
PublicKeyEnv string `yaml:"public_key_env,omitempty"`
PrivateKeyEnv string `yaml:"secret_key_env"`
Algorithm string `yaml:"algorithm"`
}
type TokenExpiration struct {
Account int `yaml:"account"`
Refresh int `yaml:"refresh"`
}
type TokenConfig struct {
Expiration TokenExpiration `yaml:"expiration_hours"`
Length int `yaml:"length"`
}
type WebSocketConfig struct {
EndpointEnv string `yaml:"endpoint_env"`
Timeout int `yaml:"timeout"`
}
type PasswordChecks struct {
Digit bool `yaml:"digit"`
Upper bool `yaml:"upper"`
Lower bool `yaml:"lower"`
Special bool `yaml:"special"`
MinLength int `yaml:"min_length"`
}
type PasswordConfig struct {
TokenLength int `yaml:"token_length"`
Check PasswordChecks `yaml:"check"`
}
type Config struct {
DomainEnv string `yaml:"domain_env"`
EndPointEnv string `yaml:"api_endpoint_env"`
APIProtocolEnv string `yaml:"api_protocol_env"`
Signature Signature `yaml:"signature"`
CORS CORSSettings `yaml:"CORS"`
WebSocket WebSocketConfig `yaml:"websocket"`
Messaging messaging.Config `yaml:"message_broker"`
Token TokenConfig `yaml:"token"`
Password PasswordConfig `yaml:"password"`
}
type MapClaims = map[string]any

View File

@@ -0,0 +1,138 @@
package apiimp
import (
"os"
"github.com/go-chi/chi/v5"
cm "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/go-chi/metrics"
api "github.com/tech/sendico/pkg/api/http"
amr "github.com/tech/sendico/pkg/api/routers"
"github.com/tech/sendico/pkg/api/routers/health"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/db"
"github.com/tech/sendico/pkg/messaging"
notifications "github.com/tech/sendico/pkg/messaging/notifications/processor"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api/sresponse"
wsh "github.com/tech/sendico/server/interface/api/ws"
"github.com/tech/sendico/server/interface/middleware"
"github.com/tech/sendico/server/internal/api/routers"
mr "github.com/tech/sendico/server/internal/api/routers/metrics"
"github.com/tech/sendico/server/internal/api/ws"
"go.uber.org/zap"
"moul.io/chizap"
)
type Middleware struct {
logger mlogger.Logger
router *chi.Mux
apiEndpoint string
health amr.Health
metrics mr.Metrics
wshandler ws.Router
messaging amr.Messaging
epdispatcher *routers.Dispatcher
}
func (mw *Middleware) Handler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.HandlerFunc) {
mw.epdispatcher.Handler(service, endpoint, method, handler)
}
func (mw *Middleware) AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc) {
mw.epdispatcher.AccountHandler(service, endpoint, method, handler)
}
func (mw *Middleware) WSHandler(messageType string, handler wsh.HandlerFunc) {
mw.wshandler.InstallHandler(messageType, handler)
}
func (mw *Middleware) Consumer(processor notifications.EnvelopeProcessor) error {
return mw.messaging.Consumer(processor)
}
func (mw *Middleware) Producer() messaging.Producer {
return mw.messaging.Producer()
}
func (mw *Middleware) Messaging() messaging.Register {
return mw
}
func (mw *Middleware) Finish() {
mw.messaging.Finish()
mw.health.Finish()
}
func (mw *Middleware) SetStatus(status health.ServiceStatus) {
mw.health.SetStatus(status)
}
func (mw *Middleware) installMiddleware(config *middleware.Config, debug bool) {
mw.logger.Debug("Installing middleware stack...")
// Collect metrics for all incoming HTTP requests
mw.router.Use(metrics.Collector(metrics.CollectorOpts{
Host: false, // avoid high-cardinality "host" label
Proto: true, // include HTTP protocol label
}))
mw.router.Use(cm.RequestID)
mw.router.Use(cm.RealIP)
if debug {
mw.router.Use(chizap.New(mw.logger.Named("http_trace"), &chizap.Opts{
WithReferer: true,
WithUserAgent: true,
}))
}
mw.router.Use(cors.Handler(cors.Options{
AllowedOrigins: config.CORS.AllowedOrigins,
AllowedMethods: config.CORS.AllowedMethods,
AllowedHeaders: config.CORS.AllowedHeaders,
ExposedHeaders: config.CORS.ExposedHeaders,
AllowCredentials: config.CORS.AllowCredentials,
MaxAge: config.CORS.MaxAge,
OptionsPassthrough: false,
Debug: debug,
}))
mw.router.Use(cm.Recoverer)
mw.router.Handle("/metrics", metrics.Handler())
}
func CreateMiddleware(logger mlogger.Logger, db db.Factory, enforcer auth.Enforcer, router *chi.Mux, config *middleware.Config, debug bool) (*Middleware, error) {
p := &Middleware{
logger: logger.Named("middleware"),
router: router,
apiEndpoint: os.Getenv(config.EndPointEnv),
}
p.logger.Info("Set endpoint", zap.String("endpoint", p.apiEndpoint))
p.installMiddleware(config, debug)
var err error
if p.messaging, err = amr.NewMessagingRouter(p.logger, &config.Messaging); err != nil {
p.logger.Error("Failed to create messaging router", zap.Error(err))
return nil, err
}
if p.health, err = amr.NewHealthRouter(p.logger, p.router, p.apiEndpoint); err != nil {
p.logger.Error("Failed to create healthcheck router", zap.Error(err), zap.String("api_endpoint", p.apiEndpoint))
return nil, err
}
if p.metrics, err = mr.NewMetricsRouter(p.logger, p.router, p.apiEndpoint); err != nil {
p.logger.Error("Failed to create metrics router", zap.Error(err), zap.String("api_endpoint", p.apiEndpoint))
return nil, err
}
adb, err := db.NewAccountDB()
if err != nil {
p.logger.Error("Faild to create account database", zap.Error(err))
return nil, err
}
rtdb, err := db.NewRefreshTokensDB()
if err != nil {
p.logger.Error("Faild to create refresh token management database", zap.Error(err))
return nil, err
}
p.epdispatcher = routers.NewDispatcher(p.logger, p.router, adb, rtdb, enforcer, config)
p.wshandler = ws.NewRouter(p.logger, p.router, &config.WebSocket, p.apiEndpoint)
return p, nil
}

View File

@@ -0,0 +1,56 @@
package routers
import (
"errors"
"net/http"
"github.com/go-chi/jwtauth/v5"
api "github.com/tech/sendico/pkg/api/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/server/interface/api/sresponse"
emodel "github.com/tech/sendico/server/interface/model"
"go.uber.org/zap"
)
type tokenHandlerFunc = func(r *http.Request, t *emodel.AccountToken) http.HandlerFunc
func (ar *AuthorizedRouter) tokenHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler tokenHandlerFunc) {
hndlr := func(r *http.Request) http.HandlerFunc {
_, claims, err := jwtauth.FromContext(r.Context())
if err != nil {
ar.logger.Debug("Authorization failed", zap.Error(err), zap.String("request", r.URL.Path))
return response.Unauthorized(ar.logger, ar.service, "credentials required")
}
t, err := emodel.Claims2Token(claims)
if err != nil {
ar.logger.Debug("Failed to decode account token", zap.Error(err))
return response.BadRequest(ar.logger, ar.service, "credentials_unreadable", "faild to parse credentials")
}
return handler(r, t)
}
ar.imp.InstallHandler(service, endpoint, method, hndlr)
}
func (ar *AuthorizedRouter) AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc) {
hndlr := func(r *http.Request, t *emodel.AccountToken) http.HandlerFunc {
var a model.Account
if err := ar.db.Get(r.Context(), t.AccountRef, &a); err != nil {
if errors.Is(err, merrors.ErrNoData) {
ar.logger.Debug("Failed to find related user", zap.Error(err), mzap.ObjRef("account_ref", t.AccountRef))
return response.NotFound(ar.logger, ar.service, err.Error())
}
return response.Internal(ar.logger, ar.service, err)
}
accessToken, err := ar.imp.CreateAccessToken(&a)
if err != nil {
ar.logger.Warn("Failed to generate access token", zap.Error(err))
return response.Internal(ar.logger, ar.service, err)
}
return handler(r, &a, &accessToken)
}
ar.tokenHandler(service, endpoint, method, hndlr)
}

View File

@@ -0,0 +1,34 @@
package routers
import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/jwtauth/v5"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/db/account"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/middleware"
re "github.com/tech/sendico/server/internal/api/routers/endpoint"
)
type AuthorizedRouter struct {
logger mlogger.Logger
db account.DB
imp *re.HttpEndpointRouter
service mservice.Type
}
func NewRouter(logger mlogger.Logger, apiEndpoint string, router chi.Router, db account.DB, enforcer auth.Enforcer, config *middleware.TokenConfig, signature *middleware.Signature) *AuthorizedRouter {
ja := jwtauth.New(signature.Algorithm, signature.PrivateKey, signature.PublicKey)
router.Use(jwtauth.Verifier(ja))
router.Use(jwtauth.Authenticator(ja))
l := logger.Named("authorized")
ar := AuthorizedRouter{
logger: l,
db: db,
imp: re.NewHttpEndpointRouter(l, apiEndpoint, router, config, signature),
service: mservice.Accounts,
}
return &ar
}

View File

@@ -0,0 +1,50 @@
package routers
import (
"os"
"github.com/go-chi/chi/v5"
api "github.com/tech/sendico/pkg/api/http"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/db/account"
"github.com/tech/sendico/pkg/db/refreshtokens"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api/sresponse"
"github.com/tech/sendico/server/interface/middleware"
rauthorized "github.com/tech/sendico/server/internal/api/routers/authorized"
rpublic "github.com/tech/sendico/server/internal/api/routers/public"
)
type Dispatcher struct {
logger mlogger.Logger
public APIRouter
protected ProtectedAPIRouter
}
func (d *Dispatcher) Handler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.HandlerFunc) {
d.public.InstallHandler(service, endpoint, method, handler)
}
func (d *Dispatcher) AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc) {
d.protected.AccountHandler(service, endpoint, method, handler)
}
func NewDispatcher(logger mlogger.Logger, router chi.Router, db account.DB, rtdb refreshtokens.DB, enforcer auth.Enforcer, config *middleware.Config) *Dispatcher {
d := &Dispatcher{
logger: logger.Named("api_dispatcher"),
}
d.logger.Debug("Installing endpoints middleware...")
endpoint := os.Getenv(config.EndPointEnv)
signature := middleware.SignatureConf(config)
router.Group(func(r chi.Router) {
d.public = rpublic.NewRouter(d.logger, endpoint, db, rtdb, r, &config.Token, &signature)
})
router.Group(func(r chi.Router) {
d.protected = rauthorized.NewRouter(d.logger, endpoint, r, db, enforcer, &config.Token, &signature)
})
return d
}

View File

@@ -0,0 +1,36 @@
package routers
import (
"context"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/server/interface/api/sresponse"
"github.com/tech/sendico/server/interface/middleware"
)
type (
RegistratorT = func(chi.Router, string, http.HandlerFunc)
ResponderFunc = func(ctx context.Context, r *http.Request, session *model.SessionIdentifier, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc
)
type HttpEndpointRouter struct {
logger mlogger.Logger
apiEndpoint string
router chi.Router
config middleware.TokenConfig
signature middleware.Signature
}
func NewHttpEndpointRouter(logger mlogger.Logger, apiEndpoint string, router chi.Router, config *middleware.TokenConfig, signature *middleware.Signature) *HttpEndpointRouter {
er := HttpEndpointRouter{
logger: logger.Named("http"),
apiEndpoint: apiEndpoint,
router: router,
signature: *signature,
config: *config,
}
return &er
}

View File

@@ -0,0 +1,50 @@
package routers
import (
"fmt"
"net/http"
"path"
"github.com/go-chi/chi/v5"
api "github.com/tech/sendico/pkg/api/http"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api/sresponse"
"go.uber.org/zap"
)
func (er *HttpEndpointRouter) chooseMethod(method api.HTTPMethod) RegistratorT {
switch method {
case api.Get:
return func(r chi.Router, p string, h http.HandlerFunc) { r.Get(p, h) }
case api.Post:
return func(r chi.Router, p string, h http.HandlerFunc) { r.Post(p, h) }
case api.Put:
return func(r chi.Router, p string, h http.HandlerFunc) { r.Put(p, h) }
case api.Delete:
return func(r chi.Router, p string, h http.HandlerFunc) { r.Delete(p, h) }
case api.Patch:
return func(r chi.Router, p string, h http.HandlerFunc) { r.Patch(p, h) }
case api.Options:
return func(r chi.Router, p string, h http.HandlerFunc) { r.Options(p, h) }
case api.Head:
return func(r chi.Router, p string, h http.HandlerFunc) { r.Head(p, h) }
default:
}
er.logger.Error("Unknown method provided", zap.String("method", api.HTTPMethod2String(method)))
panic(fmt.Sprintf("Unknown method provided: %d", method))
}
func (er *HttpEndpointRouter) endpoint(service mservice.Type, handler string) string {
return path.Join(er.apiEndpoint, service, handler)
}
func (er *HttpEndpointRouter) InstallHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.HandlerFunc) {
ep := er.endpoint(service, endpoint)
hm := er.chooseMethod(method)
hndlr := func(w http.ResponseWriter, r *http.Request) {
res := handler(r)
res(w, r)
}
hm(er.router, ep, hndlr)
er.logger.Info("Handler installed", zap.String("endpoint", ep), zap.String("method", api.HTTPMethod2String(method)))
}

View File

@@ -0,0 +1,20 @@
package routers
import (
"time"
"github.com/go-chi/jwtauth/v5"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/server/interface/api/sresponse"
emodel "github.com/tech/sendico/server/interface/model"
)
func (er *HttpEndpointRouter) CreateAccessToken(user *model.Account) (sresponse.TokenData, error) {
ja := jwtauth.New(er.signature.Algorithm, er.signature.PrivateKey, er.signature.PublicKey)
_, res, err := ja.Encode(emodel.Account2Claims(user, er.config.Expiration.Account))
token := sresponse.TokenData{
Token: res,
Expiration: time.Now().Add(time.Duration(er.config.Expiration.Account) * time.Hour),
}
return token, err
}

View File

@@ -0,0 +1,40 @@
package routers
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/metrics"
"github.com/tech/sendico/pkg/mlogger"
"go.uber.org/zap"
)
type metricsRouter struct {
logger mlogger.Logger
handler http.Handler
}
func (mr *metricsRouter) Finish() {
mr.logger.Debug("Stopped")
}
func (mr *metricsRouter) handle(w http.ResponseWriter, r *http.Request) {
mr.logger.Debug("Serving metrics request...")
mr.handler.ServeHTTP(w, r)
}
func newMetricsRouter(logger mlogger.Logger, router chi.Router, endpoint string) *metricsRouter {
mr := metricsRouter{
logger: logger.Named("metrics"),
handler: metrics.Handler(),
}
logger.Debug("Installing Prometheus middleware...")
router.Group(func(r chi.Router) {
ep := endpoint + "/metrics"
r.Get(ep, mr.handle)
logger.Info("Prometheus handler installed", zap.String("endpoint", ep))
})
return &mr
}

View File

@@ -0,0 +1,14 @@
package routers
import (
"github.com/go-chi/chi/v5"
"github.com/tech/sendico/pkg/mlogger"
)
type Metrics interface {
Finish()
}
func NewMetricsRouter(logger mlogger.Logger, router chi.Router, endpoint string) (Metrics, error) {
return newMetricsRouter(logger, router, endpoint), nil
}

View File

@@ -0,0 +1,63 @@
package routers
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api/srequest"
"go.uber.org/zap"
)
func (pr *PublicRouter) logUserIn(ctx context.Context, r *http.Request, req *srequest.Login) http.HandlerFunc {
// Get the account database entry
trimmedLogin := strings.TrimSpace(req.Login)
account, err := pr.db.GetByEmail(ctx, strings.ToLower(trimmedLogin))
if errors.Is(err, merrors.ErrNoData) || (account == nil) {
pr.logger.Debug("User not found while logging in", zap.Error(err), zap.String("login", req.Login))
return response.Unauthorized(pr.logger, pr.service, "user not found")
}
if err != nil {
pr.logger.Warn("Failed to query user with email", zap.Error(err), zap.String("login", req.Login))
return response.Internal(pr.logger, pr.service, err)
}
if account.VerifyToken != "" {
return response.Forbidden(pr.logger, pr.service, "account_not_verified", "Account verification required")
}
if !account.MatchPassword(req.Password) {
return response.Unauthorized(pr.logger, pr.service, "password does not match")
}
accessToken, err := pr.imp.CreateAccessToken(account)
if err != nil {
pr.logger.Warn("Failed to generate access token", zap.Error(err))
return response.Internal(pr.logger, pr.service, err)
}
return pr.refreshAndRespondLogin(ctx, r, &req.SessionIdentifier, account, &accessToken)
}
func (a *PublicRouter) login(r *http.Request) http.HandlerFunc {
// TODO: add rate check
var req srequest.Login
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
a.logger.Info("Failed to decode login request", zap.Error(err))
return response.BadPayload(a.logger, mservice.Accounts, err)
}
req.Login = strings.TrimSpace(req.Login)
req.Password = strings.TrimSpace(req.Password)
if req.Login == "" {
return response.BadRequest(a.logger, mservice.Accounts, "email_missing", "login request has no user name")
}
if req.Password == "" {
return response.BadRequest(a.logger, mservice.Accounts, "password_missing", "login request has no password")
}
return a.logUserIn(r.Context(), r, &req)
}

View File

@@ -0,0 +1,29 @@
package routers
import (
"encoding/json"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api/srequest"
"github.com/tech/sendico/server/interface/api/sresponse"
"go.uber.org/zap"
)
func (pr *PublicRouter) refreshAccessToken(r *http.Request) http.HandlerFunc {
pr.logger.Debug("Processing access token refresh request")
var req srequest.AccessTokenRefresh
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
pr.logger.Info("Failed to decode token rotation request", zap.Error(err))
return response.BadPayload(pr.logger, mservice.RefreshTokens, err)
}
account, token, err := pr.validateRefreshToken(r.Context(), r, &req)
if err != nil {
pr.logger.Warn("Failed to process access token refreshment request", zap.Error(err))
return response.Auto(pr.logger, pr.service, err)
}
return sresponse.Account(pr.logger, account, token)
}

View File

@@ -0,0 +1,77 @@
package routers
import (
"context"
"crypto/rand"
"encoding/base64"
"io"
"net/http"
"time"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/server/interface/api/sresponse"
"go.uber.org/zap"
)
func generateRefreshTokenData(length int) (string, error) {
randomBytes := make([]byte, length)
if _, err := io.ReadFull(rand.Reader, randomBytes); err != nil {
return "", merrors.Internal("failed to generate secure random bytes: " + err.Error())
}
return base64.URLEncoding.EncodeToString(randomBytes), nil
}
func (er *PublicRouter) prepareRefreshToken(ctx context.Context, r *http.Request, session *model.SessionIdentifier, account *model.Account) (*model.RefreshToken, error) {
refreshToken, err := generateRefreshTokenData(er.config.Length)
if err != nil {
er.logger.Warn("Failed to generate refresh token", zap.Error(err), mzap.StorableRef(account))
return nil, err
}
token := &model.RefreshToken{
AccountBoundBase: model.AccountBoundBase{
AccountRef: account.GetID(),
},
ClientRefreshToken: model.ClientRefreshToken{
SessionIdentifier: *session,
RefreshToken: refreshToken,
},
ExpiresAt: time.Now().Add(time.Duration(er.config.Expiration.Refresh) * time.Hour),
IsRevoked: false,
UserAgent: r.UserAgent(),
IPAddress: r.RemoteAddr,
}
if err = er.rtdb.Create(ctx, token); err != nil {
er.logger.Warn("Failed to store a refresh token", zap.Error(err), mzap.StorableRef(account),
zap.String("client_id", token.ClientID), zap.String("device_id", token.DeviceID))
return nil, err
}
return token, nil
}
func (pr *PublicRouter) refreshAndRespondLogin(
ctx context.Context,
r *http.Request,
session *model.SessionIdentifier,
account *model.Account,
accessToken *sresponse.TokenData,
) http.HandlerFunc {
refreshToken, err := pr.prepareRefreshToken(ctx, r, session, account)
if err != nil {
pr.logger.Warn("Failed to create refresh token", zap.Error(err), mzap.StorableRef(account),
zap.String("client_id", session.ClientID), zap.String("device_id", session.DeviceID))
return response.Internal(pr.logger, pr.service, err)
}
token := sresponse.TokenData{
Token: refreshToken.RefreshToken,
Expiration: refreshToken.ExpiresAt,
}
return sresponse.Login(pr.logger, account, accessToken, &token)
}

View File

@@ -0,0 +1,28 @@
package routers
import (
"encoding/json"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api/srequest"
"go.uber.org/zap"
)
func (pr *PublicRouter) rotateRefreshToken(r *http.Request) http.HandlerFunc {
pr.logger.Debug("Processing token rotation request...")
var req srequest.TokenRefreshRotate
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
pr.logger.Info("Failed to decode token rotation request", zap.Error(err))
return response.BadPayload(pr.logger, mservice.RefreshTokens, err)
}
account, token, err := pr.validateRefreshToken(r.Context(), r, &req)
if err != nil {
pr.logger.Warn("Failed to validate refresh token", zap.Error(err))
return response.Auto(pr.logger, pr.service, err)
}
return pr.refreshAndRespondLogin(r.Context(), r, &req.SessionIdentifier, account, token)
}

View File

@@ -0,0 +1,46 @@
package routers
import (
"github.com/go-chi/chi/v5"
api "github.com/tech/sendico/pkg/api/http"
"github.com/tech/sendico/pkg/db/account"
"github.com/tech/sendico/pkg/db/refreshtokens"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api/sresponse"
"github.com/tech/sendico/server/interface/middleware"
re "github.com/tech/sendico/server/internal/api/routers/endpoint"
)
type PublicRouter struct {
logger mlogger.Logger
db account.DB
imp *re.HttpEndpointRouter
rtdb refreshtokens.DB
config middleware.TokenConfig
signature middleware.Signature
service mservice.Type
}
func (pr *PublicRouter) InstallHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.HandlerFunc) {
pr.imp.InstallHandler(service, endpoint, method, handler)
}
func NewRouter(logger mlogger.Logger, apiEndpoint string, db account.DB, rtdb refreshtokens.DB, router chi.Router, config *middleware.TokenConfig, signature *middleware.Signature) *PublicRouter {
l := logger.Named("public")
hr := PublicRouter{
logger: l,
db: db,
rtdb: rtdb,
config: *config,
signature: *signature,
imp: re.NewHttpEndpointRouter(l, apiEndpoint, router, config, signature),
service: mservice.Accounts,
}
hr.InstallHandler(hr.service, "/login", api.Post, hr.login)
hr.InstallHandler(hr.service, "/rotate", api.Post, hr.rotateRefreshToken)
hr.InstallHandler(hr.service, "/refresh", api.Post, hr.refreshAccessToken)
return &hr
}

View File

@@ -0,0 +1,59 @@
package routers
import (
"context"
"errors"
"net/http"
"time"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/server/interface/api/srequest"
"github.com/tech/sendico/server/interface/api/sresponse"
"go.uber.org/zap"
)
func validateToken(token string, rt *model.RefreshToken) string {
if rt.AccountRef == nil {
return "missing account reference"
}
if token != rt.RefreshToken {
return "tokens do not match"
}
if rt.ExpiresAt.Before(time.Now()) {
return "token expired"
}
if rt.IsRevoked {
return "token has been revoked"
}
return ""
}
func (pr *PublicRouter) validateRefreshToken(ctx context.Context, _ *http.Request, req *srequest.TokenRefreshRotate) (*model.Account, *sresponse.TokenData, error) {
rt, err := pr.rtdb.GetByCRT(ctx, req)
if errors.Is(err, merrors.ErrNoData) {
pr.logger.Info("Refresh token not found", zap.String("client_id", req.ClientID), zap.String("device_id", req.DeviceID))
return nil, nil, err
}
if reason := validateToken(req.RefreshToken, rt); len(reason) > 0 {
pr.logger.Info("Token validation failed", zap.String("reason", reason))
return nil, nil, merrors.Unauthorized(reason)
}
var account model.Account
if err := pr.db.Get(ctx, *rt.AccountRef, &account); errors.Is(err, merrors.ErrNoData) {
pr.logger.Info("User not found while rotating refresh token", zap.Error(err), mzap.ObjRef("account_ref", *rt.AccountRef))
return nil, nil, merrors.Unauthorized("user not found")
}
accessToken, err := pr.imp.CreateAccessToken(&account)
if err != nil {
pr.logger.Warn("Failed to generate access token", zap.Error(err))
return nil, nil, err
}
return &account, &accessToken, nil
}

View File

@@ -0,0 +1,15 @@
package routers
import (
api "github.com/tech/sendico/pkg/api/http"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/server/interface/api/sresponse"
)
type APIRouter interface {
InstallHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.HandlerFunc)
}
type ProtectedAPIRouter interface {
AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc)
}

View File

@@ -0,0 +1,68 @@
package ws
import (
"context"
"fmt"
"net/http"
"os"
"time"
"github.com/go-chi/chi/v5"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/server/interface/api/ws"
ac "github.com/tech/sendico/server/internal/api/config"
"go.uber.org/zap"
"golang.org/x/net/websocket"
)
type DispatcherImpl struct {
logger mlogger.Logger
handlers map[string]ws.HandlerFunc
timeout int
}
func (d *DispatcherImpl) InstallHandler(messageType string, handler ws.HandlerFunc) {
d.handlers[messageType] = handler
d.logger.Info("Handler installed", zap.String("message_type", messageType))
}
func (d *DispatcherImpl) dispatchMessage(ctx context.Context, conn *websocket.Conn) {
var msg ws.Message
err := websocket.JSON.Receive(conn, &msg)
if err != nil {
d.logger.Warn("Failed to read websocket message", zap.Error(err))
return
}
if handler, exists := d.handlers[msg.MessageType]; exists {
responseHandler := handler(ctx, msg)
responseHandler(msg.MessageType, conn)
} else {
d.logger.Warn("Unknown websocket message type", zap.String("message_type", msg.MessageType), zap.Any("message", &msg))
}
}
func (d *DispatcherImpl) handle(w http.ResponseWriter, r *http.Request) {
websocket.Handler(func(conn *websocket.Conn) {
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(d.timeout)*time.Second)
defer cancel()
d.dispatchMessage(ctx, conn)
}).ServeHTTP(w, r)
}
func NewDispatcher(logger mlogger.Logger, router chi.Router, config *ac.WebSocketConfig, apiEndpoint string) *DispatcherImpl {
d := &DispatcherImpl{
logger: logger.Named("websocket"),
handlers: make(map[string]ws.HandlerFunc),
timeout: config.Timeout,
}
d.logger.Debug("Installing websocket middleware...")
router.Group(func(r chi.Router) {
ep := fmt.Sprintf("%s%s", apiEndpoint, os.Getenv(config.EndpointEnv))
d.logger.Info("Installing websockets handler", zap.String("endpoint", ep))
r.Get(ep, d.handle)
})
return d
}

View File

@@ -0,0 +1,15 @@
package ws
import (
"github.com/go-chi/chi/v5"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/server/interface/api/ws"
)
type Router interface {
InstallHandler(messageType string, handler ws.HandlerFunc)
}
func NewRouter(logger mlogger.Logger, router chi.Router, config *ws.Config, apiEndpoint string) Router {
return NewDispatcher(logger, router, config, apiEndpoint)
}

View File

@@ -0,0 +1,27 @@
package appversion
import (
"github.com/tech/sendico/pkg/version"
vf "github.com/tech/sendico/pkg/version/factory"
)
// Build information. Populated at build-time.
var (
Version string
Revision string
Branch string
BuildUser string
BuildDate string
)
func Create() version.Printer {
vi := version.Info{
Program: "MeetX Connectica Backend Service",
Revision: Revision,
Branch: Branch,
BuildUser: BuildUser,
BuildDate: BuildDate,
Version: Version,
}
return vf.Create(&vi)
}

View File

@@ -0,0 +1,35 @@
package flrstring
import (
"math/rand"
"time"
)
// Constants and variables for random string generation
const (
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
letterIdxBits = 6
letterIdxMask = 1<<letterIdxBits - 1
letterIdxMax = 63 / letterIdxBits
)
var src = rand.NewSource(time.Now().UnixNano())
// createRandString creates a random string with the size of n
// See: http://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang
func CreateRandString(n int) string {
b := make([]byte, n)
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
b[i] = letterBytes[idx]
i--
}
cache >>= letterIdxBits
remain--
}
return string(b)
}

View File

@@ -0,0 +1,15 @@
package imagewriter
import (
"net/http"
"strconv"
)
func WriteImage(w http.ResponseWriter, buffer *[]byte, fileType string) error {
w.Header().Set("Content-Type", fileType)
w.Header().Set("Content-Length", strconv.Itoa(len(*buffer)))
w.WriteHeader(http.StatusOK)
_, err := w.Write(*buffer)
return err
}

View File

@@ -0,0 +1,39 @@
package mutil
import (
"fmt"
"strings"
)
func AddParam(base string, param string) string {
base = strings.TrimSuffix(base, "/")
return fmt.Sprintf("%s/{%s}", base, param)
}
func AddAccountRef(base string) string {
return AddParam(base, AccountRefName())
}
func AddObjRef(base string) string {
return AddParam(base, ObjRefName())
}
func AddOrganizaztionRef(base string) string {
return AddParam(base, OrganizationRefName())
}
func AddStatusRef(base string) string {
return AddParam(base, StatusRefName())
}
func AddProjectRef(base string) string {
return AddParam(base, ProjectRefName())
}
func AddInvitationRef(base string) string {
return AddParam(base, InvitationRefName())
}
func AddToken(base string) string {
return AddParam(base, TokenName())
}

View File

@@ -0,0 +1,135 @@
package mutil
import (
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
func GetParam(r *http.Request, paramName string) string {
return chi.URLParam(r, paramName)
}
func GetID(r *http.Request) string {
return GetParam(r, "id")
}
func GetAccountID(r *http.Request) string {
return GetParam(r, AccountRefName())
}
func GetObjRef(r *http.Request) string {
return GetParam(r, ObjRefName())
}
func GetOrganizationID(r *http.Request) string {
return GetParam(r, OrganizationRefName())
}
func GetOrganizationRef(r *http.Request) (primitive.ObjectID, error) {
return primitive.ObjectIDFromHex(GetOrganizationID(r))
}
func GetStatusID(r *http.Request) string {
return GetParam(r, OrganizationRefName())
}
func GetStatusRef(r *http.Request) (primitive.ObjectID, error) {
return primitive.ObjectIDFromHex(GetStatusID(r))
}
func GetProjectID(r *http.Request) string {
return GetParam(r, ProjectRefName())
}
func GetProjectRef(r *http.Request) (primitive.ObjectID, error) {
return primitive.ObjectIDFromHex(GetProjectID(r))
}
func GetInvitationID(r *http.Request) string {
return GetParam(r, InvitationRefName())
}
func GetInvitationRef(r *http.Request) (primitive.ObjectID, error) {
return primitive.ObjectIDFromHex(GetOrganizationID(r))
}
func GetToken(r *http.Request) string {
return GetParam(r, TokenName())
}
// parseFunc is a function type that parses a string to a specific type
type parseFunc[T any] func(string) (T, error)
// getOptionalParam is a generic function that handles optional query parameters
func GetOptionalParam[T any](logger mlogger.Logger, r *http.Request, key string, parse parseFunc[T]) (*T, error) {
vals := r.URL.Query()
s := vals.Get(key)
if s == "" {
return nil, nil
}
val, err := parse(s)
if err != nil {
logger.Debug("Malformed query parameter", zap.Error(err), zap.String(key, s))
return nil, err
}
return &val, nil
}
// getOptionalInt64Param gets an optional int64 query parameter
func GetOptionalInt64Param(logger mlogger.Logger, r *http.Request, key string) (*int64, error) {
return GetOptionalParam(logger, r, key, func(s string) (int64, error) {
return strconv.ParseInt(s, 10, 64)
})
}
func GetLimit(logger mlogger.Logger, r *http.Request) (*int64, error) {
return GetOptionalInt64Param(logger, r, "limit")
}
func GetOffset(logger mlogger.Logger, r *http.Request) (*int64, error) {
return GetOptionalInt64Param(logger, r, "offset")
}
func GetLimitAndOffset(logger mlogger.Logger, r *http.Request) (*int64, *int64, error) {
limit, err := GetLimit(logger, r)
if err != nil {
return nil, nil, err
}
offset, err := GetOffset(logger, r)
if err != nil {
return nil, nil, err
}
return limit, offset, nil
}
func GetOptionalBoolParam(logger mlogger.Logger, r *http.Request, key string) (*bool, error) {
return GetOptionalParam(logger, r, key, strconv.ParseBool)
}
func GetCascadeParam(logger mlogger.Logger, r *http.Request) (*bool, error) {
return GetOptionalBoolParam(logger, r, "cascade")
}
func GetArchiveParam(logger mlogger.Logger, r *http.Request) (*bool, error) {
return GetOptionalBoolParam(logger, r, "archived")
}
func GetViewCursor(logger mlogger.Logger, r *http.Request) (*model.ViewCursor, error) {
var res model.ViewCursor
var err error
if res.Limit, res.Offset, err = GetLimitAndOffset(logger, r); err != nil {
return nil, err
}
if res.IsArchived, err = GetArchiveParam(logger, r); err != nil {
return nil, err
}
return &res, nil
}

View File

@@ -0,0 +1,142 @@
package mutil
import (
"net/http"
"testing"
"github.com/tech/sendico/pkg/mlogger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)
func TestGetOptionalBoolParam(t *testing.T) {
logger := mlogger.Logger(zap.NewNop())
tests := []struct {
name string
query string
expected *bool
hasError bool
}{
{
name: "valid true",
query: "?param=true",
expected: boolPtr(true),
hasError: false,
},
{
name: "valid false",
query: "?param=false",
expected: boolPtr(false),
hasError: false,
},
{
name: "missing parameter",
query: "?other=value",
expected: nil,
hasError: false,
},
{
name: "invalid value",
query: "?param=invalid",
expected: nil,
hasError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, err := http.NewRequest("GET", "http://example.com"+tt.query, nil)
require.NoError(t, err)
result, err := GetOptionalBoolParam(logger, req, "param")
if tt.hasError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
if tt.expected == nil {
assert.Nil(t, result)
} else {
assert.NotNil(t, result)
assert.Equal(t, *tt.expected, *result)
}
})
}
}
func TestGetOptionalInt64Param(t *testing.T) {
logger := mlogger.Logger(zap.NewNop())
tests := []struct {
name string
query string
expected *int64
hasError bool
}{
{
name: "valid positive number",
query: "?param=123",
expected: int64Ptr(123),
hasError: false,
},
{
name: "valid negative number",
query: "?param=-456",
expected: int64Ptr(-456),
hasError: false,
},
{
name: "valid zero",
query: "?param=0",
expected: int64Ptr(0),
hasError: false,
},
{
name: "missing parameter",
query: "?other=value",
expected: nil,
hasError: false,
},
{
name: "invalid value",
query: "?param=invalid",
expected: nil,
hasError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, err := http.NewRequest("GET", "http://example.com"+tt.query, nil)
require.NoError(t, err)
result, err := GetOptionalInt64Param(logger, req, "param")
if tt.hasError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
if tt.expected == nil {
assert.Nil(t, result)
} else {
assert.NotNil(t, result)
assert.Equal(t, *tt.expected, *result)
}
})
}
}
// Helper functions for creating pointers to values
func boolPtr(b bool) *bool {
return &b
}
func int64Ptr(i int64) *int64 {
return &i
}

View File

@@ -0,0 +1,44 @@
package mutil
import (
"net/http"
mutilimp "github.com/tech/sendico/server/internal/mutil/param/internal"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type ParamHelper interface {
Name() string
RefName() string
GetID(r *http.Request) string
GetRef(r *http.Request) (primitive.ObjectID, error)
AddRef(base string) string
}
func CreatePH(resource string) ParamHelper {
return mutilimp.CreateImp(resource)
}
type DependentParamHelper struct {
p ParamHelper
c ParamHelper
}
func (ph *DependentParamHelper) Parent() ParamHelper {
return ph.p
}
func (ph *DependentParamHelper) Child() ParamHelper {
return ph.c
}
func (ph *DependentParamHelper) AddRef(base string) string {
return ph.Parent().AddRef(ph.Child().AddRef(base))
}
func CreateDPH(pRes, cRes string) *DependentParamHelper {
return &DependentParamHelper{
p: CreatePH(pRes),
c: CreatePH(cRes),
}
}

View File

@@ -0,0 +1,51 @@
package mutilimp
import (
"fmt"
"net/http"
"strings"
"github.com/go-chi/chi/v5"
"go.mongodb.org/mongo-driver/bson/primitive"
)
func addParam(base string, param string) string {
base = strings.TrimSuffix(base, "/")
return fmt.Sprintf("%s/{%s}", base, param)
}
func _ref(param string) string {
return param + "_ref"
}
func getParam(r *http.Request, paramName string) string {
return chi.URLParam(r, paramName)
}
type ParamHelper struct {
t string
}
func (ph *ParamHelper) Name() string {
return ph.t
}
func (ph *ParamHelper) RefName() string {
return _ref(ph.Name())
}
func (ph *ParamHelper) GetID(r *http.Request) string {
return getParam(r, ph.RefName())
}
func (ph *ParamHelper) GetRef(r *http.Request) (primitive.ObjectID, error) {
return primitive.ObjectIDFromHex(ph.GetID(r))
}
func (ph *ParamHelper) AddRef(base string) string {
return addParam(base, ph.RefName())
}
func CreateImp(resource string) *ParamHelper {
return &ParamHelper{t: resource}
}

View File

@@ -0,0 +1,15 @@
package mutil
import (
"net/http"
"go.uber.org/zap"
)
func PLog(ph ParamHelper, r *http.Request) zap.Field {
return zap.String(ph.Name(), ph.GetID(r))
}
func PLogType(ph ParamHelper) zap.Field {
return zap.String("object", ph.Name())
}

View File

@@ -0,0 +1,33 @@
package mutil
func _ref(param string) string {
return param + "_ref"
}
func AccountRefName() string {
return _ref("account")
}
func ObjRefName() string {
return _ref("obj")
}
func OrganizationRefName() string {
return _ref("org")
}
func StatusRefName() string {
return _ref("status")
}
func ProjectRefName() string {
return _ref("project")
}
func InvitationRefName() string {
return _ref("invitation")
}
func TokenName() string {
return "token"
}

View File

@@ -0,0 +1,11 @@
package mutil
import (
"net/http"
"go.mongodb.org/mongo-driver/bson/primitive"
)
func GetAccountRef(r *http.Request) (primitive.ObjectID, error) {
return primitive.ObjectIDFromHex(GetAccountID(r))
}

View File

@@ -0,0 +1,15 @@
package mutil
import "time"
func ToDate(t time.Time) string {
return t.Format(time.DateOnly)
}
func ToTime(t time.Time) string {
return t.Format(time.TimeOnly)
}
func ToDateTime(t time.Time) string {
return t.Format(time.DateTime)
}

View File

@@ -0,0 +1,130 @@
package aapitemplate
import (
"github.com/tech/sendico/server/interface/api/sresponse"
)
type HandlerResolver func(sresponse.AccountHandlerFunc) sresponse.AccountHandlerFunc
type Config interface {
WithNoCreate() Config
WithCreateHandler(handler sresponse.AccountHandlerFunc) Config
WithNoList() Config
WithListHandler(handler sresponse.AccountHandlerFunc) Config
WithNoGet() Config
WithGetHandler(handler sresponse.AccountHandlerFunc) Config
WithNoUpdate() Config
WithUpdateHandler(handler sresponse.AccountHandlerFunc) Config
WithNoDelete() Config
WithDeleteHandler(handler sresponse.AccountHandlerFunc) Config
WithReorderHandler(reorder ReorderConfig) Config
}
type AAPIConfig struct {
CreateResolver HandlerResolver
ListResolver HandlerResolver
GetResolver HandlerResolver
UpdateResolver HandlerResolver
DeleteResolver HandlerResolver
ArchiveResolver HandlerResolver
Reorder *ReorderConfig
}
// WithNoCreate disables the create endpoint by replacing its resolver.
func (cfg *AAPIConfig) WithNoCreate() *AAPIConfig {
cfg.CreateResolver = disableResolver
return cfg
}
// WithCreateHandler overrides the create endpoint by replacing its resolver.
func (cfg *AAPIConfig) WithCreateHandler(handler sresponse.AccountHandlerFunc) *AAPIConfig {
cfg.CreateResolver = overrideResolver(handler)
return cfg
}
// WithNoList disables the list endpoint.
func (cfg *AAPIConfig) WithNoList() *AAPIConfig {
cfg.ListResolver = disableResolver
return cfg
}
// WithListHandler overrides the list endpoint.
func (cfg *AAPIConfig) WithListHandler(handler sresponse.AccountHandlerFunc) *AAPIConfig {
cfg.ListResolver = overrideResolver(handler)
return cfg
}
// WithNoGet disables the get endpoint.
func (cfg *AAPIConfig) WithNoGet() *AAPIConfig {
cfg.GetResolver = disableResolver
return cfg
}
// WithGetHandler overrides the get endpoint.
func (cfg *AAPIConfig) WithGetHandler(handler sresponse.AccountHandlerFunc) *AAPIConfig {
cfg.GetResolver = overrideResolver(handler)
return cfg
}
// WithNoUpdate disables the update endpoint.
func (cfg *AAPIConfig) WithNoUpdate() *AAPIConfig {
cfg.UpdateResolver = disableResolver
return cfg
}
// WithUpdateHandler overrides the update endpoint.
func (cfg *AAPIConfig) WithUpdateHandler(handler sresponse.AccountHandlerFunc) *AAPIConfig {
cfg.UpdateResolver = overrideResolver(handler)
return cfg
}
// WithNoDelete disables the delete endpoint.
func (cfg *AAPIConfig) WithNoDelete() *AAPIConfig {
cfg.DeleteResolver = disableResolver
return cfg
}
// WithDeleteHandler overrides the delete endpoint.
func (cfg *AAPIConfig) WithDeleteHandler(handler sresponse.AccountHandlerFunc) *AAPIConfig {
cfg.DeleteResolver = overrideResolver(handler)
return cfg
}
func (cfg *AAPIConfig) WithNoArchive() *AAPIConfig {
cfg.ArchiveResolver = disableResolver
return cfg
}
func (cfg *AAPIConfig) WithArchiveHandler(handler sresponse.AccountHandlerFunc) *AAPIConfig {
cfg.ArchiveResolver = overrideResolver(handler)
return cfg
}
// defaultResolver returns the default handler unchanged.
func defaultResolver(defaultHandler sresponse.AccountHandlerFunc) sresponse.AccountHandlerFunc {
return defaultHandler
}
// disableResolver always returns nil, disabling the endpoint.
func disableResolver(_ sresponse.AccountHandlerFunc) sresponse.AccountHandlerFunc {
return nil
}
// overrideResolver returns a resolver that always returns the given custom handler.
func overrideResolver(custom sresponse.AccountHandlerFunc) HandlerResolver {
return func(_ sresponse.AccountHandlerFunc) sresponse.AccountHandlerFunc {
return custom
}
}
func NewConfig() *AAPIConfig {
return &AAPIConfig{
CreateResolver: defaultResolver,
ListResolver: defaultResolver,
GetResolver: defaultResolver,
UpdateResolver: defaultResolver,
DeleteResolver: defaultResolver,
ArchiveResolver: defaultResolver,
Reorder: nil,
}
}

Some files were not shown because too many files have changed in this diff Show More