Compare commits

..

14 Commits

Author SHA1 Message Date
1bcad97ed2 🐛fix a bug 2024-06-14 13:43:40 +08:00
c2a9618f27 🎨恢复定时任务 2024-06-14 10:25:53 +08:00
8dab976cba 🐛修复CPU负载过高 2024-06-14 08:45:16 +08:00
a424a5e0b3 🎨监听任务更改为定时任务 2024-06-13 17:06:23 +08:00
073bcc4a1d 🎨优化sql日志打印 2024-06-13 15:34:22 +08:00
5df6ccefe2 🐛修复启动的时候报错 2024-06-13 14:57:57 +08:00
6f249d20b0 🎨新增客户端离线通知选项配置以及监听任务 2024-06-13 14:34:03 +08:00
67f394f136 🎨新增用户头像更换、客户端邮件通知 2024-06-13 11:28:02 +08:00
d1bb49c208 🎨优化如果一个页面退出了,其他都退出 2024-06-07 17:09:21 +08:00
92f5f26ad5 🎨调整客户端列表卡片样式 2024-06-07 14:44:47 +08:00
97e6e80d5a 🐛修复不可更改服务端密钥 2024-06-07 11:59:44 +08:00
0d28bd3445 🎨调整客户端检测离线时间为三分钟 2024-06-07 10:46:22 +08:00
c1d81b9af5 🎨 2024-06-07 10:10:24 +08:00
36951a5bb8 🐛fix a bug 2024-06-07 09:54:30 +08:00
36 changed files with 454 additions and 285 deletions

View File

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

17
cron_task/cron.go Normal file
View 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()
}

View File

@@ -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 {
clientInfo, err := repository.Client().GetByPublicKey(p.PublicKey.String())
if err != nil {
continue
}
// 没有启用离线监听时,即使客户端已经离线则也不执行
if clientInfo.OfflineMonitoring == nil || *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\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
}
}
}
}

11
go.mod
View File

