Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
6f249d20b0 | |||
67f394f136 | |||
d1bb49c208 |
@@ -7,6 +7,6 @@ type config struct {
|
||||
Database *database `yaml:"database"`
|
||||
Redis *redis `yaml:"redis"`
|
||||
File *file `yaml:"file"`
|
||||
Mail *mail `yaml:"mail"`
|
||||
Mail *mail `yaml:"email"`
|
||||
Wireguard *wireguard `yaml:"wireguard"`
|
||||
}
|
||||
|
5
go.mod
5
go.mod
@@ -11,6 +11,8 @@ require (
|
||||
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.5.1
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
@@ -18,7 +20,6 @@ require (
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||
gopkg.in/fsnotify.v1 v1.4.7
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/mysql v1.5.4
|
||||
gorm.io/driver/postgres v1.5.6
|
||||
@@ -58,7 +59,6 @@ require (
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/jpillora/backoff v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/lixh00/loki-client-go v1.0.1 // indirect
|
||||
@@ -99,7 +99,6 @@ require (
|
||||
google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55 // indirect
|
||||
google.golang.org/grpc v1.50.1 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
|
6
go.sum
6
go.sum
@@ -498,6 +498,8 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
|
||||
@@ -1273,8 +1275,6 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -1286,8 +1286,6 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE=
|
||||
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
|
@@ -202,49 +202,15 @@ func (clients) Download(c *gin.Context) {
|
||||
var keys template_data.Keys
|
||||
_ = json.Unmarshal([]byte(data.Keys), &keys)
|
||||
|
||||
setting, err := repository.System().GetServerSetting()
|
||||
serverSetting, err := repository.System().GetServerSetting()
|
||||
if err != nil {
|
||||
utils.GinResponse(c).FailedWithMsg("获取设置失败")
|
||||
return
|
||||
}
|
||||
|
||||
var serverDNS []string
|
||||
if *data.UseServerDns == 1 {
|
||||
serverDNS = setting.DnsServer
|
||||
}
|
||||
|
||||
// 处理一下数据
|
||||
execData := template_data.ClientConfig{
|
||||
PrivateKey: keys.PrivateKey,
|
||||
IpAllocation: data.IpAllocation,
|
||||
MTU: setting.MTU,
|
||||
DNS: strings.Join(serverDNS, ","),
|
||||
PublicKey: data.Server.PublicKey,
|
||||
PresharedKey: keys.PresharedKey,
|
||||
AllowedIPS: data.AllowedIps,
|
||||
Endpoint: setting.EndpointAddress,
|
||||
ListenPort: data.Server.ListenPort,
|
||||
PersistentKeepalive: setting.PersistentKeepalive,
|
||||
}
|
||||
|
||||
// 不同环境下处理文件路径
|
||||
var outPath = "/tmp/" + fmt.Sprintf("%s.conf", data.Name)
|
||||
var templatePath = "./template/wg.client.conf"
|
||||
if os.Getenv("GIN_MODE") != "release" {
|
||||
outPath = "E:\\Workspace\\Go\\wireguard-dashboard\\template\\" + fmt.Sprintf("%s.conf", data.Name)
|
||||
templatePath = "E:\\Workspace\\Go\\wireguard-dashboard\\template\\wg.client.conf"
|
||||
}
|
||||
|
||||
// 渲染数据
|
||||
parseTemplate, err := utils.Template().Parse(templatePath)
|
||||
outPath, err := utils.Wireguard().GenerateClientFile(&data, serverSetting)
|
||||
if err != nil {
|
||||
utils.GinResponse(c).FailedWithMsg("读取模板文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
err = utils.Template().Execute(parseTemplate, execData, outPath)
|
||||
if err != nil {
|
||||
utils.GinResponse(c).FailedWithMsg("文件渲染失败")
|
||||
utils.GinResponse(c).FailedWithErr("生成失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -279,49 +245,15 @@ func (clients) GenerateQrCode(c *gin.Context) {
|
||||
var keys template_data.Keys
|
||||
_ = json.Unmarshal([]byte(data.Keys), &keys)
|
||||
|
||||
setting, err := repository.System().GetServerSetting()
|
||||
serverSetting, err := repository.System().GetServerSetting()
|
||||
if err != nil {
|
||||
utils.GinResponse(c).FailedWithMsg("获取设置失败")
|
||||
return
|
||||
}
|
||||
|
||||
var serverDNS []string
|
||||
if *data.UseServerDns == 1 {
|
||||
serverDNS = setting.DnsServer
|
||||
}
|
||||
|
||||
// 处理一下数据
|
||||
execData := template_data.ClientConfig{
|
||||
PrivateKey: keys.PrivateKey,
|
||||
IpAllocation: data.IpAllocation,
|
||||
MTU: setting.MTU,
|
||||
DNS: strings.Join(serverDNS, ","),
|
||||
PublicKey: data.Server.PublicKey,
|
||||
PresharedKey: keys.PresharedKey,
|
||||
AllowedIPS: data.AllowedIps,
|
||||
Endpoint: setting.EndpointAddress,
|
||||
ListenPort: data.Server.ListenPort,
|
||||
PersistentKeepalive: setting.PersistentKeepalive,
|
||||
}
|
||||
|
||||
// 不同环境下处理文件路径
|
||||
var outPath = "/tmp/" + fmt.Sprintf("%s.conf", data.Name)
|
||||
var templatePath = "./template/wg.client.conf"
|
||||
if os.Getenv("GIN_MODE") != "release" {
|
||||
outPath = "E:\\Workspace\\Go\\wireguard-dashboard\\template\\" + fmt.Sprintf("%s.conf", data.Name)
|
||||
templatePath = "E:\\Workspace\\Go\\wireguard-dashboard\\template\\wg.client.conf"
|
||||
}
|
||||
|
||||
// 渲染数据
|
||||
parseTemplate, err := utils.Template().Parse(templatePath)
|
||||
outPath, err := utils.Wireguard().GenerateClientFile(&data, serverSetting)
|
||||
if err != nil {
|
||||
utils.GinResponse(c).FailedWithMsg("读取模板文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
err = utils.Template().Execute(parseTemplate, execData, outPath)
|
||||
if err != nil {
|
||||
utils.GinResponse(c).FailedWithMsg("文件渲染失败")
|
||||
utils.GinResponse(c).FailedWithErr("生成失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -347,6 +279,59 @@ func (clients) GenerateQrCode(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@@ -1,8 +1,11 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"gitee.ltd/lxh/logger/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"strings"
|
||||
"wireguard-dashboard/client"
|
||||
"wireguard-dashboard/component"
|
||||
"wireguard-dashboard/constant"
|
||||
@@ -157,6 +160,30 @@ func (user) Save(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 只有修改才有头像值
|
||||
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,
|
||||
@@ -250,3 +277,17 @@ func (user) DeleteUser(c *gin.Context) {
|
||||
|
||||
utils.GinResponse(c).OK()
|
||||
}
|
||||
|
||||
// ChangeAvatar
|
||||
// @description: 切换头像
|
||||
// @receiver user
|
||||
// @param c
|
||||
func (user) ChangeAvatar(c *gin.Context) {
|
||||
avatar, err := utils.Avatar().GenerateAvatar(false)
|
||||
if err != nil {
|
||||
utils.GinResponse(c).FailedWithErr("生成头像失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
utils.GinResponse(c).OKWithData(fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString([]byte(avatar))))
|
||||
}
|
||||
|
@@ -35,6 +35,7 @@ type SaveClient struct {
|
||||
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
|
||||
|
@@ -35,6 +35,7 @@ type Client struct {
|
||||
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"`
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ type Client struct {
|
||||
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"`
|
||||
}
|
||||
|
@@ -3,5 +3,8 @@ package queues
|
||||
// StartConsumer
|
||||
// @description: 启动消费者
|
||||
func StartConsumer() {
|
||||
// 同步配置文件
|
||||
go asyncWireguardConfigFile()
|
||||
// 离线监听
|
||||
go offlineMonitoring()
|
||||
}
|
||||
|
65
queues/offline_monitoring.go
Normal file
65
queues/offline_monitoring.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package queues
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitee.ltd/lxh/logger/log"
|
||||
"strings"
|
||||
"time"
|
||||
"wireguard-dashboard/client"
|
||||
"wireguard-dashboard/repository"
|
||||
"wireguard-dashboard/utils"
|
||||
)
|
||||
|
||||
// offlineMonitoring
|
||||
// @description: 离线监听任务
|
||||
func offlineMonitoring() {
|
||||
for {
|
||||
devices, err := client.WireguardClient.Devices()
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Minute) // 休眠五分钟再执行
|
||||
offlineMonitoring()
|
||||
continue
|
||||
}
|
||||
|
||||
// 遍历客户端数据,并渲染数据信息
|
||||
for _, d := range devices {
|
||||
for _, p := range d.Peers {
|
||||
clientInfo, err := repository.Client().GetByPublicKey(p.PublicKey.String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 没有启用离线监听时,即使客户端已经离线则也不执行
|
||||
if *clientInfo.OfflineMonitoring != 1 || clientInfo.Email == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
content := fmt.Sprintf("客户端:%s\r\n", clientInfo.Name)
|
||||
content += fmt.Sprintf("客户端IP:%s\r\n", ipAllocation)
|
||||
content += fmt.Sprintf("端点IP:%s", p.Endpoint.String())
|
||||
content += fmt.Sprintf("最后握手时间:%s\r\n", p.LastHandshakeTime.Format("2006-01-02 15:04:05"))
|
||||
content += fmt.Sprintf("离线时间:%s\r\n", time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
// 离线并且配置了邮箱,准备发送邮件
|
||||
err = utils.Mail().SendMail(clientInfo.Email, fmt.Sprintf("客户端离线通知"), content, "")
|
||||
if err != nil {
|
||||
log.Errorf("发送离线通知邮件失败: %v", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -38,7 +38,7 @@ func (r clientRepo) List(p param.ClientList) (data []vo.Client, total int64, err
|
||||
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.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+"%")
|
||||
@@ -104,6 +104,7 @@ func (r clientRepo) Save(p param.SaveClient, adminId string) (client *entity.Cli
|
||||
EnableAfterCreation: p.EnabledAfterCreation,
|
||||
UserId: adminId,
|
||||
Enabled: p.Enabled,
|
||||
OfflineMonitoring: p.OfflineMonitoring,
|
||||
}
|
||||
|
||||
// id不为空,更新信息
|
||||
@@ -113,7 +114,7 @@ func (r clientRepo) Save(p param.SaveClient, adminId string) (client *entity.Cli
|
||||
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").
|
||||
"user_id", "enabled", "offline_monitoring").
|
||||
Updates(ent).Error; err != nil {
|
||||
return
|
||||
}
|
||||
@@ -186,6 +187,7 @@ func (r clientRepo) Save(p param.SaveClient, adminId string) (client *entity.Cli
|
||||
Keys: string(keysStr),
|
||||
UserId: adminId,
|
||||
Enabled: p.Enabled,
|
||||
OfflineMonitoring: p.OfflineMonitoring,
|
||||
}
|
||||
|
||||
err = r.Model(&entity.Client{}).Create(ent).Error
|
||||
|
@@ -86,7 +86,7 @@ func (r user) Save(ent *entity.User) (err error) {
|
||||
|
||||
// 没有头像就生成一个头像
|
||||
if ent.Avatar == "" {
|
||||
ent.Avatar, _ = utils.Avatar().GenerateAvatar()
|
||||
ent.Avatar, _ = utils.Avatar().GenerateAvatar(true)
|
||||
}
|
||||
|
||||
// 创建
|
||||
|
@@ -14,6 +14,7 @@ func ClientApi(r *gin.RouterGroup) {
|
||||
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
|
||||
|
@@ -23,5 +23,6 @@ func UserApi(r *gin.RouterGroup) {
|
||||
userApi.GET("list", middleware.Permission(), api.UserApi().List) // 用户列表
|
||||
userApi.PUT("change-status", middleware.Permission(), api.UserApi().ChangeUserState) // 变更状态
|
||||
userApi.DELETE("delete/:id", middleware.Permission(), api.UserApi().DeleteUser) // 删除用户
|
||||
userApi.POST("change-avatar", api.UserApi().ChangeAvatar) // 更换头像
|
||||
}
|
||||
}
|
||||
|
@@ -69,7 +69,7 @@ func (s Script) CreateSuperAdmin() error {
|
||||
}
|
||||
|
||||
// 生成一下头像
|
||||
avatarPath, err := utils.Avatar().GenerateAvatar()
|
||||
avatarPath, err := utils.Avatar().GenerateAvatar(true)
|
||||
if err != nil {
|
||||
log.Errorf("生成头像失败: %v", err.Error())
|
||||
return err
|
||||
|
@@ -19,7 +19,7 @@ func Avatar() avatar {
|
||||
// @receiver avatar
|
||||
// @return path
|
||||
// @return err
|
||||
func (avatar) GenerateAvatar() (path string, err error) {
|
||||
func (avatar) GenerateAvatar(isUpload bool) (path string, err error) {
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
r := client.HttpClient.R()
|
||||
result, err := r.Get(fmt.Sprintf("https://api.dicebear.com/7.x/croodles/png?seed=%d&scale=120&size=200&clip=true&randomizeIds=true&beard=variant01,variant02,variant03&"+
|
||||
@@ -30,10 +30,13 @@ func (avatar) GenerateAvatar() (path string, err error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
filePath, err := FileSystem().UploadFile(result.Body(), ".png")
|
||||
if isUpload {
|
||||
path, err = FileSystem().UploadFile(result.Body(), ".png")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return filePath, nil
|
||||
return string(result.Body()), nil
|
||||
}
|
||||
|
105
utils/mail.go
105
utils/mail.go
@@ -2,77 +2,74 @@ package utils
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gitee.ltd/lxh/logger/log"
|
||||
"gopkg.in/gomail.v2"
|
||||
"github.com/jordan-wright/email"
|
||||
"mime"
|
||||
"net/smtp"
|
||||
"net/textproto"
|
||||
"path/filepath"
|
||||
"wireguard-dashboard/config"
|
||||
)
|
||||
|
||||
type mail struct {
|
||||
md *gomail.Dialer
|
||||
*email.Email
|
||||
addr string
|
||||
auth smtp.Auth
|
||||
}
|
||||
|
||||
func Mail() mail {
|
||||
mailDialer := gomail.NewDialer(config.Config.Mail.Host, config.Config.Mail.Port, config.Config.Mail.User, config.Config.Mail.Password)
|
||||
mailDialer.TLSConfig = &tls.Config{
|
||||
InsecureSkipVerify: config.Config.Mail.SkipTls,
|
||||
func Mail() *mail {
|
||||
var m mail
|
||||
em := email.NewEmail()
|
||||
m.Email = em
|
||||
m.auth = smtp.PlainAuth("", config.Config.Mail.User, config.Config.Mail.Password, config.Config.Mail.Host)
|
||||
m.addr = fmt.Sprintf("%s:%d", config.Config.Mail.Host, config.Config.Mail.Port)
|
||||
return &m
|
||||
}
|
||||
|
||||
func (m *mail) VerifyConfig() (err error) {
|
||||
if m == nil {
|
||||
return errors.New("邮件客户端初始化失败")
|
||||
}
|
||||
return mail{mailDialer}
|
||||
|
||||
if m.auth == nil || m.addr == "" {
|
||||
return errors.New("邮件客户端未完成初始化")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendMail
|
||||
// @description: 发送普通邮件
|
||||
// @receiver mail
|
||||
// @param subject
|
||||
// @param toAddress
|
||||
// @param content
|
||||
// @return err
|
||||
func (m mail) SendMail(subject, toAddress, content string) (err error) {
|
||||
msg := gomail.NewMessage()
|
||||
msg.SetHeader("From", msg.FormatAddress(m.md.Username, "wireguard-dashboard"))
|
||||
msg.SetHeader("To", toAddress)
|
||||
msg.SetHeader("Subject", subject)
|
||||
msg.SetBody("text/plain", content)
|
||||
|
||||
if err = m.md.DialAndSend(msg); err != nil {
|
||||
log.Errorf("发送普通邮件失败: %v", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SendMailWithAttach
|
||||
// @description: 发送并携带附件
|
||||
// @description: 发送附件
|
||||
// @receiver m
|
||||
// @param to
|
||||
// @param subject
|
||||
// @param toAddress
|
||||
// @param content
|
||||
// @param attachPath
|
||||
// @param attacheFilePath
|
||||
// @return err
|
||||
func (m mail) SendMailWithAttach(subject, toAddress, content, attachPath string) (err error) {
|
||||
msg := gomail.NewMessage()
|
||||
msg.SetHeader("From", msg.FormatAddress(m.md.Username, "wireguard-dashboard"))
|
||||
msg.SetHeader("To", toAddress)
|
||||
msg.SetHeader("Subject", subject)
|
||||
msg.SetBody("text/plain", content)
|
||||
|
||||
rename := m.getFileName(attachPath)
|
||||
|
||||
msg.Attach(attachPath, gomail.Rename(rename), gomail.SetHeader(map[string][]string{
|
||||
"Content-Disposition": {
|
||||
fmt.Sprintf(`attachment; filename="%s"`, mime.BEncoding.Encode("UTF-8", rename)),
|
||||
},
|
||||
}))
|
||||
|
||||
if err = m.md.DialAndSend(msg); err != nil {
|
||||
log.Errorf("发送普通邮件失败: %v", err.Error())
|
||||
return
|
||||
func (m *mail) SendMail(to, subject, content, attacheFilePath string) (err error) {
|
||||
m.From = fmt.Sprintf("wg-dashboard <%s>", config.Config.Mail.User)
|
||||
m.To = []string{to}
|
||||
m.Subject = subject
|
||||
m.Text = []byte(content)
|
||||
if attacheFilePath != "" {
|
||||
atch, err := m.AttachFile(attacheFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取附件文件失败: %v", err.Error())
|
||||
}
|
||||
emHeader := textproto.MIMEHeader{}
|
||||
emHeader.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, mime.BEncoding.Encode("UTF-8", m.getFileName(attacheFilePath))))
|
||||
atch.Header = emHeader
|
||||
}
|
||||
|
||||
return
|
||||
if config.Config.Mail.SkipTls {
|
||||
return m.Send(m.addr, m.auth)
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{}
|
||||
tlsConfig.InsecureSkipVerify = config.Config.Mail.SkipTls
|
||||
tlsConfig.ServerName = config.Config.Mail.Host
|
||||
|
||||
return m.SendWithTLS(m.addr, m.auth, tlsConfig)
|
||||
}
|
||||
|
||||
// getFileName
|
||||
@@ -80,6 +77,6 @@ func (m mail) SendMailWithAttach(subject, toAddress, content, attachPath string)
|
||||
// @receiver m
|
||||
// @param filePath
|
||||
// @return string
|
||||
func (m mail) getFileName(filePath string) string {
|
||||
func (m *mail) getFileName(filePath string) string {
|
||||
return filepath.Base(filePath)
|
||||
}
|
||||
|
@@ -1,13 +1,19 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/spf13/cast"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
"math/rand"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"wireguard-dashboard/client"
|
||||
"wireguard-dashboard/model/entity"
|
||||
"wireguard-dashboard/model/template_data"
|
||||
"wireguard-dashboard/model/vo"
|
||||
)
|
||||
|
||||
type wireguard struct{}
|
||||
@@ -68,6 +74,58 @@ func (w wireguard) GenerateClientIP(serverIP, rule string, assignedIPS ...string
|
||||
return fmt.Sprintf("%s.%s", prefix, suffix)
|
||||
}
|
||||
|
||||
// GenerateClientFile
|
||||
// @description: 生成客户端临时配置文件
|
||||
// @receiver w
|
||||
// @param clientInfo
|
||||
// @param setting
|
||||
// @return tmpFilePath
|
||||
// @return err
|
||||
func (w wireguard) GenerateClientFile(clientInfo *entity.Client, setting *vo.ServerSetting) (tmpFilePath string, err error) {
|
||||
var keys template_data.Keys
|
||||
_ = json.Unmarshal([]byte(clientInfo.Keys), &keys)
|
||||
|
||||
var serverDNS []string
|
||||
if *clientInfo.UseServerDns == 1 {
|
||||
serverDNS = setting.DnsServer
|
||||
}
|
||||
|
||||
// 处理一下数据
|
||||
execData := template_data.ClientConfig{
|
||||
PrivateKey: keys.PrivateKey,
|
||||
IpAllocation: clientInfo.IpAllocation,
|
||||
MTU: setting.MTU,
|
||||
DNS: strings.Join(serverDNS, ","),
|
||||
PublicKey: clientInfo.Server.PublicKey,
|
||||
PresharedKey: keys.PresharedKey,
|
||||
AllowedIPS: clientInfo.AllowedIps,
|
||||
Endpoint: setting.EndpointAddress,
|
||||
ListenPort: clientInfo.Server.ListenPort,
|
||||
PersistentKeepalive: setting.PersistentKeepalive,
|
||||
}
|
||||
|
||||
// 不同环境下处理文件路径
|
||||
var outPath = "/tmp/" + fmt.Sprintf("%s.conf", clientInfo.Name)
|
||||
var templatePath = "./template/wg.client.conf"
|
||||
if os.Getenv("GIN_MODE") != "release" {
|
||||
outPath = "E:\\Workspace\\Go\\wireguard-dashboard\\template\\" + fmt.Sprintf("%s.conf", clientInfo.Name)
|
||||
templatePath = "E:\\Workspace\\Go\\wireguard-dashboard\\template\\wg.client.conf"
|
||||
}
|
||||
|
||||
// 渲染数据
|
||||
parseTemplate, err := Template().Parse(templatePath)
|
||||
if err != nil {
|
||||
return "", errors.New("读取模板文件失败")
|
||||
}
|
||||
|
||||
err = Template().Execute(parseTemplate, execData, outPath)
|
||||
if err != nil {
|
||||
return "", errors.New("文件渲染失败")
|
||||
}
|
||||
|
||||
return outPath, nil
|
||||
}
|
||||
|
||||
// random
|
||||
// @description: 随机模式
|
||||
// @receiver w
|
||||
|
18
web-src/.vscode/extensions.json
vendored
18
web-src/.vscode/extensions.json
vendored
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"christian-kohler.path-intellisense",
|
||||
"vscode-icons-team.vscode-icons",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"stylelint.vscode-stylelint",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"redhat.vscode-yaml",
|
||||
"csstools.postcss",
|
||||
"mikestead.dotenv",
|
||||
"eamodio.gitlens",
|
||||
"antfu.iconify",
|
||||
"Vue.volar"
|
||||
]
|
||||
}
|
31
web-src/.vscode/settings.json
vendored
31
web-src/.vscode/settings.json
vendored
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"editor.formatOnType": true,
|
||||
"editor.formatOnSave": true,
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.tabSize": 2,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.guides.bracketPairs": "active",
|
||||
"files.autoSave": "afterDelay",
|
||||
"git.confirmSync": false,
|
||||
"workbench.startupEditor": "newUntitledFile",
|
||||
"editor.suggestSelection": "first",
|
||||
"editor.acceptSuggestionOnCommitCharacter": false,
|
||||
"css.lint.propertyIgnoredDueToDisplay": "ignore",
|
||||
"editor.quickSuggestions": {
|
||||
"other": true,
|
||||
"comments": true,
|
||||
"strings": true
|
||||
},
|
||||
"files.associations": {
|
||||
"editor.snippetSuggestions": "top"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"iconify.excludes": ["el"]
|
||||
}
|
22
web-src/.vscode/vue3.0.code-snippets
vendored
22
web-src/.vscode/vue3.0.code-snippets
vendored
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"Vue3.0快速生成模板": {
|
||||
"scope": "vue",
|
||||
"prefix": "Vue3.0",
|
||||
"body": [
|
||||
"<template>",
|
||||
"\t<div>test</div>",
|
||||
"</template>\n",
|
||||
"<script lang='ts'>",
|
||||
"export default {",
|
||||
"\tsetup() {",
|
||||
"\t\treturn {}",
|
||||
"\t}",
|
||||
"}",
|
||||
"</script>\n",
|
||||
"<style lang='scss' scoped>\n",
|
||||
"</style>",
|
||||
"$2"
|
||||
],
|
||||
"description": "Vue3.0"
|
||||
}
|
||||
}
|
17
web-src/.vscode/vue3.2.code-snippets
vendored
17
web-src/.vscode/vue3.2.code-snippets
vendored
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"Vue3.2+快速生成模板": {
|
||||
"scope": "vue",
|
||||
"prefix": "Vue3.2+",
|
||||
"body": [
|
||||
"<script setup lang='ts'>",
|
||||
"</script>\n",
|
||||
"<template>",
|
||||
"\t<div>test</div>",
|
||||
"</template>\n",
|
||||
"<style lang='scss' scoped>\n",
|
||||
"</style>",
|
||||
"$2"
|
||||
],
|
||||
"description": "Vue3.2+"
|
||||
}
|
||||
}
|
20
web-src/.vscode/vue3.3.code-snippets
vendored
20
web-src/.vscode/vue3.3.code-snippets
vendored
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"Vue3.3+defineOptions快速生成模板": {
|
||||
"scope": "vue",
|
||||
"prefix": "Vue3.3+",
|
||||
"body": [
|
||||
"<script setup lang='ts'>",
|
||||
"defineOptions({",
|
||||
"\tname: ''",
|
||||
"})",
|
||||
"</script>\n",
|
||||
"<template>",
|
||||
"\t<div>test</div>",
|
||||
"</template>\n",
|
||||
"<style lang='scss' scoped>\n",
|
||||
"</style>",
|
||||
"$2"
|
||||
],
|
||||
"description": "Vue3.3+defineOptions快速生成模板"
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"Version": "5.5.0",
|
||||
"Title": "WG-Dashboard",
|
||||
"Title": "wg-dashboard",
|
||||
"FixedHeader": true,
|
||||
"HiddenSideBar": false,
|
||||
"MultiTagsCache": false,
|
||||
|
@@ -47,3 +47,8 @@ export const getClientConnects = () => {
|
||||
export const offlineClient = (id: string) => {
|
||||
return http.request<any>("post", baseUri("/client/offline/" + id));
|
||||
};
|
||||
|
||||
// 发送邮件
|
||||
export const sendMail = (id: string) => {
|
||||
return http.request<any>("post", baseUri("/client/to-email/" + id));
|
||||
};
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { http } from "@/utils/http";
|
||||
import { baseUri } from "@/api/utils";
|
||||
import { data } from "autoprefixer";
|
||||
|
||||
// 获取当前登陆用户信息
|
||||
export const getUser = () => {
|
||||
@@ -30,3 +31,8 @@ export const deleteUser = (userId: string) => {
|
||||
export const changePassword = (data?: object) => {
|
||||
return http.request("post", baseUri("/user/change-password"), { data });
|
||||
};
|
||||
|
||||
// 生成头像
|
||||
export const generateAvatar = () => {
|
||||
return http.request<any>("post", baseUri("/user/change-avatar"));
|
||||
};
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { FormInstance } from "element-plus";
|
||||
import { generateAvatar } from "@/api/user";
|
||||
|
||||
// 声明 props 类型
|
||||
export interface FormProps {
|
||||
@@ -41,11 +42,29 @@ function getUserEditFormRef() {
|
||||
return userEditFormRef.value;
|
||||
}
|
||||
|
||||
// 切换头像
|
||||
const changeAvatar = () => {
|
||||
generateAvatar().then(res => {
|
||||
if (res.code === 200) {
|
||||
userEditForm.value.avatar = res.data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({ getUserEditFormRef });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-form ref="userEditFormRef" :model="userEditForm" label-width="20%">
|
||||
<el-form-item prop="avatar">
|
||||
<el-avatar
|
||||
style="cursor: pointer; margin-left: 25%"
|
||||
size="large"
|
||||
fit="cover"
|
||||
:src="userEditForm.avatar"
|
||||
@click="changeAvatar()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
prop="name"
|
||||
label="名称"
|
||||
|
@@ -1,9 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
deleteClient,
|
||||
downloadClient, generateClientIP,
|
||||
downloadClient,
|
||||
generateClientIP,
|
||||
getClients,
|
||||
saveClient
|
||||
saveClient,
|
||||
sendMail
|
||||
} from "@/api/clients";
|
||||
import { h, reactive, ref } from "vue";
|
||||
import { addDialog } from "@/components/ReDialog/index";
|
||||
@@ -14,6 +16,7 @@ import "plus-pro-components/es/components/search/style/css";
|
||||
import { storageLocal } from "@pureadmin/utils";
|
||||
import { ArrowDown } from "@element-plus/icons-vue";
|
||||
import { ElMessageBox } from "element-plus";
|
||||
import { message } from "@/utils/message";
|
||||
|
||||
defineOptions({
|
||||
// name 作为一种规范最好必须写上并且和路由的name保持一致
|
||||
@@ -151,7 +154,7 @@ const openAddClientDialog = () => {
|
||||
}
|
||||
});
|
||||
const serverInfo = storageLocal().getItem("server-info");
|
||||
const restartRule = storageLocal().getItem("restart-rule") ? 1 : 0;
|
||||
const restartRule = !storageLocal().getItem("restart-rule") ? 1 : 0;
|
||||
addDialog({
|
||||
width: "40%",
|
||||
title: "新增",
|
||||
@@ -174,7 +177,8 @@ const openAddClientDialog = () => {
|
||||
publicKey: "",
|
||||
presharedKey: ""
|
||||
},
|
||||
enabled: 1
|
||||
enabled: 1,
|
||||
offlineMonitoring: 0
|
||||
}
|
||||
},
|
||||
beforeSure: (done, { options }) => {
|
||||
@@ -213,7 +217,8 @@ const openEditClientDialog = (client?: any) => {
|
||||
useServerDNS: client.useServerDNS,
|
||||
enableAfterCreation: client.enableAfterCreation,
|
||||
keys: client.keys,
|
||||
enabled: Number(client.enabled)
|
||||
enabled: Number(client.enabled),
|
||||
offlineMonitoring: client.offlineMonitoring
|
||||
}
|
||||
},
|
||||
beforeSure: (done, { options }) => {
|
||||
@@ -279,6 +284,15 @@ const ellipsis = (str: string) => {
|
||||
return str;
|
||||
};
|
||||
|
||||
// 发送到邮件
|
||||
const sendToEmail = (clientID: string) => {
|
||||
sendMail(clientID).then(res => {
|
||||
if (res.code === 200) {
|
||||
message("发送邮件成功", { type: "success" });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
getClientsApi(clientSearchForm.value);
|
||||
</script>
|
||||
|
||||
@@ -309,7 +323,10 @@ getClientsApi(clientSearchForm.value);
|
||||
</div>
|
||||
<div class="content">
|
||||
<el-card body-style="padding: inherit" shadow="hover">
|
||||
<div class="flex flex-wrap gap-4" style="display: flex;justify-content: center;">
|
||||
<div
|
||||
class="flex flex-wrap gap-4"
|
||||
style="display: flex; justify-content: center"
|
||||
>
|
||||
<el-card
|
||||
v-for="val in clientsList.data"
|
||||
style="float: left; width: 500px"
|
||||
@@ -338,6 +355,11 @@ getClientsApi(clientSearchForm.value);
|
||||
>下载</el-button
|
||||
>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item>
|
||||
<el-button type="success" @click="sendToEmail(val.id)"
|
||||
>邮件</el-button
|
||||
>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item>
|
||||
<el-button
|
||||
type="danger"
|
||||
@@ -418,7 +440,10 @@ getClientsApi(clientSearchForm.value);
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
<div class="paginate" style="background-color: #ffffff; margin-top: 5px">
|
||||
<div
|
||||
class="paginate"
|
||||
style="background-color: #ffffff; margin-top: 5px"
|
||||
>
|
||||
<el-card>
|
||||
<el-pagination
|
||||
small
|
||||
|
@@ -4,7 +4,7 @@ import { FormInstance } from "element-plus";
|
||||
import { storageLocal } from "@pureadmin/utils";
|
||||
import { userKey } from "@/utils/auth";
|
||||
import { clientFormRules } from "@/views/server/component/rules";
|
||||
import {generateClientKeys} from "@/api/clients";
|
||||
import { generateClientKeys } from "@/api/clients";
|
||||
|
||||
// 声明 props 类型
|
||||
export interface DetailFormProps {
|
||||
@@ -26,6 +26,7 @@ export interface DetailFormProps {
|
||||
presharedKey: string;
|
||||
};
|
||||
enabled: number;
|
||||
offlineMonitoring: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -49,7 +50,8 @@ const props = withDefaults(defineProps<DetailFormProps>(), {
|
||||
publicKey: "",
|
||||
presharedKey: ""
|
||||
},
|
||||
enabled: 1
|
||||
enabled: 1,
|
||||
offlineMonitoring: 0
|
||||
})
|
||||
});
|
||||
|
||||
@@ -88,7 +90,10 @@ defineExpose({ getDetailFormRef });
|
||||
<el-input v-model="detailForm.name" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="email" label="邮箱">
|
||||
<el-input v-model="detailForm.email" />
|
||||
<el-input
|
||||
v-model="detailForm.email"
|
||||
placeholder="可用于离线监听通知或接收客户端配置文件"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="subnetRange" label="子网范围">
|
||||
<el-input v-model="detailForm.subnetRange" />
|
||||
@@ -146,27 +151,33 @@ defineExpose({ getDetailFormRef });
|
||||
v-if="detailForm.id === ''"
|
||||
v-model="detailForm.keys.privateKey"
|
||||
/>
|
||||
<el-input v-else disabled v-model="detailForm.keys.privateKey" />
|
||||
<el-input v-else v-model="detailForm.keys.privateKey" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item prop="publicKey" label="公钥">
|
||||
<el-input
|
||||
v-if="detailForm.id === ''"
|
||||
v-model="detailForm.keys.publicKey"
|
||||
/>
|
||||
<el-input v-else disabled v-model="detailForm.keys.publicKey" />
|
||||
<el-input v-else v-model="detailForm.keys.publicKey" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item prop="presharedKey" label="共享密钥">
|
||||
<el-input
|
||||
v-if="detailForm.id === ''"
|
||||
v-model="detailForm.keys.presharedKey"
|
||||
/>
|
||||
<el-input v-else disabled v-model="detailForm.keys.presharedKey" />
|
||||
<el-input v-else v-model="detailForm.keys.presharedKey" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="detailForm.id === ''">
|
||||
<el-button type="primary" size="small" @click="generateClientKeysApi()"
|
||||
>生成密钥对</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
<el-form-item prop="OfflineMonitoring" label="是否启用离线监听">
|
||||
<el-radio-group v-model="detailForm.offlineMonitoring">
|
||||
<el-radio :value="1">是</el-radio>
|
||||
<el-radio :value="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item prop="useServerDNS" label="是否使用服务端DNS">
|
||||
<el-radio-group v-model="detailForm.useServerDNS">
|
||||
<el-radio :value="1">是</el-radio>
|
||||
|
Reference in New Issue
Block a user