Compare commits

...

No commits in common. "main" and "fb97082c0c419de38818fc182dfc9daf9531b628" have entirely different histories.

336 changed files with 10872 additions and 6274 deletions

View File

@ -1,19 +0,0 @@
kind: pipeline
type: docker
name: wireguard-dashboard
trigger:
event: [tag]
steps:
- name: builder
image: plugins/docker
settings:
registry: gitea.mrx.ltd # 镜像仓库地址
repo: gitea.mrx.ltd/go-pkg/wireguard-dashboard # 镜像仓库地址
username:
from_secret: docker_user
password:
from_secret: docker_pwd
use_cache: true
auto_tag: true

127
.gitignore vendored
View File

@ -1,5 +1,84 @@
### GoLand+all template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Go template ### Go template
# If you prefer the allow list template_data instead of the deny list, see community template_data: # If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
# #
# Binaries for programs and plugins # Binaries for programs and plugins
@ -17,21 +96,39 @@
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
.idea
logs
web/assets
web/static
web/*.ico
web/*.gz
web/*.br
web/*.html
web/*.svg
web/*.json
# Go workspace file # Go workspace file
go.work go.work
wg.db
wg0.conf .idea
web/.idea
web/node_modules
web/.DS_Store
web/dist
web/dist-ssr
web/*.local
web/.eslintcache
web/report.html
web/vite.config.*.timestamp*
web/yarn.lock
web/npm-debug.log*
web/.pnpm-error.log*
web/.pnpm-debug.log
web/tests/**/coverage/
# Editor directories and files
web/*.suo
web/*.ntvs*
web/*.njsproj
web/*.sln
web/tsconfig.tsbuildinfo
template/tmp/*
logs/*
app.yaml
*.db *.db
*.yaml .env
*.env

View File

@ -1,45 +0,0 @@
# 打包前端
FROM node:18-alpine as build-stage
WORKDIR front
COPY . .
WORKDIR web-src
RUN corepack enable
RUN corepack prepare pnpm@8.6.10 --activate
RUN npm config set registry https://registry.npmmirror.com
RUN pnpm install
RUN pnpm build
# 前后端集成打包
FROM golang:alpine as build
RUN apk add upx
WORKDIR /build
COPY . .
COPY --from=build-stage /front/web/ /build/web
# sqlite必须
ENV GO111MODULE=on
ENV GOPROXY=https://goproxy.cn,direct
RUN go build -ldflags="-s -w" -o wgui && upx -9 wgui
RUN ls -lh && chmod +x ./wgui
FROM alpine
RUN addgroup -S wgui && \
adduser -S -D -G wgui wgui
ENV GIN_MODE=release
RUN apk --no-cache add ca-certificates wireguard-tools jq iptables
WORKDIR /app
RUN mkdir -p db
COPY --from=build --chown=wgui:wgui /build/wgui /app
COPY --from=build /build/template/* /app/template/
RUN chmod +x wgui
ENTRYPOINT ["./wgui"]

View File

View File

@ -5,14 +5,14 @@ import (
"gitee.ltd/lxh/logger/log" "gitee.ltd/lxh/logger/log"
"os/exec" "os/exec"
"strings" "strings"
"wireguard-dashboard/repository" "wireguard-ui/service"
) )
// getConfigFileName // getConfigFileName
// @description: 获取服务端配置文件名称 // @description: 获取服务端配置文件名称
// @return string // @return string
func getConfigFileName() string { func getConfigFileName() string {
data, err := repository.System().GetServerSetting() data, err := service.Setting().GetWGSetForConfig()
if err != nil { if err != nil {
log.Errorf("获取服务端配置失败: %v", err.Error()) log.Errorf("获取服务端配置失败: %v", err.Error())
return "" return ""

View File

@ -6,29 +6,29 @@ import (
"os" "os"
"strings" "strings"
"time" "time"
"wireguard-dashboard/client" "wireguard-ui/global/client"
"wireguard-dashboard/constant" "wireguard-ui/global/constant"
) )
type CaptchaStore struct{} type Captcha struct{}
// Set // Set
// @description: 验证码放入指定存储 // @description: 验证码放入指定存储
// @receiver CaptchaStore // @receiver Captcha
// @param id // @param id
// @param value // @param value
// @return error // @return error
func (CaptchaStore) Set(id string, value string) error { func (Captcha) Set(id string, value string) error {
return client.Redis.Set(context.Background(), fmt.Sprintf("%s:%s", constant.Captcha, id), value, 2*time.Minute).Err() return client.Redis.Set(context.Background(), fmt.Sprintf("%s:%s", constant.Captcha, id), value, 2*time.Minute).Err()
} }
// Get // Get
// @description: 获取验证码信息 // @description: 获取验证码信息
// @receiver CaptchaStore // @receiver Captcha
// @param id // @param id
// @param clear // @param clear
// @return string // @return string
func (CaptchaStore) Get(id string, clear bool) string { func (Captcha) Get(id string, clear bool) string {
val, err := client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.Captcha, id)).Result() val, err := client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.Captcha, id)).Result()
if err != nil { if err != nil {
return "" return ""
@ -44,12 +44,12 @@ func (CaptchaStore) Get(id string, clear bool) string {
// Verify // Verify
// @description: 校验 // @description: 校验
// @receiver CaptchaStore // @receiver Captcha
// @param id // @param id
// @param answer // @param answer
// @param clear // @param clear
// @return bool // @return bool
func (c CaptchaStore) Verify(id, answer string, clear bool) bool { func (c Captcha) Verify(id, answer string, clear bool) bool {
if os.Getenv("GIN_MODE") != "release" { if os.Getenv("GIN_MODE") != "release" {
return true return true
} }

View File

@ -9,69 +9,76 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"strings" "strings"
"time" "time"
"wireguard-dashboard/client" "wireguard-ui/config"
"wireguard-dashboard/config" "wireguard-ui/global/client"
"wireguard-dashboard/constant" "wireguard-ui/global/constant"
) )
const Secret = "IK8MSs76Pb2VJxleTDadf1Wzu3h9QROLv0XtmnCUErYgBG5wAyjk4cioqFZHNpZG" // jwt密钥
const secret = "JQo7L1RYa8ArFWuj0wC9PyM3VzmDIfXZ2d5tsTOBhNgviE64bnKqGpSckxUlHey6"
type JwtClaims struct { type JwtComponent struct {
ID string `json:"id"` ID string `json:"id"`
jwt.RegisteredClaims jwt.RegisteredClaims
} }
func JWT() JwtClaims { // JWT
return JwtClaims{} // @description: 初始化JWT组件
// @return JwtComponent
func JWT() JwtComponent {
return JwtComponent{}
} }
// GenerateToken // GenerateToken
// @description: 生成token // @description: 生成token
// @receiver Jwt // @receiver JwtComponent
// @param userId
// @return token // @return token
// @return expireTime
// @return err // @return err
func (j JwtClaims) GenerateToken(userId string) (token string, expireTime *jwt.NumericDate, err error) { func (JwtComponent) GenerateToken(userId string) (token string, expireTime *jwt.NumericDate, err error) {
timeNow := time.Now().Local() timeNow := time.Now().Local()
expireTime = jwt.NewNumericDate(timeNow.Add(7 * time.Hour)) expireTime = jwt.NewNumericDate(timeNow.Add(7 * time.Hour))
notBefore := jwt.NewNumericDate(timeNow) notBefore := jwt.NewNumericDate(timeNow)
issuedAt := jwt.NewNumericDate(timeNow) issuedAt := jwt.NewNumericDate(timeNow)
claims := JwtClaims{ claims := JwtComponent{
ID: userId, ID: userId,
RegisteredClaims: jwt.RegisteredClaims{ RegisteredClaims: jwt.RegisteredClaims{
Issuer: config.Config.Http.Endpoint, // 颁发站点 Issuer: config.Config.Http.Endpoint, // 颁发站点
Subject: "wg-dashboard", Subject: "you can you up,no can no bb", // 发布主题
ExpiresAt: expireTime, ExpiresAt: expireTime, // 过期时间
NotBefore: notBefore, NotBefore: notBefore, // token不得早于该时间
IssuedAt: issuedAt, IssuedAt: issuedAt, // token颁发时间
ID: uuid.NewString(), ID: strings.ReplaceAll(uuid.NewString(), "-", ""), // 该token的id
}, },
} }
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) t := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
token, err = t.SignedString([]byte(Secret)) token, err = t.SignedString([]byte(secret))
if err != nil { if err != nil {
log.Errorf("生成token失败: %v", err.Error()) log.Errorf("生成token失败: %v", err.Error())
return "", nil, errors.New("生成token失败") return "", nil, errors.New("生成token失败")
} }
client.Redis.Set(context.Background(), fmt.Sprintf("%s:%s", constant.Token, userId), token, 7*time.Hour) client.Redis.Set(context.Background(), fmt.Sprintf("%s:%s", constant.UserToken, userId), token, 7*time.Hour)
return return
} }
// ParseToken // ParseToken
// @description: 解析token // @description: 解析token
// @receiver Jwt // @receiver JwtComponent
// @return Jwt // @param token
// @return *JwtComponent
// @return error // @return error
func (JwtClaims) ParseToken(token string) (*JwtClaims, error) { func (JwtComponent) ParseToken(token string) (*JwtComponent, error) {
tokenStr := strings.Split(token, "Bearer ")[1] tokenStr := strings.Split(token, "Bearer ")[1]
t, err := jwt.ParseWithClaims(tokenStr, &JwtClaims{}, func(token *jwt.Token) (any, error) { t, err := jwt.ParseWithClaims(tokenStr, &JwtComponent{}, func(token *jwt.Token) (any, error) {
return []byte(Secret), nil return []byte(secret), nil
}) })
if claims, ok := t.Claims.(*JwtClaims); ok && t.Valid { if claims, ok := t.Claims.(*JwtComponent); ok && t.Valid {
userToken, err := client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.Token, claims.ID)).Result() userToken, err := client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.UserToken, claims.ID)).Result()
if err != nil { if err != nil {
log.Errorf("缓存中用户[%s]的token查找失败: %v", claims.ID, err.Error()) log.Errorf("缓存中用户[%s]的token查找失败: %v", claims.ID, err.Error())
return nil, errors.New("token不存在") return nil, errors.New("token不存在")
@ -90,9 +97,9 @@ func (JwtClaims) ParseToken(token string) (*JwtClaims, error) {
// Logout // Logout
// @description: 退出登陆 // @description: 退出登陆
// @receiver JwtClaims // @receiver JwtComponent
// @param userId // @param userId
// @return err // @return error
func (j JwtClaims) Logout(userId string) (err error) { func (JwtComponent) Logout(userId string) error {
return client.Redis.Del(context.Background(), fmt.Sprintf("%s:%s", constant.Token, userId)).Err() return client.Redis.Del(context.Background(), fmt.Sprintf("%s:%s", constant.UserToken, userId)).Err()
} }

76
component/template.go Normal file
View File

@ -0,0 +1,76 @@
package component
import (
"gitee.ltd/lxh/logger/log"
"html/template"
"os"
)
type TemplateComponent struct{}
func Template() TemplateComponent {
return TemplateComponent{}
}
// Execute
// @description: 渲染数据模板并生成对应文件
// @receiver t
// @param templateFilePath
// @param outFilePath
// @param data
// @return err
func (t TemplateComponent) Execute(templateFilePath, outFilePath string, data any) (err error) {
parseTemplate, err := t.ParseTemplate(templateFilePath)
if err != nil {
log.Errorf("解析模板信息失败:%v", err.Error())
return err
}
err = t.Render(parseTemplate, data, outFilePath)
if err != nil {
log.Errorf("渲染模板失败: %v", err.Error())
return err
}
return nil
}
// ParseTemplate
// @description: 解析模板
// @receiver t
// @param filepath
// @return t
// @return err
func (t TemplateComponent) ParseTemplate(filepath string) (tp *template.Template, err error) {
file, err := os.ReadFile(filepath)
if err != nil {
return
}
tp, err = template.New("wg.conf").Parse(string(file))
return
}
// Render
// @description: 渲染模板
// @receiver t
// @param tp
// @param data
// @param filepath
// @return err
func (t TemplateComponent) Render(tp *template.Template, data any, filepath string) (err error) {
wg0Conf, err := os.Create(filepath)
if err != nil {
log.Errorf("创建文件[%s]失败: %v", filepath, err.Error())
return
}
defer func() {
if err = wg0Conf.Close(); err != nil {
log.Errorf("关闭文件[%s]失败: %v", filepath, err.Error())
return
}
}()
return tp.Execute(wg0Conf, data)
}

92
component/validator.go Normal file
View File

@ -0,0 +1,92 @@
package component
import (
"errors"
"gitee.ltd/lxh/logger/log"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
zhTranslations "github.com/go-playground/validator/v10/translations/zh"
"reflect"
"strings"
)
var validatorTrans ut.Translator
func init() {
initValidatorTranslator()
}
func Error(err error) string {
var errs validator.ValidationErrors
ok := errors.As(err, &errs)
if !ok {
return err.Error()
}
errMap := errs.Translate(validatorTrans)
var errMsg string
for _, v := range errMap {
errMsg = v
}
return errMsg
}
// initValidatorTranslator
// @description: 初始化翻译机
// @receiver vli
func initValidatorTranslator() {
//修改gin框架中的Validator属性实现自定制
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 注册一个获取json tag的自定义方法
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("label"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
zhT := zh.New() //中文翻译器
enT := en.New() //英文翻译器
// 第一个参数是备用fallback的语言环境
// 后面的参数是应该支持的语言环境(支持多个)
// uni := ut.New(zhT, zhT) 也是可以的
uni := ut.New(enT, zhT, enT)
// locale 通常取决于 http 请求头的 'Accept-Language'
var ok bool
// 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找
validatorTrans, ok = uni.GetTranslator("zh")
if !ok {
log.Errorf("获取翻译机失败")
return
}
err := overrideTranslator(v, validatorTrans)
if err != nil {
log.Errorf("覆盖原有翻译失败: %v", err.Error())
return
}
}
}
// overrideTranslator
// @description: 覆盖原有翻译
// @param v
// @param translator
// @return error
func overrideTranslator(v *validator.Validate, translator ut.Translator) error {
err := zhTranslations.RegisterDefaultTranslations(v, translator)
if err != nil {
return err
}
return nil
}

View File

@ -1,51 +1,132 @@
package component package component
import ( import (
"encoding/json"
"errors"
"fmt"
"gitee.ltd/lxh/logger/log" "gitee.ltd/lxh/logger/log"
"gopkg.in/fsnotify.v1" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"wireguard-dashboard/command" "gopkg.in/fsnotify/fsnotify.v1"
"wireguard-dashboard/config" "os"
"wireguard-dashboard/utils" "strings"
"time"
"wireguard-ui/command"
"wireguard-ui/config"
"wireguard-ui/global/client"
"wireguard-ui/model"
"wireguard-ui/template/render_data"
) )
type wireguard struct{} type WireguardComponent struct{}
func Wireguard() wireguard { func Wireguard() WireguardComponent {
return wireguard{} return WireguardComponent{}
} }
// Apply // GetClients
// @description: 应用配置 // @description: 获取所有链接的客户端信息
// @receiver wireguard // @receiver w
// @return peers
// @return err // @return err
func (w wireguard) Apply(templateFilePath, configFilePath string, data any) (err error) { func (w WireguardComponent) GetClients() (peers []wgtypes.Peer, err error) {
device, err := client.WireguardClient.Devices()
parseTemplate, err := utils.Template().Parse(templateFilePath)
if err != nil { if err != nil {
log.Errorf("解析模板信息失败") return
return err
} }
err = utils.Template().Execute(parseTemplate, data, configFilePath) for _, v := range device {
if err != nil { return v.Peers, nil
log.Errorf("应用配置失败: %v", err.Error())
return err
} }
// 判断服务端重启规则 return
switch config.Config.Wireguard.ListenConfig {
case "auto":
w.watchListConfig(configFilePath)
}
return nil
} }
// watchListConfig // GetClientByPublicKey
// @description: 监听配置文件变化 // @description: 根据公钥获取指定客户端信息
// @receiver wireguard // @receiver w
// @return peer
// @return err // @return err
func (wireguard) watchListConfig(filePath string) { func (w WireguardComponent) GetClientByPublicKey(pk string) (peer *wgtypes.Peer, err error) {
peers, err := w.GetClients()
if err != nil {
return
}
for _, v := range peers {
if v.PublicKey.String() == pk {
return &v, nil
}
}
return
}
// GenerateClientFile
// @description: 生成客户端文件
// @receiver w
// @param clientInfo
// @param server
// @param setting
// @return filePath
// @return err
func (w WireguardComponent) GenerateClientFile(clientInfo *model.Client, server *render_data.Server, setting *render_data.ServerSetting) (filePath string, err error) {
var keys render_data.Keys
_ = json.Unmarshal([]byte(clientInfo.Keys), &keys)
var serverDNS []string
if clientInfo.UseServerDns == 1 {
serverDNS = setting.DnsServer
}
// 处理一下数据
execData := render_data.ClientConfig{
PrivateKey: keys.PrivateKey,
IpAllocation: clientInfo.IpAllocation,
MTU: setting.MTU,
DNS: strings.Join(serverDNS, ","),
PublicKey: server.PublicKey,
PresharedKey: keys.PresharedKey,
AllowedIPS: clientInfo.AllowedIps,
Endpoint: setting.EndpointAddress,
ListenPort: int(server.ListenPort),
PersistentKeepalive: setting.PersistentKeepalive,
}
// 不同环境下处理文件路径
var outPath = "/tmp/" + fmt.Sprintf("%s.conf", clientInfo.Name)
var templatePath = "./template/wg.client.conf"
if os.Getenv("GIN_MODE") != "release" {
outPath = "E:\\Workspace\\Go\\wireguard-dashboard\\template\\" + fmt.Sprintf("%s.conf", clientInfo.Name)
templatePath = "E:\\Workspace\\Go\\wireguard-dashboard\\template\\wg.client.conf"
}
err = Template().Execute(templatePath, outPath, execData)
if err != nil {
return "", errors.New("文件渲染失败")
}
return outPath, nil
}
// ServerControl
// @description: 服务端控制
// @receiver w
// @return error
func (w WireguardComponent) ServerControl(filePath string) {
switch config.Config.Wireguard.RestartMode {
case "NOW": // 立即执行
w.watchConfigFile(filePath)
case "DELAY": // 延迟执行
time.Sleep(time.Duration(config.Config.Wireguard.DelayTime) * time.Second)
w.watchConfigFile(filePath)
}
}
// watchConfigFile
// @description: 监听并重新操作配置文件
// @receiver w
// @param filepath
func (w WireguardComponent) watchConfigFile(filepath string) {
go func() { go func() {
watcher, err := fsnotify.NewWatcher() watcher, err := fsnotify.NewWatcher()
if err != nil { if err != nil {
@ -78,8 +159,8 @@ func (wireguard) watchListConfig(filePath string) {
} }
}() }()
if err = watcher.Add(filePath); err != nil { if err = watcher.Add(filepath); err != nil {
log.Errorf("添加[%s]监听失败: %v", filePath, err.Error()) log.Errorf("添加[%s]监听失败: %v", filepath, err.Error())
return return
} }
<-done <-done

View File

@ -1,6 +1,7 @@
package config package config
type redis struct { type cache struct {
Type string `yaml:"type"`
Host string `yaml:"host"` Host string `yaml:"host"`
Port int `yaml:"port"` Port int `yaml:"port"`
Password string `yaml:"password"` Password string `yaml:"password"`

View File

@ -5,7 +5,7 @@ var Config *config
type config struct { type config struct {
Http *http `yaml:"http"` Http *http `yaml:"http"`
Database *database `yaml:"database"` Database *database `yaml:"database"`
Redis *redis `yaml:"redis"` Cache *cache `yaml:"redis"`
File *file `yaml:"file"` File *file `yaml:"file"`
Mail *mail `yaml:"email"` Mail *mail `yaml:"email"`
Wireguard *wireguard `yaml:"wireguard"` Wireguard *wireguard `yaml:"wireguard"`

View File

@ -1,5 +1,6 @@
package config package config
type wireguard struct { type wireguard struct {
ListenConfig string `json:"listenConfig" yaml:"listenConfig"` RestartMode string `json:"restartMode"` // 重启模式 NOW - 立即重启 | DELAY - 延时 | HAND - 手动重启
DelayTime int64 `json:"delayTime"` // 延时重启的间隔(单位:秒)
} }

View File

@ -1,10 +0,0 @@
package constant
const (
Token = "token" // 登陆token
Captcha = "captcha" // 验证码
)
const (
SyncWgConfigFile = "queues:wg:sync-file"
)

View File

@ -1 +0,0 @@
package constant

View File

@ -1,17 +0,0 @@
package cron_task
import (
"gitee.ltd/lxh/logger/log"
"github.com/go-co-op/gocron/v2"
"time"
)
func StartCronTask() {
s, err := gocron.NewScheduler()
if err != nil {
log.Errorf("初始化定时任务失败: %v", err.Error())
return
}
_, _ = s.NewJob(gocron.DurationJob(time.Hour), gocron.NewTask(offlineMonitoring)) // 每小时执行一次
s.Start()
}

View File

@ -1,63 +0,0 @@
package cron_task
import (
"fmt"
"gitee.ltd/lxh/logger/log"
"strings"
"time"
"wireguard-dashboard/client"
"wireguard-dashboard/repository"
"wireguard-dashboard/utils"
)
// offlineMonitoring
// @description: 离线监听任务
func offlineMonitoring() {
devices, err := client.WireguardClient.Devices()
if err != nil {
time.Sleep(5 * time.Minute) // 休眠五分钟再执行
offlineMonitoring()
return
}
// 遍历客户端数据,并渲染数据信息
for _, d := range devices {
for _, p := range d.Peers {
var ipAllocation string
for _, iaip := range p.AllowedIPs {
ipAllocation += iaip.String() + ","
}
ipAllocation = strings.TrimRight(ipAllocation, ",")
isOnline := time.Since(p.LastHandshakeTime).Minutes() < 3
// 未离线
if isOnline {
continue
}
clientInfo, err := repository.Client().GetByPublicKey(p.PublicKey.String())
if err != nil {
continue
}
// 没有启用离线监听时,即使客户端已经离线则也不执行
if clientInfo.OfflineMonitoring == nil || *clientInfo.OfflineMonitoring != 1 || clientInfo.Email == "" {
continue
}
content := fmt.Sprintf("客户端:%s\r\n", clientInfo.Name)
content += fmt.Sprintf("客户端IP%s\r\n", ipAllocation)
content += fmt.Sprintf("端点IP%s\r\n", p.Endpoint.String())
content += fmt.Sprintf("最后握手时间:%s\r\n", p.LastHandshakeTime.Format("2006-01-02 15:04:05"))
// 离线并且配置了邮箱,准备发送邮件
err = utils.Mail().SendMail(clientInfo.Email, fmt.Sprintf("客户端[%s]离线通知", clientInfo.Name), content, "")
if err != nil {
log.Errorf("发送离线通知邮件失败: %v", err.Error())
continue
}
}
}
}

View File

@ -0,0 +1,22 @@
package constant
// Status 启用禁用
type Status int
const (
Disabled Status = iota
Enabled
)
var StatusMap = map[Status]string{
Disabled: "禁用",
Enabled: "启用",
}
func (u Status) String() string {
if v, ok := StatusMap[u]; ok {
return v
}
return "未知类型"
}

View File

@ -0,0 +1,6 @@
package constant
const (
Captcha = "captcha:"
UserToken = "token:"
)

View File

@ -1,5 +1,6 @@
package constant package constant
// UserType 用户类型
type UserType int type UserType int
const ( const (
@ -19,23 +20,3 @@ func (u UserType) String() string {
return "未知类型" return "未知类型"
} }
type UserStatus int
const (
Disabled UserStatus = iota
Normal
)
var UserStatusMap = map[UserStatus]string{
Disabled: "禁用",
Normal: "正常",
}
func (u UserStatus) String() string {
if v, ok := UserStatusMap[u]; ok {
return v
}
return "未知类型"
}

View File

@ -0,0 +1,7 @@
package constant
const (
DefaultPostUpScript = "iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE"
DefaultPostDownScript = "iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE"
DefaultPreDownScript = ""
)

85
go.mod
View File

@ -1,30 +1,21 @@
module wireguard-dashboard module wireguard-ui
go 1.21 go 1.21
require ( require (
gitee.ltd/lxh/logger v1.0.15 gitee.ltd/lxh/logger v1.0.15
github.com/cowardmrx/go_aliyun_oss v1.0.7 github.com/cowardmrx/go_aliyun_oss v1.0.7
github.com/dustin/go-humanize v1.0.1 github.com/fsnotify/fsnotify v1.7.0
github.com/gin-gonic/gin v1.9.1 github.com/gin-contrib/pprof v1.5.0
github.com/gin-gonic/gin v1.10.0
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
github.com/go-co-op/gocron/v2 v2.5.0 github.com/go-resty/resty/v2 v2.13.1
github.com/go-resty/resty/v2 v2.11.0 github.com/redis/go-redis/v9 v9.5.3
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/spf13/viper v1.19.0
github.com/google/uuid v1.6.0
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/json-iterator/go v1.1.12
github.com/mojocn/base64Captcha v1.3.6
github.com/redis/go-redis/v9 v9.5.1
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/spf13/cast v1.6.0
golang.org/x/crypto v0.22.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
gopkg.in/fsnotify.v1 v1.4.7 gorm.io/driver/mysql v1.5.7
gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/postgres v1.5.9
gorm.io/driver/mysql v1.5.4 gorm.io/gorm v1.25.10
gorm.io/driver/postgres v1.5.6
gorm.io/gorm v1.25.7
) )
require ( require (
@ -34,13 +25,11 @@ require (
github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/caarlos0/env/v6 v6.10.1 // indirect github.com/caarlos0/env/v6 v6.10.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/pprof v1.5.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-kit/kit v0.12.0 // indirect github.com/go-kit/kit v0.12.0 // indirect
@ -48,26 +37,31 @@ require (
github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/go-playground/validator/v10 v10.22.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect
github.com/josharian/native v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lixh00/loki-client-go v1.0.1 // indirect github.com/lixh00/loki-client-go v1.0.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect
@ -76,6 +70,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mojocn/base64Captcha v1.3.6 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect
@ -86,28 +81,38 @@ require (
github.com/prometheus/procfs v0.8.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect
github.com/prometheus/prometheus v1.8.2-0.20201028100903-3245b3267b24 // indirect github.com/prometheus/prometheus v1.8.2-0.20201028100903-3245b3267b24 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/atomic v1.10.0 // indirect go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.9.0 // indirect
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.23.0 // indirect
golang.org/x/arch v0.8.0 // indirect golang.org/x/arch v0.8.0 // indirect
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect golang.org/x/crypto v0.23.0 // indirect
golang.org/x/image v0.15.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.24.0 // indirect golang.org/x/image v0.13.0 // indirect
golang.org/x/oauth2 v0.1.0 // indirect golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/oauth2 v0.18.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.20.0 // indirect golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.3.0 // indirect golang.org/x/time v0.5.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b // indirect golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect
google.golang.org/grpc v1.50.1 // indirect google.golang.org/grpc v1.62.1 // indirect
google.golang.org/protobuf v1.34.0 // indirect google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect modernc.org/memory v1.5.0 // indirect

169
go.sum
View File

@ -107,6 +107,8 @@ github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN
github.com/aws/aws-sdk-go v1.35.5/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k= github.com/aws/aws-sdk-go v1.35.5/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@ -120,9 +122,6 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
@ -139,9 +138,6 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -166,8 +162,9 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg= github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-bitstream v0.0.0-20180413035011-3522498ce2c8/go.mod h1:VMaSuZ+SZcx/wljOQKvp5srsbCiKDEb6K2wC4+PiBmQ= github.com/dgryski/go-bitstream v0.0.0-20180413035011-3522498ce2c8/go.mod h1:VMaSuZ+SZcx/wljOQKvp5srsbCiKDEb6K2wC4+PiBmQ=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
@ -209,8 +206,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@ -219,8 +214,8 @@ github.com/gin-contrib/pprof v1.5.0 h1:E/Oy7g+kNw94KfdCy3bZxQFtyDnAX2V7axRS7sNYV
github.com/gin-contrib/pprof v1.5.0/go.mod h1:GqFL6LerKoCQ/RSWnkYczkTJ+tOAUVN/8sbnEtaqOKs= github.com/gin-contrib/pprof v1.5.0/go.mod h1:GqFL6LerKoCQ/RSWnkYczkTJ+tOAUVN/8sbnEtaqOKs=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
@ -229,8 +224,6 @@ github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
github.com/go-co-op/gocron/v2 v2.5.0 h1:ff/TJX9GdTJBDL1il9cyd/Sj3WnS+BB7ZzwHKSNL5p8=
github.com/go-co-op/gocron/v2 v2.5.0/go.mod h1:ckPQw96ZuZLRUGu88vVpd9a6d9HakI14KWahFZtGvNw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -314,12 +307,12 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8= github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
@ -391,8 +384,9 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@ -411,8 +405,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@ -474,6 +468,8 @@ github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
@ -514,8 +510,6 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
@ -548,8 +542,6 @@ github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg= github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg=
@ -570,8 +562,6 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@ -580,6 +570,8 @@ github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0U
github.com/lixh00/loki-client-go v1.0.1 h1:y/ePf/s66N77eikIujRS/QQAKvbMmPmesMxAuMuP8lM= github.com/lixh00/loki-client-go v1.0.1 h1:y/ePf/s66N77eikIujRS/QQAKvbMmPmesMxAuMuP8lM=
github.com/lixh00/loki-client-go v1.0.1/go.mod h1:JSeu3fIBPjnmf5bBq6I8hvJlhYum2eLQEzwU149vyfQ= github.com/lixh00/loki-client-go v1.0.1/go.mod h1:JSeu3fIBPjnmf5bBq6I8hvJlhYum2eLQEzwU149vyfQ=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@ -598,8 +590,6 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
@ -693,8 +683,6 @@ github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144T
github.com/paulbellamy/ratecounter v0.2.0/go.mod h1:Hfx1hDpSGoqxkVVpBi/IlYD7kChlfo5C6hzIHwPqfFE= github.com/paulbellamy/ratecounter v0.2.0/go.mod h1:Hfx1hDpSGoqxkVVpBi/IlYD7kChlfo5C6hzIHwPqfFE=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
@ -709,8 +697,9 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pkg/term v0.0.0-20180730021639-bffc007b7fd5/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ= github.com/pkg/term v0.0.0-20180730021639-bffc007b7fd5/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/alertmanager v0.21.0/go.mod h1:h7tJ81NA0VLWvWEayi1QltevFkLF3KxmC/malTcT8Go= github.com/prometheus/alertmanager v0.21.0/go.mod h1:h7tJ81NA0VLWvWEayi1QltevFkLF3KxmC/malTcT8Go=
@ -758,25 +747,27 @@ github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0ua
github.com/prometheus/prometheus v1.8.2-0.20201028100903-3245b3267b24 h1:V/4Cj2GytqdqK7OMEz6c4LNjey3SNyfw3pg5jPKtJvQ= github.com/prometheus/prometheus v1.8.2-0.20201028100903-3245b3267b24 h1:V/4Cj2GytqdqK7OMEz6c4LNjey3SNyfw3pg5jPKtJvQ=
github.com/prometheus/prometheus v1.8.2-0.20201028100903-3245b3267b24/go.mod h1:MDRkz271loM/PrYN+wUNEaTMDGSP760MQzB0yEjdgSQ= github.com/prometheus/prometheus v1.8.2-0.20201028100903-3245b3267b24/go.mod h1:MDRkz271loM/PrYN+wUNEaTMDGSP760MQzB0yEjdgSQ=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU=
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc= github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/samuel/go-zookeeper v0.0.0-20200724154423-2164a8ac840e/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/samuel/go-zookeeper v0.0.0-20200724154423-2164a8ac840e/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
@ -799,8 +790,12 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
@ -808,7 +803,10 @@ github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
@ -828,11 +826,11 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@ -841,8 +839,6 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/uber/jaeger-client-go v2.25.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-client-go v2.25.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.4.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/uber/jaeger-lib v2.4.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
@ -879,21 +875,19 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@ -914,11 +908,9 @@ golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -932,14 +924,13 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk= golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -1008,11 +999,9 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1021,8 +1010,8 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.1.0 h1:isLCZuhj4v+tYv7eskaN4v/TM+A1begWWgyVJDdl1+Y= golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -1037,8 +1026,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1105,21 +1094,19 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1127,10 +1114,10 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
@ -1139,8 +1126,8 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -1248,8 +1235,8 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@ -1284,8 +1271,8 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55 h1:U1u4KB2kx6KR/aJDjQ97hZ15wQs8ZPvDcGcRynBhkvg= google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c h1:lfpJ/2rWPa/kJgxyyXM8PrNnfCzcmxJ265mADgwmvLI=
google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55/go.mod h1:45EK0dUbEZ2NHjCeAd2LXmyjAgGUGrpGROgjhC3ADck= google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
@ -1305,8 +1292,8 @@ google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY= google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -1319,10 +1306,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -1331,11 +1316,13 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 h1:XNNYLJHt73EyYiCZi6+xjupS9CpvmiDgjPTAjrBlQbo=
gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE= gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
@ -1355,13 +1342,13 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.4 h1:igQmHfKcbaTVyAIHNhhB888vvxh8EdQ2uSUT0LPcBso= gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.4/go.mod h1:9rYxJph/u9SWkWc9yY4XJ1F/+xO0S/ChOmbk3+Z5Tvs= gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.6 h1:ydr9xEd5YAM0vxVDY0X139dyzNz10spDiDlC7+ibLeU= gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.6/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

20
http/api/api.go Normal file
View File

@ -0,0 +1,20 @@
package api
import (
"github.com/gin-gonic/gin"
"wireguard-ui/http/response"
"wireguard-ui/http/vo"
)
// GetCurrentLoginUser
// @description: 获取当前登陆用户
// @param c
// @return *vo.User
func GetCurrentLoginUser(c *gin.Context) *vo.User {
if user, ok := c.Get("user"); ok {
return user.(*vo.User)
}
response.R(c).AuthorizationFailed("暂未登陆")
c.Abort()
return nil
}

View File

@ -1,36 +0,0 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
"wireguard-dashboard/component"
"wireguard-dashboard/utils"
)
type captcha struct{}
func Captcha() captcha {
return captcha{}
}
// GenerateCaptcha
// @description: 生成验证码
// @receiver captcha
// @param c
func (captcha) GenerateCaptcha(c *gin.Context) {
math := base64Captcha.DriverMath{Height: 120, Width: 480, Fonts: []string{"ApothecaryFont.ttf", "3Dumb.ttf"}}
mathDriver := math.ConvertFonts()
capt := base64Captcha.NewCaptcha(mathDriver, component.CaptchaStore{})
id, base64Str, _, err := capt.Generate()
if err != nil {
utils.GinResponse(c).FailedWithErr("生成验证码失败: %v", err)
return
}
utils.GinResponse(c).OKWithData(map[string]any{
"id": id,
"captcha": base64Str,
})
}

View File

@ -10,416 +10,250 @@ import (
"os" "os"
"strings" "strings"
"time" "time"
"wireguard-dashboard/client" "wireguard-ui/component"
"wireguard-dashboard/http/param" "wireguard-ui/http/param"
"wireguard-dashboard/model/entity" "wireguard-ui/http/response"
"wireguard-dashboard/model/template_data" "wireguard-ui/http/vo"
"wireguard-dashboard/model/vo" "wireguard-ui/model"
"wireguard-dashboard/queues" "wireguard-ui/service"
"wireguard-dashboard/repository" "wireguard-ui/utils"
"wireguard-dashboard/utils"
) )
type clients struct{} type ClientApi struct{}
func Client() clients { func Client() ClientApi {
return clients{} return ClientApi{}
}
// List
// @description: 客户端列表
// @receiver clients
// @param c
func (clients) List(c *gin.Context) {
var p param.ClientList
if err := c.ShouldBind(&p); err != nil {
utils.GinResponse(c).FailedWithErr("参数错误", err)
return
}
data, total, err := repository.Client().List(p)
if err != nil {
utils.GinResponse(c).FailedWithMsg("获取失败")
return
}
utils.GinResponse(c).OkWithPage(data, total, p.Current, p.Size)
} }
// Save // Save
// @description: 新增/更新客户端 // @description: 新增/编辑客户端
// @receiver clients
// @param c // @param c
func (clients) Save(c *gin.Context) { func (ClientApi) Save(c *gin.Context) {
var p param.SaveClient var p param.SaveClient
if err := c.ShouldBind(&p); err != nil { if err := c.ShouldBind(&p); err != nil {
utils.GinResponse(c).FailedWithErr("参数错误", err) response.R(c).Validator(err)
return return
} }
info, ok := c.Get("user") var loginUser *vo.User
if !ok { if loginUser = GetCurrentLoginUser(c); c.IsAborted() {
utils.GinResponse(c).FailedWithMsg("获取信息失败") return
}
if err := service.Client().SaveClient(p, loginUser); err != nil {
response.R(c).FailedWithError(err)
return return
} }
_, err := repository.Client().Save(p, info.(*entity.User).Id) response.R(c).OK()
if err != nil {
utils.GinResponse(c).FailedWithErr("操作失败", err)
return
}
go func() {
if err = queues.PutAsyncWireguardConfigFile(p.ServerId); err != nil {
log.Errorf("[新增/编辑客户端]同步配置文件失败: %v", err.Error())
}
}()
utils.GinResponse(c).OK()
} }
// AssignIPAndAllowedIP // Delete
// @description: 分配客户端IP和允许访问的IP段 // @description: 删除客户端
// @receiver clients // @receiver ClientApi
// @param c // @param c
func (clients) AssignIPAndAllowedIP(c *gin.Context) { func (ClientApi) Delete(c *gin.Context) {
var p param.AssignIPAndAllowedIP id := c.Param("id")
if id == "" || id == "undefined" {
response.R(c).FailedWithError("id不能为空")
return
}
if err := service.Client().Delete(id); err != nil {
response.R(c).FailedWithError(err)
return
}
response.R(c).OK()
}
// List
// @description: 客户端分页列表
// @receiver ClientApi
// @param c
func (ClientApi) List(c *gin.Context) {
var p param.ClientList
if err := c.ShouldBind(&p); err != nil { if err := c.ShouldBind(&p); err != nil {
utils.GinResponse(c).FailedWithErr("参数错误", err) response.R(c).Validator(err)
return return
} }
// 获取一下服务端信息因为IP分配需要根据服务端的IP制定 data, total, err := service.Client().List(p)
serverInfo, err := repository.Server().GetServer()
if err != nil { if err != nil {
utils.GinResponse(c).FailedWithErr("获取服务端信息失败", err) response.R(c).FailedWithError(err)
return return
} }
var assignIPS []string for i, v := range data {
assignIPS = append(assignIPS, serverInfo.IpScope) // 获取客户端链接信息
switch p.Rule { peer, err := component.Wireguard().GetClientByPublicKey(v.Keys.PublicKey)
case "AUTO": if err != nil {
// 只获取最新的一个 continue
var clientInfo *entity.Client
if err = repository.Client().Order("created_at DESC").Take(&clientInfo).Error; err == nil {
if cast.ToInt64(utils.Wireguard().GetIPSuffix(clientInfo.IpAllocation)) >= 255 {
utils.GinResponse(c).FailedWithMsg("当前IP分配错误请手动进行分配")
return
}
assignIPS = append(assignIPS, clientInfo.IpAllocation)
} }
var ipAllocation string
case "RANDOM": for _, iaip := range peer.AllowedIPs {
// 查询全部客户端不管是禁用还是没禁用的 ipAllocation += iaip.String() + ","
var clientsInfo []entity.Client
if err = repository.Client().Find(&clientsInfo).Error; err != nil {
utils.GinResponse(c).FailedWithErr("获取失败", err)
return
} }
data[i].DataTraffic = &vo.DataTraffic{
for _, v := range clientsInfo { Online: time.Since(peer.LastHandshakeTime).Minutes() < 3,
assignIPS = append(assignIPS, v.IpAllocation) ReceiveBytes: utils.FlowCalculation().Parse(peer.TransmitBytes),
TransmitBytes: utils.FlowCalculation().Parse(peer.ReceiveBytes),
ConnectEndpoint: ipAllocation,
LastHandAt: peer.LastHandshakeTime.Format("2006-01-02 15:04:05"),
} }
} }
clientIP := utils.Wireguard().GenerateClientIP(serverInfo.IpScope, p.Rule, assignIPS...) response.R(c).Paginate(data, total, p.Current, p.Size)
utils.GinResponse(c).OKWithData(map[string]any{
"clientIP": []string{fmt.Sprintf("%s/32", clientIP)},
"serverIP": []string{serverInfo.IpScope},
})
} }
// GenerateKeys // GenerateKeys
// @description: 生成密钥对 // @description: 生成客户端密钥信息
// @receiver clients // @receiver ClientApi
// @param c // @param c
func (clients) GenerateKeys(c *gin.Context) { func (ClientApi) GenerateKeys(c *gin.Context) {
// 为空,新增 // 为空,新增
privateKey, err := wgtypes.GeneratePrivateKey() privateKey, err := wgtypes.GeneratePrivateKey()
if err != nil { if err != nil {
utils.GinResponse(c).FailedWithErr("生成密钥失败", err) response.R(c).FailedWithError(fmt.Errorf("生成密钥失败: %v", err.Error()))
return return
} }
publicKey := privateKey.PublicKey().String() publicKey := privateKey.PublicKey().String()
presharedKey, err := wgtypes.GenerateKey() presharedKey, err := wgtypes.GenerateKey()
if err != nil { if err != nil {
response.R(c).FailedWithError(fmt.Errorf("生成密钥失败: %v", err.Error()))
return return
} }
keys := template_data.Keys{ keys := vo.Keys{
PrivateKey: privateKey.String(), PrivateKey: privateKey.String(),
PublicKey: publicKey, PublicKey: publicKey,
PresharedKey: presharedKey.String(), PresharedKey: presharedKey.String(),
} }
utils.GinResponse(c).OKWithData(keys) response.R(c).OkWithData(keys)
} }
// Delete // GenerateIP
// @description: 删除客户端 // @description: 生成客户端IP
// @receiver clients // @receiver ClientApi
// @param c // @param c
func (clients) Delete(c *gin.Context) { func (ClientApi) GenerateIP(c *gin.Context) {
var id = c.Param("id") // 获取一下服务端信息因为IP分配需要根据服务端的IP制定
if id == "" || id == "undefined" { serverInfo, err := service.Setting().GetWGServerForConfig()
utils.GinResponse(c).FailedWithMsg("参数错误") if err != nil {
response.R(c).FailedWithError("获取服务端信息失败")
return return
} }
if err := repository.Client().Delete(id); err != nil { var assignIPS []string
utils.GinResponse(c).FailedWithMsg("操作失败") // 只获取最新的一个
return var clientInfo *model.Client
} if err = service.Client().Order("created_at DESC").Take(&clientInfo).Error; err == nil {
// 遍历每一个ip是否可允许再分配
// 再同步一下配置文件 for _, ip := range strings.Split(clientInfo.IpAllocation, ",") {
go func() { if cast.ToInt64(utils.Network().GetIPSuffix(ip)) >= 255 {
if err := queues.PutAsyncWireguardConfigFile(""); err != nil { log.Errorf("IP[%s]已无法分配新IP", ip)
log.Errorf("[下线客户端]同步配置文件失败: %v", err.Error()) continue
} else {
assignIPS = append(assignIPS, ip)
}
} }
}() }
utils.GinResponse(c).OK() ips := utils.Network().GenerateIPByIPS(serverInfo.Address, assignIPS...)
response.R(c).OkWithData(ips)
} }
// Download // Download
// @description: 下载配置文件 // @description: 下载客户端配置文件
// @receiver clients // @receiver ClientApi
// @param c // @param c
func (clients) Download(c *gin.Context) { func (ClientApi) Download(c *gin.Context) {
var id = c.Param("id") var id = c.Param("id")
if id == "" || id == "undefined" { if id == "" || id == "undefined" {
utils.GinResponse(c).FailedWithMsg("参数错误") response.R(c).FailedWithError("id不能为空")
return
}
var downloadType = c.Param("type")
if downloadType == "" {
response.R(c).FailedWithError("参数错误")
return return
} }
data, err := repository.Client().GetById(id) data, err := service.Client().GetByID(id)
if err != nil { if err != nil {
utils.GinResponse(c).FailedWithMsg("获取失败") response.R(c).FailedWithError("获取客户端信息失败")
return return
} }
var keys template_data.Keys var keys vo.Keys
_ = json.Unmarshal([]byte(data.Keys), &keys) _ = json.Unmarshal([]byte(data.Keys), &keys)
serverSetting, err := repository.System().GetServerSetting() globalSet, err := service.Setting().GetWGSetForConfig()
if err != nil { if err != nil {
utils.GinResponse(c).FailedWithMsg("获取设置失败") response.R(c).FailedWithError("获取失败")
return return
} }
outPath, err := utils.Wireguard().GenerateClientFile(&data, serverSetting) serverConf, err := service.Setting().GetWGServerForConfig()
if err != nil { if err != nil {
utils.GinResponse(c).FailedWithErr("生成失败", err) response.R(c).FailedWithError("获取失败")
return return
} }
// 输出文件流 outPath, err := component.Wireguard().GenerateClientFile(data, serverConf, globalSet)
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename="+outPath)
c.Header("Content-Transfer-Encoding", "binary")
c.Header("Connection", "keep-alive")
c.File(outPath)
if err = os.Remove(outPath); err != nil {
log.Errorf("删除临时文件失败: %s", err.Error())
}
}
// GenerateQrCode
// @description: 生成客户端信息二维码
// @receiver clients
// @param c
func (clients) GenerateQrCode(c *gin.Context) {
var id = c.Param("id")
if id == "" || id == "undefined" {
utils.GinResponse(c).FailedWithMsg("参数错误")
return
}
data, err := repository.Client().GetById(id)
if err != nil { if err != nil {
utils.GinResponse(c).FailedWithMsg("获取失败") response.R(c).FailedWithError(fmt.Errorf("生成失败: %v", err.Error()))
return return
} }
var keys template_data.Keys // 根据不同下载类型执行不同逻辑
_ = json.Unmarshal([]byte(data.Keys), &keys) switch downloadType {
case "QRCODE": // 二维码
serverSetting, err := repository.System().GetServerSetting() // 读取文件内容
if err != nil { fileContent, err := os.ReadFile(outPath)
utils.GinResponse(c).FailedWithMsg("获取设置失败") if err != nil {
return response.R(c).FailedWithError("读取文件失败")
} return
outPath, err := utils.Wireguard().GenerateClientFile(&data, serverSetting)
if err != nil {
utils.GinResponse(c).FailedWithErr("生成失败", err)
return
}
// 读取文件内容
fileContent, err := os.ReadFile(outPath)
if err != nil {
utils.GinResponse(c).FailedWithMsg("读取文件失败")
return
}
png, err := utils.QRCode().GenerateQrCodeBase64(fileContent, 256)
if err != nil {
utils.GinResponse(c).FailedWithErr("生成二维码失败", err)
return
}
if err = os.Remove(outPath); err != nil {
log.Errorf("删除临时文件失败: %s", err.Error())
}
utils.GinResponse(c).OKWithData(map[string]interface{}{
"qrCode": png,
})
}
// SendEmail
// @description: 发送邮件
// @receiver clients
// @param c
func (clients) SendEmail(c *gin.Context) {
var id = c.Param("id")
if id == "" || id == "undefined" {
utils.GinResponse(c).FailedWithMsg("id不能为空")
return
}
// 先校验一下邮箱发送是否可用
if err := utils.Mail().VerifyConfig(); err != nil {
utils.GinResponse(c).FailedWithMsg(err.Error())
return
}
// 获取该客户端信息
clientInfo, err := repository.Client().GetById(id)
if err != nil {
utils.GinResponse(c).FailedWithErr("获取失败", err)
return
}
if clientInfo.Email == "" {
utils.GinResponse(c).FailedWithMsg("当前客户端未配置联系邮箱!")
return
}
serverSetting, err := repository.System().GetServerSetting()
if err != nil {
utils.GinResponse(c).FailedWithMsg("获取设置失败")
return
}
outPath, err := utils.Wireguard().GenerateClientFile(&clientInfo, serverSetting)
if err != nil {
utils.GinResponse(c).FailedWithErr("生成失败", err)
return
}
err = utils.Mail().SendMail(clientInfo.Email, fmt.Sprintf("客户端: %s", clientInfo.Name), "请查收附件", outPath)
if err != nil {
utils.GinResponse(c).FailedWithErr("发送邮件失败", err)
return
}
if err = os.Remove(outPath); err != nil {
log.Errorf("删除临时文件失败: %s", err.Error())
}
utils.GinResponse(c).OK()
}
// Status
// @description: 获取客户端状态信息,链接状态等
// @receiver clients
// @param c
func (clients) Status(c *gin.Context) {
// 使用sdk拉取一下客户端信息
devices, err := client.WireguardClient.Devices()
if err != nil {
utils.GinResponse(c).FailedWithErr("获取客户端信息失败", err)
return
}
var data []vo.ClientStatus
// 遍历客户端数据,并渲染数据信息
for _, d := range devices {
for _, p := range d.Peers {
clientInfo, err := repository.Client().GetByPublicKey(p.PublicKey.String())
if err != nil {
log.Errorf("没有找到公钥匹配的客户端: %s", p.PublicKey.String())
continue
}
var ipAllocation string
for _, iaip := range p.AllowedIPs {
ipAllocation += iaip.String() + ","
}
ipAllocation = strings.TrimRight(ipAllocation, ",")
isOnline := time.Since(p.LastHandshakeTime).Minutes() < 3
data = append(data, vo.ClientStatus{
ID: clientInfo.Id,
Name: clientInfo.Name,
Email: clientInfo.Email,
IpAllocation: ipAllocation,
Endpoint: p.Endpoint.String(),
Received: utils.FlowCalculation().Parse(p.ReceiveBytes),
Transmitted: utils.FlowCalculation().Parse(p.TransmitBytes),
IsOnline: isOnline,
LastHandShake: p.LastHandshakeTime.Format("2006-01-02 15:04:05"),
})
} }
}
utils.GinResponse(c).OKWithData(data) png, err := utils.QRCode().GenerateQrCodeBase64(fileContent, 256)
} if err != nil {
response.R(c).FailedWithError("生成二维码失败")
// Offline return
// @description: 强制下线指定客户端
// @receiver clients
// @param c
func (clients) Offline(c *gin.Context) {
id := c.Param("id")
if id == "" || id == "undefined" {
utils.GinResponse(c).FailedWithMsg("参数错误")
return
}
// 查询一下客户端信息
clientInfo, err := repository.Client().GetById(id)
if err != nil {
utils.GinResponse(c).FailedWithMsg("获取信息失败")
return
}
keys := template_data.Keys{}
_ = json.Unmarshal([]byte(clientInfo.Keys), &keys)
connectInfo, err := utils.Wireguard().GetSpecClient(keys.PublicKey)
if err != nil {
utils.GinResponse(c).FailedWithMsg("获取客户端信息失败")
return
}
if connectInfo == nil {
utils.GinResponse(c).FailedWithMsg("未获取到该客户端链接信息")
return
}
// 获取到了,执行踢下线操作。此处踢下线就是禁用该客户端
if err = repository.Client().Disabled(clientInfo.Id); err != nil {
utils.GinResponse(c).FailedWithErr("客户端下线失败: %v", err)
return
}
// 再同步一下配置文件
go func() {
if err = queues.PutAsyncWireguardConfigFile(clientInfo.ServerId); err != nil {
log.Errorf("[下线客户端]同步配置文件失败: %v", err.Error())
} }
}()
utils.GinResponse(c).OK() if err = os.Remove(outPath); err != nil {
log.Errorf("删除临时文件失败: %s", err.Error())
}
response.R(c).OkWithData(map[string]interface{}{
"qrCode": png,
})
case "FILE": // 文件
// 输出文件流
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename="+outPath)
c.Header("Content-Transfer-Encoding", "binary")
c.Header("Connection", "keep-alive")
c.File(outPath)
if err = os.Remove(outPath); err != nil {
log.Errorf("删除临时文件失败: %s", err.Error())
}
case "EMAIL": // 邮件
if data.Email == "" {
response.R(c).FailedWithError("当前客户端并未配置通知邮箱!")
return
}
err = utils.Mail().SendMail(data.Email, fmt.Sprintf("客户端: %s", data.Name), "请查收附件", outPath)
if err != nil {
response.R(c).FailedWithError("发送邮件失败")
return
}
if err = os.Remove(outPath); err != nil {
log.Errorf("删除临时文件失败: %s", err.Error())
}
response.R(c).OK()
}
} }

View File

@ -1,42 +0,0 @@
package api
import (
"github.com/gin-gonic/gin"
"wireguard-dashboard/http/param"
"wireguard-dashboard/model/entity"
"wireguard-dashboard/repository"
"wireguard-dashboard/utils"
)
type dashboard struct{}
func Dashboard() dashboard {
return dashboard{}
}
// List
// @description: 操作日志分页列表
// @receiver d
// @param c
func (d dashboard) List(c *gin.Context) {
var p param.OnlyPage
if err := c.ShouldBind(&p); err != nil {
utils.GinResponse(c).FailedWithErr("参数错误", err)
return
}
// 如果不是超级管理员只能看自己的
userInfo, ok := c.Get("user")
if !ok {
utils.GinResponse(c).AuthorizationFailed()
return
}
data, count, err := repository.SystemLog().List(p, userInfo.(*entity.User))
if err != nil {
utils.GinResponse(c).FailedWithErr("获取失败", err)
return
}
utils.GinResponse(c).OkWithPage(data, count, p.Current, p.Size)
}

87
http/api/login.go Normal file
View File

@ -0,0 +1,87 @@
package api
import (
"fmt"
"gitee.ltd/lxh/logger/log"
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
"wireguard-ui/component"
"wireguard-ui/http/param"
"wireguard-ui/http/response"
"wireguard-ui/service"
"wireguard-ui/utils"
)
type LoginApi struct{}
func Login() LoginApi {
return LoginApi{}
}
// Captcha
// @description: 获取验证码
// @receiver login
// @param c
func (LoginApi) Captcha(c *gin.Context) {
math := base64Captcha.DriverMath{Height: 120, Width: 480, Fonts: []string{"ApothecaryFont.ttf", "3Dumb.ttf"}}
mathDriver := math.ConvertFonts()
capt := base64Captcha.NewCaptcha(mathDriver, component.Captcha{})
id, base64Str, _, err := capt.Generate()
if err != nil {
response.R(c).FailedWithError(fmt.Errorf("生成验证码失败: %s", err.Error()))
return
}
response.R(c).OkWithData(map[string]any{
"id": id,
"captcha": base64Str,
})
}
// Login
// @description: 登陆
// @receiver login
// @param c
func (LoginApi) Login(c *gin.Context) {
var p param.Login
if err := c.ShouldBind(&p); err != nil {
response.R(c).Validator(err)
return
}
// 验证验证码是否正确
ok := component.Captcha{}.Verify(p.CaptchaId, p.CaptchaCode, true)
if !ok {
response.R(c).FailedWithError("验证码错误")
return
}
// 验证码正确,查询用户信息
user, err := service.User().GetUserByAccount(p.Account)
if err != nil {
response.R(c).FailedWithError("获取用户信息失败")
return
}
// 对比密码
if !utils.Password().ComparePassword(user.Password, p.Password) {
response.R(c).FailedWithError("密码错误")
return
}
// 生成token
token, expireAt, err := component.JWT().GenerateToken(user.Id)
if err != nil {
log.Errorf("用户[%s]生成token失败: %v", user.Account, err.Error())
response.R(c).FailedWithError("登陆失败!")
return
}
response.R(c).OkWithData(map[string]any{
"token": token,
"type": "Bearer",
"expireAt": expireAt,
})
}

View File

@ -1,110 +0,0 @@
package api
import (
"gitee.ltd/lxh/logger/log"
"github.com/gin-gonic/gin"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"wireguard-dashboard/command"
"wireguard-dashboard/http/param"
"wireguard-dashboard/model/entity"
"wireguard-dashboard/queues"
"wireguard-dashboard/repository"
"wireguard-dashboard/utils"
)
type server struct{}
func Server() server {
return server{}
}
// SaveServer
// @description: 新增/更新服务端信息
// @receiver server
// @param c
func (server) SaveServer(c *gin.Context) {
var p param.SaveServer
if err := c.ShouldBind(&p); err != nil {
utils.GinResponse(c).FailedWithErr("参数错误", err)
return
}
var err error
var serverId string
if p.Id != "" {
serverId = p.Id
if err = repository.Server().Update(p); err != nil {
log.Errorf("更改服务端信息失败: %v", err.Error())
}
} else {
privateKey, err := wgtypes.GeneratePrivateKey()
if err != nil {
utils.GinResponse(c).FailedWithMsg("生成密钥失败")
return
}
publicKey := privateKey.PublicKey()
serverInfo := &entity.Server{
IpScope: p.IpScope,
ListenPort: p.ListenPort,
PrivateKey: privateKey.String(),
PublicKey: publicKey.String(),
PostUpScript: p.PostUpScript,
PreDownScript: p.PreDownScript,
PostDownScript: p.PostDownScript,
}
if err = repository.Server().Save(serverInfo); err != nil {
log.Errorf("新增服务端失败: %v", err.Error())
}
serverId = serverInfo.Id
}
if err != nil {
utils.GinResponse(c).FailedWithMsg("操作失败")
return
}
go func() {
if err = queues.PutAsyncWireguardConfigFile(serverId); err != nil {
log.Errorf("[新增/编辑]投递同步配置文件任务失败: %s", err.Error())
}
}()
utils.GinResponse(c).OK()
}
// GetServer
// @description: 获取服务端信息
// @receiver wireguard
// @param c
func (server) GetServer(c *gin.Context) {
data, err := repository.Server().GetServer()
if err != nil {
log.Errorf("获取服务端信息失败: %v", err.Error())
}
utils.GinResponse(c).OKWithData(data)
}
// ControlServer
// @description: 服务端控制器
// @receiver server
// @param c
func (server) ControlServer(c *gin.Context) {
var p param.ControlServer
if err := c.ShouldBind(&p); err != nil {
utils.GinResponse(c).FailedWithErr("参数错误", err)
return
}
switch p.Status {
case "START":
command.StartWireguard()
case "STOP":
command.StopWireguard()
case "RESTART":
command.RestartWireguard(false)
}
utils.GinResponse(c).OK()
}

View File

@ -1,15 +1,11 @@
package api package api
import ( import (
"encoding/json"
"gitee.ltd/lxh/logger/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"wireguard-dashboard/config" "wireguard-ui/http/param"
"wireguard-dashboard/http/param" "wireguard-ui/http/response"
"wireguard-dashboard/model/entity" "wireguard-ui/model"
"wireguard-dashboard/queues" "wireguard-ui/service"
"wireguard-dashboard/repository"
"wireguard-dashboard/utils"
) )
type setting struct{} type setting struct{}
@ -18,88 +14,63 @@ func Setting() setting {
return setting{} return setting{}
} }
// SetSetting // Set
// @description: 添加/更改设置 // @description: 置配
// @receiver setting // @receiver setting
// @param c // @param c
func (setting) SetSetting(c *gin.Context) { func (setting) Set(c *gin.Context) {
var p param.SetSetting var p param.SetSetting
if err := c.ShouldBind(&p); err != nil { if err := c.ShouldBind(&p); err != nil {
utils.GinResponse(c).FailedWithErr("参数错误", err) response.R(c).Validator(err)
return return
} }
if err := repository.System().Save(&entity.Setting{ if err := service.Setting().SetData(&model.Setting{
Code: p.Code, Code: p.Code,
Data: p.Data, Data: p.Data,
Describe: p.Describe,
}); err != nil { }); err != nil {
utils.GinResponse(c).FailedWithErr("操作失败", err) response.R(c).FailedWithError(err)
return return
} }
utils.GinResponse(c).OK() response.R(c).OK()
} }
// SetServerGlobal // Delete
// @description: 设置服务端的全局设定 // @description: 删除配置
// @receiver setting // @receiver setting
// @param c // @param c
func (setting) SetServerGlobal(c *gin.Context) { func (setting) Delete(c *gin.Context) {
var p param.SetServerGlobal code := c.Param("code")
if err := c.ShouldBind(&p); err != nil { if code == "" || code == "undefined" {
utils.GinResponse(c).FailedWithErr("参数错误", err) response.R(c).FailedWithError("code不能为空")
return return
} }
data, _ := json.Marshal(p) if err := service.Setting().Model(&model.Setting{}).Where("code = ?", code).Delete(&model.Setting{}).Error; err != nil {
response.R(c).FailedWithError("删除失败")
var ent entity.Setting
ent.Code = "SERVER_SETTING"
ent.Data = string(data)
if err := repository.System().Save(&ent); err != nil {
utils.GinResponse(c).FailedWithErr("操作失败", err)
return return
} }
go func() { response.R(c).OK()
if err := queues.PutAsyncWireguardConfigFile(""); err != nil {
log.Errorf("[设置服务端],发起同步配置文件失败: %v", err.Error())
}
}()
utils.GinResponse(c).OK()
} }
// GetGlobalSetting // GetSetting
// @description: 获取全局设置配置 // @description: 获取指定配置
// @receiver setting // @receiver setting
// @param c // @param c
func (setting) GetGlobalSetting(c *gin.Context) { func (setting) GetSetting(c *gin.Context) {
data, err := repository.System().GetServerSetting() code := c.Query("code")
if err != nil { if code == "" {
log.Errorf("获取配置失败: %v", err.Error()) response.R(c).FailedWithError("code不能为空")
return
} }
utils.GinResponse(c).OKWithData(data) var data *model.Setting
} if err := service.Setting().Model(&model.Setting{}).Where("code = ?", code).Take(&data).Error; err != nil {
response.R(c).FailedWithError("获取指定配置失败")
return
}
// GetPublicNetworkIP response.R(c).OkWithData(data.Data)
// @description: 获取当前机器的公网IP
// @receiver setting
// @param c
func (setting) GetPublicNetworkIP(c *gin.Context) {
utils.GinResponse(c).OKWithData(map[string]string{
"IP": utils.Network().GetHostPublicIP(),
})
}
// GetServerRestartRule
// @description: 获取服务重启规则
// @receiver setting
// @param c
func (setting) GetServerRestartRule(c *gin.Context) {
utils.GinResponse(c).OKWithData(map[string]string{
"rule": config.Config.Wireguard.ListenConfig,
})
} }

View File

@ -2,292 +2,267 @@ package api
import ( import (
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"gitee.ltd/lxh/logger/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"strings" "wireguard-ui/global/constant"
"wireguard-dashboard/client" "wireguard-ui/http/param"
"wireguard-dashboard/component" "wireguard-ui/http/response"
"wireguard-dashboard/constant" "wireguard-ui/model"
"wireguard-dashboard/http/param" "wireguard-ui/service"
"wireguard-dashboard/model/entity" "wireguard-ui/utils"
"wireguard-dashboard/model/vo"
"wireguard-dashboard/repository"
"wireguard-dashboard/utils"
) )
type user struct{} type UserApi struct{}
func UserApi() user { func User() UserApi {
return user{} return UserApi{}
} }
// Login // GetLoginUser
// @description: 登陆
// @receiver u
// @param c
func (user) Login(c *gin.Context) {
var p param.Login
if err := c.ShouldBind(&p); err != nil {
utils.GinResponse(c).FailedWithErr("参数错误", err)
return
}
// 校验验证码
pass := component.CaptchaStore{}.Verify(p.CaptchaId, p.CaptchaAnswer, true)
if !pass {
utils.GinResponse(c).FailedWithMsg("验证码错误")
return
}
// 校验用户是否存在
user, err := repository.User().GetUserByAccount(p.Account)
if err != nil {
utils.GinResponse(c).FailedWithMsg("账户不存在")
return
}
if user.Status != constant.Normal {
utils.GinResponse(c).FailedWithMsg("账户状态异常")
return
}
// 校验密码
if !utils.Password().ComparePassword(user.Password, p.Password) {
utils.GinResponse(c).FailedWithMsg("密码错误")
return
}
// 生成token
token, expireTime, err := component.JWT().GenerateToken(user.Id)
if err != nil {
utils.GinResponse(c).FailedWithMsg("登陆失败")
return
}
utils.GinResponse(c).OKWithData(map[string]any{
"token": token,
"type": "Bearer",
"expireAt": expireTime.Unix(),
})
}
// Logout
// @description: 退出登陆
// @receiver u
// @param c
func (user) Logout(c *gin.Context) {
data, ok := c.Get("user")
if !ok {
utils.GinResponse(c).FailedWithMsg("你还没有登陆")
return
}
if err := component.JWT().Logout(data.(*entity.User).Id); err != nil {
log.Errorf("退出登陆失败: %v", err.Error())
utils.GinResponse(c).FailedWithMsg("退出登陆失败")
return
}
utils.GinResponse(c).OK()
}
// List
// @description: 用户列表
// @receiver u
// @param c
func (user) List(c *gin.Context) {
var p param.UserList
if err := c.ShouldBind(&p); err != nil {
utils.GinResponse(c).FailedWithErr("参数错误", err)
return
}
data, total, err := repository.User().List(p)
if err != nil {
utils.GinResponse(c).FailedWithMsg("获取失败")
return
}
utils.GinResponse(c).OkWithPage(data, total, p.Current, p.Size)
}
// GetUser
// @description: 获取登陆用户信息 // @description: 获取登陆用户信息
// @receiver u // @receiver UserApi
// @param c // @param c
func (user) GetUser(c *gin.Context) { func (UserApi) GetLoginUser(c *gin.Context) {
info, ok := c.Get("user") loginUser, ok := c.Get("user")
if !ok { if !ok {
utils.GinResponse(c).FailedWithMsg("获取信息失败") response.R(c).AuthorizationFailed("未登陆")
return return
} }
data := &vo.User{
Id: info.(*entity.User).Id, response.R(c).OkWithData(loginUser)
Name: info.(*entity.User).Name,
Avatar: info.(*entity.User).Avatar,
Account: info.(*entity.User).Account,
Email: info.(*entity.User).Email,
IsAdmin: info.(*entity.User).IsAdmin,
Status: info.(*entity.User).Status,
CreatedAt: info.(*entity.User).CreatedAt,
UpdatedAt: info.(*entity.User).UpdatedAt,
}
utils.GinResponse(c).OKWithData(data)
} }
// Save // SaveUser
// @description: 新增/更改用户信息 // @description: 新增/编辑用户信息
// @receiver u // @receiver UserApi
// @param c // @param c
func (user) Save(c *gin.Context) { func (UserApi) SaveUser(c *gin.Context) {
var p param.SaveUser var p param.SaveUser
if err := c.ShouldBind(&p); err != nil { if err := c.ShouldBind(&p); err != nil {
utils.GinResponse(c).FailedWithErr("参数错误", err) response.R(c).Validator(err)
return return
} }
// 只有新增才会判断 // 如果是新增用户判断该用户是否已经存在
if p.ID == "" { if p.Id == "" {
// 判断用户是否已经存在 if len(p.Account) < 2 || len(p.Account) > 20 {
response.R(c).FailedWithError(errors.New("账号长度在2-20位"))
return
}
if len(p.Password) < 8 || len(p.Password) > 32 {
response.R(c).FailedWithError(errors.New("密码长度在8-32位"))
return
}
var count int64 var count int64
if err := client.DB.Model(&entity.User{}).Where("account = ?", p.Account).Count(&count).Error; err != nil { if err := service.User().Model(&model.User{}).Where("account = ?", p.Account).Count(&count).Error; err != nil {
utils.GinResponse(c).FailedWithMsg("查询失败") response.R(c).FailedWithError(err)
return return
} }
if count > 0 { if count > 0 {
utils.GinResponse(c).FailedWithMsg("用户已存在!") response.R(c).FailedWithError(errors.New("该账号已存在"))
return return
} }
} }
// 只有修改才有头像值 userEnt := &model.User{
if p.Avatar != "" && p.ID != "" { Base: model.Base{
// 判断头像是base64开头的就需要重新上传更新 Id: p.Id,
if strings.HasPrefix(p.Avatar, "data:image/png;base64,") {
avatar := strings.Replace(p.Avatar, "data:image/png;base64,", "", -1)
avatarByte, err := base64.StdEncoding.DecodeString(avatar)
if err != nil {
log.Errorf("反解析头像失败: %v", err.Error())
utils.GinResponse(c).FailedWithMsg("上传头像失败")
return
}
file, err := utils.FileSystem().UploadFile(avatarByte, ".png")
if err != nil {
log.Errorf("上传头像失败: %v", err.Error())
utils.GinResponse(c).FailedWithMsg("上传头像失败")
return
}
p.Avatar = file
}
}
if err := repository.User().Save(&entity.User{
Base: entity.Base{
Id: p.ID,
}, },
Avatar: p.Avatar,
Name: p.Name,
Account: p.Account, Account: p.Account,
Email: p.Email,
Password: p.Password, Password: p.Password,
Nickname: p.Nickname,
Avatar: p.Avatar,
Contact: p.Contact,
IsAdmin: *p.IsAdmin, IsAdmin: *p.IsAdmin,
Status: *p.Status, Status: *p.Status,
}); err != nil { }
utils.GinResponse(c).FailedWithMsg(err.Error())
if err := service.User().CreateUser(userEnt); err != nil {
response.R(c).FailedWithError(err)
return return
} }
utils.GinResponse(c).OK() response.R(c).OK()
}
// List
// @description: 用户列表
// @receiver UserApi
// @param c
func (UserApi) List(c *gin.Context) {
var p param.Page
if err := c.ShouldBind(&p); err != nil {
response.R(c).Validator(err)
return
}
data, total, err := service.User().List(p)
if err != nil {
response.R(c).FailedWithError(err)
return
}
response.R(c).Paginate(data, total, p.Current, p.Size)
}
// Delete
// @description: 删除用户
// @receiver UserApi
// @param c
func (UserApi) Delete(c *gin.Context) {
id := c.Param("id")
if id == "" || id == "undefined" {
response.R(c).FailedWithError("id不能为空")
return
}
// 是不是自己删除自己
if id == GetCurrentLoginUser(c).Id && c.IsAborted() {
response.R(c).FailedWithError("非法操作")
return
}
// 先查询一下
user, err := service.User().GetUserById(id)
if err != nil {
response.R(c).FailedWithError("获取用户信息失败")
return
}
// admin用户不能被删除
if user.Account == "admin" {
response.R(c).FailedWithError("当前用户不能被删除")
return
}
if err = service.User().Delete(id); err != nil {
response.R(c).FailedWithError("删除用户失败")
return
}
response.R(c).OK()
}
// Status
// @description: 设置用户状态
// @receiver UserApi
// @param c
func (UserApi) Status(c *gin.Context) {
id := c.Param("id")
if id == "" || id == "undefined" {
response.R(c).FailedWithError("id不能为空")
return
}
// 是不是自己删除自己
if id == GetCurrentLoginUser(c).Id && c.IsAborted() {
response.R(c).FailedWithError("非法操作")
return
}
// 先查询一下
user, err := service.User().GetUserById(id)
if err != nil {
response.R(c).FailedWithError("获取用户信息失败")
return
}
// admin用户不能被删除
if user.Account == "admin" {
response.R(c).FailedWithError("当前用户状态不可被变更")
return
}
var state = constant.Enabled
if user.Status == constant.Enabled {
state = constant.Disabled
}
if err := service.User().Status(id, state); err != nil {
response.R(c).FailedWithError(err)
return
}
response.R(c).OK()
} }
// ChangePassword // ChangePassword
// @description: 更改密码 // @description: 改密码
// @receiver u // @receiver UserApi
// @param c // @param c
func (user) ChangePassword(c *gin.Context) { func (UserApi) ChangePassword(c *gin.Context) {
var p param.ChangePassword var p param.ChangePassword
if err := c.ShouldBind(&p); err != nil { if err := c.ShouldBind(&p); err != nil {
utils.GinResponse(c).FailedWithErr("参数错误", err) response.R(c).Validator(err)
return return
} }
user, ok := c.Get("user") user := GetCurrentLoginUser(c)
if !ok { if user == nil {
utils.GinResponse(c).AuthorizationFailed() response.R(c).FailedWithError("用户信息错误")
return return
} }
if !utils.Password().ComparePassword(user.(*entity.User).Password, p.OriginPassword) { // 判断原密码是否对
utils.GinResponse(c).FailedWithMsg("原密码错误") if !utils.Password().ComparePassword(user.Password, p.OriginalPassword) {
response.R(c).FailedWithError("原密码错误")
return return
} }
// 开始变更密码 // 修改密码
if err := repository.User().ChangePassword(p, user.(*entity.User).Id); err != nil { if err := service.User().ChangePassword(user.Id, p.NewPassword); err != nil {
utils.GinResponse(c).FailedWithMsg("更改密码失败") response.R(c).FailedWithError(err)
return return
} }
utils.GinResponse(c).OK() response.R(c).OK()
} }
// ChangeUserState // ResetPassword
// @description: 改变用户状态 // @description: 重置密码
// @receiver u // @receiver UserApi
// @param c // @param c
func (user) ChangeUserState(c *gin.Context) { func (UserApi) ResetPassword(c *gin.Context) {
var p param.ChangeUserState
if err := c.ShouldBind(&p); err != nil {
utils.GinResponse(c).FailedWithErr("参数错误", err)
return
}
if err := repository.User().ChangeUserState(p); err != nil {
utils.GinResponse(c).FailedWithMsg("操作失败")
return
}
utils.GinResponse(c).OK()
}
// DeleteUser
// @description: 删除用户
// @receiver user
// @param c
func (user) DeleteUser(c *gin.Context) {
var id = c.Param("id") var id = c.Param("id")
if id == "" || id == "undefined" { if id == "" || id == "undefined" {
utils.GinResponse(c).FailedWithMsg("参数错误") response.R(c).FailedWithError("id不能为空")
return return
} }
loginUser, ok := c.Get("user") // 先查询一下
if !ok { user, err := service.User().GetUserById(id)
utils.GinResponse(c).FailedWithMsg("获取信息失败") if err != nil {
response.R(c).FailedWithError("获取用户信息失败")
return return
} }
if err := repository.User().DeleteUser(loginUser.(*entity.User), id); err != nil { if user.Status != constant.Enabled {
utils.GinResponse(c).FailedWithErr("操作失败", err) response.R(c).FailedWithError("当前用户不可重置密码")
return return
} }
utils.GinResponse(c).OK() // 修改密码
if err := service.User().ChangePassword(user.Id, "admin123"); err != nil {
response.R(c).FailedWithError(err)
return
}
response.R(c).OK()
} }
// ChangeAvatar // GenerateAvatar
// @description: 切换头像 // @description: 生成头像
// @receiver user // @receiver UserApi
// @param c // @param c
func (user) ChangeAvatar(c *gin.Context) { func (UserApi) GenerateAvatar(c *gin.Context) {
avatar, err := utils.Avatar().GenerateAvatar(false) avatar, err := utils.Avatar().GenerateAvatar(false)
if err != nil { if err != nil {
utils.GinResponse(c).FailedWithErr("生成头像失败", err) response.R(c).FailedWithError(fmt.Errorf("生成头像失败: %s", err.Error()))
return return
} }
utils.GinResponse(c).OKWithData(fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString([]byte(avatar)))) response.R(c).OkWithData(fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString([]byte(avatar))))
} }

View File

@ -0,0 +1,57 @@
package middleware
import (
"github.com/gin-gonic/gin"
"strings"
"wireguard-ui/component"
"wireguard-ui/global/constant"
"wireguard-ui/http/response"
"wireguard-ui/service"
"wireguard-ui/utils"
)
// Authorization
// @description: 授权中间件
// @return gin.HandlerFunc
func Authorization() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" || !strings.HasPrefix(token, "Bearer ") {
response.R(c).AuthorizationFailed("未登陆")
c.Abort()
return
}
userClaims, err := component.JWT().ParseToken(token)
if err != nil {
response.R(c).AuthorizationFailed("未登陆")
c.Abort()
return
}
// 如果token的颁发者与请求的站点不一致那么就给它抬出去
if userClaims.Issuer != utils.WebSite().GetHost(c.Request.Header.Get("Referer")) {
response.R(c).AuthorizationFailed("未登陆")
c.Abort()
return
}
// 查询用户
user, err := service.User().GetUserById(userClaims.ID)
if err != nil {
response.R(c).FailedWithError("用户不存在")
c.Abort()
return
}
if user.Status != constant.Enabled {
response.R(c).FailedWithError("用户状态异常,请联系管理员处理!")
c.Abort()
return
}
// 将用户信息放入上下文
c.Set("user", &user)
c.Next()
}
}

View File

@ -1,10 +0,0 @@
package param
type page struct {
Current int `json:"current" form:"current" binding:"required"`
Size int `json:"size" form:"size" binding:"required"`
}
type OnlyPage struct {
page
}

View File

@ -1,51 +1,40 @@
package param package param
import "wireguard-dashboard/model/template_data" import (
"wireguard-ui/global/constant"
// ClientList )
// @description: 客户端列表
type ClientList struct {
Name string `json:"name" form:"name"`
Email string `json:"email" form:"email"`
Ip string `json:"ip" form:"ip"`
CreateUser string `json:"createUser" form:"createUser"`
Enabled *int `json:"enabled" form:"enabled"`
page
}
// ClientStatusList
// @description: 客户端状态列表
type ClientStatusList struct {
page
}
// SaveClient // SaveClient
// @description: 新增/编辑客户端 // @description: 新增/编辑客户端
type SaveClient struct { type SaveClient struct {
Id string `json:"id" form:"id" binding:"omitempty"` Id string `json:"id" form:"id" label:"id" binding:"omitempty"` // id
ServerId string `json:"serverId" form:"serverId" binding:"required"` Name string `json:"name" form:"name" label:"名称" binding:"required,min=1,max=64"` // 名称
Name string `json:"name" form:"name" binding:"required"` Email string `json:"email" form:"email" label:"联系邮箱" binding:"omitempty"` // 联系邮箱
Email string `json:"email" form:"email" binding:"omitempty"` SubnetRange string `json:"subnetRange" form:"subnetRange" label:"子网范围" binding:"omitempty"` // 子网范围
SubnetRange string `json:"subnetRange" form:"subnetRange" binding:"omitempty"` IpAllocation []string `json:"ipAllocation" form:"ipAllocation" label:"客户端IP" binding:"required,dive"` // IP地址
IpAllocation []string `json:"ipAllocation" form:"ipAllocation" binding:"required"` AllowedIps []string `json:"allowedIps" form:"allowedIps" label:"allowedIps" binding:"omitempty,dive"` // 允许访问的IP段
AllowedIPS []string `json:"allowedIPS" form:"allowedIPS" binding:"required"` ExtraAllowedIps []string `json:"extraAllowedIps" form:"extraAllowedIps" label:"extraAllowedIps" binding:"omitempty,dive"` // 其他允许访问的IP段
ExtraAllowedIPS []string `json:"extraAllowedIPS" form:"extraAllowedIPS" binding:"omitempty"` Endpoint string `json:"endpoint" form:"endpoint" label:"endpoint" binding:"omitempty"` // 服务端地址
Endpoint string `json:"endpoint" form:"endpoint" binding:"omitempty"` UseServerDns *constant.Status `json:"useServerDns" form:"useServerDns" label:"useServerDns" binding:"required,oneof=0 1"` // 是否使用服务端DNS 1 - 是 | 0 - 否
UseServerDNS *int `json:"useServerDNS" form:"useServerDNS" binding:"required,oneof=1 0"` Keys *Keys `json:"keys" form:"keys" label:"密钥信息" binding:"required"` // 密钥
EnabledAfterCreation *int `json:"enableAfterCreation" form:"enableAfterCreation" binding:"required,oneof=1 0"` Enabled *constant.Status `json:"enabled" form:"enabled" label:"状态" binding:"required,oneof=0 1"` // 状态 1 - 启用 | 0 - 禁用
Keys *template_data.Keys `json:"keys" form:"keys" binding:"omitempty"` OfflineMonitoring *constant.Status `json:"offlineMonitoring" form:"offlineMonitoring" label:"离线通知" binding:"required,oneof=0 1"` // 离线通知 1 - 启用 | 0 - 禁用
Enabled *int `json:"enabled" form:"enabled" binding:"required,oneof=1 0"`
OfflineMonitoring *int `json:"offlineMonitoring" form:"offlineMonitoring" binding:"required,oneof=1 0"`
} }
// ControlServer // Keys
// @description: 服务端控制 // @description: 客户端密钥信息
type ControlServer struct { type Keys struct {
Status string `json:"status" form:"status" binding:"required,oneof=START STOP RESTART"` PrivateKey string `json:"privateKey" form:"privateKey" label:"私钥" binding:"required"`
PublicKey string `json:"publicKey" form:"publicKey" label:"公钥" binding:"required"`
PresharedKey string `json:"presharedKey" form:"presharedKey" label:"共享密钥" binding:"required"`
} }
// AssignIPAndAllowedIP // ClientList
// @description: 分配IP和允许访问的IP段 // @description: 客户端列表
type AssignIPAndAllowedIP struct { type ClientList struct {
Rule string `json:"rule" form:"rule" binding:"required,oneof=RANDOM AUTO"` // 分配IP的规则 RANDOM - 固定 | AUTO - 自动生成 Name string `json:"name" form:"name" label:"名称" binding:"omitempty"` // 客户端名称
Email string `json:"email" form:"email" label:"邮箱" binding:"omitempty,email"` // 联系邮箱
IpAllocation string `json:"ipAllocation" form:"ipAllocation" label:"IP范围段" binding:"omitempty"` // 客户端IP
Enabled *int `json:"enabled" form:"enabled" label:"状态" binding:"omitempty,oneof=0 1"` // 客户端状态
Page
} }

10
http/param/login.go Normal file
View File

@ -0,0 +1,10 @@
package param
// Login
// @description: 登陆
type Login struct {
Account string `json:"account" form:"account" label:"账号" binding:"required,min=2,max=20"`
Password string `json:"password" form:"password" label:"密码" binding:"required,min=8,max=32"`
CaptchaId string `json:"captchaId" form:"captchaId" label:"验证码ID" binding:"required"`
CaptchaCode string `json:"captchaCode" form:"captchaCode" label:"验证码" binding:"required,len=4"`
}

6
http/param/request.go Normal file
View File

@ -0,0 +1,6 @@
package param
type Page struct {
Current int64 `json:"current" form:"current" label:"页码数" binding:"required"`
Size int64 `json:"size" form:"size" label:"每页数量" binging:"required"`
}

View File

@ -1,12 +1,11 @@
package param package param
type SaveServer struct { type SaveServer struct {
Id string `json:"id" form:"id" binding:"omitempty"` // id IPScope []string `json:"ipScope" form:"IPScope" label:"IPScope" binding:"required"`
IpScope string `json:"ipScope" form:"ipScope" binding:"required"` // 内网ip范围段 ListenPort uint64 `json:"listenPort" form:"listenPort" label:"listenPort" binding:"required"`
ListenPort int `json:"listenPort" form:"listenPort" binding:"required"` // 监听端口 PrivateKey string `json:"privateKey" form:"privateKey" label:"privateKey" binding:"required"`
PrivateKey string `json:"privateKey" form:"privateKey"` // 私钥 PublicKey string `json:"publicKey" form:"publicKey" label:"publicKey" binding:"required"`
PublicKey string `json:"publicKey" form:"publicKey"` // 密钥 PostUpScript string `json:"postUpScript,omitempty" form:"postUpScript" label:"postUpScript" binding:"omitempty"`
PostUpScript string `json:"postUpScript" form:"postUpScript" binding:"omitempty"` PreDownScript string `json:"preDownScript,omitempty" form:"preDownScript" label:"preDownScript" binding:"omitempty"`
PreDownScript string `json:"preDownScript" form:"preDownScript" binding:"omitempty"` PostDownScript string `json:"postDownScript,omitempty" form:"postDownScript" label:"postDownScript" binding:"omitempty"`
PostDownScript string `json:"postDownScript" form:"postDownScript" binding:"omitempty"`
} }

View File

@ -1,21 +1,8 @@
package param package param
// SetSetting // SetSetting
// @description: 设置 // @description: 添加/编辑设置
type SetSetting struct { type SetSetting struct {
Code string `json:"code" form:"code" binding:"required"` // 设置的唯一编码 Code string `json:"code" form:"code" binding:"required"`
Data string `json:"data" form:"data" binding:"required"` // 数据 Data string `json:"data" form:"data" binding:"required"`
Describe string `json:"describe" form:"describe" binding:"omitempty"` // 描述
}
// SetServerGlobal
// @description: 设置服务端全局配置
type SetServerGlobal struct {
EndpointAddress string `json:"endpointAddress" binding:"required"` // 服务公网IP
DnsServer []string `json:"dnsServer" binding:"required"` // DNS列表
MTU int `json:"MTU" binding:"required"`
PersistentKeepalive int `json:"persistentKeepalive" binding:"omitempty"`
FirewallMark string `json:"firewallMark" binding:"omitempty"`
Table string `json:"table" binding:"omitempty"`
ConfigFilePath string `json:"configFilePath" binding:"required"` // 配置文件对外输出目录
} }

View File

@ -1,46 +1,24 @@
package param package param
import "wireguard-dashboard/constant" import "wireguard-ui/global/constant"
// Login
// @description: 登陆
type Login struct {
Account string `json:"account" form:"account" binding:"required"` // 账号
Password string `json:"password" form:"password" binding:"required"` // 密码
CaptchaId string `json:"captchaId" form:"captchaId" binding:"required"` // 验证码id
CaptchaAnswer string `json:"captchaAnswer" form:"captchaAnswer" binding:"required"` // 验证码
}
// SaveUser // SaveUser
// @description: 新增/编辑用户信息 // @description: 新增/编辑用户信息
type SaveUser struct { type SaveUser struct {
ID string `json:"id" form:"id" binding:"omitempty"` Id string `json:"id" form:"id" label:"id" binding:"omitempty"` // id
Name string `json:"name" form:"name" binding:"required"` // 用户名 Account string `json:"account" form:"account" label:"账户号" binding:"required_without=Id"` // 账户号
Account string `json:"account" form:"account" binding:"required"` // 账号 唯一 Password string `json:"password" form:"password" label:"密码" binding:"required_without=Id"` // 密码
Avatar string `json:"avatar" form:"avatar" binding:"omitempty"` // 头像 Nickname string `json:"nickname" form:"nickname" label:"昵称" binding:"required,min=2"` // 昵称
Email string `json:"email" form:"email" binding:"omitempty"` // 联系邮箱 Avatar string `json:"avatar" form:"avatar" label:"头像" binding:"omitempty"` // 头像
Password string `json:"password" form:"password" binding:"omitempty"` // 密码 Contact string `json:"contact" form:"contact" label:"联系方式" binding:"omitempty"` // 联系方式
IsAdmin *constant.UserType `json:"isAdmin" form:"isAdmin" binding:"omitempty"` // 是否为管理员 0 - 否 | 1 - 是 IsAdmin *constant.UserType `json:"isAdmin" form:"isAdmin" label:"是否为管理员" binding:"required,oneof=0 1"` // 是否为管理员 0 - 否 | 1 - 是
Status *constant.UserStatus `json:"status" form:"status" binding:"required"` // 用户状态 0 - 禁用 | 1 - 正常 Status *constant.Status `json:"status" form:"status" label:"状态" binding:"required,oneof=0 1"` // 用户状态 0 - 禁用 | 1 - 启用
} }
// ChangePassword // ChangePassword
// @description: 改密码 // @description: 改密码
type ChangePassword struct { type ChangePassword struct {
OriginPassword string `json:"originPassword" form:"originPassword" binding:"required"` // 原密码 OriginalPassword string `json:"originalPassword" form:"originalPassword" label:"原密码" binding:"required,min=8,max=32"` // 原密码
NewPassword string `json:"newPassword" form:"newPassword" binding:"required"` // 新密码 NewPassword string `json:"newPassword" form:"newPassword" label:"新密码" binding:"required,min=8,max=32"` // 新密码
ConfirmPassword string `json:"confirmPassword" form:"confirmPassword" binding:"required,eqfield=NewPassword"` // 确认密码 ConfirmPassword string `json:"confirmPassword" form:"confirmPassword" label:"确认密码" binding:"eqfield=NewPassword"` // 确认密码
}
// UserList
// @description: 用户列表
type UserList struct {
page
}
// ChangeUserState
// @description: 变更状态
type ChangeUserState struct {
ID string `json:"id" form:"id" binding:"required"` // 用户id
Status string `json:"status" form:"status" binding:"required,oneof=0 1"` // 用户状态
} }

109
http/response/response.go Normal file
View File

@ -0,0 +1,109 @@
package response
import (
"github.com/gin-gonic/gin"
"net/http"
"wireguard-ui/component"
"wireguard-ui/utils"
)
type PageData[T any] struct {
Current int `json:"current"` // 当前页码
Size int `json:"size"` // 每页数量
Total int64 `json:"total"` // 总数
TotalPage int `json:"totalPage"` // 总页数
Records T `json:"records"` // 返回数据
}
type response struct {
c *gin.Context
}
func R(c *gin.Context) response {
return response{c}
}
func (r response) OK() {
r.c.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"message": "success",
})
return
}
func (r response) OkWithData(data any) {
r.c.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"message": "success",
"data": data,
})
}
// Paginate
// @description: 页码数
// @receiver r
// @param data
// @param total
// @param current
// @param size
func (r response) Paginate(data any, total int64, current, size int64) {
// 处理一下页码、页数量
if current == -1 {
current = 1
size = total
}
// 计算总页码
totalPage := utils.Paginate().Generate(total, int(size))
// 返回结果
r.c.JSON(http.StatusOK, map[string]any{
"code": http.StatusOK,
"data": &PageData[any]{Current: int(current), Size: int(size), Total: total, TotalPage: totalPage, Records: data},
"message": "success",
})
}
func (r response) AuthorizationFailed(msg string) {
if msg == "" {
msg = "authorized failed"
}
r.c.JSON(http.StatusUnauthorized, gin.H{
"code": http.StatusUnauthorized,
"message": msg,
})
}
func (r response) Failed() {
r.c.JSON(http.StatusBadRequest, gin.H{
"code": http.StatusBadRequest,
"message": "failed",
})
}
func (r response) FailedWithError(err any) {
var errStr string
switch err.(type) {
case error:
errStr = err.(error).Error()
case string:
errStr = err.(string)
}
r.c.JSON(http.StatusBadRequest, gin.H{
"code": http.StatusBadRequest,
"message": errStr,
})
}
func (r response) Validator(err error) {
r.c.JSON(http.StatusBadRequest, gin.H{
"code": http.StatusBadRequest,
"message": component.Error(err),
})
}
func (r response) Internal() {
r.c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"message": "server error",
})
}

23
http/router/client.go Normal file
View File

@ -0,0 +1,23 @@
package router
import (
"github.com/gin-gonic/gin"
"wireguard-ui/http/api"
"wireguard-ui/http/middleware"
)
// ClientApi
// @description: 登陆相关API
// @param r
func ClientApi(r *gin.RouterGroup) {
client := r.Group("client", middleware.Authorization())
{
client.POST("", api.Client().Save) // 新增/编辑客户端
client.DELETE("/:id", api.Client().Delete) // 删除客户端
client.GET("/list", api.Client().List) // 客户端列表
client.POST("/generate-keys", api.Client().GenerateKeys) // 生成客户端密钥
client.POST("/generate-ip", api.Client().GenerateIP) // 生成客户端IP
client.GET("/download/:id/:type", api.Client().Download) // 下载客户端配置文件
}
}

18
http/router/login.go Normal file
View File

@ -0,0 +1,18 @@
package router
import (
"github.com/gin-gonic/gin"
"wireguard-ui/http/api"
)
// LoginApi
// @description: 登陆相关API
// @param r
func LoginApi(r *gin.RouterGroup) {
login := r.Group("/login")
{
login.GET("/captcha", api.Login().Captcha) // 获取登陆验证码
login.POST("", api.Login().Login) // 登陆
}
}

View File

@ -1,16 +1,15 @@
package route package router
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http" "wireguard-ui/config"
"wireguard-dashboard/web"
) )
type Option func(engine *gin.RouterGroup) type Option func(engine *gin.RouterGroup)
var options []Option var options []Option
func IncludeRouters(opts ...Option) { func includeRouters(opts ...Option) {
options = append(options, opts...) options = append(options, opts...)
} }
@ -21,11 +20,9 @@ func InitRouter() *gin.Engine {
// 将请求打印至控制台 // 将请求打印至控制台
r.Use(gin.Logger()) r.Use(gin.Logger())
//r.GET("/", func(c *gin.Context) { if config.Config.File.Type == "local" {
// c.Redirect(http.StatusMovedPermanently, "/web/") r.Static("/assets", config.Config.File.Path)
//}) }
r.StaticFS("/web", http.FS(web.Static))
for _, opt := range options { for _, opt := range options {
opt(r.Group("api")) opt(r.Group("api"))
@ -33,3 +30,12 @@ func InitRouter() *gin.Engine {
return r return r
} }
func Rooters() {
includeRouters(
LoginApi,
UserApi,
ClientApi,
SettingApi,
)
}

18
http/router/setting.go Normal file
View File

@ -0,0 +1,18 @@
package router
import (
"github.com/gin-gonic/gin"
"wireguard-ui/http/api"
)
// SettingApi
// @description: 设置相关API
// @param r
func SettingApi(r *gin.RouterGroup) {
setting := r.Group("setting")
{
setting.POST("", api.Setting().Set) // 新增/编辑设置
setting.DELETE("/:code", api.Setting().Delete) // 删除配置
setting.GET("", api.Setting().GetSetting) // 获取指定配置
}
}

24
http/router/user.go Normal file
View File

@ -0,0 +1,24 @@
package router
import (
"github.com/gin-gonic/gin"
"wireguard-ui/http/api"
"wireguard-ui/http/middleware"
)
// UserApi
// @description: 用户相关API
// @param r
func UserApi(r *gin.RouterGroup) {
userApi := r.Group("user", middleware.Authorization())
{
userApi.GET("/info", api.User().GetLoginUser) // 获取当前登陆用户信息
userApi.POST("", api.User().SaveUser) // 新增/编辑用户
userApi.DELETE("/:id", api.User().Delete) // 删除用户
userApi.GET("/list", api.User().List) // 分页列表
userApi.PUT("/status/:id", api.User().Status) // 修改用户状态
userApi.PUT("/change-password", api.User().ChangePassword) // 修改用户密码
userApi.PUT("/reset-password/:id", api.User().ResetPassword) // 重置用户密码
userApi.POST("/generate-avatar", api.User().GenerateAvatar) // 生成头像
}
}

43
http/vo/client.go Normal file
View File

@ -0,0 +1,43 @@
package vo
import "wireguard-ui/model"
// ClientItem
// @description: 客户端信息
type ClientItem struct {
Id string `json:"id"` // id
Name string `json:"name"` // 名称
Email string `json:"email"` // 通知邮箱
IpAllocation []string `json:"ipAllocation" gorm:"-"` // 分配的IP
IpAllocationStr string `json:"-" gorm:"ipAllocationStr"`
AllowedIps []string `json:"allowedIps" gorm:"-"` // 允许访问的IP
AllowedIpsStr string `json:"-" gorm:"allowedIpsStr"`
ExtraAllowedIps []string `json:"extraAllowedIps" gorm:"-"` // 其他允许访问的IP
ExtraAllowedIpsStr string `json:"-" gorm:"extraAllowedIpsStr"`
Endpoint string `json:"endpoint"` // 服务端点
UseServerDns int `json:"useServerDns"` // 是否使用服务端DNS
Keys *Keys `json:"keys" gorm:"-"` // 密钥等
KeysStr string `json:"-" gorm:"keys_str"`
CreateUser string `json:"createUser"` // 创建人
Enabled int `json:"enabled"` // 是否启用
OfflineMonitoring int `json:"offlineMonitoring"` // 离线通知
DataTraffic *DataTraffic `json:"dataTraffic" gorm:"-"` // 数据流量
CreatedAt model.JsonTime `json:"createdAt"` // 创建时间
UpdatedAt model.JsonTime `json:"updatedAt"` // 更新时间
}
type Keys struct {
PrivateKey string `json:"privateKey"`
PublicKey string `json:"publicKey"`
PresharedKey string `json:"presharedKey"`
}
// DataTraffic
// @description: 数据流量
type DataTraffic struct {
Online bool `json:"online"` // 是否在线
ReceiveBytes string `json:"receiveBytes"` // 接收流量
TransmitBytes string `json:"transmitBytes"` // 传输流量
ConnectEndpoint string `json:"connectEndpoint"` // 链接端点
LastHandAt string `json:"lastHandAt"` // 最后握手时间
}

28
http/vo/user.go Normal file
View File

@ -0,0 +1,28 @@
package vo
import "wireguard-ui/global/constant"
// UserItem
// @description: 用户列表的数据
type UserItem struct {
Id string `json:"id"`
Account string `json:"account"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Contact string `json:"contact"`
IsAdmin constant.UserType `json:"isAdmin"`
Status constant.Status `json:"status"`
}
// User
// @description: 用户信息
type User struct {
Id string `json:"id"`
Account string `json:"account"`
Password string `json:"-"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Contact string `json:"contact"`
IsAdmin constant.UserType `json:"isAdmin"`
Status constant.Status `json:"status"`
}

View File

@ -3,21 +3,21 @@ package initialize
import ( import (
"fmt" "fmt"
"gitee.ltd/lxh/logger" "gitee.ltd/lxh/logger"
"gitee.ltd/lxh/logger/log"
"github.com/cowardmrx/go_aliyun_oss" "github.com/cowardmrx/go_aliyun_oss"
"github.com/fsnotify/fsnotify"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"github.com/spf13/viper"
"golang.zx2c4.com/wireguard/wgctrl" "golang.zx2c4.com/wireguard/wgctrl"
"gopkg.in/yaml.v3"
"gorm.io/driver/mysql" "gorm.io/driver/mysql"
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/gorm" "gorm.io/gorm"
gl "gorm.io/gorm/logger" gl "gorm.io/gorm/logger"
"log"
"os"
"time" "time"
"wireguard-dashboard/client" "wireguard-ui/config"
"wireguard-dashboard/config" "wireguard-ui/global/client"
) )
// Init // Init
@ -35,15 +35,27 @@ func Init() {
// initConfig // initConfig
// @description: 初始化配置 // @description: 初始化配置
func initConfig() { func initConfig() {
configBytes, err := os.ReadFile("app.yaml") vp := viper.New()
if err != nil { vp.SetConfigFile("app.yaml")
if err := vp.ReadInConfig(); err != nil {
log.Panicf("读取配置文件失败: %v", err.Error()) log.Panicf("读取配置文件失败: %v", err.Error())
} }
err = yaml.Unmarshal(configBytes, &config.Config) if err := vp.Unmarshal(&config.Config); err != nil {
if err != nil {
log.Panicf("解析配置文件失败: %v", err.Error()) log.Panicf("解析配置文件失败: %v", err.Error())
} }
vp.OnConfigChange(func(in fsnotify.Event) {
if err := vp.Unmarshal(&config.Config); err != nil {
log.Errorf("配置文件变动,读取失败: %v", err.Error())
} else {
initDatabase()
initRedis()
initOSS()
}
})
vp.WatchConfig()
} }
// InitWireguard // InitWireguard
@ -72,9 +84,6 @@ func initDatabase() {
} }
logLevel := gl.Info logLevel := gl.Info
//if os.Getenv("GIN_MODE") == "release" {
// logLevel = gl.Error
//}
db, err := gorm.Open(dbDialector, &gorm.Config{ db, err := gorm.Open(dbDialector, &gorm.Config{
Logger: logger.NewGormLoggerWithConfig(gl.Config{ Logger: logger.NewGormLoggerWithConfig(gl.Config{
@ -96,9 +105,9 @@ func initDatabase() {
// @description: 初始化redis // @description: 初始化redis
func initRedis() { func initRedis() {
c := redis.NewClient(&redis.Options{ c := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", config.Config.Redis.Host, config.Config.Redis.Port), Addr: fmt.Sprintf("%s:%d", config.Config.Cache.Host, config.Config.Cache.Port),
Password: config.Config.Redis.Password, Password: config.Config.Cache.Password,
DB: config.Config.Redis.Db, DB: config.Config.Cache.Db,
}) })
client.Redis = c client.Redis = c

33
main.go
View File

@ -4,39 +4,32 @@ import (
"fmt" "fmt"
"gitee.ltd/lxh/logger/log" "gitee.ltd/lxh/logger/log"
"github.com/gin-contrib/pprof" "github.com/gin-contrib/pprof"
"github.com/spf13/cast"
"math/rand" "math/rand"
"net/http" "net/http"
"os"
"time" "time"
"wireguard-dashboard/config" "wireguard-ui/config"
"wireguard-dashboard/cron_task" "wireguard-ui/http/router"
"wireguard-dashboard/initialize" "wireguard-ui/initialize"
"wireguard-dashboard/queues" "wireguard-ui/script"
"wireguard-dashboard/route"
"wireguard-dashboard/script"
) )
func init() { func init() {
initialize.Init() // 初始化 initialize.Init()
if err := script.NewScript().Do(); err != nil { if err := script.New().Do(); err != nil {
log.Errorf("执行脚本失败: %v", err.Error()) log.Errorf("执行脚本失败: %v", err.Error())
} }
go queues.StartConsumer() // 启动队列
go cron_task.StartCronTask() // 启动定时任务
} }
func main() { func main() {
rand.New(rand.NewSource(time.Now().Local().UnixNano())) rand.New(rand.NewSource(time.Now().Local().UnixNano()))
route.IncludeRouters( router.Rooters()
route.CaptchaApi, handler := router.InitRouter()
route.UserApi,
route.ServerApi,
route.ClientApi,
route.SettingApi,
route.DashboardApi,
)
handler := route.InitRouter()
pprof.Register(handler) if cast.ToBool(os.Getenv("ENABLED_PPROF")) {
pprof.Register(handler, "/monitoring")
}
httpServe := http.Server{ httpServe := http.Server{
Addr: fmt.Sprintf(":%d", config.Config.Http.Port), Addr: fmt.Sprintf(":%d", config.Config.Http.Port),

View File

@ -1,56 +0,0 @@
package middleware
import (
"github.com/gin-gonic/gin"
"strings"
"wireguard-dashboard/component"
"wireguard-dashboard/constant"
"wireguard-dashboard/repository"
"wireguard-dashboard/utils"
)
// Authorization
// @description: 授权中间件
// @return gin.HandlerFunc
func Authorization() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" || !strings.HasPrefix(token, "Bearer ") {
utils.GinResponse(c).AuthorizationFailed()
c.Abort()
return
}
userClaims, err := component.JWT().ParseToken(token)
if err != nil {
utils.GinResponse(c).AuthorizationFailed()
c.Abort()
return
}
// 如果token的颁发者与请求的站点不一致则直接给它狗日的丢出去
if userClaims.Issuer != utils.GetHost(c.Request.Header.Get("Referer")) {
utils.GinResponse(c).AuthorizationFailed()
c.Abort()
return
}
// 查询用户
user, err := repository.User().GetUserById(userClaims.ID)
if err != nil {
utils.GinResponse(c).FailedWithMsg("用户不存在")
c.Abort()
return
}
if user.Status != constant.Normal {
utils.GinResponse(c).FailedWithMsg("用户状态异常,请联系管理员处理!")
c.Abort()
return
}
// 将用户信息放入上下文
c.Set("user", user)
c.Next()
}
}

View File

@ -1,30 +0,0 @@
package middleware
import (
"github.com/gin-gonic/gin"
"wireguard-dashboard/constant"
"wireguard-dashboard/model/entity"
"wireguard-dashboard/utils"
)
// Permission
// @description: 权限验证,一些操作权限
// @return gin.HandlerFunc
func Permission() gin.HandlerFunc {
return func(c *gin.Context) {
userInfo, ok := c.Get("user")
if !ok {
utils.GinResponse(c).AuthorizationFailed()
c.Abort()
return
}
if userInfo.(*entity.User).IsAdmin != constant.SuperAdmin {
utils.GinResponse(c).FailedWithMsg("你暂无权限操作")
c.Abort()
return
}
c.Next()
}
}

View File

@ -1,107 +0,0 @@
package middleware
import (
"bytes"
"gitee.ltd/lxh/logger/log"
"github.com/gin-gonic/gin"
jsoniter "github.com/json-iterator/go"
"io"
"regexp"
"strings"
"time"
"wireguard-dashboard/model/entity"
"wireguard-dashboard/repository"
)
// bodyWriter
// @description: 重写ResponseBody
type bodyWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func SystemLogRequest() gin.HandlerFunc {
return func(c *gin.Context) {
var userId string
if userInfo, ok := c.Get("user"); ok {
userId = userInfo.(*entity.User).Id
}
// 开始时间
start := time.Now()
host := c.Request.Host // 请求域名
path := c.Request.URL.Path // 接口地址
query := c.Request.URL.RawQuery // 参数
if strings.Contains(path, "/api/dashboard/list") {
c.Next()
return
}
var bodyStr string
if !strings.Contains(path, "/api/login") {
body, err := c.GetRawData() // body参数
if err == nil {
bodyStr = string(body)
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
reg := regexp.MustCompile("\\s+")
bodyStr = reg.ReplaceAllString(bodyStr, "")
}
}
method := c.Request.Method // 请求方式
ip := c.ClientIP() // 取出IP
// 处理实际客户端IP
if c.Request.Header.Get("U-Real-Ip") != "" {
ip = c.Request.Header.Get("U-Real-Ip") // 这个是网关Nginx自定义的Header头
} else if c.Request.Header.Get("U-Forwarded-For") != "" {
ip = c.Request.Header.Get("U-Forwarded-For") // 这个是网关Nginx自定义的Header头
}
ua := c.Request.UserAgent() // UA
// 重写客户端IP
c.Request.Header.Set("X-Forwarded-For", ip)
c.Request.Header.Set("X-Real-Ip", ip)
// 拦截response
bw := &bodyWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
c.Writer = bw
header := c.Request.Header
headerStr, _ := jsoniter.MarshalToString(header)
// 执行下一步请求
c.Next()
// 计算耗时
cost := time.Since(start).Milliseconds()
// 组装实体
l := entity.SystemLog{
UserId: userId,
ClientIP: ip,
Host: host,
Method: method,
Uri: path,
Header: headerStr,
Body: bodyStr,
Form: "",
Query: query,
UserAgent: ua,
Cost: cost,
StatusCode: c.Writer.Status(),
Response: "",
}
// 如果不是返回200把返回值保存一下
if c.Writer.Status() != 200 {
resp := bw.body.String()
l.Response = resp
}
go func() {
if er := repository.SystemLog().SaveLog(&l); er != nil {
log.Debugf("请求日志: %+v", l)
log.Errorf("保存请求日志失败: %v", er)
}
}()
}
}

View File

@ -1,4 +1,4 @@
package entity package model
import ( import (
"database/sql/driver" "database/sql/driver"
@ -9,6 +9,8 @@ import (
"time" "time"
) )
// Base
// @description: 数据模型基类
type Base struct { type Base struct {
Id string `json:"id" gorm:"primaryKey;type:varchar(36);not null;comment:'主键'"` Id string `json:"id" gorm:"primaryKey;type:varchar(36);not null;comment:'主键'"`
Timestamp Timestamp
@ -47,7 +49,7 @@ func (jt JsonTime) Value() (driver.Value, error) {
return jt.Time.Format("2006-01-02 15:04:05"), nil return jt.Time.Format("2006-01-02 15:04:05"), nil
} }
func (jt *JsonTime) Scan(v interface{}) error { func (jt *JsonTime) Scan(v any) error {
value, ok := v.(time.Time) value, ok := v.(time.Time)
if ok { if ok {
*jt = JsonTime{Time: value} *jt = JsonTime{Time: value}

View File

@ -1,20 +0,0 @@
package entity
import "wireguard-dashboard/constant"
// User
// @description: 用户信息
type User struct {
Base
Avatar string `json:"avatar" gorm:"type:varchar(255);not null;comment:'头像'"`
Name string `json:"name" gorm:"type:varchar(50);not null;comment:'用户名'"`
Account string `json:"account" gorm:"type:varchar(50);not null;comment:'账号'"`
Email string `json:"email" gorm:"type:varchar(255);default null;comment:'联系邮箱'"`
Password string `json:"password" gorm:"type:varchar(255);not null;comment:'密码'"`
IsAdmin constant.UserType `json:"isAdmin" gorm:"type:int(1);not null;comment:'是否为管理员'"`
Status constant.UserStatus `json:"status" gorm:"type:tinyint(1);not null;comment:'用户状态0 - 禁用 | 1 - 正常)'"`
}
func (*User) TableName() string {
return "t_user"
}

View File

@ -1,45 +0,0 @@
package entity
// Server
// @description: 服务端
type Server struct {
Base
IpScope string `json:"ipScope" gorm:"type:varchar(255);not null;comment:'ip范围'"`
ListenPort int `json:"listenPort" gorm:"type:int(10);not null;comment:'服务监听端口'"`
PrivateKey string `json:"privateKey" gorm:"type:text;not null;comment:'密钥'"`
PublicKey string `json:"publicKey" gorm:"type:text;not null;comment:'公钥'"`
PostUpScript string `json:"postUpScript" gorm:"type:text;default null;comment:'postUpScript'"`
PreDownScript string `json:"preDownScript" gorm:"type:text;default null;comment:'preDownScript'"`
PostDownScript string `json:"postDownScript" gorm:"type:text;default null;comment:postDownScript"`
Clients []Client `json:"clients" gorm:"foreignKey:ServerId"`
}
func (*Server) TableName() string {
return "t_wg_server"
}
// Client
// @description: 客户端
type Client struct {
Base
ServerId string `json:"serverId" gorm:"type:varchar(36);not null;comment:'服务端id'"`
Name string `json:"name" gorm:"type:varchar(100);not null;comment:'客户端名称'"`
Email string `json:"email" gorm:"type:varchar(100);default null;comment:'联系邮箱'"`
SubnetRange string `json:"subnetRange" gorm:"type:varchar(255);default null;comment:'子网范围'"`
IpAllocation string `json:"ipAllocation" gorm:"type:varchar(255);not null;comment:'客户端ip'"`
AllowedIps string `json:"allowedIps" gorm:"type:varchar(255);not null;comment:'允许访问的ip'"`
ExtraAllowedIps string `json:"extraAllowedIps" gorm:"type:varchar(255);default null;comment:'额外允许的ip范围'"`
Endpoint string `json:"endpoint" gorm:"type:varchar(255);default null;comment:'端点'"`
UseServerDns *int `json:"useServerDns" gorm:"type:int(1);default 1;comment:'是否使用服务端dns'"`
EnableAfterCreation *int `json:"enableAfterCreation" gorm:"type:int(1);default 1;comment:'是否创建后启用'"`
Keys string `json:"keys" gorm:"type:text;default null;comment:'公钥和密钥的json串'"`
UserId string `json:"userId" gorm:"type:char(36);not null;comment:'创建人id'"`
Enabled *int `json:"enabled" gorm:"type:tinyint(1);default 1;comment:'状态0 - 禁用 | 1 - 正常)'"`
OfflineMonitoring *int `json:"offlineMonitoring" gorm:"tinyint(1);default 0;comment:'是否启用离线监听0 - 禁用 | 1 - 启用)"`
User *User `json:"user" gorm:"foreignKey:UserId"`
Server *Server `json:"server" gorm:"foreignKey:ServerId"`
}
func (*Client) TableName() string {
return "t_wg_client"
}

View File

@ -1,19 +1,6 @@
package entity package model
type Setting struct { type RequestLog struct {
Base
Code string `json:"code" gorm:"type:char(20);not null;comment:'设定code'"`
Data string `json:"data" gorm:"type:text;not null;comment:'值'"`
Describe string `json:"describe" gorm:"type:text;default null;comment:'默认值'"`
}
func (Setting) TableName() string {
return "t_setting"
}
// SystemLog
// @description: 系统日志
type SystemLog struct {
Base Base
UserId string `json:"userId" gorm:"type:char(40);comment:'用户id'"` UserId string `json:"userId" gorm:"type:char(40);comment:'用户id'"`
ClientIP string `json:"clientIP" gorm:"type:varchar(60);not null;comment:'客户端IP'"` ClientIP string `json:"clientIP" gorm:"type:varchar(60);not null;comment:'客户端IP'"`
@ -29,7 +16,3 @@ type SystemLog struct {
StatusCode int `json:"statusCode" gorm:"type:int(10);comment:'响应状态码'"` StatusCode int `json:"statusCode" gorm:"type:int(10);comment:'响应状态码'"`
Response string `json:"response" gorm:"type:text;comment:'返回数据'"` Response string `json:"response" gorm:"type:text;comment:'返回数据'"`
} }
func (SystemLog) TableName() string {
return "t_system_log"
}

12
model/setting.go Normal file
View File

@ -0,0 +1,12 @@
package model
type Setting struct {
Base
Code string `json:"code" gorm:"type:char(20);not null;index:idx_code; comment:'设定code'"`
Data string `json:"render_data" gorm:"type:text;not null; comment:'值'"`
Describe string `json:"describe" gorm:"type:text;default null;comment:'配置说明'"`
}
func (Setting) TableName() string {
return "t_setting"
}

18
model/user.go Normal file
View File

@ -0,0 +1,18 @@
package model
import "wireguard-ui/global/constant"
type User struct {
Base
Account string `json:"account" gorm:"type:varchar(50);not null;index:idx_account;comment: '登陆账号'"`
Password string `json:"password" gorm:"type:varchar(255);not null;comment: '密码'"`
Nickname string `json:"nickname" gorm:"type:varchar(50);not null;comment: '昵称'"`
Avatar string `json:"avatar" gorm:"type:varchar(255);not null;comment: '头像'"`
Contact string `json:"contact" gorm:"type:varchar(255);default null;comment: '联系方式(邮箱|电话)'"`
IsAdmin constant.UserType `json:"isAdmin" gorm:"type:tinyint(1);not null;comment: '是否为管理员0 - 否 | 1 - 是)'"`
Status constant.Status `json:"status" gorm:"type:tinyint(1);not null;comment: '用户状态0 - 否 | 1 - 是)'"`
}
func (User) TableName() string {
return "t_user"
}

View File

@ -1,41 +0,0 @@
package vo
import (
"wireguard-dashboard/model/entity"
"wireguard-dashboard/model/template_data"
)
type Client struct {
Id string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
SubnetRange string `json:"subnetRange"`
IpAllocation []string `json:"ipAllocation" gorm:"-"`
IpAllocationStr string `json:"-" gorm:"ipAllocationStr"`
AllowedIps []string `json:"allowedIPS" gorm:"-"`
AllowedIpsStr string `json:"-" gorm:"allowedIPSStr"`
ExtraAllowedIps []string `json:"extraAllowedIPS" gorm:"-"`
ExtraAllowedIpsStr string `json:"-" gorm:"extraAllowedIPSStr"` // extra_allowed_ips_str
Endpoint string `json:"endpoint"`
UseServerDNS int `json:"useServerDNS"`
EnableAfterCreation int `json:"enableAfterCreation"`
KeysStr string `json:"-" gorm:"keys_str"`
Keys template_data.Keys `json:"keys" gorm:"-"`
CreateUser string `json:"createUser"`
Enabled bool `json:"enabled"`
OfflineMonitoring int `json:"offlineMonitoring"`
CreatedAt entity.JsonTime `json:"createdAt"`
UpdatedAt entity.JsonTime `json:"updatedAt"`
}
type ClientStatus struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
IpAllocation string `json:"ipAllocation"`
Endpoint string `json:"endpoint"`
Received string `json:"received"`
Transmitted string `json:"transmitted"`
IsOnline bool `json:"isOnline"` // 是否在线 1 - 在线 | 0 - 不在线
LastHandShake string `json:"lastHandShake"`
}

View File

@ -1,14 +0,0 @@
package vo
import "wireguard-dashboard/model/entity"
type SystemLogItem struct {
Id string `json:"id"`
Username string `json:"username"`
ClientIP string `json:"clientIP"`
Method string `json:"method"`
Host string `json:"host"`
Uri string `json:"uri"`
StatusCode int `json:"statusCode"`
CreatedAt entity.JsonTime `json:"createdAt"`
}

View File

@ -1,11 +0,0 @@
package vo
type ServerSetting struct {
EndpointAddress string `json:"endpointAddress"`
DnsServer []string `json:"dnsServer"`
MTU int `json:"MTU"`
PersistentKeepalive int `json:"persistentKeepalive"`
FirewallMark string `json:"firewallMark"`
Table string `json:"table"`
ConfigFilePath string `json:"configFilePath"`
}

View File

@ -1,20 +0,0 @@
package vo
import (
"wireguard-dashboard/constant"
"wireguard-dashboard/model/entity"
)
// User
// @description: 用户信息
type User struct {
Id string `json:"id"` // id
Name string `json:"name"` // 用户名
Avatar string `json:"avatar"` // 头像
Account string `json:"account"` // 账户
Email string `json:"email"` // 联系邮箱
IsAdmin constant.UserType `json:"isAdmin"` // 管理员
Status constant.UserStatus `json:"status"` // 状态
CreatedAt entity.JsonTime `json:"createdAt"` // 创建时间
UpdatedAt entity.JsonTime `json:"updatedAt"` // 更新时间
}

View File

@ -1,14 +0,0 @@
package vo
// Server
// @description: 服务端返回信息
type Server struct {
Id string `json:"id"` // id
IpScope string `json:"ipScope"` // ip范围
ListenPort int `json:"listenPort"` // 服务监听端口
PrivateKey string `json:"privateKey"` // 私钥
PublicKey string `json:"publicKey"` // 公钥
PostUpScript string `json:"postUpScript"`
PreDownScript string `json:"preDownScript"`
PostDownScript string `json:"postDownScript"`
}

24
model/wireguard.go Normal file
View File

@ -0,0 +1,24 @@
package model
import "wireguard-ui/global/constant"
type Client struct {
Base
Name string `json:"name" gorm:"type:varchar(100);not null;comment:'客户端名称'"`
Email string `json:"email" gorm:"type:varchar(100);default null;comment:'联系邮箱'"`
SubnetRange string `json:"subnetRange" gorm:"type:varchar(255);default null;comment:'子网范围'"`
IpAllocation string `json:"ipAllocation" gorm:"type:varchar(255);not null;comment:'客户端ip'"`
AllowedIps string `json:"allowedIps" gorm:"type:varchar(255);not null;comment:'允许访问的ip'"`
ExtraAllowedIps string `json:"extraAllowedIps" gorm:"type:varchar(255);default null;comment:'额外允许的ip范围'"`
Endpoint string `json:"endpoint" gorm:"type:varchar(255);default null;comment:'端点'"`
UseServerDns constant.Status `json:"useServerDns" gorm:"type:tinyint(1);default 1;comment:'是否使用服务端dns'"`
Keys string `json:"keys" gorm:"type:text;default null;comment:'公钥和密钥的json串'"`
UserId string `json:"userId" gorm:"type:char(36);not null;comment:'创建人id'"`
Enabled constant.Status `json:"enabled" gorm:"type:tinyint(1);default 1;comment:'状态0 - 禁用 | 1 - 正常)'"`
OfflineMonitoring constant.Status `json:"offlineMonitoring" gorm:"tinyint(1);default 0;comment:'是否启用离线监听0 - 禁用 | 1 - 启用)"`
User *User `json:"user" gorm:"foreignKey:UserId"`
}
func (Client) TableName() string {
return "t_client"
}

View File

@ -1,119 +0,0 @@
package queues
import (
"context"
"encoding/json"
"fmt"
"gitee.ltd/lxh/logger/log"
"github.com/spf13/cast"
"os"
"strconv"
"wireguard-dashboard/client"
"wireguard-dashboard/component"
"wireguard-dashboard/constant"
"wireguard-dashboard/model/template_data"
"wireguard-dashboard/repository"
)
// asyncWireguardConfigFile
// @description: 同步配置文件
func asyncWireguardConfigFile() {
for {
result, err := client.Redis.BRPop(context.Background(), 0, fmt.Sprintf("%s", constant.SyncWgConfigFile)).Result()
if err != nil {
log.Errorf("获取任务失败")
continue
}
serverId := result[1]
if serverId == "" {
serverInfo, err := repository.Server().GetServer()
if err != nil {
log.Errorf("没有找到服务端: %v", err.Error())
continue
}
serverId = serverInfo.Id
}
// 使用serverId获取服务信息
serverEnt, err := repository.Server().GetServerWithClient(serverId)
if err != nil {
log.Errorf("获取服务端信息失败: %s", err.Error())
continue
}
// 获取服务端全局配置
globalSetting, err := repository.System().GetServerSetting()
if err != nil {
log.Errorf("获取服务端配置失败: %s", err.Error())
continue
}
if globalSetting.ConfigFilePath == "" {
globalSetting.ConfigFilePath = "/etc/wireguard/wg0.conf"
}
// 获取模板文件和输出目录
var templatePath, outFilePath string
if os.Getenv("GIN_MODE") != "release" {
templatePath = "E:\\Workspace\\Go\\wireguard-dashboard\\template\\wg.conf"
outFilePath = "E:\\Workspace\\Go\\wireguard-dashboard\\wg0.conf"
} else {
templatePath = "./template/wg.conf"
outFilePath = globalSetting.ConfigFilePath
}
// 组装数据
renderServer := template_data.Server{
Address: serverEnt.IpScope,
ListenPort: serverEnt.ListenPort,
PrivateKey: serverEnt.PrivateKey,
MTU: globalSetting.MTU,
PostUp: serverEnt.PostUpScript,
PreDown: serverEnt.PreDownScript,
PostDown: serverEnt.PostDownScript,
Table: globalSetting.Table,
}
// 客户端数据
var renderClients []template_data.Client
for _, v := range serverEnt.Clients {
// 如果不是确认后创建或者未启用就不写入到wireguard配置文件当中
if *v.Enabled != 1 {
continue
}
var clientKey template_data.Keys
_ = json.Unmarshal([]byte(v.Keys), &clientKey)
var createUserName string
if v.User != nil {
createUserName = v.User.Name
}
renderClients = append(renderClients, template_data.Client{
ID: v.Id,
Name: v.Name,
Email: v.Email,
PublicKey: clientKey.PublicKey,
PresharedKey: clientKey.PresharedKey,
AllowedIPS: v.IpAllocation,
PersistentKeepalive: strconv.Itoa(globalSetting.PersistentKeepalive),
Endpoint: v.Endpoint,
CreatedAt: v.CreatedAt.String(),
UpdatedAt: v.UpdatedAt.String(),
CreateUser: createUserName,
Enabled: cast.ToBool(v.Enabled),
})
}
renderData := map[string]any{
"Server": renderServer,
"Clients": renderClients,
}
err = component.Wireguard().Apply(templatePath, outFilePath, renderData)
if err != nil {
log.Errorf("同步配置文件失败: %s", err.Error())
continue
}
}
}

View File

@ -1,10 +0,0 @@
package queues
// StartConsumer
// @description: 启动消费者
func StartConsumer() {
// 同步配置文件
go asyncWireguardConfigFile()
// 离线监听
//go offlineMonitoring()
}

View File

@ -1,16 +0,0 @@
package queues
import (
"context"
"fmt"
"wireguard-dashboard/client"
"wireguard-dashboard/constant"
)
// PutAsyncWireguardConfigFile
// @description: 启动生产者
// @param serverId
// @return error
func PutAsyncWireguardConfigFile(serverId string) error {
return client.Redis.LPush(context.Background(), fmt.Sprintf("%s", constant.SyncWgConfigFile), serverId).Err()
}

View File

@ -1,235 +0,0 @@
package repository
import (
"encoding/json"
"errors"
"fmt"
"gitee.ltd/lxh/logger/log"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"gorm.io/gorm"
"strings"
"wireguard-dashboard/client"
"wireguard-dashboard/http/param"
"wireguard-dashboard/model/entity"
"wireguard-dashboard/model/template_data"
"wireguard-dashboard/model/vo"
"wireguard-dashboard/utils"
)
type clientRepo struct {
*gorm.DB
}
func Client() clientRepo {
return clientRepo{
client.DB,
}
}
// List
// @description: 列表
// @receiver r
// @param p
// @return data
// @return total
// @return err
func (r clientRepo) List(p param.ClientList) (data []vo.Client, total int64, err error) {
sel := r.Table("t_wg_client as twc").
Scopes(utils.Page(p.Current, p.Size)).
Joins("LEFT JOIN t_user as tu ON twc.user_id = tu.id").
Select("twc.id", "twc.created_at", "twc.updated_at", "twc.name", "twc.email", "twc.subnet_range", "twc.ip_allocation as ip_allocation_str", "twc.allowed_ips as allowed_ips_str",
"twc.extra_allowed_ips as extra_allowed_ips_str", "twc.endpoint", "twc.use_server_dns", "twc.enable_after_creation", "twc.enabled", "twc.keys as keys_str", "tu.name as create_user", "twc.offline_monitoring")
if p.Name != "" {
sel.Where("twc.name LIKE ?", "%"+p.Name+"%")
}
if p.Email != "" {
sel.Where("twc.email = ?", p.Email)
}
if p.Ip != "" {
sel.Where("twc.ip_allocation LIKE ?", "%"+p.Ip+"%")
}
if p.Enabled != nil {
sel.Where("twc.enabled = ?", p.Enabled)
}
err = sel.Order("twc.created_at DESC").Find(&data).Offset(-1).Limit(-1).Count(&total).Error
if err != nil {
return
}
for i, v := range data {
if v.KeysStr != "" {
_ = json.Unmarshal([]byte(v.KeysStr), &data[i].Keys)
}
if v.IpAllocationStr != "" {
data[i].IpAllocation = strings.Split(v.IpAllocationStr, ",")
}
if v.AllowedIpsStr != "" {
data[i].AllowedIps = strings.Split(v.AllowedIpsStr, ",")
}
if v.ExtraAllowedIpsStr != "" {
data[i].ExtraAllowedIps = strings.Split(v.ExtraAllowedIpsStr, ",")
}
}
return
}
// Save
// @description: 新增/编辑客户端
// @receiver r
// @param p
// @param adminId
// @return err
func (r clientRepo) Save(p param.SaveClient, adminId string) (client *entity.Client, err error) {
ent := &entity.Client{
Base: entity.Base{
Id: p.Id,
},
ServerId: p.ServerId,
Name: p.Name,
Email: p.Email,
SubnetRange: p.SubnetRange,
IpAllocation: strings.Join(p.IpAllocation, ","),
AllowedIps: strings.Join(p.AllowedIPS, ","),
ExtraAllowedIps: strings.Join(p.ExtraAllowedIPS, ","),
Endpoint: p.Endpoint,
UseServerDns: p.UseServerDNS,
EnableAfterCreation: p.EnabledAfterCreation,
UserId: adminId,
Enabled: p.Enabled,
OfflineMonitoring: p.OfflineMonitoring,
}
// id不为空更新信息
if p.Id != "" {
keys, _ := json.Marshal(p.Keys)
ent.Keys = string(keys)
if err = r.Model(&entity.Client{}).
Where("id = ?", p.Id).Select("name", "email", "subnet_range", "ip_allocation",
"allowed_ips", "extra_allowed_ips", "endpoint", "use_server_dns", "enable_after_creation",
"user_id", "enabled", "offline_monitoring").
Updates(ent).Error; err != nil {
return
}
return
}
// 查询新增的ip地址是否已经存在了
var count int64
if err = r.Model(&entity.Client{}).Where("ip_allocation in (?)", p.IpAllocation).Count(&count).Error; err != nil {
log.Errorf("查询IP地址是否存在失败: %v", err.Error())
return
}
if count > 0 {
return nil, errors.New("该客户端的IP已经存在请检查后再添加")
}
var privateKey, presharedKey wgtypes.Key
var publicKey string
if p.Keys.PrivateKey == "" {
// 为空,新增
privateKey, err = wgtypes.GeneratePrivateKey()
if err != nil {
log.Errorf("生成密钥对失败: %v", err.Error())
return nil, errors.New("解析密钥失败")
}
} else {
privateKey, err = wgtypes.ParseKey(p.Keys.PrivateKey)
if err != nil {
log.Errorf("解析密钥对失败: %v", err.Error())
return nil, errors.New("解析密钥失败")
}
}
publicKey = privateKey.PublicKey().String()
if p.Keys.PresharedKey == "" {
presharedKey, err = wgtypes.GenerateKey()
if err != nil {
log.Errorf("生成共享密钥失败: %v", err.Error())
return nil, errors.New("解析密钥失败")
}
} else {
presharedKey, err = wgtypes.ParseKey(p.Keys.PresharedKey)
if err != nil {
log.Errorf("解析共享密钥失败: %v", err.Error())
return nil, errors.New("解析密钥失败")
}
}
keys := template_data.Keys{
PrivateKey: privateKey.String(),
PublicKey: publicKey,
PresharedKey: presharedKey.String(),
}
keysStr, _ := json.Marshal(keys)
ent = &entity.Client{
ServerId: p.ServerId,
Name: p.Name,
Email: p.Email,
SubnetRange: p.SubnetRange,
IpAllocation: strings.Join(p.IpAllocation, ","),
AllowedIps: strings.Join(p.AllowedIPS, ","),
ExtraAllowedIps: strings.Join(p.ExtraAllowedIPS, ","),
Endpoint: p.Endpoint,
UseServerDns: p.UseServerDNS,
EnableAfterCreation: p.EnabledAfterCreation,
Keys: string(keysStr),
UserId: adminId,
Enabled: p.Enabled,
OfflineMonitoring: p.OfflineMonitoring,
}
err = r.Model(&entity.Client{}).Create(ent).Error
return
}
// Delete
// @description: 删除客户端
// @receiver r
// @param id
// @return err
func (r clientRepo) Delete(id string) (err error) {
return r.Model(&entity.Client{}).Where("id = ?", id).Delete(&entity.Client{}).Error
}
// GetById
// @description: 根据id获取客户端详情
// @receiver r
// @param id
// @return data
// @return err
func (r clientRepo) GetById(id string) (data entity.Client, err error) {
err = r.Model(&entity.Client{}).Where("id = ?", id).Preload("Server").First(&data).Error
return
}
// GetByPublicKey
// @description: 根据公钥获取客户端信息
// @receiver r
// @param publicKey
// @return data
// @return err
func (r clientRepo) GetByPublicKey(publicKey string) (data entity.Client, err error) {
err = r.Model(&entity.Client{}).Where(fmt.Sprintf("json_extract(keys, '$.publicKey') = '%s'", publicKey)).Preload("Server").First(&data).Error
return
}
// Disabled
// @description: 禁用客户端
// @receiver r
// @param id
// @return err
func (r clientRepo) Disabled(id string) (err error) {
return r.Model(&entity.Client{}).Where("id = ?", id).Update("enabled", 0).Error
}

View File

@ -1,66 +0,0 @@
package repository
import (
"gorm.io/gorm"
"wireguard-dashboard/client"
"wireguard-dashboard/http/param"
"wireguard-dashboard/model/entity"
"wireguard-dashboard/model/vo"
)
type server struct {
*gorm.DB
}
func Server() server {
return server{
client.DB,
}
}
// GetServer
// @description: 获取服务端信息
// @receiver r
// @return data
// @return err
func (r server) GetServer() (data *vo.Server, err error) {
err = r.Model(&entity.Server{}).Select("id", "ip_scope", "listen_port", "private_key", "public_key", "post_up_script", "pre_down_script", "post_down_script").First(&data).Error
return
}
// GetServerWithClient
// @description: 获取服务端信息以及所属客户端
// @receiver r
// @param data
// @param err
func (r server) GetServerWithClient(id string) (data *entity.Server, err error) {
err = r.Model(&entity.Server{}).Preload("Clients").Preload("Clients.User").Where("id = ?", id).First(&data).Error
return
}
// Save
// @description: 新增服务端信息
// @receiver r
// @param ent
// @return err
func (r server) Save(ent *entity.Server) (err error) {
return r.Model(&entity.Server{}).Create(&ent).Error
}
// Update
// @description: 更新服务端信息
// @receiver r
// @param p
// @return err
func (r server) Update(p param.SaveServer) (err error) {
update := map[string]any{
"ip_scope": p.IpScope,
"listen_port": p.ListenPort,
"post_up_script": p.PostUpScript,
"pre_down_script": p.PreDownScript,
"post_down_script": p.PostDownScript,
"private_key": p.PrivateKey,
"public_key": p.PublicKey,
}
return r.Model(&entity.Server{}).Where("id = ?", p.Id).Updates(&update).Error
}

View File

@ -1,64 +0,0 @@
package repository
import (
"encoding/json"
"gorm.io/gorm"
"wireguard-dashboard/client"
"wireguard-dashboard/model/entity"
"wireguard-dashboard/model/vo"
)
type system struct {
*gorm.DB
}
func System() system {
return system{
client.DB,
}
}
// GetConfigByCode
// @description:
// @receiver r
// @param code
// @return data
// @return err
func (r system) GetConfigByCode(code string) (data *entity.Setting, err error) {
err = r.Model(&entity.Setting{}).Where("code = ?", code).First(&data).Error
return
}
// GetServerSetting
// @description: 获取服务端全局配置
// @receiver r
// @return data
// @return err
func (r system) GetServerSetting() (data *vo.ServerSetting, err error) {
config, err := r.GetConfigByCode("SERVER_SETTING")
if err != nil {
return nil, err
}
if err = json.Unmarshal([]byte(config.Data), &data); err != nil {
return
}
return
}
// Save
// @description: 新增/编辑配置
// @receiver r
// @param ent
// @return err
func (r system) Save(ent *entity.Setting) (err error) {
conf, err := r.GetConfigByCode(ent.Code)
// 新增
if err != nil || conf == nil {
return r.Model(&entity.Setting{}).Create(ent).Error
}
// 更新
return r.Model(&entity.Setting{}).Where("code = ?", ent.Code).Updates(&ent).Error
}

View File

@ -1,50 +0,0 @@
package repository
import (
"gorm.io/gorm"
"wireguard-dashboard/client"
"wireguard-dashboard/constant"
"wireguard-dashboard/http/param"
"wireguard-dashboard/model/entity"
"wireguard-dashboard/model/vo"
"wireguard-dashboard/utils"
)
type systemLog struct {
*gorm.DB
}
func SystemLog() systemLog {
return systemLog{
client.DB,
}
}
// SaveLog
// @description: 保存记录
// @receiver systemLog
// @param data
// @return err
func (s systemLog) SaveLog(data *entity.SystemLog) (err error) {
return s.Create(&data).Error
}
// List
// @description: 分页列表
// @receiver s
// @param p
// @return data
// @return total
// @return err
func (s systemLog) List(p param.OnlyPage, loginUser *entity.User) (data []vo.SystemLogItem, total int64, err error) {
sel := s.Scopes(utils.Page(p.Current, p.Size)).Table("t_system_log as tsl").
Joins("LEFT JOIN t_user as tu ON tu.id = tsl.user_id").
Select("tsl.id", "tu.name as username", "tsl.client_ip", "tsl.method", "tsl.status_code", "tsl.host", "tsl.uri", "tsl.created_at").
Order("tsl.created_at DESC")
if loginUser.IsAdmin == constant.NormalAdmin {
sel.Where("tsl.user_id = ?", loginUser.Id)
}
err = sel.Find(&data).Offset(-1).Limit(-1).Count(&total).Error
return
}

View File

@ -1,142 +0,0 @@
package repository
import (
"errors"
"gorm.io/gorm"
"wireguard-dashboard/client"
"wireguard-dashboard/constant"
"wireguard-dashboard/http/param"
"wireguard-dashboard/model/entity"
"wireguard-dashboard/model/vo"
"wireguard-dashboard/utils"
)
type user struct {
*gorm.DB
}
func User() user {
return user{
client.DB,
}
}
// List
// @description: 用户列表
// @receiver r
// @param p
// @return data
// @return total
// @return err
func (r user) List(p param.UserList) (data []vo.User, total int64, err error) {
err = r.Model(&entity.User{}).Scopes(utils.Page(p.Current, p.Size)).
Select("id", "created_at", "updated_at", "avatar", "email", "name", "account", "is_admin", "status").Order("created_at DESC").
Find(&data).Offset(-1).Limit(-1).Count(&total).Error
return
}
// GetUserById
// @description: 根据id获取用户信息
// @receiver r
// @param id
// @return *entity.User
// @return error
func (r user) GetUserById(id string) (data *entity.User, err error) {
err = r.Where("id = ?", id).First(&data).Error
return
}
// GetUserByAccount
// @description: 通过账户号获取用户信息
// @receiver r
// @param account
// @return data
// @return err
func (r user) GetUserByAccount(account string) (data *entity.User, err error) {
err = r.Where("account = ?", account).First(&data).Error
return
}
// Save
// @description: 创建/更新用户
// @receiver r
// @param ent
// @return err
func (r user) Save(ent *entity.User) (err error) {
// 更新
if ent.Id != "" {
updates := map[string]any{
"name": ent.Name,
"avatar": ent.Avatar,
"email": ent.Email,
"is_admin": ent.IsAdmin,
"status": ent.Status,
}
return r.Model(&entity.User{}).Where("id = ?", ent.Id).Updates(&updates).Error
}
defaultPassword := utils.Password().GenerateHashPassword("admin123")
if ent.Password == "" { // 没有密码给一个默认密码
ent.Password = defaultPassword
} else {
ent.Password = utils.Password().GenerateHashPassword(ent.Password)
}
// 没有头像就生成一个头像
if ent.Avatar == "" {
ent.Avatar, _ = utils.Avatar().GenerateAvatar(true)
}
// 创建
return r.Create(&ent).Error
}
// ChangePassword
// @description: 变更密码
// @receiver r
// @param p
// @param userId
// @return err
func (r user) ChangePassword(p param.ChangePassword, userId string) (err error) {
password := utils.Password().GenerateHashPassword(p.NewPassword)
return r.Model(&entity.User{}).Where("id = ?", userId).Update("password", password).Error
}
// ChangeUserState
// @description: 变更用户状态
// @receiver r
// @param p
// @return err
func (r user) ChangeUserState(p param.ChangeUserState) (err error) {
return r.Model(&entity.User{}).Where("id = ?", p.ID).Update("status", p.Status).Error
}
// DeleteUser
// @description: 删除管理员
// @receiver r
// @param id
// @return err
func (r user) DeleteUser(loginUser *entity.User, id string) (err error) {
// 不能删除自身以及超级管理员,超级管理员只有 名为admin的管理员可以删除
userInfo, err := r.GetUserById(id)
if err != nil {
return
}
if userInfo.Id == loginUser.Id {
return errors.New("不可删除自己")
}
if userInfo.IsAdmin == constant.SuperAdmin && loginUser.Account != "admin" {
return errors.New("非无敌管理员不可清空超管")
}
if userInfo.Account == "admin" {
return errors.New("不可删除宇宙第一无敌管理员删了你就G了")
}
// 可删除
return r.Model(&entity.User{}).Where("id = ?", id).Delete(userInfo).Error
}

View File

@ -1,17 +0,0 @@
package route
import (
"github.com/gin-gonic/gin"
"wireguard-dashboard/http/api"
"wireguard-dashboard/middleware"
)
// CaptchaApi
// @description: 验证码
// @param r
func CaptchaApi(r *gin.RouterGroup) {
captcha := r.Group("captcha", middleware.SystemLogRequest())
{
captcha.GET("", api.Captcha().GenerateCaptcha) // 生成验证码
}
}

View File

@ -1,23 +0,0 @@
package route
import (
"github.com/gin-gonic/gin"
"wireguard-dashboard/http/api"
"wireguard-dashboard/middleware"
)
func ClientApi(r *gin.RouterGroup) {
apiGroup := r.Group("client", middleware.Authorization(), middleware.SystemLogRequest())
{
apiGroup.GET("list", api.Client().List) // 客户端列表
apiGroup.POST("save", middleware.Permission(), api.Client().Save) // 新增/编辑客户端
apiGroup.DELETE(":id", middleware.Permission(), api.Client().Delete) // 删除客户端
apiGroup.POST("download/:id", api.Client().Download) // 下载客户端配置文件
apiGroup.POST("generate-qrcode/:id", api.Client().GenerateQrCode) // 生成客户端二维码
apiGroup.POST("to-email/:id", api.Client().SendEmail) //发送邮件
apiGroup.GET("status", api.Client().Status) // 获取客户端链接状态监听列表
apiGroup.POST("offline/:id", api.Client().Offline) // 强制下线指定客户端
apiGroup.POST("assignIP", api.Client().AssignIPAndAllowedIP) // 分配IP
apiGroup.POST("generate-keys", api.Client().GenerateKeys) // 生成密钥对
}
}

View File

@ -1,17 +0,0 @@
package route
import (
"github.com/gin-gonic/gin"
"wireguard-dashboard/http/api"
"wireguard-dashboard/middleware"
)
// DashboardApi
// @description: 控制台相关API
// @param r
func DashboardApi(r *gin.RouterGroup) {
apiGroup := r.Group("dashboard", middleware.Authorization(), middleware.SystemLogRequest())
{
apiGroup.GET("list", api.Dashboard().List) // 操作日志
}
}

View File

@ -1,16 +0,0 @@
package route
import (
"github.com/gin-gonic/gin"
"wireguard-dashboard/http/api"
"wireguard-dashboard/middleware"
)
func ServerApi(r *gin.RouterGroup) {
apiGroup := r.Group("server", middleware.Authorization(), middleware.SystemLogRequest())
{
apiGroup.GET("", api.Server().GetServer) // 获取服务端信息
apiGroup.POST("", middleware.Permission(), middleware.Permission(), api.Server().SaveServer) // 新增/更新服务端信息
apiGroup.POST("control-server", middleware.Permission(), middleware.Permission(), api.Server().ControlServer) // 服务端操作控制
}
}

View File

@ -1,21 +0,0 @@
package route
import (
"github.com/gin-gonic/gin"
"wireguard-dashboard/http/api"
"wireguard-dashboard/middleware"
)
// SettingApi
// @description: 设置相关API
// @param r
func SettingApi(r *gin.RouterGroup) {
apiGroup := r.Group("setting", middleware.Authorization(), middleware.SystemLogRequest())
{
apiGroup.POST("save", middleware.Permission(), api.Setting().SetSetting) // 添加/更改设置 - 设置其他的配置
apiGroup.POST("server-global", middleware.Permission(), api.Setting().SetServerGlobal) // 设置服务端全局配置
apiGroup.GET("server", api.Setting().GetGlobalSetting) // 获取全局服务端配置
apiGroup.GET("public-ip", api.Setting().GetPublicNetworkIP) // 获取公网IP
apiGroup.GET("restart-rule", api.Setting().GetServerRestartRule) // 服务端重启规则
}
}

View File

@ -1,28 +0,0 @@
package route
import (
"github.com/gin-gonic/gin"
"wireguard-dashboard/http/api"
"wireguard-dashboard/middleware"
)
func UserApi(r *gin.RouterGroup) {
// 登陆相关API
login := r.Group("/login", middleware.SystemLogRequest())
{
login.POST("", api.UserApi().Login)
}
// 用户登陆后相关的API
userApi := r.Group("user", middleware.Authorization(), middleware.SystemLogRequest())
{
userApi.DELETE("logout", api.UserApi().Logout) // 用户退出登陆
userApi.GET("", api.UserApi().GetUser) // 获取登陆用户信息
userApi.POST("save", middleware.Permission(), api.UserApi().Save) // 新增/编辑用户信息
userApi.POST("change-password", api.UserApi().ChangePassword) // 更改密码
userApi.GET("list", middleware.Permission(), api.UserApi().List) // 用户列表
userApi.PUT("change-status", middleware.Permission(), api.UserApi().ChangeUserState) // 变更状态
userApi.DELETE("delete/:id", middleware.Permission(), api.UserApi().DeleteUser) // 删除用户
userApi.POST("change-avatar", api.UserApi().ChangeAvatar) // 更换头像
}
}

View File

@ -6,60 +6,62 @@ import (
"github.com/spf13/cast" "github.com/spf13/cast"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"os" "os"
"wireguard-dashboard/client" "wireguard-ui/component"
"wireguard-dashboard/component" "wireguard-ui/global/client"
"wireguard-dashboard/constant" "wireguard-ui/global/constant"
"wireguard-dashboard/model/entity" "wireguard-ui/http/param"
"wireguard-dashboard/model/template_data" "wireguard-ui/model"
"wireguard-dashboard/repository" "wireguard-ui/service"
"wireguard-dashboard/utils" "wireguard-ui/template/render_data"
"wireguard-ui/utils"
) )
type Script struct{} type script struct{}
func NewScript() Script { func New() script {
return Script{} return script{}
} }
func (s Script) Do() error { func (s script) Do() error {
if err := s.DBMigrate(); err != nil { if err := s.migrate(); err != nil {
return err return err
} }
if err := s.CreateSuperAdmin(); err != nil { if err := s.createSuperAdmin(); err != nil {
return err return err
} }
if err := s.InitServer(); err != nil { if err := s.initServer(); err != nil {
log.Error(err.Error()) log.Errorf("初始化wg服务端失败: %v", err.Error())
} }
return nil return nil
} }
// DBMigrate // migrate
// @description: 实体migrate // @description: 生成数据库
// @receiver s // @receiver s
// @return error // @return error
func (s Script) DBMigrate() error { func (s script) migrate() error {
var ent = []any{ var ent = []any{
new(entity.User), new(model.User),
new(entity.Server), new(model.Client),
new(entity.Client), new(model.Setting),
new(entity.Setting), new(model.RequestLog),
new(entity.SystemLog),
} }
return client.DB.AutoMigrate(ent...) return client.DB.AutoMigrate(ent...)
} }
// CreateSuperAdmin // createSuperAdmin
// @description: 创建首个超级管理员 // @description: 创建超级管理员
// @receiver s // @receiver s
// @return error // @return error
func (s Script) CreateSuperAdmin() error { func (s script) createSuperAdmin() error {
var count int64 var count int64
if err := client.DB.Model(&entity.User{}).Where("is_admin = ?", 1).Count(&count).Error; err != nil { if err := client.DB.Model(&model.User{}).
Where("account = ?", "admin").
Where("is_admin = ?", 1).Count(&count).Error; err != nil {
return err return err
} }
@ -75,28 +77,24 @@ func (s Script) CreateSuperAdmin() error {
return err return err
} }
if err = repository.User().Save(&entity.User{ return service.User().CreateUser(&model.User{
Avatar: avatarPath, Avatar: avatarPath,
Name: "超牛管理员", Nickname: "超级管理员",
Account: "admin", Account: "admin",
Email: "", Contact: "",
Password: "admin123", Password: "admin123",
IsAdmin: constant.SuperAdmin, IsAdmin: constant.SuperAdmin,
Status: constant.Normal, Status: constant.Enabled,
}); err != nil { })
return err
}
return nil
} }
// InitServer // initServer
// @description: 初始化服务端信息 // @description: 初始化wg的一些配置
// @receiver s // @receiver s
// @return error // @return error
func (s Script) InitServer() error { func (s script) initServer() error {
var count int64 var count int64
if err := client.DB.Model(&entity.Server{}).Count(&count).Error; err != nil { if err := client.DB.Model(&model.Setting{}).Where("code = ?", "WG_SERVER").Count(&count).Error; err != nil {
return err return err
} }
@ -116,12 +114,12 @@ func (s Script) InitServer() error {
} }
dataJ, _ := json.Marshal(data) dataJ, _ := json.Marshal(data)
globalSet := &entity.Setting{ globalSet := &model.Setting{
Code: "SERVER_SETTING", Code: "WG_SETTING",
Data: string(dataJ), Data: string(dataJ),
Describe: "服务端全局配置", Describe: "服务端全局配置",
} }
if err := repository.System().Save(globalSet); err != nil { if err := service.Setting().SetData(globalSet); err != nil {
return err return err
} }
@ -134,24 +132,31 @@ func (s Script) InitServer() error {
// 根据密钥生成公钥 // 根据密钥生成公钥
publicKey := privateKey.PublicKey() publicKey := privateKey.PublicKey()
serverEnt := &entity.Server{ serverEnt := &param.SaveServer{
IpScope: "10.25.8.1/24", IPScope: []string{"10.25.8.1/24"},
ListenPort: 51820, ListenPort: 51820,
PrivateKey: privateKey.String(), PrivateKey: privateKey.String(),
PublicKey: publicKey.String(), PublicKey: publicKey.String(),
PostUpScript: "iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE", PostUpScript: constant.DefaultPostUpScript,
PreDownScript: "", PreDownScript: constant.DefaultPreDownScript,
PostDownScript: "iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE", PostDownScript: constant.DefaultPostDownScript,
}
serverJ, _ := json.Marshal(serverEnt)
serverSet := &model.Setting{
Code: "WG_SERVER",
Data: string(serverJ),
Describe: "服务端配置",
} }
// 没有服务端,开始初始化 // 没有服务端,开始初始化
if err := repository.Server().Save(serverEnt); err != nil { if err = service.Setting().SetData(serverSet); err != nil {
return err return err
} }
// 处理一下要渲染到配置文件上的数据 // 处理一下要渲染到配置文件上的数据
serverConfig := template_data.Server{ serverConfig := render_data.Server{
Address: serverEnt.IpScope, Address: serverEnt.IPScope,
ListenPort: serverEnt.ListenPort, ListenPort: serverEnt.ListenPort,
PrivateKey: serverEnt.PrivateKey, PrivateKey: serverEnt.PrivateKey,
MTU: cast.ToInt(data["MTU"]), MTU: cast.ToInt(data["MTU"]),
@ -167,19 +172,19 @@ func (s Script) InitServer() error {
var templatePath, outFilePath string var templatePath, outFilePath string
if os.Getenv("GIN_MODE") != "release" { if os.Getenv("GIN_MODE") != "release" {
templatePath = "E:\\Workspace\\Go\\wireguard-dashboard\\template\\wg.conf" templatePath = "E:\\Workspace\\Go\\wireguard-ui\\template\\conf\\wg.conf"
outFilePath = "E:\\Workspace\\Go\\wireguard-dashboard\\wg0.conf" outFilePath = "E:\\Workspace\\Go\\wireguard-ui\\template\\tmp\\wg0.conf"
} else { } else {
templatePath = "./template/wg.conf" templatePath = "./template/wg.conf"
outFilePath = cast.ToString(data["configFilePath"]) outFilePath = cast.ToString(data["configFilePath"])
} }
// 数据库保存完毕,应用配置到配置文件当中 // 先渲染模板
err = component.Wireguard().Apply(templatePath, outFilePath, execData) if err = component.Template().Execute(templatePath, outFilePath, execData); err != nil {
if err != nil { return nil
log.Errorf("应用配置文件失败: %v", err.Error())
return err
} }
// 模板渲染成功,开始执行服务端控制
component.Wireguard().ServerControl(outFilePath)
return nil return nil
} }

25
service/base.go Normal file
View File

@ -0,0 +1,25 @@
package service
import "gorm.io/gorm"
func Paginate(current, size int64) func(db *gorm.DB) *gorm.DB {
// 如果页码是-1就不分页
if current == -1 {
return func(db *gorm.DB) *gorm.DB {
return db
}
}
// 分页
return func(db *gorm.DB) *gorm.DB {
if current == 0 {
current = 1
}
if size < 1 {
size = 10
}
// 计算偏移量
offset := (current - 1) * size
// 返回组装结果
return db.Offset(int(offset)).Limit(int(size))
}
}

171
service/client.go Normal file
View File

@ -0,0 +1,171 @@
package service
import (
"encoding/json"
"errors"
"fmt"
"gorm.io/gorm"
"strings"
gdb "wireguard-ui/global/client"
"wireguard-ui/http/param"
"wireguard-ui/http/vo"
"wireguard-ui/model"
"wireguard-ui/utils"
)
type client struct {
*gorm.DB
}
func Client() client {
return client{
gdb.DB,
}
}
// SaveClient
// @description: 新增/编辑客户端
// @receiver s
// @param p
// @param loginUser
// @return error
func (s client) SaveClient(p param.SaveClient, loginUser *vo.User) error {
serverConf, err := Setting().GetWGServerForConfig()
if err != nil {
return err
}
// 对客户端IP做格式校验
for _, cip := range p.IpAllocation {
if !utils.Network().IPContains(serverConf.Address, cip) {
return fmt.Errorf("客户端IP[%s]不符合定义", cip)
}
}
// 处理一下endpoint
if p.Endpoint == "" {
globalConf, err := Setting().GetWGSetForConfig()
if err != nil {
return err
}
p.Endpoint = fmt.Sprintf("%s:%d", globalConf.EndpointAddress, serverConf.ListenPort)
}
keys, _ := json.Marshal(p.Keys)
ent := &model.Client{
Base: model.Base{
Id: p.Id,
},
Name: p.Name,
Email: p.Email,
SubnetRange: p.SubnetRange,
IpAllocation: strings.Join(p.IpAllocation, ","),
AllowedIps: strings.Join(p.AllowedIps, ","),
ExtraAllowedIps: strings.Join(p.ExtraAllowedIps, ","),
Endpoint: p.Endpoint,
UseServerDns: *p.UseServerDns,
Keys: string(keys),
UserId: loginUser.Id,
Enabled: *p.Enabled,
OfflineMonitoring: *p.OfflineMonitoring,
}
// 编辑
if p.Id != "" {
return s.Model(&model.Client{}).Select("id", "name", "email", "subnet_range",
"ip_allocation", "allowed_ips",
"extra_allowed_ips", "endpoint",
"use_server_dns", "keys", "user_id", "enabled",
"offline_monitoring").
Where("id = ?", ent.Id).Updates(&ent).Error
}
// 如果是新增判断这个客户端IP是否已经存在过了
var count int64
if err := s.Model(&model.Client{}).Where("ip_allocation = ?", strings.Join(p.IpAllocation, ",")).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New("当前IP客户端已经存在")
}
// 新增
return s.Model(&model.Client{}).Create(&ent).Error
}
// Delete
// @description: 删除
// @receiver s
// @param id
// @return error
func (s client) Delete(id string) error {
return s.Model(&model.Client{}).Where("id = ?", id).Delete(&model.Client{}).Error
}
// List
// @description: 客户端分页列表
// @receiver s
// @param p
// @return data
// @return total
// @return err
func (s client) List(p param.ClientList) (data []vo.ClientItem, total int64, err error) {
sel := s.Table("t_client as tc").Scopes(Paginate(p.Current, p.Size)).
Joins("LEFT JOIN t_user as tu ON tu.id = tc.user_id").
Select("tc.id,tc.name,tc.email,tc.ip_allocation as ip_allocation_str,"+
"tc.allowed_ips as allowed_ips_str,tc.extra_allowed_ips as extra_allowed_ips_str,"+
"tc.endpoint,tc.use_server_dns,tc.keys as keys_str,tu.nickname as create_user,"+
"tc.enabled,tc.offline_monitoring,"+
"tc.created_at", "tc.updated_at")
if p.Enabled != nil {
sel.Where("tc.enabled = ?", *p.Enabled)
}
if p.Name != "" {
sel.Where("tc.name like ?", "%"+p.Name+"%")
}
if p.Email != "" {
sel.Where("tc.email like ?", "%"+p.Email+"%")
}
if p.IpAllocation != "" {
sel.Where("tc.ip_allocation like ?", "%"+p.IpAllocation+"%")
}
err = sel.Order("tc.created_at DESC").Find(&data).Limit(-1).Offset(-1).Count(&total).Error
if err != nil {
return
}
for i, v := range data {
if v.KeysStr != "" {
_ = json.Unmarshal([]byte(v.KeysStr), &data[i].Keys)
}
if v.IpAllocationStr != "" {
data[i].IpAllocation = strings.Split(v.IpAllocationStr, ",")
}
if v.AllowedIpsStr != "" {
data[i].AllowedIps = strings.Split(v.AllowedIpsStr, ",")
} else {
data[i].AllowedIps = []string{}
}
if v.ExtraAllowedIpsStr != "" {
data[i].ExtraAllowedIps = strings.Split(v.ExtraAllowedIpsStr, ",")
} else {
data[i].ExtraAllowedIps = []string{}
}
}
return
}
// GetByID
// @description: 通过ID获取客户端
// @receiver s
// @param id
// @return data
// @return err
func (s client) GetByID(id string) (data *model.Client, err error) {
err = s.Model(&model.Client{}).Where("id = ?", id).Take(&data).Error
return
}

74
service/setting.go Normal file
View File

@ -0,0 +1,74 @@
package service
import (
"encoding/json"
"gorm.io/gorm"
gdb "wireguard-ui/global/client"
"wireguard-ui/model"
"wireguard-ui/template/render_data"
)
type setting struct{ *gorm.DB }
func Setting() setting {
return setting{gdb.DB}
}
func (s setting) SetData(data *model.Setting) error {
// 判断code是否已经存在
var count int64
if err := s.Model(&model.Setting{}).Where("code = ?", data.Code).Count(&count).Error; err != nil {
return err
}
// 存在就更新,反之新增
if count > 0 {
return s.Save(&data).Error
}
return s.Create(&data).Error
}
// GetWGSetForConfig
// @description: 获取全局配置
// @receiver s
// @return data
// @return err
func (s setting) GetWGSetForConfig() (data *render_data.ServerSetting, err error) {
var rs *model.Setting
if err = s.Model(&model.Setting{}).Where("code = ?", "WG_SETTING").Take(&rs).Error; err != nil {
return
}
if err = json.Unmarshal([]byte(rs.Data), &data); err != nil {
return
}
return
}
// GetWGServerForConfig
// @description: 获取wireguard服务端配置为了渲染数据
// @receiver s
// @return render_data
// @return err
func (s setting) GetWGServerForConfig() (data *render_data.Server, err error) {
var rs *model.Setting
if err = s.Model(&model.Setting{}).Where("code = ?", "WG_SERVER").Take(&rs).Error; err != nil {
return
}
if err = json.Unmarshal([]byte(rs.Data), &data); err != nil {
return
}
// 获取一下全局的服务配置
gs, err := s.GetWGSetForConfig()
if err != nil {
return
}
data.MTU = gs.MTU
data.Table = gs.Table
return
}

126
service/user.go Normal file
View File

@ -0,0 +1,126 @@
package service
import (
"gorm.io/gorm"
gdb "wireguard-ui/global/client"
"wireguard-ui/global/constant"
"wireguard-ui/http/param"
"wireguard-ui/http/vo"
"wireguard-ui/model"
"wireguard-ui/utils"
)
type user struct {
*gorm.DB
}
func User() user {
return user{
gdb.DB,
}
}
// CreateUser
// @description: 创建用户
// @receiver s
// @param user
// @return err
func (s user) CreateUser(user *model.User) (err error) {
// 更新
if user.Id != "" {
updates := map[string]any{
"nickname": user.Nickname,
"avatar": user.Avatar,
"contact": user.Contact,
"is_admin": user.IsAdmin,
"status": user.Status,
}
return s.Model(&model.User{}).Where("id = ?", user.Id).Updates(&updates).Error
}
defaultPassword := utils.Password().GenerateHashPassword("admin123")
if user.Password == "" { // 没有密码给一个默认密码
user.Password = defaultPassword
} else {
user.Password = utils.Password().GenerateHashPassword(user.Password)
}
// 没有头像就生成一个头像
if user.Avatar == "" {
user.Avatar, _ = utils.Avatar().GenerateAvatar(true)
}
// 创建
return s.Create(&user).Error
}
// Delete
// @description: 删除用户
// @receiver s
// @param id
// @return error
func (s user) Delete(id string) error {
return s.Model(&model.User{}).Where("id = ?", id).Delete(&model.User{}).Error
}
// List
// @description: 用户列表
// @receiver s
// @param p
// @return data
// @return count
// @return err
func (s user) List(p param.Page) (data []vo.UserItem, count int64, err error) {
if err = s.Model(&model.User{}).
Scopes(Paginate(p.Current, p.Size)).
Order("created_at DESC").
Find(&data).
Offset(-1).Limit(-1).
Count(&count).Error; err != nil {
return
}
return
}
// GetUserByAccount
// @description: 根据账户获取用户信息
// @receiver s
// @param id
// @return data
// @return err
func (s user) GetUserByAccount(account string) (data vo.User, err error) {
err = s.Model(&model.User{}).Where("account = ?", account).Take(&data).Error
return
}
// GetUserById
// @description: 根据id获取用户信息
// @receiver s
// @param id
// @return data
// @return err
func (s user) GetUserById(id string) (data vo.User, err error) {
err = s.Model(&model.User{}).Where("id = ?", id).Take(&data).Error
return
}
// Status
// @description: 修改用户状态
// @receiver s
// @param id
// @return error
func (s user) Status(id string, state constant.Status) error {
return s.Model(&model.User{}).Where("id = ?", id).Update("status", state).Error
}
// ChangePassword
// @description: 修改密码
// @receiver s
// @param id
// @param password
// @return error
func (s user) ChangePassword(id, password string) error {
return s.Model(&model.User{}).Where("id = ?", id).Update("password", utils.Password().GenerateHashPassword(password)).Error
}

View File

@ -5,7 +5,6 @@ Address = {{ .IpAllocation|html }}
MTU = {{ .MTU }} MTU = {{ .MTU }}
{{ else }}MTU = {{ .MTU }}{{ end }} {{ else }}MTU = {{ .MTU }}{{ end }}
[Peer] [Peer]
PublicKey = {{ .PublicKey|html }} PublicKey = {{ .PublicKey|html }}
PresharedKey = {{ .PresharedKey|html }} PresharedKey = {{ .PresharedKey|html }}

View File

@ -1,13 +1,24 @@
package template_data package render_data
type ServerSetting struct {
EndpointAddress string `json:"endpointAddress"`
DnsServer []string `json:"dnsServer"`
MTU int `json:"MTU"`
PersistentKeepalive int `json:"persistentKeepalive"`
FirewallMark string `json:"firewallMark"`
Table string `json:"table"`
ConfigFilePath string `json:"configFilePath"`
}
type Server struct { type Server struct {
Address string `json:"address"` Address []string `json:"ipScope"`
ListenPort int `json:"listenPort"` ListenPort uint64 `json:"listenPort"`
PrivateKey string `json:"privateKey"` PrivateKey string `json:"privateKey"`
MTU int `json:"mtu"` PublicKey string `json:"publicKey" `
PostUp string `json:"postUp"` MTU int `json:"MTU"`
PreDown string `json:"preDown"` PostUp string `json:"postUpScript"`
PostDown string `json:"postDown"` PreDown string `json:"preDownScript"`
PostDown string `json:"postDownScript"`
Table string `json:"table"` Table string `json:"table"`
Clients []Client `json:"clients"` Clients []Client `json:"clients"`
} }

View File

@ -5,7 +5,7 @@ import (
"gitee.ltd/lxh/logger/log" "gitee.ltd/lxh/logger/log"
"math/rand" "math/rand"
"time" "time"
"wireguard-dashboard/client" "wireguard-ui/global/client"
) )
type avatar struct{} type avatar struct{}

View File

@ -3,10 +3,12 @@ package utils
import ( import (
"fmt" "fmt"
"gitee.ltd/lxh/logger/log" "gitee.ltd/lxh/logger/log"
"github.com/google/uuid"
"os" "os"
"strings"
"time" "time"
"wireguard-dashboard/client" "wireguard-ui/config"
"wireguard-dashboard/config" "wireguard-ui/global/client"
) )
type fileSystem struct{} type fileSystem struct{}
@ -29,19 +31,20 @@ func (fileSystem) UploadFile(file []byte, suffix string) (filePath string, err e
return "", err return "", err
} }
return ossObj.LongPath, nil filePath = ossObj.LongPath
case "local": case "local":
filePath = fmt.Sprintf("%v/%d-avatar%s", config.Config.File.Path, time.Now().Unix(), suffix) basePath := fmt.Sprintf("%s/%d/avatar", config.Config.File.Path, time.Now().Unix())
filePath = fmt.Sprintf("%s/%s%s", basePath, strings.ReplaceAll(uuid.NewString(), "-", ""), suffix)
// 创建目录 // 创建目录
if err = os.MkdirAll(filePath, os.FileMode(0777)); err != nil { if err = os.MkdirAll(basePath, os.FileMode(0777)); err != nil {
log.Errorf("本地存储目录创建失败: %v", err) log.Errorf("本地存储目录创建失败: %v", err)
return "", err return "", err
} }
if err = os.WriteFile(filePath, file, os.FileMode(0777)); err != nil { if err = os.WriteFile(filePath, file, os.FileMode(0777)); err != nil {
return "", err return "", err
} }
subPath := strings.ReplaceAll(filePath, fmt.Sprintf("%s/", config.Config.File.Path), "")
return filePath, nil filePath = fmt.Sprintf("http://%s/assets/%s", config.Config.Http.Endpoint, subPath)
} }
return "", nil return filePath, err
} }

View File

@ -1,11 +0,0 @@
package utils
import (
"fmt"
"testing"
)
func TestParse(t *testing.T) {
f := FlowCalculation()
fmt.Println(f.Parse(13030000000))
}

View File

@ -1,82 +0,0 @@
package utils
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
type PageData[T any] struct {
Current int `json:"current"` // 当前页码
Size int `json:"size"` // 每页数量
Total int64 `json:"total"` // 总数
TotalPage int `json:"totalPage"` // 总页数
Records T `json:"records"` // 返回数据
}
type ginResponse struct {
c *gin.Context
}
func GinResponse(c *gin.Context) ginResponse {
return ginResponse{c: c}
}
func (r ginResponse) OK() {
r.c.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"message": "success",
})
}
func (r ginResponse) Failed() {
r.c.JSON(http.StatusBadRequest, gin.H{
"code": http.StatusBadRequest,
"message": "bad request",
})
}
func (r ginResponse) FailedWithErr(msg string, err error) {
r.c.JSON(http.StatusBadRequest, gin.H{
"code": http.StatusBadRequest,
"message": fmt.Errorf("%s: %s", msg, err.Error()).Error(),
})
}
func (r ginResponse) FailedWithMsg(msg string) {
r.c.JSON(http.StatusBadRequest, gin.H{
"code": http.StatusBadRequest,
"message": msg,
})
}
func (r ginResponse) AuthorizationFailed() {
r.c.JSON(http.StatusUnauthorized, gin.H{
"code": http.StatusUnauthorized,
"message": "请先登陆",
})
}
func (r ginResponse) OKWithData(data any) {
r.c.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"message": "success",
"data": data,
})
}
func (r ginResponse) OkWithPage(data any, total int64, current, size int) {
// 处理一下页码、页数量
if current == -1 {
current = 1
size = int(total)
}
// 计算总页码
totalPage := GenTotalPage(total, size)
// 返回结果
r.c.JSON(http.StatusOK, map[string]any{
"code": http.StatusOK,
"data": &PageData[any]{Current: current, Size: size, Total: total, TotalPage: totalPage, Records: data},
"message": "success",
})
}

View File

@ -9,7 +9,7 @@ import (
"net/smtp" "net/smtp"
"net/textproto" "net/textproto"
"path/filepath" "path/filepath"
"wireguard-dashboard/config" "wireguard-ui/config"
) )
type mail struct { type mail struct {

View File

@ -1,8 +1,12 @@
package utils package utils
import ( import (
"fmt"
"gitee.ltd/lxh/logger/log" "gitee.ltd/lxh/logger/log"
"wireguard-dashboard/client" "github.com/spf13/cast"
"net"
"strings"
"wireguard-ui/global/client"
) )
type network struct{} type network struct{}
@ -25,3 +29,100 @@ func (network) GetHostPublicIP() string {
return response["origin"] return response["origin"]
} }
// IPContains
// @description: 校验ip是否在指定IP段中
// @receiver network
// @param specIp
// @param checkIP
func (network) IPContains(specIp []string, checkIP string) bool {
var isContains bool
for _, sip := range specIp {
ip, _, _ := net.ParseCIDR(checkIP)
sipNet, _, _ := net.ParseCIDR(sip)
ipNet := net.IPNet{IP: sipNet, Mask: net.CIDRMask(24, 32)}
if ipNet.Contains(ip) {
isContains = true
}
}
return isContains
}
// IPContainsByIP
// @description: 校验ip是否在指定IP段中
// @receiver network
// @param specIp
// @param checkIP
func (network) IPContainsByIP(specIp string, checkIP string) bool {
ip, _, _ := net.ParseCIDR(checkIP)
sipNet, _, _ := net.ParseCIDR(specIp)
ipNet := net.IPNet{IP: sipNet, Mask: net.CIDRMask(24, 32)}
if ipNet.Contains(ip) {
return true
}
return false
}
// ParseIP
// @description: 将带有mark的ip解析
// @receiver network
// @param ip
// @return string
func (network) ParseIP(ip string) string {
ipn, _, _ := net.ParseCIDR(ip)
return ipn.String()
}
// GetIPSuffix
// @description: 获取IP后缀 [10.10.10.23] 这里只获取23
// @receiver network
// @param ip
// @return string
func (network) GetIPSuffix(ip string) string {
if strings.Contains(ip, "/") {
ip = strings.Split(ip, "/")[0]
}
// 再次拆分,只取最后一段
return strings.Split(ip, ".")[3]
}
// GetIPPrefix
// @description: 获取IP前缀[10.10.10.23] 这里只获取 10.10.10
// @receiver network
// @param ip
// @return string
func (network) GetIPPrefix(ip string) string {
if strings.Contains(ip, "/") {
ip = strings.Split(ip, "/")[0]
}
return strings.Join(strings.Split(ip, ".")[:3], ".")
}
// GenerateIPByIPS
// @description: 根据指定IP段分配IP
// @receiver n
// @param ips
// @param assignedIPS
// @return string
func (n network) GenerateIPByIPS(ips []string, assignedIPS ...string) []string {
var oips []string
for _, sip := range ips {
// 再次拆分,只取最后一段
suffix := n.GetIPSuffix(sip)
prefix := n.GetIPPrefix(sip)
for _, cip := range assignedIPS {
if n.IPContainsByIP(sip, cip) {
suffix = n.GetIPSuffix(cip)
}
}
suffix = cast.ToString(cast.ToInt64(suffix) + 1)
oips = append(oips, fmt.Sprintf("%s.%s", prefix, suffix))
}
return oips
}

View File

@ -1,47 +0,0 @@
package utils
import "gorm.io/gorm"
// Page
// @description: 分页组件
// @param current
// @param size
// @return func(db *gorm.DB) *gorm.DB
func Page(current, size int) func(db *gorm.DB) *gorm.DB {
// 如果页码是-1就不分页
if current == -1 {
return func(db *gorm.DB) *gorm.DB {
return db
}
}
// 分页
return func(db *gorm.DB) *gorm.DB {
if current == 0 {
current = 1
}
if size < 1 {
size = 10
}
// 计算偏移量
offset := (current - 1) * size
// 返回组装结果
return db.Offset(offset).Limit(size)
}
}
// GenTotalPage
// @description: 计算页码数
// @param count
// @param size
// @return int
func GenTotalPage(count int64, size int) int {
totalPage := 0
if count > 0 {
upPage := 0
if int(count)%size > 0 {
upPage = 1
}
totalPage = (int(count) / size) + upPage
}
return totalPage
}

19
utils/paginate.go Normal file
View File

@ -0,0 +1,19 @@
package utils
type paginate struct{}
func Paginate() paginate {
return paginate{}
}
func (paginate) Generate(count int64, size int) int {
totalPage := 0
if count > 0 {
upPage := 0
if int(count)%size > 0 {
upPage = 1
}
totalPage = (int(count) / size) + upPage
}
return totalPage
}

26
utils/rand.go Normal file
View File

@ -0,0 +1,26 @@
package utils
import (
"bytes"
"crypto/rand"
"math/big"
)
type random struct{}
func Random() random {
return random{}
}
func (random) RandStr(len int) string {
var container string
var str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
b := bytes.NewBufferString(str)
length := b.Len()
bigInt := big.NewInt(int64(length))
for i := 0; i < len; i++ {
randomInt, _ := rand.Int(rand.Reader, bigInt)
container += string(str[randomInt.Int64()])
}
return container
}

View File

@ -1,53 +0,0 @@
package utils
import (
"gitee.ltd/lxh/logger/log"
"html/template"
"os"
)
type templateUtils struct{}
func Template() templateUtils {
return templateUtils{}
}
// Parse
// @description: 解析指定模板文件
// @receiver templateUtils
// @param filepath
// @return parseTemplate
// @return err
func (templateUtils) Parse(filepath string) (parseTemplate *template.Template, err error) {
file, err := os.ReadFile(filepath)
if err != nil {
return
}
parseTemplate, err = template.New("wg.conf").Parse(string(file))
return
}
// Execute
// @description: 序列化数据到文件中
// @receiver templateUtils
// @param fromTemplate
// @param data
// @param filePath
// @return err
func (templateUtils) Execute(fromTemplate *template.Template, data any, filePath string) (err error) {
wg0Conf, err := os.Create(filePath)
if err != nil {
log.Errorf("创建文件[%s]失败: %v", filePath, err.Error())
return
}
defer func() {
if err = wg0Conf.Close(); err != nil {
log.Errorf("关闭文件[%s]失败: %v", filePath, err.Error())
return
}
}()
return fromTemplate.Execute(wg0Conf, data)
}

View File

@ -1,16 +0,0 @@
package utils
import "net/url"
// GetHost
// @description: 获取指定地址的host
// @param addr
// @return string
func GetHost(addr string) string {
uu, err := url.Parse(addr)
if err != nil {
return ""
}
return uu.Host
}

17
utils/website.go Normal file
View File

@ -0,0 +1,17 @@
package utils
import "net/url"
type website struct{}
func WebSite() website {
return website{}
}
func (website) GetHost(addr string) string {
uu, err := url.Parse(addr)
if err != nil {
return ""
}
return uu.Host
}

View File

@ -1,173 +0,0 @@
package utils
import (
"encoding/json"
"errors"
"fmt"
"github.com/spf13/cast"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"math/rand"
"os"
"slices"
"strings"
"wireguard-dashboard/client"
"wireguard-dashboard/model/entity"
"wireguard-dashboard/model/template_data"
"wireguard-dashboard/model/vo"
)
type wireguard struct{}
func Wireguard() wireguard {
return wireguard{}
}
// GetSpecClient
// @description: 获取指定客户端链接信息
// @receiver wireguard
// @param pk
// @return *wgtypes.Peer
// @return error
func (wireguard) GetSpecClient(pk string) (*wgtypes.Peer, error) {
device, err := client.WireguardClient.Devices()
if err != nil {
return nil, err
}
for _, v := range device {
for _, p := range v.Peers {
if p.PublicKey.String() == pk {
return &p, nil
}
}
}
return nil, nil
}
// GenerateClientIP
// @description: 生成客户端IP
// @receiver wireguard
// @param serverIP
// @param rule
// @return string
func (w wireguard) GenerateClientIP(serverIP, rule string, assignedIPS ...string) string {
// 再次拆分,只取最后一段
suffix := w.GetIPSuffix(serverIP)
prefix := w.GetIPPrefix(serverIP)
// 如果是随机模式,则需要结尾数字小于等于 255并且生成的数字不能是已分配当中任何一个
// 如果是自动模式,只需要是已经分配的最后一位自增
switch rule {
case "RANDOM":
suffix = w.random(assignedIPS...)
case "AUTO":
switch len(assignedIPS) {
case 1:
suffix = w.GetIPSuffix(assignedIPS[0])
case 2:
suffix = w.GetIPSuffix(assignedIPS[1])
}
suffix = cast.ToString(cast.ToInt64(suffix) + 1)
}
return fmt.Sprintf("%s.%s", prefix, suffix)
}
// GenerateClientFile
// @description: 生成客户端临时配置文件
// @receiver w
// @param clientInfo
// @param setting
// @return tmpFilePath
// @return err
func (w wireguard) GenerateClientFile(clientInfo *entity.Client, setting *vo.ServerSetting) (tmpFilePath string, err error) {
var keys template_data.Keys
_ = json.Unmarshal([]byte(clientInfo.Keys), &keys)
var serverDNS []string
if *clientInfo.UseServerDns == 1 {
serverDNS = setting.DnsServer
}
// 处理一下数据
execData := template_data.ClientConfig{
PrivateKey: keys.PrivateKey,
IpAllocation: clientInfo.IpAllocation,
MTU: setting.MTU,
DNS: strings.Join(serverDNS, ","),
PublicKey: clientInfo.Server.PublicKey,
PresharedKey: keys.PresharedKey,
AllowedIPS: clientInfo.AllowedIps,
Endpoint: setting.EndpointAddress,
ListenPort: clientInfo.Server.ListenPort,
PersistentKeepalive: setting.PersistentKeepalive,
}
// 不同环境下处理文件路径
var outPath = "/tmp/" + fmt.Sprintf("%s.conf", clientInfo.Name)
var templatePath = "./template/wg.client.conf"
if os.Getenv("GIN_MODE") != "release" {
outPath = "E:\\Workspace\\Go\\wireguard-dashboard\\template\\" + fmt.Sprintf("%s.conf", clientInfo.Name)
templatePath = "E:\\Workspace\\Go\\wireguard-dashboard\\template\\wg.client.conf"
}
// 渲染数据
parseTemplate, err := Template().Parse(templatePath)
if err != nil {
return "", errors.New("读取模板文件失败")
}
err = Template().Execute(parseTemplate, execData, outPath)
if err != nil {
return "", errors.New("文件渲染失败")
}
return outPath, nil
}
// random
// @description: 随机模式
// @receiver w
// @param assignedIPS
// @return string
func (w wireguard) random(assignedIPS ...string) string {
randomNumber := rand.Int63n(256)
var assuffixIP []int64
for _, v := range assignedIPS {
assuffixIP = append(assuffixIP, cast.ToInt64(strings.Split(v, ".")[3]))
}
if slices.Contains(assuffixIP, randomNumber) {
return w.random(assignedIPS...)
}
return cast.ToString(randomNumber)
}
// GetIPSuffix
// @description: 获取IP后缀 [10.10.10.23] 这里只获取23
// @receiver w
// @param ip
// @return string
func (w wireguard) GetIPSuffix(ip string) string {
if strings.Contains(ip, "/") {
ip = strings.Split(ip, "/")[0]
}
// 再次拆分,只取最后一段
return strings.Split(ip, ".")[3]
}
// GetIPPrefix
// @description: 获取IP前缀[10.10.10.23] 这里只获取 10.10.10
// @receiver w
// @param ip
// @return string
func (w wireguard) GetIPPrefix(ip string) string {
if strings.Contains(ip, "/") {
ip = strings.Split(ip, "/")[0]
}
return strings.Join(strings.Split(ip, ".")[:3], ".")
}

Some files were not shown because too many files have changed in this diff Show More