feat: implement VPN platform management and real protocol services

This commit is contained in:
Logan.Li committed 2026-07-22 21:49:13 +08:00
commit 58c036c34c
45 files changed
+9244

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
<template>
<el-card class="page-card" shadow="never">
<template #header><div style="display:flex;justify-content:space-between;align-items:center"><div><b>VPN 拨号用户</b><span style="margin-left:8px;color:#909399;font-size:12px">本地密码或 LDAP 身份,按 OpenVPN 用户统计流量</span></div><el-button type="primary" @click="openCreate">添加用户</el-button></div></template>
<el-table :data="users" size="small">
<el-table-column prop="username" label="用户名" /><el-table-column prop="display_name" label="姓名" /><el-table-column label="隧道"><template #default="{row}">{{ tunnelName(row.tunnel_id) }}</template></el-table-column>
<el-table-column prop="source" label="来源" width="90" /><el-table-column label="流量 ↓ / ↑" width="170"><template #default="{row}">{{ bytes(row.rx_bytes) }} / {{ bytes(row.tx_bytes) }}</template></el-table-column>
<el-table-column label="最近在线" width="180"><template #default="{row}">{{ row.last_seen_at ? new Date(row.last_seen_at).toLocaleString('zh-CN') : '-' }}</template></el-table-column>
<el-table-column label="状态" width="90"><template #default="{row}"><el-tag :type="row.enabled ? 'success':'info'" size="small">{{ row.enabled ? '启用':'禁用' }}</el-tag></template></el-table-column>
<el-table-column label="操作" width="180"><template #default="{row}"><el-button link size="small" @click="download(row)">下载配置</el-button><el-button link size="small" @click="toggle(row)">{{ row.enabled ? '禁用':'启用' }}</el-button><el-button type="danger" link size="small" @click="remove(row)">删除</el-button></template></el-table-column>
</el-table>
</el-card>
<el-dialog v-model="visible" title="添加 VPN 拨号用户" width="480px"><el-form label-width="100px"><el-form-item label="OpenVPN 隧道" required><el-select v-model="form.tunnel_id" style="width:100%"><el-option v-for="t in tunnels" :key="t.id" :value="t.id" :label="t.name" /></el-select></el-form-item><el-form-item label="用户名" required><el-input v-model="form.username" /></el-form-item><el-form-item label="显示名"><el-input v-model="form.display_name" /></el-form-item><el-form-item label="认证来源"><el-radio-group v-model="form.source"><el-radio-button value="local">本地</el-radio-button><el-radio-button value="ldap">LDAP</el-radio-button></el-radio-group></el-form-item><el-form-item v-if="form.source==='local'" label="密码" required><el-input v-model="form.password" type="password" show-password placeholder="至少 8 位" /></el-form-item><el-form-item v-else label="LDAP DN" required><el-input v-model="form.ldap_dn" placeholder="uid=user,ou=people,dc=example,dc=com" /></el-form-item><el-form-item label="邮箱"><el-input v-model="form.email" /></el-form-item></el-form><template #footer><el-button @click="visible=false">取消</el-button><el-button type="primary" @click="create">创建</el-button></template></el-dialog>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import api from '../api'
const users=ref([]), tunnels=ref([]), visible=ref(false)
const form=ref({tunnel_id:null,username:'',display_name:'',source:'local',password:'',ldap_dn:'',email:'',enabled:true})
const bytes=n=>{if(!n)return '0 B';const u=['B','KB','MB','GB','TB'];let i=0;while(n>=1024&&i<4){n/=1024;i++}return `${n.toFixed(1)} ${u[i]}`}
const tunnelName=id=>tunnels.value.find(t=>t.id===id)?.name||`#${id}`
async function load(){const [u,t]=await Promise.all([api.get('/vpn-users'),api.get('/tunnels',{params:{protocol:'openvpn'}})]);users.value=u.data||[];tunnels.value=t.data||[]}
function openCreate(){form.value={tunnel_id:tunnels.value[0]?.id||null,username:'',display_name:'',source:'local',password:'',ldap_dn:'',email:'',enabled:true};visible.value=true}
async function create(){try{await api.post('/vpn-users',form.value);visible.value=false;ElMessage.success('拨号用户已创建');load()}catch(e){ElMessage.error(e.response?.data?.error||'创建失败')}}
async function toggle(row){await api.put(`/vpn-users/${row.id}`,{enabled:!row.enabled,display_name:row.display_name,email:row.email});load()}
async function remove(row){await ElMessageBox.confirm(`删除拨号用户 ${row.username}?`,'确认');await api.delete(`/vpn-users/${row.id}`);load()}
async function download(row){try{const res=await fetch(`/api/vpn-users/${row.id}/config`,{headers:{Authorization:`Bearer ${localStorage.getItem('vpn_token')}`}});if(!res.ok){const data=await res.json();throw new Error(data.error)}const url=URL.createObjectURL(await res.blob());const a=document.createElement('a');a.href=url;a.download=`openvpn-${row.username}.ovpn`;a.click();URL.revokeObjectURL(url)}catch(e){ElMessage.error(e.message||'下载失败')}}
onMounted(load)
</script>