wip: 完成 client api 分析

This commit is contained in:
loveuer
2024-12-19 15:03:36 +08:00
commit 64cdd0cb0e
76 changed files with 5146 additions and 0 deletions

70
internal/api/api.go Normal file
View File

@ -0,0 +1,70 @@
package api
import (
"context"
"fmt"
"time"
"esway/internal/handler"
"esway/internal/middleware/auth"
"esway/internal/middleware/logger"
"esway/internal/middleware/oplog"
"esway/internal/middleware/privilege"
"esway/internal/model"
"esway/internal/opt"
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/resp"
)
func initApp(ctx context.Context) *nf.App {
engine := nf.New(nf.Config{DisableLogger: true, DisableBanner: true, DisableMessagePrint: 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() nf.HandlerFunc {
return func(c *nf.Ctx) error {
now := time.Now()
return resp.Resp200(c, nf.Map{"ok": opt.OK, "start": opt.Start, "now": now, "duration": fmt.Sprint(now.Sub(opt.Start))})
}
}())
{
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)
api.Post("/update", auth.NewAuth(), handler.UserUpdate)
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: 添加自己的接口后删除该行
}
return engine
}

45
internal/api/start.go Normal file
View File

@ -0,0 +1,45 @@
package api
import (
"context"
"fmt"
"net"
"esway/internal/opt"
"esway/internal/tool"
"github.com/loveuer/nf/nft/log"
)
func Start(ctx context.Context) error {
app := initApp(ctx)
ready := make(chan bool)
ln, err := net.Listen("tcp", opt.Cfg.Listen.Dashboard)
if err != nil {
return fmt.Errorf("api.MustStart: net listen tcp address=%v err=%v", opt.Cfg.Listen.Dashboard, err)
}
go func() {
ready <- true
fmt.Printf("esway: dashboard listen at %s\n", opt.Cfg.Listen.Dashboard)
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(2)); err != nil {
log.Error("api.MustStart: app shutdown err=%v", err)
}
}()
<-ready
return nil
}

78
internal/cmd/cli.go Normal file
View File

@ -0,0 +1,78 @@
package cmd
import (
"fmt"
"net/rpc"
"net/url"
"esway/internal/log"
"esway/internal/opt"
"esway/internal/unix"
"github.com/spf13/cobra"
)
var (
cliCommand = &cobra.Command{
Use: "cli",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
log.Debug(cmd.Context(), "[cmd.cli] svc address: %s", svc)
uri, err := url.Parse(svc)
if err != nil {
return err
}
if cliClient, err = rpc.Dial(uri.Scheme, uri.Host+uri.Path); err != nil {
return fmt.Errorf("rpc dial [%s] [%s] err: %w", uri.Scheme, uri.Host+uri.Path, err)
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
log.Debug(cmd.Context(), "[cli] start run cli... all args: %v", args)
return nil
},
}
svc string
cliClient *rpc.Client
)
func initCli() {
cliCommand.PersistentFlags().StringVar(&svc, "svc", opt.RpcAddress, "server unix listen address")
cliCommand.AddCommand(&cobra.Command{
Use: "set",
RunE: func(cmd *cobra.Command, args []string) (err error) {
log.Debug(cmd.Context(), "[cli.set] all args: %v", args)
if len(args) < 2 {
return fmt.Errorf("at least 2 args required")
}
switch args[0] {
case "debug":
out := &unix.Resp[bool]{}
in := &unix.SettingReq{Debug: false}
switch args[1] {
case "true":
in.Debug = true
case "false":
default:
return fmt.Errorf("unknown debug value")
}
if err = cliClient.Call("svc.Setting", in, out); err != nil {
return err
}
log.Info(cmd.Context(), out.Msg)
default:
return fmt.Errorf("unknown set variable(debug is available now)")
}
return nil
},
})
}

37
internal/cmd/execute.go Normal file
View File

@ -0,0 +1,37 @@
package cmd
import (
"context"
"esway/internal/api"
"esway/internal/controller"
"esway/internal/database/cache"
"esway/internal/database/db"
"esway/internal/gateway"
"esway/internal/log"
"esway/internal/model"
"esway/internal/opt"
"esway/internal/tool"
)
func execute(ctx context.Context) error {
tool.Must(opt.Init(opt.Cfg.Config))
tool.Must(db.Init(ctx, opt.Cfg.DB.Uri))
tool.Must(cache.Init())
tool.Must(model.Init(db.Default.Session()))
tool.Must(controller.Init(ctx))
tool.Must(gateway.Start(ctx))
tool.Must(api.Start(ctx))
<-ctx.Done()
log.Warn(ctx, "received quit signal...(2s)")
<-tool.Timeout(2).Done()
return nil
}
func Execute(ctx context.Context) error {
return rootCommand.ExecuteContext(ctx)
}

