Compare commits

...

54 Commits
v2.0.2 ... v2

Author SHA1 Message Date
coward
9497729378 🐳消除打包时的docker警告
All checks were successful
continuous-integration/drone/tag Build is passing
2024-12-27 15:36:32 +08:00
coward
48a58759da 🐳修改dockerfile在编译时存在警告
All checks were successful
continuous-integration/drone/tag Build is passing
2024-12-27 15:16:44 +08:00
coward
36f0202c57 🎨定时任务掉了
All checks were successful
continuous-integration/drone/tag Build is passing
2024-12-27 11:50:52 +08:00
coward
904907f796 Merge branch 'v2-develop' into v2
All checks were successful
continuous-integration/drone/tag Build is passing
2024-12-24 16:38:06 +08:00
coward
9674796f0a 🎨删除没必要的代码 2024-12-24 16:37:45 +08:00
coward
b245c26515 🆕新增tui管理模式[能用就行] 2024-12-24 16:35:57 +08:00
62cdfc6839 Merge pull request 'v2-develop' (#3) from v2-develop into v2
All checks were successful
continuous-integration/drone/tag Build is passing
Reviewed-on: #3
2024-12-20 10:33:41 +08:00
coward
014c97f877 ⬆️升级依赖 2024-12-20 10:32:49 +08:00
coward
1c2bfb0a29 ⬆️升级依赖 2024-12-17 16:47:47 +08:00
coward
78bd62a964 🎨变更了app name 2024-12-13 15:09:24 +08:00
coward
eba3e841c3 🎨变更启动方式 2024-12-13 15:07:57 +08:00
coward
628c09fdde 🎨新增客户端离线后上线通知
All checks were successful
continuous-integration/drone/tag Build is passing
2024-12-09 09:06:51 +08:00
coward
96999be84a 🎨修改通知模板
All checks were successful
continuous-integration/drone/tag Build is passing
2024-12-05 15:09:52 +08:00
coward
fc0a1a04b0 🎨修改通知模板
All checks were successful
continuous-integration/drone/tag Build is passing
2024-12-05 14:53:43 +08:00
coward
19a025e888 🐛fix a bug
All checks were successful
continuous-integration/drone/tag Build is passing
2024-12-05 14:33:38 +08:00
coward
dfe89d465c 🐛fix a bug
All checks were successful
continuous-integration/drone/tag Build is passing
2024-12-05 14:19:54 +08:00
coward
93911536d9 🎨调整定时任务执行时间,以及启动时间
All checks were successful
continuous-integration/drone/tag Build is passing
2024-12-05 11:52:17 +08:00
coward
f09d0d2994 🎨调整定时任务执行时间,以及启动时间
Some checks reported errors
continuous-integration/drone/tag Build was killed
2024-12-05 11:49:29 +08:00
coward
ca42c72e0f 🐛修复微信通知bug
All checks were successful
continuous-integration/drone/tag Build is passing
2024-12-05 11:25:21 +08:00
coward
683e7b2cc3 🐛修复定时任务bug
All checks were successful
continuous-integration/drone/tag Build is passing
2024-12-05 11:06:21 +08:00
coward
accb060e27 🐛修复定时任务bug
All checks were successful
continuous-integration/drone/tag Build is passing
2024-12-05 10:43:40 +08:00
coward
8884d945aa 🆕新增离线客户端监听并且发送通知
All checks were successful
continuous-integration/drone/tag Build is passing
2024-12-05 09:54:55 +08:00
coward
788def6dc4 👋这是 2024-11-06 09:10:30 +08:00
coward
29c1c791d4 调整了客户端列表页面的卡片布局
All checks were successful
continuous-integration/drone/tag Build is passing
2024-11-04 17:25:27 +08:00
coward
40019568f0 🎨备份导出、导入新增其他配置 2024-11-04 16:50:50 +08:00
coward
db065073e2 修复一下页面配置更改时无法修改配置描述
All checks were successful
continuous-integration/drone/tag Build is passing
2024-10-16 14:37:16 +08:00
coward
befaa17426 📝更新readme
All checks were successful
continuous-integration/drone/tag Build is passing
2024-10-15 17:26:10 +08:00
coward
fae96eccf5 📝更新readme 2024-10-15 17:22:39 +08:00
coward
4a0ec7bdc9 📝更新readme 2024-10-15 17:21:02 +08:00
coward
331849522f 🎨将邮箱smtp配置改为系统当中配置 2024-10-15 17:16:22 +08:00
coward
fa04d9c83a 🎨首页控制面板的客户端链接列表新增右键刷新
All checks were successful
continuous-integration/drone/tag Build is passing
2024-10-10 15:03:57 +08:00
coward
958b1b4aec ⬆️升级依赖 2024-10-10 14:29:21 +08:00
coward
72b8473591 🐛导入配置时,获取当前系统的公网IP,避免手动去更改
All checks were successful
continuous-integration/drone/tag Build is passing
2024-09-24 10:23:20 +08:00
coward
13e4006592 🎨加了一些组件数据监听优化
All checks were successful
continuous-integration/drone/tag Build is passing
2024-09-23 17:17:29 +08:00
coward
f2dcb13e0d 🆕导入配置文件完成 2024-09-23 15:58:46 +08:00
coward
edaf9ba770 🆕为实现数据迁移以及备份新增配置导出 2024-09-20 17:26:41 +08:00
coward
c3ef51e87f 🐛errors包导入掉了
All checks were successful
continuous-integration/drone/tag Build is passing
2024-09-04 11:42:01 +08:00
coward
72420f2ede 🐛修复可重复创建相同账号的bug
Some checks failed
continuous-integration/drone/tag Build is failing
2024-09-04 10:06:29 +08:00
coward
a12552a608 🎨去除首页的诗词
All checks were successful
continuous-integration/drone/tag Build is passing
2024-08-27 09:48:48 +08:00
coward
5f200ea989 🎨不想写啦 2024-08-23 11:25:27 +08:00
coward
3f14df72be 📝修改文档 2024-08-23 10:36:21 +08:00
coward
29902afe65 📝添加readme 2024-08-23 10:29:37 +08:00
ddef41dfca 添加 web/.env
All checks were successful
continuous-integration/drone/tag Build is passing
2024-08-22 16:02:01 +08:00
coward
6c6b40593e 🎨优化 2024-08-22 16:01:00 +08:00
coward
45d83da5c7 🎨优化一哈
All checks were successful
continuous-integration/drone/tag Build is passing
2024-08-22 15:37:21 +08:00
coward
4fa123baa8 🎨优化一下子
All checks were successful
continuous-integration/drone/tag Build is passing
2024-08-22 14:16:33 +08:00
coward
c7a56b1dde 🐛修复一个牛逼的bug
All checks were successful
continuous-integration/drone/tag Build is passing
2024-08-22 11:43:23 +08:00
coward
37446ea958 🎨 2024-08-22 11:14:28 +08:00
coward
1b28b196a0 🎨打印日志看输出
Some checks reported errors
continuous-integration/drone/tag Build was killed
2024-08-21 16:39:30 +08:00
coward
1a5ab341ab 🐛
All checks were successful
continuous-integration/drone/tag Build is passing
2024-08-21 14:33:50 +08:00
coward
299ec93199 🐛修复客户端链接列表的bug
All checks were successful
continuous-integration/drone/tag Build is passing
2024-08-21 14:12:43 +08:00
coward
32f61a0eef 🎨优化 2024-08-21 11:43:57 +08:00
coward
2c96984490 🐳
All checks were successful
continuous-integration/drone/tag Build is passing
2024-08-21 09:22:58 +08:00
coward
12e551b4e9 🎨添加静态打包
Some checks failed
continuous-integration/drone/tag Build is failing
2024-08-21 09:10:01 +08:00
188 changed files with 23245 additions and 152 deletions

62
.gitignore vendored
View File

@ -233,38 +233,44 @@ fabric.properties
# vendor/
# Go workspace file
go.work
./go.work
.idea
web/.idea
./.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/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/
web/.vscode/
./web/yarn.lock
./web/npm-debug.log*
./web/.pnpm-error.log*
./web/.pnpm-debug.log
./web/tests/**/coverage/
./web/.vscode/
# Editor directories and files
web/*.suo
web/*.ntvs*
web/*.njsproj
web/*.sln
web/tsconfig.tsbuildinfo
./web/*.suo
./web/*.ntvs*
./web/*.njsproj
./web/*.sln
./web/tsconfig.tsbuildinfo
template/tmp/*
logs/*
app.yaml
*.db
.env
*.env
dist/assets
dist/resource
dist/favicon.png
dist/favicon.svg
dist/index.html
./template/tmp/*
./logs/*
./app.yaml
./*.db
./.env
./*.env

View File

@ -1,9 +1,24 @@
# 打包前端
FROM node:18-alpine AS build-front
WORKDIR /front
COPY . .
WORKDIR ./web
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
RUN ls -lh && pwd
# 前后端集成打包
FROM golang:alpine as build
FROM golang:alpine AS build-backend
RUN apk add upx
WORKDIR /build
COPY . .
COPY --from=build-front /front/web/dist/ /build/dist
# sqlite必须
ENV GO111MODULE=on
ENV GOPROXY=https://goproxy.cn,direct
@ -23,9 +38,9 @@ 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/
COPY --from=build-backend --chown=wgui:wgui /build/wgui /app
COPY --from=build-backend /build/template/* /app/template/
RUN chmod +x wgui
ENTRYPOINT ["./wgui"]
ENTRYPOINT ["./wgui","http:serve"]

94
README.md Normal file
View File

@ -0,0 +1,94 @@
# Wireguard-UI
> wireguard的管理面板UI
## 安装(仅提供docker方式)
OS X & Linux & Windows:
```sh
# 先要创建一个配置文件 app.yaml
http:
port: 6687
endpoint: localhost:3100,localhost:6687
database:
driver: sqlite # sqlite时只填写db即可目前仅支持sqlite | mysql | pgsql
host:
port:
user:
password:
db: wg
cache:
type: redis # 缓存类型 暂时仅支持redis
host: 192.168.1.1
port: 6379
password: pGhQKwj7DE7FbFL1
db: 15
file:
type: oss # 文件类型支持本地文件存储与阿里云oss存储
path: test/ # oss填写前缀目录
endpoint: # oss必填
accessId: # oss必填
accessSecret: # oss必填
bucketName: # oss必填
# 一些系统配置
wireguard:
restartMode: DELAY
delayTime: 20
# 其中依赖了redis等自行安装一个即可
```
```sh
# 创建docker-compose.yaml
version: "3"
services:
wg:
image: gitea.mrx.ltd/go-pkg/wireguard-srv:2.1.0
container_name: wg-srv
restart: always
cap_add:
- NET_ADMIN
network_mode: host
logging:
driver: json-file
options:
max-size: 50m
volumes:
- ./app.yaml:/app/app.yaml
- ./db:/app/db
- ./logs:/app/logs
- /etc/wireguard:/etc/wireguard
- /etc/localtime:/etc/localtime
```
```sh
默认账户密码
账户: admin
密码: admin123
```
## 配置示例
```text
1. 邮箱配置如下:
code: EMAIL_SMTP
配置项:
1. host: "xxxx.xxx"
2. port: "123"
3. user: "haha"
4. password: "haha123"
5. skipTls: "false"
```
## 页面展示
![img.png](document/img.png)
![img_1.png](document/img_1.png)
![img_7.png](document/img_7.png)
![img_8.png](document/img_8.png)
![img_2.png](document/img_2.png)
![img_3.png](document/img_3.png)
![img_4.png](document/img_4.png)
![img_5.png](document/img_5.png)
![img_6.png](document/img_6.png)

10
cli/kernal.go Normal file
View File

@ -0,0 +1,10 @@
package cli
import (
"wireguard-ui/cli/tui"
)
func Kernel() error {
tui.NewApp().Run()
return nil
}

165
cli/tui/app.go Normal file
View File

@ -0,0 +1,165 @@
package tui
import (
"context"
"errors"
"fmt"
"github.com/google/uuid"
"time"
"wireguard-ui/component"
"wireguard-ui/global/client"
"wireguard-ui/global/constant"
"wireguard-ui/http/vo"
"wireguard-ui/service"
"wireguard-ui/utils"
)
type App struct {
TokenSecret string // token密钥
User *vo.User // 登陆用户
Client *ClientComponent // 客户端组件
Server *ServerComponent // 服务端组件
Setting *SettingComponent // 设置组件
}
func NewApp() *App {
app := &App{}
if _, err := app.Login(); err != nil {
fmt.Println("登陆失败: ", err)
return nil
}
err := app.AuthLogin()
if err != nil {
fmt.Println("登陆失败: ", err)
return nil
}
fmt.Println("\n=============== 登陆成功 ==============================================================================")
app.Client = NewClientComponent(app.User)
app.Server = NewServerComponent(app.User)
app.Setting = NewSettingComponent(app.User)
fmt.Println("=============== 欢迎使用wireguard-tui =================================================================")
fmt.Println("=============== 当前用户: ", app.User.Nickname, " ================================================================")
fmt.Println("=============== 当前时间: ", time.Now().Format("2006-01-02 15:04:05"), " =======================================================")
fmt.Println("=============== 注意事项如下: =========================================================================")
fmt.Println("=============== 1. 请确保服务端已经安装wireguard ======================================================")
fmt.Println("=============== 2. 请确保服务端和客户端配置文件路径正确 ===============================================")
fmt.Println("=============== 3. 请确保服务端和客户端配置文件权限正确 ===============================================")
fmt.Println("=============== 4. 请确保服务端和客户端配置文件内容正确 ===============================================")
fmt.Println("=============== 5. 请勿泄露配置文件内容 ===============================================================")
fmt.Println("=============== 6. 每次修改客户端、服务端配置或者全局配置过后,请使用重启功能重启服务端,以保证生效 ===")
fmt.Println("=============== 7. 当使用重启无效时,请手动执行对应命令 ===============================================")
fmt.Println("=============== 8. 请勿随意删除客户端,删除后无法恢复 =================================================")
fmt.Println("=============== 9. 手动命令 ===========================================================================")
fmt.Println("=============== 10. 启动wireguard服务端: wg-quick up wg0 ==============================================")
fmt.Println("=============== 11. 停止wireguard服务端: wg-quick down wg0 ============================================")
return app
}
func (a *App) Run() {
if a == nil {
return
}
for {
PrintMenu()
chooseMenu := readInput("请选择菜单: ")
switch chooseMenu {
case "1":
a.Client.ConnectList()
case "2":
a.Client.Menus()
case "3":
a.Server.Menus()
case "4":
a.Setting.Menus()
case "q":
return
}
}
}
// AuthLogin
// @description: 登陆认证
// @receiver a
// @return string
func (a *App) AuthLogin() error {
// 先判断token是否存在
tokenStr, err := client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.TUIUserToken, a.User.Id)).Result()
if err != nil {
// 不存在,去登陆
tokenStr, err = a.Login()
if err != nil {
return err
}
}
// 存在,不必要再次登陆,解析token
claims, err := component.JWT().ParseToken("Bearer "+tokenStr, a.TokenSecret, "tui")
if err != nil {
return err
}
user, err := service.User().GetUserById(claims.ID)
if err != nil {
return err
}
if user.Status != constant.Enabled {
return errors.New("用户状态异常,请联系管理员处理")
}
a.User = &vo.User{
Id: user.Id,
Account: user.Account,
Nickname: user.Nickname,
Avatar: user.Avatar,
Contact: user.Contact,
IsAdmin: user.IsAdmin,
Status: user.Status,
}
return nil
}
// Login
// @description: 登陆
// @receiver a
// @return string
func (a *App) Login() (tokenStr string, err error) {
fmt.Println("============== 登陆 ==============")
username := readInput("请输入用户名: ")
password := readInput("请输入密码: ")
// 验证码正确,查询用户信息
user, err := service.User().GetUserByAccount(username)
if err != nil {
return "", fmt.Errorf("用户不存在: %v", err)
}
// 对比密码
if !utils.Password().ComparePassword(user.Password, password) {
return "", errors.New("密码错误")
}
secret := component.JWT().GenerateSecret(password, uuid.NewString(), time.Now().Local().String())
// 生成token
token, _, err := component.JWT().GenerateToken(user.Id, secret, "tui")
if err != nil {
return "", fmt.Errorf("登陆失败: %v", err)
}
a.User = &vo.User{
Id: user.Id,
Account: user.Account,
Nickname: user.Nickname,
Avatar: user.Avatar,
Contact: user.Contact,
IsAdmin: user.IsAdmin,
Status: user.Status,
}
a.TokenSecret = secret
return "Bearer " + token, nil
}

520
cli/tui/client.go Normal file
View File

@ -0,0 +1,520 @@
package tui
import (
"encoding/json"
"fmt"
"gitee.ltd/lxh/logger/log"
"github.com/charmbracelet/bubbles/table"
"github.com/spf13/cast"
"os"
"strconv"
"strings"
"time"
"wireguard-ui/component"
"wireguard-ui/global/constant"
"wireguard-ui/http/param"
"wireguard-ui/http/vo"
"wireguard-ui/service"
"wireguard-ui/utils"
)
type ClientComponent struct {
LoginUser *vo.User
Menu []string
Clients [][]string
}
func NewClientComponent(loginUser *vo.User) *ClientComponent {
ccp := &ClientComponent{
LoginUser: loginUser,
Menu: []string{"[1] 客户端列表", "[2] 查看客户端配置", "[3] 添加客户端", "[4] 编辑客户端", "[5] 删除客户端", "[q] 返回上一级菜单"},
}
return ccp
}
// Menus
// @description: 客户端菜单
// @receiver c
func (c *ClientComponent) Menus() {
fmt.Println("")
for _, r := range c.Menu {
fmt.Println(" -> " + r)
}
chooseMenu := readInput("\n请选择: ")
switch chooseMenu {
case "1":
c.List(true)
case "2":
c.ShowConfig()
case "3":
c.Add()
case "4":
c.Edit()
case "5":
c.Delete()
case "q":
return
}
}
// ConnectList
// @description: 客户端链接列表
// @receiver c
// @return string
func (c *ClientComponent) ConnectList() {
fmt.Println("\n客户端链接列表")
connectList, err := component.Wireguard().GetClients()
if err != nil {
fmt.Println("获取客户端链接列表失败: ", err.Error())
return
}
var data [][]string
for _, peer := range connectList {
// 获取客户端链接信息
clientInfo, err := service.Client().GetByPublicKey(peer.PublicKey.String())
if err != nil {
continue
}
var ipAllocation string
for _, iaip := range peer.AllowedIPs {
ipAllocation += iaip.String() + ","
}
// 去除一下最右边的逗号
if len(ipAllocation) > 0 {
ipAllocation = strings.TrimRight(ipAllocation, ",")
}
var isOnline = "否"
if time.Since(peer.LastHandshakeTime).Minutes() < 3 {
isOnline = "是"
}
data = append(data, []string{clientInfo.Name,
clientInfo.Email,
ipAllocation,
isOnline,
utils.FlowCalculation().Parse(peer.TransmitBytes),
utils.FlowCalculation().Parse(peer.ReceiveBytes),
peer.Endpoint.String(),
peer.LastHandshakeTime.Format("2006-01-02 15:04:05")})
}
//if len(data) <= 0 {
// //data = append(data, []string{"暂无数据"})
// // data = append(data, []string{"名称1", "12345678910@qq.com", "192.168.100.1", "是", "10G", "20G", "1.14.30.133:51280", "2024-12-20 15:07:36"}, []string{"名称2", "12345678910@qq.com", "192.168.100.2", "否", "20G", "40G", "1.14.30.133:51280", "2024-12-22 15:07:36"})
//}
title := []table.Column{
{
Title: "客户端名称",
Width: 20,
}, {
Title: "联系邮箱",
Width: 20,
}, {
Title: "分配的IP",
Width: 30,
}, {
Title: "是否在线",
Width: 10,
}, {
Title: "接收流量",
Width: 10,
}, {
Title: "传输流量",
Width: 10,
}, {
Title: "链接端点",
Width: 30,
}, {
Title: "最后握手时间",
Width: 30,
}}
Show(GenerateTable(title, data))
return
}
// List
// @description: 客户端列表
// @receiver c
func (c *ClientComponent) List(showMenu bool) {
title := []table.Column{
{
Title: "序号",
Width: 5,
},
{
Title: "ID",
Width: 35,
},
{
Title: "名称",
Width: 25,
},
{
Title: "联系邮箱",
Width: 30,
},
{
Title: "客户端IP",
Width: 20,
},
{
Title: "状态",
Width: 10,
},
{
Title: "离线通知",
Width: 10,
},
}
records, _, err := service.Client().List(param.ClientList{
Page: param.Page{
Current: -1,
},
})
if err != nil {
fmt.Println("获取客户端列表失败: ", err.Error())
return
}
fmt.Println("\n客户端列表")
var data [][]string
for i, client := range records {
var status, offlineNotify string
if client.Enabled == 1 {
status = "启用"
} else {
status = "禁用"
}
if client.OfflineMonitoring == 1 {
offlineNotify = "开启"
} else {
offlineNotify = "关闭"
}
data = append(data, []string{
cast.ToString(i + 1),
client.Id,
client.Name,
client.Email,
client.IpAllocationStr,
status,
offlineNotify,
})
}
Show(GenerateTable(title, data))
c.Clients = data
if showMenu {
c.Menus()
}
return
}
// ShowConfig
// @description: 显示配置
// @receiver c
func (c *ClientComponent) ShowConfig() {
c.List(false)
clientIdx := readInput("请输入客户端序号: ")
downloadType := readInput("请输入下载类型(FILE - 文件 | EMAIL - 邮件): ")
idx, err := strconv.Atoi(clientIdx)
if err != nil {
fmt.Println("输入有误: ", err.Error())
return
}
if idx < 1 || idx > len(c.Clients) {
fmt.Println("输入有误")
return
}
client := c.Clients[idx-1]
// 取到id
clientID := client[1]
// 查询客户端信息
clientInfo, err := service.Client().GetByID(clientID)
if err != nil {
fmt.Println("获取客户端信息失败: ", err.Error())
return
}
// 渲染配置
var keys vo.Keys
_ = json.Unmarshal([]byte(clientInfo.Keys), &keys)
globalSet, err := service.Setting().GetWGSetForConfig()
if err != nil {
fmt.Println("获取全局配置失败: ", err.Error())
return
}
serverConf, err := service.Setting().GetWGServerForConfig()
if err != nil {
fmt.Println("获取服务器配置失败: ", err.Error())
return
}
outPath, err := component.Wireguard().GenerateClientFile(clientInfo, serverConf, globalSet)
if err != nil {
fmt.Println("生成客户端配置失败: ", err.Error())
return
}
// 根据不同下载类型执行不同逻辑
switch downloadType {
case "FILE": // 二维码
// 读取文件内容
fileContent, err := os.ReadFile(outPath)
if err != nil {
fmt.Println("读取文件失败: ", err.Error())
return
}
fmt.Println("\n#请将以下内容复制到客户端配置文件中【不包含本行提示语】")
fmt.Println("\n" + string(fileContent))
if err = os.Remove(outPath); err != nil {
log.Errorf("删除临时文件失败: %s", err.Error())
}
case "EMAIL": // 邮件
if clientInfo.Email == "" {
fmt.Println("当前客户端并未配置通知邮箱")
return
}
// 获取邮箱配置
emailConf, err := service.Setting().GetByCode("EMAIL_SMTP")
if err != nil {
fmt.Println("获取邮箱配置失败请先到设置页面的【其他】里面添加code为【EMAIL_SMTP】的具体配置")
return
}
err = utils.Mail(emailConf).SendMail(clientInfo.Email, fmt.Sprintf("客户端: %s", clientInfo.Name), "请查收附件", outPath)
if err != nil {
fmt.Println("发送邮件失败")
return
}
if err = os.Remove(outPath); err != nil {
log.Errorf("删除临时文件失败: %s", err.Error())
}
fmt.Println("发送邮件成功,请注意查收!")
}
c.Menus()
}
// Add
// @description: 添加客户端
// @receiver c
func (c *ClientComponent) Add() {
fmt.Println("\n添加客户端")
clientIP, serverIP, err := GenerateIP()
if err != nil {
fmt.Println("生成客户端IP失败: ", err.Error())
return
}
keys, err := GenerateKeys()
if err != nil {
fmt.Println("生成密钥对失败: ", err.Error())
return
}
var p param.SaveClient
p.Name = readInput("请输入客户端名称: ")
p.Email = readInput("请输入联系邮箱: ")
clientIPIn := readInput("请输入客户端IP(默认自动生成,多个采用 ',' 分割,例如 10.10.0.1/32,10.10.0.2/32): ")
if clientIPIn == "" {
p.IpAllocation = clientIP
} else {
p.IpAllocation = strings.Split(clientIPIn, ",")
}
p.AllowedIps = serverIP
p.Keys = &param.Keys{
PrivateKey: keys.PrivateKey,
PublicKey: keys.PublicKey,
PresharedKey: keys.PresharedKey,
}
var useServerDNS, enabled, offlineNotify constant.Status
useServerDNSIn := readInput("是否使用服务器DNS(默认不使用 1 - 是 | 0 - 否 ): ")
switch useServerDNSIn {
case "1":
useServerDNS = constant.Enabled
case "0":
useServerDNS = constant.Disabled
default:
useServerDNS = constant.Disabled
}
enabledIn := readInput("是否启用(默认启用 1 - 是 | 0 - 否 ): ")
switch enabledIn {
case "1":
enabled = constant.Enabled
case "0":
enabled = constant.Disabled
default:
enabled = constant.Enabled
}
offlineNotifyIn := readInput("是否开启离线通知(默认关闭 1 - 是 | 0 - 否 ): ")
switch offlineNotifyIn {
case "1":
offlineNotify = constant.Enabled
case "0":
offlineNotify = constant.Disabled
default:
offlineNotify = constant.Disabled
}
p.UseServerDns = &useServerDNS
p.Enabled = &enabled
p.OfflineMonitoring = &offlineNotify
err = service.Client().SaveClient(p, c.LoginUser)
if err != nil {
fmt.Println("添加客户端失败: ", err.Error())
return
}
fmt.Println("添加客户端成功")
c.List(true)
return
}
// Edit
// @description: 编辑客户端
// @receiver c
func (c *ClientComponent) Edit() {
fmt.Println("\n编辑客户端")
c.List(false)
clientIdx := readInput("请输入客户端序号: ")
idx, err := strconv.Atoi(clientIdx)
if err != nil {
fmt.Println("输入有误: ", err.Error())
return
}
if idx < 1 || idx > len(c.Clients) {
fmt.Println("输入有误")
return
}
client := c.Clients[idx-1]
// 取到id
clientID := client[1]
// 查询客户端信息
clientInfo, err := service.Client().GetByID(clientID)
if err != nil {
fmt.Println("获取客户端信息失败: ", err.Error())
return
}
var p param.SaveClient
p.Id = clientID
p.Name = readInput("请输入客户端名称[无需改变请回车跳过,下同]: ")
p.Email = readInput("请输入联系邮箱: ")
clientIPIn := readInput("请输入客户端IP(默认自动生成,多个采用 ',' 分割,例如 10.10.0.1/32,10.10.0.2/32): ")
if clientIPIn == "" {
p.IpAllocation = strings.Split(clientInfo.IpAllocation, ",")
} else {
p.IpAllocation = strings.Split(clientIPIn, ",")
}
p.AllowedIps = strings.Split(clientInfo.AllowedIps, ",")
var keys *param.Keys
_ = json.Unmarshal([]byte(clientInfo.Keys), &keys)
p.Keys = keys
var useServerDNS, enabled, offlineNotify constant.Status
useServerDNSIn := readInput("是否使用服务器DNS(默认不使用 1 - 是 | 0 - 否 ): ")
switch useServerDNSIn {
case "1":
useServerDNS = constant.Enabled
case "0":
useServerDNS = constant.Disabled
default:
useServerDNS = clientInfo.UseServerDns
}
enabledIn := readInput("是否启用(默认启用 1 - 是 | 0 - 否 ): ")
switch enabledIn {
case "1":
enabled = constant.Enabled
case "0":
enabled = constant.Disabled
default:
enabled = clientInfo.Enabled
}
offlineNotifyIn := readInput("是否开启离线通知(默认关闭 1 - 是 | 0 - 否 ): ")
switch offlineNotifyIn {
case "1":
offlineNotify = constant.Enabled
case "0":
offlineNotify = constant.Disabled
default:
offlineNotify = clientInfo.OfflineMonitoring
}
p.UseServerDns = &useServerDNS
p.Enabled = &enabled
p.OfflineMonitoring = &offlineNotify
err = service.Client().SaveClient(p, c.LoginUser)
if err != nil {
fmt.Println("编辑客户端失败: ", err.Error())
return
}
fmt.Println("编辑客户端成功")
c.List(true)
return
}
// Delete
// @description: 删除客户端
// @receiver c
func (c *ClientComponent) Delete() {
fmt.Println("\n删除客户端")
c.List(false)
clientIdx := readInput("请输入客户端序号: ")
idx, err := strconv.Atoi(clientIdx)
if err != nil {
fmt.Println("输入有误: ", err.Error())
return
}
if idx < 1 || idx > len(c.Clients) {
fmt.Println("输入有误")
return
}
client := c.Clients[idx-1]
// 取到id
clientID := client[1]
if err := service.Client().Delete(clientID); err != nil {
fmt.Println("删除客户端失败: ", err.Error())
return
}
c.List(true)
fmt.Println("删除客户端成功")
return
}

62
cli/tui/server.go Normal file
View File

@ -0,0 +1,62 @@
package tui
import (
"fmt"
"wireguard-ui/command"
"wireguard-ui/http/vo"
)
type ServerComponent struct {
LoginUser *vo.User
Menu []string
}
func NewServerComponent(loginUser *vo.User) *ServerComponent {
return &ServerComponent{
LoginUser: loginUser,
Menu: []string{"[1] 启动服务", "[2] 关闭服务", "[3] 重启服务", "[q] 返回上一级菜单"},
}
}
// Menus
// @description: 服务端菜单
// @receiver s
func (s *ServerComponent) Menus() {
fmt.Println("")
for _, r := range s.Menu {
fmt.Println(" -> " + r)
}
chooseMenu := readInput("\n请选择: ")
switch chooseMenu {
case "1":
s.Start()
case "2":
s.Stop()
case "3":
s.Restart()
case "q":
return
}
}
// Start
// @description: 启动服务
// @receiver s
func (s *ServerComponent) Start() {
command.StartWireguard("")
}
// Stop
// @description: 停止服务
// @receiver s
func (s *ServerComponent) Stop() {
command.StopWireguard("")
}
// Restart
// @description: 重启服务
// @receiver s
func (s *ServerComponent) Restart() {
command.RestartWireguard(false, "")
}

440
cli/tui/setting.go Normal file
View File

@ -0,0 +1,440 @@
package tui
import (
"encoding/json"
"fmt"
"github.com/charmbracelet/bubbles/table"
"github.com/eiannone/keyboard"
"github.com/spf13/cast"
"strings"
"wireguard-ui/http/vo"
"wireguard-ui/model"
"wireguard-ui/service"
"wireguard-ui/utils"
)
type SettingComponent struct {
LoginUser *vo.User
Menu []string
Other *OtherSettingComponent
}
func NewSettingComponent(loginUser *vo.User) *SettingComponent {
return &SettingComponent{
LoginUser: loginUser,
Menu: []string{"[1] 服务端配置", "[2] 全局设置", "[3] 其他配置", "[q] 返回上一级菜单"},
Other: NewOtherSettingComponent(),
}
}
// Menus
// @description: 设置菜单
// @receiver s
func (s *SettingComponent) Menus() {
fmt.Println("")
for _, r := range s.Menu {
fmt.Println(" -> " + r)
}
chooseMenu := readInput("\n请选择: ")
switch chooseMenu {
case "1":
s.ServerSetting()
case "2":
s.GlobalSetting()
case "3":
s.Other.Menus(s)
case "q":
return
}
}
// ServerSetting
// @description: 服务端配置
// @receiver s
func (s *SettingComponent) ServerSetting() {
fmt.Println("\n服务端配置")
// 先读取一下服务端配置
servConf, err := service.Setting().GetByCode("WG_SERVER")
if err != nil {
fmt.Println("获取服务端配置失败: ", err)
return
}
type serverConf struct {
IpScope []string `json:"ipScope"`
ListenPort int `json:"listenPort"`
PrivateKey string `json:"privateKey"`
PublicKey string `json:"publicKey"`
PostUpScript string `json:"postUpScript"`
PostDownScript string `json:"postDownScript"`
}
// 解析出来好渲染
var conf serverConf
if err = json.Unmarshal([]byte(servConf.Data), &conf); err != nil {
fmt.Println("解析服务端配置失败: ", err)
return
}
ipScopeIn := readInput(fmt.Sprintf("请输入IP段多个采用 ',[英文逗号]' 分割,不填写默认当前值,下同,当前值[%s] ", strings.Replace(strings.Join(conf.IpScope, ","), " ", "", -1)))
listenPortIn := readInput(fmt.Sprintf("请输入监听端口,当前值[%d] ", conf.ListenPort))
privateKeyIn := readInput(fmt.Sprintf("请输入私钥,当前值[%s] ", conf.PrivateKey))
publicKeyIn := readInput(fmt.Sprintf("请输入公钥,当前值[%s] ", conf.PublicKey))
postUpScriptIn := readInput(fmt.Sprintf("请输入PostUp脚本当前值[%s] ", conf.PostUpScript))
postDownScriptIn := readInput(fmt.Sprintf("请输入PostDown脚本当前值[%s] ", conf.PostDownScript))
if ipScopeIn != "" {
conf.IpScope = strings.Split(ipScopeIn, ",")
}
if listenPortIn != "" {
conf.ListenPort = cast.ToInt(listenPortIn)
}
if privateKeyIn != "" {
conf.PrivateKey = privateKeyIn
}
if publicKeyIn != "" {
conf.PublicKey = publicKeyIn
}
if postUpScriptIn != "" {
conf.PostUpScript = postUpScriptIn
}
if postDownScriptIn != "" {
conf.PostDownScript = postDownScriptIn
}
data, _ := json.Marshal(conf)
if err := service.Setting().SetData(&model.Setting{
Code: "WG_SERVER",
Data: string(data),
}); err != nil {
fmt.Println("保存服务端配置失败: ", err)
return
}
fmt.Println("修改服务端配置成功")
return
}
// GlobalSetting
// @description: 全局设置
// @receiver s
func (s *SettingComponent) GlobalSetting() {
fmt.Println("\n服务端配置")
// 先读取一下服务端配置
globalConf, err := service.Setting().GetByCode("WG_SETTING")
if err != nil {
fmt.Println("获取服务端配置失败: ", err)
return
}
type gConf struct {
MTU int `json:"MTU"`
ConfigFilePath string `json:"configFilePath"`
DnsServer []string `json:"dnsServer"`
EndpointAddress string `json:"endpointAddress"`
FirewallMark string `json:"firewallMark"`
PersistentKeepalive int `json:"persistentKeepalive"`
Table string `json:"table"`
}
// 解析出来好渲染
var conf gConf
if err = json.Unmarshal([]byte(globalConf.Data), &conf); err != nil {
fmt.Println("解析全局配置失败: ", err)
return
}
mtu := readInput(fmt.Sprintf("请输入mtu不填写默认当前值下同当前值[%d] ", conf.MTU))
configFilePath := readInput(fmt.Sprintf("请输入配置文件地址,当前值[%s] ", conf.ConfigFilePath))
dnsServer := readInput(fmt.Sprintf("请输入dns多个采用 ',[英文逗号]' 分割,当前值[%s] ", strings.Replace(strings.Join(conf.DnsServer, ","), " ", "", -1)))
endpointAddress := readInput(fmt.Sprintf("请输入公网IP默认系统自动获取当前值[%s] ", conf.EndpointAddress))
firewallMark := readInput(fmt.Sprintf("请输入FirewallMark当前值[%s] ", conf.FirewallMark))
persistentKeepalive := readInput(fmt.Sprintf("请输入PersistentKeepalive当前值[%d] ", conf.PersistentKeepalive))
tableRule := readInput(fmt.Sprintf("请输入Table当前值[%s] ", conf.Table))
if mtu != "" {
conf.MTU = cast.ToInt(mtu)
}
if configFilePath != "" {
conf.ConfigFilePath = configFilePath
}
if dnsServer != "" {
conf.DnsServer = strings.Split(dnsServer, ",")
}
if endpointAddress != "" {
conf.EndpointAddress = endpointAddress
} else {
conf.EndpointAddress = utils.Network().GetHostPublicIP()
}
if firewallMark != "" {
conf.FirewallMark = firewallMark
}
if persistentKeepalive != "" {
conf.PersistentKeepalive = cast.ToInt(persistentKeepalive)
}
if tableRule != "" {
conf.Table = tableRule
}
data, _ := json.Marshal(conf)
if err := service.Setting().SetData(&model.Setting{
Code: "WG_SETTING",
Data: string(data),
}); err != nil {
fmt.Println("保存全局配置失败: ", err)
return
}
fmt.Println("修改全局配置成功")
return
}
// OtherSettingComponent
// @description: 其他配置杂项
type OtherSettingComponent struct {
Setting *SettingComponent
Menu []string
}
func NewOtherSettingComponent() *OtherSettingComponent {
return &OtherSettingComponent{
Menu: []string{"[1] 列表", "[2] 添加", "[3] 编辑", "[4] 删除", "[q] 返回上一级菜单"},
}
}
func (s *OtherSettingComponent) Menus(setting *SettingComponent) {
if s.Setting == nil {
s.Setting = setting
}
fmt.Println("")
for _, r := range s.Menu {
fmt.Println(" -> " + r)
}
chooseMenu := readInput("\n请选择: ")
switch chooseMenu {
case "1":
s.List(true)
case "2":
s.Add()
case "3":
s.Edit()
case "4":
s.Delete()
case "q":
s.Setting.Menus()
}
}
// List
// @description: 其他配置
// @receiver s
// @param showMenu
func (s *OtherSettingComponent) List(showMenu bool) {
fmt.Println("\n其他配置列表")
// 不查询的配置
var blackList = []string{"WG_SETTING", "WG_SERVER"}
data, err := service.Setting().GetAllSetting(blackList)
if err != nil {
fmt.Println("获取配置失败")
return
}
title := []table.Column{
{
Title: "序号",
Width: 10,
},
{
Title: "编码",
Width: 40,
},
{
Title: "描述",
Width: 50,
},
{
Title: "创建时间",
Width: 30,
},
{
Title: "更新时间",
Width: 30,
},
}
var result [][]string
for i, v := range data {
result = append(result, []string{
cast.ToString(i + 1),
v.Code,
v.Describe,
v.CreatedAt.Format("2006-01-02 15:04:05"),
v.UpdatedAt.Format("2006-01-02 15:04:05"),
})
}
Show(GenerateTable(title, result))
if showMenu {
s.Menus(nil)
}
}
// Add
// @description: 新增其他配置
// @receiver s
func (s *OtherSettingComponent) Add() {
fmt.Println("\n新增其他配置")
code := readInput("请输入配置编码,此编码是唯一编码不可重复:")
desc := readInput("请输入配置描述:")
// 监听键盘事件,只监听 + 和 - 和 enter
if err := keyboard.Open(); err != nil {
return
}
defer func() {
_ = keyboard.Close()
}()
// + <=> 43 | - <=> 45 | enter <=> 0
fmt.Println("请按下 + 或者 - 进行配置项的新增和删除")
fmt.Println("每一项配置如此: key=val ")
fmt.Println("确认输入完成后 enter[按一次代表当前配置项输入完成,两次代表新增完成]")
fmt.Println("首先进入时请输入 + 进行第一个配置项填写")
var breakCycle bool
var keyVal []string
for {
char, _, err := keyboard.GetKey()
if err != nil {
break
}
if breakCycle {
break
}
switch char {
case 0:
// 收到enter事件触发执行后面的
var dm = make(map[string]any)
for _, kv := range keyVal {
kvs := strings.Split(kv, "=")
key := kvs[0]
val := kvs[1]
dm[key] = val
}
dms, err := json.Marshal(dm)
if err != nil {
breakCycle = true
continue
}
if err = service.Setting().SetData(&model.Setting{
Code: code,
Data: string(dms),
Describe: desc,
}); err != nil {
breakCycle = true
fmt.Println("保存配置失败: ", err.Error())
continue
}
fmt.Println("保存配置成功")
s.List(true)
breakCycle = true
case 43:
keyVal = append(keyVal, readInput("请输入配置项:"))
case 45:
keyVal = keyVal[:len(keyVal)-1]
fmt.Println("已删除最后一个配置项,当前配置项为:", keyVal)
}
}
}
// Edit
// @description: 编辑
// @receiver s
func (s *OtherSettingComponent) Edit() {
fmt.Println("\n编辑其他配置")
s.List(false)
code := readInput("请输入需要编辑的配置编码:")
// 通过编码查询配置
setting, err := service.Setting().GetByCode(code)
if err != nil {
fmt.Println("查找["+code+"]配置失败:", err.Error())
return
}
desc := readInput("请输入需要编辑的配置描述:")
if desc != "" {
setting.Describe = desc
}
var kvm = make(map[string]any)
if err = json.Unmarshal([]byte(setting.Data), &kvm); err != nil {
fmt.Println("配置解析失败: ", err.Error())
return
}
for k, v := range kvm {
valIn := readInput(fmt.Sprintf("请输入配置项值,仅能修改值,当前键值对:%s=%v ", k, v))
if valIn != "" {
kvm[k] = valIn
}
}
// 处理以下数据
kvmStr, err := json.Marshal(kvm)
if err != nil {
fmt.Println("序列化数据失败: ", err.Error())
return
}
setting.Data = string(kvmStr)
if err = service.Setting().SetData(setting); err != nil {
fmt.Println("修改配置失败: ", err.Error())
return
}
fmt.Println("修改配置成功")
}
// Delete
// @description: 删除
// @receiver s
func (s *OtherSettingComponent) Delete() {
fmt.Println("\n 删除指定配置")
s.List(false)
code := readInput("请输入要删除的配置项编码:")
// 查询配置是否存在
if err := service.Setting().Model(&model.Setting{}).
Where("code NOT IN (?)", []string{"WG_SETTING", "WG_SERVER"}).
Where("code = ?", code).Delete(&model.Setting{}).Error; err != nil {
fmt.Println("删除[" + code + "]配置失败")
return
}
fmt.Println("删除成功")
s.List(true)
}

136
cli/tui/utils.go Normal file
View File

@ -0,0 +1,136 @@
package tui
import (
"bufio"
"fmt"
"gitee.ltd/lxh/logger/log"
"github.com/charmbracelet/bubbles/table"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cast"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"os"
"strings"
"wireguard-ui/http/vo"
"wireguard-ui/model"
"wireguard-ui/service"
"wireguard-ui/utils"
)
var BaseStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240"))
var menus = []string{"[1] 客户端链接列表", "[2] 客户端", "[3] 服务端", "[4] 设置", "[q] 退出"}
// PrintMenu
// @description: 打印一下菜单
func PrintMenu() {
fmt.Println("\n菜单:")
for _, menu := range menus {
fmt.Println(menu)
}
}
// GenerateTable
// @description: 生成数据表
// @param column
// @param rows
// @return string
func GenerateTable(column []table.Column, rows [][]string) string {
var data []table.Row
for _, v := range rows {
data = append(data, v)
}
tl := table.New(
table.WithColumns(column),
table.WithRows(data),
table.WithHeight(len(data)+1),
)
s := table.DefaultStyles()
s.Header = s.Header.
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240")).
BorderBottom(true).
Bold(false)
s.Selected = lipgloss.NewStyle()
tl.SetStyles(s)
return BaseStyle.Render(tl.View()+"\n") + "\n一共有 【" + fmt.Sprintf("%d", len(data)) + "】 条数据"
}
// GenerateIP
// @description: 生成IP
// @return clientIPS
// @return serverIPS
// @return err
func GenerateIP() (clientIPS, serverIPS []string, err error) {
// 获取一下服务端信息因为IP分配需要根据服务端的IP制定
serverInfo, err := service.Setting().GetWGServerForConfig()
if err != nil {
return nil, nil, err
}
var assignIPS []string
// 只获取最新的一个
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, ",") {
if cast.ToInt64(utils.Network().GetIPSuffix(ip)) >= 255 {
log.Errorf("IP[%s]已无法分配新IP", ip)
continue
} else {
assignIPS = append(assignIPS, ip)
}
}
}
ips := utils.Network().GenerateIPByIPS(serverInfo.Address, assignIPS...)
return ips, serverInfo.Address, nil
}
// GenerateKeys
// @description: 生成密钥对
// @return keys
// @return err
func GenerateKeys() (keys *vo.Keys, err error) {
// 为空,新增
privateKey, err := wgtypes.GeneratePrivateKey()
if err != nil {
return nil, err
}
publicKey := privateKey.PublicKey().String()
presharedKey, err := wgtypes.GenerateKey()
if err != nil {
return nil, fmt.Errorf("生成预共享密钥失败: %s", err.Error())
}
keys = &vo.Keys{
PrivateKey: privateKey.String(),
PublicKey: publicKey,
PresharedKey: presharedKey.String(),
}
return keys, nil
}
// Show
// @description: 展示
// @param data
func Show(data any) {
fmt.Println(data)
}
// readInput
// @description: 读取输入
// @param prompt
// @return string
func readInput(prompt string) string {
fmt.Print(prompt)
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
return scanner.Text()
}

View File

@ -36,7 +36,7 @@ func JWT() JwtComponent {
// @return token
// @return expireTime
// @return err
func (JwtComponent) GenerateToken(userId, secret string, times ...time.Time) (token string, expireTime *jwt.NumericDate, err error) {
func (JwtComponent) GenerateToken(userId, secret, source string, times ...time.Time) (token string, expireTime *jwt.NumericDate, err error) {
var notBefore, issuedAt *jwt.NumericDate
if len(times) != 0 {
expireTime = jwt.NewNumericDate(times[0])
@ -68,10 +68,19 @@ func (JwtComponent) GenerateToken(userId, secret string, times ...time.Time) (to
return "", nil, errors.New("生成token失败")
}
client.Redis.Set(context.Background(),
fmt.Sprintf("%s:%s", constant.UserToken, userId),
token,
time.Duration(expireTime.Sub(time.Now()).Abs().Seconds())*time.Second)
switch source {
case "http":
client.Redis.Set(context.Background(),
fmt.Sprintf("%s:%s", constant.UserToken, userId),
token,
time.Duration(expireTime.Sub(time.Now()).Abs().Seconds())*time.Second)
case "tui":
client.Redis.Set(context.Background(),
fmt.Sprintf("%s:%s", constant.TUIUserToken, userId),
token,
time.Duration(expireTime.Sub(time.Now()).Abs().Seconds())*time.Second)
}
return
}
@ -81,7 +90,7 @@ func (JwtComponent) GenerateToken(userId, secret string, times ...time.Time) (to
// @param token
// @return *JwtComponent
// @return error
func (JwtComponent) ParseToken(token, secret string) (*JwtComponent, error) {
func (JwtComponent) ParseToken(token, secret, source string) (*JwtComponent, error) {
tokenStr := strings.Split(token, "Bearer ")[1]
t, err := jwt.ParseWithClaims(tokenStr, &JwtComponent{}, func(token *jwt.Token) (any, error) {
@ -89,10 +98,20 @@ func (JwtComponent) ParseToken(token, secret string) (*JwtComponent, error) {
})
if claims, ok := t.Claims.(*JwtComponent); ok && t.Valid {
userToken, err := client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.UserToken, claims.ID)).Result()
if err != nil {
log.Errorf("缓存中用户[%s]的token查找失败: %v", claims.ID, err.Error())
return nil, errors.New("token不存在")
var userToken string
switch source {
case "http":
userToken, err = client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.UserToken, claims.ID)).Result()
if err != nil {
log.Errorf("缓存中用户[%s]的token查找失败: %v", claims.ID, err.Error())
return nil, errors.New("token不存在")
}
case "tui":
userToken, err = client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.TUIUserToken, claims.ID)).Result()
if err != nil {
log.Errorf("缓存中用户[%s]的token查找失败: %v", claims.ID, err.Error())
return nil, errors.New("token不存在")
}
}
if userToken != tokenStr {

View File

@ -36,6 +36,18 @@ func Error(err error) string {
return errMsg
}
// Validate
// @description: 校验
// @param data
// @return string
func Validate(data any) error {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
return v.Struct(data)
}
return nil
}
// initValidatorTranslator
// @description: 初始化翻译机
// @receiver vli

View File

@ -97,8 +97,8 @@ func (w WireguardComponent) GenerateClientFile(clientInfo *model.Client, server
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"
outPath = "E:\\Workspace\\Go\\wireguard-ui\\template\\tmp\\" + fmt.Sprintf("%s.conf", clientInfo.Name)
templatePath = "E:\\Workspace\\Go\\wireguard-ui\\template\\conf\\wg.client.conf"
}
err = Template().Execute(templatePath, outPath, execData)

View File

@ -7,6 +7,5 @@ type config struct {
Database *database `yaml:"database"`
Cache *cache `yaml:"redis"`
File *file `yaml:"file"`
Mail *mail `yaml:"email"`
Wireguard *wireguard `yaml:"wireguard"`
}

View File

@ -1,9 +0,0 @@
package config
type mail struct {
Host string `json:"host" yaml:"host"`
Port int `json:"port" yaml:"port"`
User string `json:"user" yaml:"user"`
Password string `json:"password" yaml:"password"`
SkipTls bool `json:"skipTls" yaml:"skipTls"`
}

19
cron/cron.go Normal file
View File

@ -0,0 +1,19 @@
package cron
import (
"gitee.ltd/lxh/logger/log"
"github.com/go-co-op/gocron/v2"
"time"
"wireguard-ui/cron/task"
)
func Task() {
sch, err := gocron.NewScheduler(gocron.WithLocation(time.Local))
if err != nil {
log.Errorf("初始化定时任务失败")
return
}
_, _ = sch.NewJob(gocron.DurationJob(1*time.Minute), gocron.NewTask(task.NetworkClient().ClientOfflineNotify)) // 每分钟执行一次
sch.Start()
}

119
cron/task/client.go Normal file
View File

@ -0,0 +1,119 @@
package task
import (
"context"
"fmt"
"gitee.ltd/lxh/logger/log"
jsoniter "github.com/json-iterator/go"
"golang.org/x/exp/slices"
"strings"
"time"
"wireguard-ui/component"
"wireguard-ui/global/client"
"wireguard-ui/global/constant"
"wireguard-ui/model"
"wireguard-ui/service"
"wireguard-ui/utils"
)
type NetworkClientImpl interface {
ClientOfflineNotify() // 客户端离线通知
}
type networkClient struct{}
func NetworkClient() NetworkClientImpl {
return networkClient{}
}
// ClientOfflineNotify
// @description: 客户端离线通知
// @receiver c
// @return error
func (c networkClient) ClientOfflineNotify() {
log.Debugf("开始执行离线通知任务")
// 开始扫描已经链接过的客户端
connectedPeers, err := component.Wireguard().GetClients()
if err != nil {
log.Errorf("获取已连接客户端失败: %v", err.Error())
return
}
// 查询一下通知配置
code, err := service.Setting().GetByCode("WECHAT_NOTIFY")
if err != nil {
log.Errorf("获取微信通知配置失败: %v", err.Error())
return
}
// 查询出所有配置了离线通知的客户端
var clients []model.Client
if err := client.DB.Where("offline_monitoring = ?", 1).Find(&clients).Error; err != nil {
return
}
if len(clients) <= 0 {
return
}
for _, peer := range connectedPeers {
var clientName string
if !slices.ContainsFunc(clients, func(cli model.Client) bool {
isExist := peer.PublicKey.String() == jsoniter.Get([]byte(cli.Keys), "publicKey").ToString()
if isExist {
clientName = cli.Name
}
return isExist
}) {
continue
}
online := time.Since(peer.LastHandshakeTime).Minutes() < 3
log.Debugf("客户端[%v]在线状态: %v离线时间: %v", clientName, online, time.Since(peer.LastHandshakeTime).Minutes())
var ipAllocation string
for _, iaip := range peer.AllowedIPs {
ipAllocation += iaip.String() + ","
}
// 去除一下最右边的逗号
if len(ipAllocation) > 0 {
ipAllocation = strings.TrimRight(ipAllocation, ",")
}
// 如果存在,判断离线时间
if !online {
// 已经离线,发送通知
msg := fmt.Sprintf(`[离线通知]
客户端名称 : %v
客户端IP : %v
最后在线时间 : %v`, clientName, ipAllocation, peer.LastHandshakeTime.Format("2006-01-02 15:04:05"))
err := utils.WechatNotify(code).SendTextMessage(msg)
if err != nil {
log.Errorf("微信消息[%v]通知失败: %v", msg, err.Error())
continue
}
// 离线了,设置离线标识
client.Redis.Set(context.Background(), fmt.Sprintf("%s%s", constant.ClientOffline, utils.Hash().MD5(ipAllocation)), true, 0)
} else {
// 判断是否存在缓存
if client.Redis.Exists(context.Background(), fmt.Sprintf("%s%s", constant.ClientOffline, utils.Hash().MD5(ipAllocation))).Val() > 0 {
// 存在,删除离线标识
client.Redis.Del(context.Background(), fmt.Sprintf("%s%s", constant.ClientOffline, utils.Hash().MD5(ipAllocation)))
// 微信通知该客户端已经上线
msg := fmt.Sprintf(`[上线通知]
客户端名称 : %v
客户端IP : %v
最后在线时间 : %v`, clientName, ipAllocation, peer.LastHandshakeTime.Format("2006-01-02 15:04:05"))
err := utils.WechatNotify(code).SendTextMessage(msg)
if err != nil {
log.Errorf("微信消息[%v]通知失败: %v", msg, err.Error())
continue
}
}
}
}
return
}

6
dist/static.go vendored Normal file
View File

@ -0,0 +1,6 @@
package dist
import "embed"
//go:embed index.html favicon.png favicon.svg assets resource
var Static embed.FS

BIN
document/img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

BIN
document/img_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
document/img_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
document/img_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
document/img_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
document/img_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
document/img_6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
document/img_7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
document/img_8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -1,6 +1,8 @@
package constant
const (
Captcha = "captcha"
UserToken = "token"
Captcha = "captcha"
UserToken = "token"
TUIUserToken = "tui:token"
ClientOffline = "client:offline:"
)

73
go.mod
View File

@ -3,51 +3,68 @@ module wireguard-ui
go 1.21
require (
gitee.ltd/lxh/logger v1.0.15
gitee.ltd/lxh/logger v1.0.18
github.com/charmbracelet/bubbles v0.20.0
github.com/charmbracelet/lipgloss v0.13.0
github.com/cowardmrx/go_aliyun_oss v1.0.7
github.com/dustin/go-humanize v1.0.1
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
github.com/fsnotify/fsnotify v1.7.0
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/go-co-op/gocron/v2 v2.12.4
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.22.0
github.com/go-resty/resty/v2 v2.13.1
github.com/redis/go-redis/v9 v9.5.3
github.com/golang-jwt/jwt/v5 v5.2.1
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.7.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/spf13/cast v1.6.0
github.com/spf13/viper v1.19.0
github.com/urfave/cli/v2 v2.27.5
golang.org/x/crypto v0.31.0
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
gopkg.in/fsnotify/fsnotify.v1 v1.4.7
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.9
gorm.io/driver/postgres v1.5.11
gorm.io/gorm v1.25.10
)
require (
github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/bytedance/sonic v1.12.5 // indirect
github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/caarlos0/env/v6 v6.10.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/charmbracelet/bubbletea v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.2.3 // indirect
github.com/charmbracelet/x/term v0.2.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/cors v1.7.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-kit/kit v0.12.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.5.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/validator/v10 v10.22.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.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/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // 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/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
@ -55,15 +72,17 @@ require (
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
github.com/josharian/native v1.1.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/leodido/go-urn v1.4.0 // indirect
github.com/lixh00/loki-client-go v1.0.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
@ -71,9 +90,12 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mojocn/base64Captcha v1.3.6 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
github.com/panjf2000/ants/v2 v2.10.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.13.0 // indirect
@ -82,35 +104,34 @@ require (
github.com/prometheus/procfs v0.8.0 // indirect
github.com/prometheus/prometheus v1.8.2-0.20201028100903-3245b3267b24 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.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/ugorji/go/codec v1.2.12 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/image v0.13.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/net v0.33.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/text v0.15.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect
google.golang.org/grpc v1.62.1 // 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.v3 v3.0.1 // indirect

111
go.sum
View File

@ -35,8 +35,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
collectd.org v0.3.0/go.mod h1:A/8DzQBkF6abtvrT2j/AU/4tiBgJWYyh0y/oB/4MlWE=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gitee.ltd/lxh/logger v1.0.15 h1:x/HIHujq01ofltBK6WduYs890hiJVlU2ES6ytKusVZA=
gitee.ltd/lxh/logger v1.0.15/go.mod h1:Ef3o9duDaGApRUlibvkG92QBFM3HcndI0U30n/SMfYk=
gitee.ltd/lxh/logger v1.0.18 h1:LKc0Glk+jx08qDaX1kYIEHZUxUJtV3uqP5ceYK352YY=
gitee.ltd/lxh/logger v1.0.18/go.mod h1:hzd8prYXbac9YMaEDFMHYHMKKm1LqiFVtHjENWi+qQE=
github.com/Azure/azure-sdk-for-go v46.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
@ -60,8 +60,8 @@ github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw=
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
@ -106,6 +106,10 @@ github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQ
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
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/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
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=
@ -122,10 +126,11 @@ 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/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
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/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic v1.12.5 h1:hoZxY8uW+mT+OpkcUWw4k0fDINtOcVavEsGfzwzFU/w=
github.com/bytedance/sonic v1.12.5/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/c-bata/go-prompt v0.2.2/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34=
github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II=
github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=
@ -138,6 +143,18 @@ 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.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c=
github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4=
github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY=
github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
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/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -159,6 +176,8 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc
github.com/cowardmrx/go_aliyun_oss v1.0.7 h1:MCSKUWi4RZnHhwe4fd7VAsgeRXL0kT9z56TTde+1lME=
github.com/cowardmrx/go_aliyun_oss v1.0.7/go.mod h1:xz6B8H840TX7yPcgSLUbK7q6nnEsxFutaltR08Aetdg=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -186,6 +205,8 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg=
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
@ -193,6 +214,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
@ -210,10 +233,6 @@ github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uq
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 v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
github.com/gin-contrib/pprof v1.5.0 h1:E/Oy7g+kNw94KfdCy3bZxQFtyDnAX2V7axRS7sNYVrU=
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/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
@ -226,6 +245,8 @@ 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/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/go-co-op/gocron/v2 v2.12.4 h1:h1HWApo3T+61UrZqEY2qG1LUpDnB7tkYITxf6YIK354=
github.com/go-co-op/gocron/v2 v2.12.4/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w=
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-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -309,8 +330,6 @@ 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/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/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.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
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=
@ -512,6 +531,8 @@ 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/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.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/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
@ -571,6 +592,8 @@ github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-b
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
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/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
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=
@ -594,8 +617,12 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
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-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@ -637,6 +664,12 @@ github.com/mojocn/base64Captcha v1.3.6/go.mod h1:i5CtHvm+oMbj1UzEPXaA8IH/xHFZ3DG
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
@ -680,6 +713,8 @@ github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJ
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/panjf2000/ants/v2 v2.10.0 h1:zhRg1pQUtkyRiOFo2Sbqwjp0GfBNo9cUY2/Grpx1p+8=
github.com/panjf2000/ants/v2 v2.10.0/go.mod h1:7ZxyxsqE4vvW0M7LSD8aI3cKwgFhBHbxnlN8mDqHa1I=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/paulbellamy/ratecounter v0.2.0/go.mod h1:Hfx1hDpSGoqxkVVpBi/IlYD7kChlfo5C6hzIHwPqfFE=
@ -749,12 +784,17 @@ 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/go.mod h1:MDRkz271loM/PrYN+wUNEaTMDGSP760MQzB0yEjdgSQ=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU=
github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
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/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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 v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@ -764,6 +804,8 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
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/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/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 v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
@ -828,6 +870,7 @@ 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.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.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
@ -845,6 +888,8 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E
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.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
@ -852,6 +897,8 @@ github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHM
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=
github.com/xlab/treeprint v1.0.0/go.mod h1:IoImgRak9i3zJyuxOKUP1v4UZd1tMoKkq/Cimt1uhCg=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -877,8 +924,8 @@ 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/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
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.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
@ -889,7 +936,6 @@ 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.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
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.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@ -911,8 +957,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
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-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -926,13 +973,14 @@ 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-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-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
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-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.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -1002,8 +1050,9 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
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.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1028,8 +1077,9 @@ 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-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.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1093,6 +1143,7 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -1101,8 +1152,9 @@ 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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -1121,8 +1173,9 @@ 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.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -1346,8 +1399,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
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=

View File

@ -243,7 +243,14 @@ func (ClientApi) Download(c *gin.Context) {
return
}
err = utils.Mail().SendMail(data.Email, fmt.Sprintf("客户端: %s", data.Name), "请查收附件", outPath)
// 获取邮箱配置
emailConf, err := service.Setting().GetByCode("EMAIL_SMTP")
if err != nil {
response.R(c).FailedWithError("获取邮箱配置失败请先到设置页面的【其他】里面添加code为【EMAIL_SMTP】的具体配置")
return
}
err = utils.Mail(emailConf).SendMail(data.Email, fmt.Sprintf("客户端: %s", data.Name), "请查收附件", outPath)
if err != nil {
response.R(c).FailedWithError("发送邮件失败")
return

View File

@ -3,6 +3,7 @@ package api
import (
"fmt"
"github.com/gin-gonic/gin"
"strings"
"time"
"wireguard-ui/component"
"wireguard-ui/http/param"
@ -77,17 +78,25 @@ func (DashboardApi) ConnectionList(c *gin.Context) {
for _, iaip := range peer.AllowedIPs {
ipAllocation += iaip.String() + ","
}
// 去除一下最右边的逗号
if len(ipAllocation) > 0 {
ipAllocation = strings.TrimRight(ipAllocation, ",")
}
connections = append(connections, vo.DataTraffic{
Name: clientInfo.Name,
Email: clientInfo.Email,
IpAllocation: clientInfo.IpAllocation,
IpAllocation: ipAllocation,
Online: time.Since(peer.LastHandshakeTime).Minutes() < 3,
ReceiveBytes: utils.FlowCalculation().Parse(peer.TransmitBytes),
TransmitBytes: utils.FlowCalculation().Parse(peer.ReceiveBytes),
ConnectEndpoint: ipAllocation,
ConnectEndpoint: peer.Endpoint.String(),
LastHandAt: peer.LastHandshakeTime.Format("2006-01-02 15:04:05"),
})
}
if len(connections) <= 0 {
connections = []vo.DataTraffic{}
}
response.R(c).OkWithData(connections)
}

View File

@ -76,7 +76,7 @@ func (LoginApi) Login(c *gin.Context) {
secret := component.JWT().GenerateSecret(p.Password, uuid.NewString(), time.Now().Local().String())
// 生成token
token, expireAt, err := component.JWT().GenerateToken(user.Id, secret)
token, expireAt, err := component.JWT().GenerateToken(user.Id, secret, "http")
if err != nil {
log.Errorf("用户[%s]生成token失败: %v", user.Account, err.Error())
response.R(c).FailedWithError("登陆失败!")

33
http/api/remote.go Normal file
View File

@ -0,0 +1,33 @@
package api
import "github.com/gin-gonic/gin"
type remote struct{}
func Remote() remote {
return remote{}
}
// SaveAuthClient
// @description: 添加授权客户端
// @receiver remote
// @param c
func (remote) SaveAuthClient(c *gin.Context) {
return
}
// DeleteAuthClient
// @description: 删除授权客户端
// @receiver remote
// @param c
func (remote) DeleteAuthClient(c *gin.Context) {
return
}
// GetClientNodes
// @description: 获取客户端节点
// @receiver remote
// @param c
func (remote) GetClientNodes(c *gin.Context) {
return
}

View File

@ -1,11 +1,17 @@
package api
import (
"encoding/json"
"errors"
"fmt"
"gitee.ltd/lxh/logger/log"
"github.com/gin-gonic/gin"
"os"
"slices"
"wireguard-ui/component"
"wireguard-ui/http/param"
"wireguard-ui/http/response"
"wireguard-ui/http/vo"
"wireguard-ui/model"
"wireguard-ui/script"
"wireguard-ui/service"
@ -61,7 +67,7 @@ func (setting) Delete(c *gin.Context) {
return
}
if err := service.Setting().Model(&model.Setting{}).Where("code = ?", code).Delete(&model.Setting{}).Error; err != nil {
if err := service.Setting().Model(&model.Setting{}).Where("code NOT IN (?)", []string{"WG_SETTING", "WG_SERVER"}).Where("code = ?", code).Delete(&model.Setting{}).Error; err != nil {
response.R(c).FailedWithError("删除失败")
return
}
@ -113,3 +119,99 @@ func (setting) GetAllSetting(c *gin.Context) {
func (setting) GetPublicAddr(c *gin.Context) {
response.R(c).OkWithData(utils.Network().GetHostPublicIP())
}
// Export
// @description: 导出配置
// @receiver setting
// @param c
func (setting) Export(c *gin.Context) {
// 获取当前登陆用户
var loginUser *vo.User
if loginUser = GetCurrentLoginUser(c); c.IsAborted() {
return
}
if loginUser.Account != "admin" {
response.R(c).FailedWithError("非法操作,你被捕啦!")
return
}
// 获取配置
data, err := service.Setting().Export()
if err != nil {
response.R(c).FailedWithError(err)
return
}
// 生成配置文件
dataBytes, _ := json.Marshal(data)
filepath, err := utils.FileUtils().GenerateFile("config.json", dataBytes)
if err != nil {
response.R(c).FailedWithError(err)
return
}
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename="+filepath)
c.Header("Content-Transfer-Encoding", "binary")
c.Header("Connection", "keep-alive")
c.File(filepath)
if err = os.Remove(filepath); err != nil {
log.Errorf("删除临时文件失败: %s", err.Error())
}
}
// Import
// @description: 导入配置
// @receiver setting
// @param c
func (setting) Import(c *gin.Context) {
var p param.Import
if err := c.ShouldBind(&p); err != nil {
response.R(c).Validator(err)
return
}
// 校验文件是否合规
if p.File.Filename != "config.json" {
response.R(c).Validator(errors.New("文件名不合规"))
return
}
// 校验文件内容是否符合
fileBytes, err := p.File.Open()
if err != nil {
response.R(c).FailedWithError(err)
return
}
var data vo.Export
if err := json.NewDecoder(fileBytes).Decode(&data); err != nil {
response.R(c).FailedWithError(err)
return
}
// 校验json串是否合规
if err = component.Validate(&data); err != nil {
response.R(c).Validator(err)
return
}
// 获取当前登陆用户
var loginUser *vo.User
if loginUser = GetCurrentLoginUser(c); c.IsAborted() {
return
}
if loginUser.Account != "admin" {
response.R(c).FailedWithError("非法操作,你被捕啦!")
return
}
if err = service.Setting().Import(&data, loginUser); err != nil {
response.R(c).FailedWithError(fmt.Errorf("导入失败: %v", err.Error()))
return
}
response.R(c).OK()
}

26
http/kernel.go Normal file
View File

@ -0,0 +1,26 @@
package http
import (
"fmt"
"gitee.ltd/lxh/logger/log"
"net/http"
"wireguard-ui/config"
"wireguard-ui/http/router"
)
// Kernel
// @description: http启动
// @return error
func Kernel() error {
router.Rooters()
handler := router.InitRouter()
addr := fmt.Sprintf(":%d", config.Config.Http.Port)
httpServer := http.Server{
Addr: addr,
Handler: handler,
}
log.Infof("[HTTP] server runing in %s", addr)
return httpServer.ListenAndServe()
}

View File

@ -33,7 +33,7 @@ func Authorization() gin.HandlerFunc {
return
}
userClaims, err := component.JWT().ParseToken(token, hashPassword)
userClaims, err := component.JWT().ParseToken(token, hashPassword, "http")
if err != nil {
response.R(c).AuthorizationFailed("未登陆")
c.Abort()
@ -50,13 +50,13 @@ func Authorization() gin.HandlerFunc {
// 查询用户
user, err := service.User().GetUserById(userClaims.ID)
if err != nil {
response.R(c).FailedWithError("用户不存在")
response.R(c).AuthorizationFailed("用户不存在")
c.Abort()
return
}
if user.Status != constant.Enabled {
response.R(c).FailedWithError("用户状态异常,请联系管理员处理!")
response.R(c).AuthorizationFailed("用户状态异常,请联系管理员处理!")
c.Abort()
return
}
@ -70,7 +70,7 @@ func Authorization() gin.HandlerFunc {
// 生成一个新token
secret := component.JWT().GenerateSecret(user.Password, uuid.NewString(), time.Now().Local().String())
tokenStr, _, err := component.JWT().GenerateToken(user.Id, secret, userClaims.ExpiresAt.Time, time.Now().Local())
tokenStr, _, err := component.JWT().GenerateToken(user.Id, secret, "http", userClaims.ExpiresAt.Time, time.Now().Local())
if err != nil {
response.R(c).AuthorizationFailed("校验失败")
c.Abort()

View File

@ -52,8 +52,8 @@ func RequestLog() gin.HandlerFunc {
method := c.Request.Method // 请求方式
ip := c.ClientIP() // 取出IP
// 处理实际客户端IP
if c.Request.Header.Get("X-Real-IP") != "" {
ip = c.Request.Header.Get("X-Real-IP") // 这个是网关Nginx自定义的Header头
if c.Request.Header.Get("X-Real-Ip") != "" {
ip = c.Request.Header.Get("X-Real-Ip") // 这个是网关Nginx自定义的Header头
} else if c.Request.Header.Get("X-Forwarded-For") != "" {
ip = c.Request.Header.Get("X-Forwarded-For") // 这个是网关Nginx自定义的Header头
}

View File

@ -1,5 +1,7 @@
package param
import "mime/multipart"
// SetSetting
// @description: 添加/编辑设置
type SetSetting struct {
@ -7,3 +9,9 @@ type SetSetting struct {
Data string `json:"data" form:"data" binding:"required"`
Describe string `json:"describe" form:"describe" binding:"omitempty"`
}
// Import
// @description: 导入
type Import struct {
File *multipart.FileHeader `json:"file" form:"file" binding:"required"`
}

View File

@ -2,7 +2,9 @@ package router
import (
"github.com/gin-gonic/gin"
"net/http"
"wireguard-ui/config"
"wireguard-ui/dist"
)
type Option func(engine *gin.RouterGroup)
@ -24,6 +26,8 @@ func InitRouter() *gin.Engine {
r.Static("/assets", config.Config.File.Path)
}
r.StaticFS("/web", http.FS(dist.Static))
for _, opt := range options {
opt(r.Group("api"))
}

View File

@ -17,5 +17,7 @@ func SettingApi(r *gin.RouterGroup) {
setting.GET("", api.Setting().GetSetting) // 获取指定配置
setting.GET("/all", api.Setting().GetAllSetting) // 获取全部配置
setting.GET("/public-addr", api.Setting().GetPublicAddr) // 获取公网IP
setting.GET("/export", api.Setting().Export) // 导出配置文件
setting.POST("/import", api.Setting().Import) // 导入配置
}
}

View File

@ -1,9 +1,74 @@
package vo
import (
"wireguard-ui/global/constant"
"wireguard-ui/model"
)
// SettingItem
// @description: 设置单项
type SettingItem struct {
Code string `json:"code"`
Data string `json:"data"`
Describe string `json:"describe"`
Code string `json:"code"`
Data string `json:"data"`
Describe string `json:"describe"`
CreatedAt model.JsonTime `json:"createdAt"`
UpdatedAt model.JsonTime `json:"updatedAt"`
}
type Export struct {
Global *Global `json:"global" label:"全局配置" binding:"required"`
Server *Server `json:"server" label:"服务端配置" binding:"required"`
Clients []Client `json:"clients" label:"客户端" binding:"omitempty"`
Other []Other `json:"other" label:"其他" binding:"omitempty"`
}
// Global
// @description: 全局配置
type Global struct {
MTU int `json:"MTU" label:"MTU" binding:"required"`
ConfigFilePath string `json:"configFilePath" label:"配置文件路径" binding:"required"`
DnsServer []string `json:"dnsServer" label:"DNS" binding:"omitempty"`
EndpointAddress string `json:"endpointAddress" label:"公网地址" binding:"required"`
FirewallMark string `json:"firewallMark" label:"firewallMark" binding:"omitempty"`
PersistentKeepalive int `json:"persistentKeepalive" label:"persistentKeepalive" binding:"required"`
Table string `json:"table" label:"table" binding:"omitempty"`
}
// Server
// @description: 服务端信息
type Server struct {
IpScope []string `json:"ipScope" label:"ipScope" binding:"min=1,dive,required"`
ListenPort int `json:"listenPort" label:"listenPort" binding:"required"`
PrivateKey string `json:"privateKey" label:"privateKey" binding:"required"`
PublicKey string `json:"publicKey" label:"publicKey" binding:"required"`
PostUpScript string `json:"postUpScript" label:"postUpScript" binding:"omitempty"`
PostDownScript string `json:"postDownScript" label:"postDownScript" binding:"omitempty"`
}
// Client
// @description: 客户端信息
type Client struct {
Name string `json:"name" label:"name" binding:"required"`
Email string `json:"email" label:"email" binding:"omitempty"`
SubnetRange string `json:"subnetRange" label:"subnetRange" binding:"omitempty"`
IpAllocation []string `json:"ipAllocation" label:"ipAllocation" binding:"min=1,dive,required"`
AllowedIps []string `json:"allowedIps" label:"allowedIps" binding:"min=1,dive,required"`
ExtraAllowedIps []string `json:"extraAllowedIps" label:"extraAllowedIps" binding:"omitempty"`
Endpoint string `json:"endpoint" label:"endpoint" binding:"endpoint"`
UseServerDns *constant.Status `json:"useServerDns" label:"useServerDns" binding:"omitempty"`
Keys struct {
PresharedKey string `json:"presharedKey" label:"presharedKey" binding:"required"`
PrivateKey string `json:"privateKey" label:"privateKey" binding:"required"`
PublicKey string `json:"publicKey" label:"publicKey" binding:"required"`
} `json:"keys" label:"keys" binding:"required"`
Enabled *constant.Status `json:"enabled" label:"enabled" binding:"required"`
OfflineMonitoring *constant.Status `json:"offlineMonitoring" label:"offlineMonitoring" binding:"required"`
}
// Other
// @description: 其他配置
type Other struct {
Code string `json:"code" label:"code" binding:"required"`
Data string `json:"data" label:"data" binding:"required"`
Describe string `json:"describe" label:"describe" binding:"omitempty"`
}

View File

@ -6,6 +6,7 @@ import (
"gitee.ltd/lxh/logger/log"
"github.com/cowardmrx/go_aliyun_oss"
"github.com/fsnotify/fsnotify"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"github.com/go-resty/resty/v2"
"github.com/redis/go-redis/v9"
@ -15,6 +16,7 @@ import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
gl "gorm.io/gorm/logger"
"os"
"time"
"wireguard-ui/config"
"wireguard-ui/global/client"
@ -140,8 +142,14 @@ func initOSS() {
// initLogger
// @description: 初始化日志
func initLogger() {
mode := logger.Dev
if os.Getenv("GIN_MODE") == gin.ReleaseMode {
mode = logger.Prod
}
logger.InitLogger(logger.LogConfig{
Mode: logger.Dev,
Mode: mode,
FileEnable: true,
})
}

44
main.go
View File

@ -1,16 +1,15 @@
package main
import (
"fmt"
"gitee.ltd/lxh/logger/log"
"github.com/gin-contrib/pprof"
"github.com/spf13/cast"
"github.com/urfave/cli/v2"
"math/rand"
"net/http"
"os"
"sort"
"time"
"wireguard-ui/config"
"wireguard-ui/http/router"
tui "wireguard-ui/cli"
"wireguard-ui/cron"
"wireguard-ui/http"
"wireguard-ui/initialize"
"wireguard-ui/script"
)
@ -20,23 +19,38 @@ func init() {
if err := script.New().Do(); err != nil {
log.Errorf("执行脚本失败: %v", err.Error())
}
cron.Task()
}
func main() {
rand.New(rand.NewSource(time.Now().Local().UnixNano()))
router.Rooters()
handler := router.InitRouter()
if cast.ToBool(os.Getenv("ENABLED_PPROF")) {
pprof.Register(handler, "/monitoring")
app := &cli.App{
Name: "wireguard-ui",
Usage: "wireguard-manager-ui",
}
httpServe := http.Server{
Addr: fmt.Sprintf(":%d", config.Config.Http.Port),
Handler: handler,
app.Commands = []*cli.Command{
{
Name: "http:serve",
Aliases: []string{"app:serve"},
Usage: "",
Action: func(ctx *cli.Context) error {
return http.Kernel()
},
},
{
Name: "cmd:serve",
Aliases: []string{"command:serve"},
Usage: "use command exec",
Action: func(ctx *cli.Context) error {
return tui.Kernel()
},
},
}
if err := httpServe.ListenAndServe(); err != nil {
log.Panicf("启动http服务端失败: %v", err.Error())
sort.Sort(cli.CommandsByName(app.Commands))
if err := app.Run(os.Args); err != nil {
log.Fatalf("服务启动失败: %v", err.Error())
}
}

View File

@ -22,3 +22,16 @@ type Client struct {
func (Client) TableName() string {
return "t_client"
}
// Watcher
// @description: 监听日志
type Watcher struct {
Base
ClientId string `json:"clientId" gorm:"type:char(36);not null;comment:'客户端id'"`
NotifyResult string `json:"notifyResult" gorm:"type:text;default null;comment:'通知结果'"`
IsSend int `json:"isSend" gorm:"type:tinyint(1);default 0;comment:'是否已通知'"`
}
func (Watcher) TableName() string {
return "t_watcher"
}

View File

@ -45,7 +45,7 @@ func (script) GenerateConfig() error {
Email: client.Email,
PublicKey: client.Keys.PublicKey,
PresharedKey: client.Keys.PresharedKey,
AllowedIPS: client.AllowedIpsStr,
AllowedIPS: client.IpAllocationStr,
Endpoint: client.Endpoint,
CreateUser: client.CreateUser,
Enabled: cast.ToBool(client.Enabled),

View File

@ -3,7 +3,7 @@ package service
import "gorm.io/gorm"
func Paginate(current, size int64) func(db *gorm.DB) *gorm.DB {
// 如果页码是-1就不分页
// 如果页码是-1就不分页👋
if current == -1 {
return func(db *gorm.DB) *gorm.DB {
return db

View File

@ -2,11 +2,16 @@ package service
import (
"encoding/json"
"fmt"
slog "gitee.ltd/lxh/logger/log"
"gorm.io/gorm"
"strings"
gdb "wireguard-ui/global/client"
"wireguard-ui/http/param"
"wireguard-ui/http/vo"
"wireguard-ui/model"
"wireguard-ui/template/render_data"
"wireguard-ui/utils"
)
type setting struct{ *gorm.DB }
@ -81,6 +86,157 @@ func (s setting) GetWGServerForConfig() (data *render_data.Server, err error) {
// @return data
// @return err
func (s setting) GetAllSetting(blackList []string) (data []vo.SettingItem, err error) {
err = s.Model(&model.Setting{}).Select("code, data, describe").Where("code not in ?", blackList).Find(&data).Error
err = s.Model(&model.Setting{}).Select("code, data, describe,created_at,updated_at").Where("code not in ?", blackList).Find(&data).Error
return
}
// GetByCode
// @description: 获取指定code的配置
// @receiver s
// @param code
// @return data
// @return err
func (s setting) GetByCode(code string) (data *model.Setting, err error) {
err = s.Model(&model.Setting{}).Where("code = ?", code).Take(&data).Error
return
}
// Export
// @description: 导出
// @receiver s
// @return data
// @return err
func (s setting) Export() (data vo.Export, err error) {
// 先查询global配置
var gs, ss *model.Setting
if err = s.Model(&model.Setting{}).Where("code = ?", "WG_SETTING").Take(&gs).Error; err != nil {
return
}
if err = json.Unmarshal([]byte(gs.Data), &data.Global); err != nil {
return
}
// 查询server配置
if err = s.Model(&model.Setting{}).Where("code = ?", "WG_SERVER").Take(&ss).Error; err != nil {
return
}
if err = json.Unmarshal([]byte(ss.Data), &data.Server); err != nil {
return
}
// 查询client配置
var clients []vo.ClientItem
if err = s.Model(&model.Client{}).Select("id,name,email,ip_allocation as ip_allocation_str," +
"allowed_ips as allowed_ips_str,extra_allowed_ips as extra_allowed_ips_str," +
"endpoint,use_server_dns,keys as keys_str," +
"enabled,offline_monitoring").Order("created_at DESC").Find(&clients).Error; err != nil {
return
}
for i, v := range clients {
if v.KeysStr != "" {
_ = json.Unmarshal([]byte(v.KeysStr), &clients[i].Keys)
}
if v.IpAllocationStr != "" {
clients[i].IpAllocation = strings.Split(v.IpAllocationStr, ",")
}
if v.AllowedIpsStr != "" {
clients[i].AllowedIps = strings.Split(v.AllowedIpsStr, ",")
} else {
clients[i].AllowedIps = []string{}
}
if v.ExtraAllowedIpsStr != "" {
clients[i].ExtraAllowedIps = strings.Split(v.ExtraAllowedIpsStr, ",")
} else {
clients[i].ExtraAllowedIps = []string{}
}
}
// 查询其他配置
var others []vo.Other
if err = s.Model(&model.Setting{}).Where("code not in ?", []string{"WG_SETTING", "WG_SERVER"}).Find(&others).Error; err != nil {
return
}
data.Other = others
cj, _ := json.Marshal(clients)
_ = json.Unmarshal(cj, &data.Clients)
return
}
// Import
// @description: 导入
// @receiver s
// @param data
// @return err
func (s setting) Import(data *vo.Export, loginUser *vo.User) (err error) {
// 获取导入系统的公网IP地址
pubAddr := utils.Network().GetHostPublicIP()
data.Global.EndpointAddress = pubAddr
// 先更新global配置
gst, _ := json.Marshal(data.Global)
gs := &model.Setting{
Code: "WG_SETTING",
Data: string(gst),
Describe: "服务端全局配置",
}
if err = s.SetData(gs); err != nil {
return
}
st, _ := json.Marshal(data.Server)
ss := &model.Setting{
Code: "WG_SERVER",
Data: string(st),
Describe: "服务端配置",
}
if err = s.SetData(ss); err != nil {
return
}
// 更新client配置
for _, v := range data.Clients {
keys := &param.Keys{
PrivateKey: v.Keys.PrivateKey,
PublicKey: v.Keys.PublicKey,
PresharedKey: v.Keys.PresharedKey,
}
cc := param.SaveClient{
Name: v.Name,
Email: v.Email,
IpAllocation: v.IpAllocation,
AllowedIps: v.AllowedIps,
ExtraAllowedIps: v.ExtraAllowedIps,
UseServerDns: v.UseServerDns,
Keys: keys,
Enabled: v.Enabled,
OfflineMonitoring: v.OfflineMonitoring,
}
if v.Endpoint != "" {
port := strings.Split(v.Endpoint, ":")[1]
endpoint := fmt.Sprintf("%s:%s", pubAddr, port)
cc.Endpoint = endpoint
}
if err := Client().SaveClient(cc, loginUser); err != nil {
slog.Errorf("客户端[%s]导入失败: %v", v.Name, err)
continue
}
}
// 其他配置写入
for _, v := range data.Other {
if err = s.SetData(&model.Setting{
Code: v.Code,
Data: v.Data,
Describe: v.Describe,
}); err != nil {
slog.Errorf("其他配置[%s]导入失败: %v", v.Code, err)
continue
}
}
return nil
}

View File

@ -1,6 +1,7 @@
package service
import (
"errors"
"gorm.io/gorm"
gdb "wireguard-ui/global/client"
"wireguard-ui/global/constant"
@ -39,6 +40,11 @@ func (s user) CreateUser(user *model.User) (err error) {
return s.Model(&model.User{}).Where("id = ?", user.Id).Updates(&updates).Error
}
// 判断账号是否已经存在
if _, err = s.GetUserByAccount(user.Account); err == nil {
return errors.New("账号已经存在,请勿重复创建!")
}
defaultPassword := utils.Password().GenerateHashPassword("admin123")
if user.Password == "" { // 没有密码给一个默认密码
user.Password = defaultPassword

28
utils/file.go Normal file
View File

@ -0,0 +1,28 @@
package utils
import (
"fmt"
"os"
)
type fileUtils struct{}
func FileUtils() fileUtils {
return fileUtils{}
}
// GenerateFile
// @description: 生成文件
// @receiver fileUtils
// @param filename 文件名称
// @param content 文件内容
// @return error
func (fileUtils) GenerateFile(filename string, content []byte) (filepath string, err error) {
path := "/tmp/"
if os.Getenv("GIN_MODE") != "release" {
path = "E:\\Workspace\\Go\\wireguard-ui\\template\\tmp\\"
}
filepath = fmt.Sprintf("%s%s", path, filename)
err = os.WriteFile(filepath, content, 0777)
return filepath, err
}

View File

@ -2,28 +2,35 @@ package utils
import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"github.com/jordan-wright/email"
"github.com/spf13/cast"
"mime"
"net/smtp"
"net/textproto"
"path/filepath"
"wireguard-ui/config"
"wireguard-ui/model"
)
type mail struct {
*email.Email
addr string
auth smtp.Auth
conf map[string]string
}
func Mail() *mail {
func Mail(conf *model.Setting) *mail {
// 解析配置文件
var mailConf = make(map[string]string)
_ = json.Unmarshal([]byte(conf.Data), &mailConf)
var m mail
em := email.NewEmail()
m.Email = em
m.auth = smtp.PlainAuth("", config.Config.Mail.User, config.Config.Mail.Password, config.Config.Mail.Host)
m.addr = fmt.Sprintf("%s:%d", config.Config.Mail.Host, config.Config.Mail.Port)
m.auth = smtp.PlainAuth("", mailConf["user"], mailConf["password"], mailConf["host"])
m.addr = fmt.Sprintf("%s:%s", mailConf["host"], mailConf["port"])
m.conf = mailConf
return &m
}
@ -47,7 +54,7 @@ func (m *mail) VerifyConfig() (err error) {
// @param attacheFilePath
// @return err
func (m *mail) SendMail(to, subject, content, attacheFilePath string) (err error) {
m.From = fmt.Sprintf("wg-dashboard <%s>", config.Config.Mail.User)
m.From = fmt.Sprintf("wg-dashboard <%s>", m.conf["user"])
m.To = []string{to}
m.Subject = subject
m.Text = []byte(content)
@ -61,13 +68,13 @@ func (m *mail) SendMail(to, subject, content, attacheFilePath string) (err error
atch.Header = emHeader
}
if config.Config.Mail.SkipTls {
if cast.ToBool(m.conf["skipTls"]) {
return m.Send(m.addr, m.auth)
}
tlsConfig := &tls.Config{}
tlsConfig.InsecureSkipVerify = config.Config.Mail.SkipTls
tlsConfig.ServerName = config.Config.Mail.Host
tlsConfig.InsecureSkipVerify = cast.ToBool(m.conf["skipTls"])
tlsConfig.ServerName = m.conf["host"]
return m.SendWithTLS(m.addr, m.auth, tlsConfig)
}

65
utils/wechat_notify.go Normal file
View File

@ -0,0 +1,65 @@
package utils
import (
"encoding/json"
"errors"
"fmt"
"gitee.ltd/lxh/logger/log"
jsoniter "github.com/json-iterator/go"
"strings"
"wireguard-ui/global/client"
"wireguard-ui/model"
)
// wxid_472vas3av5ug22 /api/sendTextMsg
type wechatNotify struct {
Addr string
Path string
Method string
toUserId string
}
func WechatNotify(setting *model.Setting) wechatNotify {
var sm = make(map[string]string)
_ = json.Unmarshal([]byte(setting.Data), &sm)
return wechatNotify{
Addr: sm["addr"],
Path: sm["path"],
Method: sm["method"],
toUserId: sm["toUserWxId"],
}
}
// SendTextMessage
// @description: 发送文字通知
// @receiver website
// @param msg
// @return error
func (w wechatNotify) SendTextMessage(msg string) error {
log.Debugf("发送通知到微信: %v", msg)
req := client.HttpClient.R()
req.SetHeader("Content-Type", "application/json")
req.SetBody(map[string]string{
"wxid": w.toUserId,
"msg": msg,
})
req.URL = fmt.Sprintf("%s:%s", w.Addr, w.Path)
switch strings.ToUpper(w.Method) {
case "POST":
req.Method = "POST"
}
result, err := req.SetDebug(true).Send()
if err != nil {
return err
}
if jsoniter.Get(result.Body(), "code").ToInt() != 1 {
log.Errorf("发送通知到微信失败: %v", jsoniter.Get(result.Body(), "msg").ToString())
return errors.New("发送通知到微信失败")
}
return nil
}

45
web/.cz-config.js Normal file
View File

@ -0,0 +1,45 @@
module.exports = {
types: [
{ value: 'feat', name:'feat: 新增功能' },
{ value: 'fix', name:'fix: 修复bug' },
{ value: 'docs', name:'docs: 文档变更' },
{ value: 'style', name:'style: 代码格式(不影响功能,例如空格、分号等格式修正)' },
{ value: 'refactor', name:'refactor: 代码重构(不包括 bug 修复、功能新增)' },
{ value: 'perf', name:'perf: 性能优化' },
{ value: 'test', name:'test: 添加、修改测试用例' },
{ value: 'build', name:'build: 构建流程、外部依赖变更(如升级 npm 包、修改 脚手架 配置等)' },
{ value: 'ci', name:'ci: 修改 CI 配置、脚本' },
{ value: 'chore', name:'chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)' },
{ value: 'revert', name:'revert: 回滚 commit' },
{ value: 'wip', name:'wip: 开发中' },
{ value: 'mod', name:'mod: 不确定分类的修改' },
{ value: 'release', name:'release: 发布' },
],
scopes: [
['custom', '自定义'],
['projects', '项目搭建'],
['components', '组件相关'],
['utils', 'utils 相关'],
['styles', '样式相关'],
['deps', '项目依赖'],
['other', '其他修改'],
].map(([value, description]) => {
return {
value,
name: `${value.padEnd(30)} (${description})`
}
}),
messages: {
type: '确保本次提交遵循 Angular 规范!选择你要提交的类型:\n',
scope: '选择一个 scope可选',
customScope: '请输入自定义的 scope',
subject: '填写简短精炼的变更描述:',
body: '填写更加详细的变更描述(可选)。使用 "|" 换行:',
breaking: '列举非兼容性重大的变更(可选):',
footer: '列举出所有变更的 Issues Closed可选。 例如: #31, #34',
confirmCommit: '确认提交?'
},
allowBreakingChanges: ['feat', 'fix'],
subjectLimit: 100,
breaklineChar: '|'
}

19
web/.drone.yml Normal file
View File

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

5
web/.editorconfig Normal file
View File

@ -0,0 +1,5 @@
root = true
[*]
charset = utf-8
end_of_line = lf

3
web/.env Normal file
View File

@ -0,0 +1,3 @@
VITE_TITLE = 'Wireguard-UI'
VITE_PORT = 3100

13
web/.env.development Normal file
View File

@ -0,0 +1,13 @@
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/'
VITE_USE_HASH = true
# 是否启用MOCK
VITE_USE_MOCK = true
# 是否启用MOCK
VITE_USE_PROXY = true
# base api
VITE_BASE_API = '/api'

13
web/.env.github Normal file
View File

@ -0,0 +1,13 @@
# 自定义域名CNAME
# VITE_CNAME = 'template.isme.top'
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/vue-naive-admin/'
VITE_USE_HASH = true
# 是否启用MOCK
VITE_USE_MOCK = true
# base api
VITE_BASE_API = '/api'

16
web/.env.production Normal file
View File

@ -0,0 +1,16 @@
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/web'
VITE_USE_HASH = true
# 是否启用MOCK
VITE_USE_MOCK = false
# base api
VITE_BASE_API = '/api'
# 是否启用压缩
VITE_USE_COMPRESS = false
# 压缩类型
VITE_COMPRESS_TYPE = gzip

View File

@ -0,0 +1,62 @@
{
"globals": {
"$loadingBar": true,
"$message": true,
"defineOptions": true,
"$dialog": true,
"$notification": true,
"EffectScope": true,
"computed": true,
"createApp": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"effectScope": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onUnmounted": true,
"onUpdated": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useRoute": true,
"useRouter": true,
"useSlots": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true
}
}

4
web/.eslintignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
dist
public
package.json

36
web/.husky/_/husky.sh Normal file
View File

@ -0,0 +1,36 @@
#!/usr/bin/env sh
if [ -z "$husky_skip_init" ]; then
debug () {
if [ "$HUSKY_DEBUG" = "1" ]; then
echo "husky (debug) - $1"
fi
}
readonly hook_name="$(basename -- "$0")"
debug "starting $hook_name..."
if [ "$HUSKY" = "0" ]; then
debug "HUSKY env variable is set to 0, skipping hook"
exit 0
fi
if [ -f ~/.huskyrc ]; then
debug "sourcing ~/.huskyrc"
. ~/.huskyrc
fi
readonly husky_skip_init=1
export husky_skip_init
sh -e "$0" "$@"
exitCode="$?"
if [ $exitCode != 0 ]; then
echo "husky - $hook_name hook exited with code $exitCode (error)"
fi
if [ $exitCode = 127 ]; then
echo "husky - command not found in PATH=$PATH"
fi
exit $exitCode
fi

4
web/.husky/commit-msg Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install commitlint --edit "$1"

4
web/.husky/pre-commit Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint:staged

3
web/.prettierignore Normal file
View File

@ -0,0 +1,3 @@
/node_modules/**
/dist/*
/public/*

7
web/.prettierrc.json Normal file
View File

@ -0,0 +1,7 @@
{
"printWidth": 100,
"singleQuote": true,
"semi": false,
"endOfLine": "lf",
"htmlWhitespaceSensitivity": "ignore"
}

22
web/Dockerfile Normal file
View File

@ -0,0 +1,22 @@
# 打包前端
FROM node:18-alpine as build-stage
WORKDIR front
COPY . .
WORKDIR web
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 run build
# 前端集成打包
FROM nginx:alpine
WORKDIR /data
# 替换掉Nginx默认配置
RUN rm /etc/nginx/conf.d/default.conf
COPY --from=build-stage /front/docker/web.conf /etc/nginx/conf.d/default.conf
COPY --from=build-stage /front/dist/ ./
# 复制编译后的文件到容器内
RUN /bin/sh -c 'echo init ok'

21
web/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Ronnie Zhang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

208
web/README.EN.md Normal file
View File

@ -0,0 +1,208 @@
<p align="center">
<a href="https://github.com/zclzone/vue-naive-admin">
<img alt="Vue Naive Admin Logo" width="200" src="./src/assets/images/logo.png">
</a>
</p>
<p align="center">
<a href="./LICENSE"><img allt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
</p>
<p align='center'>
<b>英文</b> |
<a href="https://github.com/zclzone/vue-naive-admin/blob/main/README.md">中文</a>
</p>
### Introduction
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin) is a **completely open source free and commercially allowed ** admin templateBased on the latest technology stack of front-end such as `Vue3、Vite3、Pinia、Unocss and Naive UI`. Compared with other more popular backend management templates, this project is more concise, lightweight, fresh style, very low learning costs, ideal for small and medium-sized projects or personal projects.
### Features
- 🍒 Integrated [Naive UI](https://www.naiveui.com)recommended by Evan You.
- 🍑 Integrated login, logout and permission verification.
- 🍐 Integrated multi-environment configuration, dev, test, production and github pages environments.
- 🍎 Integrated `eslint + prettier`.
- 🍌 Integrated `husky + commitlint`.
- 🍉 Integrated `Mock`.
- 🍍 Integrated `pinia`lightweight, simple and easy to use alternative to vuex.
- 📦 Integrated `unplugin` auto import.
- 🤹 Integrated `iconify` iconsupport custom svg icons.
- 🍇 Integrated `unocss`.
### Preview
[https://template.isme.top](https://template.isme.top)
[https://base.isme.top](https://base.isme.top)
### Docs
[Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs)
### Getting Started
```shell
# Recommended setup git autocrlf 为 false
git config --global core.autocrlf false
# Clone Project
git clone https://github.com/zclzone/vue-naive-admin.git
cd vue-naive-admin
# Install dependencies(Recommended use pnpm: https://pnpm.io/zh/installation)
npm i -g pnpm # Installed and can be ignored
pnpm i # or npm i
# Start
pnpm dev
```
### Build and Release
```shell
# Test Environment
pnpm build:test
# Github Environment
pnpm build:github
# Prod Environment
pnpm build
```
### Other
```shell
# eslint check
pnpm lint
# eslint check and fix
pnpm lint:fix
# PreviewNeed to build first
pnpm preview
# Commithusky+commitlint
pnpm cz
```
### Directory description
```
Vue Naive Admin
|-- .github // github相关如推送github仓库后自动部署gh pages
|-- .husky // git commit钩子
|-- .vscode // vscode编辑器相关
| |-- extensions.json // 插件推荐
| |-- settings.json // 项目级别的vscode配置优先级大于全局vscode配置
|-- build // 构建相关配置
| |-- constant.js // 构建相关的常量
| |-- utils.js // 构建相关的工具方法
| |-- config
| | |-- define.js // 注入全局常量启动或打包后将添加到window中
| | |-- proxy.js // 代理配置
| |-- plugin
| | |-- html.js // vite-plugin-html插件用于注入变量或者html标签
| | |-- mock.js // vite-plugin-mock插件处理mock
| | |-- unplugin.js // unplugin相关插件包含DefineOptions和自动导入
| |-- script // 打包完成后执行的一些node脚本不重要
| |-- build-cname.js // 自动生成cname
|-- mock // mock
| |-- utils.js // mock请求需要用到的工具方法
| |-- api // mock接口
|-- public // 公共资源文件夹下的文件会在打包后会直接加到dist根目录下
|-- settings // 项目配置
| |-- proxy-config.js // 代理配置文件
| |-- theme.json // 主题配置项,主题色等
|-- src
| |-- api // 公共api
| |-- assets // 静态资源
| | |-- images // 图片
| | |-- svg // svg图标
| |-- components // 全局组件
| | |-- common // 公共组件
| | |-- icon // icon相关组件
| | |-- page // 页面组件
| | |-- query-bar // 查询选项
| | |-- table // 封装的表格组件
| |-- composables // 封装的组合式函数
| |-- layout // 布局相关组件
| | |-- components
| | |-- AppMain.vue // 主体内容
| | |-- header // 头部
| | |-- sidebar // 侧边菜单栏
| | |-- tags // 多页签栏
| |-- router // 路由
| | |-- guard // 路由守卫
| | |-- routes // 路由列表
| |-- store // 状态管理pinia
| | |-- modules // 模块
| | |-- app // 管理页面重新加载、折叠菜单栏和keepAlive等
| | |-- permission // 权限相关,管理权限菜单
| | |-- tags // 管理多页签
| | |-- user // 用户模块,管理用户信息、登录登出
| |-- styles // 样式
| |-- utils // 封装的工具方法
| | |-- auth // 权限相关如token、跳转登录页等
| | |-- common // 通用
| | |-- http // 封装axios
| | |-- storage // 封装localStorage和sessionStorage
| |-- views // 页面
| | |-- demo // 示例
| | |-- error-page // 错误页
| | |-- login // 登录页
| | |-- workbench // 首页
| |-- App.vue
| |-- main.js
|-- .cz-config.js // git提交配置
|-- .editorconfig // 编辑器配置
|-- .env // 环境文件,所有环境都会载入
|-- .env.development // 开发环境文件
|-- .env.production // 生产环境文件
|-- .env.test // 测试环境文件
|-- .eslintignore // eslint忽略
|-- .eslintrc.js // eslint配置
|-- .gitignore // git忽略
|-- .prettierignore // prettier格式化忽略
|-- commitlint.config.js // commitlint规范配置
|-- index.html
|-- jsconfig.json // js配置
|-- LICENSE // 协议
|-- package.json // 依赖描述文件
|-- pnpm-lock.yaml // 依赖锁定文件
|-- prettier.config.js // prettier格式化配置
|-- README.md // 项目描述文档(英文)
|-- README.zh-CN.md // 项目描述文档(中文)
|-- unocss.config.js // unocss配置
|-- vite.config.js // vite配置
```
### TS version: Qs Admin
#### source code
- github: [https://github.com/zclzone/qs-admin](https://github.com/zclzone/qs-admin)
- gitee: [https://gitee.com/zclzone/qs-admin-ts](https://gitee.com/zclzone/qs-admin-ts)
#### preview
- [https://admin.isme.top](https://admin.isme.top)
- [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin)
### Open source projects that use this project:
- [gin-vue-blog](https://github.com/szluyu99/gin-vue-blog): A full-stack blog project in Golang, the frontend of the blog backend is based on vue-naive-admin and integrates with a real backend service, implementing features such as backend-controlled routing.
- [vue-fastapi-admin](https://github.com/mizhexiaoxiao/vue-fastapi-admin): A Python backend management project that integrates RBAC permission management, dynamic routing, and JWT authentication, helping small and medium-sized applications to quickly establish a foundation.
### Communication group & About the author
<a href="https://blog.isme.top/about/">
<img src="https://static.isme.top/images/about.png?t=123" style="max-width: 400px" />
</a>

229
web/README.md Normal file
View File

@ -0,0 +1,229 @@
<p align="center">
<a href="https://github.com/zclzone/vue-naive-admin">
<img alt="Vue Naive Admin Logo" width="200" src="./src/assets/images/logo.png">
</a>
</p>
<p align="center">
<a href="./LICENSE"><img alt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
</p>
<p align='center'>
<b>中文</b> |
<a href="https://github.com/zclzone/vue-naive-admin/blob/main/README.EN.md">English</a>
</p>
> 🎉🎉🎉 2.0 已开源,全新重构,全面简化,后端使用 nestjs + mysql + typeOrm[👉点击前往2.0版本 | 分支 2.x](https://github.com/zclzone/vue-naive-admin/tree/2.x),
- 体验地址: [admin.isme.top](https://admin.isme.top)
- 后端服务: [isme-nest-serve](https://github.com/zclzone/isme-nest-serve)
- 文档: [vue-naive-admin-docs](https://docs.isme.top/web/#/624306705)
### 简介
[Vue Naive Admin](https://github.com/zclzone/vue-naive-admin) 是一个 **开源免费且允许商用** 的后台管理模板,基于 `Vue3、Vite4、Pinia、Unocss 和 Naive UI` 等前端最新技术栈。相较于其他比较流行的后台管理模板,此项目更加简洁、轻量,风格清新,上手成本非常低,非常适合中小型项目或者个人项目。
### 功能
- 🍒 集成 [Naive UI](https://www.naiveui.com)
- 🍑 集成登陆、注销及权限验证
- 🍐 集成多环境配置dev、测试、生产环境
- 🍎 集成 `eslint + prettier`,代码约束和格式化统一
- 🍌 集成 `husky + commitlint`,代码提交规范化
- 🍉 集成 `mock` 接口服务dev 环境和发布环境都支持,可动态配置是否启用 mock 服务,不启用时不会加载 mock 包,减少打包体积
- 🍍 集成 `pinia`vuex 的替代方案,轻量、简单、易用
- 📦 集成 `unplugin` 插件,自动导入,解放双手,开发效率直接起飞
- 🤹 集成 `iconify` 图标,支持自定义 svg 图标, 优雅使用icon
- 🍇 集成 `unocss`antfu 开源的原子 css 解决方案,非常轻量
> ✨✨ 双十一香港特惠服务器推荐,~~**2C4G 100M** `71/年` `142/两年`~~[👉点击前往](https://blog.isme.top/vps-recommend/)
### 预览
[https://template.isme.top](https://template.isme.top)
[https://base.isme.top](https://base.isme.top)
### 文档
项目文档: [Vue Naive Admin Docs](https://zclzone.github.io/vue-naive-admin-docs)
从0到1搭建后台: [从0到1带你搭建Vite+Vue3+Pinia+Naive UI后台](https://juejin.cn/column/7093180796424421384)
如何安装pnpm: [安装pnpm](docs/安装pnpm.md)
如何使用图标: [使用图标](docs/使用图标.md)
如何使用unocss: [保熟的UnoCSS使用指北优雅使用antfu大佬的原子化CSS](https://juejin.cn/post/7142466784971456548)
### 快速开始
```shell
# 推荐配置git autocrlf 为 false本项目规范使用lf换行符此配置是为防止git自动将源文件转换为crlf
# 不清楚为什么要这样做的请参考这篇文章https://www.freesion.com/article/4532642129
git config --global core.autocrlf false
# 克隆项目
git clone https://github.com/zclzone/vue-naive-admin.git
# 进入项目目录
cd vue-naive-admin
# 安装依赖(建议使用pnpm: https://pnpm.io/zh/installation)
npm i -g pnpm # 装了可忽略
pnpm i # 或者 npm i
# 启动
pnpm dev
```
### 构建发布
```shell
# 构建测试环境
pnpm build:test
# 构建github pages环境
pnpm build:github
# 构建生产环境
pnpm build
```
### 其他指令
```shell
# eslint代码格式检查
pnpm lint
# 代码检查并修复
pnpm lint:fix
# 预览发布包效果(需先执行构建指令)
pnpm preview
# 提交代码husky+commitlint
pnpm cz
```
### 目录说明
```
Vue Naive Admin
|-- .github // github相关如推送github仓库后自动部署gh pages
|-- .husky // git commit钩子
|-- .vscode // vscode编辑器相关
| |-- extensions.json // 插件推荐
| |-- settings.json // 项目级别的vscode配置优先级大于全局vscode配置
|-- build // 构建相关配置
| |-- constant.js // 构建相关的常量
| |-- utils.js // 构建相关的工具方法
| |-- config
| | |-- define.js // 注入全局常量启动或打包后将添加到window中
| | |-- proxy.js // 代理配置
| |-- plugin
| | |-- html.js // vite-plugin-html插件用于注入变量或者html标签
| | |-- mock.js // vite-plugin-mock插件处理mock
| | |-- unplugin.js // unplugin相关插件包含DefineOptions和自动导入
| |-- script // 打包完成后执行的一些node脚本不重要
| |-- build-cname.js // 自动生成cname
|-- mock // mock
| |-- utils.js // mock请求需要用到的工具方法
| |-- api // mock接口
|-- public // 公共资源文件夹下的文件会在打包后会直接加到dist根目录下
|-- settings // 项目配置
| |-- proxy-config.js // 代理配置文件
| |-- theme.json // 主题配置项,主题色等
|-- src
| |-- api // 公共api
| |-- assets // 静态资源
| | |-- images // 图片
| | |-- svg // svg图标
| |-- components // 全局组件
| | |-- common // 公共组件
| | |-- icon // icon相关组件
| | |-- page // 页面组件
| | |-- query-bar // 查询选项
| | |-- table // 封装的表格组件
| |-- composables // 封装的组合式函数
| |-- layout // 布局相关组件
| | |-- components
| | |-- AppMain.vue // 主体内容
| | |-- header // 头部
| | |-- sidebar // 侧边菜单栏
| | |-- tags // 多页签栏
| |-- router // 路由
| | |-- guard // 路由守卫
| | |-- routes // 路由列表
| |-- store // 状态管理pinia
| | |-- modules // 模块
| | |-- app // 管理页面重新加载、折叠菜单栏和keepAlive等
| | |-- permission // 权限相关,管理权限菜单
| | |-- tags // 管理多页签
| | |-- user // 用户模块,管理用户信息、登录登出
| |-- styles // 样式
| |-- utils // 封装的工具方法
| | |-- auth // 权限相关如token、跳转登录页等
| | |-- common // 通用
| | |-- http // 封装axios
| | |-- storage // 封装localStorage和sessionStorage
| |-- views // 页面
| | |-- demo // 示例
| | |-- error-page // 错误页
| | |-- login // 登录页
| | |-- workbench // 首页
| |-- App.vue
| |-- main.js
|-- .cz-config.js // git提交配置
|-- .editorconfig // 编辑器配置
|-- .env // 环境文件,所有环境都会载入
|-- .env.development // 开发环境文件
|-- .env.production // 生产环境文件
|-- .env.test // 测试环境文件
|-- .eslintignore // eslint忽略
|-- .eslintrc.js // eslint配置
|-- .gitignore // git忽略
|-- .prettierignore // prettier格式化忽略
|-- commitlint.config.js // commitlint规范配置
|-- index.html
|-- jsconfig.json // js配置
|-- LICENSE // 协议
|-- package.json // 依赖描述文件
|-- pnpm-lock.yaml // 依赖锁定文件
|-- prettier.config.js // prettier格式化配置
|-- README.md // 项目描述文档(英文)
|-- README.zh-CN.md // 项目描述文档(中文)
|-- unocss.config.js // unocss配置
|-- vite.config.js // vite配置
```
### TS 版本: Qs Admin
#### 源码
- github: [https://github.com/zclzone/qs-admin](https://github.com/zclzone/qs-admin)
- gitee: [https://gitee.com/zclzone/qs-admin-ts](https://gitee.com/zclzone/qs-admin-ts)
#### 预览
- [https://admin.isme.top](https://admin.isme.top)
- [https://zclzone.github.io/qs-admin](https://zclzone.github.io/qs-admin)
### 使用该项目的开源项目
- [gin-vue-blog](https://github.com/szluyu99/gin-vue-blog): Golang 全栈博客项目, 博客后台的前端基于 vue-naive-admin对接真实后端服务实现了后端控制路由等特性。
- [vue-fastapi-admin](https://github.com/mizhexiaoxiao/vue-fastapi-admin): Python 后台管理项目, 融合了 RBAC 权限管理、动态路由JWT 鉴权,助力中小型应用快速搭建。
### 入群交流 & 关于作者
<a href="https://blog.isme.top/about/">
<img src="https://static.isme.top/images/about.png?t=123" style="max-width: 400px" />
</a>
### ☕ 赞助我
> 开源不易,请作者喝杯咖啡吧
<p>
<img src="https://static.isme.top/images/zhifu_weixin.jpg" style="width: 220px" />
<img src="https://static.isme.top/images/zhifu_zhifubao.jpg" style="width: 220px" />
</p>

33
web/build/constant.js Normal file
View File

@ -0,0 +1,33 @@
export const OUTPUT_DIR = 'dist'
export const PROXY_CONFIG = {
/**
* @desc 替换匹配值
* @请求路径 http://localhost:3100/api/user
* @转发路径 http://localhost:8080/user
*/
'/api': {
target: 'http://localhost:6687/api',
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp('^/api'), ''),
},
/**
* @desc 不替换匹配值
* @请求路径 http://localhost:3100/api/v2/user
* @转发路径 http://localhost:8080/api/v2/user
*/
'/api/v2': {
target: 'http://localhost:8080',
changeOrigin: true,
},
/**
* @desc 替换部分匹配值
* @请求路径 http://localhost:3100/api/v3/user
* @转发路径 http://localhost:8080/user
*/
'/api/v3': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp('^/api'), ''),
},
}

