wip: alpha version
60
internal/api/api.go
Normal file
@ -0,0 +1,60 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nfflow/internal/handler"
|
||||
"github.com/loveuer/nfflow/internal/middleware/auth"
|
||||
"github.com/loveuer/nfflow/internal/middleware/front"
|
||||
"github.com/loveuer/nfflow/internal/middleware/oplog"
|
||||
"github.com/loveuer/nfflow/internal/middleware/privilege"
|
||||
"github.com/loveuer/nfflow/internal/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
func initApp(ctx context.Context) *nf.App {
|
||||
engine := nf.New(nf.Config{})
|
||||
|
||||
app := engine.Group("/api")
|
||||
app.Get("/available", func(c *nf.Ctx) error {
|
||||
return c.JSON(nf.Map{"status": 200, "ok": true, "time": time.Now()})
|
||||
})
|
||||
|
||||
{
|
||||
api := app.Group("/user")
|
||||
api.Post("/auth/login", oplog.NewOpLog(ctx), handler.AuthLogin)
|
||||
api.Get("/auth/login", auth.NewAuth(), handler.AuthVerify)
|
||||
api.Post("/auth/logout", auth.NewAuth(), oplog.NewOpLog(ctx), handler.AuthLogout)
|
||||
|
||||
mng := api.Group("/manage")
|
||||
mng.Use(auth.NewAuth(), privilege.Verify(
|
||||
privilege.RelationAnd,
|
||||
model.PrivilegeUserManage,
|
||||
))
|
||||
|
||||
mng.Get("/user/list", handler.ManageUserList)
|
||||
mng.Post("/user/create", oplog.NewOpLog(ctx), handler.ManageUserCreate)
|
||||
mng.Post("/user/update", oplog.NewOpLog(ctx), handler.ManageUserUpdate)
|
||||
mng.Post("/user/delete", oplog.NewOpLog(ctx), handler.ManageUserDelete)
|
||||
}
|
||||
|
||||
{
|
||||
api := app.Group("/log")
|
||||
api.Use(auth.NewAuth(), privilege.Verify(privilege.RelationAnd, model.PrivilegeOpLog))
|
||||
api.Get("/category/list", handler.LogCategories())
|
||||
api.Get("/content/list", handler.LogList)
|
||||
}
|
||||
|
||||
{
|
||||
api := app.Group("/task")
|
||||
|
||||
api.Get("/list", handler.TaskList)
|
||||
api.Post("/create", handler.TaskCreate)
|
||||
api.Post("/input/create", handler.TaskInputCreate)
|
||||
api.Post("/input/update", handler.TaskInputUpdate)
|
||||
}
|
||||
|
||||
engine.Use(front.NewFront(&front.DefaultFront, "dist/front/browser"))
|
||||
|
||||
return engine
|
||||
}
|
40
internal/api/start.go
Normal file
@ -0,0 +1,40 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/nfflow/internal/opt"
|
||||
"github.com/loveuer/nfflow/internal/util"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net"
|
||||
)
|
||||
|
||||
func MustStart(ctx context.Context) {
|
||||
|
||||
app := initApp(ctx)
|
||||
ready := make(chan bool)
|
||||
|
||||
ln, err := net.Listen("tcp", opt.Cfg.Address)
|
||||
if err != nil {
|
||||
logrus.Panicf("api.MustStart: net listen tcp address=%v err=%v", opt.Cfg.Address, err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
ready <- true
|
||||
|
||||
if err = app.RunListener(ln); err != nil {
|
||||
logrus.Panicf("api.MustStart: app run err=%v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
<-ready
|
||||
|
||||
go func() {
|
||||
ready <- true
|
||||
<-ctx.Done()
|
||||
if err = app.Shutdown(util.Timeout(1)); err != nil {
|
||||
logrus.Errorf("api.MustStart: app shutdown err=%v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
<-ready
|
||||
}
|
24
internal/cmd/execute.go
Normal file
@ -0,0 +1,24 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/nfflow/internal/api"
|
||||
"github.com/loveuer/nfflow/internal/controller"
|
||||
"github.com/loveuer/nfflow/internal/database"
|
||||
"github.com/loveuer/nfflow/internal/model"
|
||||
"github.com/loveuer/nfflow/internal/opt"
|
||||
)
|
||||
|
||||
func Execute(ctx context.Context) error {
|
||||
opt.MustInitConfig()
|
||||
|
||||
database.MustInitClient()
|
||||
model.MustInit(database.DB)
|
||||
controller.Init(database.DB, database.Cache)
|
||||
|
||||
api.MustStart(ctx)
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
return nil
|
||||
}
|
11
internal/cmd/init.go
Normal file
@ -0,0 +1,11 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"github.com/loveuer/nfflow/internal/opt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&opt.ConfigFile, "c", "etc/config.json", "config json file path")
|
||||
flag.IntVar(&opt.Debug, "debug", 0, "")
|
||||
}
|
19
internal/controller/impl.go
Normal file
@ -0,0 +1,19 @@
|
||||
package controller
|
||||
|
||||
import "github.com/loveuer/nfflow/internal/database"
|
||||
|
||||
type uc struct {
|
||||
db database.Store
|
||||
c database.Caches
|
||||
}
|
||||
|
||||
var (
|
||||
_ userController = uc{}
|
||||
|
||||
// UserController todo: 可以实现自己的 controller
|
||||
UserController userController
|
||||
)
|
||||
|
||||
func Init(db database.Store, cache database.Caches) {
|
||||
UserController = uc{db: db, c: cache}
|
||||
}
|
108
internal/controller/input/es7/client.go
Normal file
@ -0,0 +1,108 @@
|
||||
package es7
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
elastic "github.com/elastic/go-elasticsearch/v7"
|
||||
"github.com/elastic/go-elasticsearch/v7/esapi"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Endpoints []string `json:"endpoints"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
CA string `json:"ca"`
|
||||
|
||||
cli *elastic.Client
|
||||
}
|
||||
|
||||
func (c *Client) InitClient(ctx context.Context) error {
|
||||
var (
|
||||
errCh = make(chan error)
|
||||
cliCh = make(chan *elastic.Client)
|
||||
hiddenCa = func(cs string) string {
|
||||
if len(cs) > 0 {
|
||||
return "******"
|
||||
}
|
||||
|
||||
return "nil"
|
||||
}
|
||||
)
|
||||
|
||||
logrus.Debugf("es7.NewClient: endpoints=%v (username=%s password=%s ca=%s)", c.Endpoints, c.Username, c.Password, hiddenCa(c.CA))
|
||||
|
||||
ncFunc := func(endpoints []string, username, password, ca string) {
|
||||
var (
|
||||
err error
|
||||
cli *elastic.Client
|
||||
infoResp *esapi.Response
|
||||
)
|
||||
|
||||
if cli, err = elastic.NewClient(
|
||||
elastic.Config{
|
||||
Addresses: endpoints,
|
||||
Username: username,
|
||||
Password: password,
|
||||
CACert: []byte(c.CA),
|
||||
RetryOnStatus: []int{429},
|
||||
MaxRetries: 3,
|
||||
RetryBackoff: nil,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
},
|
||||
); err != nil {
|
||||
logrus.Debugf("es7.NewClient: elastic new client with endponts=%v err=%v", endpoints, err)
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
if infoResp, err = cli.Info(); err != nil {
|
||||
logrus.Debugf("es7.NewClient: ping err=%v", err)
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
if infoResp.StatusCode != 200 {
|
||||
err = fmt.Errorf("info es status=%d", infoResp.StatusCode)
|
||||
logrus.Debugf("es7.NewClient: status err=%v", err)
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
cliCh <- cli
|
||||
}
|
||||
|
||||
go ncFunc(c.Endpoints, c.Username, c.Password, c.CA)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("dial es=%s err=%v", c.Endpoints, context.DeadlineExceeded)
|
||||
case c.cli = <-cliCh:
|
||||
return nil
|
||||
case e := <-errCh:
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Ping(ctx context.Context) error {
|
||||
rr, err := c.cli.Info(
|
||||
c.cli.Info.WithContext(ctx),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rr.StatusCode != 200 {
|
||||
return fmt.Errorf("ping status=%d msg=%s", rr.StatusCode, rr.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Save(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
12
internal/controller/interface.go
Normal file
@ -0,0 +1,12 @@
|
||||
package controller
|
||||
|
||||
import "github.com/loveuer/nfflow/internal/model"
|
||||
|
||||
type userController interface {
|
||||
GetUser(id uint64) (*model.User, error)
|
||||
GetUserByToken(token string) (*model.User, error)
|
||||
CacheUser(user *model.User) error
|
||||
CacheToken(token string, user *model.User) error
|
||||
RmUserCache(id uint64) error
|
||||
DeleteUser(id uint64) error
|
||||
}
|
139
internal/controller/user.go
Normal file
@ -0,0 +1,139 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"github.com/loveuer/nfflow/internal/database"
|
||||
"github.com/loveuer/nfflow/internal/model"
|
||||
"github.com/loveuer/nfflow/internal/opt"
|
||||
"github.com/loveuer/nfflow/internal/util"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cast"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (u uc) GetUser(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 = u.c.Get(util.Timeout(3), key); err != nil {
|
||||
logrus.Warnf("controller.GetUser: get user by cache key=%s err=%v", key, err)
|
||||
goto ByDB
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(bs, target); err != nil {
|
||||
logrus.Warnf("controller.GetUser: json unmarshal key=%s by=%s err=%v", key, string(bs), err)
|
||||
goto ByDB
|
||||
}
|
||||
|
||||
return target, nil
|
||||
}
|
||||
|
||||
ByDB:
|
||||
if err = u.db.Session(util.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(target); err != nil {
|
||||
logrus.Warnf("controller.GetUser: cache user key=%s err=%v", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func (u uc) GetUserByToken(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 := u.c.Get(util.Timeout(3), key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logrus.Tracef("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(userId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return op, nil
|
||||
}
|
||||
|
||||
func (u uc) CacheUser(target *model.User) error {
|
||||
key := fmt.Sprintf("%s:user:id:%d", opt.CachePrefix, target.Id)
|
||||
return u.c.Set(util.Timeout(3), key, target)
|
||||
}
|
||||
|
||||
func (u uc) CacheToken(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 u.c.SetEx(util.Timeout(3), key, user.Id, opt.TokenTimeout)
|
||||
}
|
||||
func (u uc) RmUserCache(id uint64) error {
|
||||
key := fmt.Sprintf("%s:user:id:%d", opt.CachePrefix, id)
|
||||
return u.c.Del(util.Timeout(3), key)
|
||||
}
|
||||
|
||||
func (u uc) DeleteUser(id uint64) error {
|
||||
var (
|
||||
err error
|
||||
now = time.Now()
|
||||
username = "CONCAT(username, '@del')"
|
||||
)
|
||||
|
||||
if opt.Cfg.Database.Type == "sqlite" {
|
||||
username = "username || '@del'"
|
||||
}
|
||||
|
||||
if err = database.DB.Session(util.Timeout(5)).
|
||||
Model(&model.User{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]any{
|
||||
"deleted_at": now.UnixMilli(),
|
||||
"username": gorm.Expr(username),
|
||||
}).Error; err != nil {
|
||||
return resp.NewError(500, "", err, nil)
|
||||
}
|
||||
|
||||
if opt.EnableUserCache {
|
||||
if err = u.RmUserCache(id); err != nil {
|
||||
logrus.Warnf("controller.DeleteUser: rm user=%d cache err=%v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
26
internal/database/cache.go
Normal file
@ -0,0 +1,26 @@
|
||||
package database
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type encoded_value interface {
|
||||
MarshalBinary() ([]byte, error)
|
||||
}
|
||||
|
||||
type decoded_value interface {
|
||||
UnmarshalBinary(bs []byte) error
|
||||
}
|
||||
|
||||
func handleValue(value any) ([]byte, error) {
|
||||
var (
|
||||
bs []byte
|
||||
err error
|
||||
)
|
||||
|
||||
if imp, ok := value.(encoded_value); ok {
|
||||
bs, err = imp.MarshalBinary()
|
||||
} else {
|
||||
bs, err = json.Marshal(value)
|
||||
}
|
||||
|
||||
return bs, err
|
||||
}
|
63
internal/database/cache_memory.go
Normal file
@ -0,0 +1,63 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"gitea.com/taozitaozi/gredis"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _ Caches = (*_mem)(nil)
|
||||
|
||||
type _mem struct {
|
||||
client *gredis.Gredis
|
||||
}
|
||||
|
||||
func (m *_mem) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
v, err := m.client.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bs, ok := v.([]byte)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid value type=%T", v)
|
||||
}
|
||||
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
func (m *_mem) GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error) {
|
||||
v, err := m.client.GetEx(key, duration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bs, ok := v.([]byte)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid value type=%T", v)
|
||||
}
|
||||
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
func (m *_mem) Set(ctx context.Context, key string, value any) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.client.Set(key, bs)
|
||||
}
|
||||
|
||||
func (m *_mem) SetEx(ctx context.Context, key string, value any, duration time.Duration) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.client.SetEx(key, bs, duration)
|
||||
}
|
||||
|
||||
func (m *_mem) Del(ctx context.Context, keys ...string) error {
|
||||
m.client.Delete(keys...)
|
||||
return nil
|
||||
}
|
54
internal/database/cache_redis.go
Normal file
@ -0,0 +1,54 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"time"
|
||||
)
|
||||
|
||||
type _redis struct {
|
||||
client *redis.Client
|
||||
}
|
||||
|
||||
func (r *_redis) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
result, err := r.client.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(result), nil
|
||||
}
|
||||
|
||||
func (r *_redis) GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error) {
|
||||
result, err := r.client.GetEx(ctx, key, duration).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(result), nil
|
||||
}
|
||||
|
||||
func (r *_redis) Set(ctx context.Context, key string, value any) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = r.client.Set(ctx, key, bs, redis.KeepTTL).Result()
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *_redis) SetEx(ctx context.Context, key string, value any, duration time.Duration) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = r.client.SetEX(ctx, key, bs, duration).Result()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *_redis) Del(ctx context.Context, keys ...string) error {
|
||||
return r.client.Del(ctx, keys...).Err()
|
||||
}
|
6
internal/database/client.go
Normal file
@ -0,0 +1,6 @@
|
||||
package database
|
||||
|
||||
var (
|
||||
DB Store
|
||||
Cache Caches
|
||||
)
|
89
internal/database/init.go
Normal file
@ -0,0 +1,89 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitea.com/taozitaozi/gredis"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/loveuer/nfflow/internal/opt"
|
||||
"github.com/loveuer/nfflow/internal/util"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func MustInitClient() {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
// todo: 可以实现自己的 "Store" sql_db, like sqlite, postgresql
|
||||
if DB, err = initSql(); err != nil {
|
||||
panic(fmt.Errorf("database.MustInitClient: init sql err=%v", err))
|
||||
}
|
||||
|
||||
// todo: 可以实现自己的 "Caches" 缓存, like redis
|
||||
if Cache, err = initCacher(); err != nil {
|
||||
panic(fmt.Errorf("database.MustInitCache: init cache err=%v", err))
|
||||
}
|
||||
}
|
||||
|
||||
func initSql() (Store, error) {
|
||||
var (
|
||||
err error
|
||||
client *gorm.DB
|
||||
)
|
||||
|
||||
switch opt.Cfg.Database.Type {
|
||||
case "postgresql":
|
||||
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai",
|
||||
opt.Cfg.Database.Host, opt.Cfg.Database.Username, opt.Cfg.Database.Password, opt.Cfg.Database.DB, opt.Cfg.Database.Port)
|
||||
if client, err = gorm.Open(postgres.Open(dsn)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "mysql":
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
opt.Cfg.Database.Username, opt.Cfg.Database.Password, opt.Cfg.Database.Host, opt.Cfg.Database.Port, opt.Cfg.Database.DB)
|
||||
if client, err = gorm.Open(mysql.Open(dsn)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "sqlite":
|
||||
if client, err = gorm.Open(sqlite.Open(opt.Cfg.Database.Path)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupportted databsae type=%s", opt.Cfg.Database.Type)
|
||||
}
|
||||
|
||||
return &_db{client: client}, nil
|
||||
}
|
||||
|
||||
func initCacher() (Caches, error) {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
switch opt.Cfg.Cache.Type {
|
||||
case "redis":
|
||||
var rc *redis.Client
|
||||
rc = redis.NewClient(&redis.Options{
|
||||
Addr: fmt.Sprintf("%s:%d", opt.Cfg.Cache.Host, opt.Cfg.Cache.Port),
|
||||
Username: opt.Cfg.Cache.Username,
|
||||
Password: opt.Cfg.Cache.Password,
|
||||
})
|
||||
|
||||
if err = rc.Ping(util.Timeout(5)).Err(); err != nil {
|
||||
return nil, fmt.Errorf("redis ping err=%v", err)
|
||||
}
|
||||
|
||||
return &_redis{client: rc}, nil
|
||||
case "memory":
|
||||
var mc *gredis.Gredis
|
||||
mc = gredis.NewGredis(-1)
|
||||
|
||||
return &_mem{client: mc}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupportted cache type=%s", opt.Cfg.Cache.Type)
|
||||
}
|
||||
|
||||
}
|
21
internal/database/interface.go
Normal file
@ -0,0 +1,21 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Caches 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
|
||||
}
|
||||
|
||||
type Store interface {
|
||||
Session(ctx context.Context) *gorm.DB
|
||||
}
|
20
internal/database/sql.go
Normal file
@ -0,0 +1,20 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/nfflow/internal/opt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type _db struct {
|
||||
client *gorm.DB
|
||||
}
|
||||
|
||||
func (d *_db) Session(ctx context.Context) *gorm.DB {
|
||||
s := d.client.Session(&gorm.Session{})
|
||||
if opt.Debug > 0 || opt.DBDebug {
|
||||
s = s.Debug()
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
102
internal/handler/log.go
Normal file
@ -0,0 +1,102 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"github.com/loveuer/nfflow/internal/database"
|
||||
"github.com/loveuer/nfflow/internal/model"
|
||||
"github.com/loveuer/nfflow/internal/opt"
|
||||
"github.com/loveuer/nfflow/internal/sqlType"
|
||||
"github.com/loveuer/nfflow/internal/util"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
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(database.DB.Session(util.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(database.DB.Session(util.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 _, log := range list {
|
||||
m := make(map[string]any)
|
||||
if err = log.Content.Bind(&m); err != nil {
|
||||
logrus.Warnf("handler.LogList: log=%d content=%v bind map[string]any err=%v", log.Id, log.Content, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if log.HTML, err = log.Type.Render(m); err != nil {
|
||||
logrus.Warnf("handler.LogList: log=%d template=%s render map=%+v err=%v", log.Id, log.Type.Template(), m, err)
|
||||
continue
|
||||
}
|
||||
|
||||
logrus.Tracef("handler.LogList: log=%d render map=%+v string=%s", log.Id, m, log.HTML)
|
||||
}
|
||||
|
||||
return resp.Resp200(c, nf.Map{"list": list, "total": total})
|
||||
}
|
125
internal/handler/task.go
Normal file
@ -0,0 +1,125 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"github.com/loveuer/nfflow/internal/database"
|
||||
"github.com/loveuer/nfflow/internal/model"
|
||||
"github.com/loveuer/nfflow/internal/opt"
|
||||
"github.com/loveuer/nfflow/internal/sqlType"
|
||||
"github.com/loveuer/nfflow/internal/util"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TaskList(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Keyword string `query:"keyword,omitempty"`
|
||||
Page int `query:"page,omitempty"`
|
||||
Size int `query:"size,omitempty"`
|
||||
Targets sqlType.NumSlice[uint64] `query:"targets,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
total int
|
||||
list = make([]*model.Task, 0)
|
||||
)
|
||||
|
||||
if err = c.QueryParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.Size <= 0 {
|
||||
req.Size = 20
|
||||
}
|
||||
|
||||
txGet := database.DB.Session(util.Timeout(10)).
|
||||
Model(&model.Task{})
|
||||
txCount := database.DB.Session(util.Timeout(5)).
|
||||
Model(&model.Task{}).
|
||||
Select("COUNT(id)")
|
||||
|
||||
if req.Keyword != "" {
|
||||
key := fmt.Sprintf("%%%s%%", req.Keyword)
|
||||
txGet = txGet.Where("task_name", key)
|
||||
txCount = txCount.Where("task_name", key)
|
||||
}
|
||||
|
||||
if err = txCount.Find(&total).Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
if err = txGet.
|
||||
Order("updated_at DESC").
|
||||
Offset(req.Page * req.Size).
|
||||
Limit(req.Size).
|
||||
Find(&list).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
return resp.Resp200(c, nf.Map{"list": list, "total": total})
|
||||
}
|
||||
|
||||
func TaskCreate(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
TaskName string `json:"task_name"`
|
||||
TaskType string `json:"task_type"`
|
||||
Timeout int `json:"timeout"`
|
||||
Cron string `json:"cron"`
|
||||
RunAt int64 `json:"run_at"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
now = time.Now()
|
||||
)
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.TaskName != "" {
|
||||
return resp.Resp400(c, req)
|
||||
}
|
||||
|
||||
task := &model.Task{TaskName: req.TaskName}
|
||||
|
||||
if req.Timeout < opt.TaskMinTimeout || req.Timeout > opt.TaskMaxTimeout {
|
||||
return resp.Resp400(c, req, fmt.Sprintf("timeout 时长过短(%d - %d)", opt.TaskMinTimeout, opt.TaskMaxTimeout))
|
||||
}
|
||||
|
||||
task.TimeoutSecond = req.Timeout
|
||||
|
||||
switch req.TaskType {
|
||||
case "once":
|
||||
case "timing":
|
||||
rt := time.UnixMilli(req.RunAt)
|
||||
if rt.Sub(now).Seconds() > opt.TaskFetchInterval {
|
||||
return resp.Resp400(c, req, "任务执行时间距离当前时间太短")
|
||||
}
|
||||
task.TaskRunType = fmt.Sprintf("T-%d", req.RunAt)
|
||||
case "cron":
|
||||
task.TaskRunType = fmt.Sprintf("C-%s", req.TaskType)
|
||||
default:
|
||||
return resp.Resp400(c, req, "任务执行类型: once/timing/cron")
|
||||
}
|
||||
|
||||
if err = database.DB.Session(util.Timeout(5)).
|
||||
Create(task).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
return resp.Resp200(c, task)
|
||||
}
|
||||
|
||||
func TaskInputCreate(c *nf.Ctx) error {
|
||||
panic("impl")
|
||||
}
|
||||
func TaskInputUpdate(c *nf.Ctx) error {
|
||||
panic("impl")
|
||||
}
|
485
internal/handler/user.go
Normal file
@ -0,0 +1,485 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"github.com/loveuer/nfflow/internal/controller"
|
||||
"github.com/loveuer/nfflow/internal/database"
|
||||
"github.com/loveuer/nfflow/internal/middleware/oplog"
|
||||
"github.com/loveuer/nfflow/internal/model"
|
||||
"github.com/loveuer/nfflow/internal/opt"
|
||||
"github.com/loveuer/nfflow/internal/sqlType"
|
||||
"github.com/loveuer/nfflow/internal/util"
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"time"
|
||||
)
|
||||
|
||||
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 = database.DB.Session(util.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 !util.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(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(token, target); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
if !opt.MultiLogin {
|
||||
var (
|
||||
last = fmt.Sprintf("%s:user:last_token:%d", opt.CachePrefix, target.Id)
|
||||
bs []byte
|
||||
)
|
||||
|
||||
if bs, err = database.Cache.Get(util.Timeout(3), last); err == nil {
|
||||
key := fmt.Sprintf("%s:user:token:%s", opt.CachePrefix, string(bs))
|
||||
_ = database.Cache.Del(util.Timeout(3), key)
|
||||
}
|
||||
|
||||
if err = database.Cache.Set(util.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(),
|
||||
}})
|
||||
|
||||
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(op.Id)
|
||||
|
||||
c.Locals(opt.OpLogLocalKey, &oplog.OpLog{
|
||||
Type: model.OpLogTypeLogout,
|
||||
Content: map[string]any{
|
||||
"time": time.Now().UnixMilli(),
|
||||
"ip": c.IP(),
|
||||
},
|
||||
})
|
||||
|
||||
return resp.Resp200(c, nil)
|
||||
}
|
||||
|
||||
func ManageUserList(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Page int `query:"page"`
|
||||
Size int `query:"size"`
|
||||
Keyword string `query:"keyword"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
ok bool
|
||||
op *model.User
|
||||
req = new(Req)
|
||||
list = make([]*model.User, 0)
|
||||
total = 0
|
||||
)
|
||||
|
||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if err = c.QueryParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.Size == 0 {
|
||||
req.Size = opt.DefaultSize
|
||||
}
|
||||
|
||||
if req.Size > opt.MaxSize {
|
||||
return resp.Resp400(c, nf.Map{"msg": "size over max", "max": opt.MaxSize})
|
||||
}
|
||||
|
||||
if err = op.Role.Where(database.DB.Session(util.Timeout(10)).
|
||||
Model(&model.User{}).
|
||||
Where("deleted_at = 0")).
|
||||
Order("updated_at DESC").
|
||||
Offset(req.Page * req.Size).
|
||||
Limit(req.Size).
|
||||
Find(&list).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
if err = op.Role.Where(database.DB.Session(util.Timeout(5)).
|
||||
Model(&model.User{}).
|
||||
Select("COUNT(id)").
|
||||
Where("deleted_at = 0")).
|
||||
Find(&total).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
return resp.Resp200(c, nf.Map{"list": list, "total": total})
|
||||
}
|
||||
|
||||
func ManageUserCreate(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Password string `json:"password"`
|
||||
Status model.Status `json:"status"`
|
||||
Role model.Role `json:"role"`
|
||||
Privileges sqlType.NumSlice[model.Privilege] `json:"privileges"`
|
||||
Comment string `json:"comment"`
|
||||
ActiveAt int64 `json:"active_at"`
|
||||
Deadline int64 `json:"deadline"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
ok bool
|
||||
op *model.User
|
||||
req = new(Req)
|
||||
now = time.Now()
|
||||
)
|
||||
|
||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.Username == "" || req.Password == "" {
|
||||
return resp.Resp400(c, req)
|
||||
}
|
||||
|
||||
if err = util.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: util.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 = database.DB.Session(util.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 req.Id == op.Id {
|
||||
return resp.Resp400(c, "无法修改自己")
|
||||
}
|
||||
|
||||
if target, err = controller.UserController.GetUser(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 _, 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 = util.CheckPassword(req.Password); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
updates["password"] = util.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 = database.DB.Session(util.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(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(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(target.Id); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
c.Locals(opt.OpLogLocalKey, &oplog.OpLog{Type: model.OpLogTypeDeleteUser, Content: map[string]any{
|
||||
"target_id": target.Id,
|
||||
"target_username": target.Username,
|
||||
}})
|
||||
|
||||
return resp.Resp200(c, nil, "删除成功")
|
||||
}
|
55
internal/middleware/auth/auth.go
Normal file
@ -0,0 +1,55 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"gitea.com/taozitaozi/gredis"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"github.com/loveuer/nfflow/internal/controller"
|
||||
"github.com/loveuer/nfflow/internal/opt"
|
||||
"github.com/sirupsen/logrus"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
logrus.Tracef("middleware.NewAuth: token=%s", token)
|
||||
|
||||
target, err := controller.UserController.GetUserByToken(token)
|
||||
if err != nil {
|
||||
logrus.Errorf("middleware.NewAuth: get user by token=%s err=%v", token, err)
|
||||
if errors.Is(err, redis.Nil) || errors.Is(err, gredis.ErrKeyNotFound) {
|
||||
return resp.Resp401(c, err)
|
||||
}
|
||||
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
if err = target.IsValid(true); err != nil {
|
||||
return resp.Resp401(c, err.Error(), err.Error())
|
||||
}
|
||||
|
||||
c.Locals("user", target)
|
||||
c.Locals("token", token)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
370
internal/middleware/front/dist/front/3rdpartylicenses.txt
vendored
Normal file
@ -0,0 +1,370 @@
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/core
|
||||
License: "MIT"
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: rxjs
|
||||
License: "Apache-2.0"
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: tslib
|
||||
License: "0BSD"
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/common
|
||||
License: "MIT"
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/platform-browser
|
||||
License: "MIT"
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/router
|
||||
License: "MIT"
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/cdk
|
||||
License: "MIT"
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2024 Google LLC.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/material
|
||||
License: "MIT"
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2024 Google LLC.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/animations
|
||||
License: "MIT"
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/forms
|
||||
License: "MIT"
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: js-base64
|
||||
License: "BSD-3-Clause"
|
||||
|
||||
Copyright (c) 2014, Dan Kogai
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of {{{project}}} nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/service-worker
|
||||
License: "MIT"
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: zone.js
|
||||
License: "MIT"
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
BIN
internal/middleware/front/dist/front/browser/assets/icons/icon-128x128.png
vendored
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
internal/middleware/front/dist/front/browser/assets/icons/icon-144x144.png
vendored
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
internal/middleware/front/dist/front/browser/assets/icons/icon-152x152.png
vendored
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
internal/middleware/front/dist/front/browser/assets/icons/icon-192x192.png
vendored
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
internal/middleware/front/dist/front/browser/assets/icons/icon-384x384.png
vendored
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
internal/middleware/front/dist/front/browser/assets/icons/icon-512x512.png
vendored
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
internal/middleware/front/dist/front/browser/assets/icons/icon-72x72.png
vendored
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
internal/middleware/front/dist/front/browser/assets/icons/icon-96x96.png
vendored
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
internal/middleware/front/dist/front/browser/favicon.ico
vendored
Normal file
After Width: | Height: | Size: 15 KiB |
19
internal/middleware/front/dist/front/browser/index.html
vendored
Normal file
15
internal/middleware/front/dist/front/browser/main-K7KCRENT.js
vendored
Normal file
59
internal/middleware/front/dist/front/browser/manifest.webmanifest
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "front",
|
||||
"short_name": "front",
|
||||
"theme_color": "#1976d2",
|
||||
"background_color": "#fafafa",
|
||||
"display": "standalone",
|
||||
"scope": "./",
|
||||
"start_url": "./",
|
||||
"icons": [
|
||||
{
|
||||
"src": "assets/icons/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
}
|
||||
]
|
||||
}
|
1860
internal/middleware/front/dist/front/browser/ngsw-worker.js
vendored
Executable file
79
internal/middleware/front/dist/front/browser/ngsw.json
vendored
Normal file
@ -0,0 +1,79 @@
|
||||
{
|
||||
"configVersion": 1,
|
||||
"timestamp": 1710749227562,
|
||||
"index": "/index.html",
|
||||
"assetGroups": [
|
||||
{
|
||||
"name": "app",
|
||||
"installMode": "prefetch",
|
||||
"updateMode": "prefetch",
|
||||
"cacheQueryOptions": {
|
||||
"ignoreVary": true
|
||||
},
|
||||
"urls": [
|
||||
"/favicon.ico",
|
||||
"/index.html",
|
||||
"/main-K7KCRENT.js",
|
||||
"/manifest.webmanifest",
|
||||
"/polyfills-TMVK3KFA.js",
|
||||
"/styles-3T3ZIQFC.css"
|
||||
],
|
||||
"patterns": []
|
||||
},
|
||||
{
|
||||
"name": "assets",
|
||||
"installMode": "lazy",
|
||||
"updateMode": "prefetch",
|
||||
"cacheQueryOptions": {
|
||||
"ignoreVary": true
|
||||
},
|
||||
"urls": [
|
||||
"/assets/icons/icon-128x128.png",
|
||||
"/assets/icons/icon-144x144.png",
|
||||
"/assets/icons/icon-152x152.png",
|
||||
"/assets/icons/icon-192x192.png",
|
||||
"/assets/icons/icon-384x384.png",
|
||||
"/assets/icons/icon-512x512.png",
|
||||
"/assets/icons/icon-72x72.png",
|
||||
"/assets/icons/icon-96x96.png"
|
||||
],
|
||||
"patterns": []
|
||||
}
|
||||
],
|
||||
"dataGroups": [],
|
||||
"hashTable": {
|
||||
"/assets/icons/icon-128x128.png": "f912963bdc6d5a38d8f1dd0afbaab2ce8d657acb",
|
||||
"/assets/icons/icon-144x144.png": "b155fd5f2fd5d2ea7760721063003c4cd95fd783",
|
||||
"/assets/icons/icon-152x152.png": "eae6e0f0c8afb8aaae339e2a569b43e9238e5e6c",
|
||||
"/assets/icons/icon-192x192.png": "15c4180454633880d98674aa7394656196c91584",
|
||||
"/assets/icons/icon-384x384.png": "94a915751ef6c9282df9aea405ad679230631814",
|
||||
"/assets/icons/icon-512x512.png": "1de182f76f7329dfa8f9fcdd1fdcdd695bea6a99",
|
||||
"/assets/icons/icon-72x72.png": "aa88a6096bd973be7f6d7a5489bfa6bc2463f8c4",
|
||||
"/assets/icons/icon-96x96.png": "7fb8f59c30ce2ff12c700321a0b39e14b3dc8f95",
|
||||
"/favicon.ico": "9c39f434fe1261f68c5e3eefdf734630d99c5670",
|
||||
"/index.html": "3b8d9a492329d3a6f13280951666e6c073c32071",
|
||||
"/main-K7KCRENT.js": "b2c1bced1f984d3a635681ba3b28da91ac1b4dc4",
|
||||
"/manifest.webmanifest": "4a17033d41b19f97cc660c6a603fdc95226d1704",
|
||||
"/polyfills-TMVK3KFA.js": "fda1c82c5c620f1fa442f687b3df45a037f6fcc9",
|
||||
"/styles-3T3ZIQFC.css": "e1406fd244b1e7e7ab47d07124fffd55ccc16178"
|
||||
},
|
||||
"navigationUrls": [
|
||||
{
|
||||
"positive": true,
|
||||
"regex": "^\\/.*$"
|
||||
},
|
||||
{
|
||||
"positive": false,
|
||||
"regex": "^\\/(?:.+\\/)?[^/]*\\.[^/]*$"
|
||||
},
|
||||
{
|
||||
"positive": false,
|
||||
"regex": "^\\/(?:.+\\/)?[^/]*__[^/]*$"
|
||||
},
|
||||
{
|
||||
"positive": false,
|
||||
"regex": "^\\/(?:.+\\/)?[^/]*__[^/]*\\/.*$"
|
||||
}
|
||||
],
|
||||
"navigationRequestStrategy": "performance"
|
||||
}
|
2
internal/middleware/front/dist/front/browser/polyfills-TMVK3KFA.js
vendored
Normal file
26
internal/middleware/front/dist/front/browser/safety-worker.js
vendored
Executable file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
// tslint:disable:no-console
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(self.clients.claim());
|
||||
|
||||
event.waitUntil(self.registration.unregister().then(() => {
|
||||
console.log('NGSW Safety Worker - unregistered old service worker');
|
||||
}));
|
||||
|
||||
event.waitUntil(caches.keys().then(cacheNames => {
|
||||
const ngswCacheNames = cacheNames.filter(name => /^ngsw:/.test(name));
|
||||
return Promise.all(ngswCacheNames.map(name => caches.delete(name)));
|
||||
}));
|
||||
});
|
1
internal/middleware/front/dist/front/browser/styles-3T3ZIQFC.css
vendored
Normal file
26
internal/middleware/front/dist/front/browser/worker-basic.min.js
vendored
Executable file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
// tslint:disable:no-console
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(self.clients.claim());
|
||||
|
||||
event.waitUntil(self.registration.unregister().then(() => {
|
||||
console.log('NGSW Safety Worker - unregistered old service worker');
|
||||
}));
|
||||
|
||||
event.waitUntil(caches.keys().then(cacheNames => {
|
||||
const ngswCacheNames = cacheNames.filter(name => /^ngsw:/.test(name));
|
||||
return Promise.all(ngswCacheNames.map(name => caches.delete(name)));
|
||||
}));
|
||||
});
|
61
internal/middleware/front/front.go
Normal file
@ -0,0 +1,61 @@
|
||||
package front
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed dist/front/browser
|
||||
var DefaultFront embed.FS
|
||||
|
||||
func NewFront(ff *embed.FS, basePath string) nf.HandlerFunc {
|
||||
var (
|
||||
e error
|
||||
indexBytes []byte
|
||||
index string
|
||||
)
|
||||
|
||||
index = fmt.Sprintf("%s/index.html", basePath)
|
||||
|
||||
if indexBytes, e = ff.ReadFile(index); e != nil {
|
||||
logrus.Panicf("read index file err: %v", e)
|
||||
}
|
||||
|
||||
return func(c *nf.Ctx) error {
|
||||
var (
|
||||
err error
|
||||
bs []byte
|
||||
path = c.Path()
|
||||
)
|
||||
|
||||
if bs, err = ff.ReadFile(basePath + path); err != nil {
|
||||
logrus.Debugf("embed read file [%s]%s err: %v", basePath, path, err)
|
||||
c.Set("Content-Type", "text/html")
|
||||
_, err = c.Write(indexBytes)
|
||||
return err
|
||||
}
|
||||
|
||||
var dbs []byte
|
||||
if len(bs) > 512 {
|
||||
dbs = bs[:512]
|
||||
} else {
|
||||
dbs = bs
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasSuffix(path, ".js"):
|
||||
c.Set("Content-Type", "application/javascript")
|
||||
case strings.HasSuffix(path, ".css"):
|
||||
c.Set("Content-Type", "text/css")
|
||||
default:
|
||||
c.Set("Content-Type", http.DetectContentType(dbs))
|
||||
}
|
||||
|
||||
_, err = c.Write(bs)
|
||||
return err
|
||||
}
|
||||
}
|
117
internal/middleware/oplog/new.go
Normal file
@ -0,0 +1,117 @@
|
||||
package oplog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nfflow/internal/database"
|
||||
"github.com/loveuer/nfflow/internal/model"
|
||||
"github.com/loveuer/nfflow/internal/opt"
|
||||
"github.com/loveuer/nfflow/internal/sqlType"
|
||||
"github.com/loveuer/nfflow/internal/util"
|
||||
"github.com/sirupsen/logrus"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
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 = database.DB.Session(util.Timeout(10)).
|
||||
Model(&model.OpLog{}).
|
||||
Create(&list).
|
||||
Error; err != nil {
|
||||
logrus.Errorf("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)
|
||||
log, ok := opv.(*OpLog)
|
||||
if !ok {
|
||||
logrus.Warnf("middleware.NewOpLog: %s - %s local '%s' to [*OpLog] invalid", c.Method(), c.Path(), opt.OpLogLocalKey)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Content["time"] = now.UnixMilli()
|
||||
log.Content["user_id"] = op.Id
|
||||
log.Content["username"] = op.Username
|
||||
log.Content["created_at"] = now.UnixMilli()
|
||||
|
||||
select {
|
||||
case lc <- &model.OpLog{
|
||||
CreatedAt: now.UnixMilli(),
|
||||
UpdatedAt: now.UnixMilli(),
|
||||
UserId: op.Id,
|
||||
Username: op.Username,
|
||||
Type: log.Type,
|
||||
Content: sqlType.NewJSONB(log.Content),
|
||||
}:
|
||||
case <-util.Timeout(3).Done():
|
||||
logrus.Warnf("middleware.NewOpLog: %s - %s log -> chan timeout[3s]", c.Method, c.Path())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
8
internal/middleware/oplog/oplog.go
Normal file
@ -0,0 +1,8 @@
|
||||
package oplog
|
||||
|
||||
import "github.com/loveuer/nfflow/internal/model"
|
||||
|
||||
type OpLog struct {
|
||||
Type model.OpLogType
|
||||
Content map[string]any
|
||||
}
|
86
internal/middleware/privilege/privilege.go
Normal file
@ -0,0 +1,86 @@
|
||||
package privilege
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"github.com/loveuer/nfflow/internal/model"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sirupsen/logrus"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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:
|
||||
logrus.Panicf("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()
|
||||
}
|
||||
}
|
74
internal/model/init.go
Normal file
@ -0,0 +1,74 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/loveuer/nfflow/internal/database"
|
||||
"github.com/loveuer/nfflow/internal/opt"
|
||||
"github.com/loveuer/nfflow/internal/util"
|
||||
"github.com/sirupsen/logrus"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func MustInit(db database.Store) {
|
||||
var err error
|
||||
|
||||
if err = initModel(db); err != nil {
|
||||
logrus.Fatalf("model.MustInit: init models err=%v", err)
|
||||
}
|
||||
|
||||
logrus.Info("MustInitModels: auto_migrate privilege model success")
|
||||
|
||||
if err = initData(db); err != nil {
|
||||
logrus.Fatalf("model.MustInit: init datas err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func initModel(client database.Store) error {
|
||||
if err := client.Session(util.Timeout(10)).AutoMigrate(
|
||||
&User{},
|
||||
&OpLog{},
|
||||
|
||||
&Task{},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Info("MustInitModels: auto_migrate user model success")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initData(client database.Store) error {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
{
|
||||
count := 0
|
||||
|
||||
if err = client.Session(util.Timeout()).Model(&User{}).Select("count(id)").Take(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count < len(initUsers) {
|
||||
logrus.Warn("mustInitDatas: user count = 0, start init...")
|
||||
for _, user := range initUsers {
|
||||
if err = client.Session(util.Timeout(5)).Model(&User{}).Create(user).Error; err != nil {
|
||||
if !strings.Contains(err.Error(), "SQLSTATE 23505") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if opt.Cfg.Database.Type == "postgresql" {
|
||||
if err = client.Session(util.Timeout(5)).Exec(`SELECT setval('users_id_seq', (SELECT MAX(id) FROM users))`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Info("mustInitDatas: creat init users success")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
17
internal/model/interface.go
Normal file
@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
type Enum interface {
|
||||
Value() int64
|
||||
Code() string
|
||||
Label() string
|
||||
|
||||
MarshalJSON() ([]byte, error)
|
||||
|
||||
All() []Enum
|
||||
}
|
||||
|
||||
type OpLogger interface {
|
||||
Enum
|
||||
Render(content map[string]any) (string, error)
|
||||
Template() string
|
||||
}
|
311
internal/model/oplog.go
Normal file
@ -0,0 +1,311 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"github.com/loveuer/nfflow/internal/sqlType"
|
||||
"github.com/spf13/cast"
|
||||
"github.com/tdewolff/minify/v2"
|
||||
"github.com/tdewolff/minify/v2/html"
|
||||
"html/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
FuncMap = template.FuncMap{
|
||||
"time_format": func(mil any, format string) string {
|
||||
return time.UnixMilli(cast.ToInt64(mil)).Format(format)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
_ OpLogger = OpLogType(0)
|
||||
)
|
||||
|
||||
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 {
|
||||
//if len(v) == 0 {
|
||||
// return ""
|
||||
//}
|
||||
//
|
||||
//bs := make([]byte, 0, len(v))
|
||||
//
|
||||
//for _, b := range v {
|
||||
// if b == '\t' || b == '\n' {
|
||||
// continue
|
||||
// }
|
||||
//
|
||||
// bs = append(bs, b)
|
||||
//}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(v)
|
||||
//return string(bs)
|
||||
}
|
||||
|
||||
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 string(bs), nil
|
||||
|
||||
return _trimHTML(bs), nil
|
||||
}
|
||||
|
||||
const (
|
||||
oplogTemplateLogin = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user="{{ .user_id }}"
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time="{{ .time }}"
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
在
|
||||
<span
|
||||
class="nf-op-log-ip nf-op-log-keyword"
|
||||
>{{ .ip }}
|
||||
</span>
|
||||
上
|
||||
<span
|
||||
class="nf-op-log-op nf-op-log-keyword"
|
||||
>
|
||||
登入
|
||||
</span>
|
||||
了系统
|
||||
</div>
|
||||
`
|
||||
oplogTemplateLogout = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user="{{ .user_id }}"
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time="{{ .time }}"
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
在
|
||||
<span
|
||||
class="nf-op-log-ip nf-op-log-keyword"
|
||||
>{{ .ip }}
|
||||
</span>
|
||||
上
|
||||
<span
|
||||
class="nf-op-log-op nf-op-log-keyword"
|
||||
>
|
||||
登出
|
||||
</span>
|
||||
了系统
|
||||
</div>
|
||||
`
|
||||
oplogTemplateCreateUser = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user="{{ .user_id }}"
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time="{{ .time }}"
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
<span class="nf-op-log-keyword">
|
||||
创建
|
||||
</span>
|
||||
了用户
|
||||
<span
|
||||
class="nf-op-log-target nf-op-log-keyword"
|
||||
nf-op-log-target="{{ .target_id }}"
|
||||
>{{ .target_username }}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
oplogTemplateUpdateUser = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user='{{ .user_id }}'
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time='{{ .time }}'
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
<span class="nf-op-log-keyword">
|
||||
编辑
|
||||
</span>
|
||||
了用户
|
||||
<span
|
||||
class="nf-op-log-target nf-op-log-keyword"
|
||||
nf-op-log-target="{{ .target_id }}"
|
||||
>{{ .target_username }}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
oplogTemplateDeleteUser = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user="{{ .user_id }}"
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time="{{ .time }}"
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
<span class="nf-op-log-keyword">
|
||||
删除
|
||||
</span>
|
||||
了用户
|
||||
<span
|
||||
class="nf-op-log-target nf-op-log-keyword"
|
||||
nf-op-log-target="{{ .target_id }}"
|
||||
>{{ .target_username }}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
|
||||
func (o OpLogType) Template() string {
|
||||
switch o {
|
||||
case OpLogTypeLogin:
|
||||
return oplogTemplateLogin
|
||||
case OpLogTypeLogout:
|
||||
return oplogTemplateLogout
|
||||
case OpLogTypeCreateUser:
|
||||
return oplogTemplateCreateUser
|
||||
case OpLogTypeUpdateUser:
|
||||
return oplogTemplateUpdateUser
|
||||
case OpLogTypeDeleteUser:
|
||||
return oplogTemplateDeleteUser
|
||||
default:
|
||||
return `<div>错误的日志类型</div>`
|
||||
}
|
||||
}
|
||||
|
||||
type OpLog struct {
|
||||
Id uint64 `json:"id" gorm:"primaryKey;column:id"`
|
||||
CreatedAt int64 `json:"created_at" gorm:"column:created_at;autoCreateTime:milli"`
|
||||
UpdatedAt int64 `json:"updated_at" gorm:"column:updated_at;autoUpdateTime:milli"`
|
||||
DeletedAt int64 `json:"deleted_at" gorm:"index;column:deleted_at;default:0"`
|
||||
|
||||
UserId uint64 `json:"user_id" gorm:"column:user_id"`
|
||||
Username string `json:"username" gorm:"column:username;varchar(128)"`
|
||||
Type OpLogType `json:"type" gorm:"column:type;type:varchar(128)"`
|
||||
Content sqlType.JSONB `json:"content" gorm:"column:content;type:jsonb"`
|
||||
HTML string `json:"html" gorm:"-"`
|
||||
}
|
61
internal/model/privilege.go
Normal file
@ -0,0 +1,61 @@
|
||||
package model
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Privilege uint64
|
||||
|
||||
type _privilege struct {
|
||||
Value int64 `json:"value"`
|
||||
Code string `json:"code"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
const (
|
||||
PrivilegeUserManage Privilege = iota + 1
|
||||
PrivilegeOpLog
|
||||
)
|
||||
|
||||
func (p Privilege) Value() int64 {
|
||||
return int64(p)
|
||||
}
|
||||
|
||||
func (p Privilege) Code() string {
|
||||
switch p {
|
||||
case PrivilegeUserManage:
|
||||
return "user_manage"
|
||||
case PrivilegeOpLog:
|
||||
return "oplog"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (p Privilege) Label() string {
|
||||
switch p {
|
||||
case PrivilegeUserManage:
|
||||
return "用户管理"
|
||||
case PrivilegeOpLog:
|
||||
return "操作日志"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
func (p Privilege) MarshalJSON() ([]byte, error) {
|
||||
_p := &_privilege{
|
||||
Value: int64(p),
|
||||
Code: p.Code(),
|
||||
Label: p.Label(),
|
||||
}
|
||||
|
||||
return json.Marshal(_p)
|
||||
}
|
||||
|
||||
func (p Privilege) All() []Enum {
|
||||
return []Enum{
|
||||
PrivilegeUserManage,
|
||||
PrivilegeOpLog,
|
||||
}
|
||||
}
|
||||
|
||||
var _ Enum = (*Privilege)(nil)
|
85
internal/model/role.go
Normal file
@ -0,0 +1,85 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/loveuer/nfflow/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
|
||||
}
|
128
internal/model/task.go
Normal file
@ -0,0 +1,128 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/loveuer/nfflow/internal/sqlType"
|
||||
)
|
||||
|
||||
type InputType int64
|
||||
|
||||
const (
|
||||
InputTypeES InputType = iota + 1
|
||||
InputTypePG
|
||||
InputTypeMQ
|
||||
)
|
||||
|
||||
type OutputType int64
|
||||
|
||||
const (
|
||||
OutputTypeES OutputType = iota + 1
|
||||
OutputTypeFile
|
||||
OutputTypeMQ
|
||||
)
|
||||
|
||||
type TaskStatus int64
|
||||
|
||||
func (t TaskStatus) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"value": t.Value(),
|
||||
"code": t.Code(),
|
||||
"label": t.Label(),
|
||||
})
|
||||
}
|
||||
|
||||
func (t TaskStatus) All() []Enum {
|
||||
return []Enum{
|
||||
TaskStatusNotReady,
|
||||
TaskStatusReady,
|
||||
TaskStatusRunning,
|
||||
TaskStatusSucceed,
|
||||
TaskStatusFailed,
|
||||
}
|
||||
}
|
||||
|
||||
var _ Enum = TaskStatus(0)
|
||||
|
||||
const (
|
||||
TaskStatusNotReady TaskStatus = iota
|
||||
TaskStatusReady
|
||||
TaskStatusRunning
|
||||
TaskStatusSucceed
|
||||
TaskStatusFailed
|
||||
)
|
||||
|
||||
func (t TaskStatus) Value() int64 {
|
||||
return int64(t)
|
||||
}
|
||||
|
||||
func (t TaskStatus) Code() string {
|
||||
switch t {
|
||||
case TaskStatusNotReady:
|
||||
return "not_ready"
|
||||
case TaskStatusReady:
|
||||
return "ready"
|
||||
case TaskStatusRunning:
|
||||
return "running"
|
||||
case TaskStatusSucceed:
|
||||
return "succeed"
|
||||
case TaskStatusFailed:
|
||||
return "failed"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (t TaskStatus) Label() string {
|
||||
switch t {
|
||||
case TaskStatusNotReady:
|
||||
return "未完善"
|
||||
case TaskStatusReady:
|
||||
return "等待执行"
|
||||
case TaskStatusRunning:
|
||||
return "执行中"
|
||||
case TaskStatusSucceed:
|
||||
return "执行成功"
|
||||
case TaskStatusFailed:
|
||||
return "执行失败"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
type Task 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"`
|
||||
|
||||
TaskName string `json:"task_name" gorm:"column:task_name;type:varchar(256)"`
|
||||
TaskRunType string `json:"task_run_type" gorm:"column:task_run_type;type:varchar(16);default:once"` // cron: C-"cron syntax", "once", timestamp: T-1234567890123 毫秒时间戳
|
||||
TimeoutSecond int `json:"timeout_second" gorm:"column:timeout_second"`
|
||||
TaskStatus TaskStatus `json:"task_status" gorm:"column:task_status"`
|
||||
TaskLog string `json:"task_log" gorm:"task_log"`
|
||||
}
|
||||
|
||||
type Input 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"`
|
||||
InputType InputType `json:"input_type"`
|
||||
InputConfig sqlType.JSONB `json:"input_config"`
|
||||
}
|
||||
|
||||
type Output 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"`
|
||||
OutputType OutputType
|
||||
OutputConfig sqlType.JSONB
|
||||
}
|
||||
|
||||
type Pipe 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"`
|
||||
}
|
257
internal/model/user.go
Normal file
@ -0,0 +1,257 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/loveuer/nfflow/internal/opt"
|
||||
"github.com/loveuer/nfflow/internal/sqlType"
|
||||
"github.com/loveuer/nfflow/internal/util"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cast"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
initUsers = []*User{
|
||||
{
|
||||
Id: 1,
|
||||
Username: "root",
|
||||
Password: util.NewPassword("404@Ro0t"),
|
||||
Nickname: "admin",
|
||||
Role: RoleRoot,
|
||||
//Privileges: sqlType.NumSlice[Privilege]{
|
||||
// PrivilegeUserManage,
|
||||
//},
|
||||
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(),
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Username: "admin",
|
||||
Password: util.NewPassword("Foobar123"),
|
||||
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(),
|
||||
},
|
||||
{
|
||||
Id: 3,
|
||||
Username: "user",
|
||||
Password: util.NewPassword("Foobar123"),
|
||||
Nickname: "user",
|
||||
Role: RoleUser,
|
||||
Privileges: sqlType.NumSlice[Privilege]{
|
||||
PrivilegeOpLog,
|
||||
},
|
||||
CreatedById: 2,
|
||||
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)
|
||||
logrus.Error(err)
|
||||
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 {
|
||||
logrus.Errorf("jwt parse err: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !pt.Valid {
|
||||
logrus.Warn("parsed jwt invalid")
|
||||
return nil
|
||||
}
|
||||
|
||||
if claims, ok = pt.Claims.(jwt.MapClaims); !ok {
|
||||
logrus.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,
|
||||
})
|
||||
}
|
84
internal/opt/opt.go
Normal file
@ -0,0 +1,84 @@
|
||||
package opt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type database struct {
|
||||
Type string `json:"type"` // postgresql, mysql, sqlite
|
||||
Path string `json:"path"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
DB string `json:"db"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type cache struct {
|
||||
Type string `json:"type"` // redis, memor
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type config struct {
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
Database database `json:"database"`
|
||||
Cache cache `json:"cache"`
|
||||
}
|
||||
|
||||
var (
|
||||
ConfigFile string
|
||||
Debug int
|
||||
Cfg = &config{}
|
||||
)
|
||||
|
||||
func MustInitConfig() {
|
||||
flag.Parse()
|
||||
|
||||
var (
|
||||
err error
|
||||
cf *os.File
|
||||
bs []byte
|
||||
)
|
||||
|
||||
if cf, err = os.Open(ConfigFile); err != nil {
|
||||
logrus.Panicf("MustInitConfig: open config file=%s err=%v", ConfigFile, err)
|
||||
}
|
||||
defer cf.Close()
|
||||
|
||||
if bs, err = io.ReadAll(cf); err != nil {
|
||||
logrus.Panicf("MustInitConfig: read in config file err=%v", err)
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(bs, Cfg); err != nil {
|
||||
logrus.Panicf("MustInitConfig: json marshal config=%s err=%v", string(bs), err)
|
||||
}
|
||||
|
||||
if Debug > 0 {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
|
||||
if Debug >= 9999 {
|
||||
logrus.SetLevel(logrus.TraceLevel)
|
||||
logrus.SetReportCaller(true)
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
ForceColors: true,
|
||||
CallerPrettyfier: func(frame *runtime.Frame) (string, string) {
|
||||
file := path.Base(frame.File)
|
||||
return "", " " + file + ":" + strconv.Itoa(frame.Line)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Infof("read in config detail: \n%s", string(bs))
|
||||
}
|
46
internal/opt/var.go
Normal file
@ -0,0 +1,46 @@
|
||||
package opt
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
Version = "v0.0.1"
|
||||
|
||||
// todo: 可以替换自己生生成的 secret
|
||||
JwtTokenSecret = "7^D+UW3BPB2Mnz)bY3uVrAUyv&dj8Kdz"
|
||||
|
||||
// todo: 是否打开 gorm 的 debug 打印 (开发和 dev 环境时可以打开)
|
||||
DBDebug = true
|
||||
|
||||
// todo: 同一个账号是否可以多 client 登录
|
||||
MultiLogin = false
|
||||
|
||||
// todo: 用户量不大的情况, 并没有缓存用户具体信息, 如果需要可以打开
|
||||
EnableUserCache = true
|
||||
|
||||
// todo: 缓存时, key 的前缀
|
||||
CachePrefix = "ultone"
|
||||
|
||||
// todo: 登录颁发的 cookie 的 name
|
||||
CookieName = "utlone-token"
|
||||
|
||||
// todo: 用户列表,日志列表 size 参数
|
||||
DefaultSize, MaxSize = 20, 200
|
||||
|
||||
// todo: 操作用户时, role 相等时能否操作: 包括 列表, 能否新建,修改,删除同样 role 的用户
|
||||
RoleMustLess = false
|
||||
|
||||
// todo: 通过 c.Local() 存入 oplog 时的 key 值
|
||||
OpLogLocalKey = "oplog"
|
||||
|
||||
// todo: 操作日志 最多延迟多少秒写入(最多缓存多少秒的日志,然后 bulk 写入)
|
||||
OpLogWriteDurationSecond = 5
|
||||
|
||||
TaskMinTimeout = 10
|
||||
TaskMaxTimeout = 24 * 3600
|
||||
TaskFetchInterval = 5 * 60
|
||||
)
|
||||
|
||||
var (
|
||||
// todo: 颁发的 token, (cookie) 在缓存中存在的时间 (每次请求该时间也会被刷新)
|
||||
TokenTimeout = time.Duration(3600*12) * time.Second
|
||||
)
|
9
internal/sqlType/err.go
Normal file
@ -0,0 +1,9 @@
|
||||
package sqlType
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrConvertScanVal = errors.New("convert scan val to str err")
|
||||
ErrInvalidScanVal = errors.New("scan val invalid")
|
||||
ErrConvertVal = errors.New("convert err")
|
||||
)
|
76
internal/sqlType/jsonb.go
Normal file
@ -0,0 +1,76 @@
|
||||
package sqlType
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/jackc/pgtype"
|
||||
)
|
||||
|
||||
type JSONB struct {
|
||||
Val pgtype.JSONB
|
||||
Valid bool
|
||||
}
|
||||
|
||||
func NewJSONB(v interface{}) JSONB {
|
||||
j := new(JSONB)
|
||||
j.Val = pgtype.JSONB{}
|
||||
if err := j.Val.Set(v); err == nil {
|
||||
j.Valid = true
|
||||
return *j
|
||||
}
|
||||
|
||||
return *j
|
||||
}
|
||||
|
||||
func (j *JSONB) Set(value interface{}) error {
|
||||
if err := j.Val.Set(value); err != nil {
|
||||
j.Valid = false
|
||||
return err
|
||||
}
|
||||
|
||||
j.Valid = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *JSONB) Bind(model interface{}) error {
|
||||
return j.Val.AssignTo(model)
|
||||
}
|
||||
|
||||
func (j *JSONB) Scan(value interface{}) error {
|
||||
j.Val = pgtype.JSONB{}
|
||||
if value == nil {
|
||||
j.Valid = false
|
||||
return nil
|
||||
}
|
||||
|
||||
j.Valid = true
|
||||
|
||||
return j.Val.Scan(value)
|
||||
}
|
||||
|
||||
func (j JSONB) Value() (driver.Value, error) {
|
||||
if j.Valid {
|
||||
return j.Val.Value()
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (j JSONB) MarshalJSON() ([]byte, error) {
|
||||
if j.Valid {
|
||||
return j.Val.MarshalJSON()
|
||||
}
|
||||
|
||||
return json.Marshal(nil)
|
||||
}
|
||||
|
||||
func (j *JSONB) UnmarshalJSON(b []byte) error {
|
||||
if string(b) == "null" {
|
||||
j.Valid = false
|
||||
return j.Val.UnmarshalJSON(b)
|
||||
}
|
||||
|
||||
return j.Val.UnmarshalJSON(b)
|
||||
}
|
42
internal/sqlType/nullStr.go
Normal file
@ -0,0 +1,42 @@
|
||||
package sqlType
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// type NullString struct {
|
||||
// sql.NullString
|
||||
// }
|
||||
|
||||
type NullString struct{ sql.NullString }
|
||||
|
||||
func NewNullString(val string) NullString {
|
||||
if val == "" {
|
||||
return NullString{}
|
||||
}
|
||||
|
||||
return NullString{sql.NullString{Valid: true, String: val}}
|
||||
}
|
||||
|
||||
func (ns NullString) MarshalJSON() ([]byte, error) {
|
||||
if !ns.Valid {
|
||||
return json.Marshal(nil)
|
||||
}
|
||||
|
||||
return json.Marshal(ns.String)
|
||||
}
|
||||
|
||||
func (ns *NullString) UnmarshalJSON(data []byte) error {
|
||||
if string(data) == "null" {
|
||||
ns.Valid = false
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &ns.String); err != nil {
|
||||
ns.Valid = true
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
53
internal/sqlType/set.go
Normal file
@ -0,0 +1,53 @@
|
||||
package sqlType
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Set map[string]struct{}
|
||||
|
||||
func (s Set) MarshalJSON() ([]byte, error) {
|
||||
array := make([]string, 0)
|
||||
for name := range s {
|
||||
array = append(array, name)
|
||||
}
|
||||
|
||||
return json.Marshal(array)
|
||||
}
|
||||
|
||||
func (s *Set) UnmarshalJSON(b []byte) error {
|
||||
array := make([]string, 0)
|
||||
if err := json.Unmarshal(b, &array); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
set := make(map[string]struct{})
|
||||
|
||||
for _, name := range array {
|
||||
set[name] = struct{}{}
|
||||
}
|
||||
|
||||
*s = set
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s Set) ToStringSlice() []string {
|
||||
var (
|
||||
result = make([]string, 0, len(s))
|
||||
)
|
||||
|
||||
for key := range s {
|
||||
result = append(result, key)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Set) FromStringSlice(ss *[]string) {
|
||||
if s == nil {
|
||||
m := make(Set)
|
||||
s = &m
|
||||
}
|
||||
|
||||
for idx := range *(ss) {
|
||||
(*s)[(*ss)[idx]] = struct{}{}
|
||||
}
|
||||
}
|
109
internal/sqlType/strSlice.go
Normal file
@ -0,0 +1,109 @@
|
||||
package sqlType
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type StrSlice []string
|
||||
|
||||
func (s *StrSlice) Scan(val interface{}) error {
|
||||
|
||||
str, ok := val.(string)
|
||||
if !ok {
|
||||
return ErrConvertScanVal
|
||||
}
|
||||
|
||||
if len(str) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
bs := make([]byte, 0, 128)
|
||||
bss := make([]byte, 0, 2*len(str))
|
||||
|
||||
quoteCount := 0
|
||||
|
||||
for idx := 1; idx < len(str)-1; idx++ {
|
||||
// 44: , 92: \ 34: "
|
||||
quote := str[idx]
|
||||
switch quote {
|
||||
case 44:
|
||||
if quote == 44 && str[idx-1] != 92 && quoteCount == 0 {
|
||||
if len(bs) > 0 {
|
||||
if !(bs[0] == 34 && bs[len(bs)-1] == 34) {
|
||||
bs = append([]byte{34}, bs...)
|
||||
bs = append(bs, 34)
|
||||
}
|
||||
|
||||
bss = append(bss, bs...)
|
||||
bss = append(bss, 44)
|
||||
}
|
||||
bs = bs[:0]
|
||||
} else {
|
||||
bs = append(bs, quote)
|
||||
}
|
||||
case 34:
|
||||
if str[idx-1] != 92 {
|
||||
quoteCount = (quoteCount + 1) % 2
|
||||
}
|
||||
bs = append(bs, quote)
|
||||
default:
|
||||
bs = append(bs, quote)
|
||||
}
|
||||
|
||||
//bs = append(bs, str[idx])
|
||||
}
|
||||
|
||||
if len(bs) > 0 {
|
||||
if !(bs[0] == 34 && bs[len(bs)-1] == 34) {
|
||||
bs = append([]byte{34}, bs...)
|
||||
bs = append(bs, 34)
|
||||
}
|
||||
|
||||
bss = append(bss, bs...)
|
||||
} else {
|
||||
if len(bss) > 2 {
|
||||
bss = bss[:len(bss)-2]
|
||||
}
|
||||
}
|
||||
|
||||
bss = append([]byte{'['}, append(bss, ']')...)
|
||||
|
||||
if err := json.Unmarshal(bss, s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s StrSlice) Value() (driver.Value, error) {
|
||||
if s == nil {
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
if len(s) == 0 {
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
encoder := json.NewEncoder(buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
|
||||
if err := encoder.Encode(s); err != nil {
|
||||
return "{}", err
|
||||
}
|
||||
|
||||
bs := buf.Bytes()
|
||||
|
||||
bs[0] = '{'
|
||||
|
||||
if bs[len(bs)-1] == 10 {
|
||||
bs = bs[:len(bs)-1]
|
||||
}
|
||||
|
||||
bs[len(bs)-1] = '}'
|
||||
|
||||
return string(bs), nil
|
||||
}
|
71
internal/sqlType/uint64Slice.go
Normal file
@ -0,0 +1,71 @@
|
||||
package sqlType
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
type NumSlice[T ~int | ~int64 | ~uint | ~uint64] []T
|
||||
|
||||
func (n *NumSlice[T]) Scan(val interface{}) error {
|
||||
str, ok := val.(string)
|
||||
if !ok {
|
||||
return ErrConvertScanVal
|
||||
}
|
||||
|
||||
length := len(str)
|
||||
|
||||
if length <= 0 {
|
||||
*n = make(NumSlice[T], 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
if str[0] != '{' || str[length-1] != '}' {
|
||||
return ErrInvalidScanVal
|
||||
}
|
||||
|
||||
str = str[1 : length-1]
|
||||
if len(str) == 0 {
|
||||
*n = make(NumSlice[T], 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
numStrs := strings.Split(str, ",")
|
||||
nums := make([]T, len(numStrs))
|
||||
|
||||
for idx := range numStrs {
|
||||
num, err := cast.ToInt64E(strings.TrimSpace(numStrs[idx]))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: can't convert to %T", ErrConvertVal, T(0))
|
||||
}
|
||||
|
||||
nums[idx] = T(num)
|
||||
}
|
||||
|
||||
*n = nums
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n NumSlice[T]) Value() (driver.Value, error) {
|
||||
if n == nil {
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
if len(n) == 0 {
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
ss := make([]string, 0, len(n))
|
||||
for idx := range n {
|
||||
ss = append(ss, strconv.Itoa(int(n[idx])))
|
||||
}
|
||||
|
||||
s := strings.Join(ss, ", ")
|
||||
|
||||
return fmt.Sprintf("{%s}", s), nil
|
||||
}
|
22
internal/util/ctx.go
Normal file
@ -0,0 +1,22 @@
|
||||
package util
|
||||
|
||||
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
|
||||
}
|
84
internal/util/password.go
Normal file
@ -0,0 +1,84 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/sirupsen/logrus"
|
||||
"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 {
|
||||
logrus.Errorf("password in db invalid: %s", db)
|
||||
return false
|
||||
}
|
||||
|
||||
encs := strings.Split(strs[0], ":")
|
||||
if len(encs) != 3 {
|
||||
logrus.Errorf("password in db invalid: %s", db)
|
||||
return false
|
||||
}
|
||||
|
||||
encIteration, err := strconv.Atoi(encs[2])
|
||||
if err != nil {
|
||||
logrus.Errorf("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 {
|
||||
logrus.Warnf("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
|
||||
}
|
54
internal/util/random.go
Normal file
@ -0,0 +1,54 @@
|
||||
package util
|
||||
|
||||
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)
|
||||
}
|