13
internal/cmd/init.go Normal file
View File

@ -0,0 +1,13 @@
package cmd
import (
"time"
)
func init() {
time.Local = time.FixedZone("CST", 8*3600)
initCli()
initRoot(cliCommand)
}

26
internal/cmd/root.go Normal file
View File

@ -0,0 +1,26 @@
package cmd
import (
"esway/internal/opt"
"github.com/loveuer/nf/nft/log"
"github.com/spf13/cobra"
)
var rootCommand = &cobra.Command{
Use: "esway",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if opt.Cfg.Debug {
log.SetLogLevel(log.LogLevelDebug)
}
},
RunE: func(cmd *cobra.Command, args []string) error {
return execute(cmd.Context())
},
}
func initRoot(cmds ...*cobra.Command) {
rootCommand.PersistentFlags().BoolVar(&opt.Cfg.Debug, "debug", false, "debug mode")
rootCommand.PersistentFlags().StringVarP(&opt.Cfg.Config, "config", "c", "etc/config.json", "config json file path")
rootCommand.AddCommand(cmds...)
}

View File

@ -0,0 +1,14 @@
package controller
import "context"
var (
// UserController todo: 可以实现自己的 controller
UserController userController
)
func Init(ctx context.Context) error {
UserController = uc{}
return nil
}

164
internal/controller/user.go Normal file
View File

@ -0,0 +1,164 @@
package controller
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"esway/internal/database/cache"
"esway/internal/database/db"
"esway/internal/log"
"esway/internal/model"
"esway/internal/opt"
"esway/internal/tool"
"github.com/loveuer/nf/nft/resp"
"github.com/spf13/cast"
"gorm.io/gorm"
)
type userController interface {
GetUser(ctx context.Context, id uint64) (*model.User, error)
GetUserByToken(ctx context.Context, token string) (*model.User, error)
CacheUser(ctx context.Context, user *model.User) error
CacheToken(ctx context.Context, token string, user *model.User) error
RmToken(ctx context.Context, token string) error
RmUserCache(ctx context.Context, id uint64) error
DeleteUser(ctx context.Context, target *model.User) error
}
type uc struct{}
var _ userController = (*uc)(nil)
func (u uc) GetUser(ctx context.Context, 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(ctx, "controller.GetUser: get user by cache key=%s err=%v", key, err)
goto ByDB
}
if err = json.Unmarshal(bs, target); err != nil {
log.Warn(ctx, "controller.GetUser: json unmarshal key=%s by=%s err=%v", key, string(bs), err)
goto ByDB
}
return target, nil
}
ByDB:
if err = db.Default.Session(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(ctx, target); err != nil {
log.Warn(ctx, "controller.GetUser: cache user key=%s err=%v", key, err)
}
}
return target, nil
}
func (u uc) GetUserByToken(ctx context.Context, 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(ctx, "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(ctx, userId); err != nil {
return nil, err
}
return op, nil
}
func (u uc) CacheUser(ctx context.Context, 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(ctx context.Context, 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) RmToken(ctx context.Context, token string) 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.Del(tool.Timeout(3), key)
}
func (u uc) RmUserCache(ctx context.Context, 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(ctx context.Context, target *model.User) error {
var (
err error
now = time.Now()
username = fmt.Sprintf("%s@%d", target.Username, now.UnixMilli())
)
if err = db.Default.Session(tool.Timeout(5)).
Model(&model.User{}).
Where("id = ?", target.Id).
Updates(map[string]any{
"deleted_at": now.UnixMilli(),
"username": username,
}).Error; err != nil {
return resp.NewError(500, "", err, nil)
}
if opt.EnableUserCache {
if err = u.RmUserCache(ctx, target.Id); err != nil {
log.Warn(ctx, "controller.DeleteUser: rm user=%d cache err=%v", target.Id, err)
}
}
return nil
}

111
internal/database/cache/cache_lru.go vendored Normal file
View File

@ -0,0 +1,111 @@
package cache
import (
"context"
"time"
"esway/internal/interfaces"
"github.com/hashicorp/golang-lru/v2/expirable"
_ "github.com/hashicorp/golang-lru/v2/expirable"
)
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](1024*1024, nil, 0)
return &_lru{client: client}, nil
}

75
internal/database/cache/cache_memory.go vendored Normal file
View File

