Compare commits

...

15 Commits

Author SHA1 Message Date
0f1c8e327e Mod permission middleware to only request database once
All checks were successful
Build Backend (NixCN CMS) TeamCity build finished
Build Frontend (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-06 10:40:48 +08:00
ddffb0da23 Add permission middleware
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-06 10:36:51 +08:00
b4d0959de4 Add EnableKYC for event table
All checks were successful
Build Backend (NixCN CMS) TeamCity build finished
Build Frontend (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-05 22:48:27 +08:00
c2fd1c5cc8 Fix missed saving file (auth/redirect service)
All checks were successful
Build Backend (NixCN CMS) TeamCity build finished
Build Frontend (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-05 22:22:12 +08:00
eddfa9a884 Remove jwt_secret from config
Some checks failed
Build Backend (NixCN CMS) TeamCity build failed
Build Frontend (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-05 22:02:28 +08:00
b0684492fa Change authcode using redis, authtoken use client secret to sign jwt
Some checks failed
Build Backend (NixCN CMS) TeamCity build failed
Build Frontend (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-05 21:59:37 +08:00
aea7fddef0 Go mod tidy
All checks were successful
Build Backend (NixCN CMS) TeamCity build finished
Build Frontend (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-05 21:31:42 +08:00
ef64c29ea7 Add Attendance state for attendance table
All checks were successful
Build Backend (NixCN CMS) TeamCity build finished
Build Frontend (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-05 16:24:35 +08:00
5f7f078f02 Add description for event table
All checks were successful
Build Backend (NixCN CMS) TeamCity build finished
Build Frontend (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-05 16:22:10 +08:00
1adfda54a6 Add AliId2MetaVerify OpenAPI pkg
All checks were successful
Build Backend (NixCN CMS) TeamCity build finished
Build Frontend (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-05 16:11:08 +08:00
3510d6c1f8 Add Aliyun Id2MetaVerify encode impl
All checks were successful
Build Backend (NixCN CMS) TeamCity build finished
Build Frontend (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-05 15:33:49 +08:00
1fa90b15c3 Add kycinfo for attendance table ane related utils
All checks were successful
Build Backend (NixCN CMS) TeamCity build finished
Build Frontend (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-05 15:06:24 +08:00
aa8e57bd89 Add user full table api
All checks were successful
Build Backend (NixCN CMS) TeamCity build finished
Build Frontend (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-05 14:36:10 +08:00
d6acae1625 Add owner to event table
All checks were successful
Build Backend (NixCN CMS) TeamCity build finished
Build Frontend (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-05 14:08:20 +08:00
8dbdb58327 Add bio base64 verification
All checks were successful
Build Backend (NixCN CMS) TeamCity build finished
Build Frontend (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-02 16:56:26 +08:00
23 changed files with 991 additions and 333 deletions

View File

@@ -28,11 +28,14 @@ email:
insecure_skip_verify:
from:
secrets:
jwt_secret: example
turnstile_secret: example
client_secret_key: example
client_secret_key: aes_32_byte_string
kyc_info_key: aes_32_byte_string
ttl:
auth_code_ttl: 10m
access_ttl: 15s
refresh_ttl: 168h
checkin_code_ttl: 10m
kyc:
ali_access_key_id: example
ali_access_key_secret: example

View File

@@ -8,6 +8,7 @@ type config struct {
Email email `yaml:"email"`
Secrets secrets `yaml:"secrets"`
TTL ttl `yaml:"ttl"`
KYC kyc `yaml:"kyc"`
}
type server struct {
@@ -50,9 +51,9 @@ type email struct {
}
type secrets struct {
JwtSecret string `yaml:"jwt_secret"`
TurnstileSecret string `yaml:"turnstile_secret"`
ClientSecretKey string `yaml:"client_secret_key"`
KycInfoKey string `yaml:"kyc_info_key"`
}
type ttl struct {
@@ -61,3 +62,8 @@ type ttl struct {
RefreshTTL string `yaml:"refresh_ttl"`
CheckinCodeTTL string `yaml:"checkin_code_ttl"`
}
type kyc struct {
AliAccessKeyId string `yaml:"ali_access_key_id"`
AliAccessKeySecret string `yaml:"ali_access_key_secret"`
}

13
data/agenda.go Normal file
View File

@@ -0,0 +1,13 @@
package data
import "github.com/google/uuid"
type Agenda struct {
Id uint `json:"id" gorm:"primarykey;autoIncrement"`
UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueIndex;not null"`
AgendaId uuid.UUID `json:"agenda_id" gorm:"type:uuid;uniqueIndex;not null"`
EventId uuid.UUID `json:"event_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"`
UserId uuid.UUID `json:"user_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"`
Name string `json:"name" gorm:"type:varchar(255);not null"`
Description string `json:"description" gorm:"type:text;not null"`
}

View File

@@ -21,6 +21,8 @@ type Attendance struct {
EventId uuid.UUID `json:"event_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"`
UserId uuid.UUID `json:"user_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"`
Role string `json:"role" gorm:"type:varchar(255);not null"`
State string `json:"state" gorm:"type:varchar(255);not null"`
KycInfo string `json:"kyc_info" gorm:"type:text"`
CheckinAt time.Time `json:"checkin_at"`
}

View File

@@ -10,20 +10,25 @@ import (
)
type Event struct {
Id uint `json:"id" gorm:"primarykey;autoincrement"`
UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueIndex;not null"`
EventId uuid.UUID `json:"event_id" gorm:"type:uuid;uniqueIndex;not null"`
Name string `json:"name" gorm:"type:varchar(255);index;not null"`
Type string `json:"type" gotm:"type:varchar(255);index;not null"`
StartTime time.Time `json:"start_time" gorm:"index"`
EndTime time.Time `json:"end_time" gorm:"index"`
Id uint `json:"id" gorm:"primarykey;autoincrement"`
UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueIndex;not null"`
EventId uuid.UUID `json:"event_id" gorm:"type:uuid;uniqueIndex;not null"`
Name string `json:"name" gorm:"type:varchar(255);index;not null"`
Type string `json:"type" gotm:"type:varchar(255);index;not null"`
Description string `json:"description" gorm:"type:text;not null"`
StartTime time.Time `json:"start_time" gorm:"index"`
EndTime time.Time `json:"end_time" gorm:"index"`
Owner uuid.UUID `json:"owner" gorm:"type:uuid;index;not null"`
EnableKYC bool `json:"enable_kyc" gorm:"not null"`
}
type EventSearchDoc struct {
EventId string `json:"event_id"`
Name string `json:"name"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
EventId string `json:"event_id"`
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
}
func (self *Event) GetEventById(eventId uuid.UUID) (*Event, error) {
@@ -124,10 +129,12 @@ func (self *Event) FastListEvents(limit, offset int64) (*[]EventSearchDoc, error
func (self *Event) UpdateSearchIndex() error {
doc := EventSearchDoc{
EventId: self.EventId.String(),
Name: self.Name,
StartTime: self.StartTime,
EndTime: self.EndTime,
EventId: self.EventId.String(),
Name: self.Name,
Type: self.Type,
Description: self.Description,
StartTime: self.StartTime,
EndTime: self.EndTime,
}
index := MeiliSearch.Index("event")

43
go.mod
View File

@@ -2,72 +2,83 @@ module nixcn-cms
go 1.25.5
require (
github.com/alibabacloud-go/cloudauth-20190307/v4 v4.12.1
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13
github.com/alibabacloud-go/tea v1.3.13
github.com/alibabacloud-go/tea-utils/v2 v2.0.7
github.com/aliyun/credentials-go v1.4.5
github.com/gin-gonic/gin v1.11.0
github.com/go-viper/mapstructure/v2 v2.4.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/meilisearch/meilisearch-go v0.35.0
github.com/redis/go-redis/v9 v9.17.2
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.21.0
golang.org/x/crypto v0.46.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gorm.io/datatypes v1.2.7
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/gin-gonic/gin v1.11.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.29.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.1 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/meilisearch/meilisearch-go v0.35.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect
github.com/redis/go-redis/v9 v9.17.2 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
gorm.io/datatypes v1.2.7 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gorm.io/driver/mysql v1.5.6 // indirect
gorm.io/driver/postgres v1.6.0 // indirect
gorm.io/gorm v1.31.1 // indirect
)

272
go.sum
View File

@@ -1,21 +1,85 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
github.com/alibabacloud-go/cloudauth-20190307/v4 v4.12.1 h1:bVUYai/CnqhFMcofU8TsJGnZEf9zCB1WakLxh0zmpjQ=
github.com/alibabacloud-go/cloudauth-20190307/v4 v4.12.1/go.mod h1:m3NrP7nlncob5/2ATBk0thsZAozpDy/+vUG4ZuXwdR8=
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 h1:Q00FU3H94Ts0ZIHDmY+fYGgB7dV9D/YX6FGsgorQPgw=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
github.com/alibabacloud-go/openapi-util v0.1.1 h1:ujGErJjG8ncRW6XtBBMphzHTvCxn4DjrVw4m04HsS28=
github.com/alibabacloud-go/openapi-util v0.1.1/go.mod h1:/UehBSE2cf1gYT43GV4E+RxTdLRzURImCYY0aRmlXpw=
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
github.com/alibabacloud-go/tea v1.3.13 h1:WhGy6LIXaMbBM6VBYcsDCz6K/TPsT1Ri2hPmmZffZ94=
github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
github.com/aliyun/credentials-go v1.4.5 h1:O76WYKgdy1oQYYiJkERjlA2dxGuvLRrzuO2ScrtGWSk=
github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
@@ -24,6 +88,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
@@ -43,9 +109,33 @@ github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXe
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -58,36 +148,56 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
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/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/meilisearch/meilisearch-go v0.35.0 h1:Gh4vO+PinVQZ58iiFdUX9Hld8uXKzKh+C7mSSsCDlI8=
github.com/meilisearch/meilisearch-go v0.35.0/go.mod h1:cUVJZ2zMqTvvwIMEEAdsWH+zrHsrLpAw6gm8Lt1MXK0=
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
github.com/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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
@@ -99,52 +209,196 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
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.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/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.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/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.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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
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.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/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/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
@@ -152,6 +406,12 @@ gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -0,0 +1,20 @@
package cryptography
import (
"encoding/base64"
"strings"
)
func IsBase64Std(s string) bool {
if s == "" {
return false
}
s = strings.TrimSpace(s)
if len(s)%4 != 0 {
return false
}
_, err := base64.StdEncoding.Strict().DecodeString(s)
return err == nil
}

View File

@@ -14,14 +14,12 @@ func JWTAuth(required bool) gin.HandlerFunc {
authtoken := new(authtoken.Token)
uid, err := authtoken.HeaderVerify(auth)
if err != nil {
c.JSON(401, gin.H{"status": err.Error()})
c.Abort()
c.AbortWithStatusJSON(401, gin.H{"status": err.Error()})
return
}
if required == true && uid == "" {
c.JSON(401, gin.H{"status": "unauthorized"})
c.Abort()
c.AbortWithStatusJSON(401, gin.H{"status": "unauthorized"})
return
}

46
middleware/permission.go Normal file
View File

@@ -0,0 +1,46 @@
package middleware
import (
"nixcn-cms/data"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func Permission(requiredLevel uint) gin.HandlerFunc {
return func(c *gin.Context) {
var permissionLevel uint
permissionLevelPrev, ok := c.Get("permission_level")
if !ok {
userIdOrig, ok := c.Get("user_id")
if !ok || userIdOrig.(string) == "" {
c.AbortWithStatusJSON(401, gin.H{"status": "missing user id"})
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
c.AbortWithStatusJSON(500, gin.H{"status": "error parsing user id"})
return
}
userData, err := new(data.User).GetByUserId(userId)
if err != nil {
c.AbortWithStatusJSON(404, gin.H{"status": "user not found"})
return
}
permissionLevel = userData.PermissionLevel
c.Set("permission_level", userData.PermissionLevel)
} else {
permissionLevel = permissionLevelPrev.(uint)
}
if permissionLevel < requiredLevel {
c.AbortWithStatusJSON(403, gin.H{"status": "permission denied"})
return
}
c.Next()
}
}

View File

@@ -1,53 +1,68 @@
package authcode
import (
"context"
"crypto/rand"
"encoding/base64"
"sync"
"time"
"nixcn-cms/data"
"github.com/spf13/viper"
)
type Token struct {
Email string
ExpiresAt time.Time
ClientId string
Email string
}
var (
store = sync.Map{}
)
func NewAuthCode(clientId string, email string) (string, error) {
ctx := context.Background()
// Generate magic token
func NewAuthCode(email string) (string, error) {
// generate random code
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
code := base64.RawURLEncoding.EncodeToString(b)
key := "auth_code:" + code
store.Store(code, Token{
Email: email,
ExpiresAt: time.Now().Add(viper.GetDuration("ttl.auth_code_ttl")),
})
ttl := viper.GetDuration("ttl.auth_code_ttl")
// store auth code metadata in Redis
if err := data.Redis.HSet(
ctx,
key,
map[string]any{
"client_id": clientId,
"email": email,
},
).Err(); err != nil {
return "", err
}
// set expiration (one-time auth code)
if err := data.Redis.Expire(ctx, key, ttl).Err(); err != nil {
return "", err
}
return code, nil
}
// Verify magic token
func VerifyAuthCode(code string) (string, bool) {
val, ok := store.Load(code)
if !ok {
return "", false
func VerifyAuthCode(code string) (*Token, bool) {
ctx := context.Background()
key := "auth_code:" + code
// Read auth code payload
dataMap, err := data.Redis.HGetAll(ctx, key).Result()
if err != nil || len(dataMap) == 0 {
return nil, false
}
t := val.(Token)
if time.Now().After(t.ExpiresAt) {
store.Delete(code)
return "", false
}
// Delete auth code immediately (one-time use)
_ = data.Redis.Del(ctx, key).Err()
store.Delete(code)
return t.Email, true
return &Token{
ClientId: dataMap["client_id"],
Email: dataMap["email"],
}, true
}

295
pkgs/authtoken/authtoken.go Normal file
View File

@@ -0,0 +1,295 @@
package authtoken
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"nixcn-cms/data"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/spf13/viper"
)
type Token struct {
Application string
}
type JwtClaims struct {
ClientId string `json:"client_id"`
UserID uuid.UUID `json:"user_id"`
jwt.RegisteredClaims
}
// Generate jwt clames
func (self *Token) NewClaims(clientId string, userId uuid.UUID) JwtClaims {
return JwtClaims{
ClientId: clientId,
UserID: userId,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(viper.GetDuration("ttl.access_ttl"))),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: self.Application,
},
}
}
// Generate access token
func (self *Token) GenerateAccessToken(clientId string, userId uuid.UUID) (string, error) {
claims := self.NewClaims(clientId, userId)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
clientData, err := new(data.Client).GetClientByClientId(clientId)
if err != nil {
return "", fmt.Errorf("error getting client data: %v", err)
}
secret, err := clientData.GetDecryptedSecret()
if err != nil {
return "", fmt.Errorf("error getting decrypted secret: %v", err)
}
signedToken, err := token.SignedString([]byte(secret))
if err != nil {
return "", fmt.Errorf("error signing token: %v", err)
}
return signedToken, nil
}
// Generate refresh token
func (self *Token) GenerateRefreshToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
// Issue both access and refresh token
func (self *Token) IssueTokens(clientId string, userId uuid.UUID) (string, string, error) {
// access token
access, err := self.GenerateAccessToken(clientId, userId)
if err != nil {
return "", "", err
}
// refresh token
refresh, err := self.GenerateRefreshToken()
if err != nil {
return "", "", err
}
ctx := context.Background()
ttl := viper.GetDuration("ttl.refresh_ttl")
refreshKey := "refresh:" + refresh
// refresh -> user + client
if err := data.Redis.HSet(
ctx,
refreshKey,
map[string]any{
"user_id": userId.String(),
"client_id": clientId,
},
).Err(); err != nil {
return "", "", err
}
if err := data.Redis.Expire(ctx, refreshKey, ttl).Err(); err != nil {
return "", "", err
}
// user -> refresh tokens
userSetKey := "user:" + userId.String() + ":refresh_tokens"
if err := data.Redis.SAdd(ctx, userSetKey, refresh).Err(); err != nil {
return "", "", err
}
_ = data.Redis.Expire(ctx, userSetKey, ttl).Err()
// client -> refresh tokens
clientSetKey := "client:" + clientId + ":refresh_tokens"
_ = data.Redis.SAdd(ctx, clientSetKey, refresh).Err()
_ = data.Redis.Expire(ctx, clientSetKey, ttl).Err()
return access, refresh, nil
}
// Refresh access token
func (self *Token) RefreshAccessToken(refreshToken string) (string, error) {
ctx := context.Background()
key := "refresh:" + refreshToken
// read refresh token bind data
dataMap, err := data.Redis.HGetAll(ctx, key).Result()
if err != nil || len(dataMap) == 0 {
return "", errors.New("invalid refresh token")
}
userIdStr := dataMap["user_id"]
clientId := dataMap["client_id"]
if userIdStr == "" || clientId == "" {
return "", errors.New("refresh token corrupted")
}
userId, err := uuid.Parse(userIdStr)
if err != nil {
return "", err
}
// Generate new access token
return self.GenerateAccessToken(clientId, userId)
}
func (self *Token) RenewRefreshToken(refreshToken string) (string, error) {
ctx := context.Background()
ttl := viper.GetDuration("ttl.refresh_ttl")
oldKey := "refresh:" + refreshToken
// read old refresh token bind data
dataMap, err := data.Redis.HGetAll(ctx, oldKey).Result()
if err != nil || len(dataMap) == 0 {
return "", errors.New("invalid refresh token")
}
userIdStr := dataMap["user_id"]
clientId := dataMap["client_id"]
if userIdStr == "" || clientId == "" {
return "", errors.New("refresh token corrupted")
}
// generate new refresh token
newRefresh, err := self.GenerateRefreshToken()
if err != nil {
return "", err
}
// revoke old refresh token
if err := self.RevokeRefreshToken(refreshToken); err != nil {
return "", err
}
newKey := "refresh:" + newRefresh
// refresh -> user + client
if err := data.Redis.HSet(
ctx,
newKey,
map[string]any{
"user_id": userIdStr,
"client_id": clientId,
},
).Err(); err != nil {
return "", err
}
if err := data.Redis.Expire(ctx, newKey, ttl).Err(); err != nil {
return "", err
}
// user -> refresh tokens
userSetKey := "user:" + userIdStr + ":refresh_tokens"
if err := data.Redis.SAdd(ctx, userSetKey, newRefresh).Err(); err != nil {
return "", err
}
_ = data.Redis.Expire(ctx, userSetKey, ttl).Err()
// client -> refresh tokens
clientSetKey := "client:" + clientId + ":refresh_tokens"
_ = data.Redis.SAdd(ctx, clientSetKey, newRefresh).Err()
_ = data.Redis.Expire(ctx, clientSetKey, ttl).Err()
return newRefresh, nil
}
func (self *Token) RevokeRefreshToken(refreshToken string) error {
ctx := context.Background()
refreshKey := "refresh:" + refreshToken
// read refresh token metadata (user_id, client_id)
dataMap, err := data.Redis.HGetAll(ctx, refreshKey).Result()
if err != nil || len(dataMap) == 0 {
// Token already revoked or not found
return nil
}
userID := dataMap["user_id"]
clientID := dataMap["client_id"]
// build index keys
userSetKey := "user:" + userID + ":refresh_tokens"
clientSetKey := "client:" + clientID + ":refresh_tokens"
// remove refresh token and all related indexes atomically
pipe := data.Redis.TxPipeline()
// remove main refresh token record
pipe.Del(ctx, refreshKey)
// remove refresh token from user's active refresh token set
pipe.SRem(ctx, userSetKey, refreshToken)
// remove refresh token from client's active refresh token set
pipe.SRem(ctx, clientSetKey, refreshToken)
_, err = pipe.Exec(ctx)
return err
}
func (self *Token) HeaderVerify(header string) (string, error) {
if header == "" {
return "", nil
}
// Split header to 2
parts := strings.SplitN(header, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return "", errors.New("invalid Authorization header format")
}
tokenStr := parts[1]
// Verify access token
claims := &JwtClaims{}
token, err := jwt.ParseWithClaims(
tokenStr,
claims,
func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method")
}
if claims.ClientId == "" {
return nil, errors.New("client_id missing in token")
}
clientData, err := new(data.Client).GetClientByClientId(claims.ClientId)
if err != nil {
return nil, fmt.Errorf("error getting client data: %v", err)
}
secret, err := clientData.GetDecryptedSecret()
if err != nil {
return nil, fmt.Errorf("error getting decrypted secret: %v", err)
}
return secret, nil
},
)
if err != nil || !token.Valid {
return "", errors.New("invalid or expired token")
}
return claims.UserID.String(), nil
}

View File

@@ -1,222 +0,0 @@
package authtoken
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"nixcn-cms/data"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/spf13/viper"
)
type Token struct {
Application string
}
type JwtClaims struct {
UserID uuid.UUID `json:"user_id"`
jwt.RegisteredClaims
}
// Generate jwt clames
func (self *Token) NewClaims(userId uuid.UUID) JwtClaims {
return JwtClaims{
UserID: userId,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(viper.GetDuration("ttl.access_ttl"))),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: self.Application,
},
}
}
// Generate access token
func (self *Token) GenerateAccessToken(userId uuid.UUID) (string, error) {
claims := self.NewClaims(userId)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
secret := viper.GetString("secrets.jwt_secret")
signedToken, err := token.SignedString([]byte(secret))
if err != nil {
return "", fmt.Errorf("error signing token: %v", err)
}
return signedToken, nil
}
// Generate refresh token
func (self *Token) GenerateRefreshToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
// Issue both access and refresh token
func (self *Token) IssueTokens(userId uuid.UUID) (string, string, error) {
// Gen atk
access, err := self.GenerateAccessToken(userId)
if err != nil {
return "", "", err
}
// Gen rtk
refresh, err := self.GenerateRefreshToken()
if err != nil {
return "", "", err
}
// Store to redis
ctx := context.Background()
ttl := viper.GetDuration("ttl.refresh_ttl")
// refresh -> user
if err := data.Redis.Set(
ctx,
"refresh:"+refresh,
userId.String(),
ttl,
).Err(); err != nil {
return "", "", err
}
// user -> refresh tokens
userSetKey := "user:" + userId.String() + ":refresh_tokens"
if err := data.Redis.SAdd(
ctx,
userSetKey,
refresh,
).Err(); err != nil {
return "", "", err
}
// set user ttl >= all refresh token
_ = data.Redis.Expire(ctx, userSetKey, ttl).Err()
return access, refresh, nil
}
// Refresh access token
func (self *Token) RefreshAccessToken(refreshToken string) (string, error) {
// Read rtk:userid from redis
ctx := context.Background()
key := "refresh:" + refreshToken
userIdStr, err := data.Redis.Get(ctx, key).Result()
if err != nil {
return "", err
}
userId, err := uuid.Parse(userIdStr)
if err != nil {
return "", err
}
// Generate access token
return self.GenerateAccessToken(userId)
}
func (self *Token) RenewRefreshToken(refreshToken string) (string, error) {
ctx := context.Background()
ttl := viper.GetDuration("ttl.refresh_ttl")
key := "refresh:" + refreshToken
userIdStr, err := data.Redis.Get(ctx, key).Result()
if err != nil {
return "", err
}
refresh, err := self.GenerateRefreshToken()
if err != nil {
return "", err
}
err = self.RevokeRefreshToken(refreshToken)
if err != nil {
return "", err
}
// refresh -> user
if err := data.Redis.Set(
ctx,
"refresh:"+refresh,
userIdStr,
ttl,
).Err(); err != nil {
return "", err
}
// user -> refresh tokens
userSetKey := "user:" + userIdStr + ":refresh_tokens"
if err := data.Redis.SAdd(
ctx,
userSetKey,
refresh,
).Err(); err != nil {
return "", err
}
// set user ttl >= all refresh token
_ = data.Redis.Expire(ctx, userSetKey, ttl).Err()
return refresh, nil
}
func (self *Token) RevokeRefreshToken(refreshToken string) error {
ctx := context.Background()
key := "refresh:" + refreshToken
userIDStr, err := data.Redis.Get(ctx, key).Result()
if err != nil {
return nil
}
userSetKey := "user:" + userIDStr + ":refresh_tokens"
// Delete rtk from redis
pipe := data.Redis.TxPipeline()
pipe.Del(ctx, key) // rtk:userid index
pipe.SRem(ctx, userSetKey, refreshToken) // userid:rtk index
_, err = pipe.Exec(ctx)
return err
}
func (self *Token) HeaderVerify(header string) (string, error) {
if header == "" {
return "", nil
}
jwtSecret := []byte(viper.GetString("secrets.jwt_secret"))
// Split header to 2
parts := strings.SplitN(header, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return "", errors.New("invalid Authorization header format")
}
tokenStr := parts[1]
// Verify access token
claims := &JwtClaims{}
token, err := jwt.ParseWithClaims(
tokenStr,
claims,
func(token *jwt.Token) (any, error) {
return jwtSecret, nil
},
)
if err != nil || !token.Valid {
return "", errors.New("invalid or expired token")
}
return claims.UserID.String(), nil
}

168
pkgs/kyc/kyc.go Normal file
View File

@@ -0,0 +1,168 @@
package kyc
import (
"crypto/md5"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"nixcn-cms/internal/cryptography"
"unicode/utf8"
alicloudauth20190307 "github.com/alibabacloud-go/cloudauth-20190307/v4/client"
aliopenapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
aliutil "github.com/alibabacloud-go/tea-utils/v2/service"
alitea "github.com/alibabacloud-go/tea/tea"
alicredential "github.com/aliyun/credentials-go/credentials"
"github.com/spf13/viper"
)
func DecodeB64Json(b64Json string) (*KycInfo, error) {
rawJson, err := base64.StdEncoding.DecodeString(b64Json)
if err != nil {
return nil, errors.New("invalid base64 json")
}
var kyc KycInfo
if err := json.Unmarshal(rawJson, &kyc); err != nil {
return nil, errors.New("invalid json structure")
}
return &kyc, nil
}
func EncodeAES(kyc *KycInfo) (*string, error) {
plainJson, err := json.Marshal(kyc)
if err != nil {
return nil, err
}
aesKey := viper.GetString("secrets.kyc_info_key")
encrypted, err := cryptography.AESCBCEncrypt(plainJson, []byte(aesKey))
if err != nil {
return nil, err
}
return &encrypted, nil
}
func DecodeAES(cipherStr string) (*KycInfo, error) {
aesKey := viper.GetString("secrets.kyc_info_key")
plainBytes, err := cryptography.AESCBCDecrypt(cipherStr, []byte(aesKey))
if err != nil {
return nil, err
}
var kyc KycInfo
if err := json.Unmarshal(plainBytes, &kyc); err != nil {
return nil, errors.New("invalid decrypted json")
}
return &kyc, nil
}
func MD5AliEnc(kyc *KycInfo) (*KycAli, error) {
if kyc.Type != "Chinese" {
return nil, nil
}
// MD5 Legal Name rule: First Chinese char md5enc, remaining plain, at least 2 Chinese chars
if len(kyc.LegalName) < 2 || utf8.RuneCountInString(kyc.LegalName) < 2 {
return nil, fmt.Errorf("input string must have at least 2 Chinese characters")
}
lnFirstRune, size := utf8.DecodeRuneInString(kyc.LegalName)
if lnFirstRune == utf8.RuneError {
return nil, fmt.Errorf("invalid first character")
}
lnHash := md5.New()
lnHash.Write([]byte(string(lnFirstRune)))
lnFirstHash := hex.EncodeToString(lnHash.Sum(nil))
lnRemaining := kyc.LegalName[size:]
ln := lnFirstHash + lnRemaining
// MD5 Resident Id rule: First 6 char plain, middle birthdate md5enc, last 4 char plain, at least 18 chars
if len(kyc.ResidentId) < 18 {
return nil, fmt.Errorf("input string must have at least 18 characters")
}
ridPrefix := kyc.ResidentId[:6]
ridSuffix := kyc.ResidentId[len(kyc.ResidentId)-4:]
ridMiddle := kyc.ResidentId[6 : len(kyc.ResidentId)-4]
ridHash := md5.New()
ridHash.Write([]byte(ridMiddle))
ridMiddleHash := hex.EncodeToString(ridHash.Sum(nil))
rid := ridPrefix + ridMiddleHash + ridSuffix
// Aliyun Id2MetaVerify API Params
var kycAli KycAli
kycAli.ParamType = "md5"
kycAli.UserName = ln
kycAli.IdentifyNum = rid
return &kycAli, nil
}
func AliId2MetaVerify(kycAli *KycAli) (*string, error) {
// Create aliyun openapi credential
credentialConfig := new(alicredential.Config).
SetType("access_key").
SetAccessKeyId(viper.GetString("kyc.ali_access_key_id")).
SetAccessKeySecret(viper.GetString("kyc.ali_access_key_secret"))
credential, err := alicredential.NewCredential(credentialConfig)
if err != nil {
return nil, err
}
// Create aliyun cloudauth client
config := &aliopenapi.Config{
Credential: credential,
}
config.Endpoint = alitea.String("cloudauth.aliyuncs.com")
client := &alicloudauth20190307.Client{}
client, err = alicloudauth20190307.NewClient(config)
if err != nil {
return nil, err
}
// Create Id2MetaVerify request
id2MetaVerifyRequest := &alicloudauth20190307.Id2MetaVerifyRequest{
ParamType: &kycAli.ParamType,
UserName: &kycAli.UserName,
IdentifyNum: &kycAli.IdentifyNum,
}
// Create client runtime request
runtime := &aliutil.RuntimeOptions{}
resp, tryErr := func() (*alicloudauth20190307.Id2MetaVerifyResponse, error) {
defer func() {
if r := alitea.Recover(recover()); r != nil {
err = r
}
}()
resp, err := client.Id2MetaVerifyWithOptions(id2MetaVerifyRequest, runtime)
if err != nil {
return nil, err
}
return resp, nil
}()
// Try error handler ??? from ali generated sdk
if tryErr != nil {
var error = &alitea.SDKError{}
if t, ok := tryErr.(*alitea.SDKError); ok {
error = t
} else {
error.Message = alitea.String(tryErr.Error())
}
return nil, error
}
return resp.Body.ResultObject.BizCode, err
}

13
pkgs/kyc/types.go Normal file
View File

@@ -0,0 +1,13 @@
package kyc
type KycInfo struct {
Type string `json:"type"` // Chinese / Foreigner
LegalName string `json:"legal_name"`
ResidentId string `json:"rsident_id"`
}
type KycAli struct {
ParamType string `json:"param_type"`
IdentifyNum string `json:"identify_num"`
UserName string `json:"user_name"`
}

View File

@@ -33,7 +33,7 @@ func Magic(c *gin.Context) {
return
}
code, err := authcode.NewAuthCode(req.Email)
code, err := authcode.NewAuthCode(req.ClientId, req.Email)
if err != nil {
c.JSON(500, gin.H{"status": "code gen failed"})
}

View File

@@ -50,7 +50,7 @@ func Redirect(c *gin.Context) {
return
}
code, err := authcode.NewAuthCode(user.Email)
code, err := authcode.NewAuthCode(clientId, user.Email)
if err != nil {
c.JSON(500, gin.H{"status": "code gen failed"})
return
@@ -69,7 +69,7 @@ func Redirect(c *gin.Context) {
}
// Verify email token
email, ok := authcode.VerifyAuthCode(code)
authCode, ok := authcode.VerifyAuthCode(code)
if !ok {
c.JSON(403, gin.H{"status": "invalid or expired token"})
return
@@ -77,14 +77,14 @@ func Redirect(c *gin.Context) {
// Verify if user exists
userData := new(data.User)
user, err := userData.GetByEmail(email)
user, err := userData.GetByEmail(authCode.Email)
if err != nil {
if err == gorm.ErrRecordNotFound {
// Create user
user.UUID = uuid.New()
user.UserId = uuid.New()
user.Email = email
user.Email = authCode.Email
user.PermissionLevel = 10
if err := user.Create(); err != nil {
c.JSON(500, gin.H{"status": "internal server error"})
@@ -109,7 +109,7 @@ func Redirect(c *gin.Context) {
return
}
newCode, err := authcode.NewAuthCode(email)
newCode, err := authcode.NewAuthCode(clientId, authCode.Email)
if err != nil {
c.JSON(500, gin.H{"status": "internal server error"})
return

View File

@@ -22,14 +22,14 @@ func Token(c *gin.Context) {
return
}
email, ok := authcode.VerifyAuthCode(req.Code)
authCode, ok := authcode.VerifyAuthCode(req.Code)
if !ok {
c.JSON(403, gin.H{"status": "invalid or expired token"})
return
}
userData := new(data.User)
user, err := userData.GetByEmail(email)
user, err := userData.GetByEmail(authCode.Email)
if err != nil {
c.JSON(500, gin.H{"status": "internal server error"})
return
@@ -39,7 +39,7 @@ func Token(c *gin.Context) {
JwtTool := authtoken.Token{
Application: viper.GetString("server.application"),
}
accessToken, refreshToken, err := JwtTool.IssueTokens(user.UserId)
accessToken, refreshToken, err := JwtTool.IssueTokens(authCode.ClientId, user.UserId)
if err != nil {
c.JSON(500, gin.H{"status": "error generating tokens"})
return

View File

@@ -43,29 +43,13 @@ func Checkin(c *gin.Context) {
}
func CheckinSubmit(c *gin.Context) {
userIdOrig, ok := c.Get("user_id")
if userIdOrig.(string) == "" || !ok {
c.JSON(401, gin.H{"status": "unauthorized"})
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
c.JSON(500, gin.H{"status": "failed to parse uuid"})
}
userData := new(data.User)
userData.GetByUserId(userId)
if userData.PermissionLevel <= 20 {
c.JSON(403, gin.H{"status": "access denied"})
return
}
var req struct {
ChekinCode string `json:"checkin_code"`
}
c.ShouldBindJSON(&req)
attendanceData := new(data.Attendance)
err = attendanceData.VerifyCheckinCode(req.ChekinCode)
err := attendanceData.VerifyCheckinCode(req.ChekinCode)
if err != nil {
c.JSON(400, gin.H{"status": "error verify checkin code"})
return

View File

@@ -1 +1,35 @@
package user
import (
"nixcn-cms/data"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func Full(c *gin.Context) {
userIdOrig, ok := c.Get("user_id")
if !ok {
c.JSON(403, gin.H{"status": "userid error"})
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
c.JSON(500, gin.H{"status": "failed to parse uuid"})
return
}
userData, err := new(data.User).GetByUserId(userId)
if err != nil {
c.JSON(404, gin.H{"status": "user not found"})
return
}
data, err := userData.GetFullTable()
if err != nil {
c.JSON(500, gin.H{"status": "database error"})
return
}
c.JSON(200, gin.H{"user_table": data})
}

View File

@@ -7,11 +7,12 @@ import (
)
func Handler(r *gin.RouterGroup) {
r.Use(middleware.JWTAuth(true))
r.Use(middleware.JWTAuth(true), middleware.Permission(10))
r.GET("/info", Info)
r.GET("/checkin", Checkin)
r.POST("/checkin/submit", CheckinSubmit)
r.POST("/checkin/submit", CheckinSubmit, middleware.Permission(20))
r.PATCH("/update", Update)
r.GET("/list", List)
r.GET("/query", Query)
r.POST("/full", Full, middleware.Permission(50))
}

View File

@@ -8,8 +8,6 @@ import (
)
func List(c *gin.Context) {
data := new(data.User)
// Get limit and offset from query
limit, ok := c.GetQuery("limit")
if !ok {
@@ -34,7 +32,7 @@ func List(c *gin.Context) {
}
// Get user list from search engine
list, err := data.FastListUsers(limitNum, offsetNum)
list, err := new(data.User).FastListUsers(limitNum, offsetNum)
if err != nil {
c.JSON(500, gin.H{"status": "failed list users from meilisearch"})
}

View File

@@ -2,6 +2,7 @@ package user
import (
"nixcn-cms/data"
"nixcn-cms/internal/cryptography"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -9,7 +10,6 @@ import (
func Update(c *gin.Context) {
// New user model
user := new(data.User)
userIdOrig, ok := c.Get("user_id")
if !ok {
c.JSON(403, gin.H{"status": "userid error"})
@@ -18,27 +18,33 @@ func Update(c *gin.Context) {
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
c.JSON(500, gin.H{"status": "failed to parse uuid"})
return
}
var ReqInfo data.User
c.BindJSON(&ReqInfo)
// Get user info
user.GetByUserId(userId)
// Reject permission 0 user
if user.PermissionLevel == 0 {
c.JSON(403, gin.H{"status": "premission denied"})
userData, err := new(data.User).GetByUserId(userId)
if err != nil {
c.JSON(500, gin.H{"status": "failed to find user"})
return
}
user.Avatar = ReqInfo.Avatar
user.Email = ReqInfo.Email
user.Nickname = ReqInfo.Nickname
user.Subtitle = ReqInfo.Subtitle
userData.Avatar = ReqInfo.Avatar
userData.Email = ReqInfo.Email
userData.Nickname = ReqInfo.Nickname
userData.Subtitle = ReqInfo.Subtitle
if ReqInfo.Bio != "" {
if !cryptography.IsBase64Std(ReqInfo.Bio) {
c.JSON(400, gin.H{"status": "invalid base64"})
}
}
userData.Bio = ReqInfo.Bio
// Update user info
user.UpdateByUserID(userId)
userData.UpdateByUserID(userId)
c.JSON(200, gin.H{"status": "success"})
}