Compare commits

..

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

487 changed files with 20938 additions and 27134 deletions

@ -1,6 +1,6 @@
kind: pipeline
type: docker
name: wireguard-srv
name: wireguard-dashboard
trigger:
event: [tag]
@ -10,7 +10,7 @@ steps:
image: plugins/docker
settings:
registry: gitea.mrx.ltd # 镜像仓库地址
repo: gitea.mrx.ltd/go-pkg/wireguard-srv # 镜像仓库地址
repo: gitea.mrx.ltd/go-pkg/wireguard-dashboard # 镜像仓库地址
username:
from_secret: docker_user
password:

269
.gitignore vendored

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

@ -1,29 +1,28 @@
# 打包前端
FROM node:18-alpine AS build-front
FROM node:18-alpine as build-stage
WORKDIR /front
WORKDIR front
COPY . .
WORKDIR ./web
WORKDIR web-src
RUN corepack enable
RUN corepack prepare pnpm@8.6.10 --activate
RUN npm config set registry https://registry.npmmirror.com
RUN pnpm install
RUN pnpm build
RUN ls -lh && pwd
# 前后端集成打包
FROM golang:alpine AS build-backend
FROM golang:alpine as build
RUN apk add upx
WORKDIR /build
COPY . .
COPY --from=build-front /front/web/dist/ /build/dist
COPY --from=build-stage /front/web/ /build/web
# sqlite必须
ENV GO111MODULE=on
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
@ -38,9 +37,9 @@ RUN apk --no-cache add ca-certificates wireguard-tools jq iptables
WORKDIR /app
RUN mkdir -p db
COPY --from=build-backend --chown=wgui:wgui /build/wgui /app
COPY --from=build-backend /build/template/* /app/template/
COPY --from=build --chown=wgui:wgui /build/wgui /app
COPY --from=build /build/template/* /app/template/
RUN chmod +x wgui
ENTRYPOINT ["./wgui","http:serve"]
ENTRYPOINT ["./wgui"]

@ -1,94 +0,0 @@
# 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)

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

@ -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
}

@ -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
}

@ -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, "")
}

@ -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)
}

@ -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()
}

@ -3,23 +3,16 @@ package command
import (
"fmt"
"gitee.ltd/lxh/logger/log"
"os"
"os/exec"
"strings"
"wireguard-ui/service"
"wireguard-dashboard/repository"
)
// 分隔符
// getConfigFileName
// @description: 获取服务端配置文件名称
// @return string
func getConfigFileName(filePath string) string {
if filePath != "" {
filePath = strings.Split(filePath, string(os.PathSeparator))[len(strings.Split(filePath, string(os.PathSeparator)))-1] // 这里取到的是wg0.conf
filePath = strings.Split(filePath, ".conf")[0] // 这里取到的就是wg0
return filePath
}
data, err := service.Setting().GetWGSetForConfig()
func getConfigFileName() string {
data, err := repository.System().GetServerSetting()
if err != nil {
log.Errorf("获取服务端配置失败: %v", err.Error())
return ""
@ -36,23 +29,23 @@ func getConfigFileName(filePath string) string {
// RestartWireguard
// @description: 是否重启
// @param isAsync // 是否异步执行
func RestartWireguard(isAsync bool, filePath string) {
func RestartWireguard(isAsync bool) {
if isAsync {
go func() {
StopWireguard(filePath) // 停止
StartWireguard(filePath) // 启动
StopWireguard() // 停止
StartWireguard() // 启动
}()
} else {
StopWireguard(filePath) // 停止
StartWireguard(filePath) // 启动
StopWireguard() // 停止
StartWireguard() // 启动
}
return
}
// StopWireguard
// @description: 停止服务端
func StopWireguard(filePath string) {
configFileName := getConfigFileName(filePath)
func StopWireguard() {
configFileName := getConfigFileName()
cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("wg-quick down %s", configFileName))
if err := cmd.Run(); err == nil {
@ -64,13 +57,12 @@ func StopWireguard(filePath string) {
// StartWireguard
// @description: 启动服务端
func StartWireguard(filePath string) {
configFileName := getConfigFileName(filePath)
func StartWireguard() {
configFileName := getConfigFileName()
cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("wg-quick up %s", configFileName))
if err := cmd.Run(); err == nil {
log.Infof("启动wireguard[%s]服务端成功", configFileName)
}
return
}

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

@ -7,111 +7,74 @@ import (
"gitee.ltd/lxh/logger/log"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"math/rand"
"strings"
"time"
"wireguard-ui/config"
"wireguard-ui/global/client"
"wireguard-ui/global/constant"
"wireguard-ui/utils"
"wireguard-dashboard/client"
"wireguard-dashboard/config"
"wireguard-dashboard/constant"
)
type JwtComponent struct {
const Secret = "IK8MSs76Pb2VJxleTDadf1Wzu3h9QROLv0XtmnCUErYgBG5wAyjk4cioqFZHNpZG"
type JwtClaims struct {
ID string `json:"id"`
jwt.RegisteredClaims
}
// JWT
// @description: 初始化JWT组件
// @return JwtComponent
func JWT() JwtComponent {
return JwtComponent{}
func JWT() JwtClaims {
return JwtClaims{}
}
// GenerateToken
// @description: 生成token
// @receiver JwtComponent
// @param userId
// @param password
// @receiver Jwt
// @return token
// @return expireTime
// @return err
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])
notBefore = jwt.NewNumericDate(times[1])
issuedAt = jwt.NewNumericDate(times[1])
} else {
timeNow := time.Now().Local()
expireTime = jwt.NewNumericDate(timeNow.Add(7 * time.Hour))
notBefore = jwt.NewNumericDate(timeNow)
issuedAt = jwt.NewNumericDate(timeNow)
}
claims := JwtComponent{
func (j JwtClaims) GenerateToken(userId string) (token string, expireTime *jwt.NumericDate, err error) {
timeNow := time.Now().Local()
expireTime = jwt.NewNumericDate(timeNow.Add(7 * time.Hour))
notBefore := jwt.NewNumericDate(timeNow)
issuedAt := jwt.NewNumericDate(timeNow)
claims := JwtClaims{
ID: userId,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: config.Config.Http.Endpoint, // 颁发站点
Subject: "you can you up,no can no bb", // 发布主题
ExpiresAt: expireTime, // 过期时间
NotBefore: notBefore, // token不得早于该时间
IssuedAt: issuedAt, // token颁发时间
ID: strings.ReplaceAll(uuid.NewString(), "-", ""), // 该token的id
Issuer: config.Config.Http.Endpoint, // 颁发站点
Subject: "wg-dashboard",
ExpiresAt: expireTime,
NotBefore: notBefore,
IssuedAt: issuedAt,
ID: uuid.NewString(),
},
}
t := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
token, err = t.SignedString([]byte(secret))
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err = t.SignedString([]byte(Secret))
if err != nil {
log.Errorf("生成token失败: %v", err.Error())
return "", nil, errors.New("生成token失败")
}
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)
}
client.Redis.Set(context.Background(), fmt.Sprintf("%s:%s", constant.Token, userId), token, 7*time.Hour)
return
}
// ParseToken
// @description: 解析token
// @receiver JwtComponent
// @param token
// @return *JwtComponent
// @receiver Jwt
// @return Jwt
// @return error
func (JwtComponent) ParseToken(token, secret, source string) (*JwtComponent, error) {
func (JwtClaims) ParseToken(token string) (*JwtClaims, error) {
tokenStr := strings.Split(token, "Bearer ")[1]
t, err := jwt.ParseWithClaims(tokenStr, &JwtComponent{}, func(token *jwt.Token) (any, error) {
return []byte(secret), nil
t, err := jwt.ParseWithClaims(tokenStr, &JwtClaims{}, func(token *jwt.Token) (any, error) {
return []byte(Secret), nil
})
if claims, ok := t.Claims.(*JwtComponent); ok && t.Valid {
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 claims, ok := t.Claims.(*JwtClaims); ok && t.Valid {
userToken, err := client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.Token, claims.ID)).Result()
if err != nil {
log.Errorf("缓存中用户[%s]的token查找失败: %v", claims.ID, err.Error())
return nil, errors.New("token不存在")
}
if userToken != tokenStr {
@ -125,31 +88,11 @@ func (JwtComponent) ParseToken(token, secret, source string) (*JwtComponent, err
}
}
// GenerateSecret
// @description: 生成token解析密钥【每个用户的secret不一样提高安全性】
// @receiver JwtComponent
// @param secret
// @return string
func (JwtComponent) GenerateSecret(secret ...string) string {
// 添加10个元素,增加随机性
for i := 0; i <= 10; i++ {
secret = append(secret, uuid.NewString())
}
// 混淆一下明文secret的顺序
n := len(secret)
for i := n - 1; i > 0; i-- {
j := rand.Intn(i + 1)
secret[i], secret[j] = secret[j], secret[i]
}
secretStr := strings.Join(secret, ".")
return utils.Hash().MD5(utils.Hash().SHA256(utils.Hash().SHA512(secretStr)))
}
// Logout
// @description: 退出登陆
// @receiver JwtComponent
// @receiver JwtClaims
// @param userId
// @return error
func (JwtComponent) Logout(userId string) error {
return client.Redis.Del(context.Background(), fmt.Sprintf("%s:%s", constant.UserToken, userId)).Err()
// @return err
func (j JwtClaims) Logout(userId string) (err error) {
return client.Redis.Del(context.Background(), fmt.Sprintf("%s:%s", constant.Token, userId)).Err()
}

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

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

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

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

9
config/mail.go Normal 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"`
}

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

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

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

10
constant/cache_prefix.go Normal file

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

1
constant/wireguard.go Normal file

@ -0,0 +1 @@
package constant

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

@ -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
}

17
cron_task/cron.go Normal file

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

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

6
dist/static.go vendored

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

Binary file not shown.

Before

(image error) Size: 209 KiB

Binary file not shown.

Before

(image error) Size: 52 KiB

Binary file not shown.

Before

(image error) Size: 34 KiB

Binary file not shown.

Before

(image error) Size: 29 KiB

Binary file not shown.

Before

(image error) Size: 49 KiB

Binary file not shown.

Before

(image error) Size: 34 KiB

Binary file not shown.

Before

(image error) Size: 31 KiB

Binary file not shown.

Before

(image error) Size: 59 KiB

Binary file not shown.

Before

(image error) Size: 55 KiB

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

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

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

144
go.mod

@ -1,77 +1,61 @@
module wireguard-ui
module wireguard-dashboard
go 1.22.7
toolchain go1.23.0
go 1.21
require (
gitee.ltd/lxh/logger v1.0.19
github.com/charmbracelet/bubbles v0.20.0
github.com/charmbracelet/lipgloss v0.13.0
gitee.ltd/lxh/logger v1.0.15
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.8.0
github.com/gin-contrib/pprof v1.5.2
github.com/gin-gonic/gin v1.10.0
github.com/gin-gonic/gin v1.9.1
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.23.0
github.com/go-resty/resty/v2 v2.15.3
github.com/go-co-op/gocron/v2 v2.5.0
github.com/go-resty/resty/v2 v2.11.0
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/redis/go-redis/v9 v9.5.1
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.org/x/crypto v0.22.0
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.11
gorm.io/gorm v1.25.10
gopkg.in/fsnotify.v1 v1.4.7
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.4
gorm.io/driver/postgres v1.5.6
gorm.io/gorm v1.25.7
)
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.12.9 // indirect
github.com/bytedance/sonic/loader v0.2.2 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/caarlos0/env/v6 v6.10.1 // indirect
github.com/cespare/xxhash/v2 v2.3.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.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/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/gabriel-vasile/mimetype v1.4.7 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/pprof v1.5.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/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.20.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/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/grafana/loki-client-go v0.0.0-20240913122146-e119d400c3a5 // 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/google/go-cmp v0.5.9 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
@ -81,69 +65,49 @@ require (
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/klauspost/cpuid/v2 v2.2.9 // 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/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/lixh00/loki-client-go v1.0.1 // 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
github.com/mdlayher/socket v0.4.1 // indirect
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/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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // 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.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.61.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/prometheus/prometheus v0.35.0 // indirect
github.com/prometheus/client_golang v1.13.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
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/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // 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/rogpeppe/go-internal v1.12.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.11.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
golang.org/x/arch v0.12.0 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/oauth2 v0.24.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.8.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
golang.org/x/image v0.15.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/oauth2 v0.1.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.3.0 // 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/grpc v1.69.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55 // indirect
google.golang.org/grpc v1.50.1 // indirect
google.golang.org/protobuf v1.34.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect

1571
go.sum

File diff suppressed because it is too large Load Diff

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

36
http/api/captcha.go Normal file

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

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

@ -1,102 +1,42 @@
package api
import (
"fmt"
"github.com/gin-gonic/gin"
"strings"
"time"
"wireguard-ui/component"
"wireguard-ui/http/param"
"wireguard-ui/http/response"
"wireguard-ui/http/vo"
"wireguard-ui/service"
"wireguard-ui/utils"
"wireguard-dashboard/http/param"
"wireguard-dashboard/model/entity"
"wireguard-dashboard/repository"
"wireguard-dashboard/utils"
)
type DashboardApi struct{}
type dashboard struct{}
func Dashboard() DashboardApi {
return DashboardApi{}
func Dashboard() dashboard {
return dashboard{}
}
// List
// @description: 操作日志
// @receiver DashboardApi
// @description: 操作日志分页列表
// @receiver d
// @param c
func (DashboardApi) List(c *gin.Context) {
var p param.Page
func (d dashboard) List(c *gin.Context) {
var p param.OnlyPage
if err := c.ShouldBind(&p); err != nil {
response.R(c).Validator(err)
utils.GinResponse(c).FailedWithErr("参数错误", err)
return
}
var loginUser *vo.User
if loginUser = GetCurrentLoginUser(c); c.IsAborted() {
// 如果不是超级管理员只能看自己的
userInfo, ok := c.Get("user")
if !ok {
utils.GinResponse(c).AuthorizationFailed()
return
}
data, total, err := service.Log().List(p, loginUser)
data, count, err := repository.SystemLog().List(p, userInfo.(*entity.User))
if err != nil {
response.R(c).FailedWithError(fmt.Errorf("获取操作日志失败: %v", err.Error()))
return
}
response.R(c).Paginate(data, total, p.Current, p.Size)
}
// DailyPoetry
// @description: 每日诗词
// @receiver DashboardApi
// @param c
func (DashboardApi) DailyPoetry(c *gin.Context) {
data, err := utils.DailyPoetry().HitokotoPoetry()
if err != nil {
response.R(c).FailedWithError("获取失败")
return
}
response.R(c).OkWithData(data)
}
// ConnectionList
// @description: 客户端链接信息列表
// @receiver DashboardApi
// @param c
func (DashboardApi) ConnectionList(c *gin.Context) {
peers, err := component.Wireguard().GetClients()
if err != nil {
response.R(c).FailedWithError("获取失败")
utils.GinResponse(c).FailedWithErr("获取失败", err)
return
}
var connections []vo.DataTraffic
for _, peer := range peers {
// 获取客户端链接信息
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, ",")
}
connections = append(connections, vo.DataTraffic{
Name: clientInfo.Name,
Email: clientInfo.Email,
IpAllocation: ipAllocation,
Online: time.Since(peer.LastHandshakeTime).Minutes() < 3,
ReceiveBytes: utils.FlowCalculation().Parse(peer.TransmitBytes),
TransmitBytes: utils.FlowCalculation().Parse(peer.ReceiveBytes),
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)
utils.GinResponse(c).OkWithPage(data, count, p.Current, p.Size)
}

@ -1,111 +0,0 @@
package api
import (
"fmt"
"gitee.ltd/lxh/logger/log"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/mojocn/base64Captcha"
"time"
"wireguard-ui/component"
"wireguard-ui/http/param"
"wireguard-ui/http/response"
"wireguard-ui/http/vo"
"wireguard-ui/service"
"wireguard-ui/utils"
)
type LoginApi struct{}
func Login() LoginApi {
return LoginApi{}
}
// Captcha
// @description: 获取验证码
// @receiver login
// @param c
func (LoginApi) Captcha(c *gin.Context) {
math := base64Captcha.DriverMath{Height: 120, Width: 480, Fonts: []string{"ApothecaryFont.ttf", "3Dumb.ttf"}}
mathDriver := math.ConvertFonts()
capt := base64Captcha.NewCaptcha(mathDriver, component.Captcha{})
id, base64Str, _, err := capt.Generate()
if err != nil {
response.R(c).FailedWithError(fmt.Errorf("生成验证码失败: %s", err.Error()))
return
}
response.R(c).OkWithData(map[string]any{
"id": id,
"captcha": base64Str,
})
}
// Login
// @description: 登陆
// @receiver login
// @param c
func (LoginApi) Login(c *gin.Context) {
var p param.Login
if err := c.ShouldBind(&p); err != nil {
response.R(c).Validator(err)
return
}
// 验证验证码是否正确
ok := component.Captcha{}.Verify(p.CaptchaId, p.CaptchaCode, true)
if !ok {
response.R(c).FailedWithError("验证码错误")
return
}
// 验证码正确,查询用户信息
user, err := service.User().GetUserByAccount(p.Account)
if err != nil {
response.R(c).FailedWithError("获取用户信息失败")
return
}
// 对比密码
if !utils.Password().ComparePassword(user.Password, p.Password) {
response.R(c).FailedWithError("密码错误")
return
}
secret := component.JWT().GenerateSecret(p.Password, uuid.NewString(), time.Now().Local().String())
// 生成token
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("登陆失败!")
return
}
c.Writer.Header().Set("X-TOKEN", secret)
response.R(c).OkWithData(map[string]any{
"token": token,
"type": "Bearer",
"expireAt": expireAt,
})
}
// Logout
// @description: 退出登陆
// @receiver LoginApi
// @param c
func (LoginApi) Logout(c *gin.Context) {
loginUser, ok := c.Get("user")
if !ok {
response.R(c).AuthorizationFailed("未登陆")
return
}
if err := component.JWT().Logout(loginUser.(*vo.User).Id); err != nil {
response.R(c).FailedWithError("退出登陆失败")
return
}
response.R(c).OK()
}

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

110
http/api/server.go Normal file

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

@ -2,20 +2,14 @@ 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"
"wireguard-ui/utils"
"wireguard-dashboard/config"
"wireguard-dashboard/http/param"
"wireguard-dashboard/model/entity"
"wireguard-dashboard/queues"
"wireguard-dashboard/repository"
"wireguard-dashboard/utils"
)
type setting struct{}
@ -24,194 +18,88 @@ func Setting() setting {
return setting{}
}
// Set
// @description: 置配
// SetSetting
// @description: 添加/更改设置
// @receiver setting
// @param c
func (setting) Set(c *gin.Context) {
func (setting) SetSetting(c *gin.Context) {
var p param.SetSetting
if err := c.ShouldBind(&p); err != nil {
response.R(c).Validator(err)
utils.GinResponse(c).FailedWithErr("参数错误", err)
return
}
if err := service.Setting().SetData(&model.Setting{
if err := repository.System().Save(&entity.Setting{
Code: p.Code,
Data: p.Data,
Describe: p.Describe,
}); err != nil {
response.R(c).FailedWithError(err)
utils.GinResponse(c).FailedWithErr("操作失败", err)
return
}
var whiteCodes = []string{"WG_SETTING", "WG_SERVER"}
if slices.Contains(whiteCodes, p.Code) {
go func() {
if err := script.New().GenerateConfig(); err != nil {
log.Errorf("执行脚本失败")
}
}()
}
response.R(c).OK()
utils.GinResponse(c).OK()
}
// Delete
// @description: 删除配置
// SetServerGlobal
// @description: 设置服务端的全局设定
// @receiver setting
// @param c
func (setting) Delete(c *gin.Context) {
code := c.Param("code")
if code == "" || code == "undefined" {
response.R(c).FailedWithError("code不能为空")
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 {
response.R(c).FailedWithError("删除失败")
return
}
response.R(c).OK()
}
// GetSetting
// @description: 获取指定配置
// @receiver setting
// @param c
func (setting) GetSetting(c *gin.Context) {
code := c.Query("code")
if code == "" {
response.R(c).FailedWithError("code不能为空")
return
}
var data *model.Setting
if err := service.Setting().Model(&model.Setting{}).Where("code = ?", code).Take(&data).Error; err != nil {
response.R(c).FailedWithError("获取指定配置失败")
return
}
response.R(c).OkWithData(data.Data)
}
// GetAllSetting
// @description: 获取全部配置
// @receiver setting
// @param c
func (setting) GetAllSetting(c *gin.Context) {
// 不查询的配置
var blackList = []string{"WG_SETTING", "WG_SERVER"}
data, err := service.Setting().GetAllSetting(blackList)
if err != nil {
response.R(c).FailedWithError("获取配置失败")
return
}
response.R(c).OkWithData(data)
}
// GetPublicAddr
// @description: 获取公网地址
// @receiver setting
// @param c
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
func (setting) SetServerGlobal(c *gin.Context) {
var p param.SetServerGlobal
if err := c.ShouldBind(&p); err != nil {
response.R(c).Validator(err)
utils.GinResponse(c).FailedWithErr("参数错误", err)
return
}
// 校验文件是否合规
if p.File.Filename != "config.json" {
response.R(c).Validator(errors.New("文件名不合规"))
data, _ := json.Marshal(p)
var ent entity.Setting
ent.Code = "SERVER_SETTING"
ent.Data = string(data)
if err := repository.System().Save(&ent); err != nil {
utils.GinResponse(c).FailedWithErr("操作失败", err)
return
}
// 校验文件内容是否符合
fileBytes, err := p.File.Open()
if err != nil {
response.R(c).FailedWithError(err)
return
}
go func() {
if err := queues.PutAsyncWireguardConfigFile(""); err != nil {
log.Errorf("[设置服务端],发起同步配置文件失败: %v", err.Error())
}
}()
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()
utils.GinResponse(c).OK()
}
// GetGlobalSetting
// @description: 获取全局设置配置
// @receiver setting
// @param c
func (setting) GetGlobalSetting(c *gin.Context) {
data, err := repository.System().GetServerSetting()
if err != nil {
log.Errorf("获取配置失败: %v", err.Error())
}
utils.GinResponse(c).OKWithData(data)
}
// GetPublicNetworkIP
// @description: 获取当前机器的公网IP
// @receiver setting
// @param c
func (setting) GetPublicNetworkIP(c *gin.Context) {
utils.GinResponse(c).OKWithData(map[string]string{
"IP": utils.Network().GetHostPublicIP(),
})
}
// GetServerRestartRule
// @description: 获取服务重启规则
// @receiver setting
// @param c
func (setting) GetServerRestartRule(c *gin.Context) {
utils.GinResponse(c).OKWithData(map[string]string{
"rule": config.Config.Wireguard.ListenConfig,
})
}

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

@ -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()
}

@ -1,84 +0,0 @@
package middleware
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"slices"
"strings"
"time"
"wireguard-ui/component"
"wireguard-ui/global/constant"
"wireguard-ui/http/response"
"wireguard-ui/service"
"wireguard-ui/utils"
)
// Authorization
// @description: 授权中间件
// @return gin.HandlerFunc
func Authorization() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" || !strings.HasPrefix(token, "Bearer ") {
response.R(c).AuthorizationFailed("未登陆")
c.Abort()
return
}
hashPassword := c.Request.Header.Get("X-TOKEN")
if hashPassword == "" {
response.R(c).AuthorizationFailed("未登陆")
c.Abort()
return
}
userClaims, err := component.JWT().ParseToken(token, hashPassword, "http")
if err != nil {
response.R(c).AuthorizationFailed("未登陆")
c.Abort()
return
}
// 如果token的颁发者与请求的站点不一致那么就给它抬出去
if !slices.Contains(strings.Split(userClaims.Issuer, ","), utils.WebSite().GetHost(c.Request.Header.Get("Referer"))) {
response.R(c).AuthorizationFailed("未登陆")
c.Abort()
return
}
// 查询用户
user, err := service.User().GetUserById(userClaims.ID)
if err != nil {
response.R(c).AuthorizationFailed("用户不存在")
c.Abort()
return
}
if user.Status != constant.Enabled {
response.R(c).AuthorizationFailed("用户状态异常,请联系管理员处理!")
c.Abort()
return
}
// 将用户信息放入上下文
c.Set("user", &user)
if c.Request.RequestURI == "/api/user/logout" {
c.Next()
}
// 生成一个新token
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())
if err != nil {
response.R(c).AuthorizationFailed("校验失败")
c.Abort()
return
}
c.Writer.Header().Set("Authorization", fmt.Sprintf("Bearer %s", tokenStr))
c.Writer.Header().Set("X-TOKEN", secret)
c.Next()
}
}

10
http/param/base.go Normal file

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

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

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

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

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

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

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

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

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

@ -1,19 +0,0 @@
package router
import (
"github.com/gin-gonic/gin"
"wireguard-ui/http/api"
"wireguard-ui/http/middleware"
)
// DashboardApi
// @description: 控制台相关接口
// @param r
func DashboardApi(r *gin.RouterGroup) {
dashboard := r.Group("dashboard", middleware.Authorization(), middleware.RequestLog())
{
dashboard.GET("/request/list", api.Dashboard().List) // 请求日志
dashboard.GET("/daily-poetry", api.Dashboard().DailyPoetry) // 每日诗词
dashboard.GET("/connections", api.Dashboard().ConnectionList) // 客户端列表列表
}
}

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

@ -1,23 +0,0 @@
package router
import (
"github.com/gin-gonic/gin"
"wireguard-ui/http/api"
"wireguard-ui/http/middleware"
)
// SettingApi
// @description: 设置相关API
// @param r
func SettingApi(r *gin.RouterGroup) {
setting := r.Group("setting", middleware.Authorization(), middleware.RequestLog())
{
setting.POST("", api.Setting().Set) // 新增/编辑设置
setting.DELETE("/:code", api.Setting().Delete) // 删除配置
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) // 导入配置
}
}

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

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

@ -1,6 +0,0 @@
package vo
type Poetry struct {
Content string `json:"content"`
Author string `json:"author"`
}

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

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

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

@ -3,23 +3,21 @@ package initialize
import (
"fmt"
"gitee.ltd/lxh/logger"
"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"
"github.com/spf13/viper"
"golang.zx2c4.com/wireguard/wgctrl"
"gopkg.in/yaml.v3"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm"
gl "gorm.io/gorm/logger"
"log"
"os"
"time"
"wireguard-ui/config"
"wireguard-ui/global/client"
"wireguard-dashboard/client"
"wireguard-dashboard/config"
)
// Init
@ -37,27 +35,15 @@ func Init() {
// initConfig
// @description: 初始化配置
func initConfig() {
vp := viper.New()
vp.SetConfigFile("app.yaml")
if err := vp.ReadInConfig(); err != nil {
configBytes, err := os.ReadFile("app.yaml")
if err != nil {
log.Panicf("读取配置文件失败: %v", err.Error())
}
if err := vp.Unmarshal(&config.Config); err != nil {
err = yaml.Unmarshal(configBytes, &config.Config)
if err != nil {
log.Panicf("解析配置文件失败: %v", err.Error())
}
vp.OnConfigChange(func(in fsnotify.Event) {
if err := vp.Unmarshal(&config.Config); err != nil {
log.Errorf("配置文件变动,读取失败: %v", err.Error())
} else {
initDatabase()
initRedis()
initOSS()
}
})
vp.WatchConfig()
}
// InitWireguard
@ -86,6 +72,9 @@ func initDatabase() {
}
logLevel := gl.Info
//if os.Getenv("GIN_MODE") == "release" {
// logLevel = gl.Error
//}
db, err := gorm.Open(dbDialector, &gorm.Config{
Logger: logger.NewGormLoggerWithConfig(gl.Config{
@ -107,9 +96,9 @@ func initDatabase() {
// @description: 初始化redis
func initRedis() {
c := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", config.Config.Cache.Host, config.Config.Cache.Port),
Password: config.Config.Cache.Password,
DB: config.Config.Cache.Db,
Addr: fmt.Sprintf("%s:%d", config.Config.Redis.Host, config.Config.Redis.Port),
Password: config.Config.Redis.Password,
DB: config.Config.Redis.Db,
})
client.Redis = c
@ -142,14 +131,8 @@ func initOSS() {
// initLogger
// @description: 初始化日志
func initLogger() {
mode := logger.Dev
if os.Getenv("GIN_MODE") == gin.ReleaseMode {
mode = logger.Prod
}
logger.InitLogger(logger.LogConfig{
Mode: mode,
Mode: logger.Dev,
FileEnable: true,
})
}

65
main.go

@ -1,56 +1,49 @@
package main
import (
"fmt"
"gitee.ltd/lxh/logger/log"
"github.com/urfave/cli/v2"
"github.com/gin-contrib/pprof"
"math/rand"
"os"
"sort"
"net/http"
"time"
tui "wireguard-ui/cli"
"wireguard-ui/cron"
"wireguard-ui/http"
"wireguard-ui/initialize"
"wireguard-ui/script"
"wireguard-dashboard/config"
"wireguard-dashboard/cron_task"
"wireguard-dashboard/initialize"
"wireguard-dashboard/queues"
"wireguard-dashboard/route"
"wireguard-dashboard/script"
)
func init() {
initialize.Init()
if err := script.New().Do(); err != nil {
initialize.Init() // 初始化
if err := script.NewScript().Do(); err != nil {
log.Errorf("执行脚本失败: %v", err.Error())
}
cron.Task()
go queues.StartConsumer() // 启动队列
go cron_task.StartCronTask() // 启动定时任务
}
func main() {
rand.New(rand.NewSource(time.Now().Local().UnixNano()))
route.IncludeRouters(
route.CaptchaApi,
route.UserApi,
route.ServerApi,
route.ClientApi,
route.SettingApi,
route.DashboardApi,
)
handler := route.InitRouter()
app := &cli.App{
Name: "wireguard-ui",
Usage: "wireguard-manager-ui",
pprof.Register(handler)
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()
},
},
}
sort.Sort(cli.CommandsByName(app.Commands))
if err := app.Run(os.Args); err != nil {
log.Fatalf("服务启动失败: %v", err.Error())
if err := httpServe.ListenAndServe(); err != nil {
log.Panicf("启动http服务端失败: %v", err.Error())
}
}

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

30
middleware/permission.go Normal file

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

@ -9,9 +9,8 @@ import (
"regexp"
"strings"
"time"
"wireguard-ui/http/vo"
"wireguard-ui/model"
"wireguard-ui/service"
"wireguard-dashboard/model/entity"
"wireguard-dashboard/repository"
)
// bodyWriter
@ -21,11 +20,11 @@ type bodyWriter struct {
body *bytes.Buffer
}
func RequestLog() gin.HandlerFunc {
func SystemLogRequest() gin.HandlerFunc {
return func(c *gin.Context) {
var userId string
if userInfo, ok := c.Get("user"); ok {
userId = userInfo.(*vo.User).Id
userId = userInfo.(*entity.User).Id
}
// 开始时间
@ -33,7 +32,7 @@ func RequestLog() gin.HandlerFunc {
host := c.Request.Host // 请求域名
path := c.Request.URL.Path // 接口地址
query := c.Request.URL.RawQuery // 参数
if strings.Contains(path, "/api/dashboard/request/list") {
if strings.Contains(path, "/api/dashboard/list") {
c.Next()
return
}
@ -52,16 +51,16 @@ 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头
} else if c.Request.Header.Get("X-Forwarded-For") != "" {
ip = c.Request.Header.Get("X-Forwarded-For") // 这个是网关Nginx自定义的Header头
if c.Request.Header.Get("U-Real-Ip") != "" {
ip = c.Request.Header.Get("U-Real-Ip") // 这个是网关Nginx自定义的Header头
} else if c.Request.Header.Get("U-Forwarded-For") != "" {
ip = c.Request.Header.Get("U-Forwarded-For") // 这个是网关Nginx自定义的Header头
}
ua := c.Request.UserAgent() // UA
// 重写客户端IP
c.Request.Header.Set("X-Forwarded-For", ip)
c.Request.Header.Set("X-Real-IP", ip)
c.Request.Header.Set("X-Real-Ip", ip)
// 拦截response
bw := &bodyWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
@ -76,7 +75,7 @@ func RequestLog() gin.HandlerFunc {
cost := time.Since(start).Milliseconds()
// 组装实体
l := model.RequestLog{
l := entity.SystemLog{
UserId: userId,
ClientIP: ip,
Host: host,
@ -99,7 +98,7 @@ func RequestLog() gin.HandlerFunc {
}
go func() {
if er := service.Log().CreateLog(l); er != nil {
if er := repository.SystemLog().SaveLog(&l); er != nil {
log.Debugf("请求日志: %+v", l)
log.Errorf("保存请求日志失败: %v", er)
}

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

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

20
model/entity/user.go Normal file

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

45
model/entity/wireguard.go Normal file

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

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

@ -1,24 +1,13 @@
package render_data
type ServerSetting struct {
EndpointAddress string `json:"endpointAddress"`
DnsServer []string `json:"dnsServer"`
MTU int `json:"MTU"`
PersistentKeepalive int `json:"persistentKeepalive"`
FirewallMark string `json:"firewallMark"`
Table string `json:"table"`
ConfigFilePath string `json:"configFilePath"`
}
package template_data
type Server struct {
Address []string `json:"ipScope"`
ListenPort uint64 `json:"listenPort"`
Address string `json:"address"`
ListenPort int `json:"listenPort"`
PrivateKey string `json:"privateKey"`
PublicKey string `json:"publicKey" `
MTU int `json:"MTU"`
PostUp string `json:"postUpScript"`
PreDown string `json:"preDownScript"`
PostDown string `json:"postDownScript"`
MTU int `json:"mtu"`
PostUp string `json:"postUp"`
PreDown string `json:"preDown"`
PostDown string `json:"postDown"`
Table string `json:"table"`
Clients []Client `json:"clients"`
}
@ -30,13 +19,12 @@ type Client struct {
PublicKey string `json:"publicKey"`
PresharedKey string `json:"presharedKey"`
AllowedIPS string `json:"allowedIps"`
PersistentKeepalive int `json:"persistentKeepalive"`
PersistentKeepalive string `json:"persistentKeepalive"`
Endpoint string `json:"endpoint"`
CreateUser string `json:"createUser"`
Enabled bool `json:"enabled"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
SyncAt string `json:"syncAt"`
}
type Keys struct {

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

41
model/vo/client.go Normal file

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

14
model/vo/dashboard.go Normal file

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

11
model/vo/system.go Normal file

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

20
model/vo/user.go Normal file

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

14
model/vo/wireguard.go Normal file

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

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

119
queues/async_wg_config.go Normal file

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

10
queues/consumer.go Normal file

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

16
queues/producer.go Normal file

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

235
repository/client.go Normal file

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

66
repository/server.go Normal file

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

64
repository/system.go Normal file

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

50
repository/system_log.go Normal file

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

142
repository/user.go Normal file

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

17
route/captcha.go Normal file

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

23
route/client.go Normal file

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

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