wip: 完成 client api 分析
This commit is contained in:
69
internal/middleware/analysis/new.go
Normal file
69
internal/middleware/analysis/new.go
Normal 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()
|
||||
}
|
||||
}
|
54
internal/middleware/auth/auth.go
Normal file
54
internal/middleware/auth/auth.go
Normal 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
129
internal/middleware/cache/cache.go
vendored
Normal 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
|
||||
}
|
||||
}
|
71
internal/middleware/logger/logger.go
Normal file
71
internal/middleware/logger/logger.go
Normal 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
|
||||
}
|
||||
}
|
118
internal/middleware/oplog/new.go
Normal file
118
internal/middleware/oplog/new.go
Normal 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
|
||||
}
|
||||
}
|
8
internal/middleware/oplog/oplog.go
Normal file
8
internal/middleware/oplog/oplog.go
Normal file
@ -0,0 +1,8 @@
|
||||
package oplog
|
||||
|
||||
import "esway/internal/model"
|
||||
|
||||
type OpLog struct {
|
||||
Type model.OpLogType
|
||||
Content map[string]any
|
||||
}
|
87
internal/middleware/privilege/privilege.go
Normal file
87
internal/middleware/privilege/privilege.go
Normal 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()
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user