mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 21:00:06 +01:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
165d2a2af1 | ||
|
|
fc1c83ba18 | ||
|
|
2be0eb8180 | ||
|
|
81043419e1 | ||
|
|
f2ff08993a | ||
|
|
fabf456786 | ||
|
|
23c60d4ce8 | ||
|
|
c9bc09ae92 | ||
|
|
6ec8305962 | ||
|
|
8d34198477 | ||
|
|
f79224b831 | ||
|
|
cd8e3f2d86 | ||
|
|
ad0ec48027 | ||
|
|
7a3e56b3ff | ||
|
|
015044abb1 | ||
|
|
afd8c8ceb2 | ||
|
|
b358911b1b | ||
|
|
ad8141a497 | ||
|
|
9485b2ae4f | ||
|
|
cc8e8b38c7 | ||
|
|
586e51b8cc | ||
|
|
ea4c625269 | ||
|
|
b4c108a5be | ||
|
|
3a1ce33bca | ||
|
|
dbb7ec398e | ||
|
|
82847e5027 | ||
|
|
4314599427 | ||
|
|
9108ee8f8f | ||
|
|
1feb3e29e1 | ||
|
|
0e9ce98624 | ||
|
|
96aef81959 | ||
|
|
62232e18f3 | ||
|
|
515327153c | ||
|
|
5f2e885a8e | ||
|
|
01b48efba0 | ||
|
|
1c87566c5b | ||
|
|
ad00a3c45d | ||
|
|
158fdce780 | ||
|
|
c4fd344364 | ||
|
|
953e724ba0 | ||
|
|
256acfd686 | ||
|
|
1dd0a39aad | ||
|
|
7ef0605c2b | ||
|
|
8a92727b88 | ||
|
|
1f9920a04c | ||
|
|
5fd96ec40f | ||
|
|
bc72df3c9a | ||
|
|
56ea963b47 |
2
.github/workflows/docker-publish.yml
vendored
2
.github/workflows/docker-publish.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
# https://github.com/sigstore/cosign-installer
|
||||
- name: Install cosign
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
with:
|
||||
cosign-release: 'v2.2.4'
|
||||
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -137,4 +137,6 @@ notes.txt
|
||||
|
||||
.terraform
|
||||
.terraform.lock*
|
||||
terraform.tfstate*
|
||||
terraform.tfstate*
|
||||
|
||||
ui/src/sdk
|
||||
8
Makefile
8
Makefile
@@ -23,7 +23,7 @@ MODULE_PATH ?= $(GIT_HOST)/$(GIT_USER)/$(BIN)
|
||||
|
||||
# SDK / module settings (Go)
|
||||
SDK_REPO ?= $(BIN)-sdk-go # repo name used for module path
|
||||
SDK_OUTDIR ?= sdk/go # output directory (inside repo)
|
||||
SDK_OUTDIR ?= ../autoglue-sdk-go # output directory (inside repo)
|
||||
SDK_PKG ?= ${BIN} # package name inside the SDK
|
||||
|
||||
UI_SSG_ROUTES ?= /,/login,/docs,/pricing
|
||||
@@ -70,7 +70,7 @@ export GO_POST_PROCESS_FILE := gofmt -w
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
# --- version metadata (ldflags) ---
|
||||
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||
VERSION := $(shell git describe --tags --always 2>/dev/null || echo "dev")
|
||||
COMMIT := $(shell git rev-parse HEAD 2>/dev/null || echo "none")
|
||||
DATE := $(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
BUILT_BY := $(shell whoami)
|
||||
@@ -247,7 +247,6 @@ TS_PROPS := -p npmName=$(SDK_TS_NPM_NAME) -p npmVersion=$(SDK_TS_NPM_VER) $
|
||||
# --- sdk generation (Go) ---
|
||||
sdk-go: $(DOCS_JSON) validate-spec check-tags ## Generate Go SDK + tidy module
|
||||
@echo ">> Generating Go SDK (module $(GIT_HOST_CLEAN)/$(GIT_USER_CLEAN)/$(SDK_REPO_CLEAN), Go $(GO_VERSION))..."
|
||||
@rm -rf "$(SDK_OUTDIR_CLEAN)"; mkdir -p "$(SDK_OUTDIR_CLEAN)"
|
||||
@$(call OGC_GENERATE,go,$(SDK_OUTDIR_CLEAN),--additional-properties=packageName=$(SDK_PKG_CLEAN) $(OAG_GIT_PROPS))
|
||||
@cd "$(SDK_OUTDIR_CLEAN)"; \
|
||||
$(GOCMD) mod edit -go=$(GO_VERSION); \
|
||||
@@ -311,6 +310,9 @@ doctor: ## Print environment diagnostics (shell, versions, generator availabilit
|
||||
$(OGC_BIN) version || true; \
|
||||
}
|
||||
|
||||
fetch-pgweb: ## Fetch PGWeb Binaries for embedding
|
||||
go run ./tools/pgweb_fetch.go
|
||||
|
||||
help: ## Show this help
|
||||
@grep -hE '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \
|
||||
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
51
cmd/db.go
Normal file
51
cmd/db.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/glueops/autoglue/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var dbCmd = &cobra.Command{
|
||||
Use: "db",
|
||||
Short: "Database utilities",
|
||||
}
|
||||
|
||||
var dbPsqlCmd = &cobra.Command{
|
||||
Use: "psql",
|
||||
Short: "Open a psql session to the app database",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.DbURL == "" {
|
||||
return errors.New("database.url is empty")
|
||||
}
|
||||
psql := "psql"
|
||||
if runtime.GOOS == "windows" {
|
||||
psql = "psql.exe"
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 72*time.Hour)
|
||||
defer cancel()
|
||||
|
||||
psqlCmd := exec.CommandContext(ctx, psql, cfg.DbURL)
|
||||
psqlCmd.Stdin, psqlCmd.Stdout, psqlCmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
fmt.Println("Launching psql…")
|
||||
return psqlCmd.Run()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
dbCmd.AddCommand(dbPsqlCmd)
|
||||
|
||||
rootCmd.AddCommand(dbCmd)
|
||||
}
|
||||
110
cmd/serve.go
110
cmd/serve.go
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/glueops/autoglue/internal/auth"
|
||||
"github.com/glueops/autoglue/internal/bg"
|
||||
"github.com/glueops/autoglue/internal/config"
|
||||
"github.com/glueops/autoglue/internal/models"
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -38,6 +39,8 @@ var serveCmd = &cobra.Command{
|
||||
log.Fatalf("failed to init background jobs: %v", err)
|
||||
}
|
||||
|
||||
rt.DB.Where("status IN ?", []string{"scheduled", "queued", "pending"}).Delete(&models.Job{})
|
||||
|
||||
// Start workers in background ONCE
|
||||
go func() {
|
||||
if err := jobs.Start(); err != nil {
|
||||
@@ -50,7 +53,7 @@ var serveCmd = &cobra.Command{
|
||||
{
|
||||
// schedule next 03:30 local time
|
||||
next := time.Now().Truncate(24 * time.Hour).Add(24 * time.Hour).Add(3*time.Hour + 30*time.Minute)
|
||||
_, _ = jobs.Enqueue(
|
||||
_, err = jobs.Enqueue(
|
||||
context.Background(),
|
||||
uuid.NewString(),
|
||||
"archer_cleanup",
|
||||
@@ -58,10 +61,13 @@ var serveCmd = &cobra.Command{
|
||||
archer.WithScheduleTime(next),
|
||||
archer.WithMaxRetries(1),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to enqueue archer cleanup job: %v", err)
|
||||
}
|
||||
|
||||
// schedule next 03:45 local time
|
||||
next2 := time.Now().Truncate(24 * time.Hour).Add(24 * time.Hour).Add(3*time.Hour + 45*time.Minute)
|
||||
_, _ = jobs.Enqueue(
|
||||
_, err = jobs.Enqueue(
|
||||
context.Background(),
|
||||
uuid.NewString(),
|
||||
"tokens_cleanup",
|
||||
@@ -69,46 +75,47 @@ var serveCmd = &cobra.Command{
|
||||
archer.WithScheduleTime(next2),
|
||||
archer.WithMaxRetries(1),
|
||||
)
|
||||
}
|
||||
|
||||
// Periodic scheduler
|
||||
schedCtx, schedCancel := context.WithCancel(context.Background())
|
||||
defer schedCancel()
|
||||
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
_, err := jobs.Enqueue(
|
||||
context.Background(),
|
||||
uuid.NewString(),
|
||||
"bootstrap_bastion",
|
||||
bg.BastionBootstrapArgs{},
|
||||
archer.WithMaxRetries(3),
|
||||
// while debugging, avoid extra schedule delay:
|
||||
archer.WithScheduleTime(time.Now().Add(60*time.Second)),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("failed to enqueue bootstrap_bastion: %v", err)
|
||||
}
|
||||
/*
|
||||
_, _ = jobs.Enqueue(
|
||||
context.Background(),
|
||||
uuid.NewString(),
|
||||
"tokens_cleanup",
|
||||
bg.TokensCleanupArgs{},
|
||||
archer.WithMaxRetries(3),
|
||||
archer.WithScheduleTime(time.Now().Add(10*time.Second)),
|
||||
)
|
||||
*/
|
||||
case <-schedCtx.Done():
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("failed to enqueue token cleanup job: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = jobs.Enqueue(
|
||||
context.Background(),
|
||||
uuid.NewString(),
|
||||
"db_backup_s3",
|
||||
bg.DbBackupArgs{IntervalS: 3600},
|
||||
archer.WithMaxRetries(1),
|
||||
archer.WithScheduleTime(time.Now().Add(1*time.Hour)),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to enqueue backup jobs: %v", err)
|
||||
}
|
||||
|
||||
_, err = jobs.Enqueue(
|
||||
context.Background(),
|
||||
uuid.NewString(),
|
||||
"dns_reconcile",
|
||||
bg.DNSReconcileArgs{MaxDomains: 25, MaxRecords: 100, IntervalS: 10},
|
||||
archer.WithScheduleTime(time.Now().Add(5*time.Second)),
|
||||
archer.WithMaxRetries(1),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to enqueue dns reconcile: %v", err)
|
||||
}
|
||||
|
||||
_, err := jobs.Enqueue(
|
||||
context.Background(),
|
||||
uuid.NewString(),
|
||||
"bootstrap_bastion",
|
||||
bg.BastionBootstrapArgs{IntervalS: 10},
|
||||
archer.WithMaxRetries(3),
|
||||
// while debugging, avoid extra schedule delay:
|
||||
archer.WithScheduleTime(time.Now().Add(60*time.Second)),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("failed to enqueue bootstrap_bastion: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
_ = auth.Refresh(rt.DB, rt.Cfg.JWTPrivateEncKey)
|
||||
go func() {
|
||||
@@ -119,7 +126,26 @@ var serveCmd = &cobra.Command{
|
||||
}
|
||||
}()
|
||||
|
||||
r := api.NewRouter(rt.DB, jobs)
|
||||
r := api.NewRouter(rt.DB, jobs, nil)
|
||||
|
||||
if cfg.DBStudioEnabled {
|
||||
dbURL := cfg.DbURLRO
|
||||
if dbURL == "" {
|
||||
dbURL = cfg.DbURL
|
||||
}
|
||||
|
||||
studio, err := api.PgwebHandler(
|
||||
dbURL,
|
||||
"db-studio",
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to init db studio: %v", err)
|
||||
} else {
|
||||
r = api.NewRouter(rt.DB, jobs, studio)
|
||||
log.Printf("pgweb mounted at /db-studio/")
|
||||
}
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.Port)
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ services:
|
||||
- postgres
|
||||
|
||||
mailpit:
|
||||
image: axllent/mailpit@sha256:6abc8e633df15eaf785cfcf38bae48e66f64beecdc03121e249d0f9ec15f0707
|
||||
image: axllent/mailpit@sha256:e22dce5b36f93c77082e204a3942fb6b283b7896e057458400a4c88344c3df68
|
||||
restart: always
|
||||
ports:
|
||||
- "1025:1025"
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1120
docs/swagger.yaml
1120
docs/swagger.yaml
File diff suppressed because it is too large
Load Diff
74
go.mod
74
go.mod
@@ -4,21 +4,29 @@ go 1.25.4
|
||||
|
||||
require (
|
||||
github.com/alexedwards/argon2id v1.0.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.6
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.18
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.22
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.59.4
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0
|
||||
github.com/coreos/go-oidc/v3 v3.16.0
|
||||
github.com/dyaksa/archer v1.1.3
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/go-chi/chi/v5 v5.2.3
|
||||
github.com/go-chi/cors v1.2.2
|
||||
github.com/go-chi/httprate v0.15.0
|
||||
github.com/go-playground/validator/v10 v10.28.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/sosedoff/pgweb v0.16.2
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/swaggo/http-swagger/v2 v2.0.2
|
||||
github.com/swaggo/swag/v2 v2.0.0-rc4
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/oauth2 v0.32.0
|
||||
golang.org/x/crypto v0.44.0
|
||||
golang.org/x/oauth2 v0.33.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/datatypes v1.2.7
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
@@ -27,32 +35,74 @@ require (
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/BurntSushi/toml v1.1.0 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5 // 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.40.0 // indirect
|
||||
github.com/aws/smithy-go v1.23.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/spec v0.20.9 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jessevdk/go-flags v1.5.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_golang v1.19.1 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
@@ -61,12 +111,20 @@ require (
|
||||
github.com/sv-tools/openapi v0.2.1 // indirect
|
||||
github.com/swaggo/files/v2 v2.0.0 // indirect
|
||||
github.com/swaggo/swag v1.8.1 // indirect
|
||||
github.com/tuvistavie/securerandom v0.0.0-20140719024926-15512123a948 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // 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
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gorm.io/driver/mysql v1.5.6 // indirect
|
||||
)
|
||||
|
||||
199
go.sum
199
go.sum
@@ -1,13 +1,89 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
|
||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5 h1:VauE2GcJNZFun2Och6tIT2zJZK1v6jxALQDA9BIji/E=
|
||||
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5/go.mod h1:gxOHeajFfvGQh/fxlC8oOKBe23xnnJTif00IFFbiT+o=
|
||||
github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs=
|
||||
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
|
||||
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
||||
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.18 h1:RouG3AcF2fLFhw+Z0qbnuIl9HZ0Kh4E/U9sKwTMRpMI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.18/go.mod h1:aXZ13mSQC8S2VEHwGfL1COMuJ1Zty6pX5xU7hyqjvCg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.19 h1:qdUtOw4JhZr2YcKO3g0ho/IcFXfXrrb8xlX05Y6EvSw=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.19/go.mod h1:tMJ8bur01t8eEm0atLadkIIFA154OJ4JCKZeQ+o+R7k=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.20 h1:/jWF4Wu90EhKCgjTdy1DGxcbcbNrjfBHvksEL79tfQc=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.20/go.mod h1:95Hh1Tc5VYKL9NJ7tAkDcqeKt+MCXQB1hQZaRdJIZE0=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.22 h1:hyIVGBHhQPaNP9D4BaVRwpjLMCwMMdAkHqB3gGMiykU=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.22/go.mod h1:B9E2qHs3/YGfeQZ4jrIE/nPvqxtyafZrJ5EQiZBG6pk=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.23 h1:IQILcxVgMO2BVLaJ2aAv21dKWvE1MduNrbvuK43XL2Q=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.23/go.mod h1:JRodHszhVdh5TPUknxDzJzrMiznG+M+FfR3WSWKgCI8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.24 h1:iJ2FmPT35EaIB0+kMa6TnQ+PwG5A1prEdAw+PsMzfHg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.24/go.mod h1:U91+DrfjAiXPDEGYhh/x29o4p0qHX5HDqG7y5VViv64=
|
||||
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/route53 v1.59.4 h1:KEszjusgJ2dAqE5nSJY+5AHBkakfah8Sx6Vk3pjgrq8=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.59.4/go.mod h1:TUbfYOisWZWyT2qjmlMh93ERw1Ry8G4q/yT2Q8TsDag=
|
||||
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/s3 v1.90.1 h1:kKJk9r6iLMfCGy8RL9GWg3n9gUE1IpSwqYP3/5bdL1s=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.1/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2 h1:DhdbtDl4FdNlj31+xiRXANxEE+eC7n8JQz+/ilwQ8Uc=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2/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/sso v1.30.2 h1:/p6MxkbQoCzaGQT3WO0JwG0FlQyG9RD8VmdmoKc5xqU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.2/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 h1:NjShtS1t8r5LUfFVtFeI8xLAHQNTa7UI0VawXlrBMFQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3/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/ssooidc v1.35.6 h1:0dES42T2dhICCbVB3JSTTn7+Bz93wfJEK1b7jksZIyQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.6/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 h1:gTsnx0xXNQ6SBbymoDvcoRHL+q4l/dAFsQuKfDWSaGc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 h1:ZGDJVmlpPFiNFCb/I42nYVKUanJAdFUiSmUo/32AqPQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.40.0/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.40.1 h1:5sbIM57lHLaEaNWdIx23JH30LNBsSDkjN/QXGcRLAFc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.40.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 h1:HK5ON3KmQV2HcAunnx4sKLB9aPf3gKGwVAf7xnx0QT0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2/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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
|
||||
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
@@ -16,12 +92,20 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
||||
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/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU=
|
||||
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0=
|
||||
github.com/dyaksa/archer v1.1.3 h1:jfe51tSNzzscFpu+Vilm4SKb0Lnv6FR1yaGspjab4x4=
|
||||
github.com/dyaksa/archer v1.1.3/go.mod h1:IYSp67u14JHTNuvvy6gG1eaX2TPywXvfk1FiyZwVEK4=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
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=
|
||||
@@ -43,6 +127,15 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
@@ -50,6 +143,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
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/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
@@ -57,8 +152,9 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
@@ -71,16 +167,22 @@ github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
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=
|
||||
@@ -89,6 +191,9 @@ 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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
@@ -99,12 +204,23 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
|
||||
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
|
||||
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
|
||||
@@ -114,14 +230,30 @@ 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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sosedoff/pgweb v0.16.2 h1:1F1CWlCLSEgSctMva+nYuUibdhyiCUzlXyU5MQUJbFM=
|
||||
github.com/sosedoff/pgweb v0.16.2/go.mod h1:ER7fsBddI3h7MQKO5RsUPi7Q/PWZYSKcI61kTp369Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
@@ -141,6 +273,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
|
||||
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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
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=
|
||||
@@ -160,38 +293,56 @@ github.com/swaggo/swag v1.8.1 h1:JuARzFX1Z1njbCGz+ZytBR15TFJwF2Q7fu8puJHhQYI=
|
||||
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
|
||||
github.com/swaggo/swag/v2 v2.0.0-rc4 h1:SZ8cK68gcV6cslwrJMIOqPkJELRwq4gmjvk77MrvHvY=
|
||||
github.com/swaggo/swag/v2 v2.0.0-rc4/go.mod h1:Ow7Y8gF16BTCDn8YxZbyKn8FkMLRUHekv1kROJZpbvE=
|
||||
github.com/tuvistavie/securerandom v0.0.0-20140719024926-15512123a948 h1:yL0l/u242MzDP6D0B5vGC+wxm5WRY+alQZy+dJk3bFI=
|
||||
github.com/tuvistavie/securerandom v0.0.0-20140719024926-15512123a948/go.mod h1:a06d/M1pxWi51qiSrfGMHaEydtuXT06nha8N2aNQuXk=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/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/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.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-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -199,30 +350,32 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.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/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
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/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
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/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
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-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -49,6 +49,14 @@ func AuthMiddleware(db *gorm.DB, requireOrg bool) func(http.Handler) http.Handle
|
||||
} else if appKey := r.Header.Get("X-APP-KEY"); appKey != "" {
|
||||
secret := r.Header.Get("X-APP-SECRET")
|
||||
user = auth.ValidateAppKeyPair(appKey, secret, db)
|
||||
} else if c, err := r.Cookie("ag_jwt"); err == nil {
|
||||
tok := strings.TrimSpace(c.Value)
|
||||
if strings.HasPrefix(strings.ToLower(tok), "bearer ") {
|
||||
tok = tok[7:]
|
||||
}
|
||||
if tok != "" {
|
||||
user = auth.ValidateJWT(tok, db)
|
||||
}
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
|
||||
26
internal/api/mount_admin_routes.go
Normal file
26
internal/api/mount_admin_routes.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||
"github.com/glueops/autoglue/internal/bg"
|
||||
"github.com/glueops/autoglue/internal/handlers"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func mountAdminRoutes(r chi.Router, db *gorm.DB, jobs *bg.Jobs, authUser func(http.Handler) http.Handler) {
|
||||
r.Route("/admin", func(admin chi.Router) {
|
||||
admin.Route("/archer", func(archer chi.Router) {
|
||||
archer.Use(authUser)
|
||||
archer.Use(httpmiddleware.RequirePlatformAdmin())
|
||||
|
||||
archer.Get("/jobs", handlers.AdminListArcherJobs(db))
|
||||
archer.Post("/jobs", handlers.AdminEnqueueArcherJob(db, jobs))
|
||||
archer.Post("/jobs/{id}/retry", handlers.AdminRetryArcherJob(db))
|
||||
archer.Post("/jobs/{id}/cancel", handlers.AdminCancelArcherJob(db))
|
||||
archer.Get("/queues", handlers.AdminListArcherQueues(db))
|
||||
})
|
||||
})
|
||||
}
|
||||
20
internal/api/mount_annotation_routes.go
Normal file
20
internal/api/mount_annotation_routes.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/glueops/autoglue/internal/handlers"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func mountAnnotationRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||
r.Route("/annotations", func(a chi.Router) {
|
||||
a.Use(authOrg)
|
||||
a.Get("/", handlers.ListAnnotations(db))
|
||||
a.Post("/", handlers.CreateAnnotation(db))
|
||||
a.Get("/{id}", handlers.GetAnnotation(db))
|
||||
a.Patch("/{id}", handlers.UpdateAnnotation(db))
|
||||
a.Delete("/{id}", handlers.DeleteAnnotation(db))
|
||||
})
|
||||
}
|
||||
37
internal/api/mount_api_routes.go
Normal file
37
internal/api/mount_api_routes.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||
"github.com/glueops/autoglue/internal/bg"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func mountAPIRoutes(r chi.Router, db *gorm.DB, jobs *bg.Jobs) {
|
||||
r.Route("/api", func(api chi.Router) {
|
||||
api.Route("/v1", func(v1 chi.Router) {
|
||||
authUser := httpmiddleware.AuthMiddleware(db, false)
|
||||
authOrg := httpmiddleware.AuthMiddleware(db, true)
|
||||
|
||||
// shared basics
|
||||
mountMetaRoutes(v1)
|
||||
mountAuthRoutes(v1, db)
|
||||
|
||||
// admin
|
||||
mountAdminRoutes(v1, db, jobs, authUser)
|
||||
|
||||
// user/org scoped
|
||||
mountMeRoutes(v1, db, authUser)
|
||||
mountOrgRoutes(v1, db, authUser, authOrg)
|
||||
|
||||
mountCredentialRoutes(v1, db, authOrg)
|
||||
mountSSHRoutes(v1, db, authOrg)
|
||||
mountServerRoutes(v1, db, authOrg)
|
||||
mountTaintRoutes(v1, db, authOrg)
|
||||
mountLabelRoutes(v1, db, authOrg)
|
||||
mountAnnotationRoutes(v1, db, authOrg)
|
||||
mountNodePoolRoutes(v1, db, authOrg)
|
||||
mountDNSRoutes(v1, db, authOrg)
|
||||
})
|
||||
})
|
||||
}
|
||||
16
internal/api/mount_auth_routes.go
Normal file
16
internal/api/mount_auth_routes.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/glueops/autoglue/internal/handlers"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func mountAuthRoutes(r chi.Router, db *gorm.DB) {
|
||||
r.Route("/auth", func(a chi.Router) {
|
||||
a.Post("/{provider}/start", handlers.AuthStart(db))
|
||||
a.Get("/{provider}/callback", handlers.AuthCallback(db))
|
||||
a.Post("/refresh", handlers.Refresh(db))
|
||||
a.Post("/logout", handlers.Logout(db))
|
||||
})
|
||||
}
|
||||
21
internal/api/mount_credential_routes.go
Normal file
21
internal/api/mount_credential_routes.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/glueops/autoglue/internal/handlers"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func mountCredentialRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||
r.Route("/credentials", func(c chi.Router) {
|
||||
c.Use(authOrg)
|
||||
c.Get("/", handlers.ListCredentials(db))
|
||||
c.Post("/", handlers.CreateCredential(db))
|
||||
c.Get("/{id}", handlers.GetCredential(db))
|
||||
c.Patch("/{id}", handlers.UpdateCredential(db))
|
||||
c.Delete("/{id}", handlers.DeleteCredential(db))
|
||||
c.Post("/{id}/reveal", handlers.RevealCredential(db))
|
||||
})
|
||||
}
|
||||
53
internal/api/mount_db_studio.go
Normal file
53
internal/api/mount_db_studio.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
pgapi "github.com/sosedoff/pgweb/pkg/api"
|
||||
pgclient "github.com/sosedoff/pgweb/pkg/client"
|
||||
pgcmd "github.com/sosedoff/pgweb/pkg/command"
|
||||
)
|
||||
|
||||
func PgwebHandler(dbURL, prefix string, readonly bool) (http.Handler, error) {
|
||||
// Normalize prefix for pgweb:
|
||||
// - no leading slash
|
||||
// - always trailing slash if not empty
|
||||
prefix = strings.Trim(prefix, "/")
|
||||
if prefix != "" {
|
||||
prefix = prefix + "/"
|
||||
}
|
||||
|
||||
pgcmd.Opts = pgcmd.Options{
|
||||
URL: dbURL,
|
||||
Prefix: prefix, // e.g. "db-studio/"
|
||||
ReadOnly: readonly,
|
||||
Sessions: false,
|
||||
LockSession: true,
|
||||
SkipOpen: true,
|
||||
}
|
||||
|
||||
cli, err := pgclient.NewFromUrl(dbURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if readonly {
|
||||
_ = cli.SetReadOnlyMode()
|
||||
}
|
||||
|
||||
if err := cli.Test(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pgapi.DbClient = cli
|
||||
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
g := gin.New()
|
||||
g.Use(gin.Recovery())
|
||||
|
||||
pgapi.SetupRoutes(g)
|
||||
pgapi.SetupMetrics(g)
|
||||
|
||||
return g, nil
|
||||
}
|
||||
26
internal/api/mount_dns_routes.go
Normal file
26
internal/api/mount_dns_routes.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/glueops/autoglue/internal/handlers"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func mountDNSRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||
r.Route("/dns", func(d chi.Router) {
|
||||
d.Use(authOrg)
|
||||
|
||||
d.Get("/domains", handlers.ListDomains(db))
|
||||
d.Post("/domains", handlers.CreateDomain(db))
|
||||
d.Get("/domains/{id}", handlers.GetDomain(db))
|
||||
d.Patch("/domains/{id}", handlers.UpdateDomain(db))
|
||||
d.Delete("/domains/{id}", handlers.DeleteDomain(db))
|
||||
|
||||
d.Get("/domains/{domain_id}/records", handlers.ListRecordSets(db))
|
||||
d.Post("/domains/{domain_id}/records", handlers.CreateRecordSet(db))
|
||||
d.Patch("/records/{id}", handlers.UpdateRecordSet(db))
|
||||
d.Delete("/records/{id}", handlers.DeleteRecordSet(db))
|
||||
})
|
||||
}
|
||||
20
internal/api/mount_label_routes.go
Normal file
20
internal/api/mount_label_routes.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/glueops/autoglue/internal/handlers"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func mountLabelRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||
r.Route("/labels", func(l chi.Router) {
|
||||
l.Use(authOrg)
|
||||
l.Get("/", handlers.ListLabels(db))
|
||||
l.Post("/", handlers.CreateLabel(db))
|
||||
l.Get("/{id}", handlers.GetLabel(db))
|
||||
l.Patch("/{id}", handlers.UpdateLabel(db))
|
||||
l.Delete("/{id}", handlers.DeleteLabel(db))
|
||||
})
|
||||
}
|
||||
22
internal/api/mount_me_routes.go
Normal file
22
internal/api/mount_me_routes.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/glueops/autoglue/internal/handlers"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func mountMeRoutes(r chi.Router, db *gorm.DB, authUser func(http.Handler) http.Handler) {
|
||||
r.Route("/me", func(me chi.Router) {
|
||||
me.Use(authUser)
|
||||
|
||||
me.Get("/", handlers.GetMe(db))
|
||||
me.Patch("/", handlers.UpdateMe(db))
|
||||
|
||||
me.Get("/api-keys", handlers.ListUserAPIKeys(db))
|
||||
me.Post("/api-keys", handlers.CreateUserAPIKey(db))
|
||||
me.Delete("/api-keys/{id}", handlers.DeleteUserAPIKey(db))
|
||||
})
|
||||
}
|
||||
13
internal/api/mount_meta_routes.go
Normal file
13
internal/api/mount_meta_routes.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/glueops/autoglue/internal/handlers"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func mountMetaRoutes(r chi.Router) {
|
||||
// Versioned JWKS for swagger
|
||||
r.Get("/.well-known/jwks.json", handlers.JWKSHandler)
|
||||
r.Get("/healthz", handlers.HealthCheck)
|
||||
r.Get("/version", handlers.Version)
|
||||
}
|
||||
40
internal/api/mount_node_pool_routes.go
Normal file
40
internal/api/mount_node_pool_routes.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/glueops/autoglue/internal/handlers"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func mountNodePoolRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||
r.Route("/node-pools", func(n chi.Router) {
|
||||
n.Use(authOrg)
|
||||
n.Get("/", handlers.ListNodePools(db))
|
||||
n.Post("/", handlers.CreateNodePool(db))
|
||||
n.Get("/{id}", handlers.GetNodePool(db))
|
||||
n.Patch("/{id}", handlers.UpdateNodePool(db))
|
||||
n.Delete("/{id}", handlers.DeleteNodePool(db))
|
||||
|
||||
// Servers
|
||||
n.Get("/{id}/servers", handlers.ListNodePoolServers(db))
|
||||
n.Post("/{id}/servers", handlers.AttachNodePoolServers(db))
|
||||
n.Delete("/{id}/servers/{serverId}", handlers.DetachNodePoolServer(db))
|
||||
|
||||
// Taints
|
||||
n.Get("/{id}/taints", handlers.ListNodePoolTaints(db))
|
||||
n.Post("/{id}/taints", handlers.AttachNodePoolTaints(db))
|
||||
n.Delete("/{id}/taints/{taintId}", handlers.DetachNodePoolTaint(db))
|
||||
|
||||
// Labels
|
||||
n.Get("/{id}/labels", handlers.ListNodePoolLabels(db))
|
||||
n.Post("/{id}/labels", handlers.AttachNodePoolLabels(db))
|
||||
n.Delete("/{id}/labels/{labelId}", handlers.DetachNodePoolLabel(db))
|
||||
|
||||
// Annotations
|
||||
n.Get("/{id}/annotations", handlers.ListNodePoolAnnotations(db))
|
||||
n.Post("/{id}/annotations", handlers.AttachNodePoolAnnotations(db))
|
||||
n.Delete("/{id}/annotations/{annotationId}", handlers.DetachNodePoolAnnotation(db))
|
||||
})
|
||||
}
|
||||
35
internal/api/mount_org_routes.go
Normal file
35
internal/api/mount_org_routes.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/glueops/autoglue/internal/handlers"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func mountOrgRoutes(r chi.Router, db *gorm.DB, authUser, authOrg func(http.Handler) http.Handler) {
|
||||
r.Route("/orgs", func(o chi.Router) {
|
||||
o.Use(authUser)
|
||||
o.Get("/", handlers.ListMyOrgs(db))
|
||||
o.Post("/", handlers.CreateOrg(db))
|
||||
|
||||
o.Group(func(og chi.Router) {
|
||||
og.Use(authOrg)
|
||||
|
||||
og.Get("/{id}", handlers.GetOrg(db))
|
||||
og.Patch("/{id}", handlers.UpdateOrg(db))
|
||||
og.Delete("/{id}", handlers.DeleteOrg(db))
|
||||
|
||||
// members
|
||||
og.Get("/{id}/members", handlers.ListMembers(db))
|
||||
og.Post("/{id}/members", handlers.AddOrUpdateMember(db))
|
||||
og.Delete("/{id}/members/{user_id}", handlers.RemoveMember(db))
|
||||
|
||||
// org-scoped key/secret pair
|
||||
og.Get("/{id}/api-keys", handlers.ListOrgKeys(db))
|
||||
og.Post("/{id}/api-keys", handlers.CreateOrgKey(db))
|
||||
og.Delete("/{id}/api-keys/{key_id}", handlers.DeleteOrgKey(db))
|
||||
})
|
||||
})
|
||||
}
|
||||
24
internal/api/mount_pprof_routes.go
Normal file
24
internal/api/mount_pprof_routes.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
httpPprof "net/http/pprof"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func mountPprofRoutes(r chi.Router) {
|
||||
r.Route("/debug/pprof", func(pr chi.Router) {
|
||||
pr.Get("/", httpPprof.Index)
|
||||
pr.Get("/cmdline", httpPprof.Cmdline)
|
||||
pr.Get("/profile", httpPprof.Profile)
|
||||
pr.Get("/symbol", httpPprof.Symbol)
|
||||
pr.Get("/trace", httpPprof.Trace)
|
||||
|
||||
pr.Handle("/allocs", httpPprof.Handler("allocs"))
|
||||
pr.Handle("/block", httpPprof.Handler("block"))
|
||||
pr.Handle("/goroutine", httpPprof.Handler("goroutine"))
|
||||
pr.Handle("/heap", httpPprof.Handler("heap"))
|
||||
pr.Handle("/mutex", httpPprof.Handler("mutex"))
|
||||
pr.Handle("/threadcreate", httpPprof.Handler("threadcreate"))
|
||||
})
|
||||
}
|
||||
21
internal/api/mount_server_routes.go
Normal file
21
internal/api/mount_server_routes.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/glueops/autoglue/internal/handlers"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func mountServerRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||
r.Route("/servers", func(s chi.Router) {
|
||||
s.Use(authOrg)
|
||||
s.Get("/", handlers.ListServers(db))
|
||||
s.Post("/", handlers.CreateServer(db))
|
||||
s.Get("/{id}", handlers.GetServer(db))
|
||||
s.Patch("/{id}", handlers.UpdateServer(db))
|
||||
s.Delete("/{id}", handlers.DeleteServer(db))
|
||||
s.Post("/{id}/reset-hostkey", handlers.ResetServerHostKey(db))
|
||||
})
|
||||
}
|
||||
20
internal/api/mount_ssh_routes.go
Normal file
20
internal/api/mount_ssh_routes.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/glueops/autoglue/internal/handlers"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func mountSSHRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||
r.Route("/ssh", func(s chi.Router) {
|
||||
s.Use(authOrg)
|
||||
s.Get("/", handlers.ListPublicSshKeys(db))
|
||||
s.Post("/", handlers.CreateSSHKey(db))
|
||||
s.Get("/{id}", handlers.GetSSHKey(db))
|
||||
s.Delete("/{id}", handlers.DeleteSSHKey(db))
|
||||
s.Get("/{id}/download", handlers.DownloadSSHKey(db))
|
||||
})
|
||||
}
|
||||
15
internal/api/mount_swagger_routes.go
Normal file
15
internal/api/mount_swagger_routes.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/glueops/autoglue/docs"
|
||||
"github.com/go-chi/chi/v5"
|
||||
httpSwagger "github.com/swaggo/http-swagger/v2"
|
||||
)
|
||||
|
||||
func mountSwaggerRoutes(r chi.Router) {
|
||||
r.Get("/swagger/*", httpSwagger.Handler(
|
||||
httpSwagger.URL("swagger.json"),
|
||||
))
|
||||
r.Get("/swagger/swagger.json", serveSwaggerFromEmbed(docs.SwaggerJSON, "application/json"))
|
||||
r.Get("/swagger/swagger.yaml", serveSwaggerFromEmbed(docs.SwaggerYAML, "application/x-yaml"))
|
||||
}
|
||||
20
internal/api/mount_taint_routes.go
Normal file
20
internal/api/mount_taint_routes.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/glueops/autoglue/internal/handlers"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func mountTaintRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||
r.Route("/taints", func(t chi.Router) {
|
||||
t.Use(authOrg)
|
||||
t.Get("/", handlers.ListTaints(db))
|
||||
t.Post("/", handlers.CreateTaint(db))
|
||||
t.Get("/{id}", handlers.GetTaint(db))
|
||||
t.Patch("/{id}", handlers.UpdateTaint(db))
|
||||
t.Delete("/{id}", handlers.DeleteTaint(db))
|
||||
})
|
||||
}
|
||||
@@ -36,7 +36,7 @@ func SecurityHeaders(next http.Handler) http.Handler {
|
||||
// Google font files
|
||||
"font-src 'self' data: https://fonts.gstatic.com",
|
||||
// HMR connections
|
||||
"connect-src 'self' http://localhost:5173 ws://localhost:5173 ws://localhost:8080",
|
||||
"connect-src 'self' http://localhost:5173 ws://localhost:5173 ws://localhost:8080 https://api.github.com",
|
||||
"frame-ancestors 'none'",
|
||||
}, "; "))
|
||||
} else {
|
||||
@@ -53,7 +53,7 @@ func SecurityHeaders(next http.Handler) http.Handler {
|
||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
||||
"img-src 'self' data: blob:",
|
||||
"font-src 'self' data: https://fonts.gstatic.com",
|
||||
"connect-src 'self'",
|
||||
"connect-src 'self' ws://localhost:8080 https://api.github.com",
|
||||
"frame-ancestors 'none'",
|
||||
}, "; "))
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ package api
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
httpPprof "net/http/pprof"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/glueops/autoglue/docs"
|
||||
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||
"github.com/glueops/autoglue/internal/bg"
|
||||
"github.com/glueops/autoglue/internal/config"
|
||||
@@ -23,11 +22,9 @@ import (
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
httpSwagger "github.com/swaggo/http-swagger/v2"
|
||||
)
|
||||
|
||||
func NewRouter(db *gorm.DB, jobs *bg.Jobs) http.Handler {
|
||||
func NewRouter(db *gorm.DB, jobs *bg.Jobs, studio http.Handler) http.Handler {
|
||||
zerolog.TimeFieldFormat = time.RFC3339
|
||||
|
||||
l := log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: "15:04:05"})
|
||||
@@ -59,176 +56,44 @@ func NewRouter(db *gorm.DB, jobs *bg.Jobs) http.Handler {
|
||||
MaxAge: 600,
|
||||
}))
|
||||
|
||||
r.Use(middleware.AllowContentType("application/json"))
|
||||
r.Use(middleware.Maybe(
|
||||
middleware.AllowContentType("application/json"),
|
||||
func(r *http.Request) bool {
|
||||
// return true => run AllowContentType
|
||||
// return false => skip AllowContentType for this request
|
||||
return !strings.HasPrefix(r.URL.Path, "/db-studio")
|
||||
}))
|
||||
//r.Use(middleware.AllowContentType("application/json"))
|
||||
|
||||
// Unversioned, non-auth endpoints
|
||||
r.Get("/.well-known/jwks.json", handlers.JWKSHandler)
|
||||
r.Route("/api", func(api chi.Router) {
|
||||
api.Route("/v1", func(v1 chi.Router) {
|
||||
|
||||
// Versioned API
|
||||
mountAPIRoutes(r, db, jobs)
|
||||
|
||||
// Optional DB studio
|
||||
if studio != nil {
|
||||
r.Group(func(gr chi.Router) {
|
||||
authUser := httpmiddleware.AuthMiddleware(db, false)
|
||||
authOrg := httpmiddleware.AuthMiddleware(db, true)
|
||||
|
||||
// Also serving a versioned JWKS for swagger, which uses BasePath
|
||||
v1.Get("/.well-known/jwks.json", handlers.JWKSHandler)
|
||||
|
||||
v1.Get("/healthz", handlers.HealthCheck)
|
||||
|
||||
v1.Route("/auth", func(a chi.Router) {
|
||||
a.Post("/{provider}/start", handlers.AuthStart(db))
|
||||
a.Get("/{provider}/callback", handlers.AuthCallback(db))
|
||||
a.Post("/refresh", handlers.Refresh(db))
|
||||
a.Post("/logout", handlers.Logout(db))
|
||||
})
|
||||
|
||||
v1.Route("/admin", func(admin chi.Router) {
|
||||
admin.Route("/archer", func(archer chi.Router) {
|
||||
archer.Use(authUser)
|
||||
archer.Use(httpmiddleware.RequirePlatformAdmin())
|
||||
|
||||
archer.Get("/jobs", handlers.AdminListArcherJobs(db))
|
||||
archer.Post("/jobs", handlers.AdminEnqueueArcherJob(db, jobs))
|
||||
archer.Post("/jobs/{id}/retry", handlers.AdminRetryArcherJob(db))
|
||||
archer.Post("/jobs/{id}/cancel", handlers.AdminCancelArcherJob(db))
|
||||
archer.Get("/queues", handlers.AdminListArcherQueues(db))
|
||||
})
|
||||
})
|
||||
|
||||
v1.Route("/me", func(me chi.Router) {
|
||||
me.Use(authUser)
|
||||
|
||||
me.Get("/", handlers.GetMe(db))
|
||||
me.Patch("/", handlers.UpdateMe(db))
|
||||
|
||||
me.Get("/api-keys", handlers.ListUserAPIKeys(db))
|
||||
me.Post("/api-keys", handlers.CreateUserAPIKey(db))
|
||||
me.Delete("/api-keys/{id}", handlers.DeleteUserAPIKey(db))
|
||||
})
|
||||
|
||||
v1.Route("/orgs", func(o chi.Router) {
|
||||
o.Use(authUser)
|
||||
o.Get("/", handlers.ListMyOrgs(db))
|
||||
o.Post("/", handlers.CreateOrg(db))
|
||||
|
||||
o.Group(func(og chi.Router) {
|
||||
og.Use(authOrg)
|
||||
og.Get("/{id}", handlers.GetOrg(db))
|
||||
og.Patch("/{id}", handlers.UpdateOrg(db))
|
||||
og.Delete("/{id}", handlers.DeleteOrg(db))
|
||||
|
||||
// members
|
||||
og.Get("/{id}/members", handlers.ListMembers(db))
|
||||
og.Post("/{id}/members", handlers.AddOrUpdateMember(db))
|
||||
og.Delete("/{id}/members/{user_id}", handlers.RemoveMember(db))
|
||||
|
||||
// org-scoped key/secret pair
|
||||
og.Get("/{id}/api-keys", handlers.ListOrgKeys(db))
|
||||
og.Post("/{id}/api-keys", handlers.CreateOrgKey(db))
|
||||
og.Delete("/{id}/api-keys/{key_id}", handlers.DeleteOrgKey(db))
|
||||
})
|
||||
})
|
||||
|
||||
v1.Route("/ssh", func(s chi.Router) {
|
||||
s.Use(authOrg)
|
||||
s.Get("/", handlers.ListPublicSshKeys(db))
|
||||
s.Post("/", handlers.CreateSSHKey(db))
|
||||
s.Get("/{id}", handlers.GetSSHKey(db))
|
||||
s.Delete("/{id}", handlers.DeleteSSHKey(db))
|
||||
s.Get("/{id}/download", handlers.DownloadSSHKey(db))
|
||||
})
|
||||
|
||||
v1.Route("/servers", func(s chi.Router) {
|
||||
s.Use(authOrg)
|
||||
s.Get("/", handlers.ListServers(db))
|
||||
s.Post("/", handlers.CreateServer(db))
|
||||
s.Get("/{id}", handlers.GetServer(db))
|
||||
s.Patch("/{id}", handlers.UpdateServer(db))
|
||||
s.Delete("/{id}", handlers.DeleteServer(db))
|
||||
})
|
||||
|
||||
v1.Route("/taints", func(s chi.Router) {
|
||||
s.Use(authOrg)
|
||||
s.Get("/", handlers.ListTaints(db))
|
||||
s.Post("/", handlers.CreateTaint(db))
|
||||
s.Get("/{id}", handlers.GetTaint(db))
|
||||
s.Patch("/{id}", handlers.UpdateTaint(db))
|
||||
s.Delete("/{id}", handlers.DeleteTaint(db))
|
||||
})
|
||||
|
||||
v1.Route("/labels", func(l chi.Router) {
|
||||
l.Use(authOrg)
|
||||
l.Get("/", handlers.ListLabels(db))
|
||||
l.Post("/", handlers.CreateLabel(db))
|
||||
l.Get("/{id}", handlers.GetLabel(db))
|
||||
l.Patch("/{id}", handlers.UpdateLabel(db))
|
||||
l.Delete("/{id}", handlers.DeleteLabel(db))
|
||||
})
|
||||
|
||||
v1.Route("/annotations", func(a chi.Router) {
|
||||
a.Use(authOrg)
|
||||
a.Get("/", handlers.ListAnnotations(db))
|
||||
a.Post("/", handlers.CreateAnnotation(db))
|
||||
a.Get("/{id}", handlers.GetAnnotation(db))
|
||||
a.Patch("/{id}", handlers.UpdateAnnotation(db))
|
||||
a.Delete("/{id}", handlers.DeleteAnnotation(db))
|
||||
})
|
||||
|
||||
v1.Route("/node-pools", func(n chi.Router) {
|
||||
n.Use(authOrg)
|
||||
n.Get("/", handlers.ListNodePools(db))
|
||||
n.Post("/", handlers.CreateNodePool(db))
|
||||
n.Get("/{id}", handlers.GetNodePool(db))
|
||||
n.Patch("/{id}", handlers.UpdateNodePool(db))
|
||||
n.Delete("/{id}", handlers.DeleteNodePool(db))
|
||||
|
||||
// Servers
|
||||
n.Get("/{id}/servers", handlers.ListNodePoolServers(db))
|
||||
n.Post("/{id}/servers", handlers.AttachNodePoolServers(db))
|
||||
n.Delete("/{id}/servers/{serverId}", handlers.DetachNodePoolServer(db))
|
||||
|
||||
// Taints
|
||||
n.Get("/{id}/taints", handlers.ListNodePoolTaints(db))
|
||||
n.Post("/{id}/taints", handlers.AttachNodePoolTaints(db))
|
||||
n.Delete("/{id}/taints/{taintId}", handlers.DetachNodePoolTaint(db))
|
||||
|
||||
// Labels
|
||||
n.Get("/{id}/labels", handlers.ListNodePoolLabels(db))
|
||||
n.Post("/{id}/labels", handlers.AttachNodePoolLabels(db))
|
||||
n.Delete("/{id}/labels/{labelId}", handlers.DetachNodePoolLabel(db))
|
||||
|
||||
// Annotations
|
||||
n.Get("/{id}/annotations", handlers.ListNodePoolAnnotations(db))
|
||||
n.Post("/{id}/annotations", handlers.AttachNodePoolAnnotations(db))
|
||||
n.Delete("/{id}/annotations/{annotationId}", handlers.DetachNodePoolAnnotation(db))
|
||||
})
|
||||
adminOnly := httpmiddleware.RequirePlatformAdmin()
|
||||
gr.Use(authUser, adminOnly)
|
||||
gr.Mount("/db-studio", studio)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// pprof
|
||||
if config.IsDebug() {
|
||||
r.Route("/debug/pprof", func(pr chi.Router) {
|
||||
pr.Get("/", httpPprof.Index)
|
||||
pr.Get("/cmdline", httpPprof.Cmdline)
|
||||
pr.Get("/profile", httpPprof.Profile)
|
||||
pr.Get("/symbol", httpPprof.Symbol)
|
||||
pr.Get("/trace", httpPprof.Trace)
|
||||
|
||||
pr.Handle("/allocs", httpPprof.Handler("allocs"))
|
||||
pr.Handle("/block", httpPprof.Handler("block"))
|
||||
pr.Handle("/goroutine", httpPprof.Handler("goroutine"))
|
||||
pr.Handle("/heap", httpPprof.Handler("heap"))
|
||||
pr.Handle("/mutex", httpPprof.Handler("mutex"))
|
||||
pr.Handle("/threadcreate", httpPprof.Handler("threadcreate"))
|
||||
})
|
||||
mountPprofRoutes(r)
|
||||
}
|
||||
|
||||
// Swagger
|
||||
if config.IsSwaggerEnabled() {
|
||||
r.Get("/swagger/*", httpSwagger.Handler(
|
||||
httpSwagger.URL("swagger.json"),
|
||||
))
|
||||
r.Get("/swagger/swagger.json", serveSwaggerFromEmbed(docs.SwaggerJSON, "application/json"))
|
||||
r.Get("/swagger/swagger.yaml", serveSwaggerFromEmbed(docs.SwaggerYAML, "application/x-yaml"))
|
||||
mountSwaggerRoutes(r)
|
||||
}
|
||||
|
||||
// UI dev/prod
|
||||
if config.IsUIDev() {
|
||||
fmt.Println("Running in development mode")
|
||||
// Dev: isolate proxy from chi middlewares so WS upgrade can hijack.
|
||||
proxy, err := web.DevProxy("http://localhost:5173")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("dev proxy init failed")
|
||||
@@ -236,22 +101,20 @@ func NewRouter(db *gorm.DB, jobs *bg.Jobs) http.Handler {
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
// Send API/Swagger/pprof to chi
|
||||
mux.Handle("/api/", r)
|
||||
mux.Handle("/api", r)
|
||||
mux.Handle("/swagger/", r)
|
||||
mux.Handle("/db-studio/", r)
|
||||
mux.Handle("/debug/pprof/", r)
|
||||
// Everything else (/, /brand-preview, assets) → proxy (no middlewares)
|
||||
mux.Handle("/", proxy)
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
fmt.Println("Running in production mode")
|
||||
if h, err := web.SPAHandler(); err == nil {
|
||||
r.NotFound(h.ServeHTTP)
|
||||
} else {
|
||||
fmt.Println("Running in production mode")
|
||||
if h, err := web.SPAHandler(); err == nil {
|
||||
r.NotFound(h.ServeHTTP)
|
||||
} else {
|
||||
log.Error().Err(err).Msg("spa handler init failed")
|
||||
}
|
||||
log.Error().Err(err).Msg("spa handler init failed")
|
||||
}
|
||||
|
||||
return r
|
||||
|
||||
@@ -40,6 +40,9 @@ func NewRuntime() *Runtime {
|
||||
&models.Annotation{},
|
||||
&models.NodePool{},
|
||||
&models.Cluster{},
|
||||
&models.Credential{},
|
||||
&models.Domain{},
|
||||
&models.RecordSet{},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
||||
264
internal/bg/backup_s3.go
Normal file
264
internal/bg/backup_s3.go
Normal file
@@ -0,0 +1,264 @@
|
||||
package bg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/dyaksa/archer"
|
||||
"github.com/dyaksa/archer/job"
|
||||
"github.com/glueops/autoglue/internal/config"
|
||||
"github.com/glueops/autoglue/internal/models"
|
||||
"github.com/glueops/autoglue/internal/utils"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DbBackupArgs struct {
|
||||
IntervalS int `json:"interval_seconds,omitempty"`
|
||||
}
|
||||
|
||||
type s3Scope struct {
|
||||
Service string `json:"service"`
|
||||
Region string `json:"region"`
|
||||
}
|
||||
|
||||
type encAWS struct {
|
||||
AccessKeyID string `json:"access_key_id"`
|
||||
SecretAccessKey string `json:"secret_access_key"`
|
||||
}
|
||||
|
||||
func DbBackupWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
||||
return func(ctx context.Context, j job.Job) (any, error) {
|
||||
args := DbBackupArgs{IntervalS: 3600}
|
||||
_ = j.ParseArguments(&args)
|
||||
|
||||
if args.IntervalS <= 0 {
|
||||
args.IntervalS = 3600
|
||||
}
|
||||
|
||||
if err := DbBackup(ctx, db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
queue := j.QueueName
|
||||
if strings.TrimSpace(queue) == "" {
|
||||
queue = "db_backup_s3"
|
||||
}
|
||||
|
||||
next := time.Now().Add(time.Duration(args.IntervalS) * time.Second)
|
||||
|
||||
payload := DbBackupArgs{}
|
||||
|
||||
opts := []archer.FnOptions{
|
||||
archer.WithScheduleTime(next),
|
||||
archer.WithMaxRetries(1),
|
||||
}
|
||||
|
||||
if _, err := jobs.Enqueue(ctx, uuid.NewString(), queue, payload, opts...); err != nil {
|
||||
log.Error().Err(err).Str("queue", queue).Time("next", next).Msg("failed to enqueue next db backup")
|
||||
} else {
|
||||
log.Info().Str("queue", queue).Time("next", next).Msg("scheduled next db backup")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func DbBackup(ctx context.Context, db *gorm.DB) error {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
|
||||
cred, sc, err := loadS3Credential(ctx, db)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load credential: %w", err)
|
||||
}
|
||||
|
||||
ak, sk, err := decryptAwsAccessKeys(ctx, db, cred)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decrypt aws keys: %w", err)
|
||||
}
|
||||
|
||||
region := sc.Region
|
||||
|
||||
if strings.TrimSpace(region) == "" {
|
||||
region = cred.Region
|
||||
if strings.TrimSpace(region) == "" {
|
||||
region = "us-west-1"
|
||||
}
|
||||
}
|
||||
|
||||
bucket := strings.ToLower(fmt.Sprintf("%s-autoglue-backups-%s", cred.OrganizationID, region))
|
||||
|
||||
s3cli, err := makeS3Client(ctx, ak, sk, region)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ensureBucket(ctx, s3cli, bucket, region); err != nil {
|
||||
return fmt.Errorf("ensure bucket: %w", err)
|
||||
}
|
||||
|
||||
tmpDir := os.TempDir()
|
||||
now := time.Now().UTC()
|
||||
key := fmt.Sprintf("%04d/%02d/%02d/backup-%02d.sql", now.Year(), now.Month(), now.Day(), now.Hour())
|
||||
outPath := filepath.Join(tmpDir, "autoglue-backup-"+now.Format("20060102T150405Z")+".sql")
|
||||
|
||||
if err := runPgDump(ctx, cfg.DbURL, outPath); err != nil {
|
||||
return fmt.Errorf("pg_dump: %w", err)
|
||||
}
|
||||
defer os.Remove(outPath)
|
||||
|
||||
if err := uploadFileToS3(ctx, s3cli, bucket, key, outPath); err != nil {
|
||||
return fmt.Errorf("s3 upload: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Str("bucket", bucket).Str("key", key).Msg("backup uploaded")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Helpers
|
||||
|
||||
func loadS3Credential(ctx context.Context, db *gorm.DB) (models.Credential, s3Scope, error) {
|
||||
var c models.Credential
|
||||
err := db.
|
||||
WithContext(ctx).
|
||||
Where("provider = ? AND kind = ? AND scope_kind = ?", "aws", "aws_access_key", "service").
|
||||
Where("scope ->> 'service' = ?", "s3").
|
||||
Order("created_at DESC").
|
||||
First(&c).Error
|
||||
if err != nil {
|
||||
return models.Credential{}, s3Scope{}, fmt.Errorf("load credential: %w", err)
|
||||
}
|
||||
|
||||
var sc s3Scope
|
||||
_ = json.Unmarshal(c.Scope, &sc)
|
||||
return c, sc, nil
|
||||
}
|
||||
|
||||
func decryptAwsAccessKeys(ctx context.Context, db *gorm.DB, c models.Credential) (string, string, error) {
|
||||
plain, err := utils.DecryptForOrg(c.OrganizationID, c.EncryptedData, c.IV, c.Tag, db)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
var payload encAWS
|
||||
if err := json.Unmarshal([]byte(plain), &payload); err != nil {
|
||||
return "", "", fmt.Errorf("parse decrypted payload: %w", err)
|
||||
}
|
||||
|
||||
if payload.AccessKeyID == "" || payload.SecretAccessKey == "" {
|
||||
return "", "", errors.New("decrypted payload missing keys")
|
||||
}
|
||||
return payload.AccessKeyID, payload.SecretAccessKey, nil
|
||||
}
|
||||
|
||||
func makeS3Client(ctx context.Context, accessKey, secret, region string) (*s3.Client, error) {
|
||||
staticCredentialsProvider := credentials.NewStaticCredentialsProvider(accessKey, secret, "")
|
||||
cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithCredentialsProvider(staticCredentialsProvider), awsconfig.WithRegion(region))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("aws config: %w", err)
|
||||
}
|
||||
return s3.NewFromConfig(cfg), nil
|
||||
}
|
||||
|
||||
func ensureBucket(ctx context.Context, s3cli *s3.Client, bucket, region string) error {
|
||||
_, err := s3cli.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: aws.String(bucket)})
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if out, err := s3cli.GetBucketLocation(ctx, &s3.GetBucketLocationInput{Bucket: aws.String(bucket)}); err == nil {
|
||||
existing := string(out.LocationConstraint)
|
||||
if existing == "" {
|
||||
existing = "us-east-1"
|
||||
}
|
||||
if existing != region {
|
||||
return fmt.Errorf("bucket %q already exists in region %q (requested %q)", bucket, existing, region)
|
||||
}
|
||||
}
|
||||
|
||||
// Create; LocationConstraint except us-east-1
|
||||
in := &s3.CreateBucketInput{Bucket: aws.String(bucket)}
|
||||
if region != "us-east-1" {
|
||||
in.CreateBucketConfiguration = &s3types.CreateBucketConfiguration{
|
||||
LocationConstraint: s3types.BucketLocationConstraint(region),
|
||||
}
|
||||
}
|
||||
if _, err := s3cli.CreateBucket(ctx, in); err != nil {
|
||||
return fmt.Errorf("create bucket: %w", err)
|
||||
}
|
||||
|
||||
// default SSE (best-effort)
|
||||
_, _ = s3cli.PutBucketEncryption(ctx, &s3.PutBucketEncryptionInput{
|
||||
Bucket: aws.String(bucket),
|
||||
ServerSideEncryptionConfiguration: &s3types.ServerSideEncryptionConfiguration{
|
||||
Rules: []s3types.ServerSideEncryptionRule{
|
||||
{ApplyServerSideEncryptionByDefault: &s3types.ServerSideEncryptionByDefault{
|
||||
SSEAlgorithm: s3types.ServerSideEncryptionAes256,
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func runPgDump(ctx context.Context, dbURL, outPath string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--no-owner",
|
||||
"--no-privileges",
|
||||
"--format=plain",
|
||||
"--file", outPath,
|
||||
dbURL,
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "pg_dump", args...)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("pg_dump failed: %v | %s", err, stderr.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func uploadFileToS3(ctx context.Context, s3cli *s3.Client, bucket, key, path string) error {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
info, _ := f.Stat()
|
||||
_, err = s3cli.PutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(key),
|
||||
Body: f,
|
||||
ContentLength: aws.Int64(info.Size()),
|
||||
ContentType: aws.String(mime.TypeByExtension(filepath.Ext(path))),
|
||||
ServerSideEncryption: s3types.ServerSideEncryptionAes256,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -2,8 +2,8 @@ package bg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -13,13 +13,16 @@ import (
|
||||
"github.com/glueops/autoglue/internal/models"
|
||||
"github.com/glueops/autoglue/internal/utils"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ----- Public types -----
|
||||
|
||||
type BastionBootstrapArgs struct{}
|
||||
type BastionBootstrapArgs struct {
|
||||
IntervalS int `json:"interval_seconds,omitempty"`
|
||||
}
|
||||
|
||||
type BastionBootstrapFailure struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
@@ -39,11 +42,17 @@ type BastionBootstrapResult struct {
|
||||
|
||||
// ----- Worker -----
|
||||
|
||||
func BastionBootstrapWorker(db *gorm.DB) archer.WorkerFn {
|
||||
func BastionBootstrapWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
||||
return func(ctx context.Context, j job.Job) (any, error) {
|
||||
args := BastionBootstrapArgs{IntervalS: 120}
|
||||
jobID := j.ID
|
||||
start := time.Now()
|
||||
|
||||
_ = j.ParseArguments(&args)
|
||||
if args.IntervalS <= 0 {
|
||||
args.IntervalS = 120
|
||||
}
|
||||
|
||||
var servers []models.Server
|
||||
if err := db.
|
||||
Preload("SshKey").
|
||||
@@ -105,7 +114,7 @@ func BastionBootstrapWorker(db *gorm.DB) archer.WorkerFn {
|
||||
// 4) SSH + install docker
|
||||
host := net.JoinHostPort(*s.PublicIPAddress, "22")
|
||||
runCtx, cancel := context.WithTimeout(ctx, perHostTimeout)
|
||||
out, err := sshInstallDockerWithOutput(runCtx, host, s.SSHUser, []byte(privKey))
|
||||
out, err := sshInstallDockerWithOutput(runCtx, db, s, host, s.SSHUser, []byte(privKey))
|
||||
cancel()
|
||||
|
||||
if err != nil {
|
||||
@@ -147,9 +156,17 @@ func BastionBootstrapWorker(db *gorm.DB) archer.WorkerFn {
|
||||
Failures: failures,
|
||||
}
|
||||
|
||||
// log.Printf("[bastion] level=INFO job=%s step=finish processed=%d ready=%d failed=%d elapsed_ms=%d",
|
||||
// jobID, proc, ok, fail, res.ElapsedMs)
|
||||
log.Debug().Int("processed", proc).Int("ready", ok).Int("failed", fail).Msg("[bastion] reconcile tick ok")
|
||||
|
||||
next := time.Now().Add(time.Duration(args.IntervalS) * time.Second)
|
||||
_, _ = jobs.Enqueue(
|
||||
ctx,
|
||||
uuid.NewString(),
|
||||
"bootstrap_bastion",
|
||||
args,
|
||||
archer.WithScheduleTime(next),
|
||||
archer.WithMaxRetries(1),
|
||||
)
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
@@ -187,16 +204,24 @@ func logHostInfo(jobID string, s *models.Server, step, msg string, kv ...any) {
|
||||
// ----- SSH & command execution -----
|
||||
|
||||
// returns combined stdout/stderr so caller can log it on error
|
||||
func sshInstallDockerWithOutput(ctx context.Context, host, user string, privateKeyPEM []byte) (string, error) {
|
||||
func sshInstallDockerWithOutput(
|
||||
ctx context.Context,
|
||||
db *gorm.DB,
|
||||
s *models.Server,
|
||||
host, user string,
|
||||
privateKeyPEM []byte,
|
||||
) (string, error) {
|
||||
signer, err := ssh.ParsePrivateKey(privateKeyPEM)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse private key: %w", err)
|
||||
}
|
||||
|
||||
hkcb := makeDBHostKeyCallback(db, s)
|
||||
|
||||
config := &ssh.ClientConfig{
|
||||
User: user,
|
||||
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // TODO: known_hosts verification
|
||||
HostKeyCallback: hkcb,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
@@ -494,3 +519,38 @@ func wrapSSHError(err error, output string) error {
|
||||
func sshEscape(s string) string {
|
||||
return fmt.Sprintf("%q", s)
|
||||
}
|
||||
|
||||
// makeDBHostKeyCallback returns a HostKeyCallback bound to a specific server row.
|
||||
// TOFU semantics:
|
||||
// - If s.SSHHostKey is empty: store the current key in DB and accept.
|
||||
// - If s.SSHHostKey is set: require exact match, else error (possible MITM/reinstall).
|
||||
func makeDBHostKeyCallback(db *gorm.DB, s *models.Server) ssh.HostKeyCallback {
|
||||
return func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
algo := key.Type()
|
||||
enc := base64.StdEncoding.EncodeToString(key.Marshal())
|
||||
|
||||
// First-time connect: persist key (TOFU).
|
||||
if s.SSHHostKey == "" {
|
||||
if err := db.Model(&models.Server{}).
|
||||
Where("id = ? AND (ssh_host_key IS NULL or ssh_host_key = '')", s.ID).
|
||||
Updates(map[string]any{
|
||||
"ssh_host_key": enc,
|
||||
"ssh_host_key_algo": algo,
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("store new host key for %s (%s): %w", hostname, s.ID, err)
|
||||
}
|
||||
|
||||
s.SSHHostKey = enc
|
||||
s.SSHHostKeyAlgo = algo
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.SSHHostKeyAlgo != algo || s.SSHHostKey != enc {
|
||||
return fmt.Errorf(
|
||||
"host key mismatch for %s (server_id=%s, stored=%s/%s, got=%s/%s) - POSSIBLE MITM or host reinstalled",
|
||||
hostname, s.ID, s.SSHHostKeyAlgo, s.SSHHostKey, algo, enc,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ func NewJobs(gdb *gorm.DB, dbUrl string) (*Jobs, error) {
|
||||
archer.WithSetTableName("jobs"), // <- ensure correct table
|
||||
archer.WithSleepInterval(1*time.Second), // fast poll while debugging
|
||||
archer.WithErrHandler(func(err error) { // bubble up worker SQL errors
|
||||
log.Printf("[archer] ERROR: %v", err)
|
||||
log.Error().Err(err).Msg("[archer] worker error")
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -75,7 +75,7 @@ func NewJobs(gdb *gorm.DB, dbUrl string) (*Jobs, error) {
|
||||
|
||||
c.Register(
|
||||
"bootstrap_bastion",
|
||||
BastionBootstrapWorker(gdb),
|
||||
BastionBootstrapWorker(gdb, jobs),
|
||||
archer.WithInstances(instances),
|
||||
archer.WithTimeout(time.Duration(timeoutSec)*time.Second),
|
||||
)
|
||||
@@ -94,6 +94,19 @@ func NewJobs(gdb *gorm.DB, dbUrl string) (*Jobs, error) {
|
||||
archer.WithTimeout(5*time.Minute),
|
||||
)
|
||||
|
||||
c.Register(
|
||||
"db_backup_s3",
|
||||
DbBackupWorker(gdb, jobs),
|
||||
archer.WithInstances(1),
|
||||
archer.WithTimeout(15*time.Minute),
|
||||
)
|
||||
|
||||
c.Register(
|
||||
"dns_reconcile",
|
||||
DNSReconsileWorker(gdb, jobs),
|
||||
archer.WithInstances(1),
|
||||
archer.WithTimeout(2*time.Minute),
|
||||
)
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
|
||||
597
internal/bg/dns.go
Normal file
597
internal/bg/dns.go
Normal file
@@ -0,0 +1,597 @@
|
||||
package bg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dyaksa/archer"
|
||||
"github.com/dyaksa/archer/job"
|
||||
"github.com/glueops/autoglue/internal/handlers/dto"
|
||||
"github.com/glueops/autoglue/internal/models"
|
||||
"github.com/glueops/autoglue/internal/utils"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
r53 "github.com/aws/aws-sdk-go-v2/service/route53"
|
||||
r53types "github.com/aws/aws-sdk-go-v2/service/route53/types"
|
||||
)
|
||||
|
||||
/************* args & small DTOs *************/
|
||||
|
||||
type DNSReconcileArgs struct {
|
||||
MaxDomains int `json:"max_domains,omitempty"`
|
||||
MaxRecords int `json:"max_records,omitempty"`
|
||||
IntervalS int `json:"interval_seconds,omitempty"`
|
||||
}
|
||||
|
||||
// TXT marker content (compact)
|
||||
type ownershipMarker struct {
|
||||
Ver string `json:"v"` // "ag1"
|
||||
Org string `json:"org"` // org UUID
|
||||
Rec string `json:"rec"` // record UUID
|
||||
Fp string `json:"fp"` // short fp (first 16 of sha256)
|
||||
}
|
||||
|
||||
// ExternalDNS poison owner id – MUST NOT match any real external-dns --txt-owner-id
|
||||
const externalDNSPoisonOwner = "autoglue-lock"
|
||||
|
||||
// ExternalDNS poison content – fake owner so real external-dns skips it.
|
||||
const externalDNSPoisonValue = "heritage=external-dns,external-dns/owner=" + externalDNSPoisonOwner + ",external-dns/resource=manual/autoglue"
|
||||
|
||||
/************* entrypoint worker *************/
|
||||
|
||||
func DNSReconsileWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
||||
return func(ctx context.Context, j job.Job) (any, error) {
|
||||
args := DNSReconcileArgs{MaxDomains: 25, MaxRecords: 100, IntervalS: 30}
|
||||
_ = j.ParseArguments(&args)
|
||||
|
||||
if args.MaxDomains <= 0 {
|
||||
args.MaxDomains = 25
|
||||
}
|
||||
if args.MaxRecords <= 0 {
|
||||
args.MaxRecords = 100
|
||||
}
|
||||
if args.IntervalS <= 0 {
|
||||
args.IntervalS = 30
|
||||
}
|
||||
|
||||
processedDomains, processedRecords, err := reconcileDNSOnce(ctx, db, args)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[dns] reconcile tick failed")
|
||||
} else {
|
||||
log.Debug().
|
||||
Int("domains", processedDomains).
|
||||
Int("records", processedRecords).
|
||||
Msg("[dns] reconcile tick ok")
|
||||
}
|
||||
|
||||
next := time.Now().Add(time.Duration(args.IntervalS) * time.Second)
|
||||
_, _ = jobs.Enqueue(ctx, uuid.NewString(), "dns_reconcile", args,
|
||||
archer.WithScheduleTime(next),
|
||||
archer.WithMaxRetries(1),
|
||||
)
|
||||
|
||||
return map[string]any{
|
||||
"domains_processed": processedDomains,
|
||||
"records_processed": processedRecords,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
/************* core tick *************/
|
||||
|
||||
func reconcileDNSOnce(ctx context.Context, db *gorm.DB, args DNSReconcileArgs) (int, int, error) {
|
||||
var domains []models.Domain
|
||||
|
||||
// 1) validate/backfill pending domains
|
||||
if err := db.
|
||||
Where("status = ?", "pending").
|
||||
Order("created_at ASC").
|
||||
Limit(args.MaxDomains).
|
||||
Find(&domains).Error; err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
domainsProcessed := 0
|
||||
for i := range domains {
|
||||
if err := processDomain(ctx, db, &domains[i]); err != nil {
|
||||
log.Error().Err(err).Str("domain", domains[i].DomainName).Msg("[dns] domain processing failed")
|
||||
} else {
|
||||
domainsProcessed++
|
||||
}
|
||||
}
|
||||
|
||||
// 2) apply pending record sets for ready domains
|
||||
var readyDomains []models.Domain
|
||||
if err := db.Where("status = ?", "ready").Find(&readyDomains).Error; err != nil {
|
||||
return domainsProcessed, 0, err
|
||||
}
|
||||
|
||||
recordsProcessed := 0
|
||||
for i := range readyDomains {
|
||||
n, err := processPendingRecordsForDomain(ctx, db, &readyDomains[i], args.MaxRecords)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("domain", readyDomains[i].DomainName).Msg("[dns] record processing failed")
|
||||
continue
|
||||
}
|
||||
recordsProcessed += n
|
||||
}
|
||||
|
||||
return domainsProcessed, recordsProcessed, nil
|
||||
}
|
||||
|
||||
/************* domain processing *************/
|
||||
|
||||
func processDomain(ctx context.Context, db *gorm.DB, d *models.Domain) error {
|
||||
orgID := d.OrganizationID
|
||||
|
||||
// 1) Load credential (org-guarded)
|
||||
var cred models.Credential
|
||||
if err := db.Where("id = ? AND organization_id = ?", d.CredentialID, orgID).First(&cred).Error; err != nil {
|
||||
return setDomainFailed(db, d, fmt.Errorf("credential not found: %w", err))
|
||||
}
|
||||
|
||||
// 2) Decrypt → dto.AWSCredential
|
||||
secret, err := utils.DecryptForOrg(orgID, cred.EncryptedData, cred.IV, cred.Tag, db)
|
||||
if err != nil {
|
||||
return setDomainFailed(db, d, fmt.Errorf("decrypt: %w", err))
|
||||
}
|
||||
var awsCred dto.AWSCredential
|
||||
if err := jsonUnmarshalStrict([]byte(secret), &awsCred); err != nil {
|
||||
return setDomainFailed(db, d, fmt.Errorf("secret decode: %w", err))
|
||||
}
|
||||
|
||||
// 3) Client
|
||||
r53c, _, err := newRoute53Client(ctx, awsCred)
|
||||
if err != nil {
|
||||
return setDomainFailed(db, d, err)
|
||||
}
|
||||
|
||||
// 4) Backfill zone id if missing
|
||||
zoneID := strings.TrimSpace(d.ZoneID)
|
||||
if zoneID == "" {
|
||||
zid, err := findHostedZoneID(ctx, r53c, d.DomainName)
|
||||
if err != nil {
|
||||
return setDomainFailed(db, d, fmt.Errorf("discover zone id: %w", err))
|
||||
}
|
||||
zoneID = zid
|
||||
d.ZoneID = zoneID
|
||||
}
|
||||
|
||||
// 5) Sanity: can fetch zone
|
||||
if _, err := r53c.GetHostedZone(ctx, &r53.GetHostedZoneInput{Id: aws.String(zoneID)}); err != nil {
|
||||
return setDomainFailed(db, d, fmt.Errorf("get hosted zone: %w", err))
|
||||
}
|
||||
|
||||
// 6) Mark ready
|
||||
d.Status = "ready"
|
||||
d.LastError = ""
|
||||
if err := db.Save(d).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setDomainFailed(db *gorm.DB, d *models.Domain, cause error) error {
|
||||
d.Status = "failed"
|
||||
d.LastError = truncateErr(cause.Error())
|
||||
_ = db.Save(d).Error
|
||||
return cause
|
||||
}
|
||||
|
||||
/************* record processing *************/
|
||||
|
||||
func processPendingRecordsForDomain(ctx context.Context, db *gorm.DB, d *models.Domain, max int) (int, error) {
|
||||
orgID := d.OrganizationID
|
||||
|
||||
// reload credential
|
||||
var cred models.Credential
|
||||
if err := db.Where("id = ? AND organization_id = ?", d.CredentialID, orgID).First(&cred).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
secret, err := utils.DecryptForOrg(orgID, cred.EncryptedData, cred.IV, cred.Tag, db)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var awsCred dto.AWSCredential
|
||||
if err := jsonUnmarshalStrict([]byte(secret), &awsCred); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
r53c, _, err := newRoute53Client(ctx, awsCred)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var records []models.RecordSet
|
||||
if err := db.
|
||||
Where("domain_id = ? AND status = ?", d.ID, "pending").
|
||||
Order("created_at ASC").
|
||||
Limit(max).
|
||||
Find(&records).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
applied := 0
|
||||
for i := range records {
|
||||
if err := applyRecord(ctx, db, r53c, d, &records[i]); err != nil {
|
||||
log.Error().Err(err).Str("rr", records[i].Name).Msg("[dns] apply record failed")
|
||||
_ = setRecordFailed(db, &records[i], err)
|
||||
continue
|
||||
}
|
||||
applied++
|
||||
}
|
||||
return applied, nil
|
||||
}
|
||||
|
||||
// core write + ownership + external-dns hardening
|
||||
|
||||
func applyRecord(ctx context.Context, db *gorm.DB, r53c *r53.Client, d *models.Domain, r *models.RecordSet) error {
|
||||
zoneID := strings.TrimSpace(d.ZoneID)
|
||||
if zoneID == "" {
|
||||
return errors.New("domain has no zone_id")
|
||||
}
|
||||
|
||||
rt := strings.ToUpper(r.Type)
|
||||
|
||||
// FQDN & marker
|
||||
fq := recordFQDN(r.Name, d.DomainName) // ends with "."
|
||||
mname := markerName(fq)
|
||||
expected := buildMarkerValue(d.OrganizationID.String(), r.ID.String(), r.Fingerprint)
|
||||
|
||||
// ---- ExternalDNS preflight ----
|
||||
extOwned, err := hasExternalDNSOwnership(ctx, r53c, zoneID, fq, rt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("external_dns_lookup: %w", err)
|
||||
}
|
||||
if extOwned {
|
||||
r.Owner = "external"
|
||||
_ = db.Save(r).Error
|
||||
return fmt.Errorf("ownership_conflict: external-dns claims %s; refusing to modify", strings.TrimSuffix(fq, "."))
|
||||
}
|
||||
|
||||
// ---- Autoglue ownership preflight via _autoglue.<fqdn> TXT ----
|
||||
markerVals, err := getMarkerTXTValues(ctx, r53c, zoneID, mname)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marker lookup: %w", err)
|
||||
}
|
||||
hasForeignOwner := false
|
||||
hasOurExact := false
|
||||
for _, v := range markerVals {
|
||||
mk, ok := parseMarkerValue(v)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case mk.Org == d.OrganizationID.String() && mk.Rec == r.ID.String() && mk.Fp == shortFP(r.Fingerprint):
|
||||
hasOurExact = true
|
||||
case mk.Org != d.OrganizationID.String() || mk.Rec != r.ID.String():
|
||||
hasForeignOwner = true
|
||||
}
|
||||
}
|
||||
if hasForeignOwner {
|
||||
r.Owner = "external"
|
||||
_ = db.Save(r).Error
|
||||
return fmt.Errorf("ownership_conflict: marker for %s is owned by another controller; refusing to modify", strings.TrimSuffix(fq, "."))
|
||||
}
|
||||
|
||||
// Build RR change (UPSERT)
|
||||
rrChange := r53types.Change{
|
||||
Action: r53types.ChangeActionUpsert,
|
||||
ResourceRecordSet: &r53types.ResourceRecordSet{
|
||||
Name: aws.String(fq),
|
||||
Type: r53types.RRType(rt),
|
||||
},
|
||||
}
|
||||
|
||||
// Decode user values
|
||||
var userVals []string
|
||||
if len(r.Values) > 0 {
|
||||
if err := jsonUnmarshalStrict([]byte(r.Values), &userVals); err != nil {
|
||||
return fmt.Errorf("values decode: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Quote TXT values as required by Route53
|
||||
recs := make([]r53types.ResourceRecord, 0, len(userVals))
|
||||
for _, v := range userVals {
|
||||
v = strings.TrimSpace(v)
|
||||
if rt == "TXT" && !(strings.HasPrefix(v, `"`) && strings.HasSuffix(v, `"`)) {
|
||||
v = strconv.Quote(v)
|
||||
}
|
||||
recs = append(recs, r53types.ResourceRecord{Value: aws.String(v)})
|
||||
}
|
||||
rrChange.ResourceRecordSet.ResourceRecords = recs
|
||||
if r.TTL != nil {
|
||||
ttl := int64(*r.TTL)
|
||||
rrChange.ResourceRecordSet.TTL = aws.Int64(ttl)
|
||||
}
|
||||
|
||||
// Build marker TXT change (UPSERT)
|
||||
markerChange := r53types.Change{
|
||||
Action: r53types.ChangeActionUpsert,
|
||||
ResourceRecordSet: &r53types.ResourceRecordSet{
|
||||
Name: aws.String(mname),
|
||||
Type: r53types.RRTypeTxt,
|
||||
TTL: aws.Int64(300),
|
||||
ResourceRecords: []r53types.ResourceRecord{
|
||||
{Value: aws.String(strconv.Quote(expected))},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Build external-dns poison TXT changes
|
||||
poisonChanges := buildExternalDNSPoisonTXTChanges(fq, rt)
|
||||
|
||||
// Apply all in one batch (atomic-ish)
|
||||
changes := []r53types.Change{rrChange, markerChange}
|
||||
changes = append(changes, poisonChanges...)
|
||||
|
||||
_, err = r53c.ChangeResourceRecordSets(ctx, &r53.ChangeResourceRecordSetsInput{
|
||||
HostedZoneId: aws.String(zoneID),
|
||||
ChangeBatch: &r53types.ChangeBatch{Changes: changes},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Success → mark ready & ownership
|
||||
r.Status = "ready"
|
||||
r.LastError = ""
|
||||
r.Owner = "autoglue"
|
||||
if err := db.Save(r).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
_ = hasOurExact // could be used to skip marker write in future
|
||||
return nil
|
||||
}
|
||||
|
||||
func setRecordFailed(db *gorm.DB, r *models.RecordSet, cause error) error {
|
||||
msg := truncateErr(cause.Error())
|
||||
r.Status = "failed"
|
||||
r.LastError = msg
|
||||
// classify ownership on conflict
|
||||
if strings.HasPrefix(msg, "ownership_conflict") {
|
||||
r.Owner = "external"
|
||||
} else if r.Owner == "" || r.Owner == "unknown" {
|
||||
r.Owner = "unknown"
|
||||
}
|
||||
_ = db.Save(r).Error
|
||||
return cause
|
||||
}
|
||||
|
||||
/************* AWS helpers *************/
|
||||
|
||||
func newRoute53Client(ctx context.Context, cred dto.AWSCredential) (*r53.Client, *aws.Config, error) {
|
||||
// Route53 is global, but config still wants a region
|
||||
region := strings.TrimSpace(cred.Region)
|
||||
if region == "" {
|
||||
region = "us-east-1"
|
||||
}
|
||||
cfg, err := config.LoadDefaultConfig(ctx,
|
||||
config.WithRegion(region),
|
||||
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
|
||||
cred.AccessKeyID, cred.SecretAccessKey, "",
|
||||
)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return r53.NewFromConfig(cfg), &cfg, nil
|
||||
}
|
||||
|
||||
func findHostedZoneID(ctx context.Context, c *r53.Client, domain string) (string, error) {
|
||||
d := normalizeDomain(domain)
|
||||
out, err := c.ListHostedZonesByName(ctx, &r53.ListHostedZonesByNameInput{
|
||||
DNSName: aws.String(d),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, hz := range out.HostedZones {
|
||||
if strings.TrimSuffix(aws.ToString(hz.Name), ".") == d {
|
||||
return trimZoneID(aws.ToString(hz.Id)), nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("hosted zone not found for %q", d)
|
||||
}
|
||||
|
||||
func trimZoneID(id string) string {
|
||||
return strings.TrimPrefix(id, "/hostedzone/")
|
||||
}
|
||||
|
||||
func normalizeDomain(s string) string {
|
||||
s = strings.TrimSpace(strings.ToLower(s))
|
||||
return strings.TrimSuffix(s, ".")
|
||||
}
|
||||
|
||||
func recordFQDN(name, domain string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" || name == "@" {
|
||||
return normalizeDomain(domain) + "."
|
||||
}
|
||||
if strings.HasSuffix(name, ".") {
|
||||
return name
|
||||
}
|
||||
return fmt.Sprintf("%s.%s.", name, normalizeDomain(domain))
|
||||
}
|
||||
|
||||
/************* TXT marker / external-dns helpers *************/
|
||||
|
||||
func markerName(fqdn string) string {
|
||||
trimmed := strings.TrimSuffix(fqdn, ".")
|
||||
return "_autoglue." + trimmed + "."
|
||||
}
|
||||
|
||||
func shortFP(full string) string {
|
||||
if len(full) > 16 {
|
||||
return full[:16]
|
||||
}
|
||||
return full
|
||||
}
|
||||
|
||||
func buildMarkerValue(orgID, recID, fp string) string {
|
||||
return "v=ag1 org=" + orgID + " rec=" + recID + " fp=" + shortFP(fp)
|
||||
}
|
||||
|
||||
func parseMarkerValue(s string) (ownershipMarker, bool) {
|
||||
out := ownershipMarker{}
|
||||
fields := strings.Fields(s)
|
||||
if len(fields) < 4 {
|
||||
return out, false
|
||||
}
|
||||
kv := map[string]string{}
|
||||
for _, f := range fields {
|
||||
parts := strings.SplitN(f, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
kv[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
if kv["v"] == "" || kv["org"] == "" || kv["rec"] == "" || kv["fp"] == "" {
|
||||
return out, false
|
||||
}
|
||||
out.Ver, out.Org, out.Rec, out.Fp = kv["v"], kv["org"], kv["rec"], kv["fp"]
|
||||
return out, true
|
||||
}
|
||||
|
||||
func getMarkerTXTValues(ctx context.Context, c *r53.Client, zoneID, marker string) ([]string, error) {
|
||||
return getTXTValues(ctx, c, zoneID, marker)
|
||||
}
|
||||
|
||||
// generic TXT fetcher
|
||||
func getTXTValues(ctx context.Context, c *r53.Client, zoneID, name string) ([]string, error) {
|
||||
out, err := c.ListResourceRecordSets(ctx, &r53.ListResourceRecordSetsInput{
|
||||
HostedZoneId: aws.String(zoneID),
|
||||
StartRecordName: aws.String(name),
|
||||
StartRecordType: r53types.RRTypeTxt,
|
||||
MaxItems: aws.Int32(1),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(out.ResourceRecordSets) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
rrset := out.ResourceRecordSets[0]
|
||||
if aws.ToString(rrset.Name) != name || rrset.Type != r53types.RRTypeTxt {
|
||||
return nil, nil
|
||||
}
|
||||
vals := make([]string, 0, len(rrset.ResourceRecords))
|
||||
for _, rr := range rrset.ResourceRecords {
|
||||
vals = append(vals, aws.ToString(rr.Value))
|
||||
}
|
||||
return vals, nil
|
||||
}
|
||||
|
||||
// detect external-dns-style ownership for this fqdn/type
|
||||
func hasExternalDNSOwnership(ctx context.Context, c *r53.Client, zoneID, fqdn, rrType string) (bool, error) {
|
||||
base := strings.TrimSuffix(fqdn, ".")
|
||||
candidates := []string{
|
||||
// with txtPrefix=extdns-, external-dns writes both:
|
||||
// extdns-<fqdn> and extdns-<rrtype-lc>-<fqdn>
|
||||
"extdns-" + base + ".",
|
||||
"extdns-" + strings.ToLower(rrType) + "-" + base + ".",
|
||||
}
|
||||
for _, name := range candidates {
|
||||
vals, err := getTXTValues(ctx, c, zoneID, name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, raw := range vals {
|
||||
v := strings.TrimSpace(raw)
|
||||
// strip surrounding quotes if present
|
||||
if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' {
|
||||
if unq, err := strconv.Unquote(v); err == nil {
|
||||
v = unq
|
||||
}
|
||||
}
|
||||
meta := parseExternalDNSMeta(v)
|
||||
if meta == nil {
|
||||
continue
|
||||
}
|
||||
if meta["heritage"] == "external-dns" &&
|
||||
meta["external-dns/owner"] != "" &&
|
||||
meta["external-dns/owner"] != externalDNSPoisonOwner {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// parseExternalDNSMeta parses the comma-separated external-dns TXT format into a small map.
|
||||
func parseExternalDNSMeta(v string) map[string]string {
|
||||
parts := strings.Split(v, ",")
|
||||
if len(parts) == 0 {
|
||||
return nil
|
||||
}
|
||||
meta := make(map[string]string, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
kv := strings.SplitN(p, "=", 2)
|
||||
if len(kv) != 2 {
|
||||
continue
|
||||
}
|
||||
meta[kv[0]] = kv[1]
|
||||
}
|
||||
if len(meta) == 0 {
|
||||
return nil
|
||||
}
|
||||
return meta
|
||||
}
|
||||
|
||||
// build poison TXT records so external-dns thinks some *other* owner manages this
|
||||
func buildExternalDNSPoisonTXTChanges(fqdn, rrType string) []r53types.Change {
|
||||
base := strings.TrimSuffix(fqdn, ".")
|
||||
names := []string{
|
||||
"extdns-" + base + ".",
|
||||
"extdns-" + strings.ToLower(rrType) + "-" + base + ".",
|
||||
}
|
||||
val := strconv.Quote(externalDNSPoisonValue)
|
||||
changes := make([]r53types.Change, 0, len(names))
|
||||
for _, n := range names {
|
||||
changes = append(changes, r53types.Change{
|
||||
Action: r53types.ChangeActionUpsert,
|
||||
ResourceRecordSet: &r53types.ResourceRecordSet{
|
||||
Name: aws.String(n),
|
||||
Type: r53types.RRTypeTxt,
|
||||
TTL: aws.Int64(300),
|
||||
ResourceRecords: []r53types.ResourceRecord{
|
||||
{Value: aws.String(val)},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
/************* misc utils *************/
|
||||
|
||||
func truncateErr(s string) string {
|
||||
const max = 2000
|
||||
if len(s) > max {
|
||||
return s[:max]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Strict unmarshal that treats "null" -> zero value correctly.
|
||||
func jsonUnmarshalStrict(b []byte, dst any) error {
|
||||
if len(b) == 0 {
|
||||
return errors.New("empty json")
|
||||
}
|
||||
return json.Unmarshal(b, dst)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
type Config struct {
|
||||
DbURL string
|
||||
DbURLRO string
|
||||
Port string
|
||||
Host string
|
||||
JWTIssuer string
|
||||
@@ -29,6 +30,12 @@ type Config struct {
|
||||
Debug bool
|
||||
Swagger bool
|
||||
SwaggerHost string
|
||||
|
||||
DBStudioEnabled bool
|
||||
DBStudioBind string
|
||||
DBStudioPort string
|
||||
DBStudioUser string
|
||||
DBStudioPass string
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -48,6 +55,12 @@ func Load() (Config, error) {
|
||||
v.SetDefault("bind.address", "127.0.0.1")
|
||||
v.SetDefault("bind.port", "8080")
|
||||
v.SetDefault("database.url", "postgres://user:pass@localhost:5432/db?sslmode=disable")
|
||||
v.SetDefault("database.url_ro", "")
|
||||
v.SetDefault("db_studio.enabled", false)
|
||||
v.SetDefault("db_studio.bind", "127.0.0.1")
|
||||
v.SetDefault("db_studio.port", "0") // 0 = random
|
||||
v.SetDefault("db_studio.user", "")
|
||||
v.SetDefault("db_studio.pass", "")
|
||||
|
||||
v.SetDefault("ui.dev", false)
|
||||
v.SetDefault("env", "development")
|
||||
@@ -63,6 +76,7 @@ func Load() (Config, error) {
|
||||
"bind.address",
|
||||
"bind.port",
|
||||
"database.url",
|
||||
"database.url_ro",
|
||||
"jwt.issuer",
|
||||
"jwt.audience",
|
||||
"jwt.private.enc.key",
|
||||
@@ -76,6 +90,11 @@ func Load() (Config, error) {
|
||||
"debug",
|
||||
"swagger",
|
||||
"swagger.host",
|
||||
"db_studio.enabled",
|
||||
"db_studio.bind",
|
||||
"db_studio.port",
|
||||
"db_studio.user",
|
||||
"db_studio.pass",
|
||||
}
|
||||
for _, k := range keys {
|
||||
_ = v.BindEnv(k)
|
||||
@@ -84,6 +103,7 @@ func Load() (Config, error) {
|
||||
// Build config
|
||||
cfg := Config{
|
||||
DbURL: v.GetString("database.url"),
|
||||
DbURLRO: v.GetString("database.url_ro"),
|
||||
Port: v.GetString("bind.port"),
|
||||
Host: v.GetString("bind.address"),
|
||||
JWTIssuer: v.GetString("jwt.issuer"),
|
||||
@@ -100,6 +120,12 @@ func Load() (Config, error) {
|
||||
Debug: v.GetBool("debug"),
|
||||
Swagger: v.GetBool("swagger"),
|
||||
SwaggerHost: v.GetString("swagger.host"),
|
||||
|
||||
DBStudioEnabled: v.GetBool("db_studio.enabled"),
|
||||
DBStudioBind: v.GetString("db_studio.bind"),
|
||||
DBStudioPort: v.GetString("db_studio.port"),
|
||||
DBStudioUser: v.GetString("db_studio.user"),
|
||||
DBStudioPass: v.GetString("db_studio.pass"),
|
||||
}
|
||||
|
||||
// Validate
|
||||
|
||||
@@ -273,6 +273,21 @@ func AuthCallback(db *gorm.DB) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
secure := strings.HasPrefix(cfg.OAuthRedirectBase, "https://")
|
||||
if xf := r.Header.Get("X-Forwarded-Proto"); xf != "" {
|
||||
secure = strings.EqualFold(xf, "https")
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "ag_jwt",
|
||||
Value: "Bearer " + access,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Secure: secure,
|
||||
MaxAge: int((time.Hour * 8).Seconds()),
|
||||
})
|
||||
|
||||
// If the state indicates SPA popup mode, postMessage tokens to the opener and close
|
||||
state := r.URL.Query().Get("state")
|
||||
if strings.Contains(state, "mode=spa") {
|
||||
@@ -356,6 +371,21 @@ func Refresh(db *gorm.DB) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
secure := strings.HasPrefix(cfg.OAuthRedirectBase, "https://")
|
||||
if xf := r.Header.Get("X-Forwarded-Proto"); xf != "" {
|
||||
secure = strings.EqualFold(xf, "https")
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "ag_jwt",
|
||||
Value: "Bearer " + access,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Secure: secure,
|
||||
MaxAge: int((time.Hour * 8).Seconds()),
|
||||
})
|
||||
|
||||
utils.WriteJSON(w, 200, dto.TokenPair{
|
||||
AccessToken: access,
|
||||
RefreshToken: newPair.Plain,
|
||||
@@ -377,6 +407,7 @@ func Refresh(db *gorm.DB) http.HandlerFunc {
|
||||
// @Router /auth/logout [post]
|
||||
func Logout(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
cfg, _ := config.Load()
|
||||
var req dto.LogoutRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
utils.WriteError(w, 400, "invalid_json", err.Error())
|
||||
@@ -385,13 +416,27 @@ func Logout(db *gorm.DB) http.HandlerFunc {
|
||||
rec, err := auth.ValidateRefreshToken(db, req.RefreshToken)
|
||||
if err != nil {
|
||||
w.WriteHeader(204) // already invalid/revoked
|
||||
return
|
||||
goto clearCookie
|
||||
}
|
||||
if err := auth.RevokeFamily(db, rec.FamilyID); err != nil {
|
||||
utils.WriteError(w, 500, "revoke_failed", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
clearCookie:
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "ag_jwt",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
MaxAge: -1,
|
||||
Expires: time.Unix(0, 0),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Secure: strings.HasPrefix(cfg.OAuthRedirectBase, "https"),
|
||||
})
|
||||
|
||||
w.WriteHeader(204)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
186
internal/handlers/clusters.go
Normal file
186
internal/handlers/clusters.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||
"github.com/glueops/autoglue/internal/common"
|
||||
"github.com/glueops/autoglue/internal/handlers/dto"
|
||||
"github.com/glueops/autoglue/internal/models"
|
||||
"github.com/glueops/autoglue/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ListClusters godoc
|
||||
//
|
||||
// @ID ListClusters
|
||||
// @Summary List clusters (org scoped)
|
||||
// @Description Returns clusters for the organization in X-Org-ID. Filter by `q` (name contains).
|
||||
// @Tags Clusters
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param q query string false "Name contains (case-insensitive)"
|
||||
// @Success 200 {array} dto.ClusterResponse
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "failed to list clusters"
|
||||
// @Router /clusters [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func ListClusters(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
|
||||
q := db.Where("organization_id = ?", orgID)
|
||||
if needle := strings.TrimSpace(r.URL.Query().Get("q")); needle != "" {
|
||||
q = q.Where(`name ILIKE ?`, "%"+needle+"%")
|
||||
}
|
||||
|
||||
var rows []models.Cluster
|
||||
if err := q.
|
||||
Preload("NodePools").
|
||||
Preload("NodePools.Labels").
|
||||
Preload("NodePools.Annotations").
|
||||
Preload("NodePools.Labels").
|
||||
Preload("NodePools.Taints").
|
||||
Preload("NodePools.Servers").
|
||||
Preload("BastionServer").
|
||||
Find(&rows).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]dto.ClusterResponse, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, clusterToDTO(row))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// CreateCluster godoc
|
||||
//
|
||||
// @ID CreateCluster
|
||||
// @Summary Create cluster (org scoped)
|
||||
// @Description Creates a cluster. If `kubeconfig` is provided, it will be encrypted per-organization and stored securely (never returned).
|
||||
// @Tags Clusters
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param body body dto.CreateClusterRequest true "payload"
|
||||
// @Success 201 {object} dto.ClusterResponse
|
||||
// @Failure 400 {string} string "invalid json"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "create failed"
|
||||
// @Router /clusters [post]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func CreateCluster(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// -- Helpers
|
||||
|
||||
func clusterToDTO(c models.Cluster) dto.ClusterResponse {
|
||||
var bastion *dto.ServerResponse
|
||||
if c.BastionServer != nil {
|
||||
b := serverToDTO(*c.BastionServer)
|
||||
bastion = &b
|
||||
}
|
||||
|
||||
nps := make([]dto.NodePoolResponse, 0, len(c.NodePools))
|
||||
for _, np := range c.NodePools {
|
||||
nps = append(nps, nodePoolToDTO(np))
|
||||
}
|
||||
|
||||
return dto.ClusterResponse{
|
||||
ID: c.ID,
|
||||
Name: c.Name,
|
||||
Provider: c.Provider,
|
||||
Region: c.Region,
|
||||
Status: c.Status,
|
||||
CaptainDomain: c.CaptainDomain,
|
||||
//ClusterLoadBalancer: c.ClusterLoadBalancer,
|
||||
RandomToken: c.RandomToken,
|
||||
CertificateKey: c.CertificateKey,
|
||||
//ControlLoadBalancer: c.ControlLoadBalancer,
|
||||
NodePools: nps,
|
||||
BastionServer: bastion,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func nodePoolToDTO(np models.NodePool) dto.NodePoolResponse {
|
||||
labels := make([]dto.LabelResponse, 0, len(np.Labels))
|
||||
for _, l := range np.Labels {
|
||||
labels = append(labels, dto.LabelResponse{
|
||||
Key: l.Key,
|
||||
Value: l.Value,
|
||||
})
|
||||
}
|
||||
|
||||
annotations := make([]dto.AnnotationResponse, 0, len(np.Annotations))
|
||||
for _, a := range np.Annotations {
|
||||
annotations = append(annotations, dto.AnnotationResponse{
|
||||
Key: a.Key,
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
|
||||
taints := make([]dto.TaintResponse, 0, len(np.Taints))
|
||||
for _, t := range np.Taints {
|
||||
taints = append(taints, dto.TaintResponse{
|
||||
Key: t.Key,
|
||||
Value: t.Value,
|
||||
Effect: t.Effect,
|
||||
})
|
||||
}
|
||||
|
||||
servers := make([]dto.ServerResponse, 0, len(np.Servers))
|
||||
for _, s := range np.Servers {
|
||||
servers = append(servers, serverToDTO(s))
|
||||
}
|
||||
|
||||
return dto.NodePoolResponse{
|
||||
AuditFields: common.AuditFields{
|
||||
ID: np.ID,
|
||||
OrganizationID: np.OrganizationID,
|
||||
CreatedAt: np.CreatedAt,
|
||||
UpdatedAt: np.UpdatedAt,
|
||||
},
|
||||
Name: np.Name,
|
||||
Role: dto.NodeRole(np.Role),
|
||||
Labels: labels,
|
||||
Annotations: annotations,
|
||||
Taints: taints,
|
||||
Servers: servers,
|
||||
}
|
||||
}
|
||||
|
||||
func serverToDTO(s models.Server) dto.ServerResponse {
|
||||
return dto.ServerResponse{
|
||||
ID: s.ID,
|
||||
Hostname: s.Hostname,
|
||||
PrivateIPAddress: s.PrivateIPAddress,
|
||||
PublicIPAddress: s.PublicIPAddress,
|
||||
Role: s.Role,
|
||||
Status: s.Status,
|
||||
SSHUser: s.SSHUser,
|
||||
SshKeyID: s.SshKeyID,
|
||||
CreatedAt: s.CreatedAt.UTC().Format(time.RFC3339),
|
||||
UpdatedAt: s.UpdatedAt.UTC().Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
561
internal/handlers/credentials.go
Normal file
561
internal/handlers/credentials.go
Normal file
@@ -0,0 +1,561 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||
"github.com/glueops/autoglue/internal/handlers/dto"
|
||||
"github.com/glueops/autoglue/internal/models"
|
||||
"github.com/glueops/autoglue/internal/utils"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ListCredentials godoc
|
||||
// @ID ListCredentials
|
||||
// @Summary List credentials (metadata only)
|
||||
// @Description Returns credential metadata for the current org. Secrets are never returned.
|
||||
// @Tags Credentials
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||
// @Param provider query string false "Filter by provider (e.g., aws)"
|
||||
// @Param kind query string false "Filter by kind (e.g., aws_access_key)"
|
||||
// @Param scope_kind query string false "Filter by scope kind (provider/service/resource)"
|
||||
// @Success 200 {array} dto.CredentialOut
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "internal server error"
|
||||
// @Router /credentials [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func ListCredentials(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
q := db.Where("organization_id = ?", orgID)
|
||||
if v := r.URL.Query().Get("provider"); v != "" {
|
||||
q = q.Where("provider = ?", v)
|
||||
}
|
||||
if v := r.URL.Query().Get("kind"); v != "" {
|
||||
q = q.Where("kind = ?", v)
|
||||
}
|
||||
if v := r.URL.Query().Get("scope_kind"); v != "" {
|
||||
q = q.Where("scope_kind = ?", v)
|
||||
}
|
||||
|
||||
var rows []models.Credential
|
||||
if err := q.Order("updated_at DESC").Find(&rows).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||
return
|
||||
}
|
||||
out := make([]dto.CredentialOut, 0, len(rows))
|
||||
for i := range rows {
|
||||
out = append(out, credOut(&rows[i]))
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusOK, out)
|
||||
}
|
||||
}
|
||||
|
||||
// GetCredential godoc
|
||||
// @ID GetCredential
|
||||
// @Summary Get credential by ID (metadata only)
|
||||
// @Tags Credentials
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||
// @Param id path string true "Credential ID (UUID)"
|
||||
// @Success 200 {object} dto.CredentialOut
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "internal server error"
|
||||
// @Router /credentials/{id} [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func GetCredential(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID")
|
||||
return
|
||||
}
|
||||
|
||||
var row models.Credential
|
||||
if err := db.Where("organization_id = ? AND id = ?", orgID, id).First(&row).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
utils.WriteError(w, http.StatusNotFound, "not_found", "credential not found")
|
||||
return
|
||||
}
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||
return
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusOK, credOut(&row))
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCredential godoc
|
||||
// @ID CreateCredential
|
||||
// @Summary Create a credential (encrypts secret)
|
||||
// @Tags Credentials
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||
// @Param body body dto.CreateCredentialRequest true "Credential payload"
|
||||
// @Success 201 {object} dto.CredentialOut
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "internal server error"
|
||||
// @Router /credentials [post]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func CreateCredential(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
|
||||
var in dto.CreateCredentialRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := dto.Validate.Struct(in); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "validation_error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cred, err := SaveCredentialWithScope(
|
||||
r.Context(), db, orgID,
|
||||
in.Provider, in.Kind, in.SchemaVersion,
|
||||
in.ScopeKind, in.ScopeVersion, json.RawMessage(in.Scope), json.RawMessage(in.Secret),
|
||||
in.Name, in.AccountID, in.Region,
|
||||
)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "save_failed", err.Error())
|
||||
return
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusCreated, credOut(cred))
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateCredential godoc
|
||||
// @ID UpdateCredential
|
||||
// @Summary Update credential metadata and/or rotate secret
|
||||
// @Tags Credentials
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||
// @Param id path string true "Credential ID (UUID)"
|
||||
// @Param body body dto.UpdateCredentialRequest true "Fields to update"
|
||||
// @Success 200 {object} dto.CredentialOut
|
||||
// @Failure 403 {string} string "X-Org-ID required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Router /credentials/{id} [patch]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func UpdateCredential(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID")
|
||||
return
|
||||
}
|
||||
|
||||
var row models.Credential
|
||||
if err := db.Where("organization_id = ? AND id = ?", orgID, id).First(&row).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
utils.WriteError(w, http.StatusNotFound, "not_found", "credential not found")
|
||||
return
|
||||
}
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var in dto.UpdateCredentialRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Update metadata
|
||||
if in.Name != nil {
|
||||
row.Name = *in.Name
|
||||
}
|
||||
if in.AccountID != nil {
|
||||
row.AccountID = *in.AccountID
|
||||
}
|
||||
if in.Region != nil {
|
||||
row.Region = *in.Region
|
||||
}
|
||||
|
||||
// Update scope (re-validate + fingerprint)
|
||||
if in.ScopeKind != nil || in.Scope != nil || in.ScopeVersion != nil {
|
||||
newKind := row.ScopeKind
|
||||
if in.ScopeKind != nil {
|
||||
newKind = *in.ScopeKind
|
||||
}
|
||||
newVersion := row.ScopeVersion
|
||||
if in.ScopeVersion != nil {
|
||||
newVersion = *in.ScopeVersion
|
||||
}
|
||||
if in.Scope == nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "validation_error", "scope must be provided when changing scope kind/version")
|
||||
return
|
||||
}
|
||||
prScopes := dto.ScopeRegistry[row.Provider]
|
||||
kScopes := prScopes[newKind]
|
||||
sdef := kScopes[newVersion]
|
||||
dst := sdef.New()
|
||||
if err := json.Unmarshal(*in.Scope, dst); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "invalid_scope_json", err.Error())
|
||||
return
|
||||
}
|
||||
if err := sdef.Validate(dst); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "invalid_scope", err.Error())
|
||||
return
|
||||
}
|
||||
canonScope, err := canonicalJSON(dst)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "canon_error", err.Error())
|
||||
return
|
||||
}
|
||||
row.Scope = canonScope
|
||||
row.ScopeKind = newKind
|
||||
row.ScopeVersion = newVersion
|
||||
row.ScopeFingerprint = sha256Hex(canonScope)
|
||||
}
|
||||
|
||||
// Rotate secret
|
||||
if in.Secret != nil {
|
||||
// validate against current Provider/Kind/SchemaVersion
|
||||
def := dto.CredentialRegistry[row.Provider][row.Kind][row.SchemaVersion]
|
||||
dst := def.New()
|
||||
if err := json.Unmarshal(*in.Secret, dst); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "invalid_secret_json", err.Error())
|
||||
return
|
||||
}
|
||||
if err := def.Validate(dst); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "invalid_secret", err.Error())
|
||||
return
|
||||
}
|
||||
canonSecret, err := canonicalJSON(dst)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "canon_error", err.Error())
|
||||
return
|
||||
}
|
||||
cipher, iv, tag, err := utils.EncryptForOrg(orgID, canonSecret, db)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "encrypt_error", err.Error())
|
||||
return
|
||||
}
|
||||
row.EncryptedData = cipher
|
||||
row.IV = iv
|
||||
row.Tag = tag
|
||||
}
|
||||
|
||||
if err := db.Save(&row).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||
return
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusOK, credOut(&row))
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteCredential godoc
|
||||
// @ID DeleteCredential
|
||||
// @Summary Delete credential
|
||||
// @Tags Credentials
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||
// @Param id path string true "Credential ID (UUID)"
|
||||
// @Success 204
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Router /credentials/{id} [delete]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func DeleteCredential(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID")
|
||||
return
|
||||
}
|
||||
res := db.Where("organization_id = ? AND id = ?", orgID, id).Delete(&models.Credential{})
|
||||
if res.Error != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", res.Error.Error())
|
||||
return
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
utils.WriteError(w, http.StatusNotFound, "not_found", "credential not found")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// RevealCredential godoc
|
||||
// @ID RevealCredential
|
||||
// @Summary Reveal decrypted secret (one-time read)
|
||||
// @Tags Credentials
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||
// @Param id path string true "Credential ID (UUID)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Router /credentials/{id}/reveal [post]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func RevealCredential(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID")
|
||||
return
|
||||
}
|
||||
|
||||
var row models.Credential
|
||||
if err := db.Where("organization_id = ? AND id = ?", orgID, id).First(&row).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
utils.WriteError(w, http.StatusNotFound, "not_found", "credential not found")
|
||||
return
|
||||
}
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
plain, err := utils.DecryptForOrg(orgID, row.EncryptedData, row.IV, row.Tag, db)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "decrypt_error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.WriteJSON(w, http.StatusOK, plain)
|
||||
}
|
||||
}
|
||||
|
||||
// -- Helpers
|
||||
|
||||
func canonicalJSON(v any) ([]byte, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m any
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return marshalSorted(m)
|
||||
}
|
||||
|
||||
func marshalSorted(v any) ([]byte, error) {
|
||||
switch vv := v.(type) {
|
||||
case map[string]any:
|
||||
keys := make([]string, 0, len(vv))
|
||||
for k := range vv {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
buf := bytes.NewBufferString("{")
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
kb, _ := json.Marshal(k)
|
||||
buf.Write(kb)
|
||||
buf.WriteByte(':')
|
||||
b, err := marshalSorted(vv[k])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf.Write(b)
|
||||
}
|
||||
buf.WriteByte('}')
|
||||
return buf.Bytes(), nil
|
||||
case []any:
|
||||
buf := bytes.NewBufferString("[")
|
||||
for i, e := range vv {
|
||||
if i > 0 {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
b, err := marshalSorted(e)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf.Write(b)
|
||||
}
|
||||
buf.WriteByte(']')
|
||||
return buf.Bytes(), nil
|
||||
default:
|
||||
return json.Marshal(v)
|
||||
}
|
||||
}
|
||||
|
||||
func sha256Hex(b []byte) string {
|
||||
sum := sha256.Sum256(b)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// SaveCredentialWithScope validates secret+scope, encrypts, fingerprints, and stores.
|
||||
func SaveCredentialWithScope(
|
||||
ctx context.Context,
|
||||
db *gorm.DB,
|
||||
orgID uuid.UUID,
|
||||
provider, kind string,
|
||||
schemaVersion int,
|
||||
scopeKind string,
|
||||
scopeVersion int,
|
||||
rawScope json.RawMessage,
|
||||
rawSecret json.RawMessage,
|
||||
name, accountID, region string,
|
||||
) (*models.Credential, error) {
|
||||
// 1) secret shape
|
||||
pv, ok := dto.CredentialRegistry[provider]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown provider %q", provider)
|
||||
}
|
||||
kv, ok := pv[kind]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown kind %q for provider %q", kind, provider)
|
||||
}
|
||||
def, ok := kv[schemaVersion]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported schema version %d for %s/%s", schemaVersion, provider, kind)
|
||||
}
|
||||
|
||||
secretDst := def.New()
|
||||
if err := json.Unmarshal(rawSecret, secretDst); err != nil {
|
||||
return nil, fmt.Errorf("payload is not valid JSON for %s/%s: %w", provider, kind, err)
|
||||
}
|
||||
if err := def.Validate(secretDst); err != nil {
|
||||
return nil, fmt.Errorf("invalid %s/%s: %w", provider, kind, err)
|
||||
}
|
||||
|
||||
// 2) scope shape
|
||||
prScopes, ok := dto.ScopeRegistry[provider]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no scopes registered for provider %q", provider)
|
||||
}
|
||||
kScopes, ok := prScopes[scopeKind]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid scope_kind %q for provider %q", scopeKind, provider)
|
||||
}
|
||||
sdef, ok := kScopes[scopeVersion]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported scope version %d for %s/%s", scopeVersion, provider, scopeKind)
|
||||
}
|
||||
|
||||
scopeDst := sdef.New()
|
||||
if err := json.Unmarshal(rawScope, scopeDst); err != nil {
|
||||
return nil, fmt.Errorf("invalid scope JSON: %w", err)
|
||||
}
|
||||
if err := sdef.Validate(scopeDst); err != nil {
|
||||
return nil, fmt.Errorf("invalid scope: %w", err)
|
||||
}
|
||||
|
||||
// 3) canonicalize scope (also what we persist in plaintext)
|
||||
canonScope, err := canonicalJSON(scopeDst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fp := sha256Hex(canonScope) // or HMAC if you have a server-side key
|
||||
|
||||
// 4) canonicalize + encrypt secret
|
||||
canonSecret, err := canonicalJSON(secretDst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cipher, iv, tag, err := utils.EncryptForOrg(orgID, canonSecret, db)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encrypt: %w", err)
|
||||
}
|
||||
|
||||
cred := &models.Credential{
|
||||
OrganizationID: orgID,
|
||||
Provider: provider,
|
||||
Kind: kind,
|
||||
SchemaVersion: schemaVersion,
|
||||
Name: name,
|
||||
ScopeKind: scopeKind,
|
||||
Scope: datatypes.JSON(canonScope),
|
||||
ScopeVersion: scopeVersion,
|
||||
AccountID: accountID,
|
||||
Region: region,
|
||||
ScopeFingerprint: fp,
|
||||
EncryptedData: cipher,
|
||||
IV: iv,
|
||||
Tag: tag,
|
||||
}
|
||||
|
||||
if err := db.WithContext(ctx).Create(cred).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cred, nil
|
||||
}
|
||||
|
||||
// credOut converts model → response DTO
|
||||
func credOut(c *models.Credential) dto.CredentialOut {
|
||||
return dto.CredentialOut{
|
||||
ID: c.ID.String(),
|
||||
Provider: c.Provider,
|
||||
Kind: c.Kind,
|
||||
SchemaVersion: c.SchemaVersion,
|
||||
Name: c.Name,
|
||||
ScopeKind: c.ScopeKind,
|
||||
ScopeVersion: c.ScopeVersion,
|
||||
Scope: dto.RawJSON(c.Scope),
|
||||
AccountID: c.AccountID,
|
||||
Region: c.Region,
|
||||
CreatedAt: c.CreatedAt.UTC().Format(time.RFC3339),
|
||||
UpdatedAt: c.UpdatedAt.UTC().Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
802
internal/handlers/dns.go
Normal file
802
internal/handlers/dns.go
Normal file
@@ -0,0 +1,802 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||
"github.com/glueops/autoglue/internal/handlers/dto"
|
||||
"github.com/glueops/autoglue/internal/models"
|
||||
"github.com/glueops/autoglue/internal/utils"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ---------- Helpers ----------
|
||||
|
||||
func normLowerNoDot(s string) string {
|
||||
s = strings.TrimSpace(strings.ToLower(s))
|
||||
return strings.TrimSuffix(s, ".")
|
||||
}
|
||||
|
||||
func fqdn(domain string, rel string) string {
|
||||
d := normLowerNoDot(domain)
|
||||
r := normLowerNoDot(rel)
|
||||
if r == "" || r == "@" {
|
||||
return d
|
||||
}
|
||||
return r + "." + d
|
||||
}
|
||||
|
||||
func canonicalJSONAny(v any) ([]byte, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var anyv any
|
||||
if err := json.Unmarshal(b, &anyv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return marshalSortedDNS(anyv)
|
||||
}
|
||||
|
||||
func marshalSortedDNS(v any) ([]byte, error) {
|
||||
switch vv := v.(type) {
|
||||
case map[string]any:
|
||||
keys := make([]string, 0, len(vv))
|
||||
for k := range vv {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sortStrings(keys)
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte('{')
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
kb, _ := json.Marshal(k)
|
||||
buf.Write(kb)
|
||||
buf.WriteByte(':')
|
||||
b, err := marshalSortedDNS(vv[k])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf.Write(b)
|
||||
}
|
||||
buf.WriteByte('}')
|
||||
return buf.Bytes(), nil
|
||||
case []any:
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte('[')
|
||||
for i, e := range vv {
|
||||
if i > 0 {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
b, err := marshalSortedDNS(e)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf.Write(b)
|
||||
}
|
||||
buf.WriteByte(']')
|
||||
return buf.Bytes(), nil
|
||||
default:
|
||||
return json.Marshal(v)
|
||||
}
|
||||
}
|
||||
|
||||
func sortStrings(a []string) {
|
||||
for i := 0; i < len(a); i++ {
|
||||
for j := i + 1; j < len(a); j++ {
|
||||
if a[j] < a[i] {
|
||||
a[i], a[j] = a[j], a[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sha256HexBytes(b []byte) string {
|
||||
sum := sha256.Sum256(b)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
/* Fingerprint (provider-agnostic) */
|
||||
type desiredRecord struct {
|
||||
ZoneID string `json:"zone_id"`
|
||||
FQDN string `json:"fqdn"`
|
||||
Type string `json:"type"`
|
||||
TTL *int `json:"ttl,omitempty"`
|
||||
Values []string `json:"values,omitempty"`
|
||||
}
|
||||
|
||||
func computeFingerprint(zoneID, fqdn, typ string, ttl *int, values datatypes.JSON) (string, error) {
|
||||
var vals []string
|
||||
if len(values) > 0 && string(values) != "null" {
|
||||
if err := json.Unmarshal(values, &vals); err != nil {
|
||||
return "", err
|
||||
}
|
||||
sortStrings(vals)
|
||||
}
|
||||
payload := &desiredRecord{
|
||||
ZoneID: zoneID, FQDN: fqdn, Type: strings.ToUpper(typ), TTL: ttl, Values: vals,
|
||||
}
|
||||
can, err := canonicalJSONAny(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sha256HexBytes(can), nil
|
||||
}
|
||||
|
||||
func mustSameOrgDomainWithCredential(db *gorm.DB, orgID uuid.UUID, credID uuid.UUID) error {
|
||||
var cred models.Credential
|
||||
if err := db.Where("id = ? AND organization_id = ?", credID, orgID).First(&cred).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("credential not found or belongs to different org")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if cred.Provider != "aws" || cred.ScopeKind != "service" {
|
||||
return fmt.Errorf("credential must be AWS Route 53 service scoped")
|
||||
}
|
||||
var scope map[string]any
|
||||
if err := json.Unmarshal(cred.Scope, &scope); err != nil {
|
||||
return fmt.Errorf("credential scope invalid json: %w", err)
|
||||
}
|
||||
if strings.ToLower(fmt.Sprint(scope["service"])) != "route53" {
|
||||
return fmt.Errorf("credential scope.service must be route53")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------- Domain Handlers ----------
|
||||
|
||||
// ListDomains godoc
|
||||
//
|
||||
// @ID ListDomains
|
||||
// @Summary List domains (org scoped)
|
||||
// @Description Returns domains for X-Org-ID. Filters: `domain_name`, `status`, `q` (contains).
|
||||
// @Tags DNS
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param domain_name query string false "Exact domain name (lowercase, no trailing dot)"
|
||||
// @Param status query string false "pending|provisioning|ready|failed"
|
||||
// @Param q query string false "Domain contains (case-insensitive)"
|
||||
// @Success 200 {array} dto.DomainResponse
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "db error"
|
||||
// @Router /dns/domains [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func ListDomains(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
q := db.Model(&models.Domain{}).Where("organization_id = ?", orgID)
|
||||
if v := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("domain_name"))); v != "" {
|
||||
q = q.Where("LOWER(domain_name) = ?", v)
|
||||
}
|
||||
if v := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("status"))); v != "" {
|
||||
q = q.Where("status = ?", v)
|
||||
}
|
||||
if needle := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("q"))); needle != "" {
|
||||
q = q.Where("LOWER(domain_name) LIKE ?", "%"+needle+"%")
|
||||
}
|
||||
|
||||
var rows []models.Domain
|
||||
if err := q.Order("created_at DESC").Find(&rows).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||
return
|
||||
}
|
||||
out := make([]dto.DomainResponse, 0, len(rows))
|
||||
for i := range rows {
|
||||
out = append(out, domainOut(&rows[i]))
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusOK, out)
|
||||
}
|
||||
}
|
||||
|
||||
// GetDomain godoc
|
||||
//
|
||||
// @ID GetDomain
|
||||
// @Summary Get a domain (org scoped)
|
||||
// @Tags DNS
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Domain ID (UUID)"
|
||||
// @Success 200 {object} dto.DomainResponse
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Router /dns/domains/{id} [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func GetDomain(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID")
|
||||
return
|
||||
}
|
||||
var row models.Domain
|
||||
if err := db.Where("organization_id = ? AND id = ?", orgID, id).First(&row).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
utils.WriteError(w, http.StatusNotFound, "not_found", "domain not found")
|
||||
return
|
||||
}
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||
return
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusOK, domainOut(&row))
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDomain godoc
|
||||
//
|
||||
// @ID CreateDomain
|
||||
// @Summary Create a domain (org scoped)
|
||||
// @Description Creates a domain bound to a Route 53 scoped credential. Archer will backfill ZoneID if omitted.
|
||||
// @Tags DNS
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param body body dto.CreateDomainRequest true "Domain payload"
|
||||
// @Success 201 {object} dto.DomainResponse
|
||||
// @Failure 400 {string} string "validation error"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "db error"
|
||||
// @Router /dns/domains [post]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func CreateDomain(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
var in dto.CreateDomainRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
||||
return
|
||||
}
|
||||
if err := dto.DNSValidate(in); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "validation_error", err.Error())
|
||||
return
|
||||
}
|
||||
credID, _ := uuid.Parse(in.CredentialID)
|
||||
if err := mustSameOrgDomainWithCredential(db, orgID, credID); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "invalid_credential", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
row := &models.Domain{
|
||||
OrganizationID: orgID,
|
||||
DomainName: normLowerNoDot(in.DomainName),
|
||||
ZoneID: strings.TrimSpace(in.ZoneID),
|
||||
Status: "pending",
|
||||
LastError: "",
|
||||
CredentialID: credID,
|
||||
}
|
||||
if err := db.Create(row).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||
return
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusCreated, domainOut(row))
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateDomain godoc
|
||||
//
|
||||
// @ID UpdateDomain
|
||||
// @Summary Update a domain (org scoped)
|
||||
// @Tags DNS
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Domain ID (UUID)"
|
||||
// @Param body body dto.UpdateDomainRequest true "Fields to update"
|
||||
// @Success 200 {object} dto.DomainResponse
|
||||
// @Failure 400 {string} string "validation error"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Router /dns/domains/{id} [patch]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func UpdateDomain(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID")
|
||||
return
|
||||
}
|
||||
var row models.Domain
|
||||
if err := db.Where("organization_id = ? AND id = ?", orgID, id).First(&row).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
utils.WriteError(w, http.StatusNotFound, "not_found", "domain not found")
|
||||
return
|
||||
}
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||
return
|
||||
}
|
||||
var in dto.UpdateDomainRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
||||
return
|
||||
}
|
||||
if err := dto.DNSValidate(in); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "validation_error", err.Error())
|
||||
return
|
||||
}
|
||||
if in.DomainName != nil {
|
||||
row.DomainName = normLowerNoDot(*in.DomainName)
|
||||
}
|
||||
if in.CredentialID != nil {
|
||||
credID, _ := uuid.Parse(*in.CredentialID)
|
||||
if err := mustSameOrgDomainWithCredential(db, orgID, credID); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "invalid_credential", err.Error())
|
||||
return
|
||||
}
|
||||
row.CredentialID = credID
|
||||
row.Status = "pending"
|
||||
row.LastError = ""
|
||||
}
|
||||
if in.ZoneID != nil {
|
||||
row.ZoneID = strings.TrimSpace(*in.ZoneID)
|
||||
}
|
||||
if in.Status != nil {
|
||||
row.Status = *in.Status
|
||||
if row.Status == "pending" {
|
||||
row.LastError = ""
|
||||
}
|
||||
}
|
||||
if err := db.Save(&row).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||
return
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusOK, domainOut(&row))
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteDomain godoc
|
||||
//
|
||||
// @ID DeleteDomain
|
||||
// @Summary Delete a domain
|
||||
// @Tags DNS
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Domain ID (UUID)"
|
||||
// @Success 204
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Router /dns/domains/{id} [delete]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func DeleteDomain(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID")
|
||||
return
|
||||
}
|
||||
res := db.Where("organization_id = ? AND id = ?", orgID, id).Delete(&models.Domain{})
|
||||
if res.Error != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", res.Error.Error())
|
||||
return
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
utils.WriteError(w, http.StatusNotFound, "not_found", "domain not found")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Record Set Handlers ----------
|
||||
|
||||
// ListRecordSets godoc
|
||||
//
|
||||
// @ID ListRecordSets
|
||||
// @Summary List record sets for a domain
|
||||
// @Description Filters: `name`, `type`, `status`.
|
||||
// @Tags DNS
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param domain_id path string true "Domain ID (UUID)"
|
||||
// @Param name query string false "Exact relative name or FQDN (server normalizes)"
|
||||
// @Param type query string false "RR type (A, AAAA, CNAME, TXT, MX, NS, SRV, CAA)"
|
||||
// @Param status query string false "pending|provisioning|ready|failed"
|
||||
// @Success 200 {array} dto.RecordSetResponse
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "domain not found"
|
||||
// @Router /dns/domains/{domain_id}/records [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func ListRecordSets(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
did, err := uuid.Parse(chi.URLParam(r, "domain_id"))
|
||||
if err != nil {
|
||||
log.Info().Msg(err.Error())
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid domain UUID:")
|
||||
return
|
||||
}
|
||||
var domain models.Domain
|
||||
if err := db.Where("organization_id = ? AND id = ?", orgID, did).First(&domain).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
utils.WriteError(w, http.StatusNotFound, "not_found", "domain not found")
|
||||
return
|
||||
}
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||
return
|
||||
}
|
||||
|
||||
q := db.Model(&models.RecordSet{}).Where("domain_id = ?", did)
|
||||
if v := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("name"))); v != "" {
|
||||
dn := strings.ToLower(domain.DomainName)
|
||||
rel := v
|
||||
// normalize apex or FQDN into relative
|
||||
if v == dn || v == dn+"." {
|
||||
rel = ""
|
||||
} else {
|
||||
rel = strings.TrimSuffix(v, "."+dn)
|
||||
rel = normLowerNoDot(rel)
|
||||
}
|
||||
q = q.Where("LOWER(name) = ?", rel)
|
||||
}
|
||||
if v := strings.TrimSpace(strings.ToUpper(r.URL.Query().Get("type"))); v != "" {
|
||||
q = q.Where("type = ?", v)
|
||||
}
|
||||
if v := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("status"))); v != "" {
|
||||
q = q.Where("status = ?", v)
|
||||
}
|
||||
|
||||
var rows []models.RecordSet
|
||||
if err := q.Order("created_at DESC").Find(&rows).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||
return
|
||||
}
|
||||
out := make([]dto.RecordSetResponse, 0, len(rows))
|
||||
for i := range rows {
|
||||
out = append(out, recordOut(&rows[i]))
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusOK, out)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateRecordSet godoc
|
||||
//
|
||||
// @ID CreateRecordSet
|
||||
// @Summary Create a record set (pending; Archer will UPSERT to Route 53)
|
||||
// @Tags DNS
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param domain_id path string true "Domain ID (UUID)"
|
||||
// @Param body body dto.CreateRecordSetRequest true "Record set payload"
|
||||
// @Success 201 {object} dto.RecordSetResponse
|
||||
// @Failure 400 {string} string "validation error"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "domain not found"
|
||||
// @Router /dns/domains/{domain_id}/records [post]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func CreateRecordSet(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
did, err := uuid.Parse(chi.URLParam(r, "domain_id"))
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid domain UUID")
|
||||
return
|
||||
}
|
||||
var domain models.Domain
|
||||
if err := db.Where("organization_id = ? AND id = ?", orgID, did).First(&domain).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
utils.WriteError(w, http.StatusNotFound, "not_found", "domain not found")
|
||||
return
|
||||
}
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var in dto.CreateRecordSetRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
||||
return
|
||||
}
|
||||
if err := dto.DNSValidate(in); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "validation_error", err.Error())
|
||||
return
|
||||
}
|
||||
t := strings.ToUpper(in.Type)
|
||||
if t == "CNAME" && len(in.Values) != 1 {
|
||||
utils.WriteError(w, http.StatusBadRequest, "validation_error", "CNAME requires exactly one value")
|
||||
return
|
||||
}
|
||||
|
||||
rel := normLowerNoDot(in.Name)
|
||||
fq := fqdn(domain.DomainName, rel)
|
||||
|
||||
// Pre-flight: block duplicate tuple and protect from non-autoglue rows
|
||||
var existing models.RecordSet
|
||||
if err := db.Where("domain_id = ? AND LOWER(name) = ? AND type = ?",
|
||||
domain.ID, strings.ToLower(rel), t).First(&existing).Error; err == nil {
|
||||
if existing.Owner != "" && existing.Owner != "autoglue" {
|
||||
utils.WriteError(w, http.StatusConflict, "ownership_conflict",
|
||||
"record with the same (name,type) exists but is not owned by autoglue")
|
||||
return
|
||||
}
|
||||
utils.WriteError(w, http.StatusConflict, "already_exists",
|
||||
"a record with the same (name,type) already exists; use PATCH to modify")
|
||||
return
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
valuesJSON, _ := json.Marshal(in.Values)
|
||||
fp, err := computeFingerprint(domain.ZoneID, fq, t, in.TTL, datatypes.JSON(valuesJSON))
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "fingerprint_error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
row := &models.RecordSet{
|
||||
DomainID: domain.ID,
|
||||
Name: rel,
|
||||
Type: t,
|
||||
TTL: in.TTL,
|
||||
Values: datatypes.JSON(valuesJSON),
|
||||
Fingerprint: fp,
|
||||
Status: "pending",
|
||||
LastError: "",
|
||||
Owner: "autoglue",
|
||||
}
|
||||
if err := db.Create(row).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||
return
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusCreated, recordOut(row))
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateRecordSet godoc
|
||||
//
|
||||
// @ID UpdateRecordSet
|
||||
// @Summary Update a record set (flips to pending for reconciliation)
|
||||
// @Tags DNS
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Record Set ID (UUID)"
|
||||
// @Param body body dto.UpdateRecordSetRequest true "Fields to update"
|
||||
// @Success 200 {object} dto.RecordSetResponse
|
||||
// @Failure 400 {string} string "validation error"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Router /dns/records/{id} [patch]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func UpdateRecordSet(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID")
|
||||
return
|
||||
}
|
||||
|
||||
var row models.RecordSet
|
||||
if err := db.
|
||||
Joins("Domain").
|
||||
Where(`record_sets.id = ? AND "Domain"."organization_id" = ?`, id, orgID).
|
||||
First(&row).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
utils.WriteError(w, http.StatusNotFound, "not_found", "record set not found")
|
||||
return
|
||||
}
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||
return
|
||||
}
|
||||
var domain models.Domain
|
||||
if err := db.Where("id = ?", row.DomainID).First(&domain).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var in dto.UpdateRecordSetRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
||||
return
|
||||
}
|
||||
if err := dto.DNSValidate(in); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "validation_error", err.Error())
|
||||
return
|
||||
}
|
||||
if row.Owner != "" && row.Owner != "autoglue" {
|
||||
utils.WriteError(w, http.StatusConflict, "ownership_conflict",
|
||||
"record is not owned by autoglue; refuse to modify")
|
||||
return
|
||||
}
|
||||
|
||||
// Mutations
|
||||
if in.Name != nil {
|
||||
row.Name = normLowerNoDot(*in.Name)
|
||||
}
|
||||
if in.Type != nil {
|
||||
row.Type = strings.ToUpper(*in.Type)
|
||||
}
|
||||
if in.TTL != nil {
|
||||
row.TTL = in.TTL
|
||||
}
|
||||
if in.Values != nil {
|
||||
t := row.Type
|
||||
if in.Type != nil {
|
||||
t = strings.ToUpper(*in.Type)
|
||||
}
|
||||
if t == "CNAME" && len(*in.Values) != 1 {
|
||||
utils.WriteError(w, http.StatusBadRequest, "validation_error", "CNAME requires exactly one value")
|
||||
return
|
||||
}
|
||||
b, _ := json.Marshal(*in.Values)
|
||||
row.Values = datatypes.JSON(b)
|
||||
}
|
||||
|
||||
if in.Status != nil {
|
||||
row.Status = *in.Status
|
||||
} else {
|
||||
row.Status = "pending"
|
||||
row.LastError = ""
|
||||
}
|
||||
|
||||
fq := fqdn(domain.DomainName, row.Name)
|
||||
fp, err := computeFingerprint(domain.ZoneID, fq, row.Type, row.TTL, row.Values)
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "fingerprint_error", err.Error())
|
||||
return
|
||||
}
|
||||
row.Fingerprint = fp
|
||||
|
||||
if err := db.Save(&row).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||
return
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusOK, recordOut(&row))
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteRecordSet godoc
|
||||
//
|
||||
// @ID DeleteRecordSet
|
||||
// @Summary Delete a record set (API removes row; worker can optionally handle external deletion policy)
|
||||
// @Tags DNS
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Record Set ID (UUID)"
|
||||
// @Success 204
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Router /dns/records/{id} [delete]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func DeleteRecordSet(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID")
|
||||
return
|
||||
}
|
||||
sub := db.Model(&models.RecordSet{}).
|
||||
Select("record_sets.id").
|
||||
Joins("JOIN domains ON domains.id = record_sets.domain_id").
|
||||
Where("record_sets.id = ? AND domains.organization_id = ?", id, orgID)
|
||||
|
||||
res := db.Where("id IN (?)", sub).Delete(&models.RecordSet{})
|
||||
if res.Error != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", res.Error.Error())
|
||||
return
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
utils.WriteError(w, http.StatusNotFound, "not_found", "record set not found")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Out mappers ----------
|
||||
|
||||
func domainOut(m *models.Domain) dto.DomainResponse {
|
||||
return dto.DomainResponse{
|
||||
ID: m.ID.String(),
|
||||
OrganizationID: m.OrganizationID.String(),
|
||||
DomainName: m.DomainName,
|
||||
ZoneID: m.ZoneID,
|
||||
Status: m.Status,
|
||||
LastError: m.LastError,
|
||||
CredentialID: m.CredentialID.String(),
|
||||
CreatedAt: m.CreatedAt.UTC().Format(time.RFC3339),
|
||||
UpdatedAt: m.UpdatedAt.UTC().Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
func recordOut(r *models.RecordSet) dto.RecordSetResponse {
|
||||
vals := r.Values
|
||||
if len(vals) == 0 {
|
||||
vals = datatypes.JSON("[]")
|
||||
}
|
||||
return dto.RecordSetResponse{
|
||||
ID: r.ID.String(),
|
||||
DomainID: r.DomainID.String(),
|
||||
Name: r.Name,
|
||||
Type: r.Type,
|
||||
TTL: r.TTL,
|
||||
Values: []byte(vals),
|
||||
Fingerprint: r.Fingerprint,
|
||||
Status: r.Status,
|
||||
LastError: r.LastError,
|
||||
Owner: r.Owner,
|
||||
CreatedAt: r.CreatedAt.UTC().Format(time.RFC3339),
|
||||
UpdatedAt: r.UpdatedAt.UTC().Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
34
internal/handlers/dto/clusters.go
Normal file
34
internal/handlers/dto/clusters.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ClusterResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
Region string `json:"region"`
|
||||
Status string `json:"status"`
|
||||
CaptainDomain string `json:"captain_domain"`
|
||||
ClusterLoadBalancer string `json:"cluster_load_balancer"`
|
||||
RandomToken string `json:"random_token"`
|
||||
CertificateKey string `json:"certificate_key"`
|
||||
ControlLoadBalancer string `json:"control_load_balancer"`
|
||||
NodePools []NodePoolResponse `json:"node_pools,omitempty"`
|
||||
BastionServer *ServerResponse `json:"bastion_server,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CreateClusterRequest struct {
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
Region string `json:"region"`
|
||||
Status string `json:"status"`
|
||||
CaptainDomain string `json:"captain_domain"`
|
||||
ClusterLoadBalancer *string `json:"cluster_load_balancer"`
|
||||
ControlLoadBalancer *string `json:"control_load_balancer"`
|
||||
}
|
||||
138
internal/handlers/dto/credentials.go
Normal file
138
internal/handlers/dto/credentials.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
// RawJSON is a swagger-friendly wrapper for json.RawMessage.
|
||||
type RawJSON = json.RawMessage
|
||||
|
||||
var Validate = validator.New()
|
||||
|
||||
func init() {
|
||||
_ = Validate.RegisterValidation("awsarn", func(fl validator.FieldLevel) bool {
|
||||
v := fl.Field().String()
|
||||
return len(v) > 10 && len(v) < 2048 && len(v) >= 4 && v[:4] == "arn:"
|
||||
})
|
||||
}
|
||||
|
||||
/*** Shapes for secrets ***/
|
||||
|
||||
type AWSCredential struct {
|
||||
AccessKeyID string `json:"access_key_id" validate:"required,alphanum,len=20"`
|
||||
SecretAccessKey string `json:"secret_access_key" validate:"required"`
|
||||
Region string `json:"region" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type BasicAuth struct {
|
||||
Username string `json:"username" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
type APIToken struct {
|
||||
Token string `json:"token" validate:"required"`
|
||||
}
|
||||
|
||||
type OAuth2Credential struct {
|
||||
ClientID string `json:"client_id" validate:"required"`
|
||||
ClientSecret string `json:"client_secret" validate:"required"`
|
||||
RefreshToken string `json:"refresh_token" validate:"required"`
|
||||
}
|
||||
|
||||
/*** Shapes for scopes ***/
|
||||
|
||||
type AWSProviderScope struct{}
|
||||
|
||||
type AWSServiceScope struct {
|
||||
Service string `json:"service" validate:"required,oneof=route53 s3 ec2 iam rds dynamodb"`
|
||||
}
|
||||
|
||||
type AWSResourceScope struct {
|
||||
ARN string `json:"arn" validate:"required,awsarn"`
|
||||
}
|
||||
|
||||
/*** Registries ***/
|
||||
|
||||
type ProviderDef struct {
|
||||
New func() any
|
||||
Validate func(any) error
|
||||
}
|
||||
|
||||
type ScopeDef struct {
|
||||
New func() any
|
||||
Validate func(any) error
|
||||
Specificity int // 0=provider, 1=service, 2=resource
|
||||
}
|
||||
|
||||
// Secret shapes per provider/kind/version
|
||||
|
||||
var CredentialRegistry = map[string]map[string]map[int]ProviderDef{
|
||||
"aws": {
|
||||
"aws_access_key": {
|
||||
1: {New: func() any { return &AWSCredential{} }, Validate: func(x any) error { return Validate.Struct(x) }},
|
||||
},
|
||||
},
|
||||
"cloudflare": {"api_token": {1: {New: func() any { return &APIToken{} }, Validate: func(x any) error { return Validate.Struct(x) }}}},
|
||||
"hetzner": {"api_token": {1: {New: func() any { return &APIToken{} }, Validate: func(x any) error { return Validate.Struct(x) }}}},
|
||||
"digitalocean": {"api_token": {1: {New: func() any { return &APIToken{} }, Validate: func(x any) error { return Validate.Struct(x) }}}},
|
||||
"generic": {
|
||||
"basic_auth": {1: {New: func() any { return &BasicAuth{} }, Validate: func(x any) error { return Validate.Struct(x) }}},
|
||||
"oauth2": {1: {New: func() any { return &OAuth2Credential{} }, Validate: func(x any) error { return Validate.Struct(x) }}},
|
||||
},
|
||||
}
|
||||
|
||||
// Scope shapes per provider/scopeKind/version
|
||||
|
||||
var ScopeRegistry = map[string]map[string]map[int]ScopeDef{
|
||||
"aws": {
|
||||
"provider": {1: {New: func() any { return &AWSProviderScope{} }, Validate: func(any) error { return nil }, Specificity: 0}},
|
||||
"service": {1: {New: func() any { return &AWSServiceScope{} }, Validate: func(x any) error { return Validate.Struct(x) }, Specificity: 1}},
|
||||
"resource": {1: {New: func() any { return &AWSResourceScope{} }, Validate: func(x any) error { return Validate.Struct(x) }, Specificity: 2}},
|
||||
},
|
||||
}
|
||||
|
||||
/*** API DTOs used by swagger ***/
|
||||
|
||||
// CreateCredentialRequest represents the POST /credentials payload
|
||||
type CreateCredentialRequest struct {
|
||||
Provider string `json:"provider" validate:"required,oneof=aws cloudflare hetzner digitalocean generic"`
|
||||
Kind string `json:"kind" validate:"required"` // aws_access_key, api_token, basic_auth, oauth2
|
||||
SchemaVersion int `json:"schema_version" validate:"required,gte=1"` // secret schema version
|
||||
Name string `json:"name" validate:"omitempty,max=100"` // human label
|
||||
ScopeKind string `json:"scope_kind" validate:"required,oneof=provider service resource"`
|
||||
ScopeVersion int `json:"scope_version" validate:"required,gte=1"` // scope schema version
|
||||
Scope RawJSON `json:"scope" validate:"required" swaggertype:"object"` // {"service":"route53"} or {"arn":"..."}
|
||||
AccountID string `json:"account_id,omitempty" validate:"omitempty,max=32"`
|
||||
Region string `json:"region,omitempty" validate:"omitempty,max=32"`
|
||||
Secret RawJSON `json:"secret" validate:"required" swaggertype:"object"` // encrypted later
|
||||
}
|
||||
|
||||
// UpdateCredentialRequest represents PATCH /credentials/{id}
|
||||
type UpdateCredentialRequest struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
AccountID *string `json:"account_id,omitempty"`
|
||||
Region *string `json:"region,omitempty"`
|
||||
ScopeKind *string `json:"scope_kind,omitempty"`
|
||||
ScopeVersion *int `json:"scope_version,omitempty"`
|
||||
Scope *RawJSON `json:"scope,omitempty" swaggertype:"object"`
|
||||
Secret *RawJSON `json:"secret,omitempty" swaggertype:"object"` // set if rotating
|
||||
|
||||
}
|
||||
|
||||
// CredentialOut is what we return (no secrets)
|
||||
type CredentialOut struct {
|
||||
ID string `json:"id"`
|
||||
Provider string `json:"provider"`
|
||||
Kind string `json:"kind"`
|
||||
SchemaVersion int `json:"schema_version"`
|
||||
Name string `json:"name"`
|
||||
ScopeKind string `json:"scope_kind"`
|
||||
ScopeVersion int `json:"scope_version"`
|
||||
Scope RawJSON `json:"scope" swaggertype:"object"`
|
||||
AccountID string `json:"account_id,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
103
internal/handlers/dto/dns.go
Normal file
103
internal/handlers/dto/dns.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
var dnsValidate = validator.New()
|
||||
|
||||
func init() {
|
||||
_ = dnsValidate.RegisterValidation("fqdn", func(fl validator.FieldLevel) bool {
|
||||
s := strings.TrimSpace(fl.Field().String())
|
||||
if s == "" || len(s) > 253 {
|
||||
return false
|
||||
}
|
||||
// Minimal: lower-cased, no trailing dot in our API (normalize server-side)
|
||||
// You can add stricter checks later.
|
||||
return !strings.HasPrefix(s, ".") && !strings.Contains(s, "..")
|
||||
})
|
||||
_ = dnsValidate.RegisterValidation("rrtype", func(fl validator.FieldLevel) bool {
|
||||
switch strings.ToUpper(fl.Field().String()) {
|
||||
case "A", "AAAA", "CNAME", "TXT", "MX", "NS", "SRV", "CAA":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---- Domains ----
|
||||
|
||||
type CreateDomainRequest struct {
|
||||
DomainName string `json:"domain_name" validate:"required,fqdn"`
|
||||
CredentialID string `json:"credential_id" validate:"required,uuid4"`
|
||||
ZoneID string `json:"zone_id,omitempty" validate:"omitempty,max=128"`
|
||||
}
|
||||
|
||||
type UpdateDomainRequest struct {
|
||||
CredentialID *string `json:"credential_id,omitempty" validate:"omitempty,uuid4"`
|
||||
ZoneID *string `json:"zone_id,omitempty" validate:"omitempty,max=128"`
|
||||
Status *string `json:"status,omitempty" validate:"omitempty,oneof=pending provisioning ready failed"`
|
||||
DomainName *string `json:"domain_name,omitempty" validate:"omitempty,fqdn"`
|
||||
}
|
||||
|
||||
type DomainResponse struct {
|
||||
ID string `json:"id"`
|
||||
OrganizationID string `json:"organization_id"`
|
||||
DomainName string `json:"domain_name"`
|
||||
ZoneID string `json:"zone_id"`
|
||||
Status string `json:"status"`
|
||||
LastError string `json:"last_error"`
|
||||
CredentialID string `json:"credential_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ---- Record Sets ----
|
||||
|
||||
type AliasTarget struct {
|
||||
HostedZoneID string `json:"hosted_zone_id" validate:"required"`
|
||||
DNSName string `json:"dns_name" validate:"required"`
|
||||
EvaluateTargetHealth bool `json:"evaluate_target_health"`
|
||||
}
|
||||
|
||||
type CreateRecordSetRequest struct {
|
||||
// Name relative to domain ("endpoint") OR FQDN ("endpoint.example.com").
|
||||
// Server normalizes to relative.
|
||||
Name string `json:"name" validate:"required,max=253"`
|
||||
Type string `json:"type" validate:"required,rrtype"`
|
||||
TTL *int `json:"ttl,omitempty" validate:"omitempty,gte=1,lte=86400"`
|
||||
Values []string `json:"values" validate:"omitempty,dive,min=1,max=1024"`
|
||||
}
|
||||
|
||||
type UpdateRecordSetRequest struct {
|
||||
// Any change flips status back to pending (worker will UPSERT)
|
||||
Name *string `json:"name,omitempty" validate:"omitempty,max=253"`
|
||||
Type *string `json:"type,omitempty" validate:"omitempty,rrtype"`
|
||||
TTL *int `json:"ttl,omitempty" validate:"omitempty,gte=1,lte=86400"`
|
||||
Values *[]string `json:"values,omitempty" validate:"omitempty,dive,min=1,max=1024"`
|
||||
Status *string `json:"status,omitempty" validate:"omitempty,oneof=pending provisioning ready failed"`
|
||||
}
|
||||
|
||||
type RecordSetResponse struct {
|
||||
ID string `json:"id"`
|
||||
DomainID string `json:"domain_id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
TTL *int `json:"ttl,omitempty"`
|
||||
Values json.RawMessage `json:"values" swaggertype:"object"` // []string JSON
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
Status string `json:"status"`
|
||||
LastError string `json:"last_error"`
|
||||
Owner string `json:"owner"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// DNSValidate Quick helper to validate DTOs in handlers
|
||||
func DNSValidate(i any) error {
|
||||
return dnsValidate.Struct(i)
|
||||
}
|
||||
@@ -57,6 +57,6 @@ type PageJob struct {
|
||||
type EnqueueRequest struct {
|
||||
Queue string `json:"queue" example:"default"`
|
||||
Type string `json:"type" example:"email.send"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
Payload json.RawMessage `json:"payload" swaggertype:"object"`
|
||||
RunAt *time.Time `json:"run_at" example:"2025-11-05T08:00:00Z"`
|
||||
}
|
||||
|
||||
@@ -370,6 +370,63 @@ func DeleteServer(db *gorm.DB) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// ResetServerHostKey godoc
|
||||
//
|
||||
// @ID ResetServerHostKey
|
||||
// @Summary Reset SSH host key (org scoped)
|
||||
// @Description Clears the stored SSH host key for this server. The next SSH connection will re-learn the host key (trust-on-first-use).
|
||||
// @Tags Servers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Server ID (UUID)"
|
||||
// @Success 200 {object} dto.ServerResponse
|
||||
// @Failure 400 {string} string "invalid id"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "reset failed"
|
||||
// @Router /servers/{id}/reset-hostkey [post]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func ResetServerHostKey(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "id_invalid", "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
var server models.Server
|
||||
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&server).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
utils.WriteError(w, http.StatusNotFound, "server_not_found", "server not found")
|
||||
return
|
||||
}
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to get server")
|
||||
return
|
||||
}
|
||||
|
||||
// Clear stored host key so next SSH handshake will TOFU and persist a new one.
|
||||
server.SSHHostKey = ""
|
||||
server.SSHHostKeyAlgo = ""
|
||||
|
||||
if err := db.Save(&server).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to reset host key")
|
||||
return
|
||||
}
|
||||
|
||||
utils.WriteJSON(w, http.StatusOK, server)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
func validStatus(status string) bool {
|
||||
|
||||
65
internal/handlers/version.go
Normal file
65
internal/handlers/version.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
|
||||
"github.com/glueops/autoglue/internal/utils"
|
||||
"github.com/glueops/autoglue/internal/version"
|
||||
)
|
||||
|
||||
type VersionResponse struct {
|
||||
Version string `json:"version" example:"1.4.2"`
|
||||
Commit string `json:"commit" example:"a1b2c3d"`
|
||||
Built string `json:"built" example:"2025-11-08T12:34:56Z"`
|
||||
BuiltBy string `json:"builtBy" example:"ci"`
|
||||
Go string `json:"go" example:"go1.23.3"`
|
||||
GOOS string `json:"goOS" example:"linux"`
|
||||
GOARCH string `json:"goArch" example:"amd64"`
|
||||
VCS string `json:"vcs,omitempty" example:"git"`
|
||||
Revision string `json:"revision,omitempty" example:"a1b2c3d4e5f6abcdef"`
|
||||
CommitTime string `json:"commitTime,omitempty" example:"2025-11-08T12:31:00Z"`
|
||||
Modified *bool `json:"modified,omitempty" example:"false"`
|
||||
}
|
||||
|
||||
// Version godoc
|
||||
//
|
||||
// @Summary Service version information
|
||||
// @Description Returns build/runtime metadata for the running service.
|
||||
// @Tags Meta
|
||||
// @ID Version // operationId
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} VersionResponse
|
||||
// @Router /version [get]
|
||||
func Version(w http.ResponseWriter, r *http.Request) {
|
||||
resp := VersionResponse{
|
||||
Version: version.Version,
|
||||
Commit: version.Commit,
|
||||
Built: version.Date,
|
||||
BuiltBy: version.BuiltBy,
|
||||
Go: runtime.Version(),
|
||||
GOOS: runtime.GOOS,
|
||||
GOARCH: runtime.GOARCH,
|
||||
}
|
||||
|
||||
if bi, ok := debug.ReadBuildInfo(); ok {
|
||||
for _, s := range bi.Settings {
|
||||
switch s.Key {
|
||||
case "vcs":
|
||||
resp.VCS = s.Value
|
||||
case "vcs.revision":
|
||||
resp.Revision = s.Value
|
||||
case "vcs.time":
|
||||
resp.CommitTime = s.Value
|
||||
case "vcs.modified":
|
||||
if b, err := strconv.ParseBool(s.Value); err == nil {
|
||||
resp.Modified = &b
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
18
internal/models/backup.go
Normal file
18
internal/models/backup.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Backup struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index;uniqueIndex:uniq_org_credential,priority:1"`
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
||||
Enabled bool `gorm:"not null;default:false" json:"enabled"`
|
||||
CredentialID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex:uniq_org_credential,priority:2" json:"credential_id"`
|
||||
Credential Credential `gorm:"foreignKey:CredentialID" json:"credential,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"`
|
||||
}
|
||||
@@ -14,16 +14,20 @@ type Cluster struct {
|
||||
Provider string `json:"provider"`
|
||||
Region string `json:"region"`
|
||||
Status string `json:"status"`
|
||||
CaptainDomain string `gorm:"not null" json:"captain_domain"`
|
||||
ClusterLoadBalancer string `json:"cluster_load_balancer"`
|
||||
RandomToken string `json:"random_token"`
|
||||
CertificateKey string `json:"certificate_key"`
|
||||
EncryptedKubeconfig string `gorm:"type:text" json:"-"`
|
||||
KubeIV string `json:"-"`
|
||||
KubeTag string `json:"-"`
|
||||
NodePools []NodePool `gorm:"many2many:cluster_node_pools;constraint:OnDelete:CASCADE" json:"node_pools,omitempty"`
|
||||
BastionServerID *uuid.UUID `gorm:"type:uuid" json:"bastion_server_id,omitempty"`
|
||||
BastionServer *Server `gorm:"foreignKey:BastionServerID" json:"bastion_server,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"`
|
||||
CaptainDomain string `gorm:"not null" json:"captain_domain"` // nonprod.earth.onglueops.rocks
|
||||
AppsLoadBalancer string `json:"cluster_load_balancer"` // {public_ip: 1.2.3.4, private_ip: 10.0.30.1, name: apps.CaqptainDomain}
|
||||
GlueOpsLoadBalancer string `json:"control_load_balancer"` // {public_ip: 5.6.7.8, private_ip: 10.0.22.1, name: CaptainDomain}
|
||||
|
||||
ControlPlane string `json:"control_plane"` // <- dns cntlpn
|
||||
|
||||
RandomToken string `json:"random_token"`
|
||||
CertificateKey string `json:"certificate_key"`
|
||||
EncryptedKubeconfig string `gorm:"type:text" json:"-"`
|
||||
KubeIV string `json:"-"`
|
||||
KubeTag string `json:"-"`
|
||||
NodePools []NodePool `gorm:"many2many:cluster_node_pools;constraint:OnDelete:CASCADE" json:"node_pools,omitempty"`
|
||||
BastionServerID *uuid.UUID `gorm:"type:uuid" json:"bastion_server_id,omitempty"`
|
||||
BastionServer *Server `gorm:"foreignKey:BastionServerID" json:"bastion_server,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"`
|
||||
}
|
||||
|
||||
29
internal/models/credential.go
Normal file
29
internal/models/credential.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
type Credential struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index;uniqueIndex:uniq_org_provider_scopekind_scope,priority:1" json:"organization_id"`
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
||||
Provider string `gorm:"type:varchar(50);not null;uniqueIndex:uniq_org_provider_scopekind_scope,priority:2;index:idx_provider_kind"`
|
||||
Kind string `gorm:"type:varchar(50);not null;index:idx_provider_kind;index:idx_kind_scope"`
|
||||
ScopeKind string `gorm:"type:varchar(20);not null;uniqueIndex:uniq_org_provider_scopekind_scope,priority:3"`
|
||||
Scope datatypes.JSON `gorm:"type:jsonb;not null;default:'{}';index:idx_kind_scope"`
|
||||
ScopeFingerprint string `gorm:"type:char(64);not null;uniqueIndex:uniq_org_provider_scopekind_scope,priority:4;index"`
|
||||
SchemaVersion int `gorm:"not null;default:1"`
|
||||
Name string `gorm:"type:varchar(100);not null;default:''"`
|
||||
ScopeVersion int `gorm:"not null;default:1"`
|
||||
AccountID string `gorm:"type:varchar(32)"`
|
||||
Region string `gorm:"type:varchar(32)"`
|
||||
EncryptedData string `gorm:"not null"`
|
||||
IV string `gorm:"not null"`
|
||||
Tag string `gorm:"not null"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()" format:"date-time"`
|
||||
}
|
||||
41
internal/models/domain.go
Normal file
41
internal/models/domain.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
type Domain struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index;uniqueIndex:uniq_org_domain,priority:1"`
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
||||
DomainName string `gorm:"type:varchar(253);not null;uniqueIndex:uniq_org_domain,priority:2"`
|
||||
ZoneID string `gorm:"type:varchar(128);not null;default:''"` // backfilled for R53 (e.g. "/hostedzone/Z123...")
|
||||
Status string `gorm:"type:varchar(20);not null;default:'pending'"` // pending, provisioning, ready, failed
|
||||
LastError string `gorm:"type:text;not null;default:''"`
|
||||
CredentialID uuid.UUID `gorm:"type:uuid;not null" json:"credential_id"`
|
||||
Credential Credential `gorm:"foreignKey:CredentialID" json:"credential,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"`
|
||||
}
|
||||
|
||||
type RecordSet struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
|
||||
DomainID uuid.UUID `gorm:"type:uuid;not null;index"`
|
||||
Domain Domain `gorm:"foreignKey:DomainID;constraint:OnDelete:CASCADE"`
|
||||
Name string `gorm:"type:varchar(253);not null"` // e.g. "endpoint" (relative to DomainName)
|
||||
Type string `gorm:"type:varchar(10);not null;index"` // A, AAAA, CNAME, TXT, MX, SRV, NS, CAA...
|
||||
TTL *int `gorm:""` // nil for alias targets (Route 53 ignores TTL for alias)
|
||||
Values datatypes.JSON `gorm:"type:jsonb;not null;default:'[]'"`
|
||||
Fingerprint string `gorm:"type:char(64);not null;index"` // sha256 of canonical(name,type,ttl,values|alias)
|
||||
Status string `gorm:"type:varchar(20);not null;default:'pending'"`
|
||||
Owner string `gorm:"type:varchar(16);not null;default:'unknown'"` // 'autoglue' | 'external' | 'unknown'
|
||||
LastError string `gorm:"type:text;not null;default:''"`
|
||||
_ struct{} `gorm:"uniqueIndex:uniq_domain_name_type,priority:1"` // tag holder
|
||||
_ struct{} `gorm:"uniqueIndex:uniq_domain_name_type,priority:2"`
|
||||
_ struct{} `gorm:"uniqueIndex:uniq_domain_name_type,priority:3"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"`
|
||||
}
|
||||
@@ -12,7 +12,7 @@ type NodePool struct {
|
||||
Annotations []Annotation `gorm:"many2many:node_annotations;constraint:OnDelete:CASCADE" json:"annotations,omitempty"`
|
||||
Labels []Label `gorm:"many2many:node_labels;constraint:OnDelete:CASCADE" json:"labels,omitempty"`
|
||||
Taints []Taint `gorm:"many2many:node_taints;constraint:OnDelete:CASCADE" json:"taints,omitempty"`
|
||||
//Clusters []Cluster `gorm:"many2many:cluster_node_pools;constraint:OnDelete:CASCADE" json:"clusters,omitempty"`
|
||||
Clusters []Cluster `gorm:"many2many:cluster_node_pools;constraint:OnDelete:CASCADE" json:"clusters,omitempty"`
|
||||
//Topology string `gorm:"not null,default:'stacked'" json:"topology,omitempty"` // stacked or external
|
||||
Role string `gorm:"not null,default:'worker'" json:"role,omitempty"` // master, worker, or etcd (etcd only if topology = external
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ type Server struct {
|
||||
Role string `gorm:"not null" json:"role" enums:"master,worker,bastion"` // e.g., "master", "worker", "bastion"
|
||||
Status string `gorm:"default:'pending'" json:"status" enums:"pending, provisioning, ready, failed"` // pending, provisioning, ready, failed
|
||||
NodePools []NodePool `gorm:"many2many:node_servers;constraint:OnDelete:CASCADE" json:"node_pools,omitempty"`
|
||||
SSHHostKey string `gorm:"column:ssh_host_key"`
|
||||
SSHHostKeyAlgo string `gorm:"column:ssh_host_key_algo"`
|
||||
CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at" format:"date-time"`
|
||||
UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at" format:"date-time"`
|
||||
}
|
||||
|
||||
79
internal/web/dist/assets/index-52pog1DZ.js
vendored
Normal file
79
internal/web/dist/assets/index-52pog1DZ.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
internal/web/dist/assets/index-52pog1DZ.js.br
vendored
Normal file
BIN
internal/web/dist/assets/index-52pog1DZ.js.br
vendored
Normal file
Binary file not shown.
BIN
internal/web/dist/assets/index-52pog1DZ.js.gz
vendored
Normal file
BIN
internal/web/dist/assets/index-52pog1DZ.js.gz
vendored
Normal file
Binary file not shown.
1
internal/web/dist/assets/index-52pog1DZ.js.map
vendored
Normal file
1
internal/web/dist/assets/index-52pog1DZ.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
internal/web/dist/assets/index-tX4seA_J.css
vendored
Normal file
2
internal/web/dist/assets/index-tX4seA_J.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
internal/web/dist/assets/index-tX4seA_J.css.br
vendored
Normal file
BIN
internal/web/dist/assets/index-tX4seA_J.css.br
vendored
Normal file
Binary file not shown.
BIN
internal/web/dist/assets/index-tX4seA_J.css.gz
vendored
Normal file
BIN
internal/web/dist/assets/index-tX4seA_J.css.gz
vendored
Normal file
Binary file not shown.
4
internal/web/dist/assets/react-B75e6Si-.js
vendored
Normal file
4
internal/web/dist/assets/react-B75e6Si-.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
internal/web/dist/assets/react-B75e6Si-.js.br
vendored
Normal file
BIN
internal/web/dist/assets/react-B75e6Si-.js.br
vendored
Normal file
Binary file not shown.
BIN
internal/web/dist/assets/react-B75e6Si-.js.gz
vendored
Normal file
BIN
internal/web/dist/assets/react-B75e6Si-.js.gz
vendored
Normal file
Binary file not shown.
1
internal/web/dist/assets/react-B75e6Si-.js.map
vendored
Normal file
1
internal/web/dist/assets/react-B75e6Si-.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
15
internal/web/dist/index.html
vendored
Normal file
15
internal/web/dist/index.html
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AutoGlue</title>
|
||||
<script type="module" crossorigin src="/assets/index-52pog1DZ.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/react-B75e6Si-.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-tX4seA_J.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
internal/web/dist/index.html.br
vendored
Normal file
BIN
internal/web/dist/index.html.br
vendored
Normal file
Binary file not shown.
BIN
internal/web/dist/index.html.gz
vendored
Normal file
BIN
internal/web/dist/index.html.gz
vendored
Normal file
Binary file not shown.
2
internal/web/dist/vite.svg
vendored
Normal file
2
internal/web/dist/vite.svg
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88"
|
||||
height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -61,6 +61,7 @@ func SPAHandler() (http.Handler, error) {
|
||||
if strings.HasPrefix(r.URL.Path, "/api/") ||
|
||||
r.URL.Path == "/api" ||
|
||||
strings.HasPrefix(r.URL.Path, "/swagger") ||
|
||||
strings.HasPrefix(r.URL.Path, "/db-studio") ||
|
||||
strings.HasPrefix(r.URL.Path, "/debug/pprof") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM postgres:17.6@sha256:00bc86618629af00d2937fdc5a5d63db3ff8450acf52f0636ec813c7f4902929
|
||||
FROM postgres:17.7@sha256:44640f16641cf36716cabd011e2f7eb4742b6b6b19f4488ddcbb7c250e5c9753
|
||||
|
||||
RUN cd /var/lib/postgresql/ && \
|
||||
openssl req -new -text -passout pass:abcd -subj /CN=localhost -out server.req -keyout privkey.pem && \
|
||||
|
||||
137
tools/pgweb_fetch.go
Normal file
137
tools/pgweb_fetch.go
Normal file
@@ -0,0 +1,137 @@
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Target struct {
|
||||
Name string
|
||||
URL string
|
||||
SHA256 string
|
||||
}
|
||||
|
||||
const version = "0.16.2"
|
||||
|
||||
func main() {
|
||||
targets := []Target{
|
||||
{
|
||||
Name: "pgweb-linux-amd64",
|
||||
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_linux_amd64.zip", version),
|
||||
SHA256: "3d6c2063e1040b8a625eb7c43c9b84f8ed12cfc9a798eacbce85179963ee2554",
|
||||
},
|
||||
{
|
||||
Name: "pgweb-linux-arm64",
|
||||
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_linux_arm64.zip", version),
|
||||
SHA256: "079c698a323ed6431ce7e6343ee5847c7da62afbf45dfb2e78f8289d7b381783",
|
||||
},
|
||||
{
|
||||
Name: "pgweb-darwin-amd64",
|
||||
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_darwin_amd64.zip", version),
|
||||
SHA256: "c0a098e2eb9cf9f7c20161a2947522eb67eacbf2b6c3389c2f8e8c5ed7238957",
|
||||
},
|
||||
{
|
||||
Name: "pgweb-darwin-arm64",
|
||||
URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_darwin_arm64.zip", version),
|
||||
SHA256: "c8f5fca847f461ba22a619e2d96cb1656cefdffd8f2aef2340e14fc5b518d3a2",
|
||||
},
|
||||
}
|
||||
|
||||
outDir := filepath.Join("internal", "web", "pgwebbin")
|
||||
_ = os.MkdirAll(outDir, 0o755)
|
||||
|
||||
for _, t := range targets {
|
||||
destZip := filepath.Join(outDir, t.Name+".zip")
|
||||
fmt.Printf("Downloading %s...\n", t.URL)
|
||||
if err := downloadFile(destZip, t.URL); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
binPath := filepath.Join(outDir, t.Name)
|
||||
if err := unzipSingle(destZip, binPath); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_ = os.Remove(destZip)
|
||||
|
||||
// Make executable
|
||||
if err := os.Chmod(binPath, 0o755); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("Saved %s\n", binPath)
|
||||
|
||||
// Compute checksum
|
||||
sum, _ := fileSHA256(binPath)
|
||||
fmt.Printf(" SHA256: %s\n", sum)
|
||||
}
|
||||
}
|
||||
|
||||
func downloadFile(dest, url string) error {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
out, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
func fileSHA256(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func unzipSingle(zipPath, outPath string) error {
|
||||
zr, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer zr.Close()
|
||||
|
||||
if len(zr.File) == 0 {
|
||||
return fmt.Errorf("zip file %s is empty", zipPath)
|
||||
}
|
||||
|
||||
f := zr.File[0]
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
out, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
if _, err := io.Copy(out, rc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -56,7 +56,7 @@
|
||||
"react-router-dom": "^7.9.5",
|
||||
"recharts": "2.15.4",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^4.1.12"
|
||||
@@ -65,12 +65,10 @@
|
||||
"@eslint/js": "9.39.1",
|
||||
"@ianvs/prettier-plugin-sort-imports": "4.7.0",
|
||||
"@types/node": "24.10.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"@vitejs/plugin-react": "5.1.0",
|
||||
"eslint": "9.39.1",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-prettier": "5.5.4",
|
||||
"eslint-plugin-react-hooks": "7.0.1",
|
||||
"eslint-plugin-react-refresh": "0.4.24",
|
||||
"globals": "16.5.0",
|
||||
@@ -79,7 +77,7 @@
|
||||
"shadcn": "3.5.0",
|
||||
"tw-animate-css": "1.4.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.46.3",
|
||||
"typescript-eslint": "8.46.4",
|
||||
"vite": "7.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Route, Routes } from "react-router-dom"
|
||||
import { ProtectedRoute } from "@/components/protected-route.tsx"
|
||||
import { AnnotationPage } from "@/pages/annotations/annotation-page.tsx"
|
||||
import { Login } from "@/pages/auth/login.tsx"
|
||||
import { CredentialPage } from "@/pages/credentials/credential-page.tsx"
|
||||
import { DnsPage } from "@/pages/dns/dns-page.tsx"
|
||||
import { JobsPage } from "@/pages/jobs/jobs-page.tsx"
|
||||
import { LabelsPage } from "@/pages/labels/labels-page.tsx"
|
||||
import { MePage } from "@/pages/me/me-page.tsx"
|
||||
@@ -33,6 +35,8 @@ export default function App() {
|
||||
<Route path="/labels" element={<LabelsPage />} />
|
||||
<Route path="/annotations" element={<AnnotationPage />} />
|
||||
<Route path="/node-pools" element={<NodePoolsPage />} />
|
||||
<Route path="/credentials" element={<CredentialPage />} />
|
||||
<Route path="/dns" element={<DnsPage />} />
|
||||
|
||||
<Route path="/admin/jobs" element={<JobsPage />} />
|
||||
</Route>
|
||||
|
||||
@@ -18,7 +18,12 @@ export const archerAdminApi = {
|
||||
return await archerAdmin.adminListArcherJobs(params)
|
||||
})
|
||||
},
|
||||
enqueue: (body: { queue: string; type: string; payload?: unknown; run_at?: string }) => {
|
||||
enqueue: (body: {
|
||||
queue: string
|
||||
type: string
|
||||
payload?: object | undefined
|
||||
run_at?: string
|
||||
}) => {
|
||||
return withRefresh(async () => {
|
||||
return await archerAdmin.adminEnqueueArcherJob({ body })
|
||||
})
|
||||
|
||||
32
ui/src/api/credentials.ts
Normal file
32
ui/src/api/credentials.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { withRefresh } from "@/api/with-refresh.ts"
|
||||
import type { DtoCreateCredentialRequest, DtoUpdateCredentialRequest } from "@/sdk"
|
||||
import { makeCredentialsApi } from "@/sdkClient.ts"
|
||||
|
||||
const credentials = makeCredentialsApi()
|
||||
|
||||
export const credentialsApi = {
|
||||
listCredentials: () =>
|
||||
withRefresh(async () => {
|
||||
return await credentials.listCredentials()
|
||||
}),
|
||||
createCredential: async (body: DtoCreateCredentialRequest) =>
|
||||
withRefresh(async () => {
|
||||
return await credentials.createCredential({ body })
|
||||
}),
|
||||
getCredential: async (id: string) =>
|
||||
withRefresh(async () => {
|
||||
return await credentials.getCredential({ id })
|
||||
}),
|
||||
deleteCredential: async (id: string) =>
|
||||
withRefresh(async () => {
|
||||
await credentials.deleteCredential({ id })
|
||||
}),
|
||||
updateCredential: async (id: string, body: DtoUpdateCredentialRequest) =>
|
||||
withRefresh(async () => {
|
||||
return await credentials.updateCredential({ id, body })
|
||||
}),
|
||||
revealCredential: async (id: string) =>
|
||||
withRefresh(async () => {
|
||||
return await credentials.revealCredential({ id })
|
||||
}),
|
||||
}
|
||||
49
ui/src/api/dns.ts
Normal file
49
ui/src/api/dns.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { withRefresh } from "@/api/with-refresh.ts"
|
||||
import type {
|
||||
DtoCreateDomainRequest,
|
||||
DtoCreateRecordSetRequest,
|
||||
DtoUpdateDomainRequest,
|
||||
DtoUpdateRecordSetRequest,
|
||||
} from "@/sdk"
|
||||
import { makeDnsApi } from "@/sdkClient.ts"
|
||||
|
||||
const dns = makeDnsApi()
|
||||
|
||||
export const dnsApi = {
|
||||
listDomains: () =>
|
||||
withRefresh(async () => {
|
||||
return await dns.listDomains()
|
||||
}),
|
||||
getDomain: (id: string) =>
|
||||
withRefresh(async () => {
|
||||
return await dns.getDomain({ id })
|
||||
}),
|
||||
createDomain: async (body: DtoCreateDomainRequest) =>
|
||||
withRefresh(async () => {
|
||||
return await dns.createDomain({ body })
|
||||
}),
|
||||
updateDomain: async (id: string, body: DtoUpdateDomainRequest) =>
|
||||
withRefresh(async () => {
|
||||
return await dns.updateDomain({ id, body })
|
||||
}),
|
||||
deleteDomain: async (id: string) =>
|
||||
withRefresh(async () => {
|
||||
return await dns.deleteDomain({ id })
|
||||
}),
|
||||
listRecordSetsByDomain: async (domainId: string) =>
|
||||
withRefresh(async () => {
|
||||
return await dns.listRecordSets({ domainId })
|
||||
}),
|
||||
createRecordSetsByDomain: async (domainId: string, body: DtoCreateRecordSetRequest) =>
|
||||
withRefresh(async () => {
|
||||
return await dns.createRecordSet({ domainId, body })
|
||||
}),
|
||||
updateRecordSetsByDomain: async (id: string, body: DtoUpdateRecordSetRequest) =>
|
||||
withRefresh(async () => {
|
||||
return await dns.updateRecordSet({ id, body })
|
||||
}),
|
||||
deleteRecordSetsByDomain: async (id: string) =>
|
||||
withRefresh(async () => {
|
||||
return await dns.deleteRecordSet({ id })
|
||||
}),
|
||||
}
|
||||
15
ui/src/api/footer.ts
Normal file
15
ui/src/api/footer.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export const metaApi = {
|
||||
footer: async () => {
|
||||
const res = await fetch("/api/v1/version", { cache: "no-store" })
|
||||
if (!res.ok) throw new Error("failed to fetch version")
|
||||
return (await res.json()) as {
|
||||
built: string
|
||||
builtBy: string
|
||||
commit: string
|
||||
go: string
|
||||
goArch: string
|
||||
goOS: string
|
||||
version: string
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { withRefresh } from "@/api/with-refresh.ts"
|
||||
import type { DtoCreateLabelRequest, DtoUpdateLabelRequest } from "@/sdk"
|
||||
import { makeLabelsApi } from "@/sdkClient.ts"
|
||||
|
||||
|
||||
const labels = makeLabelsApi()
|
||||
|
||||
export const labelsApi = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { meApi } from "@/api/me.ts"
|
||||
import { orgStore } from "@/auth/org.ts"
|
||||
import { authStore } from "@/auth/store.ts"
|
||||
import { Footer } from "@/layouts/footer.tsx"
|
||||
import { adminNav, mainNav, orgNav, userNav } from "@/layouts/nav-config.ts"
|
||||
import { OrgSwitcher } from "@/layouts/org-switcher.tsx"
|
||||
import { Topbar } from "@/layouts/topbar.tsx"
|
||||
@@ -171,11 +171,12 @@ export const AppShell = () => {
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
|
||||
<SidebarInset className="min-h-screen">
|
||||
<SidebarInset className="flex min-h-screen flex-col">
|
||||
<Topbar />
|
||||
<main className="p-4">
|
||||
<Outlet />
|
||||
</main>
|
||||
<Footer />
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
)
|
||||
|
||||
135
ui/src/layouts/footer.tsx
Normal file
135
ui/src/layouts/footer.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { memo, useMemo } from "react"
|
||||
import { metaApi } from "@/api/footer"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { Clipboard, ExternalLink, GitCommit, Info } from "lucide-react"
|
||||
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
|
||||
type VersionInfo = {
|
||||
built: string // ISO string or "unknown"
|
||||
builtBy: string
|
||||
commit: string
|
||||
go: string
|
||||
goArch: string
|
||||
goOS: string
|
||||
version: string
|
||||
}
|
||||
|
||||
function shortCommit(c?: string) {
|
||||
return c && c !== "none" ? c.slice(0, 7) : "none"
|
||||
}
|
||||
|
||||
function formatBuilt(built: string) {
|
||||
if (!built || built === "unknown") return "unknown"
|
||||
const d = new Date(built)
|
||||
return isNaN(+d) ? built : d.toLocaleString()
|
||||
}
|
||||
|
||||
function asClipboardText(v?: VersionInfo) {
|
||||
if (!v) return ""
|
||||
return `v${v.version} (${shortCommit(v.commit)}) • built ${v.built} • ${v.go} ${v.goOS}/${v.goArch}`
|
||||
}
|
||||
|
||||
export const Footer = memo(function Footer({ className }: { className?: string }) {
|
||||
const footerQ = useQuery({
|
||||
queryKey: ["footer"],
|
||||
queryFn: () => metaApi.footer() as Promise<VersionInfo>,
|
||||
staleTime: 60_000,
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
|
||||
const data = footerQ.data
|
||||
|
||||
const copyText = useMemo(() => asClipboardText(data), [data])
|
||||
|
||||
return (
|
||||
<footer className="bg-background text-muted-foreground w-full border-t px-3 py-2 text-xs sm:text-sm">
|
||||
<div className="mx-auto flex max-w-screen-2xl items-center justify-between">
|
||||
{/* Left: brand / copyright */}
|
||||
<div className="flex items-center gap-2 text-xs sm:text-sm">
|
||||
<span>© {new Date().getFullYear()} GlueOps</span>
|
||||
<Separator orientation="vertical" className="hidden h-4 sm:block" />
|
||||
<span className="hidden sm:block">All systems nominal.</span>
|
||||
</div>
|
||||
|
||||
{/* Right: version/meta */}
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs sm:text-sm">
|
||||
{footerQ.isLoading ? (
|
||||
<span className="animate-pulse">loading version…</span>
|
||||
) : footerQ.isError ? (
|
||||
<span className="text-destructive">version unavailable</span>
|
||||
) : data ? (
|
||||
<TooltipProvider>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="secondary" className="font-mono">
|
||||
{data.version}
|
||||
</Badge>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<GitCommit className="h-3.5 w-3.5" />
|
||||
<span className="font-mono">{shortCommit(data.commit)}</span>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<div className="font-mono text-xs">{data.commit}</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Info className="h-3.5 w-3.5" />
|
||||
<span>{data.go}</span>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<div className="font-mono text-xs">
|
||||
{data.goOS}/{data.goArch}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Separator orientation="vertical" className="hidden h-4 sm:block" />
|
||||
|
||||
<span className="hidden sm:inline">
|
||||
built <span className="font-mono">{formatBuilt(data.built)}</span>
|
||||
</span>
|
||||
|
||||
<Separator orientation="vertical" className="hidden h-4 sm:block" />
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => {
|
||||
navigator.clipboard?.writeText(copyText).catch(() => {})
|
||||
}}
|
||||
title="Copy version details"
|
||||
>
|
||||
<Clipboard className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<a
|
||||
href="/api/v1/version"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-1 text-xs underline-offset-4 hover:underline"
|
||||
title="Open raw version JSON"
|
||||
>
|
||||
JSON <ExternalLink className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
})
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ComponentIcon,
|
||||
FileKey2Icon,
|
||||
KeyRound,
|
||||
LockKeyholeIcon,
|
||||
ServerIcon,
|
||||
SprayCanIcon,
|
||||
TagsIcon,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
} from "lucide-react"
|
||||
import { AiOutlineCluster } from "react-icons/ai"
|
||||
import { GrUserWorker } from "react-icons/gr"
|
||||
import { MdOutlineDns } from "react-icons/md"
|
||||
|
||||
export type NavItem = {
|
||||
to: string
|
||||
@@ -22,12 +24,14 @@ export type NavItem = {
|
||||
|
||||
export const mainNav: NavItem[] = [
|
||||
{ to: "/clusters", label: "Clusters", icon: AiOutlineCluster },
|
||||
{ to: "/dns", label: "DNS", icon: MdOutlineDns },
|
||||
{ to: "/node-pools", label: "Node Pools", icon: BoxesIcon },
|
||||
{ to: "/annotations", label: "Annotations", icon: ComponentIcon },
|
||||
{ to: "/labels", label: "Labels", icon: TagsIcon },
|
||||
{ to: "/taints", label: "Taints", icon: SprayCanIcon },
|
||||
{ to: "/servers", label: "Servers", icon: ServerIcon },
|
||||
{ to: "/ssh", label: "SSH Keys", icon: FileKey2Icon },
|
||||
{ to: "/credentials", label: "Credentials", icon: LockKeyholeIcon },
|
||||
]
|
||||
|
||||
export const orgNav: NavItem[] = [
|
||||
|
||||
1387
ui/src/pages/credentials/credential-page.tsx
Normal file
1387
ui/src/pages/credentials/credential-page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
1040
ui/src/pages/dns/dns-page.tsx
Normal file
1040
ui/src/pages/dns/dns-page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -145,8 +145,12 @@ export const JobsPage: FC = () => {
|
||||
|
||||
// Mutations
|
||||
const enqueueM = useMutation({
|
||||
mutationFn: (body: { queue: string; type: string; payload?: unknown; run_at?: string }) =>
|
||||
archerAdminApi.enqueue(body),
|
||||
mutationFn: (body: {
|
||||
queue: string
|
||||
type: string
|
||||
payload?: object | undefined
|
||||
run_at?: string
|
||||
}) => archerAdminApi.enqueue(body),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["archer", "jobs"] }),
|
||||
})
|
||||
const retryM = useMutation({
|
||||
@@ -462,7 +466,7 @@ function EnqueueDialog({
|
||||
onSubmit: (body: {
|
||||
queue: string
|
||||
type: string
|
||||
payload?: unknown
|
||||
payload?: object | undefined
|
||||
run_at?: string
|
||||
}) => Promise<unknown>
|
||||
submitting?: boolean
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useEffect } from "react"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import { credentialsApi } from "@/api/credentials.ts"
|
||||
import { withRefresh } from "@/api/with-refresh.ts"
|
||||
import { orgStore } from "@/auth/org.ts"
|
||||
import type { DtoCredentialOut } from "@/sdk"
|
||||
import { makeOrgsApi } from "@/sdkClient.ts"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
@@ -8,7 +10,6 @@ import { useForm } from "react-hook-form"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod"
|
||||
|
||||
|
||||
import { Button } from "@/components/ui/button.tsx"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card.tsx"
|
||||
import {
|
||||
@@ -21,6 +22,20 @@ import {
|
||||
} from "@/components/ui/form.tsx"
|
||||
import { Input } from "@/components/ui/input.tsx"
|
||||
|
||||
const isS3 = (c: DtoCredentialOut) =>
|
||||
c.provider === "aws" &&
|
||||
c.scope_kind === "service" &&
|
||||
// scope may be JSON; allow both object and stringified JSON
|
||||
(() => {
|
||||
const s = (c as any).scope
|
||||
try {
|
||||
const obj = typeof s === "string" ? JSON.parse(s) : s || {}
|
||||
return obj?.service === "s3"
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})()
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1, "Required"),
|
||||
domain: z.string().optional(),
|
||||
@@ -39,6 +54,13 @@ export const OrgSettings = () => {
|
||||
queryFn: () => withRefresh(() => api.getOrg({ id: orgId! })),
|
||||
})
|
||||
|
||||
const credentialQ = useQuery({
|
||||
queryKey: ["credentials", "s3"],
|
||||
queryFn: () => credentialsApi.listCredentials(), // client-side filter
|
||||
})
|
||||
|
||||
const s3Credentials = useMemo(() => (credentialQ.data ?? []).filter(isS3), [credentialQ.data])
|
||||
|
||||
const form = useForm<Values>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
|
||||
@@ -5,9 +5,12 @@ import {
|
||||
ArcherAdminApi,
|
||||
AuthApi,
|
||||
Configuration,
|
||||
CredentialsApi,
|
||||
DNSApi,
|
||||
LabelsApi,
|
||||
MeApi,
|
||||
MeAPIKeysApi,
|
||||
MetaApi,
|
||||
NodePoolsApi,
|
||||
OrgsApi,
|
||||
ServersApi,
|
||||
@@ -108,3 +111,15 @@ export function makeArcherAdminApi() {
|
||||
export function makeNodePoolApi() {
|
||||
return makeApiClient(NodePoolsApi)
|
||||
}
|
||||
|
||||
export function makeMetaApi() {
|
||||
return makeApiClient(MetaApi)
|
||||
}
|
||||
|
||||
export function makeCredentialsApi() {
|
||||
return makeApiClient(CredentialsApi)
|
||||
}
|
||||
|
||||
export function makeDnsApi() {
|
||||
return makeApiClient(DNSApi)
|
||||
}
|
||||
@@ -1,31 +1,34 @@
|
||||
import path from "path"
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import react from "@vitejs/plugin-react"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
}
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
"/api": "http://localhost:8080",
|
||||
"/swagger": "http://localhost:8080",
|
||||
},
|
||||
allowedHosts: ['.getexposed.io']
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
"/api": "http://localhost:8080",
|
||||
"/swagger": "http://localhost:8080",
|
||||
"/db-studio": "http://localhost:8080",
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 1000,
|
||||
outDir: "../internal/web/dist",
|
||||
emptyOutDir: true,
|
||||
sourcemap: true,
|
||||
cssMinify: "lightningcss",
|
||||
rollupOptions: { output: { manualChunks: { react: ["react","react-dom","react-router-dom"] } } }
|
||||
allowedHosts: [".getexposed.io"],
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 1000,
|
||||
outDir: "../internal/web/dist",
|
||||
emptyOutDir: true,
|
||||
sourcemap: true,
|
||||
cssMinify: "lightningcss",
|
||||
rollupOptions: {
|
||||
output: { manualChunks: { react: ["react", "react-dom", "react-router-dom"] } },
|
||||
},
|
||||
esbuild: { legalComments: "none" }
|
||||
},
|
||||
esbuild: { legalComments: "none" },
|
||||
})
|
||||
|
||||
559
ui/yarn.lock
559
ui/yarn.lock
@@ -584,42 +584,42 @@
|
||||
"@babel/types" "^7.26.0"
|
||||
semver "^7.5.2"
|
||||
|
||||
"@inquirer/ansi@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/ansi/-/ansi-1.0.1.tgz#994f7dd16a00c547a7b110e04bf4f4eca1857929"
|
||||
integrity sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw==
|
||||
"@inquirer/ansi@^1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/ansi/-/ansi-1.0.2.tgz#674a4c4d81ad460695cb2a1fc69d78cd187f337e"
|
||||
integrity sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==
|
||||
|
||||
"@inquirer/confirm@^5.0.0":
|
||||
version "5.1.19"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.19.tgz#bf28b420898999eb7479ab55623a3fbaf1453ff4"
|
||||
integrity sha512-wQNz9cfcxrtEnUyG5PndC8g3gZ7lGDBzmWiXZkX8ot3vfZ+/BLjR8EvyGX4YzQLeVqtAlY/YScZpW7CW8qMoDQ==
|
||||
version "5.1.21"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.21.tgz#610c4acd7797d94890a6e2dde2c98eb1e891dd12"
|
||||
integrity sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==
|
||||
dependencies:
|
||||
"@inquirer/core" "^10.3.0"
|
||||
"@inquirer/type" "^3.0.9"
|
||||
"@inquirer/core" "^10.3.2"
|
||||
"@inquirer/type" "^3.0.10"
|
||||
|
||||
"@inquirer/core@^10.3.0":
|
||||
version "10.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.3.0.tgz#342e4fd62cbd33ea62089364274995dbec1f2ffe"
|
||||
integrity sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA==
|
||||
"@inquirer/core@^10.3.2":
|
||||
version "10.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.3.2.tgz#535979ff3ff4fe1e7cc4f83e2320504c743b7e20"
|
||||
integrity sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==
|
||||
dependencies:
|
||||
"@inquirer/ansi" "^1.0.1"
|
||||
"@inquirer/figures" "^1.0.14"
|
||||
"@inquirer/type" "^3.0.9"
|
||||
"@inquirer/ansi" "^1.0.2"
|
||||
"@inquirer/figures" "^1.0.15"
|
||||
"@inquirer/type" "^3.0.10"
|
||||
cli-width "^4.1.0"
|
||||
mute-stream "^2.0.0"
|
||||
signal-exit "^4.1.0"
|
||||
wrap-ansi "^6.2.0"
|
||||
yoctocolors-cjs "^2.1.2"
|
||||
yoctocolors-cjs "^2.1.3"
|
||||
|
||||
"@inquirer/figures@^1.0.14":
|
||||
version "1.0.14"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-1.0.14.tgz#12a7bfd344a83ae6cc5d6004b389ed11f6db6be4"
|
||||
integrity sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ==
|
||||
"@inquirer/figures@^1.0.15":
|
||||
version "1.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-1.0.15.tgz#dbb49ed80df11df74268023b496ac5d9acd22b3a"
|
||||
integrity sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==
|
||||
|
||||
"@inquirer/type@^3.0.9":
|
||||
version "3.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-3.0.9.tgz#f7f9696e9276e4e1ae9332767afb9199992e31d9"
|
||||
integrity sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w==
|
||||
"@inquirer/type@^3.0.10":
|
||||
version "3.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-3.0.10.tgz#11ed564ec78432a200ea2601a212d24af8150d50"
|
||||
integrity sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==
|
||||
|
||||
"@isaacs/balanced-match@^4.0.1":
|
||||
version "4.0.1"
|
||||
@@ -668,9 +668,9 @@
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
|
||||
"@modelcontextprotocol/sdk@^1.17.2":
|
||||
version "1.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.21.0.tgz#a4574443c02a8ce57e7ecbce823c6eaf04041927"
|
||||
integrity sha512-YFBsXJMFCyI1zP98u7gezMFKX4lgu/XpoZJk7ufI6UlFKXLj2hAMUuRlQX/nrmIPOmhRrG6tw2OQ2ZA/ZlXYpQ==
|
||||
version "1.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.22.0.tgz#b982143dfd36ef096b311d8ffadb2f91aabbbb1d"
|
||||
integrity sha512-VUpl106XVTCpDmTBil2ehgJZjhyLY2QZikzF8NvTXtLRF1CvO5iEE2UNZdVIUer35vFOwMKYeUGbjJtvPWan3g==
|
||||
dependencies:
|
||||
ajv "^8.17.1"
|
||||
ajv-formats "^3.0.1"
|
||||
@@ -763,11 +763,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-2.1.0.tgz#0acf32f470af2ceaf47f095cdecd40d68666efda"
|
||||
integrity sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==
|
||||
|
||||
"@pkgr/core@^0.2.9":
|
||||
version "0.2.9"
|
||||
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.9.tgz#d229a7b7f9dac167a156992ef23c7f023653f53b"
|
||||
integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==
|
||||
|
||||
"@radix-ui/number@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.1.1.tgz#7b2c9225fbf1b126539551f5985769d0048d9090"
|
||||
@@ -1375,115 +1370,115 @@
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz#fa8249860113711ad3c8053bc79cb07c79b77f62"
|
||||
integrity sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz#0f44a2f8668ed87b040b6fe659358ac9239da4db"
|
||||
integrity sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==
|
||||
"@rollup/rollup-android-arm-eabi@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz#7131f3d364805067fd5596302aad9ebef1434b32"
|
||||
integrity sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==
|
||||
|
||||
"@rollup/rollup-android-arm64@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz#25b9a01deef6518a948431564c987bcb205274f5"
|
||||
integrity sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==
|
||||
"@rollup/rollup-android-arm64@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz#7ede14d7fcf7c57821a2731c04b29ccc03145d82"
|
||||
integrity sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz#8a102869c88f3780c7d5e6776afd3f19084ecd7f"
|
||||
integrity sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==
|
||||
"@rollup/rollup-darwin-arm64@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz#d59bf9ed582b38838e86a17f91720c17db6575b9"
|
||||
integrity sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==
|
||||
|
||||
"@rollup/rollup-darwin-x64@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz#8e526417cd6f54daf1d0c04cf361160216581956"
|
||||
integrity sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==
|
||||
"@rollup/rollup-darwin-x64@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz#a76278d9b9da9f84ea7909a14d93b915d5bbe01e"
|
||||
integrity sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==
|
||||
|
||||
"@rollup/rollup-freebsd-arm64@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz#0e7027054493f3409b1f219a3eac5efd128ef899"
|
||||
integrity sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==
|
||||
"@rollup/rollup-freebsd-arm64@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz#1a94821a1f565b9eaa74187632d482e4c59a1707"
|
||||
integrity sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==
|
||||
|
||||
"@rollup/rollup-freebsd-x64@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz#72b204a920139e9ec3d331bd9cfd9a0c248ccb10"
|
||||
integrity sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==
|
||||
"@rollup/rollup-freebsd-x64@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz#aad2274680106b2b6549b1e35e5d3a7a9f1f16af"
|
||||
integrity sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz#ab1b522ebe5b7e06c99504cc38f6cd8b808ba41c"
|
||||
integrity sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz#100fe4306399ffeec47318a3c9b8c0e5e8b07ddb"
|
||||
integrity sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz#f8cc30b638f1ee7e3d18eac24af47ea29d9beb00"
|
||||
integrity sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz#b84634952604b950e18fa11fddebde898c5928d8"
|
||||
integrity sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz#7af37a9e85f25db59dc8214172907b7e146c12cc"
|
||||
integrity sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==
|
||||
"@rollup/rollup-linux-arm64-gnu@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz#dad6f2fb41c2485f29a98e40e9bd78253255dbf3"
|
||||
integrity sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz#a623eb0d3617c03b7a73716eb85c6e37b776f7e0"
|
||||
integrity sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==
|
||||
"@rollup/rollup-linux-arm64-musl@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz#0f3f77c8ce9fbf982f8a8378b70a73dc6704a706"
|
||||
integrity sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz#76ea038b549c5c6c5f0d062942627c4066642ee2"
|
||||
integrity sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==
|
||||
"@rollup/rollup-linux-loong64-gnu@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz#870bb94e9dad28bb3124ba49bd733deaa6aa2635"
|
||||
integrity sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz#d9a4c3f0a3492bc78f6fdfe8131ac61c7359ccd5"
|
||||
integrity sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==
|
||||
"@rollup/rollup-linux-ppc64-gnu@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz#188427d11abefc6c9926e3870b3e032170f5577c"
|
||||
integrity sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz#87ab033eebd1a9a1dd7b60509f6333ec1f82d994"
|
||||
integrity sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz#9dec6eadbbb5abd3b76fe624dc4f006913ff4a7f"
|
||||
integrity sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz#bda3eb67e1c993c1ba12bc9c2f694e7703958d9f"
|
||||
integrity sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==
|
||||
"@rollup/rollup-linux-riscv64-musl@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz#b26ba1c80b6f104dc5bd83ed83181fc0411a0c38"
|
||||
integrity sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz#f7bc10fbe096ab44694233dc42a2291ed5453d4b"
|
||||
integrity sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==
|
||||
"@rollup/rollup-linux-s390x-gnu@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz#dc83647189b68ad8d56a956a6fcaa4ee9c728190"
|
||||
integrity sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz#a151cb1234cc9b2cf5e8cfc02aa91436b8f9e278"
|
||||
integrity sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==
|
||||
"@rollup/rollup-linux-x64-gnu@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz#42c3b8c94e9de37bd103cb2e26fb715118ef6459"
|
||||
integrity sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz#7859e196501cc3b3062d45d2776cfb4d2f3a9350"
|
||||
integrity sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==
|
||||
"@rollup/rollup-linux-x64-musl@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz#d0e216ee1ea16bfafe35681b899b6a05258988e5"
|
||||
integrity sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==
|
||||
|
||||
"@rollup/rollup-openharmony-arm64@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz#85d0df7233734df31e547c1e647d2a5300b3bf30"
|
||||
integrity sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==
|
||||
"@rollup/rollup-openharmony-arm64@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz#3acd0157cb8976f659442bfd8a99aca46f8a2931"
|
||||
integrity sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz#e62357d00458db17277b88adbf690bb855cac937"
|
||||
integrity sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==
|
||||
"@rollup/rollup-win32-arm64-msvc@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz#3eb9e7d4d0e1d2e0850c4ee9aa2d0ddf89a8effa"
|
||||
integrity sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz#fc7cd40f44834a703c1f1c3fe8bcc27ce476cd50"
|
||||
integrity sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==
|
||||
"@rollup/rollup-win32-ia32-msvc@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz#d69280bc6680fe19e0956e965811946d542f6365"
|
||||
integrity sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz#1a22acfc93c64a64a48c42672e857ee51774d0d3"
|
||||
integrity sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==
|
||||
"@rollup/rollup-win32-x64-gnu@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz#d182ce91e342bad9cbb8b284cf33ac542b126ead"
|
||||
integrity sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz#1657f56326bbe0ac80eedc9f9c18fc1ddd24e107"
|
||||
integrity sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==
|
||||
"@rollup/rollup-win32-x64-msvc@4.53.2":
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz#d9ab606437fd072b2cb7df7e54bcdc7f1ccbe8b4"
|
||||
integrity sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==
|
||||
|
||||
"@sec-ant/readable-stream@^0.4.1":
|
||||
version "0.4.1"
|
||||
@@ -1607,17 +1602,17 @@
|
||||
"@tailwindcss/oxide" "4.1.17"
|
||||
tailwindcss "4.1.17"
|
||||
|
||||
"@tanstack/query-core@5.90.7":
|
||||
version "5.90.7"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.90.7.tgz#ebe0d692e83bd76bde36a371955b0944a204f10c"
|
||||
integrity sha512-6PN65csiuTNfBMXqQUxQhCNdtm1rV+9kC9YwWAIKcaxAauq3Wu7p18j3gQY3YIBJU70jT/wzCCZ2uqto/vQgiQ==
|
||||
"@tanstack/query-core@5.90.8":
|
||||
version "5.90.8"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.90.8.tgz#4a2600a5acf403494e4d5a3fcfac038ed84c5db7"
|
||||
integrity sha512-4E0RP/0GJCxSNiRF2kAqE/LQkTJVlL/QNU7gIJSptaseV9HP6kOuA+N11y4bZKZxa3QopK3ZuewwutHx6DqDXQ==
|
||||
|
||||
"@tanstack/react-query@^5.90.7":
|
||||
version "5.90.7"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.90.7.tgz#68a32e9fc2bf8fa8c7890177f5ec35653731a1dd"
|
||||
integrity sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ==
|
||||
version "5.90.8"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.90.8.tgz#c9b8bdbffd2df4b023094c3da144324a9daa5cef"
|
||||
integrity sha512-/3b9QGzkf4rE5/miL6tyhldQRlLXzMHcySOm/2Tm2OLEFE9P1ImkH0+OviDBSvyAvtAOJocar5xhd7vxdLi3aQ==
|
||||
dependencies:
|
||||
"@tanstack/query-core" "5.90.7"
|
||||
"@tanstack/query-core" "5.90.8"
|
||||
|
||||
"@ts-morph/common@~0.27.0":
|
||||
version "0.27.0"
|
||||
@@ -1736,12 +1731,12 @@
|
||||
dependencies:
|
||||
undici-types "~7.16.0"
|
||||
|
||||
"@types/react-dom@^19.2.2":
|
||||
"@types/react-dom@19.2.2":
|
||||
version "19.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.2.tgz#a4cc874797b7ddc9cb180ef0d5dc23f596fc2332"
|
||||
integrity sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==
|
||||
|
||||
"@types/react@^19.2.2":
|
||||
"@types/react@19.2.2":
|
||||
version "19.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.2.tgz#ba123a75d4c2a51158697160a4ea2ff70aa6bf36"
|
||||
integrity sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==
|
||||
@@ -1753,79 +1748,79 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.6.tgz#66748315cc9a96d63403baa8671b2c124f8633aa"
|
||||
integrity sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz#6f7aeaf9f5c611425db9b8f983e8d3fe5deece3c"
|
||||
integrity sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==
|
||||
"@typescript-eslint/eslint-plugin@8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz#005dc4eebcb27462f20de3afe888065f65cec100"
|
||||
integrity sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.10.0"
|
||||
"@typescript-eslint/scope-manager" "8.46.3"
|
||||
"@typescript-eslint/type-utils" "8.46.3"
|
||||
"@typescript-eslint/utils" "8.46.3"
|
||||
"@typescript-eslint/visitor-keys" "8.46.3"
|
||||
"@typescript-eslint/scope-manager" "8.46.4"
|
||||
"@typescript-eslint/type-utils" "8.46.4"
|
||||
"@typescript-eslint/utils" "8.46.4"
|
||||
"@typescript-eslint/visitor-keys" "8.46.4"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^7.0.0"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/parser@8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.46.3.tgz#3badfb62d2e2dc733d02a038073e3f65f2cb833d"
|
||||
integrity sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==
|
||||
"@typescript-eslint/parser@8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.46.4.tgz#1a5bfd48be57bc07eec64e090ac46e89f47ade31"
|
||||
integrity sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.46.3"
|
||||
"@typescript-eslint/types" "8.46.3"
|
||||
"@typescript-eslint/typescript-estree" "8.46.3"
|
||||
"@typescript-eslint/visitor-keys" "8.46.3"
|
||||
"@typescript-eslint/scope-manager" "8.46.4"
|
||||
"@typescript-eslint/types" "8.46.4"
|
||||
"@typescript-eslint/typescript-estree" "8.46.4"
|
||||
"@typescript-eslint/visitor-keys" "8.46.4"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/project-service@8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.46.3.tgz#4555c685407ea829081218fa033d7b032607aaef"
|
||||
integrity sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==
|
||||
"@typescript-eslint/project-service@8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.46.4.tgz#fa9872673b51fb57e5d5da034edbe17424ddd185"
|
||||
integrity sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.46.3"
|
||||
"@typescript-eslint/types" "^8.46.3"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.46.4"
|
||||
"@typescript-eslint/types" "^8.46.4"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.46.3.tgz#2e330f566e135ccac13477b98dd88d8f176e4dff"
|
||||
integrity sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==
|
||||
"@typescript-eslint/scope-manager@8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz#78c9b4856c0094def64ffa53ea955b46bec13304"
|
||||
integrity sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.46.3"
|
||||
"@typescript-eslint/visitor-keys" "8.46.3"
|
||||
"@typescript-eslint/types" "8.46.4"
|
||||
"@typescript-eslint/visitor-keys" "8.46.4"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.46.3", "@typescript-eslint/tsconfig-utils@^8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.3.tgz#cad33398c762c97fe56a8defda00c16505abefa3"
|
||||
integrity sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==
|
||||
"@typescript-eslint/tsconfig-utils@8.46.4", "@typescript-eslint/tsconfig-utils@^8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz#989a338093b6b91b0552f1f51331d89ec6980382"
|
||||
integrity sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==
|
||||
|
||||
"@typescript-eslint/type-utils@8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.46.3.tgz#71188df833d7697ecff256cd1d3889a20552d78c"
|
||||
integrity sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==
|
||||
"@typescript-eslint/type-utils@8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz#ae71b428a3c138b5084affe47893c129949171e0"
|
||||
integrity sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.46.3"
|
||||
"@typescript-eslint/typescript-estree" "8.46.3"
|
||||
"@typescript-eslint/utils" "8.46.3"
|
||||
"@typescript-eslint/types" "8.46.4"
|
||||
"@typescript-eslint/typescript-estree" "8.46.4"
|
||||
"@typescript-eslint/utils" "8.46.4"
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/types@8.46.3", "@typescript-eslint/types@^8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.46.3.tgz#da05ea40e91359b4275dbb3a489f2f7907a02245"
|
||||
integrity sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==
|
||||
"@typescript-eslint/types@8.46.4", "@typescript-eslint/types@^8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.46.4.tgz#38022bfda051be80e4120eeefcd2b6e3e630a69b"
|
||||
integrity sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.3.tgz#c12406afba707f9779ce0c0151a08c33b3a96d41"
|
||||
integrity sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==
|
||||
"@typescript-eslint/typescript-estree@8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz#6a9eeab0da45bf400f22c818e0f47102a980ceaa"
|
||||
integrity sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.46.3"
|
||||
"@typescript-eslint/tsconfig-utils" "8.46.3"
|
||||
"@typescript-eslint/types" "8.46.3"
|
||||
"@typescript-eslint/visitor-keys" "8.46.3"
|
||||
"@typescript-eslint/project-service" "8.46.4"
|
||||
"@typescript-eslint/tsconfig-utils" "8.46.4"
|
||||
"@typescript-eslint/types" "8.46.4"
|
||||
"@typescript-eslint/visitor-keys" "8.46.4"
|
||||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
is-glob "^4.0.3"
|
||||
@@ -1833,22 +1828,22 @@
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/utils@8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.46.3.tgz#b6c7994b7c1ee2fe338ab32f7b3d4424856a73ce"
|
||||
integrity sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==
|
||||
"@typescript-eslint/utils@8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.46.4.tgz#ea7878ddd625948cad4424dc2752b1be236556f5"
|
||||
integrity sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.7.0"
|
||||
"@typescript-eslint/scope-manager" "8.46.3"
|
||||
"@typescript-eslint/types" "8.46.3"
|
||||
"@typescript-eslint/typescript-estree" "8.46.3"
|
||||
"@typescript-eslint/scope-manager" "8.46.4"
|
||||
"@typescript-eslint/types" "8.46.4"
|
||||
"@typescript-eslint/typescript-estree" "8.46.4"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.46.3":
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.3.tgz#6811b15053501981059c58e1c01b39242bd5c0f6"
|
||||
integrity sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==
|
||||
"@typescript-eslint/visitor-keys@8.46.4":
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz#07031bd8d3ca6474e121221dae1055daead888f1"
|
||||
integrity sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.46.3"
|
||||
"@typescript-eslint/types" "8.46.4"
|
||||
eslint-visitor-keys "^4.2.1"
|
||||
|
||||
"@vitejs/plugin-react@5.1.0":
|
||||
@@ -1959,10 +1954,10 @@ balanced-match@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
baseline-browser-mapping@^2.8.19:
|
||||
version "2.8.25"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz#947dc6f81778e0fa0424a2ab9ea09a3033e71109"
|
||||
integrity sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==
|
||||
baseline-browser-mapping@^2.8.25:
|
||||
version "2.8.28"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz#9ef511f5a7c19d74a94cafcbf951608398e9bdb3"
|
||||
integrity sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==
|
||||
|
||||
body-parser@^2.2.0:
|
||||
version "2.2.0"
|
||||
@@ -2002,14 +1997,14 @@ braces@^3.0.3:
|
||||
fill-range "^7.1.1"
|
||||
|
||||
browserslist@^4.24.0, browserslist@^4.26.2:
|
||||
version "4.27.0"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.27.0.tgz#755654744feae978fbb123718b2f139bc0fa6697"
|
||||
integrity sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==
|
||||
version "4.28.0"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.0.tgz#9cefece0a386a17a3cd3d22ebf67b9deca1b5929"
|
||||
integrity sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==
|
||||
dependencies:
|
||||
baseline-browser-mapping "^2.8.19"
|
||||
caniuse-lite "^1.0.30001751"
|
||||
electron-to-chromium "^1.5.238"
|
||||
node-releases "^2.0.26"
|
||||
baseline-browser-mapping "^2.8.25"
|
||||
caniuse-lite "^1.0.30001754"
|
||||
electron-to-chromium "^1.5.249"
|
||||
node-releases "^2.0.27"
|
||||
update-browserslist-db "^1.1.4"
|
||||
|
||||
bytes@3.1.2, bytes@^3.1.2:
|
||||
@@ -2038,7 +2033,7 @@ callsites@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
|
||||
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
|
||||
|
||||
caniuse-lite@^1.0.30001751:
|
||||
caniuse-lite@^1.0.30001754:
|
||||
version "1.0.30001754"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz#7758299d9a72cce4e6b038788a15b12b44002759"
|
||||
integrity sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==
|
||||
@@ -2370,10 +2365,10 @@ ee-first@1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
|
||||
|
||||
electron-to-chromium@^1.5.238:
|
||||
version "1.5.245"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.245.tgz#81aea81adf1e06b6f703b4b35ac6d543421d0fd9"
|
||||
integrity sha512-rdmGfW47ZhL/oWEJAY4qxRtdly2B98ooTJ0pdEI4jhVLZ6tNf8fPtov2wS1IRKwFJT92le3x4Knxiwzl7cPPpQ==
|
||||
electron-to-chromium@^1.5.249:
|
||||
version "1.5.251"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.251.tgz#8168db0cb8fcc520bd4e5be720f2d8e0e4f7fe74"
|
||||
integrity sha512-lmyEOp4G0XT3qrYswNB4np1kx90k6QCXpnSHYv2xEsUuEu8JCobpDVYO6vMseirQyyCC6GCIGGxd5szMBa0tRA==
|
||||
|
||||
embla-carousel-react@^8.6.0:
|
||||
version "8.6.0"
|
||||
@@ -2492,19 +2487,6 @@ escape-string-regexp@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
|
||||
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
|
||||
|
||||
eslint-config-prettier@10.1.8:
|
||||
version "10.1.8"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz#15734ce4af8c2778cc32f0b01b37b0b5cd1ecb97"
|
||||
integrity sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==
|
||||
|
||||
eslint-plugin-prettier@5.5.4:
|
||||
version "5.5.4"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz#9d61c4ea11de5af704d4edf108c82ccfa7f2e61c"
|
||||
integrity sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==
|
||||
dependencies:
|
||||
prettier-linter-helpers "^1.0.0"
|
||||
synckit "^0.11.7"
|
||||
|
||||
eslint-plugin-react-hooks@7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz#66e258db58ece50723ef20cc159f8aa908219169"
|
||||
@@ -2715,15 +2697,10 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-diff@^1.1.2:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
|
||||
integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
|
||||
|
||||
fast-equals@^5.0.1:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.3.2.tgz#75a9c7b1c2f627851349a2db94327d79b774ce83"
|
||||
integrity sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.3.3.tgz#e55f96198269278533348c22f1ab1a0fb957e22a"
|
||||
integrity sha512-/boTcHZeIAQ2r/tL11voclBHDeP9WPxLt+tyAbVSyyXuUFyh0Tne7gJZTqGbxnvj79TjLdCXLOY7UIPhyG5MTw==
|
||||
|
||||
fast-glob@^3.3.2, fast-glob@^3.3.3:
|
||||
version "3.3.3"
|
||||
@@ -3195,9 +3172,9 @@ jiti@^2.6.1:
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
|
||||
js-yaml@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
|
||||
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
|
||||
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
@@ -3487,9 +3464,9 @@ ms@^2.1.3:
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
||||
msw@^2.10.4:
|
||||
version "2.12.0"
|
||||
resolved "https://registry.yarnpkg.com/msw/-/msw-2.12.0.tgz#92b62b42e2b81aa843151129c513e8c94b13c165"
|
||||
integrity sha512-jzf2eVnd8+iWXN74dccLrHUw3i3hFVvNVQRWS4vBl2KxaUt7Tdur0Eyda/DODGFkZDu2P5MXaeLe/9Qx8PZkrg==
|
||||
version "2.12.1"
|
||||
resolved "https://registry.yarnpkg.com/msw/-/msw-2.12.1.tgz#b3dee99d7692e92581234b4060b9a9250f5d998e"
|
||||
integrity sha512-arzsi9IZjjByiEw21gSUP82qHM8zkV69nNpWV6W4z72KiLvsDWoOp678ORV6cNfU/JGhlX0SsnD4oXo9gI6I2A==
|
||||
dependencies:
|
||||
"@inquirer/confirm" "^5.0.0"
|
||||
"@mswjs/interceptors" "^0.40.0"
|
||||
@@ -3549,7 +3526,7 @@ node-fetch@^3.3.2:
|
||||
fetch-blob "^3.1.4"
|
||||
formdata-polyfill "^4.0.10"
|
||||
|
||||
node-releases@^2.0.26:
|
||||
node-releases@^2.0.27:
|
||||
version "2.0.27"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e"
|
||||
integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==
|
||||
@@ -3754,13 +3731,6 @@ prelude-ls@^1.2.1:
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||
|
||||
prettier-linter-helpers@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
|
||||
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
|
||||
dependencies:
|
||||
fast-diff "^1.1.2"
|
||||
|
||||
prettier-plugin-tailwindcss@0.7.1:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.1.tgz#0cb15246668788e62b5b752868f5e01f0ce7eec9"
|
||||
@@ -3901,16 +3871,16 @@ react-resizable-panels@^3.0.6:
|
||||
integrity sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==
|
||||
|
||||
react-router-dom@^7.9.5:
|
||||
version "7.9.5"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.9.5.tgz#99a88cde83919bdfc84fbb3d6bf7c6fc18ca0758"
|
||||
integrity sha512-mkEmq/K8tKN63Ae2M7Xgz3c9l9YNbY+NHH6NNeUmLA3kDkhKXRsNb/ZpxaEunvGo2/3YXdk5EJU3Hxp3ocaBPw==
|
||||
version "7.9.6"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.9.6.tgz#f2a0d12961d67bd87ab48e5ef42fa1f45beae357"
|
||||
integrity sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==
|
||||
dependencies:
|
||||
react-router "7.9.5"
|
||||
react-router "7.9.6"
|
||||
|
||||
react-router@7.9.5:
|
||||
version "7.9.5"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.9.5.tgz#68722186b4c9f42be36e658d9fe5d62ac1e0808b"
|
||||
integrity sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==
|
||||
react-router@7.9.6:
|
||||
version "7.9.6"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.9.6.tgz#003c8de335fdd7390286a478dcfd9579c1826137"
|
||||
integrity sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==
|
||||
dependencies:
|
||||
cookie "^1.0.1"
|
||||
set-cookie-parser "^2.6.0"
|
||||
@@ -4013,34 +3983,34 @@ reusify@^1.0.4:
|
||||
integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==
|
||||
|
||||
rollup@^4.43.0:
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.52.5.tgz#96982cdcaedcdd51b12359981f240f94304ec235"
|
||||
integrity sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==
|
||||
version "4.53.2"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.53.2.tgz#98e73ee51e119cb9d88b07d026c959522416420a"
|
||||
integrity sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==
|
||||
dependencies:
|
||||
"@types/estree" "1.0.8"
|
||||
optionalDependencies:
|
||||
"@rollup/rollup-android-arm-eabi" "4.52.5"
|
||||
"@rollup/rollup-android-arm64" "4.52.5"
|
||||
"@rollup/rollup-darwin-arm64" "4.52.5"
|
||||
"@rollup/rollup-darwin-x64" "4.52.5"
|
||||
"@rollup/rollup-freebsd-arm64" "4.52.5"
|
||||
"@rollup/rollup-freebsd-x64" "4.52.5"
|
||||
"@rollup/rollup-linux-arm-gnueabihf" "4.52.5"
|
||||
"@rollup/rollup-linux-arm-musleabihf" "4.52.5"
|
||||
"@rollup/rollup-linux-arm64-gnu" "4.52.5"
|
||||
"@rollup/rollup-linux-arm64-musl" "4.52.5"
|
||||
"@rollup/rollup-linux-loong64-gnu" "4.52.5"
|
||||
"@rollup/rollup-linux-ppc64-gnu" "4.52.5"
|
||||
"@rollup/rollup-linux-riscv64-gnu" "4.52.5"
|
||||
"@rollup/rollup-linux-riscv64-musl" "4.52.5"
|
||||
"@rollup/rollup-linux-s390x-gnu" "4.52.5"
|
||||
"@rollup/rollup-linux-x64-gnu" "4.52.5"
|
||||
"@rollup/rollup-linux-x64-musl" "4.52.5"
|
||||
"@rollup/rollup-openharmony-arm64" "4.52.5"
|
||||
"@rollup/rollup-win32-arm64-msvc" "4.52.5"
|
||||
"@rollup/rollup-win32-ia32-msvc" "4.52.5"
|
||||
"@rollup/rollup-win32-x64-gnu" "4.52.5"
|
||||
"@rollup/rollup-win32-x64-msvc" "4.52.5"
|
||||
"@rollup/rollup-android-arm-eabi" "4.53.2"
|
||||
"@rollup/rollup-android-arm64" "4.53.2"
|
||||
"@rollup/rollup-darwin-arm64" "4.53.2"
|
||||
"@rollup/rollup-darwin-x64" "4.53.2"
|
||||
"@rollup/rollup-freebsd-arm64" "4.53.2"
|
||||
"@rollup/rollup-freebsd-x64" "4.53.2"
|
||||
"@rollup/rollup-linux-arm-gnueabihf" "4.53.2"
|
||||
"@rollup/rollup-linux-arm-musleabihf" "4.53.2"
|
||||
"@rollup/rollup-linux-arm64-gnu" "4.53.2"
|
||||
"@rollup/rollup-linux-arm64-musl" "4.53.2"
|
||||
"@rollup/rollup-linux-loong64-gnu" "4.53.2"
|
||||
"@rollup/rollup-linux-ppc64-gnu" "4.53.2"
|
||||
"@rollup/rollup-linux-riscv64-gnu" "4.53.2"
|
||||
"@rollup/rollup-linux-riscv64-musl" "4.53.2"
|
||||
"@rollup/rollup-linux-s390x-gnu" "4.53.2"
|
||||
"@rollup/rollup-linux-x64-gnu" "4.53.2"
|
||||
"@rollup/rollup-linux-x64-musl" "4.53.2"
|
||||
"@rollup/rollup-openharmony-arm64" "4.53.2"
|
||||
"@rollup/rollup-win32-arm64-msvc" "4.53.2"
|
||||
"@rollup/rollup-win32-ia32-msvc" "4.53.2"
|
||||
"@rollup/rollup-win32-x64-gnu" "4.53.2"
|
||||
"@rollup/rollup-win32-x64-msvc" "4.53.2"
|
||||
fsevents "~2.3.2"
|
||||
|
||||
router@^2.2.0:
|
||||
@@ -4329,17 +4299,10 @@ supports-color@^7.1.0:
|
||||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
synckit@^0.11.7:
|
||||
version "0.11.11"
|
||||
resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.11.tgz#c0b619cf258a97faa209155d9cd1699b5c998cb0"
|
||||
integrity sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==
|
||||
dependencies:
|
||||
"@pkgr/core" "^0.2.9"
|
||||
|
||||
tailwind-merge@^3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-3.3.1.tgz#a7e7db7c714f6020319e626ecfb7e7dac8393a4b"
|
||||
integrity sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==
|
||||
tailwind-merge@^3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-3.4.0.tgz#5a264e131a096879965f1175d11f8c36e6b64eca"
|
||||
integrity sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==
|
||||
|
||||
tailwindcss@4.1.17, tailwindcss@^4.1.17:
|
||||
version "4.1.17"
|
||||
@@ -4453,15 +4416,15 @@ type-is@^2.0.0, type-is@^2.0.1:
|
||||
media-typer "^1.1.0"
|
||||
mime-types "^3.0.0"
|
||||
|
||||
typescript-eslint@8.46.3:
|
||||
version "8.46.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.46.3.tgz#d58b337e4c6083ddef9a06542a03768a0150c564"
|
||||
integrity sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==
|
||||
typescript-eslint@8.46.4:
|
||||
version "8.46.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.46.4.tgz#73cb93b5ff16e8627217240eeb0caf6e74e0a9fc"
|
||||
integrity sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.46.3"
|
||||
"@typescript-eslint/parser" "8.46.3"
|
||||
"@typescript-eslint/typescript-estree" "8.46.3"
|
||||
"@typescript-eslint/utils" "8.46.3"
|
||||
"@typescript-eslint/eslint-plugin" "8.46.4"
|
||||
"@typescript-eslint/parser" "8.46.4"
|
||||
"@typescript-eslint/typescript-estree" "8.46.4"
|
||||
"@typescript-eslint/utils" "8.46.4"
|
||||
|
||||
typescript@5.9.3:
|
||||
version "5.9.3"
|
||||
@@ -4654,7 +4617,7 @@ yocto-queue@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||
|
||||
yoctocolors-cjs@^2.1.2:
|
||||
yoctocolors-cjs@^2.1.3:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz#7e4964ea8ec422b7a40ac917d3a344cfd2304baa"
|
||||
integrity sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==
|
||||
|
||||
Reference in New Issue
Block a user