@ -0,0 +1,75 @@
package cache
import (
"context"
"errors"
"fmt"
"time"
"esway/internal/interfaces"
"gitea.com/loveuer/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 {
if errors.Is(err, gredis.ErrKeyNotFound) {
return nil, ErrorKeyNotFound
}
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 {
if errors.Is(err, gredis.ErrKeyNotFound) {
return nil, ErrorKeyNotFound
}
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
}

63
internal/database/cache/cache_redis.go vendored Normal file
View File

@ -0,0 +1,63 @@
package cache
import (
"context"
"errors"
"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 {
if errors.Is(err, redis.Nil) {
return nil, ErrorKeyNotFound
}
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 {
if errors.Is(err, redis.Nil) {
return nil, ErrorKeyNotFound
}
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()
}

37
internal/database/cache/client.go vendored Normal file
View File

@ -0,0 +1,37 @@
package cache
import (
"encoding/json"
"esway/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
)
switch value.(type) {
case []byte:
return value.([]byte), nil
}
if imp, ok := value.(encoded_value); ok {
bs, err = imp.MarshalBinary()
} else {
bs, err = json.Marshal(value)
}
return bs, err
}

7
internal/database/cache/error.go vendored Normal file
View File

@ -0,0 +1,7 @@
package cache
import "errors"
var (
ErrorKeyNotFound = errors.New("key not found")
)

69
internal/database/cache/init.go vendored Normal file
View File

@ -0,0 +1,69 @@
package cache
import (
"fmt"
"net/url"
"strings"
"esway/internal/opt"
"esway/internal/tool"
"gitea.com/loveuer/gredis"
"github.com/go-redis/redis/v8"
)
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
}

View File

@ -0,0 +1,52 @@
package db
import (
"context"
"esway/internal/opt"
"esway/internal/tool"
"gorm.io/gorm"
)
var Default *Client
type DBType string
const (
DBTypeSqlite = "sqlite"
DBTypeMysql = "mysql"
DBTypePostgres = "postgres"
)
type Client struct {
ctx context.Context
cli *gorm.DB
dbType DBType
}
func (c *Client) Type() DBType {
return c.dbType
}
func (c *Client) Session(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 := c.cli.Session(&gorm.Session{Context: ctx})
if opt.Cfg.Debug {
session = session.Debug()
}
return session
}
func (c *Client) Close() {
d, _ := c.cli.DB()
d.Close()
}

View File

@ -0,0 +1,8 @@
package db
import (
"testing"
)
func TestOpen(t *testing.T) {
}

View File

