认证授权主要流程实现

This commit is contained in:
2025-03-18 17:57:07 +08:00
parent 19530d9d40
commit 6ddf1118a5
37 changed files with 2209 additions and 180 deletions

View File

@@ -1,16 +1,39 @@
## todo
购买使用流程
核心流程
- [ ] 注册
- [ ] 登录
- [x] 注册与登录
- [ ] 对接短信接口
- [ ] 人机风险分级验证
- [ ] jwt 签发
- [ ] 鉴权
- [ ] 实名认证
- [ ] 充值余额
- [ ] 选择套餐
- [ ] 提取 IP
- [ ] 连接
确认登录方式,决定表结构
中间件:
- [ ] CORS
- [ ] Limiter
- [ ] Compress
环境变量配置默认会话配置
oauth token 验证授权范围
保存 session 到数据库
账单数据表结构修改
captcha 自定义生成流程,弃用 store
短信发送日志
captcha_id 关联用户本机信息,实现验证码设备绑定(或者其他方式)
退出时主动断开数据库缓存等连接
固有字段统一放在最开始

View File

@@ -8,12 +8,6 @@ import (
)
func main() {
g := gen.NewGenerator(gen.Config{
OutPath: "web/queries",
ModelPkgPath: "models",
Mode: gen.WithDefaultQuery | gen.WithoutContext,
})
db, _ := gorm.Open(
postgres.Open("host=localhost user=test password=test dbname=app port=5432 sslmode=disable TimeZone=Asia/Shanghai"),
&gorm.Config{
@@ -22,6 +16,12 @@ func main() {
},
},
)
g := gen.NewGenerator(gen.Config{
OutPath: "web/queries",
ModelPkgPath: "models",
Mode: gen.WithDefaultQuery | gen.WithoutContext,
})
g.UseDB(db)
models := g.GenerateAllTable()

View File

@@ -7,12 +7,12 @@ import (
"platform/init/env"
"platform/init/logs"
"platform/init/orm"
"platform/init/rds"
"platform/web"
"syscall"
)
func main() {
logger := slog.Default()
// 退出信号
shutdown := make(chan os.Signal, 1)
@@ -22,14 +22,14 @@ func main() {
env.Init()
logs.Init()
orm.Init()
rds.Init()
// web 服务
app, err := web.New(&web.Config{
Logger: logger,
Listen: ":8080",
})
if err != nil {
logger.Error("Failed to create server", slog.Any("error", err))
slog.Error("Failed to create server", slog.Any("err", err))
return
}
@@ -38,7 +38,7 @@ func main() {
go func() {
err = app.Run()
if err != nil {
logger.Error("Failed to run server", slog.Any("error", err))
slog.Error("Failed to run server", slog.Any("err", err))
errCh <- err
}
errCh <- nil
@@ -48,19 +48,19 @@ func main() {
exit := false
select {
case <-shutdown:
logger.Info("Received shutdown signal")
slog.Info("Received shutdown signal")
app.Stop()
exit = true
case err := <-errCh:
if err != nil {
logger.Error("Server error", slog.Any("error", err))
slog.Error("Server error", slog.Any("err", err))
}
}
if exit {
err := <-errCh
if err != nil {
logger.Error("Server error", slog.Any("error", err))
slog.Error("Server error", slog.Any("err", err))
}
}
}

17
go.mod
View File

@@ -4,17 +4,22 @@ go 1.24.0
require (
github.com/gofiber/fiber/v2 v2.52.6
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/lmittmann/tint v1.0.7
github.com/redis/go-redis/v9 v9.3.0
golang.org/x/crypto v0.17.0
gorm.io/driver/postgres v1.5.11
gorm.io/gen v0.3.26
gorm.io/gorm v1.25.12
gorm.io/plugin/dbresolver v1.5.3
)
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-sql-driver/mysql v1.7.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-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
@@ -29,14 +34,12 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.17.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
gorm.io/datatypes v1.1.1-0.20230130040222-c43177d3cf8c // indirect
gorm.io/driver/mysql v1.5.7 // indirect
gorm.io/hints v1.1.0 // indirect
gorm.io/plugin/dbresolver v1.5.3 // indirect
)

25
go.sum
View File

@@ -1,8 +1,16 @@
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
@@ -46,6 +54,8 @@ github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLg
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
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/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0=
github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -66,8 +76,8 @@ golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -75,8 +85,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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=
@@ -94,14 +104,15 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
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.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

105
init/env/env.go vendored
View File

@@ -3,40 +3,20 @@ package env
import (
"log/slog"
"os"
"strconv"
"github.com/gofiber/fiber/v2/log"
"github.com/joho/godotenv"
)
// region app
var (
AppName = "platform"
AppPort = "8080"
)
var (
DbHost = "localhost"
DbPort = "3306"
DbName string
DbUserName string
DbPassword string
)
var (
LogLevel = slog.LevelDebug
)
func Init() {
err := godotenv.Load()
if err != nil {
log.Debug("❓ 没有本地环境变量")
} else {
log.Debug("✔ 加载本地环境变量")
}
check()
}
func check() {
func loadApp() {
_AppName := os.Getenv("APP_NAME")
if _AppName != "" {
AppName = _AppName
@@ -46,7 +26,21 @@ func check() {
if _AppPort != "" {
AppPort = _AppPort
}
}
// endregion
// region db
var (
DbHost = "localhost"
DbPort = "5432"
DbName string
DbUserName string
DbPassword string
)
func loadDb() {
_DbHost := os.Getenv("DB_HOST")
if _DbHost != "" {
DbHost = _DbHost
@@ -77,7 +71,54 @@ func check() {
} else {
panic("环境变量 DB_PASSWORD 的值为空")
}
}
// endregion
// region redis
var (
RedisHost = "localhost"
RedisPort = "6379"
RedisDb = 0
RedisPass = ""
)
func loadRedis() {
_RedisHost := os.Getenv("REDIS_HOST")
if _RedisHost != "" {
RedisHost = _RedisHost
}
_RedisPort := os.Getenv("REDIS_PORT")
if _RedisPort != "" {
RedisPort = _RedisPort
}
_RedisDb := os.Getenv("REDIS_DB")
if _RedisDb != "" {
atoi, err := strconv.Atoi(_RedisDb)
if err != nil {
panic("环境变量 REDIS_DB 的值不是数字")
}
RedisDb = atoi
}
_RedisPass := os.Getenv("REDIS_PASS")
if _RedisPass != "" {
RedisPass = _RedisPass
}
}
// endregion
// region log
var (
LogLevel = slog.LevelDebug
)
func loadLog() {
_LogLevel := os.Getenv("LOG_LEVEL")
switch _LogLevel {
case "debug":
@@ -90,3 +131,19 @@ func check() {
LogLevel = slog.LevelError
}
}
// endregion
func Init() {
err := godotenv.Load()
if err != nil {
log.Debug("❓ 没有本地环境变量")
} else {
log.Debug("✔ 加载本地环境变量")
}
loadApp()
loadDb()
loadRedis()
loadLog()
}

View File

@@ -9,14 +9,18 @@ import (
"github.com/lmittmann/tint"
)
var Default *slog.Logger
func Init() {
Default = slog.New(
slog.SetDefault(slog.New(
tint.NewHandler(os.Stdout, &tint.Options{
Level: env.LogLevel,
TimeFormat: time.Kitchen,
ReplaceAttr: func(_ []string, attr slog.Attr) slog.Attr {
err, ok := attr.Value.Any().(error)
if ok {
return tint.Err(err)
}
return attr
},
}),
)
slog.SetDefault(Default)
))
}

View File

@@ -4,40 +4,39 @@ import (
"fmt"
"log/slog"
"platform/init/env"
"platform/init/logs"
"platform/web/queries"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
import "gorm.io/driver/postgres"
var DB *gorm.DB
func Init() {
logger := logs.Default
// 连接数据库
dsn := fmt.Sprintf(
"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Shanghai",
env.DbName, env.DbUserName, env.DbPassword, env.DbName, env.DbPort,
env.DbHost, env.DbUserName, env.DbPassword, env.DbName, env.DbPort,
)
open, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
SingularTable: true,
},
})
if err != nil {
logger.Error("gorm 打开数据库失败", slog.Any("err", err))
slog.Error("gorm 初始化数据库失败", slog.Any("err", err))
panic(err)
}
sql, err := open.DB()
// 连接池
conn, err := db.DB()
if err != nil {
logger.Error("gorm open db error: ", slog.Any("err", err))
slog.Error("gorm 初始化数据库失败:", slog.Any("err", err))
panic(err)
}
sql.SetMaxIdleConns(10)
sql.SetMaxOpenConns(100)
conn.SetMaxIdleConns(10)
conn.SetMaxOpenConns(100)
DB = open
// 初始化查询工具
queries.SetDefault(db)
}

18
init/rds/rds.go Normal file
View File

@@ -0,0 +1,18 @@
package rds
import (
"net"
"platform/init/env"
"github.com/redis/go-redis/v9"
)
var Client *redis.Client
func Init() {
Client = redis.NewClient(&redis.Options{
Addr: net.JoinHostPort(env.RedisHost, env.RedisPort),
DB: env.RedisDb,
Password: env.RedisPass,
})
}

View File

@@ -1,6 +1,7 @@
name: server-dev
services:
postgres:
image: postgres:17
restart: always
@@ -12,6 +13,11 @@ services:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7.4
restart: always
ports:
- "6379:6379"
volumes:
postgres_data:

View File

@@ -1,3 +1,22 @@
-- 清空数据表
do
$$
declare
r record;
begin
for r in (
select
tablename
from
pg_tables
where
schemaname = 'public'
) loop
execute 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE';
end loop;
end
$$;
-- ====================
-- region 管理员信息
-- ====================
@@ -14,7 +33,7 @@ create table admin (
email varchar(255),
status int not null default 1,
last_login timestamp,
last_login_addr varchar(45),
last_login_host varchar(45),
last_login_agent varchar(255),
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
@@ -33,7 +52,7 @@ comment on column admin.phone is '手机号码';
comment on column admin.email is '邮箱';
comment on column admin.status is '状态1-正常0-禁用';
comment on column admin.last_login is '最后登录时间';
comment on column admin.last_login_addr is '最后登录地址';
comment on column admin.last_login_host is '最后登录地址';
comment on column admin.last_login_agent is '最后登录代理';
comment on column admin.created_at is '创建时间';
comment on column admin.updated_at is '更新时间';
@@ -90,7 +109,7 @@ create table "user" (
contact_qq varchar(255),
contact_wechat varchar(255),
last_login timestamp,
last_login_addr varchar(45),
last_login_host varchar(45),
last_login_agent varchar(255),
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
@@ -103,26 +122,26 @@ create index user_status_index on "user" (status);
-- user表字段注释
comment on table "user" is '用户表';
comment on column user.id is '用户ID';
comment on column user.admin_id is '管理员ID';
comment on column user.password is '用户密码';
comment on column user.username is '用户名';
comment on column user.phone is '手机号码';
comment on column user.name is '真实姓名';
comment on column user.avatar is '头像URL';
comment on column user.status is '用户状态1-正常0-禁用';
comment on column user.balance is '账户余额';
comment on column user.id_type is '认证类型0-未认证1-个人认证2-企业认证';
comment on column user.id_no is '身份证号或营业执照号';
comment on column user.id_token is '身份验证标识';
comment on column user.contact_qq is 'QQ联系方式';
comment on column user.contact_wechat is '微信联系方式';
comment on column user.last_login is '最后登录时间';
comment on column user.last_login_addr is '最后登录地址';
comment on column user.last_login_agent is '最后登录代理';
comment on column user.created_at is '创建时间';
comment on column user.updated_at is '更新时间';
comment on column user.deleted_at is '删除时间';
comment on column "user".id is '用户ID';
comment on column "user".admin_id is '管理员ID';
comment on column "user".password is '用户密码';
comment on column "user".username is '用户名';
comment on column "user".phone is '手机号码';
comment on column "user".name is '真实姓名';
comment on column "user".avatar is '头像URL';
comment on column "user".status is '用户状态1-正常0-禁用';
comment on column "user".balance is '账户余额';
comment on column "user".id_type is '认证类型0-未认证1-个人认证2-企业认证';
comment on column "user".id_no is '身份证号或营业执照号';
comment on column "user".id_token is '身份验证标识';
comment on column "user".contact_qq is 'QQ联系方式';
comment on column "user".contact_wechat is '微信联系方式';
comment on column "user".last_login is '最后登录时间';
comment on column "user".last_login_host is '最后登录地址';
comment on column "user".last_login_agent is '最后登录代理';
comment on column "user".created_at is '创建时间';
comment on column "user".updated_at is '更新时间';
comment on column "user".deleted_at is '删除时间';
-- user_role
drop table if exists user_role cascade;
@@ -150,6 +169,51 @@ comment on column user_role.deleted_at is '删除时间';
-- endregion
-- ====================
-- region 客户端信息
-- ====================
drop table if exists client cascade;
create table client (
id serial primary key,
client_id varchar(255) not null unique,
client_secret varchar(255) not null,
redirect_uri varchar(255),
grant_code bool not null default false,
grant_client bool not null default false,
grant_refresh bool not null default false,
spec int not null,
name varchar(255) not null,
version int not null,
status int not null default 1,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
);
create index client_client_id_index on client (client_id);
create index client_name_index on client (name);
create index client_status_index on client (status);
-- client表字段注释
comment on table client is '客户端表';
comment on column client.id is '客户端ID';
comment on column client.client_id is 'OAuth2客户端标识符';
comment on column client.client_secret is 'OAuth2客户端密钥';
comment on column client.redirect_uri is 'OAuth2 重定向URI';
comment on column client.grant_code is '允许授权码授予';
comment on column client.grant_client is '允许客户端凭证授予';
comment on column client.grant_refresh is '允许刷新令牌授予';
comment on column client.spec is '安全规范0-web1-native2-browser';
comment on column client.name is '名称';
comment on column client.version is '版本';
comment on column client.status is '状态1-正常0-禁用';
comment on column client.created_at is '创建时间';
comment on column client.updated_at is '更新时间';
comment on column client.deleted_at is '删除时间';
-- endregion
-- ====================
-- region 权限信息
-- ====================
@@ -168,6 +232,7 @@ create table permission (
deleted_at timestamp
);
create index permission_parent_id_index on permission (parent_id);
create index permission_name_index on permission (name);
-- permission表字段注释
comment on table permission is '权限表';
@@ -283,6 +348,32 @@ comment on column admin_role_permission_link.created_at is '创建时间';
comment on column admin_role_permission_link.updated_at is '更新时间';
comment on column admin_role_permission_link.deleted_at is '删除时间';
-- client_permission_link
drop table if exists client_permission_link cascade;
create table client_permission_link (
id serial primary key,
client_id int not null references client (id)
on update cascade
on delete cascade,
permission_id int not null references permission (id)
on update cascade
on delete cascade,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
);
create index client_permission_link_client_id_index on client_permission_link (client_id);
create index client_permission_link_permission_id_index on client_permission_link (permission_id);
-- client_permission_link表字段注释
comment on table client_permission_link is '客户端权限关联表';
comment on column client_permission_link.id is '关联ID';
comment on column client_permission_link.client_id is '客户端ID';
comment on column client_permission_link.permission_id is '权限ID';
comment on column client_permission_link.created_at is '创建时间';
comment on column client_permission_link.updated_at is '更新时间';
comment on column client_permission_link.deleted_at is '删除时间';
-- endregion
-- ====================
@@ -324,19 +415,19 @@ create table whitelist (
user_id int not null references "user" (id)
on update cascade
on delete cascade,
address varchar(45) not null,
host varchar(45) not null,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
);
create index whitelist_user_id_index on whitelist (user_id);
create index whitelist_address_index on whitelist (address);
create index whitelist_host_index on whitelist (host);
-- whitelist表字段注释
comment on table whitelist is '白名单表';
comment on column whitelist.id is '白名单ID';
comment on column whitelist.user_id is '用户ID';
comment on column whitelist.address is 'IP地址';
comment on column whitelist.host is 'IP地址';
comment on column whitelist.created_at is '创建时间';
comment on column whitelist.updated_at is '更新时间';
comment on column whitelist.deleted_at is '删除时间';
@@ -351,7 +442,7 @@ create table channel (
node_id int references node (id) --
on update cascade --
on delete set null,
user_addr varchar(255) not null,
user_host varchar(255) not null,
node_port int,
auth_ip bool not null default false,
auth_pass bool not null default false,
@@ -365,7 +456,7 @@ create table channel (
);
create index channel_user_id_index on channel (user_id);
create index channel_node_id_index on channel (node_id);
create index channel_user_addr_index on channel (user_addr);
create index channel_user_host_index on channel (user_host);
create index channel_node_port_index on channel (node_port);
create index channel_expiration_index on channel (expiration);
@@ -374,7 +465,7 @@ comment on table channel is '通道表';
comment on column channel.id is '通道ID';
comment on column channel.user_id is '用户ID';
comment on column channel.node_id is '节点ID';
comment on column channel.user_addr is '用户地址';
comment on column channel.user_host is '用户地址';
comment on column channel.node_port is '节点端口';
comment on column channel.auth_ip is 'IP认证';
comment on column channel.auth_pass is '密码认证';

View File

@@ -1 +1,48 @@
package web
import (
"platform/web/common"
"strings"
"platform/web/services"
"github.com/gofiber/fiber/v2"
)
// Protect 创建针对单个路由的鉴权中间件
func Protect(permissions ...string) fiber.Handler {
return func(c *fiber.Ctx) error {
// 获取令牌
var header = c.Get("Authorization")
var token = strings.TrimPrefix(header, "Bearer ")
if token == "" {
return c.Status(fiber.StatusUnauthorized).JSON(common.ErrResp{
Error: true,
Message: "没有权限",
})
}
// 验证令牌
auth, err := services.Session.Find(c.Context(), token)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(common.ErrResp{
Error: true,
Message: "没有权限",
})
}
// 检查权限
if len(permissions) > 0 && !auth.AnyPermission(permissions...) {
return c.Status(fiber.StatusForbidden).JSON(common.ErrResp{
Error: true,
Message: "拒绝访问",
})
}
// 将认证信息存储在上下文中
c.Locals("auth", auth)
c.Locals("access_token", token) // 存储原始令牌,便于后续操作
return c.Next()
}
}

7
web/common/types.go Normal file
View File

@@ -0,0 +1,7 @@
package common
// ErrResp 定义通用错误响应格式
type ErrResp struct {
Message string `json:"message"`
Error bool `json:"error"`
}

19
web/error.go Normal file
View File

@@ -0,0 +1,19 @@
package web
import (
"errors"
"github.com/gofiber/fiber/v2"
)
func ErrorHandler(c *fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
message := "服务器异常"
var e *fiber.Error
if errors.As(err, &e) {
code = e.Code
message = e.Message
}
c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
return c.Status(code).SendString(message)
}

52
web/handlers/client.go Normal file
View File

@@ -0,0 +1,52 @@
package handlers
import (
"platform/web/models"
q "platform/web/queries"
"time"
"github.com/gofiber/fiber/v2"
"golang.org/x/crypto/bcrypt"
)
type CreateClientReq struct {
ClientID string `query:"client_id"`
ClientSecret string `query:"client_secret"`
}
func CreateClient(c *fiber.Ctx) error {
// 验证请求参数
req := new(CreateClientReq)
if err := c.QueryParser(req); err != nil {
return err
}
if req.ClientID == "" {
return fiber.NewError(fiber.StatusBadRequest, "client_id不能为空")
}
if req.ClientSecret == "" {
return fiber.NewError(fiber.StatusBadRequest, "client_secret不能为空")
}
// 创建客户端
hashedSecret, err := bcrypt.GenerateFromPassword([]byte(req.ClientSecret), bcrypt.DefaultCost)
if err != nil {
return err
}
client := &models.Client{
ClientID: req.ClientID,
ClientSecret: string(hashedSecret),
Name: "默认客户端 - " + time.Now().String(),
Spec: 0,
GrantCode: true,
GrantClient: true,
GrantRefresh: true,
Version: 0,
}
err = q.Client.Create(client)
if err != nil {
return err
}
return c.JSON(client)
}

107
web/handlers/login.go Normal file
View File

@@ -0,0 +1,107 @@
package handlers
import (
"errors"
"platform/web/models"
q "platform/web/queries"
"platform/web/services"
"time"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
type LoginReq struct {
Username string `json:"username"`
Password string `json:"password"`
Remember bool `json:"remember"`
}
type LoginResp struct {
Token string `json:"token"`
Expires int64 `json:"expires"`
}
func Login(c *fiber.Ctx) error {
// 验证请求参数
req := new(LoginReq)
if err := c.BodyParser(req); err != nil {
return err
}
if req.Username == "" {
return fiber.NewError(fiber.StatusBadRequest, "手机号不能为空")
}
if req.Password == "" {
return fiber.NewError(fiber.StatusBadRequest, "验证码不能为空")
}
return loginByPhone(c, req)
}
func loginByPhone(c *fiber.Ctx, req *LoginReq) error {
// 验证验证码
ok, err := services.Verifier.VerifySms(c.Context(), req.Username, req.Password)
if err != nil {
return err
}
if !ok {
return fiber.NewError(fiber.StatusBadRequest, "验证码错误")
}
// 查找用户 todo 获取权限信息
var tx = q.Q.Begin()
var user *models.User
user, err = tx.User.
Where(tx.User.Phone.Eq(req.Username)).
Take()
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
// 如果用户不存在,初始化用户 todo 保存默认权限信息
if user == nil {
user = &models.User{
Phone: req.Username,
}
}
// 更新用户的登录时间
user.LastLogin = time.Now()
user.LastLoginHost = c.IP()
user.LastLoginAgent = c.Get("User-Agent")
if err := tx.User.Omit(q.User.AdminID).Save(user); err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
// 保存到会话
auth := services.AuthContext{
Permissions: map[string]struct{}{
"user": {},
},
Payload: services.Payload{
Type: services.PayloadUser,
Id: user.ID,
},
}
duration := time.Hour * 24
if req.Remember {
duration *= 7
}
token, err := services.Session.Create(c.Context(), auth)
if err != nil {
return err
}
return c.JSON(LoginResp{
Token: token.AccessToken,
Expires: token.AccessTokenExpires.Unix(),
})
}

243
web/handlers/oauth.go Normal file
View File

@@ -0,0 +1,243 @@
package handlers
import (
"encoding/base64"
"errors"
"platform/web/models"
q "platform/web/queries"
"platform/web/services"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// region Token
type TokenReq struct {
ClientID string `json:"client_id" form:"client_id"`
ClientSecret string `json:"client_secret" form:"client_secret"`
GrantType TokenGrantType `json:"grant_type" form:"grant_type"`
Code string `json:"code" form:"code"`
RedirectURI string `json:"redirect_uri" form:"redirect_uri"`
CodeVerifier string `json:"code_verifier" form:"code_verifier"`
RefreshToken string `json:"refresh_token" form:"refresh_token"`
Scope string `json:"scope" form:"scope"`
}
type TokenResp struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
TokenType string `json:"token_type"`
Scope string `json:"scope,omitempty"`
ExpiresIn int `json:"expires_in"`
}
type TokenErrResp struct {
Error string `json:"error"`
Description string `json:"error_description,omitempty"`
}
type TokenGrantType string
const (
AuthorizationCode = TokenGrantType("authorization_code")
ClientCredentials = TokenGrantType("client_credentials")
RefreshToken = TokenGrantType("refresh_token")
)
// Token 处理 OAuth2.0 授权请求
func Token(c *fiber.Ctx) error {
// 验证请求参数
req := new(TokenReq)
if err := c.BodyParser(req); err != nil {
return sendError(c, services.ErrOauthInvalidRequest, "无法解析请求参数")
}
if req.GrantType == "" {
return sendError(c, services.ErrOauthInvalidRequest, "缺少必要参数grant_type")
}
// 基于授权类型处理请求
switch req.GrantType {
case AuthorizationCode:
return authorizationCode(c, req)
case ClientCredentials:
return clientCredentials(c, req)
case RefreshToken:
return refreshToken(c, req)
default:
return sendError(c, services.ErrOauthUnsupportedGrantType)
}
}
// 授权码
func authorizationCode(c *fiber.Ctx, req *TokenReq) error {
if req.Code == "" {
return sendError(c, services.ErrOauthInvalidRequest, "缺少必要参数code")
}
client, err := protect(c, services.GrantTypeAuthorizationCode, req.ClientID, req.ClientSecret)
if err != nil {
return sendError(c, err)
}
token, err := services.Auth.OauthAuthorizationCode(c.Context(), client, req.Code, req.RedirectURI, req.CodeVerifier)
if err != nil {
return sendError(c, err.(services.AuthServiceOauthError))
}
return sendSuccess(c, token)
}
// 客户端凭证
func clientCredentials(c *fiber.Ctx, req *TokenReq) error {
client, err := protect(c, services.GrantTypeClientCredentials, req.ClientID, req.ClientSecret)
if err != nil {
return sendError(c, err)
}
scope := strings.Split(req.Scope, ",")
token, err := services.Auth.OauthClientCredentials(c.Context(), client, scope)
if err != nil {
return sendError(c, err.(services.AuthServiceOauthError))
}
return sendSuccess(c, token)
}
// 刷新令牌
func refreshToken(c *fiber.Ctx, req *TokenReq) error {
if req.RefreshToken == "" {
return sendError(c, services.ErrOauthInvalidRequest, "缺少必要参数refresh_token")
}
client, err := protect(c, services.GrantTypeRefreshToken, req.ClientID, req.ClientSecret)
if err != nil {
return sendError(c, err)
}
scope := strings.Split(req.Scope, ",")
token, err := services.Auth.OauthRefreshToken(c.Context(), client, req.RefreshToken, scope)
if err != nil {
return sendError(c, err.(services.AuthServiceOauthError))
}
return sendSuccess(c, token)
}
// 检查客户端凭证
func protect(c *fiber.Ctx, grant services.GrantType, clientId, clientSecret string) (*models.Client, error) {
header := c.Get("Authorization")
if header != "" {
basic := strings.TrimPrefix(header, "Basic ")
if basic != "" {
base, err := base64.URLEncoding.DecodeString(basic)
if err != nil {
return nil, err
}
parts := strings.SplitN(string(base), ":", 2)
if len(parts) == 2 {
clientId = parts[0]
clientSecret = parts[1]
}
}
}
// 查找客户端
if clientId == "" {
return nil, services.ErrOauthInvalidRequest
}
client, err := q.Client.Where(q.Client.ClientID.Eq(clientId)).Take()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, services.ErrOauthInvalidClient
}
return nil, err
}
// 验证客户端状态
if client.Status != 1 {
return nil, services.ErrOauthUnauthorizedClient
}
// 验证授权类型
switch grant {
case services.GrantTypeAuthorizationCode:
if !client.GrantCode {
return nil, services.ErrOauthUnauthorizedClient
}
case services.GrantTypeClientCredentials:
if !client.GrantClient || client.Spec != 0 {
return nil, services.ErrOauthUnauthorizedClient
}
case services.GrantTypeRefreshToken:
if !client.GrantRefresh {
return nil, services.ErrOauthUnauthorizedClient
}
}
// 如果客户端是 confidential验证 client_secret失败返回错误
if client.Spec == 0 {
if clientSecret == "" {
return nil, services.ErrOauthInvalidRequest
}
if bcrypt.CompareHashAndPassword([]byte(client.ClientSecret), []byte(clientSecret)) != nil {
return nil, services.ErrOauthInvalidClient
}
}
return client, nil
}
// 发送成功响应
func sendSuccess(c *fiber.Ctx, details *services.TokenDetails) error {
return c.JSON(TokenResp{
AccessToken: details.AccessToken,
TokenType: "Bearer",
ExpiresIn: int(time.Until(details.AccessTokenExpires).Seconds()),
RefreshToken: details.RefreshToken,
})
}
// 发送错误响应
func sendError(c *fiber.Ctx, err error, description ...string) error {
var sErr services.AuthServiceOauthError
if errors.As(err, &sErr) {
status := fiber.StatusBadRequest
var desc string
switch {
case errors.Is(sErr, services.ErrOauthInvalidRequest):
desc = "无效的请求"
case errors.Is(sErr, services.ErrOauthInvalidClient):
status = fiber.StatusUnauthorized
desc = "无效的客户端凭证"
case errors.Is(sErr, services.ErrOauthInvalidGrant):
desc = "无效的授权凭证"
case errors.Is(sErr, services.ErrOauthInvalidScope):
desc = "无效的授权范围"
case errors.Is(sErr, services.ErrOauthUnauthorizedClient):
desc = "未授权的客户端"
case errors.Is(sErr, services.ErrOauthUnsupportedGrantType):
desc = "不支持的授权类型"
}
if len(description) > 0 {
desc = description[0]
}
return c.Status(status).JSON(TokenErrResp{
Error: string(sErr),
Description: desc,
})
}
return err
}
// endregion

44
web/handlers/verifier.go Normal file
View File

@@ -0,0 +1,44 @@
package handlers
import (
"errors"
"platform/web/services"
"regexp"
"strconv"
"github.com/gofiber/fiber/v2"
)
type VerifierReq struct {
Purpose services.VerifierSmsPurpose `json:"purpose"`
Phone string `json:"phone"`
}
func SmsCode(c *fiber.Ctx) error {
// 解析请求参数
req := new(VerifierReq)
if err := c.BodyParser(req); err != nil {
return err
}
match, err := regexp.MatchString(`^1[3-9]\d{9}$`, req.Phone)
if err != nil {
return err
}
if !match {
return fiber.NewError(fiber.StatusBadRequest, "手机号格式错误")
}
// 发送身份验证码
err = services.Verifier.SendSms(c.Context(), req.Phone, req.Purpose)
if err != nil {
var sErr services.VerifierServiceSendLimitErr
if errors.As(err, &sErr) {
return fiber.NewError(fiber.StatusTooManyRequests, strconv.Itoa(int(sErr)))
}
return err
}
// 发送成功
return nil
}

View File

@@ -23,7 +23,7 @@ type Admin struct {
Email string `gorm:"column:email;comment:邮箱" json:"email"` // 邮箱
Status int32 `gorm:"column:status;not null;default:1;comment:状态1-正常0-禁用" json:"status"` // 状态1-正常0-禁用
LastLogin time.Time `gorm:"column:last_login;comment:最后登录时间" json:"last_login"` // 最后登录时间
LastLoginAddr string `gorm:"column:last_login_addr;comment:最后登录地址" json:"last_login_addr"` // 最后登录地址
LastLoginHost string `gorm:"column:last_login_host;comment:最后登录地址" json:"last_login_host"` // 最后登录地址
LastLoginAgent string `gorm:"column:last_login_agent;comment:最后登录代理" json:"last_login_agent"` // 最后登录代理
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间

View File

@@ -17,7 +17,7 @@ type Channel struct {
ID int32 `gorm:"column:id;primaryKey;autoIncrement:true;comment:通道ID" json:"id"` // 通道ID
UserID int32 `gorm:"column:user_id;not null;comment:用户ID" json:"user_id"` // 用户ID
NodeID int32 `gorm:"column:node_id;comment:节点ID" json:"node_id"` // 节点ID
UserAddr string `gorm:"column:user_addr;not null;comment:用户地址" json:"user_addr"` // 用户地址
UserHost string `gorm:"column:user_host;not null;comment:用户地址" json:"user_host"` // 用户地址
NodePort int32 `gorm:"column:node_port;comment:节点端口" json:"node_port"` // 节点端口
AuthIP bool `gorm:"column:auth_ip;not null;comment:IP认证" json:"auth_ip"` // IP认证
AuthPass bool `gorm:"column:auth_pass;not null;comment:密码认证" json:"auth_pass"` // 密码认证

36
web/models/client.gen.go Normal file
View File

@@ -0,0 +1,36 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package models
import (
"time"
"gorm.io/gorm"
)
const TableNameClient = "client"
// Client mapped from table <client>
type Client struct {
ID int32 `gorm:"column:id;primaryKey;autoIncrement:true;comment:客户端ID" json:"id"` // 客户端ID
ClientID string `gorm:"column:client_id;not null;comment:OAuth2客户端标识符" json:"client_id"` // OAuth2客户端标识符
ClientSecret string `gorm:"column:client_secret;not null;comment:OAuth2客户端密钥" json:"client_secret"` // OAuth2客户端密钥
RedirectURI string `gorm:"column:redirect_uri;comment:OAuth2 重定向URI" json:"redirect_uri"` // OAuth2 重定向URI
GrantCode bool `gorm:"column:grant_code;not null;comment:允许授权码授予" json:"grant_code"` // 允许授权码授予
GrantClient bool `gorm:"column:grant_client;not null;comment:允许客户端凭证授予" json:"grant_client"` // 允许客户端凭证授予
GrantRefresh bool `gorm:"column:grant_refresh;not null;comment:允许刷新令牌授予" json:"grant_refresh"` // 允许刷新令牌授予
Spec int32 `gorm:"column:spec;not null;comment:安全规范0-web1-native2-browser" json:"spec"` // 安全规范0-web1-native2-browser
Name string `gorm:"column:name;not null;comment:名称" json:"name"` // 名称
Version int32 `gorm:"column:version;not null;comment:版本" json:"version"` // 版本
Status int32 `gorm:"column:status;not null;default:1;comment:状态1-正常0-禁用" json:"status"` // 状态1-正常0-禁用
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;comment:删除时间" json:"deleted_at"` // 删除时间
}
// TableName Client's table name
func (*Client) TableName() string {
return TableNameClient
}

View File

@@ -0,0 +1,28 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package models
import (
"time"
"gorm.io/gorm"
)
const TableNameClientPermissionLink = "client_permission_link"
// ClientPermissionLink mapped from table <client_permission_link>
type ClientPermissionLink struct {
ID int32 `gorm:"column:id;primaryKey;autoIncrement:true;comment:关联ID" json:"id"` // 关联ID
ClientID int32 `gorm:"column:client_id;not null;comment:客户端ID" json:"client_id"` // 客户端ID
PermissionID int32 `gorm:"column:permission_id;not null;comment:权限ID" json:"permission_id"` // 权限ID
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;comment:删除时间" json:"deleted_at"` // 删除时间
}
// TableName ClientPermissionLink's table name
func (*ClientPermissionLink) TableName() string {
return TableNameClientPermissionLink
}

View File

@@ -14,27 +14,27 @@ const TableNameUser = "user"
// User mapped from table <user>
type User struct {
ID int32 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
AdminID int32 `gorm:"column:admin_id" json:"admin_id"`
Phone string `gorm:"column:phone;not null" json:"phone"`
Username string `gorm:"column:username" json:"username"`
ID int32 `gorm:"column:id;primaryKey;autoIncrement:true;comment:用户ID" json:"id"` // 用户ID
AdminID int32 `gorm:"column:admin_id;comment:管理员ID" json:"admin_id"` // 管理员ID
Phone string `gorm:"column:phone;not null;comment:手机号码" json:"phone"` // 手机号码
Username string `gorm:"column:username;comment:用户名" json:"username"` // 用户名
Email string `gorm:"column:email" json:"email"`
Password string `gorm:"column:password" json:"password"`
Name string `gorm:"column:name" json:"name"`
Avatar string `gorm:"column:avatar" json:"avatar"`
Status int32 `gorm:"column:status;not null;default:1" json:"status"`
Balance float64 `gorm:"column:balance;not null" json:"balance"`
IDType int32 `gorm:"column:id_type;not null" json:"id_type"`
IDNo string `gorm:"column:id_no" json:"id_no"`
IDToken string `gorm:"column:id_token" json:"id_token"`
ContactQq string `gorm:"column:contact_qq" json:"contact_qq"`
ContactWechat string `gorm:"column:contact_wechat" json:"contact_wechat"`
LastLogin time.Time `gorm:"column:last_login" json:"last_login"`
LastLoginAddr string `gorm:"column:last_login_addr" json:"last_login_addr"`
LastLoginAgent string `gorm:"column:last_login_agent" json:"last_login_agent"`
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at" json:"deleted_at"`
Password string `gorm:"column:password;comment:用户密码" json:"password"` // 用户密码
Name string `gorm:"column:name;comment:真实姓名" json:"name"` // 真实姓名
Avatar string `gorm:"column:avatar;comment:头像URL" json:"avatar"` // 头像URL
Status int32 `gorm:"column:status;not null;default:1;comment:用户状态1-正常0-禁用" json:"status"` // 用户状态1-正常0-禁用
Balance float64 `gorm:"column:balance;not null;comment:账户余额" json:"balance"` // 账户余额
IDType int32 `gorm:"column:id_type;not null;comment:认证类型0-未认证1-个人认证2-企业认证" json:"id_type"` // 认证类型0-未认证1-个人认证2-企业认证
IDNo string `gorm:"column:id_no;comment:身份证号或营业执照号" json:"id_no"` // 身份证号或营业执照号
IDToken string `gorm:"column:id_token;comment:身份验证标识" json:"id_token"` // 身份验证标识
ContactQq string `gorm:"column:contact_qq;comment:QQ联系方式" json:"contact_qq"` // QQ联系方式
ContactWechat string `gorm:"column:contact_wechat;comment:微信联系方式" json:"contact_wechat"` // 微信联系方式
LastLogin time.Time `gorm:"column:last_login;comment:最后登录时间" json:"last_login"` // 最后登录时间
LastLoginHost string `gorm:"column:last_login_host;comment:最后登录地址" json:"last_login_host"` // 最后登录地址
LastLoginAgent string `gorm:"column:last_login_agent;comment:最后登录代理" json:"last_login_agent"` // 最后登录代理
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;comment:删除时间" json:"deleted_at"` // 删除时间
}
// TableName User's table name

View File

@@ -16,7 +16,7 @@ const TableNameWhitelist = "whitelist"
type Whitelist struct {
ID int32 `gorm:"column:id;primaryKey;autoIncrement:true;comment:白名单ID" json:"id"` // 白名单ID
UserID int32 `gorm:"column:user_id;not null;comment:用户ID" json:"user_id"` // 用户ID
Address string `gorm:"column:address;not null;comment:IP地址" json:"address"` // IP地址
Host string `gorm:"column:host;not null;comment:IP地址" json:"host"` // IP地址
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;comment:删除时间" json:"deleted_at"` // 删除时间

View File

@@ -36,7 +36,7 @@ func newAdmin(db *gorm.DB, opts ...gen.DOOption) admin {
_admin.Email = field.NewString(tableName, "email")
_admin.Status = field.NewInt32(tableName, "status")
_admin.LastLogin = field.NewTime(tableName, "last_login")
_admin.LastLoginAddr = field.NewString(tableName, "last_login_addr")
_admin.LastLoginHost = field.NewString(tableName, "last_login_host")
_admin.LastLoginAgent = field.NewString(tableName, "last_login_agent")
_admin.CreatedAt = field.NewTime(tableName, "created_at")
_admin.UpdatedAt = field.NewTime(tableName, "updated_at")
@@ -60,7 +60,7 @@ type admin struct {
Email field.String // 邮箱
Status field.Int32 // 状态1-正常0-禁用
LastLogin field.Time // 最后登录时间
LastLoginAddr field.String // 最后登录地址
LastLoginHost field.String // 最后登录地址
LastLoginAgent field.String // 最后登录代理
CreatedAt field.Time // 创建时间
UpdatedAt field.Time // 更新时间
@@ -90,7 +90,7 @@ func (a *admin) updateTableName(table string) *admin {
a.Email = field.NewString(table, "email")
a.Status = field.NewInt32(table, "status")
a.LastLogin = field.NewTime(table, "last_login")
a.LastLoginAddr = field.NewString(table, "last_login_addr")
a.LastLoginHost = field.NewString(table, "last_login_host")
a.LastLoginAgent = field.NewString(table, "last_login_agent")
a.CreatedAt = field.NewTime(table, "created_at")
a.UpdatedAt = field.NewTime(table, "updated_at")
@@ -121,7 +121,7 @@ func (a *admin) fillFieldMap() {
a.fieldMap["email"] = a.Email
a.fieldMap["status"] = a.Status
a.fieldMap["last_login"] = a.LastLogin
a.fieldMap["last_login_addr"] = a.LastLoginAddr
a.fieldMap["last_login_host"] = a.LastLoginHost
a.fieldMap["last_login_agent"] = a.LastLoginAgent
a.fieldMap["created_at"] = a.CreatedAt
a.fieldMap["updated_at"] = a.UpdatedAt

View File

@@ -30,7 +30,7 @@ func newChannel(db *gorm.DB, opts ...gen.DOOption) channel {
_channel.ID = field.NewInt32(tableName, "id")
_channel.UserID = field.NewInt32(tableName, "user_id")
_channel.NodeID = field.NewInt32(tableName, "node_id")
_channel.UserAddr = field.NewString(tableName, "user_addr")
_channel.UserHost = field.NewString(tableName, "user_host")
_channel.NodePort = field.NewInt32(tableName, "node_port")
_channel.AuthIP = field.NewBool(tableName, "auth_ip")
_channel.AuthPass = field.NewBool(tableName, "auth_pass")
@@ -54,7 +54,7 @@ type channel struct {
ID field.Int32 // 通道ID
UserID field.Int32 // 用户ID
NodeID field.Int32 // 节点ID
UserAddr field.String // 用户地址
UserHost field.String // 用户地址
NodePort field.Int32 // 节点端口
AuthIP field.Bool // IP认证
AuthPass field.Bool // 密码认证
@@ -84,7 +84,7 @@ func (c *channel) updateTableName(table string) *channel {
c.ID = field.NewInt32(table, "id")
c.UserID = field.NewInt32(table, "user_id")
c.NodeID = field.NewInt32(table, "node_id")
c.UserAddr = field.NewString(table, "user_addr")
c.UserHost = field.NewString(table, "user_host")
c.NodePort = field.NewInt32(table, "node_port")
c.AuthIP = field.NewBool(table, "auth_ip")
c.AuthPass = field.NewBool(table, "auth_pass")
@@ -115,7 +115,7 @@ func (c *channel) fillFieldMap() {
c.fieldMap["id"] = c.ID
c.fieldMap["user_id"] = c.UserID
c.fieldMap["node_id"] = c.NodeID
c.fieldMap["user_addr"] = c.UserAddr
c.fieldMap["user_host"] = c.UserHost
c.fieldMap["node_port"] = c.NodePort
c.fieldMap["auth_ip"] = c.AuthIP
c.fieldMap["auth_pass"] = c.AuthPass

371
web/queries/client.gen.go Normal file
View File

@@ -0,0 +1,371 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package queries
import (
"context"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/plugin/dbresolver"
"platform/web/models"
)
func newClient(db *gorm.DB, opts ...gen.DOOption) client {
_client := client{}
_client.clientDo.UseDB(db, opts...)
_client.clientDo.UseModel(&models.Client{})
tableName := _client.clientDo.TableName()
_client.ALL = field.NewAsterisk(tableName)
_client.ID = field.NewInt32(tableName, "id")
_client.ClientID = field.NewString(tableName, "client_id")
_client.ClientSecret = field.NewString(tableName, "client_secret")
_client.RedirectURI = field.NewString(tableName, "redirect_uri")
_client.GrantCode = field.NewBool(tableName, "grant_code")
_client.GrantClient = field.NewBool(tableName, "grant_client")
_client.GrantRefresh = field.NewBool(tableName, "grant_refresh")
_client.Spec = field.NewInt32(tableName, "spec")
_client.Name = field.NewString(tableName, "name")
_client.Version = field.NewInt32(tableName, "version")
_client.Status = field.NewInt32(tableName, "status")
_client.CreatedAt = field.NewTime(tableName, "created_at")
_client.UpdatedAt = field.NewTime(tableName, "updated_at")
_client.DeletedAt = field.NewField(tableName, "deleted_at")
_client.fillFieldMap()
return _client
}
type client struct {
clientDo
ALL field.Asterisk
ID field.Int32 // 客户端ID
ClientID field.String // OAuth2客户端标识符
ClientSecret field.String // OAuth2客户端密钥
RedirectURI field.String // OAuth2 重定向URI
GrantCode field.Bool // 允许授权码授予
GrantClient field.Bool // 允许客户端凭证授予
GrantRefresh field.Bool // 允许刷新令牌授予
Spec field.Int32 // 安全规范0-web1-native2-browser
Name field.String // 名称
Version field.Int32 // 版本
Status field.Int32 // 状态1-正常0-禁用
CreatedAt field.Time // 创建时间
UpdatedAt field.Time // 更新时间
DeletedAt field.Field // 删除时间
fieldMap map[string]field.Expr
}
func (c client) Table(newTableName string) *client {
c.clientDo.UseTable(newTableName)
return c.updateTableName(newTableName)
}
func (c client) As(alias string) *client {
c.clientDo.DO = *(c.clientDo.As(alias).(*gen.DO))
return c.updateTableName(alias)
}
func (c *client) updateTableName(table string) *client {
c.ALL = field.NewAsterisk(table)
c.ID = field.NewInt32(table, "id")
c.ClientID = field.NewString(table, "client_id")
c.ClientSecret = field.NewString(table, "client_secret")
c.RedirectURI = field.NewString(table, "redirect_uri")
c.GrantCode = field.NewBool(table, "grant_code")
c.GrantClient = field.NewBool(table, "grant_client")
c.GrantRefresh = field.NewBool(table, "grant_refresh")
c.Spec = field.NewInt32(table, "spec")
c.Name = field.NewString(table, "name")
c.Version = field.NewInt32(table, "version")
c.Status = field.NewInt32(table, "status")
c.CreatedAt = field.NewTime(table, "created_at")
c.UpdatedAt = field.NewTime(table, "updated_at")
c.DeletedAt = field.NewField(table, "deleted_at")
c.fillFieldMap()
return c
}
func (c *client) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := c.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (c *client) fillFieldMap() {
c.fieldMap = make(map[string]field.Expr, 14)
c.fieldMap["id"] = c.ID
c.fieldMap["client_id"] = c.ClientID
c.fieldMap["client_secret"] = c.ClientSecret
c.fieldMap["redirect_uri"] = c.RedirectURI
c.fieldMap["grant_code"] = c.GrantCode
c.fieldMap["grant_client"] = c.GrantClient
c.fieldMap["grant_refresh"] = c.GrantRefresh
c.fieldMap["spec"] = c.Spec
c.fieldMap["name"] = c.Name
c.fieldMap["version"] = c.Version
c.fieldMap["status"] = c.Status
c.fieldMap["created_at"] = c.CreatedAt
c.fieldMap["updated_at"] = c.UpdatedAt
c.fieldMap["deleted_at"] = c.DeletedAt
}
func (c client) clone(db *gorm.DB) client {
c.clientDo.ReplaceConnPool(db.Statement.ConnPool)
return c
}
func (c client) replaceDB(db *gorm.DB) client {
c.clientDo.ReplaceDB(db)
return c
}
type clientDo struct{ gen.DO }
func (c clientDo) Debug() *clientDo {
return c.withDO(c.DO.Debug())
}
func (c clientDo) WithContext(ctx context.Context) *clientDo {
return c.withDO(c.DO.WithContext(ctx))
}
func (c clientDo) ReadDB() *clientDo {
return c.Clauses(dbresolver.Read)
}
func (c clientDo) WriteDB() *clientDo {
return c.Clauses(dbresolver.Write)
}
func (c clientDo) Session(config *gorm.Session) *clientDo {
return c.withDO(c.DO.Session(config))
}
func (c clientDo) Clauses(conds ...clause.Expression) *clientDo {
return c.withDO(c.DO.Clauses(conds...))
}
func (c clientDo) Returning(value interface{}, columns ...string) *clientDo {
return c.withDO(c.DO.Returning(value, columns...))
}
func (c clientDo) Not(conds ...gen.Condition) *clientDo {
return c.withDO(c.DO.Not(conds...))
}
func (c clientDo) Or(conds ...gen.Condition) *clientDo {
return c.withDO(c.DO.Or(conds...))
}
func (c clientDo) Select(conds ...field.Expr) *clientDo {
return c.withDO(c.DO.Select(conds...))
}
func (c clientDo) Where(conds ...gen.Condition) *clientDo {
return c.withDO(c.DO.Where(conds...))
}
func (c clientDo) Order(conds ...field.Expr) *clientDo {
return c.withDO(c.DO.Order(conds...))
}
func (c clientDo) Distinct(cols ...field.Expr) *clientDo {
return c.withDO(c.DO.Distinct(cols...))
}
func (c clientDo) Omit(cols ...field.Expr) *clientDo {
return c.withDO(c.DO.Omit(cols...))
}
func (c clientDo) Join(table schema.Tabler, on ...field.Expr) *clientDo {
return c.withDO(c.DO.Join(table, on...))
}
func (c clientDo) LeftJoin(table schema.Tabler, on ...field.Expr) *clientDo {
return c.withDO(c.DO.LeftJoin(table, on...))
}
func (c clientDo) RightJoin(table schema.Tabler, on ...field.Expr) *clientDo {
return c.withDO(c.DO.RightJoin(table, on...))
}
func (c clientDo) Group(cols ...field.Expr) *clientDo {
return c.withDO(c.DO.Group(cols...))
}
func (c clientDo) Having(conds ...gen.Condition) *clientDo {
return c.withDO(c.DO.Having(conds...))
}
func (c clientDo) Limit(limit int) *clientDo {
return c.withDO(c.DO.Limit(limit))
}
func (c clientDo) Offset(offset int) *clientDo {
return c.withDO(c.DO.Offset(offset))
}
func (c clientDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *clientDo {
return c.withDO(c.DO.Scopes(funcs...))
}
func (c clientDo) Unscoped() *clientDo {
return c.withDO(c.DO.Unscoped())
}
func (c clientDo) Create(values ...*models.Client) error {
if len(values) == 0 {
return nil
}
return c.DO.Create(values)
}
func (c clientDo) CreateInBatches(values []*models.Client, batchSize int) error {
return c.DO.CreateInBatches(values, batchSize)
}
// Save : !!! underlying implementation is different with GORM
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
func (c clientDo) Save(values ...*models.Client) error {
if len(values) == 0 {
return nil
}
return c.DO.Save(values)
}
func (c clientDo) First() (*models.Client, error) {
if result, err := c.DO.First(); err != nil {
return nil, err
} else {
return result.(*models.Client), nil
}
}
func (c clientDo) Take() (*models.Client, error) {
if result, err := c.DO.Take(); err != nil {
return nil, err
} else {
return result.(*models.Client), nil
}
}
func (c clientDo) Last() (*models.Client, error) {
if result, err := c.DO.Last(); err != nil {
return nil, err
} else {
return result.(*models.Client), nil
}
}
func (c clientDo) Find() ([]*models.Client, error) {
result, err := c.DO.Find()
return result.([]*models.Client), err
}
func (c clientDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*models.Client, err error) {
buf := make([]*models.Client, 0, batchSize)
err = c.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
defer func() { results = append(results, buf...) }()
return fc(tx, batch)
})
return results, err
}
func (c clientDo) FindInBatches(result *[]*models.Client, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return c.DO.FindInBatches(result, batchSize, fc)
}
func (c clientDo) Attrs(attrs ...field.AssignExpr) *clientDo {
return c.withDO(c.DO.Attrs(attrs...))
}
func (c clientDo) Assign(attrs ...field.AssignExpr) *clientDo {
return c.withDO(c.DO.Assign(attrs...))
}
func (c clientDo) Joins(fields ...field.RelationField) *clientDo {
for _, _f := range fields {
c = *c.withDO(c.DO.Joins(_f))
}
return &c
}
func (c clientDo) Preload(fields ...field.RelationField) *clientDo {
for _, _f := range fields {
c = *c.withDO(c.DO.Preload(_f))
}
return &c
}
func (c clientDo) FirstOrInit() (*models.Client, error) {
if result, err := c.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*models.Client), nil
}
}
func (c clientDo) FirstOrCreate() (*models.Client, error) {
if result, err := c.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*models.Client), nil
}
}
func (c clientDo) FindByPage(offset int, limit int) (result []*models.Client, count int64, err error) {
result, err = c.Offset(offset).Limit(limit).Find()
if err != nil {
return
}
if size := len(result); 0 < limit && 0 < size && size < limit {
count = int64(size + offset)
return
}
count, err = c.Offset(-1).Limit(-1).Count()
return
}
func (c clientDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = c.Count()
if err != nil {
return
}
err = c.Offset(offset).Limit(limit).Scan(result)
return
}
func (c clientDo) Scan(result interface{}) (err error) {
return c.DO.Scan(result)
}
func (c clientDo) Delete(models ...*models.Client) (result gen.ResultInfo, err error) {
return c.DO.Delete(models)
}
func (c *clientDo) withDO(do gen.Dao) *clientDo {
c.DO = *do.(*gen.DO)
return c
}

View File

@@ -0,0 +1,339 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package queries
import (
"context"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/plugin/dbresolver"
"platform/web/models"
)
func newClientPermissionLink(db *gorm.DB, opts ...gen.DOOption) clientPermissionLink {
_clientPermissionLink := clientPermissionLink{}
_clientPermissionLink.clientPermissionLinkDo.UseDB(db, opts...)
_clientPermissionLink.clientPermissionLinkDo.UseModel(&models.ClientPermissionLink{})
tableName := _clientPermissionLink.clientPermissionLinkDo.TableName()
_clientPermissionLink.ALL = field.NewAsterisk(tableName)
_clientPermissionLink.ID = field.NewInt32(tableName, "id")
_clientPermissionLink.ClientID = field.NewInt32(tableName, "client_id")
_clientPermissionLink.PermissionID = field.NewInt32(tableName, "permission_id")
_clientPermissionLink.CreatedAt = field.NewTime(tableName, "created_at")
_clientPermissionLink.UpdatedAt = field.NewTime(tableName, "updated_at")
_clientPermissionLink.DeletedAt = field.NewField(tableName, "deleted_at")
_clientPermissionLink.fillFieldMap()
return _clientPermissionLink
}
type clientPermissionLink struct {
clientPermissionLinkDo
ALL field.Asterisk
ID field.Int32 // 关联ID
ClientID field.Int32 // 客户端ID
PermissionID field.Int32 // 权限ID
CreatedAt field.Time // 创建时间
UpdatedAt field.Time // 更新时间
DeletedAt field.Field // 删除时间
fieldMap map[string]field.Expr
}
func (c clientPermissionLink) Table(newTableName string) *clientPermissionLink {
c.clientPermissionLinkDo.UseTable(newTableName)
return c.updateTableName(newTableName)
}
func (c clientPermissionLink) As(alias string) *clientPermissionLink {
c.clientPermissionLinkDo.DO = *(c.clientPermissionLinkDo.As(alias).(*gen.DO))
return c.updateTableName(alias)
}
func (c *clientPermissionLink) updateTableName(table string) *clientPermissionLink {
c.ALL = field.NewAsterisk(table)
c.ID = field.NewInt32(table, "id")
c.ClientID = field.NewInt32(table, "client_id")
c.PermissionID = field.NewInt32(table, "permission_id")
c.CreatedAt = field.NewTime(table, "created_at")
c.UpdatedAt = field.NewTime(table, "updated_at")
c.DeletedAt = field.NewField(table, "deleted_at")
c.fillFieldMap()
return c
}
func (c *clientPermissionLink) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := c.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (c *clientPermissionLink) fillFieldMap() {
c.fieldMap = make(map[string]field.Expr, 6)
c.fieldMap["id"] = c.ID
c.fieldMap["client_id"] = c.ClientID
c.fieldMap["permission_id"] = c.PermissionID
c.fieldMap["created_at"] = c.CreatedAt
c.fieldMap["updated_at"] = c.UpdatedAt
c.fieldMap["deleted_at"] = c.DeletedAt
}
func (c clientPermissionLink) clone(db *gorm.DB) clientPermissionLink {
c.clientPermissionLinkDo.ReplaceConnPool(db.Statement.ConnPool)
return c
}
func (c clientPermissionLink) replaceDB(db *gorm.DB) clientPermissionLink {
c.clientPermissionLinkDo.ReplaceDB(db)
return c
}
type clientPermissionLinkDo struct{ gen.DO }
func (c clientPermissionLinkDo) Debug() *clientPermissionLinkDo {
return c.withDO(c.DO.Debug())
}
func (c clientPermissionLinkDo) WithContext(ctx context.Context) *clientPermissionLinkDo {
return c.withDO(c.DO.WithContext(ctx))
}
func (c clientPermissionLinkDo) ReadDB() *clientPermissionLinkDo {
return c.Clauses(dbresolver.Read)
}
func (c clientPermissionLinkDo) WriteDB() *clientPermissionLinkDo {
return c.Clauses(dbresolver.Write)
}
func (c clientPermissionLinkDo) Session(config *gorm.Session) *clientPermissionLinkDo {
return c.withDO(c.DO.Session(config))
}
func (c clientPermissionLinkDo) Clauses(conds ...clause.Expression) *clientPermissionLinkDo {
return c.withDO(c.DO.Clauses(conds...))
}
func (c clientPermissionLinkDo) Returning(value interface{}, columns ...string) *clientPermissionLinkDo {
return c.withDO(c.DO.Returning(value, columns...))
}
func (c clientPermissionLinkDo) Not(conds ...gen.Condition) *clientPermissionLinkDo {
return c.withDO(c.DO.Not(conds...))
}
func (c clientPermissionLinkDo) Or(conds ...gen.Condition) *clientPermissionLinkDo {
return c.withDO(c.DO.Or(conds...))
}
func (c clientPermissionLinkDo) Select(conds ...field.Expr) *clientPermissionLinkDo {
return c.withDO(c.DO.Select(conds...))
}
func (c clientPermissionLinkDo) Where(conds ...gen.Condition) *clientPermissionLinkDo {
return c.withDO(c.DO.Where(conds...))
}
func (c clientPermissionLinkDo) Order(conds ...field.Expr) *clientPermissionLinkDo {
return c.withDO(c.DO.Order(conds...))
}
func (c clientPermissionLinkDo) Distinct(cols ...field.Expr) *clientPermissionLinkDo {
return c.withDO(c.DO.Distinct(cols...))
}
func (c clientPermissionLinkDo) Omit(cols ...field.Expr) *clientPermissionLinkDo {
return c.withDO(c.DO.Omit(cols...))
}
func (c clientPermissionLinkDo) Join(table schema.Tabler, on ...field.Expr) *clientPermissionLinkDo {
return c.withDO(c.DO.Join(table, on...))
}
func (c clientPermissionLinkDo) LeftJoin(table schema.Tabler, on ...field.Expr) *clientPermissionLinkDo {
return c.withDO(c.DO.LeftJoin(table, on...))
}
func (c clientPermissionLinkDo) RightJoin(table schema.Tabler, on ...field.Expr) *clientPermissionLinkDo {
return c.withDO(c.DO.RightJoin(table, on...))
}
func (c clientPermissionLinkDo) Group(cols ...field.Expr) *clientPermissionLinkDo {
return c.withDO(c.DO.Group(cols...))
}
func (c clientPermissionLinkDo) Having(conds ...gen.Condition) *clientPermissionLinkDo {
return c.withDO(c.DO.Having(conds...))
}
func (c clientPermissionLinkDo) Limit(limit int) *clientPermissionLinkDo {
return c.withDO(c.DO.Limit(limit))
}
func (c clientPermissionLinkDo) Offset(offset int) *clientPermissionLinkDo {
return c.withDO(c.DO.Offset(offset))
}
func (c clientPermissionLinkDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *clientPermissionLinkDo {
return c.withDO(c.DO.Scopes(funcs...))
}
func (c clientPermissionLinkDo) Unscoped() *clientPermissionLinkDo {
return c.withDO(c.DO.Unscoped())
}
func (c clientPermissionLinkDo) Create(values ...*models.ClientPermissionLink) error {
if len(values) == 0 {
return nil
}
return c.DO.Create(values)
}
func (c clientPermissionLinkDo) CreateInBatches(values []*models.ClientPermissionLink, batchSize int) error {
return c.DO.CreateInBatches(values, batchSize)
}
// Save : !!! underlying implementation is different with GORM
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
func (c clientPermissionLinkDo) Save(values ...*models.ClientPermissionLink) error {
if len(values) == 0 {
return nil
}
return c.DO.Save(values)
}
func (c clientPermissionLinkDo) First() (*models.ClientPermissionLink, error) {
if result, err := c.DO.First(); err != nil {
return nil, err
} else {
return result.(*models.ClientPermissionLink), nil
}
}
func (c clientPermissionLinkDo) Take() (*models.ClientPermissionLink, error) {
if result, err := c.DO.Take(); err != nil {
return nil, err
} else {
return result.(*models.ClientPermissionLink), nil
}
}
func (c clientPermissionLinkDo) Last() (*models.ClientPermissionLink, error) {
if result, err := c.DO.Last(); err != nil {
return nil, err
} else {
return result.(*models.ClientPermissionLink), nil
}
}
func (c clientPermissionLinkDo) Find() ([]*models.ClientPermissionLink, error) {
result, err := c.DO.Find()
return result.([]*models.ClientPermissionLink), err
}
func (c clientPermissionLinkDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*models.ClientPermissionLink, err error) {
buf := make([]*models.ClientPermissionLink, 0, batchSize)
err = c.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
defer func() { results = append(results, buf...) }()
return fc(tx, batch)
})
return results, err
}
func (c clientPermissionLinkDo) FindInBatches(result *[]*models.ClientPermissionLink, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return c.DO.FindInBatches(result, batchSize, fc)
}
func (c clientPermissionLinkDo) Attrs(attrs ...field.AssignExpr) *clientPermissionLinkDo {
return c.withDO(c.DO.Attrs(attrs...))
}
func (c clientPermissionLinkDo) Assign(attrs ...field.AssignExpr) *clientPermissionLinkDo {
return c.withDO(c.DO.Assign(attrs...))
}
func (c clientPermissionLinkDo) Joins(fields ...field.RelationField) *clientPermissionLinkDo {
for _, _f := range fields {
c = *c.withDO(c.DO.Joins(_f))
}
return &c
}
func (c clientPermissionLinkDo) Preload(fields ...field.RelationField) *clientPermissionLinkDo {
for _, _f := range fields {
c = *c.withDO(c.DO.Preload(_f))
}
return &c
}
func (c clientPermissionLinkDo) FirstOrInit() (*models.ClientPermissionLink, error) {
if result, err := c.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*models.ClientPermissionLink), nil
}
}
func (c clientPermissionLinkDo) FirstOrCreate() (*models.ClientPermissionLink, error) {
if result, err := c.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*models.ClientPermissionLink), nil
}
}
func (c clientPermissionLinkDo) FindByPage(offset int, limit int) (result []*models.ClientPermissionLink, count int64, err error) {
result, err = c.Offset(offset).Limit(limit).Find()
if err != nil {
return
}
if size := len(result); 0 < limit && 0 < size && size < limit {
count = int64(size + offset)
return
}
count, err = c.Offset(-1).Limit(-1).Count()
return
}
func (c clientPermissionLinkDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = c.Count()
if err != nil {
return
}
err = c.Offset(offset).Limit(limit).Scan(result)
return
}
func (c clientPermissionLinkDo) Scan(result interface{}) (err error) {
return c.DO.Scan(result)
}
func (c clientPermissionLinkDo) Delete(models ...*models.ClientPermissionLink) (result gen.ResultInfo, err error) {
return c.DO.Delete(models)
}
func (c *clientPermissionLinkDo) withDO(do gen.Dao) *clientPermissionLinkDo {
c.DO = *do.(*gen.DO)
return c
}

View File

@@ -23,6 +23,8 @@ var (
AdminRolePermissionLink *adminRolePermissionLink
Bill *bill
Channel *channel
Client *client
ClientPermissionLink *clientPermissionLink
Node *node
Permission *permission
Product *product
@@ -47,6 +49,8 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
AdminRolePermissionLink = &Q.AdminRolePermissionLink
Bill = &Q.Bill
Channel = &Q.Channel
Client = &Q.Client
ClientPermissionLink = &Q.ClientPermissionLink
Node = &Q.Node
Permission = &Q.Permission
Product = &Q.Product
@@ -72,6 +76,8 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
AdminRolePermissionLink: newAdminRolePermissionLink(db, opts...),
Bill: newBill(db, opts...),
Channel: newChannel(db, opts...),
Client: newClient(db, opts...),
ClientPermissionLink: newClientPermissionLink(db, opts...),
Node: newNode(db, opts...),
Permission: newPermission(db, opts...),
Product: newProduct(db, opts...),
@@ -98,6 +104,8 @@ type Query struct {
AdminRolePermissionLink adminRolePermissionLink
Bill bill
Channel channel
Client client
ClientPermissionLink clientPermissionLink
Node node
Permission permission
Product product
@@ -125,6 +133,8 @@ func (q *Query) clone(db *gorm.DB) *Query {
AdminRolePermissionLink: q.AdminRolePermissionLink.clone(db),
Bill: q.Bill.clone(db),
Channel: q.Channel.clone(db),
Client: q.Client.clone(db),
ClientPermissionLink: q.ClientPermissionLink.clone(db),
Node: q.Node.clone(db),
Permission: q.Permission.clone(db),
Product: q.Product.clone(db),
@@ -159,6 +169,8 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
AdminRolePermissionLink: q.AdminRolePermissionLink.replaceDB(db),
Bill: q.Bill.replaceDB(db),
Channel: q.Channel.replaceDB(db),
Client: q.Client.replaceDB(db),
ClientPermissionLink: q.ClientPermissionLink.replaceDB(db),
Node: q.Node.replaceDB(db),
Permission: q.Permission.replaceDB(db),
Product: q.Product.replaceDB(db),
@@ -183,6 +195,8 @@ type queryCtx struct {
AdminRolePermissionLink *adminRolePermissionLinkDo
Bill *billDo
Channel *channelDo
Client *clientDo
ClientPermissionLink *clientPermissionLinkDo
Node *nodeDo
Permission *permissionDo
Product *productDo
@@ -207,6 +221,8 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
AdminRolePermissionLink: q.AdminRolePermissionLink.WithContext(ctx),
Bill: q.Bill.WithContext(ctx),
Channel: q.Channel.WithContext(ctx),
Client: q.Client.WithContext(ctx),
ClientPermissionLink: q.ClientPermissionLink.WithContext(ctx),
Node: q.Node.WithContext(ctx),
Permission: q.Permission.WithContext(ctx),
Product: q.Product.WithContext(ctx),

View File

@@ -43,7 +43,7 @@ func newUser(db *gorm.DB, opts ...gen.DOOption) user {
_user.ContactQq = field.NewString(tableName, "contact_qq")
_user.ContactWechat = field.NewString(tableName, "contact_wechat")
_user.LastLogin = field.NewTime(tableName, "last_login")
_user.LastLoginAddr = field.NewString(tableName, "last_login_addr")
_user.LastLoginHost = field.NewString(tableName, "last_login_host")
_user.LastLoginAgent = field.NewString(tableName, "last_login_agent")
_user.CreatedAt = field.NewTime(tableName, "created_at")
_user.UpdatedAt = field.NewTime(tableName, "updated_at")
@@ -58,27 +58,27 @@ type user struct {
userDo
ALL field.Asterisk
ID field.Int32
AdminID field.Int32
Phone field.String
Username field.String
ID field.Int32 // 用户ID
AdminID field.Int32 // 管理员ID
Phone field.String // 手机号码
Username field.String // 用户名
Email field.String
Password field.String
Name field.String
Avatar field.String
Status field.Int32
Balance field.Float64
IDType field.Int32
IDNo field.String
IDToken field.String
ContactQq field.String
ContactWechat field.String
LastLogin field.Time
LastLoginAddr field.String
LastLoginAgent field.String
CreatedAt field.Time
UpdatedAt field.Time
DeletedAt field.Field
Password field.String // 用户密码
Name field.String // 真实姓名
Avatar field.String // 头像URL
Status field.Int32 // 用户状态1-正常0-禁用
Balance field.Float64 // 账户余额
IDType field.Int32 // 认证类型0-未认证1-个人认证2-企业认证
IDNo field.String // 身份证号或营业执照号
IDToken field.String // 身份验证标识
ContactQq field.String // QQ联系方式
ContactWechat field.String // 微信联系方式
LastLogin field.Time // 最后登录时间
LastLoginHost field.String // 最后登录地址
LastLoginAgent field.String // 最后登录代理
CreatedAt field.Time // 创建时间
UpdatedAt field.Time // 更新时间
DeletedAt field.Field // 删除时间
fieldMap map[string]field.Expr
}
@@ -111,7 +111,7 @@ func (u *user) updateTableName(table string) *user {
u.ContactQq = field.NewString(table, "contact_qq")
u.ContactWechat = field.NewString(table, "contact_wechat")
u.LastLogin = field.NewTime(table, "last_login")
u.LastLoginAddr = field.NewString(table, "last_login_addr")
u.LastLoginHost = field.NewString(table, "last_login_host")
u.LastLoginAgent = field.NewString(table, "last_login_agent")
u.CreatedAt = field.NewTime(table, "created_at")
u.UpdatedAt = field.NewTime(table, "updated_at")
@@ -149,7 +149,7 @@ func (u *user) fillFieldMap() {
u.fieldMap["contact_qq"] = u.ContactQq
u.fieldMap["contact_wechat"] = u.ContactWechat
u.fieldMap["last_login"] = u.LastLogin
u.fieldMap["last_login_addr"] = u.LastLoginAddr
u.fieldMap["last_login_host"] = u.LastLoginHost
u.fieldMap["last_login_agent"] = u.LastLoginAgent
u.fieldMap["created_at"] = u.CreatedAt
u.fieldMap["updated_at"] = u.UpdatedAt

View File

@@ -29,7 +29,7 @@ func newWhitelist(db *gorm.DB, opts ...gen.DOOption) whitelist {
_whitelist.ALL = field.NewAsterisk(tableName)
_whitelist.ID = field.NewInt32(tableName, "id")
_whitelist.UserID = field.NewInt32(tableName, "user_id")
_whitelist.Address = field.NewString(tableName, "address")
_whitelist.Host = field.NewString(tableName, "host")
_whitelist.CreatedAt = field.NewTime(tableName, "created_at")
_whitelist.UpdatedAt = field.NewTime(tableName, "updated_at")
_whitelist.DeletedAt = field.NewField(tableName, "deleted_at")
@@ -45,7 +45,7 @@ type whitelist struct {
ALL field.Asterisk
ID field.Int32 // 白名单ID
UserID field.Int32 // 用户ID
Address field.String // IP地址
Host field.String // IP地址
CreatedAt field.Time // 创建时间
UpdatedAt field.Time // 更新时间
DeletedAt field.Field // 删除时间
@@ -67,7 +67,7 @@ func (w *whitelist) updateTableName(table string) *whitelist {
w.ALL = field.NewAsterisk(table)
w.ID = field.NewInt32(table, "id")
w.UserID = field.NewInt32(table, "user_id")
w.Address = field.NewString(table, "address")
w.Host = field.NewString(table, "host")
w.CreatedAt = field.NewTime(table, "created_at")
w.UpdatedAt = field.NewTime(table, "updated_at")
w.DeletedAt = field.NewField(table, "deleted_at")
@@ -90,7 +90,7 @@ func (w *whitelist) fillFieldMap() {
w.fieldMap = make(map[string]field.Expr, 6)
w.fieldMap["id"] = w.ID
w.fieldMap["user_id"] = w.UserID
w.fieldMap["address"] = w.Address
w.fieldMap["host"] = w.Host
w.fieldMap["created_at"] = w.CreatedAt
w.fieldMap["updated_at"] = w.UpdatedAt
w.fieldMap["deleted_at"] = w.DeletedAt

View File

@@ -1,9 +1,21 @@
package web
import (
"platform/web/handlers"
"github.com/gofiber/fiber/v2"
)
func UseRoute(app *fiber.App) {
func ApplyRouters(app *fiber.App) {
api := app.Group("/api")
// 认证路由
auth := api.Group("/auth")
auth.Post("/verify/sms", Protect(), handlers.SmsCode)
auth.Post("/login/sms", Protect(), handlers.Login)
auth.Post("/token", handlers.Token)
// 客户端路由
client := api.Group("/client")
client.Get("/test/create", handlers.CreateClient)
}

85
web/services/auth.go Normal file
View File

@@ -0,0 +1,85 @@
package services
import (
"context"
"errors"
"platform/web/models"
)
var Auth = &authService{}
type authService struct{}
type AuthServiceError string
func (e AuthServiceError) Error() string {
return string(e)
}
type AuthServiceOauthError string
func (e AuthServiceOauthError) Error() string {
return string(e)
}
var (
ErrOauthInvalidRequest = AuthServiceOauthError("invalid_request")
ErrOauthInvalidClient = AuthServiceOauthError("invalid_client")
ErrOauthInvalidGrant = AuthServiceOauthError("invalid_grant")
ErrOauthInvalidScope = AuthServiceOauthError("invalid_scope")
ErrOauthUnauthorizedClient = AuthServiceOauthError("unauthorized_client")
ErrOauthUnsupportedGrantType = AuthServiceOauthError("unsupported_grant_type")
)
// OauthAuthorizationCode 验证授权码
func (s *authService) OauthAuthorizationCode(ctx context.Context, client *models.Client, code, redirectURI, codeVerifier string) (*TokenDetails, error) {
// TODO: 从数据库验证授权码
return nil, errors.New("TODO")
}
// OauthClientCredentials 验证客户端凭证
func (s *authService) OauthClientCredentials(ctx context.Context, client *models.Client, scope ...[]string) (*TokenDetails, error) {
var clientType PayloadType
switch client.Spec {
case 0:
clientType = PayloadClientConfidential
case 1:
clientType = PayloadClientPublic
case 2:
clientType = PayloadClientConfidential
}
// 保存会话并返回令牌
auth := AuthContext{
Permissions: map[string]struct{}{
"client": {},
},
Payload: Payload{
Type: clientType,
Id: client.ID,
},
}
// todo 数据库定义会话持续时间
token, err := Session.Create(ctx, auth)
if err != nil {
return nil, err
}
return token, nil
}
// OauthRefreshToken 验证刷新令牌
func (s *authService) OauthRefreshToken(ctx context.Context, client *models.Client, refreshToken string, scope ...[]string) (*TokenDetails, error) {
// TODO: 从数据库验证刷新令牌
return nil, errors.New("TODO")
}
type GrantType int
const (
GrantTypeAuthorizationCode GrantType = iota
GrantTypeClientCredentials
GrantTypeRefreshToken
)

288
web/services/session.go Normal file
View File

@@ -0,0 +1,288 @@
package services
import (
"context"
"encoding/json"
"errors"
"fmt"
"platform/init/rds"
"time"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
)
// region SessionService
var Session = &sessionService{}
type sessionService struct {
}
type SessionServiceError string
func (e SessionServiceError) Error() string {
return string(e)
}
var (
ErrInvalidToken = SessionServiceError("invalid_token")
)
// Find 通过访问令牌获取会话信息
func (s *sessionService) Find(ctx context.Context, token string) (*AuthContext, error) {
// 读取认证数据
authJSON, err := rds.Client.Get(ctx, accessKey(token)).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return nil, ErrInvalidToken
}
return nil, err
}
// 反序列化
auth := new(AuthContext)
if err := json.Unmarshal([]byte(authJSON), auth); err != nil {
return nil, err
}
return auth, nil
}
// Create 创建一个新的会话
func (s *sessionService) Create(ctx context.Context, auth AuthContext, config ...SessionConfig) (*TokenDetails, error) {
// 解析可选配置
cfg := DefaultSessionConfig
if len(config) > 0 {
cfg = mergeConfig(DefaultSessionConfig, config[0])
}
// 生成令牌组
accessToken := genToken()
refreshToken := genToken()
// 序列化认证数据
authData, err := json.Marshal(auth)
if err != nil {
return nil, err
}
// 序列化刷新令牌数据
refreshData, err := json.Marshal(RefreshData{
AuthContext: auth,
AccessToken: accessToken,
})
if err != nil {
return nil, err
}
// 事务保存数据到 Redis
pipe := rds.Client.TxPipeline()
pipe.Set(ctx, accessKey(accessToken), authData, cfg.AccessTokenDuration)
pipe.Set(ctx, refreshKey(refreshToken), refreshData, cfg.RefreshTokenDuration)
_, err = pipe.Exec(ctx)
if err != nil {
return nil, err
}
return &TokenDetails{
AccessToken: accessToken,
AccessTokenExpires: time.Now().Add(cfg.AccessTokenDuration),
RefreshToken: refreshToken,
RefreshTokenExpires: time.Now().Add(cfg.RefreshTokenDuration),
Auth: auth,
}, nil
}
// Refresh 刷新一个会话
func (s *sessionService) Refresh(ctx context.Context, refreshToken string, config ...SessionConfig) (*TokenDetails, error) {
// 解析可选配置
cfg := DefaultSessionConfig
if len(config) > 0 {
cfg = mergeConfig(DefaultSessionConfig, config[0])
}
rKey := refreshKey(refreshToken)
var tokenDetails *TokenDetails
// 刷新令牌
err := rds.Client.Watch(ctx, func(tx *redis.Tx) error {
// 先获取刷新令牌数据
refreshJson, err := tx.Get(ctx, rKey).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return ErrInvalidToken
}
return err
}
// 解析刷新令牌数据
refreshData := new(RefreshData)
if err := json.Unmarshal([]byte(refreshJson), refreshData); err != nil {
return err
}
// 删除旧的令牌
pipeline := tx.Pipeline()
pipeline.Del(ctx, accessKey(refreshData.AccessToken))
pipeline.Del(ctx, refreshKey(refreshToken))
// 生成新的令牌
newAccessToken := genToken()
newRefreshToken := genToken()
authData, err := json.Marshal(refreshData.AuthContext)
if err != nil {
return err
}
newRefreshData, err := json.Marshal(RefreshData{
AuthContext: refreshData.AuthContext,
AccessToken: newAccessToken,
})
if err != nil {
return err
}
pipeline.Set(ctx, accessKey(newAccessToken), authData, cfg.AccessTokenDuration)
pipeline.Set(ctx, refreshKey(newRefreshToken), newRefreshData, cfg.RefreshTokenDuration)
_, err = pipeline.Exec(ctx)
if err != nil {
return err
}
tokenDetails = &TokenDetails{
AccessToken: newAccessToken,
RefreshToken: newRefreshToken,
AccessTokenExpires: time.Now().Add(cfg.AccessTokenDuration),
RefreshTokenExpires: time.Now().Add(cfg.RefreshTokenDuration),
Auth: refreshData.AuthContext,
}
return nil
}, rKey)
if err != nil {
return nil, fmt.Errorf("刷新令牌失败: %w", err)
}
return tokenDetails, nil
}
// Remove 删除会话
func (s *sessionService) Remove(ctx context.Context, accessToken, refreshToken string) error {
rds.Client.Del(ctx, accessKey(accessToken), refreshKey(refreshToken))
return nil
}
// 令牌键的格式为 "session:<token>"
func accessKey(token string) string {
return fmt.Sprintf("session:%s", token)
}
// 刷新令牌键的格式为 "session:refreshKey:<token>"
func refreshKey(token string) string {
return fmt.Sprintf("session:refresh:%s", token)
}
// 生成一个新的令牌
func genToken() string {
return uuid.NewString()
}
// endregion
// region SessionConfig
// SessionConfig 定义会话管理的配置选项
type SessionConfig struct {
// 令牌配置
AccessTokenDuration time.Duration
RefreshTokenDuration time.Duration
}
// DefaultSessionConfig 默认会话配置
var DefaultSessionConfig = SessionConfig{
AccessTokenDuration: 2 * time.Hour,
RefreshTokenDuration: 7 * 24 * time.Hour,
}
// 合并配置,保留非零值
func mergeConfig(defaultCfg SessionConfig, customCfg SessionConfig) SessionConfig {
result := defaultCfg
if customCfg.AccessTokenDuration != 0 {
result.AccessTokenDuration = customCfg.AccessTokenDuration
}
if customCfg.RefreshTokenDuration != 0 {
result.RefreshTokenDuration = customCfg.RefreshTokenDuration
}
return result
}
// endregion
// region AuthContext
// AuthContext 定义认证信息
type AuthContext struct {
Payload Payload
Permissions map[string]struct{}
Metadata map[string]interface{}
}
// Payload 定义负载信息
type Payload struct {
Type PayloadType
Id int32
}
// PayloadType 定义负载类型
type PayloadType int
const (
// PayloadUser 用户类型
PayloadUser PayloadType = iota
// PayloadAdmin 管理员类型
PayloadAdmin
// PayloadClientPublic 公共客户端类型
PayloadClientPublic
// PayloadClientConfidential 机密客户端类型
PayloadClientConfidential
)
// AnyPermission 检查认证是否包含指定权限
func (a *AuthContext) AnyPermission(requiredPermission ...string) bool {
if a == nil || a.Permissions == nil {
return false
}
for _, permission := range requiredPermission {
if _, ok := a.Permissions[permission]; ok {
return true
}
}
return false
}
// endregion
type RefreshData struct {
AuthContext AuthContext
AccessToken string
}
// TokenDetails 存储令牌详细信息
type TokenDetails struct {
// 访问令牌
AccessToken string
// 刷新令牌
RefreshToken string
// 访问令牌过期时间
AccessTokenExpires time.Time
// 刷新令牌过期时间
RefreshTokenExpires time.Time
// 认证信息
Auth AuthContext
}

124
web/services/verifier.go Normal file
View File

@@ -0,0 +1,124 @@
package services
import (
"context"
"errors"
"fmt"
"log/slog"
"math/rand"
"platform/init/rds"
"strconv"
"time"
"github.com/redis/go-redis/v9"
)
var Verifier = &verifierService{}
type verifierService struct {
}
type VerifierServiceError string
func (e VerifierServiceError) Error() string {
return string(e)
}
var (
ErrVerifierServiceInvalid = VerifierServiceError("验证码错误")
)
type VerifierServiceSendLimitErr int
func (e VerifierServiceSendLimitErr) Error() string {
return "发送频率过快"
}
type VerifierSmsPurpose int
const (
Login VerifierSmsPurpose = iota
)
func smsKey(phone string, purpose VerifierSmsPurpose) string {
return fmt.Sprintf("verify:sms:%d:%s", purpose, phone)
}
func (s *verifierService) SendSms(ctx context.Context, phone string, purpose VerifierSmsPurpose) error {
key := smsKey(phone, purpose)
keyLock := key + ":lock"
// 生成验证码
code := rand.Intn(900000) + 100000 // 6-digit code between 100000-999999
// 检查发送频率1 分钟内只能发送一次
err := rds.Client.Watch(ctx, func(tx *redis.Tx) error {
result, err := tx.TTL(ctx, keyLock).Result()
if err != nil {
return err
}
if result > 0 {
return VerifierServiceSendLimitErr(result.Seconds())
}
if result != -2 {
return VerifierServiceError("验证码检查异常")
}
pipe := rds.Client.Pipeline()
pipe.Set(ctx, key, code, 10*time.Minute)
pipe.Set(ctx, keyLock, "", 1*time.Minute)
_, err = pipe.Exec(ctx)
if err != nil {
return err
}
return nil
}, keyLock)
if err != nil {
return err
}
// TODO: 发送短信验证码
slog.Debug("发送验证码", slog.String("phone", phone), slog.String("code", strconv.Itoa(code)))
return nil
}
func (s *verifierService) VerifySms(ctx context.Context, phone, code string) (bool, error) {
key := smsKey(phone, Login)
keyLock := key + ":lock"
err := rds.Client.Watch(ctx, func(tx *redis.Tx) error {
// 检查验证码
val, err := rds.Client.Get(ctx, key).Result()
if err != nil && !errors.Is(err, redis.Nil) {
slog.Error("验证码获取失败", slog.Any("err", err))
return err
}
if val != code {
return ErrVerifierServiceInvalid
}
// 删除验证码
_, err = tx.Pipelined(ctx, func(pipe redis.Pipeliner) error {
pipe.Del(ctx, key)
pipe.Del(ctx, keyLock)
return nil
})
if err != nil {
slog.Error("验证码删除失败", slog.Any("err", err))
return err
}
return nil
}, key)
if err != nil {
if errors.Is(err, ErrVerifierServiceInvalid) {
return false, nil
}
return false, err
}
return true, nil
}

View File

@@ -1,5 +0,0 @@
package utils
type ErrResp struct {
Cause string `json:"cause"`
}

View File

@@ -1,18 +1,20 @@
package web
import (
"platform/init/env"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/requestid"
)
import "log/slog"
type Config struct {
Logger *slog.Logger
Listen string
}
type Server struct {
config *Config
log *slog.Logger
fiber *fiber.App
}
@@ -22,33 +24,35 @@ func New(config *Config) (*Server, error) {
_config = &Config{}
}
if _config.Logger == nil {
_config.Logger = slog.Default()
}
return &Server{
config: _config,
log: _config.Logger,
}, nil
}
func (s *Server) Run() error {
s.fiber = fiber.New(fiber.Config{})
UseRoute(s.fiber)
s.fiber = fiber.New(fiber.Config{
ErrorHandler: ErrorHandler,
})
s.log.Info("Server started on :8080")
err := s.fiber.Listen(":8080")
s.fiber.Use(requestid.New())
s.fiber.Use(logger.New())
ApplyRouters(s.fiber)
port := env.AppPort
slog.Info("Server started on :" + port)
err := s.fiber.Listen(":" + port)
if err != nil {
s.log.Error("Failed to start server", slog.Any("error", err))
slog.Error("Failed to start server", slog.Any("err", err))
}
s.log.Info("Server stopped")
slog.Info("Server stopped")
return nil
}
func (s *Server) Stop() {
err := s.fiber.Shutdown()
if err != nil {
s.log.Error("Failed to shutdown server", slog.Any("error", err))
slog.Error("Failed to shutdown server", slog.Any("err", err))
}
}