wip: 继续...
This commit is contained in:
@ -1,11 +1,25 @@
|
||||
package cmd
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"uauth/internal/serve"
|
||||
)
|
||||
|
||||
func initServe() *cobra.Command {
|
||||
serve := &cobra.Command{
|
||||
Use: "serve",
|
||||
var (
|
||||
address string
|
||||
prefix string
|
||||
)
|
||||
|
||||
svc := &cobra.Command{
|
||||
Use: "svc",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return serve.Run(cmd.Context(), prefix, address)
|
||||
},
|
||||
}
|
||||
|
||||
return serve
|
||||
svc.Flags().StringVar(&address, "address", "localhost:8080", "listen address")
|
||||
svc.Flags().StringVar(&prefix, "prefix", "/api/oauth/v2", "api prefix")
|
||||
|
||||
return svc
|
||||
}
|
||||
|
22
internal/interfaces/database.go
Normal file
22
internal/interfaces/database.go
Normal file
@ -0,0 +1,22 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Cacher interface {
|
||||
Get(ctx context.Context, key string) ([]byte, error)
|
||||
GetScan(ctx context.Context, key string) Scanner
|
||||
GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error)
|
||||
GetExScan(ctx context.Context, key string, duration time.Duration) Scanner
|
||||
// Set value 会被序列化, 优先使用 MarshalBinary 方法, 没有则执行 json.Marshal
|
||||
Set(ctx context.Context, key string, value any) error
|
||||
// SetEx value 会被序列化, 优先使用 MarshalBinary 方法, 没有则执行 json.Marshal
|
||||
SetEx(ctx context.Context, key string, value any, duration time.Duration) error
|
||||
Del(ctx context.Context, keys ...string) error
|
||||
}
|
||||
|
||||
type Scanner interface {
|
||||
Scan(model any) error
|
||||
}
|
11
internal/interfaces/enum.go
Normal file
11
internal/interfaces/enum.go
Normal file
@ -0,0 +1,11 @@
|
||||
package interfaces
|
||||
|
||||
type Enum interface {
|
||||
Value() int64
|
||||
Code() string
|
||||
Label() string
|
||||
|
||||
MarshalJSON() ([]byte, error)
|
||||
|
||||
All() []Enum
|
||||
}
|
7
internal/interfaces/logger.go
Normal file
7
internal/interfaces/logger.go
Normal file
@ -0,0 +1,7 @@
|
||||
package interfaces
|
||||
|
||||
type OpLogger interface {
|
||||
Enum
|
||||
Render(content map[string]any) (string, error)
|
||||
Template() string
|
||||
}
|
144
internal/serve/serve.go
Normal file
144
internal/serve/serve.go
Normal file
@ -0,0 +1,144 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"net/http"
|
||||
"uauth/internal/tool"
|
||||
)
|
||||
|
||||
func authenticateUser(username, password string) (bool, error) {
|
||||
// 这里你应该实现真实的用户认证逻辑
|
||||
// 为了简化,我们这里直接硬编码一个用户名和密码
|
||||
if username == "user" && password == "pass" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("invalid username or password")
|
||||
}
|
||||
|
||||
// 处理登录请求
|
||||
func handleLogin(c *nf.Ctx) error {
|
||||
username := c.FormValue("username")
|
||||
password := c.FormValue("password")
|
||||
|
||||
// 认证用户
|
||||
ok, err := authenticateUser(username, password)
|
||||
if err != nil || !ok {
|
||||
return c.Status(http.StatusUnauthorized).SendString("Unauthorized")
|
||||
}
|
||||
|
||||
// 用户认证成功,重定向到授权页面
|
||||
http.Redirect(c.Writer, c.Request, "/authorize?client_id=12345&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback&scope=read%20write", http.StatusFound)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 处理授权请求
|
||||
func handleAuthorize(c *nf.Ctx) error {
|
||||
// 解析查询参数
|
||||
clientID := c.Query("client_id")
|
||||
responseType := c.Query("response_type")
|
||||
redirectURI := c.Query("redirect_uri")
|
||||
scope := c.Query("scope")
|
||||
|
||||
// 检查客户端 ID 和其他参数
|
||||
// 在实际应用中,你需要检查这些参数是否合法
|
||||
if clientID != "12345" || responseType != "code" || redirectURI != "http://localhost:8080/callback" {
|
||||
return c.Status(http.StatusBadRequest).SendString("Invalid request")
|
||||
}
|
||||
|
||||
// 显示授权页面给用户
|
||||
_, err := c.Write([]byte(`
|
||||
<html>
|
||||
<head><title>Authorization</title></head>
|
||||
<body>
|
||||
<h1>Do you want to authorize this application?</h1>
|
||||
<form action="/approve" method="post">
|
||||
<input type="hidden" name="client_id" value="` + clientID + `"/>
|
||||
<input type="hidden" name="redirect_uri" value="` + redirectURI + `"/>
|
||||
<input type="hidden" name="scope" value="` + scope + `"/>
|
||||
<button type="submit">Yes, I authorize</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// 处理用户的授权批准
|
||||
func handleApprove(c *nf.Ctx) error {
|
||||
// 获取表单数据
|
||||
clientID := c.FormValue("client_id")
|
||||
redirectURI := c.FormValue("redirect_uri")
|
||||
scope := c.FormValue("scope")
|
||||
|
||||
// 生成授权码
|
||||
authorizationCode := uuid.New().String()[:8]
|
||||
|
||||
log.Info("[D] client_id = %s, scope = %s, auth_code = %s", clientID, scope, authorizationCode)
|
||||
|
||||
// 重定向到回调 URL 并附带授权码
|
||||
http.Redirect(c.Writer, c.Request, redirectURI+"?code="+authorizationCode, http.StatusFound)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 令牌请求的处理
|
||||
func handleToken(c *nf.Ctx) error {
|
||||
// 获取请求参数
|
||||
grantType := c.FormValue("grant_type")
|
||||
code := c.FormValue("code")
|
||||
redirectURI := c.FormValue("redirect_uri")
|
||||
|
||||
// 简单验证
|
||||
if grantType != "authorization_code" {
|
||||
return c.Status(http.StatusBadRequest).SendString("Unsupported grant type")
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
// 验证授权码是否有效
|
||||
accessToken, ok := authCodes[code]
|
||||
if !ok {
|
||||
return c.Status(http.StatusBadRequest).SendString("Invalid authorization code")
|
||||
}
|
||||
|
||||
// 生成访问令牌
|
||||
token := generateAccessToken()
|
||||
|
||||
// 返回访问令牌
|
||||
return c.JSON(map[string]string{
|
||||
"access_token": token,
|
||||
"token_type": "bearer",
|
||||
"expires_in": "3600", // 访问令牌有效期(秒)
|
||||
})
|
||||
|
||||
// 清除已使用的授权码
|
||||
delete(authCodes, code)
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, prefix string, address string) error {
|
||||
|
||||
app := nf.New()
|
||||
|
||||
api := app.Group(prefix)
|
||||
// 设置路由
|
||||
api.Get("/login", handleLogin)
|
||||
api.Get("/authorize", handleAuthorize)
|
||||
api.Post("/approve", handleApprove)
|
||||
api.Post("/token", handleToken)
|
||||
|
||||
// 启动 HTTP 服务器
|
||||
log.Info("Starting server on: %s", address)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = app.Shutdown(tool.Timeout(2))
|
||||
}()
|
||||
|
||||
return app.Run(address)
|
||||
}
|
117
internal/store/cache/cache_lru.go
vendored
Normal file
117
internal/store/cache/cache_lru.go
vendored
Normal file
@ -0,0 +1,117 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/hashicorp/golang-lru/v2/expirable"
|
||||
_ "github.com/hashicorp/golang-lru/v2/expirable"
|
||||
"time"
|
||||
"uauth/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) GetScan(ctx context.Context, key string) interfaces.Scanner {
|
||||
return newScanner(l.Get(ctx, key))
|
||||
}
|
||||
|
||||
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) GetExScan(ctx context.Context, key string, duration time.Duration) interfaces.Scanner {
|
||||
return newScanner(l.GetEx(ctx, key, duration))
|
||||
}
|
||||
|
||||
func (l *_lru) Set(ctx context.Context, key string, value any) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.client.Add(key, &_lru_value{
|
||||
duration: 0,
|
||||
last: time.Now(),
|
||||
bs: bs,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *_lru) SetEx(ctx context.Context, key string, value any, duration time.Duration) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.client.Add(key, &_lru_value{
|
||||
duration: duration,
|
||||
last: time.Now(),
|
||||
bs: bs,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *_lru) Del(ctx context.Context, keys ...string) error {
|
||||
for _, key := range keys {
|
||||
l.client.Remove(key)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newLRUCache() (interfaces.Cacher, error) {
|
||||
client := expirable.NewLRU[string, *_lru_value](1024*1024, nil, 0)
|
||||
|
||||
return &_lru{client: client}, nil
|
||||
}
|
82
internal/store/cache/cache_memory.go
vendored
Normal file
82
internal/store/cache/cache_memory.go
vendored
Normal file
@ -0,0 +1,82 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
"uauth/internal/interfaces"
|
||||
|
||||
"gitea.com/taozitaozi/gredis"
|
||||
)
|
||||
|
||||
var _ interfaces.Cacher = (*_mem)(nil)
|
||||
|
||||
type _mem struct {
|
||||
client *gredis.Gredis
|
||||
}
|
||||
|
||||
func (m *_mem) GetScan(ctx context.Context, key string) interfaces.Scanner {
|
||||
return newScanner(m.Get(ctx, key))
|
||||
}
|
||||
|
||||
func (m *_mem) GetExScan(ctx context.Context, key string, duration time.Duration) interfaces.Scanner {
|
||||
return newScanner(m.GetEx(ctx, key, duration))
|
||||
}
|
||||
|
||||
func (m *_mem) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
v, err := m.client.Get(key)
|
||||
if err != nil {
|
||||
if errors.Is(err, gredis.ErrKeyNotFound) {
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bs, ok := v.([]byte)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid value type=%T", v)
|
||||
}
|
||||
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
func (m *_mem) GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error) {
|
||||
v, err := m.client.GetEx(key, duration)
|
||||
if err != nil {
|
||||
if errors.Is(err, gredis.ErrKeyNotFound) {
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bs, ok := v.([]byte)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid value type=%T", v)
|
||||
}
|
||||
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
func (m *_mem) Set(ctx context.Context, key string, value any) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.client.Set(key, bs)
|
||||
}
|
||||
|
||||
func (m *_mem) SetEx(ctx context.Context, key string, value any, duration time.Duration) error {
|
||||
bs, err := handleValue(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.client.SetEx(key, bs, duration)
|
||||
}
|
||||
|
||||
func (m *_mem) Del(ctx context.Context, keys ...string) error {
|
||||
m.client.Delete(keys...)
|
||||
return nil
|
||||
}
|
71
internal/store/cache/cache_redis.go
vendored
Normal file
71
internal/store/cache/cache_redis.go
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
"uauth/internal/interfaces"
|
||||
)
|
||||
|
||||
type _redis struct {
|
||||
client *redis.Client
|
||||
}
|
||||
|
||||
func (r *_redis) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
result, err := r.client.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(result), nil
|
||||
}
|
||||
|
||||
func (r *_redis) GetScan(ctx context.Context, key string) interfaces.Scanner {
|
||||
return newScanner(r.Get(ctx, key))
|
||||
}
|
||||
|
||||
func (r *_redis) GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error) {
|
||||
result, err := r.client.GetEx(ctx, key, duration).Result()
|
||||
if err != nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return nil, ErrorKeyNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(result), nil
|
||||
}
|
||||
|
||||
func (r *_redis) GetExScan(ctx context.Context, key string, duration time.Duration) interfaces.Scanner {
|
||||
return newScanner(r.GetEx(ctx, key, duration))
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
38
internal/store/cache/client.go
vendored
Normal file
38
internal/store/cache/client.go
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"uauth/internal/interfaces"
|
||||
)
|
||||
|
||||
var (
|
||||
Client interfaces.Cacher
|
||||
)
|
||||
|
||||
type encoded_value interface {
|
||||
MarshalBinary() ([]byte, error)
|
||||
}
|
||||
|
||||
type decoded_value interface {
|
||||
UnmarshalBinary(bs []byte) error
|
||||
}
|
||||
|
||||
func handleValue(value any) ([]byte, error) {
|
||||
var (
|
||||
bs []byte
|
||||
err error
|
||||
)
|
||||
|
||||
switch value.(type) {
|
||||
case []byte:
|
||||
return value.([]byte), nil
|
||||
}
|
||||
|
||||
if imp, ok := value.(encoded_value); ok {
|
||||
bs, err = imp.MarshalBinary()
|
||||
} else {
|
||||
bs, err = json.Marshal(value)
|
||||
}
|
||||
|
||||
return bs, err
|
||||
}
|
7
internal/store/cache/error.go
vendored
Normal file
7
internal/store/cache/error.go
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
package cache
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrorKeyNotFound = errors.New("key not found")
|
||||
)
|
69
internal/store/cache/init.go
vendored
Normal file
69
internal/store/cache/init.go
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitea.com/taozitaozi/gredis"
|
||||
"net/url"
|
||||
"strings"
|
||||
"uauth/internal/opt"
|
||||
"uauth/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
|
||||
}
|
20
internal/store/cache/scan.go
vendored
Normal file
20
internal/store/cache/scan.go
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
package cache
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type scanner struct {
|
||||
err error
|
||||
bs []byte
|
||||
}
|
||||
|
||||
func (s *scanner) Scan(model any) error {
|
||||
if s.err != nil {
|
||||
return s.err
|
||||
}
|
||||
|
||||
return json.Unmarshal(s.bs, model)
|
||||
}
|
||||
|
||||
func newScanner(bs []byte, err error) *scanner {
|
||||
return &scanner{bs: bs, err: err}
|
||||
}
|
38
internal/tool/ctx.go
Normal file
38
internal/tool/ctx.go
Normal file
@ -0,0 +1,38 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Timeout(seconds ...int) (ctx context.Context) {
|
||||
var (
|
||||
duration time.Duration
|
||||
)
|
||||
|
||||
if len(seconds) > 0 && seconds[0] > 0 {
|
||||
duration = time.Duration(seconds[0]) * time.Second
|
||||
} else {
|
||||
duration = time.Duration(30) * time.Second
|
||||
}
|
||||
|
||||
ctx, _ = context.WithTimeout(context.Background(), duration)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TimeoutCtx(ctx context.Context, seconds ...int) context.Context {
|
||||
var (
|
||||
duration time.Duration
|
||||
)
|
||||
|
||||
if len(seconds) > 0 && seconds[0] > 0 {
|
||||
duration = time.Duration(seconds[0]) * time.Second
|
||||
} else {
|
||||
duration = time.Duration(30) * time.Second
|
||||
}
|
||||
|
||||
nctx, _ := context.WithTimeout(ctx, duration)
|
||||
|
||||
return nctx
|
||||
}
|
30
internal/tool/file.go
Normal file
30
internal/tool/file.go
Normal file
@ -0,0 +1,30 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func CopyFile(src string, dst string) (err error) {
|
||||
// Open the source file
|
||||
sourceFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
|
||||
// Create the destination file
|
||||
destinationFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destinationFile.Close()
|
||||
|
||||
// Copy the contents from source to destination
|
||||
_, err = io.Copy(destinationFile, sourceFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
24
internal/tool/human.go
Normal file
24
internal/tool/human.go
Normal file
@ -0,0 +1,24 @@
|
||||
package tool
|
||||
|
||||
import "fmt"
|
||||
|
||||
func HumanDuration(nano int64) string {
|
||||
duration := float64(nano)
|
||||
unit := "ns"
|
||||
if duration >= 1000 {
|
||||
duration /= 1000
|
||||
unit = "us"
|
||||
}
|
||||
|
||||
if duration >= 1000 {
|
||||
duration /= 1000
|
||||
unit = "ms"
|
||||
}
|
||||
|
||||
if duration >= 1000 {
|
||||
duration /= 1000
|
||||
unit = " s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%6.2f%s", duration, unit)
|
||||
}
|
11
internal/tool/must.go
Normal file
11
internal/tool/must.go
Normal file
@ -0,0 +1,11 @@
|
||||
package tool
|
||||
|
||||
import "github.com/loveuer/nf/nft/log"
|
||||
|
||||
func Must(errs ...error) {
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
log.Panic(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
84
internal/tool/password.go
Normal file
84
internal/tool/password.go
Normal file
@ -0,0 +1,84 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
EncryptHeader string = "pbkdf2:sha256" // 用户密码加密
|
||||
)
|
||||
|
||||
func NewPassword(password string) string {
|
||||
return EncryptPassword(password, RandomString(8), int(RandomInt(50000)+100000))
|
||||
}
|
||||
|
||||
func ComparePassword(in, db string) bool {
|
||||
strs := strings.Split(db, "$")
|
||||
if len(strs) != 3 {
|
||||
log.Error("password in db invalid: %s", db)
|
||||
return false
|
||||
}
|
||||
|
||||
encs := strings.Split(strs[0], ":")
|
||||
if len(encs) != 3 {
|
||||
log.Error("password in db invalid: %s", db)
|
||||
return false
|
||||
}
|
||||
|
||||
encIteration, err := strconv.Atoi(encs[2])
|
||||
if err != nil {
|
||||
log.Error("password in db invalid: %s, convert iter err: %s", db, err)
|
||||
return false
|
||||
}
|
||||
|
||||
return EncryptPassword(in, strs[1], encIteration) == db
|
||||
}
|
||||
|
||||
func EncryptPassword(password, salt string, iter int) string {
|
||||
hash := pbkdf2.Key([]byte(password), []byte(salt), iter, 32, sha256.New)
|
||||
encrypted := hex.EncodeToString(hash)
|
||||
return fmt.Sprintf("%s:%d$%s$%s", EncryptHeader, iter, salt, encrypted)
|
||||
}
|
||||
|
||||
func CheckPassword(password string) error {
|
||||
if len(password) < 8 || len(password) > 32 {
|
||||
return errors.New("密码长度不符合")
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
match bool
|
||||
patternList = []string{`[0-9]+`, `[a-z]+`, `[A-Z]+`, `[!@#%]+`} //, `[~!@#$%^&*?_-]+`}
|
||||
matchAccount = 0
|
||||
tips = []string{"缺少数字", "缺少小写字母", "缺少大写字母", "缺少'!@#%'"}
|
||||
locktips = make([]string, 0)
|
||||
)
|
||||
|
||||
for idx, pattern := range patternList {
|
||||
match, err = regexp.MatchString(pattern, password)
|
||||
if err != nil {
|
||||
log.Warn("regex match string err, reg_str: %s, err: %v", pattern, err)
|
||||
return errors.New("密码强度不够")
|
||||
}
|
||||
|
||||
if match {
|
||||
matchAccount++
|
||||
} else {
|
||||
locktips = append(locktips, tips[idx])
|
||||
}
|
||||
}
|
||||
|
||||
if matchAccount < 3 {
|
||||
return fmt.Errorf("密码强度不够, 可能 %s", strings.Join(locktips, ", "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
11
internal/tool/password_test.go
Normal file
11
internal/tool/password_test.go
Normal file
@ -0,0 +1,11 @@
|
||||
package tool
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEncPassword(t *testing.T) {
|
||||
password := "123456"
|
||||
|
||||
result := EncryptPassword(password, RandomString(8), 50000)
|
||||
|
||||
t.Logf("sum => %s", result)
|
||||
}
|
54
internal/tool/random.go
Normal file
54
internal/tool/random.go
Normal file
@ -0,0 +1,54 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
var (
|
||||
letters = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
letterNum = []byte("0123456789")
|
||||
letterLow = []byte("abcdefghijklmnopqrstuvwxyz")
|
||||
letterCap = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
letterSyb = []byte("!@#$%^&*()_+-=")
|
||||
)
|
||||
|
||||
func RandomInt(max int64) int64 {
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(max))
|
||||
return num.Int64()
|
||||
}
|
||||
|
||||
func RandomString(length int) string {
|
||||
result := make([]byte, length)
|
||||
for i := 0; i < length; i++ {
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
|
||||
result[i] = letters[num.Int64()]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func RandomPassword(length int, withSymbol bool) string {
|
||||
result := make([]byte, length)
|
||||
kind := 3
|
||||
if withSymbol {
|
||||
kind++
|
||||
}
|
||||
|
||||
for i := 0; i < length; i++ {
|
||||
switch i % kind {
|
||||
case 0:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterNum))))
|
||||
result[i] = letterNum[num.Int64()]
|
||||
case 1:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterLow))))
|
||||
result[i] = letterLow[num.Int64()]
|
||||
case 2:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterCap))))
|
||||
result[i] = letterCap[num.Int64()]
|
||||
case 3:
|
||||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterSyb))))
|
||||
result[i] = letterSyb[num.Int64()]
|
||||
}
|
||||
}
|
||||
return string(result)
|
||||
}
|
5
internal/tool/slice.go
Normal file
5
internal/tool/slice.go
Normal file
@ -0,0 +1,5 @@
|
||||
package tool
|
||||
|
||||
func Bulk[T any](slice []T, size int) {
|
||||
// todo
|
||||
}
|
1
internal/tool/slice_test.go
Normal file
1
internal/tool/slice_test.go
Normal file
@ -0,0 +1 @@
|
||||
package tool
|
124
internal/tool/table.go
Normal file
124
internal/tool/table.go
Normal file
@ -0,0 +1,124 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func TablePrinter(data any, writers ...io.Writer) {
|
||||
var w io.Writer = os.Stdout
|
||||
if len(writers) > 0 && writers[0] != nil {
|
||||
w = writers[0]
|
||||
}
|
||||
|
||||
t := table.NewWriter()
|
||||
structPrinter(t, "", data)
|
||||
_, _ = fmt.Fprintln(w, t.Render())
|
||||
}
|
||||
|
||||
func structPrinter(w table.Writer, prefix string, item any) {
|
||||
Start:
|
||||
rv := reflect.ValueOf(item)
|
||||
if rv.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
for rv.Type().Kind() == reflect.Pointer {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
|
||||
switch rv.Type().Kind() {
|
||||
case reflect.Invalid,
|
||||
reflect.Uintptr,
|
||||
reflect.Chan,
|
||||
reflect.Func,
|
||||
reflect.UnsafePointer:
|
||||
case reflect.Bool,
|
||||
reflect.Int,
|
||||
reflect.Int8,
|
||||
reflect.Int16,
|
||||
reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint,
|
||||
reflect.Uint8,
|
||||
reflect.Uint16,
|
||||
reflect.Uint32,
|
||||
reflect.Uint64,
|
||||
reflect.Float32,
|
||||
reflect.Float64,
|
||||
reflect.Complex64,
|
||||
reflect.Complex128,
|
||||
reflect.Interface:
|
||||
w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), rv.Interface()})
|
||||
case reflect.String:
|
||||
val := rv.String()
|
||||
if len(val) <= 160 {
|
||||
w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), val})
|
||||
return
|
||||
}
|
||||
|
||||
w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), val[0:64] + "..." + val[len(val)-64:]})
|
||||
case reflect.Array, reflect.Slice:
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
p := strings.Join([]string{prefix, fmt.Sprintf("[%d]", i)}, ".")
|
||||
structPrinter(w, p, rv.Index(i).Interface())
|
||||
}
|
||||
case reflect.Map:
|
||||
for _, k := range rv.MapKeys() {
|
||||
structPrinter(w, fmt.Sprintf("%s.{%v}", prefix, k), rv.MapIndex(k).Interface())
|
||||
}
|
||||
case reflect.Pointer:
|
||||
goto Start
|
||||
case reflect.Struct:
|
||||
for i := 0; i < rv.NumField(); i++ {
|
||||
p := fmt.Sprintf("%s.%s", prefix, rv.Type().Field(i).Name)
|
||||
field := rv.Field(i)
|
||||
|
||||
//log.Debug("TablePrinter: prefix: %s, field: %v", p, rv.Field(i))
|
||||
|
||||
if !field.CanInterface() {
|
||||
return
|
||||
}
|
||||
|
||||
structPrinter(w, p, field.Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TableMapPrinter(data []byte) {
|
||||
m := make(map[string]any)
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
log.Warn(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
t := table.NewWriter()
|
||||
addRow(t, "", m)
|
||||
fmt.Println(t.Render())
|
||||
}
|
||||
|
||||
func addRow(w table.Writer, prefix string, m any) {
|
||||
rv := reflect.ValueOf(m)
|
||||
switch rv.Type().Kind() {
|
||||
case reflect.Map:
|
||||
for _, k := range rv.MapKeys() {
|
||||
key := k.String()
|
||||
if prefix != "" {
|
||||
key = strings.Join([]string{prefix, k.String()}, ".")
|
||||
}
|
||||
addRow(w, key, rv.MapIndex(k).Interface())
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
addRow(w, fmt.Sprintf("%s[%d]", prefix, i), rv.Index(i).Interface())
|
||||
}
|
||||
default:
|
||||
w.AppendRow(table.Row{prefix, m})
|
||||
}
|
||||
}
|
13
internal/tool/time.go
Normal file
13
internal/tool/time.go
Normal file
@ -0,0 +1,13 @@
|
||||
package tool
|
||||
|
||||
import "time"
|
||||
|
||||
// TodayMidnight 返回今日凌晨
|
||||
func TodayMidnight() (midnight time.Time) {
|
||||
now := time.Now()
|
||||
|
||||
year, month, day := now.Date()
|
||||
midnight = time.Date(year, month, day, 0, 0, 0, 0, time.Local)
|
||||
|
||||
return
|
||||
}
|
Reference in New Issue
Block a user