🎉: init project
66
internal/api/api.go
Normal file
@ -0,0 +1,66 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/nf"
|
||||
"time"
|
||||
"ultone/internal/handler"
|
||||
"ultone/internal/middleware/auth"
|
||||
"ultone/internal/middleware/front"
|
||||
"ultone/internal/middleware/logger"
|
||||
"ultone/internal/middleware/oplog"
|
||||
"ultone/internal/middleware/privilege"
|
||||
"ultone/internal/model"
|
||||
"ultone/internal/opt"
|
||||
)
|
||||
|
||||
func initApp(ctx context.Context) *nf.App {
|
||||
engine := nf.New(nf.Config{DisableLogger: true})
|
||||
engine.Use(logger.New())
|
||||
|
||||
// todo: add project prefix, if you need
|
||||
// for example: app := engine.Group("/api/{project}")
|
||||
app := engine.Group("/api")
|
||||
app.Get("/available", func(c *nf.Ctx) error {
|
||||
return c.JSON(nf.Map{"status": 200, "ok": true, "time": time.Now()})
|
||||
})
|
||||
|
||||
{
|
||||
api := app.Group("/user")
|
||||
api.Post("/auth/login", oplog.NewOpLog(ctx), handler.AuthLogin)
|
||||
api.Get("/auth/login", auth.NewAuth(), handler.AuthVerify)
|
||||
api.Post("/auth/logout", auth.NewAuth(), oplog.NewOpLog(ctx), handler.AuthLogout)
|
||||
|
||||
mng := api.Group("/manage")
|
||||
mng.Use(auth.NewAuth(), privilege.Verify(
|
||||
privilege.RelationAnd,
|
||||
model.PrivilegeUserManage,
|
||||
))
|
||||
|
||||
mng.Get("/user/list", handler.ManageUserList)
|
||||
mng.Post("/user/create", oplog.NewOpLog(ctx), handler.ManageUserCreate)
|
||||
mng.Post("/user/update", oplog.NewOpLog(ctx), handler.ManageUserUpdate)
|
||||
mng.Post("/user/delete", oplog.NewOpLog(ctx), handler.ManageUserDelete)
|
||||
}
|
||||
|
||||
{
|
||||
api := app.Group("/log")
|
||||
api.Use(auth.NewAuth(), privilege.Verify(privilege.RelationAnd, model.PrivilegeOpLog))
|
||||
api.Get("/category/list", handler.LogCategories())
|
||||
api.Get("/content/list", handler.LogList)
|
||||
}
|
||||
|
||||
{
|
||||
// todo: 替换 xxx
|
||||
// todo: 这里写你的模块和接口
|
||||
api := app.Group("/xxx")
|
||||
api.Use(auth.NewAuth())
|
||||
_ = api // todo: 添加自己的接口后删除该行
|
||||
}
|
||||
|
||||
if opt.EnableFront {
|
||||
engine.Use(front.NewFront(&front.DefaultFront, "dist/front/browser"))
|
||||
}
|
||||
|
||||
return engine
|
||||
}
|
43
internal/api/start.go
Normal file
@ -0,0 +1,43 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"net"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
func Start(ctx context.Context) error {
|
||||
|
||||
app := initApp(ctx)
|
||||
ready := make(chan bool)
|
||||
|
||||
ln, err := net.Listen("tcp", opt.Cfg.Listen.Http)
|
||||
if err != nil {
|
||||
return fmt.Errorf("api.MustStart: net listen tcp address=%v err=%v", opt.Cfg.Listen.Http, err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
ready <- true
|
||||
|
||||
if err = app.RunListener(ln); err != nil {
|
||||
log.Panic("api.MustStart: app run err=%v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
<-ready
|
||||
|
||||
go func() {
|
||||
ready <- true
|
||||
<-ctx.Done()
|
||||
if err = app.Shutdown(tool.Timeout(1)); err != nil {
|
||||
log.Error("api.MustStart: app shutdown err=%v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
<-ready
|
||||
|
||||
return nil
|
||||
}
|
37
internal/cmd/execute.go
Normal file
@ -0,0 +1,37 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"ultone/internal/api"
|
||||
"ultone/internal/controller"
|
||||
"ultone/internal/database/cache"
|
||||
"ultone/internal/database/db"
|
||||
"ultone/internal/model"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
var (
|
||||
filename string
|
||||
)
|
||||
|
||||
func Execute(ctx context.Context) error {
|
||||
|
||||
tool.Must(opt.Init(filename))
|
||||
tool.Must(db.Init())
|
||||
tool.Must(cache.Init())
|
||||
|
||||
// todo: if elastic search required
|
||||
// tool.Must(es.Init())
|
||||
|
||||
// todo: if nebula required
|
||||
// tool.Must(nebula.Init(ctx, opt.Cfg.Nebula))
|
||||
|
||||
tool.Must(model.Init(db.New()))
|
||||
tool.Must(controller.Init())
|
||||
tool.Must(api.Start(ctx))
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
return nil
|
||||
}
|
16
internal/cmd/init.go
Normal file
@ -0,0 +1,16 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"time"
|
||||
"ultone/internal/opt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
time.Local = time.FixedZone("CST", 8*3600)
|
||||
|
||||
flag.StringVar(&filename, "c", "etc/config.json", "config json file path")
|
||||
flag.BoolVar(&opt.Debug, "debug", false, "")
|
||||
|
||||
flag.Parse()
|
||||
}
|
12
internal/controller/impl.go
Normal file
@ -0,0 +1,12 @@
|
||||
package controller
|
||||
|
||||
var (
|
||||
// UserController todo: 可以实现自己的 controller
|
||||
UserController userController
|
||||
)
|
||||
|
||||
func Init() error {
|
||||
UserController = uc{}
|
||||
|
||||
return nil
|
||||
}
|
154
internal/controller/user.go
Normal file
@ -0,0 +1,154 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"github.com/spf13/cast"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
"time"
|
||||
"ultone/internal/database/cache"
|
||||
"ultone/internal/database/db"
|
||||
"ultone/internal/log"
|
||||
"ultone/internal/model"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
type userController interface {
|
||||
GetUser(c *nf.Ctx, id uint64) (*model.User, error)
|
||||
GetUserByToken(c *nf.Ctx, token string) (*model.User, error)
|
||||
CacheUser(c *nf.Ctx, user *model.User) error
|
||||
CacheToken(c *nf.Ctx, token string, user *model.User) error
|
||||
RmUserCache(c *nf.Ctx, id uint64) error
|
||||
DeleteUser(c *nf.Ctx, id uint64) error
|
||||
}
|
||||
|
||||
type uc struct{}
|
||||
|
||||
var _ userController = (*uc)(nil)
|
||||
|
||||
func (u uc) GetUser(c *nf.Ctx, id uint64) (*model.User, error) {
|
||||
var (
|
||||
err error
|
||||
target = new(model.User)
|
||||
key = fmt.Sprintf("%s:user:id:%d", opt.CachePrefix, id)
|
||||
bs []byte
|
||||
)
|
||||
|
||||
if opt.EnableUserCache {
|
||||
if bs, err = cache.Client.Get(tool.Timeout(3), key); err != nil {
|
||||
log.Warn(c, "controller.GetUser: get user by cache key=%s err=%v", key, err)
|
||||
goto ByDB
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(bs, target); err != nil {
|
||||
log.Warn(c, "controller.GetUser: json unmarshal key=%s by=%s err=%v", key, string(bs), err)
|
||||
goto ByDB
|
||||
}
|
||||
|
||||
return target, nil
|
||||
}
|
||||
|
||||
ByDB:
|
||||
if err = db.New(tool.Timeout(3)).
|
||||
Model(&model.User{}).
|
||||
Where("id = ?", id).
|
||||
Take(target).
|
||||
Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// tips: 公开项目需要考虑击穿处理
|
||||
return target, resp.NewError(400, "目标不存在", err, nil)
|
||||
}
|
||||
|
||||
return target, resp.NewError(500, "", err, nil)
|
||||
}
|
||||
|
||||
if opt.EnableUserCache {
|
||||
if err = u.CacheUser(c, target); err != nil {
|
||||
log.Warn(c, "controller.GetUser: cache user key=%s err=%v", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func (u uc) GetUserByToken(c *nf.Ctx, token string) (*model.User, error) {
|
||||
strs := strings.Split(token, ".")
|
||||
if len(strs) != 3 {
|
||||
return nil, fmt.Errorf("controller.GetUserByToken: jwt token invalid, token=%s", token)
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s:user:token:%s", opt.CachePrefix, strs[2])
|
||||
bs, err := cache.Client.Get(tool.Timeout(3), key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debug(c, "controller.GetUserByToken: key=%s cache bytes=%s", key, string(bs))
|
||||
|
||||
userId := cast.ToUint64(string(bs))
|
||||
if userId == 0 {
|
||||
return nil, fmt.Errorf("controller.GetUserByToken: bs=%s cast to uint64 err", string(bs))
|
||||
}
|
||||
|
||||
var op *model.User
|
||||
|
||||
if op, err = u.GetUser(c, userId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return op, nil
|
||||
}
|
||||
|
||||
func (u uc) CacheUser(c *nf.Ctx, target *model.User) error {
|
||||
key := fmt.Sprintf("%s:user:id:%d", opt.CachePrefix, target.Id)
|
||||
return cache.Client.Set(tool.Timeout(3), key, target)
|
||||
}
|
||||
|
||||
func (u uc) CacheToken(c *nf.Ctx, token string, user *model.User) error {
|
||||
strs := strings.Split(token, ".")
|
||||
if len(strs) != 3 {
|
||||
return fmt.Errorf("controller.CacheToken: jwt token invalid")
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s:user:token:%s", opt.CachePrefix, strs[2])
|
||||
return cache.Client.SetEx(tool.Timeout(3), key, user.Id, opt.TokenTimeout)
|
||||
}
|
||||
func (u uc) RmUserCache(c *nf.Ctx, id uint64) error {
|
||||
key := fmt.Sprintf("%s:user:id:%d", opt.CachePrefix, id)
|
||||
return cache.Client.Del(tool.Timeout(3), key)
|
||||
}
|
||||
|
||||
func (u uc) DeleteUser(c *nf.Ctx, id uint64) error {
|
||||
var (
|
||||
err error
|
||||
now = time.Now()
|
||||
username = "CONCAT(username, '@del')"
|
||||
)
|
||||
|
||||
if opt.Cfg.DB.Type == "sqlite" {
|
||||
username = "username || '@del'"
|
||||
}
|
||||
|
||||
if err = db.New(tool.Timeout(5)).
|
||||
Model(&model.User{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]any{
|
||||
"deleted_at": now.UnixMilli(),
|
||||
"username": gorm.Expr(username),
|
||||
}).Error; err != nil {
|
||||
return resp.NewError(500, "", err, nil)
|
||||
}
|
||||
|
||||
if opt.EnableUserCache {
|
||||
if err = u.RmUserCache(c, id); err != nil {
|
||||
log.Warn(c, "controller.DeleteUser: rm user=%d cache err=%v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
109
internal/database/cache/cache_lru.go
vendored
Normal file
@ -0,0 +1,109 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/hashicorp/golang-lru/v2/expirable"
|
||||
_ "github.com/hashicorp/golang-lru/v2/expirable"
|
||||
"time"
|
||||
"ultone/internal/interfaces"
|
||||
)
|
||||
|
||||
var _ interfaces.Cacher = (*_lru)(nil)
|
||||
|
||||
type _lru struct {
|
||||
client *expirable.LRU[string, *_lru_value]
|
||||
}
|
||||
|
||||
type _lru_value struct {
|
||||
duration time.Duration
|
||||
last time.Time
|
||||
bs []byte
|
||||
}
|
||||
|
||||
func (l *_lru) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
v, ok := l.client.Get(key)
|
||||
if !ok {
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
if v.duration == 0 {
|
||||
return v.bs, nil
|
||||
}
|
||||
|
||||
if time.Now().Sub(v.last) > v.duration {
|
||||
l.client.Remove(key)
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
return v.bs, nil
|
||||
}
|
||||
|
||||
func (l *_lru) GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error) {
|
||||
v, ok := l.client.Get(key)
|
||||
if !ok {
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
if v.duration == 0 {
|
||||
return v.bs, nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
if now.Sub(v.last) > v.duration {
|
||||
l.client.Remove(key)
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
l.client.Add(key, &_lru_value{
|
||||
duration: duration,
|
||||
last: now,
|
||||
bs: v.bs,
|
||||
})
|
||||
|
||||
return v.bs, nil
|
||||
}
|
||||
|
||||
func (l *_lru) Set(ctx context.Context, key string, value any) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.client.Add(key, &_lru_value{
|
||||
duration: 0,
|
||||
last: time.Now(),
|
||||
bs: bs,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *_lru) SetEx(ctx context.Context, key string, value any, duration time.Duration) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.client.Add(key, &_lru_value{
|
||||
duration: duration,
|
||||
last: time.Now(),
|
||||
bs: bs,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *_lru) Del(ctx context.Context, keys ...string) error {
|
||||
for _, key := range keys {
|
||||
l.client.Remove(key)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newLRUCache() (interfaces.Cacher, error) {
|
||||
client := expirable.NewLRU[string, *_lru_value](0, nil, 0)
|
||||
|
||||
return &_lru{client: client}, nil
|
||||
}
|
65
internal/database/cache/cache_memory.go
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
"ultone/internal/interfaces"
|
||||
|
||||
"gitea.com/taozitaozi/gredis"
|
||||
)
|
||||
|
||||
var _ interfaces.Cacher = (*_mem)(nil)
|
||||
|
||||
type _mem struct {
|
||||
client *gredis.Gredis
|
||||
}
|
||||
|
||||
func (m *_mem) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
v, err := m.client.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bs, ok := v.([]byte)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid value type=%T", v)
|
||||
}
|
||||
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
func (m *_mem) GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error) {
|
||||
v, err := m.client.GetEx(key, duration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bs, ok := v.([]byte)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid value type=%T", v)
|
||||
}
|
||||
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
func (m *_mem) Set(ctx context.Context, key string, value any) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.client.Set(key, bs)
|
||||
}
|
||||
|
||||
func (m *_mem) SetEx(ctx context.Context, key string, value any, duration time.Duration) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.client.SetEx(key, bs, duration)
|
||||
}
|
||||
|
||||
func (m *_mem) Del(ctx context.Context, keys ...string) error {
|
||||
m.client.Delete(keys...)
|
||||
return nil
|
||||
}
|
54
internal/database/cache/cache_redis.go
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"time"
|
||||
)
|
||||
|
||||
type _redis struct {
|
||||
client *redis.Client
|
||||
}
|
||||
|
||||
func (r *_redis) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
result, err := r.client.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(result), nil
|
||||
}
|
||||
|
||||
func (r *_redis) GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error) {
|
||||
result, err := r.client.GetEx(ctx, key, duration).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(result), nil
|
||||
}
|
||||
|
||||
func (r *_redis) Set(ctx context.Context, key string, value any) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = r.client.Set(ctx, key, bs, redis.KeepTTL).Result()
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *_redis) SetEx(ctx context.Context, key string, value any, duration time.Duration) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = r.client.SetEX(ctx, key, bs, duration).Result()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *_redis) Del(ctx context.Context, keys ...string) error {
|
||||
return r.client.Del(ctx, keys...).Err()
|
||||
}
|
33
internal/database/cache/client.go
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"ultone/internal/interfaces"
|
||||
)
|
||||
|
||||
var (
|
||||
Client interfaces.Cacher
|
||||
)
|
||||
|
||||
type encoded_value interface {
|
||||
MarshalBinary() ([]byte, error)
|
||||
}
|
||||
|
||||
type decoded_value interface {
|
||||
UnmarshalBinary(bs []byte) error
|
||||
}
|
||||
|
||||
func handleValue(value any) ([]byte, error) {
|
||||
var (
|
||||
bs []byte
|
||||
err error
|
||||
)
|
||||
|
||||
if imp, ok := value.(encoded_value); ok {
|
||||
bs, err = imp.MarshalBinary()
|
||||
} else {
|
||||
bs, err = json.Marshal(value)
|
||||
}
|
||||
|
||||
return bs, err
|
||||
}
|
7
internal/database/cache/error.go
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
package cache
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrorKeyNotFound = errors.New("key not found")
|
||||
)
|
70
internal/database/cache/init.go
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitea.com/taozitaozi/gredis"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"net/url"
|
||||
"strings"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
func Init() error {
|
||||
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
strs := strings.Split(opt.Cfg.Cache.Uri, "::")
|
||||
|
||||
switch strs[0] {
|
||||
case "memory":
|
||||
gc := gredis.NewGredis(1024 * 1024)
|
||||
Client = &_mem{client: gc}
|
||||
case "lru":
|
||||
if Client, err = newLRUCache(); err != nil {
|
||||
return err
|
||||
}
|
||||
case "redis":
|
||||
var (
|
||||
ins *url.URL
|
||||
err error
|
||||
)
|
||||
|
||||
if len(strs) != 2 {
|
||||
return fmt.Errorf("cache.Init: invalid cache uri: %s", opt.Cfg.Cache.Uri)
|
||||
}
|
||||
|
||||
uri := strs[1]
|
||||
|
||||
if !strings.Contains(uri, "://") {
|
||||
uri = fmt.Sprintf("redis://%s", uri)
|
||||
}
|
||||
|
||||
if ins, err = url.Parse(uri); err != nil {
|
||||
return fmt.Errorf("cache.Init: url parse cache uri: %s, err: %s", opt.Cfg.Cache.Uri, err.Error())
|
||||
}
|
||||
|
||||
addr := ins.Host
|
||||
username := ins.User.Username()
|
||||
password, _ := ins.User.Password()
|
||||
|
||||
var rc *redis.Client
|
||||
rc = redis.NewClient(&redis.Options{
|
||||
Addr: addr,
|
||||
Username: username,
|
||||
Password: password,
|
||||
})
|
||||
|
||||
if err = rc.Ping(tool.Timeout(5)).Err(); err != nil {
|
||||
return fmt.Errorf("cache.Init: redis ping err: %s", err.Error())
|
||||
}
|
||||
|
||||
Client = &_redis{client: rc}
|
||||
default:
|
||||
return fmt.Errorf("cache type %s not support", strs[0])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
38
internal/database/db/client.go
Normal file
@ -0,0 +1,38 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gorm.io/gorm"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
var (
|
||||
cli = &client{}
|
||||
)
|
||||
|
||||
type client struct {
|
||||
cli *gorm.DB
|
||||
ttype string
|
||||
}
|
||||
|
||||
func Type() string {
|
||||
return cli.ttype
|
||||
}
|
||||
|
||||
func New(ctxs ...context.Context) *gorm.DB {
|
||||
var ctx context.Context
|
||||
if len(ctxs) > 0 && ctxs[0] != nil {
|
||||
ctx = ctxs[0]
|
||||
} else {
|
||||
ctx = tool.Timeout(30)
|
||||
}
|
||||
|
||||
session := cli.cli.Session(&gorm.Session{Context: ctx})
|
||||
|
||||
if opt.Debug {
|
||||
session = session.Debug()
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
46
internal/database/db/init.go
Normal file
@ -0,0 +1,46 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
"ultone/internal/opt"
|
||||
)
|
||||
|
||||
func Init() error {
|
||||
strs := strings.Split(opt.Cfg.DB.Uri, "::")
|
||||
|
||||
if len(strs) != 2 {
|
||||
return fmt.Errorf("db.Init: opt db uri invalid: %s", opt.Cfg.DB.Uri)
|
||||
}
|
||||
|
||||
cli.ttype = strs[0]
|
||||
|
||||
var (
|
||||
err error
|
||||
dsn = strs[1]
|
||||
)
|
||||
|
||||
switch strs[0] {
|
||||
case "sqlite":
|
||||
opt.Cfg.DB.Type = "sqlite"
|
||||
cli.cli, err = gorm.Open(sqlite.Open(dsn))
|
||||
case "mysql":
|
||||
opt.Cfg.DB.Type = "mysql"
|
||||
cli.cli, err = gorm.Open(mysql.Open(dsn))
|
||||
case "postgres":
|
||||
opt.Cfg.DB.Type = "postgres"
|
||||
cli.cli, err = gorm.Open(postgres.Open(dsn))
|
||||
default:
|
||||
return fmt.Errorf("db type only support: [sqlite, mysql, postgres], unsupported db type: %s", strs[0])
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("db.Init: open %s with dsn:%s, err: %w", strs[0], dsn, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
29
internal/database/es/client.go
Normal file
@ -0,0 +1,29 @@
|
||||
package es
|
||||
|
||||
import (
|
||||
elastic "github.com/elastic/go-elasticsearch/v7"
|
||||
"github.com/loveuer/esgo2dump/xes/es7"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"net/url"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
var (
|
||||
Client *elastic.Client
|
||||
)
|
||||
|
||||
func Init() error {
|
||||
ins, err := url.Parse(opt.Cfg.ES.Uri)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug("es.InitClient url parse uri: %s, result: %+v", opt.Cfg.ES.Uri, ins)
|
||||
|
||||
if Client, err = es7.NewClient(tool.Timeout(10), ins); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
89
internal/database/mq/client.go
Normal file
@ -0,0 +1,89 @@
|
||||
package mq
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
"net/url"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Init - init mq client:
|
||||
// - @param.uri: "{scheme[amqp/amqps]}://{username}:{password}@{endpoint}/{virtual_host}"
|
||||
// - @param.certs: with amqps, certs[0]=client crt bytes, certs[0]=client key bytes
|
||||
|
||||
type _client struct {
|
||||
sync.Mutex
|
||||
uri string
|
||||
tlsCfg *tls.Config
|
||||
conn *amqp.Connection
|
||||
ch *amqp.Channel
|
||||
consume <-chan amqp.Delivery
|
||||
queue *queueOption
|
||||
}
|
||||
|
||||
func (c *_client) open() error {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
if c.tlsCfg != nil {
|
||||
c.conn, err = amqp.DialTLS(c.uri, c.tlsCfg)
|
||||
} else {
|
||||
c.conn, err = amqp.Dial(c.uri)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.ch, err = c.conn.Channel(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if client.queue != nil && client.queue.name != "" {
|
||||
if _, err = client.ch.QueueDeclare(
|
||||
client.queue.name,
|
||||
client.queue.durable,
|
||||
client.queue.autoDelete,
|
||||
client.queue.exclusive,
|
||||
client.queue.noWait,
|
||||
client.queue.args,
|
||||
); err != nil {
|
||||
return fmt.Errorf("declare queue: %s, err: %w", client.queue.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
client = &_client{
|
||||
uri: "amqp://guest:guest@127.0.0.1:5672/",
|
||||
tlsCfg: nil,
|
||||
}
|
||||
)
|
||||
|
||||
// Init - init mq client
|
||||
func Init(opts ...OptionFn) error {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
for _, fn := range opts {
|
||||
fn(client)
|
||||
}
|
||||
|
||||
if _, err = url.Parse(client.uri); err != nil {
|
||||
return fmt.Errorf("url parse uri err: %w", err)
|
||||
}
|
||||
|
||||
if err = client.open(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
111
internal/database/mq/client_test.go
Normal file
@ -0,0 +1,111 @@
|
||||
package mq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConsume(t *testing.T) {
|
||||
clientCert, err := tls.LoadX509KeyPair(
|
||||
"/Users/loveuer/codes/project/bifrost-pro/search_v3/internal/database/mq/tls/client.crt",
|
||||
"/Users/loveuer/codes/project/bifrost-pro/search_v3/internal/database/mq/tls/client.key",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
ca, err := os.ReadFile("/Users/loveuer/codes/project/bifrost-pro/search_v3/internal/database/mq/tls/ca.crt")
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
if !caCertPool.AppendCertsFromPEM(ca) {
|
||||
t.Fatal("ca pool append ca crt err")
|
||||
}
|
||||
|
||||
if err := Init(
|
||||
WithURI("amqps://admin:password@mq.dev:5671/export"),
|
||||
WithTLS(&tls.Config{
|
||||
Certificates: []tls.Certificate{clientCert},
|
||||
RootCAs: caCertPool,
|
||||
InsecureSkipVerify: true,
|
||||
}),
|
||||
WithQueueDeclare("export", false, false, false, false, amqp.Table{"x-max-priority": 100}),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
defer cancel()
|
||||
|
||||
ch, err := Consume(ctx, "export", &ConsumeOpt{MaxReconnection: -1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Log("[TEST] start consume msg")
|
||||
for msg := range ch {
|
||||
t.Logf("[TEST] [%s] [msg: %s]", time.Now().Format("060102T150405"), string(msg.Body))
|
||||
_ = msg.Ack(false)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublish(t *testing.T) {
|
||||
clientCert, err := tls.LoadX509KeyPair(
|
||||
"/Users/loveuer/codes/project/bifrost-pro/search_v3/internal/database/mq/tls/client.crt",
|
||||
"/Users/loveuer/codes/project/bifrost-pro/search_v3/internal/database/mq/tls/client.key",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
ca, err := os.ReadFile("/Users/loveuer/codes/project/bifrost-pro/search_v3/internal/database/mq/tls/ca.crt")
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
if !caCertPool.AppendCertsFromPEM(ca) {
|
||||
t.Fatal("ca pool append ca crt err")
|
||||
}
|
||||
|
||||
if err := Init(
|
||||
WithURI("amqps://admin:password@mq.dev:5671/export"),
|
||||
WithTLS(&tls.Config{
|
||||
Certificates: []tls.Certificate{clientCert},
|
||||
RootCAs: caCertPool,
|
||||
InsecureSkipVerify: true,
|
||||
}),
|
||||
WithQueueDeclare("export", false, false, false, false, amqp.Table{"x-max-priority": 100}),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
defer cancel()
|
||||
|
||||
count := 1
|
||||
|
||||
t.Log("[TEST] start publish msg...")
|
||||
|
||||
for {
|
||||
if err = Publish(ctx, "export", amqp.Publishing{
|
||||
ContentType: "text/plain",
|
||||
Body: []byte(time.Now().Format(time.RFC3339) + " => hello_world@" + strconv.Itoa(count)),
|
||||
}); err != nil {
|
||||
t.Log(err.Error())
|
||||
}
|
||||
|
||||
time.Sleep(11 * time.Second)
|
||||
count++
|
||||
}
|
||||
}
|
97
internal/database/mq/consume.go
Normal file
@ -0,0 +1,97 @@
|
||||
package mq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/loveuer/esgo2dump/log"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
"os"
|
||||
"time"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
// ConsumeOpt
|
||||
// - Name: consumer's name, default unamed_<timestamp>
|
||||
// - MaxReconnection: when mq connection closed, max reconnection times, default 3, -1 for unlimited
|
||||
type ConsumeOpt struct {
|
||||
Name string // consumer's name, default unamed_<timestamp>
|
||||
AutoAck bool
|
||||
Exclusive bool
|
||||
NoLocal bool
|
||||
NoWait bool
|
||||
MaxReconnection int // when mq connection closed, max reconnection times, default 3, -1 for unlimited
|
||||
Args amqp.Table
|
||||
}
|
||||
|
||||
func Consume(ctx context.Context, queue string, opts ...*ConsumeOpt) (<-chan amqp.Delivery, error) {
|
||||
var (
|
||||
err error
|
||||
res = make(chan amqp.Delivery, 1)
|
||||
opt = &ConsumeOpt{
|
||||
Name: os.Getenv("HOSTNAME"),
|
||||
AutoAck: false,
|
||||
Exclusive: false,
|
||||
NoLocal: false,
|
||||
NoWait: false,
|
||||
Args: nil,
|
||||
MaxReconnection: 3,
|
||||
}
|
||||
)
|
||||
|
||||
if len(opts) > 0 && opts[0] != nil {
|
||||
opt = opts[0]
|
||||
}
|
||||
|
||||
if opt.Name == "" {
|
||||
opt.Name = fmt.Sprintf("unamed_%d", time.Now().UnixMilli())
|
||||
}
|
||||
|
||||
client.Lock()
|
||||
if client.consume, err = client.ch.Consume(queue, opt.Name, opt.AutoAck, opt.Exclusive, opt.NoLocal, opt.NoWait, opt.Args); err != nil {
|
||||
client.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
client.Unlock()
|
||||
|
||||
go func() {
|
||||
Run:
|
||||
retry := 0
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
close(res)
|
||||
return
|
||||
case m, ok := <-client.consume:
|
||||
if !ok {
|
||||
log.Warn("[mq] consume channel closed!!!")
|
||||
goto Reconnect
|
||||
}
|
||||
|
||||
res <- m
|
||||
}
|
||||
}
|
||||
|
||||
Reconnect:
|
||||
if opt.MaxReconnection == -1 || opt.MaxReconnection > retry {
|
||||
retry++
|
||||
|
||||
log.Warn("[mq] try reconnect[%d/%d] to mq server after %d seconds...err: %v", retry, opt.MaxReconnection, tool.Min(60, retry*5), err)
|
||||
time.Sleep(time.Duration(tool.Min(60, retry*5)) * time.Second)
|
||||
if err = client.open(); err != nil {
|
||||
goto Reconnect
|
||||
}
|
||||
|
||||
client.Lock()
|
||||
if client.consume, err = client.ch.Consume(queue, opt.Name, opt.AutoAck, opt.Exclusive, opt.NoLocal, opt.NoWait, opt.Args); err != nil {
|
||||
client.Unlock()
|
||||
goto Reconnect
|
||||
}
|
||||
client.Unlock()
|
||||
|
||||
log.Info("[mq] reconnect success!!!")
|
||||
goto Run
|
||||
}
|
||||
}()
|
||||
|
||||
return res, nil
|
||||
}
|
48
internal/database/mq/opt.go
Normal file
@ -0,0 +1,48 @@
|
||||
package mq
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
)
|
||||
|
||||
type OptionFn func(*_client)
|
||||
|
||||
// WithURI
|
||||
// - amqp uri, e.g. amqp://guest:guest@127.0.0.1:5672/vhost
|
||||
// - tips: with tls, scheme should be amqps, amqps://xx:xx@127.0.0.1:5672/vhost
|
||||
func WithURI(uri string) OptionFn {
|
||||
return func(c *_client) {
|
||||
c.uri = uri
|
||||
}
|
||||
}
|
||||
|
||||
// WithTLS
|
||||
// - amqps tls config
|
||||
// - include client cert, client key, ca cert
|
||||
func WithTLS(tlsCfg *tls.Config) OptionFn {
|
||||
return func(c *_client) {
|
||||
c.tlsCfg = tlsCfg
|
||||
}
|
||||
}
|
||||
|
||||
type queueOption struct {
|
||||
name string
|
||||
durable bool
|
||||
autoDelete bool
|
||||
exclusive bool
|
||||
noWait bool
|
||||
args amqp.Table
|
||||
}
|
||||
|
||||
func WithQueueDeclare(name string, durable, autoDelete, exclusive, noWait bool, args amqp.Table) OptionFn {
|
||||
return func(c *_client) {
|
||||
c.queue = &queueOption{
|
||||
name: name,
|
||||
durable: durable,
|
||||
autoDelete: autoDelete,
|
||||
exclusive: exclusive,
|
||||
noWait: noWait,
|
||||
args: args,
|
||||
}
|
||||
}
|
||||
}
|
62
internal/database/mq/publish.go
Normal file
@ -0,0 +1,62 @@
|
||||
package mq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/loveuer/esgo2dump/log"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
"time"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
// PublishOpt
|
||||
// - MaxReconnect: publish msg auto retry with reconnect, should not be big, case memory leak
|
||||
type PublishOpt struct {
|
||||
Exchange string
|
||||
Mandatory bool
|
||||
Immediate bool
|
||||
MaxReconnect uint8 // publish msg auto retry with reconnect, should not be big(default 1), case memory leak
|
||||
}
|
||||
|
||||
func Publish(ctx context.Context, queue string, msg amqp.Publishing, opts ...*PublishOpt) error {
|
||||
var (
|
||||
err error
|
||||
opt = &PublishOpt{
|
||||
Exchange: amqp.DefaultExchange,
|
||||
Mandatory: false,
|
||||
Immediate: false,
|
||||
MaxReconnect: 1,
|
||||
}
|
||||
retry = 0
|
||||
)
|
||||
|
||||
if len(opts) > 0 && opts[0] != nil {
|
||||
opt = opts[0]
|
||||
}
|
||||
|
||||
for ; retry <= int(opt.MaxReconnect); retry++ {
|
||||
if err = client.ch.PublishWithContext(ctx, opt.Exchange, queue, opt.Mandatory, opt.Immediate, msg); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if errors.Is(err, amqp.ErrClosed) {
|
||||
sleep := tool.Min(120, (retry+1)*30)
|
||||
|
||||
log.Warn("[mq] connection closed, reconnect[%d/%d] after %d seconds", retry+1, opt.MaxReconnect, sleep)
|
||||
|
||||
time.Sleep(time.Duration(sleep) * time.Second)
|
||||
|
||||
if oerr := client.open(); oerr != nil {
|
||||
log.Error("[mq] reconnect[%d/%d] mq err: %v", oerr, retry+1, opt.MaxReconnect)
|
||||
} else {
|
||||
log.Info("[mq] reconnect mq success!!!")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
34
internal/database/nebula/client.go
Normal file
@ -0,0 +1,34 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/ngorm/v2"
|
||||
"strings"
|
||||
"ultone/internal/opt"
|
||||
)
|
||||
|
||||
var (
|
||||
client *ngorm.Client
|
||||
)
|
||||
|
||||
func Init(ctx context.Context, cfg opt.Nebula) error {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
if client, err = ngorm.NewClient(ctx, &ngorm.Config{
|
||||
Endpoints: strings.Split(cfg.Uri, ","),
|
||||
Username: cfg.Username,
|
||||
Password: cfg.Password,
|
||||
DefaultSpace: cfg.Space,
|
||||
Logger: nil,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func New(ctx context.Context, cfgs ...*ngorm.SessCfg) *ngorm.Session {
|
||||
return client.Session(cfgs...)
|
||||
}
|
102
internal/handler/log.go
Normal file
@ -0,0 +1,102 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"ultone/internal/database/db"
|
||||
"ultone/internal/log"
|
||||
"ultone/internal/model"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/sqlType"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
func LogCategories() nf.HandlerFunc {
|
||||
return func(c *nf.Ctx) error {
|
||||
return resp.Resp200(c, model.OpLogType(0).All())
|
||||
}
|
||||
}
|
||||
|
||||
func LogList(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Page int `query:"page"`
|
||||
Size int `query:"size"`
|
||||
UserIds []uint64 `query:"user_ids"`
|
||||
Types sqlType.NumSlice[model.OpLogType] `query:"types"`
|
||||
}
|
||||
|
||||
var (
|
||||
ok bool
|
||||
op *model.User
|
||||
err error
|
||||
req = new(Req)
|
||||
list = make([]*model.OpLog, 0)
|
||||
total int
|
||||
)
|
||||
|
||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if err = c.QueryParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.Size <= 0 {
|
||||
req.Size = opt.DefaultSize
|
||||
}
|
||||
|
||||
if req.Size > opt.MaxSize {
|
||||
return resp.Resp400(c, req, "参数过大")
|
||||
}
|
||||
|
||||
txCount := op.Role.Where(db.New(tool.Timeout(3)).
|
||||
Model(&model.OpLog{}).
|
||||
Select("COUNT(`op_logs`.`id`)").
|
||||
Joins("LEFT JOIN users ON `users`.`id` = `op_logs`.`user_id`"))
|
||||
txGet := op.Role.Where(db.New(tool.Timeout(10)).
|
||||
Model(&model.OpLog{}).
|
||||
Joins("LEFT JOIN users ON `users`.`id` = `op_logs`.`user_id`"))
|
||||
|
||||
if len(req.UserIds) != 0 {
|
||||
txCount = txCount.Where("op_logs.user_id IN ?", req.UserIds)
|
||||
txGet = txGet.Where("op_logs.user_id IN ?", req.UserIds)
|
||||
}
|
||||
|
||||
if len(req.Types) != 0 {
|
||||
txCount = txCount.Where("op_logs.type IN ?", req.Types)
|
||||
txGet = txGet.Where("op_logs.type IN ?", req.Types)
|
||||
}
|
||||
|
||||
if err = txCount.
|
||||
Find(&total).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
if err = txGet.
|
||||
Offset(req.Page * req.Size).
|
||||
Limit(req.Size).
|
||||
Order("`op_logs`.`created_at` DESC").
|
||||
Find(&list).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
for _, logItem := range list {
|
||||
m := make(map[string]any)
|
||||
if err = logItem.Content.Bind(&m); err != nil {
|
||||
log.Warn(c, "handler.LogList: log=%d content=%v bind map[string]any err=%v", logItem.Id, logItem.Content, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if logItem.HTML, err = logItem.Type.Render(m); err != nil {
|
||||
log.Warn(c, "handler.LogList: log=%d template=%s render map=%+v err=%v", logItem.Id, logItem.Type.Template(), m, err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debug(c, "handler.LogList: log=%d render map=%+v string=%s", logItem.Id, m, logItem.HTML)
|
||||
}
|
||||
|
||||
return resp.Resp200(c, nf.Map{"list": list, "total": total})
|
||||
}
|
486
internal/handler/user.go
Normal file
@ -0,0 +1,486 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"time"
|
||||
"ultone/internal/controller"
|
||||
"ultone/internal/database/cache"
|
||||
"ultone/internal/database/db"
|
||||
"ultone/internal/middleware/oplog"
|
||||
"ultone/internal/model"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/sqlType"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
func AuthLogin(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
target = new(model.User)
|
||||
token string
|
||||
now = time.Now()
|
||||
)
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if err = db.New(tool.Timeout(3)).
|
||||
Model(&model.User{}).
|
||||
Where("username = ?", req.Username).
|
||||
Where("deleted_at = 0").
|
||||
Take(target).
|
||||
Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return resp.Resp400(c, err.Error(), "用户名或密码错误")
|
||||
}
|
||||
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
if !tool.ComparePassword(req.Password, target.Password) {
|
||||
return resp.Resp400(c, nil, "用户名或密码错误")
|
||||
}
|
||||
|
||||
if err = target.IsValid(true); err != nil {
|
||||
return resp.Resp401(c, nil, err.Error())
|
||||
}
|
||||
|
||||
if err = controller.UserController.CacheUser(c, target); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
if token, err = target.JwtEncode(); err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
if err = controller.UserController.CacheToken(c, token, target); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
if !opt.MultiLogin {
|
||||
var (
|
||||
last = fmt.Sprintf("%s:user:last_token:%d", opt.CachePrefix, target.Id)
|
||||
bs []byte
|
||||
)
|
||||
|
||||
if bs, err = cache.Client.Get(tool.Timeout(3), last); err == nil {
|
||||
key := fmt.Sprintf("%s:user:token:%s", opt.CachePrefix, string(bs))
|
||||
_ = cache.Client.Del(tool.Timeout(3), key)
|
||||
}
|
||||
|
||||
if err = cache.Client.Set(tool.Timeout(3), last, token); err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
c.Set("Set-Cookie", fmt.Sprintf("%s=%s; Path=/", opt.CookieName, token))
|
||||
c.Locals("user", target)
|
||||
c.Locals(opt.OpLogLocalKey, &oplog.OpLog{Type: model.OpLogTypeLogin, Content: map[string]any{
|
||||
"time": now.UnixMilli(),
|
||||
"ip": c.IP(true),
|
||||
}})
|
||||
|
||||
return resp.Resp200(c, nf.Map{"token": token, "user": target})
|
||||
}
|
||||
|
||||
func AuthVerify(c *nf.Ctx) error {
|
||||
op, ok := c.Locals("user").(*model.User)
|
||||
if !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
token, ok := c.Locals("token").(string)
|
||||
if !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
return resp.Resp200(c, nf.Map{"token": token, "user": op})
|
||||
}
|
||||
|
||||
func AuthLogout(c *nf.Ctx) error {
|
||||
op, ok := c.Locals("user").(*model.User)
|
||||
if !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
_ = controller.UserController.RmUserCache(c, op.Id)
|
||||
|
||||
c.Locals(opt.OpLogLocalKey, &oplog.OpLog{
|
||||
Type: model.OpLogTypeLogout,
|
||||
Content: map[string]any{
|
||||
"time": time.Now().UnixMilli(),
|
||||
"ip": c.IP(),
|
||||
},
|
||||
})
|
||||
|
||||
return resp.Resp200(c, nil)
|
||||
}
|
||||
|
||||
func ManageUserList(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Page int `query:"page"`
|
||||
Size int `query:"size"`
|
||||
Keyword string `query:"keyword"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
ok bool
|
||||
op *model.User
|
||||
req = new(Req)
|
||||
list = make([]*model.User, 0)
|
||||
total = 0
|
||||
)
|
||||
|
||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if err = c.QueryParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.Size == 0 {
|
||||
req.Size = opt.DefaultSize
|
||||
}
|
||||
|
||||
if req.Size > opt.MaxSize {
|
||||
return resp.Resp400(c, nf.Map{"msg": "size over max", "max": opt.MaxSize})
|
||||
}
|
||||
|
||||
if err = op.Role.Where(db.New(tool.Timeout(10)).
|
||||
Model(&model.User{}).
|
||||
Where("deleted_at = 0")).
|
||||
Order("updated_at DESC").
|
||||
Offset(req.Page * req.Size).
|
||||
Limit(req.Size).
|
||||
Find(&list).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
if err = op.Role.Where(db.New(tool.Timeout(5)).
|
||||
Model(&model.User{}).
|
||||
Select("COUNT(id)").
|
||||
Where("deleted_at = 0")).
|
||||
Find(&total).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
return resp.Resp200(c, nf.Map{"list": list, "total": total})
|
||||
}
|
||||
|
||||
func ManageUserCreate(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Password string `json:"password"`
|
||||
Status model.Status `json:"status"`
|
||||
Role model.Role `json:"role"`
|
||||
Privileges sqlType.NumSlice[model.Privilege] `json:"privileges"`
|
||||
Comment string `json:"comment"`
|
||||
ActiveAt int64 `json:"active_at"`
|
||||
Deadline int64 `json:"deadline"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
ok bool
|
||||
op *model.User
|
||||
req = new(Req)
|
||||
now = time.Now()
|
||||
)
|
||||
|
||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.Username == "" || req.Password == "" {
|
||||
return resp.Resp400(c, req)
|
||||
}
|
||||
|
||||
if err = tool.CheckPassword(req.Password); err != nil {
|
||||
return resp.Resp400(c, req, err.Error())
|
||||
}
|
||||
|
||||
if req.Nickname == "" {
|
||||
req.Nickname = req.Username
|
||||
}
|
||||
|
||||
if req.Status.Code() == "unknown" {
|
||||
return resp.Resp400(c, req, "用户状态不正常")
|
||||
}
|
||||
|
||||
if req.Role == 0 {
|
||||
req.Role = model.RoleUser
|
||||
}
|
||||
|
||||
if req.ActiveAt == 0 {
|
||||
req.ActiveAt = now.UnixMilli()
|
||||
}
|
||||
|
||||
if req.Deadline == 0 {
|
||||
req.Deadline = now.AddDate(99, 0, 0).UnixMilli()
|
||||
}
|
||||
|
||||
newUser := &model.User{
|
||||
CreatedAt: now.UnixMilli(),
|
||||
UpdatedAt: now.UnixMilli(),
|
||||
Username: req.Username,
|
||||
Password: tool.NewPassword(req.Password),
|
||||
Status: req.Status,
|
||||
Nickname: req.Nickname,
|
||||
Comment: req.Comment,
|
||||
Role: req.Role,
|
||||
Privileges: req.Privileges,
|
||||
CreatedById: op.Id,
|
||||
CreatedByName: op.CreatedByName,
|
||||
ActiveAt: op.ActiveAt,
|
||||
Deadline: op.Deadline,
|
||||
}
|
||||
|
||||
if err = newUser.IsValid(false); err != nil {
|
||||
return resp.Resp400(c, newUser, err.Error())
|
||||
}
|
||||
|
||||
if !newUser.Role.CanOP(op) {
|
||||
return resp.Resp403(c, newUser, "角色不符合")
|
||||
}
|
||||
|
||||
if err = db.New(tool.Timeout(5)).
|
||||
Create(newUser).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
c.Locals(opt.OpLogLocalKey, &oplog.OpLog{Type: model.OpLogTypeCreateUser, Content: map[string]any{
|
||||
"target_id": newUser.Id,
|
||||
"target_username": newUser.Username,
|
||||
"target_nickname": newUser.Nickname,
|
||||
"target_status": newUser.Status.Label(),
|
||||
"target_role": newUser.Role.Label(),
|
||||
"target_privileges": lo.Map(newUser.Privileges, func(item model.Privilege, index int) string {
|
||||
return item.Label()
|
||||
}),
|
||||
"target_active_at": op.ActiveAt,
|
||||
"target_deadline": op.Deadline,
|
||||
}})
|
||||
|
||||
return resp.Resp200(c, newUser)
|
||||
}
|
||||
|
||||
func ManageUserUpdate(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Id uint64 `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Password string `json:"password"`
|
||||
Status model.Status `json:"status"`
|
||||
Comment string `json:"comment"`
|
||||
Role model.Role `json:"role"`
|
||||
Privileges sqlType.NumSlice[model.Privilege] `json:"privileges"`
|
||||
ActiveAt int64 `json:"active_at"`
|
||||
Deadline int64 `json:"deadline"`
|
||||
}
|
||||
|
||||
type Change struct {
|
||||
Old any `json:"old"`
|
||||
New any `json:"new"`
|
||||
}
|
||||
|
||||
var (
|
||||
ok bool
|
||||
op *model.User
|
||||
target *model.User
|
||||
err error
|
||||
req = new(Req)
|
||||
rm = make(map[string]any)
|
||||
updates = make(map[string]any)
|
||||
changes = make(map[string]Change)
|
||||
)
|
||||
|
||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if err = c.BodyParser(&rm); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.Id == 0 {
|
||||
return resp.Resp400(c, "未指定目标用户")
|
||||
}
|
||||
|
||||
if target, err = controller.UserController.GetUser(c, req.Id); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
if op.Role < target.Role || ((op.Role == target.Role) && opt.RoleMustLess) {
|
||||
return resp.Resp403(c, req)
|
||||
}
|
||||
|
||||
if op.Id == req.Id {
|
||||
return resp.Resp403(c, req, "无法更新自己")
|
||||
}
|
||||
|
||||
if _, ok = rm["nickname"]; ok {
|
||||
if req.Nickname == "" {
|
||||
return resp.Resp400(c, req)
|
||||
}
|
||||
|
||||
updates["nickname"] = req.Nickname
|
||||
changes["昵称"] = Change{Old: target.Nickname, New: req.Nickname}
|
||||
}
|
||||
|
||||
if _, ok = rm["password"]; ok {
|
||||
if err = tool.CheckPassword(req.Password); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
updates["password"] = tool.NewPassword(req.Password)
|
||||
changes["密码"] = Change{Old: "******", New: "******"}
|
||||
}
|
||||
|
||||
if _, ok = rm["status"]; ok {
|
||||
if req.Status.Code() == "unknown" {
|
||||
return resp.Resp400(c, req, "用户状态不符合")
|
||||
}
|
||||
|
||||
updates["status"] = req.Status
|
||||
changes["状态"] = Change{Old: target.Status.Label(), New: req.Status.Label()}
|
||||
}
|
||||
|
||||
if _, ok = rm["comment"]; ok {
|
||||
updates["comment"] = req.Comment
|
||||
changes["备注"] = Change{Old: target.Comment, New: req.Comment}
|
||||
}
|
||||
|
||||
if _, ok = rm["role"]; ok {
|
||||
if op.Role < req.Role || ((op.Role == req.Role) && opt.RoleMustLess) {
|
||||
return resp.Resp400(c, req, "用户角色不符合")
|
||||
}
|
||||
|
||||
updates["role"] = req.Role
|
||||
changes["角色"] = Change{Old: target.Role.Label(), New: req.Role.Label()}
|
||||
}
|
||||
|
||||
if _, ok = rm["privileges"]; ok {
|
||||
for _, val := range req.Privileges {
|
||||
if lo.IndexOf(op.Privileges, val) < 0 {
|
||||
return resp.Resp400(c, req, fmt.Sprintf("权限: %s 不符合", val.Label()))
|
||||
}
|
||||
}
|
||||
|
||||
changes["权限"] = Change{
|
||||
Old: lo.Map(target.Privileges, func(item model.Privilege, index int) string {
|
||||
return item.Label()
|
||||
}),
|
||||
New: lo.Map(req.Privileges, func(item model.Privilege, index int) string {
|
||||
return item.Label()
|
||||
}),
|
||||
}
|
||||
updates["privileges"] = req.Privileges
|
||||
}
|
||||
|
||||
if _, ok = rm["active_at"]; ok {
|
||||
updates["active_at"] = time.UnixMilli(req.ActiveAt).UnixMilli()
|
||||
changes["激活时间"] = Change{Old: target.ActiveAt, New: req.ActiveAt}
|
||||
}
|
||||
|
||||
if _, ok = rm["deadline"]; ok {
|
||||
updates["deadline"] = time.UnixMilli(req.Deadline).UnixMilli()
|
||||
changes["到期时间"] = Change{Old: target.Deadline, New: req.Deadline}
|
||||
}
|
||||
|
||||
updated := new(model.User)
|
||||
if err = db.New(tool.Timeout(5)).
|
||||
Model(updated).
|
||||
Clauses(clause.Returning{}).
|
||||
Where("id = ?", req.Id).
|
||||
Updates(updates).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
if err = controller.UserController.RmUserCache(c, req.Id); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
c.Locals(opt.OpLogLocalKey, &oplog.OpLog{Type: model.OpLogTypeUpdateUser, Content: map[string]any{
|
||||
"target_id": target.Id,
|
||||
"target_username": target.Username,
|
||||
"changes": changes,
|
||||
}})
|
||||
|
||||
return resp.Resp200(c, updated)
|
||||
}
|
||||
|
||||
func ManageUserDelete(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Id uint64 `json:"id"`
|
||||
}
|
||||
|
||||
var (
|
||||
ok bool
|
||||
op *model.User
|
||||
target *model.User
|
||||
err error
|
||||
req = new(Req)
|
||||
)
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.Id == 0 {
|
||||
return resp.Resp400(c, req)
|
||||
}
|
||||
|
||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if req.Id == op.Id {
|
||||
return resp.Resp400(c, nil, "无法删除自己")
|
||||
}
|
||||
|
||||
if target, err = controller.UserController.GetUser(c, req.Id); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
if op.Role < target.Role || (op.Role == target.Role && opt.RoleMustLess) {
|
||||
return resp.Resp403(c, nil)
|
||||
}
|
||||
|
||||
if err = controller.UserController.DeleteUser(c, target.Id); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
c.Locals(opt.OpLogLocalKey, &oplog.OpLog{Type: model.OpLogTypeDeleteUser, Content: map[string]any{
|
||||
"target_id": target.Id,
|
||||
"target_username": target.Username,
|
||||
}})
|
||||
|
||||
return resp.Resp200(c, nil, "删除成功")
|
||||
}
|
16
internal/interfaces/database.go
Normal file
@ -0,0 +1,16 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Cacher interface {
|
||||
Get(ctx context.Context, key string) ([]byte, error)
|
||||
GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error)
|
||||
// Set value 会被序列化, 优先使用 MarshalBinary 方法, 没有则执行 json.Marshal
|
||||
Set(ctx context.Context, key string, value any) error
|
||||
// SetEx value 会被序列化, 优先使用 MarshalBinary 方法, 没有则执行 json.Marshal
|
||||
SetEx(ctx context.Context, key string, value any, duration time.Duration) error
|
||||
Del(ctx context.Context, keys ...string) error
|
||||
}
|
11
internal/interfaces/enum.go
Normal file
@ -0,0 +1,11 @@
|
||||
package interfaces
|
||||
|
||||
type Enum interface {
|
||||
Value() int64
|
||||
Code() string
|
||||
Label() string
|
||||
|
||||
MarshalJSON() ([]byte, error)
|
||||
|
||||
All() []Enum
|
||||
}
|
7
internal/interfaces/logger.go
Normal file
@ -0,0 +1,7 @@
|
||||
package interfaces
|
||||
|
||||
type OpLogger interface {
|
||||
Enum
|
||||
Render(content map[string]any) (string, error)
|
||||
Template() string
|
||||
}
|
91
internal/invoke/client.go
Normal file
@ -0,0 +1,91 @@
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"sync"
|
||||
"time"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
const (
|
||||
SCHEME = "sonar"
|
||||
)
|
||||
|
||||
type Client[T any] struct {
|
||||
domain string
|
||||
endpoints []string
|
||||
fn func(grpc.ClientConnInterface) T
|
||||
opts []grpc.DialOption
|
||||
|
||||
cc *grpc.ClientConn
|
||||
}
|
||||
|
||||
func (c *Client[T]) Session() T {
|
||||
return c.fn(c.cc)
|
||||
}
|
||||
|
||||
var (
|
||||
clients = &sync.Map{}
|
||||
)
|
||||
|
||||
// NewClient
|
||||
/*
|
||||
* domain => Example: sonar_search
|
||||
* endpoints => Example: []string{"sonar_search:8080", "sonar_search:80801"} or []string{"10.10.10.10:32000", "10.10.10.10:32001"}
|
||||
* fn => Example: system.NewSystemSrvClient
|
||||
* opts => Example: grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
*/
|
||||
func NewClient[T any](
|
||||
domain string,
|
||||
endpoints []string,
|
||||
fn func(grpc.ClientConnInterface) T,
|
||||
opts ...grpc.DialOption,
|
||||
) (*Client[T], error) {
|
||||
|
||||
cached, ok := clients.Load(domain)
|
||||
if ok {
|
||||
if client, ok := cached.(*Client[T]); ok {
|
||||
return client, nil
|
||||
}
|
||||
}
|
||||
|
||||
resolved := resolver.State{Addresses: make([]resolver.Address, 0)}
|
||||
|
||||
locker.Lock()
|
||||
for _, item := range endpoints {
|
||||
resolved.Addresses = append(resolved.Addresses, resolver.Address{Addr: item})
|
||||
}
|
||||
ips[domain] = resolved
|
||||
locker.Unlock()
|
||||
|
||||
fullAddress := fmt.Sprintf("%s://%s", SCHEME, domain)
|
||||
|
||||
opts = append(opts,
|
||||
grpc.WithResolvers(myBuilder),
|
||||
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
|
||||
grpc.WithChainUnaryInterceptor(retryInterceptor(3, 3*time.Second)),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
|
||||
conn, err := grpc.DialContext(
|
||||
tool.Timeout(3),
|
||||
fullAddress,
|
||||
opts...,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &Client[T]{
|
||||
cc: conn,
|
||||
fn: fn,
|
||||
}
|
||||
|
||||
clients.Store(domain, c)
|
||||
|
||||
return c, nil
|
||||
}
|
82
internal/invoke/resolve.go
Normal file
@ -0,0 +1,82 @@
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
scheme = "bifrost"
|
||||
)
|
||||
|
||||
type CustomBuilder struct{}
|
||||
|
||||
func (cb *CustomBuilder) Scheme() string {
|
||||
return scheme
|
||||
}
|
||||
|
||||
func (cb *CustomBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
|
||||
cr := &customResolver{
|
||||
cc: cc,
|
||||
target: target,
|
||||
}
|
||||
|
||||
cr.ResolveNow(resolver.ResolveNowOptions{})
|
||||
|
||||
return cr, nil
|
||||
}
|
||||
|
||||
type customResolver struct {
|
||||
sync.Mutex
|
||||
target resolver.Target
|
||||
cc resolver.ClientConn
|
||||
ips map[string]string
|
||||
}
|
||||
|
||||
func (cr *customResolver) ResolveNow(o resolver.ResolveNowOptions) {
|
||||
var (
|
||||
addrs = make([]resolver.Address, 0)
|
||||
hp []string
|
||||
)
|
||||
|
||||
cr.Lock()
|
||||
defer cr.Unlock()
|
||||
|
||||
if hp = strings.Split(cr.target.URL.Host, ":"); len(hp) >= 2 {
|
||||
if ip, ok := pool[hp[0]]; ok {
|
||||
addr := fmt.Sprintf("%s:%s", ip, hp[1])
|
||||
addrs = append(addrs, resolver.Address{Addr: addr})
|
||||
}
|
||||
}
|
||||
|
||||
_ = cr.cc.UpdateState(resolver.State{Addresses: addrs})
|
||||
}
|
||||
|
||||
func (cr *customResolver) Close() {}
|
||||
|
||||
var (
|
||||
cb = &CustomBuilder{}
|
||||
pool = make(map[string]string)
|
||||
)
|
||||
|
||||
func init() {
|
||||
resolver.Register(cb)
|
||||
}
|
||||
|
||||
type CustomDomain struct {
|
||||
Domain string
|
||||
IP string
|
||||
}
|
||||
|
||||
func NewCustomBuilder(cds ...CustomDomain) resolver.Builder {
|
||||
locker.Lock()
|
||||
defer locker.Unlock()
|
||||
|
||||
for _, cd := range cds {
|
||||
pool[cd.Domain] = cd.IP
|
||||
}
|
||||
|
||||
return cb
|
||||
}
|
43
internal/invoke/resolve_v2.go
Normal file
@ -0,0 +1,43 @@
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"sync"
|
||||
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
type Builder struct{}
|
||||
|
||||
func (b *Builder) Scheme() string {
|
||||
return SCHEME
|
||||
}
|
||||
|
||||
func (b *Builder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
|
||||
cr := &Resolver{
|
||||
cc: cc,
|
||||
target: target,
|
||||
}
|
||||
|
||||
cr.ResolveNow(resolver.ResolveNowOptions{})
|
||||
|
||||
return cr, nil
|
||||
}
|
||||
|
||||
type Resolver struct {
|
||||
target resolver.Target
|
||||
cc resolver.ClientConn
|
||||
}
|
||||
|
||||
func (r *Resolver) ResolveNow(o resolver.ResolveNowOptions) {
|
||||
logrus.Tracef("resolve_v2 ResolveNow => target: %s, %v", r.target.URL.Host, ips)
|
||||
_ = r.cc.UpdateState(ips[r.target.URL.Host])
|
||||
}
|
||||
|
||||
func (cr *Resolver) Close() {}
|
||||
|
||||
var (
|
||||
locker = &sync.Mutex{}
|
||||
myBuilder = &Builder{}
|
||||
ips = map[string]resolver.State{}
|
||||
)
|
43
internal/invoke/retry.go
Normal file
@ -0,0 +1,43 @@
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func retryInterceptor(maxAttempt int, interval time.Duration) grpc.UnaryClientInterceptor {
|
||||
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
|
||||
if maxAttempt == 0 {
|
||||
return invoker(ctx, method, req, reply, cc, opts...)
|
||||
}
|
||||
|
||||
duration := interval
|
||||
|
||||
for attempt := 1; attempt <= maxAttempt; attempt++ {
|
||||
|
||||
if err := invoker(ctx, method, req, reply, cc, opts...); err != nil {
|
||||
if s, ok := status.FromError(err); ok && s.Code() == codes.Unavailable {
|
||||
logrus.Debugf("Connection failed err: %v, retry %d after %fs", err, attempt, duration.Seconds())
|
||||
|
||||
time.Sleep(duration)
|
||||
duration *= 2
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil // 请求成功,不需要重试
|
||||
}
|
||||
|
||||
return fmt.Errorf("max retry attempts reached")
|
||||
}
|
||||
}
|
40
internal/log/log.go
Normal file
@ -0,0 +1,40 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/loveuer/nf"
|
||||
ulog "github.com/loveuer/nf/nft/log"
|
||||
"ultone/internal/opt"
|
||||
)
|
||||
|
||||
func _mix(c *nf.Ctx, msg string) string {
|
||||
if c == nil {
|
||||
return msg
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v | %s", c.Locals(opt.LocalShortTraceKey), msg)
|
||||
}
|
||||
|
||||
func Debug(c *nf.Ctx, msg string, data ...any) {
|
||||
ulog.Debug(_mix(c, msg), data...)
|
||||
}
|
||||
|
||||
func Info(c *nf.Ctx, msg string, data ...any) {
|
||||
ulog.Info(_mix(c, msg), data...)
|
||||
}
|
||||
|
||||
func Warn(c *nf.Ctx, msg string, data ...any) {
|
||||
ulog.Warn(_mix(c, msg), data...)
|
||||
}
|
||||
|
||||
func Error(c *nf.Ctx, msg string, data ...any) {
|
||||
ulog.Error(_mix(c, msg), data...)
|
||||
}
|
||||
|
||||
func Panic(c *nf.Ctx, msg string, data ...any) {
|
||||
ulog.Panic(_mix(c, msg), data...)
|
||||
}
|
||||
|
||||
func Fatal(c *nf.Ctx, msg string, data ...any) {
|
||||
ulog.Fatal(_mix(c, msg), data...)
|
||||
}
|
55
internal/middleware/auth/auth.go
Normal file
@ -0,0 +1,55 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"gitea.com/taozitaozi/gredis"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"strings"
|
||||
"ultone/internal/controller"
|
||||
"ultone/internal/log"
|
||||
"ultone/internal/opt"
|
||||
)
|
||||
|
||||
var (
|
||||
tokenFunc = func(c *nf.Ctx) string {
|
||||
token := c.Get("Authorization")
|
||||
if token == "" {
|
||||
token = c.Cookies(opt.CookieName)
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
)
|
||||
|
||||
func NewAuth() nf.HandlerFunc {
|
||||
return func(c *nf.Ctx) error {
|
||||
token := tokenFunc(c)
|
||||
|
||||
if token = strings.TrimPrefix(token, "Bearer "); token == "" {
|
||||
return resp.Resp401(c, token)
|
||||
}
|
||||
|
||||
log.Debug(c, "middleware.NewAuth: token=%s", token)
|
||||
|
||||
target, err := controller.UserController.GetUserByToken(c, token)
|
||||
if err != nil {
|
||||
log.Error(c, "middleware.NewAuth: get user by token=%s err=%v", token, err)
|
||||
if errors.Is(err, redis.Nil) || errors.Is(err, gredis.ErrKeyNotFound) {
|
||||
return resp.Resp401(c, err)
|
||||
}
|
||||
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
if err = target.IsValid(true); err != nil {
|
||||
return resp.Resp401(c, err.Error(), err.Error())
|
||||
}
|
||||
|
||||
c.Locals("user", target)
|
||||
c.Locals("token", token)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
370
internal/middleware/front/dist/front/3rdpartylicenses.txt
vendored
Normal file
@ -0,0 +1,370 @@
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/core
|
||||
License: "MIT"
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: rxjs
|
||||
License: "Apache-2.0"
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: tslib
|
||||
License: "0BSD"
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/common
|
||||
License: "MIT"
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/platform-browser
|
||||
License: "MIT"
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/router
|
||||
License: "MIT"
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/cdk
|
||||
License: "MIT"
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2024 Google LLC.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/material
|
||||
License: "MIT"
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2024 Google LLC.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/animations
|
||||
License: "MIT"
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/forms
|
||||
License: "MIT"
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: js-base64
|
||||
License: "BSD-3-Clause"
|
||||
|
||||
Copyright (c) 2014, Dan Kogai
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of {{{project}}} nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/service-worker
|
||||
License: "MIT"
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: zone.js
|
||||
License: "MIT"
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
BIN
internal/middleware/front/dist/front/browser/assets/icons/icon-128x128.png
vendored
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
internal/middleware/front/dist/front/browser/assets/icons/icon-144x144.png
vendored
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
internal/middleware/front/dist/front/browser/assets/icons/icon-152x152.png
vendored
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
internal/middleware/front/dist/front/browser/assets/icons/icon-192x192.png
vendored
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
internal/middleware/front/dist/front/browser/assets/icons/icon-384x384.png
vendored
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
internal/middleware/front/dist/front/browser/assets/icons/icon-512x512.png
vendored
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
internal/middleware/front/dist/front/browser/assets/icons/icon-72x72.png
vendored
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
internal/middleware/front/dist/front/browser/assets/icons/icon-96x96.png
vendored
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
internal/middleware/front/dist/front/browser/favicon.ico
vendored
Normal file
After Width: | Height: | Size: 15 KiB |
18
internal/middleware/front/dist/front/browser/index.html
vendored
Normal file
12
internal/middleware/front/dist/front/browser/main-CLMZMBYY.js
vendored
Normal file
59
internal/middleware/front/dist/front/browser/manifest.webmanifest
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "front",
|
||||
"short_name": "front",
|
||||
"theme_color": "#1976d2",
|
||||
"background_color": "#fafafa",
|
||||
"display": "standalone",
|
||||
"scope": "./",
|
||||
"start_url": "./",
|
||||
"icons": [
|
||||
{
|
||||
"src": "assets/icons/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
}
|
||||
]
|
||||
}
|
1863
internal/middleware/front/dist/front/browser/ngsw-worker.js
vendored
Executable file
79
internal/middleware/front/dist/front/browser/ngsw.json
vendored
Normal file
@ -0,0 +1,79 @@
|
||||
{
|
||||
"configVersion": 1,
|
||||
"timestamp": 1720682538867,
|
||||
"index": "/index.html",
|
||||
"assetGroups": [
|
||||
{
|
||||
"name": "app",
|
||||
"installMode": "prefetch",
|
||||
"updateMode": "prefetch",
|
||||
"cacheQueryOptions": {
|
||||
"ignoreVary": true
|
||||
},
|
||||
"urls": [
|
||||
"/favicon.ico",
|
||||
"/index.html",
|
||||
"/main-CLMZMBYY.js",
|
||||
"/manifest.webmanifest",
|
||||
"/polyfills-TMVK3KFA.js",
|
||||
"/styles-NJ66DI6F.css"
|
||||
],
|
||||
"patterns": []
|
||||
},
|
||||
{
|
||||
"name": "assets",
|
||||
"installMode": "lazy",
|
||||
"updateMode": "prefetch",
|
||||
"cacheQueryOptions": {
|
||||
"ignoreVary": true
|
||||
},
|
||||
"urls": [
|
||||
"/assets/icons/icon-128x128.png",
|
||||
"/assets/icons/icon-144x144.png",
|
||||
"/assets/icons/icon-152x152.png",
|
||||
"/assets/icons/icon-192x192.png",
|
||||
"/assets/icons/icon-384x384.png",
|
||||
"/assets/icons/icon-512x512.png",
|
||||
"/assets/icons/icon-72x72.png",
|
||||
"/assets/icons/icon-96x96.png"
|
||||
],
|
||||
"patterns": []
|
||||
}
|
||||
],
|
||||
"dataGroups": [],
|
||||
"hashTable": {
|
||||
"/assets/icons/icon-128x128.png": "f912963bdc6d5a38d8f1dd0afbaab2ce8d657acb",
|
||||
"/assets/icons/icon-144x144.png": "b155fd5f2fd5d2ea7760721063003c4cd95fd783",
|
||||
"/assets/icons/icon-152x152.png": "eae6e0f0c8afb8aaae339e2a569b43e9238e5e6c",
|
||||
"/assets/icons/icon-192x192.png": "15c4180454633880d98674aa7394656196c91584",
|
||||
"/assets/icons/icon-384x384.png": "94a915751ef6c9282df9aea405ad679230631814",
|
||||
"/assets/icons/icon-512x512.png": "1de182f76f7329dfa8f9fcdd1fdcdd695bea6a99",
|
||||
"/assets/icons/icon-72x72.png": "aa88a6096bd973be7f6d7a5489bfa6bc2463f8c4",
|
||||
"/assets/icons/icon-96x96.png": "7fb8f59c30ce2ff12c700321a0b39e14b3dc8f95",
|
||||
"/favicon.ico": "9c39f434fe1261f68c5e3eefdf734630d99c5670",
|
||||
"/index.html": "f5334c6295211e902dbebf00c2b0f2be6cb3c2f6",
|
||||
"/main-CLMZMBYY.js": "35afe20a8438fd03626e7fb1cca7c1583d1a9fc8",
|
||||
"/manifest.webmanifest": "4a17033d41b19f97cc660c6a603fdc95226d1704",
|
||||
"/polyfills-TMVK3KFA.js": "fda1c82c5c620f1fa442f687b3df45a037f6fcc9",
|
||||
"/styles-NJ66DI6F.css": "765db3018d28d3c08e15645f053b34af5946b7e5"
|
||||
},
|
||||
"navigationUrls": [
|
||||
{
|
||||
"positive": true,
|
||||
"regex": "^\\/.*$"
|
||||
},
|
||||
{
|
||||
"positive": false,
|
||||
"regex": "^\\/(?:.+\\/)?[^/]*\\.[^/]*$"
|
||||
},
|
||||
{
|
||||
"positive": false,
|
||||
"regex": "^\\/(?:.+\\/)?[^/]*__[^/]*$"
|
||||
},
|
||||
{
|
||||
"positive": false,
|
||||
"regex": "^\\/(?:.+\\/)?[^/]*__[^/]*\\/.*$"
|
||||
}
|
||||
],
|
||||
"navigationRequestStrategy": "performance"
|
||||
}
|
2
internal/middleware/front/dist/front/browser/polyfills-TMVK3KFA.js
vendored
Normal file
30
internal/middleware/front/dist/front/browser/safety-worker.js
vendored
Executable file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
// tslint:disable:no-console
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(self.clients.claim());
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.unregister().then(() => {
|
||||
console.log('NGSW Safety Worker - unregistered old service worker');
|
||||
}),
|
||||
);
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
const ngswCacheNames = cacheNames.filter((name) => /^ngsw:/.test(name));
|
||||
return Promise.all(ngswCacheNames.map((name) => caches.delete(name)));
|
||||
}),
|
||||
);
|
||||
});
|
1
internal/middleware/front/dist/front/browser/styles-NJ66DI6F.css
vendored
Normal file
30
internal/middleware/front/dist/front/browser/worker-basic.min.js
vendored
Executable file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
// tslint:disable:no-console
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(self.clients.claim());
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.unregister().then(() => {
|
||||
console.log('NGSW Safety Worker - unregistered old service worker');
|
||||
}),
|
||||
);
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
const ngswCacheNames = cacheNames.filter((name) => /^ngsw:/.test(name));
|
||||
return Promise.all(ngswCacheNames.map((name) => caches.delete(name)));
|
||||
}),
|
||||
);
|
||||
});
|
61
internal/middleware/front/front.go
Normal file
@ -0,0 +1,61 @@
|
||||
package front
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf"
|
||||
"net/http"
|
||||
"strings"
|
||||
"ultone/internal/log"
|
||||
)
|
||||
|
||||
//go:embed dist/front/browser
|
||||
var DefaultFront embed.FS
|
||||
|
||||
func NewFront(ff *embed.FS, basePath string) nf.HandlerFunc {
|
||||
var (
|
||||
e error
|
||||
indexBytes []byte
|
||||
index string
|
||||
)
|
||||
|
||||
index = fmt.Sprintf("%s/index.html", basePath)
|
||||
|
||||
if indexBytes, e = ff.ReadFile(index); e != nil {
|
||||
log.Panic(nil, "read index file err: %v", e)
|
||||
}
|
||||
|
||||
return func(c *nf.Ctx) error {
|
||||
var (
|
||||
err error
|
||||
bs []byte
|
||||
path = c.Path()
|
||||
)
|
||||
|
||||
if bs, err = ff.ReadFile(basePath + path); err != nil {
|
||||
log.Debug(c, "embed read file [%s]%s err: %v", basePath, path, err)
|
||||
c.Set("Content-Type", "text/html")
|
||||
_, err = c.Write(indexBytes)
|
||||
return err
|
||||
}
|
||||
|
||||
var dbs []byte
|
||||
if len(bs) > 512 {
|
||||
dbs = bs[:512]
|
||||
} else {
|
||||
dbs = bs
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasSuffix(path, ".js"):
|
||||
c.Set("Content-Type", "application/javascript")
|
||||
case strings.HasSuffix(path, ".css"):
|
||||
c.Set("Content-Type", "text/css")
|
||||
default:
|
||||
c.Set("Content-Type", http.DetectContentType(dbs))
|
||||
}
|
||||
|
||||
_, err = c.Write(bs)
|
||||
return err
|
||||
}
|
||||
}
|
62
internal/middleware/logger/logger.go
Normal file
@ -0,0 +1,62 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/loveuer/esgo2dump/log"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
var (
|
||||
Header = http.CanonicalHeaderKey("X-Trace-Id")
|
||||
)
|
||||
|
||||
func New() nf.HandlerFunc {
|
||||
|
||||
return func(c *nf.Ctx) error {
|
||||
var (
|
||||
now = time.Now()
|
||||
trace = c.Get(Header)
|
||||
logFn func(msg string, data ...any)
|
||||
ip = c.IP()
|
||||
)
|
||||
|
||||
if trace == "" {
|
||||
trace = uuid.Must(uuid.NewV7()).String()
|
||||
}
|
||||
|
||||
c.SetHeader(Header, trace)
|
||||
|
||||
traces := strings.Split(trace, "-")
|
||||
shortTrace := traces[len(traces)-1]
|
||||
|
||||
c.Locals(opt.LocalTraceKey, trace)
|
||||
c.Locals(opt.LocalShortTraceKey, shortTrace)
|
||||
|
||||
err := c.Next()
|
||||
status, _ := strconv.Atoi(c.Writer.Header().Get(resp.RealStatusHeader))
|
||||
duration := time.Since(now)
|
||||
|
||||
msg := fmt.Sprintf("%s | %15s | %d[%3d] | %s | %6s | %s", shortTrace, ip, c.StatusCode, status, tool.HumanDuration(duration.Nanoseconds()), c.Method(), c.Path())
|
||||
|
||||
switch {
|
||||
case status >= 500:
|
||||
logFn = log.Error
|
||||
case status >= 400:
|
||||
logFn = log.Warn
|
||||
default:
|
||||
logFn = log.Info
|
||||
}
|
||||
|
||||
logFn(msg)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
117
internal/middleware/oplog/new.go
Normal file
@ -0,0 +1,117 @@
|
||||
package oplog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"sync"
|
||||
"time"
|
||||
"ultone/internal/database/db"
|
||||
"ultone/internal/model"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/sqlType"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
var (
|
||||
_once = &sync.Once{}
|
||||
lc = make(chan *model.OpLog, 1024)
|
||||
)
|
||||
|
||||
// NewOpLog
|
||||
//
|
||||
// * 记录操作日志的 中间件使用方法如下:
|
||||
//
|
||||
// app := nf.New()
|
||||
// app.Post("/login", oplog.NewOpLog(ctx), HandleLog)
|
||||
//
|
||||
// func HandleLog(c *nf.Ctx) error {
|
||||
// // 你的操作逻辑
|
||||
// c.Local(opt.OpLogLocalKey, &oplog.OpLog{})
|
||||
// // 剩下某些逻辑
|
||||
// // return xxx
|
||||
// }
|
||||
func NewOpLog(ctx context.Context) nf.HandlerFunc {
|
||||
|
||||
_once.Do(func() {
|
||||
go func() {
|
||||
var (
|
||||
err error
|
||||
ticker = time.NewTicker(time.Duration(opt.OpLogWriteDurationSecond) * time.Second)
|
||||
list = make([]*model.OpLog, 0, 1024)
|
||||
|
||||
write = func() {
|
||||
if len(list) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if err = db.New(tool.Timeout(10)).
|
||||
Model(&model.OpLog{}).
|
||||
Create(&list).
|
||||
Error; err != nil {
|
||||
log.Error("middleware.NewOpLog: write logs err=%v", err)
|
||||
}
|
||||
|
||||
list = list[:0]
|
||||
}
|
||||
)
|
||||
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break Loop
|
||||
case <-ticker.C:
|
||||
write()
|
||||
case item, ok := <-lc:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
list = append(list, item)
|
||||
|
||||
if len(list) >= 100 {
|
||||
write()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write()
|
||||
}()
|
||||
})
|
||||
|
||||
return func(c *nf.Ctx) error {
|
||||
now := time.Now()
|
||||
|
||||
err := c.Next()
|
||||
|
||||
op, ok := c.Locals("user").(*model.User)
|
||||
|
||||
opv := c.Locals(opt.OpLogLocalKey)
|
||||
logItem, ok := opv.(*OpLog)
|
||||
if !ok {
|
||||
log.Warn("middleware.NewOpLog: %s - %s local '%s' to [*OpLog] invalid", c.Method(), c.Path(), opt.OpLogLocalKey)
|
||||
return err
|
||||
}
|
||||
|
||||
logItem.Content["time"] = now.UnixMilli()
|
||||
logItem.Content["user_id"] = op.Id
|
||||
logItem.Content["username"] = op.Username
|
||||
logItem.Content["created_at"] = now.UnixMilli()
|
||||
|
||||
select {
|
||||
case lc <- &model.OpLog{
|
||||
CreatedAt: now.UnixMilli(),
|
||||
UpdatedAt: now.UnixMilli(),
|
||||
UserId: op.Id,
|
||||
Username: op.Username,
|
||||
Type: logItem.Type,
|
||||
Content: sqlType.NewJSONB(logItem.Content),
|
||||
}:
|
||||
case <-tool.Timeout(3).Done():
|
||||
log.Warn("middleware.NewOpLog: %s - %s log -> chan timeout[3s]", c.Method, c.Path())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
8
internal/middleware/oplog/oplog.go
Normal file
@ -0,0 +1,8 @@
|
||||
package oplog
|
||||
|
||||
import "ultone/internal/model"
|
||||
|
||||
type OpLog struct {
|
||||
Type model.OpLogType
|
||||
Content map[string]any
|
||||
}
|
86
internal/middleware/privilege/privilege.go
Normal file
@ -0,0 +1,86 @@
|
||||
package privilege
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"github.com/samber/lo"
|
||||
"strings"
|
||||
"ultone/internal/model"
|
||||
)
|
||||
|
||||
type Relation int64
|
||||
|
||||
type vf func(user *model.User, ps ...model.Privilege) error
|
||||
|
||||
const (
|
||||
RelationAnd Relation = iota + 1
|
||||
RelationOr
|
||||
)
|
||||
|
||||
var (
|
||||
AndFunc vf = func(user *model.User, ps ...model.Privilege) error {
|
||||
pm := lo.SliceToMap(user.Privileges, func(item model.Privilege) (int64, struct{}) {
|
||||
return item.Value(), struct{}{}
|
||||
})
|
||||
|
||||
for _, p := range ps {
|
||||
if _, exist := pm[p.Value()]; !exist {
|
||||
return fmt.Errorf("缺少权限: %d, %s, %s", p.Value(), p.Code(), p.Label())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
OrFunc vf = func(user *model.User, ps ...model.Privilege) error {
|
||||
pm := lo.SliceToMap(user.Privileges, func(item model.Privilege) (int64, struct{}) {
|
||||
return item.Value(), struct{}{}
|
||||
})
|
||||
|
||||
for _, p := range ps {
|
||||
if _, exist := pm[p.Value()]; exist {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("缺少权限: %s", strings.Join(
|
||||
lo.Map(ps, func(item model.Privilege, index int) string {
|
||||
return item.Code()
|
||||
}),
|
||||
", ",
|
||||
))
|
||||
}
|
||||
)
|
||||
|
||||
func Verify(relation Relation, privileges ...model.Privilege) nf.HandlerFunc {
|
||||
|
||||
var _vf vf
|
||||
|
||||
switch relation {
|
||||
case RelationAnd:
|
||||
_vf = AndFunc
|
||||
case RelationOr:
|
||||
_vf = OrFunc
|
||||
default:
|
||||
log.Panic("middleware.Verify: unknown relation")
|
||||
}
|
||||
|
||||
return func(c *nf.Ctx) error {
|
||||
if len(privileges) == 0 {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
op, ok := c.Locals("user").(*model.User)
|
||||
if !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if err := _vf(op, privileges...); err != nil {
|
||||
return resp.Resp403(c, err.Error())
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
87
internal/model/init.go
Normal file
@ -0,0 +1,87 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/sqlType"
|
||||
)
|
||||
|
||||
func Init(db *gorm.DB) error {
|
||||
var err error
|
||||
|
||||
if err = initModel(db); err != nil {
|
||||
return fmt.Errorf("model.MustInit: init models err=%v", err)
|
||||
}
|
||||
|
||||
log.Info("MustInitModels: auto_migrate privilege model success")
|
||||
|
||||
if err = initData(db); err != nil {
|
||||
return fmt.Errorf("model.MustInit: init datas err=%v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initModel(client *gorm.DB) error {
|
||||
if err := client.AutoMigrate(
|
||||
&User{},
|
||||
&OpLog{},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("InitModels: auto_migrate user model success")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initData(client *gorm.DB) error {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
{
|
||||
count := 0
|
||||
|
||||
if err = client.Model(&User{}).Select("count(id)").Take(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count < len(initUsers) {
|
||||
log.Warn("mustInitDatas: user count = 0, start init...")
|
||||
for _, user := range initUsers {
|
||||
if err = client.Model(&User{}).Create(user).Error; err != nil {
|
||||
if !strings.Contains(err.Error(), "SQLSTATE 23505") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if opt.Cfg.DB.Type == "postgresql" {
|
||||
if err = client.Exec(`SELECT setval('users_id_seq', (SELECT MAX(id) FROM users))`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("InitDatas: creat init users success")
|
||||
} else {
|
||||
ps := make(sqlType.NumSlice[Privilege], 0)
|
||||
for _, item := range Privilege(0).All() {
|
||||
ps = append(ps, item.(Privilege))
|
||||
}
|
||||
if err = client.Model(&User{}).Where("id = ?", initUsers[0].Id).
|
||||
Updates(map[string]any{
|
||||
"privileges": ps,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("initDatas: update init users success")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
17
internal/model/interface.go
Normal file
@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
type Enum interface {
|
||||
Value() int64
|
||||
Code() string
|
||||
Label() string
|
||||
|
||||
MarshalJSON() ([]byte, error)
|
||||
|
||||
All() []Enum
|
||||
}
|
||||
|
||||
type OpLogger interface {
|
||||
Enum
|
||||
Render(content map[string]any) (string, error)
|
||||
Template() string
|
||||
}
|
294
internal/model/oplog.go
Normal file
@ -0,0 +1,294 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"github.com/spf13/cast"
|
||||
"github.com/tdewolff/minify/v2"
|
||||
"github.com/tdewolff/minify/v2/html"
|
||||
"html/template"
|
||||
"time"
|
||||
"ultone/internal/sqlType"
|
||||
)
|
||||
|
||||
var (
|
||||
FuncMap = template.FuncMap{
|
||||
"time_format": func(mil any, format string) string {
|
||||
return time.UnixMilli(cast.ToInt64(mil)).Format(format)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
_ OpLogger = (*OpLogType)(nil)
|
||||
)
|
||||
|
||||
type OpLogType uint64
|
||||
|
||||
const (
|
||||
OpLogTypeLogin OpLogType = iota + 1
|
||||
OpLogTypeLogout
|
||||
OpLogTypeCreateUser
|
||||
OpLogTypeUpdateUser
|
||||
OpLogTypeDeleteUser
|
||||
|
||||
// todo: 添加自己的操作日志 分类
|
||||
)
|
||||
|
||||
func (o OpLogType) Value() int64 {
|
||||
return int64(o)
|
||||
}
|
||||
|
||||
func (o OpLogType) Code() string {
|
||||
switch o {
|
||||
case OpLogTypeLogin:
|
||||
return "login"
|
||||
case OpLogTypeLogout:
|
||||
return "logout"
|
||||
case OpLogTypeCreateUser:
|
||||
return "create_user"
|
||||
case OpLogTypeUpdateUser:
|
||||
return "update_user"
|
||||
case OpLogTypeDeleteUser:
|
||||
return "delete_user"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (o OpLogType) Label() string {
|
||||
switch o {
|
||||
case OpLogTypeLogin:
|
||||
return "登入"
|
||||
case OpLogTypeLogout:
|
||||
return "登出"
|
||||
case OpLogTypeCreateUser:
|
||||
return "创建用户"
|
||||
case OpLogTypeUpdateUser:
|
||||
return "修改用户"
|
||||
case OpLogTypeDeleteUser:
|
||||
return "删除用户"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
func (o OpLogType) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"value": o.Value(),
|
||||
"code": o.Code(),
|
||||
"label": o.Label(),
|
||||
})
|
||||
}
|
||||
|
||||
func (o OpLogType) All() []Enum {
|
||||
return []Enum{
|
||||
OpLogTypeLogin,
|
||||
OpLogTypeLogout,
|
||||
OpLogTypeCreateUser,
|
||||
OpLogTypeUpdateUser,
|
||||
OpLogTypeDeleteUser,
|
||||
}
|
||||
}
|
||||
|
||||
func _trimHTML(v []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(v)
|
||||
}
|
||||
|
||||
var (
|
||||
_mini = minify.New()
|
||||
)
|
||||
|
||||
func init() {
|
||||
_mini.AddFunc("text/html", html.Minify)
|
||||
}
|
||||
|
||||
func (o OpLogType) Render(content map[string]any) (string, error) {
|
||||
var (
|
||||
err error
|
||||
render *template.Template
|
||||
buf bytes.Buffer
|
||||
bs []byte
|
||||
)
|
||||
|
||||
if render, err = template.New(o.Code()).
|
||||
Funcs(FuncMap).
|
||||
Parse(o.Template()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = render.Execute(&buf, content); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if bs, err = _mini.Bytes("text/html", buf.Bytes()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return _trimHTML(bs), nil
|
||||
}
|
||||
|
||||
const (
|
||||
oplogTemplateLogin = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user="{{ .user_id }}"
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time="{{ .time }}"
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
在
|
||||
<span
|
||||
class="nf-op-log-ip nf-op-log-keyword"
|
||||
>{{ .ip }}
|
||||
</span>
|
||||
上
|
||||
<span
|
||||
class="nf-op-log-op nf-op-log-keyword"
|
||||
>
|
||||
登入
|
||||
</span>
|
||||
了系统
|
||||
</div>
|
||||
`
|
||||
oplogTemplateLogout = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user="{{ .user_id }}"
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time="{{ .time }}"
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
在
|
||||
<span
|
||||
class="nf-op-log-ip nf-op-log-keyword"
|
||||
>{{ .ip }}
|
||||
</span>
|
||||
上
|
||||
<span
|
||||
class="nf-op-log-op nf-op-log-keyword"
|
||||
>
|
||||
登出
|
||||
</span>
|
||||
了系统
|
||||
</div>
|
||||
`
|
||||
oplogTemplateCreateUser = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user="{{ .user_id }}"
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time="{{ .time }}"
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
<span class="nf-op-log-keyword">
|
||||
创建
|
||||
</span>
|
||||
了用户
|
||||
<span
|
||||
class="nf-op-log-target nf-op-log-keyword"
|
||||
nf-op-log-target="{{ .target_id }}"
|
||||
>{{ .target_username }}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
oplogTemplateUpdateUser = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user='{{ .user_id }}'
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time='{{ .time }}'
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
<span class="nf-op-log-keyword">
|
||||
编辑
|
||||
</span>
|
||||
了用户
|
||||
<span
|
||||
class="nf-op-log-target nf-op-log-keyword"
|
||||
nf-op-log-target="{{ .target_id }}"
|
||||
>{{ .target_username }}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
oplogTemplateDeleteUser = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user="{{ .user_id }}"
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time="{{ .time }}"
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
<span class="nf-op-log-keyword">
|
||||
删除
|
||||
</span>
|
||||
了用户
|
||||
<span
|
||||
class="nf-op-log-target nf-op-log-keyword"
|
||||
nf-op-log-target="{{ .target_id }}"
|
||||
>{{ .target_username }}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
|
||||
func (o OpLogType) Template() string {
|
||||
switch o {
|
||||
case OpLogTypeLogin:
|
||||
return oplogTemplateLogin
|
||||
case OpLogTypeLogout:
|
||||
return oplogTemplateLogout
|
||||
case OpLogTypeCreateUser:
|
||||
return oplogTemplateCreateUser
|
||||
case OpLogTypeUpdateUser:
|
||||
return oplogTemplateUpdateUser
|
||||
case OpLogTypeDeleteUser:
|
||||
return oplogTemplateDeleteUser
|
||||
default:
|
||||
return `<div>错误的日志类型</div>`
|
||||
}
|
||||
}
|
||||
|
||||
type OpLog struct {
|
||||
Id uint64 `json:"id" gorm:"primaryKey;column:id"`
|
||||
CreatedAt int64 `json:"created_at" gorm:"column:created_at;autoCreateTime:milli"`
|
||||
UpdatedAt int64 `json:"updated_at" gorm:"column:updated_at;autoUpdateTime:milli"`
|
||||
DeletedAt int64 `json:"deleted_at" gorm:"index;column:deleted_at;default:0"`
|
||||
|
||||
UserId uint64 `json:"user_id" gorm:"column:user_id"`
|
||||
Username string `json:"username" gorm:"column:username;varchar(128)"`
|
||||
Type OpLogType `json:"type" gorm:"column:type;type:varchar(128)"`
|
||||
Content sqlType.JSONB `json:"content" gorm:"column:content;type:jsonb"`
|
||||
HTML string `json:"html" gorm:"-"`
|
||||
}
|
61
internal/model/privilege.go
Normal file
@ -0,0 +1,61 @@
|
||||
package model
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Privilege uint64
|
||||
|
||||
type _privilege struct {
|
||||
Value int64 `json:"value"`
|
||||
Code string `json:"code"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
const (
|
||||
PrivilegeUserManage Privilege = iota + 1
|
||||
PrivilegeOpLog
|
||||
)
|
||||
|
||||
func (p Privilege) Value() int64 {
|
||||
return int64(p)
|
||||
}
|
||||
|
||||
func (p Privilege) Code() string {
|
||||
switch p {
|
||||
case PrivilegeUserManage:
|
||||
return "user_manage"
|
||||
case PrivilegeOpLog:
|
||||
return "oplog"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (p Privilege) Label() string {
|
||||
switch p {
|
||||
case PrivilegeUserManage:
|
||||
return "用户管理"
|
||||
case PrivilegeOpLog:
|
||||
return "操作日志"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
func (p Privilege) MarshalJSON() ([]byte, error) {
|
||||
_p := &_privilege{
|
||||
Value: int64(p),
|
||||
Code: p.Code(),
|
||||
Label: p.Label(),
|
||||
}
|
||||
|
||||
return json.Marshal(_p)
|
||||
}
|
||||
|
||||
func (p Privilege) All() []Enum {
|
||||
return []Enum{
|
||||
PrivilegeUserManage,
|
||||
PrivilegeOpLog,
|
||||
}
|
||||
}
|
||||
|
||||
var _ Enum = (*Privilege)(nil)
|
85
internal/model/role.go
Normal file
@ -0,0 +1,85 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"gorm.io/gorm"
|
||||
"ultone/internal/opt"
|
||||
)
|
||||
|
||||
type _role struct {
|
||||
Value uint8 `json:"value"`
|
||||
Code string `json:"code"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
type Role uint8
|
||||
|
||||
var _ Enum = Role(0)
|
||||
|
||||
func (u Role) MarshalJSON() ([]byte, error) {
|
||||
m := _role{
|
||||
Value: uint8(u),
|
||||
Code: u.Code(),
|
||||
Label: u.Label(),
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
const (
|
||||
RoleRoot Role = 255
|
||||
RoleAdmin Role = 254
|
||||
RoleUser Role = 100
|
||||
)
|
||||
|
||||
func (u Role) Code() string {
|
||||
switch u {
|
||||
case RoleRoot:
|
||||
return "root"
|
||||
case RoleAdmin:
|
||||
return "admin"
|
||||
case RoleUser:
|
||||
return "user"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (u Role) Label() string {
|
||||
switch u {
|
||||
case RoleRoot:
|
||||
return "根用户"
|
||||
case RoleAdmin:
|
||||
return "管理员"
|
||||
case RoleUser:
|
||||
return "用户"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
func (u Role) Value() int64 {
|
||||
return int64(u)
|
||||
}
|
||||
|
||||
func (u Role) All() []Enum {
|
||||
return []Enum{
|
||||
RoleAdmin,
|
||||
RoleUser,
|
||||
}
|
||||
}
|
||||
|
||||
func (u Role) Where(db *gorm.DB) *gorm.DB {
|
||||
if opt.RoleMustLess {
|
||||
return db.Where("users.role < ?", u.Value())
|
||||
} else {
|
||||
return db.Where("users.role <= ?", u.Value())
|
||||
}
|
||||
}
|
||||
|
||||
func (u Role) CanOP(op *User) bool {
|
||||
if opt.RoleMustLess {
|
||||
return op.Role > u
|
||||
}
|
||||
|
||||
return op.Role >= u
|
||||
}
|
226
internal/model/user.go
Normal file
@ -0,0 +1,226 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
"strings"
|
||||
"time"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/sqlType"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
var (
|
||||
initUsers = []*User{
|
||||
{
|
||||
Id: 1,
|
||||
Username: "admin",
|
||||
Password: tool.NewPassword("123456"),
|
||||
Nickname: "admin",
|
||||
Role: RoleAdmin,
|
||||
Privileges: lo.Map(Privilege(0).All(), func(item Enum, index int) Privilege {
|
||||
return item.(Privilege)
|
||||
}),
|
||||
CreatedById: 1,
|
||||
CreatedByName: "admin",
|
||||
ActiveAt: time.Now().UnixMilli(),
|
||||
Deadline: time.Now().AddDate(100, 0, 0).UnixMilli(),
|
||||
},
|
||||
}
|
||||
|
||||
_ Enum = Status(0)
|
||||
)
|
||||
|
||||
type Status uint64
|
||||
|
||||
const (
|
||||
StatusNormal Status = iota
|
||||
StatusFrozen
|
||||
)
|
||||
|
||||
func (s Status) Value() int64 {
|
||||
return int64(s)
|
||||
}
|
||||
|
||||
func (s Status) Code() string {
|
||||
switch s {
|
||||
case StatusNormal:
|
||||
return "normal"
|
||||
case StatusFrozen:
|
||||
return "frozen"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (s Status) Label() string {
|
||||
switch s {
|
||||
case StatusNormal:
|
||||
return "正常"
|
||||
case StatusFrozen:
|
||||
return "冻结"
|
||||
default:
|
||||
return "异常"
|
||||
}
|
||||
}
|
||||
|
||||
func (s Status) All() []Enum {
|
||||
return []Enum{
|
||||
StatusNormal,
|
||||
StatusFrozen,
|
||||
}
|
||||
}
|
||||
|
||||
func (s Status) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"value": s.Value(),
|
||||
"code": s.Code(),
|
||||
"label": s.Label(),
|
||||
})
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Id uint64 `json:"id" gorm:"primaryKey;column:id"`
|
||||
CreatedAt int64 `json:"created_at" gorm:"column:created_at;autoCreateTime:milli"`
|
||||
UpdatedAt int64 `json:"updated_at" gorm:"column:updated_at;autoUpdateTime:milli"`
|
||||
DeletedAt int64 `json:"deleted_at" gorm:"index;column:deleted_at;default:0"`
|
||||
|
||||
Username string `json:"username" gorm:"column:username;type:varchar(64);unique"`
|
||||
Password string `json:"-" gorm:"column:password;type:varchar(256)"`
|
||||
|
||||
Status Status `json:"status" gorm:"column:status;default:0"`
|
||||
|
||||
Nickname string `json:"nickname" gorm:"column:nickname;type:varchar(64)"`
|
||||
Comment string `json:"comment" gorm:"column:comment"`
|
||||
|
||||
Role Role `json:"role" gorm:"column:role"`
|
||||
Privileges sqlType.NumSlice[Privilege] `json:"privileges" gorm:"column:privileges;type:bigint[]"`
|
||||
|
||||
CreatedById uint64 `json:"created_by_id" gorm:"column:created_by_id"`
|
||||
CreatedByName string `json:"created_by_name" gorm:"column:created_by_name;type:varchar(64)"`
|
||||
|
||||
ActiveAt int64 `json:"active_at" gorm:"column:active_at"`
|
||||
Deadline int64 `json:"deadline" gorm:"column:deadline"`
|
||||
|
||||
LoginAt int64 `json:"login_at" gorm:"-"`
|
||||
}
|
||||
|
||||
func (u *User) CheckStatus(mustOk bool) error {
|
||||
switch u.Status {
|
||||
case StatusNormal:
|
||||
case StatusFrozen:
|
||||
if mustOk {
|
||||
return errors.New("用户被冻结")
|
||||
}
|
||||
default:
|
||||
return errors.New("用户状态未知")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) IsValid(mustOk bool) error {
|
||||
now := time.Now()
|
||||
|
||||
if now.UnixMilli() >= u.Deadline {
|
||||
return errors.New("用户已过期")
|
||||
}
|
||||
|
||||
if now.UnixMilli() < u.ActiveAt {
|
||||
return errors.New("用户未启用")
|
||||
}
|
||||
|
||||
if u.DeletedAt > 0 {
|
||||
return errors.New("用户不存在")
|
||||
}
|
||||
|
||||
return u.CheckStatus(mustOk)
|
||||
}
|
||||
|
||||
func (u *User) JwtEncode() (token string, err error) {
|
||||
|
||||
now := time.Now()
|
||||
|
||||
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{
|
||||
"id": u.Id,
|
||||
"username": u.Username,
|
||||
"status": u.Status,
|
||||
"deadline": u.Deadline,
|
||||
"login_at": now.UnixMilli(),
|
||||
})
|
||||
|
||||
if token, err = jwtToken.SignedString([]byte(opt.JwtTokenSecret)); err != nil {
|
||||
err = fmt.Errorf("JwtEncode: jwt token signed secret err: %v", err)
|
||||
log.Error(err.Error())
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (u *User) FromJwt(token string) *User {
|
||||
var (
|
||||
ok bool
|
||||
err error
|
||||
pt *jwt.Token
|
||||
claims jwt.MapClaims
|
||||
)
|
||||
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
|
||||
if pt, err = jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok = t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
|
||||
return []byte(opt.JwtTokenSecret), nil
|
||||
}); err != nil {
|
||||
log.Error("jwt parse err: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !pt.Valid {
|
||||
log.Warn("parsed jwt invalid")
|
||||
return nil
|
||||
}
|
||||
|
||||
if claims, ok = pt.Claims.(jwt.MapClaims); !ok {
|
||||
log.Error("convert jwt claims err")
|
||||
return nil
|
||||
}
|
||||
|
||||
u.Id = cast.ToUint64(claims["user_id"])
|
||||
u.Username = cast.ToString(claims["username"])
|
||||
u.Status = Status(cast.ToInt64(claims["status"]))
|
||||
u.Deadline = cast.ToInt64(claims["deadline"])
|
||||
u.LoginAt = cast.ToInt64(claims["login_at"])
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
func (u User) MarshalBinary() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"id": u.Id,
|
||||
"created_at": u.CreatedAt,
|
||||
"updated_at": u.UpdatedAt,
|
||||
"deleted_at": u.DeletedAt,
|
||||
"username": u.Username,
|
||||
"status": u.Status.Value(),
|
||||
"nickname": u.Nickname,
|
||||
"comment": u.Comment,
|
||||
"role": uint8(u.Role),
|
||||
"privileges": lo.Map(u.Privileges, func(item Privilege, index int) int64 {
|
||||
return item.Value()
|
||||
}),
|
||||
"created_by_id": u.CreatedById,
|
||||
"created_by_name": u.CreatedByName,
|
||||
"active_at": u.ActiveAt,
|
||||
"deadline": u.Deadline,
|
||||
"login_at": u.LoginAt,
|
||||
})
|
||||
}
|
78
internal/opt/opt.go
Normal file
@ -0,0 +1,78 @@
|
||||
package opt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"os"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
type listen struct {
|
||||
Http string `json:"http"`
|
||||
Grpc string `json:"grpc"`
|
||||
Unix string `json:"unix"`
|
||||
}
|
||||
|
||||
type db struct {
|
||||
Type string `json:"-"` // postgres, mysql, sqlite
|
||||
Uri string `json:"uri"`
|
||||
}
|
||||
|
||||
type cache struct {
|
||||
Uri string `json:"uri"`
|
||||
}
|
||||
|
||||
type es struct {
|
||||
Uri string `json:"uri"`
|
||||
Index struct {
|
||||
Staff string `json:"staff"`
|
||||
} `json:"index"`
|
||||
}
|
||||
|
||||
type Nebula struct {
|
||||
Uri string `json:"uri"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Space string `json:"space"`
|
||||
}
|
||||
|
||||
type config struct {
|
||||
Name string `json:"name"`
|
||||
Listen listen `json:"listen"`
|
||||
DB db `json:"db"`
|
||||
Cache cache `json:"cache"`
|
||||
ES es `json:"es"`
|
||||
Nebula Nebula `json:"nebula"`
|
||||
}
|
||||
|
||||
var (
|
||||
Debug bool
|
||||
Cfg = &config{}
|
||||
)
|
||||
|
||||
func Init(filename string) error {
|
||||
|
||||
var (
|
||||
err error
|
||||
bs []byte
|
||||
)
|
||||
|
||||
log.Info("opt.Init: start reading config file: %s", filename)
|
||||
|
||||
if bs, err = os.ReadFile(filename); err != nil {
|
||||
return fmt.Errorf("opt.Init: read config file=%s err=%v", filename, err)
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(bs, Cfg); err != nil {
|
||||
return fmt.Errorf("opt.Init: json marshal config=%s err=%v", string(bs), err)
|
||||
}
|
||||
|
||||
if Debug {
|
||||
log.SetLogLevel(log.LogLevelDebug)
|
||||
}
|
||||
|
||||
tool.TablePrinter(Cfg)
|
||||
|
||||
return nil
|
||||
}
|
46
internal/opt/var.go
Normal file
@ -0,0 +1,46 @@
|
||||
package opt
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
// todo: 可以替换自己生生成的 secret
|
||||
JwtTokenSecret = "7^D+UW3BPB2Mnz)bY3uVrAUyv&dj8Kdz"
|
||||
|
||||
// todo: 是否打开 gorm 的 debug 打印 (开发和 dev 环境时可以打开)
|
||||
DBDebug = true
|
||||
|
||||
// todo: 是否加载默认的前端用户管理界面
|
||||
EnableFront = false
|
||||
|
||||
// todo: 同一个账号是否可以多 client 登录
|
||||
MultiLogin = false
|
||||
|
||||
// todo: 用户量不大的情况, 并没有缓存用户具体信息, 如果需要可以打开
|
||||
EnableUserCache = true
|
||||
|
||||
// todo: 缓存时, key 的前缀
|
||||
CachePrefix = "ultone"
|
||||
|
||||
// todo: 登录颁发的 cookie 的 name
|
||||
CookieName = "utlone-token"
|
||||
|
||||
// todo: 用户列表,日志列表 size 参数
|
||||
DefaultSize, MaxSize = 20, 200
|
||||
|
||||
// todo: 操作用户时, role 相等时能否操作: 包括 列表, 能否新建,修改,删除同样 role 的用户
|
||||
RoleMustLess = false
|
||||
|
||||
// todo: 通过 c.Local() 存入 oplog 时的 key 值
|
||||
OpLogLocalKey = "oplog"
|
||||
|
||||
// todo: 操作日志 最多延迟多少秒写入(最多缓存多少秒的日志,然后 bulk 写入)
|
||||
OpLogWriteDurationSecond = 5
|
||||
|
||||
LocalTraceKey = "X-Trace"
|
||||
LocalShortTraceKey = "X-Short-Trace"
|
||||
)
|
||||
|
||||
var (
|
||||
// todo: 颁发的 token, (cookie) 在缓存中存在的时间 (每次请求该时间也会被刷新)
|
||||
TokenTimeout = time.Duration(3600*12) * time.Second
|
||||
)
|
9
internal/sqlType/err.go
Normal file
@ -0,0 +1,9 @@
|
||||
package sqlType
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrConvertScanVal = errors.New("convert scan val to str err")
|
||||
ErrInvalidScanVal = errors.New("scan val invalid")
|
||||
ErrConvertVal = errors.New("convert err")
|
||||
)
|
76
internal/sqlType/jsonb.go
Normal file
@ -0,0 +1,76 @@
|
||||
package sqlType
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/jackc/pgtype"
|
||||
)
|
||||
|
||||
type JSONB struct {
|
||||
Val pgtype.JSONB
|
||||
Valid bool
|
||||
}
|
||||
|
||||
func NewJSONB(v interface{}) JSONB {
|
||||
j := new(JSONB)
|
||||
j.Val = pgtype.JSONB{}
|
||||
if err := j.Val.Set(v); err == nil {
|
||||
j.Valid = true
|
||||
return *j
|
||||
}
|
||||
|
||||
return *j
|
||||
}
|
||||
|
||||
func (j *JSONB) Set(value interface{}) error {
|
||||
if err := j.Val.Set(value); err != nil {
|
||||
j.Valid = false
|
||||
return err
|
||||
}
|
||||
|
||||
j.Valid = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *JSONB) Bind(model interface{}) error {
|
||||
return j.Val.AssignTo(model)
|
||||
}
|
||||
|
||||
func (j *JSONB) Scan(value interface{}) error {
|
||||
j.Val = pgtype.JSONB{}
|
||||
if value == nil {
|
||||
j.Valid = false
|
||||
return nil
|
||||
}
|
||||
|
||||
j.Valid = true
|
||||
|
||||
return j.Val.Scan(value)
|
||||
}
|
||||
|
||||
func (j JSONB) Value() (driver.Value, error) {
|
||||
if j.Valid {
|
||||
return j.Val.Value()
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (j JSONB) MarshalJSON() ([]byte, error) {
|
||||
if j.Valid {
|
||||
return j.Val.MarshalJSON()
|
||||
}
|
||||
|
||||
return json.Marshal(nil)
|
||||
}
|
||||
|
||||
func (j *JSONB) UnmarshalJSON(b []byte) error {
|
||||
if string(b) == "null" {
|
||||
j.Valid = false
|
||||
return j.Val.UnmarshalJSON(b)
|
||||
}
|
||||
|
||||
return j.Val.UnmarshalJSON(b)
|
||||
}
|
42
internal/sqlType/nullStr.go
Normal file
@ -0,0 +1,42 @@
|
||||
package sqlType
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// type NullString struct {
|
||||
// sql.NullString
|
||||
// }
|
||||
|
||||
type NullString struct{ sql.NullString }
|
||||
|
||||
func NewNullString(val string) NullString {
|
||||
if val == "" {
|
||||
return NullString{}
|
||||
}
|
||||
|
||||
return NullString{sql.NullString{Valid: true, String: val}}
|
||||
}
|
||||
|
||||
func (ns NullString) MarshalJSON() ([]byte, error) {
|
||||
if !ns.Valid {
|
||||
return json.Marshal(nil)
|
||||
}
|
||||
|
||||
return json.Marshal(ns.String)
|
||||
}
|
||||
|
||||
func (ns *NullString) UnmarshalJSON(data []byte) error {
|
||||
if string(data) == "null" {
|
||||
ns.Valid = false
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &ns.String); err != nil {
|
||||
ns.Valid = true
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
53
internal/sqlType/set.go
Normal file
@ -0,0 +1,53 @@
|
||||
package sqlType
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Set map[string]struct{}
|
||||
|
||||
func (s Set) MarshalJSON() ([]byte, error) {
|
||||
array := make([]string, 0)
|
||||
for name := range s {
|
||||
array = append(array, name)
|
||||
}
|
||||
|
||||
return json.Marshal(array)
|
||||
}
|
||||
|
||||
func (s *Set) UnmarshalJSON(b []byte) error {
|
||||
array := make([]string, 0)
|
||||
if err := json.Unmarshal(b, &array); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
set := make(map[string]struct{})
|
||||
|
||||
for _, name := range array {
|
||||
set[name] = struct{}{}
|
||||
}
|
||||
|
||||
*s = set
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s Set) ToStringSlice() []string {
|
||||
var (
|
||||
result = make([]string, 0, len(s))
|
||||
)
|
||||
|
||||
for key := range s {
|
||||
result = append(result, key)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Set) FromStringSlice(ss *[]string) {
|
||||
if s == nil {
|
||||
m := make(Set)
|
||||
s = &m
|
||||
}
|
||||
|
||||
for idx := range *(ss) {
|
||||
(*s)[(*ss)[idx]] = struct{}{}
|
||||
}
|
||||
}
|
109
internal/sqlType/strSlice.go
Normal file
@ -0,0 +1,109 @@
|
||||
package sqlType
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type StrSlice []string
|
||||
|
||||
func (s *StrSlice) Scan(val interface{}) error {
|
||||
|
||||
str, ok := val.(string)
|
||||
if !ok {
|
||||
return ErrConvertScanVal
|
||||
}
|
||||
|
||||
if len(str) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
bs := make([]byte, 0, 128)
|
||||
bss := make([]byte, 0, 2*len(str))
|
||||
|
||||
quoteCount := 0
|
||||
|
||||
for idx := 1; idx < len(str)-1; idx++ {
|
||||
// 44: , 92: \ 34: "
|
||||
quote := str[idx]
|
||||
switch quote {
|
||||
case 44:
|
||||
if quote == 44 && str[idx-1] != 92 && quoteCount == 0 {
|
||||
if len(bs) > 0 {
|
||||
if !(bs[0] == 34 && bs[len(bs)-1] == 34) {
|
||||
bs = append([]byte{34}, bs...)
|
||||
bs = append(bs, 34)
|
||||
}
|
||||
|
||||
bss = append(bss, bs...)
|
||||
bss = append(bss, 44)
|
||||
}
|
||||
bs = bs[:0]
|
||||
} else {
|
||||
bs = append(bs, quote)
|
||||
}
|
||||
case 34:
|
||||
if str[idx-1] != 92 {
|
||||
quoteCount = (quoteCount + 1) % 2
|
||||
}
|
||||
bs = append(bs, quote)
|
||||
default:
|
||||
bs = append(bs, quote)
|
||||
}
|
||||
|
||||
//bs = append(bs, str[idx])
|
||||
}
|
||||
|
||||
if len(bs) > 0 {
|
||||
if !(bs[0] == 34 && bs[len(bs)-1] == 34) {
|
||||
bs = append([]byte{34}, bs...)
|
||||
bs = append(bs, 34)
|
||||
}
|
||||
|
||||
bss = append(bss, bs...)
|
||||
} else {
|
||||
if len(bss) > 2 {
|
||||
bss = bss[:len(bss)-2]
|
||||
}
|
||||
}
|
||||
|
||||
bss = append([]byte{'['}, append(bss, ']')...)
|
||||
|
||||
if err := json.Unmarshal(bss, s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s StrSlice) Value() (driver.Value, error) {
|
||||
if s == nil {
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
if len(s) == 0 {
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
encoder := json.NewEncoder(buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
|
||||
if err := encoder.Encode(s); err != nil {
|
||||
return "{}", err
|
||||
}
|
||||
|
||||
bs := buf.Bytes()
|
||||
|
||||
bs[0] = '{'
|
||||
|
||||
if bs[len(bs)-1] == 10 {
|
||||
bs = bs[:len(bs)-1]
|
||||
}
|
||||
|
||||
bs[len(bs)-1] = '}'
|
||||
|
||||
return string(bs), nil
|
||||
}
|
71
internal/sqlType/uint64Slice.go
Normal file
@ -0,0 +1,71 @@
|
||||
package sqlType
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
type NumSlice[T ~int | ~int64 | ~uint | ~uint64] []T
|
||||
|
||||
func (n *NumSlice[T]) Scan(val interface{}) error {
|
||||
str, ok := val.(string)
|
||||
if !ok {
|
||||
return ErrConvertScanVal
|
||||
}
|
||||
|
||||
length := len(str)
|
||||
|
||||
if length <= 0 {
|
||||
*n = make(NumSlice[T], 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
if str[0] != '{' || str[length-1] != '}' {
|
||||
return ErrInvalidScanVal
|
||||
}
|
||||
|
||||
str = str[1 : length-1]
|
||||
if len(str) == 0 {
|
||||
*n = make(NumSlice[T], 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
numStrs := strings.Split(str, ",")
|
||||
nums := make([]T, len(numStrs))
|
||||
|
||||
for idx := range numStrs {
|
||||
num, err := cast.ToInt64E(strings.TrimSpace(numStrs[idx]))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: can't convert to %T", ErrConvertVal, T(0))
|
||||
}
|
||||
|
||||
nums[idx] = T(num)
|
||||
}
|
||||
|
||||
*n = nums
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n NumSlice[T]) Value() (driver.Value, error) {
|
||||
if n == nil {
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
if len(n) == 0 {
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
ss := make([]string, 0, len(n))
|
||||
for idx := range n {
|
||||
ss = append(ss, strconv.Itoa(int(n[idx])))
|
||||
}
|
||||
|
||||
s := strings.Join(ss, ", ")
|
||||
|
||||
return fmt.Sprintf("{%s}", s), nil
|
||||
}
|
38
internal/tool/ctx.go
Normal file
@ -0,0 +1,38 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Timeout(seconds ...int) (ctx context.Context) {
|
||||
var (
|
||||
duration time.Duration
|
||||
)
|
||||
|
||||
if len(seconds) > 0 && seconds[0] > 0 {
|
||||
duration = time.Duration(seconds[0]) * time.Second
|
||||
} else {
|
||||
duration = time.Duration(30) * time.Second
|
||||
}
|
||||
|
||||
ctx, _ = context.WithTimeout(context.Background(), duration)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TimeoutCtx(ctx context.Context, seconds ...int) context.Context {
|
||||
var (
|
||||
duration time.Duration
|
||||
)
|
||||
|
||||
if len(seconds) > 0 && seconds[0] > 0 {
|
||||
duration = time.Duration(seconds[0]) * time.Second
|
||||
} else {
|
||||
duration = time.Duration(30) * time.Second
|
||||
}
|
||||
|
||||
nctx, _ := context.WithTimeout(ctx, duration)
|
||||
|
||||
return nctx
|
||||
}
|
24
internal/tool/human.go
Normal file
@ -0,0 +1,24 @@
|
||||
package tool
|
||||
|
||||
import "fmt"
|
||||
|
||||
func HumanDuration(nano int64) string {
|
||||
duration := float64(nano)
|
||||
unit := "ns"
|
||||
if duration >= 1000 {
|
||||
duration /= 1000
|
||||
unit = "us"
|
||||
}
|
||||
|
||||
if duration >= 1000 {
|
||||
duration /= 1000
|
||||
unit = "ms"
|
||||
}
|
||||
|
||||
if duration >= 1000 {
|
||||
duration /= 1000
|
||||
unit = " s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%6.2f%s", duration, unit)
|
||||
}
|
11
internal/tool/must.go
Normal file
@ -0,0 +1,11 @@
|
||||
package tool
|
||||
|
||||
import "github.com/loveuer/nf/nft/log"
|
||||
|
||||
func Must(errs ...error) {
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
log.Panic(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
84
internal/tool/password.go
Normal file
@ -0,0 +1,84 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
EncryptHeader string = "pbkdf2:sha256" // 用户密码加密
|
||||
)
|
||||
|
||||
func NewPassword(password string) string {
|
||||
return EncryptPassword(password, RandomString(8), int(RandomInt(50000)+100000))
|
||||
}
|
||||
|
||||
func ComparePassword(in, db string) bool {
|
||||
strs := strings.Split(db, "$")
|
||||
if len(strs) != 3 {
|
||||
log.Error("password in db invalid: %s", db)
|
||||
return false
|
||||
}
|
||||
|
||||
encs := strings.Split(strs[0], ":")
|
||||
if len(encs) != 3 {
|
||||
log.Error("password in db invalid: %s", db)
|
||||
return false
|
||||
}
|
||||
|
||||
encIteration, err := strconv.Atoi(encs[2])
|
||||
if err != nil {
|
||||
log.Error("password in db invalid: %s, convert iter err: %s", db, err)
|
||||
return false
|
||||
}
|
||||
|
||||
return EncryptPassword(in, strs[1], encIteration) == db
|
||||
}
|
||||
|
||||
func EncryptPassword(password, salt string, iter int) string {
|
||||
hash := pbkdf2.Key([]byte(password), []byte(salt), iter, 32, sha256.New)
|
||||
encrypted := hex.EncodeToString(hash)
|
||||
return fmt.Sprintf("%s:%d$%s$%s", EncryptHeader, iter, salt, encrypted)
|
||||
}
|
||||
|
||||
func CheckPassword(password string) error {
|
||||
if len(password) < 8 || len(password) > 32 {
|
||||
return errors.New("密码长度不符合")
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
match bool
|
||||
patternList = []string{`[0-9]+`, `[a-z]+`, `[A-Z]+`, `[!@#%]+`} //, `[~!@#$%^&*?_-]+`}
|
||||
matchAccount = 0
|
||||
tips = []string{"缺少数字", "缺少小写字母", "缺少大写字母", "缺少'!@#%'"}
|
||||
locktips = make([]string, 0)
|
||||
)
|
||||
|
||||
for idx, pattern := range patternList {
|
||||
match, err = regexp.MatchString(pattern, password)
|
||||
if err != nil {
|
||||
log.Warn("regex match string err, reg_str: %s, err: %v", pattern, err)
|
||||
return errors.New("密码强度不够")
|
||||
}
|
||||
|
||||
if match {
|
||||
matchAccount++
|
||||
} else {
|
||||
locktips = append(locktips, tips[idx])
|
||||
}
|
||||
}
|
||||
|
||||
if matchAccount < 3 {
|
||||
return fmt.Errorf("密码强度不够, 可能 %s", strings.Join(locktips, ", "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
11
internal/tool/password_test.go
Normal file
@ -0,0 +1,11 @@
|
||||
package tool
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEncPassword(t *testing.T) {
|
||||
password := "123456"
|
||||
|
||||
result := EncryptPassword(password, RandomString(8), 50000)
|
||||
|
||||
t.Logf("sum => %s", result)
|
||||
}
|
54
internal/tool/random.go
Normal file
@ -0,0 +1,54 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
var (
|
||||
letters = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
letterNum = []byte("0123456789")
|
||||
letterLow = []byte("abcdefghijklmnopqrstuvwxyz")
|
||||
letterCap = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
letterSyb = []byte("!@#$%^&*()_+-=")
|
||||
)
|
||||
|
||||
func RandomInt(max int64) int64 {
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(max))
|
||||
return num.Int64()
|
||||
}
|
||||
|
||||
func RandomString(length int) string {
|
||||
result := make([]byte, length)
|
||||
for i := 0; i < length; i++ {
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
|
||||
result[i] = letters[num.Int64()]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func RandomPassword(length int, withSymbol bool) string {
|
||||
result := make([]byte, length)
|
||||
kind := 3
|
||||
if withSymbol {
|
||||
kind++
|
||||
}
|
||||
|
||||
for i := 0; i < length; i++ {
|
||||
switch i % kind {
|
||||
case 0:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterNum))))
|
||||
result[i] = letterNum[num.Int64()]
|
||||
case 1:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterLow))))
|
||||
result[i] = letterLow[num.Int64()]
|
||||
case 2:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterCap))))
|
||||
result[i] = letterCap[num.Int64()]
|
||||
case 3:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterSyb))))
|
||||
result[i] = letterSyb[num.Int64()]
|
||||
}
|
||||
}
|
||||
return string(result)
|
||||
}
|
117
internal/tool/table.go
Normal file
@ -0,0 +1,117 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func TablePrinter(data any, writers ...io.Writer) {
|
||||
var w io.Writer = os.Stdout
|
||||
if len(writers) > 0 && writers[0] != nil {
|
||||
w = writers[0]
|
||||
}
|
||||
|
||||
t := table.NewWriter()
|
||||
structPrinter(t, "", data)
|
||||
_, _ = fmt.Fprintln(w, t.Render())
|
||||
}
|
||||
|
||||
func structPrinter(w table.Writer, prefix string, item any) {
|
||||
Start:
|
||||
rv := reflect.ValueOf(item)
|
||||
if rv.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
for rv.Type().Kind() == reflect.Pointer {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
|
||||
switch rv.Type().Kind() {
|
||||
case reflect.Invalid,
|
||||
reflect.Uintptr,
|
||||
reflect.Chan,
|
||||
reflect.Func,
|
||||
reflect.UnsafePointer:
|
||||
case reflect.Bool,
|
||||
reflect.Int,
|
||||
reflect.Int8,
|
||||
reflect.Int16,
|
||||
reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint,
|
||||
reflect.Uint8,
|
||||
reflect.Uint16,
|
||||
reflect.Uint32,
|
||||
reflect.Uint64,
|
||||
reflect.Float32,
|
||||
reflect.Float64,
|
||||
reflect.Complex64,
|
||||
reflect.Complex128,
|
||||
reflect.Interface,
|
||||
reflect.String:
|
||||
w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), rv.Interface()})
|
||||
case reflect.Array, reflect.Slice:
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
p := strings.Join([]string{prefix, fmt.Sprintf("[%d]", i)}, ".")
|
||||
structPrinter(w, p, rv.Index(i).Interface())
|
||||
}
|
||||
case reflect.Map:
|
||||
for _, k := range rv.MapKeys() {
|
||||
structPrinter(w, fmt.Sprintf("%s.{%v}", prefix, k), rv.MapIndex(k).Interface())
|
||||
}
|
||||
case reflect.Pointer:
|
||||
goto Start
|
||||
case reflect.Struct:
|
||||
for i := 0; i < rv.NumField(); i++ {
|
||||
p := fmt.Sprintf("%s.%s", prefix, rv.Type().Field(i).Name)
|
||||
field := rv.Field(i)
|
||||
|
||||
//log.Debug("TablePrinter: prefix: %s, field: %v", p, rv.Field(i))
|
||||
|
||||
if !field.CanInterface() {
|
||||
return
|
||||
}
|
||||
|
||||
structPrinter(w, p, field.Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TableMapPrinter(data []byte) {
|
||||
m := make(map[string]any)
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
log.Warn(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
t := table.NewWriter()
|
||||
addRow(t, "", m)
|
||||
fmt.Println(t.Render())
|
||||
}
|
||||
|
||||
func addRow(w table.Writer, prefix string, m any) {
|
||||
rv := reflect.ValueOf(m)
|
||||
switch rv.Type().Kind() {
|
||||
case reflect.Map:
|
||||
for _, k := range rv.MapKeys() {
|
||||
key := k.String()
|
||||
if prefix != "" {
|
||||
key = strings.Join([]string{prefix, k.String()}, ".")
|
||||
}
|
||||
addRow(w, key, rv.MapIndex(k).Interface())
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
addRow(w, fmt.Sprintf("%s[%d]", prefix, i), rv.Index(i).Interface())
|
||||
}
|
||||
default:
|
||||
w.AppendRow(table.Row{prefix, m})
|
||||
}
|
||||
}
|
19
internal/tool/tools.go
Normal file
@ -0,0 +1,19 @@
|
||||
package tool
|
||||
|
||||
import "cmp"
|
||||
|
||||
func Min[T cmp.Ordered](a, b T) T {
|
||||
if a <= b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func Max[T cmp.Ordered](a, b T) T {
|
||||
if a >= b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|