Compare commits
4
Commits
v1.0.0
..
4822fbc092
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4822fbc092 | ||
|
|
a17b9bb16b | ||
|
|
f3861184b6 | ||
|
|
eb1744bd0d |
+1
-4
@@ -1,7 +1,4 @@
|
|||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
.DS_Store
|
.DS_Store
|
||||||
xtest
|
xtest
|
||||||
*.db
|
|
||||||
*.sqlite
|
|
||||||
*.sqlite3
|
|
||||||
@@ -4,7 +4,7 @@ go 1.20
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
gitea.com/taozitaozi/gredis v0.0.0-20240131032054-b02845ce1e9d
|
gitea.com/taozitaozi/gredis v0.0.0-20240131032054-b02845ce1e9d
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/go-redis/redis/v8 v8.11.5
|
github.com/go-redis/redis/v8 v8.11.5
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
@@ -14,41 +14,43 @@ require (
|
|||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
golang.org/x/crypto v0.26.0
|
golang.org/x/crypto v0.26.0
|
||||||
golang.org/x/oauth2 v0.23.0
|
golang.org/x/oauth2 v0.23.0
|
||||||
gorm.io/driver/mysql v1.5.7
|
|
||||||
gorm.io/driver/postgres v1.5.9
|
|
||||||
gorm.io/gorm v1.25.12
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
|
||||||
github.com/fatih/color v1.17.0 // indirect
|
github.com/fatih/color v1.17.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // 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.15 // indirect
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/onsi/gomega v1.27.6 // indirect
|
github.com/onsi/gomega v1.27.6 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.2 // indirect
|
github.com/sirupsen/logrus v1.9.2 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/stretchr/testify v1.9.0 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
golang.org/x/net v0.28.0 // indirect
|
golang.org/x/net v0.28.0 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
|
||||||
golang.org/x/sys v0.23.0 // indirect
|
golang.org/x/sys v0.23.0 // indirect
|
||||||
golang.org/x/text v0.17.0 // indirect
|
golang.org/x/text v0.17.0 // indirect
|
||||||
modernc.org/libc v1.22.5 // indirect
|
google.golang.org/protobuf v1.34.1 // indirect
|
||||||
modernc.org/mathutil v1.5.0 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/memory v1.5.0 // indirect
|
|
||||||
modernc.org/sqlite v1.23.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,51 +1,62 @@
|
|||||||
gitea.com/taozitaozi/gredis v0.0.0-20240131032054-b02845ce1e9d h1:TpEOdRGqwzxx+DaN18nFE+g4EQYjneZOO1jcHtSon/g=
|
gitea.com/taozitaozi/gredis v0.0.0-20240131032054-b02845ce1e9d h1:TpEOdRGqwzxx+DaN18nFE+g4EQYjneZOO1jcHtSon/g=
|
||||||
gitea.com/taozitaozi/gredis v0.0.0-20240131032054-b02845ce1e9d/go.mod h1:QtcL846XUtSnhmW6TZAujUQ9V5jalY7frxzZOs00kFI=
|
gitea.com/taozitaozi/gredis v0.0.0-20240131032054-b02845ce1e9d/go.mod h1:QtcL846XUtSnhmW6TZAujUQ9V5jalY7frxzZOs00kFI=
|
||||||
|
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
|
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
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=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
|
||||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
|
||||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
|
||||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
|
||||||
github.com/jedib0t/go-pretty/v6 v6.6.1 h1:iJ65Xjb680rHcikRj6DSIbzCex2huitmc7bDtxYVWyc=
|
github.com/jedib0t/go-pretty/v6 v6.6.1 h1:iJ65Xjb680rHcikRj6DSIbzCex2huitmc7bDtxYVWyc=
|
||||||
github.com/jedib0t/go-pretty/v6 v6.6.1/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E=
|
github.com/jedib0t/go-pretty/v6 v6.6.1/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/loveuer/nf v0.2.12 h1:1Og+ORHsOWKFmy9kKJhjvXDkdbaurH82HjIxuGA3nNM=
|
github.com/loveuer/nf v0.2.12 h1:1Og+ORHsOWKFmy9kKJhjvXDkdbaurH82HjIxuGA3nNM=
|
||||||
github.com/loveuer/nf v0.2.12/go.mod h1:M6reF17/kJBis30H4DxR5hrtgo/oJL4AV4cBe4HzJLw=
|
github.com/loveuer/nf v0.2.12/go.mod h1:M6reF17/kJBis30H4DxR5hrtgo/oJL4AV4cBe4HzJLw=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
@@ -55,15 +66,19 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
@@ -74,42 +89,45 @@ github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3k
|
|||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
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.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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||||
|
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
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.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
||||||
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
|
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||||
|
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
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.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=
|
||||||
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
|
||||||
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
|
||||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
|
||||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
|
||||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
|
||||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
|
||||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
|
||||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
|
||||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
|
||||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
|
||||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
|
||||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
|
||||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
|
||||||
|
|||||||
+20
-105
@@ -2,118 +2,33 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
_ "embed"
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/loveuer/nf"
|
|
||||||
"github.com/loveuer/nf/nft/log"
|
|
||||||
"github.com/loveuer/nf/nft/resp"
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"net/http"
|
"golang.org/x/oauth2/authhandler"
|
||||||
"uauth/internal/tool"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed login.html
|
|
||||||
var page string
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
config = oauth2.Config{
|
State = uuid.New().String()[:8]
|
||||||
ClientID: "test",
|
|
||||||
ClientSecret: "Foobar123",
|
|
||||||
Endpoint: oauth2.Endpoint{
|
|
||||||
AuthURL: "http://localhost:8080/oauth/v2/authorize",
|
|
||||||
TokenURL: "http://localhost:8080/oauth/v2/token",
|
|
||||||
},
|
|
||||||
RedirectURL: "http://localhost:18080/oauth/v2/redirect",
|
|
||||||
Scopes: []string{"test"},
|
|
||||||
}
|
|
||||||
state = uuid.New().String()[:8]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Run(ctx context.Context) error {
|
func Run(ctx context.Context) error {
|
||||||
app := nf.New()
|
oauth2.NewClient(ctx, authhandler.TokenSource(
|
||||||
app.Get("/login", handleLoginPage)
|
ctx,
|
||||||
app.Post("/login", handleLoginAction)
|
&oauth2.Config{
|
||||||
app.Get("/oauth/v2/redirect", handleRedirect)
|
ClientID: "",
|
||||||
|
ClientSecret: "",
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: "",
|
||||||
|
DeviceAuthURL: "",
|
||||||
|
TokenURL: "",
|
||||||
|
AuthStyle: 0,
|
||||||
|
},
|
||||||
|
RedirectURL: "",
|
||||||
|
Scopes: nil,
|
||||||
|
},
|
||||||
|
State,
|
||||||
|
func(authCodeURL string) (code string, state string, err error) {
|
||||||
|
|
||||||
go func() {
|
},
|
||||||
<-ctx.Done()
|
))
|
||||||
_ = app.Shutdown(tool.Timeout(2))
|
|
||||||
}()
|
|
||||||
|
|
||||||
return app.Run(":18080")
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleLoginAction(c *nf.Ctx) error {
|
|
||||||
type Req struct {
|
|
||||||
Username string `form:"username"`
|
|
||||||
Password string `form:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
req = new(Req)
|
|
||||||
token *oauth2.Token
|
|
||||||
)
|
|
||||||
|
|
||||||
if err = c.BodyParser(req); err != nil {
|
|
||||||
return c.Status(http.StatusBadRequest).SendString(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("[C] password mode: username = %s, password = %s", req.Username, req.Password)
|
|
||||||
|
|
||||||
if token, err = config.PasswordCredentialsToken(c.Context(), req.Username, req.Password); err != nil {
|
|
||||||
log.Error("[C] config do password token err = %s", err)
|
|
||||||
return c.Status(http.StatusBadRequest).SendString(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
bs, _ := json.Marshal(token)
|
|
||||||
log.Info("[C] oauth finally token =\n%s", string(bs))
|
|
||||||
|
|
||||||
return resp.Resp200(c, token)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleLoginPage(c *nf.Ctx) error {
|
|
||||||
if c.Query("oauth") != "" {
|
|
||||||
uri := config.AuthCodeURL(state, oauth2.ApprovalForce)
|
|
||||||
log.Info("[C] oauth config client_secret = %s", config.ClientSecret)
|
|
||||||
log.Info("[C] redirect to oauth2 server uri = %s", uri)
|
|
||||||
return c.Redirect(uri, http.StatusFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.HTML(page)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleRedirect(c *nf.Ctx) error {
|
|
||||||
type Req struct {
|
|
||||||
State string `query:"state"`
|
|
||||||
Code string `query:"code"`
|
|
||||||
Scope string `query:"scope"`
|
|
||||||
ClientId string `query:"client_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
req = new(Req)
|
|
||||||
token *oauth2.Token
|
|
||||||
)
|
|
||||||
|
|
||||||
if err = c.QueryParser(req); err != nil {
|
|
||||||
return resp.Resp400(c, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.State != state {
|
|
||||||
log.Error("[C] state mismatch, want = %s, got = %s", state, req.State)
|
|
||||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: state mismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
if token, err = config.Exchange(c.Context(), req.Code); err != nil {
|
|
||||||
log.Error("[C] oauth config exchange err: %s", err.Error())
|
|
||||||
return resp.Resp500(c, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
bs, _ := json.Marshal(token)
|
|
||||||
log.Info("[C] oauth finally token =\n%s", string(bs))
|
|
||||||
|
|
||||||
return resp.Resp200(c, token)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div>
|
<div>
|
||||||
<h3>这里是 xx 产品登录页面</h3>
|
<h3>这里是 xx 产品登录页面</h3>
|
||||||
<form action="/login" method="post">
|
<form>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>
|
<label>
|
||||||
Username
|
Username
|
||||||
@@ -41,12 +41,11 @@
|
|||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<button
|
<input
|
||||||
data-tooltip="采用 OAuth V2 登录(密码模式)"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
|
value="登录"
|
||||||
>登录</button>
|
/>
|
||||||
<a href="/login?oauth=true">使用 OAuth V2 账号登录</a>
|
<a href="http://localhost:8080/api/oauth/v2/login?client_id=test&client_secret=test&scope=test1,test2&redirect_uri=http://localhost:9119/api/login/callback">使用 Pro 账号登录</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"uauth/internal/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
func initClient() *cobra.Command {
|
|
||||||
return &cobra.Command{
|
|
||||||
Use: "client",
|
|
||||||
Short: "Run the client",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
return client.Run(cmd.Context())
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,6 +26,5 @@ func init() {
|
|||||||
|
|
||||||
Command.AddCommand(
|
Command.AddCommand(
|
||||||
initServe(),
|
initServe(),
|
||||||
initClient(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import (
|
|||||||
"uauth/internal/opt"
|
"uauth/internal/opt"
|
||||||
"uauth/internal/serve"
|
"uauth/internal/serve"
|
||||||
"uauth/internal/store/cache"
|
"uauth/internal/store/cache"
|
||||||
"uauth/internal/store/db"
|
|
||||||
"uauth/internal/tool"
|
"uauth/internal/tool"
|
||||||
"uauth/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func initServe() *cobra.Command {
|
func initServe() *cobra.Command {
|
||||||
@@ -16,16 +14,13 @@ func initServe() *cobra.Command {
|
|||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
tool.TablePrinter(opt.Cfg)
|
tool.TablePrinter(opt.Cfg)
|
||||||
tool.Must(cache.Init(opt.Cfg.Svc.Cache))
|
tool.Must(cache.Init(opt.Cfg.Svc.Cache))
|
||||||
tool.Must(db.Init(cmd.Context(), opt.Cfg.Svc.DB))
|
|
||||||
tool.Must(model.Init(db.Default.Session()))
|
|
||||||
return serve.Run(cmd.Context())
|
return serve.Run(cmd.Context())
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
svc.Flags().StringVar(&opt.Cfg.Svc.Address, "address", "localhost:8080", "listen address")
|
svc.Flags().StringVar(&opt.Cfg.Svc.Address, "address", "localhost:8080", "listen address")
|
||||||
svc.Flags().StringVar(&opt.Cfg.Svc.Prefix, "prefix", "/oauth/v2", "api prefix")
|
svc.Flags().StringVar(&opt.Cfg.Svc.Prefix, "prefix", "/api/oauth/v2", "api prefix")
|
||||||
svc.Flags().StringVar(&opt.Cfg.Svc.Cache, "cache", "lru::", "cache uri")
|
svc.Flags().StringVar(&opt.Cfg.Svc.Cache, "cache", "lru::", "cache uri")
|
||||||
svc.Flags().StringVar(&opt.Cfg.Svc.DB, "db", "sqlite::data.sqlite", "database uri")
|
|
||||||
|
|
||||||
return svc
|
return svc
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"github.com/loveuer/nf"
|
|
||||||
"github.com/loveuer/nf/nft/log"
|
|
||||||
"github.com/loveuer/nf/nft/resp"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
"uauth/internal/store/cache"
|
|
||||||
"uauth/internal/tool"
|
|
||||||
"uauth/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
IgnoreFn func(c *nf.Ctx) bool
|
|
||||||
TokenFn func(c *nf.Ctx) (string, bool)
|
|
||||||
GetUserFn func(c *nf.Ctx, token string) (*model.User, error)
|
|
||||||
NextOnError bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
defaultIgnoreFn = func(c *nf.Ctx) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultTokenFn = func(c *nf.Ctx) (string, bool) {
|
|
||||||
var token string
|
|
||||||
|
|
||||||
if token = c.Request.Header.Get("Authorization"); token != "" {
|
|
||||||
return token, true
|
|
||||||
}
|
|
||||||
|
|
||||||
if token = c.Query("access_token"); token != "" {
|
|
||||||
return token, true
|
|
||||||
}
|
|
||||||
|
|
||||||
if token = c.Cookies("access_token"); token != "" {
|
|
||||||
return token, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultGetUserFn = func(c *nf.Ctx, token string) (*model.User, error) {
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
op = new(model.User)
|
|
||||||
key = cache.Prefix + "token:" + token
|
|
||||||
)
|
|
||||||
|
|
||||||
if err = cache.Client.GetExScan(tool.Timeout(3), key, 24*time.Hour).Scan(op); err != nil {
|
|
||||||
if errors.Is(err, cache.ErrorKeyNotFound) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Error("[M] cache client get user by token key = %s, err = %s", key, err.Error())
|
|
||||||
return nil, errors.New("Internal Server Error")
|
|
||||||
}
|
|
||||||
|
|
||||||
return op, nil
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func New(cfgs ...*Config) nf.HandlerFunc {
|
|
||||||
var cfg = &Config{}
|
|
||||||
|
|
||||||
if len(cfgs) > 0 && cfgs[0] != nil {
|
|
||||||
cfg = cfgs[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.IgnoreFn == nil {
|
|
||||||
cfg.IgnoreFn = defaultIgnoreFn
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.TokenFn == nil {
|
|
||||||
cfg.TokenFn = defaultTokenFn
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.GetUserFn == nil {
|
|
||||||
cfg.GetUserFn = defaultGetUserFn
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(c *nf.Ctx) error {
|
|
||||||
if cfg.IgnoreFn(c) {
|
|
||||||
return c.Next()
|
|
||||||
}
|
|
||||||
|
|
||||||
token, ok := cfg.TokenFn(c)
|
|
||||||
if !ok {
|
|
||||||
if cfg.NextOnError {
|
|
||||||
return c.Next()
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp.Resp401(c, nil, "请登录")
|
|
||||||
}
|
|
||||||
|
|
||||||
op, err := cfg.GetUserFn(c, token)
|
|
||||||
if err != nil {
|
|
||||||
if cfg.NextOnError {
|
|
||||||
return c.Next()
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.Is(err, cache.ErrorKeyNotFound) {
|
|
||||||
return c.Status(http.StatusUnauthorized).JSON(map[string]any{
|
|
||||||
"status": 500,
|
|
||||||
"msg": "用户认证信息不存在或已过期, 请重新登录",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Locals("user", op)
|
|
||||||
|
|
||||||
return c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ type svc struct {
|
|||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
Prefix string `json:"prefix"`
|
Prefix string `json:"prefix"`
|
||||||
Cache string `json:"cache"`
|
Cache string `json:"cache"`
|
||||||
DB string `json:"db"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
"errors"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/loveuer/nf"
|
|
||||||
"github.com/loveuer/nf/nft/log"
|
|
||||||
"github.com/loveuer/nf/nft/resp"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
"uauth/internal/store/cache"
|
|
||||||
"uauth/internal/store/db"
|
|
||||||
"uauth/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed serve_approve.html
|
|
||||||
var pageApprove string
|
|
||||||
|
|
||||||
func Approve(c *nf.Ctx) error {
|
|
||||||
// 获取表单数据
|
|
||||||
type Req struct {
|
|
||||||
ClientId string `form:"client_id"`
|
|
||||||
RedirectURI string `form:"redirect_uri"`
|
|
||||||
Scope string `form:"scope"`
|
|
||||||
State string `form:"state"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ok bool
|
|
||||||
op *model.User
|
|
||||||
err error
|
|
||||||
req = new(Req)
|
|
||||||
uri *url.URL
|
|
||||||
client = new(model.Client)
|
|
||||||
)
|
|
||||||
|
|
||||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
|
||||||
return resp.Resp401(c, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = c.BodyParser(req); err != nil {
|
|
||||||
return resp.Resp400(c, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if uri, err = url.Parse(req.RedirectURI); err != nil {
|
|
||||||
log.Warn("[S] parse redirect uri = %s, err = %s", req.RedirectURI, err.Error())
|
|
||||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: invalid redirect uri")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = db.Default.Session().Where("client_id", req.ClientId).Take(client).Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: invalid client_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Error("[S] get client by id fail, client_id = %s, err = %s", req.ClientId, err.Error())
|
|
||||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
|
||||||
}
|
|
||||||
|
|
||||||
db.Default.Session().Clauses(clause.OnConflict{DoNothing: true}).
|
|
||||||
Create(&model.AuthorizationRecord{
|
|
||||||
UserId: op.Id,
|
|
||||||
ClientId: client.Id,
|
|
||||||
})
|
|
||||||
|
|
||||||
authorizationCode := uuid.New().String()[:8]
|
|
||||||
if err = cache.Client.SetEx(c.Context(), cache.Prefix+"auth_code:"+authorizationCode, op.Id, 10*time.Minute); err != nil {
|
|
||||||
return resp.Resp500(c, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
qs := uri.Query()
|
|
||||||
qs.Add("code", authorizationCode)
|
|
||||||
qs.Add("client_id", req.ClientId)
|
|
||||||
qs.Add("scope", req.Scope)
|
|
||||||
qs.Add("state", req.State)
|
|
||||||
|
|
||||||
uri.ForceQuery = true
|
|
||||||
value := uri.String() + qs.Encode()
|
|
||||||
|
|
||||||
return c.RenderHTML("approve", pageApprove, map[string]interface{}{
|
|
||||||
"redirect_uri": value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,121 +1,39 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
|
||||||
"errors"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/loveuer/nf"
|
"github.com/loveuer/nf"
|
||||||
"github.com/loveuer/nf/nft/log"
|
|
||||||
"github.com/loveuer/nf/nft/resp"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
"uauth/internal/store/cache"
|
|
||||||
"uauth/internal/store/db"
|
|
||||||
"uauth/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
//go:embed serve_authorize.html
|
|
||||||
pageAuthorize string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Authorize(c *nf.Ctx) error {
|
func Authorize(c *nf.Ctx) error {
|
||||||
|
|
||||||
type Req struct {
|
// 解析查询参数
|
||||||
ClientId string `query:"client_id"`
|
clientID := c.Query("client_id")
|
||||||
ResponseType string `query:"response_type"`
|
responseType := c.Query("response_type")
|
||||||
RedirectURI string `query:"redirect_uri"`
|
redirectURI := c.Query("redirect_uri")
|
||||||
Scope string `query:"scope"`
|
scope := c.Query("scope")
|
||||||
State string `query:"state"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
// 检查客户端 ID 和其他参数
|
||||||
ok bool
|
// 在实际应用中,你需要检查这些参数是否合法
|
||||||
op *model.User
|
if clientID != "12345" || responseType != "code" || redirectURI != "http://localhost:8080/callback" {
|
||||||
req = new(Req)
|
|
||||||
err error
|
|
||||||
client = &model.Client{}
|
|
||||||
authRecord = &model.AuthorizationRecord{}
|
|
||||||
uri *url.URL
|
|
||||||
)
|
|
||||||
|
|
||||||
if err = c.QueryParser(req); err != nil {
|
|
||||||
log.Error("[S] query parser err = %s", err.Error())
|
|
||||||
return c.Status(http.StatusBadRequest).SendString("Invalid request")
|
return c.Status(http.StatusBadRequest).SendString("Invalid request")
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.ResponseType != "code" {
|
// 显示授权页面给用户
|
||||||
log.Warn("[S] response type = %s", req.ResponseType)
|
_, err := c.Write([]byte(`
|
||||||
return c.Status(http.StatusBadRequest).SendString("Invalid request")
|
<html>
|
||||||
}
|
<head><title>Authorization</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Do you want to authorize this application?</h1>
|
||||||
|
<form action="/approve" method="post">
|
||||||
|
<input type="hidden" name="client_id" value="` + clientID + `"/>
|
||||||
|
<input type="hidden" name="redirect_uri" value="` + redirectURI + `"/>
|
||||||
|
<input type="hidden" name="scope" value="` + scope + `"/>
|
||||||
|
<button type="submit">Yes, I authorize</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`))
|
||||||
|
|
||||||
// 如果未登录,则跳转到登录界面
|
return err
|
||||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
|
||||||
log.Info("[S] op not logined, redirect to login page")
|
|
||||||
return c.Redirect("/oauth/v2/login?"+c.Request.URL.Query().Encode(), http.StatusFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("[S] Authorize: username = %s, client_id = %s", op.Username, req.ClientId)
|
|
||||||
|
|
||||||
if err = db.Default.Session().Where("client_id", req.ClientId).Take(client).Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: invalid client_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Error("[Authorize]: db take clients err = %s", err.Error())
|
|
||||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = db.Default.Session().Model(&model.AuthorizationRecord{}).
|
|
||||||
Where("user_id", op.Id).
|
|
||||||
Where("client_id", client.Id).
|
|
||||||
Take(authRecord).
|
|
||||||
Error; err != nil {
|
|
||||||
// 用户第一次对该 client 进行授权
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
|
|
||||||
return c.RenderHTML("authorize", pageAuthorize, map[string]any{
|
|
||||||
"user": map[string]any{
|
|
||||||
"username": op.Username,
|
|
||||||
"avatar": "https://picsum.photos/200",
|
|
||||||
},
|
|
||||||
"client_id": req.ClientId,
|
|
||||||
"redirect_uri": req.RedirectURI,
|
|
||||||
"scope": req.Scope,
|
|
||||||
"state": req.State,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Error("[Authorize]: db take authorization_records err = %s", err.Error())
|
|
||||||
return resp.Resp500(c, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 当用户已经授权过时
|
|
||||||
// 生成授权码并缓存授权码
|
|
||||||
log.Debug("[Authorize]: username = %s already approved %s", op.Username, client.Name)
|
|
||||||
|
|
||||||
authorizationCode := uuid.New().String()[:8]
|
|
||||||
if err = cache.Client.SetEx(c.Context(), cache.Prefix+"auth_code:"+authorizationCode, op.Id, 10*time.Minute); err != nil {
|
|
||||||
return resp.Resp500(c, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if uri, err = url.Parse(req.RedirectURI); err != nil {
|
|
||||||
log.Warn("[S] parse redirect uri = %s, err = %s", req.RedirectURI, err.Error())
|
|
||||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: invalid redirect uri")
|
|
||||||
}
|
|
||||||
|
|
||||||
qs := uri.Query()
|
|
||||||
qs.Add("code", authorizationCode)
|
|
||||||
qs.Add("client_id", req.ClientId)
|
|
||||||
qs.Add("scope", req.Scope)
|
|
||||||
qs.Add("state", req.State)
|
|
||||||
|
|
||||||
uri.ForceQuery = true
|
|
||||||
value := uri.String() + qs.Encode()
|
|
||||||
|
|
||||||
return c.RenderHTML("approve", pageApprove, map[string]interface{}{
|
|
||||||
"redirect_uri": value,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,136 +2,76 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"errors"
|
|
||||||
"github.com/loveuer/nf"
|
"github.com/loveuer/nf"
|
||||||
"github.com/loveuer/nf/nft/log"
|
|
||||||
"github.com/loveuer/nf/nft/resp"
|
"github.com/loveuer/nf/nft/resp"
|
||||||
"gorm.io/gorm"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"net/url"
|
||||||
"uauth/internal/store/cache"
|
|
||||||
"uauth/internal/store/db"
|
|
||||||
"uauth/internal/tool"
|
|
||||||
"uauth/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
//go:embed serve_login.html
|
//go:embed serve_login.html
|
||||||
pageLogin string
|
page string
|
||||||
)
|
)
|
||||||
|
|
||||||
func LoginPage(c *nf.Ctx) error {
|
func LoginPage(c *nf.Ctx) error {
|
||||||
type Req struct {
|
type Req struct {
|
||||||
ClientId string `query:"client_id" json:"client_id"`
|
ClientID string `query:"client_id"`
|
||||||
Scope string `query:"scope" json:"scope"`
|
ClientSecret string `query:"client_secret"`
|
||||||
RedirectURI string `query:"redirect_uri" json:"redirect_uri"`
|
Scope string `query:"scope"`
|
||||||
State string `query:"state" json:"state"`
|
RedirectURI string `query:"redirect_uri"`
|
||||||
ResponseType string `query:"response_type" json:"response_type"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
req = new(Req)
|
req = new(Req)
|
||||||
client = new(model.Client)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if err = c.QueryParser(req); err != nil {
|
if err = c.QueryParser(req); err != nil {
|
||||||
return resp.Resp400(c, err.Error())
|
return resp.Resp400(c, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.ClientId == "" || req.RedirectURI == "" {
|
if req.ClientID == "" || req.ClientSecret == "" || req.RedirectURI == "" {
|
||||||
return resp.Resp400(c, req)
|
return resp.Resp400(c, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = db.Default.Session().Model(&model.Client{}).
|
// todo: 验证 client id, client secret, scoop
|
||||||
Where("client_id = ?", req.ClientId).
|
|
||||||
Take(client).
|
|
||||||
Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return c.Status(http.StatusForbidden).SendString("Client Not Registry")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Error("[S] model take client id = %s, err = %s", req.ClientId, err.Error())
|
// todo: 如果用户是已登录状态,则直接带上信息返回到 authorize 页面
|
||||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.RenderHTML("login", pageLogin, map[string]interface{}{
|
return c.RenderHTML("login", page, map[string]interface{}{
|
||||||
"client_id": req.ClientId,
|
"client_id": req.ClientID,
|
||||||
|
"client_secret": req.ClientSecret,
|
||||||
"redirect_uri": req.RedirectURI,
|
"redirect_uri": req.RedirectURI,
|
||||||
"scope": req.Scope,
|
"scope": req.Scope,
|
||||||
"state": req.State,
|
|
||||||
"response_type": req.ResponseType,
|
|
||||||
"client_name": client.Name,
|
|
||||||
"client_icon": client.Icon,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed serve_login_success.html
|
|
||||||
var pageLoginSuccess string
|
|
||||||
|
|
||||||
func LoginAction(c *nf.Ctx) error {
|
func LoginAction(c *nf.Ctx) error {
|
||||||
type Req struct {
|
type Req struct {
|
||||||
Username string `form:"username"`
|
Username string `form:"username"`
|
||||||
Password string `form:"password"`
|
Password string `form:"password"`
|
||||||
ClientId string `form:"client_id"`
|
ClientId string `form:"client_id"`
|
||||||
|
ClientSecret string `form:"client_secret"`
|
||||||
RedirectURI string `form:"redirect_uri"`
|
RedirectURI string `form:"redirect_uri"`
|
||||||
Scope string `form:"scope"`
|
Scope string `form:"scope"`
|
||||||
State string `form:"state"`
|
|
||||||
ResponseType string `form:"response_type"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
req = new(Req)
|
req = new(Req)
|
||||||
op = new(model.User)
|
|
||||||
token string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if err = c.BodyParser(req); err != nil {
|
if err = c.BodyParser(req); err != nil {
|
||||||
log.Warn("[S] LoginAction: body parser err = %s", err.Error())
|
return resp.Resp400(c, err.Error())
|
||||||
return c.Status(http.StatusBadRequest).SendString("Bad Request")
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Username == "" || req.Password == "" {
|
|
||||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: username, password is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = db.Default.Session().Model(&model.User{}).
|
|
||||||
Where("username = ?", req.Username).
|
|
||||||
Take(op).Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
log.Warn("[S] LoginAction: username = %s not found", req.Username)
|
|
||||||
return c.Status(http.StatusBadRequest).SendString("Bad Request")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Error("[S] LoginAction: model take username = %s, err = %s", req.Username, err.Error())
|
|
||||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: 验证用户登录是否成功,等等
|
// todo: 验证用户登录是否成功,等等
|
||||||
if !tool.ComparePassword(req.Password, op.Password) {
|
|
||||||
log.Warn("[S] LoginAction: model take username = %s, password is invalid", req.Username)
|
|
||||||
return c.Status(http.StatusBadRequest).SendString("Bad Request")
|
|
||||||
}
|
|
||||||
|
|
||||||
if token, err = op.JwtEncode(); err != nil {
|
queries := make(url.Values)
|
||||||
log.Error("[S] LoginAction: jwtEncode err = %s", err.Error())
|
queries.Add("client_id", req.ClientId)
|
||||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
queries.Add("client_secret", req.ClientSecret)
|
||||||
}
|
queries.Add("redirect_uri", req.RedirectURI)
|
||||||
|
queries.Add("scope", req.Scope)
|
||||||
|
|
||||||
key := cache.Prefix + "token:" + token
|
return c.Redirect("/api/oauth/v2/authorize?"+queries.Encode(), http.StatusFound)
|
||||||
if err = cache.Client.SetEx(c.Context(), key, op, 24*time.Hour); err != nil {
|
|
||||||
log.Error("[S] LoginAction: cache SetEx err = %s", err.Error())
|
|
||||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Writer.Header().Add("Set-Cookie", "access_token="+token)
|
|
||||||
|
|
||||||
return c.RenderHTML("login_success", pageLoginSuccess, map[string]interface{}{
|
|
||||||
"client_id": req.ClientId,
|
|
||||||
"redirect_uri": req.RedirectURI,
|
|
||||||
"scope": req.Scope,
|
|
||||||
"state": req.State,
|
|
||||||
"response_type": req.ResponseType,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
"errors"
|
|
||||||
"github.com/loveuer/nf"
|
|
||||||
"github.com/loveuer/nf/nft/resp"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"uauth/internal/store/db"
|
|
||||||
"uauth/internal/tool"
|
|
||||||
"uauth/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ClientRegistry(c *nf.Ctx) error {
|
|
||||||
type Req struct {
|
|
||||||
ClientId string `json:"client_id"`
|
|
||||||
Icon string `json:"icon"` // url
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
req = new(Req)
|
|
||||||
)
|
|
||||||
|
|
||||||
if err = c.BodyParser(req); err != nil {
|
|
||||||
return resp.Resp400(c, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
Secret := tool.RandomString(32)
|
|
||||||
|
|
||||||
platform := &model.Client{
|
|
||||||
ClientId: req.ClientId,
|
|
||||||
Icon: req.Icon,
|
|
||||||
Name: req.Name,
|
|
||||||
ClientSecret: Secret,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = db.Default.Session().Create(platform).Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
|
||||||
return resp.Resp400(c, err, "当前平台已经存在")
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp.Resp500(c, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp.Resp200(c, platform)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
//go:embed serve_registry.html
|
|
||||||
registryLogin string
|
|
||||||
)
|
|
||||||
|
|
||||||
func UserRegistryPage(c *nf.Ctx) error {
|
|
||||||
return c.HTML(registryLogin)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UserRegistryAction(c *nf.Ctx) error {
|
|
||||||
type Req struct {
|
|
||||||
Username string `form:"username"`
|
|
||||||
Nickname string `form:"nickname"`
|
|
||||||
Password string `form:"password"`
|
|
||||||
ConfirmPassword string `form:"confirm_password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
req = new(Req)
|
|
||||||
)
|
|
||||||
|
|
||||||
if err = c.BodyParser(req); err != nil {
|
|
||||||
return resp.Resp400(c, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = tool.CheckPassword(req.Password); err != nil {
|
|
||||||
return resp.Resp400(c, req, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
op := &model.User{
|
|
||||||
Username: req.Username,
|
|
||||||
Nickname: req.Nickname,
|
|
||||||
Password: tool.NewPassword(req.Password),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = db.Default.Session().Create(op).Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return resp.Resp400(c, err, "用户名已存在")
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp.Resp500(c, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.HTML(`
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.jade.min.css"
|
|
||||||
>
|
|
||||||
<title>注册成功</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>注册成功</h1>
|
|
||||||
<h3>快去试试吧</h3>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>授权成功</title>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.jade.min.css"
|
|
||||||
>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<span aria-busy="true">授权成功, 正在跳转回原网页...</span>
|
|
||||||
<div style="display: none">
|
|
||||||
<input type="hidden" id="redirect_uri" value="{{ .redirect_uri }}"/>
|
|
||||||
</div>
|
|
||||||
<script type="text/javascript">
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('[D] after 1s console')
|
|
||||||
let redirect_uri = document.getElementById('redirect_uri').value
|
|
||||||
window.location.href = redirect_uri
|
|
||||||
}, 1000)
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.jade.min.css"
|
|
||||||
>
|
|
||||||
<title>Server Login</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<h3>授权登录到 {{ .client_name }} 平台</h3>
|
|
||||||
<div class="userinfo">
|
|
||||||
<article style="display: flex; align-items: center;">
|
|
||||||
<div style="height:50px; width:50px;">
|
|
||||||
<img src="{{ .user.avatar }}"/>
|
|
||||||
</div>
|
|
||||||
<div style="margin-left:20px; ">
|
|
||||||
{{ .user.username }}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
<form action="/oauth/v2/approve" method="POST">
|
|
||||||
<fieldset>
|
|
||||||
<input type="hidden" name="client_id" value="{{ .client_id }}"/>
|
|
||||||
<input type="hidden" name="redirect_uri" value="{{ .redirect_uri }}"/>
|
|
||||||
<input type="hidden" name="scope" value="{{ .scope }}"/>
|
|
||||||
<input type="hidden" name="state" value="{{ .state }}"/>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<div style="display: flex;">
|
|
||||||
<button type="button" class="contrast" style="flex:1; margin-right: 10px">取消</button>
|
|
||||||
<button type="submit" style="flex: 1;">授权</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -15,45 +15,12 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
div.row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
max-width: 33%;
|
|
||||||
height: 50px;
|
|
||||||
overflow: hidden;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
div.row:nth-child(2) {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
div.row:last-child {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div>
|
<div>
|
||||||
<h3>欢迎来到 UAuth</h3>
|
<h3>欢迎来到 Pro</h3>
|
||||||
<article style="display: flex; align-items: center;">
|
<form action="/api/oauth/v2/login" method="POST">
|
||||||
<div class="row">
|
|
||||||
<div style="height:50px; width:50px;">
|
|
||||||
<img src="https://picsum.photos/seed/drealism/200"/>
|
|
||||||
</div>
|
|
||||||
<div style="margin-left:10px; ">UAuth</div>
|
|
||||||
</div>
|
|
||||||
<div style="transform: rotate(90deg);" class="row">
|
|
||||||
<svg t="1730168415342" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1981" xmlns:xlink="http://www.w3.org/1999/xlink" width="40" height="40"><path d="M428.3 66.4c-12-5-25.7-2.2-34.9 6.9l-320 319.6c-12.5 12.5-12.5 32.7 0 45.3 12.5 12.5 32.7 12.5 45.3 0l265.4-265V928c0 17.7 14.3 32 32 32s32-14.3 32-32V96c-0.1-12.9-7.9-24.6-19.8-29.6zM950.6 585.8c-12.5-12.5-32.8-12.5-45.3 0L640 850.8V96c0-17.7-14.3-32-32-32s-32 14.3-32 32v832c0 12.9 7.8 24.6 19.7 29.6 4 1.6 8.1 2.4 12.2 2.4 8.3 0 16.5-3.2 22.6-9.4l320-319.6c12.6-12.4 12.6-32.7 0.1-45.2z" p-id="1982"></path></svg>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div style="height:50px; width:50px;">
|
|
||||||
<img src="{{ .client_icon }}"/>
|
|
||||||
</div>
|
|
||||||
<div style="margin-left:10px; ">
|
|
||||||
{{ .client_name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<form action="/oauth/v2/login" method="POST">
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>
|
<label>
|
||||||
Username
|
Username
|
||||||
@@ -73,10 +40,9 @@
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<input type="hidden" name="client_id" value="{{ .client_id }}"/>
|
<input type="hidden" name="client_id" value="{{ .client_id }}"/>
|
||||||
|
<input type="hidden" name="client_secret" value="{{ .client_secret }}"/>
|
||||||
<input type="hidden" name="redirect_uri" value="{{ .redirect_uri }}"/>
|
<input type="hidden" name="redirect_uri" value="{{ .redirect_uri }}"/>
|
||||||
<input type="hidden" name="scope" value="{{ .scope }}"/>
|
<input type="hidden" name="scope" value="{{ .scope }}"/>
|
||||||
<input type="hidden" name="state" value="{{ .state }}" />
|
|
||||||
<input type="hidden" name="response_type" value="{{ .response_type }}" />
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>登录成功</title>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.jade.min.css"
|
|
||||||
>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<span aria-busy="true">登录成功, 正在跳转...</span>
|
|
||||||
<div style="display: none">
|
|
||||||
<input type="hidden" id="client_id" value="{{ .client_id }}"/>
|
|
||||||
<input type="hidden" id="scope" value="{{ .scope }}"/>
|
|
||||||
<input type="hidden" id="state" value="{{ .state }}"/>
|
|
||||||
<input type="hidden" id="redirect_uri" value="{{ .redirect_uri }}"/>
|
|
||||||
<input type="hidden" id="response_type" value="{{ .response_type }}"/>
|
|
||||||
</div>
|
|
||||||
<script type="text/javascript">
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('[D] after 1s console')
|
|
||||||
let client_id = document.querySelector('#client_id').value;
|
|
||||||
let scope = document.querySelector('#scope').value;
|
|
||||||
let state = document.querySelector('#state').value;
|
|
||||||
let redirect_uri = document.querySelector('#redirect_uri').value;
|
|
||||||
let response_type = document.querySelector('#response_type').value;
|
|
||||||
window.location.href = `/oauth/v2/authorize?client_id=${client_id}&scope=${scope}&redirect_uri=${redirect_uri}&state=${state}&response_type=${response_type}`;
|
|
||||||
}, 1000)
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.jade.min.css"
|
|
||||||
>
|
|
||||||
<title>Server Login</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<h3>欢迎注册 UAuth</h3>
|
|
||||||
<form action="/api/oauth/v2/registry/user" method="POST" id="form">
|
|
||||||
<fieldset>
|
|
||||||
<label>
|
|
||||||
用户名
|
|
||||||
<input id="username" name="username" autocomplete="given-name"/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
昵称
|
|
||||||
<input id="nickname" name="nickname" autocomplete="given-name"/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
密码
|
|
||||||
<input id="password" type="password" name="password" autocomplete="password"/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
重复密码
|
|
||||||
<input id="confirm_password" type="password" name="confirm_password" autocomplete="password"/>
|
|
||||||
</label>
|
|
||||||
<button type="button" style="flex: 1;width: 100%;" onclick="registry()">注册</button>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<script type="text/javascript">
|
|
||||||
function registry() {
|
|
||||||
let user = {
|
|
||||||
username: document.querySelector("#username").value,
|
|
||||||
nickname: document.querySelector("#nickname").value,
|
|
||||||
password: document.querySelector("#password").value,
|
|
||||||
confirm_password: document.querySelector("#confirm_password").value,
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[D] user = ', user)
|
|
||||||
|
|
||||||
if (!user.username || !user.password || !user.nickname) {
|
|
||||||
window.alert("参数均不能为空")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.password !== user.confirm_password) {
|
|
||||||
window.alert("两次密码不一致")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelector("#form").submit()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/loveuer/nf"
|
|
||||||
"github.com/loveuer/nf/nft/log"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"uauth/internal/store/cache"
|
|
||||||
"uauth/internal/store/db"
|
|
||||||
"uauth/internal/tool"
|
|
||||||
"uauth/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func verifyClient(c *nf.Ctx) (*model.Client, error) {
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
basic string
|
|
||||||
bs []byte
|
|
||||||
strs []string
|
|
||||||
client = new(model.Client)
|
|
||||||
)
|
|
||||||
|
|
||||||
if basic = c.Get("Authorization"); basic == "" {
|
|
||||||
return nil, errors.New("authorization header missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(basic, "Basic "):
|
|
||||||
basic = strings.TrimPrefix(basic, "Basic ")
|
|
||||||
default:
|
|
||||||
return nil, errors.New("authorization scheme not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
if bs, err = base64.StdEncoding.DecodeString(basic); err != nil {
|
|
||||||
log.Warn("[Token] base64 decode failed, raw = %s, err = %s", basic, err.Error())
|
|
||||||
return nil, errors.New("invalid basic authorization")
|
|
||||||
}
|
|
||||||
|
|
||||||
if strs = strings.SplitN(string(bs), ":", 2); len(strs) != 2 {
|
|
||||||
log.Warn("[Token] basic split err, decode = %s", string(bs))
|
|
||||||
return nil, errors.New("invalid basic authorization")
|
|
||||||
}
|
|
||||||
|
|
||||||
clientId, clientSecret := strs[0], strs[1]
|
|
||||||
if err = db.Default.Session().Model(&model.Client{}).
|
|
||||||
Where("client_id", clientId).
|
|
||||||
Take(client).
|
|
||||||
Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, errors.New("client invalid")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Error("[Token] db take client by id = %s, err = %s", clientId, err.Error())
|
|
||||||
return nil, errors.New("unknown server error")
|
|
||||||
}
|
|
||||||
|
|
||||||
if client.ClientSecret != clientSecret {
|
|
||||||
log.Warn("[Token] client_secret invalid, want = %s, got = %s", client.ClientSecret, clientSecret)
|
|
||||||
return nil, errors.New("client secret invalid")
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleToken(c *nf.Ctx) error {
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
opId uint64
|
|
||||||
op = new(model.User)
|
|
||||||
token string
|
|
||||||
client *model.Client
|
|
||||||
grantType = c.Form("grant_type")
|
|
||||||
)
|
|
||||||
|
|
||||||
switch grantType {
|
|
||||||
case "password":
|
|
||||||
if client, err = verifyClient(c); err != nil {
|
|
||||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
username := c.Form("username")
|
|
||||||
password := c.Form("password")
|
|
||||||
if err = db.Default.Session().Model(&model.User{}).
|
|
||||||
Where("username = ?", username).
|
|
||||||
Take(op).Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: invalid username or password")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Error("[Token] db take user by username = %s, err = %s", username, err.Error())
|
|
||||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tool.ComparePassword(password, op.Password) {
|
|
||||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: invalid username or password")
|
|
||||||
}
|
|
||||||
case "authorization_code":
|
|
||||||
if client, err = verifyClient(c); err != nil {
|
|
||||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
code := c.Form("code")
|
|
||||||
if code == "" {
|
|
||||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: no code provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = cache.Client.GetScan(tool.Timeout(2), cache.Prefix+"auth_code:"+code).Scan(&opId); err != nil {
|
|
||||||
if errors.Is(err, cache.ErrorKeyNotFound) {
|
|
||||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: invalid code")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Error("[S] handleToken: get code from cache err = %s", err.Error())
|
|
||||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
|
||||||
}
|
|
||||||
|
|
||||||
op.Id = opId
|
|
||||||
if err = db.Default.Session().Take(op).Error; err != nil {
|
|
||||||
log.Error("[S] handleToken: get op by id err, id = %d, err = %s", opId, err.Error())
|
|
||||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return c.Status(http.StatusBadRequest).SendString("Bad Request: invalid grant type")
|
|
||||||
}
|
|
||||||
|
|
||||||
if token, err = op.JwtEncode(); err != nil {
|
|
||||||
log.Error("[S] handleToken: encode token err, id = %d, err = %s", opId, err.Error())
|
|
||||||
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshToken := uuid.New().String()
|
|
||||||
|
|
||||||
_ = client
|
|
||||||
|
|
||||||
return c.JSON(map[string]any{
|
|
||||||
"access_token": token,
|
|
||||||
"refresh_token": refreshToken,
|
|
||||||
"token_type": "Bearer",
|
|
||||||
"expires_in": 24 * 3600,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
+122
-12
@@ -2,33 +2,143 @@ package serve
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/loveuer/nf"
|
"github.com/loveuer/nf"
|
||||||
"github.com/loveuer/nf/nft/log"
|
"github.com/loveuer/nf/nft/log"
|
||||||
"uauth/internal/middleware/auth"
|
"net/http"
|
||||||
|
"time"
|
||||||
"uauth/internal/opt"
|
"uauth/internal/opt"
|
||||||
"uauth/internal/serve/handler"
|
"uauth/internal/serve/handler"
|
||||||
|
"uauth/internal/store/cache"
|
||||||
"uauth/internal/tool"
|
"uauth/internal/tool"
|
||||||
|
"uauth/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func authenticateUser(username, password string) (bool, error) {
|
||||||
|
// 这里你应该实现真实的用户认证逻辑
|
||||||
|
// 为了简化,我们这里直接硬编码一个用户名和密码
|
||||||
|
if username == "user" && password == "pass" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, fmt.Errorf("invalid username or password")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理登录请求
|
||||||
|
func handleLogin(c *nf.Ctx) error {
|
||||||
|
username := c.FormValue("username")
|
||||||
|
password := c.FormValue("password")
|
||||||
|
|
||||||
|
// 认证用户
|
||||||
|
ok, err := authenticateUser(username, password)
|
||||||
|
if err != nil || !ok {
|
||||||
|
return c.Status(http.StatusUnauthorized).SendString("Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户认证成功,重定向到授权页面
|
||||||
|
http.Redirect(c.Writer, c.Request, "/authorize?client_id=12345&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback&scope=read%20write", http.StatusFound)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理用户的授权批准
|
||||||
|
func handleApprove(c *nf.Ctx) error {
|
||||||
|
// 获取表单数据
|
||||||
|
clientID := c.FormValue("client_id")
|
||||||
|
redirectURI := c.FormValue("redirect_uri")
|
||||||
|
scope := c.FormValue("scope")
|
||||||
|
|
||||||
|
// 生成授权码
|
||||||
|
authorizationCode := uuid.New().String()[:8]
|
||||||
|
|
||||||
|
log.Info("[D] client_id = %s, scope = %s, auth_code = %s", clientID, scope, authorizationCode)
|
||||||
|
|
||||||
|
// 重定向到回调 URL 并附带授权码
|
||||||
|
http.Redirect(c.Writer, c.Request, redirectURI+"?code="+authorizationCode, http.StatusFound)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 令牌请求的处理
|
||||||
|
func handleToken(c *nf.Ctx) error {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
grantType string
|
||||||
|
code string
|
||||||
|
redirectURI string
|
||||||
|
|
||||||
|
// 记录 user
|
||||||
|
accessToken string
|
||||||
|
refreshToken string
|
||||||
|
user = new(model.User)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 获取请求参数
|
||||||
|
grantType = c.FormValue("grant_type")
|
||||||
|
code = cache.Prefix + c.FormValue("code")
|
||||||
|
redirectURI = c.FormValue("redirect_uri")
|
||||||
|
|
||||||
|
// 简单验证
|
||||||
|
if grantType != "authorization_code" {
|
||||||
|
return c.Status(http.StatusBadRequest).SendString("Unsupported grant type")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = cache.Client.GetScan(tool.Timeout(3), code).Scan(&user); err != nil {
|
||||||
|
return c.Status(http.StatusBadRequest).SendString("Invalid authorization code")
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
// 清除已使用的授权码
|
||||||
|
cache.Client.Del(tool.Timeout(3), code)
|
||||||
|
_ = redirectURI
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 生成访问令牌
|
||||||
|
if accessToken, refreshToken, err = generateAccessToken(user); err != nil {
|
||||||
|
return c.Status(http.StatusInternalServerError).SendString(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Writer.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
|
||||||
|
// 返回访问令牌
|
||||||
|
return c.JSON(map[string]string{
|
||||||
|
"access_token": accessToken,
|
||||||
|
"refresh_token": refreshToken,
|
||||||
|
"token_type": "bearer",
|
||||||
|
"expires_in": "3600", // 访问令牌有效期(秒)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateAccessToken(user *model.User) (accessToken string, refreshToken string, err error) {
|
||||||
|
if accessToken, err = user.JwtEncode(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken = uuid.New().String()[:8]
|
||||||
|
|
||||||
|
if err = cache.Client.SetEx(tool.Timeout(3), cache.Prefix+"refresh_token", refreshToken, 24*time.Hour); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = cache.Client.SetEx(tool.Timeout(3), cache.Prefix+"access_token", accessToken, time.Hour); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func Run(ctx context.Context) error {
|
func Run(ctx context.Context) error {
|
||||||
|
|
||||||
app := nf.New()
|
app := nf.New()
|
||||||
|
|
||||||
api := app.Group(opt.Cfg.Svc.Prefix)
|
api := app.Group(opt.Cfg.Svc.Prefix)
|
||||||
|
// 设置路由
|
||||||
api.Get("/registry/user", handler.UserRegistryPage)
|
|
||||||
api.Post("/registry/user", handler.UserRegistryAction)
|
|
||||||
api.Post("/registry/client", handler.ClientRegistry)
|
|
||||||
api.Get("/login", handler.LoginPage)
|
api.Get("/login", handler.LoginPage)
|
||||||
api.Post("/login", handler.LoginAction)
|
api.Post("/login", handler.LoginAction)
|
||||||
api.Get("/authorize", auth.New(&auth.Config{NextOnError: true}), handler.Authorize)
|
api.Get("/authorize", handler.Authorize)
|
||||||
api.Post("/approve", auth.New(), handler.Approve)
|
api.Post("/approve", handleApprove)
|
||||||
api.Post("/token", handler.HandleToken)
|
api.Post("/token", handleToken)
|
||||||
|
|
||||||
api.Get("/after", auth.New(), func(c *nf.Ctx) error {
|
|
||||||
return c.SendString("hello world")
|
|
||||||
})
|
|
||||||
|
|
||||||
|
// 启动 HTTP 服务器
|
||||||
log.Info("Starting server on: %s", opt.Cfg.Svc.Address)
|
log.Info("Starting server on: %s", opt.Cfg.Svc.Address)
|
||||||
go func() {
|
go func() {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"uauth/internal/opt"
|
|
||||||
"uauth/internal/tool"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
Default *Client
|
|
||||||
)
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
ctx context.Context
|
|
||||||
cli *gorm.DB
|
|
||||||
ttype string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Type() string {
|
|
||||||
return c.ttype
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Session(ctxs ...context.Context) *gorm.DB {
|
|
||||||
var ctx context.Context
|
|
||||||
if len(ctxs) > 0 && ctxs[0] != nil {
|
|
||||||
ctx = ctxs[0]
|
|
||||||
} else {
|
|
||||||
ctx = tool.Timeout(30)
|
|
||||||
}
|
|
||||||
|
|
||||||
session := c.cli.Session(&gorm.Session{Context: ctx})
|
|
||||||
|
|
||||||
if opt.Cfg.Debug {
|
|
||||||
session = session.Debug()
|
|
||||||
}
|
|
||||||
|
|
||||||
return session
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Close() {
|
|
||||||
d, _ := c.cli.DB()
|
|
||||||
d.Close()
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestOpen(t *testing.T) {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/glebarez/sqlite"
|
|
||||||
"gorm.io/driver/mysql"
|
|
||||||
"gorm.io/driver/postgres"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
func New(ctx context.Context, uri string) (*Client, error) {
|
|
||||||
strs := strings.Split(uri, "::")
|
|
||||||
|
|
||||||
if len(strs) != 2 {
|
|
||||||
return nil, fmt.Errorf("db.Init: opt db uri invalid: %s", uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := &Client{ttype: strs[0]}
|
|
||||||
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
dsn = strs[1]
|
|
||||||
)
|
|
||||||
|
|
||||||
switch strs[0] {
|
|
||||||
case "sqlite":
|
|
||||||
c.cli, err = gorm.Open(sqlite.Open(dsn))
|
|
||||||
case "mysql":
|
|
||||||
c.cli, err = gorm.Open(mysql.Open(dsn))
|
|
||||||
case "postgres":
|
|
||||||
c.cli, err = gorm.Open(postgres.Open(dsn))
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("db type only support: [sqlite, mysql, postgres], unsupported db type: %s", strs[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("db.Init: open %s with dsn:%s, err: %w", strs[0], dsn, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Init(ctx context.Context, uri string) (err error) {
|
|
||||||
if Default, err = New(ctx, uri); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
type AuthorizationRecord struct {
|
|
||||||
Id uint64 `json:"id" gorm:"primaryKey;column:id"`
|
|
||||||
CreatedAt int64 `json:"created_at" gorm:"column:created_at;autoCreateTime:milli"`
|
|
||||||
UpdatedAt int64 `json:"updated_at" gorm:"column:updated_at;autoUpdateTime:milli"`
|
|
||||||
DeletedAt int64 `json:"deleted_at" gorm:"index;column:deleted_at;default:0"`
|
|
||||||
|
|
||||||
UserId uint64 `json:"user_id" gorm:"index:unique_idx,unique;column:user_id"`
|
|
||||||
ClientId uint64 `json:"client_id" gorm:"index:unique_idx,unique;column:client_id"`
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
Id uint64 `json:"id" gorm:"primaryKey;column:id"`
|
|
||||||
CreatedAt int64 `json:"created_at" gorm:"column:created_at;autoCreateTime:milli"`
|
|
||||||
UpdatedAt int64 `json:"updated_at" gorm:"column:updated_at;autoUpdateTime:milli"`
|
|
||||||
DeletedAt int64 `json:"deleted_at" gorm:"index;column:deleted_at;default:0"`
|
|
||||||
|
|
||||||
ClientId string `json:"client_id" gorm:"unique;column:client_id;type:varchar(128)"`
|
|
||||||
ClientSecret string `json:"client_secret" gorm:"column:client_secret;type:varchar(32)"`
|
|
||||||
Name string `json:"name" gorm:"column:name;type:varchar(256)"`
|
|
||||||
Icon string `json:"icon" gorm:"column:icon"`
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
"uauth/internal/tool"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init(tx *gorm.DB) error {
|
|
||||||
var err error
|
|
||||||
if err = tx.AutoMigrate(
|
|
||||||
&User{},
|
|
||||||
&Client{},
|
|
||||||
&AuthorizationRecord{},
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = tx.Clauses(clause.OnConflict{DoNothing: true}).
|
|
||||||
Create(&User{Username: "admin", Nickname: "admin", Password: tool.NewPassword("Foobar123")}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = tx.Clauses(clause.OnConflict{DoNothing: true}).
|
|
||||||
Create(&Client{ClientId: "test", ClientSecret: "Foobar123", Name: "测试", Icon: "https://picsum.photos/seed/loveuer/200"}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -22,7 +22,6 @@ type User struct {
|
|||||||
Status Status `json:"status" gorm:"column:status;default:0"`
|
Status Status `json:"status" gorm:"column:status;default:0"`
|
||||||
|
|
||||||
Nickname string `json:"nickname" gorm:"column:nickname;type:varchar(64)"`
|
Nickname string `json:"nickname" gorm:"column:nickname;type:varchar(64)"`
|
||||||
Avatar string `json:"avatar" gorm:"column:avatar;type:varchar(256)"`
|
|
||||||
Comment string `json:"comment" gorm:"column:comment"`
|
Comment string `json:"comment" gorm:"column:comment"`
|
||||||
|
|
||||||
CreatedById uint64 `json:"created_by_id" gorm:"column:created_by_id"`
|
CreatedById uint64 `json:"created_by_id" gorm:"column:created_by_id"`
|
||||||
@@ -38,7 +37,6 @@ func (u *User) JwtEncode() (token string, err error) {
|
|||||||
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{
|
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{
|
||||||
"id": u.Id,
|
"id": u.Id,
|
||||||
"username": u.Username,
|
"username": u.Username,
|
||||||
"nickname": u.Nickname,
|
|
||||||
"status": u.Status,
|
"status": u.Status,
|
||||||
"login_at": now.UnixMilli(),
|
"login_at": now.UnixMilli(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
# uauth
|
|
||||||
|
|
||||||
## update:
|
|
||||||
|
|
||||||
- 添加 password 模式示例
|
|
||||||
|
|
||||||
## run
|
|
||||||
|
|
||||||
- `go run . svc`
|
|
||||||
- `go run . client`
|
|
||||||
- `浏览器打开`[http://localhost:18080/login](http://localhost:18080/login)
|
|
||||||
|
|
||||||
## oauth2 authorization flow(password mode)
|
|
||||||
|
|
||||||
- 1. 客户端直接拿到用户的账号和密码来请求 oauth2 服务器获取 token
|
|
||||||
|
|
||||||
## oauth2 authorization flow(authorization_code mode)
|
|
||||||
|
|
||||||
- 1. 某某 系统/平台(比如: xx_platform) 的用户想要登录该 系统/平台, 并点击到登录页面
|
|
||||||
- 2. 用户发现该平台上有 `通过 {oauth2} 登录` 的按钮, 用户点击该按钮, 跳转到 `{oauth2}` 服务的登录页面如: `/oauth2/login`
|
|
||||||
|
|
||||||
> example: https://oauth2.com/login?client_id=...&scope=...&redirect_uri=...
|
|
||||||
|
|
||||||
> <b>需要注意, 这些参数都是必须的</b>
|
|
||||||
|
|
||||||
- 3. 跳转过来后, 有两种情况: 1. 用户没有在 {oauth2} 服务上登录过, 且当前的 token 依旧有效, 2. 用户在 {oauth2} 服务上未登录或者 token 已经过期
|
|
||||||
* 3.1 如果用户登录过且 token 有效, 则跳转到 `/oauth2/authorize` 进行授权
|
|
||||||
* 3.2 如果用户没有登录过, 或 token 已经失效, 则在登录成功后, 则跳转到 `/oauth2/authorize` 进行授权
|
|
||||||
- 4. 授权后, 会跳转到 redirect_uri 参数指定的地址, 并带上 code 和 state 参数
|
|
||||||
- 5. xx_platform 拿到 code 参数后, 向 {oauth2} 服务通过 code 请求 access_token
|
|
||||||
Reference in New Issue
Block a user