@ -0,0 +1,55 @@
package db
import (
"context"
"fmt"
"strings"
"github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func New(ctx context.Context, uri string) (*Client, error) {
parts := strings.SplitN(uri, "::", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("db.Init: opt db uri invalid: %s", uri)
}
c := &Client{}
var (
err error
dsn = parts[1]
)
switch parts[0] {
case "sqlite":
c.dbType = DBTypeSqlite
c.cli, err = gorm.Open(sqlite.Open(dsn))
case "mysql":
c.dbType = DBTypeMysql
c.cli, err = gorm.Open(mysql.Open(dsn))
case "postgres":
c.dbType = DBTypePostgres
c.cli, err = gorm.Open(postgres.Open(dsn))
default:
return nil, fmt.Errorf("db type only support: [sqlite, mysql, postgres], unsupported db type: %s", parts[0])
}
if err != nil {
return nil, fmt.Errorf("db.Init: open %s with dsn:%s, err: %w", parts[0], dsn, err)
}
return c, nil
}
func Init(ctx context.Context, uri string) (err error) {
if Default, err = New(ctx, uri); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,39 @@
package es
import (
"context"
"net/url"
"esway/internal/tool"
elastic "github.com/elastic/go-elasticsearch/v7"
"github.com/loveuer/esgo2dump/xes/es7"
"github.com/loveuer/nf/nft/log"
)
var Default *elastic.Client
func New(ctx context.Context, uri string) (*elastic.Client, error) {
var (
err error
client *elastic.Client
ins *url.URL
)
if ins, err = url.Parse(uri); err != nil {
return nil, err
}
log.Debug("es.InitClient url parse uri: %s, result: %+v", uri, ins)
if client, err = es7.NewClient(tool.Timeout(10), ins); err != nil {
return nil, err
}
return client, nil
}
func Init(ctx context.Context, uri string) (err error) {
Default, err = New(ctx, uri)
return err
}

View File

@ -0,0 +1 @@
package es

View File

@ -0,0 +1,63 @@
package gateway
import (
"context"
"fmt"
"net"
"strings"
"esway/internal/middleware/analysis"
"esway/internal/middleware/logger"
"esway/internal/opt"
"esway/internal/tool"
"github.com/loveuer/nf"
)
func Start(ctx context.Context) error {
ch := make(chan struct{})
app := nf.New(nf.Config{
BodyLimit: 4 * 1024 * 1024,
DisableLogger: true,
DisableBanner: true,
DisableMessagePrint: true,
})
h, err := proxy(ctx)
if err != nil {
return err
}
app.Use(logger.New(logger.Config{IgnoreFn: func(c *nf.Ctx) bool {
path := c.Path()
if strings.HasPrefix(path, "/.") || strings.HasPrefix(path, "/_") {
return true
}
return false
}}))
app.Use(analysis.New())
app.Any("/*any", h)
ln, err := net.Listen("tcp", opt.Cfg.Listen.Gateway)
if err != nil {
return fmt.Errorf("gateway listen at %s err: %s", opt.Cfg.Listen.Gateway, err.Error())
}
go func() {
ch <- struct{}{}
fmt.Printf("esway: gateway listen at %s\n", opt.Cfg.Listen.Gateway)
_ = app.RunListener(ln)
}()
<-ch
go func() {
ch <- struct{}{}
<-ctx.Done()
_ = app.Shutdown(tool.Timeout(2))
}()
<-ch
return nil
}

51
internal/gateway/proxy.go Normal file
View File

@ -0,0 +1,51 @@
package gateway
import (
"context"
"fmt"
"net/http/httputil"
"net/url"
"sync/atomic"
"esway/internal/log"
"esway/internal/opt"
"github.com/loveuer/nf"
)
func proxy(ctx context.Context) (nf.HandlerFunc, error) {
if len(opt.Cfg.Endpoints) == 0 {
return nil, fmt.Errorf("gateway: 必须要指定 elasticsearch endpoints")
}
urls := make([]*url.URL, 0)
for _, item := range opt.Cfg.Endpoints {
ins, err := url.Parse(item)
if err != nil {
log.Warn(ctx, "gateway: endpoint invalid, endpoint = %s, err = %s", item, err.Error())
}
urls = append(urls, ins)
}
if len(urls) == 0 {
return nil, fmt.Errorf("gateway: no valid elasticsearch endpoint")
}
svcs := make([]*httputil.ReverseProxy, len(urls))
for idx, item := range urls {
svcs[idx] = httputil.NewSingleHostReverseProxy(item)
}
var (
round int64 = 0
length = int64(len(svcs))
)
return func(c *nf.Ctx) error {
svc := svcs[atomic.SwapInt64(&round, (round+1)%length)]
svc.ServeHTTP(c.Writer, c.Request)
return nil
}, nil
}

103
internal/handler/log.go Normal file
View File

@ -0,0 +1,103 @@
package handler
import (
"esway/internal/database/db"
"esway/internal/log"
"esway/internal/model"
"esway/internal/opt"
"esway/internal/sqlType"
"esway/internal/tool"
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/resp"
)
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.Default.Session(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.Default.Session(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.Context(), "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.Context(), "handler.LogList: log=%d template=%s render map=%+v err=%v", logItem.Id, logItem.Type.Template(), m, err)
continue
}
log.Debug(c.Context(), "handler.LogList: log=%d render map=%+v string=%s", logItem.Id, m, logItem.HTML)
}
return resp.Resp200(c, nf.Map{"list": list, "total": total})
}

585
internal/handler/user.go Normal file
View File