15
web/build/plugin/html.js Normal file
View File

@ -0,0 +1,15 @@
import { createHtmlPlugin } from 'vite-plugin-html'
export function configHtmlPlugin(viteEnv, isBuild) {
const { VITE_TITLE } = viteEnv
const htmlPlugin = createHtmlPlugin({
minify: isBuild,
inject: {
data: {
title: VITE_TITLE,
},
},
})
return htmlPlugin
}

42
web/build/plugin/index.js Normal file
View File

@ -0,0 +1,42 @@
import vue from '@vitejs/plugin-vue'
/**
* * unocss插件原子css
* https://github.com/antfu/unocss
*/
import Unocss from 'unocss/vite'
// rollup打包分析插件
import visualizer from 'rollup-plugin-visualizer'
// 压缩
import viteCompression from 'vite-plugin-compression'
// vite-vuedevtool
import VueDevTools from 'vite-plugin-vue-devtools'
import { configHtmlPlugin } from './html'
import { configMockPlugin } from './mock'
import unplugin from './unplugin'
export function createVitePlugins(viteEnv, isBuild) {
const plugins = [VueDevTools(), vue(), ...unplugin, configHtmlPlugin(viteEnv, isBuild), Unocss()]
if (viteEnv?.VITE_USE_MOCK) {
plugins.push(configMockPlugin(isBuild))
}
if (viteEnv.VITE_USE_COMPRESS) {
plugins.push(viteCompression({ algorithm: viteEnv.VITE_COMPRESS_TYPE || 'gzip' }))
}
if (isBuild) {
plugins.push(
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
})
)
}
return plugins
}

