diff --git a/backend/plugins/domain/admin/handler/logs.go b/backend/plugins/domain/admin/handler/logs.go
index aac63a14..3bb2083c 100644
--- a/backend/plugins/domain/admin/handler/logs.go
+++ b/backend/plugins/domain/admin/handler/logs.go
@@ -14,6 +14,7 @@ import (
"errors"
"net/http"
"strconv"
+ "strings"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
@@ -179,11 +180,31 @@ func GetLogsAnalytics(c *gin.Context) {
func getUpgrader() *websocket.Upgrader {
return &websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
- return service.IsAllowedLogOrigin(r.Context(), r.Header.Get("Origin"), r.Host)
+ return service.IsAllowedLogOrigin(
+ r.Context(),
+ r.Header.Get("Origin"),
+ r.Host,
+ forwardedHosts(r)...,
+ )
},
}
}
+func forwardedHosts(r *http.Request) []string {
+ raw := r.Header.Get("X-Forwarded-Host")
+ if raw == "" {
+ return nil
+ }
+ parts := strings.Split(raw, ",")
+ hosts := make([]string, 0, len(parts))
+ for _, part := range parts {
+ if h := strings.TrimSpace(part); h != "" {
+ hosts = append(hosts, h)
+ }
+ }
+ return hosts
+}
+
// errNegativeParam 表示查询参数解析出了负数。
var errNegativeParam = errors.New("parameter must not be negative")
diff --git a/backend/plugins/domain/admin/service/log.go b/backend/plugins/domain/admin/service/log.go
index e0cca4a6..f138613f 100644
--- a/backend/plugins/domain/admin/service/log.go
+++ b/backend/plugins/domain/admin/service/log.go
@@ -11,6 +11,7 @@ import (
"Wavelet/plugins/domain/admin/repository"
"context"
"fmt"
+ "net"
"net/url"
"strings"
"time"
@@ -47,18 +48,20 @@ func RobotsTxtBody(ctx context.Context) string {
}
// IsAllowedLogOrigin reports whether a WebSocket handshake origin may subscribe to logs.
-func IsAllowedLogOrigin(ctx context.Context, origin, host string) bool {
+// extraHosts are reverse-proxy hosts such as X-Forwarded-Host (the browser origin
+// when Next.js rewrites /api to the backend).
+func IsAllowedLogOrigin(ctx context.Context, origin, host string, extraHosts ...string) bool {
if origin == "" {
return true
}
- // 1. 同源检查 (Same-origin check)
u, err := url.Parse(origin)
- if err == nil && strings.EqualFold(u.Host, host) {
- return true
+ if err == nil {
+ if originMatchesHost(u.Host, host, extraHosts...) {
+ return true
+ }
}
- // 2. 检查配置的允许跨域 Origin (Check allowed origins in system config)
sc, cfgErr := repository.GetSystemConfigByKey(ctx, model.ConfigKeyServerAddress)
if cfgErr != nil || sc.Value == "" {
return false
@@ -73,6 +76,41 @@ func IsAllowedLogOrigin(ctx context.Context, origin, host string) bool {
return false
}
+func originMatchesHost(originHost, host string, extraHosts ...string) bool {
+ if originHost == "" {
+ return false
+ }
+ if hostMatches(originHost, host) {
+ return true
+ }
+ for _, extra := range extraHosts {
+ if hostMatches(originHost, extra) {
+ return true
+ }
+ }
+ return false
+}
+
+func hostMatches(originHost, candidate string) bool {
+ candidate = strings.TrimSpace(candidate)
+ if candidate == "" {
+ return false
+ }
+ if strings.EqualFold(originHost, candidate) {
+ return true
+ }
+ // Reverse-proxy / local Next rewrite: Origin is :3000, backend Host is :8000.
+ return strings.EqualFold(hostName(originHost), hostName(candidate))
+}
+
+func hostName(hostport string) string {
+ h, _, err := net.SplitHostPort(hostport)
+ if err != nil {
+ return hostport
+ }
+ return h
+}
+
// AccessLogs queries the analytical access log store and decorates rows with user names.
func AccessLogs(ctx context.Context, q model.AccessLogQuery) (model.AccessLogsResponse, error) {
rc := GetRiskControlService(ctx)
diff --git a/backend/plugins/domain/admin/service/log_origin_test.go b/backend/plugins/domain/admin/service/log_origin_test.go
new file mode 100644
index 00000000..092310c2
--- /dev/null
+++ b/backend/plugins/domain/admin/service/log_origin_test.go
@@ -0,0 +1,55 @@
+// Copyright 2026 Arctel.net
+// SPDX-License-Identifier: Apache-2.0
+
+package service_test
+
+import (
+ "Wavelet/plugins/domain/admin/service"
+ "context"
+ "testing"
+)
+
+func TestIsAllowedLogOrigin(t *testing.T) {
+ ctx := context.Background()
+
+ tests := []struct {
+ name string
+ origin string
+ host string
+ extraHosts []string
+ want bool
+ }{
+ {name: "empty origin", origin: "", host: "localhost:8000", want: true},
+ {name: "same host", origin: "http://localhost:8000", host: "localhost:8000", want: true},
+ {name: "same host different case", origin: "http://LocalHost:8000", host: "localhost:8000", want: true},
+ {
+ name: "next rewrite different port same hostname",
+ origin: "http://localhost:3000",
+ host: "localhost:8000",
+ want: true,
+ },
+ {
+ name: "x-forwarded-host matches origin",
+ origin: "http://localhost:3000",
+ host: "backend:8080",
+ extraHosts: []string{"localhost:3000"},
+ want: true,
+ },
+ {
+ name: "unrelated origin",
+ origin: "https://evil.example",
+ host: "localhost:8000",
+ want: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := service.IsAllowedLogOrigin(ctx, tt.origin, tt.host, tt.extraHosts...)
+ if got != tt.want {
+ t.Errorf("IsAllowedLogOrigin(%q, %q, %v) = %v, want %v",
+ tt.origin, tt.host, tt.extraHosts, got, tt.want)
+ }
+ })
+ }
+}
diff --git a/frontend/app/(main)/admin/logs/components/app-logs.tsx b/frontend/app/(main)/admin/logs/components/app-logs.tsx
index 2429166d..8c9fc99f 100644
--- a/frontend/app/(main)/admin/logs/components/app-logs.tsx
+++ b/frontend/app/(main)/admin/logs/components/app-logs.tsx
@@ -252,10 +252,16 @@ export function AppLogs() {
// ---- Initialize ------------------------------------------------------
useEffect(() => {
- loadHistory(0).then(() => connectWs());
+ let cancelled = false;
+ loadHistory(0).then(() => {
+ if (cancelled) return;
+ connectWs();
+ });
return () => {
- wsRef.current?.close();
+ cancelled = true;
+ const ws = wsRef.current;
wsRef.current = null;
+ ws?.close();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
diff --git a/frontend/app/(main)/admin/push/components/events-tab.tsx b/frontend/app/(main)/admin/push/components/events-tab.tsx
index aabcfdc2..b45e9f17 100644
--- a/frontend/app/(main)/admin/push/components/events-tab.tsx
+++ b/frontend/app/(main)/admin/push/components/events-tab.tsx
@@ -713,8 +713,8 @@ export function EventsTab() {
{newEventType === 'task'
- ? t('taskTemplateVars')
- : t('eventTemplateVars')}
+ ? t.raw('taskTemplateVars')
+ : t.raw('eventTemplateVars')}