Sync managed gateway routes on startup and fix layout initialization

This commit is contained in:
opsx committed 2026-10-22 10:49:32 +08:00
1 parent 5ef1cb48cf
commit 0528bc4b9a
7 files changed
+40 -6

No files matched your search

+15
View File
@@ -42,6 +42,21 @@ func (*gatewayConflict) Error() string {
return "当前域名存在多条运行中的 OIDC 专线,请先保留一条"
}
// RefreshManagedGatewayOnStart keeps the installed route allowlist in sync
// with the binary after upgrades. It only touches this application's managed
// Nginx site; RefreshAuth validates it and rolls back on failed reloads.
func (s *Server) RefreshManagedGatewayOnStart() error {
status := tlsservice.Status()
if !status.Valid || status.Mode != "nginx" || status.RestartRequired || nginxgateway.Stage() != "https" {
return nil
}
port, err := authGatewayPort(status.Domain)
if err != nil {
return err
}
return nginxgateway.RefreshAuth(status.Domain, s.router.Routes(), port)
}
func (s *Server) identityGateway(c *gin.Context) {
status := tlsservice.Status()
if !status.Valid || status.Mode != "nginx" || status.RestartRequired || nginxgateway.Stage() != "https" {
@@ -66,11 +66,16 @@ func TestRenderRejectsInjectedDomainAndRoute(t *testing.T) {
}
func TestRenderIdentityAndSingleAuthPort(t *testing.T) {
_, site, err := RenderWithAuth("vpn.example.com", gin.RoutesInfo{{Method: "GET", Path: "/api/health"}}, 19090)
_, site, err := RenderWithAuth("vpn.example.com", gin.RoutesInfo{
{Method: "GET", Path: "/api/health"},
{Method: "PUT", Path: "/api/system/tls/domain"},
{Method: "PUT", Path: "/api/system/identity"},
{Method: "POST", Path: "/api/system/identity/gateway"},
}, 19090)
if err != nil {
t.Fatal(err)
}
for _, route := range []string{"location = /id/.well-known/openid-configuration", "location = /id/feishu/callback", "location = /id/token", "location ^~ /oauth2/", "proxy_pass http://127.0.0.1:19090;", "location / { return 444; }"} {
for _, route := range []string{"location = /id/.well-known/openid-configuration", "location = /id/feishu/callback", "location = /id/token", "location = /api/system/tls/domain", "location = /api/system/identity", "location = /api/system/identity/gateway", "location ^~ /oauth2/", "proxy_pass http://127.0.0.1:19090;", "location / { return 444; }"} {
if !strings.Contains(site, route) {
t.Errorf("identity route missing: %s", route)
}
+3
View File
@@ -79,6 +79,9 @@ func RefreshAuth(domain string, routes gin.RoutesInfo, authPort int) error {
if err != nil {
return err
}
if string(previous) == httpSite+httpsSite {
return nil
}
if err := writeSite(httpSite + httpsSite); err != nil {
return err
}
+3
View File
@@ -115,6 +115,9 @@ func main() {
if err := tlsservice.Start(r, dataDir); err != nil {
log.Fatalf("HTTPS/ACME 启动失败: %v", err)
}
if err := srv.RefreshManagedGatewayOnStart(); err != nil {
log.Printf("Nginx 路由白名单同步失败,仍可通过本机入口检查: %v", err)
}
addr := os.Getenv("VPN_LISTEN_ADDR")
if addr == "" {
@@ -46,3 +46,9 @@
现有专线编辑页仍把外部 OIDC 的 issuer 与 VPN Client Secret 输入框作为主流程,容易让管理员误把飞书 App ID/Secret 填入专线。管理员仅在「设置 → 飞书身份」维护全局飞书 App ID、App Secret 和 tenant_key;专线的内置模式自动引用当前 issuer、独立 VPN Client ID 和公网入口。编辑既有外部 OIDC 专线时保持原始设置,只有明确切换来源才更改字段。外部 OIDC 仍允许手工配置,不把飞书上游凭据放入专线。
验收:专线编辑和新建都能直达身份设置;内置模式不展示飞书密钥或可编辑的 VPN 密钥,提示飞书回调与 VPN 回调的区别;外部模式保留原先的输入能力;服务端对内置标记校验已启用的发行者、Client ID 和当前域名,不因提交旧密钥而回退至外部模式。该迭代不自动切换、启用或重启已有专线;上线前构建前端并跑相关 Go 测试,上线后检查管理入口与禁用的身份入口,失败恢复上一版二进制。
## 6. 公网白名单与页面故障修复
线上新增 `/api/system/identity` 后托管 Nginx 仍使用旧白名单,浏览器看到 `ERR_EMPTY_RESPONSE`(Nginx 444);本机管理进程可正常返回 401。已有 `/api/system/tls/domain` 路由在无凭据时返回 401,不能把用户同时报告的域名保存失败归结于同一缺失路由。Vue 侧栏在 `route` 创建前注册 watch,导致初始化时引用尚未声明的变量;WireGuard SVG 含截断的路径数据。
每次启动且确认当前是有效 Nginx HTTPS 托管站点时,按已注册 Gin 路由重建站点白名单;文件内容相同则不重载,冲突或 `nginx -t` 失败时保留旧站点并记录日志,不阻止本机服务启动。公网只同步已注册 API、固定身份端点与当前单条运行的同域认证组件,未知路径仍 444。验收:新身份管理 API 不再返回空连接,未经认证返回 401;既有域名 PUT 也仍返回 401,授权后的保存需单独实测;设置和编辑页无布局初始化异常;SVG 路径有效;Nginx 测试通过。恢复时使用 `/tmp` 的上一版二进制和原 Nginx 站点快照重新加载,不修改专线或飞书凭据。
+3
View File
@@ -75,6 +75,9 @@ const ICON_PATHS = {
const pathData = computed(() => {
const key = props.protocol.startsWith('custom:') ? 'custom' : props.protocol
// The imported WireGuard path was truncated in the original asset. Keep a
// valid compact tunnel symbol until the full vector is replaced.
if (key === 'wireguard') return 'M12 2 3 6v6c0 5.25 3.84 9.74 9 10 5.16-.26 9-4.75 9-10V6L12 2zm0 2.2 7 2.8v5c0 4.1-2.6 7.5-7 8.7-4.4-1.2-7-4.6-7-8.7V7l7-2.8zM7 11h10v2H7zm2-3h6v2H9zm0 6h6v2H9z'
return ICON_PATHS[key] || ICON_PATHS.custom
})
</script>
+3 -4
View File
@@ -109,6 +109,9 @@ import { useI18n } from 'vue-i18n'
import { DataAnalysis, Connection, Setting, Avatar, ArrowDown, UserFilled, Iphone, Lock, Monitor, Promotion, Document, Link, Expand, Fold, Close, Operation } from '@element-plus/icons-vue'
import api, { auth } from '../api'
const router = useRouter()
const route = useRoute()
const { t, locale } = useI18n()
const collapsed = ref(localStorage.getItem('vpn_nav_collapsed') === '1')
const mobileOpen = ref(false)
function toggleCollapsed() {
@@ -119,10 +122,6 @@ watch(() => route.fullPath, () => { mobileOpen.value = false })
function onEscape(event) { if (event.key === 'Escape') mobileOpen.value = false }
onMounted(() => window.addEventListener('keydown', onEscape))
onUnmounted(() => window.removeEventListener('keydown', onEscape))
const router = useRouter()
const route = useRoute()
const { t, locale } = useI18n()
const roleLabel = computed(() => ({ admin: t('users.roleAdmin'), operator: t('users.roleOperator'), viewer: t('users.roleViewer') }[auth.role] || auth.role))
const roleTagType = computed(() => auth.role === 'admin' ? 'danger' : auth.role === 'operator' ? 'warning' : 'info')