13
web/build/plugin/mock.js Normal file
View File

@ -0,0 +1,13 @@
import { viteMockServe } from 'vite-plugin-mock'
export function configMockPlugin(isBuild) {
return viteMockServe({
mockPath: 'mock/api',
localEnabled: !isBuild,
prodEnabled: isBuild,
injectCode: `
import { setupProdMockServer } from '../mock';
setupProdMockServer();
`,
})
}

View File

@ -0,0 +1,46 @@
import { resolve } from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
import IconsResolver from 'unplugin-icons/resolver'
/**
* * unplugin-icons插件自动引入iconify图标
* usage: https://github.com/antfu/unplugin-icons
* 图标库: https://icones.js.org/
*/
import Icons from 'unplugin-icons/vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import { getSrcPath } from '../utils'
const customIconPath = resolve(getSrcPath(), 'assets/svg')
export default [
AutoImport({
imports: ['vue', 'vue-router'],
dts: false,
}),
Icons({
compiler: 'vue3',
customCollections: {
custom: FileSystemIconLoader(customIconPath),
},
scale: 1,
defaultClass: 'inline-block',
}),
Components({
resolvers: [
NaiveUiResolver(),
IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' }),
],
dts: false,
}),
createSvgIconsPlugin({
iconDirs: [customIconPath],
symbolId: 'icon-custom-[dir]-[name]',
inject: 'body-last',
customDomId: '__CUSTOM_SVG_ICON__',
}),
]

