mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-07-04 17:20:19 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66f4a29975 | |||
| 0c449321ff | |||
| c10c33c664 | |||
| dcb503b3be | |||
| fb48f1eb2d | |||
| 33a5b859cf |
@@ -32,6 +32,8 @@ TINYAUTH_SERVER_PORT=3000
|
|||||||
TINYAUTH_SERVER_ADDRESS="0.0.0.0"
|
TINYAUTH_SERVER_ADDRESS="0.0.0.0"
|
||||||
# The path to the Unix socket.
|
# The path to the Unix socket.
|
||||||
TINYAUTH_SERVER_SOCKETPATH=
|
TINYAUTH_SERVER_SOCKETPATH=
|
||||||
|
# Enable API docs with Scalar under /scalar.
|
||||||
|
TINYAUTH_SERVER_SCALARENABLED=true
|
||||||
|
|
||||||
# auth config
|
# auth config
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ PROD_COMPOSE := $(shell test -f "docker-compose.test.prod.yml" && echo "docker-c
|
|||||||
|
|
||||||
.DEFAULT_GOAL := binary
|
.DEFAULT_GOAL := binary
|
||||||
|
|
||||||
.PHONY: deps clean-data clean-webui webui binary binary-linux-amd64 binary-linux-arm64 test vet test-race dev dev-infisical prod prod-infisical sql generate docker docker-distroless
|
.PHONY: deps clean-data clean-webui webui binary binary-linux-amd64 binary-linux-arm64 test vet test-race dev dev-infisical prod prod-infisical sql generate docker docker-distroless swagger swagger-fmt
|
||||||
|
|
||||||
# Deps
|
# Deps
|
||||||
deps:
|
deps:
|
||||||
@@ -102,3 +102,11 @@ docker:
|
|||||||
# Docker image distroless
|
# Docker image distroless
|
||||||
docker-distroless:
|
docker-distroless:
|
||||||
docker buildx build -t tinyauthapp/tinyauth:dev-distroless --build-arg=VERSION=$(TAG_NAME) --build-arg=COMMIT_HASH=$(COMMIT_HASH) --build-arg=BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) -f Dockerfile.distroless .
|
docker buildx build -t tinyauthapp/tinyauth:dev-distroless --build-arg=VERSION=$(TAG_NAME) --build-arg=COMMIT_HASH=$(COMMIT_HASH) --build-arg=BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) -f Dockerfile.distroless .
|
||||||
|
|
||||||
|
# Swagger
|
||||||
|
swagger:
|
||||||
|
swag init -d ./internal -g bootstrap/router_bootstrap.go -o ./internal/swagger
|
||||||
|
|
||||||
|
# Swagger Format
|
||||||
|
swagger-fmt:
|
||||||
|
swag fmt -d ./internal -g bootstrap/router_bootstrap.go
|
||||||
@@ -62,6 +62,11 @@ export default defineConfig({
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/authorize/, ""),
|
rewrite: (path) => path.replace(/^\/authorize/, ""),
|
||||||
},
|
},
|
||||||
|
"/scalar": {
|
||||||
|
target: "http://tinyauth-backend:3000/scalar",
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/scalar/, ""),
|
||||||
|
}
|
||||||
},
|
},
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ go 1.26.4
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
charm.land/huh/v2 v2.0.3
|
charm.land/huh/v2 v2.0.3
|
||||||
|
charm.land/lipgloss/v2 v2.0.1
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3
|
github.com/cenkalti/backoff/v5 v5.0.3
|
||||||
github.com/docker/docker v28.5.2+incompatible
|
github.com/docker/docker v28.5.2+incompatible
|
||||||
github.com/gin-gonic/gin v1.12.0
|
github.com/gin-gonic/gin v1.12.0
|
||||||
@@ -19,12 +20,15 @@ require (
|
|||||||
github.com/rs/zerolog v1.35.1
|
github.com/rs/zerolog v1.35.1
|
||||||
github.com/steveiliop56/ding v0.2.0
|
github.com/steveiliop56/ding v0.2.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
|
github.com/swaggo/swag v1.16.6
|
||||||
|
github.com/tinyauthapp/gin-scalar v0.0.0-20260704144252-280c60a0cf2c
|
||||||
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298
|
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298
|
||||||
github.com/weppos/publicsuffix-go v0.50.3
|
github.com/weppos/publicsuffix-go v0.50.3
|
||||||
go.uber.org/dig v1.19.0
|
go.uber.org/dig v1.19.0
|
||||||
golang.org/x/crypto v0.53.0
|
golang.org/x/crypto v0.53.0
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
golang.org/x/tools v0.47.0
|
golang.org/x/tools v0.47.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
k8s.io/apimachinery v0.36.2
|
k8s.io/apimachinery v0.36.2
|
||||||
k8s.io/client-go v0.36.2
|
k8s.io/client-go v0.36.2
|
||||||
modernc.org/sqlite v1.53.0
|
modernc.org/sqlite v1.53.0
|
||||||
@@ -34,11 +38,11 @@ require (
|
|||||||
require (
|
require (
|
||||||
charm.land/bubbles/v2 v2.0.0 // indirect
|
charm.land/bubbles/v2 v2.0.0 // indirect
|
||||||
charm.land/bubbletea/v2 v2.0.2 // indirect
|
charm.land/bubbletea/v2 v2.0.2 // indirect
|
||||||
charm.land/lipgloss/v2 v2.0.1 // indirect
|
|
||||||
dario.cat/mergo v1.0.1 // indirect
|
dario.cat/mergo v1.0.1 // indirect
|
||||||
filippo.io/edwards25519 v1.2.0 // indirect
|
filippo.io/edwards25519 v1.2.0 // indirect
|
||||||
github.com/Azure/go-ntlmssp v0.1.1 // indirect
|
github.com/Azure/go-ntlmssp v0.1.1 // indirect
|
||||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||||
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||||
@@ -82,6 +86,10 @@ require (
|
|||||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
|
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.20.4 // indirect
|
||||||
|
github.com/go-openapi/spec v0.20.4 // indirect
|
||||||
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||||
@@ -98,12 +106,14 @@ require (
|
|||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
|
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.18.5 // indirect
|
github.com/klauspost/compress v1.18.5 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.20 // indirect
|
github.com/mattn/go-runewidth v0.0.20 // indirect
|
||||||
@@ -169,7 +179,6 @@ require (
|
|||||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
gotest.tools/v3 v3.5.2 // indirect
|
gotest.tools/v3 v3.5.2 // indirect
|
||||||
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect
|
gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect
|
||||||
k8s.io/klog/v2 v2.140.0 // indirect
|
k8s.io/klog/v2 v2.140.0 // indirect
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDP
|
|||||||
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||||
@@ -30,6 +32,8 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe
|
|||||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
||||||
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||||
@@ -133,6 +137,7 @@ github.com/creachadair/msync v0.7.1 h1:SeZmuEBXQPe5GqV/C94ER7QIZPwtvFbeQiykzt/7u
|
|||||||
github.com/creachadair/msync v0.7.1/go.mod h1:8CcFlLsSujfHE5wWm19uUBLHIPDAUr6LXDwneVMO008=
|
github.com/creachadair/msync v0.7.1/go.mod h1:8CcFlLsSujfHE5wWm19uUBLHIPDAUr6LXDwneVMO008=
|
||||||
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
|
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
|
||||||
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
|
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -192,10 +197,17 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
|||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
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.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||||
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
|
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
|
||||||
github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
|
github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
|
||||||
|
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||||
|
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||||
|
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
|
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
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.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
@@ -297,8 +309,11 @@ github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS
|
|||||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
||||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
@@ -307,6 +322,9 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
|||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
@@ -361,6 +379,7 @@ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOF
|
|||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
@@ -414,6 +433,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 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
@@ -421,6 +441,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
|||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
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/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d h1:JcGKBZAL7ePLwOhUdN8qGQZlP5GueEiIZwY7R62pejE=
|
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d h1:JcGKBZAL7ePLwOhUdN8qGQZlP5GueEiIZwY7R62pejE=
|
||||||
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
|
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
|
||||||
github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89 h1:glgVc1ZYMjwN1Q/ITWeuSQyl029uayagaR2sjsifehc=
|
github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89 h1:glgVc1ZYMjwN1Q/ITWeuSQyl029uayagaR2sjsifehc=
|
||||||
@@ -445,6 +467,8 @@ github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp
|
|||||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
||||||
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
|
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
|
||||||
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
||||||
|
github.com/tinyauthapp/gin-scalar v0.0.0-20260704144252-280c60a0cf2c h1:N91CdjSrEXoFtC+buYFz4CVcOQwAu3u7xHLlzkpuctA=
|
||||||
|
github.com/tinyauthapp/gin-scalar v0.0.0-20260704144252-280c60a0cf2c/go.mod h1:fIFpOPONYJcCZr0oJBV8kXCVxJu+0Wc73bnkITihYnY=
|
||||||
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298 h1:EYSb5jv8ZL/0/NVFZtY7Ejplk0QG5+3lrdL3mSrjFZQ=
|
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298 h1:EYSb5jv8ZL/0/NVFZtY7Ejplk0QG5+3lrdL3mSrjFZQ=
|
||||||
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298/go.mod h1:TlUDoCF66hMqFZqoBym9bUdJ0bKAWYMir6hLJeYN5z0=
|
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298/go.mod h1:TlUDoCF66hMqFZqoBym9bUdJ0bKAWYMir6hLJeYN5z0=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
@@ -509,6 +533,7 @@ golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
|||||||
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||||
|
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
@@ -516,16 +541,22 @@ golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7
|
|||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||||
@@ -542,13 +573,19 @@ google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07
|
|||||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
||||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
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-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
|
|||||||
@@ -6,17 +6,26 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
ginScalar "github.com/tinyauthapp/gin-scalar"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/controller"
|
"github.com/tinyauthapp/tinyauth/internal/controller"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/middleware"
|
"github.com/tinyauthapp/tinyauth/internal/middleware"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
|
docs "github.com/tinyauthapp/tinyauth/internal/swagger"
|
||||||
"go.uber.org/dig"
|
"go.uber.org/dig"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// @title Tinyauth API
|
||||||
|
// @version development
|
||||||
|
// @description Documentation for Tinyauth's API.
|
||||||
|
// @license.name AGPL-3.0
|
||||||
|
// @license.url https://github.com/tinyauthapp/tinyauth/blob/main/LICENSE
|
||||||
|
// @BasePath /
|
||||||
func (app *BootstrapApp) setupRouter() error {
|
func (app *BootstrapApp) setupRouter() error {
|
||||||
// we don't want gin debug mode
|
// we don't want gin debug mode
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
@@ -80,6 +89,14 @@ func (app *BootstrapApp) setupRouter() error {
|
|||||||
return fmt.Errorf("failed to provide api router group: %w", err)
|
return fmt.Errorf("failed to provide api router group: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if app.config.Server.ScalarEnabled {
|
||||||
|
err = app.setupScalar()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to setup scalar: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
controllerProvideFor := []any{
|
controllerProvideFor := []any{
|
||||||
controller.NewContextController,
|
controller.NewContextController,
|
||||||
controller.NewOAuthController,
|
controller.NewOAuthController,
|
||||||
@@ -125,6 +142,34 @@ func (app *BootstrapApp) setupRouter() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *BootstrapApp) setupScalar() error {
|
||||||
|
appUrl, err := url.Parse(app.runtime.AppURL)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse app url: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
docs.SwaggerInfo.Host = appUrl.Host
|
||||||
|
docs.SwaggerInfo.Schemes = []string{appUrl.Scheme}
|
||||||
|
docs.SwaggerInfo.Version = model.Version
|
||||||
|
|
||||||
|
type scalarInput struct {
|
||||||
|
dig.In
|
||||||
|
|
||||||
|
RouterGroup *gin.RouterGroup `name:"mainRouterGroup"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = app.dig.Invoke(func(i scalarInput) {
|
||||||
|
i.RouterGroup.GET("/scalar/*any", ginScalar.WrapHandler(nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to invoke scalar: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Top down
|
// Top down
|
||||||
// 1. Tailscale (if tailscale.listen)
|
// 1. Tailscale (if tailscale.listen)
|
||||||
// 2. Unix socket (if server.socketPath)
|
// 2. Unix socket (if server.socketPath)
|
||||||
|
|||||||
@@ -107,6 +107,14 @@ func NewContextController(i ContextControllerInput) *ContextController {
|
|||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserContext godoc
|
||||||
|
//
|
||||||
|
// @Summary User context
|
||||||
|
// @Description Get the user context
|
||||||
|
// @Tags context
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} UserContextResponse
|
||||||
|
// @Router /api/context/user [get]
|
||||||
func (controller *ContextController) userContextHandler(c *gin.Context) {
|
func (controller *ContextController) userContextHandler(c *gin.Context) {
|
||||||
context, err := new(model.UserContext).NewFromGin(c)
|
context, err := new(model.UserContext).NewFromGin(c)
|
||||||
|
|
||||||
@@ -147,6 +155,14 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
|
|||||||
c.JSON(200, userContext)
|
c.JSON(200, userContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AppContext godoc
|
||||||
|
//
|
||||||
|
// @Summary App context
|
||||||
|
// @Description Get the app context
|
||||||
|
// @Tags context
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} AppContextResponse
|
||||||
|
// @Router /api/context/app [get]
|
||||||
func (controller *ContextController) appContextHandler(c *gin.Context) {
|
func (controller *ContextController) appContextHandler(c *gin.Context) {
|
||||||
c.JSON(200, AppContextResponse{
|
c.JSON(200, AppContextResponse{
|
||||||
Status: 200,
|
Status: 200,
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ const (
|
|||||||
FrontendLoginForApp FrontendLoginFor = "app"
|
FrontendLoginForApp FrontendLoginFor = "app"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SimpleResponse struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
type UnauthorizedQuery struct {
|
type UnauthorizedQuery struct {
|
||||||
Username string `url:"username"`
|
Username string `url:"username"`
|
||||||
Resource string `url:"resource"`
|
Resource string `url:"resource"`
|
||||||
|
|||||||
@@ -23,9 +23,18 @@ func NewHealthController(i HealthControllerInput) *HealthController {
|
|||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HealthCheck godoc
|
||||||
|
//
|
||||||
|
// @Summary Healthcheck
|
||||||
|
// @Description Check if the server is up and running
|
||||||
|
// @Tags health
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} SimpleResponse
|
||||||
|
// @Router /api/healthz [get]
|
||||||
|
// @Router /api/healthz [head]
|
||||||
func (controller *HealthController) healthHandler(c *gin.Context) {
|
func (controller *HealthController) healthHandler(c *gin.Context) {
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, SimpleResponse{
|
||||||
"status": 200,
|
Status: 200,
|
||||||
"message": "Healthy",
|
Message: "Healthy",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ func TestHealthController(t *testing.T) {
|
|||||||
path: "/api/healthz",
|
path: "/api/healthz",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
expected: func() string {
|
expected: func() string {
|
||||||
expectedHealthResponse := map[string]any{
|
expectedHealthResponse := SimpleResponse{
|
||||||
"status": 200,
|
Status: 200,
|
||||||
"message": "Healthy",
|
Message: "Healthy",
|
||||||
}
|
}
|
||||||
bytes, err := json.Marshal(expectedHealthResponse)
|
bytes, err := json.Marshal(expectedHealthResponse)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -37,9 +37,9 @@ func TestHealthController(t *testing.T) {
|
|||||||
path: "/api/healthz",
|
path: "/api/healthz",
|
||||||
method: "HEAD",
|
method: "HEAD",
|
||||||
expected: func() string {
|
expected: func() string {
|
||||||
expectedHealthResponse := map[string]any{
|
expectedHealthResponse := SimpleResponse{
|
||||||
"status": 200,
|
Status: 200,
|
||||||
"message": "Healthy",
|
Message: "Healthy",
|
||||||
}
|
}
|
||||||
bytes, err := json.Marshal(expectedHealthResponse)
|
bytes, err := json.Marshal(expectedHealthResponse)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -54,6 +54,27 @@ func NewOAuthController(i OAuthControllerInput) *OAuthController {
|
|||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OAuthURLSuccessResponse struct {
|
||||||
|
SimpleResponse
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuthURL godoc
|
||||||
|
//
|
||||||
|
// @Summary OAuth URL
|
||||||
|
// @Description Get an OAuth URL for the specified provider
|
||||||
|
// @Tags oauth
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Provider ID"
|
||||||
|
// @Param login_for query string false "Login for"
|
||||||
|
// @Param oidc_ticket query string false "OpenID Connect Ticket"
|
||||||
|
// @Param oidc_scope query string false "OpenID Connect Scope"
|
||||||
|
// @Param oidc_name query string false "OpenID Connect Name"
|
||||||
|
// @Param redirect_uri query string false "Redirect URI"
|
||||||
|
// @Success 200 {object} OAuthURLSuccessResponse
|
||||||
|
// @Failure 400 {object} SimpleResponse
|
||||||
|
// @Failure 500 {object} SimpleResponse
|
||||||
|
// @Router /api/oauth/url/{id} [get]
|
||||||
func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
||||||
var req OAuthRequest
|
var req OAuthRequest
|
||||||
|
|
||||||
@@ -111,23 +132,33 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
|||||||
|
|
||||||
c.SetCookie(controller.runtime.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", controller.getCookieDomain(), controller.config.Auth.SecureCookie, true)
|
c.SetCookie(controller.runtime.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", controller.getCookieDomain(), controller.config.Auth.SecureCookie, true)
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, OAuthURLSuccessResponse{
|
||||||
"status": 200,
|
SimpleResponse: SimpleResponse{
|
||||||
"message": "OK",
|
Status: 200,
|
||||||
"url": authUrl,
|
Message: "OK",
|
||||||
|
},
|
||||||
|
URL: authUrl,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OAuthCallback godoc
|
||||||
|
//
|
||||||
|
// @Summary OAuth Callback
|
||||||
|
// @Description Callback URL for OAuth providers
|
||||||
|
// @Tags oauth
|
||||||
|
// @Param id path string true "Provider ID"
|
||||||
|
// @Param code query string true "State"
|
||||||
|
// @Param state query string true "Code"
|
||||||
|
// @Success 302
|
||||||
|
// @Failure 302
|
||||||
|
// @Router /api/oauth/callback/{id} [get]
|
||||||
func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||||
var req OAuthRequest
|
var req OAuthRequest
|
||||||
|
|
||||||
err := c.BindUri(&req)
|
err := c.BindUri(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to bind URI")
|
controller.log.App.Error().Err(err).Msg("Failed to get provider ID")
|
||||||
c.JSON(400, gin.H{
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
"status": 400,
|
|
||||||
"message": "Bad Request",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +166,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to get OAuth session cookie")
|
controller.log.App.Error().Err(err).Msg("Failed to get OAuth session cookie")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +176,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to get pending OAuth session")
|
controller.log.App.Error().Err(err).Msg("Failed to get pending OAuth session")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +185,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
state := c.Query("state")
|
state := c.Query("state")
|
||||||
if state != oauthPendingSession.State {
|
if state != oauthPendingSession.State {
|
||||||
controller.log.App.Warn().Msg("OAuth state mismatch")
|
controller.log.App.Warn().Msg("OAuth state mismatch")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +194,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to exchange code for token")
|
controller.log.App.Error().Err(err).Msg("Failed to exchange code for token")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,19 +202,19 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to get user info from OAuth provider")
|
controller.log.App.Error().Err(err).Msg("Failed to get user info from OAuth provider")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
controller.log.App.Warn().Msg("OAuth provider did not return user info")
|
controller.log.App.Warn().Msg("OAuth provider did not return user info")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Email == "" {
|
if user.Email == "" {
|
||||||
controller.log.App.Warn().Msg("OAuth provider did not return an email")
|
controller.log.App.Warn().Msg("OAuth provider did not return an email")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,13 +222,13 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to get OAuth service for session")
|
controller.log.App.Error().Err(err).Msg("Failed to get OAuth service for session")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if svc.ID() != req.Provider {
|
if svc.ID() != req.Provider {
|
||||||
controller.log.App.Warn().Msgf("OAuth provider mismatch: expected %s, got %s", req.Provider, svc.ID())
|
controller.log.App.Warn().Msgf("OAuth provider mismatch: expected %s, got %s", req.Provider, svc.ID())
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,11 +242,11 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to encode unauthorized query")
|
controller.log.App.Error().Err(err).Msg("Failed to encode unauthorized query")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.runtime.AppURL, queries.Encode()))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/unauthorized?%s", controller.runtime.AppURL, queries.Encode()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +291,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to create session cookie")
|
controller.log.App.Error().Err(err).Msg("Failed to create session cookie")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,10 +304,10 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
queries, err := query.Values(oauthPendingSession.CallbackParams)
|
queries, err := query.Values(oauthPendingSession.CallbackParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to encode OIDC callback query")
|
controller.log.App.Error().Err(err).Msg("Failed to encode OIDC callback query")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/oidc/authorize?%s", controller.runtime.AppURL, queries.Encode()))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/oidc/authorize?%s", controller.runtime.AppURL, queries.Encode()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,15 +319,15 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to encode redirect query")
|
controller.log.App.Error().Err(err).Msg("Failed to encode redirect query")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.runtime.AppURL, queries.Encode()))
|
c.Redirect(http.StatusFound, fmt.Sprintf("%s/continue?%s", controller.runtime.AppURL, queries.Encode()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, controller.runtime.AppURL)
|
c.Redirect(http.StatusFound, controller.runtime.AppURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (controller *OAuthController) isOidcRequest(params service.OAuthCallbackParams) bool {
|
func (controller *OAuthController) isOidcRequest(params service.OAuthCallbackParams) bool {
|
||||||
|
|||||||
@@ -82,6 +82,15 @@ type AuthorizeCompleteRequest struct {
|
|||||||
Ticket string `json:"ticket" binding:"required"`
|
Ticket string `json:"ticket" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthorizeCompleteResponse struct {
|
||||||
|
SimpleResponse
|
||||||
|
RedirectURI string `json:"redirect_uri"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OIDCErrorResponse struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
type OIDCControllerInput struct {
|
type OIDCControllerInput struct {
|
||||||
dig.In
|
dig.In
|
||||||
|
|
||||||
@@ -114,6 +123,36 @@ func NewOIDCController(i OIDCControllerInput) *OIDCController {
|
|||||||
// This endpoint does **not** return a code, it handles param validation, ticket creation
|
// This endpoint does **not** return a code, it handles param validation, ticket creation
|
||||||
// and then redirects to the frontend to handle the consent screen. It performs no destructive
|
// and then redirects to the frontend to handle the consent screen. It performs no destructive
|
||||||
// actions (like logging out an existing session)
|
// actions (like logging out an existing session)
|
||||||
|
// Authorize godoc
|
||||||
|
//
|
||||||
|
// @Summary Authorize
|
||||||
|
// @Description OpenID Connect Authorize Endpoint
|
||||||
|
// @Accept x-www-form-urlencoded
|
||||||
|
// @Tags oidc
|
||||||
|
// @Param scope query string false "OAuth scopes (space separated, must include openid)"
|
||||||
|
// @Param response_type query string false "Response type (e.g. code)"
|
||||||
|
// @Param client_id query string false "Client ID"
|
||||||
|
// @Param redirect_uri query string false "Redirect URI"
|
||||||
|
// @Param state query string false "Opaque state value returned to the client"
|
||||||
|
// @Param nonce query string false "Nonce for ID token replay protection"
|
||||||
|
// @Param code_challenge query string false "PKCE code challenge"
|
||||||
|
// @Param code_challenge_method query string false "PKCE code challenge method (S256 or plain)"
|
||||||
|
// @Param prompt query string false "Prompt parameter (none, login, consent)"
|
||||||
|
// @Param max_age query string false "Max authentication age in seconds"
|
||||||
|
// @Param scope formData string false "OAuth scopes (space separated, must include openid)"
|
||||||
|
// @Param response_type formData string false "Response type (e.g. code)"
|
||||||
|
// @Param client_id formData string false "Client ID"
|
||||||
|
// @Param redirect_uri formData string false "Redirect URI"
|
||||||
|
// @Param state formData string false "Opaque state value returned to the client"
|
||||||
|
// @Param nonce formData string false "Nonce for ID token replay protection"
|
||||||
|
// @Param code_challenge formData string false "PKCE code challenge"
|
||||||
|
// @Param code_challenge_method formData string false "PKCE code challenge method (S256 or plain)"
|
||||||
|
// @Param prompt formData string false "Prompt parameter (none, login, consent)"
|
||||||
|
// @Param max_age formData string false "Max authentication age in seconds"
|
||||||
|
// @Success 302
|
||||||
|
// @Failure 302
|
||||||
|
// @Router /authorize [get]
|
||||||
|
// @Router /authorize [post]
|
||||||
func (controller *OIDCController) authorize(c *gin.Context) {
|
func (controller *OIDCController) authorize(c *gin.Context) {
|
||||||
if controller.oidc == nil {
|
if controller.oidc == nil {
|
||||||
controller.authorizeError(c, authorizeErrorParams{
|
controller.authorizeError(c, authorizeErrorParams{
|
||||||
@@ -261,6 +300,16 @@ func (controller *OIDCController) authorize(c *gin.Context) {
|
|||||||
|
|
||||||
// The actual **internal** endpoint that actually creates the code and session.
|
// The actual **internal** endpoint that actually creates the code and session.
|
||||||
// It is called by the frontend after the user has logged in and given consent.
|
// It is called by the frontend after the user has logged in and given consent.
|
||||||
|
// AuthorizeComplete godoc
|
||||||
|
//
|
||||||
|
// @Summary Authorize Complete
|
||||||
|
// @Description Internal endpoint for the completion of the OpenID Connect authorization flow
|
||||||
|
// @Tags oidc
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} AuthorizeCompleteResponse
|
||||||
|
// @Failure 500
|
||||||
|
// @Router /api/oidc/authorize-complete [post]
|
||||||
func (controller *OIDCController) authorizeComplete(c *gin.Context) {
|
func (controller *OIDCController) authorizeComplete(c *gin.Context) {
|
||||||
if controller.oidc == nil {
|
if controller.oidc == nil {
|
||||||
// For this endpoint we return JSON errors since it's called
|
// For this endpoint we return JSON errors since it's called
|
||||||
@@ -361,17 +410,44 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, AuthorizeCompleteResponse{
|
||||||
"status": 200,
|
SimpleResponse: SimpleResponse{
|
||||||
"redirect_uri": fmt.Sprintf("%s?%s", authorizeReq.RedirectURI, queries.Encode()),
|
Status: 200,
|
||||||
|
},
|
||||||
|
RedirectURI: fmt.Sprintf("%s?%s", authorizeReq.RedirectURI, queries.Encode()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Token godoc
|
||||||
|
//
|
||||||
|
// @Summary Token
|
||||||
|
// @Description OpenID Connect Token Endpoint
|
||||||
|
// @Tags oidc
|
||||||
|
// @Accept x-www-form-urlencoded
|
||||||
|
// @Produce json
|
||||||
|
// @Param grant_type query string true "Grant type (authorization_code or refresh_token)"
|
||||||
|
// @Param code query string false "Authorization code (required for authorization_code grant)"
|
||||||
|
// @Param redirect_uri query string false "Redirect URI (must match the one from the authorize request)"
|
||||||
|
// @Param refresh_token query string false "Refresh token (required for refresh_token grant)"
|
||||||
|
// @Param client_id query string false "Client ID (required if not using Basic auth)"
|
||||||
|
// @Param client_secret query string false "Client secret (required for confidential clients without Basic auth)"
|
||||||
|
// @Param code_verifier query string false "PKCE code verifier (required if code_challenge was sent)"
|
||||||
|
// @Param grant_type formData string false "Grant type (authorization_code or refresh_token)"
|
||||||
|
// @Param code formData string false "Authorization code (required for authorization_code grant)"
|
||||||
|
// @Param redirect_uri formData string false "Redirect URI (must match the one from the authorize request)"
|
||||||
|
// @Param refresh_token formData string false "Refresh token (required for refresh_token grant)"
|
||||||
|
// @Param client_id formData string false "Client ID (required if not using Basic auth)"
|
||||||
|
// @Param client_secret formData string false "Client secret (required for confidential clients without Basic auth)"
|
||||||
|
// @Param code_verifier formData string false "PKCE code verifier (required if code_challenge was sent)"
|
||||||
|
// @Success 200 {object} service.TokenResponse
|
||||||
|
// @Failure 400 {object} OIDCErrorResponse
|
||||||
|
// @Failure 500 {object} OIDCErrorResponse
|
||||||
|
// @Router /oidc/token [post]
|
||||||
func (controller *OIDCController) Token(c *gin.Context) {
|
func (controller *OIDCController) Token(c *gin.Context) {
|
||||||
if controller.oidc == nil {
|
if controller.oidc == nil {
|
||||||
controller.log.App.Warn().Msg("Received OIDC request but OIDC server is not configured")
|
controller.log.App.Warn().Msg("Received OIDC request but OIDC server is not configured")
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, OIDCErrorResponse{
|
||||||
"error": "server_error",
|
Error: "server_error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -381,8 +457,8 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
err := c.Bind(&req)
|
err := c.Bind(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Warn().Err(err).Msg("Failed to bind token request")
|
controller.log.App.Warn().Err(err).Msg("Failed to bind token request")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_request",
|
Error: "invalid_request",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -390,8 +466,8 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
err = controller.oidc.ValidateGrantType(req.GrantType)
|
err = controller.oidc.ValidateGrantType(req.GrantType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Warn().Err(err).Msg("Invalid grant type")
|
controller.log.App.Warn().Err(err).Msg("Invalid grant type")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -411,8 +487,8 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
controller.log.App.Warn().Msg("Client credentials not found in basic auth")
|
controller.log.App.Warn().Msg("Client credentials not found in basic auth")
|
||||||
c.Header("www-authenticate", `Basic realm="Tinyauth OIDC Token Endpoint"`)
|
c.Header("www-authenticate", `Basic realm="Tinyauth OIDC Token Endpoint"`)
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_client",
|
Error: "invalid_client",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -427,16 +503,16 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
controller.log.App.Warn().Str("clientId", creds.ClientID).Msg("Client not found")
|
controller.log.App.Warn().Str("clientId", creds.ClientID).Msg("Client not found")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_client",
|
Error: "invalid_client",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if client.ClientSecret != creds.ClientSecret {
|
if client.ClientSecret != creds.ClientSecret {
|
||||||
controller.log.App.Warn().Str("clientId", creds.ClientID).Msg("Invalid client secret")
|
controller.log.App.Warn().Str("clientId", creds.ClientID).Msg("Invalid client secret")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_client",
|
Error: "invalid_client",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -457,15 +533,15 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to delete session for reused code")
|
controller.log.App.Error().Err(err).Msg("Failed to delete session for reused code")
|
||||||
}
|
}
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_grant",
|
Error: "invalid_grant",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.log.App.Warn().Msg("Code not found")
|
controller.log.App.Warn().Msg("Code not found")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_grant",
|
Error: "invalid_grant",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -475,8 +551,8 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
|
|
||||||
if entry.RedirectURI != req.RedirectURI {
|
if entry.RedirectURI != req.RedirectURI {
|
||||||
controller.log.App.Warn().Msg("Redirect URI does not match")
|
controller.log.App.Warn().Msg("Redirect URI does not match")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_grant",
|
Error: "invalid_grant",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -485,8 +561,8 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
controller.log.App.Warn().Msg("PKCE validation failed")
|
controller.log.App.Warn().Msg("PKCE validation failed")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_grant",
|
Error: "invalid_grant",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -495,8 +571,8 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to generate access token")
|
controller.log.App.Error().Err(err).Msg("Failed to generate access token")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "server_error",
|
Error: "server_error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -508,23 +584,23 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, service.ErrTokenExpired) {
|
if errors.Is(err, service.ErrTokenExpired) {
|
||||||
controller.log.App.Warn().Msg("Refresh token expired")
|
controller.log.App.Warn().Msg("Refresh token expired")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_grant",
|
Error: "invalid_grant",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(err, service.ErrInvalidClient) {
|
if errors.Is(err, service.ErrInvalidClient) {
|
||||||
controller.log.App.Warn().Msg("Refresh token does not belong to client")
|
controller.log.App.Warn().Msg("Refresh token does not belong to client")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_grant",
|
Error: "invalid_grant",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to refresh access token")
|
controller.log.App.Error().Err(err).Msg("Failed to refresh access token")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "server_error",
|
Error: "server_error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -538,11 +614,25 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
c.JSON(200, tokenResponse)
|
c.JSON(200, tokenResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Userinfo godoc
|
||||||
|
//
|
||||||
|
// @Summary Userinfo
|
||||||
|
// @Description OpenID Connect Userinfo Endpoint
|
||||||
|
// @Accept x-www-form-urlencoded
|
||||||
|
// @Tags oidc
|
||||||
|
// @Param access_token formData string false "OpenID Connect Access Token"
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} service.UserinfoResponse
|
||||||
|
// @Failure 400 {object} OIDCErrorResponse
|
||||||
|
// @Failure 401 {object} OIDCErrorResponse
|
||||||
|
// @Failure 500 {object} OIDCErrorResponse
|
||||||
|
// @Router /oidc/userinfo [get]
|
||||||
|
// @Router /oidc/userinfo [post]
|
||||||
func (controller *OIDCController) Userinfo(c *gin.Context) {
|
func (controller *OIDCController) Userinfo(c *gin.Context) {
|
||||||
if controller.oidc == nil {
|
if controller.oidc == nil {
|
||||||
controller.log.App.Warn().Msg("Received OIDC userinfo request but OIDC server is not configured")
|
controller.log.App.Warn().Msg("Received OIDC userinfo request but OIDC server is not configured")
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, OIDCErrorResponse{
|
||||||
"error": "server_error",
|
Error: "server_error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -554,16 +644,16 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
|||||||
tokenType, bearerToken, ok := strings.Cut(authorization, " ")
|
tokenType, bearerToken, ok := strings.Cut(authorization, " ")
|
||||||
if !ok {
|
if !ok {
|
||||||
controller.log.App.Warn().Msg("OIDC userinfo accessed with invalid authorization header")
|
controller.log.App.Warn().Msg("OIDC userinfo accessed with invalid authorization header")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, OIDCErrorResponse{
|
||||||
"error": "invalid_request",
|
Error: "invalid_request",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.ToLower(tokenType) != "bearer" {
|
if strings.ToLower(tokenType) != "bearer" {
|
||||||
controller.log.App.Warn().Msg("OIDC userinfo accessed with non-bearer token")
|
controller.log.App.Warn().Msg("OIDC userinfo accessed with non-bearer token")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, OIDCErrorResponse{
|
||||||
"error": "invalid_request",
|
Error: "invalid_request",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -572,23 +662,23 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
|||||||
} else if c.Request.Method == http.MethodPost {
|
} else if c.Request.Method == http.MethodPost {
|
||||||
if c.ContentType() != "application/x-www-form-urlencoded" {
|
if c.ContentType() != "application/x-www-form-urlencoded" {
|
||||||
controller.log.App.Warn().Msg("OIDC userinfo POST accessed with invalid content type")
|
controller.log.App.Warn().Msg("OIDC userinfo POST accessed with invalid content type")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, OIDCErrorResponse{
|
||||||
"error": "invalid_request",
|
Error: "invalid_request",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
token = c.PostForm("access_token")
|
token = c.PostForm("access_token")
|
||||||
if token == "" {
|
if token == "" {
|
||||||
controller.log.App.Warn().Msg("OIDC userinfo POST accessed without access_token")
|
controller.log.App.Warn().Msg("OIDC userinfo POST accessed without access_token")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, OIDCErrorResponse{
|
||||||
"error": "invalid_request",
|
Error: "invalid_request",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
controller.log.App.Warn().Msg("OIDC userinfo accessed without authorization header or POST body")
|
controller.log.App.Warn().Msg("OIDC userinfo accessed without authorization header or POST body")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, OIDCErrorResponse{
|
||||||
"error": "invalid_request",
|
Error: "invalid_request",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -598,15 +688,15 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, service.ErrTokenNotFound) {
|
if errors.Is(err, service.ErrTokenNotFound) {
|
||||||
controller.log.App.Warn().Msg("OIDC userinfo accessed with invalid token")
|
controller.log.App.Warn().Msg("OIDC userinfo accessed with invalid token")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, OIDCErrorResponse{
|
||||||
"error": "invalid_grant",
|
Error: "invalid_grant",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to get access token")
|
controller.log.App.Error().Err(err).Msg("Failed to get access token")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, OIDCErrorResponse{
|
||||||
"error": "server_error",
|
Error: "server_error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -614,8 +704,8 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
|||||||
// If we don't have the openid scope, return an error
|
// If we don't have the openid scope, return an error
|
||||||
if !slices.Contains(strings.Split(entry.Scope, " "), "openid") {
|
if !slices.Contains(strings.Split(entry.Scope, " "), "openid") {
|
||||||
controller.log.App.Warn().Msg("OIDC userinfo accessed with missing openid scope")
|
controller.log.App.Warn().Msg("OIDC userinfo accessed with missing openid scope")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, OIDCErrorResponse{
|
||||||
"error": "invalid_scope",
|
Error: "invalid_scope",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -626,8 +716,8 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to get user info")
|
controller.log.App.Error().Err(err).Msg("Failed to get user info")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, OIDCErrorResponse{
|
||||||
"error": "server_error",
|
Error: "server_error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -662,9 +752,11 @@ func (controller *OIDCController) authorizeError(c *gin.Context, params authoriz
|
|||||||
redirectUrl := fmt.Sprintf("%s?%s", params.callback, queries.Encode())
|
redirectUrl := fmt.Sprintf("%s?%s", params.callback, queries.Encode())
|
||||||
|
|
||||||
if params.json {
|
if params.json {
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, AuthorizeCompleteResponse{
|
||||||
"status": 200,
|
SimpleResponse: SimpleResponse{
|
||||||
"redirect_uri": redirectUrl,
|
Status: 200,
|
||||||
|
},
|
||||||
|
RedirectURI: redirectUrl,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -694,9 +786,11 @@ func (controller *OIDCController) authorizeError(c *gin.Context, params authoriz
|
|||||||
}
|
}
|
||||||
|
|
||||||
if params.json {
|
if params.json {
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, AuthorizeCompleteResponse{
|
||||||
"status": 200,
|
SimpleResponse: SimpleResponse{
|
||||||
"redirect_uri": redirectUrl,
|
Status: 200,
|
||||||
|
},
|
||||||
|
RedirectURI: redirectUrl,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,15 +86,38 @@ func NewProxyController(i ProxyControllerInput) *ProxyController {
|
|||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Proxy godoc
|
||||||
|
//
|
||||||
|
// @Summary Proxy
|
||||||
|
// @Description Forward-Auth Proxy Endpoint
|
||||||
|
// @Tags forward-auth
|
||||||
|
// @Produce json
|
||||||
|
// @Param proxy path string true "Proxy Name"
|
||||||
|
// @Success 200 {object} SimpleResponse
|
||||||
|
// @Failure 302
|
||||||
|
// @Failure 400 {object} SimpleResponse
|
||||||
|
// @Failure 401 {object} SimpleResponse
|
||||||
|
// @Failure 403 {object} SimpleResponse
|
||||||
|
// @Failure 500 {object} SimpleResponse
|
||||||
|
// @Router /api/auth/traefik [get]
|
||||||
|
// @Router /api/auth/caddy [get]
|
||||||
|
// @Router /api/auth/nginx [get]
|
||||||
|
// @Router /api/auth/envoy [get]
|
||||||
|
// @Router /api/auth/envoy [post]
|
||||||
|
// @Router /api/auth/envoy [head]
|
||||||
|
// @Router /api/auth/envoy [put]
|
||||||
|
// @Router /api/auth/envoy [patch]
|
||||||
|
// @Router /api/auth/envoy [delete]
|
||||||
|
// @Router /api/auth/envoy [options]
|
||||||
func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||||
// Load proxy context based on the request type
|
// Load proxy context based on the request type
|
||||||
proxyCtx, err := controller.getProxyContext(c)
|
proxyCtx, err := controller.getProxyContext(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to get proxy context from request")
|
controller.log.App.Error().Err(err).Msg("Failed to get proxy context from request")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, SimpleResponse{
|
||||||
"status": 400,
|
Status: 400,
|
||||||
"message": "Bad request",
|
Message: "Bad request",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -118,9 +141,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if controller.policyEngine.Evaluate(service.RuleIPBypassed, aclsCtx) {
|
if controller.policyEngine.Evaluate(service.RuleIPBypassed, aclsCtx) {
|
||||||
controller.setHeaders(c, acls)
|
controller.setHeaders(c, acls)
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, SimpleResponse{
|
||||||
"status": 200,
|
Status: 200,
|
||||||
"message": "Authenticated",
|
Message: "Authenticated",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -128,9 +151,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
if controller.policyEngine.Evaluate(service.RuleAuthEnabled, aclsCtx) {
|
if controller.policyEngine.Evaluate(service.RuleAuthEnabled, aclsCtx) {
|
||||||
controller.log.App.Debug().Msg("Authentication is disabled for this resource, allowing access without authentication")
|
controller.log.App.Debug().Msg("Authentication is disabled for this resource, allowing access without authentication")
|
||||||
controller.setHeaders(c, acls)
|
controller.setHeaders(c, acls)
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, SimpleResponse{
|
||||||
"status": 200,
|
Status: 200,
|
||||||
"message": "Authenticated",
|
Message: "Authenticated",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -151,9 +174,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if !controller.useBrowserResponse(proxyCtx) {
|
if !controller.useBrowserResponse(proxyCtx) {
|
||||||
c.Header("x-tinyauth-location", redirectURL)
|
c.Header("x-tinyauth-location", redirectURL)
|
||||||
c.JSON(403, gin.H{
|
c.JSON(403, SimpleResponse{
|
||||||
"status": 403,
|
Status: 403,
|
||||||
"message": "Forbidden",
|
Message: "Forbidden",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -200,9 +223,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if !controller.useBrowserResponse(proxyCtx) {
|
if !controller.useBrowserResponse(proxyCtx) {
|
||||||
c.Header("x-tinyauth-location", redirectURL)
|
c.Header("x-tinyauth-location", redirectURL)
|
||||||
c.JSON(403, gin.H{
|
c.JSON(403, SimpleResponse{
|
||||||
"status": 403,
|
Status: 403,
|
||||||
"message": "Forbidden",
|
Message: "Forbidden",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -244,9 +267,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if !controller.useBrowserResponse(proxyCtx) {
|
if !controller.useBrowserResponse(proxyCtx) {
|
||||||
c.Header("x-tinyauth-location", redirectURL)
|
c.Header("x-tinyauth-location", redirectURL)
|
||||||
c.JSON(403, gin.H{
|
c.JSON(403, SimpleResponse{
|
||||||
"status": 403,
|
Status: 403,
|
||||||
"message": "Forbidden",
|
Message: "Forbidden",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -271,9 +294,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
|
|
||||||
controller.setHeaders(c, acls)
|
controller.setHeaders(c, acls)
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, SimpleResponse{
|
||||||
"status": 200,
|
Status: 200,
|
||||||
"message": "Authenticated",
|
Message: "Authenticated",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -293,9 +316,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if !controller.useBrowserResponse(proxyCtx) {
|
if !controller.useBrowserResponse(proxyCtx) {
|
||||||
c.Header("x-tinyauth-location", redirectURL)
|
c.Header("x-tinyauth-location", redirectURL)
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, SimpleResponse{
|
||||||
"status": 401,
|
Status: 401,
|
||||||
"message": "Unauthorized",
|
Message: "Unauthorized",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -329,9 +352,9 @@ func (controller *ProxyController) handleError(c *gin.Context, proxyCtx ProxyCon
|
|||||||
|
|
||||||
if !controller.useBrowserResponse(proxyCtx) {
|
if !controller.useBrowserResponse(proxyCtx) {
|
||||||
c.Header("x-tinyauth-location", redirectURL)
|
c.Header("x-tinyauth-location", redirectURL)
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, SimpleResponse{
|
||||||
"status": 500,
|
Status: 500,
|
||||||
"message": "Internal Server Error",
|
Message: "Internal Server Error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,18 +33,28 @@ func NewResourcesController(i ResourcesControllerInput) *ResourcesController {
|
|||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resources godoc
|
||||||
|
//
|
||||||
|
// @Summary Resources Endpoint
|
||||||
|
// @Description Get a resource by file name
|
||||||
|
// @Tags resources
|
||||||
|
// @Param resource path string true "Resource Name"
|
||||||
|
// @Success 200
|
||||||
|
// @Failure 404 {object} SimpleResponse
|
||||||
|
// @Failure 403 {object} SimpleResponse
|
||||||
|
// @Router /resources/{resource} [get]
|
||||||
func (controller *ResourcesController) resourcesHandler(c *gin.Context) {
|
func (controller *ResourcesController) resourcesHandler(c *gin.Context) {
|
||||||
if controller.config.Resources.Path == "" {
|
if controller.config.Resources.Path == "" {
|
||||||
c.JSON(404, gin.H{
|
c.JSON(404, SimpleResponse{
|
||||||
"status": 404,
|
Status: 404,
|
||||||
"message": "Resource not found",
|
Message: "Resource not found",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !controller.config.Resources.Enabled {
|
if !controller.config.Resources.Enabled {
|
||||||
c.JSON(403, gin.H{
|
c.JSON(403, SimpleResponse{
|
||||||
"status": 403,
|
Status: 403,
|
||||||
"message": "Resources are disabled",
|
Message: "Resources are disabled",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ type UserController struct {
|
|||||||
auth *service.AuthService
|
auth *service.AuthService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TotpPendingResponse struct {
|
||||||
|
SimpleResponse
|
||||||
|
TotpPending bool `json:"totpPending"`
|
||||||
|
}
|
||||||
|
|
||||||
type UserControllerInput struct {
|
type UserControllerInput struct {
|
||||||
dig.In
|
dig.In
|
||||||
|
|
||||||
@@ -57,15 +62,29 @@ func NewUserController(i UserControllerInput) *UserController {
|
|||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Login godoc
|
||||||
|
//
|
||||||
|
// @Summary Login
|
||||||
|
// @Description Login Endpoint
|
||||||
|
// @Tags accounts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} SimpleResponse
|
||||||
|
// @Success 200 {object} TotpPendingResponse
|
||||||
|
// @Failure 400 {object} SimpleResponse
|
||||||
|
// @Failure 401 {object} SimpleResponse
|
||||||
|
// @Failure 500 {object} SimpleResponse
|
||||||
|
// @Failure 429 {object} SimpleResponse
|
||||||
|
// @Router /api/user/login [post]
|
||||||
func (controller *UserController) loginHandler(c *gin.Context) {
|
func (controller *UserController) loginHandler(c *gin.Context) {
|
||||||
var req LoginRequest
|
var req LoginRequest
|
||||||
|
|
||||||
err := c.ShouldBindJSON(&req)
|
err := c.ShouldBindJSON(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to bind JSON")
|
controller.log.App.Error().Err(err).Msg("Failed to bind JSON")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, SimpleResponse{
|
||||||
"status": 400,
|
Status: 400,
|
||||||
"message": "Bad Request",
|
Message: "Bad Request",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -79,9 +98,9 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
controller.log.AuditLoginFailure(req.Username, "local", c.ClientIP(), "account locked")
|
controller.log.AuditLoginFailure(req.Username, "local", c.ClientIP(), "account locked")
|
||||||
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
|
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
|
||||||
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
||||||
c.JSON(429, gin.H{
|
c.JSON(429, SimpleResponse{
|
||||||
"status": 429,
|
Status: 429,
|
||||||
"message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remaining),
|
Message: fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remaining),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -93,16 +112,16 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
controller.log.App.Warn().Str("username", req.Username).Msg("User not found during login attempt")
|
controller.log.App.Warn().Str("username", req.Username).Msg("User not found during login attempt")
|
||||||
controller.auth.RecordLoginAttempt(req.Username, false)
|
controller.auth.RecordLoginAttempt(req.Username, false)
|
||||||
controller.log.AuditLoginFailure(req.Username, "unknown", c.ClientIP(), "user not found")
|
controller.log.AuditLoginFailure(req.Username, "unknown", c.ClientIP(), "user not found")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, SimpleResponse{
|
||||||
"status": 401,
|
Status: 401,
|
||||||
"message": "Unauthorized",
|
Message: "Unauthorized",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
controller.log.App.Error().Err(err).Str("username", req.Username).Msg("Error searching for user during login attempt")
|
controller.log.App.Error().Err(err).Str("username", req.Username).Msg("Error searching for user during login attempt")
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, SimpleResponse{
|
||||||
"status": 500,
|
Status: 500,
|
||||||
"message": "Internal Server Error",
|
Message: "Internal Server Error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -115,9 +134,9 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
} else {
|
} else {
|
||||||
controller.log.AuditLoginFailure(req.Username, "ldap", c.ClientIP(), "invalid password")
|
controller.log.AuditLoginFailure(req.Username, "ldap", c.ClientIP(), "invalid password")
|
||||||
}
|
}
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, SimpleResponse{
|
||||||
"status": 401,
|
Status: 401,
|
||||||
"message": "Unauthorized",
|
Message: "Unauthorized",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -129,9 +148,9 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if localUser == nil {
|
if localUser == nil {
|
||||||
controller.log.App.Error().Str("username", req.Username).Msg("Local user not found after successful password verification")
|
controller.log.App.Error().Str("username", req.Username).Msg("Local user not found after successful password verification")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, SimpleResponse{
|
||||||
"status": 401,
|
Status: 401,
|
||||||
"message": "Unauthorized",
|
Message: "Unauthorized",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -159,19 +178,21 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Str("username", req.Username).Msg("Failed to create pending TOTP session")
|
controller.log.App.Error().Err(err).Str("username", req.Username).Msg("Failed to create pending TOTP session")
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, SimpleResponse{
|
||||||
"status": 500,
|
Status: 500,
|
||||||
"message": "Internal Server Error",
|
Message: "Internal Server Error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.SetCookie(c.Writer, cookie)
|
http.SetCookie(c.Writer, cookie)
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, TotpPendingResponse{
|
||||||
"status": 200,
|
SimpleResponse: SimpleResponse{
|
||||||
"message": "TOTP required",
|
Status: 200,
|
||||||
"totpPending": true,
|
Message: "TOTP required",
|
||||||
|
},
|
||||||
|
TotpPending: true,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -204,9 +225,9 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Str("username", req.Username).Msg("Failed to create session cookie after successful login")
|
controller.log.App.Error().Err(err).Str("username", req.Username).Msg("Failed to create session cookie after successful login")
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, SimpleResponse{
|
||||||
"status": 500,
|
Status: 500,
|
||||||
"message": "Internal Server Error",
|
Message: "Internal Server Error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -223,12 +244,21 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
|
|
||||||
controller.auth.RecordLoginAttempt(req.Username, true)
|
controller.auth.RecordLoginAttempt(req.Username, true)
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, SimpleResponse{
|
||||||
"status": 200,
|
Status: 200,
|
||||||
"message": "Login successful",
|
Message: "Login successful",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logout godoc
|
||||||
|
//
|
||||||
|
// @Summary Logout
|
||||||
|
// @Description Logout Endpoint
|
||||||
|
// @Tags accounts
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} SimpleResponse
|
||||||
|
// @Failure 500 {object} SimpleResponse
|
||||||
|
// @Router /api/user/logout [post]
|
||||||
func (controller *UserController) logoutHandler(c *gin.Context) {
|
func (controller *UserController) logoutHandler(c *gin.Context) {
|
||||||
controller.log.App.Debug().Msg("Logout attempt")
|
controller.log.App.Debug().Msg("Logout attempt")
|
||||||
|
|
||||||
@@ -237,16 +267,16 @@ func (controller *UserController) logoutHandler(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, http.ErrNoCookie) {
|
if errors.Is(err, http.ErrNoCookie) {
|
||||||
controller.log.App.Warn().Msg("Logout attempt without session cookie, treating as successful logout")
|
controller.log.App.Warn().Msg("Logout attempt without session cookie, treating as successful logout")
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, SimpleResponse{
|
||||||
"status": 200,
|
Status: 200,
|
||||||
"message": "Logout successful",
|
Message: "Logout successful",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
controller.log.App.Error().Err(err).Msg("Error retrieving session cookie on logout")
|
controller.log.App.Error().Err(err).Msg("Error retrieving session cookie on logout")
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, SimpleResponse{
|
||||||
"status": 500,
|
Status: 500,
|
||||||
"message": "Internal Server Error",
|
Message: "Internal Server Error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -255,9 +285,9 @@ func (controller *UserController) logoutHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Error deleting session on logout")
|
controller.log.App.Error().Err(err).Msg("Error deleting session on logout")
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, SimpleResponse{
|
||||||
"status": 500,
|
Status: 500,
|
||||||
"message": "Internal Server Error",
|
Message: "Internal Server Error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -273,21 +303,34 @@ func (controller *UserController) logoutHandler(c *gin.Context) {
|
|||||||
|
|
||||||
http.SetCookie(c.Writer, cookie)
|
http.SetCookie(c.Writer, cookie)
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, SimpleResponse{
|
||||||
"status": 200,
|
Status: 200,
|
||||||
"message": "Logout successful",
|
Message: "Logout successful",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TOTP godoc
|
||||||
|
//
|
||||||
|
// @Summary TOTP
|
||||||
|
// @Description TOTP Endpoint
|
||||||
|
// @Tags accounts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} SimpleResponse
|
||||||
|
// @Failure 400 {object} SimpleResponse
|
||||||
|
// @Failure 401 {object} SimpleResponse
|
||||||
|
// @Failure 429 {object} SimpleResponse
|
||||||
|
// @Failure 500 {object} SimpleResponse
|
||||||
|
// @Router /api/user/totp [post]
|
||||||
func (controller *UserController) totpHandler(c *gin.Context) {
|
func (controller *UserController) totpHandler(c *gin.Context) {
|
||||||
var req TotpRequest
|
var req TotpRequest
|
||||||
|
|
||||||
err := c.ShouldBindJSON(&req)
|
err := c.ShouldBindJSON(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to bind JSON for TOTP verification")
|
controller.log.App.Error().Err(err).Msg("Failed to bind JSON for TOTP verification")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, SimpleResponse{
|
||||||
"status": 400,
|
Status: 400,
|
||||||
"message": "Bad Request",
|
Message: "Bad Request",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -297,25 +340,25 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, model.ErrUserContextNotFound) {
|
if errors.Is(err, model.ErrUserContextNotFound) {
|
||||||
controller.log.App.Warn().Msg("TOTP verification attempt without user context")
|
controller.log.App.Warn().Msg("TOTP verification attempt without user context")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, SimpleResponse{
|
||||||
"status": 401,
|
Status: 401,
|
||||||
"message": "Unauthorized",
|
Message: "Unauthorized",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to create user context from request for TOTP verification")
|
controller.log.App.Error().Err(err).Msg("Failed to create user context from request for TOTP verification")
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, SimpleResponse{
|
||||||
"status": 500,
|
Status: 500,
|
||||||
"message": "Internal Server Error",
|
Message: "Internal Server Error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !context.TOTPPending() {
|
if !context.TOTPPending() {
|
||||||
controller.log.App.Warn().Str("username", context.GetUsername()).Msg("TOTP verification attempt without pending TOTP session")
|
controller.log.App.Warn().Str("username", context.GetUsername()).Msg("TOTP verification attempt without pending TOTP session")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, SimpleResponse{
|
||||||
"status": 401,
|
Status: 401,
|
||||||
"message": "Unauthorized",
|
Message: "Unauthorized",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -329,9 +372,9 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
controller.log.AuditLoginFailure(context.GetUsername(), "local", c.ClientIP(), "account locked")
|
controller.log.AuditLoginFailure(context.GetUsername(), "local", c.ClientIP(), "account locked")
|
||||||
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
|
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
|
||||||
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
||||||
c.JSON(429, gin.H{
|
c.JSON(429, SimpleResponse{
|
||||||
"status": 429,
|
Status: 429,
|
||||||
"message": fmt.Sprintf("Too many failed TOTP attempts. Try again in %d seconds", remaining),
|
Message: fmt.Sprintf("Too many failed TOTP attempts. Try again in %d seconds", remaining),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -340,9 +383,9 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
controller.log.App.Error().Str("username", context.GetUsername()).Msg("Local user not found during TOTP verification")
|
controller.log.App.Error().Str("username", context.GetUsername()).Msg("Local user not found during TOTP verification")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, SimpleResponse{
|
||||||
"status": 401,
|
Status: 401,
|
||||||
"message": "Unauthorized",
|
Message: "Unauthorized",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -353,9 +396,9 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
controller.log.App.Warn().Str("username", context.GetUsername()).Msg("Invalid TOTP code during verification attempt")
|
controller.log.App.Warn().Str("username", context.GetUsername()).Msg("Invalid TOTP code during verification attempt")
|
||||||
controller.auth.RecordLoginAttempt(context.GetUsername(), false)
|
controller.auth.RecordLoginAttempt(context.GetUsername(), false)
|
||||||
controller.log.AuditLoginFailure(context.GetUsername(), "local", c.ClientIP(), "invalid TOTP code")
|
controller.log.AuditLoginFailure(context.GetUsername(), "local", c.ClientIP(), "invalid TOTP code")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, SimpleResponse{
|
||||||
"status": 401,
|
Status: 401,
|
||||||
"message": "Unauthorized",
|
Message: "Unauthorized",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -391,9 +434,9 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Str("username", context.GetUsername()).Msg("Failed to create session cookie after successful TOTP verification")
|
controller.log.App.Error().Err(err).Str("username", context.GetUsername()).Msg("Failed to create session cookie after successful TOTP verification")
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, SimpleResponse{
|
||||||
"status": 500,
|
Status: 500,
|
||||||
"message": "Internal Server Error",
|
Message: "Internal Server Error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -403,37 +446,48 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
controller.log.App.Info().Str("username", context.GetUsername()).Msg("TOTP verification successful, login complete")
|
controller.log.App.Info().Str("username", context.GetUsername()).Msg("TOTP verification successful, login complete")
|
||||||
controller.log.AuditLoginSuccess(context.GetUsername(), "local", c.ClientIP())
|
controller.log.AuditLoginSuccess(context.GetUsername(), "local", c.ClientIP())
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, SimpleResponse{
|
||||||
"status": 200,
|
Status: 200,
|
||||||
"message": "Login successful",
|
Message: "Login successful",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tailscale godoc
|
||||||
|
//
|
||||||
|
// @Summary Tailscale
|
||||||
|
// @Description Tailscale Auth Endpoint (Experimental)
|
||||||
|
// @Tags accounts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} SimpleResponse
|
||||||
|
// @Failure 401 {object} SimpleResponse
|
||||||
|
// @Failure 500 {object} SimpleResponse
|
||||||
|
// @Router /api/user/tailscale [post]
|
||||||
func (controller *UserController) tailscaleHandler(c *gin.Context) {
|
func (controller *UserController) tailscaleHandler(c *gin.Context) {
|
||||||
context, err := new(model.UserContext).NewFromGin(c)
|
context, err := new(model.UserContext).NewFromGin(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, model.ErrUserContextNotFound) {
|
if errors.Is(err, model.ErrUserContextNotFound) {
|
||||||
controller.log.App.Warn().Msg("Tailscale login attempt without user context")
|
controller.log.App.Warn().Msg("Tailscale login attempt without user context")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, SimpleResponse{
|
||||||
"status": 401,
|
Status: 401,
|
||||||
"message": "Unauthorized",
|
Message: "Unauthorized",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to create user context from request")
|
controller.log.App.Error().Err(err).Msg("Failed to create user context from request")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, SimpleResponse{
|
||||||
"status": 401,
|
Status: 401,
|
||||||
"message": "Unauthorized",
|
Message: "Unauthorized",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if context.Tailscale == nil {
|
if context.Tailscale == nil {
|
||||||
controller.log.App.Warn().Msg("Tailscale login attempt without Tailscale context")
|
controller.log.App.Warn().Msg("Tailscale login attempt without Tailscale context")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, SimpleResponse{
|
||||||
"status": 401,
|
Status: 401,
|
||||||
"message": "Unauthorized",
|
Message: "Unauthorized",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -449,9 +503,9 @@ func (controller *UserController) tailscaleHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
controller.log.App.Error().Err(err).Str("username", context.GetUsername()).Msg("Failed to create session cookie after successful Tailscale login")
|
controller.log.App.Error().Err(err).Str("username", context.GetUsername()).Msg("Failed to create session cookie after successful Tailscale login")
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, SimpleResponse{
|
||||||
"status": 500,
|
Status: 500,
|
||||||
"message": "Internal Server Error",
|
Message: "Internal Server Error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -461,8 +515,8 @@ func (controller *UserController) tailscaleHandler(c *gin.Context) {
|
|||||||
controller.log.App.Info().Str("username", context.GetUsername()).Msg("Tailscale login successful, login complete")
|
controller.log.App.Info().Str("username", context.GetUsername()).Msg("Tailscale login successful, login complete")
|
||||||
controller.log.AuditLoginSuccess(context.GetUsername(), "tailscale", c.ClientIP())
|
controller.log.AuditLoginSuccess(context.GetUsername(), "tailscale", c.ClientIP())
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, SimpleResponse{
|
||||||
"status": 200,
|
Status: 200,
|
||||||
"message": "Login successful",
|
Message: "Login successful",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,18 +58,27 @@ func NewWellKnownController(i WellKnownControllerInput) *WellKnownController {
|
|||||||
oidc: i.OIDCService,
|
oidc: i.OIDCService,
|
||||||
}
|
}
|
||||||
|
|
||||||
i.RouterGroup.GET("/.well-known/openid-configuration", controller.OpenIDConnectConfiguration)
|
i.RouterGroup.GET("/.well-known/openid-configuration", controller.openIDConnectConfiguration)
|
||||||
i.RouterGroup.GET("/.well-known/jwks.json", controller.JWKS)
|
i.RouterGroup.GET("/.well-known/jwks.json", controller.jwks)
|
||||||
i.RouterGroup.GET("/.well-known/webfinger", controller.WebFinger)
|
i.RouterGroup.GET("/.well-known/webfinger", controller.webFinger)
|
||||||
|
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
|
||||||
func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context) {
|
// OpenIDConnectConfiguration godoc
|
||||||
|
//
|
||||||
|
// @Summary OpenID Connect Configuration
|
||||||
|
// @Description OpenID Connect Configuration Discovery Endpoint
|
||||||
|
// @Tags well-known
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} OpenIDConnectConfiguration
|
||||||
|
// @Failure 500 {object} SimpleResponse
|
||||||
|
// @Router /.well-known/openid-configuration [get]
|
||||||
|
func (controller *WellKnownController) openIDConnectConfiguration(c *gin.Context) {
|
||||||
if controller.oidc == nil {
|
if controller.oidc == nil {
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, SimpleResponse{
|
||||||
"status": 500,
|
Status: 500,
|
||||||
"message": "OIDC service not configured",
|
Message: "OIDC service not configured",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -94,11 +103,20 @@ func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (controller *WellKnownController) JWKS(c *gin.Context) {
|
// JWKS godoc
|
||||||
|
//
|
||||||
|
// @Summary JWKS
|
||||||
|
// @Description JWKS Endpoint
|
||||||
|
// @Tags well-known
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200
|
||||||
|
// @Failure 500 {object} SimpleResponse
|
||||||
|
// @Router /.well-known/jwks.json [get]
|
||||||
|
func (controller *WellKnownController) jwks(c *gin.Context) {
|
||||||
if controller.oidc == nil {
|
if controller.oidc == nil {
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, SimpleResponse{
|
||||||
"status": 500,
|
Status: 500,
|
||||||
"message": "OIDC service not configured",
|
Message: "OIDC service not configured",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -106,9 +124,9 @@ func (controller *WellKnownController) JWKS(c *gin.Context) {
|
|||||||
jwks, err := controller.oidc.GetJWK()
|
jwks, err := controller.oidc.GetJWK()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, SimpleResponse{
|
||||||
"status": 500,
|
Status: 500,
|
||||||
"message": "failed to get JWK",
|
Message: "failed to get JWK",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -122,16 +140,27 @@ func (controller *WellKnownController) JWKS(c *gin.Context) {
|
|||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (controller *WellKnownController) WebFinger(c *gin.Context) {
|
// WebFinger godoc
|
||||||
|
//
|
||||||
|
// @Summary WebFinger
|
||||||
|
// @Description WebFinger Endpoint
|
||||||
|
// @Tags well-known
|
||||||
|
// @Produce json
|
||||||
|
// @Param resource query string true "Resource"
|
||||||
|
// @Param rel query string false "Rel"
|
||||||
|
// @Success 200 {object} WebfingerResponse
|
||||||
|
// @Failure 400 {object} SimpleResponse
|
||||||
|
// @Router /.well-known/webfinger [get]
|
||||||
|
func (controller *WellKnownController) webFinger(c *gin.Context) {
|
||||||
c.Header("Content-Type", "application/jrd+json")
|
c.Header("Content-Type", "application/jrd+json")
|
||||||
c.Header("Access-Control-Allow-Origin", "*")
|
c.Header("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
resource := c.Query("resource")
|
resource := c.Query("resource")
|
||||||
|
|
||||||
if !controller.validateWebFingerResource(resource) {
|
if !controller.validateWebFingerResource(resource) {
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, SimpleResponse{
|
||||||
"status": 400,
|
Status: 400,
|
||||||
"message": "invalid resource",
|
Message: "invalid resource",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func (m *UIMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
path := strings.TrimPrefix(c.Request.URL.Path, "/")
|
path := strings.TrimPrefix(c.Request.URL.Path, "/")
|
||||||
|
|
||||||
switch strings.SplitN(path, "/", 2)[0] {
|
switch strings.SplitN(path, "/", 2)[0] {
|
||||||
case "api", "resources", ".well-known", "authorize":
|
case "api", "resources", ".well-known", "authorize", "scalar":
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
case "robots.txt":
|
case "robots.txt":
|
||||||
|
|||||||
@@ -34,8 +34,9 @@ func NewDefaultConfiguration(runtimeEnv RuntimeEnv) *Config {
|
|||||||
Path: "./resources",
|
Path: "./resources",
|
||||||
},
|
},
|
||||||
Server: ServerConfig{
|
Server: ServerConfig{
|
||||||
Port: 3000,
|
Port: 3000,
|
||||||
Address: "0.0.0.0",
|
Address: "0.0.0.0",
|
||||||
|
ScalarEnabled: true,
|
||||||
},
|
},
|
||||||
Auth: AuthConfig{
|
Auth: AuthConfig{
|
||||||
SubdomainsEnabled: true,
|
SubdomainsEnabled: true,
|
||||||
@@ -134,9 +135,10 @@ type ResourcesConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Port int `description:"The port on which the server listens." yaml:"port,omitempty"`
|
Port int `description:"The port on which the server listens." yaml:"port,omitempty"`
|
||||||
Address string `description:"The address on which the server listens." yaml:"address,omitempty"`
|
Address string `description:"The address on which the server listens." yaml:"address,omitempty"`
|
||||||
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath,omitempty"`
|
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath,omitempty"`
|
||||||
|
ScalarEnabled bool `description:"Enable API docs with Scalar under /scalar." yaml:"scalarEnabled,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthConfig struct {
|
type AuthConfig struct {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user