@ -0,0 +1,585 @@
package handler
import (
"errors"
"fmt"
"net/http"
"time"
"esway/internal/controller"
"esway/internal/database/cache"
"esway/internal/database/db"
"esway/internal/middleware/oplog"
"esway/internal/model"
"esway/internal/opt"
"esway/internal/sqlType"
"esway/internal/tool"
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/resp"
"github.com/samber/lo"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
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.Default.Session(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.Context(), 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.Context(), 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
)
// 获取之前的 token
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)
}
// 删掉之前的 token
if len(bs) > 0 {
_ = controller.UserController.RmToken(c.Context(), string(bs))
}
// 将当前的 token 存入 last_token
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.Context(), 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 UserUpdate(c *nf.Ctx) error {
type Req struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
Nickname string `json:"nickname"`
}
type Model struct {
Password string `gorm:"column:password"`
}
var (
ok bool
err error
req = new(Req)
user *model.User
m = new(Model)
updates = make(map[string]any)
changes = make(map[string]any)
)
if user, ok = c.Locals("user").(*model.User); !ok {
return resp.Resp401(c, nil)
}
if err = c.BodyParser(req); err != nil {
return resp.Resp400(c, err)
}
if err = c.BodyParser(&changes); err != nil {
return resp.Resp400(c, err)
}
if _, ok = changes["nickname"]; ok {
updates["nickname"] = req.Nickname
}
if req.OldPassword != "" && req.NewPassword != "" {
if err = tool.CheckPassword(req.NewPassword); err != nil {
return resp.Resp400(c, req, err.Error())
}
if err = db.Default.Session(tool.Timeout(3)).
Select("password").
Model(&model.User{}).
Where("username = ?", user.Username).
Where("deleted_at = 0").
Take(m).
Error; err != nil {
return resp.Resp500(c, err.Error())
}
if !tool.ComparePassword(req.OldPassword, m.Password) {
return resp.Resp400(c, nil, "原密码错误")
}
updates["password"] = tool.NewPassword(req.NewPassword)
}
if len(updates) == 0 {
return resp.Resp400(c, nf.Map{"req": req, "reason": "nothing to update"}, "没有需要更新的内容")
}
if err = db.Default.Session(tool.Timeout(5)).
Model(&model.User{}).
Where("id = ?", user.Id).
Updates(updates).
Error; err != nil {
return resp.Resp500(c, err.Error())
}
if _, ok = updates["password"]; ok {
_ = controller.UserController.RmUserCache(c.Context(), user.Id)
c.SetHeader("Set-Cookie", fmt.Sprintf("%s=;Path=/", opt.CookieName))
return c.Redirect(opt.LoginURL, http.StatusFound)
}
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})
}
txList := op.Role.Where(db.Default.Session(tool.Timeout(10)).
Model(&model.User{}).
Where("deleted_at = 0"))
txCount := op.Role.Where(db.Default.Session(tool.Timeout(5)).
Model(&model.User{}).
Select("COUNT(id)").
Where("deleted_at = 0"))
if req.Keyword != "" {
keyword := fmt.Sprintf("%%%s%%", req.Keyword)
txList = txList.Where("username LIKE ?", keyword)
txCount = txCount.Where("username LIKE ?", keyword)
}
if err = txList.
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 = txCount.
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.Default.Session(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.Context(), 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.Default.Session(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.Context(), 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.Context(), 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.Context(), target); 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, "删除成功")
}

View 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
}

View File

@ -0,0 +1,11 @@
package interfaces
type Enum interface {
Value() int64
Code() string
Label() string
MarshalJSON() ([]byte, error)
All() []Enum
}

View File

@ -0,0 +1,14 @@
package interfaces
type OpLogger interface {
Enum
Render(content map[string]any) (string, error)
Template() string
}
type Logger interface {
Debug(msg string, data ...any)
Info(msg string, data ...any)
Warn(msg string, data ...any)
Error(msg string, data ...any)
}

89
internal/invoke/client.go Normal file
View File

@ -0,0 +1,89 @@
package invoke
import (
"fmt"
"sync"
"time"
"esway/internal/tool"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/resolver"
)
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
}

View 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
}

View 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
View 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")
}
}

46
internal/log/log.go Normal file
View File

@ -0,0 +1,46 @@
package log
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/loveuer/nf"
ulog "github.com/loveuer/nf/nft/log"
)
func _mix(ctx context.Context, msg string) string {
if ctx == nil {
return fmt.Sprintf("%s | %s", uuid.Must(uuid.NewV7()).String(), msg)
}
traceId := ctx.Value(nf.TraceKey)
if traceId == nil {
return fmt.Sprintf("%s | %s", uuid.Must(uuid.NewV7()).String(), msg)
}
return fmt.Sprintf("%s | %s", traceId, msg)
}
func Debug(ctx context.Context, msg string, data ...any) {
ulog.Debug(_mix(ctx, msg), data...)
}
func Info(ctx context.Context, msg string, data ...any) {
ulog.Info(_mix(ctx, msg), data...)
}
func Warn(ctx context.Context, msg string, data ...any) {
ulog.Warn(_mix(ctx, msg), data...)
}
func Error(ctx context.Context, msg string, data ...any) {
ulog.Error(_mix(ctx, msg), data...)
}
func Panic(ctx context.Context, msg string, data ...any) {
ulog.Panic(_mix(ctx, msg), data...)
}
func Fatal(ctx context.Context, msg string, data ...any) {
ulog.Fatal(_mix(ctx, msg), data...)
}

View File