View File

@ -0,0 +1,15 @@
import { resolve } from 'path'
import chalk from 'chalk'
import { writeFileSync } from 'fs-extra'
import { OUTPUT_DIR } from '../constant'
import { getEnvConfig, getRootPath } from '../utils'
export function runBuildCNAME() {
const { VITE_CNAME } = getEnvConfig()
if (!VITE_CNAME) return
try {
writeFileSync(resolve(getRootPath(), `${OUTPUT_DIR}/CNAME`), VITE_CNAME)
} catch (error) {
console.log(chalk.red('CNAME file failed to package:\n' + error))
}
}

14
web/build/script/index.js Normal file
View File

@ -0,0 +1,14 @@
import chalk from 'chalk'
import { runBuildCNAME } from './build-cname'
export const runBuild = async () => {
try {
runBuildCNAME()
console.log(`${chalk.cyan('build successfully!')}`)
} catch (error) {
console.log(chalk.red('vite build error:\n' + error))
process.exit(1)
}
}
runBuild()

70
web/build/utils.js Normal file
View File

@ -0,0 +1,70 @@
import fs from 'fs'
import path from 'path'
import dotenv from 'dotenv'
/**
* * 项目根路径
* @description 结尾不带/
*/
export function getRootPath() {
return path.resolve(process.cwd())
}
/**
* * 项目src路径
* @param srcName src目录名称(默认: "src")
* @description 结尾不带斜杠
*/
export function getSrcPath(srcName = 'src') {
return path.resolve(getRootPath(), srcName)
}
export function convertEnv(envOptions) {
const result = {}
if (!envOptions) return result
for (const envKey in envOptions) {
let envVal = envOptions[envKey]
if (['true', 'false'].includes(envVal)) envVal = envVal === 'true'
if (['VITE_PORT'].includes(envKey)) envVal = +envVal
result[envKey] = envVal
}
return result
}
/**
* 获取当前环境下生效的配置文件名
*/
function getConfFiles() {
const script = process.env.npm_lifecycle_script
const reg = new RegExp('--mode ([a-z_\\d]+)')
const result = reg.exec(script)
if (result) {
const mode = result[1]
return ['.env', '.env.local', `.env.${mode}`]
}
return ['.env', '.env.local', '.env.production']
}
export function getEnvConfig(match = 'VITE_', confFiles = getConfFiles()) {
let envConfig = {}
confFiles.forEach((item) => {
try {
if (fs.existsSync(path.resolve(process.cwd(), item))) {
const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item)))
envConfig = { ...envConfig, ...env }
}
} catch (e) {
console.error(`Error in parsing ${item}`, e)
}
})
const reg = new RegExp(`^(${match})`)
Object.keys(envConfig).forEach((key) => {
if (!reg.test(key)) {
Reflect.deleteProperty(envConfig, key)
}
})
return envConfig
}

