Compare commits

4 Commits
Author SHA1 Message Date
loveuer 4822fbc092 wip: 继续 oauth 2024-10-25 18:18:11 +08:00
loveuer a17b9bb16b wip: 继续 day2 2024-10-24 18:01:44 +08:00
loveuer f3861184b6 wip: 继续... 2024-10-23 22:42:13 +08:00
loveuer eb1744bd0d wip: 刚刚开始, 探索阶段 2024-10-23 17:46:15 +08:00
64 changed files with 397 additions and 2225 deletions
-3
View File
@@ -2,6 +2,3 @@
.vscode
.DS_Store
xtest
*.db
*.sqlite
*.sqlite3
+23 -25
View File
@@ -3,56 +3,54 @@ module uauth
go 1.20
require (
gitea.com/loveuer/gredis v1.0.0
github.com/glebarez/sqlite v1.11.0
gitea.com/taozitaozi/gredis v0.0.0-20240131032054-b02845ce1e9d
github.com/gin-gonic/gin v1.10.0
github.com/go-redis/redis/v8 v8.11.5
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/jackc/pgtype v1.14.4
github.com/jedib0t/go-pretty/v6 v6.6.1
github.com/loveuer/nf v0.2.12
github.com/samber/lo v1.47.0
github.com/spf13/cast v1.7.0
github.com/spf13/cobra v1.8.1
golang.org/x/crypto v0.26.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 (
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/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/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // 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/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // 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/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // 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/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/sirupsen/logrus v1.9.2 // 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/sync v0.8.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+53 -234
View File
@@ -1,314 +1,133 @@
gitea.com/loveuer/gredis v1.0.0 h1:fbRS8YZObcp1KV1KGj8pDpIj1WrI0W8pwU9Ny/2fJys=
gitea.com/loveuer/gredis v1.0.0/go.mod h1:TQlubgDiyNTRXqASd/XIUrqPBLj9NZRR2DmV3V2ZyMY=
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=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
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/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
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/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/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/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/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
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/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
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/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
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/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
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/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
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/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8=
github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU=
github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
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 v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
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/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
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/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
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/go.mod h1:M6reF17/kJBis30H4DxR5hrtgo/oJL4AV4cBe4HzJLw=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
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/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/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/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/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
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/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
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/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
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/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
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=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
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=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
View File
+18
View File
@@ -0,0 +1,18 @@
meta {
name: auth req
type: http
seq: 2
}
get {
url: http://127.0.0.1:8080/authorize?client_id=12345&response_type=code&redirect_uri=http://localhost:8080/callback&scopde=balaba
body: none
auth: none
}
params:query {
client_id: 12345
response_type: code
redirect_uri: http://localhost:8080/callback
scopde: balaba
}
+9
View File
@@ -0,0 +1,9 @@
{
"version": "1",
"name": "uauth",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}
+20 -105
View File
@@ -2,118 +2,33 @@ package client
import (
"context"
_ "embed"
"encoding/json"
"github.com/google/uuid"
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/log"
"github.com/loveuer/nf/nft/resp"
"golang.org/x/oauth2"
"net/http"
"uauth/tool"
"golang.org/x/oauth2/authhandler"
)
//go:embed login.html
var page string
var (
config = oauth2.Config{
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]
State = uuid.New().String()[:8]
)
func Run(ctx context.Context) error {
app := nf.New()
app.Get("/login", handleLoginPage)
app.Post("/login", handleLoginAction)
app.Get("/oauth/v2/redirect", handleRedirect)
oauth2.NewClient(ctx, authhandler.TokenSource(
ctx,
&oauth2.Config{
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)
},
))
}
+5 -6
View File
@@ -20,7 +20,7 @@
<body>
<div>
<h3>这里是 xx 产品登录页面</h3>
<form action="/login" method="post">
<form>
<fieldset>
<label>
Username
@@ -41,12 +41,11 @@
</label>
</fieldset>
<button
data-tooltip="采用 OAuth V2 登录(密码模式)"
<input
type="submit"
>登录</button>
<a href="/login?oauth=true">使用 OAuth V2 账号登录</a>
value="登录"
/>
<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>
</div>
</body>
-16
View File
@@ -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())
},
}
}
-1
View File
@@ -26,6 +26,5 @@ func init() {
Command.AddCommand(
initServe(),
initClient(),
)
}
+3 -10
View File
@@ -4,11 +4,8 @@ import (
"github.com/spf13/cobra"
"uauth/internal/opt"
"uauth/internal/serve"
"uauth/model"
"uauth/pkg/cache"
"uauth/pkg/rbac"
"uauth/pkg/store"
"uauth/tool"
"uauth/internal/store/cache"
"uauth/internal/tool"
)
func initServe() *cobra.Command {
@@ -17,17 +14,13 @@ func initServe() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
tool.TablePrinter(opt.Cfg)
tool.Must(cache.Init(opt.Cfg.Svc.Cache))
tool.Must(store.Init(opt.Cfg.Svc.DB, store.Config{Debug: opt.Cfg.Debug}))
tool.Must(model.Init(store.Default.Session(tool.Timeout())))
tool.Must(rbac.Init(store.Default, cache.Client))
return serve.Run(cmd.Context())
},
}
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.DB, "db", "sqlite::data.sqlite", "database uri")
return svc
}
@@ -1,17 +1,11 @@
package cache
package interfaces
import (
"context"
"encoding/json"
"time"
)
const (
// todo: config this
Prefix = "sys:uauth:"
)
type Cache interface {
type Cacher interface {
Get(ctx context.Context, key string) ([]byte, error)
GetScan(ctx context.Context, key string) Scanner
GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error)
@@ -26,35 +20,3 @@ type Cache interface {
type Scanner interface {
Scan(model any) error
}
var (
Client Cache
)
type encoded_value interface {
MarshalBinary() ([]byte, error)
}
type decoded_value interface {
UnmarshalBinary(bs []byte) error
}
func handleValue(value any) ([]byte, error) {
var (
bs []byte
err error
)
switch value.(type) {
case []byte:
return value.([]byte), nil
}
if imp, ok := value.(encoded_value); ok {
bs, err = imp.MarshalBinary()
} else {
bs, err = json.Marshal(value)
}
return bs, err
}
+11
View File
@@ -0,0 +1,11 @@
package interfaces
type Enum interface {
Value() int64
Code() string
Label() string
MarshalJSON() ([]byte, error)
All() []Enum
}
+7
View File
@@ -0,0 +1,7 @@
package interfaces
type OpLogger interface {
Enum
Render(content map[string]any) (string, error)
Template() string
}
-1
View File
@@ -4,7 +4,6 @@ type svc struct {
Address string `json:"address"`
Prefix string `json:"prefix"`
Cache string `json:"cache"`
DB string `json:"db"`
}
type config struct {
-89
View File
@@ -1,89 +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/model"
"uauth/pkg/cache"
"uauth/pkg/store"
"uauth/tool"
)
//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 = store.Default.Session(tool.TimeoutCtx(c.Context(), 3)).
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")
}
store.Default.Session(tool.TimeoutCtx(c.Context(), 3)).
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,
})
}
+24 -109
View File
@@ -1,124 +1,39 @@
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"
"net/http"
"net/url"
"time"
"uauth/model"
"uauth/pkg/cache"
"uauth/pkg/store"
"uauth/tool"
)
var (
//go:embed serve_authorize.html
pageAuthorize string
)
func Authorize(c *nf.Ctx) error {
type Req struct {
ClientId string `query:"client_id"`
ResponseType string `query:"response_type"`
RedirectURI string `query:"redirect_uri"`
Scope string `query:"scope"`
State string `query:"state"`
}
// 解析查询参数
clientID := c.Query("client_id")
responseType := c.Query("response_type")
redirectURI := c.Query("redirect_uri")
scope := c.Query("scope")
var (
ok bool
op *model.User
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())
// 检查客户端 ID 和其他参数
// 在实际应用中,你需要检查这些参数是否合法
if clientID != "12345" || responseType != "code" || redirectURI != "http://localhost:8080/callback" {
return c.Status(http.StatusBadRequest).SendString("Invalid request")
}
if req.ResponseType != "code" {
log.Warn("[S] response type = %s", req.ResponseType)
return c.Status(http.StatusBadRequest).SendString("Invalid request")
}
// 显示授权页面给用户
_, err := c.Write([]byte(`
<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>
`))
// 如果未登录,则跳转到登录界面
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 = store.Default.Session(tool.TimeoutCtx(c.Context(), 3)).
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 = store.Default.Session(tool.TimeoutCtx(c.Context(), 3)).
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,
})
return err
}
+24 -86
View File
@@ -2,138 +2,76 @@ package handler
import (
_ "embed"
"errors"
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/log"
"github.com/loveuer/nf/nft/resp"
"gorm.io/gorm"
"net/http"
"time"
"uauth/model"
"uauth/pkg/cache"
"uauth/pkg/store"
"uauth/tool"
"net/url"
)
var (
//go:embed serve_login.html
pageLogin string
page string
)
func LoginPage(c *nf.Ctx) error {
type Req struct {
ClientId string `query:"client_id" json:"client_id"`
Scope string `query:"scope" json:"scope"`
RedirectURI string `query:"redirect_uri" json:"redirect_uri"`
State string `query:"state" json:"state"`
ResponseType string `query:"response_type" json:"response_type"`
ClientID string `query:"client_id"`
ClientSecret string `query:"client_secret"`
Scope string `query:"scope"`
RedirectURI string `query:"redirect_uri"`
}
var (
err error
req = new(Req)
client = new(model.Client)
err error
req = new(Req)
)
if err = c.QueryParser(req); err != nil {
return resp.Resp400(c, err.Error())
}
if req.ClientId == "" || req.RedirectURI == "" {
if req.ClientID == "" || req.ClientSecret == "" || req.RedirectURI == "" {
return resp.Resp400(c, req)
}
if err = store.Default.Session(tool.TimeoutCtx(c.Context(), 3)).
Model(&model.Client{}).
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")
}
// todo: 验证 client id, client secret, scoop
log.Error("[S] model take client id = %s, err = %s", req.ClientId, err.Error())
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
}
// todo: 如果用户是已登录状态,则直接带上信息返回到 authorize 页面
return c.RenderHTML("login", pageLogin, map[string]interface{}{
"client_id": req.ClientId,
return c.RenderHTML("login", page, map[string]interface{}{
"client_id": req.ClientID,
"client_secret": req.ClientSecret,
"redirect_uri": req.RedirectURI,
"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 {
type Req struct {
Username string `form:"username"`
Password string `form:"password"`
ClientId string `form:"client_id"`
ClientSecret string `form:"client_secret"`
RedirectURI string `form:"redirect_uri"`
Scope string `form:"scope"`
State string `form:"state"`
ResponseType string `form:"response_type"`
}
var (
err error
req = new(Req)
op = new(model.User)
token string
err error
req = new(Req)
)
if err = c.BodyParser(req); err != nil {
log.Warn("[S] LoginAction: body parser err = %s", 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 = store.Default.Session(tool.TimeoutCtx(c.Context(), 3)).
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")
return resp.Resp400(c, err.Error())
}
// 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 {
log.Error("[S] LoginAction: jwtEncode err = %s", err.Error())
return c.Status(http.StatusInternalServerError).SendString("Internal Server Error")
}
queries := make(url.Values)
queries.Add("client_id", req.ClientId)
queries.Add("client_secret", req.ClientSecret)
queries.Add("redirect_uri", req.RedirectURI)
queries.Add("scope", req.Scope)
key := cache.Prefix + "token:" + token
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,
})
return c.Redirect("/api/oauth/v2/authorize?"+queries.Encode(), http.StatusFound)
}
-113
View File
@@ -1,113 +0,0 @@
package handler
import (
_ "embed"
"errors"
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/resp"
"gorm.io/gorm"
"uauth/model"
"uauth/pkg/store"
"uauth/tool"
)
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 = store.Default.Session(tool.TimeoutCtx(c.Context(), 3)).
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 = store.Default.Session(tool.TimeoutCtx(c.Context(), 3)).
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>
`)
}
-33
View File
@@ -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>
+3 -37
View File
@@ -15,45 +15,12 @@
justify-content: 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>
</head>
<body>
<div>
<h3>欢迎来到 UAuth</h3>
<article style="display: flex; align-items: center;">
<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">
<h3>欢迎来到 Pro</h3>
<form action="/api/oauth/v2/login" method="POST">
<fieldset>
<label>
Username
@@ -73,10 +40,9 @@
/>
</label>
<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="scope" value="{{ .scope }}"/>
<input type="hidden" name="state" value="{{ .state }}" />
<input type="hidden" name="response_type" value="{{ .response_type }}" />
</fieldset>
<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>
-147
View File
@@ -1,147 +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/model"
"uauth/pkg/cache"
"uauth/pkg/store"
"uauth/tool"
)
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 = store.Default.Session(tool.TimeoutCtx(c.Context(), 3)).
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 = store.Default.Session(tool.TimeoutCtx(c.Context(), 3)).
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 = store.Default.Session(tool.TimeoutCtx(c.Context(), 3)).
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,
})
}
+123 -13
View File
@@ -2,33 +2,143 @@ package serve
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/log"
"net/http"
"time"
"uauth/internal/opt"
"uauth/internal/serve/handler"
"uauth/pkg/middleware/auth"
"uauth/tool"
"uauth/internal/store/cache"
"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 {
app := nf.New()
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.Post("/login", handler.LoginAction)
api.Get("/authorize", auth.New(&auth.Config{NextOnError: true}), handler.Authorize)
api.Post("/approve", auth.New(), handler.Approve)
api.Post("/token", handler.HandleToken)
api.Get("/after", auth.New(), func(c *nf.Ctx) error {
return c.SendString("hello world")
})
api.Get("/authorize", handler.Authorize)
api.Post("/approve", handleApprove)
api.Post("/token", handleToken)
// 启动 HTTP 服务器
log.Info("Starting server on: %s", opt.Cfg.Svc.Address)
go func() {
<-ctx.Done()
@@ -5,9 +5,10 @@ import (
"github.com/hashicorp/golang-lru/v2/expirable"
_ "github.com/hashicorp/golang-lru/v2/expirable"
"time"
"uauth/internal/interfaces"
)
var _ Cache = (*_lru)(nil)
var _ interfaces.Cacher = (*_lru)(nil)
type _lru struct {
client *expirable.LRU[string, *_lru_value]
@@ -37,7 +38,7 @@ func (l *_lru) Get(ctx context.Context, key string) ([]byte, error) {
return v.bs, nil
}
func (l *_lru) GetScan(ctx context.Context, key string) Scanner {
func (l *_lru) GetScan(ctx context.Context, key string) interfaces.Scanner {
return newScanner(l.Get(ctx, key))
}
@@ -67,7 +68,7 @@ func (l *_lru) GetEx(ctx context.Context, key string, duration time.Duration) ([
return v.bs, nil
}
func (l *_lru) GetExScan(ctx context.Context, key string, duration time.Duration) Scanner {
func (l *_lru) GetExScan(ctx context.Context, key string, duration time.Duration) interfaces.Scanner {
return newScanner(l.GetEx(ctx, key, duration))
}
@@ -109,7 +110,7 @@ func (l *_lru) Del(ctx context.Context, keys ...string) error {
return nil
}
func newLRUCache() (Cache, error) {
func newLRUCache() (interfaces.Cacher, error) {
client := expirable.NewLRU[string, *_lru_value](1024*1024, nil, 0)
return &_lru{client: client}, nil
@@ -5,21 +5,22 @@ import (
"errors"
"fmt"
"time"
"uauth/internal/interfaces"
"gitea.com/loveuer/gredis"
"gitea.com/taozitaozi/gredis"
)
var _ Cache = (*_mem)(nil)
var _ interfaces.Cacher = (*_mem)(nil)
type _mem struct {
client *gredis.Gredis
}
func (m *_mem) GetScan(ctx context.Context, key string) Scanner {
func (m *_mem) GetScan(ctx context.Context, key string) interfaces.Scanner {
return newScanner(m.Get(ctx, key))
}
func (m *_mem) GetExScan(ctx context.Context, key string, duration time.Duration) Scanner {
func (m *_mem) GetExScan(ctx context.Context, key string, duration time.Duration) interfaces.Scanner {
return newScanner(m.GetEx(ctx, key, duration))
}
@@ -5,6 +5,7 @@ import (
"errors"
"github.com/go-redis/redis/v8"
"time"
"uauth/internal/interfaces"
)
type _redis struct {
@@ -24,7 +25,7 @@ func (r *_redis) Get(ctx context.Context, key string) ([]byte, error) {
return []byte(result), nil
}
func (r *_redis) GetScan(ctx context.Context, key string) Scanner {
func (r *_redis) GetScan(ctx context.Context, key string) interfaces.Scanner {
return newScanner(r.Get(ctx, key))
}
@@ -41,7 +42,7 @@ func (r *_redis) GetEx(ctx context.Context, key string, duration time.Duration)
return []byte(result), nil
}
func (r *_redis) GetExScan(ctx context.Context, key string, duration time.Duration) Scanner {
func (r *_redis) GetExScan(ctx context.Context, key string, duration time.Duration) interfaces.Scanner {
return newScanner(r.GetEx(ctx, key, duration))
}
+42
View File
@@ -0,0 +1,42 @@
package cache
import (
"encoding/json"
"uauth/internal/interfaces"
)
const (
Prefix = "sys:uauth:"
)
var (
Client interfaces.Cacher
)
type encoded_value interface {
MarshalBinary() ([]byte, error)
}
type decoded_value interface {
UnmarshalBinary(bs []byte) error
}
func handleValue(value any) ([]byte, error) {
var (
bs []byte
err error
)
switch value.(type) {
case []byte:
return value.([]byte), nil
}
if imp, ok := value.(encoded_value); ok {
bs, err = imp.MarshalBinary()
} else {
bs, err = json.Marshal(value)
}
return bs, err
}
+13 -18
View File
@@ -2,22 +2,17 @@ package cache
import (
"fmt"
"gitea.com/loveuer/gredis"
"gitea.com/taozitaozi/gredis"
"github.com/go-redis/redis/v8"
"net/url"
"strings"
"uauth/tool"
"uauth/internal/tool"
)
func Init(uri string) (err error) {
Client, err = New(uri)
return err
}
func Init(uri string) error {
func New(uri string) (Cache, error) {
var (
client Cache
err error
err error
)
strs := strings.Split(uri, "::")
@@ -25,10 +20,10 @@ func New(uri string) (Cache, error) {
switch strs[0] {
case "memory":
gc := gredis.NewGredis(1024 * 1024)
client = &_mem{client: gc}
Client = &_mem{client: gc}
case "lru":
if client, err = newLRUCache(); err != nil {
return nil, err
if Client, err = newLRUCache(); err != nil {
return err
}
case "redis":
var (
@@ -37,7 +32,7 @@ func New(uri string) (Cache, error) {
)
if len(strs) != 2 {
return nil, fmt.Errorf("cache.Init: invalid cache uri: %s", uri)
return fmt.Errorf("cache.Init: invalid cache uri: %s", uri)
}
uri := strs[1]
@@ -47,7 +42,7 @@ func New(uri string) (Cache, error) {
}
if ins, err = url.Parse(uri); err != nil {
return nil, fmt.Errorf("cache.Init: url parse cache uri: %s, err: %s", uri, err.Error())
return fmt.Errorf("cache.Init: url parse cache uri: %s, err: %s", uri, err.Error())
}
addr := ins.Host
@@ -62,13 +57,13 @@ func New(uri string) (Cache, error) {
})
if err = rc.Ping(tool.Timeout(5)).Err(); err != nil {
return nil, fmt.Errorf("cache.Init: redis ping err: %s", err.Error())
return fmt.Errorf("cache.Init: redis ping err: %s", err.Error())
}
client = &_redis{client: rc}
Client = &_redis{client: rc}
default:
return nil, fmt.Errorf("cache type %s not support", strs[0])
return fmt.Errorf("cache type %s not support", strs[0])
}
return client, nil
return nil
}
+1
View File
@@ -0,0 +1 @@
package tool
-11
View File
@@ -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"`
}
-13
View File
@@ -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"`
}
-30
View File
@@ -1,30 +0,0 @@
package model
import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
"uauth/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
}
-47
View File
@@ -1,47 +0,0 @@
package model
import (
"fmt"
"strings"
)
// platform:module:class:action
// admin:*:*:*
// admin:audit:*:*
// admin:audit:flow:*
// admin:audit:flow:operate
type Privilege struct {
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"`
Code string `json:"code" gorm:"column:code;primaryKey"`
Label string `json:"label" gorm:"column:label"`
Parent string `json:"parent" gorm:"column:parent"`
Scope string `json:"scope" gorm:"column:scope"`
}
func (p *Privilege) Validate() error {
ss := strings.Split(p.Code, ":")
if len(ss) != 4 {
return fmt.Errorf("privilege must consist of four parts: (platform:module:class:action)")
}
for _, s := range ss {
if len(s) == 0 {
return fmt.Errorf("privilege code parts must not be empty")
}
}
code := strings.Clone(p.Code)
for strings.HasSuffix(code, ":*") {
code = code[:len(code)-2]
}
if code != "*" && strings.Contains(code, "*") {
return fmt.Errorf("privilege can only have trailing wildcard search")
}
return nil
}
-74
View File
@@ -1,74 +0,0 @@
package model
import (
"testing"
"uauth/internal/util"
)
func TestPrivilege_Validate(t *testing.T) {
type fields struct {
Code string
}
tests := []struct {
name string
fields fields
wantErr bool
}{
{
name: "全匹配",
fields: fields{Code: "*:*:*:*"},
wantErr: false,
},
{
name: "部分全匹配1",
fields: fields{Code: "user:ok:*:*"},
wantErr: false,
},
{
name: "部分全匹配2",
fields: fields{Code: "user:ok:nice:*"},
wantErr: false,
},
{
name: "中间全匹配1",
fields: fields{Code: "user:*:*:ok"},
wantErr: true,
},
{
name: "中间全匹配2",
fields: fields{Code: "user:*:nice:*"},
wantErr: true,
},
{
name: "精确权限",
fields: fields{Code: "user:1:2:3"},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Privilege{
Code: tt.fields.Code,
}
if err := p.Validate(); (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func BenchmarkPrivilegeValidate(b *testing.B) {
ps := make([]*Privilege, 100000)
for i := 0; i < 100000; i++ {
ps[i] = &Privilege{
Code: tool.RandomString(4) + ":" + tool.RandomString(5) + ":" + tool.RandomString(6) + ":" + tool.RandomString(7),
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ps[i%100000].Validate()
}
}
-13
View File
@@ -1,13 +0,0 @@
package model
import "uauth/pkg/sqlType"
type Role struct {
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"`
Code string `json:"code" gorm:"primaryKey;column:code"`
Label string `json:"label" gorm:"column:label"`
Parent string `json:"parent" gorm:"column:parent"`
PrivilegeCodes sqlType.StrSlice `json:"privilege_codes" gorm:"column:privilege_codes"`
}
-11
View File
@@ -1,11 +0,0 @@
package model
// 用户权限作用域
type Scope struct {
Code string `json:"code" gorm:"column:code;type:varchar(8);not null;primaryKey"`
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"`
Label string `json:"label" gorm:"column:label;type:varchar(64)"`
Parent string `json:"parent" gorm:"column:parent;type:varchar(8)"`
}
+2 -14
View File
@@ -6,15 +6,9 @@ import (
"github.com/loveuer/nf/nft/log"
"time"
"uauth/internal/opt"
"uauth/pkg/sqlType"
)
type Status string
const (
StatusActive Status = "active"
StatusDisabled Status = "disabled"
)
type Status int64
type User struct {
Id uint64 `json:"id" gorm:"primaryKey;column:id"`
@@ -25,19 +19,15 @@ type User struct {
Username string `json:"username" gorm:"column:username;type:varchar(64);unique"`
Password string `json:"-" gorm:"column:password;type:varchar(256)"`
Status Status `json:"status" gorm:"column:status;default:''"`
Status Status `json:"status" gorm:"column:status;default:0"`
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"`
CreatedById uint64 `json:"created_by_id" gorm:"column:created_by_id"`
CreatedByName string `json:"created_by_name" gorm:"column:created_by_name;type:varchar(64)"`
LoginAt int64 `json:"login_at" gorm:"-"`
Roles []*Role `json:"-" gorm:"-"`
RoleNames sqlType.StrSlice `json:"role_names" column:"role_names"`
}
func (u *User) JwtEncode() (token string, err error) {
@@ -47,9 +37,7 @@ func (u *User) JwtEncode() (token string, err error) {
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{
"id": u.Id,
"username": u.Username,
"nickname": u.Nickname,
"status": u.Status,
"avatar": u.Avatar,
"login_at": now.UnixMilli(),
})
-118
View File
@@ -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/model"
"uauth/pkg/cache"
"uauth/tool"
)
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()
}
}
-47
View File
@@ -1,47 +0,0 @@
package logger
import (
"fmt"
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/log"
"github.com/loveuer/nf/nft/resp"
"strconv"
"time"
"uauth/tool"
)
func New() nf.HandlerFunc {
return func(c *nf.Ctx) error {
var (
now = time.Now()
logFn func(msg string, data ...any)
ip = c.IP()
)
traceId := c.Context().Value(nf.TraceKey)
c.Locals(nf.TraceKey, traceId)
err := c.Next()
c.Writer.Header().Set(nf.TraceKey, fmt.Sprint(traceId))
status, _ := strconv.Atoi(c.Writer.Header().Get(resp.RealStatusHeader))
duration := time.Since(now)
msg := fmt.Sprintf("%s | %15s | %d[%3d] | %s | %6s | %s", traceId, ip, c.StatusCode, status, tool.HumanDuration(duration.Nanoseconds()), c.Method(), c.Path())
switch {
case status >= 500:
logFn = log.Error
case status >= 400:
logFn = log.Warn
default:
logFn = log.Info
}
logFn(msg)
return err
}
}
-110
View File
@@ -1,110 +0,0 @@
package rbac
import (
"context"
"fmt"
"github.com/samber/lo"
"strings"
"uauth/model"
)
func (u *RBAC) newScope(ctx context.Context, code, label, parent string) (*model.Scope, error) {
s := &model.Scope{Code: code, Label: label, Parent: parent}
if err := u.store.Session(ctx).Create(s).Error; err != nil {
return s, err
}
return s, nil
}
func (u *RBAC) GetScopeGroup(ctx context.Context, name string) (*model.Scope, error) {
scope := new(model.Scope)
err := u.store.Session(ctx).Where("name = ?", name).Take(scope).Error
return scope, err
}
func (u *RBAC) newRole(ctx context.Context, code, label, parent string, privileges ...*model.Privilege) (*model.Role, error) {
ps := lo.FilterMap(
privileges,
func(p *model.Privilege, _ int) (string, bool) {
if p == nil {
return "", false
}
return p.Code, p.Code != ""
},
)
r := &model.Role{
Code: code,
Label: label,
Parent: parent,
PrivilegeCodes: ps,
}
if err := u.store.Session(ctx).Create(r).Error; err != nil {
return r, err
}
return r, nil
}
func (u *RBAC) GetRole(ctx context.Context, name string) (*model.Role, error) {
var r model.Role
if err := u.store.Session(ctx).Take(&r, "name = ?", name).Error; err != nil {
return nil, err
}
return &r, nil
}
func (u *RBAC) newPrivilege(ctx context.Context, code, label string, parent string, scope string) (*model.Privilege, error) {
p := &model.Privilege{Code: code, Label: label, Parent: parent, Scope: scope}
codes := strings.SplitN(code, ":", 4)
if len(codes) != 4 {
return nil, fmt.Errorf("invalid code format")
}
wailcard := false
for _, item := range codes {
if item == "*" {
wailcard = true
}
if wailcard && item != "*" {
return nil, fmt.Errorf("invalid code format")
}
if len(item) > 8 {
return nil, fmt.Errorf("invalid code format: code snippet too long")
}
}
if codes[0] != "*" {
if _, err := u.GetScopeGroup(ctx, codes[0]); err != nil {
return nil, err
}
}
if err := u.store.Session(ctx).Create(p).Error; err != nil {
return p, err
}
return p, nil
}
func (u *RBAC) newUser(ctx context.Context, target *model.User) (*model.User, error) {
result := u.store.Session(ctx).
Create(target)
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected != 1 {
return nil, fmt.Errorf("invalid rows affected")
}
return target, nil
}
-83
View File
@@ -1,83 +0,0 @@
package rbac
import (
"fmt"
"strings"
"uauth/model"
"uauth/pkg/cache"
"uauth/pkg/store"
"uauth/tool"
)
type RBAC struct {
cache cache.Cache
store store.Store
}
var (
Default *RBAC
)
func New(store store.Store, cache cache.Cache) (*RBAC, error) {
var (
err error
u = &RBAC{
store: store,
cache: cache,
}
rootPrivilege *model.Privilege
rootRole *model.Role
rootScope *model.Scope
rootUser *model.User
)
if err = u.store.Session(tool.Timeout()).AutoMigrate(
&model.Scope{},
&model.Privilege{},
&model.Role{},
&model.User{},
); err != nil {
return nil, fmt.Errorf("urbac migrate err: %w", err)
}
if rootPrivilege, err = u.newPrivilege(tool.Timeout(), "*:*:*:*", "admin", "", "*"); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "unique") {
return nil, err
}
}
if rootRole, err = u.newRole(tool.Timeout(), "admin", "管理员", "", rootPrivilege); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "unique") {
return nil, err
}
}
if rootScope, err = u.newScope(tool.Timeout(), "*", "全部", ""); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "unique") {
return nil, err
}
}
rootUser = &model.User{
Username: "admin",
Password: tool.NewPassword("123456"),
Status: model.StatusActive,
Nickname: "管理员",
RoleNames: []string{rootRole.Code},
}
if _, err = u.newUser(tool.Timeout(3), rootUser); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "unique") {
return nil, err
}
}
_ = rootScope
return u, nil
}
func Init(store store.Store, cache cache.Cache) (err error) {
Default, err = New(store, cache)
return err
}
-9
View File
@@ -1,9 +0,0 @@
package sqlType
import "errors"
var (
ErrConvertScanVal = errors.New("convert scan val to str err")
ErrInvalidScanVal = errors.New("scan val invalid")
ErrConvertVal = errors.New("convert err")
)
-76
View File
@@ -1,76 +0,0 @@
package sqlType
import (
"database/sql/driver"
"encoding/json"
"github.com/jackc/pgtype"
)
type JSONB struct {
Val pgtype.JSONB
Valid bool
}
func NewJSONB(v interface{}) JSONB {
j := new(JSONB)
j.Val = pgtype.JSONB{}
if err := j.Val.Set(v); err == nil {
j.Valid = true
return *j
}
return *j
}
func (j *JSONB) Set(value interface{}) error {
if err := j.Val.Set(value); err != nil {
j.Valid = false
return err
}
j.Valid = true
return nil
}
func (j *JSONB) Bind(model interface{}) error {
return j.Val.AssignTo(model)
}
func (j *JSONB) Scan(value interface{}) error {
j.Val = pgtype.JSONB{}
if value == nil {
j.Valid = false
return nil
}
j.Valid = true
return j.Val.Scan(value)
}
func (j JSONB) Value() (driver.Value, error) {
if j.Valid {
return j.Val.Value()
}
return nil, nil
}
func (j JSONB) MarshalJSON() ([]byte, error) {
if j.Valid {
return j.Val.MarshalJSON()
}
return json.Marshal(nil)
}
func (j *JSONB) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
j.Valid = false
return j.Val.UnmarshalJSON(b)
}
return j.Val.UnmarshalJSON(b)
}
-71
View File
@@ -1,71 +0,0 @@
package sqlType
import (
"database/sql/driver"
"fmt"
"strconv"
"strings"
"github.com/spf13/cast"
)
type NumSlice[T ~int | ~int64 | ~uint | ~uint64] []T
func (n *NumSlice[T]) Scan(val interface{}) error {
str, ok := val.(string)
if !ok {
return ErrConvertScanVal
}
length := len(str)
if length <= 0 {
*n = make(NumSlice[T], 0)
return nil
}
if str[0] != '{' || str[length-1] != '}' {
return ErrInvalidScanVal
}
str = str[1 : length-1]
if len(str) == 0 {
*n = make(NumSlice[T], 0)
return nil
}
numStrs := strings.Split(str, ",")
nums := make([]T, len(numStrs))
for idx := range numStrs {
num, err := cast.ToInt64E(strings.TrimSpace(numStrs[idx]))
if err != nil {
return fmt.Errorf("%w: can't convert to %T", ErrConvertVal, T(0))
}
nums[idx] = T(num)
}
*n = nums
return nil
}
func (n NumSlice[T]) Value() (driver.Value, error) {
if n == nil {
return "{}", nil
}
if len(n) == 0 {
return "{}", nil
}
ss := make([]string, 0, len(n))
for idx := range n {
ss = append(ss, strconv.Itoa(int(n[idx])))
}
s := strings.Join(ss, ", ")
return fmt.Sprintf("{%s}", s), nil
}
-107
View File
@@ -1,107 +0,0 @@
package sqlType
import (
"bytes"
"database/sql/driver"
"encoding/json"
)
type StrSlice []string
func (s *StrSlice) Scan(val interface{}) error {
str, ok := val.(string)
if !ok {
return ErrConvertScanVal
}
if len(str) < 2 {
return nil
}
bs := make([]byte, 0, 128)
bss := make([]byte, 0, 2*len(str))
quoteCount := 0
for idx := 1; idx < len(str)-1; idx++ {
quote := str[idx]
switch quote {
case 44:
if quote == 44 && str[idx-1] != 92 && quoteCount == 0 {
if len(bs) > 0 {
if !(bs[0] == 34 && bs[len(bs)-1] == 34) {
bs = append([]byte{34}, bs...)
bs = append(bs, 34)
}
bss = append(bss, bs...)
bss = append(bss, 44)
}
bs = bs[:0]
} else {
bs = append(bs, quote)
}
case 34:
if str[idx-1] != 92 {
quoteCount = (quoteCount + 1) % 2
}
bs = append(bs, quote)
default:
bs = append(bs, quote)
}
}
if len(bs) > 0 {
if !(bs[0] == 34 && bs[len(bs)-1] == 34) {
bs = append([]byte{34}, bs...)
bs = append(bs, 34)
}
bss = append(bss, bs...)
} else {
if len(bss) > 2 {
bss = bss[:len(bss)-2]
}
}
bss = append([]byte{'['}, append(bss, ']')...)
if err := json.Unmarshal(bss, s); err != nil {
return err
}
return nil
}
func (s StrSlice) Value() (driver.Value, error) {
if s == nil {
return "{}", nil
}
if len(s) == 0 {
return "{}", nil
}
buf := &bytes.Buffer{}
encoder := json.NewEncoder(buf)
encoder.SetEscapeHTML(false)
if err := encoder.Encode(s); err != nil {
return "{}", err
}
bs := buf.Bytes()
bs[0] = '{'
if bs[len(bs)-1] == 10 {
bs = bs[:len(bs)-1]
}
bs[len(bs)-1] = '}'
return string(bs), nil
}
-46
View File
@@ -1,46 +0,0 @@
package store
import (
"context"
"uauth/tool"
"gorm.io/gorm"
)
type Store interface {
Session(ctx context.Context) *gorm.DB
}
var (
Default *Client
)
type Client struct {
ctx context.Context
cli *gorm.DB
ttype string
debug bool
}
func (c *Client) Type() string {
return c.ttype
}
func (c *Client) Session(ctx context.Context) *gorm.DB {
if ctx == nil {
ctx = tool.Timeout(30)
}
session := c.cli.Session(&gorm.Session{Context: ctx})
if c.debug {
session = session.Debug()
}
return session
}
func (c *Client) Close() {
d, _ := c.cli.DB()
d.Close()
}
-59
View File
@@ -1,59 +0,0 @@
package store
import (
"fmt"
"strings"
"github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
type Config struct {
Debug bool
}
func New(uri string, configs ...Config) (*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]}
if len(configs) > 0 && configs[0].Debug {
c.debug = true
}
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(uri string, configs ...Config) (err error) {
if Default, err = New(uri, configs...); err != nil {
return err
}
return nil
}
-30
View File
@@ -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