@ -0,0 +1,69 @@
/*
analysis:
对访问 es 的 api 做一个初步的分类:
- 是不是一次 client 的请求
- 操作的 indixes 是哪些
- 使用的对应的 es 的 api 是哪个?
*/
package analysis
import (
"strings"
"esway/internal/log"
"esway/internal/model"
"esway/internal/opt"
"github.com/loveuer/nf"
"github.com/samber/lo"
)
const LocalKey = "es"
func New() nf.HandlerFunc {
return func(c *nf.Ctx) error {
local := &model.ESReqRes{
ClientRequest: false,
Path: c.Path(),
Method: c.Method(),
Indixes: []string{},
Api: model.ESApiUnknown,
}
defer func() {
c.Locals(LocalKey, local)
if opt.Cfg.Debug {
log.Debug(c.Context(), "middleware.analysis: local = %#v", *local)
}
}()
if len(local.Path) == 0 || strings.HasPrefix(local.Path, "/_") || strings.HasPrefix(local.Method, "/.") {
return c.Next()
}
paths := strings.Split(local.Path[1:], "/")
local.ClientRequest = true
local.Indixes = lo.FilterMap(
strings.Split(paths[0], ","),
func(item string, idx int) (string, bool) {
val := strings.TrimSpace(item)
return val, val != ""
},
)
if len(paths) < 2 {
return c.Next()
}
switch paths[1] {
case "_doc":
case "_search":
case "_update":
case "_update_by_query":
}
return c.Next()
}
}

View File

@ -0,0 +1,54 @@
package auth
import (
"errors"
"strings"
"esway/internal/controller"
"esway/internal/database/cache"
"esway/internal/log"
"esway/internal/opt"
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/resp"
)
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.Context(), "middleware.NewAuth: token=%s", token)
target, err := controller.UserController.GetUserByToken(c.Context(), token)
if err != nil {
log.Error(c.Context(), "middleware.NewAuth: get user by token=%s err=%v", token, err)
if errors.Is(err, cache.ErrorKeyNotFound) {
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()
}
}

129
internal/middleware/cache/cache.go vendored Normal file
View File

@ -0,0 +1,129 @@
package cache
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"time"
"esway/internal/database/cache"
"esway/internal/model"
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/log"
)
var (
defaultKeyFn = func(c *nf.Ctx) string {
return c.Request.URL.String()
}
defaultTimeout = 3600
defaultPrefix = "midd:cache"
)
type Config struct {
// if return "" (won't cache)
KeyFn func(c *nf.Ctx) string
// cache timeout(seconds)
Timeout int
Prefix string
Refresh bool
}
type store struct {
Body []byte `json:"body"`
Header http.Header `json:"header"`
When int64 `json:"when"`
}
func New(cfgs ...Config) nf.HandlerFunc {
if cache.Client == nil {
log.Panic("[middleware.cache] database cache client is nil")
}
var cfg Config
if len(cfgs) > 0 {
cfg = cfgs[0]
}
if cfg.KeyFn == nil {
cfg.KeyFn = defaultKeyFn
}
if cfg.Timeout <= 0 {
cfg.Timeout = defaultTimeout
}
if cfg.Prefix == "" {
cfg.Prefix = defaultPrefix
}
return func(c *nf.Ctx) error {
var (
key string
err error
bs []byte
res = new(store)
)
if key = cfg.KeyFn(c); key == "" {
return c.Next()
}
key = cfg.Prefix + ":" + key
duration := time.Duration(cfg.Timeout) * time.Second
if cfg.Refresh {
if bs, err = cache.Client.GetEx(c.Context(), key, duration); err != nil {
if !errors.Is(err, cache.ErrorKeyNotFound) {
log.Warn("[middleware.cache] cache get err: %s", err.Error())
}
goto FromNext
}
} else {
if bs, err = cache.Client.Get(c.Context(), key); err != nil {
if !errors.Is(err, cache.ErrorKeyNotFound) {
log.Warn("[middleware.cache] cache get err: %s", err.Error())
}
goto FromNext
}
}
if err = json.Unmarshal(bs, res); err != nil {
log.Warn("[middleware.cache] cache data unamrshal err: %s", err.Error())
goto FromNext
}
for key := range res.Header {
for idx := range res.Header[key] {
c.SetHeader(key, res.Header[key][idx])
}
}
c.SetHeader("X-Nf-Cache-At", strconv.Itoa(int(res.When)))
_, err = c.Write(res.Body)
return err
FromNext:
blw := model.NewCopyWriter(c.Writer)
c.Writer = blw
rerr := c.Next()
resp := blw.Bytes()
data := &store{Body: resp, Header: blw.Header().Clone(), When: time.Now().UnixMilli()}
cbs, _ := json.Marshal(data)
if err = cache.Client.SetEx(c.Context(), key, cbs, duration); err != nil {
log.Warn("[middleware.cache] cache client setex err: %s", err.Error())
}
return rerr
}
}

View File