26
web/commitlint.config.js Normal file
View File

@ -0,0 +1,26 @@
module.exports = {
ignores: [(commit) => commit.includes('init')],
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat',
'fix',
'docs',
'style',
'refactor',
'perf',
'test',
'build',
'ci',
'chore',
'revert',
'wip',
'mod',
'release',
],
],
},
}

26
web/docker/web.conf Normal file
View File

@ -0,0 +1,26 @@
server {
listen 80;
server_name localhost;
proxy_send_timeout 600s; # 设置发送超时时间,
proxy_read_timeout 600s; # 设置读取超时时间。
# 前端打包好的dist目录文件
root /data/;
location / {
try_files $uri $uri/ /index.html;
}
# 若新增后端路由前缀注意在此处添加(|新增)
location ^~ /api/ {
proxy_pass http://wireguard-srv:6687;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme;
}
}

3
web/docs/使用unocss.md Normal file
View File

@ -0,0 +1,3 @@
推荐阅读作者在掘金的文章:
[保熟的UnoCSS使用指北优雅使用antfu大佬的原子化CSS](https://juejin.cn/post/7142466784971456548)

40
web/docs/使用图标.md Normal file
View File

@ -0,0 +1,40 @@
## 使用 iconify 图标
首先去图标库地址:[icones](https://icones.js.org/) 找合适的图标
### 1. 结合 unocss 使用
```html
<i i-carbon-sun />
<i class="i-carbon-sun" />
```
### 2. 结合插件 unplugin-icons 自定义标签使用
`<icon-[iconify图标名称]`
```html
<icon-ant-design:fullscreen-exit-outlined />
<icon-ant-design:fullscreen-outlined />
```
这种方式还支持自定义 svg 图标,本项目自定义 svg 图标固定放在 src/assets/svg 下
`<icon-custom-[svg图标文件名]`
```
<icon-custom-logo />
```
具体配置参看 build/plugin/unplugin.js
### 3. 结合 Naive UI 的 NIcon 组件封装使用
```html
<!-- iconify图标 -->
<TheIcon icon="material-symbols:delete-outline" />
<!-- 自定义svg图标 -->
<TheIcon icon="logo" type="custom" />
```
封装组件参看 src/components/icon

32
web/docs/安装pnpm.md Normal file
View File

@ -0,0 +1,32 @@
## 安装pnpm
### 使用Corepack安装推荐
从 v16.13 开始Node.js 发布了 Corepack 来管理包管理器。 这是一项实验性功能,需要通过运行如下脚本来启用它:
```
npx corepack enable // 可能需要管理员权限
```
这将自动在您的系统上安装 pnpm。 但是,它可能不是最新版本的 pnpm。 若要升级,请检查[最新的 pnpm 版本](https://github.com/pnpm/pnpm/releases/latest) 并运行,如 7.14.0
```
corepack prepare pnpm@7.14.0 --activate
```
如果是 Node.js v16.17 或者更新的版本,可以直接安装最新版本的 pnpm
```
corepack prepare pnpm@latest --activate
```
### 使用npm安装
```
npm i -g pnpm
```
更新,卸了重新装
```
npm uninstall -g pnpm
npm i -g pnpm
```

35
web/index.html Normal file
View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="cn">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Expires" content="0" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Cache-control" content="no-cache" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.png" />
<link rel="stylesheet" href="/resource/loading.css" />
<title><%= title %></title>
</head>
<body>
<div id="app">
<!-- 白屏时的loading效果 -->
<div class="loading-container">
<img src="/resource/logo.png" alt="logo" height="128" />
<div class="loading-spin__container">
<div class="loading-spin">
<div class="left-0 top-0 loading-spin-item"></div>
<div class="left-0 bottom-0 loading-spin-item loading-delay-500"></div>
<div class="right-0 top-0 loading-spin-item loading-delay-1000"></div>
<div class="right-0 bottom-0 loading-spin-item loading-delay-1500"></div>
</div>
</div>
<div class="loading-title"><%= title %></div>
</div>
<script src="/resource/loading.js"></script>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

14
web/jsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ESNext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": ["src/*"],
"~/*": ["./*"]
},
"jsx": "preserve",
"allowJs": true
},
"exclude": ["node_modules", "dist"]
}

40
web/mock/api/auth.js Normal file
View File

@ -0,0 +1,40 @@
import { resolveToken } from '../utils'
const token = {
admin: 'admin',
editor: 'editor',
}
export default [
{
url: '/api/auth/login',
method: 'post',
response: ({ body }) => {
if (['admin', 'editor'].includes(body?.name)) {
return {
code: 0,
data: {
token: token[body.name],
},
}
} else {
return {
code: -1,
message: '没有此用户',
}
}
},
},
{
url: '/api/auth/refreshToken',
method: 'post',
response: ({ headers }) => {
return {
code: 0,
data: {
token: resolveToken(headers?.authorization),
},
}
},
},
]

5
web/mock/api/index.js Normal file
View File

@ -0,0 +1,5 @@
import auth from './auth'
import user from './user'
import post from './post'
export default [...auth, ...user, ...post]

138
web/mock/api/post.js Normal file
View File

@ -0,0 +1,138 @@
const posts = [
{
title: '使用纯css优雅配置移动端rem布局',
author: '大脸怪',
category: 'Css',
description: '通常配置rem布局会使用js进行处理比如750的设计稿会这样...',
content: '通常配置rem布局会使用js进行处理比如750的设计稿会这样',
isRecommend: true,
isPublish: true,
createDate: '2021-11-04T04:03:36.000Z',
updateDate: '2021-11-04T04:03:36.000Z',
},
{
title: 'Vue2&Vue3项目风格指南',
author: 'Ronnie',
category: 'Vue',
description: '总结的Vue2和Vue3的项目风格',
content: '### 1. 命名风格\n\n> 文件夹如果是由多个单词组成,应该始终是横线连接 ',
isRecommend: true,
isPublish: true,
createDate: '2021-10-25T08:57:47.000Z',
updateDate: '2022-02-28T04:02:39.000Z',
},
{
title: '如何优雅的给图片添加水印',
author: '大脸怪',
category: 'JavaScript',
description: '优雅的给图片添加水印',
content: '我之前写过一篇文章记录了一次上传图片的优化史',
isRecommend: true,
isPublish: true,
createDate: '2021-06-24T18:46:19.000Z',
updateDate: '2021-09-23T07:51:22.000Z',
},
{
title: '前端缓存的理解',
author: '大脸怪',
category: 'Http',
description: '谈谈前端缓存的理解',
content:
'> 背景\n\n公司有个vue-cli3移动端web项目发版更新后发现部分用户手机在钉钉内置浏览器打开出现了缓存',
isRecommend: true,
isPublish: true,
createDate: '2021-06-10T18:51:19.000Z',
updateDate: '2021-09-17T09:33:24.000Z',
},
{
title: 'Promise的五个静态方法',
author: '大脸怪',
category: 'JavaScript',
description: '简单介绍下在 Promise 类中有5 种静态方法及它们的使用场景',
content:
'## 1. Promise.all\n\n并行执行多个 promise并等待所有 promise 都准备就绪。再对它们进行处理。',
isRecommend: true,
isPublish: true,
createDate: '2021-02-22T22:37:06.000Z',
updateDate: '2021-09-17T09:33:24.000Z',
},
]
export default [
{
url: '/api/posts',
method: 'get',
response: (data = {}) => {
const { title, pageNo, pageSize } = data.query
let pageData = []
let total = 60
const filterData = posts.filter(
(item) => item.title.includes(title) || (!title && title !== 0)
)
if (filterData.length) {
if (pageSize) {
while (pageData.length < pageSize) {
pageData.push(filterData[Math.round(Math.random() * (filterData.length - 1))])
}
} else {
pageData = filterData
}
pageData = pageData.map((item, index) => ({
id: pageSize * (pageNo - 1) + index + 1,
...item,
}))
} else {
total = 0
}
return {
code: 0,
message: 'ok',
data: {
pageData,
total,
pageNo,
pageSize,
},
}
},
},
{
url: '/api/post',
method: 'post',
response: ({ body }) => {
return {
code: 0,
message: 'ok',
data: body,
}
},
},
{
url: '/api/post/:id',
method: 'put',
response: ({ query, body }) => {
return {
code: 0,
message: 'ok',
data: {
id: query.id,
body,
},
}
},
},
{
url: '/api/post/:id',
method: 'delete',
response: ({ query }) => {
return {
code: 0,
message: 'ok',
data: {
id: query.id,
},
}
},
},
]

39
web/mock/api/user.js Normal file
View File

@ -0,0 +1,39 @@
import { resolveToken } from '../utils'
const users = {
admin: {
id: 1,
name: '大脸怪(admin)',
avatar: 'https://static.isme.top/images/avatar.jpg',
email: 'Ronnie@123.com',
role: ['admin'],
},
editor: {
id: 2,
name: '大脸怪(editor)',
avatar: 'https://static.isme.top/images/avatar.jpg',
email: 'Ronnie@123.com',
role: ['editor'],
},
guest: {
id: 3,
name: '访客(guest)',
avatar: 'https://static.isme.top/images/avatar.jpg',
role: [],
},
}
export default [
{
url: '/api/user',
method: 'get',
response: ({ headers }) => {
const token = resolveToken(headers?.authorization)
return {
code: 0,
data: {
...(users[token] || users.guest),
},
}
},
},
]

6
web/mock/index.js Normal file
View File

@ -0,0 +1,6 @@
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
import api from './api'
export function setupProdMockServer() {
createProdMockServer(api)
}

12
web/mock/utils.js Normal file
View File

@ -0,0 +1,12 @@
export function resolveToken(authorization) {
/**
* * jwt token
* * Bearer + token
* ! 认证方案: Bearer
*/
const reqTokenSplit = authorization.split(' ')
if (reqTokenSplit.length === 2) {
return reqTokenSplit[1]
}
return ''
}

84
web/package.json Normal file
View File

@ -0,0 +1,84 @@
{
"name": "vue-naive-admin",
"version": "1.0.0",
"scripts": {
"build": "vite build",
"build:github": "vite build --mode github && esno ./build/script",
"build:test": "vite build --mode test",
"cz": "cz",
"dev": "vite",
"lint": "eslint --ext .js,.vue .",
"lint:fix": "eslint --fix --ext .js,.vue .",
"lint:staged": "lint-staged",
"prepare": "husky install",
"preview": "vite preview"
},
"lint-staged": {
"*.{js,vue}": [
"eslint --ext .js,.vue ."
]
},
"config": {
"commitizen": {
"path": "node_modules/cz-customizable"
}
},
"eslintConfig": {
"extends": [
"@zclzone",
"@unocss",
".eslint-global-variables.json"
]
},
"dependencies": {
"@unocss/eslint-config": "^0.55.7",
"@vicons/ionicons5": "^0.12.0",
"@vueuse/core": "^10.4.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "5.1.12",
"axios": "^1.5.1",
"dayjs": "^1.11.10",
"echarts": "^5.4.3",
"lodash-es": "^4.17.21",
"md-editor-v3": "^4.7.0",
"mitt": "^3.0.1",
"mockjs": "^1.1.0",
"pinia": "^2.1.6",
"vite": "^4.4.11",
"vue": "3.3.4",
"vue-echarts": "^6.6.1",
"vue-router": "^4.2.5",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@commitlint/cli": "^17.7.2",
"@commitlint/config-conventional": "^17.7.0",
"@iconify/json": "^2.2.125",
"@iconify/vue": "^4.1.1",
"@unocss/preset-rem-to-px": "^0.55.7",
"@vitejs/plugin-vue": "^4.4.0",
"@vue/compiler-sfc": "^3.3.4",
"@zclzone/eslint-config": "^0.0.5",
"chalk": "^5.3.0",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
"cz-customizable": "^7.0.0",
"dotenv": "^16.3.1",
"esno": "^0.17.0",
"fs-extra": "^11.1.1",
"husky": "^8.0.3",
"lint-staged": "^14.0.1",
"naive-ui": "^2.39.0",
"rollup-plugin-visualizer": "^5.9.2",
"sass": "^1.69.0",
"unocss": "0.55.3",
"unplugin-auto-import": "^0.16.6",
"unplugin-icons": "^0.16.6",
"unplugin-vue-components": "^0.25.2",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.0",
"vite-plugin-mock": "2.9.6",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-vue-devtools": "1.0.0-rc.7"
}
}

9008
web/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
web/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

1
web/public/favicon.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 448 512" data-v-fba6e5d0=""><path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-92.2 312.9c-63.4 0-85.4-28.6-97.1-64.1c-16.3-51-21.5-84.3-63-84.3c-22.4 0-45.1 16.1-45.1 61.2c0 35.2 18 57.2 43.3 57.2c28.6 0 47.6-21.3 47.6-21.3l11.7 31.9s-19.8 19.4-61.2 19.4c-51.3 0-79.9-30.1-79.9-85.8c0-57.9 28.6-92 82.5-92c73.5 0 80.8 41.4 100.8 101.9c8.8 26.8 24.2 46.2 61.2 46.2c24.9 0 38.1-5.5 38.1-19.1c0-19.9-21.8-22-49.9-28.6c-30.4-7.3-42.5-23.1-42.5-48c0-40 32.3-52.4 65.2-52.4c37.4 0 60.1 13.6 63 46.6l-36.7 4.4c-1.5-15.8-11-22.4-28.6-22.4c-16.1 0-26 7.3-26 19.8c0 11 4.8 17.6 20.9 21.3c32.7 7.1 71.8 12 71.8 57.5c.1 36.7-30.7 50.6-76.1 50.6z" fill="#316c72"></path></svg>

After

Width:  |  Height:  |  Size: 825 B

View File

@ -0,0 +1,85 @@
.loading-container {
position: fixed;
left: 0;
top: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.loading-spin__container {
width: 56px;
height: 56px;
margin: 36px 0;
}
.loading-spin {
position: relative;
height: 100%;
animation: loadingSpin 1s linear infinite;
}
.left-0 {
left: 0;
}
.right-0 {
right: 0;
}
.top-0 {
top: 0;
}
.bottom-0 {
bottom: 0;
}
.loading-spin-item {
position: absolute;
height: 16px;
width: 16px;
background-color: var(--primary-color);
border-radius: 8px;
-webkit-animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes loadingSpin {
from {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loadingPulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: .5;
}
}
.loading-delay-500 {
-webkit-animation-delay: 500ms;
animation-delay: 500ms;
}
.loading-delay-1000 {
-webkit-animation-delay: 1000ms;
animation-delay: 1000ms;
}
.loading-delay-1500 {
-webkit-animation-delay: 1500ms;
animation-delay: 1500ms;
}
.loading-title {
font-size: 28px;
font-weight: 500;
color: #6a6a6a;
}

View File

@ -0,0 +1,9 @@
function addThemeColorCssVars() {
const key = '__THEME_COLOR__'
const defaultColor = '#316c72'
const themeColor = window.localStorage.getItem(key) || defaultColor
const cssVars = `--primary-color: ${themeColor}`
document.documentElement.style.cssText = cssVars
}
addThemeColorCssVars()

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

1
web/settings/index.js Normal file
View File

@ -0,0 +1 @@
export * from './theme.json'

37
web/settings/theme.json Normal file
View File

@ -0,0 +1,37 @@
{
"header": {
"height": 60
},
"tags": {
"visible": true,
"height": 50
},
"naiveThemeOverrides": {
"common": {
"primaryColor": "#316C72FF",
"primaryColorHover": "#316C72E3",
"primaryColorPressed": "#2B4C59FF",
"primaryColorSuppl": "#316C72E3",
"infoColor": "#2080F0FF",
"infoColorHover": "#4098FCFF",
"infoColorPressed": "#1060C9FF",
"infoColorSuppl": "#4098FCFF",
"successColor": "#18A058FF",
"successColorHover": "#36AD6AFF",
"successColorPressed": "#0C7A43FF",
"successColorSuppl": "#36AD6AFF",
"warningColor": "#F0A020FF",
"warningColorHover": "#FCB040FF",
"warningColorPressed": "#C97C10FF",
"warningColorSuppl": "#FCB040FF",
"errorColor": "#D03050FF",
"errorColorHover": "#DE576DFF",
"errorColorPressed": "#AB1F3FFF",
"errorColorSuppl": "#DE576DFF"
}
}
}

11
web/src/App.vue Normal file
View File

@ -0,0 +1,11 @@
<template>
<AppProvider>
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
</AppProvider>
</template>
<script setup>
import AppProvider from '@/components/common/AppProvider.vue'
</script>

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