Compare commits

..

No commits in common. "v2" and "v2.1.3" have entirely different histories.
v2 ... v2.1.3

46 changed files with 602 additions and 3042 deletions

View File

@ -1,9 +1,9 @@
# 打包前端 # 打包前端
FROM node:18-alpine AS build-front FROM node:18-alpine as build-front
WORKDIR /front WORKDIR front
COPY . . COPY . .
WORKDIR ./web WORKDIR web
RUN corepack enable RUN corepack enable
RUN corepack prepare pnpm@8.6.10 --activate RUN corepack prepare pnpm@8.6.10 --activate
@ -13,7 +13,7 @@ RUN pnpm build
RUN ls -lh && pwd RUN ls -lh && pwd
# 前后端集成打包 # 前后端集成打包
FROM golang:alpine AS build-backend FROM golang:alpine as build-backend
RUN apk add upx RUN apk add upx
WORKDIR /build WORKDIR /build
@ -23,7 +23,7 @@ COPY --from=build-front /front/web/dist/ /build/dist
ENV GO111MODULE=on ENV GO111MODULE=on
ENV GOPROXY=https://goproxy.cn,direct ENV GOPROXY=https://goproxy.cn,direct
RUN go version && go build -ldflags="-s -w" -o wgui && upx -9 wgui RUN go build -ldflags="-s -w" -o wgui && upx -9 wgui
RUN ls -lh && chmod +x ./wgui RUN ls -lh && chmod +x ./wgui
@ -43,4 +43,4 @@ COPY --from=build-backend /build/template/* /app/template/
RUN chmod +x wgui RUN chmod +x wgui
ENTRYPOINT ["./wgui","http:serve"] ENTRYPOINT ["./wgui"]

View File

@ -34,6 +34,14 @@ file:
accessSecret: # oss必填 accessSecret: # oss必填
bucketName: # oss必填 bucketName: # oss必填
# 邮件设置
mail:
host:
port:
user:
password:
skipTls:
# 一些系统配置 # 一些系统配置
wireguard: wireguard:
restartMode: DELAY restartMode: DELAY
@ -70,25 +78,14 @@ services:
账户: admin 账户: admin
密码: admin123 密码: 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.png](img.png)
![img_1.png](document/img_1.png) ![img_1.png](img_1.png)
![img_7.png](document/img_7.png) ![img_7.png](img_7.png)
![img_8.png](document/img_8.png) ![img_8.png](img_8.png)
![img_2.png](document/img_2.png) ![img_2.png](img_2.png)
![img_3.png](document/img_3.png) ![img_3.png](img_3.png)
![img_4.png](document/img_4.png) ![img_4.png](img_4.png)
![img_5.png](document/img_5.png) ![img_5.png](img_5.png)
![img_6.png](document/img_6.png) ![img_6.png](img_6.png)

View File

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

View File

@ -1,165 +0,0 @@
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
}

View File

@ -1,520 +0,0 @@
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
}

View File

@ -1,62 +0,0 @@
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, "")
}

View File

@ -1,440 +0,0 @@
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)
}

View File

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

View File

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

9
config/mail.go Normal file
View File

@ -0,0 +1,9 @@
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"`
}

View File

@ -1,24 +1 @@
package cron package cron
import (
"gitee.ltd/lxh/logger/log"
"github.com/go-co-op/gocron/v2"
"github.com/spf13/cast"
"os"
"time"
"wireguard-ui/cron/task"
)
func Task() {
if !cast.ToBool(os.Getenv("ENABLED_CRON")) {
return
}
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()
}

View File

@ -1,119 +0,0 @@
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
}

View File

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

137
go.mod
View File

@ -1,76 +1,53 @@
module wireguard-ui module wireguard-ui
go 1.22.7 go 1.21
toolchain go1.23.0
require ( require (
gitee.ltd/lxh/logger v1.0.19 gitee.ltd/lxh/logger v1.0.15
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/cowardmrx/go_aliyun_oss v1.0.7
github.com/dustin/go-humanize v1.0.1 github.com/fsnotify/fsnotify v1.7.0
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 github.com/gin-contrib/pprof v1.5.0
github.com/fsnotify/fsnotify v1.8.0
github.com/gin-contrib/pprof v1.5.2
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
github.com/go-co-op/gocron/v2 v2.12.4 github.com/go-resty/resty/v2 v2.13.1
github.com/go-playground/locales v0.14.1 github.com/redis/go-redis/v9 v9.5.3
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.23.0
github.com/go-resty/resty/v2 v2.15.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/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 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/mysql v1.5.7
gorm.io/driver/postgres v1.5.11 gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.10 gorm.io/gorm v1.25.10
) )
require ( require (
github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible // indirect 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/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.12.9 // indirect github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.2.2 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/caarlos0/env/v6 v6.10.1 // indirect github.com/caarlos0/env/v6 v6.10.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/charmbracelet/bubbletea v1.1.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect
github.com/charmbracelet/x/ansi v0.2.3 // indirect github.com/cloudwego/iasm v0.2.0 // indirect
github.com/charmbracelet/x/term v0.2.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dennwc/varint v1.0.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.7 // 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/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-kit/kit v0.12.0 // indirect github.com/go-kit/kit v0.12.0 // indirect
github.com/go-kit/log v0.2.1 // indirect github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/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/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.4 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/grafana/loki-client-go v0.0.0-20240913122146-e119d400c3a5 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/grafana/loki/pkg/push v0.0.0-20240912152814-63e84b476a9a // indirect
github.com/grafana/regexp v0.0.0-20220304095617-2e8d9baf4ac2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
@ -78,71 +55,63 @@ require (
github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/joho/godotenv v1.5.1 // 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/josharian/native v1.1.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lixh00/loki-client-go v1.0.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect github.com/mdlayher/socket v0.4.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/mojocn/base64Captcha v1.3.6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
github.com/panjf2000/ants/v2 v2.10.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.13.0 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/common v0.61.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/prometheus v1.8.2-0.20201028100903-3245b3267b24 // indirect
github.com/prometheus/prometheus v0.35.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.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/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.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/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.uber.org/atomic v1.10.0 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.9.0 // indirect
go.uber.org/goleak v1.3.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.23.0 // indirect go.uber.org/zap v1.23.0 // indirect
golang.org/x/arch v0.12.0 // indirect golang.org/x/arch v0.8.0 // indirect
golang.org/x/image v0.18.0 // indirect golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.33.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/oauth2 v0.24.0 // indirect golang.org/x/image v0.13.0 // indirect
golang.org/x/sync v0.10.0 // indirect golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.28.0 // indirect golang.org/x/oauth2 v0.18.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/sync v0.6.0 // indirect
golang.org/x/time v0.8.0 // indirect golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b // indirect golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/grpc v1.69.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect
google.golang.org/protobuf v1.36.5 // 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/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect modernc.org/libc v1.22.5 // indirect

1506
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -243,14 +243,7 @@ func (ClientApi) Download(c *gin.Context) {
return 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 { if err != nil {
response.R(c).FailedWithError("发送邮件失败") response.R(c).FailedWithError("发送邮件失败")
return return

View File

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

View File

@ -67,7 +67,7 @@ func (setting) Delete(c *gin.Context) {
return return
} }
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 { if err := service.Setting().Model(&model.Setting{}).Where("code = ?", code).Delete(&model.Setting{}).Error; err != nil {
response.R(c).FailedWithError("删除失败") response.R(c).FailedWithError("删除失败")
return return
} }

View File

@ -1,33 +0,0 @@
package http
import (
"fmt"
"gitee.ltd/lxh/logger/log"
"github.com/gin-contrib/pprof"
"github.com/spf13/cast"
"net/http"
"os"
"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)
if cast.ToBool(os.Getenv("ENABLED_PPROF")) {
pprof.Register(handler, "/monitoring")
}
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 return
} }
userClaims, err := component.JWT().ParseToken(token, hashPassword, "http") userClaims, err := component.JWT().ParseToken(token, hashPassword)
if err != nil { if err != nil {
response.R(c).AuthorizationFailed("未登陆") response.R(c).AuthorizationFailed("未登陆")
c.Abort() c.Abort()
@ -70,7 +70,7 @@ func Authorization() gin.HandlerFunc {
// 生成一个新token // 生成一个新token
secret := component.JWT().GenerateSecret(user.Password, uuid.NewString(), time.Now().Local().String()) secret := component.JWT().GenerateSecret(user.Password, uuid.NewString(), time.Now().Local().String())
tokenStr, _, err := component.JWT().GenerateToken(user.Id, secret, "http", userClaims.ExpiresAt.Time, time.Now().Local()) tokenStr, _, err := component.JWT().GenerateToken(user.Id, secret, userClaims.ExpiresAt.Time, time.Now().Local())
if err != nil { if err != nil {
response.R(c).AuthorizationFailed("校验失败") response.R(c).AuthorizationFailed("校验失败")
c.Abort() c.Abort()

View File

@ -10,7 +10,7 @@ import (
// @description: 登陆相关API // @description: 登陆相关API
// @param r // @param r
func ClientApi(r *gin.RouterGroup) { func ClientApi(r *gin.RouterGroup) {
client := r.Group("client", middleware.RequestLog(), middleware.Authorization()) client := r.Group("client", middleware.Authorization(), middleware.RequestLog())
{ {
client.POST("", api.Client().Save) // 新增/编辑客户端 client.POST("", api.Client().Save) // 新增/编辑客户端
client.DELETE("/:id", api.Client().Delete) // 删除客户端 client.DELETE("/:id", api.Client().Delete) // 删除客户端

View File

@ -10,7 +10,7 @@ import (
// @description: 控制台相关接口 // @description: 控制台相关接口
// @param r // @param r
func DashboardApi(r *gin.RouterGroup) { func DashboardApi(r *gin.RouterGroup) {
dashboard := r.Group("dashboard", middleware.RequestLog(), middleware.Authorization()) dashboard := r.Group("dashboard", middleware.Authorization(), middleware.RequestLog())
{ {
dashboard.GET("/request/list", api.Dashboard().List) // 请求日志 dashboard.GET("/request/list", api.Dashboard().List) // 请求日志
dashboard.GET("/daily-poetry", api.Dashboard().DailyPoetry) // 每日诗词 dashboard.GET("/daily-poetry", api.Dashboard().DailyPoetry) // 每日诗词

View File

@ -10,7 +10,7 @@ import (
// @description: 设置相关API // @description: 设置相关API
// @param r // @param r
func SettingApi(r *gin.RouterGroup) { func SettingApi(r *gin.RouterGroup) {
setting := r.Group("setting", middleware.RequestLog(), middleware.Authorization()) setting := r.Group("setting", middleware.Authorization(), middleware.RequestLog())
{ {
setting.POST("", api.Setting().Set) // 新增/编辑设置 setting.POST("", api.Setting().Set) // 新增/编辑设置
setting.DELETE("/:code", api.Setting().Delete) // 删除配置 setting.DELETE("/:code", api.Setting().Delete) // 删除配置

View File

@ -10,7 +10,7 @@ import (
// @description: 用户相关API // @description: 用户相关API
// @param r // @param r
func UserApi(r *gin.RouterGroup) { func UserApi(r *gin.RouterGroup) {
userApi := r.Group("user", middleware.RequestLog(), middleware.Authorization()) userApi := r.Group("user", middleware.Authorization(), middleware.RequestLog())
{ {
userApi.GET("/info", api.User().GetLoginUser) // 获取当前登陆用户信息 userApi.GET("/info", api.User().GetLoginUser) // 获取当前登陆用户信息
userApi.POST("", api.User().SaveUser) // 新增/编辑用户 userApi.POST("", api.User().SaveUser) // 新增/编辑用户

View File

@ -1,29 +1,21 @@
package vo package vo
import ( import "wireguard-ui/global/constant"
"wireguard-ui/global/constant"
"wireguard-ui/model"
)
// SettingItem // SettingItem
// @description: 设置单项 // @description: 设置单项
type SettingItem struct { type SettingItem struct {
Code string `json:"code"` Code string `json:"code"`
Data string `json:"data"` Data string `json:"data"`
Describe string `json:"describe"` Describe string `json:"describe"`
CreatedAt model.JsonTime `json:"createdAt"`
UpdatedAt model.JsonTime `json:"updatedAt"`
} }
type Export struct { type Export struct {
Global *Global `json:"global" label:"全局配置" binding:"required"` Global *Global `json:"global" label:"全局配置" binding:"required"`
Server *Server `json:"server" label:"服务端配置" binding:"required"` Server *Server `json:"server" label:"服务端配置" binding:"required"`
Clients []Client `json:"clients" label:"客户端" binding:"omitempty"` Clients []Client `json:"clients" label:"客户端" binding:"omitempty"`
Other []Other `json:"other" label:"其他" binding:"omitempty"`
} }
// Global
// @description: 全局配置
type Global struct { type Global struct {
MTU int `json:"MTU" label:"MTU" binding:"required"` MTU int `json:"MTU" label:"MTU" binding:"required"`
ConfigFilePath string `json:"configFilePath" label:"配置文件路径" binding:"required"` ConfigFilePath string `json:"configFilePath" label:"配置文件路径" binding:"required"`
@ -34,8 +26,6 @@ type Global struct {
Table string `json:"table" label:"table" binding:"omitempty"` Table string `json:"table" label:"table" binding:"omitempty"`
} }
// Server
// @description: 服务端信息
type Server struct { type Server struct {
IpScope []string `json:"ipScope" label:"ipScope" binding:"min=1,dive,required"` IpScope []string `json:"ipScope" label:"ipScope" binding:"min=1,dive,required"`
ListenPort int `json:"listenPort" label:"listenPort" binding:"required"` ListenPort int `json:"listenPort" label:"listenPort" binding:"required"`
@ -45,8 +35,6 @@ type Server struct {
PostDownScript string `json:"postDownScript" label:"postDownScript" binding:"omitempty"` PostDownScript string `json:"postDownScript" label:"postDownScript" binding:"omitempty"`
} }
// Client
// @description: 客户端信息
type Client struct { type Client struct {
Name string `json:"name" label:"name" binding:"required"` Name string `json:"name" label:"name" binding:"required"`
Email string `json:"email" label:"email" binding:"omitempty"` Email string `json:"email" label:"email" binding:"omitempty"`
@ -64,11 +52,3 @@ type Client struct {
Enabled *constant.Status `json:"enabled" label:"enabled" binding:"required"` Enabled *constant.Status `json:"enabled" label:"enabled" binding:"required"`
OfflineMonitoring *constant.Status `json:"offlineMonitoring" label:"offlineMonitoring" 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

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 209 KiB

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -6,10 +6,8 @@ import (
"gitee.ltd/lxh/logger/log" "gitee.ltd/lxh/logger/log"
"github.com/cowardmrx/go_aliyun_oss" "github.com/cowardmrx/go_aliyun_oss"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/joho/godotenv"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"github.com/spf13/viper" "github.com/spf13/viper"
"golang.zx2c4.com/wireguard/wgctrl" "golang.zx2c4.com/wireguard/wgctrl"
@ -17,7 +15,6 @@ import (
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/gorm" "gorm.io/gorm"
gl "gorm.io/gorm/logger" gl "gorm.io/gorm/logger"
"os"
"time" "time"
"wireguard-ui/config" "wireguard-ui/config"
"wireguard-ui/global/client" "wireguard-ui/global/client"
@ -28,7 +25,6 @@ import (
func Init() { func Init() {
initLogger() // 初始化日志 initLogger() // 初始化日志
initConfig() // 读取配置文件 initConfig() // 读取配置文件
initEnv() // 加载环境变量文件
initWireguard() // 初始化wireguard客户端 initWireguard() // 初始化wireguard客户端
initDatabase() // 初始化数据库 initDatabase() // 初始化数据库
initRedis() // 初始化redis initRedis() // 初始化redis
@ -144,22 +140,8 @@ func initOSS() {
// initLogger // initLogger
// @description: 初始化日志 // @description: 初始化日志
func initLogger() { func initLogger() {
mode := logger.Dev
if os.Getenv("GIN_MODE") == gin.ReleaseMode {
mode = logger.Prod
}
logger.InitLogger(logger.LogConfig{ logger.InitLogger(logger.LogConfig{
Mode: mode, Mode: logger.Dev,
FileEnable: true, FileEnable: true,
}) })
} }
// initEnv
// @description: 初始化环境变量
func initEnv() {
if err := godotenv.Load(".env"); err != nil {
log.Errorf("加载.env文件失败: %v", err.Error())
}
}

44
main.go
View File

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

16
model/oauth_client.go Normal file
View File

@ -0,0 +1,16 @@
package model
// AuthClient
// @description: 认证客户端
type AuthClient struct {
Base
Name string `json:"name" gorm:"type:varchar(255);not null;comment: '客户端名称'"`
ClientID string `json:"clientID" gorm:"type:varchar(255);not null;comment: '客户端ID'"`
ClientKey string `json:"clientKey" gorm:"type:varchar(255);not null;comment: '客户端key'"`
ExpireAt string `json:"expireAt" gorm:"type:varchar(255);not null;comment: '过期时间'"`
IsEnabled int `json:"isEnabled" gorm:"type:int(1);not null;comment: '是否启用'"`
}
func (AuthClient) TableName() string {
return "t_oauth_client"
}

View File

@ -22,3 +22,16 @@ type Client struct {
func (Client) TableName() string { func (Client) TableName() string {
return "t_client" 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

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

View File

@ -2,7 +2,6 @@ package service
import ( import (
"encoding/json" "encoding/json"
"fmt"
slog "gitee.ltd/lxh/logger/log" slog "gitee.ltd/lxh/logger/log"
"gorm.io/gorm" "gorm.io/gorm"
"strings" "strings"
@ -11,7 +10,6 @@ import (
"wireguard-ui/http/vo" "wireguard-ui/http/vo"
"wireguard-ui/model" "wireguard-ui/model"
"wireguard-ui/template/render_data" "wireguard-ui/template/render_data"
"wireguard-ui/utils"
) )
type setting struct{ *gorm.DB } type setting struct{ *gorm.DB }
@ -86,18 +84,7 @@ func (s setting) GetWGServerForConfig() (data *render_data.Server, err error) {
// @return data // @return data
// @return err // @return err
func (s setting) GetAllSetting(blackList []string) (data []vo.SettingItem, err error) { func (s setting) GetAllSetting(blackList []string) (data []vo.SettingItem, err error) {
err = s.Model(&model.Setting{}).Select("code, data, describe,created_at,updated_at").Where("code not in ?", blackList).Find(&data).Error err = s.Model(&model.Setting{}).Select("code, data, describe").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 return
} }
@ -152,13 +139,6 @@ func (s setting) Export() (data vo.Export, err error) {
} }
} }
// 查询其他配置
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) cj, _ := json.Marshal(clients)
_ = json.Unmarshal(cj, &data.Clients) _ = json.Unmarshal(cj, &data.Clients)
@ -171,9 +151,6 @@ func (s setting) Export() (data vo.Export, err error) {
// @param data // @param data
// @return err // @return err
func (s setting) Import(data *vo.Export, loginUser *vo.User) (err error) { func (s setting) Import(data *vo.Export, loginUser *vo.User) (err error) {
// 获取导入系统的公网IP地址
pubAddr := utils.Network().GetHostPublicIP()
data.Global.EndpointAddress = pubAddr
// 先更新global配置 // 先更新global配置
gst, _ := json.Marshal(data.Global) gst, _ := json.Marshal(data.Global)
gs := &model.Setting{ gs := &model.Setting{
@ -202,41 +179,25 @@ func (s setting) Import(data *vo.Export, loginUser *vo.User) (err error) {
PublicKey: v.Keys.PublicKey, PublicKey: v.Keys.PublicKey,
PresharedKey: v.Keys.PresharedKey, PresharedKey: v.Keys.PresharedKey,
} }
cc := param.SaveClient{ cc := param.SaveClient{
Name: v.Name, Name: v.Name,
Email: v.Email, Email: v.Email,
IpAllocation: v.IpAllocation, IpAllocation: v.IpAllocation,
AllowedIps: v.AllowedIps, AllowedIps: v.AllowedIps,
ExtraAllowedIps: v.ExtraAllowedIps, ExtraAllowedIps: v.ExtraAllowedIps,
Endpoint: v.Endpoint,
UseServerDns: v.UseServerDns, UseServerDns: v.UseServerDns,
Keys: keys, Keys: keys,
Enabled: v.Enabled, Enabled: v.Enabled,
OfflineMonitoring: v.OfflineMonitoring, 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 { if err := Client().SaveClient(cc, loginUser); err != nil {
slog.Errorf("客户端[%s]导入失败: %v", v.Name, err) slog.Errorf("客户端[%s]导入失败: %v", v.Name, err)
continue 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 return nil
} }

View File

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

View File

@ -1,65 +0,0 @@
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
}

View File

@ -85,14 +85,14 @@
{{ cip }} {{ cip }}
</n-button> </n-button>
</n-form-item> </n-form-item>
<!-- <n-form-item label="可访问IP:">--> <n-form-item label="可访问IP:">
<!-- <n-button v-if="row.allowedIps.length <= 0" dashed size="small">--> <n-button v-if="row.allowedIps.length <= 0" dashed size="small">
<!-- - --> -
<!-- </n-button>--> </n-button>
<!-- <n-button v-else dashed mr-2 type="warning" v-for="aip in row.allowedIps" size="small">--> <n-button v-else dashed mr-2 type="warning" v-for="aip in row.allowedIps" size="small">
<!-- {{ aip }}--> {{ aip }}
<!-- </n-button>--> </n-button>
<!-- </n-form-item>--> </n-form-item>
<n-form-item label="创建人:"> <n-form-item label="创建人:">
<n-button color="#54150F" dashed size="small"> <n-button color="#54150F" dashed size="small">
{{ row.createUser }} {{ row.createUser }}
@ -106,7 +106,7 @@
禁用 禁用
</n-button> </n-button>
</n-form-item> </n-form-item>
<n-form-item label="离线通知:"> <n-form-item label="离线监听:">
<n-button v-if="row.offlineMonitoring === 1" color="#067748" round :bordered="false" size="small"> <n-button v-if="row.offlineMonitoring === 1" color="#067748" round :bordered="false" size="small">
启用 启用
</n-button> </n-button>
@ -114,12 +114,12 @@
禁用 禁用
</n-button> </n-button>
</n-form-item> </n-form-item>
<!-- <n-form-item class="timeItem" label="时间:">--> <n-form-item class="timeItem" label="时间:">
<!-- <n-space vertical>--> <n-space vertical>
<!-- <span> 创建时间: {{ row.createdAt }}</span>--> <span> 创建时间: {{ row.createdAt }}</span>
<!-- <span> 更新时间: {{ row.updatedAt }}</span>--> <span> 更新时间: {{ row.updatedAt }}</span>
<!-- </n-space>--> </n-space>
<!-- </n-form-item>--> </n-form-item>
</n-form> </n-form>
</div> </div>
</n-card> </n-card>
@ -228,18 +228,12 @@
<n-radio :value="0" :checked="editModalForm.enabled === 0" @change="editModalForm.enabled = 0">禁用</n-radio> <n-radio :value="0" :checked="editModalForm.enabled === 0" @change="editModalForm.enabled = 0">禁用</n-radio>
</n-radio-group> </n-radio-group>
</n-form-item> </n-form-item>
<n-form-item label="离线通知" path="offlineMonitoring"> <n-form-item label="离线监听" path="offlineMonitoring">
<n-radio-group :value="editModalForm.offlineMonitoring"> <n-radio-group :value="editModalForm.offlineMonitoring">
<n-radio :value="1" :checked="editModalForm.offlineMonitoring === 1" @change="editModalForm.offlineMonitoring = 1">启用</n-radio> <n-radio :value="1" :checked="editModalForm.offlineMonitoring === 1" @change="editModalForm.offlineMonitoring = 1">启用</n-radio>
<n-radio :value="0" :checked="editModalForm.offlineMonitoring === 0" @change="editModalForm.offlineMonitoring = 0">禁用</n-radio> <n-radio :value="0" :checked="editModalForm.offlineMonitoring === 0" @change="editModalForm.offlineMonitoring = 0">禁用</n-radio>
</n-radio-group> </n-radio-group>
</n-form-item> </n-form-item>
<n-form-item class="timeItem" label="时间:">
<n-space vertical>
<span> 创建时间: {{ editModalForm.createdAt }}</span>
<span> 更新时间: {{ editModalForm.updatedAt }}</span>
</n-space>
</n-form-item>
<n-button type="info" style="margin-left: 40%" @click="updateClient()">确认</n-button> <n-button type="info" style="margin-left: 40%" @click="updateClient()">确认</n-button>
</n-form> </n-form>
</n-modal> </n-modal>
@ -325,7 +319,7 @@
<n-radio :value="0" :checked="addModalForm.enabled === 0" @change="addModalForm.enabled = 0">禁用</n-radio> <n-radio :value="0" :checked="addModalForm.enabled === 0" @change="addModalForm.enabled = 0">禁用</n-radio>
</n-radio-group> </n-radio-group>
</n-form-item> </n-form-item>
<n-form-item label="离线通知" path="offlineMonitoring"> <n-form-item label="离线监听" path="offlineMonitoring">
<n-radio-group :value="addModalForm.offlineMonitoring"> <n-radio-group :value="addModalForm.offlineMonitoring">
<n-radio :value="1" :checked="addModalForm.offlineMonitoring === 1" @change="addModalForm.offlineMonitoring = 1">启用</n-radio> <n-radio :value="1" :checked="addModalForm.offlineMonitoring === 1" @change="addModalForm.offlineMonitoring = 1">启用</n-radio>
<n-radio :value="0" :checked="addModalForm.offlineMonitoring === 0" @change="addModalForm.offlineMonitoring = 0">禁用</n-radio> <n-radio :value="0" :checked="addModalForm.offlineMonitoring === 0" @change="addModalForm.offlineMonitoring = 0">禁用</n-radio>
@ -446,9 +440,7 @@ const editModalForm = ref({
presharedKey: '' presharedKey: ''
}, },
enabled: 1, enabled: 1,
offlineMonitoring: 1, offlineMonitoring: 1
createdAt: '',
updatedAt: ''
}) })
// //
@ -584,8 +576,6 @@ function openEditModal(row) {
editModalForm.value.keys.presharedKey = row.keys.presharedKey editModalForm.value.keys.presharedKey = row.keys.presharedKey
editModalForm.value.enabled = row.enabled editModalForm.value.enabled = row.enabled
editModalForm.value.offlineMonitoring = row.offlineMonitoring editModalForm.value.offlineMonitoring = row.offlineMonitoring
editModalForm.value.createdAt = row.createdAt
editModalForm.value.updatedAt = row.updatedAt
} }
// //

View File

@ -174,9 +174,6 @@
<n-radio :value="false" :checked="editFormModel.data[index] === false" @change="editFormModel.data[index] = false"></n-radio> <n-radio :value="false" :checked="editFormModel.data[index] === false" @change="editFormModel.data[index] = false"></n-radio>
</n-radio-group> </n-radio-group>
</n-form-item> </n-form-item>
<n-form-item label="配置描述">
<n-input v-model:value="editFormModel.describe" />
</n-form-item>
<n-form-item> <n-form-item>
<n-button type="info" @click="updateSetting">确认</n-button> <n-button type="info" @click="updateSetting">确认</n-button>
</n-form-item> </n-form-item>

View File

@ -27,18 +27,6 @@
remote remote
:columns="connectionsColumns" :columns="connectionsColumns"
:data="connectionsData.data" :data="connectionsData.data"
:row-props="rowProps"
/>
<n-dropdown
placement="bottom-start"
trigger="manual"
size="small"
:x="xRef"
:y="yRef"
:options="rightMenuOpts"
@select="rowSelect"
@clickoutside="rowClick"
:show="showDropdownRef"
/> />
</n-card> </n-card>
</AppPage> </AppPage>
@ -47,7 +35,7 @@
<script setup> <script setup>
import { useUserStore } from '@/store' import { useUserStore } from '@/store'
import api from '@/views/workbench/api' import api from '@/views/workbench/api'
import { debounce, renderIcon } from '@/utils' import { debounce } from '@/utils'
import { NTag } from 'naive-ui' import { NTag } from 'naive-ui'
const userStore = useUserStore() const userStore = useUserStore()
@ -164,49 +152,6 @@ const connectionsColumns = [
} }
] ]
//
const rightMenuOpts = [
{
label: () => h('span',{ style: { color: 'green' }}, '刷新'),
key: "refresh",
icon: renderIcon('tabler:refresh',{ size: 14 })
}
]
//
const showDropdownRef = ref(false);
const xRef = ref(0);
const yRef = ref(0);
//
function rowProps(row) {
return {
onContextmenu: (e) => {
// $message.info(JSON.stringify(row, null, 2));
e.preventDefault();
showDropdownRef.value = false;
nextTick().then(() => {
showDropdownRef.value = true;
xRef.value = e.clientX;
yRef.value = e.clientY;
});
}
};
}
//
function rowSelect(row) {
switch (row) {
case "refresh":
getClientConnections()
showDropdownRef.value = false
}
}
function rowClick() {
showDropdownRef.value = false
}
// //
const tableData = ref({ const tableData = ref({
data: [] data: []