@ -0,0 +1,71 @@
package logger
import (
"fmt"
"strconv"
"time"
"esway/internal/opt"
"esway/internal/tool"
"github.com/loveuer/esgo2dump/log"
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/resp"
)
type Config struct {
IgnoreFn func(c *nf.Ctx) bool
}
var defaultConfig = Config{
IgnoreFn: func(c *nf.Ctx) bool { return false },
}
func New(configs ...Config) nf.HandlerFunc {
return func(c *nf.Ctx) error {
var (
now = time.Now()
logFn func(msg string, data ...any)
ip = c.IP()
cfg Config
)
if len(configs) > 0 {
cfg = configs[0]
}
if cfg.IgnoreFn == nil {
cfg.IgnoreFn = defaultConfig.IgnoreFn
}
traceId := c.Context().Value(nf.TraceKey)
c.Locals(nf.TraceKey, traceId)
err := c.Next()
c.Writer.Header().Set(nf.TraceKey, fmt.Sprint(traceId))
c.Writer.Header().Add("X-NF-Module", opt.Cfg.Name)
if cfg.IgnoreFn(c) {
return err
}
status, _ := strconv.Atoi(c.Writer.Header().Get(resp.RealStatusHeader))
duration := time.Since(now)
msg := fmt.Sprintf("%s | %15s | %d[%3d] | %s | %6s | %s", traceId, ip, c.StatusCode, status, tool.HumanDuration(duration.Nanoseconds()), c.Method(), c.Path())
switch {
case status >= 500:
logFn = log.Error
case status >= 400:
logFn = log.Warn
default:
logFn = log.Info
}
logFn(msg)
return err
}
}

View File

@ -0,0 +1,118 @@
package oplog
import (
"context"
"sync"
"time"
"esway/internal/database/db"
"esway/internal/model"
"esway/internal/opt"
"esway/internal/sqlType"
"esway/internal/tool"
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/log"
)
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.Default.Session(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
}
}

View File

@ -0,0 +1,8 @@
package oplog
import "esway/internal/model"
type OpLog struct {
Type model.OpLogType
Content map[string]any
}

View File

@ -0,0 +1,87 @@
package privilege
import (
"fmt"
"strings"
"esway/internal/model"
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/log"
"github.com/loveuer/nf/nft/resp"
"github.com/samber/lo"
)
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()
}
}

9
internal/model/es.go Normal file
View File

@ -0,0 +1,9 @@
package model
type ESReqRes struct {
ClientRequest bool // 是否是 es client 的请求
Path string
Method string
Indixes []string
Api ESApi
}

89
internal/model/init.go Normal file
View File

@ -0,0 +1,89 @@
package model
import (
"fmt"
"strings"
"esway/internal/opt"
"esway/internal/sqlType"
"github.com/loveuer/nf/nft/log"
"gorm.io/gorm"
)
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{},
&Token{},
&TokenIndex{},
); 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
}

View 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
}

290
internal/model/oplog.go Normal file
View File

@ -0,0 +1,290 @@
package model
import (
"bytes"
"encoding/base64"
"encoding/json"
"html/template"
"time"
"esway/internal/sqlType"
"github.com/spf13/cast"
"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/html"
)
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"`
Content sqlType.JSONB `json:"content" gorm:"column:content;type:jsonb"`
HTML string `json:"html" gorm:"-"`
}

View 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)

87
internal/model/role.go Normal file
View File

@ -0,0 +1,87 @@
package model
import (
"encoding/json"
"esway/internal/opt"
"gorm.io/gorm"
)
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
}

36
internal/model/token.go Normal file
View File

@ -0,0 +1,36 @@
package model
import "esway/internal/sqlType"
type Token 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"`
CreatedBy uint64
Token string
Comment string
}
type TokenIndex 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"`
TokenId uint64
Index string
APIs sqlType.StrSlice
}
type ESApi string
const (
ESApiUnknown ESApi = "unknown"
ESApiSearch ESApi = "search"
ESApiGetDoc ESApi = "doc"
ESApiUpdateDoc ESApi = "update"
ESApiUpdateByQuery ESApi = "update_by_query"
ESApiReindex ESApi = "reindex"
)

227
internal/model/user.go Normal file
View File

@ -0,0 +1,227 @@
package model
import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"esway/internal/opt"
"esway/internal/sqlType"
"esway/internal/tool"
"github.com/golang-jwt/jwt/v5"
"github.com/loveuer/nf/nft/log"
"github.com/samber/lo"
"github.com/spf13/cast"
)
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,
})
}

34
internal/model/writer.go Normal file
View File

