Compare commits

22 Commits

Author SHA1 Message Date
67e22eb793 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-08 00:52:35 +08:00
aaedddfd2f Add Exchange SMTP Oauth2 Support (not verified)
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-07 18:32:27 +08:00
f8a3d0ca45 Remove some useless comments
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 15:19:54 +08:00
6a9c013799 Use utils.HttpResponse/Abort to replace c.JSON/Abort
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 12:49:55 +08:00
70846e0d1e Reorder checkin api location (move to event)
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 11:36:08 +08:00
0710ffce72 Tune permission level
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 11:27:23 +08:00
9e840901d1 Tune user API permission level
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-01-06 11:19:54 +08:00
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
34 changed files with 1565 additions and 565 deletions

View File

@@ -27,12 +27,21 @@ email:
security:
insecure_skip_verify:
from:
auth:
oauth2:
tenant_id:
client_id:
client_secret:
scope:
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 {
@@ -39,20 +40,29 @@ type search struct {
ApiKey string `yaml:"api_key"`
}
type _email_oauth2 struct {
Tenantid string `yaml:"tenant_id"`
ClientId string `yaml:"client_id"`
ClientSecret string `yaml:"client_secret"`
Scope string `yaml:"scope"`
}
type email struct {
Host string `yaml:"host"`
Port string `yaml:"port"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Security string `yaml:"security"`
InsecureSkipVerify bool `yaml:"insecure_skip_verify"`
From string `yaml:"from"`
Host string `yaml:"host"`
Port string `yaml:"port"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Security string `yaml:"security"`
InsecureSkipVerify bool `yaml:"insecure_skip_verify"`
From string `yaml:"from"`
Auth string `yaml:"auth"`
Oauth2 _email_oauth2 `yaml:"oauth2"`
}
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 +71,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")

View File

@@ -7,32 +7,28 @@ import (
"gorm.io/gorm"
)
// Permission Level
// Banned User: 0
// Normal User: 10
// Admin User: 20
// Super User: 30
type User struct {
Id uint `json:"id" gorm:"primarykey;autoincrement"`
UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueindex;not null"`
UserId uuid.UUID `json:"user_id" gorm:"type:uuid;uniqueindex;not null"`
Email string `json:"email" gorm:"type:varchar(255);uniqueindex;not null"`
Nickname string `json:"nickname"`
Subtitle string `json:"subtitle"`
Avatar string `json:"avatar"`
Username string `json:"username" gorm:"type:varchar(255);uniqueindex;not null"`
Nickname string `json:"nickname" gorm:"type:text"`
Subtitle string `json:"subtitle" gorm:"type:text"`
Avatar string `json:"avatar" gorm:"type:text"`
Bio string `json:"bio" gorm:"type:text"`
PermissionLevel uint `json:"permission_level" gorm:"default:10;not null"`
AllowPublic bool `json:"allow_public" gorm:"default:false;not null"`
}
type UserSearchDoc struct {
UserId string `json:"user_id"`
Email string `json:"email"`
Type string `json:"type"`
Nickname string `json:"nickname"`
Subtitle string `json:"subtitle"`
Avatar string `json:"avatar"`
PermissionLevel uint `json:"permission_level"`
UserId string `json:"user_id"`
Email string `json:"email"`
Username string `json:"username"`
Type string `json:"type"`
Nickname string `json:"nickname"`
Subtitle string `json:"subtitle"`
Avatar string `json:"avatar"`
}
func (self *User) GetByEmail(email string) (*User, error) {
@@ -132,12 +128,12 @@ func (self *User) FastListUsers(limit, offset int64) (*[]UserSearchDoc, error) {
func (self *User) UpdateSearchIndex() error {
doc := UserSearchDoc{
UserId: self.UserId.String(),
Email: self.Email,
Nickname: self.Nickname,
Subtitle: self.Subtitle,
Avatar: self.Avatar,
PermissionLevel: self.PermissionLevel,
UserId: self.UserId.String(),
Email: self.Email,
Username: self.Username,
Nickname: self.Nickname,
Subtitle: self.Subtitle,
Avatar: self.Avatar,
}
index := MeiliSearch.Index("user")

44
go.mod
View File

@@ -2,72 +2,84 @@ 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
golang.org/x/oauth2 v0.34.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
)

274
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,198 @@ 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/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
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 +408,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

@@ -2,6 +2,7 @@ package middleware
import (
"nixcn-cms/pkgs/authtoken"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
)
@@ -14,14 +15,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()
utils.HttpAbort(c, 401, "", "unauthorized")
return
}
if required == true && uid == "" {
c.JSON(401, gin.H{"status": "unauthorized"})
c.Abort()
utils.HttpAbort(c, 401, "", "unauthorized")
return
}

47
middleware/permission.go Normal file
View File

@@ -0,0 +1,47 @@
package middleware
import (
"nixcn-cms/data"
"nixcn-cms/utils"
"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) == "" {
utils.HttpAbort(c, 401, "", "missing user id")
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
utils.HttpAbort(c, 500, "", "error parsing user id")
return
}
userData, err := new(data.User).GetByUserId(userId)
if err != nil {
utils.HttpAbort(c, 404, "", "user not found")
return
}
permissionLevel = userData.PermissionLevel
c.Set("permission_level", userData.PermissionLevel)
} else {
permissionLevel = permissionLevelPrev.(uint)
}
if permissionLevel < requiredLevel {
utils.HttpAbort(c, 403, "", "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
}

View File

@@ -1,18 +1,67 @@
package email
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/smtp"
"strings"
"sync"
"time"
"github.com/spf13/viper"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
gomail "gopkg.in/gomail.v2"
)
type Client struct {
// basic smtp
dialer *gomail.Dialer
from string
// shared
from string
host string
port int
username string
security string
insecure bool
// auth mode
authMode string
// oauth2
oauth *oauthTokenProvider
}
type oauthTokenProvider struct {
cfg clientcredentials.Config
mu sync.Mutex
token *oauth2.Token
fetchErr error
}
func (p *oauthTokenProvider) getToken(ctx context.Context) (string, error) {
p.mu.Lock()
defer p.mu.Unlock()
if p.token != nil && p.token.Valid() && time.Until(p.token.Expiry) > 60*time.Second {
return p.token.AccessToken, nil
}
tok, err := p.cfg.Token(ctx)
if err != nil {
p.fetchErr = err
return "", err
}
p.token = tok
p.fetchErr = nil
return tok.AccessToken, nil
}
func NewSMTPClient() (*Client, error) {
@@ -25,46 +74,240 @@ func NewSMTPClient() (*Client, error) {
security := strings.ToLower(viper.GetString("email.security"))
insecure := viper.GetBool("email.insecure_skip_verify")
if host == "" || port == 0 || user == "" || pass == "" {
authMode := strings.ToLower(viper.GetString("email.auth"))
if authMode == "" {
authMode = "basic"
}
if host == "" || port == 0 || user == "" {
return nil, errors.New("SMTP config not set")
}
dialer := gomail.NewDialer(host, port, user, pass)
dialer.TLSConfig = &tls.Config{
ServerName: host,
InsecureSkipVerify: insecure,
c := &Client{
from: from,
host: host,
port: port,
username: user,
security: security,
insecure: insecure,
authMode: authMode,
}
switch security {
case "ssl":
dialer.SSL = true
case "starttls":
dialer.SSL = false
case "plain", "":
dialer.SSL = false
dialer.TLSConfig = nil
switch authMode {
case "basic":
if pass == "" {
return nil, errors.New("SMTP basic auth requires email.password")
}
dialer := gomail.NewDialer(host, port, user, pass)
dialer.TLSConfig = &tls.Config{
ServerName: host,
InsecureSkipVerify: insecure,
}
switch security {
case "ssl":
dialer.SSL = true
case "starttls":
dialer.SSL = false
case "plain", "":
dialer.SSL = false
dialer.TLSConfig = nil
default:
return nil, errors.New("unknown smtp security mode: " + security)
}
c.dialer = dialer
return c, nil
case "oauth2":
if security == "" {
security = "starttls"
c.security = "starttls"
}
if security == "plain" {
return nil, errors.New("oauth2 requires TLS (starttls or ssl); plain is not allowed")
}
tenantID := viper.GetString("email.oauth2.tenant_id")
clientID := viper.GetString("email.oauth2.client_id")
clientSecret := viper.GetString("email.oauth2.client_secret")
scope := viper.GetString("email.oauth2.scope")
if scope == "" {
// Microsoft Learn: client credentials for SMTP uses https://outlook.office365.com/.default :contentReference[oaicite:3]{index=3}
scope = "https://outlook.office365.com/.default"
}
if tenantID == "" || clientID == "" || clientSecret == "" {
return nil, errors.New("oauth2 requires email.oauth2.tenant_id/client_id/client_secret")
}
c.oauth = &oauthTokenProvider{
cfg: clientcredentials.Config{
ClientID: clientID,
ClientSecret: clientSecret,
TokenURL: fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantID),
Scopes: []string{scope},
},
}
return c, nil
default:
return nil, errors.New("unknown smtp security mode: " + security)
return nil, errors.New("unknown email.auth: " + authMode)
}
return &Client{
dialer: dialer,
from: from,
}, nil
}
func (c *Client) Send(to, subject, html string) (string, error) {
m := gomail.NewMessage()
m.SetHeader("From", c.from)
m.SetHeader("To", to)
m.SetHeader("Subject", subject)
m.SetBody("text/html", html)
if err := c.dialer.DialAndSend(m); err != nil {
return "", err
switch c.authMode {
case "basic":
if c.dialer == nil {
return "", errors.New("basic dialer not initialized")
}
if err := c.dialer.DialAndSend(m); err != nil {
return "", err
}
return time.Now().Format(time.RFC3339Nano), nil
case "oauth2":
if err := c.sendWithXOAUTH2(m, to); err != nil {
return "", err
}
return time.Now().Format(time.RFC3339Nano), nil
default:
return "", errors.New("unsupported auth mode: " + c.authMode)
}
}
// XOAUTH2 auth for net/smtp
type xoauth2Auth struct {
username string
token string
}
func (a *xoauth2Auth) Start(server *smtp.ServerInfo) (string, []byte, error) {
if !server.TLS {
return "", nil, errors.New("refusing to authenticate over insecure connection")
}
return time.Now().Format(time.RFC3339Nano), nil
// Microsoft Learn XOAUTH2 Format: user=<user>\x01auth=Bearer <token>\x01\x01 :contentReference[oaicite:4]{index=4}
resp := fmt.Sprintf("user=%s\x01auth=Bearer %s\x01\x01", a.username, a.token)
return "XOAUTH2", []byte(resp), nil
}
func (a *xoauth2Auth) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
return nil, errors.New("unexpected server challenge during XOAUTH2 auth")
}
return nil, nil
}
func (c *Client) sendWithXOAUTH2(m *gomail.Message, rcpt string) error {
if c.oauth == nil {
return errors.New("oauth2 provider not initialized")
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
token, err := c.oauth.getToken(ctx)
if err != nil {
return fmt.Errorf("oauth2 token error: %w", err)
}
// write gomail.Message to RFC822
var buf bytes.Buffer
if _, err := m.WriteTo(&buf); err != nil {
return err
}
msg := buf.Bytes()
addr := fmt.Sprintf("%s:%d", c.host, c.port)
tlsCfg := &tls.Config{
ServerName: c.host,
InsecureSkipVerify: c.insecure,
}
var (
conn net.Conn
cl *smtp.Client
)
switch c.security {
case "ssl":
conn, err = tls.Dial("tcp", addr, tlsCfg)
if err != nil {
return err
}
cl, err = smtp.NewClient(conn, c.host)
if err != nil {
_ = conn.Close()
return err
}
case "starttls", "":
conn, err = net.Dial("tcp", addr)
if err != nil {
return err
}
cl, err = smtp.NewClient(conn, c.host)
if err != nil {
_ = conn.Close()
return err
}
// Upgrade with STARTTLS
if ok, _ := cl.Extension("STARTTLS"); ok {
if err := cl.StartTLS(tlsCfg); err != nil {
_ = cl.Close()
return err
}
} else {
_ = cl.Close()
return errors.New("server does not support STARTTLS")
}
default:
return errors.New("unknown smtp security mode: " + c.security)
}
defer func() { _ = cl.Quit() }()
// AUTH XOAUTH2
if err := cl.Auth(&xoauth2Auth{username: c.username, token: token}); err != nil {
return err
}
// MAIL FROM / RCPT TO / DATA
if err := cl.Mail(extractAddress(c.from)); err != nil {
return err
}
if err := cl.Rcpt(rcpt); err != nil {
return err
}
w, err := cl.Data()
if err != nil {
return err
}
if _, err := w.Write(msg); err != nil {
_ = w.Close()
return err
}
return w.Close()
}
func extractAddress(from string) string {
if i := strings.LastIndex(from, "<"); i >= 0 {
if j := strings.LastIndex(from, ">"); j > i {
return strings.TrimSpace(from[i+1 : j])
}
}
return strings.TrimSpace(from)
}

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

@@ -9,6 +9,6 @@ import (
func Handler(r *gin.RouterGroup) {
r.GET("/redirect", Redirect, middleware.JWTAuth(false))
r.POST("/magic", Magic)
r.POST("/refresh", Refresh)
r.POST("/token", Token)
r.POST("/refresh", Refresh)
}

View File

@@ -5,6 +5,7 @@ import (
"nixcn-cms/pkgs/authcode"
"nixcn-cms/pkgs/email"
"nixcn-cms/pkgs/turnstile"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
@@ -22,26 +23,28 @@ func Magic(c *gin.Context) {
// Parse request
var req MagicRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "invalid request"})
utils.HttpResponse(c, 400, "", "invalid request")
return
}
// Cloudflare turnstile
ok, err := turnstile.VerifyTurnstile(req.TurnstileToken, c.ClientIP())
if err != nil || !ok {
c.JSON(403, gin.H{"error": "turnstile failed"})
utils.HttpResponse(c, 403, "", "turnstile failed")
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"})
utils.HttpResponse(c, 500, "", "code gen failed")
return
}
externalUrl := viper.GetString("server.external_url")
url, err := url.Parse(externalUrl)
if err != nil {
c.JSON(500, gin.H{"status": "invalid external url"})
utils.HttpResponse(c, 500, "", "invalid external url")
return
}
url.Path = "/api/v1/auth/redirect"
@@ -54,13 +57,17 @@ func Magic(c *gin.Context) {
debugMode := viper.GetBool("server.debug_mode")
if debugMode {
c.JSON(200, gin.H{"status": "magiclink sent", "uri": url.String()})
uriData := struct {
Uri string `json:"uri"`
}{url.String()}
utils.HttpResponse(c, 200, "", "magiclink sent", uriData)
return
} else {
// Send email using resend
emailClient, err := email.NewSMTPClient()
if err != nil {
c.JSON(500, gin.H{"status": "invalid email config"})
utils.HttpResponse(c, 500, "", "invalid email config")
return
}
emailClient.Send(
@@ -70,5 +77,5 @@ func Magic(c *gin.Context) {
)
}
c.JSON(200, gin.H{"status": "magic link sent"})
utils.HttpResponse(c, 200, "", "magic link sent")
}

View File

@@ -4,6 +4,7 @@ import (
"net/url"
"nixcn-cms/data"
"nixcn-cms/pkgs/authcode"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -13,19 +14,19 @@ import (
func Redirect(c *gin.Context) {
clientId := c.Query("client_id")
if clientId == "" {
c.JSON(400, gin.H{"status": "invalid request"})
utils.HttpResponse(c, 400, "", "invalid request")
return
}
redirectUri := c.Query("redirect_uri")
if redirectUri == "" {
c.JSON(400, gin.H{"status": "invalid request"})
utils.HttpResponse(c, 400, "", "invalid request")
return
}
state := c.Query("state")
if state == "" {
c.JSON(400, gin.H{"status": "invalid request"})
utils.HttpResponse(c, 400, "", "invalid request")
return
}
@@ -33,32 +34,32 @@ func Redirect(c *gin.Context) {
if code == "" {
userIdOrig, ok := c.Get("user_id")
if !ok || userIdOrig == "" {
c.JSON(401, gin.H{"status": "unauthorized"})
utils.HttpResponse(c, 401, "", "unauthorized")
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
c.JSON(500, gin.H{"status": "failed to parse uuid"})
utils.HttpResponse(c, 500, "", "failed to parse uuid")
return
}
userData := new(data.User)
user, err := userData.GetByUserId(userId)
if err != nil {
c.JSON(500, gin.H{"status": "failed to get user id"})
utils.HttpResponse(c, 500, "", "failed to get user id")
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"})
utils.HttpResponse(c, 500, "", "code gen failed")
return
}
url, err := url.Parse(redirectUri)
if err != nil {
c.JSON(400, gin.H{"status": "invalid redirect uri"})
utils.HttpResponse(c, 400, "", "invalid redirect uri")
return
}
query := url.Query()
@@ -69,29 +70,30 @@ 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"})
utils.HttpResponse(c, 403, "", "invalid or expired token")
return
}
// 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.Username = user.UserId.String()
user.PermissionLevel = 10
if err := user.Create(); err != nil {
c.JSON(500, gin.H{"status": "internal server error"})
utils.HttpResponse(c, 500, "", "internal server error")
return
}
} else {
c.JSON(500, gin.H{"status": "internal server error"})
utils.HttpResponse(c, 500, "", "internal server error")
return
}
}
@@ -99,25 +101,25 @@ func Redirect(c *gin.Context) {
clientData := new(data.Client)
client, err := clientData.GetClientByClientId(clientId)
if err != nil {
c.JSON(400, gin.H{"status": "client not found"})
utils.HttpResponse(c, 400, "", "client not found")
return
}
err = client.ValidateRedirectURI(redirectUri)
if err != nil {
c.JSON(400, gin.H{"status": "redirect uri not match"})
utils.HttpResponse(c, 400, "", "redirect uri not match")
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"})
utils.HttpResponse(c, 500, "", "internal server error")
return
}
url, err := url.Parse(redirectUri)
if err != nil {
c.JSON(400, gin.H{"status": "invalid redirect uri"})
utils.HttpResponse(c, 400, "", "invalid redirect uri")
return
}
query := url.Query()

View File

@@ -2,6 +2,7 @@ package auth
import (
"nixcn-cms/pkgs/authtoken"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
@@ -13,7 +14,7 @@ func Refresh(c *gin.Context) {
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"status": "invalid request"})
utils.HttpResponse(c, 400, "", "invalid request")
return
}
@@ -21,20 +22,22 @@ func Refresh(c *gin.Context) {
Application: viper.GetString("server.application"),
}
access, err := JwtTool.RefreshAccessToken(req.RefreshToken)
accessToken, err := JwtTool.RefreshAccessToken(req.RefreshToken)
if err != nil {
c.JSON(401, gin.H{"status": "invalid refresh token"})
utils.HttpResponse(c, 401, "", "invalid refresh token")
return
}
refresh, err := JwtTool.RenewRefreshToken(req.RefreshToken)
refreshToken, err := JwtTool.RenewRefreshToken(req.RefreshToken)
if err != nil {
c.JSON(500, gin.H{"statis": "error renew refresh token"})
utils.HttpResponse(c, 500, "", "error renew refresh token")
return
}
c.JSON(200, gin.H{
"access_token": access,
"refresh_token": refresh,
})
tokenResp := struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}{accessToken, refreshToken}
utils.HttpResponse(c, 200, "", "success", tokenResp)
}

View File

@@ -4,6 +4,7 @@ import (
"nixcn-cms/data"
"nixcn-cms/pkgs/authcode"
"nixcn-cms/pkgs/authtoken"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
@@ -18,20 +19,20 @@ func Token(c *gin.Context) {
err := c.ShouldBindJSON(&req)
if err != nil {
c.JSON(400, gin.H{"status": "invalid request"})
utils.HttpResponse(c, 400, "", "invalid request")
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"})
utils.HttpResponse(c, 403, "", "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"})
utils.HttpResponse(c, 500, "", "internal server error")
return
}
@@ -39,14 +40,16 @@ 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"})
utils.HttpResponse(c, 500, "", "error generating tokens")
return
}
c.JSON(200, gin.H{
"access_token": accessToken,
"refresh_token": refreshToken,
})
tokenResp := struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}{accessToken, refreshToken}
utils.HttpResponse(c, 200, "", "success", tokenResp)
}

108
service/event/checkin.go Normal file
View File

@@ -0,0 +1,108 @@
package event
import (
"nixcn-cms/data"
"nixcn-cms/utils"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func Checkin(c *gin.Context) {
data := new(data.Attendance)
userIdOrig, ok := c.Get("user_id")
if !ok {
utils.HttpResponse(c, 403, "", "userid error")
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
utils.HttpResponse(c, 500, "", "failed to parse uuid")
}
// Get event id from query
eventIdOrig, ok := c.GetQuery("event_id")
if !ok {
utils.HttpResponse(c, 400, "", "undefinded event id")
return
}
// Parse event id to uuid
eventId, err := uuid.Parse(eventIdOrig)
if err != nil {
utils.HttpResponse(c, 500, "", "error parsing string to uuid")
return
}
data.UserId = userId
code, err := data.GenCheckinCode(eventId)
if err != nil {
utils.HttpResponse(c, 500, "", "error generating code")
return
}
checkinCodeResp := struct {
CheckinCode *string `json:"checkin_code"`
}{code}
utils.HttpResponse(c, 200, "", "success", checkinCodeResp)
}
func CheckinSubmit(c *gin.Context) {
var req struct {
ChekinCode string `json:"checkin_code"`
}
c.ShouldBindJSON(&req)
attendanceData := new(data.Attendance)
err := attendanceData.VerifyCheckinCode(req.ChekinCode)
if err != nil {
utils.HttpResponse(c, 400, "", "error verify checkin code")
return
}
utils.HttpResponse(c, 200, "", "success")
}
func CheckinQuery(c *gin.Context) {
userIdOrig, ok := c.Get("user_id")
if !ok {
utils.HttpResponse(c, 400, "", "userid error")
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
utils.HttpResponse(c, 500, "", "failed to parse uuid")
return
}
eventIdOrig, ok := c.GetQuery("event_id")
if !ok {
utils.HttpResponse(c, 400, "", "could not found event_id")
return
}
eventId, err := uuid.Parse(eventIdOrig)
if err != nil {
utils.HttpResponse(c, 400, "", "event_id is not valid")
return
}
attendanceData := new(data.Attendance)
attendance, err := attendanceData.GetAttendance(userId, eventId)
if err != nil {
utils.HttpResponse(c, 500, "", "database error")
return
} else if attendance == nil {
utils.HttpResponse(c, 404, "", "event checkin record not found")
return
} else if attendance.CheckinAt.IsZero() {
utils.HttpResponse(c, 200, "", "success", gin.H{"checkin_at": nil})
return
}
checkInAtResp := struct {
CheckinAt time.Time `json:"checkin_at"`
}{attendance.CheckinAt}
utils.HttpResponse(c, 200, "", "success", checkInAtResp)
}

View File

@@ -7,6 +7,9 @@ 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.GET("/checkin/query", CheckinQuery)
r.POST("/checkin/submit", CheckinSubmit, middleware.Permission(20))
}

View File

@@ -2,6 +2,8 @@ package event
import (
"nixcn-cms/data"
"nixcn-cms/utils"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -11,26 +13,28 @@ func Info(c *gin.Context) {
eventData := new(data.Event)
eventIdOrig, ok := c.GetQuery("event_id")
if !ok {
c.JSON(400, gin.H{"status": "undefinded event id"})
utils.HttpResponse(c, 400, "", "undefinded event id")
return
}
// Parse event id
eventId, err := uuid.Parse(eventIdOrig)
if err != nil {
c.JSON(500, gin.H{"status": "error parsing string to uuid"})
utils.HttpResponse(c, 500, "", "error parsing string to uuid")
return
}
event, err := eventData.GetEventById(eventId)
if err != nil {
c.JSON(404, gin.H{"status": "event id not found"})
utils.HttpResponse(c, 404, "", "event id not found")
return
}
c.JSON(200, gin.H{
"name": event.Name,
"start_time": event.StartTime,
"end_time": event.EndTime,
})
eventInfoResp := struct {
Name string `json:"name"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
}{event.Name, event.StartTime, event.EndTime}
utils.HttpResponse(c, 200, "", "success", eventInfoResp)
}

View File

@@ -1,75 +0,0 @@
package user
import (
"nixcn-cms/data"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func Checkin(c *gin.Context) {
data := new(data.Attendance)
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"})
}
// Get event id from query
eventIdOrig, ok := c.GetQuery("event_id")
if !ok {
c.JSON(400, gin.H{"status": "undefinded event id"})
return
}
// Parse event id to uuid
eventId, err := uuid.Parse(eventIdOrig)
if err != nil {
c.JSON(500, gin.H{"status": "error parsing string to uuid"})
return
}
data.UserId = userId
code, err := data.GenCheckinCode(eventId)
if err != nil {
c.JSON(500, gin.H{"status": "error generating code"})
return
}
c.JSON(200, gin.H{"checkin_code": code})
}
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)
if err != nil {
c.JSON(400, gin.H{"status": "error verify checkin code"})
return
}
c.JSON(200, gin.H{"status": "success"})
}

7
service/user/create.go Normal file
View File

@@ -0,0 +1,7 @@
package user
import "github.com/gin-gonic/gin"
func Create(c *gin.Context) {
}

View File

@@ -1 +1,39 @@
package user
import (
"nixcn-cms/data"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func Full(c *gin.Context) {
userIdOrig, ok := c.Get("user_id")
if !ok {
utils.HttpResponse(c, 403, "", "userid error")
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
utils.HttpResponse(c, 500, "", "failed to parse uuid")
return
}
userData, err := new(data.User).GetByUserId(userId)
if err != nil {
utils.HttpResponse(c, 404, "", "user not found")
return
}
users, err := userData.GetFullTable()
if err != nil {
utils.HttpResponse(c, 500, "", "database error")
return
}
userFullResp := struct {
UserTable *[]data.User `json:"user_table"`
}{users}
utils.HttpResponse(c, 200, "", "success", userFullResp)
}

View File

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

View File

@@ -2,6 +2,7 @@ package user
import (
"nixcn-cms/data"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -11,29 +12,32 @@ func Info(c *gin.Context) {
userData := new(data.User)
userIdOrig, ok := c.Get("user_id")
if !ok {
c.JSON(403, gin.H{"status": "userid error"})
utils.HttpResponse(c, 403, "", "userid error")
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
c.JSON(500, gin.H{"status": "failed to parse uuid"})
utils.HttpResponse(c, 500, "", "failed to parse uuid")
return
}
// Get user from database
user, err := userData.GetByUserId(userId)
if err != nil {
c.JSON(404, gin.H{"status": "user not found"})
utils.HttpResponse(c, 404, "", "user not found")
return
}
c.JSON(200, gin.H{
"user_id": user.UserId,
"email": user.Email,
"nickname": user.Nickname,
"subtitle": user.Subtitle,
"avatar": user.Avatar,
"bio": user.Bio,
"permission_level": user.PermissionLevel,
})
userInfoResp := struct {
UserId uuid.UUID `json:"user_id"`
Email string `json:"email"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Subtitle string `json:"subtitle"`
Avatar string `json:"avatar"`
Bio string `json:"bio"`
PermissionLevel uint `json:"permission_level"`
}{user.UserId, user.Email, user.Username, user.Nickname, user.Subtitle, user.Avatar, user.Bio, user.PermissionLevel}
utils.HttpResponse(c, 200, "", "success", userInfoResp)
}

View File

@@ -2,14 +2,13 @@ package user
import (
"nixcn-cms/data"
"nixcn-cms/utils"
"strconv"
"github.com/gin-gonic/gin"
)
func List(c *gin.Context) {
data := new(data.User)
// Get limit and offset from query
limit, ok := c.GetQuery("limit")
if !ok {
@@ -17,26 +16,30 @@ func List(c *gin.Context) {
}
offset, ok := c.GetQuery("offset")
if !ok {
c.JSON(400, gin.H{"status": "offset not found"})
utils.HttpResponse(c, 400, "", "offset not found")
return
}
// Parse string to int64
limitNum, err := strconv.ParseInt(limit, 10, 64)
if err != nil {
c.JSON(400, gin.H{"status": "parse string to int error"})
utils.HttpResponse(c, 400, "", "parse string to int error")
return
}
offsetNum, err := strconv.ParseInt(offset, 10, 64)
if err != nil {
c.JSON(400, gin.H{"status": "parse string to int error"})
utils.HttpResponse(c, 400, "", "parse string to int error")
return
}
// 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"})
utils.HttpResponse(c, 500, "", "failed list users from meilisearch")
}
c.JSON(200, list)
userListResp := struct {
List *[]data.UserSearchDoc `json:"list"`
}{list}
utils.HttpResponse(c, 200, "", "success", userListResp)
}

View File

@@ -1,48 +0,0 @@
package user
import (
"nixcn-cms/data"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func Query(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",
})
}
eventIdOrig, ok := c.GetQuery("event_id")
if !ok {
c.JSON(400, gin.H{"status": "could not found event_id"})
return
}
eventId, err := uuid.Parse(eventIdOrig)
if err != nil {
c.JSON(400, gin.H{"status": "event_id is not valid"})
return
}
attendanceData := new(data.Attendance)
attendance, err := attendanceData.GetAttendance(userId, eventId)
if err != nil {
c.JSON(500, gin.H{"status": "database error"})
return
} else if attendance == nil {
c.JSON(404, gin.H{"status": "event checkin record not found"})
return
} else if attendance.CheckinAt.IsZero() {
c.JSON(200, gin.H{"checkin_at": nil})
return
}
c.JSON(200, gin.H{"checkin_at": attendance.CheckinAt})
}

View File

@@ -2,6 +2,8 @@ package user
import (
"nixcn-cms/data"
"nixcn-cms/internal/cryptography"
"nixcn-cms/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -9,36 +11,52 @@ 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"})
utils.HttpResponse(c, 403, "", "userid error")
return
}
userId, err := uuid.Parse(userIdOrig.(string))
if err != nil {
c.JSON(500, gin.H{"status": "failed to parse uuid"})
utils.HttpResponse(c, 500, "", "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 {
utils.HttpResponse(c, 500, "", "failed to find user")
return
}
user.Avatar = ReqInfo.Avatar
user.Email = ReqInfo.Email
user.Nickname = ReqInfo.Nickname
user.Subtitle = ReqInfo.Subtitle
if len(ReqInfo.Email) < 5 || len(ReqInfo.Email) >= 255 {
utils.HttpResponse(c, 400, "", "invilad email")
return
}
userData.Email = ReqInfo.Email
if len(ReqInfo.Username) < 5 || len(ReqInfo.Username) >= 255 {
utils.HttpResponse(c, 400, "", "invilad user name")
return
}
userData.Username = ReqInfo.Username
userData.Nickname = ReqInfo.Nickname
userData.Subtitle = ReqInfo.Subtitle
userData.Avatar = ReqInfo.Avatar
if ReqInfo.Bio != "" {
if !cryptography.IsBase64Std(ReqInfo.Bio) {
utils.HttpResponse(c, 400, "", "invalid base64")
}
}
userData.Bio = ReqInfo.Bio
// Update user info
user.UpdateByUserID(userId)
userData.UpdateByUserID(userId)
c.JSON(200, gin.H{"status": "success"})
utils.HttpResponse(c, 200, "", "success")
}

30
utils/response.go Normal file
View File

@@ -0,0 +1,30 @@
package utils
import "github.com/gin-gonic/gin"
type RespStatus struct {
Code int `json:"code"`
ErrorId string `json:"error_id"`
Status string `json:"status"`
Data any `json:"data"`
}
func HttpResponse(c *gin.Context, code int, errorId string, status string, data ...any) {
var resp = RespStatus{
Code: code,
ErrorId: errorId,
Status: status,
Data: data,
}
c.JSON(code, resp)
}
func HttpAbort(c *gin.Context, code int, errorId string, status string, data ...any) {
var resp = RespStatus{
Code: code,
ErrorId: errorId,
Status: status,
Data: data,
}
c.AbortWithStatusJSON(code, resp)
}