mirror of
https://github.com/Noooste/garage-ui.git
synced 2026-07-26 07:48:13 +00:00
feat: add garage.toml support (#30)
This commit is contained in:
@@ -77,6 +77,41 @@ helm install garage-ui garage-ui/garage-ui \
|
||||
--set garage.adminToken=your-token
|
||||
```
|
||||
|
||||
Access at http://localhost:8080
|
||||
|
||||
### Quick Start with garage.toml
|
||||
|
||||
If you already have a running Garage instance, you can point Garage UI directly at your `garage.toml` -- no `config.yaml` needed:
|
||||
|
||||
```bash
|
||||
./garage-ui --garage-toml /etc/garage.toml
|
||||
```
|
||||
|
||||
Garage UI reads the S3 endpoint, admin endpoint, admin token, and S3 region straight from the TOML file. When no authentication method is explicitly configured, **token auth auto-enables**: the login page asks for the Garage admin token, giving you a login wall with zero extra config.
|
||||
|
||||
**Bind address handling:** Wildcard addresses like `0.0.0.0` or `[::]` are converted to `127.0.0.1` so the UI can reach Garage on localhost. Inside containers this won't work -- override the endpoint explicitly with environment variables or a config file.
|
||||
|
||||
**Docker:**
|
||||
|
||||
```bash
|
||||
docker run -d -p 8080:8080 \
|
||||
-v /etc/garage.toml:/etc/garage.toml:ro \
|
||||
-e GARAGE_UI_GARAGE_TOML=/etc/garage.toml \
|
||||
-e GARAGE_UI_GARAGE_ENDPOINT=http://garage:3900 \
|
||||
-e GARAGE_UI_GARAGE_ADMIN_ENDPOINT=http://garage:3903 \
|
||||
noooste/garage-ui:latest
|
||||
```
|
||||
|
||||
The endpoint overrides are needed because the container cannot reach `127.0.0.1` on the host.
|
||||
|
||||
**Combining flags:** Use `--garage-toml` for Garage connection values and `--config` for everything else (auth, CORS, logging, etc.):
|
||||
|
||||
```bash
|
||||
./garage-ui --garage-toml /etc/garage.toml --config config.yaml
|
||||
```
|
||||
|
||||
**Precedence order** (highest wins): built-in defaults < `garage.toml` < `config.yaml` < environment variables.
|
||||
|
||||
## Configuration
|
||||
|
||||
Minimum required config:
|
||||
|
||||
+3
-7
@@ -10,9 +10,9 @@ require (
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/minio/minio-go/v7 v7.0.100
|
||||
github.com/pelletier/go-toml/v2 v2.3.0
|
||||
github.com/rs/zerolog v1.35.0
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/swaggo/swag v1.16.6
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
)
|
||||
|
||||
@@ -36,7 +36,6 @@ require (
|
||||
github.com/go-openapi/jsonpointer v0.23.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.5 // indirect
|
||||
github.com/go-openapi/spec v0.22.4 // indirect
|
||||
github.com/go-openapi/swag v0.26.0 // indirect
|
||||
github.com/go-openapi/swag/conv v0.26.0 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.26.0 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.26.0 // indirect
|
||||
@@ -48,27 +47,24 @@ require (
|
||||
github.com/gofiber/schema v1.7.1 // indirect
|
||||
github.com/gofiber/utils/v2 v2.0.3 // indirect
|
||||
github.com/google/gopacket v1.1.19 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.2 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/refraction-networking/utls v1.8.2 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.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
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/swaggo/files/v2 v2.0.2 // indirect
|
||||
github.com/swaggo/swag v1.16.6 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.70.0 // indirect
|
||||
@@ -80,5 +76,5 @@ require (
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
)
|
||||
|
||||
+17
-100
@@ -2,8 +2,6 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Noooste/azuretls-client v1.12.11 h1:8IvtfPf+K6wOqiRROL/APGkxQCO/+jyjH0S39rnItfQ=
|
||||
github.com/Noooste/azuretls-client v1.12.11/go.mod h1:lvXW8wpaOwrwtDrSt8nv/Dd8NAbCMVNRoU4sFrAaxYs=
|
||||
github.com/Noooste/azuretls-client v1.13.2 h1:8Dli5aKP5O6qN/FNSGFVkpQ1V1F1gGawAkLIE2Nrk+U=
|
||||
github.com/Noooste/azuretls-client v1.13.2/go.mod h1:ON+SmiBm4Zy5vAhJmBNZk61Y7nqf4iM/b1MC1lN47Bk=
|
||||
github.com/Noooste/fhttp v1.0.15 h1:sYRWOKgr1x4L+wA6REMJCs4Z/lFOSJmuQHSIXMXCcPs=
|
||||
@@ -12,80 +10,57 @@ github.com/Noooste/go-socks4 v0.0.2 h1:DwHCYiCEAdjfNrQOFIid7qgKCll7ubhGS1ji5O8FY
|
||||
github.com/Noooste/go-socks4 v0.0.2/go.mod h1:+oOgtOFRsU8FoK7NBOhHSjiH5pveY8LgYNF5XcqVgjE=
|
||||
github.com/Noooste/swagger v1.2.0 h1:zGHin8k2V9mXDB1gxXOdKe4V8zhw79ycsw+/L2hH/pk=
|
||||
github.com/Noooste/swagger v1.2.0/go.mod h1:5N+iUZlFA43k2Paf42EZ+SFndBG1niSA1FAnwiNP1PM=
|
||||
github.com/Noooste/uquic-go v1.0.3 h1:VP8npQmU4lkVLm9Ug5Q18SJ8ExFDfUZIzd13YjYaLHE=
|
||||
github.com/Noooste/uquic-go v1.0.3/go.mod h1:MxkrvgpNcbIOSQxqglC3e/798O/6zuL3mBhlFN+04w4=
|
||||
github.com/Noooste/uquic-go v1.0.5 h1:HWfrxhxgB1a9Y2Au5mfFs2Y5Dy13OQIwa86D/kULPtE=
|
||||
github.com/Noooste/uquic-go v1.0.5/go.mod h1:1y+qiy23PqLKudi4kQiJ0b3zXXYcyctEBRfZPTuyBz4=
|
||||
github.com/Noooste/utls v1.3.20 h1:QzBNGGJ184bNMLodOzvM9YWc4vZ36QodIjqFQOHoZ88=
|
||||
github.com/Noooste/utls v1.3.20/go.mod h1:XEy+VEbTxmH6krfSG5YT7wDbjHTEi2zUXTG33R0PAAg=
|
||||
github.com/Noooste/utls v1.3.21 h1:5yEzTibikzF0/d0REfbjXURGHxJDKCrRghVAU/OQBko=
|
||||
github.com/Noooste/utls v1.3.21/go.mod h1:XEy+VEbTxmH6krfSG5YT7wDbjHTEi2zUXTG33R0PAAg=
|
||||
github.com/Noooste/websocket v1.0.3 h1:drW7tvZ3YqzqI9wApnaH1Q0syFMXO7gbLlsBWjZvMNA=
|
||||
github.com/Noooste/websocket v1.0.3/go.mod h1:Qhw0Rtuju/fPPbcb3R5XGq7poa51qPDL462jTltl9nQ=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q=
|
||||
github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A=
|
||||
github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
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/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gaukas/clienthellod v0.4.2 h1:LPJ+LSeqt99pqeCV4C0cllk+pyWmERisP7w6qWr7eqE=
|
||||
github.com/gaukas/clienthellod v0.4.2/go.mod h1:M57+dsu0ZScvmdnNxaxsDPM46WhSEdPYAOdNgfL7IKA=
|
||||
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
||||
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4=
|
||||
github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
|
||||
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
|
||||
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||
github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=
|
||||
github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-openapi/swag v0.26.0 h1:GVDXCmfvhfu1BxiHo8/FA+BbKmhecHnG3varjON5/RI=
|
||||
github.com/go-openapi/swag v0.26.0/go.mod h1:82g3193sZJRbocs7bNCqGfIgq8pkuwVwCfhKIRlEQF0=
|
||||
github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I=
|
||||
github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE=
|
||||
github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w=
|
||||
github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M=
|
||||
github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA=
|
||||
github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y=
|
||||
github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko=
|
||||
github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg=
|
||||
github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg=
|
||||
@@ -94,22 +69,19 @@ github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFu
|
||||
github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE=
|
||||
github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ=
|
||||
github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE=
|
||||
github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4=
|
||||
github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=
|
||||
github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=
|
||||
github.com/gofiber/schema v1.7.0 h1:yNM+FNRZjyYEli9Ey0AXRBrAY9jTnb+kmGs3lJGPvKg=
|
||||
github.com/gofiber/schema v1.7.0/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=
|
||||
github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=
|
||||
github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=
|
||||
github.com/gofiber/utils/v2 v2.0.2 h1:ShRRssz0F3AhTlAQcuEj54OEDtWF7+HJDwEi/aa6QLI=
|
||||
github.com/gofiber/utils/v2 v2.0.2/go.mod h1:+9Ub4NqQ+IaJoTliq5LfdmOJAA/Hzwf4pXOxOa3RrJ0=
|
||||
github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=
|
||||
github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
@@ -118,19 +90,13 @@ 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/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a h1://KbezygeMJZCSHH+HgUZiTeSoiuFspbMg1ge+eFj18=
|
||||
github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
||||
github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f h1:HU1RgM6NALf/KW9HEY6zry3ADbDKcmpQ+hJedoNGQYQ=
|
||||
github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f/go.mod h1:67FPmZWbr+KDT/VlpWtw6sO9XSjpJmLuHpoLmWiTGgY=
|
||||
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/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/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
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/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||
@@ -139,63 +105,40 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M=
|
||||
github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
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-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0=
|
||||
github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM=
|
||||
github.com/minio/minio-go/v7 v7.0.100 h1:ShkWi8Tyj9RtU57OQB2HIXKz4bFgtVib0bbT1sbtLI8=
|
||||
github.com/minio/minio-go/v7 v7.0.100/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8=
|
||||
github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM=
|
||||
github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
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/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
|
||||
github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
||||
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
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/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
|
||||
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
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/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU=
|
||||
github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc=
|
||||
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=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
@@ -212,78 +155,52 @@ github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU
|
||||
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
||||
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
|
||||
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=
|
||||
github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
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.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateAdminToken(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
configured string
|
||||
provided string
|
||||
want bool
|
||||
}{
|
||||
{"correct token", "my-secret-token", "my-secret-token", true},
|
||||
{"wrong token", "my-secret-token", "wrong-token", false},
|
||||
{"empty provided", "my-secret-token", "", false},
|
||||
{"empty configured", "", "any-token", false},
|
||||
{"both empty", "", "", true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := subtle.ConstantTimeCompare([]byte(tc.configured), []byte(tc.provided)) == 1
|
||||
if got != tc.want {
|
||||
t.Errorf("ValidateAdminToken(%q, %q) = %v, want %v", tc.configured, tc.provided, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ type GarageConfig struct {
|
||||
type AuthConfig struct {
|
||||
Admin AdminAuthConfig `mapstructure:"admin"`
|
||||
OIDC OIDCConfig `mapstructure:"oidc"`
|
||||
Token TokenAuthConfig `mapstructure:"token"`
|
||||
JWTPrivKey string `mapstructure:"jwt_private_key"` // Ed25519 private key in PEM format for JWT signing (64 bytes)
|
||||
}
|
||||
|
||||
@@ -56,6 +57,12 @@ type AdminAuthConfig struct {
|
||||
Password string `mapstructure:"password"`
|
||||
}
|
||||
|
||||
// TokenAuthConfig contains admin token authentication settings.
|
||||
// When enabled, users can log in using the Garage admin token.
|
||||
type TokenAuthConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
}
|
||||
|
||||
// OIDCConfig contains OIDC authentication settings
|
||||
type OIDCConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
@@ -95,8 +102,28 @@ type LoggingConfig struct {
|
||||
Format string `mapstructure:"format"`
|
||||
}
|
||||
|
||||
// LoadOption configures optional behaviour of Load.
|
||||
type LoadOption func(*loadOptions)
|
||||
|
||||
type loadOptions struct {
|
||||
garageTomlPath string
|
||||
}
|
||||
|
||||
// WithGarageToml tells Load to parse a garage.toml file and use its values as
|
||||
// lowest-priority defaults (below YAML, below env vars).
|
||||
func WithGarageToml(path string) LoadOption {
|
||||
return func(o *loadOptions) {
|
||||
o.garageTomlPath = path
|
||||
}
|
||||
}
|
||||
|
||||
// Load reads the configuration from the specified file
|
||||
func Load(configPath string) (*Config, error) {
|
||||
func Load(configPath string, opts ...LoadOption) (*Config, error) {
|
||||
var lo loadOptions
|
||||
for _, fn := range opts {
|
||||
fn(&lo)
|
||||
}
|
||||
|
||||
// Set default config file name if not specified
|
||||
if configPath == "" {
|
||||
configPath = "config.yaml"
|
||||
@@ -106,6 +133,28 @@ func Load(configPath string) (*Config, error) {
|
||||
viper.SetConfigFile(configPath)
|
||||
viper.SetConfigType("yaml")
|
||||
|
||||
// Built-in defaults (lowest priority)
|
||||
viper.SetDefault("server.host", "0.0.0.0")
|
||||
viper.SetDefault("server.port", 8080)
|
||||
viper.SetDefault("server.environment", "production")
|
||||
viper.SetDefault("garage.force_path_style", true)
|
||||
viper.SetDefault("logging.level", "info")
|
||||
viper.SetDefault("logging.format", "text")
|
||||
|
||||
// If garage.toml path is provided, parse it and set values as viper
|
||||
// defaults. Defaults sit below config-file and env-var values in viper's
|
||||
// priority order, so YAML and env vars will still win.
|
||||
if lo.garageTomlPath != "" {
|
||||
tomlResult, err := ParseGarageToml(lo.garageTomlPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing garage.toml: %w", err)
|
||||
}
|
||||
viper.SetDefault("garage.endpoint", tomlResult.Endpoint)
|
||||
viper.SetDefault("garage.admin_endpoint", tomlResult.AdminEndpoint)
|
||||
viper.SetDefault("garage.admin_token", tomlResult.AdminToken)
|
||||
viper.SetDefault("garage.region", tomlResult.Region)
|
||||
}
|
||||
|
||||
// Allow environment variables to override config values
|
||||
// Environment variables take precedence over config file
|
||||
viper.AutomaticEnv()
|
||||
@@ -165,6 +214,9 @@ func bindEnvVars() {
|
||||
viper.BindEnv("auth.admin.password", "GARAGE_UI_AUTH_ADMIN_PASSWORD")
|
||||
viper.BindEnv("auth.jwt_private_key", "GARAGE_UI_AUTH_JWT_PRIVATE_KEY")
|
||||
|
||||
// Token auth config
|
||||
viper.BindEnv("auth.token.enabled", "GARAGE_UI_AUTH_TOKEN_ENABLED")
|
||||
|
||||
// OIDC config
|
||||
viper.BindEnv("auth.oidc.enabled", "GARAGE_UI_AUTH_OIDC_ENABLED")
|
||||
viper.BindEnv("auth.oidc.provider_name", "GARAGE_UI_AUTH_OIDC_PROVIDER_NAME")
|
||||
@@ -250,6 +302,15 @@ func (c *Config) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveTokenAuth auto-enables token auth when no other auth method is
|
||||
// configured, unless it was explicitly set. This ensures the app never
|
||||
// starts without a login wall.
|
||||
func (c *Config) ResolveTokenAuth() {
|
||||
if !c.Auth.Admin.Enabled && !c.Auth.OIDC.Enabled && !c.Auth.Token.Enabled {
|
||||
c.Auth.Token.Enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
// GetAddress returns the full server address (host:port)
|
||||
func (c *Config) GetAddress() string {
|
||||
return fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port)
|
||||
|
||||
@@ -374,6 +374,130 @@ func TestIsDevelopment(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func writeToml(t *testing.T, content string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "garage.toml")
|
||||
if err := os.WriteFile(path, []byte(content), 0600); err != nil {
|
||||
t.Fatalf("write toml: %v", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
const testGarageToml = `
|
||||
[admin]
|
||||
api_bind_addr = "[::]:3903"
|
||||
admin_token = "toml-token"
|
||||
|
||||
[s3_api]
|
||||
api_bind_addr = "[::]:3900"
|
||||
s3_region = "garage"
|
||||
`
|
||||
|
||||
func TestLoad_GarageTomlOnly(t *testing.T) {
|
||||
resetViper(t)
|
||||
tomlPath := writeToml(t, testGarageToml)
|
||||
missingYaml := filepath.Join(t.TempDir(), "nope.yaml")
|
||||
|
||||
cfg, err := Load(missingYaml, WithGarageToml(tomlPath))
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.Garage.AdminToken != "toml-token" {
|
||||
t.Errorf("AdminToken = %q, want toml-token", cfg.Garage.AdminToken)
|
||||
}
|
||||
if cfg.Garage.Endpoint != "http://127.0.0.1:3900" {
|
||||
t.Errorf("Endpoint = %q, want http://127.0.0.1:3900", cfg.Garage.Endpoint)
|
||||
}
|
||||
if cfg.Garage.AdminEndpoint != "http://127.0.0.1:3903" {
|
||||
t.Errorf("AdminEndpoint = %q, want http://127.0.0.1:3903", cfg.Garage.AdminEndpoint)
|
||||
}
|
||||
if cfg.Garage.Region != "garage" {
|
||||
t.Errorf("Region = %q, want garage", cfg.Garage.Region)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_YAMLOverridesToml(t *testing.T) {
|
||||
resetViper(t)
|
||||
tomlPath := writeToml(t, testGarageToml)
|
||||
yaml := `
|
||||
server:
|
||||
host: "0.0.0.0"
|
||||
port: 8080
|
||||
garage:
|
||||
endpoint: http://custom:3900
|
||||
admin_endpoint: http://custom:3903
|
||||
admin_token: yaml-wins
|
||||
`
|
||||
yamlPath := writeConfigFile(t, yaml)
|
||||
|
||||
cfg, err := Load(yamlPath, WithGarageToml(tomlPath))
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.Garage.AdminToken != "yaml-wins" {
|
||||
t.Errorf("AdminToken = %q, want yaml-wins (yaml overrides toml)", cfg.Garage.AdminToken)
|
||||
}
|
||||
if cfg.Garage.Endpoint != "http://custom:3900" {
|
||||
t.Errorf("Endpoint = %q, want http://custom:3900", cfg.Garage.Endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_EnvOverridesToml(t *testing.T) {
|
||||
resetViper(t)
|
||||
tomlPath := writeToml(t, testGarageToml)
|
||||
missingYaml := filepath.Join(t.TempDir(), "nope.yaml")
|
||||
t.Setenv("GARAGE_UI_GARAGE_ADMIN_TOKEN", "env-wins")
|
||||
|
||||
cfg, err := Load(missingYaml, WithGarageToml(tomlPath))
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.Garage.AdminToken != "env-wins" {
|
||||
t.Errorf("AdminToken = %q, want env-wins (env overrides toml)", cfg.Garage.AdminToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_TokenAuthAutoEnabled(t *testing.T) {
|
||||
cfg := validBaseConfig()
|
||||
cfg.ResolveTokenAuth()
|
||||
if !cfg.Auth.Token.Enabled {
|
||||
t.Error("expected token auth to be auto-enabled when no other auth is configured")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_TokenAuthNotAutoEnabledWhenAdminEnabled(t *testing.T) {
|
||||
cfg := validBaseConfig()
|
||||
cfg.Auth.Admin.Enabled = true
|
||||
cfg.Auth.Admin.Username = "u"
|
||||
cfg.Auth.Admin.Password = "p"
|
||||
cfg.ResolveTokenAuth()
|
||||
if cfg.Auth.Token.Enabled {
|
||||
t.Error("expected token auth to stay disabled when admin auth is configured")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_TokenAuthNotAutoEnabledWhenOIDCEnabled(t *testing.T) {
|
||||
cfg := validBaseConfig()
|
||||
applyValidOIDC(&cfg)
|
||||
cfg.ResolveTokenAuth()
|
||||
if cfg.Auth.Token.Enabled {
|
||||
t.Error("expected token auth to stay disabled when OIDC is configured")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_TokenAuthExplicitlyEnabled(t *testing.T) {
|
||||
cfg := validBaseConfig()
|
||||
cfg.Auth.Admin.Enabled = true
|
||||
cfg.Auth.Admin.Username = "u"
|
||||
cfg.Auth.Admin.Password = "p"
|
||||
cfg.Auth.Token.Enabled = true
|
||||
cfg.ResolveTokenAuth()
|
||||
if !cfg.Auth.Token.Enabled {
|
||||
t.Error("expected token auth to stay enabled when explicitly set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsProduction(t *testing.T) {
|
||||
tests := []struct {
|
||||
env string
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
toml "github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
// GarageTomlResult holds the values extracted from a garage.toml file.
|
||||
type GarageTomlResult struct {
|
||||
Endpoint string
|
||||
AdminEndpoint string
|
||||
AdminToken string
|
||||
Region string
|
||||
}
|
||||
|
||||
// garageTomlFile represents the subset of garage.toml we care about.
|
||||
type garageTomlFile struct {
|
||||
S3API struct {
|
||||
APIBindAddr string `toml:"api_bind_addr"`
|
||||
S3Region string `toml:"s3_region"`
|
||||
} `toml:"s3_api"`
|
||||
Admin struct {
|
||||
APIBindAddr string `toml:"api_bind_addr"`
|
||||
AdminToken string `toml:"admin_token"`
|
||||
} `toml:"admin"`
|
||||
}
|
||||
|
||||
// ParseGarageToml reads a garage.toml file and extracts the values needed
|
||||
// for garage-ui configuration.
|
||||
func ParseGarageToml(path string) (*GarageTomlResult, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading garage.toml: %w", err)
|
||||
}
|
||||
|
||||
var f garageTomlFile
|
||||
if err := toml.Unmarshal(data, &f); err != nil {
|
||||
return nil, fmt.Errorf("parsing garage.toml: %w", err)
|
||||
}
|
||||
|
||||
if f.Admin.AdminToken == "" {
|
||||
return nil, fmt.Errorf("garage.toml: [admin].admin_token is required")
|
||||
}
|
||||
if f.Admin.APIBindAddr == "" {
|
||||
return nil, fmt.Errorf("garage.toml: [admin].api_bind_addr is required")
|
||||
}
|
||||
if f.S3API.APIBindAddr == "" {
|
||||
return nil, fmt.Errorf("garage.toml: [s3_api].api_bind_addr is required")
|
||||
}
|
||||
|
||||
adminEndpoint, err := convertBindAddr(f.Admin.APIBindAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("garage.toml: converting admin api_bind_addr: %w", err)
|
||||
}
|
||||
|
||||
s3Endpoint, err := convertBindAddr(f.S3API.APIBindAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("garage.toml: converting s3_api api_bind_addr: %w", err)
|
||||
}
|
||||
|
||||
region := f.S3API.S3Region
|
||||
if region == "" {
|
||||
region = "garage"
|
||||
}
|
||||
|
||||
return &GarageTomlResult{
|
||||
Endpoint: s3Endpoint,
|
||||
AdminEndpoint: adminEndpoint,
|
||||
AdminToken: f.Admin.AdminToken,
|
||||
Region: region,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// convertBindAddr converts a bind address like "[::]:3900" into an HTTP
|
||||
// endpoint like "http://127.0.0.1:3900". Wildcard hosts (::, 0.0.0.0, empty)
|
||||
// are replaced with 127.0.0.1.
|
||||
func convertBindAddr(bindAddr string) (string, error) {
|
||||
if bindAddr == "" {
|
||||
return "", fmt.Errorf("bind address is empty")
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(bindAddr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid bind address %q: %w", bindAddr, err)
|
||||
}
|
||||
|
||||
if port == "" {
|
||||
return "", fmt.Errorf("bind address %q has no port", bindAddr)
|
||||
}
|
||||
|
||||
switch host {
|
||||
case "", "::", "0.0.0.0":
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
|
||||
if strings.Contains(host, ":") {
|
||||
host = "[" + host + "]"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("http://%s:%s", host, port), nil
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeTomlFile(t *testing.T, content string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "garage.toml")
|
||||
if err := os.WriteFile(path, []byte(content), 0600); err != nil {
|
||||
t.Fatalf("write toml: %v", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
const validGarageToml = `
|
||||
metadata_dir = "/var/lib/garage/meta"
|
||||
data_dir = "/var/lib/garage/data"
|
||||
replication_factor = 1
|
||||
|
||||
rpc_bind_addr = "[::]:3901"
|
||||
rpc_public_addr = "127.0.0.1:3901"
|
||||
rpc_secret = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
[s3_api]
|
||||
s3_region = "garage"
|
||||
api_bind_addr = "[::]:3900"
|
||||
root_domain = ".s3.garage"
|
||||
|
||||
[s3_web]
|
||||
bind_addr = "[::]:3902"
|
||||
root_domain = ".web.garage"
|
||||
index = "index.html"
|
||||
|
||||
[admin]
|
||||
api_bind_addr = "[::]:3903"
|
||||
admin_token = "my-secret-admin-token"
|
||||
`
|
||||
|
||||
func TestParseGarageToml_ValidFile(t *testing.T) {
|
||||
path := writeTomlFile(t, validGarageToml)
|
||||
result, err := ParseGarageToml(path)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseGarageToml: %v", err)
|
||||
}
|
||||
if result.AdminToken != "my-secret-admin-token" {
|
||||
t.Errorf("AdminToken = %q, want my-secret-admin-token", result.AdminToken)
|
||||
}
|
||||
if result.AdminEndpoint != "http://127.0.0.1:3903" {
|
||||
t.Errorf("AdminEndpoint = %q, want http://127.0.0.1:3903", result.AdminEndpoint)
|
||||
}
|
||||
if result.Endpoint != "http://127.0.0.1:3900" {
|
||||
t.Errorf("Endpoint = %q, want http://127.0.0.1:3900", result.Endpoint)
|
||||
}
|
||||
if result.Region != "garage" {
|
||||
t.Errorf("Region = %q, want garage", result.Region)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGarageToml_MissingAdminToken(t *testing.T) {
|
||||
toml := `
|
||||
[admin]
|
||||
api_bind_addr = "[::]:3903"
|
||||
[s3_api]
|
||||
api_bind_addr = "[::]:3900"
|
||||
`
|
||||
path := writeTomlFile(t, toml)
|
||||
_, err := ParseGarageToml(path)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing admin_token, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGarageToml_MissingAdminBindAddr(t *testing.T) {
|
||||
toml := `
|
||||
[admin]
|
||||
admin_token = "tok"
|
||||
[s3_api]
|
||||
api_bind_addr = "[::]:3900"
|
||||
`
|
||||
path := writeTomlFile(t, toml)
|
||||
_, err := ParseGarageToml(path)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing admin api_bind_addr, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGarageToml_MissingS3BindAddr(t *testing.T) {
|
||||
toml := `
|
||||
[admin]
|
||||
admin_token = "tok"
|
||||
api_bind_addr = "[::]:3903"
|
||||
[s3_api]
|
||||
s3_region = "garage"
|
||||
`
|
||||
path := writeTomlFile(t, toml)
|
||||
_, err := ParseGarageToml(path)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing s3_api api_bind_addr, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGarageToml_DefaultRegion(t *testing.T) {
|
||||
toml := `
|
||||
[admin]
|
||||
admin_token = "tok"
|
||||
api_bind_addr = "[::]:3903"
|
||||
[s3_api]
|
||||
api_bind_addr = "[::]:3900"
|
||||
`
|
||||
path := writeTomlFile(t, toml)
|
||||
result, err := ParseGarageToml(path)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseGarageToml: %v", err)
|
||||
}
|
||||
if result.Region != "garage" {
|
||||
t.Errorf("Region = %q, want garage (default)", result.Region)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGarageToml_FileNotFound(t *testing.T) {
|
||||
_, err := ParseGarageToml("/nonexistent/garage.toml")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing file, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertBindAddr(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
bindAddr string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{"ipv6 wildcard", "[::]:3900", "http://127.0.0.1:3900", false},
|
||||
{"ipv4 wildcard", "0.0.0.0:3900", "http://127.0.0.1:3900", false},
|
||||
{"localhost", "127.0.0.1:3900", "http://127.0.0.1:3900", false},
|
||||
{"specific ipv4", "192.168.1.1:3900", "http://192.168.1.1:3900", false},
|
||||
{"ipv6 localhost", "[::1]:3900", "http://[::1]:3900", false},
|
||||
{"specific ipv6", "[2001:db8::1]:3900", "http://[2001:db8::1]:3900", false},
|
||||
{"empty host", ":3900", "http://127.0.0.1:3900", false},
|
||||
{"empty string", "", "", true},
|
||||
{"no port", "127.0.0.1", "", true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := convertBindAddr(tc.bindAddr)
|
||||
if tc.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got %q", got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("convertBindAddr(%q) = %q, want %q", tc.bindAddr, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
|
||||
"Noooste/garage-ui/internal/auth"
|
||||
"Noooste/garage-ui/internal/config"
|
||||
"Noooste/garage-ui/internal/models"
|
||||
@@ -38,6 +40,9 @@ func (h *AuthHandler) GetAuthConfig(c fiber.Ctx) error {
|
||||
"oidc": fiber.Map{
|
||||
"enabled": h.cfg.Auth.OIDC.Enabled,
|
||||
},
|
||||
"token": fiber.Map{
|
||||
"enabled": h.cfg.Auth.Token.Enabled,
|
||||
},
|
||||
}
|
||||
|
||||
// Add provider name if OIDC is enabled
|
||||
@@ -108,6 +113,47 @@ func (h *AuthHandler) LoginAdmin(c fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
// LoginTokenRequest represents the token auth login request
|
||||
type LoginTokenRequest struct {
|
||||
Token string `json:"token" validate:"required"`
|
||||
}
|
||||
|
||||
// LoginToken handles admin token authentication login
|
||||
func (h *AuthHandler) LoginToken(c fiber.Ctx) error {
|
||||
var req LoginTokenRequest
|
||||
if err := c.Bind().JSON(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(
|
||||
models.ErrorResponse(models.ErrCodeBadRequest, "Invalid request body"),
|
||||
)
|
||||
}
|
||||
|
||||
// Constant-time comparison to prevent timing attacks
|
||||
if subtle.ConstantTimeCompare([]byte(h.cfg.Garage.AdminToken), []byte(req.Token)) != 1 {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(
|
||||
models.ErrorResponse(models.ErrCodeUnauthorized, "Invalid admin token"),
|
||||
)
|
||||
}
|
||||
|
||||
userInfo := &auth.UserInfo{
|
||||
Username: "admin-token",
|
||||
}
|
||||
|
||||
sessionToken, err := h.authService.GenerateSessionToken(userInfo)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(
|
||||
models.ErrorResponse(models.ErrCodeInternalError, "Failed to create session"),
|
||||
)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"token": sessionToken,
|
||||
"user": fiber.Map{
|
||||
"username": userInfo.Username,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetMe returns the current authenticated user's information
|
||||
//
|
||||
// @Summary Get current user
|
||||
|
||||
@@ -250,6 +250,139 @@ func TestLoginAdmin_MalformedJSONReturns400(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginToken_Success(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Garage: config.GarageConfig{
|
||||
AdminToken: "test-admin-token",
|
||||
Endpoint: "http://g:3900",
|
||||
AdminEndpoint: "http://g:3903",
|
||||
},
|
||||
Auth: config.AuthConfig{
|
||||
Token: config.TokenAuthConfig{Enabled: true},
|
||||
},
|
||||
}
|
||||
svc := newAuthTestService(t, cfg.Auth.Admin)
|
||||
h := NewAuthHandler(cfg, svc)
|
||||
app := fiber.New()
|
||||
app.Post("/auth/login-token", h.LoginToken)
|
||||
|
||||
body := `{"token":"test-admin-token"}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/auth/login-token", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("status = %d, want 200\nbody: %s", resp.StatusCode, raw)
|
||||
}
|
||||
|
||||
var decoded struct {
|
||||
Success bool `json:"success"`
|
||||
Token string `json:"token"`
|
||||
User struct {
|
||||
Username string `json:"username"`
|
||||
} `json:"user"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if !decoded.Success {
|
||||
t.Error("success = false")
|
||||
}
|
||||
if decoded.Token == "" {
|
||||
t.Error("token empty")
|
||||
}
|
||||
if decoded.User.Username != "admin-token" {
|
||||
t.Errorf("username = %q, want admin-token", decoded.User.Username)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginToken_WrongToken(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Garage: config.GarageConfig{
|
||||
AdminToken: "test-admin-token",
|
||||
Endpoint: "http://g:3900",
|
||||
AdminEndpoint: "http://g:3903",
|
||||
},
|
||||
Auth: config.AuthConfig{
|
||||
Token: config.TokenAuthConfig{Enabled: true},
|
||||
},
|
||||
}
|
||||
svc := newAuthTestService(t, cfg.Auth.Admin)
|
||||
h := NewAuthHandler(cfg, svc)
|
||||
app := fiber.New()
|
||||
app.Post("/auth/login-token", h.LoginToken)
|
||||
|
||||
body := `{"token":"wrong-token"}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/auth/login-token", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Fatalf("status = %d, want 401", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginToken_MalformedJSONReturns400(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Garage: config.GarageConfig{
|
||||
AdminToken: "test-admin-token",
|
||||
Endpoint: "http://g:3900",
|
||||
AdminEndpoint: "http://g:3903",
|
||||
},
|
||||
Auth: config.AuthConfig{
|
||||
Token: config.TokenAuthConfig{Enabled: true},
|
||||
},
|
||||
}
|
||||
svc := newAuthTestService(t, cfg.Auth.Admin)
|
||||
h := NewAuthHandler(cfg, svc)
|
||||
app := fiber.New()
|
||||
app.Post("/auth/login-token", h.LoginToken)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/auth/login-token", strings.NewReader("{not-json"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want 400", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAuthConfig_TokenEnabled(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Auth: config.AuthConfig{
|
||||
Token: config.TokenAuthConfig{Enabled: true},
|
||||
},
|
||||
}
|
||||
app, _ := newAuthTestApp(t, cfg)
|
||||
req := httptest.NewRequest(http.MethodGet, "/auth/config", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var body struct {
|
||||
Token struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
} `json:"token"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if !body.Token.Enabled {
|
||||
t.Error("token.enabled = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMe_OIDCUserInfoLocal(t *testing.T) {
|
||||
cfg := &config.Config{Auth: config.AuthConfig{}}
|
||||
app, h := newAuthTestApp(t, cfg)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"Noooste/garage-ui/internal/auth"
|
||||
"Noooste/garage-ui/internal/config"
|
||||
"Noooste/garage-ui/internal/models"
|
||||
@@ -17,14 +19,14 @@ import (
|
||||
func AuthMiddleware(cfg *config.AuthConfig, authService *auth.Service) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
// If no auth is enabled, allow all requests.
|
||||
if !cfg.Admin.Enabled && !cfg.OIDC.Enabled {
|
||||
if !cfg.Admin.Enabled && !cfg.OIDC.Enabled && !cfg.Token.Enabled {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
authHeader := c.Get("Authorization")
|
||||
|
||||
// Try admin auth if enabled and header is present.
|
||||
if cfg.Admin.Enabled && authHeader != "" {
|
||||
// Try bearer token auth (works for admin, token, or any JWT session)
|
||||
if (cfg.Admin.Enabled || cfg.Token.Enabled) && authHeader != "" {
|
||||
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
|
||||
token := authHeader[7:]
|
||||
userInfo, err := authService.ValidateSessionToken(token)
|
||||
@@ -80,14 +82,18 @@ func enrichRequestLogger(c fiber.Ctx, userID, authMethod string) {
|
||||
}
|
||||
|
||||
func authMethodsEnabled(cfg *config.AuthConfig) string {
|
||||
switch {
|
||||
case cfg.Admin.Enabled && cfg.OIDC.Enabled:
|
||||
return "admin+oidc"
|
||||
case cfg.Admin.Enabled:
|
||||
return "admin"
|
||||
case cfg.OIDC.Enabled:
|
||||
return "oidc"
|
||||
default:
|
||||
methods := []string{}
|
||||
if cfg.Admin.Enabled {
|
||||
methods = append(methods, "admin")
|
||||
}
|
||||
if cfg.OIDC.Enabled {
|
||||
methods = append(methods, "oidc")
|
||||
}
|
||||
if cfg.Token.Enabled {
|
||||
methods = append(methods, "token")
|
||||
}
|
||||
if len(methods) == 0 {
|
||||
return "none"
|
||||
}
|
||||
return strings.Join(methods, "+")
|
||||
}
|
||||
|
||||
@@ -152,8 +152,13 @@ func SetupRoutes(
|
||||
app.Post("/auth/login", authHandler.LoginAdmin)
|
||||
}
|
||||
|
||||
// Token auth login endpoint (only if token auth is enabled)
|
||||
if cfg.Auth.Token.Enabled {
|
||||
app.Post("/auth/login-token", authHandler.LoginToken)
|
||||
}
|
||||
|
||||
// Auth "me" endpoint (if any auth is enabled)
|
||||
if cfg.Auth.Admin.Enabled || cfg.Auth.OIDC.Enabled {
|
||||
if cfg.Auth.Admin.Enabled || cfg.Auth.OIDC.Enabled || cfg.Auth.Token.Enabled {
|
||||
app.Get("/auth/me", middleware.AuthMiddleware(&cfg.Auth, authService), authHandler.GetMe)
|
||||
}
|
||||
|
||||
|
||||
+28
-1
@@ -64,15 +64,32 @@ var version = "dev"
|
||||
func main() {
|
||||
// Parse command-line flags
|
||||
configPath := flag.String("config", "config.yaml", "Path to configuration file")
|
||||
garageTomlPath := flag.String("garage-toml", "", "Path to garage.toml file (extracts Garage connection values)")
|
||||
flag.Parse()
|
||||
|
||||
// Env var fallback for --garage-toml
|
||||
if *garageTomlPath == "" {
|
||||
if envPath := os.Getenv("GARAGE_UI_GARAGE_TOML"); envPath != "" {
|
||||
*garageTomlPath = envPath
|
||||
}
|
||||
}
|
||||
|
||||
// Build load options
|
||||
var loadOpts []config.LoadOption
|
||||
if *garageTomlPath != "" {
|
||||
loadOpts = append(loadOpts, config.WithGarageToml(*garageTomlPath))
|
||||
}
|
||||
|
||||
// Load configuration first (before initializing logger)
|
||||
cfg, err := config.Load(*configPath)
|
||||
cfg, err := config.Load(*configPath, loadOpts...)
|
||||
if err != nil {
|
||||
// If config fails to load, use default logger to report the error
|
||||
logger.Get().Fatal().Err(err).Str("config_path", *configPath).Msg("Failed to load configuration")
|
||||
}
|
||||
|
||||
// Auto-enable token auth if no other auth method is configured
|
||||
cfg.ResolveTokenAuth()
|
||||
|
||||
// Initialize logger with configuration from config file
|
||||
logger.Init(logger.Config{
|
||||
Level: cfg.Logging.Level,
|
||||
@@ -87,6 +104,13 @@ func main() {
|
||||
Str("environment", cfg.Server.Environment).
|
||||
Msg("Starting Garage UI Backend")
|
||||
|
||||
if *garageTomlPath != "" {
|
||||
logger.Warn().
|
||||
Str("s3_endpoint", cfg.Garage.Endpoint).
|
||||
Str("admin_endpoint", cfg.Garage.AdminEndpoint).
|
||||
Msg("Endpoints inferred from garage.toml bind addresses — override with GARAGE_UI_GARAGE_ENDPOINT / GARAGE_UI_GARAGE_ADMIN_ENDPOINT for remote/container setups")
|
||||
}
|
||||
|
||||
// Initialize services
|
||||
logger.Info().Msg("Detecting Garage API version")
|
||||
adminResult, err := services.NewAdminService(&cfg.Garage, cfg.Logging.Level)
|
||||
@@ -107,6 +131,9 @@ func main() {
|
||||
if cfg.Auth.OIDC.Enabled {
|
||||
authMethods = append(authMethods, "oidc")
|
||||
}
|
||||
if cfg.Auth.Token.Enabled {
|
||||
authMethods = append(authMethods, "token")
|
||||
}
|
||||
if len(authMethods) == 0 {
|
||||
authMethods = append(authMethods, "none")
|
||||
}
|
||||
|
||||
@@ -40,6 +40,12 @@ auth:
|
||||
username: "admin"
|
||||
password: "changeme"
|
||||
|
||||
# Admin Token Authentication
|
||||
# When enabled, users can log in using the Garage admin token
|
||||
# Auto-enabled when no other auth method is configured (zero-config fallback)
|
||||
token:
|
||||
enabled: false # Set to true to explicitly enable, or leave all auth disabled for auto-enable
|
||||
|
||||
# OIDC Configuration
|
||||
# NOTE: When OIDC is enabled, server.root_url is required for OAuth2 redirects
|
||||
# The redirect URL will be automatically constructed as: {root_url}/auth/oidc/callback
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useAuthStore } from '@/store/auth-store';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
export function TokenLoginForm() {
|
||||
const [token, setToken] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const { loginToken } = useAuthStore();
|
||||
|
||||
const returnUrl = searchParams.get('returnUrl') || '/';
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await loginToken(token);
|
||||
navigate(decodeURIComponent(returnUrl));
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader className="space-y-1">
|
||||
<div className="flex items-center justify-center mb-4">
|
||||
<img
|
||||
src="/garage.png"
|
||||
alt="Garage Logo"
|
||||
className="h-16 w-16 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<CardTitle className="text-2xl text-center">
|
||||
Welcome to Garage UI
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="admin-token" className="text-sm font-medium">Admin Token</label>
|
||||
<Input
|
||||
id="admin-token"
|
||||
type="password"
|
||||
placeholder="Enter your Garage admin token"
|
||||
value={token}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
required
|
||||
disabled={isLoading}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isLoading || !token}
|
||||
>
|
||||
{isLoading ? 'Signing in...' : 'Sign in'}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -133,6 +133,7 @@ export const authApi = {
|
||||
const response = await authApiClient.get<{
|
||||
admin: { enabled: boolean };
|
||||
oidc: { enabled: boolean; provider?: string };
|
||||
token: { enabled: boolean };
|
||||
}>('/config');
|
||||
return response;
|
||||
},
|
||||
@@ -145,6 +146,13 @@ export const authApi = {
|
||||
return response;
|
||||
},
|
||||
|
||||
loginToken: async (token: string) => {
|
||||
const response = await authApiClient.post<{ success: boolean; token: string; user: AuthUser }>('/login-token', {
|
||||
token,
|
||||
});
|
||||
return response;
|
||||
},
|
||||
|
||||
me: async () => {
|
||||
const response = await authApiClient.get<{ success: boolean; user: AuthUser }>('/me');
|
||||
return response;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useAuthStore } from '@/store/auth-store';
|
||||
import { BasicLoginForm } from '@/components/auth/BasicLoginForm';
|
||||
import { OIDCLoginView } from '@/components/auth/OIDCLoginView';
|
||||
import { TokenLoginForm } from '@/components/auth/TokenLoginForm';
|
||||
import { LoadingSpinner } from '@/components/auth/LoadingSpinner';
|
||||
|
||||
export function Login() {
|
||||
@@ -14,9 +15,7 @@ export function Login() {
|
||||
const returnUrl = searchParams.get('returnUrl') || '/';
|
||||
|
||||
useEffect(() => {
|
||||
// Handle OIDC callback
|
||||
if (loginSuccess === 'success') {
|
||||
// OIDC login successful, re-initialize auth to fetch user
|
||||
initialize().then(() => {
|
||||
navigate(decodeURIComponent(returnUrl));
|
||||
});
|
||||
@@ -24,7 +23,6 @@ export function Login() {
|
||||
}, [loginSuccess, initialize, navigate, returnUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
// If already authenticated, redirect to return URL
|
||||
if (isAuthenticated && !loginSuccess) {
|
||||
navigate(decodeURIComponent(returnUrl));
|
||||
}
|
||||
@@ -35,16 +33,27 @@ export function Login() {
|
||||
}
|
||||
|
||||
// No auth enabled, redirect to dashboard immediately
|
||||
if (config && !config.admin.enabled && !config.oidc.enabled) {
|
||||
if (config && !config.admin.enabled && !config.oidc.enabled && !config.token.enabled) {
|
||||
navigate('/');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Show login options based on what's enabled
|
||||
const showAdmin = config?.admin.enabled || false;
|
||||
const showOIDC = config?.oidc.enabled || false;
|
||||
const showToken = config?.token.enabled || false;
|
||||
|
||||
// If both are enabled, show both options in single modal
|
||||
// Token-only auth (zero-config fallback)
|
||||
if (showToken && !showAdmin && !showOIDC) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<TokenLoginForm />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Both admin and OIDC enabled
|
||||
if (showAdmin && showOIDC) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
||||
@@ -55,12 +64,12 @@ export function Login() {
|
||||
);
|
||||
}
|
||||
|
||||
// Show only OIDC if enabled
|
||||
// Only OIDC
|
||||
if (showOIDC) {
|
||||
return <OIDCLoginView />;
|
||||
}
|
||||
|
||||
// Show only admin if enabled
|
||||
// Only admin
|
||||
if (showAdmin) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
||||
@@ -71,6 +80,5 @@ export function Login() {
|
||||
);
|
||||
}
|
||||
|
||||
// Still loading config
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ interface AuthStore extends AuthState {
|
||||
// Async actions
|
||||
initialize: () => Promise<void>;
|
||||
loginAdmin: (username: string, password: string) => Promise<void>;
|
||||
loginToken: (token: string) => Promise<void>;
|
||||
loginOIDC: () => void;
|
||||
logout: () => Promise<void>;
|
||||
}
|
||||
@@ -45,7 +46,7 @@ export const useAuthStore = create<AuthStore>()(
|
||||
set({ config });
|
||||
|
||||
// If no auth is enabled, mark as authenticated immediately
|
||||
if (!config.admin.enabled && !config.oidc.enabled) {
|
||||
if (!config.admin.enabled && !config.oidc.enabled && !config.token.enabled) {
|
||||
set({
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
@@ -113,6 +114,36 @@ export const useAuthStore = create<AuthStore>()(
|
||||
}
|
||||
},
|
||||
|
||||
loginToken: async (token) => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
const response = await authApi.loginToken(token);
|
||||
const { token: sessionToken, user } = response.data;
|
||||
|
||||
localStorage.setItem('auth-token', sessionToken);
|
||||
|
||||
set({
|
||||
user,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
(error as { response?: { data?: { error?: { message?: string } } } })
|
||||
.response?.data?.error?.message ||
|
||||
(error instanceof Error ? error.message : 'Login failed');
|
||||
set({
|
||||
error: errorMessage,
|
||||
isLoading: false,
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
loginOIDC: () => {
|
||||
// Redirect to OIDC login endpoint
|
||||
window.location.href = '/auth/oidc/login';
|
||||
|
||||
@@ -6,6 +6,9 @@ export interface AuthConfig {
|
||||
enabled: boolean;
|
||||
provider?: string;
|
||||
};
|
||||
token: {
|
||||
enabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AuthUser {
|
||||
|
||||
Reference in New Issue
Block a user