:tada:: init project
No files matched your search
@@ -0,0 +1,66 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/nf"
|
||||
"time"
|
||||
"ultone/internal/handler"
|
||||
"ultone/internal/middleware/auth"
|
||||
"ultone/internal/middleware/front"
|
||||
"ultone/internal/middleware/logger"
|
||||
"ultone/internal/middleware/oplog"
|
||||
"ultone/internal/middleware/privilege"
|
||||
"ultone/internal/model"
|
||||
"ultone/internal/opt"
|
||||
)
|
||||
|
||||
func initApp(ctx context.Context) *nf.App {
|
||||
engine := nf.New(nf.Config{DisableLogger: true})
|
||||
engine.Use(logger.New())
|
||||
|
||||
// todo: add project prefix, if you need
|
||||
// for example: app := engine.Group("/api/{project}")
|
||||
app := engine.Group("/api")
|
||||
app.Get("/available", func(c *nf.Ctx) error {
|
||||
return c.JSON(nf.Map{"status": 200, "ok": true, "time": time.Now()})
|
||||
})
|
||||
|
||||
{
|
||||
api := app.Group("/user")
|
||||
api.Post("/auth/login", oplog.NewOpLog(ctx), handler.AuthLogin)
|
||||
api.Get("/auth/login", auth.NewAuth(), handler.AuthVerify)
|
||||
api.Post("/auth/logout", auth.NewAuth(), oplog.NewOpLog(ctx), handler.AuthLogout)
|
||||
|
||||
mng := api.Group("/manage")
|
||||
mng.Use(auth.NewAuth(), privilege.Verify(
|
||||
privilege.RelationAnd,
|
||||
model.PrivilegeUserManage,
|
||||
))
|
||||
|
||||
mng.Get("/user/list", handler.ManageUserList)
|
||||
mng.Post("/user/create", oplog.NewOpLog(ctx), handler.ManageUserCreate)
|
||||
mng.Post("/user/update", oplog.NewOpLog(ctx), handler.ManageUserUpdate)
|
||||
mng.Post("/user/delete", oplog.NewOpLog(ctx), handler.ManageUserDelete)
|
||||
}
|
||||
|
||||
{
|
||||
api := app.Group("/log")
|
||||
api.Use(auth.NewAuth(), privilege.Verify(privilege.RelationAnd, model.PrivilegeOpLog))
|
||||
api.Get("/category/list", handler.LogCategories())
|
||||
api.Get("/content/list", handler.LogList)
|
||||
}
|
||||
|
||||
{
|
||||
// todo: 替换 xxx
|
||||
// todo: 这里写你的模块和接口
|
||||
api := app.Group("/xxx")
|
||||
api.Use(auth.NewAuth())
|
||||
_ = api // todo: 添加自己的接口后删除该行
|
||||
}
|
||||
|
||||
if opt.EnableFront {
|
||||
engine.Use(front.NewFront(&front.DefaultFront, "dist/front/browser"))
|
||||
}
|
||||
|
||||
return engine
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"net"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
func Start(ctx context.Context) error {
|
||||
|
||||
app := initApp(ctx)
|
||||
ready := make(chan bool)
|
||||
|
||||
ln, err := net.Listen("tcp", opt.Cfg.Listen.Http)
|
||||
if err != nil {
|
||||
return fmt.Errorf("api.MustStart: net listen tcp address=%v err=%v", opt.Cfg.Listen.Http, err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
ready <- true
|
||||
|
||||
if err = app.RunListener(ln); err != nil {
|
||||
log.Panic("api.MustStart: app run err=%v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
<-ready
|
||||
|
||||
go func() {
|
||||
ready <- true
|
||||
<-ctx.Done()
|
||||
if err = app.Shutdown(tool.Timeout(1)); err != nil {
|
||||
log.Error("api.MustStart: app shutdown err=%v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
<-ready
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"ultone/internal/api"
|
||||
"ultone/internal/controller"
|
||||
"ultone/internal/database/cache"
|
||||
"ultone/internal/database/db"
|
||||
"ultone/internal/model"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
var (
|
||||
filename string
|
||||
)
|
||||
|
||||
func Execute(ctx context.Context) error {
|
||||
|
||||
tool.Must(opt.Init(filename))
|
||||
tool.Must(db.Init())
|
||||
tool.Must(cache.Init())
|
||||
|
||||
// todo: if elastic search required
|
||||
// tool.Must(es.Init())
|
||||
|
||||
// todo: if nebula required
|
||||
// tool.Must(nebula.Init(ctx, opt.Cfg.Nebula))
|
||||
|
||||
tool.Must(model.Init(db.New()))
|
||||
tool.Must(controller.Init())
|
||||
tool.Must(api.Start(ctx))
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"time"
|
||||
"ultone/internal/opt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
time.Local = time.FixedZone("CST", 8*3600)
|
||||
|
||||
flag.StringVar(&filename, "c", "etc/config.json", "config json file path")
|
||||
flag.BoolVar(&opt.Debug, "debug", false, "")
|
||||
|
||||
flag.Parse()
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package controller
|
||||
|
||||
var (
|
||||
// UserController todo: 可以实现自己的 controller
|
||||
UserController userController
|
||||
)
|
||||
|
||||
func Init() error {
|
||||
UserController = uc{}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"github.com/spf13/cast"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
"time"
|
||||
"ultone/internal/database/cache"
|
||||
"ultone/internal/database/db"
|
||||
"ultone/internal/log"
|
||||
"ultone/internal/model"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
type userController interface {
|
||||
GetUser(c *nf.Ctx, id uint64) (*model.User, error)
|
||||
GetUserByToken(c *nf.Ctx, token string) (*model.User, error)
|
||||
CacheUser(c *nf.Ctx, user *model.User) error
|
||||
CacheToken(c *nf.Ctx, token string, user *model.User) error
|
||||
RmUserCache(c *nf.Ctx, id uint64) error
|
||||
DeleteUser(c *nf.Ctx, id uint64) error
|
||||
}
|
||||
|
||||
type uc struct{}
|
||||
|
||||
var _ userController = (*uc)(nil)
|
||||
|
||||
func (u uc) GetUser(c *nf.Ctx, id uint64) (*model.User, error) {
|
||||
var (
|
||||
err error
|
||||
target = new(model.User)
|
||||
key = fmt.Sprintf("%s:user:id:%d", opt.CachePrefix, id)
|
||||
bs []byte
|
||||
)
|
||||
|
||||
if opt.EnableUserCache {
|
||||
if bs, err = cache.Client.Get(tool.Timeout(3), key); err != nil {
|
||||
log.Warn(c, "controller.GetUser: get user by cache key=%s err=%v", key, err)
|
||||
goto ByDB
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(bs, target); err != nil {
|
||||
log.Warn(c, "controller.GetUser: json unmarshal key=%s by=%s err=%v", key, string(bs), err)
|
||||
goto ByDB
|
||||
}
|
||||
|
||||
return target, nil
|
||||
}
|
||||
|
||||
ByDB:
|
||||
if err = db.New(tool.Timeout(3)).
|
||||
Model(&model.User{}).
|
||||
Where("id = ?", id).
|
||||
Take(target).
|
||||
Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// tips: 公开项目需要考虑击穿处理
|
||||
return target, resp.NewError(400, "目标不存在", err, nil)
|
||||
}
|
||||
|
||||
return target, resp.NewError(500, "", err, nil)
|
||||
}
|
||||
|
||||
if opt.EnableUserCache {
|
||||
if err = u.CacheUser(c, target); err != nil {
|
||||
log.Warn(c, "controller.GetUser: cache user key=%s err=%v", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func (u uc) GetUserByToken(c *nf.Ctx, token string) (*model.User, error) {
|
||||
strs := strings.Split(token, ".")
|
||||
if len(strs) != 3 {
|
||||
return nil, fmt.Errorf("controller.GetUserByToken: jwt token invalid, token=%s", token)
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s:user:token:%s", opt.CachePrefix, strs[2])
|
||||
bs, err := cache.Client.Get(tool.Timeout(3), key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debug(c, "controller.GetUserByToken: key=%s cache bytes=%s", key, string(bs))
|
||||
|
||||
userId := cast.ToUint64(string(bs))
|
||||
if userId == 0 {
|
||||
return nil, fmt.Errorf("controller.GetUserByToken: bs=%s cast to uint64 err", string(bs))
|
||||
}
|
||||
|
||||
var op *model.User
|
||||
|
||||
if op, err = u.GetUser(c, userId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return op, nil
|
||||
}
|
||||
|
||||
func (u uc) CacheUser(c *nf.Ctx, target *model.User) error {
|
||||
key := fmt.Sprintf("%s:user:id:%d", opt.CachePrefix, target.Id)
|
||||
return cache.Client.Set(tool.Timeout(3), key, target)
|
||||
}
|
||||
|
||||
func (u uc) CacheToken(c *nf.Ctx, token string, user *model.User) error {
|
||||
strs := strings.Split(token, ".")
|
||||
if len(strs) != 3 {
|
||||
return fmt.Errorf("controller.CacheToken: jwt token invalid")
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s:user:token:%s", opt.CachePrefix, strs[2])
|
||||
return cache.Client.SetEx(tool.Timeout(3), key, user.Id, opt.TokenTimeout)
|
||||
}
|
||||
func (u uc) RmUserCache(c *nf.Ctx, id uint64) error {
|
||||
key := fmt.Sprintf("%s:user:id:%d", opt.CachePrefix, id)
|
||||
return cache.Client.Del(tool.Timeout(3), key)
|
||||
}
|
||||
|
||||
func (u uc) DeleteUser(c *nf.Ctx, id uint64) error {
|
||||
var (
|
||||
err error
|
||||
now = time.Now()
|
||||
username = "CONCAT(username, '@del')"
|
||||
)
|
||||
|
||||
if opt.Cfg.DB.Type == "sqlite" {
|
||||
username = "username || '@del'"
|
||||
}
|
||||
|
||||
if err = db.New(tool.Timeout(5)).
|
||||
Model(&model.User{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]any{
|
||||
"deleted_at": now.UnixMilli(),
|
||||
"username": gorm.Expr(username),
|
||||
}).Error; err != nil {
|
||||
return resp.NewError(500, "", err, nil)
|
||||
}
|
||||
|
||||
if opt.EnableUserCache {
|
||||
if err = u.RmUserCache(c, id); err != nil {
|
||||
log.Warn(c, "controller.DeleteUser: rm user=%d cache err=%v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/hashicorp/golang-lru/v2/expirable"
|
||||
_ "github.com/hashicorp/golang-lru/v2/expirable"
|
||||
"time"
|
||||
"ultone/internal/interfaces"
|
||||
)
|
||||
|
||||
var _ interfaces.Cacher = (*_lru)(nil)
|
||||
|
||||
type _lru struct {
|
||||
client *expirable.LRU[string, *_lru_value]
|
||||
}
|
||||
|
||||
type _lru_value struct {
|
||||
duration time.Duration
|
||||
last time.Time
|
||||
bs []byte
|
||||
}
|
||||
|
||||
func (l *_lru) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
v, ok := l.client.Get(key)
|
||||
if !ok {
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
if v.duration == 0 {
|
||||
return v.bs, nil
|
||||
}
|
||||
|
||||
if time.Now().Sub(v.last) > v.duration {
|
||||
l.client.Remove(key)
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
return v.bs, nil
|
||||
}
|
||||
|
||||
func (l *_lru) GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error) {
|
||||
v, ok := l.client.Get(key)
|
||||
if !ok {
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
if v.duration == 0 {
|
||||
return v.bs, nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
if now.Sub(v.last) > v.duration {
|
||||
l.client.Remove(key)
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
l.client.Add(key, &_lru_value{
|
||||
duration: duration,
|
||||
last: now,
|
||||
bs: v.bs,
|
||||
})
|
||||
|
||||
return v.bs, nil
|
||||
}
|
||||
|
||||
func (l *_lru) Set(ctx context.Context, key string, value any) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.client.Add(key, &_lru_value{
|
||||
duration: 0,
|
||||
last: time.Now(),
|
||||
bs: bs,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *_lru) SetEx(ctx context.Context, key string, value any, duration time.Duration) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.client.Add(key, &_lru_value{
|
||||
duration: duration,
|
||||
last: time.Now(),
|
||||
bs: bs,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *_lru) Del(ctx context.Context, keys ...string) error {
|
||||
for _, key := range keys {
|
||||
l.client.Remove(key)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newLRUCache() (interfaces.Cacher, error) {
|
||||
client := expirable.NewLRU[string, *_lru_value](0, nil, 0)
|
||||
|
||||
return &_lru{client: client}, nil
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
"ultone/internal/interfaces"
|
||||
|
||||
"gitea.com/taozitaozi/gredis"
|
||||
)
|
||||
|
||||
var _ interfaces.Cacher = (*_mem)(nil)
|
||||
|
||||
type _mem struct {
|
||||
client *gredis.Gredis
|
||||
}
|
||||
|
||||
func (m *_mem) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
v, err := m.client.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bs, ok := v.([]byte)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid value type=%T", v)
|
||||
}
|
||||
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
func (m *_mem) GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error) {
|
||||
v, err := m.client.GetEx(key, duration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bs, ok := v.([]byte)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid value type=%T", v)
|
||||
}
|
||||
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
func (m *_mem) Set(ctx context.Context, key string, value any) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.client.Set(key, bs)
|
||||
}
|
||||
|
||||
func (m *_mem) SetEx(ctx context.Context, key string, value any, duration time.Duration) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.client.SetEx(key, bs, duration)
|
||||
}
|
||||
|
||||
func (m *_mem) Del(ctx context.Context, keys ...string) error {
|
||||
m.client.Delete(keys...)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"time"
|
||||
)
|
||||
|
||||
type _redis struct {
|
||||
client *redis.Client
|
||||
}
|
||||
|
||||
func (r *_redis) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
result, err := r.client.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(result), nil
|
||||
}
|
||||
|
||||
func (r *_redis) GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error) {
|
||||
result, err := r.client.GetEx(ctx, key, duration).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(result), nil
|
||||
}
|
||||
|
||||
func (r *_redis) Set(ctx context.Context, key string, value any) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = r.client.Set(ctx, key, bs, redis.KeepTTL).Result()
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *_redis) SetEx(ctx context.Context, key string, value any, duration time.Duration) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = r.client.SetEX(ctx, key, bs, duration).Result()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *_redis) Del(ctx context.Context, keys ...string) error {
|
||||
return r.client.Del(ctx, keys...).Err()
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"ultone/internal/interfaces"
|
||||
)
|
||||
|
||||
var (
|
||||
Client interfaces.Cacher
|
||||
)
|
||||
|
||||
type encoded_value interface {
|
||||
MarshalBinary() ([]byte, error)
|
||||
}
|
||||
|
||||
type decoded_value interface {
|
||||
UnmarshalBinary(bs []byte) error
|
||||
}
|
||||
|
||||
func handleValue(value any) ([]byte, error) {
|
||||
var (
|
||||
bs []byte
|
||||
err error
|
||||
)
|
||||
|
||||
if imp, ok := value.(encoded_value); ok {
|
||||
bs, err = imp.MarshalBinary()
|
||||
} else {
|
||||
bs, err = json.Marshal(value)
|
||||
}
|
||||
|
||||
return bs, err
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package cache
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrorKeyNotFound = errors.New("key not found")
|
||||
)
|
||||
@@ -0,0 +1,70 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitea.com/taozitaozi/gredis"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"net/url"
|
||||
"strings"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
func Init() error {
|
||||
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
strs := strings.Split(opt.Cfg.Cache.Uri, "::")
|
||||
|
||||
switch strs[0] {
|
||||
case "memory":
|
||||
gc := gredis.NewGredis(1024 * 1024)
|
||||
Client = &_mem{client: gc}
|
||||
case "lru":
|
||||
if Client, err = newLRUCache(); err != nil {
|
||||
return err
|
||||
}
|
||||
case "redis":
|
||||
var (
|
||||
ins *url.URL
|
||||
err error
|
||||
)
|
||||
|
||||
if len(strs) != 2 {
|
||||
return fmt.Errorf("cache.Init: invalid cache uri: %s", opt.Cfg.Cache.Uri)
|
||||
}
|
||||
|
||||
uri := strs[1]
|
||||
|
||||
if !strings.Contains(uri, "://") {
|
||||
uri = fmt.Sprintf("redis://%s", uri)
|
||||
}
|
||||
|
||||
if ins, err = url.Parse(uri); err != nil {
|
||||
return fmt.Errorf("cache.Init: url parse cache uri: %s, err: %s", opt.Cfg.Cache.Uri, err.Error())
|
||||
}
|
||||
|
||||
addr := ins.Host
|
||||
username := ins.User.Username()
|
||||
password, _ := ins.User.Password()
|
||||
|
||||
var rc *redis.Client
|
||||
rc = redis.NewClient(&redis.Options{
|
||||
Addr: addr,
|
||||
Username: username,
|
||||
Password: password,
|
||||
})
|
||||
|
||||
if err = rc.Ping(tool.Timeout(5)).Err(); err != nil {
|
||||
return fmt.Errorf("cache.Init: redis ping err: %s", err.Error())
|
||||
}
|
||||
|
||||
Client = &_redis{client: rc}
|
||||
default:
|
||||
return fmt.Errorf("cache type %s not support", strs[0])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gorm.io/gorm"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
var (
|
||||
cli = &client{}
|
||||
)
|
||||
|
||||
type client struct {
|
||||
cli *gorm.DB
|
||||
ttype string
|
||||
}
|
||||
|
||||
func Type() string {
|
||||
return cli.ttype
|
||||
}
|
||||
|
||||
func New(ctxs ...context.Context) *gorm.DB {
|
||||
var ctx context.Context
|
||||
if len(ctxs) > 0 && ctxs[0] != nil {
|
||||
ctx = ctxs[0]
|
||||
} else {
|
||||
ctx = tool.Timeout(30)
|
||||
}
|
||||
|
||||
session := cli.cli.Session(&gorm.Session{Context: ctx})
|
||||
|
||||
if opt.Debug {
|
||||
session = session.Debug()
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
"ultone/internal/opt"
|
||||
)
|
||||
|
||||
func Init() error {
|
||||
strs := strings.Split(opt.Cfg.DB.Uri, "::")
|
||||
|
||||
if len(strs) != 2 {
|
||||
return fmt.Errorf("db.Init: opt db uri invalid: %s", opt.Cfg.DB.Uri)
|
||||
}
|
||||
|
||||
cli.ttype = strs[0]
|
||||
|
||||
var (
|
||||
err error
|
||||
dsn = strs[1]
|
||||
)
|
||||
|
||||
switch strs[0] {
|
||||
case "sqlite":
|
||||
opt.Cfg.DB.Type = "sqlite"
|
||||
cli.cli, err = gorm.Open(sqlite.Open(dsn))
|
||||
case "mysql":
|
||||
opt.Cfg.DB.Type = "mysql"
|
||||
cli.cli, err = gorm.Open(mysql.Open(dsn))
|
||||
case "postgres":
|
||||
opt.Cfg.DB.Type = "postgres"
|
||||
cli.cli, err = gorm.Open(postgres.Open(dsn))
|
||||
default:
|
||||
return fmt.Errorf("db type only support: [sqlite, mysql, postgres], unsupported db type: %s", strs[0])
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("db.Init: open %s with dsn:%s, err: %w", strs[0], dsn, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package es
|
||||
|
||||
import (
|
||||
elastic "github.com/elastic/go-elasticsearch/v7"
|
||||
"github.com/loveuer/esgo2dump/xes/es7"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"net/url"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
var (
|
||||
Client *elastic.Client
|
||||
)
|
||||
|
||||
func Init() error {
|
||||
ins, err := url.Parse(opt.Cfg.ES.Uri)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug("es.InitClient url parse uri: %s, result: %+v", opt.Cfg.ES.Uri, ins)
|
||||
|
||||
if Client, err = es7.NewClient(tool.Timeout(10), ins); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package mq
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
"net/url"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Init - init mq client:
|
||||
// - @param.uri: "{scheme[amqp/amqps]}://{username}:{password}@{endpoint}/{virtual_host}"
|
||||
// - @param.certs: with amqps, certs[0]=client crt bytes, certs[0]=client key bytes
|
||||
|
||||
type _client struct {
|
||||
sync.Mutex
|
||||
uri string
|
||||
tlsCfg *tls.Config
|
||||
conn *amqp.Connection
|
||||
ch *amqp.Channel
|
||||
consume <-chan amqp.Delivery
|
||||
queue *queueOption
|
||||
}
|
||||
|
||||
func (c *_client) open() error {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
if c.tlsCfg != nil {
|
||||
c.conn, err = amqp.DialTLS(c.uri, c.tlsCfg)
|
||||
} else {
|
||||
c.conn, err = amqp.Dial(c.uri)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.ch, err = c.conn.Channel(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if client.queue != nil && client.queue.name != "" {
|
||||
if _, err = client.ch.QueueDeclare(
|
||||
client.queue.name,
|
||||
client.queue.durable,
|
||||
client.queue.autoDelete,
|
||||
client.queue.exclusive,
|
||||
client.queue.noWait,
|
||||
client.queue.args,
|
||||
); err != nil {
|
||||
return fmt.Errorf("declare queue: %s, err: %w", client.queue.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
client = &_client{
|
||||
uri: "amqp://guest:guest@127.0.0.1:5672/",
|
||||
tlsCfg: nil,
|
||||
}
|
||||
)
|
||||
|
||||
// Init - init mq client
|
||||
func Init(opts ...OptionFn) error {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
for _, fn := range opts {
|
||||
fn(client)
|
||||
}
|
||||
|
||||
if _, err = url.Parse(client.uri); err != nil {
|
||||
return fmt.Errorf("url parse uri err: %w", err)
|
||||
}
|
||||
|
||||
if err = client.open(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package mq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConsume(t *testing.T) {
|
||||
clientCert, err := tls.LoadX509KeyPair(
|
||||
"/Users/loveuer/codes/project/bifrost-pro/search_v3/internal/database/mq/tls/client.crt",
|
||||
"/Users/loveuer/codes/project/bifrost-pro/search_v3/internal/database/mq/tls/client.key",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
ca, err := os.ReadFile("/Users/loveuer/codes/project/bifrost-pro/search_v3/internal/database/mq/tls/ca.crt")
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
if !caCertPool.AppendCertsFromPEM(ca) {
|
||||
t.Fatal("ca pool append ca crt err")
|
||||
}
|
||||
|
||||
if err := Init(
|
||||
WithURI("amqps://admin:password@mq.dev:5671/export"),
|
||||
WithTLS(&tls.Config{
|
||||
Certificates: []tls.Certificate{clientCert},
|
||||
RootCAs: caCertPool,
|
||||
InsecureSkipVerify: true,
|
||||
}),
|
||||
WithQueueDeclare("export", false, false, false, false, amqp.Table{"x-max-priority": 100}),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
defer cancel()
|
||||
|
||||
ch, err := Consume(ctx, "export", &ConsumeOpt{MaxReconnection: -1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Log("[TEST] start consume msg")
|
||||
for msg := range ch {
|
||||
t.Logf("[TEST] [%s] [msg: %s]", time.Now().Format("060102T150405"), string(msg.Body))
|
||||
_ = msg.Ack(false)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublish(t *testing.T) {
|
||||
clientCert, err := tls.LoadX509KeyPair(
|
||||
"/Users/loveuer/codes/project/bifrost-pro/search_v3/internal/database/mq/tls/client.crt",
|
||||
"/Users/loveuer/codes/project/bifrost-pro/search_v3/internal/database/mq/tls/client.key",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
ca, err := os.ReadFile("/Users/loveuer/codes/project/bifrost-pro/search_v3/internal/database/mq/tls/ca.crt")
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
if !caCertPool.AppendCertsFromPEM(ca) {
|
||||
t.Fatal("ca pool append ca crt err")
|
||||
}
|
||||
|
||||
if err := Init(
|
||||
WithURI("amqps://admin:password@mq.dev:5671/export"),
|
||||
WithTLS(&tls.Config{
|
||||
Certificates: []tls.Certificate{clientCert},
|
||||
RootCAs: caCertPool,
|
||||
InsecureSkipVerify: true,
|
||||
}),
|
||||
WithQueueDeclare("export", false, false, false, false, amqp.Table{"x-max-priority": 100}),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
defer cancel()
|
||||
|
||||
count := 1
|
||||
|
||||
t.Log("[TEST] start publish msg...")
|
||||
|
||||
for {
|
||||
if err = Publish(ctx, "export", amqp.Publishing{
|
||||
ContentType: "text/plain",
|
||||
Body: []byte(time.Now().Format(time.RFC3339) + " => hello_world@" + strconv.Itoa(count)),
|
||||
}); err != nil {
|
||||
t.Log(err.Error())
|
||||
}
|
||||
|
||||
time.Sleep(11 * time.Second)
|
||||
count++
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package mq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/loveuer/esgo2dump/log"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
"os"
|
||||
"time"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
// ConsumeOpt
|
||||
// - Name: consumer's name, default unamed_<timestamp>
|
||||
// - MaxReconnection: when mq connection closed, max reconnection times, default 3, -1 for unlimited
|
||||
type ConsumeOpt struct {
|
||||
Name string // consumer's name, default unamed_<timestamp>
|
||||
AutoAck bool
|
||||
Exclusive bool
|
||||
NoLocal bool
|
||||
NoWait bool
|
||||
MaxReconnection int // when mq connection closed, max reconnection times, default 3, -1 for unlimited
|
||||
Args amqp.Table
|
||||
}
|
||||
|
||||
func Consume(ctx context.Context, queue string, opts ...*ConsumeOpt) (<-chan amqp.Delivery, error) {
|
||||
var (
|
||||
err error
|
||||
res = make(chan amqp.Delivery, 1)
|
||||
opt = &ConsumeOpt{
|
||||
Name: os.Getenv("HOSTNAME"),
|
||||
AutoAck: false,
|
||||
Exclusive: false,
|
||||
NoLocal: false,
|
||||
NoWait: false,
|
||||
Args: nil,
|
||||
MaxReconnection: 3,
|
||||
}
|
||||
)
|
||||
|
||||
if len(opts) > 0 && opts[0] != nil {
|
||||
opt = opts[0]
|
||||
}
|
||||
|
||||
if opt.Name == "" {
|
||||
opt.Name = fmt.Sprintf("unamed_%d", time.Now().UnixMilli())
|
||||
}
|
||||
|
||||
client.Lock()
|
||||
if client.consume, err = client.ch.Consume(queue, opt.Name, opt.AutoAck, opt.Exclusive, opt.NoLocal, opt.NoWait, opt.Args); err != nil {
|
||||
client.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
client.Unlock()
|
||||
|
||||
go func() {
|
||||
Run:
|
||||
retry := 0
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
close(res)
|
||||
return
|
||||
case m, ok := <-client.consume:
|
||||
if !ok {
|
||||
log.Warn("[mq] consume channel closed!!!")
|
||||
goto Reconnect
|
||||
}
|
||||
|
||||
res <- m
|
||||
}
|
||||
}
|
||||
|
||||
Reconnect:
|
||||
if opt.MaxReconnection == -1 || opt.MaxReconnection > retry {
|
||||
retry++
|
||||
|
||||
log.Warn("[mq] try reconnect[%d/%d] to mq server after %d seconds...err: %v", retry, opt.MaxReconnection, tool.Min(60, retry*5), err)
|
||||
time.Sleep(time.Duration(tool.Min(60, retry*5)) * time.Second)
|
||||
if err = client.open(); err != nil {
|
||||
goto Reconnect
|
||||
}
|
||||
|
||||
client.Lock()
|
||||
if client.consume, err = client.ch.Consume(queue, opt.Name, opt.AutoAck, opt.Exclusive, opt.NoLocal, opt.NoWait, opt.Args); err != nil {
|
||||
client.Unlock()
|
||||
goto Reconnect
|
||||
}
|
||||
client.Unlock()
|
||||
|
||||
log.Info("[mq] reconnect success!!!")
|
||||
goto Run
|
||||
}
|
||||
}()
|
||||
|
||||
return res, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package mq
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
)
|
||||
|
||||
type OptionFn func(*_client)
|
||||
|
||||
// WithURI
|
||||
// - amqp uri, e.g. amqp://guest:guest@127.0.0.1:5672/vhost
|
||||
// - tips: with tls, scheme should be amqps, amqps://xx:xx@127.0.0.1:5672/vhost
|
||||
func WithURI(uri string) OptionFn {
|
||||
return func(c *_client) {
|
||||
c.uri = uri
|
||||
}
|
||||
}
|
||||
|
||||
// WithTLS
|
||||
// - amqps tls config
|
||||
// - include client cert, client key, ca cert
|
||||
func WithTLS(tlsCfg *tls.Config) OptionFn {
|
||||
return func(c *_client) {
|
||||
c.tlsCfg = tlsCfg
|
||||
}
|
||||
}
|
||||
|
||||
type queueOption struct {
|
||||
name string
|
||||
durable bool
|
||||
autoDelete bool
|
||||
exclusive bool
|
||||
noWait bool
|
||||
args amqp.Table
|
||||
}
|
||||
|
||||
func WithQueueDeclare(name string, durable, autoDelete, exclusive, noWait bool, args amqp.Table) OptionFn {
|
||||
return func(c *_client) {
|
||||
c.queue = &queueOption{
|
||||
name: name,
|
||||
durable: durable,
|
||||
autoDelete: autoDelete,
|
||||
exclusive: exclusive,
|
||||
noWait: noWait,
|
||||
args: args,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package mq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/loveuer/esgo2dump/log"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
"time"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
// PublishOpt
|
||||
// - MaxReconnect: publish msg auto retry with reconnect, should not be big, case memory leak
|
||||
type PublishOpt struct {
|
||||
Exchange string
|
||||
Mandatory bool
|
||||
Immediate bool
|
||||
MaxReconnect uint8 // publish msg auto retry with reconnect, should not be big(default 1), case memory leak
|
||||
}
|
||||
|
||||
func Publish(ctx context.Context, queue string, msg amqp.Publishing, opts ...*PublishOpt) error {
|
||||
var (
|
||||
err error
|
||||
opt = &PublishOpt{
|
||||
Exchange: amqp.DefaultExchange,
|
||||
Mandatory: false,
|
||||
Immediate: false,
|
||||
MaxReconnect: 1,
|
||||
}
|
||||
retry = 0
|
||||
)
|
||||
|
||||
if len(opts) > 0 && opts[0] != nil {
|
||||
opt = opts[0]
|
||||
}
|
||||
|
||||
for ; retry <= int(opt.MaxReconnect); retry++ {
|
||||
if err = client.ch.PublishWithContext(ctx, opt.Exchange, queue, opt.Mandatory, opt.Immediate, msg); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if errors.Is(err, amqp.ErrClosed) {
|
||||
sleep := tool.Min(120, (retry+1)*30)
|
||||
|
||||
log.Warn("[mq] connection closed, reconnect[%d/%d] after %d seconds", retry+1, opt.MaxReconnect, sleep)
|
||||
|
||||
time.Sleep(time.Duration(sleep) * time.Second)
|
||||
|
||||
if oerr := client.open(); oerr != nil {
|
||||
log.Error("[mq] reconnect[%d/%d] mq err: %v", oerr, retry+1, opt.MaxReconnect)
|
||||
} else {
|
||||
log.Info("[mq] reconnect mq success!!!")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/ngorm/v2"
|
||||
"strings"
|
||||
"ultone/internal/opt"
|
||||
)
|
||||
|
||||
var (
|
||||
client *ngorm.Client
|
||||
)
|
||||
|
||||
func Init(ctx context.Context, cfg opt.Nebula) error {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
if client, err = ngorm.NewClient(ctx, &ngorm.Config{
|
||||
Endpoints: strings.Split(cfg.Uri, ","),
|
||||
Username: cfg.Username,
|
||||
Password: cfg.Password,
|
||||
DefaultSpace: cfg.Space,
|
||||
Logger: nil,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func New(ctx context.Context, cfgs ...*ngorm.SessCfg) *ngorm.Session {
|
||||
return client.Session(cfgs...)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"ultone/internal/database/db"
|
||||
"ultone/internal/log"
|
||||
"ultone/internal/model"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/sqlType"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
func LogCategories() nf.HandlerFunc {
|
||||
return func(c *nf.Ctx) error {
|
||||
return resp.Resp200(c, model.OpLogType(0).All())
|
||||
}
|
||||
}
|
||||
|
||||
func LogList(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Page int `query:"page"`
|
||||
Size int `query:"size"`
|
||||
UserIds []uint64 `query:"user_ids"`
|
||||
Types sqlType.NumSlice[model.OpLogType] `query:"types"`
|
||||
}
|
||||
|
||||
var (
|
||||
ok bool
|
||||
op *model.User
|
||||
err error
|
||||
req = new(Req)
|
||||
list = make([]*model.OpLog, 0)
|
||||
total int
|
||||
)
|
||||
|
||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if err = c.QueryParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.Size <= 0 {
|
||||
req.Size = opt.DefaultSize
|
||||
}
|
||||
|
||||
if req.Size > opt.MaxSize {
|
||||
return resp.Resp400(c, req, "参数过大")
|
||||
}
|
||||
|
||||
txCount := op.Role.Where(db.New(tool.Timeout(3)).
|
||||
Model(&model.OpLog{}).
|
||||
Select("COUNT(`op_logs`.`id`)").
|
||||
Joins("LEFT JOIN users ON `users`.`id` = `op_logs`.`user_id`"))
|
||||
txGet := op.Role.Where(db.New(tool.Timeout(10)).
|
||||
Model(&model.OpLog{}).
|
||||
Joins("LEFT JOIN users ON `users`.`id` = `op_logs`.`user_id`"))
|
||||
|
||||
if len(req.UserIds) != 0 {
|
||||
txCount = txCount.Where("op_logs.user_id IN ?", req.UserIds)
|
||||
txGet = txGet.Where("op_logs.user_id IN ?", req.UserIds)
|
||||
}
|
||||
|
||||
if len(req.Types) != 0 {
|
||||
txCount = txCount.Where("op_logs.type IN ?", req.Types)
|
||||
txGet = txGet.Where("op_logs.type IN ?", req.Types)
|
||||
}
|
||||
|
||||
if err = txCount.
|
||||
Find(&total).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
if err = txGet.
|
||||
Offset(req.Page * req.Size).
|
||||
Limit(req.Size).
|
||||
Order("`op_logs`.`created_at` DESC").
|
||||
Find(&list).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
for _, logItem := range list {
|
||||
m := make(map[string]any)
|
||||
if err = logItem.Content.Bind(&m); err != nil {
|
||||
log.Warn(c, "handler.LogList: log=%d content=%v bind map[string]any err=%v", logItem.Id, logItem.Content, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if logItem.HTML, err = logItem.Type.Render(m); err != nil {
|
||||
log.Warn(c, "handler.LogList: log=%d template=%s render map=%+v err=%v", logItem.Id, logItem.Type.Template(), m, err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debug(c, "handler.LogList: log=%d render map=%+v string=%s", logItem.Id, m, logItem.HTML)
|
||||
}
|
||||
|
||||
return resp.Resp200(c, nf.Map{"list": list, "total": total})
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"time"
|
||||
"ultone/internal/controller"
|
||||
"ultone/internal/database/cache"
|
||||
"ultone/internal/database/db"
|
||||
"ultone/internal/middleware/oplog"
|
||||
"ultone/internal/model"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/sqlType"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
func AuthLogin(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
target = new(model.User)
|
||||
token string
|
||||
now = time.Now()
|
||||
)
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if err = db.New(tool.Timeout(3)).
|
||||
Model(&model.User{}).
|
||||
Where("username = ?", req.Username).
|
||||
Where("deleted_at = 0").
|
||||
Take(target).
|
||||
Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return resp.Resp400(c, err.Error(), "用户名或密码错误")
|
||||
}
|
||||
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
if !tool.ComparePassword(req.Password, target.Password) {
|
||||
return resp.Resp400(c, nil, "用户名或密码错误")
|
||||
}
|
||||
|
||||
if err = target.IsValid(true); err != nil {
|
||||
return resp.Resp401(c, nil, err.Error())
|
||||
}
|
||||
|
||||
if err = controller.UserController.CacheUser(c, target); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
if token, err = target.JwtEncode(); err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
if err = controller.UserController.CacheToken(c, token, target); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
if !opt.MultiLogin {
|
||||
var (
|
||||
last = fmt.Sprintf("%s:user:last_token:%d", opt.CachePrefix, target.Id)
|
||||
bs []byte
|
||||
)
|
||||
|
||||
if bs, err = cache.Client.Get(tool.Timeout(3), last); err == nil {
|
||||
key := fmt.Sprintf("%s:user:token:%s", opt.CachePrefix, string(bs))
|
||||
_ = cache.Client.Del(tool.Timeout(3), key)
|
||||
}
|
||||
|
||||
if err = cache.Client.Set(tool.Timeout(3), last, token); err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
c.Set("Set-Cookie", fmt.Sprintf("%s=%s; Path=/", opt.CookieName, token))
|
||||
c.Locals("user", target)
|
||||
c.Locals(opt.OpLogLocalKey, &oplog.OpLog{Type: model.OpLogTypeLogin, Content: map[string]any{
|
||||
"time": now.UnixMilli(),
|
||||
"ip": c.IP(true),
|
||||
}})
|
||||
|
||||
return resp.Resp200(c, nf.Map{"token": token, "user": target})
|
||||
}
|
||||
|
||||
func AuthVerify(c *nf.Ctx) error {
|
||||
op, ok := c.Locals("user").(*model.User)
|
||||
if !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
token, ok := c.Locals("token").(string)
|
||||
if !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
return resp.Resp200(c, nf.Map{"token": token, "user": op})
|
||||
}
|
||||
|
||||
func AuthLogout(c *nf.Ctx) error {
|
||||
op, ok := c.Locals("user").(*model.User)
|
||||
if !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
_ = controller.UserController.RmUserCache(c, op.Id)
|
||||
|
||||
c.Locals(opt.OpLogLocalKey, &oplog.OpLog{
|
||||
Type: model.OpLogTypeLogout,
|
||||
Content: map[string]any{
|
||||
"time": time.Now().UnixMilli(),
|
||||
"ip": c.IP(),
|
||||
},
|
||||
})
|
||||
|
||||
return resp.Resp200(c, nil)
|
||||
}
|
||||
|
||||
func ManageUserList(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Page int `query:"page"`
|
||||
Size int `query:"size"`
|
||||
Keyword string `query:"keyword"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
ok bool
|
||||
op *model.User
|
||||
req = new(Req)
|
||||
list = make([]*model.User, 0)
|
||||
total = 0
|
||||
)
|
||||
|
||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if err = c.QueryParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.Size == 0 {
|
||||
req.Size = opt.DefaultSize
|
||||
}
|
||||
|
||||
if req.Size > opt.MaxSize {
|
||||
return resp.Resp400(c, nf.Map{"msg": "size over max", "max": opt.MaxSize})
|
||||
}
|
||||
|
||||
if err = op.Role.Where(db.New(tool.Timeout(10)).
|
||||
Model(&model.User{}).
|
||||
Where("deleted_at = 0")).
|
||||
Order("updated_at DESC").
|
||||
Offset(req.Page * req.Size).
|
||||
Limit(req.Size).
|
||||
Find(&list).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
if err = op.Role.Where(db.New(tool.Timeout(5)).
|
||||
Model(&model.User{}).
|
||||
Select("COUNT(id)").
|
||||
Where("deleted_at = 0")).
|
||||
Find(&total).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
return resp.Resp200(c, nf.Map{"list": list, "total": total})
|
||||
}
|
||||
|
||||
func ManageUserCreate(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Password string `json:"password"`
|
||||
Status model.Status `json:"status"`
|
||||
Role model.Role `json:"role"`
|
||||
Privileges sqlType.NumSlice[model.Privilege] `json:"privileges"`
|
||||
Comment string `json:"comment"`
|
||||
ActiveAt int64 `json:"active_at"`
|
||||
Deadline int64 `json:"deadline"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
ok bool
|
||||
op *model.User
|
||||
req = new(Req)
|
||||
now = time.Now()
|
||||
)
|
||||
|
||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.Username == "" || req.Password == "" {
|
||||
return resp.Resp400(c, req)
|
||||
}
|
||||
|
||||
if err = tool.CheckPassword(req.Password); err != nil {
|
||||
return resp.Resp400(c, req, err.Error())
|
||||
}
|
||||
|
||||
if req.Nickname == "" {
|
||||
req.Nickname = req.Username
|
||||
}
|
||||
|
||||
if req.Status.Code() == "unknown" {
|
||||
return resp.Resp400(c, req, "用户状态不正常")
|
||||
}
|
||||
|
||||
if req.Role == 0 {
|
||||
req.Role = model.RoleUser
|
||||
}
|
||||
|
||||
if req.ActiveAt == 0 {
|
||||
req.ActiveAt = now.UnixMilli()
|
||||
}
|
||||
|
||||
if req.Deadline == 0 {
|
||||
req.Deadline = now.AddDate(99, 0, 0).UnixMilli()
|
||||
}
|
||||
|
||||
newUser := &model.User{
|
||||
CreatedAt: now.UnixMilli(),
|
||||
UpdatedAt: now.UnixMilli(),
|
||||
Username: req.Username,
|
||||
Password: tool.NewPassword(req.Password),
|
||||
Status: req.Status,
|
||||
Nickname: req.Nickname,
|
||||
Comment: req.Comment,
|
||||
Role: req.Role,
|
||||
Privileges: req.Privileges,
|
||||
CreatedById: op.Id,
|
||||
CreatedByName: op.CreatedByName,
|
||||
ActiveAt: op.ActiveAt,
|
||||
Deadline: op.Deadline,
|
||||
}
|
||||
|
||||
if err = newUser.IsValid(false); err != nil {
|
||||
return resp.Resp400(c, newUser, err.Error())
|
||||
}
|
||||
|
||||
if !newUser.Role.CanOP(op) {
|
||||
return resp.Resp403(c, newUser, "角色不符合")
|
||||
}
|
||||
|
||||
if err = db.New(tool.Timeout(5)).
|
||||
Create(newUser).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
c.Locals(opt.OpLogLocalKey, &oplog.OpLog{Type: model.OpLogTypeCreateUser, Content: map[string]any{
|
||||
"target_id": newUser.Id,
|
||||
"target_username": newUser.Username,
|
||||
"target_nickname": newUser.Nickname,
|
||||
"target_status": newUser.Status.Label(),
|
||||
"target_role": newUser.Role.Label(),
|
||||
"target_privileges": lo.Map(newUser.Privileges, func(item model.Privilege, index int) string {
|
||||
return item.Label()
|
||||
}),
|
||||
"target_active_at": op.ActiveAt,
|
||||
"target_deadline": op.Deadline,
|
||||
}})
|
||||
|
||||
return resp.Resp200(c, newUser)
|
||||
}
|
||||
|
||||
func ManageUserUpdate(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Id uint64 `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Password string `json:"password"`
|
||||
Status model.Status `json:"status"`
|
||||
Comment string `json:"comment"`
|
||||
Role model.Role `json:"role"`
|
||||
Privileges sqlType.NumSlice[model.Privilege] `json:"privileges"`
|
||||
ActiveAt int64 `json:"active_at"`
|
||||
Deadline int64 `json:"deadline"`
|
||||
}
|
||||
|
||||
type Change struct {
|
||||
Old any `json:"old"`
|
||||
New any `json:"new"`
|
||||
}
|
||||
|
||||
var (
|
||||
ok bool
|
||||
op *model.User
|
||||
target *model.User
|
||||
err error
|
||||
req = new(Req)
|
||||
rm = make(map[string]any)
|
||||
updates = make(map[string]any)
|
||||
changes = make(map[string]Change)
|
||||
)
|
||||
|
||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if err = c.BodyParser(&rm); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.Id == 0 {
|
||||
return resp.Resp400(c, "未指定目标用户")
|
||||
}
|
||||
|
||||
if target, err = controller.UserController.GetUser(c, req.Id); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
if op.Role < target.Role || ((op.Role == target.Role) && opt.RoleMustLess) {
|
||||
return resp.Resp403(c, req)
|
||||
}
|
||||
|
||||
if op.Id == req.Id {
|
||||
return resp.Resp403(c, req, "无法更新自己")
|
||||
}
|
||||
|
||||
if _, ok = rm["nickname"]; ok {
|
||||
if req.Nickname == "" {
|
||||
return resp.Resp400(c, req)
|
||||
}
|
||||
|
||||
updates["nickname"] = req.Nickname
|
||||
changes["昵称"] = Change{Old: target.Nickname, New: req.Nickname}
|
||||
}
|
||||
|
||||
if _, ok = rm["password"]; ok {
|
||||
if err = tool.CheckPassword(req.Password); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
updates["password"] = tool.NewPassword(req.Password)
|
||||
changes["密码"] = Change{Old: "******", New: "******"}
|
||||
}
|
||||
|
||||
if _, ok = rm["status"]; ok {
|
||||
if req.Status.Code() == "unknown" {
|
||||
return resp.Resp400(c, req, "用户状态不符合")
|
||||
}
|
||||
|
||||
updates["status"] = req.Status
|
||||
changes["状态"] = Change{Old: target.Status.Label(), New: req.Status.Label()}
|
||||
}
|
||||
|
||||
if _, ok = rm["comment"]; ok {
|
||||
updates["comment"] = req.Comment
|
||||
changes["备注"] = Change{Old: target.Comment, New: req.Comment}
|
||||
}
|
||||
|
||||
if _, ok = rm["role"]; ok {
|
||||
if op.Role < req.Role || ((op.Role == req.Role) && opt.RoleMustLess) {
|
||||
return resp.Resp400(c, req, "用户角色不符合")
|
||||
}
|
||||
|
||||
updates["role"] = req.Role
|
||||
changes["角色"] = Change{Old: target.Role.Label(), New: req.Role.Label()}
|
||||
}
|
||||
|
||||
if _, ok = rm["privileges"]; ok {
|
||||
for _, val := range req.Privileges {
|
||||
if lo.IndexOf(op.Privileges, val) < 0 {
|
||||
return resp.Resp400(c, req, fmt.Sprintf("权限: %s 不符合", val.Label()))
|
||||
}
|
||||
}
|
||||
|
||||
changes["权限"] = Change{
|
||||
Old: lo.Map(target.Privileges, func(item model.Privilege, index int) string {
|
||||
return item.Label()
|
||||
}),
|
||||
New: lo.Map(req.Privileges, func(item model.Privilege, index int) string {
|
||||
return item.Label()
|
||||
}),
|
||||
}
|
||||
updates["privileges"] = req.Privileges
|
||||
}
|
||||
|
||||
if _, ok = rm["active_at"]; ok {
|
||||
updates["active_at"] = time.UnixMilli(req.ActiveAt).UnixMilli()
|
||||
changes["激活时间"] = Change{Old: target.ActiveAt, New: req.ActiveAt}
|
||||
}
|
||||
|
||||
if _, ok = rm["deadline"]; ok {
|
||||
updates["deadline"] = time.UnixMilli(req.Deadline).UnixMilli()
|
||||
changes["到期时间"] = Change{Old: target.Deadline, New: req.Deadline}
|
||||
}
|
||||
|
||||
updated := new(model.User)
|
||||
if err = db.New(tool.Timeout(5)).
|
||||
Model(updated).
|
||||
Clauses(clause.Returning{}).
|
||||
Where("id = ?", req.Id).
|
||||
Updates(updates).
|
||||
Error; err != nil {
|
||||
return resp.Resp500(c, err.Error())
|
||||
}
|
||||
|
||||
if err = controller.UserController.RmUserCache(c, req.Id); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
c.Locals(opt.OpLogLocalKey, &oplog.OpLog{Type: model.OpLogTypeUpdateUser, Content: map[string]any{
|
||||
"target_id": target.Id,
|
||||
"target_username": target.Username,
|
||||
"changes": changes,
|
||||
}})
|
||||
|
||||
return resp.Resp200(c, updated)
|
||||
}
|
||||
|
||||
func ManageUserDelete(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Id uint64 `json:"id"`
|
||||
}
|
||||
|
||||
var (
|
||||
ok bool
|
||||
op *model.User
|
||||
target *model.User
|
||||
err error
|
||||
req = new(Req)
|
||||
)
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return resp.Resp400(c, err.Error())
|
||||
}
|
||||
|
||||
if req.Id == 0 {
|
||||
return resp.Resp400(c, req)
|
||||
}
|
||||
|
||||
if op, ok = c.Locals("user").(*model.User); !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if req.Id == op.Id {
|
||||
return resp.Resp400(c, nil, "无法删除自己")
|
||||
}
|
||||
|
||||
if target, err = controller.UserController.GetUser(c, req.Id); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
if op.Role < target.Role || (op.Role == target.Role && opt.RoleMustLess) {
|
||||
return resp.Resp403(c, nil)
|
||||
}
|
||||
|
||||
if err = controller.UserController.DeleteUser(c, target.Id); err != nil {
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
c.Locals(opt.OpLogLocalKey, &oplog.OpLog{Type: model.OpLogTypeDeleteUser, Content: map[string]any{
|
||||
"target_id": target.Id,
|
||||
"target_username": target.Username,
|
||||
}})
|
||||
|
||||
return resp.Resp200(c, nil, "删除成功")
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Cacher interface {
|
||||
Get(ctx context.Context, key string) ([]byte, error)
|
||||
GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error)
|
||||
// Set value 会被序列化, 优先使用 MarshalBinary 方法, 没有则执行 json.Marshal
|
||||
Set(ctx context.Context, key string, value any) error
|
||||
// SetEx value 会被序列化, 优先使用 MarshalBinary 方法, 没有则执行 json.Marshal
|
||||
SetEx(ctx context.Context, key string, value any, duration time.Duration) error
|
||||
Del(ctx context.Context, keys ...string) error
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package interfaces
|
||||
|
||||
type Enum interface {
|
||||
Value() int64
|
||||
Code() string
|
||||
Label() string
|
||||
|
||||
MarshalJSON() ([]byte, error)
|
||||
|
||||
All() []Enum
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package interfaces
|
||||
|
||||
type OpLogger interface {
|
||||
Enum
|
||||
Render(content map[string]any) (string, error)
|
||||
Template() string
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"sync"
|
||||
"time"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
const (
|
||||
SCHEME = "sonar"
|
||||
)
|
||||
|
||||
type Client[T any] struct {
|
||||
domain string
|
||||
endpoints []string
|
||||
fn func(grpc.ClientConnInterface) T
|
||||
opts []grpc.DialOption
|
||||
|
||||
cc *grpc.ClientConn
|
||||
}
|
||||
|
||||
func (c *Client[T]) Session() T {
|
||||
return c.fn(c.cc)
|
||||
}
|
||||
|
||||
var (
|
||||
clients = &sync.Map{}
|
||||
)
|
||||
|
||||
// NewClient
|
||||
/*
|
||||
* domain => Example: sonar_search
|
||||
* endpoints => Example: []string{"sonar_search:8080", "sonar_search:80801"} or []string{"10.10.10.10:32000", "10.10.10.10:32001"}
|
||||
* fn => Example: system.NewSystemSrvClient
|
||||
* opts => Example: grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
*/
|
||||
func NewClient[T any](
|
||||
domain string,
|
||||
endpoints []string,
|
||||
fn func(grpc.ClientConnInterface) T,
|
||||
opts ...grpc.DialOption,
|
||||
) (*Client[T], error) {
|
||||
|
||||
cached, ok := clients.Load(domain)
|
||||
if ok {
|
||||
if client, ok := cached.(*Client[T]); ok {
|
||||
return client, nil
|
||||
}
|
||||
}
|
||||
|
||||
resolved := resolver.State{Addresses: make([]resolver.Address, 0)}
|
||||
|
||||
locker.Lock()
|
||||
for _, item := range endpoints {
|
||||
resolved.Addresses = append(resolved.Addresses, resolver.Address{Addr: item})
|
||||
}
|
||||
ips[domain] = resolved
|
||||
locker.Unlock()
|
||||
|
||||
fullAddress := fmt.Sprintf("%s://%s", SCHEME, domain)
|
||||
|
||||
opts = append(opts,
|
||||
grpc.WithResolvers(myBuilder),
|
||||
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
|
||||
grpc.WithChainUnaryInterceptor(retryInterceptor(3, 3*time.Second)),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
|
||||
conn, err := grpc.DialContext(
|
||||
tool.Timeout(3),
|
||||
fullAddress,
|
||||
opts...,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &Client[T]{
|
||||
cc: conn,
|
||||
fn: fn,
|
||||
}
|
||||
|
||||
clients.Store(domain, c)
|
||||
|
||||
return c, nil
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
scheme = "bifrost"
|
||||
)
|
||||
|
||||
type CustomBuilder struct{}
|
||||
|
||||
func (cb *CustomBuilder) Scheme() string {
|
||||
return scheme
|
||||
}
|
||||
|
||||
func (cb *CustomBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
|
||||
cr := &customResolver{
|
||||
cc: cc,
|
||||
target: target,
|
||||
}
|
||||
|
||||
cr.ResolveNow(resolver.ResolveNowOptions{})
|
||||
|
||||
return cr, nil
|
||||
}
|
||||
|
||||
type customResolver struct {
|
||||
sync.Mutex
|
||||
target resolver.Target
|
||||
cc resolver.ClientConn
|
||||
ips map[string]string
|
||||
}
|
||||
|
||||
func (cr *customResolver) ResolveNow(o resolver.ResolveNowOptions) {
|
||||
var (
|
||||
addrs = make([]resolver.Address, 0)
|
||||
hp []string
|
||||
)
|
||||
|
||||
cr.Lock()
|
||||
defer cr.Unlock()
|
||||
|
||||
if hp = strings.Split(cr.target.URL.Host, ":"); len(hp) >= 2 {
|
||||
if ip, ok := pool[hp[0]]; ok {
|
||||
addr := fmt.Sprintf("%s:%s", ip, hp[1])
|
||||
addrs = append(addrs, resolver.Address{Addr: addr})
|
||||
}
|
||||
}
|
||||
|
||||
_ = cr.cc.UpdateState(resolver.State{Addresses: addrs})
|
||||
}
|
||||
|
||||
func (cr *customResolver) Close() {}
|
||||
|
||||
var (
|
||||
cb = &CustomBuilder{}
|
||||
pool = make(map[string]string)
|
||||
)
|
||||
|
||||
func init() {
|
||||
resolver.Register(cb)
|
||||
}
|
||||
|
||||
type CustomDomain struct {
|
||||
Domain string
|
||||
IP string
|
||||
}
|
||||
|
||||
func NewCustomBuilder(cds ...CustomDomain) resolver.Builder {
|
||||
locker.Lock()
|
||||
defer locker.Unlock()
|
||||
|
||||
for _, cd := range cds {
|
||||
pool[cd.Domain] = cd.IP
|
||||
}
|
||||
|
||||
return cb
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"sync"
|
||||
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
type Builder struct{}
|
||||
|
||||
func (b *Builder) Scheme() string {
|
||||
return SCHEME
|
||||
}
|
||||
|
||||
func (b *Builder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
|
||||
cr := &Resolver{
|
||||
cc: cc,
|
||||
target: target,
|
||||
}
|
||||
|
||||
cr.ResolveNow(resolver.ResolveNowOptions{})
|
||||
|
||||
return cr, nil
|
||||
}
|
||||
|
||||
type Resolver struct {
|
||||
target resolver.Target
|
||||
cc resolver.ClientConn
|
||||
}
|
||||
|
||||
func (r *Resolver) ResolveNow(o resolver.ResolveNowOptions) {
|
||||
logrus.Tracef("resolve_v2 ResolveNow => target: %s, %v", r.target.URL.Host, ips)
|
||||
_ = r.cc.UpdateState(ips[r.target.URL.Host])
|
||||
}
|
||||
|
||||
func (cr *Resolver) Close() {}
|
||||
|
||||
var (
|
||||
locker = &sync.Mutex{}
|
||||
myBuilder = &Builder{}
|
||||
ips = map[string]resolver.State{}
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func retryInterceptor(maxAttempt int, interval time.Duration) grpc.UnaryClientInterceptor {
|
||||
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
|
||||
if maxAttempt == 0 {
|
||||
return invoker(ctx, method, req, reply, cc, opts...)
|
||||
}
|
||||
|
||||
duration := interval
|
||||
|
||||
for attempt := 1; attempt <= maxAttempt; attempt++ {
|
||||
|
||||
if err := invoker(ctx, method, req, reply, cc, opts...); err != nil {
|
||||
if s, ok := status.FromError(err); ok && s.Code() == codes.Unavailable {
|
||||
logrus.Debugf("Connection failed err: %v, retry %d after %fs", err, attempt, duration.Seconds())
|
||||
|
||||
time.Sleep(duration)
|
||||
duration *= 2
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil // 请求成功,不需要重试
|
||||
}
|
||||
|
||||
return fmt.Errorf("max retry attempts reached")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/loveuer/nf"
|
||||
ulog "github.com/loveuer/nf/nft/log"
|
||||
"ultone/internal/opt"
|
||||
)
|
||||
|
||||
func _mix(c *nf.Ctx, msg string) string {
|
||||
if c == nil {
|
||||
return msg
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v | %s", c.Locals(opt.LocalShortTraceKey), msg)
|
||||
}
|
||||
|
||||
func Debug(c *nf.Ctx, msg string, data ...any) {
|
||||
ulog.Debug(_mix(c, msg), data...)
|
||||
}
|
||||
|
||||
func Info(c *nf.Ctx, msg string, data ...any) {
|
||||
ulog.Info(_mix(c, msg), data...)
|
||||
}
|
||||
|
||||
func Warn(c *nf.Ctx, msg string, data ...any) {
|
||||
ulog.Warn(_mix(c, msg), data...)
|
||||
}
|
||||
|
||||
func Error(c *nf.Ctx, msg string, data ...any) {
|
||||
ulog.Error(_mix(c, msg), data...)
|
||||
}
|
||||
|
||||
func Panic(c *nf.Ctx, msg string, data ...any) {
|
||||
ulog.Panic(_mix(c, msg), data...)
|
||||
}
|
||||
|
||||
func Fatal(c *nf.Ctx, msg string, data ...any) {
|
||||
ulog.Fatal(_mix(c, msg), data...)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"gitea.com/taozitaozi/gredis"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"strings"
|
||||
"ultone/internal/controller"
|
||||
"ultone/internal/log"
|
||||
"ultone/internal/opt"
|
||||
)
|
||||
|
||||
var (
|
||||
tokenFunc = func(c *nf.Ctx) string {
|
||||
token := c.Get("Authorization")
|
||||
if token == "" {
|
||||
token = c.Cookies(opt.CookieName)
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
)
|
||||
|
||||
func NewAuth() nf.HandlerFunc {
|
||||
return func(c *nf.Ctx) error {
|
||||
token := tokenFunc(c)
|
||||
|
||||
if token = strings.TrimPrefix(token, "Bearer "); token == "" {
|
||||
return resp.Resp401(c, token)
|
||||
}
|
||||
|
||||
log.Debug(c, "middleware.NewAuth: token=%s", token)
|
||||
|
||||
target, err := controller.UserController.GetUserByToken(c, token)
|
||||
if err != nil {
|
||||
log.Error(c, "middleware.NewAuth: get user by token=%s err=%v", token, err)
|
||||
if errors.Is(err, redis.Nil) || errors.Is(err, gredis.ErrKeyNotFound) {
|
||||
return resp.Resp401(c, err)
|
||||
}
|
||||
|
||||
return resp.RespError(c, err)
|
||||
}
|
||||
|
||||
if err = target.IsValid(true); err != nil {
|
||||
return resp.Resp401(c, err.Error(), err.Error())
|
||||
}
|
||||
|
||||
c.Locals("user", target)
|
||||
c.Locals("token", token)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-critters-container>
|
||||
<head><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<meta charset="utf-8">
|
||||
<title>Front</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<style>@font-face{font-family:'Roboto';font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fCRc4EsA.woff2) format('woff2');unicode-range:U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face{font-family:'Roboto';font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fABc4EsA.woff2) format('woff2');unicode-range:U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face{font-family:'Roboto';font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fCBc4EsA.woff2) format('woff2');unicode-range:U+1F00-1FFF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fBxc4EsA.woff2) format('woff2');unicode-range:U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fCxc4EsA.woff2) format('woff2');unicode-range:U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face{font-family:'Roboto';font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fChc4EsA.woff2) format('woff2');unicode-range:U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fBBc4.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKOzY.woff2) format('woff2');unicode-range:U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu5mxKOzY.woff2) format('woff2');unicode-range:U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu7mxKOzY.woff2) format('woff2');unicode-range:U+1F00-1FFF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4WxKOzY.woff2) format('woff2');unicode-range:U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu7WxKOzY.woff2) format('woff2');unicode-range:U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu7GxKOzY.woff2) format('woff2');unicode-range:U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxK.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face{font-family:'Roboto';font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmEU9fCRc4EsA.woff2) format('woff2');unicode-range:U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face{font-family:'Roboto';font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmEU9fABc4EsA.woff2) format('woff2');unicode-range:U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face{font-family:'Roboto';font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmEU9fCBc4EsA.woff2) format('woff2');unicode-range:U+1F00-1FFF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmEU9fBxc4EsA.woff2) format('woff2');unicode-ranLine truncated
|
||||
<style>@font-face{font-family:'Material Icons';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/materialicons/v142/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2) format('woff2');}.material-icons{font-family:'Material Icons';font-weight:normal;font-style:normal;font-size:24px;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased;}</style>
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
<meta name="theme-color" content="#1976d2">
|
||||
<style>html{--mat-ripple-color:rgba(0, 0, 0, .1)}html{--mat-option-selected-state-label-text-color:#3f51b5;--mat-option-label-text-color:rgba(0, 0, 0, .87);--mat-option-hover-state-layer-color:rgba(0, 0, 0, .04);--mat-option-focus-state-layer-color:rgba(0, 0, 0, .04);--mat-option-selected-state-layer-color:rgba(0, 0, 0, .04)}html{--mat-optgroup-label-text-color:rgba(0, 0, 0, .87)}html{--mat-full-pseudo-checkbox-selected-icon-color:#ff4081;--mat-full-pseudo-checkbox-selected-checkmark-color:#fafafa;--mat-full-pseudo-checkbox-unselected-icon-color:rgba(0, 0, 0, .54);--mat-full-pseudo-checkbox-disabled-selected-checkmark-color:#fafafa;--mat-full-pseudo-checkbox-disabled-unselected-icon-color:#b0b0b0;--mat-full-pseudo-checkbox-disabled-selected-icon-color:#b0b0b0;--mat-minimal-pseudo-checkbox-selected-checkmark-color:#ff4081;--mat-minimal-pseudo-checkbox-disabled-selected-checkmark-color:#b0b0b0}html{--mat-app-background-color:#fafafa;--mat-app-text-color:rgba(0, 0, 0, .87)}html{--mat-option-label-text-font:Roboto, sans-serif;--mat-option-label-text-line-height:24px;--mat-option-label-text-size:16px;--mat-option-label-text-tracking:.03125em;--mat-option-label-text-weight:400}html{--mat-optgroup-label-text-font:Roboto, sans-serif;--mat-optgroup-label-text-line-height:24px;--mat-optgroup-label-text-size:16px;--mat-optgroup-label-text-tracking:.03125em;--mat-optgroup-label-text-weight:400}html{--mdc-elevated-card-container-shape:4px;--mdc-outlined-card-container-shape:4px;--mdc-outlined-card-outline-width:1px}html{--mdc-elevated-card-container-color:white;--mdc-elevated-card-container-elevation:0px 2px 1px -1px rgba(0, 0, 0, .2), 0px 1px 1px 0px rgba(0, 0, 0, .14), 0px 1px 3px 0px rgba(0, 0, 0, .12);--mdc-outlined-card-container-color:white;--mdc-outlined-card-outline-color:rgba(0, 0, 0, .12);--mdc-outlined-card-container-elevation:0px 0px 0px 0px rgba(0, 0, 0, .2), 0px 0px 0px 0px rgba(0, 0, 0, .14), 0px 0px 0px 0px rgba(0, 0, 0, .12);--mat-card-subtitle-text-color:rgba(0, 0, 0, .54)}html{--mat-card-title-text-font:Roboto, sans-serif;--mat-card-title-text-line-height:32px;--mat-card-title-text-size:20px;--mat-card-title-text-tracking:.0125em;--mat-card-title-text-weight:500;--mat-card-subtitle-text-font:Roboto, sans-serif;--mat-card-subtitle-text-line-height:22px;--mat-card-subtitle-text-size:14px;--mat-card-subtitle-text-tracking:.0071428571em;--mat-card-subtitle-text-weight:500}html{--mdc-linear-progress-active-indicator-height:4px;--mdc-linear-progress-track-height:4px;--mdc-linear-progress-track-shape:0}html{--mdc-plain-tooltip-container-shape:4px;--mdc-plain-tooltip-supporting-text-line-height:16px}html{--mdc-plain-tooltip-container-color:#616161;--mdc-plain-tooltip-supporting-text-color:#fff}html{--mdc-plain-tooltip-supporting-text-font:Roboto, sans-serif;--mdc-plain-tooltip-supporting-text-size:12px;--mdc-plain-tooltip-supporting-text-weight:400;--mdc-plain-tooltip-supporting-text-tracking:.0333333333em}html{--mdc-filled-text-field-active-indicator-height:1px;--mdc-filled-text-field-focus-active-indicator-height:2px;--mdc-filled-text-field-container-shape:4px;--mdc-outlined-text-field-outline-width:1px;--mdc-outlined-text-field-focus-outline-width:2px;--mdc-outlined-text-field-container-shape:4px}html{--mdc-filled-text-field-caret-color:#3f51b5;--mdc-filled-text-field-focus-active-indicator-color:#3f51b5;--mdc-filled-text-field-focus-label-text-color:rgba(63, 81, 181, .87);--mdc-filled-text-field-container-color:whitesmoke;--mdc-filled-text-field-disabled-container-color:#fafafa;--mdc-filled-text-field-label-text-color:rgba(0, 0, 0, .6);--mdc-filled-text-field-hover-label-text-color:rgba(0, 0, 0, .6);--mdc-filled-text-field-disabled-label-text-color:rgba(0, 0, 0, .38);--mdc-filled-text-field-input-text-color:rgba(0, 0, 0, .87);--mdc-filled-text-field-disabled-input-text-color:rgba(0, 0, 0, .38);--mdc-filled-text-field-input-text-placeholder-color:rgba(0, 0, 0, .6);--mdc-filled-text-field-error-hover-label-text-color:#f44336;--mdc-filled-text-field-error-focus-label-text-color:#f44336;--mdc-filled-text-field-error-label-text-color:#f44336;--mdc-filled-text-field-error-caret-color:#f44336;--mdc-filled-text-field-active-indicator-color:rgba(0, 0, 0, .42);--mdc-filled-text-field-disabled-active-indicator-color:rgba(0, 0, 0, .06);--mdc-filled-text-field-hover-active-indicator-color:rgba(0, 0, 0, .87);--mdc-filled-text-field-error-active-indicator-color:#f44336;--mdc-filled-text-field-error-focus-active-indicator-color:#f44336;--mdc-filled-text-field-error-hover-active-indicator-color:#f44336;--mdc-outlined-text-field-caret-color:#3f51b5;--mdc-outlined-text-field-focus-outline-color:#3f51b5;--mdc-outlined-text-field-focus-label-text-color:rgba(63, 81, 181, .87);--mdc-outlined-text-field-label-text-color:rgba(0, 0, 0, .6);--mdc-outlined-text-field-hover-label-text-color:rgba(0, 0, 0, .6);--mdc-outlined-text-field-disabled-label-text-color:rgba(0, 0, 0, .38);--mdc-outlined-text-field-input-text-color:rgba(0, 0, 0Line truncated
|
||||
<body class="mat-typography">
|
||||
<app-root></app-root>
|
||||
<noscript>Please enable JavaScript to continue using this application.</noscript>
|
||||
<script src="polyfills-TMVK3KFA.js" type="module"></script><script src="main-CLMZMBYY.js" type="module"></script></body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
var xI=Object.defineProperty,DI=Object.defineProperties;var CI=Object.getOwnPropertyDescriptors;var Gl=Object.getOwnPropertySymbols;var Tb=Object.prototype.hasOwnProperty,Ab=Object.prototype.propertyIsEnumerable;var Mb=(t,e,i)=>e in t?xI(t,e,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[e]=i,w=(t,e)=>{for(var i in e||={})Tb.call(e,i)&&Mb(t,i,e[i]);if(Gl)for(var i of Gl(e))Ab.call(e,i)&&Mb(t,i,e[i]);return t},Z=(t,e)=>DI(t,CI(e));var Xf=(t,e)=>{var i={};for(var n in t)Tb.call(t,n)&&e.indexOf(n)<0&&(i[n]=t[n]);if(t!=null&&Gl)for(var n of Gl(t))e.indexOf(n)<0&&Ab.call(t,n)&&(i[n]=t[n]);return i};var ql=(t,e,i)=>new Promise((n,r)=>{var o=l=>{try{a(i.next(l))}catch(c){r(c)}},s=l=>{try{a(i.throw(l))}catch(c){r(c)}},a=l=>l.done?n(l.value):Promise.resolve(l.value).then(o,s);a((i=i.apply(t,e)).next())});function EI(t,e){return Object.is(t,e)}var pt=null,Qs=!1,Yl=1,$i=Symbol("SIGNAL");function Ce(t){let e=pt;return pt=t,e}function II(){return Qs}var Zl={version:0,lastCleanEpoch:0,dirty:!1,producerNode:void 0,producerLastReadVersion:void 0,producerIndexOfThis:void 0,nextProducerIndex:0,liveConsumerNode:void 0,liveConsumerIndexOfThis:void 0,consumerAllowSignalWrites:!1,consumerIsAlwaysLive:!1,producerMustRecompute:()=>!1,producerRecomputeValue:()=>{},consumerMarkedDirty:()=>{},consumerOnSignalRead:()=>{}};function Rb(t){if(Qs)throw new Error("");if(pt===null)return;pt.consumerOnSignalRead(t);let e=pt.nextProducerIndex++;if(Xl(pt),e<pt.producerNode.length&&pt.producerNode[e]!==t&&Ks(pt)){let i=pt.producerNode[e];Kl(i,pt.producerIndexOfThis[e])}pt.producerNode[e]!==t&&(pt.producerNode[e]=t,pt.producerIndexOfThis[e]=Ks(pt)?Fb(t,pt,e):0),pt.producerLastReadVersion[e]=t.version}function SI(){Yl++}function MI(t){if(!(Ks(t)&&!t.dirty)&&!(!t.dirty&&t.lastCleanEpoch===Yl)){if(!t.producerMustRecompute(t)&&!Ql(t)){t.dirty=!1,t.lastCleanEpoch=Yl;return}t.producerRecomputeValue(t),t.dirty=!1,t.lastCleanEpoch=Yl}}function Ob(t){if(t.liveConsumerNode===void 0)return;let e=Qs;Qs=!0;try{for(let i of t.liveConsumerNode)i.dirty||Nb(i)}finally{Qs=e}}function kb(){return pt?.consumerAllowSignalWrites!==!1}function Nb(t){t.dirty=!0,Ob(t),t.consumerMarkedDirty?.(t)}function eh(t){return t&&(t.nextProducerIndex=0),Ce(t)}function th(t,e){if(Ce(e),!(!t||t.producerNode===void 0||t.producerIndexOfThis===void 0||t.producerLastReadVersion===void 0)){if(Ks(t))for(let i=t.nextProducerIndex;i<t.producerNode.length;i++)Kl(t.producerNode[i],t.producerIndexOfThis[i]);for(;t.producerNode.length>t.nextProducerIndex;)t.producerNode.pop(),t.producerLastReadVersion.pop(),t.producerIndexOfThis.pop()}}function Ql(t){Xl(t);for(let e=0;e<t.producerNode.length;e++){let i=t.producerNode[e],n=t.producerLastReadVersion[e];if(n!==i.version||(MI(i),n!==i.version))return!0}return!1}function nh(t){if(Xl(t),Ks(t))for(let e=0;e<t.producerNode.length;e++)Kl(t.producerNode[e],t.producerIndexOfThis[e]);t.producerNode.length=t.producerLastReadVersion.length=t.producerIndexOfThis.length=0,t.liveConsumerNode&&(t.liveConsumerNode.length=t.liveConsumerIndexOfThis.length=0)}function Fb(t,e,i){if(Pb(t),t.liveConsumerNode.length===0&&Lb(t))for(let n=0;n<t.producerNode.length;n++)t.producerIndexOfThis[n]=Fb(t.producerNode[n],t,n);return t.liveConsumerIndexOfThis.push(i),t.liveConsumerNode.push(e)-1}function Kl(t,e){if(Pb(t),t.liveConsumerNode.length===1&&Lb(t))for(let n=0;n<t.producerNode.length;n++)Kl(t.producerNode[n],t.producerIndexOfThis[n]);let i=t.liveConsumerNode.length-1;if(t.liveConsumerNode[e]=t.liveConsumerNode[i],t.liveConsumerIndexOfThis[e]=t.liveConsumerIndexOfThis[i],t.liveConsumerNode.length--,t.liveConsumerIndexOfThis.length--,e<t.liveConsumerNode.length){let n=t.liveConsumerIndexOfThis[e],r=t.liveConsumerNode[e];Xl(r),r.producerIndexOfThis[n]=e}}function Ks(t){return t.consumerIsAlwaysLive||(t?.liveConsumerNode?.length??0)>0}function Xl(t){t.producerNode??=[],t.producerIndexOfThis??=[],t.producerLastReadVersion??=[]}function Pb(t){t.liveConsumerNode??=[],t.liveConsumerIndexOfThis??=[]}function Lb(t){return t.producerNode!==void 0}function TI(){throw new Error}var jb=TI;function Vb(){jb()}function Bb(t){jb=t}var AI=null;function zb(t){let e=Object.create(Hb);e.value=t;let i=()=>(Rb(e),e.value);return i[$i]=e,i}function ih(t,e){kb()||Vb(),t.equal(t.value,e)||(t.value=e,RI(t))}function Ub(t,e){kb()||Vb(),ih(t,e(t.value))}var Hb=Z(w({},Zl),{equal:EI,value:void 0});function RI(t){t.version++,SI(),Ob(t),AI?.()}function $b(t,e,i){let n=Object.create(OI);i&&(n.consumerAllowSignalWrites=!0),n.fn=t,n.schedule=e;let r=l=>{n.cleanupFn=l};function o(l){return l.fn===null&&l.schedule===null}function s(l){o(l)||(nh(l),l.cleanupFn(),l.fn=null,l.schedule=null,l.cleanupFn=Jf)}let a=()=>{if(n.fn===null)return;if(II())throw new Error("Schedulers cannot synchronously execute watches while scheduling.");if(n.dirty=!1,n.hasRun&&!Ql(n))return;n.hasRun=!0;let l=eh(n);try{n.cleanupFn(),n.cleanupFn=Jf,n.fn(r)}finally{th(n,l)}};return n.ref={notify:()=>Nb(n),run:a,cleanup:()=>n.cleanuLine truncated
|
||||
${i.map((n,r)=>`${r+1}) ${n.toString()}`).join(`
|
||||
`)}`:"",this.name="UnsubscriptionError",this.errors=i});function Dr(t,e){if(t){let i=t.indexOf(e);0<=i&&t.splice(i,1)}}var ge=class t{constructor(e){this.initialTeardown=e,this.closed=!1,this._parentage=null,this._finalizers=null}unsubscribe(){let e;if(!this.closed){this.closed=!0;let{_parentage:i}=this;if(i)if(this._parentage=null,Array.isArray(i))for(let o of i)o.remove(this);else i.remove(this);let{initialTeardown:n}=this;if(H(n))try{n()}catch(o){e=o instanceof Jl?o.errors:[o]}let{_finalizers:r}=this;if(r){this._finalizers=null;for(let o of r)try{Wb(o)}catch(s){e=e??[],s instanceof Jl?e=[...e,...s.errors]:e.push(s)}}if(e)throw new Jl(e)}}add(e){var i;if(e&&e!==this)if(this.closed)Wb(e);else{if(e instanceof t){if(e.closed||e._hasParent(this))return;e._addParent(this)}(this._finalizers=(i=this._finalizers)!==null&&i!==void 0?i:[]).push(e)}}_hasParent(e){let{_parentage:i}=this;return i===e||Array.isArray(i)&&i.includes(e)}_addParent(e){let{_parentage:i}=this;this._parentage=Array.isArray(i)?(i.push(e),i):i?[i,e]:e}_removeParent(e){let{_parentage:i}=this;i===e?this._parentage=null:Array.isArray(i)&&Dr(i,e)}remove(e){let{_finalizers:i}=this;i&&Dr(i,e),e instanceof t&&e._removeParent(this)}};ge.EMPTY=(()=>{let t=new ge;return t.closed=!0,t})();var rh=ge.EMPTY;function ec(t){return t instanceof ge||t&&"closed"in t&&H(t.remove)&&H(t.add)&&H(t.unsubscribe)}function Wb(t){H(t)?t():t.unsubscribe()}var wn={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var Co={setTimeout(t,e,...i){let{delegate:n}=Co;return n?.setTimeout?n.setTimeout(t,e,...i):setTimeout(t,e,...i)},clearTimeout(t){let{delegate:e}=Co;return(e?.clearTimeout||clearTimeout)(t)},delegate:void 0};function tc(t){Co.setTimeout(()=>{let{onUnhandledError:e}=wn;if(e)e(t);else throw t})}function fi(){}var Gb=oh("C",void 0,void 0);function qb(t){return oh("E",void 0,t)}function Yb(t){return oh("N",t,void 0)}function oh(t,e,i){return{kind:t,value:e,error:i}}var Cr=null;function Eo(t){if(wn.useDeprecatedSynchronousErrorHandling){let e=!Cr;if(e&&(Cr={errorThrown:!1,error:null}),t(),e){let{errorThrown:i,error:n}=Cr;if(Cr=null,i)throw n}}else t()}function Zb(t){wn.useDeprecatedSynchronousErrorHandling&&Cr&&(Cr.errorThrown=!0,Cr.error=t)}var Er=class extends ge{constructor(e){super(),this.isStopped=!1,e?(this.destination=e,ec(e)&&e.add(this)):this.destination=FI}static create(e,i,n){return new hi(e,i,n)}next(e){this.isStopped?ah(Yb(e),this):this._next(e)}error(e){this.isStopped?ah(qb(e),this):(this.isStopped=!0,this._error(e))}complete(){this.isStopped?ah(Gb,this):(this.isStopped=!0,this._complete())}unsubscribe(){this.closed||(this.isStopped=!0,super.unsubscribe(),this.destination=null)}_next(e){this.destination.next(e)}_error(e){try{this.destination.error(e)}finally{this.unsubscribe()}}_complete(){try{this.destination.complete()}finally{this.unsubscribe()}}},kI=Function.prototype.bind;function sh(t,e){return kI.call(t,e)}var lh=class{constructor(e){this.partialObserver=e}next(e){let{partialObserver:i}=this;if(i.next)try{i.next(e)}catch(n){nc(n)}}error(e){let{partialObserver:i}=this;if(i.error)try{i.error(e)}catch(n){nc(n)}else nc(e)}complete(){let{partialObserver:e}=this;if(e.complete)try{e.complete()}catch(i){nc(i)}}},hi=class extends Er{constructor(e,i,n){super();let r;if(H(e)||!e)r={next:e??void 0,error:i??void 0,complete:n??void 0};else{let o;this&&wn.useDeprecatedNextContext?(o=Object.create(e),o.unsubscribe=()=>this.unsubscribe(),r={next:e.next&&sh(e.next,o),error:e.error&&sh(e.error,o),complete:e.complete&&sh(e.complete,o)}):r=e}this.destination=new lh(r)}};function nc(t){wn.useDeprecatedSynchronousErrorHandling?Zb(t):tc(t)}function NI(t){throw t}function ah(t,e){let{onStoppedNotification:i}=wn;i&&Co.setTimeout(()=>i(t,e))}var FI={closed:!0,next:fi,error:NI,complete:fi};var Io=typeof Symbol=="function"&&Symbol.observable||"@@observable";function Rt(t){return t}function ch(...t){return dh(t)}function dh(t){return t.length===0?Rt:t.length===1?t[0]:function(i){return t.reduce((n,r)=>r(n),i)}}var q=(()=>{class t{constructor(i){i&&(this._subscribe=i)}lift(i){let n=new t;return n.source=this,n.operator=i,n}subscribe(i,n,r){let o=LI(i)?i:new hi(i,n,r);return Eo(()=>{let{operator:s,source:a}=this;o.add(s?s.call(o,a):a?this._subscribe(o):this._trySubscribe(o))}),o}_trySubscribe(i){try{return this._subscribe(i)}catch(n){i.error(n)}}forEach(i,n){return n=Qb(n),new n((r,o)=>{let s=new hi({next:a=>{try{i(a)}catch(l){o(l),s.unsubscribe()}},error:o,complete:r});this.subscribe(s)})}_subscribe(i){var n;return(n=this.source)===null||n===void 0?void 0:n.subscribe(i)}[Io](){return this}pipe(...i){return dh(i)(this)}toPromise(i){return i=Qb(i),new i((n,r)=>{let o;this.subscribe(s=>o=s,s=>r(s),()=>n(o))})}}return t.create=e=>new t(e),t})();function Qb(t){var e;return(e=t??wn.Promise)!==null&&e!==void 0?e:Promise}function PI(t){return t&&H(t.next)&&H(t.error)&&H(t.complete)}function LI(t){returLine truncated
|
||||
`);return i===-1?e:e.substring(0,i)}function jh(t,e){return t==null||t===""?e===null?"":e:e==null||e===""?t:t+" "+e}var hS=ze({__forward_ref__:ze});function tr(t){return t.__forward_ref__=tr,t.toString=function(){return zt(this())},t}function wt(t){return my(t)?t():t}function my(t){return typeof t=="function"&&t.hasOwnProperty(hS)&&t.__forward_ref__===tr}function C(t){return{token:t.token,providedIn:t.providedIn||null,factory:t.factory,value:void 0}}function de(t){return{providers:t.providers||[],imports:t.imports||[]}}function hd(t){return D_(t,gy)||D_(t,vy)}function py(t){return hd(t)!==null}function D_(t,e){return t.hasOwnProperty(e)?t[e]:null}function mS(t){let e=t&&(t[gy]||t[vy]);return e||null}function C_(t){return t&&(t.hasOwnProperty(E_)||t.hasOwnProperty(pS))?t[E_]:null}var gy=ze({\u0275prov:ze}),E_=ze({\u0275inj:ze}),vy=ze({ngInjectableDef:ze}),pS=ze({ngInjectorDef:ze}),D=class{constructor(e,i){this._desc=e,this.ngMetadataName="InjectionToken",this.\u0275prov=void 0,typeof i=="number"?this.__NG_ELEMENT_ID__=i:i!==void 0&&(this.\u0275prov=C({token:this,providedIn:i.providedIn||"root",factory:i.factory}))}get multi(){return this}toString(){return`InjectionToken ${this._desc}`}};function by(t){return t&&!!t.\u0275providers}var gS=ze({\u0275cmp:ze}),vS=ze({\u0275dir:ze}),bS=ze({\u0275pipe:ze}),_S=ze({\u0275mod:ze}),Bc=ze({\u0275fac:ze}),la=ze({__NG_ELEMENT_ID__:ze}),I_=ze({__NG_ENV_ID__:ze});function md(t){return typeof t=="string"?t:t==null?"":String(t)}function yS(t){return typeof t=="function"?t.name||t.toString():typeof t=="object"&&t!=null&&typeof t.type=="function"?t.type.name||t.type.toString():md(t)}function wS(t,e){let i=e?`. Dependency path: ${e.join(" > ")} > ${t}`:"";throw new S(-200,t)}function Wm(t,e){throw new S(-201,!1)}var we=function(t){return t[t.Default=0]="Default",t[t.Host=1]="Host",t[t.Self=2]="Self",t[t.SkipSelf=4]="SkipSelf",t[t.Optional=8]="Optional",t}(we||{}),Vh;function _y(){return Vh}function Vt(t){let e=Vh;return Vh=t,e}function yy(t,e,i){let n=hd(t);if(n&&n.providedIn=="root")return n.value===void 0?n.value=n.factory():n.value;if(i&we.Optional)return null;if(e!==void 0)return e;Wm(t,"Injector")}var xS={},ca=xS,Bh="__NG_DI_FLAG__",zc="ngTempTokenPath",DS="ngTokenPath",CS=/\n/gm,ES="\u0275",S_="__source",Bo;function IS(){return Bo}function qi(t){let e=Bo;return Bo=t,e}function SS(t,e=we.Default){if(Bo===void 0)throw new S(-203,!1);return Bo===null?yy(t,void 0,e):Bo.get(t,e&we.Optional?null:void 0,e)}function g(t,e=we.Default){return(_y()||SS)(wt(t),e)}function y(t,e=we.Default){return g(t,pd(e))}function pd(t){return typeof t>"u"||typeof t=="number"?t:0|(t.optional&&8)|(t.host&&1)|(t.self&&2)|(t.skipSelf&&4)}function zh(t){let e=[];for(let i=0;i<t.length;i++){let n=wt(t[i]);if(Array.isArray(n)){if(n.length===0)throw new S(900,!1);let r,o=we.Default;for(let s=0;s<n.length;s++){let a=n[s],l=MS(a);typeof l=="number"?l===-1?r=a.token:o|=l:r=a}e.push(g(r,o))}else e.push(g(n))}return e}function wy(t,e){return t[Bh]=e,t.prototype[Bh]=e,t}function MS(t){return t[Bh]}function TS(t,e,i,n){let r=t[zc];throw e[S_]&&r.unshift(e[S_]),t.message=AS(`
|
||||
`+t.message,r,i,n),t[DS]=r,t[zc]=null,t}function AS(t,e,i,n=null){t=t&&t.charAt(0)===`
|
||||
`&&t.charAt(1)==ES?t.slice(2):t;let r=zt(e);if(Array.isArray(e))r=e.map(zt).join(" -> ");else if(typeof e=="object"){let o=[];for(let s in e)if(e.hasOwnProperty(s)){let a=e[s];o.push(s+":"+(typeof a=="string"?JSON.stringify(a):zt(a)))}r=`{${o.join(", ")}}`}return`${i}${n?"("+n+")":""}[${r}]: ${t.replace(CS,`
|
||||
`)}`}var ya=wy(hy("Optional"),8);var gd=wy(hy("SkipSelf"),4);function Fr(t,e){let i=t.hasOwnProperty(Bc);return i?t[Bc]:null}function RS(t,e,i){if(t.length!==e.length)return!1;for(let n=0;n<t.length;n++){let r=t[n],o=e[n];if(i&&(r=i(r),o=i(o)),o!==r)return!1}return!0}function OS(t){return t.flat(Number.POSITIVE_INFINITY)}function Gm(t,e){t.forEach(i=>Array.isArray(i)?Gm(i,e):e(i))}function xy(t,e,i){e>=t.length?t.push(i):t.splice(e,0,i)}function Uc(t,e){return e>=t.length-1?t.pop():t.splice(e,1)[0]}function kS(t,e){let i=[];for(let n=0;n<t;n++)i.push(e);return i}function NS(t,e,i,n){let r=t.length;if(r==e)t.push(i,n);else if(r===1)t.push(n,t[0]),t[0]=i;else{for(r--,t.push(t[r-1],t[r]);r>e;){let o=r-2;t[r]=t[o],r--}t[e]=i,t[e+1]=n}}function vd(t,e,i){let n=wa(t,e);return n>=0?t[n|1]=i:(n=~n,NS(t,n,e,i)),n}function xh(t,e){let i=wa(t,e);if(i>=0)return t[i|1]}function wa(t,e){return FS(t,e,1)}function FS(t,e,i){let n=0,r=t.length>>i;for(;r!==n;){let o=n+(r-n>>1),s=t[o<<i];if(e===s)return o<<i;s>e?r=o:n=o+1}return~(r<<i)}var pi={},Bt=[],Uo=new D(""),Dy=new D("",-1),Cy=new D(""),Hc=class{get(e,i=ca){if(i===ca){let n=new Error(`NullInjectorError: No provider for ${zt(e)}!`);throw n.name="NullInjectorError",n}return i}},Ey=function(t){return t[t.OnPush=0]="OnPush",t[t.Default=1]="Default",t}(Ey||{}),Yn=function(t){return t[t.Emulated=0]="Emulated",t[t.None=2]="None",t[t.ShadowDom=3]="ShadowDom",t}(Yn||{}),Ho=function(t){return t[t.None=0]="None",t[t.SignalBased=1]="SignalBased",t[t.HasDecoratorInputTransform=2]="HasDecoratorInputTransform",t}(Ho||{});function PS(t,e,i){let n=t.length;for(;;){let r=t.indexOf(e,i);if(r===-1)return r;if(r===0||t.charCodeAt(r-1)<=32){let o=e.length;if(r+o===n||t.charCodeAt(r+o)<=32)return r}i=r+1}}function Uh(t,e,i){let n=0;for(;n<i.length;){let r=i[n];if(typeof r=="number"){if(r!==0)break;n++;let o=i[n++],s=i[n++],a=i[n++];t.setAttribute(e,s,a,o)}else{let o=r,s=i[++n];LS(o)?t.setProperty(e,o,s):t.setAttribute(e,o,s),n++}}return n}function Iy(t){return t===3||t===4||t===6}function LS(t){return t.charCodeAt(0)===64}function da(t,e){if(!(e===null||e.length===0))if(t===null||t.length===0)t=e.slice();else{let i=-1;for(let n=0;n<e.length;n++){let r=e[n];typeof r=="number"?i=r:i===0||(i===-1||i===2?M_(t,i,r,null,e[++n]):M_(t,i,r,null,null))}}return t}function M_(t,e,i,n,r){let o=0,s=t.length;if(e===-1)s=-1;else for(;o<t.length;){let a=t[o++];if(typeof a=="number"){if(a===e){s=-1;break}else if(a>e){s=o-1;break}}}for(;o<t.length;){let a=t[o];if(typeof a=="number")break;if(a===i){if(n===null){r!==null&&(t[o+1]=r);return}else if(n===t[o+1]){t[o+2]=r;return}}o++,n!==null&&o++,r!==null&&o++}s!==-1&&(t.splice(s,0,e),o=s+1),t.splice(o++,0,i),n!==null&&t.splice(o++,0,n),r!==null&&t.splice(o++,0,r)}var Sy="ng-template";function jS(t,e,i,n){let r=0;if(n){for(;r<e.length&&typeof e[r]=="string";r+=2)if(e[r]==="class"&&PS(e[r+1].toLowerCase(),i,0)!==-1)return!0}else if(qm(t))return!1;if(r=e.indexOf(1,r),r>-1){let o;for(;++r<e.length&&typeof(o=e[r])=="string";)if(o.toLowerCase()===i)return!0}return!1}function qm(t){return t.type===4&&t.value!==Sy}function VS(t,e,i){let n=t.type===4&&!i?Sy:t.value;return e===n}function BS(t,e,i){let n=4,r=t.attrs,o=r!==null?HS(r):0,s=!1;for(let a=0;a<e.length;a++){let l=e[a];if(typeof l=="number"){if(!s&&!xn(n)&&!xn(l))return!1;if(s&&xn(l))continue;s=!1,n=l|n&1;continue}if(!s)if(n&4){if(n=2|n&1,l!==""&&!VS(t,l,i)||l===""&&e.length===1){if(xn(n))return!1;s=!0}}else if(n&8){if(r===null||!jS(t,r,l,i)){if(xn(n))return!1;s=!0}}else{let c=e[++a],d=zS(l,r,qm(t),i);if(d===-1){if(xn(n))return!1;s=!0;continue}if(c!==""){let u;if(d>o?u="":u=r[d+1].toLowerCase(),n&2&&c!==u){if(xn(n))return!1;s=!0}}}}return xn(n)||s}function xn(t){return(t&1)===0}function zS(t,e,i,n){if(e===null)return-1;let r=0;if(n||!i){let o=!1;for(;r<e.length;){let s=e[r];if(s===t)return r;if(s===3||s===6)o=!0;else if(s===1||s===2){let a=e[++r];for(;typeof a=="string";)a=e[++r];continue}else{if(s===4)break;if(s===0){r+=4;continue}}r+=o?1:2}return-1}else return $S(e,t)}function My(t,e,i=!1){for(let n=0;n<e.length;n++)if(BS(t,e[n],i))return!0;return!1}function US(t){let e=t.attrs;if(e!=null){let i=e.indexOf(5);if(!(i&1))return e[i+1]}return null}function HS(t){for(let e=0;e<t.length;e++){let i=t[e];if(Iy(i))return e}return t.length}function $S(t,e){let i=t.indexOf(4);if(i>-1)for(i++;i<t.length;){let n=t[i];if(typeof n=="number")return-1;if(n===e)return i;i++}return-1}function WS(t,e){e:for(let i=0;i<e.length;i++){let n=e[i];if(t.length===n.length){for(let r=0;r<t.length;r++)if(t[r]!==n[r])continue e;return!0}}return!1}function T_(t,e){return t?":not("+e.trim()+")":e}function GS(t){let e=t[0],i=1,n=2,r="",o=!1;for(;i<t.length;){let s=t[i];if(typeof s=="string")if(n&2){let a=t[++i];r+="["+s+(a.length>0?'="'+a+'"':"")+"]"}else n&8?r+="."+s:n&4&&(r+=" "+s);else r!==""&&!xn(s)&&(e+=T_(o,r),r=""),n=s,o=o||!xn(n);i++}return r!==""&&(e+=T_(o,r)),e}function qS(t){return t.map(GS).join(",")}function YS(t){let e=[],i=[],n=1,Line truncated
|
||||
`).forEach(i=>{let n=i.indexOf(":");if(n>0){let r=i.slice(0,n),o=r.toLowerCase(),s=i.slice(n+1).trim();this.maybeSetNormalizedName(r,o),this.headers.has(o)?this.headers.get(o).push(s):this.headers.set(o,[s])}})}:typeof Headers<"u"&&e instanceof Headers?(this.headers=new Map,e.forEach((i,n)=>{this.setHeaderEntries(n,i)})):this.lazyInit=()=>{this.headers=new Map,Object.entries(e).forEach(([i,n])=>{this.setHeaderEntries(i,n)})}:this.headers=new Map}has(e){return this.init(),this.headers.has(e.toLowerCase())}get(e){this.init();let i=this.headers.get(e.toLowerCase());return i&&i.length>0?i[0]:null}keys(){return this.init(),Array.from(this.normalizedNames.values())}getAll(e){return this.init(),this.headers.get(e.toLowerCase())||null}append(e,i){return this.clone({name:e,value:i,op:"a"})}set(e,i){return this.clone({name:e,value:i,op:"s"})}delete(e,i){return this.clone({name:e,value:i,op:"d"})}maybeSetNormalizedName(e,i){this.normalizedNames.has(i)||this.normalizedNames.set(i,e)}init(){this.lazyInit&&(this.lazyInit instanceof t?this.copyFrom(this.lazyInit):this.lazyInit(),this.lazyInit=null,this.lazyUpdate&&(this.lazyUpdate.forEach(e=>this.applyUpdate(e)),this.lazyUpdate=null))}copyFrom(e){e.init(),Array.from(e.headers.keys()).forEach(i=>{this.headers.set(i,e.headers.get(i)),this.normalizedNames.set(i,e.normalizedNames.get(i))})}clone(e){let i=new t;return i.lazyInit=this.lazyInit&&this.lazyInit instanceof t?this.lazyInit:this,i.lazyUpdate=(this.lazyUpdate||[]).concat([e]),i}applyUpdate(e){let i=e.name.toLowerCase();switch(e.op){case"a":case"s":let n=e.value;if(typeof n=="string"&&(n=[n]),n.length===0)return;this.maybeSetNormalizedName(e.name,i);let r=(e.op==="a"?this.headers.get(i):void 0)||[];r.push(...n),this.headers.set(i,r);break;case"d":let o=e.value;if(!o)this.headers.delete(i),this.normalizedNames.delete(i);else{let s=this.headers.get(i);if(!s)return;s=s.filter(a=>o.indexOf(a)===-1),s.length===0?(this.headers.delete(i),this.normalizedNames.delete(i)):this.headers.set(i,s)}break}}setHeaderEntries(e,i){let n=(Array.isArray(i)?i:[i]).map(o=>o.toString()),r=e.toLowerCase();this.headers.set(r,n),this.maybeSetNormalizedName(e,r)}forEach(e){this.init(),Array.from(this.normalizedNames.keys()).forEach(i=>e(this.normalizedNames.get(i),this.headers.get(i)))}};var Up=class{encodeKey(e){return vx(e)}encodeValue(e){return vx(e)}decodeKey(e){return decodeURIComponent(e)}decodeValue(e){return decodeURIComponent(e)}};function Sk(t,e){let i=new Map;return t.length>0&&t.replace(/^\?/,"").split("&").forEach(r=>{let o=r.indexOf("="),[s,a]=o==-1?[e.decodeKey(r),""]:[e.decodeKey(r.slice(0,o)),e.decodeValue(r.slice(o+1))],l=i.get(s)||[];l.push(a),i.set(s,l)}),i}var Mk=/%(\d[a-f0-9])/gi,Tk={40:"@","3A":":",24:"$","2C":",","3B":";","3D":"=","3F":"?","2F":"/"};function vx(t){return encodeURIComponent(t).replace(Mk,(e,i)=>Tk[i]??e)}function nu(t){return`${t}`}var ar=class t{constructor(e={}){if(this.updates=null,this.cloneFrom=null,this.encoder=e.encoder||new Up,e.fromString){if(e.fromObject)throw new Error("Cannot specify both fromString and fromObject.");this.map=Sk(e.fromString,this.encoder)}else e.fromObject?(this.map=new Map,Object.keys(e.fromObject).forEach(i=>{let n=e.fromObject[i],r=Array.isArray(n)?n.map(nu):[nu(n)];this.map.set(i,r)})):this.map=null}has(e){return this.init(),this.map.has(e)}get(e){this.init();let i=this.map.get(e);return i?i[0]:null}getAll(e){return this.init(),this.map.get(e)||null}keys(){return this.init(),Array.from(this.map.keys())}append(e,i){return this.clone({param:e,value:i,op:"a"})}appendAll(e){let i=[];return Object.keys(e).forEach(n=>{let r=e[n];Array.isArray(r)?r.forEach(o=>{i.push({param:n,value:o,op:"a"})}):i.push({param:n,value:r,op:"a"})}),this.clone(i)}set(e,i){return this.clone({param:e,value:i,op:"s"})}delete(e,i){return this.clone({param:e,value:i,op:"d"})}toString(){return this.init(),this.keys().map(e=>{let i=this.encoder.encodeKey(e);return this.map.get(e).map(n=>i+"="+this.encoder.encodeValue(n)).join("&")}).filter(e=>e!=="").join("&")}clone(e){let i=new t({encoder:this.encoder});return i.cloneFrom=this.cloneFrom||this,i.updates=(this.updates||[]).concat(e),i}init(){this.map===null&&(this.map=new Map),this.cloneFrom!==null&&(this.cloneFrom.init(),this.cloneFrom.keys().forEach(e=>this.map.set(e,this.cloneFrom.map.get(e))),this.updates.forEach(e=>{switch(e.op){case"a":case"s":let i=(e.op==="a"?this.map.get(e.param):void 0)||[];i.push(nu(e.value)),this.map.set(e.param,i);break;case"d":if(e.value!==void 0){let n=this.map.get(e.param)||[],r=n.indexOf(nu(e.value));r!==-1&&n.splice(r,1),n.length>0?this.map.set(e.param,n):this.map.delete(e.param)}else{this.map.delete(e.param);break}}}),this.cloneFrom=this.updates=null)}};var Hp=class{constructor(){this.map=new Map}set(e,i){return this.map.set(e,i),this}get(e){return this.map.has(e)||this.map.set(e,e.defaultValue()),this.map.get(e)}delete(e){return this.map.delete(e),this}has(e){return this.map.has(e)}keys(){return this.map.keys()}};function
Line truncated
|
||||
`),E(),be(o.hasAction?2:-1))},dependencies:[$t,kP,NP,FP],styles:[".mat-mdc-simple-snack-bar{display:flex}"],encapsulation:2,changeDetection:0});let t=e;return t})(),LP={snackBarState:ai("state",[li("void, hidden",dt({transform:"scale(0.8)",opacity:0})),li("visible",dt({transform:"scale(1)",opacity:1})),tn("* => visible",bn("150ms cubic-bezier(0, 0, 0.2, 1)")),tn("* => void, * => hidden",bn("75ms cubic-bezier(0.4, 0.0, 1, 1)",dt({opacity:0})))])},jP=0,VP=(()=>{let e=class e extends hr{constructor(n,r,o,s,a){super(),this._ngZone=n,this._elementRef=r,this._changeDetectorRef=o,this._platform=s,this.snackBarConfig=a,this._document=y(j),this._trackedModals=new Set,this._announceDelay=150,this._destroyed=!1,this._onAnnounce=new M,this._onExit=new M,this._onEnter=new M,this._animationState="void",this._liveElementId=`mat-snack-bar-container-live-${jP++}`,this.attachDomPortal=l=>{this._assertNotAttached();let c=this._portalOutlet.attachDomPortal(l);return this._afterPortalAttached(),c},a.politeness==="assertive"&&!a.announcementMessage?this._live="assertive":a.politeness==="off"?this._live="off":this._live="polite",this._platform.FIREFOX&&(this._live==="polite"&&(this._role="status"),this._live==="assertive"&&(this._role="alert"))}attachComponentPortal(n){this._assertNotAttached();let r=this._portalOutlet.attachComponentPortal(n);return this._afterPortalAttached(),r}attachTemplatePortal(n){this._assertNotAttached();let r=this._portalOutlet.attachTemplatePortal(n);return this._afterPortalAttached(),r}onAnimationEnd(n){let{fromState:r,toState:o}=n;if((o==="void"&&r!=="void"||o==="hidden")&&this._completeExit(),o==="visible"){let s=this._onEnter;this._ngZone.run(()=>{s.next(),s.complete()})}}enter(){this._destroyed||(this._animationState="visible",this._changeDetectorRef.markForCheck(),this._changeDetectorRef.detectChanges(),this._screenReaderAnnounce())}exit(){return this._ngZone.run(()=>{this._animationState="hidden",this._changeDetectorRef.markForCheck(),this._elementRef.nativeElement.setAttribute("mat-exit",""),clearTimeout(this._announceTimeoutId)}),this._onExit}ngOnDestroy(){this._destroyed=!0,this._clearFromModals(),this._completeExit()}_completeExit(){queueMicrotask(()=>{this._onExit.next(),this._onExit.complete()})}_afterPortalAttached(){let n=this._elementRef.nativeElement,r=this.snackBarConfig.panelClass;r&&(Array.isArray(r)?r.forEach(a=>n.classList.add(a)):n.classList.add(r)),this._exposeToModals();let o=this._label.nativeElement,s="mdc-snackbar__label";o.classList.toggle(s,!o.querySelector(`.${s}`))}_exposeToModals(){let n=this._liveElementId,r=this._document.querySelectorAll('body > .cdk-overlay-container [aria-modal="true"]');for(let o=0;o<r.length;o++){let s=r[o],a=s.getAttribute("aria-owns");this._trackedModals.add(s),a?a.indexOf(n)===-1&&s.setAttribute("aria-owns",a+" "+n):s.setAttribute("aria-owns",n)}}_clearFromModals(){this._trackedModals.forEach(n=>{let r=n.getAttribute("aria-owns");if(r){let o=r.replace(this._liveElementId,"").trim();o.length>0?n.setAttribute("aria-owns",o):n.removeAttribute("aria-owns")}}),this._trackedModals.clear()}_assertNotAttached(){this._portalOutlet.hasAttached()}_screenReaderAnnounce(){this._announceTimeoutId||this._ngZone.runOutsideAngular(()=>{this._announceTimeoutId=setTimeout(()=>{let n=this._elementRef.nativeElement.querySelector("[aria-hidden]"),r=this._elementRef.nativeElement.querySelector("[aria-live]");if(n&&r){let o=null;this._platform.isBrowser&&document.activeElement instanceof HTMLElement&&n.contains(document.activeElement)&&(o=document.activeElement),n.removeAttribute("aria-hidden"),r.appendChild(n),o?.focus(),this._onAnnounce.next(),this._onAnnounce.complete()}},this._announceDelay)})}};e.\u0275fac=function(r){return new(r||e)(h(A),h(O),h(Fe),h(ye),h(As))},e.\u0275cmp=P({type:e,selectors:[["mat-snack-bar-container"]],viewQuery:function(r,o){if(r&1&&(Ye(mr,7),Ye(AP,7)),r&2){let s;Q(s=K())&&(o._portalOutlet=s.first),Q(s=K())&&(o._label=s.first)}},hostAttrs:[1,"mdc-snackbar","mat-mdc-snack-bar-container"],hostVars:1,hostBindings:function(r,o){r&1&&Ap("@state.done",function(a){return o.onAnimationEnd(a)}),r&2&&Tp("@state",o._animationState)},standalone:!0,features:[ve,L],decls:6,vars:3,consts:[["label",""],[1,"mdc-snackbar__surface","mat-mdc-snackbar-surface"],[1,"mat-mdc-snack-bar-label"],["aria-hidden","true"],["cdkPortalOutlet",""]],template:function(r,o){r&1&&(v(0,"div",1)(1,"div",2,0)(3,"div",3),B(4,RP,0,0,"ng-template",4),b(),ie(5,"div"),b()()),r&2&&(E(5),De("aria-live",o._live)("role",o._role)("id",o._liveElementId))},dependencies:[mr],styles:[".mat-mdc-snack-bar-container{display:flex;align-items:center;justify-content:center;box-sizing:border-box;-webkit-tap-highlight-color:rgba(0,0,0,0);margin:8px}.mat-mdc-snack-bar-handset .mat-mdc-snack-bar-container{width:100vw}.mat-mdc-snackbar-surface{box-shadow:0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12);display:flex;align-items:center;justif
Line truncated
|
||||
--mat-mdc-form-field-label-transform,
|
||||
${pj} translateX(${u})
|
||||
)`}_isAttachedToDom(){let n=this._elementRef.nativeElement;if(n.getRootNode){let r=n.getRootNode();return r&&r!==n}return document.documentElement.contains(n)}};e.\u0275fac=function(r){return new(r||e)(h(O),h(Fe),h(A),h(bt),h(ye),h(hj,8),h(ft,8),h(j))},e.\u0275cmp=P({type:e,selectors:[["mat-form-field"]],contentQueries:function(r,o,s){if(r&1&&(Be(s,ji,5),Be(s,ji,7),Be(s,Al,5),Be(s,cj,5),Be(s,dj,5),Be(s,aj,5),Be(s,Tv,5)),r&2){let a;Q(a=K())&&(o._labelChildNonStatic=a.first),Q(a=K())&&(o._labelChildStatic=a.first),Q(a=K())&&(o._formFieldControl=a.first),Q(a=K())&&(o._prefixChildren=a),Q(a=K())&&(o._suffixChildren=a),Q(a=K())&&(o._errorChildren=a),Q(a=K())&&(o._hintChildren=a)}},viewQuery:function(r,o){if(r&1&&(Ye(VL,5),Ye(BL,5),Ye(zL,5),Ye(YC,5),Ye(KC,5),Ye(QC,5)),r&2){let s;Q(s=K())&&(o._textField=s.first),Q(s=K())&&(o._iconPrefixContainer=s.first),Q(s=K())&&(o._textPrefixContainer=s.first),Q(s=K())&&(o._floatingLabel=s.first),Q(s=K())&&(o._notchedOutline=s.first),Q(s=K())&&(o._lineRipple=s.first)}},hostAttrs:[1,"mat-mdc-form-field"],hostVars:42,hostBindings:function(r,o){r&2&&xe("mat-mdc-form-field-label-always-float",o._shouldAlwaysFloat())("mat-mdc-form-field-has-icon-prefix",o._hasIconPrefix)("mat-mdc-form-field-has-icon-suffix",o._hasIconSuffix)("mat-form-field-invalid",o._control.errorState)("mat-form-field-disabled",o._control.disabled)("mat-form-field-autofilled",o._control.autofilled)("mat-form-field-no-animations",o._animationMode==="NoopAnimations")("mat-form-field-appearance-fill",o.appearance=="fill")("mat-form-field-appearance-outline",o.appearance=="outline")("mat-form-field-hide-placeholder",o._hasFloatingLabel()&&!o._shouldLabelFloat())("mat-focused",o._control.focused)("mat-primary",o.color!=="accent"&&o.color!=="warn")("mat-accent",o.color==="accent")("mat-warn",o.color==="warn")("ng-untouched",o._shouldForward("untouched"))("ng-touched",o._shouldForward("touched"))("ng-pristine",o._shouldForward("pristine"))("ng-dirty",o._shouldForward("dirty"))("ng-valid",o._shouldForward("valid"))("ng-invalid",o._shouldForward("invalid"))("ng-pending",o._shouldForward("pending"))},inputs:{hideRequiredMarker:"hideRequiredMarker",color:"color",floatLabel:"floatLabel",appearance:"appearance",subscriptSizing:"subscriptSizing",hintLabel:"hintLabel"},exportAs:["matFormField"],standalone:!0,features:[Ne([{provide:Rl,useExisting:e},{provide:tE,useExisting:e}]),L],ngContentSelectors:HL,decls:18,vars:21,consts:[["labelTemplate",""],["textField",""],["iconPrefixContainer",""],["textPrefixContainer",""],[1,"mat-mdc-text-field-wrapper","mdc-text-field",3,"click"],[1,"mat-mdc-form-field-focus-overlay"],[1,"mat-mdc-form-field-flex"],["matFormFieldNotchedOutline","",3,"matFormFieldNotchedOutlineOpen"],[1,"mat-mdc-form-field-icon-prefix"],[1,"mat-mdc-form-field-text-prefix"],[1,"mat-mdc-form-field-infix"],[3,"ngTemplateOutlet"],[1,"mat-mdc-form-field-text-suffix"],[1,"mat-mdc-form-field-icon-suffix"],["matFormFieldLineRipple",""],[1,"mat-mdc-form-field-subscript-wrapper","mat-mdc-form-field-bottom-align"],[1,"mat-mdc-form-field-error-wrapper"],[1,"mat-mdc-form-field-hint-wrapper"],["matFormFieldFloatingLabel","",3,"floating","monitorResize","id"],["aria-hidden","true",1,"mat-mdc-form-field-required-marker","mdc-floating-label--required"],[3,"id"],[1,"mat-mdc-form-field-hint-spacer"]],template:function(r,o){if(r&1){let s=lt();et(UL),B(0,GL,1,1,"ng-template",null,0,Qw),v(2,"div",4,1),z("click",function(l){return Te(s),Ae(o._control.onContainerClick(l))}),B(4,qL,1,0,"div",5),v(5,"div",6),B(6,QL,2,2,"div",7)(7,KL,3,0,"div",8)(8,XL,3,0,"div",9),v(9,"div",10),B(10,ej,1,1,null,11),_e(11),b(),B(12,tj,2,0,"div",12)(13,nj,2,0,"div",13),b(),B(14,ij,1,0,"div",14),b(),v(15,"div",15),B(16,rj,2,1,"div",16)(17,sj,5,2,"div",17),b()}if(r&2){let s;E(2),xe("mdc-text-field--filled",!o._hasOutline())("mdc-text-field--outlined",o._hasOutline())("mdc-text-field--no-label",!o._hasFloatingLabel())("mdc-text-field--disabled",o._control.disabled)("mdc-text-field--invalid",o._control.errorState),E(2),be(!o._hasOutline()&&!o._control.disabled?4:-1),E(2),be(o._hasOutline()?6:-1),E(),be(o._hasIconPrefix?7:-1),E(),be(o._hasTextPrefix?8:-1),E(2),be(!o._hasOutline()||o._forceDisplayInfixLabel()?10:-1),E(2),be(o._hasTextSuffix?12:-1),E(),be(o._hasIconSuffix?13:-1),E(),be(o._hasOutline()?-1:14),E(),xe("mat-mdc-form-field-subscript-dynamic-size",o.subscriptSizing==="dynamic"),E(),be((s=o._getDisplayedMessages())==="error"?16:s==="hint"?17:-1)}},dependencies:[YC,KC,px,QC,Tv],styles:['.mdc-text-field{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:0;border-bottom-left-radius:0;display:inline-flex;align-items:baseline;padding:0 16px;position:relative;box-sizing:border-box;overflow:hidden;will-change:opacity,transform,color}.mdc-text-field .mdc-floating-label{top:50%;transform:translateY(-50%);pointer-events:none}.mdc-tex
Line truncated
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"configVersion": 1,
|
||||
"timestamp": 1720682538867,
|
||||
"index": "/index.html",
|
||||
"assetGroups": [
|
||||
{
|
||||
"name": "app",
|
||||
"installMode": "prefetch",
|
||||
"updateMode": "prefetch",
|
||||
"cacheQueryOptions": {
|
||||
"ignoreVary": true
|
||||
},
|
||||
"urls": [
|
||||
"/favicon.ico",
|
||||
"/index.html",
|
||||
"/main-CLMZMBYY.js",
|
||||
"/manifest.webmanifest",
|
||||
"/polyfills-TMVK3KFA.js",
|
||||
"/styles-NJ66DI6F.css"
|
||||
],
|
||||
"patterns": []
|
||||
},
|
||||
{
|
||||
"name": "assets",
|
||||
"installMode": "lazy",
|
||||
"updateMode": "prefetch",
|
||||
"cacheQueryOptions": {
|
||||
"ignoreVary": true
|
||||
},
|
||||
"urls": [
|
||||
"/assets/icons/icon-128x128.png",
|
||||
"/assets/icons/icon-144x144.png",
|
||||
"/assets/icons/icon-152x152.png",
|
||||
"/assets/icons/icon-192x192.png",
|
||||
"/assets/icons/icon-384x384.png",
|
||||
"/assets/icons/icon-512x512.png",
|
||||
"/assets/icons/icon-72x72.png",
|
||||
"/assets/icons/icon-96x96.png"
|
||||
],
|
||||
"patterns": []
|
||||
}
|
||||
],
|
||||
"dataGroups": [],
|
||||
"hashTable": {
|
||||
"/assets/icons/icon-128x128.png": "f912963bdc6d5a38d8f1dd0afbaab2ce8d657acb",
|
||||
"/assets/icons/icon-144x144.png": "b155fd5f2fd5d2ea7760721063003c4cd95fd783",
|
||||
"/assets/icons/icon-152x152.png": "eae6e0f0c8afb8aaae339e2a569b43e9238e5e6c",
|
||||
"/assets/icons/icon-192x192.png": "15c4180454633880d98674aa7394656196c91584",
|
||||
"/assets/icons/icon-384x384.png": "94a915751ef6c9282df9aea405ad679230631814",
|
||||
"/assets/icons/icon-512x512.png": "1de182f76f7329dfa8f9fcdd1fdcdd695bea6a99",
|
||||
"/assets/icons/icon-72x72.png": "aa88a6096bd973be7f6d7a5489bfa6bc2463f8c4",
|
||||
"/assets/icons/icon-96x96.png": "7fb8f59c30ce2ff12c700321a0b39e14b3dc8f95",
|
||||
"/favicon.ico": "9c39f434fe1261f68c5e3eefdf734630d99c5670",
|
||||
"/index.html": "f5334c6295211e902dbebf00c2b0f2be6cb3c2f6",
|
||||
"/main-CLMZMBYY.js": "35afe20a8438fd03626e7fb1cca7c1583d1a9fc8",
|
||||
"/manifest.webmanifest": "4a17033d41b19f97cc660c6a603fdc95226d1704",
|
||||
"/polyfills-TMVK3KFA.js": "fda1c82c5c620f1fa442f687b3df45a037f6fcc9",
|
||||
"/styles-NJ66DI6F.css": "765db3018d28d3c08e15645f053b34af5946b7e5"
|
||||
},
|
||||
"navigationUrls": [
|
||||
{
|
||||
"positive": true,
|
||||
"regex": "^\\/.*$"
|
||||
},
|
||||
{
|
||||
"positive": false,
|
||||
"regex": "^\\/(?:.+\\/)?[^/]*\\.[^/]*$"
|
||||
},
|
||||
{
|
||||
"positive": false,
|
||||
"regex": "^\\/(?:.+\\/)?[^/]*__[^/]*$"
|
||||
},
|
||||
{
|
||||
"positive": false,
|
||||
"regex": "^\\/(?:.+\\/)?[^/]*__[^/]*\\/.*$"
|
||||
}
|
||||
],
|
||||
"navigationRequestStrategy": "performance"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
(function(e){let n=e.performance;function c(L){n&&n.mark&&n.mark(L)}function r(L,t){n&&n.measure&&n.measure(L,t)}c("Zone");let a=e.__Zone_symbol_prefix||"__zone_symbol__";function l(L){return a+L}let y=e[l("forceDuplicateZoneCheck")]===!0;if(e.Zone){if(y||typeof e.Zone.__symbol__!="function")throw new Error("Zone already loaded.");return e.Zone}let oe=class oe{static assertZonePatched(){if(e.Promise!==re.ZoneAwarePromise)throw new Error("Zone.js has detected that ZoneAwarePromise `(window|global).Promise` has been overwritten.\nMost likely cause is that a Promise polyfill has been loaded after Zone.js (Polyfilling Promise api is not necessary when zone.js is loaded. If you must load one, do so before loading zone.js.)")}static get root(){let t=oe.current;for(;t.parent;)t=t.parent;return t}static get current(){return U.zone}static get currentTask(){return te}static __load_patch(t,s,o=!1){if(re.hasOwnProperty(t)){if(!o&&y)throw Error("Already loaded patch: "+t)}else if(!e["__Zone_disable_"+t]){let v="Zone:"+t;c(v),re[t]=s(e,oe,z),r(v,v)}}get parent(){return this._parent}get name(){return this._name}constructor(t,s){this._parent=t,this._name=s?s.name||"unnamed":"<root>",this._properties=s&&s.properties||{},this._zoneDelegate=new k(this,this._parent&&this._parent._zoneDelegate,s)}get(t){let s=this.getZoneWith(t);if(s)return s._properties[t]}getZoneWith(t){let s=this;for(;s;){if(s._properties.hasOwnProperty(t))return s;s=s._parent}return null}fork(t){if(!t)throw new Error("ZoneSpec required!");return this._zoneDelegate.fork(this,t)}wrap(t,s){if(typeof t!="function")throw new Error("Expecting function got: "+t);let o=this._zoneDelegate.intercept(this,t,s),v=this;return function(){return v.runGuarded(o,this,arguments,s)}}run(t,s,o,v){U={parent:U,zone:this};try{return this._zoneDelegate.invoke(this,t,s,o,v)}finally{U=U.parent}}runGuarded(t,s=null,o,v){U={parent:U,zone:this};try{try{return this._zoneDelegate.invoke(this,t,s,o,v)}catch(F){if(this._zoneDelegate.handleError(this,F))throw F}}finally{U=U.parent}}runTask(t,s,o){if(t.zone!=this)throw new Error("A task can only be run in the zone of creation! (Creation: "+(t.zone||$).name+"; Execution: "+this.name+")");if(t.state===H&&(t.type===K||t.type===P))return;let v=t.state!=T;v&&t._transitionTo(T,M),t.runCount++;let F=te;te=t,U={parent:U,zone:this};try{t.type==P&&t.data&&!t.data.isPeriodic&&(t.cancelFn=void 0);try{return this._zoneDelegate.invokeTask(this,t,s,o)}catch(u){if(this._zoneDelegate.handleError(this,u))throw u}}finally{t.state!==H&&t.state!==d&&(t.type==K||t.data&&t.data.isPeriodic?v&&t._transitionTo(M,T):(t.runCount=0,this._updateTaskCount(t,-1),v&&t._transitionTo(H,T,H))),U=U.parent,te=F}}scheduleTask(t){if(t.zone&&t.zone!==this){let o=this;for(;o;){if(o===t.zone)throw Error(`can not reschedule task to ${this.name} which is descendants of the original zone ${t.zone.name}`);o=o.parent}}t._transitionTo(X,H);let s=[];t._zoneDelegates=s,t._zone=this;try{t=this._zoneDelegate.scheduleTask(this,t)}catch(o){throw t._transitionTo(d,X,H),this._zoneDelegate.handleError(this,o),o}return t._zoneDelegates===s&&this._updateTaskCount(t,1),t.state==X&&t._transitionTo(M,X),t}scheduleMicroTask(t,s,o,v){return this.scheduleTask(new m(N,t,s,o,v,void 0))}scheduleMacroTask(t,s,o,v,F){return this.scheduleTask(new m(P,t,s,o,v,F))}scheduleEventTask(t,s,o,v,F){return this.scheduleTask(new m(K,t,s,o,v,F))}cancelTask(t){if(t.zone!=this)throw new Error("A task can only be cancelled in the zone of creation! (Creation: "+(t.zone||$).name+"; Execution: "+this.name+")");if(!(t.state!==M&&t.state!==T)){t._transitionTo(x,M,T);try{this._zoneDelegate.cancelTask(this,t)}catch(s){throw t._transitionTo(d,x),this._zoneDelegate.handleError(this,s),s}return this._updateTaskCount(t,-1),t._transitionTo(H,x),t.runCount=0,t}}_updateTaskCount(t,s){let o=t._zoneDelegates;s==-1&&(t._zoneDelegates=null);for(let v=0;v<o.length;v++)o[v]._updateTaskCount(t.type,s)}};oe.__symbol__=l;let _=oe,b={name:"",onHasTask:(L,t,s,o)=>L.hasTask(s,o),onScheduleTask:(L,t,s,o)=>L.scheduleTask(s,o),onInvokeTask:(L,t,s,o,v,F)=>L.invokeTask(s,o,v,F),onCancelTask:(L,t,s,o)=>L.cancelTask(s,o)};class k{constructor(t,s,o){this._taskCounts={microTask:0,macroTask:0,eventTask:0},this.zone=t,this._parentDelegate=s,this._forkZS=o&&(o&&o.onFork?o:s._forkZS),this._forkDlgt=o&&(o.onFork?s:s._forkDlgt),this._forkCurrZone=o&&(o.onFork?this.zone:s._forkCurrZone),this._interceptZS=o&&(o.onIntercept?o:s._interceptZS),this._interceptDlgt=o&&(o.onIntercept?s:s._interceptDlgt),this._interceptCurrZone=o&&(o.onIntercept?this.zone:s._interceptCurrZone),this._invokeZS=o&&(o.onInvoke?o:s._invokeZS),this._invokeDlgt=o&&(o.onInvoke?s:s._invokeDlgt),this._invokeCurrZone=o&&(o.onInvoke?this.zone:s._invokeCurrZone),this._handleErrorZS=o&&(o.onHandleError?o:s._handleErrorZS),this._handleErrorDlgt=o&&(o.onHandleError?s:s._handleErrorDlgt),this._handleErrorCurrZone=o&&(o.onHandleError?this.zone:s._handleErrorCurrZone),this._scheduleTaskZS=o&&(o.onScheduleTaskLine truncated
|
||||
`+i.stack:""))}catch(C){p=C}b&&(p.throwOriginal=!0),p.rejection=i,p.promise=u,p.zone=n.current,p.task=n.currentTask,_.push(p),c.scheduleMicroTask()}}}return u}let U=y("rejectionHandledHandler");function te(u){if(u[q]===d){try{let f=n[U];f&&typeof f=="function"&&f.call(this,{rejection:u[R],promise:u})}catch{}u[q]=x;for(let f=0;f<_.length;f++)u===_[f].promise&&_.splice(f,1)}}function Q(u,f,i,h,g){te(u);let w=u[q],p=w?typeof h=="function"?h:A:typeof g=="function"?g:J;f.scheduleMicroTask(X,()=>{try{let C=u[R],S=!!i&&E===i[E];S&&(i[$]=C,i[H]=w);let D=f.run(p,void 0,S&&p!==J&&p!==A?[]:[C]);z(i,!0,D)}catch(C){z(i,!1,C)}},i)}let W="function ZoneAwarePromise() { [native code] }",oe=function(){},L=e.AggregateError;class t{static toString(){return W}static resolve(f){return z(new this(null),T,f)}static reject(f){return z(new this(null),x,f)}static any(f){if(!f||typeof f[Symbol.iterator]!="function")return Promise.reject(new L([],"All promises were rejected"));let i=[],h=0;try{for(let p of f)h++,i.push(t.resolve(p))}catch{return Promise.reject(new L([],"All promises were rejected"))}if(h===0)return Promise.reject(new L([],"All promises were rejected"));let g=!1,w=[];return new t((p,C)=>{for(let S=0;S<i.length;S++)i[S].then(D=>{g||(g=!0,p(D))},D=>{w.push(D),h--,h===0&&(g=!0,C(new L(w,"All promises were rejected")))})})}static race(f){let i,h,g=new this((C,S)=>{i=C,h=S});function w(C){i(C)}function p(C){h(C)}for(let C of f)B(C)||(C=this.resolve(C)),C.then(w,p);return g}static all(f){return t.allWithCallback(f)}static allSettled(f){return(this&&this.prototype instanceof t?this:t).allWithCallback(f,{thenCallback:h=>({status:"fulfilled",value:h}),errorCallback:h=>({status:"rejected",reason:h})})}static allWithCallback(f,i){let h,g,w=new this((D,G)=>{h=D,g=G}),p=2,C=0,S=[];for(let D of f){B(D)||(D=this.resolve(D));let G=C;try{D.then(V=>{S[G]=i?i.thenCallback(V):V,p--,p===0&&h(S)},V=>{i?(S[G]=i.errorCallback(V),p--,p===0&&h(S)):g(V)})}catch(V){g(V)}p++,C++}return p-=2,p===0&&h(S),w}constructor(f){let i=this;if(!(i instanceof t))throw new Error("Must be an instanceof Promise.");i[q]=M,i[R]=[];try{let h=P();f&&f(h(N(i,T)),h(N(i,x)))}catch(h){z(i,!1,h)}}get[Symbol.toStringTag](){return"Promise"}get[Symbol.species](){return t}then(f,i){let h=this.constructor?.[Symbol.species];(!h||typeof h!="function")&&(h=this.constructor||t);let g=new h(oe),w=n.current;return this[q]==M?this[R].push(w,g,f,i):Q(this,w,g,f,i),g}catch(f){return this.then(null,f)}finally(f){let i=this.constructor?.[Symbol.species];(!i||typeof i!="function")&&(i=t);let h=new i(oe);h[E]=E;let g=n.current;return this[q]==M?this[R].push(g,h,f,f):Q(this,g,h,f,f),h}}t.resolve=t.resolve,t.reject=t.reject,t.race=t.race,t.all=t.all;let s=e[k]=e.Promise;e.Promise=t;let o=y("thenPatched");function v(u){let f=u.prototype,i=r(f,"then");if(i&&(i.writable===!1||!i.configurable))return;let h=f.then;f[m]=h,u.prototype.then=function(g,w){return new t((C,S)=>{h.call(this,C,S)}).then(g,w)},u[o]=!0}c.patchThen=v;function F(u){return function(f,i){let h=u.apply(f,i);if(h instanceof t)return h;let g=h.constructor;return g[o]||v(g),h}}return s&&(v(s),le(e,"fetch",u=>F(u))),Promise[n.__symbol__("uncaughtPromiseErrors")]=_,t});Zone.__load_patch("toString",e=>{let n=Function.prototype.toString,c=j("OriginalDelegate"),r=j("Promise"),a=j("Error"),l=function(){if(typeof this=="function"){let k=this[c];if(k)return typeof k=="function"?n.call(k):Object.prototype.toString.call(k);if(this===Promise){let m=e[r];if(m)return n.call(m)}if(this===Error){let m=e[a];if(m)return n.call(m)}}return n.call(this)};l[c]=n,Function.prototype.toString=l;let y=Object.prototype.toString,_="[object Promise]";Object.prototype.toString=function(){return typeof Promise=="function"&&this instanceof Promise?_:y.call(this)}});var _e=!1;if(typeof window<"u")try{let e=Object.defineProperty({},"passive",{get:function(){_e=!0}});window.addEventListener("test",e,e),window.removeEventListener("test",e,e)}catch{_e=!1}var dt={useG:!0},ee={},Qe={},et=new RegExp("^"+pe+"(\\w+)(true|false)$"),tt=j("propagationStopped");function nt(e,n){let c=(n?n(e):e)+ce,r=(n?n(e):e)+ie,a=pe+c,l=pe+r;ee[e]={},ee[e][ce]=a,ee[e][ie]=l}function _t(e,n,c,r){let a=r&&r.add||Me,l=r&&r.rm||Le,y=r&&r.listeners||"eventListeners",_=r&&r.rmAll||"removeAllListeners",b=j(a),k="."+a+":",m="prependListener",I="."+m+":",Z=function(R,E,$){if(R.isRemoved)return;let H=R.callback;typeof H=="object"&&H.handleEvent&&(R.callback=T=>H.handleEvent(T),R.originalDelegate=H);let X;try{R.invoke(R,E,[$])}catch(T){X=T}let M=R.options;if(M&&typeof M=="object"&&M.once){let T=R.originalDelegate?R.originalDelegate:R.callback;E[l].call(E,$.type,T,M)}return X};function O(R,E,$){if(E=E||e.event,!E)return;let H=R||E.target||e,X=H[ee[E.type][$?ie:ce]];if(X){let M=[];if(X.length===1){let T=Z(X[0],H,E);T&&M.push(T)}else{let T=X.slice();for(let x=0;x<T.length&&!(E&&E[tt]===!0);x++){let d=Z(T[x],H,E);d&&M.push(d)}}if(M.length===1)throw M[0];for(let T=0;T<M.length;T++){let x=M[T];n.nativeSch
Line truncated
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
// tslint:disable:no-console
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(self.clients.claim());
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.unregister().then(() => {
|
||||
console.log('NGSW Safety Worker - unregistered old service worker');
|
||||
}),
|
||||
);
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
const ngswCacheNames = cacheNames.filter((name) => /^ngsw:/.test(name));
|
||||
return Promise.all(ngswCacheNames.map((name) => caches.delete(name)));
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
.mat-ripple{overflow:hidden;position:relative}.mat-ripple:not(:empty){transform:translateZ(0)}.mat-ripple.mat-ripple-unbounded{overflow:visible}.mat-ripple-element{position:absolute;border-radius:50%;pointer-events:none;transition:opacity,transform 0ms cubic-bezier(0,0,.2,1);transform:scale3d(0,0,0);background-color:var(--mat-ripple-color, rgba(0, 0, 0, .1))}.cdk-high-contrast-active .mat-ripple-element,.cdk-drag-preview .mat-ripple-element,.cdk-drag-placeholder .mat-ripple-element{display:none}.cdk-visually-hidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;white-space:nowrap;outline:0;-webkit-appearance:none;-moz-appearance:none;left:0}[dir=rtl] .cdk-visually-hidden{left:auto;right:0}.cdk-overlay-container,.cdk-global-overlay-wrapper{pointer-events:none;top:0;left:0;height:100%;width:100%}.cdk-overlay-container{position:fixed;z-index:1000}.cdk-overlay-container:empty{display:none}.cdk-global-overlay-wrapper{display:flex;position:absolute;z-index:1000}.cdk-overlay-pane{position:absolute;pointer-events:auto;box-sizing:border-box;z-index:1000;display:flex;max-width:100%;max-height:100%}.cdk-overlay-backdrop{position:absolute;inset:0;z-index:1000;pointer-events:auto;-webkit-tap-highlight-color:rgba(0,0,0,0);transition:opacity .4s cubic-bezier(.25,.8,.25,1);opacity:0}.cdk-overlay-backdrop.cdk-overlay-backdrop-showing{opacity:1}.cdk-high-contrast-active .cdk-overlay-backdrop.cdk-overlay-backdrop-showing{opacity:.6}.cdk-overlay-dark-backdrop{background:#00000052}.cdk-overlay-transparent-backdrop{transition:visibility 1ms linear,opacity 1ms linear;visibility:hidden;opacity:1}.cdk-overlay-transparent-backdrop.cdk-overlay-backdrop-showing{opacity:0;visibility:visible}.cdk-overlay-backdrop-noop-animation{transition:none}.cdk-overlay-connected-position-bounding-box{position:absolute;z-index:1000;display:flex;flex-direction:column;min-width:1px;min-height:1px}.cdk-global-scrollblock{position:fixed;width:100%;overflow-y:scroll}textarea.cdk-textarea-autosize{resize:none}textarea.cdk-textarea-autosize-measuring{padding:2px 0!important;box-sizing:content-box!important;height:auto!important;overflow:hidden!important}textarea.cdk-textarea-autosize-measuring-firefox{padding:2px 0!important;box-sizing:content-box!important;height:0!important}@keyframes cdk-text-field-autofill-start{}@keyframes cdk-text-field-autofill-end{}.cdk-text-field-autofill-monitored:-webkit-autofill{animation:cdk-text-field-autofill-start 0s 1ms}.cdk-text-field-autofill-monitored:not(:-webkit-autofill){animation:cdk-text-field-autofill-end 0s 1ms}.mat-focus-indicator{position:relative}.mat-focus-indicator:before{inset:0;position:absolute;box-sizing:border-box;pointer-events:none;display:var(--mat-focus-indicator-display, none);border:var(--mat-focus-indicator-border-width, 3px) var(--mat-focus-indicator-border-style, solid) var(--mat-focus-indicator-border-color, transparent);border-radius:var(--mat-focus-indicator-border-radius, 4px)}.mat-focus-indicator:focus:before{content:""}.cdk-high-contrast-active{--mat-focus-indicator-display: block}.mat-mdc-focus-indicator{position:relative}.mat-mdc-focus-indicator:before{inset:0;position:absolute;box-sizing:border-box;pointer-events:none;display:var(--mat-mdc-focus-indicator-display, none);border:var(--mat-mdc-focus-indicator-border-width, 3px) var(--mat-mdc-focus-indicator-border-style, solid) var(--mat-mdc-focus-indicator-border-color, transparent);border-radius:var(--mat-mdc-focus-indicator-border-radius, 4px)}.mat-mdc-focus-indicator:focus:before{content:""}.cdk-high-contrast-active{--mat-mdc-focus-indicator-display: block}.mat-app-background{background-color:var(--mat-app-background-color, transparent);color:var(--mat-app-text-color, inherit)}.mat-theme-loaded-marker{display:none}html{--mat-ripple-color:rgba(0, 0, 0, .1)}html{--mat-option-selected-state-label-text-color:#3f51b5;--mat-option-label-text-color:rgba(0, 0, 0, .87);--mat-option-hover-state-layer-color:rgba(0, 0, 0, .04);--mat-option-focus-state-layer-color:rgba(0, 0, 0, .04);--mat-option-selected-state-layer-color:rgba(0, 0, 0, .04)}.mat-accent{--mat-option-selected-state-label-text-color:#ff4081;--mat-option-label-text-color:rgba(0, 0, 0, .87);--mat-option-hover-state-layer-color:rgba(0, 0, 0, .04);--mat-option-focus-state-layer-color:rgba(0, 0, 0, .04);--mat-option-selected-state-layer-color:rgba(0, 0, 0, .04)}.mat-warn{--mat-option-selected-state-label-text-color:#f44336;--mat-option-label-text-color:rgba(0, 0, 0, .87);--mat-option-hover-state-layer-color:rgba(0, 0, 0, .04);--mat-option-focus-state-layer-color:rgba(0, 0, 0, .04);--mat-option-selected-state-layer-color:rgba(0, 0, 0, .04)}html{--mat-optgroup-label-text-color:rgba(0, 0, 0, .87)}.mat-primary{--mat-full-pseudo-checkbox-selected-icon-color:#3f51b5;--mat-full-pseudo-checkbox-selected-checkmark-color:#fafafa;--mat-full-pseudo-checkbox-unselected-icon-color:rgba(0, 0, 0, .54);--mat-full-pseudo-checkbox-disabled-selected-cLine truncated
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
// tslint:disable:no-console
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(self.clients.claim());
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.unregister().then(() => {
|
||||
console.log('NGSW Safety Worker - unregistered old service worker');
|
||||
}),
|
||||
);
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
const ngswCacheNames = cacheNames.filter((name) => /^ngsw:/.test(name));
|
||||
return Promise.all(ngswCacheNames.map((name) => caches.delete(name)));
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
package front
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf"
|
||||
"net/http"
|
||||
"strings"
|
||||
"ultone/internal/log"
|
||||
)
|
||||
|
||||
//go:embed dist/front/browser
|
||||
var DefaultFront embed.FS
|
||||
|
||||
func NewFront(ff *embed.FS, basePath string) nf.HandlerFunc {
|
||||
var (
|
||||
e error
|
||||
indexBytes []byte
|
||||
index string
|
||||
)
|
||||
|
||||
index = fmt.Sprintf("%s/index.html", basePath)
|
||||
|
||||
if indexBytes, e = ff.ReadFile(index); e != nil {
|
||||
log.Panic(nil, "read index file err: %v", e)
|
||||
}
|
||||
|
||||
return func(c *nf.Ctx) error {
|
||||
var (
|
||||
err error
|
||||
bs []byte
|
||||
path = c.Path()
|
||||
)
|
||||
|
||||
if bs, err = ff.ReadFile(basePath + path); err != nil {
|
||||
log.Debug(c, "embed read file [%s]%s err: %v", basePath, path, err)
|
||||
c.Set("Content-Type", "text/html")
|
||||
_, err = c.Write(indexBytes)
|
||||
return err
|
||||
}
|
||||
|
||||
var dbs []byte
|
||||
if len(bs) > 512 {
|
||||
dbs = bs[:512]
|
||||
} else {
|
||||
dbs = bs
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasSuffix(path, ".js"):
|
||||
c.Set("Content-Type", "application/javascript")
|
||||
case strings.HasSuffix(path, ".css"):
|
||||
c.Set("Content-Type", "text/css")
|
||||
default:
|
||||
c.Set("Content-Type", http.DetectContentType(dbs))
|
||||
}
|
||||
|
||||
_, err = c.Write(bs)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/loveuer/esgo2dump/log"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
var (
|
||||
Header = http.CanonicalHeaderKey("X-Trace-Id")
|
||||
)
|
||||
|
||||
func New() nf.HandlerFunc {
|
||||
|
||||
return func(c *nf.Ctx) error {
|
||||
var (
|
||||
now = time.Now()
|
||||
trace = c.Get(Header)
|
||||
logFn func(msg string, data ...any)
|
||||
ip = c.IP()
|
||||
)
|
||||
|
||||
if trace == "" {
|
||||
trace = uuid.Must(uuid.NewV7()).String()
|
||||
}
|
||||
|
||||
c.SetHeader(Header, trace)
|
||||
|
||||
traces := strings.Split(trace, "-")
|
||||
shortTrace := traces[len(traces)-1]
|
||||
|
||||
c.Locals(opt.LocalTraceKey, trace)
|
||||
c.Locals(opt.LocalShortTraceKey, shortTrace)
|
||||
|
||||
err := c.Next()
|
||||
status, _ := strconv.Atoi(c.Writer.Header().Get(resp.RealStatusHeader))
|
||||
duration := time.Since(now)
|
||||
|
||||
msg := fmt.Sprintf("%s | %15s | %d[%3d] | %s | %6s | %s", shortTrace, ip, c.StatusCode, status, tool.HumanDuration(duration.Nanoseconds()), c.Method(), c.Path())
|
||||
|
||||
switch {
|
||||
case status >= 500:
|
||||
logFn = log.Error
|
||||
case status >= 400:
|
||||
logFn = log.Warn
|
||||
default:
|
||||
logFn = log.Info
|
||||
}
|
||||
|
||||
logFn(msg)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package oplog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"sync"
|
||||
"time"
|
||||
"ultone/internal/database/db"
|
||||
"ultone/internal/model"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/sqlType"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
var (
|
||||
_once = &sync.Once{}
|
||||
lc = make(chan *model.OpLog, 1024)
|
||||
)
|
||||
|
||||
// NewOpLog
|
||||
//
|
||||
// * 记录操作日志的 中间件使用方法如下:
|
||||
//
|
||||
// app := nf.New()
|
||||
// app.Post("/login", oplog.NewOpLog(ctx), HandleLog)
|
||||
//
|
||||
// func HandleLog(c *nf.Ctx) error {
|
||||
// // 你的操作逻辑
|
||||
// c.Local(opt.OpLogLocalKey, &oplog.OpLog{})
|
||||
// // 剩下某些逻辑
|
||||
// // return xxx
|
||||
// }
|
||||
func NewOpLog(ctx context.Context) nf.HandlerFunc {
|
||||
|
||||
_once.Do(func() {
|
||||
go func() {
|
||||
var (
|
||||
err error
|
||||
ticker = time.NewTicker(time.Duration(opt.OpLogWriteDurationSecond) * time.Second)
|
||||
list = make([]*model.OpLog, 0, 1024)
|
||||
|
||||
write = func() {
|
||||
if len(list) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if err = db.New(tool.Timeout(10)).
|
||||
Model(&model.OpLog{}).
|
||||
Create(&list).
|
||||
Error; err != nil {
|
||||
log.Error("middleware.NewOpLog: write logs err=%v", err)
|
||||
}
|
||||
|
||||
list = list[:0]
|
||||
}
|
||||
)
|
||||
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break Loop
|
||||
case <-ticker.C:
|
||||
write()
|
||||
case item, ok := <-lc:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
list = append(list, item)
|
||||
|
||||
if len(list) >= 100 {
|
||||
write()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write()
|
||||
}()
|
||||
})
|
||||
|
||||
return func(c *nf.Ctx) error {
|
||||
now := time.Now()
|
||||
|
||||
err := c.Next()
|
||||
|
||||
op, ok := c.Locals("user").(*model.User)
|
||||
|
||||
opv := c.Locals(opt.OpLogLocalKey)
|
||||
logItem, ok := opv.(*OpLog)
|
||||
if !ok {
|
||||
log.Warn("middleware.NewOpLog: %s - %s local '%s' to [*OpLog] invalid", c.Method(), c.Path(), opt.OpLogLocalKey)
|
||||
return err
|
||||
}
|
||||
|
||||
logItem.Content["time"] = now.UnixMilli()
|
||||
logItem.Content["user_id"] = op.Id
|
||||
logItem.Content["username"] = op.Username
|
||||
logItem.Content["created_at"] = now.UnixMilli()
|
||||
|
||||
select {
|
||||
case lc <- &model.OpLog{
|
||||
CreatedAt: now.UnixMilli(),
|
||||
UpdatedAt: now.UnixMilli(),
|
||||
UserId: op.Id,
|
||||
Username: op.Username,
|
||||
Type: logItem.Type,
|
||||
Content: sqlType.NewJSONB(logItem.Content),
|
||||
}:
|
||||
case <-tool.Timeout(3).Done():
|
||||
log.Warn("middleware.NewOpLog: %s - %s log -> chan timeout[3s]", c.Method, c.Path())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package oplog
|
||||
|
||||
import "ultone/internal/model"
|
||||
|
||||
type OpLog struct {
|
||||
Type model.OpLogType
|
||||
Content map[string]any
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package privilege
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"github.com/samber/lo"
|
||||
"strings"
|
||||
"ultone/internal/model"
|
||||
)
|
||||
|
||||
type Relation int64
|
||||
|
||||
type vf func(user *model.User, ps ...model.Privilege) error
|
||||
|
||||
const (
|
||||
RelationAnd Relation = iota + 1
|
||||
RelationOr
|
||||
)
|
||||
|
||||
var (
|
||||
AndFunc vf = func(user *model.User, ps ...model.Privilege) error {
|
||||
pm := lo.SliceToMap(user.Privileges, func(item model.Privilege) (int64, struct{}) {
|
||||
return item.Value(), struct{}{}
|
||||
})
|
||||
|
||||
for _, p := range ps {
|
||||
if _, exist := pm[p.Value()]; !exist {
|
||||
return fmt.Errorf("缺少权限: %d, %s, %s", p.Value(), p.Code(), p.Label())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
OrFunc vf = func(user *model.User, ps ...model.Privilege) error {
|
||||
pm := lo.SliceToMap(user.Privileges, func(item model.Privilege) (int64, struct{}) {
|
||||
return item.Value(), struct{}{}
|
||||
})
|
||||
|
||||
for _, p := range ps {
|
||||
if _, exist := pm[p.Value()]; exist {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("缺少权限: %s", strings.Join(
|
||||
lo.Map(ps, func(item model.Privilege, index int) string {
|
||||
return item.Code()
|
||||
}),
|
||||
", ",
|
||||
))
|
||||
}
|
||||
)
|
||||
|
||||
func Verify(relation Relation, privileges ...model.Privilege) nf.HandlerFunc {
|
||||
|
||||
var _vf vf
|
||||
|
||||
switch relation {
|
||||
case RelationAnd:
|
||||
_vf = AndFunc
|
||||
case RelationOr:
|
||||
_vf = OrFunc
|
||||
default:
|
||||
log.Panic("middleware.Verify: unknown relation")
|
||||
}
|
||||
|
||||
return func(c *nf.Ctx) error {
|
||||
if len(privileges) == 0 {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
op, ok := c.Locals("user").(*model.User)
|
||||
if !ok {
|
||||
return resp.Resp401(c, nil)
|
||||
}
|
||||
|
||||
if err := _vf(op, privileges...); err != nil {
|
||||
return resp.Resp403(c, err.Error())
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/sqlType"
|
||||
)
|
||||
|
||||
func Init(db *gorm.DB) error {
|
||||
var err error
|
||||
|
||||
if err = initModel(db); err != nil {
|
||||
return fmt.Errorf("model.MustInit: init models err=%v", err)
|
||||
}
|
||||
|
||||
log.Info("MustInitModels: auto_migrate privilege model success")
|
||||
|
||||
if err = initData(db); err != nil {
|
||||
return fmt.Errorf("model.MustInit: init datas err=%v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initModel(client *gorm.DB) error {
|
||||
if err := client.AutoMigrate(
|
||||
&User{},
|
||||
&OpLog{},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("InitModels: auto_migrate user model success")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initData(client *gorm.DB) error {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
{
|
||||
count := 0
|
||||
|
||||
if err = client.Model(&User{}).Select("count(id)").Take(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count < len(initUsers) {
|
||||
log.Warn("mustInitDatas: user count = 0, start init...")
|
||||
for _, user := range initUsers {
|
||||
if err = client.Model(&User{}).Create(user).Error; err != nil {
|
||||
if !strings.Contains(err.Error(), "SQLSTATE 23505") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if opt.Cfg.DB.Type == "postgresql" {
|
||||
if err = client.Exec(`SELECT setval('users_id_seq', (SELECT MAX(id) FROM users))`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("InitDatas: creat init users success")
|
||||
} else {
|
||||
ps := make(sqlType.NumSlice[Privilege], 0)
|
||||
for _, item := range Privilege(0).All() {
|
||||
ps = append(ps, item.(Privilege))
|
||||
}
|
||||
if err = client.Model(&User{}).Where("id = ?", initUsers[0].Id).
|
||||
Updates(map[string]any{
|
||||
"privileges": ps,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("initDatas: update init users success")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"github.com/spf13/cast"
|
||||
"github.com/tdewolff/minify/v2"
|
||||
"github.com/tdewolff/minify/v2/html"
|
||||
"html/template"
|
||||
"time"
|
||||
"ultone/internal/sqlType"
|
||||
)
|
||||
|
||||
var (
|
||||
FuncMap = template.FuncMap{
|
||||
"time_format": func(mil any, format string) string {
|
||||
return time.UnixMilli(cast.ToInt64(mil)).Format(format)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
_ OpLogger = (*OpLogType)(nil)
|
||||
)
|
||||
|
||||
type OpLogType uint64
|
||||
|
||||
const (
|
||||
OpLogTypeLogin OpLogType = iota + 1
|
||||
OpLogTypeLogout
|
||||
OpLogTypeCreateUser
|
||||
OpLogTypeUpdateUser
|
||||
OpLogTypeDeleteUser
|
||||
|
||||
// todo: 添加自己的操作日志 分类
|
||||
)
|
||||
|
||||
func (o OpLogType) Value() int64 {
|
||||
return int64(o)
|
||||
}
|
||||
|
||||
func (o OpLogType) Code() string {
|
||||
switch o {
|
||||
case OpLogTypeLogin:
|
||||
return "login"
|
||||
case OpLogTypeLogout:
|
||||
return "logout"
|
||||
case OpLogTypeCreateUser:
|
||||
return "create_user"
|
||||
case OpLogTypeUpdateUser:
|
||||
return "update_user"
|
||||
case OpLogTypeDeleteUser:
|
||||
return "delete_user"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (o OpLogType) Label() string {
|
||||
switch o {
|
||||
case OpLogTypeLogin:
|
||||
return "登入"
|
||||
case OpLogTypeLogout:
|
||||
return "登出"
|
||||
case OpLogTypeCreateUser:
|
||||
return "创建用户"
|
||||
case OpLogTypeUpdateUser:
|
||||
return "修改用户"
|
||||
case OpLogTypeDeleteUser:
|
||||
return "删除用户"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
func (o OpLogType) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"value": o.Value(),
|
||||
"code": o.Code(),
|
||||
"label": o.Label(),
|
||||
})
|
||||
}
|
||||
|
||||
func (o OpLogType) All() []Enum {
|
||||
return []Enum{
|
||||
OpLogTypeLogin,
|
||||
OpLogTypeLogout,
|
||||
OpLogTypeCreateUser,
|
||||
OpLogTypeUpdateUser,
|
||||
OpLogTypeDeleteUser,
|
||||
}
|
||||
}
|
||||
|
||||
func _trimHTML(v []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(v)
|
||||
}
|
||||
|
||||
var (
|
||||
_mini = minify.New()
|
||||
)
|
||||
|
||||
func init() {
|
||||
_mini.AddFunc("text/html", html.Minify)
|
||||
}
|
||||
|
||||
func (o OpLogType) Render(content map[string]any) (string, error) {
|
||||
var (
|
||||
err error
|
||||
render *template.Template
|
||||
buf bytes.Buffer
|
||||
bs []byte
|
||||
)
|
||||
|
||||
if render, err = template.New(o.Code()).
|
||||
Funcs(FuncMap).
|
||||
Parse(o.Template()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = render.Execute(&buf, content); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if bs, err = _mini.Bytes("text/html", buf.Bytes()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return _trimHTML(bs), nil
|
||||
}
|
||||
|
||||
const (
|
||||
oplogTemplateLogin = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user="{{ .user_id }}"
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time="{{ .time }}"
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
在
|
||||
<span
|
||||
class="nf-op-log-ip nf-op-log-keyword"
|
||||
>{{ .ip }}
|
||||
</span>
|
||||
上
|
||||
<span
|
||||
class="nf-op-log-op nf-op-log-keyword"
|
||||
>
|
||||
登入
|
||||
</span>
|
||||
了系统
|
||||
</div>
|
||||
`
|
||||
oplogTemplateLogout = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user="{{ .user_id }}"
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time="{{ .time }}"
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
在
|
||||
<span
|
||||
class="nf-op-log-ip nf-op-log-keyword"
|
||||
>{{ .ip }}
|
||||
</span>
|
||||
上
|
||||
<span
|
||||
class="nf-op-log-op nf-op-log-keyword"
|
||||
>
|
||||
登出
|
||||
</span>
|
||||
了系统
|
||||
</div>
|
||||
`
|
||||
oplogTemplateCreateUser = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user="{{ .user_id }}"
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time="{{ .time }}"
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
<span class="nf-op-log-keyword">
|
||||
创建
|
||||
</span>
|
||||
了用户
|
||||
<span
|
||||
class="nf-op-log-target nf-op-log-keyword"
|
||||
nf-op-log-target="{{ .target_id }}"
|
||||
>{{ .target_username }}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
oplogTemplateUpdateUser = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user='{{ .user_id }}'
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time='{{ .time }}'
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
<span class="nf-op-log-keyword">
|
||||
编辑
|
||||
</span>
|
||||
了用户
|
||||
<span
|
||||
class="nf-op-log-target nf-op-log-keyword"
|
||||
nf-op-log-target="{{ .target_id }}"
|
||||
>{{ .target_username }}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
oplogTemplateDeleteUser = `
|
||||
<div class="nf-op-log">
|
||||
用户
|
||||
<span
|
||||
class="nf-op-log-user nf-op-log-keyword"
|
||||
nf-op-log-user="{{ .user_id }}"
|
||||
>{{ .username }}
|
||||
</span>
|
||||
于
|
||||
<span
|
||||
class="nf-op-log-time nf-op-log-keyword"
|
||||
nf-op-log-time="{{ .time }}"
|
||||
>{{ time_format .time "2006-01-02 15:04:05" }}
|
||||
</span>
|
||||
<span class="nf-op-log-keyword">
|
||||
删除
|
||||
</span>
|
||||
了用户
|
||||
<span
|
||||
class="nf-op-log-target nf-op-log-keyword"
|
||||
nf-op-log-target="{{ .target_id }}"
|
||||
>{{ .target_username }}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
|
||||
func (o OpLogType) Template() string {
|
||||
switch o {
|
||||
case OpLogTypeLogin:
|
||||
return oplogTemplateLogin
|
||||
case OpLogTypeLogout:
|
||||
return oplogTemplateLogout
|
||||
case OpLogTypeCreateUser:
|
||||
return oplogTemplateCreateUser
|
||||
case OpLogTypeUpdateUser:
|
||||
return oplogTemplateUpdateUser
|
||||
case OpLogTypeDeleteUser:
|
||||
return oplogTemplateDeleteUser
|
||||
default:
|
||||
return `<div>错误的日志类型</div>`
|
||||
}
|
||||
}
|
||||
|
||||
type OpLog struct {
|
||||
Id uint64 `json:"id" gorm:"primaryKey;column:id"`
|
||||
CreatedAt int64 `json:"created_at" gorm:"column:created_at;autoCreateTime:milli"`
|
||||
UpdatedAt int64 `json:"updated_at" gorm:"column:updated_at;autoUpdateTime:milli"`
|
||||
DeletedAt int64 `json:"deleted_at" gorm:"index;column:deleted_at;default:0"`
|
||||
|
||||
UserId uint64 `json:"user_id" gorm:"column:user_id"`
|
||||
Username string `json:"username" gorm:"column:username;varchar(128)"`
|
||||
Type OpLogType `json:"type" gorm:"column:type;type:varchar(128)"`
|
||||
Content sqlType.JSONB `json:"content" gorm:"column:content;type:jsonb"`
|
||||
HTML string `json:"html" gorm:"-"`
|
||||
}
|
||||
@@ -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)
|
||||
@@ -0,0 +1,85 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"gorm.io/gorm"
|
||||
"ultone/internal/opt"
|
||||
)
|
||||
|
||||
type _role struct {
|
||||
Value uint8 `json:"value"`
|
||||
Code string `json:"code"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
type Role uint8
|
||||
|
||||
var _ Enum = Role(0)
|
||||
|
||||
func (u Role) MarshalJSON() ([]byte, error) {
|
||||
m := _role{
|
||||
Value: uint8(u),
|
||||
Code: u.Code(),
|
||||
Label: u.Label(),
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
const (
|
||||
RoleRoot Role = 255
|
||||
RoleAdmin Role = 254
|
||||
RoleUser Role = 100
|
||||
)
|
||||
|
||||
func (u Role) Code() string {
|
||||
switch u {
|
||||
case RoleRoot:
|
||||
return "root"
|
||||
case RoleAdmin:
|
||||
return "admin"
|
||||
case RoleUser:
|
||||
return "user"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (u Role) Label() string {
|
||||
switch u {
|
||||
case RoleRoot:
|
||||
return "根用户"
|
||||
case RoleAdmin:
|
||||
return "管理员"
|
||||
case RoleUser:
|
||||
return "用户"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
func (u Role) Value() int64 {
|
||||
return int64(u)
|
||||
}
|
||||
|
||||
func (u Role) All() []Enum {
|
||||
return []Enum{
|
||||
RoleAdmin,
|
||||
RoleUser,
|
||||
}
|
||||
}
|
||||
|
||||
func (u Role) Where(db *gorm.DB) *gorm.DB {
|
||||
if opt.RoleMustLess {
|
||||
return db.Where("users.role < ?", u.Value())
|
||||
} else {
|
||||
return db.Where("users.role <= ?", u.Value())
|
||||
}
|
||||
}
|
||||
|
||||
func (u Role) CanOP(op *User) bool {
|
||||
if opt.RoleMustLess {
|
||||
return op.Role > u
|
||||
}
|
||||
|
||||
return op.Role >= u
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
"strings"
|
||||
"time"
|
||||
"ultone/internal/opt"
|
||||
"ultone/internal/sqlType"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
var (
|
||||
initUsers = []*User{
|
||||
{
|
||||
Id: 1,
|
||||
Username: "admin",
|
||||
Password: tool.NewPassword("123456"),
|
||||
Nickname: "admin",
|
||||
Role: RoleAdmin,
|
||||
Privileges: lo.Map(Privilege(0).All(), func(item Enum, index int) Privilege {
|
||||
return item.(Privilege)
|
||||
}),
|
||||
CreatedById: 1,
|
||||
CreatedByName: "admin",
|
||||
ActiveAt: time.Now().UnixMilli(),
|
||||
Deadline: time.Now().AddDate(100, 0, 0).UnixMilli(),
|
||||
},
|
||||
}
|
||||
|
||||
_ Enum = Status(0)
|
||||
)
|
||||
|
||||
type Status uint64
|
||||
|
||||
const (
|
||||
StatusNormal Status = iota
|
||||
StatusFrozen
|
||||
)
|
||||
|
||||
func (s Status) Value() int64 {
|
||||
return int64(s)
|
||||
}
|
||||
|
||||
func (s Status) Code() string {
|
||||
switch s {
|
||||
case StatusNormal:
|
||||
return "normal"
|
||||
case StatusFrozen:
|
||||
return "frozen"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (s Status) Label() string {
|
||||
switch s {
|
||||
case StatusNormal:
|
||||
return "正常"
|
||||
case StatusFrozen:
|
||||
return "冻结"
|
||||
default:
|
||||
return "异常"
|
||||
}
|
||||
}
|
||||
|
||||
func (s Status) All() []Enum {
|
||||
return []Enum{
|
||||
StatusNormal,
|
||||
StatusFrozen,
|
||||
}
|
||||
}
|
||||
|
||||
func (s Status) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"value": s.Value(),
|
||||
"code": s.Code(),
|
||||
"label": s.Label(),
|
||||
})
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Id uint64 `json:"id" gorm:"primaryKey;column:id"`
|
||||
CreatedAt int64 `json:"created_at" gorm:"column:created_at;autoCreateTime:milli"`
|
||||
UpdatedAt int64 `json:"updated_at" gorm:"column:updated_at;autoUpdateTime:milli"`
|
||||
DeletedAt int64 `json:"deleted_at" gorm:"index;column:deleted_at;default:0"`
|
||||
|
||||
Username string `json:"username" gorm:"column:username;type:varchar(64);unique"`
|
||||
Password string `json:"-" gorm:"column:password;type:varchar(256)"`
|
||||
|
||||
Status Status `json:"status" gorm:"column:status;default:0"`
|
||||
|
||||
Nickname string `json:"nickname" gorm:"column:nickname;type:varchar(64)"`
|
||||
Comment string `json:"comment" gorm:"column:comment"`
|
||||
|
||||
Role Role `json:"role" gorm:"column:role"`
|
||||
Privileges sqlType.NumSlice[Privilege] `json:"privileges" gorm:"column:privileges;type:bigint[]"`
|
||||
|
||||
CreatedById uint64 `json:"created_by_id" gorm:"column:created_by_id"`
|
||||
CreatedByName string `json:"created_by_name" gorm:"column:created_by_name;type:varchar(64)"`
|
||||
|
||||
ActiveAt int64 `json:"active_at" gorm:"column:active_at"`
|
||||
Deadline int64 `json:"deadline" gorm:"column:deadline"`
|
||||
|
||||
LoginAt int64 `json:"login_at" gorm:"-"`
|
||||
}
|
||||
|
||||
func (u *User) CheckStatus(mustOk bool) error {
|
||||
switch u.Status {
|
||||
case StatusNormal:
|
||||
case StatusFrozen:
|
||||
if mustOk {
|
||||
return errors.New("用户被冻结")
|
||||
}
|
||||
default:
|
||||
return errors.New("用户状态未知")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) IsValid(mustOk bool) error {
|
||||
now := time.Now()
|
||||
|
||||
if now.UnixMilli() >= u.Deadline {
|
||||
return errors.New("用户已过期")
|
||||
}
|
||||
|
||||
if now.UnixMilli() < u.ActiveAt {
|
||||
return errors.New("用户未启用")
|
||||
}
|
||||
|
||||
if u.DeletedAt > 0 {
|
||||
return errors.New("用户不存在")
|
||||
}
|
||||
|
||||
return u.CheckStatus(mustOk)
|
||||
}
|
||||
|
||||
func (u *User) JwtEncode() (token string, err error) {
|
||||
|
||||
now := time.Now()
|
||||
|
||||
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{
|
||||
"id": u.Id,
|
||||
"username": u.Username,
|
||||
"status": u.Status,
|
||||
"deadline": u.Deadline,
|
||||
"login_at": now.UnixMilli(),
|
||||
})
|
||||
|
||||
if token, err = jwtToken.SignedString([]byte(opt.JwtTokenSecret)); err != nil {
|
||||
err = fmt.Errorf("JwtEncode: jwt token signed secret err: %v", err)
|
||||
log.Error(err.Error())
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (u *User) FromJwt(token string) *User {
|
||||
var (
|
||||
ok bool
|
||||
err error
|
||||
pt *jwt.Token
|
||||
claims jwt.MapClaims
|
||||
)
|
||||
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
|
||||
if pt, err = jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok = t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
|
||||
return []byte(opt.JwtTokenSecret), nil
|
||||
}); err != nil {
|
||||
log.Error("jwt parse err: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !pt.Valid {
|
||||
log.Warn("parsed jwt invalid")
|
||||
return nil
|
||||
}
|
||||
|
||||
if claims, ok = pt.Claims.(jwt.MapClaims); !ok {
|
||||
log.Error("convert jwt claims err")
|
||||
return nil
|
||||
}
|
||||
|
||||
u.Id = cast.ToUint64(claims["user_id"])
|
||||
u.Username = cast.ToString(claims["username"])
|
||||
u.Status = Status(cast.ToInt64(claims["status"]))
|
||||
u.Deadline = cast.ToInt64(claims["deadline"])
|
||||
u.LoginAt = cast.ToInt64(claims["login_at"])
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
func (u User) MarshalBinary() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"id": u.Id,
|
||||
"created_at": u.CreatedAt,
|
||||
"updated_at": u.UpdatedAt,
|
||||
"deleted_at": u.DeletedAt,
|
||||
"username": u.Username,
|
||||
"status": u.Status.Value(),
|
||||
"nickname": u.Nickname,
|
||||
"comment": u.Comment,
|
||||
"role": uint8(u.Role),
|
||||
"privileges": lo.Map(u.Privileges, func(item Privilege, index int) int64 {
|
||||
return item.Value()
|
||||
}),
|
||||
"created_by_id": u.CreatedById,
|
||||
"created_by_name": u.CreatedByName,
|
||||
"active_at": u.ActiveAt,
|
||||
"deadline": u.Deadline,
|
||||
"login_at": u.LoginAt,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package opt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"os"
|
||||
"ultone/internal/tool"
|
||||
)
|
||||
|
||||
type listen struct {
|
||||
Http string `json:"http"`
|
||||
Grpc string `json:"grpc"`
|
||||
Unix string `json:"unix"`
|
||||
}
|
||||
|
||||
type db struct {
|
||||
Type string `json:"-"` // postgres, mysql, sqlite
|
||||
Uri string `json:"uri"`
|
||||
}
|
||||
|
||||
type cache struct {
|
||||
Uri string `json:"uri"`
|
||||
}
|
||||
|
||||
type es struct {
|
||||
Uri string `json:"uri"`
|
||||
Index struct {
|
||||
Staff string `json:"staff"`
|
||||
} `json:"index"`
|
||||
}
|
||||
|
||||
type Nebula struct {
|
||||
Uri string `json:"uri"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Space string `json:"space"`
|
||||
}
|
||||
|
||||
type config struct {
|
||||
Name string `json:"name"`
|
||||
Listen listen `json:"listen"`
|
||||
DB db `json:"db"`
|
||||
Cache cache `json:"cache"`
|
||||
ES es `json:"es"`
|
||||
Nebula Nebula `json:"nebula"`
|
||||
}
|
||||
|
||||
var (
|
||||
Debug bool
|
||||
Cfg = &config{}
|
||||
)
|
||||
|
||||
func Init(filename string) error {
|
||||
|
||||
var (
|
||||
err error
|
||||
bs []byte
|
||||
)
|
||||
|
||||
log.Info("opt.Init: start reading config file: %s", filename)
|
||||
|
||||
if bs, err = os.ReadFile(filename); err != nil {
|
||||
return fmt.Errorf("opt.Init: read config file=%s err=%v", filename, err)
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(bs, Cfg); err != nil {
|
||||
return fmt.Errorf("opt.Init: json marshal config=%s err=%v", string(bs), err)
|
||||
}
|
||||
|
||||
if Debug {
|
||||
log.SetLogLevel(log.LogLevelDebug)
|
||||
}
|
||||
|
||||
tool.TablePrinter(Cfg)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package opt
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
// todo: 可以替换自己生生成的 secret
|
||||
JwtTokenSecret = "7^D+UW3BPB2Mnz)bY3uVrAUyv&dj8Kdz"
|
||||
|
||||
// todo: 是否打开 gorm 的 debug 打印 (开发和 dev 环境时可以打开)
|
||||
DBDebug = true
|
||||
|
||||
// todo: 是否加载默认的前端用户管理界面
|
||||
EnableFront = false
|
||||
|
||||
// todo: 同一个账号是否可以多 client 登录
|
||||
MultiLogin = false
|
||||
|
||||
// todo: 用户量不大的情况, 并没有缓存用户具体信息, 如果需要可以打开
|
||||
EnableUserCache = true
|
||||
|
||||
// todo: 缓存时, key 的前缀
|
||||
CachePrefix = "ultone"
|
||||
|
||||
// todo: 登录颁发的 cookie 的 name
|
||||
CookieName = "utlone-token"
|
||||
|
||||
// todo: 用户列表,日志列表 size 参数
|
||||
DefaultSize, MaxSize = 20, 200
|
||||
|
||||
// todo: 操作用户时, role 相等时能否操作: 包括 列表, 能否新建,修改,删除同样 role 的用户
|
||||
RoleMustLess = false
|
||||
|
||||
// todo: 通过 c.Local() 存入 oplog 时的 key 值
|
||||
OpLogLocalKey = "oplog"
|
||||
|
||||
// todo: 操作日志 最多延迟多少秒写入(最多缓存多少秒的日志,然后 bulk 写入)
|
||||
OpLogWriteDurationSecond = 5
|
||||
|
||||
LocalTraceKey = "X-Trace"
|
||||
LocalShortTraceKey = "X-Short-Trace"
|
||||
)
|
||||
|
||||
var (
|
||||
// todo: 颁发的 token, (cookie) 在缓存中存在的时间 (每次请求该时间也会被刷新)
|
||||
TokenTimeout = time.Duration(3600*12) * time.Second
|
||||
)
|
||||
@@ -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")
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{}{}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Timeout(seconds ...int) (ctx context.Context) {
|
||||
var (
|
||||
duration time.Duration
|
||||
)
|
||||
|
||||
if len(seconds) > 0 && seconds[0] > 0 {
|
||||
duration = time.Duration(seconds[0]) * time.Second
|
||||
} else {
|
||||
duration = time.Duration(30) * time.Second
|
||||
}
|
||||
|
||||
ctx, _ = context.WithTimeout(context.Background(), duration)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TimeoutCtx(ctx context.Context, seconds ...int) context.Context {
|
||||
var (
|
||||
duration time.Duration
|
||||
)
|
||||
|
||||
if len(seconds) > 0 && seconds[0] > 0 {
|
||||
duration = time.Duration(seconds[0]) * time.Second
|
||||
} else {
|
||||
duration = time.Duration(30) * time.Second
|
||||
}
|
||||
|
||||
nctx, _ := context.WithTimeout(ctx, duration)
|
||||
|
||||
return nctx
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package tool
|
||||
|
||||
import "fmt"
|
||||
|
||||
func HumanDuration(nano int64) string {
|
||||
duration := float64(nano)
|
||||
unit := "ns"
|
||||
if duration >= 1000 {
|
||||
duration /= 1000
|
||||
unit = "us"
|
||||
}
|
||||
|
||||
if duration >= 1000 {
|
||||
duration /= 1000
|
||||
unit = "ms"
|
||||
}
|
||||
|
||||
if duration >= 1000 {
|
||||
duration /= 1000
|
||||
unit = " s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%6.2f%s", duration, unit)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package tool
|
||||
|
||||
import "github.com/loveuer/nf/nft/log"
|
||||
|
||||
func Must(errs ...error) {
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
log.Panic(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
EncryptHeader string = "pbkdf2:sha256" // 用户密码加密
|
||||
)
|
||||
|
||||
func NewPassword(password string) string {
|
||||
return EncryptPassword(password, RandomString(8), int(RandomInt(50000)+100000))
|
||||
}
|
||||
|
||||
func ComparePassword(in, db string) bool {
|
||||
strs := strings.Split(db, "$")
|
||||
if len(strs) != 3 {
|
||||
log.Error("password in db invalid: %s", db)
|
||||
return false
|
||||
}
|
||||
|
||||
encs := strings.Split(strs[0], ":")
|
||||
if len(encs) != 3 {
|
||||
log.Error("password in db invalid: %s", db)
|
||||
return false
|
||||
}
|
||||
|
||||
encIteration, err := strconv.Atoi(encs[2])
|
||||
if err != nil {
|
||||
log.Error("password in db invalid: %s, convert iter err: %s", db, err)
|
||||
return false
|
||||
}
|
||||
|
||||
return EncryptPassword(in, strs[1], encIteration) == db
|
||||
}
|
||||
|
||||
func EncryptPassword(password, salt string, iter int) string {
|
||||
hash := pbkdf2.Key([]byte(password), []byte(salt), iter, 32, sha256.New)
|
||||
encrypted := hex.EncodeToString(hash)
|
||||
return fmt.Sprintf("%s:%d$%s$%s", EncryptHeader, iter, salt, encrypted)
|
||||
}
|
||||
|
||||
func CheckPassword(password string) error {
|
||||
if len(password) < 8 || len(password) > 32 {
|
||||
return errors.New("密码长度不符合")
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
match bool
|
||||
patternList = []string{`[0-9]+`, `[a-z]+`, `[A-Z]+`, `[!@#%]+`} //, `[~!@#$%^&*?_-]+`}
|
||||
matchAccount = 0
|
||||
tips = []string{"缺少数字", "缺少小写字母", "缺少大写字母", "缺少'!@#%'"}
|
||||
locktips = make([]string, 0)
|
||||
)
|
||||
|
||||
for idx, pattern := range patternList {
|
||||
match, err = regexp.MatchString(pattern, password)
|
||||
if err != nil {
|
||||
log.Warn("regex match string err, reg_str: %s, err: %v", pattern, err)
|
||||
return errors.New("密码强度不够")
|
||||
}
|
||||
|
||||
if match {
|
||||
matchAccount++
|
||||
} else {
|
||||
locktips = append(locktips, tips[idx])
|
||||
}
|
||||
}
|
||||
|
||||
if matchAccount < 3 {
|
||||
return fmt.Errorf("密码强度不够, 可能 %s", strings.Join(locktips, ", "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package tool
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEncPassword(t *testing.T) {
|
||||
password := "123456"
|
||||
|
||||
result := EncryptPassword(password, RandomString(8), 50000)
|
||||
|
||||
t.Logf("sum => %s", result)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
var (
|
||||
letters = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
letterNum = []byte("0123456789")
|
||||
letterLow = []byte("abcdefghijklmnopqrstuvwxyz")
|
||||
letterCap = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
letterSyb = []byte("!@#$%^&*()_+-=")
|
||||
)
|
||||
|
||||
func RandomInt(max int64) int64 {
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(max))
|
||||
return num.Int64()
|
||||
}
|
||||
|
||||
func RandomString(length int) string {
|
||||
result := make([]byte, length)
|
||||
for i := 0; i < length; i++ {
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
|
||||
result[i] = letters[num.Int64()]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func RandomPassword(length int, withSymbol bool) string {
|
||||
result := make([]byte, length)
|
||||
kind := 3
|
||||
if withSymbol {
|
||||
kind++
|
||||
}
|
||||
|
||||
for i := 0; i < length; i++ {
|
||||
switch i % kind {
|
||||
case 0:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterNum))))
|
||||
result[i] = letterNum[num.Int64()]
|
||||
case 1:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterLow))))
|
||||
result[i] = letterLow[num.Int64()]
|
||||
case 2:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterCap))))
|
||||
result[i] = letterCap[num.Int64()]
|
||||
case 3:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterSyb))))
|
||||
result[i] = letterSyb[num.Int64()]
|
||||
}
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func TablePrinter(data any, writers ...io.Writer) {
|
||||
var w io.Writer = os.Stdout
|
||||
if len(writers) > 0 && writers[0] != nil {
|
||||
w = writers[0]
|
||||
}
|
||||
|
||||
t := table.NewWriter()
|
||||
structPrinter(t, "", data)
|
||||
_, _ = fmt.Fprintln(w, t.Render())
|
||||
}
|
||||
|
||||
func structPrinter(w table.Writer, prefix string, item any) {
|
||||
Start:
|
||||
rv := reflect.ValueOf(item)
|
||||
if rv.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
for rv.Type().Kind() == reflect.Pointer {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
|
||||
switch rv.Type().Kind() {
|
||||
case reflect.Invalid,
|
||||
reflect.Uintptr,
|
||||
reflect.Chan,
|
||||
reflect.Func,
|
||||
reflect.UnsafePointer:
|
||||
case reflect.Bool,
|
||||
reflect.Int,
|
||||
reflect.Int8,
|
||||
reflect.Int16,
|
||||
reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint,
|
||||
reflect.Uint8,
|
||||
reflect.Uint16,
|
||||
reflect.Uint32,
|
||||
reflect.Uint64,
|
||||
reflect.Float32,
|
||||
reflect.Float64,
|
||||
reflect.Complex64,
|
||||
reflect.Complex128,
|
||||
reflect.Interface,
|
||||
reflect.String:
|
||||
w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), rv.Interface()})
|
||||
case reflect.Array, reflect.Slice:
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
p := strings.Join([]string{prefix, fmt.Sprintf("[%d]", i)}, ".")
|
||||
structPrinter(w, p, rv.Index(i).Interface())
|
||||
}
|
||||
case reflect.Map:
|
||||
for _, k := range rv.MapKeys() {
|
||||
structPrinter(w, fmt.Sprintf("%s.{%v}", prefix, k), rv.MapIndex(k).Interface())
|
||||
}
|
||||
case reflect.Pointer:
|
||||
goto Start
|
||||
case reflect.Struct:
|
||||
for i := 0; i < rv.NumField(); i++ {
|
||||
p := fmt.Sprintf("%s.%s", prefix, rv.Type().Field(i).Name)
|
||||
field := rv.Field(i)
|
||||
|
||||
//log.Debug("TablePrinter: prefix: %s, field: %v", p, rv.Field(i))
|
||||
|
||||
if !field.CanInterface() {
|
||||
return
|
||||
}
|
||||
|
||||
structPrinter(w, p, field.Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TableMapPrinter(data []byte) {
|
||||
m := make(map[string]any)
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
log.Warn(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
t := table.NewWriter()
|
||||
addRow(t, "", m)
|
||||
fmt.Println(t.Render())
|
||||
}
|
||||
|
||||
func addRow(w table.Writer, prefix string, m any) {
|
||||
rv := reflect.ValueOf(m)
|
||||
switch rv.Type().Kind() {
|
||||
case reflect.Map:
|
||||
for _, k := range rv.MapKeys() {
|
||||
key := k.String()
|
||||
if prefix != "" {
|
||||
key = strings.Join([]string{prefix, k.String()}, ".")
|
||||
}
|
||||
addRow(w, key, rv.MapIndex(k).Interface())
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
addRow(w, fmt.Sprintf("%s[%d]", prefix, i), rv.Index(i).Interface())
|
||||
}
|
||||
default:
|
||||
w.AppendRow(table.Row{prefix, m})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package tool
|
||||
|
||||
import "cmp"
|
||||
|
||||
func Min[T cmp.Ordered](a, b T) T {
|
||||
if a <= b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func Max[T cmp.Ordered](a, b T) T {
|
||||
if a >= b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||