feat: 实现管理后台 A3 注册群消息概览与 metrics 门禁

This commit is contained in:
Nixevol
2026-09-30 08:12:19 +08:00
parent 19a8ec95fa
commit 77d2dbddb1
13 changed files with 1593 additions and 82 deletions
+29
View File
@@ -0,0 +1,29 @@
package httpx
import (
"net/http"
"strings"
)
// MetricsGate 实现 DEVELOPMENT 4.3 节 /metrics 访问规则(共用端口)。
// token 为空返回 404;Authorization Bearer 不匹配返回 401;匹配则交给 next。
// 后台单独监听时不要包这层,直接挂 metrics Handler。
func MetricsGate(token string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if token == "" {
http.NotFound(w, r)
return
}
auth := r.Header.Get("Authorization")
const prefix = "Bearer "
if !strings.HasPrefix(auth, prefix) || auth[len(prefix):] != token {
w.WriteHeader(http.StatusUnauthorized)
return
}
if next == nil {
http.NotFound(w, r)
return
}
next.ServeHTTP(w, r)
})
}
+98
View File
@@ -0,0 +1,98 @@
package httpx_test
import (
"io"
"net/http"
"net/http/httptest"
"testing"
"git.asio.asia/nixevol/NixMsg/internal/httpx"
"git.asio.asia/nixevol/NixMsg/internal/listener"
"git.asio.asia/nixevol/NixMsg/internal/metrics"
)
func TestMetricsGateSharedPort(t *testing.T) {
reg := metrics.New()
okHandler := reg.Handler()
t.Run("empty token 404", func(t *testing.T) {
mux := listener.NewMux(listener.RoleShared, listener.Handlers{
Metrics: okHandler,
MetricsToken: "",
})
res := httptest.NewRecorder()
mux.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/metrics", nil))
if res.Code != http.StatusNotFound {
t.Fatalf("want 404 got %d", res.Code)
}
})
t.Run("wrong token 401", func(t *testing.T) {
mux := listener.NewMux(listener.RoleShared, listener.Handlers{
Metrics: okHandler,
MetricsToken: "secret-token",
})
req := httptest.NewRequest(http.MethodGet, "/metrics", nil)
req.Header.Set("Authorization", "Bearer wrong")
res := httptest.NewRecorder()
mux.ServeHTTP(res, req)
if res.Code != http.StatusUnauthorized {
t.Fatalf("want 401 got %d", res.Code)
}
})
t.Run("no auth 401", func(t *testing.T) {
mux := listener.NewMux(listener.RoleShared, listener.Handlers{
Metrics: okHandler,
MetricsToken: "secret-token",
})
res := httptest.NewRecorder()
mux.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/metrics", nil))
if res.Code != http.StatusUnauthorized {
t.Fatalf("want 401 got %d", res.Code)
}
})
t.Run("good token 200", func(t *testing.T) {
mux := listener.NewMux(listener.RoleShared, listener.Handlers{
Metrics: okHandler,
MetricsToken: "secret-token",
})
req := httptest.NewRequest(http.MethodGet, "/metrics", nil)
req.Header.Set("Authorization", "Bearer secret-token")
res := httptest.NewRecorder()
mux.ServeHTTP(res, req)
if res.Code != http.StatusOK {
t.Fatalf("want 200 got %d", res.Code)
}
body, _ := io.ReadAll(res.Body)
if len(body) == 0 {
t.Fatal("empty metrics body")
}
})
t.Run("admin role no auth", func(t *testing.T) {
mux := listener.NewMux(listener.RoleAdmin, listener.Handlers{
Metrics: okHandler,
})
res := httptest.NewRecorder()
mux.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/metrics", nil))
if res.Code != http.StatusOK {
t.Fatalf("admin metrics want 200 got %d", res.Code)
}
})
}
func TestMetricsGateDirect(t *testing.T) {
h := httpx.MetricsGate("tok", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}))
res := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/metrics", nil)
req.Header.Set("Authorization", "Bearer tok")
h.ServeHTTP(res, req)
if res.Code != 200 || res.Body.String() != "ok" {
t.Fatalf("got %d %q", res.Code, res.Body.String())
}
}