fix(security): P0 修复——备份恢复路径与权限、frps 安装白名单与 SHA-256、SSH 主机密钥 TOFU、三处凭据掩码,另修状态徽标与仪表盘文案(v1.2.7)
CI / verify (push) Canceled after 0s
CI / verify (push) Canceled after 0s
This commit is contained in:
@@ -312,7 +312,7 @@ export const api = {
|
||||
}),
|
||||
getFrpAgentRoutes: (id: number) => request<import('./types').FrpAgentRouteDetail[]>(`/api/frpmulti/servers/${id}/agent/routes`),
|
||||
getFrpAgentRouteCandidates: (id: number) => request<import('./types').FrpProxy[]>(`/api/frpmulti/servers/${id}/agent/route-candidates`),
|
||||
deployFrpAgentRoute: (serverId: number, proxyId: number) => request<{ ok: boolean }>(`/api/frpmulti/servers/${serverId}/agent/routes/${proxyId}`, { method: 'POST' }),
|
||||
deployFrpAgentRoute: (serverId: number, proxyId: number) => request<{ ok: boolean; basic_auth_user?: string; basic_auth_password?: string }>(`/api/frpmulti/servers/${serverId}/agent/routes/${proxyId}`, { method: 'POST' }),
|
||||
getFrpAgentRouteConf: (serverId: number, proxyId: number) =>
|
||||
request<{ proxy_id: number; domain: string; content: string }>(`/api/frpmulti/servers/${serverId}/agent/routes/${proxyId}/conf`),
|
||||
installFrpAgentNginx: (serverId: number) =>
|
||||
|
||||
@@ -173,7 +173,7 @@ const menuDefs = [
|
||||
{ label: '证书管理', key: 'certificates', icon: LockClosedOutline },
|
||||
{ label: '反向代理', key: 'proxies', icon: SwapHorizontalOutline },
|
||||
{ label: '内网穿透', key: 'frp', icon: GitNetworkOutline },
|
||||
{ label: '公网 Agent', key: 'frp-agent', icon: HardwareChipOutline },
|
||||
{ label: '公网服务端', key: 'frp-agent', icon: HardwareChipOutline },
|
||||
{ label: '公网反代', key: 'public-proxy', icon: ShieldCheckmarkOutline },
|
||||
{ label: '日志中心', key: 'logs', icon: DocumentTextOutline },
|
||||
{ label: '系统设置', key: 'settings', icon: CogOutline },
|
||||
|
||||
@@ -579,7 +579,7 @@
|
||||
<div v-if="viewingServer" class="server-detail">
|
||||
<div class="server-detail__top"><div><h2>{{ viewingServer.name }}</h2><p>服务端详情和运行监测</p></div><n-button v-if="viewingServer.boot_status === 'running'" size="small" type="error" ghost @click="stopServer(viewingServer)"><template #icon><n-icon :component="StopOutline" /></template>停止</n-button><n-button v-else size="small" type="success" ghost @click="startServer(viewingServer)"><template #icon><n-icon :component="PlayOutline" /></template>启动</n-button><n-button size="small" @click="openFRPSConfig(viewingServer)"><template #icon><n-icon :component="DocumentOutline" /></template>frps.toml</n-button></div>
|
||||
<div class="server-detail__grid">
|
||||
<section class="server-detail__panel"><h3>基本信息</h3><div class="detail-fields"><div><label>主机地址</label><strong class="mono">{{ viewingServer.server_addr }}</strong></div><div><label>端口</label><strong>{{ viewingServer.server_port }}</strong></div><div><label>用户</label><strong>{{ viewingServer.options.user || '未设置' }}</strong></div><div><label>位置</label><strong class="server-detail__location"><n-icon :component="LocationOutline" /> {{ locationText(viewingServer) }}</strong></div><div><label>状态</label><StatusBadge :value="serverStartupType(viewingServer)" :text="serverStartupText(viewingServer)" /></div><div><label>协议</label><strong>{{ viewingServer.options.protocol.toUpperCase() }}</strong></div><div><label>创建时间</label><span>{{ formatDate(viewingServer.created_at) }}</span></div><div><label>最后更新</label><span>{{ formatDate(viewingServer.updated_at) }}</span></div></div></section>
|
||||
<section class="server-detail__panel"><h3>基本信息</h3><div class="detail-fields"><div><label>主机地址</label><strong class="mono">{{ viewingServer.server_addr }}</strong></div><div><label>端口</label><strong>{{ viewingServer.server_port }}</strong></div><div><label>用户</label><strong>{{ viewingServer.options.user || '未设置' }}</strong></div><div><label>位置</label><strong class="server-detail__location"><n-icon :component="LocationOutline" /> {{ locationText(viewingServer) }}</strong></div><div><label>状态</label><StatusBadge :kind="serverStartupKind(viewingServer)" :text="serverStartupText(viewingServer)" /></div><div><label>协议</label><strong>{{ viewingServer.options.protocol.toUpperCase() }}</strong></div><div><label>创建时间</label><span>{{ formatDate(viewingServer.created_at) }}</span></div><div><label>最后更新</label><span>{{ formatDate(viewingServer.updated_at) }}</span></div></div></section>
|
||||
<section class="server-detail__panel">
|
||||
<h3>配置</h3>
|
||||
<div class="detail-fields detail-fields--single">
|
||||
@@ -665,6 +665,7 @@ import type {
|
||||
FrpServerOptions, FrpServerPayload, FrpServerStatusDetail,
|
||||
} from '../api/types'
|
||||
import { formatBytes, formatDate, formatRelativeTime, formatUptime } from '../utils/format'
|
||||
import type { StatusKind } from '../utils/status'
|
||||
import { copyText } from '../utils/clipboard'
|
||||
|
||||
use([CanvasRenderer, LineChart, ScatterChart, GridComponent, TooltipComponent])
|
||||
@@ -1386,6 +1387,14 @@ function serverStartupType(server: FrpServer) {
|
||||
return 'default'
|
||||
}
|
||||
|
||||
// StatusBadge 的 kind 只认 success/warning/error/disabled/unknown,n-tag 的 'default' 要映射成 unknown
|
||||
function serverStartupKind(server: FrpServer): StatusKind {
|
||||
if (server.boot_status === 'running') return 'success'
|
||||
if (server.boot_status === 'starting') return 'warning'
|
||||
if (server.boot_status === 'error') return 'error'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
function processStateText(server: FrpServer) {
|
||||
return ({ starting: '启动中', running: '运行中', stopped: '已停止', error: '异常', unknown: '未知' } as Record<string, string>)[server.process_state] ?? '未知'
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
<StatCard label="域名" tone="green">
|
||||
<template #icon><n-icon :component="WifiOutline" /></template>
|
||||
<template #extra><StatusBadge :value="status?.ddns_status" text="正常" /></template>
|
||||
<template #extra><StatusBadge v-if="status?.ddns_status" :value="status.ddns_status" :text="statusLabel(status.ddns_status)" /></template>
|
||||
<template #value>{{ domainCountLabel }}</template>
|
||||
<p class="stat-desc">{{ domainSubLabel }}</p>
|
||||
<div class="stat-foot">
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
<StatCard label="HTTPS 证书" tone="blue">
|
||||
<template #icon><n-icon :component="ShieldCheckmarkOutline" /></template>
|
||||
<template #extra><StatusBadge :value="status?.certificate_status" text="正常" /></template>
|
||||
<template #extra><StatusBadge v-if="status?.certificate_status" :value="status.certificate_status" :text="statusLabel(status.certificate_status)" /></template>
|
||||
<template #value>{{ certCountLabel }}</template>
|
||||
<p class="stat-desc">{{ certSubLabel }}</p>
|
||||
<div v-if="primaryCert" class="stat-foot">
|
||||
@@ -395,7 +395,7 @@ import {
|
||||
formatUptime,
|
||||
} from '../utils/format'
|
||||
import { displaySystemLog } from '../utils/logDisplay'
|
||||
import { httpStatusKind } from '../utils/status'
|
||||
import { httpStatusKind, statusLabel } from '../utils/status'
|
||||
|
||||
const message = useMessage()
|
||||
const status = ref<DashboardStatus | null>(null)
|
||||
|
||||
@@ -4,13 +4,6 @@
|
||||
description="把穿透规则的域名发布到 VPS 的 Nginx:部署 / 移除、部署设置(HTTPS、跳转、白名单、Basic Auth、限流)、健康检查与配置查看"
|
||||
>
|
||||
<template #actions>
|
||||
<n-select
|
||||
v-model:value="selectedId"
|
||||
:options="serverOptions"
|
||||
placeholder="选择公网服务端"
|
||||
class="proxy-remote-select"
|
||||
:disabled="servers.length === 0"
|
||||
/>
|
||||
<n-button :loading="manualRefreshing" :disabled="!server" @click="refreshAll({ silent: false })">
|
||||
<template #icon><n-icon :component="RefreshOutline" /></template>
|
||||
刷新
|
||||
@@ -24,14 +17,47 @@
|
||||
<EmptyState
|
||||
v-if="servers.length === 0"
|
||||
title="尚未配置 FRP 服务端"
|
||||
description="先在「内网穿透」页添加公网服务端并保存 Agent/SSH 信息,再回到本页管理公网反代。"
|
||||
description="服务端的添加与 Agent / SSH 配置在「公网服务端」页;本页只把启用中的 http 规则发布到 VPS 的 Nginx。"
|
||||
>
|
||||
<template #action>
|
||||
<router-link to="/frp"><n-button type="primary">去内网穿透页</n-button></router-link>
|
||||
<router-link to="/frp-agent"><n-button type="primary">去公网服务端页</n-button></router-link>
|
||||
</template>
|
||||
</EmptyState>
|
||||
|
||||
<template v-else-if="server">
|
||||
<div v-else-if="server" class="proxy-layout">
|
||||
<!-- 左:服务端列表;右:该服务端的漂移提示 / 统计 / Nginx 与反代台账 -->
|
||||
<aside class="server-sidebar">
|
||||
<div class="server-sidebar__head">
|
||||
<div class="server-sidebar__head-row">
|
||||
<h3 class="server-sidebar__title">服务端列表</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-sidebar__list">
|
||||
<button
|
||||
v-for="item in servers"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="server-item"
|
||||
:class="{ 'server-item--active': item.id === selectedId }"
|
||||
@click="selectedId = item.id"
|
||||
>
|
||||
<div class="server-item__top">
|
||||
<span class="server-item__name">{{ item.name }}</span>
|
||||
<n-tag size="tiny" :bordered="false" :type="item.agent_configured ? 'success' : 'warning'">{{ item.agent_configured ? 'Agent 就绪' : 'Agent 未配' }}</n-tag>
|
||||
</div>
|
||||
<div class="server-item__endpoint mono">{{ item.server_addr }}:{{ item.server_port }}</div>
|
||||
<div class="server-item__meta">
|
||||
<span>{{ item.ssh_configured ? 'SSH 已配置' : 'SSH 未配置' }}</span>
|
||||
</div>
|
||||
<div class="server-item__tags">
|
||||
<n-tag size="tiny" :bordered="false" :type="item.tls_enabled ? 'success' : 'warning'">TLS</n-tag>
|
||||
<n-tag size="tiny" :bordered="false" :type="item.has_token ? 'success' : 'error'">Token</n-tag>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="proxy-detail">
|
||||
<!-- 数据读取失败:必须可见(原先 loadErrorMsg 写了但从未渲染) -->
|
||||
<n-alert v-if="loadErrorMsg" type="error" :bordered="false" class="page-alert">{{ loadErrorMsg }}</n-alert>
|
||||
|
||||
@@ -76,7 +102,7 @@
|
||||
<!-- 入口行:agent / frps 生命周期在另一页 -->
|
||||
<div class="entrance-row">
|
||||
<span class="entrance-row__text">
|
||||
Agent 与 frps 的安装、升级、启停、配置下发在「公网 Agent」页;本页只负责把规则发布到 Nginx
|
||||
Agent 与 frps 的安装、升级、启停、配置下发在「公网服务端」页;本页只负责把规则发布到 Nginx
|
||||
</span>
|
||||
<router-link to="/frp-agent"><n-button size="small" quaternary>前往公网 Agent</n-button></router-link>
|
||||
</div>
|
||||
@@ -179,6 +205,8 @@
|
||||
</details>
|
||||
</div>
|
||||
</FonuCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 部署设置 -->
|
||||
<n-modal v-model:show="settingsModal" :mask-closable="false">
|
||||
@@ -333,7 +361,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -405,9 +432,6 @@ const confContent = ref('')
|
||||
let timer: number | undefined
|
||||
|
||||
const server = computed(() => servers.value.find((item) => item.id === selectedId.value) ?? null)
|
||||
const serverOptions = computed(() => servers.value
|
||||
.filter((item) => item.agent_configured || item.ssh_configured)
|
||||
.map((item) => ({ label: item.name, value: item.id })))
|
||||
const agentStatusOk = computed(() => statusLoaded.value && agentStatus.value !== null)
|
||||
|
||||
// 统计:已生效 / 待处理 / 健康通过 / 当前连接
|
||||
@@ -725,7 +749,12 @@ async function deployRoute(candidate: FrpProxy) {
|
||||
const current = server.value
|
||||
if (!current) return
|
||||
const result = await run(`deploy:${candidate.id}`, '部署反代', () => api.deployFrpAgentRoute(current.id, candidate.id))
|
||||
if (result !== undefined) await refreshAll()
|
||||
if (result === undefined) return
|
||||
// 自动生成的 Basic Auth 密码只在这一次响应里返回:不再写进 nginx 配置,错过就得在「部署设置」里重设
|
||||
if (result.basic_auth_password) {
|
||||
message.warning(`Basic Auth 已生成(仅显示这一次,请立即保存):${result.basic_auth_user} / ${result.basic_auth_password}`, { duration: 15000 })
|
||||
}
|
||||
await refreshAll()
|
||||
}
|
||||
|
||||
async function removeRoute(candidate: FrpProxy) {
|
||||
@@ -953,7 +982,25 @@ onBeforeUnmount(() => { if (timer) window.clearInterval(timer) })
|
||||
.form-hint { color: var(--fonu-text-secondary); font-size: 12.5px; }
|
||||
.mono { font-family: var(--fonu-mono); font-size: 13px; }
|
||||
.mono-wrap { white-space: normal; word-break: break-all; }
|
||||
.proxy-remote-select { width: 220px; }
|
||||
|
||||
/* 左:服务端列表 / 右:该服务端的反代台账(与「内网穿透」「公网服务端」页同构) */
|
||||
.proxy-layout { display: grid; grid-template-columns: minmax(240px, 300px) minmax(0, 1fr); gap: var(--fonu-space-4); align-items: start; }
|
||||
.proxy-layout .section-card { min-width: 0; }
|
||||
.proxy-detail { min-width: 0; }
|
||||
.server-sidebar { background: var(--fonu-surface); border: 1px solid var(--fonu-border); border-radius: var(--fonu-radius); box-shadow: var(--fonu-shadow); overflow: hidden; }
|
||||
.server-sidebar__head { padding: var(--fonu-space-4); border-bottom: 1px solid var(--fonu-border); }
|
||||
.server-sidebar__head-row { display: flex; align-items: center; justify-content: space-between; gap: var(--fonu-space-3); }
|
||||
.server-sidebar__title { margin: 0; font-size: 15px; font-weight: 600; color: var(--fonu-text); flex-shrink: 0; }
|
||||
.server-sidebar__list { display: flex; flex-direction: column; gap: 10px; padding: var(--fonu-space-3); max-height: 720px; overflow: auto; }
|
||||
.server-item { width: 100%; font: inherit; text-align: left; padding: 12px 14px; border: 1px solid var(--fonu-border); border-radius: var(--fonu-radius); background: var(--fonu-surface); cursor: pointer; transition: border-color 0.15s ease, box-shadow 0.15s ease; }
|
||||
.server-item:hover { border-color: var(--fonu-brand-hover); }
|
||||
.server-item--active { border-color: var(--fonu-brand); box-shadow: 0 0 0 1px var(--fonu-brand-soft); }
|
||||
.server-item__top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
.server-item__name { font-size: 14px; font-weight: 600; color: var(--fonu-text); overflow-wrap: anywhere; }
|
||||
.server-item__endpoint { margin-top: 6px; font-size: 12px; color: var(--fonu-text-secondary); overflow-wrap: anywhere; }
|
||||
.server-item__meta { display: flex; align-items: center; flex-wrap: wrap; gap: var(--fonu-space-3); margin-top: 6px; font-size: 12px; color: var(--fonu-text-muted); }
|
||||
.server-item__tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
||||
@media (max-width: 1100px) { .proxy-layout { grid-template-columns: 1fr; } .server-sidebar__list { max-height: none; } }
|
||||
|
||||
/* 首屏一次性入场;用户偏好减少动效时关闭 */
|
||||
@keyframes fonu-fade-up {
|
||||
|
||||
Reference in New Issue
Block a user