🎨新增客户端离线通知选项配置以及监听任务
All checks were successful
continuous-integration/drone/tag Build is passing

This commit is contained in:
coward 2024-06-13 14:34:03 +08:00
parent 67f394f136
commit 6f249d20b0
18 changed files with 155 additions and 132 deletions

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

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

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

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

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

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

@ -135,14 +135,6 @@ class PureHttp {
$error.isCancelRequest = Axios.isCancel($error); $error.isCancelRequest = Axios.isCancel($error);
// 关闭进度条动画 // 关闭进度条动画
NProgress.done(); NProgress.done();
if ($error.response.status === 401) {
router.replace({
path: "/login",
query: {
redirect: router.currentRoute.value.fullPath
}
});
}
// 所有的响应异常 区分来源为取消请求/非取消请求 // 所有的响应异常 区分来源为取消请求/非取消请求
return Promise.reject($error); return Promise.reject($error);
} }

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,7 +154,7 @@ const openAddClientDialog = () => {
} }
}); });
const serverInfo = storageLocal().getItem("server-info"); const serverInfo = storageLocal().getItem("server-info");
const restartRule = storageLocal().getItem("restart-rule") ? 1 : 0; const restartRule = !storageLocal().getItem("restart-rule") ? 1 : 0;
addDialog({ addDialog({
width: "40%", width: "40%",
title: "新增", title: "新增",
@ -174,7 +177,8 @@ const openAddClientDialog = () => {
publicKey: "", publicKey: "",
presharedKey: "" presharedKey: ""
}, },
enabled: 1 enabled: 1,
offlineMonitoring: 0
} }
}, },
beforeSure: (done, { options }) => { beforeSure: (done, { options }) => {
@ -213,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 }) => {
@ -279,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>
@ -309,7 +323,10 @@ 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" style="display: flex;justify-content: center;"> <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="float: left; width: 500px" style="float: left; width: 500px"
@ -338,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"
@ -418,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>