@ -0,0 +1,34 @@
package model
import (
"bytes"
"io"
"github.com/loveuer/nf"
)
type CopyResponseWriter struct {
nf.ResponseWriter
buf *bytes.Buffer
}
func (w CopyResponseWriter) Write(b []byte) (int, error) {
w.buf.Write(b)
return w.ResponseWriter.Write(b)
}
func (w CopyResponseWriter) String() string {
return w.buf.String()
}
func (w CopyResponseWriter) Bytes() []byte {
return w.buf.Bytes()
}
func (w CopyResponseWriter) Reader() io.Reader {
return w.buf
}
func NewCopyWriter(w nf.ResponseWriter) *CopyResponseWriter {
return &CopyResponseWriter{buf: bytes.NewBuffer(make([]byte, 0, 1024)), ResponseWriter: w}
}

62
internal/opt/opt.go Normal file
View File

@ -0,0 +1,62 @@
package opt
import (
"encoding/json"
"fmt"
"os"
"esway/internal/tool"
"github.com/loveuer/nf/nft/log"
)
type listen struct {
Gateway string `json:"gateway"`
Dashboard string `json:"dashboard"`
}
type db struct {
Type string `json:"-"` // postgres, mysql, sqlite
Uri string `json:"uri"`
}
type cache struct {
Uri string `json:"uri"`
}
type config struct {
Name string `json:"name"`
Debug bool `json:"-"`
Dev bool `json:"-"`
Config string `json:"-"`
Listen listen `json:"listen"`
Endpoints []string `json:"endpoints"`
DB db `json:"db"`
Cache cache `json:"cache"`
}
var (
Mode string
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)
}
tool.TablePrinter(Cfg)
return nil
}

55
internal/opt/var.go Normal file
View File

@ -0,0 +1,55 @@
package opt
import (
"sync"
"time"
)
const (
RpcAddress = "tcp://127.0.0.1:8081" // unix:///tmp/xxx.sock
// 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 = "esway"
// 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-Id"
LoginURL = "/login"
)
var (
Locker = &sync.Mutex{}
// todo: 颁发的 token, (cookie) 在缓存中存在的时间 (每次请求该时间也会被刷新)
TokenTimeout = time.Duration(3600*12) * time.Second
Start = time.Now()
OK bool
)

9
internal/sqlType/err.go Normal file
View 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
View 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)
}

View 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
View 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{}{}
}
}

View 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
}

View 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
View 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
View 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
View 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
View 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
}

View 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
View 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)
}

124
internal/tool/table.go Normal file
View File

@ -0,0 +1,124 @@
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:
w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), rv.Interface()})
case reflect.String:
val := rv.String()
if len(val) <= 160 {
w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), val})
return
}
w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), val[0:64] + "..." + val[len(val)-64:]})
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
View 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
}

59
internal/unix/handler.go Normal file
View File

@ -0,0 +1,59 @@
package unix
import (
"context"
"fmt"
"time"
"esway/internal/log"
"esway/internal/opt"
)
type Handler struct {
Ctx context.Context
}
type (
AvailableReq struct{}
AvailableResp struct {
OK bool
Now time.Time
Start time.Time
Duration string
}
)
func (*Handler) Available(_ *AvailableReq, out *AvailableResp) error {
now := time.Now()
out.OK, out.Now = opt.OK, now
out.Start = opt.Start
out.Duration = fmt.Sprint(now.Sub(opt.Start))
return nil
}
type SettingReq struct {
Debug bool
}
type Resp[T any] struct {
Status uint32
Msg string
Data T
}
func (h *Handler) Setting(in *SettingReq, out *Resp[bool]) error {
opt.Locker.Lock()
defer opt.Locker.Unlock()
if in.Debug {
opt.Cfg.Debug = true
log.Info(h.Ctx, "set global debug[true]")
} else {
opt.Cfg.Debug = false
log.Info(h.Ctx, "set global debug[false]")
}
out.Status = 200
out.Msg = "操作成功"
return nil
}

52
internal/unix/start.go Normal file
View File

@ -0,0 +1,52 @@
package unix
import (
"context"
"net"
"net/rpc"
"net/url"
"esway/internal/log"
"esway/internal/opt"
)
func Start(ctx context.Context) error {
ready := make(chan bool)
defer close(ready)
uri, err := url.Parse(opt.RpcAddress)
if err != nil {
return err
}
address := uri.Host + uri.Path
log.Debug(ctx, "[rpc-svc] listen at [%s] [%s]", uri.Scheme, address)
ln, err := net.Listen(uri.Scheme, address)
if err != nil {
return err
}
go func() {
ready <- true
<-ctx.Done()
_ = ln.Close()
}()
<-ready
svc := rpc.NewServer()
if err = svc.RegisterName("svc", &Handler{Ctx: ctx}); err != nil {
return err
}
go func() {
log.Info(ctx, "[rpc-svc] start at: [%s] [%s]", uri.Scheme, address)
ready <- true
svc.Accept(ln)
}()
<-ready
return nil
}