@@ -8,9 +8,12 @@ require (
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.1
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
github.com/go-co-op/gocron/v2 v2.5.0
github.com/go-resty/resty/v2 v2.11.0 github.com/go-resty/resty/v2 v2.11.0
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/json-iterator/go v1.1.12
github.com/mojocn/base64Captcha v1.3.6 github.com/mojocn/base64Captcha v1.3.6
github.com/redis/go-redis/v9 v9.5.1 github.com/redis/go-redis/v9 v9.5.1
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
@@ -18,7 +21,6 @@ require (
golang.org/x/crypto v0.21.0 golang.org/x/crypto v0.21.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
gopkg.in/fsnotify.v1 v1.4.7 gopkg.in/fsnotify.v1 v1.4.7
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.4 gorm.io/driver/mysql v1.5.4
gorm.io/driver/postgres v1.5.6 gorm.io/driver/postgres v1.5.6
@@ -56,9 +58,9 @@ require (
github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
github.com/josharian/native v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect
github.com/lixh00/loki-client-go v1.0.1 // indirect github.com/lixh00/loki-client-go v1.0.1 // indirect
@@ -80,6 +82,7 @@ require (
github.com/prometheus/procfs v0.8.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect
github.com/prometheus/prometheus v1.8.2-0.20201028100903-3245b3267b24 // indirect github.com/prometheus/prometheus v1.8.2-0.20201028100903-3245b3267b24 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
@@ -87,10 +90,11 @@ require (
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.27.0 // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
golang.org/x/image v0.15.0 // indirect golang.org/x/image v0.15.0 // indirect
golang.org/x/net v0.21.0 // indirect golang.org/x/net v0.21.0 // indirect
golang.org/x/oauth2 v0.1.0 // indirect golang.org/x/oauth2 v0.1.0 // indirect
golang.org/x/sync v0.1.0 // indirect golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.18.0 // indirect golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect golang.org/x/time v0.3.0 // indirect
@@ -99,7 +103,6 @@ require (
google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55 // indirect google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55 // indirect
google.golang.org/grpc v1.50.1 // indirect google.golang.org/grpc v1.50.1 // indirect
google.golang.org/protobuf v1.33.0 // 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 gopkg.in/yaml.v2 v2.4.0 // indirect
modernc.org/libc v1.22.5 // indirect modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect modernc.org/mathutil v1.5.0 // indirect

20
go.sum
View File

@@ -217,6 +217,8 @@ github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
github.com/go-co-op/gocron/v2 v2.5.0 h1:ff/TJX9GdTJBDL1il9cyd/Sj3WnS+BB7ZzwHKSNL5p8=
github.com/go-co-op/gocron/v2 v2.5.0/go.mod h1:ckPQw96ZuZLRUGu88vVpd9a6d9HakI14KWahFZtGvNw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -498,6 +500,10 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
@@ -735,6 +741,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qq
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc= github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -797,8 +805,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@@ -892,6 +901,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -992,8 +1003,9 @@ golang.org/x/sync v0.0.0-20200930132711-30421366ff76/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1273,8 +1285,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 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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/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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -1286,8 +1296,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.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE= gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/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/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 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=

View File

@@ -202,49 +202,15 @@ func (clients) Download(c *gin.Context) {
var keys template_data.Keys var keys template_data.Keys
_ = json.Unmarshal([]byte(data.Keys), &keys) _ = json.Unmarshal([]byte(data.Keys), &keys)
setting, err := repository.System().GetServerSetting() serverSetting, err := repository.System().GetServerSetting()
if err != nil { if err != nil {
utils.GinResponse(c).FailedWithMsg("获取设置失败") utils.GinResponse(c).FailedWithMsg("获取设置失败")
return return
} }
var serverDNS []string outPath, err := utils.Wireguard().GenerateClientFile(&data, serverSetting)
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)
if err != nil { if err != nil {
utils.GinResponse(c).FailedWithMsg("读取模板文件失败") utils.GinResponse(c).FailedWithErr("生成失败", err)
return
}
err = utils.Template().Execute(parseTemplate, execData, outPath)
if err != nil {
utils.GinResponse(c).FailedWithMsg("文件渲染失败")
return return
} }
@@ -279,49 +245,15 @@ func (clients) GenerateQrCode(c *gin.Context) {
var keys template_data.Keys var keys template_data.Keys
_ = json.Unmarshal([]byte(data.Keys), &keys) _ = json.Unmarshal([]byte(data.Keys), &keys)
setting, err := repository.System().GetServerSetting() serverSetting, err := repository.System().GetServerSetting()
if err != nil { if err != nil {
utils.GinResponse(c).FailedWithMsg("获取设置失败") utils.GinResponse(c).FailedWithMsg("获取设置失败")
return return
} }
var serverDNS []string outPath, err := utils.Wireguard().GenerateClientFile(&data, serverSetting)
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)
if err != nil { if err != nil {
utils.GinResponse(c).FailedWithMsg("读取模板文件失败") utils.GinResponse(c).FailedWithErr("生成失败", err)
return
}
err = utils.Template().Execute(parseTemplate, execData, outPath)
if err != nil {
utils.GinResponse(c).FailedWithMsg("文件渲染失败")
return 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 // Status
// @description: 获取客户端状态信息,链接状态等 // @description: 获取客户端状态信息,链接状态等
// @receiver clients // @receiver clients
@@ -373,7 +358,7 @@ func (clients) Status(c *gin.Context) {
ipAllocation += iaip.String() + "," ipAllocation += iaip.String() + ","
} }
ipAllocation = strings.TrimRight(ipAllocation, ",") ipAllocation = strings.TrimRight(ipAllocation, ",")
isOnline := time.Since(p.LastHandshakeTime).Minutes() < 1 isOnline := time.Since(p.LastHandshakeTime).Minutes() < 3
data = append(data, vo.ClientStatus{ data = append(data, vo.ClientStatus{
ID: clientInfo.Id, ID: clientInfo.Id,
Name: clientInfo.Name, Name: clientInfo.Name,

View File

@@ -1,8 +1,11 @@
package api package api
import ( import (
"encoding/base64"
"fmt"
"gitee.ltd/lxh/logger/log" "gitee.ltd/lxh/logger/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"strings"
"wireguard-dashboard/client" "wireguard-dashboard/client"
"wireguard-dashboard/component" "wireguard-dashboard/component"
"wireguard-dashboard/constant" "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{ if err := repository.User().Save(&entity.User{
Base: entity.Base{ Base: entity.Base{
Id: p.ID, Id: p.ID,
@@ -250,3 +277,17 @@ func (user) DeleteUser(c *gin.Context) {
utils.GinResponse(c).OK() 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))))
}

View File

@@ -35,6 +35,7 @@ type SaveClient struct {
EnabledAfterCreation *int `json:"enableAfterCreation" form:"enableAfterCreation" 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"` Keys *template_data.Keys `json:"keys" form:"keys" binding:"omitempty"`
Enabled *int `json:"enabled" form:"enabled" binding:"required,oneof=1 0"` Enabled *int `json:"enabled" form:"enabled" binding:"required,oneof=1 0"`
OfflineMonitoring *int `json:"offlineMonitoring" form:"offlineMonitoring" binding:"required,oneof=1 0"`
} }
// ControlServer // ControlServer

View File

@@ -12,8 +12,10 @@ import (
"gorm.io/driver/mysql" "gorm.io/driver/mysql"
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/gorm" "gorm.io/gorm"
gl "gorm.io/gorm/logger"
"log" "log"
"os" "os"
"time"
"wireguard-dashboard/client" "wireguard-dashboard/client"
"wireguard-dashboard/config" "wireguard-dashboard/config"
) )
@@ -69,8 +71,18 @@ func initDatabase() {
dbDialector = sqlite.Open(config.Config.Database.GetDSN()) dbDialector = sqlite.Open(config.Config.Database.GetDSN())
} }
logLevel := gl.Info
if os.Getenv("GIN_MODE") == "release" {
logLevel = gl.Error
}
db, err := gorm.Open(dbDialector, &gorm.Config{ db, err := gorm.Open(dbDialector, &gorm.Config{
Logger: logger.DefaultGormLogger(), Logger: logger.NewGormLoggerWithConfig(gl.Config{
SlowThreshold: time.Second, // Slow SQL threshold
IgnoreRecordNotFoundError: false, // 忽略没找到结果的错误
LogLevel: logLevel, // Log level
Colorful: false, // Disable color
}),
}) })
if err != nil { if err != nil {

View File

@@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"time" "time"
"wireguard-dashboard/config" "wireguard-dashboard/config"
"wireguard-dashboard/cron_task"
"wireguard-dashboard/initialize" "wireguard-dashboard/initialize"
"wireguard-dashboard/queues" "wireguard-dashboard/queues"
"wireguard-dashboard/route" "wireguard-dashboard/route"
@@ -18,7 +19,8 @@ func init() {
if err := script.NewScript().Do(); err != nil { if err := script.NewScript().Do(); err != nil {
log.Errorf("执行脚本失败: %v", err.Error()) log.Errorf("执行脚本失败: %v", err.Error())
} }
go queues.StartConsumer() // 启动队列 go queues.StartConsumer() // 启动队列
go cron_task.StartCronTask() // 启动定时任务
} }
func main() { func main() {

View File

@@ -35,6 +35,7 @@ type Client struct {
Keys string `json:"keys" gorm:"type:text;default null;comment:'公钥和密钥的json串'"` Keys string `json:"keys" gorm:"type:text;default null;comment:'公钥和密钥的json串'"`
UserId string `json:"userId" gorm:"type:char(36);not null;comment:'创建人id'"` 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 - 正常)'"` 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"` User *User `json:"user" gorm:"foreignKey:UserId"`
Server *Server `json:"server" gorm:"foreignKey:ServerId"` Server *Server `json:"server" gorm:"foreignKey:ServerId"`
} }

View File

@@ -23,6 +23,7 @@ type Client struct {
Keys template_data.Keys `json:"keys" gorm:"-"` Keys template_data.Keys `json:"keys" gorm:"-"`
CreateUser string `json:"createUser"` CreateUser string `json:"createUser"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
OfflineMonitoring int `json:"offlineMonitoring"`
CreatedAt entity.JsonTime `json:"createdAt"` CreatedAt entity.JsonTime `json:"createdAt"`
UpdatedAt entity.JsonTime `json:"updatedAt"` UpdatedAt entity.JsonTime `json:"updatedAt"`
} }

View File

@@ -79,7 +79,7 @@ func asyncWireguardConfigFile() {
var renderClients []template_data.Client var renderClients []template_data.Client
for _, v := range serverEnt.Clients { for _, v := range serverEnt.Clients {
// 如果不是确认后创建或者未启用就不写入到wireguard配置文件当中 // 如果不是确认后创建或者未启用就不写入到wireguard配置文件当中
if *v.EnableAfterCreation != 1 || *v.Enabled != 1 { if *v.Enabled != 1 {
continue continue
} }
var clientKey template_data.Keys var clientKey template_data.Keys

View File

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

View File

@@ -38,7 +38,7 @@ func (r clientRepo) List(p param.ClientList) (data []vo.Client, total int64, err
Scopes(utils.Page(p.Current, p.Size)). Scopes(utils.Page(p.Current, p.Size)).
Joins("LEFT JOIN t_user as tu ON twc.user_id = tu.id"). 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", 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 != "" { if p.Name != "" {
sel.Where("twc.name LIKE ?", "%"+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, EnableAfterCreation: p.EnabledAfterCreation,
UserId: adminId, UserId: adminId,
Enabled: p.Enabled, Enabled: p.Enabled,
OfflineMonitoring: p.OfflineMonitoring,
} }
// id不为空更新信息 // id不为空更新信息
@@ -113,7 +114,7 @@ func (r clientRepo) Save(p param.SaveClient, adminId string) (client *entity.Cli
if err = r.Model(&entity.Client{}). if err = r.Model(&entity.Client{}).
Where("id = ?", p.Id).Select("name", "email", "subnet_range", "ip_allocation", Where("id = ?", p.Id).Select("name", "email", "subnet_range", "ip_allocation",
"allowed_ips", "extra_allowed_ips", "endpoint", "use_server_dns", "enable_after_creation", "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 { Updates(ent).Error; err != nil {
return return
} }
@@ -186,6 +187,7 @@ func (r clientRepo) Save(p param.SaveClient, adminId string) (client *entity.Cli
Keys: string(keysStr), Keys: string(keysStr),
UserId: adminId, UserId: adminId,
Enabled: p.Enabled, Enabled: p.Enabled,
OfflineMonitoring: p.OfflineMonitoring,
} }
err = r.Model(&entity.Client{}).Create(ent).Error err = r.Model(&entity.Client{}).Create(ent).Error

View File

@@ -59,6 +59,8 @@ func (r server) Update(p param.SaveServer) (err error) {
"post_up_script": p.PostUpScript, "post_up_script": p.PostUpScript,
"pre_down_script": p.PreDownScript, "pre_down_script": p.PreDownScript,
"post_down_script": p.PostDownScript, "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 return r.Model(&entity.Server{}).Where("id = ?", p.Id).Updates(&update).Error
} }

View File

@@ -86,7 +86,7 @@ func (r user) Save(ent *entity.User) (err error) {
// 没有头像就生成一个头像 // 没有头像就生成一个头像
if ent.Avatar == "" { if ent.Avatar == "" {
ent.Avatar, _ = utils.Avatar().GenerateAvatar() ent.Avatar, _ = utils.Avatar().GenerateAvatar(true)
} }
// 创建 // 创建

View File

@@ -14,6 +14,7 @@ func ClientApi(r *gin.RouterGroup) {
apiGroup.DELETE(":id", middleware.Permission(), api.Client().Delete) // 删除客户端 apiGroup.DELETE(":id", middleware.Permission(), api.Client().Delete) // 删除客户端
apiGroup.POST("download/:id", api.Client().Download) // 下载客户端配置文件 apiGroup.POST("download/:id", api.Client().Download) // 下载客户端配置文件
apiGroup.POST("generate-qrcode/:id", api.Client().GenerateQrCode) // 生成客户端二维码 apiGroup.POST("generate-qrcode/:id", api.Client().GenerateQrCode) // 生成客户端二维码
apiGroup.POST("to-email/:id", api.Client().SendEmail) //发送邮件
apiGroup.GET("status", api.Client().Status) // 获取客户端链接状态监听列表 apiGroup.GET("status", api.Client().Status) // 获取客户端链接状态监听列表
apiGroup.POST("offline/:id", api.Client().Offline) // 强制下线指定客户端 apiGroup.POST("offline/:id", api.Client().Offline) // 强制下线指定客户端
apiGroup.POST("assignIP", api.Client().AssignIPAndAllowedIP) // 分配IP apiGroup.POST("assignIP", api.Client().AssignIPAndAllowedIP) // 分配IP

View File

@@ -23,5 +23,6 @@ func UserApi(r *gin.RouterGroup) {
userApi.GET("list", middleware.Permission(), api.UserApi().List) // 用户列表 userApi.GET("list", middleware.Permission(), api.UserApi().List) // 用户列表
userApi.PUT("change-status", middleware.Permission(), api.UserApi().ChangeUserState) // 变更状态 userApi.PUT("change-status", middleware.Permission(), api.UserApi().ChangeUserState) // 变更状态
userApi.DELETE("delete/:id", middleware.Permission(), api.UserApi().DeleteUser) // 删除用户 userApi.DELETE("delete/:id", middleware.Permission(), api.UserApi().DeleteUser) // 删除用户
userApi.POST("change-avatar", api.UserApi().ChangeAvatar) // 更换头像
} }
} }

View File

@@ -69,7 +69,7 @@ func (s Script) CreateSuperAdmin() error {
} }
// 生成一下头像 // 生成一下头像
avatarPath, err := utils.Avatar().GenerateAvatar() avatarPath, err := utils.Avatar().GenerateAvatar(true)
if err != nil { if err != nil {
log.Errorf("生成头像失败: %v", err.Error()) log.Errorf("生成头像失败: %v", err.Error())
return err return err

View File

@@ -19,7 +19,7 @@ func Avatar() avatar {
// @receiver avatar // @receiver avatar
// @return path // @return path
// @return err // @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())) rand.New(rand.NewSource(time.Now().UnixNano()))
r := client.HttpClient.R() 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&"+ 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 return "", err
} }
filePath, err := FileSystem().UploadFile(result.Body(), ".png") if isUpload {
if err != nil { path, err = FileSystem().UploadFile(result.Body(), ".png")
return "", err if err != nil {
return "", err
}
return
} }
return filePath, nil return string(result.Body()), nil
} }

View File

@@ -2,77 +2,74 @@ package utils
import ( import (
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"gitee.ltd/lxh/logger/log" "github.com/jordan-wright/email"
"gopkg.in/gomail.v2"
"mime" "mime"
"net/smtp"
"net/textproto"
"path/filepath" "path/filepath"
"wireguard-dashboard/config" "wireguard-dashboard/config"
) )
type mail struct { type mail struct {
md *gomail.Dialer *email.Email
addr string
auth smtp.Auth
} }
func Mail() mail { func Mail() *mail {
mailDialer := gomail.NewDialer(config.Config.Mail.Host, config.Config.Mail.Port, config.Config.Mail.User, config.Config.Mail.Password) var m mail
mailDialer.TLSConfig = &tls.Config{ em := email.NewEmail()
InsecureSkipVerify: config.Config.Mail.SkipTls, 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 // SendMail
// @description: 发送普通邮 // @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: 发送并携带附件
// @receiver m // @receiver m
// @param to
// @param subject // @param subject
// @param toAddress // @param attacheFilePath
// @param content
// @param attachPath
// @return err // @return err
func (m mail) SendMailWithAttach(subject, toAddress, content, attachPath string) (err error) { func (m *mail) SendMail(to, subject, content, attacheFilePath string) (err error) {
msg := gomail.NewMessage() m.From = fmt.Sprintf("wg-dashboard <%s>", config.Config.Mail.User)
msg.SetHeader("From", msg.FormatAddress(m.md.Username, "wireguard-dashboard")) m.To = []string{to}
msg.SetHeader("To", toAddress) m.Subject = subject
msg.SetHeader("Subject", subject) m.Text = []byte(content)
msg.SetBody("text/plain", content) if attacheFilePath != "" {
atch, err := m.AttachFile(attacheFilePath)
rename := m.getFileName(attachPath) if err != nil {
return fmt.Errorf("读取附件文件失败: %v", err.Error())
msg.Attach(attachPath, gomail.Rename(rename), gomail.SetHeader(map[string][]string{ }
"Content-Disposition": { emHeader := textproto.MIMEHeader{}
fmt.Sprintf(`attachment; filename="%s"`, mime.BEncoding.Encode("UTF-8", rename)), emHeader.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, mime.BEncoding.Encode("UTF-8", m.getFileName(attacheFilePath))))
}, atch.Header = emHeader
}))
if err = m.md.DialAndSend(msg); err != nil {
log.Errorf("发送普通邮件失败: %v", err.Error())
return
} }
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 // getFileName
@@ -80,6 +77,6 @@ func (m mail) SendMailWithAttach(subject, toAddress, content, attachPath string)
// @receiver m // @receiver m
// @param filePath // @param filePath
// @return string // @return string
func (m mail) getFileName(filePath string) string { func (m *mail) getFileName(filePath string) string {
return filepath.Base(filePath) return filepath.Base(filePath)
} }

View File

@@ -1,13 +1,19 @@
package utils package utils
import ( import (
"encoding/json"
"errors"
"fmt" "fmt"
"github.com/spf13/cast" "github.com/spf13/cast"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"math/rand" "math/rand"
"os"
"slices" "slices"
"strings" "strings"
"wireguard-dashboard/client" "wireguard-dashboard/client"
"wireguard-dashboard/model/entity"
"wireguard-dashboard/model/template_data"
"wireguard-dashboard/model/vo"
) )
type wireguard struct{} type wireguard struct{}
@@ -68,6 +74,58 @@ func (w wireguard) GenerateClientIP(serverIP, rule string, assignedIPS ...string
return fmt.Sprintf("%s.%s", prefix, suffix) 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 // random
// @description: 随机模式 // @description: 随机模式
// @receiver w // @receiver w

View File

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

View File

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

View File

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

View File

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

View File

@@ -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快速生成模板"
}
}

View File

@@ -1,6 +1,6 @@
{ {
"Version": "5.5.0", "Version": "5.5.0",
"Title": "WG-Dashboard", "Title": "wg-dashboard",
"FixedHeader": true, "FixedHeader": true,
"HiddenSideBar": false, "HiddenSideBar": false,
"MultiTagsCache": false, "MultiTagsCache": false,

View File

@@ -47,3 +47,8 @@ export const getClientConnects = () => {
export const offlineClient = (id: string) => { export const offlineClient = (id: string) => {
return http.request<any>("post", baseUri("/client/offline/" + id)); return http.request<any>("post", baseUri("/client/offline/" + id));
}; };
// 发送邮件
export const sendMail = (id: string) => {
return http.request<any>("post", baseUri("/client/to-email/" + id));
};

View File

@@ -1,5 +1,6 @@
import { http } from "@/utils/http"; import { http } from "@/utils/http";
import { baseUri } from "@/api/utils"; import { baseUri } from "@/api/utils";
import { data } from "autoprefixer";
// 获取当前登陆用户信息 // 获取当前登陆用户信息
export const getUser = () => { export const getUser = () => {
@@ -30,3 +31,8 @@ export const deleteUser = (userId: string) => {
export const changePassword = (data?: object) => { export const changePassword = (data?: object) => {
return http.request("post", baseUri("/user/change-password"), { data }); return http.request("post", baseUri("/user/change-password"), { data });
}; };
// 生成头像
export const generateAvatar = () => {
return http.request<any>("post", baseUri("/user/change-avatar"));
};

View File

@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import { FormInstance } from "element-plus"; import { FormInstance } from "element-plus";
import { generateAvatar } from "@/api/user";
// 声明 props 类型 // 声明 props 类型
export interface FormProps { export interface FormProps {
@@ -41,11 +42,29 @@ function getUserEditFormRef() {
return userEditFormRef.value; return userEditFormRef.value;
} }
// 切换头像
const changeAvatar = () => {
generateAvatar().then(res => {
if (res.code === 200) {
userEditForm.value.avatar = res.data;
}
});
};
defineExpose({ getUserEditFormRef }); defineExpose({ getUserEditFormRef });
</script> </script>
<template> <template>
<el-form ref="userEditFormRef" :model="userEditForm" label-width="20%"> <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 <el-form-item
prop="name" prop="name"
label="名称" label="名称"

View File

@@ -132,6 +132,7 @@ const getRule = () => {
rule.value = res.data.rule === "hand"; rule.value = res.data.rule === "hand";
} }
}); });
storageLocal().setItem("restart-rule", rule.value);
return rule; return rule;
}; };

View File

@@ -2,9 +2,9 @@
import { useServerStoreHook } from "@/store/modules/server"; import { useServerStoreHook } from "@/store/modules/server";
import { getSystemLog } from "@/api/dashboard"; import { getSystemLog } from "@/api/dashboard";
import { reactive } from "vue"; import { reactive } from "vue";
import {getClientConnects, offlineClient} from "@/api/clients"; import { getClientConnects, offlineClient } from "@/api/clients";
import {Refresh} from "@element-plus/icons-vue"; import { Refresh } from "@element-plus/icons-vue";
import {message} from "@/utils/message"; import { message } from "@/utils/message";
defineOptions({ defineOptions({
name: "Dashboard" name: "Dashboard"
@@ -87,7 +87,13 @@ getClientsStatus();
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>操作日志</span> <span>操作日志</span>
<el-button style="float: right" type="primary" :icon="Refresh" @click="refreshClick('systemLog')">刷新</el-button> <el-button
style="float: right"
type="primary"
:icon="Refresh"
@click="refreshClick('systemLog')"
>刷新</el-button
>
</div> </div>
</template> </template>
<el-table <el-table
@@ -194,7 +200,13 @@ getClientsStatus();
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>客户端链接状态</span> <span>客户端链接状态</span>
<el-button style="float: right" type="primary" :icon="Refresh" @click="refreshClick('clientStatus')">刷新</el-button> <el-button
style="float: right"
type="primary"
:icon="Refresh"
@click="refreshClick('clientStatus')"
>刷新</el-button
>
</div> </div>
</template> </template>
<el-table <el-table
@@ -256,11 +268,7 @@ getClientsStatus();
min-width="80" min-width="80"
align="center" align="center"
/> />
<el-table-column <el-table-column label="操作" min-width="80" align="center">
label="操作"
min-width="80"
align="center"
>
<template #default="scope"> <template #default="scope">
<el-button <el-button
v-if="scope.row.isOnline" v-if="scope.row.isOnline"

View File

@@ -1,9 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
deleteClient, deleteClient,
downloadClient, generateClientIP, downloadClient,
generateClientIP,
getClients, getClients,
saveClient saveClient,
sendMail
} from "@/api/clients"; } from "@/api/clients";
import { h, reactive, ref } from "vue"; import { h, reactive, ref } from "vue";
import { addDialog } from "@/components/ReDialog/index"; 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 { storageLocal } from "@pureadmin/utils";
import { ArrowDown } from "@element-plus/icons-vue"; import { ArrowDown } from "@element-plus/icons-vue";
import { ElMessageBox } from "element-plus"; import { ElMessageBox } from "element-plus";
import { message } from "@/utils/message";
defineOptions({ defineOptions({
// name 作为一种规范最好必须写上并且和路由的name保持一致 // name 作为一种规范最好必须写上并且和路由的name保持一致
@@ -151,6 +154,7 @@ const openAddClientDialog = () => {
} }
}); });
const serverInfo = storageLocal().getItem("server-info"); const serverInfo = storageLocal().getItem("server-info");
const restartRule = !storageLocal().getItem("restart-rule") ? 1 : 0;
addDialog({ addDialog({
width: "40%", width: "40%",
title: "新增", title: "新增",
@@ -167,13 +171,14 @@ const openAddClientDialog = () => {
extraAllowedIPS: "", extraAllowedIPS: "",
endpoint: "", endpoint: "",
useServerDNS: 0, useServerDNS: 0,
enableAfterCreation: 0, enableAfterCreation: restartRule,
keys: { keys: {
privateKey: "", privateKey: "",
publicKey: "", publicKey: "",
presharedKey: "" presharedKey: ""
}, },
enabled: 1 enabled: 1,
offlineMonitoring: 0
} }
}, },
beforeSure: (done, { options }) => { beforeSure: (done, { options }) => {
@@ -212,7 +217,8 @@ const openEditClientDialog = (client?: any) => {
useServerDNS: client.useServerDNS, useServerDNS: client.useServerDNS,
enableAfterCreation: client.enableAfterCreation, enableAfterCreation: client.enableAfterCreation,
keys: client.keys, keys: client.keys,
enabled: Number(client.enabled) enabled: Number(client.enabled),
offlineMonitoring: client.offlineMonitoring
} }
}, },
beforeSure: (done, { options }) => { beforeSure: (done, { options }) => {
@@ -278,6 +284,15 @@ const ellipsis = (str: string) => {
return str; return str;
}; };
// 发送到邮件
const sendToEmail = (clientID: string) => {
sendMail(clientID).then(res => {
if (res.code === 200) {
message("发送邮件成功", { type: "success" });
}
});
};
getClientsApi(clientSearchForm.value); getClientsApi(clientSearchForm.value);
</script> </script>
@@ -308,10 +323,13 @@ getClientsApi(clientSearchForm.value);
</div> </div>
<div class="content"> <div class="content">
<el-card body-style="padding: inherit" shadow="hover"> <el-card body-style="padding: inherit" shadow="hover">
<div class="flex flex-wrap gap-4"> <div
class="flex flex-wrap gap-4"
style="display: flex; justify-content: center"
>
<el-card <el-card
v-for="val in clientsList.data" v-for="val in clientsList.data"
style="width: 540px" style="float: left; width: 500px"
shadow="hover" shadow="hover"
> >
<template #header> <template #header>
@@ -337,6 +355,11 @@ getClientsApi(clientSearchForm.value);
>下载</el-button >下载</el-button
> >
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item>
<el-button type="success" @click="sendToEmail(val.id)"
>邮件</el-button
>
</el-dropdown-item>
<el-dropdown-item> <el-dropdown-item>
<el-button <el-button
type="danger" type="danger"
@@ -417,7 +440,10 @@ getClientsApi(clientSearchForm.value);
</el-form> </el-form>
</el-card> </el-card>
</div> </div>
<div class="paginate" style="background-color: #ffffff; margin-top: 5px"> <div
class="paginate"
style="background-color: #ffffff; margin-top: 5px"
>
<el-card> <el-card>
<el-pagination <el-pagination
small small

View File

@@ -4,7 +4,7 @@ import { FormInstance } from "element-plus";
import { storageLocal } from "@pureadmin/utils"; import { storageLocal } from "@pureadmin/utils";
import { userKey } from "@/utils/auth"; import { userKey } from "@/utils/auth";
import { clientFormRules } from "@/views/server/component/rules"; import { clientFormRules } from "@/views/server/component/rules";
import {generateClientKeys} from "@/api/clients"; import { generateClientKeys } from "@/api/clients";
// 声明 props 类型 // 声明 props 类型
export interface DetailFormProps { export interface DetailFormProps {
@@ -26,6 +26,7 @@ export interface DetailFormProps {
presharedKey: string; presharedKey: string;
}; };
enabled: number; enabled: number;
offlineMonitoring: number;
}; };
} }
@@ -49,7 +50,8 @@ const props = withDefaults(defineProps<DetailFormProps>(), {
publicKey: "", publicKey: "",
presharedKey: "" presharedKey: ""
}, },
enabled: 1 enabled: 1,
offlineMonitoring: 0
}) })
}); });
@@ -88,7 +90,10 @@ defineExpose({ getDetailFormRef });
<el-input v-model="detailForm.name" /> <el-input v-model="detailForm.name" />
</el-form-item> </el-form-item>
<el-form-item prop="email" label="邮箱"> <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>
<el-form-item prop="subnetRange" label="子网范围"> <el-form-item prop="subnetRange" label="子网范围">
<el-input v-model="detailForm.subnetRange" /> <el-input v-model="detailForm.subnetRange" />
@@ -146,27 +151,33 @@ defineExpose({ getDetailFormRef });
v-if="detailForm.id === ''" v-if="detailForm.id === ''"
v-model="detailForm.keys.privateKey" 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>
<el-form-item prop="publicKey" label="公钥"> <el-form-item prop="publicKey" label="公钥">
<el-input <el-input
v-if="detailForm.id === ''" v-if="detailForm.id === ''"
v-model="detailForm.keys.publicKey" 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>
<el-form-item prop="presharedKey" label="共享密钥"> <el-form-item prop="presharedKey" label="共享密钥">
<el-input <el-input
v-if="detailForm.id === ''" v-if="detailForm.id === ''"
v-model="detailForm.keys.presharedKey" 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>
<el-form-item v-if="detailForm.id === ''"> <el-form-item v-if="detailForm.id === ''">
<el-button type="primary" size="small" @click="generateClientKeysApi()" <el-button type="primary" size="small" @click="generateClientKeysApi()"
>生成密钥对</el-button >生成密钥对</el-button
> >
</el-form-item> </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-form-item prop="useServerDNS" label="是否使用服务端DNS">
<el-radio-group v-model="detailForm.useServerDNS"> <el-radio-group v-model="detailForm.useServerDNS">
<el-radio :value="1"></el-radio> <el-radio :value="1"></el-radio>