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

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