Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// extractIP returns a clean IP without port. Falls back to "" so the INET
|
||||
// column can take NULL via $5 when the value is empty.
|
||||
func extractIP(r *http.Request) any {
|
||||
addr := r.RemoteAddr
|
||||
if host, _, err := net.SplitHostPort(addr); err == nil {
|
||||
addr = host
|
||||
}
|
||||
if addr == "" {
|
||||
return nil
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
// AuditLog records API access to the audit_logs table for important operations.
|
||||
func AuditLog(pool *pgxpool.Pool) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
// Only audit write operations
|
||||
if r.Method == "GET" || r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
userID := GetUserID(r.Context())
|
||||
if userID.String() == "00000000-0000-0000-0000-000000000000" {
|
||||
return
|
||||
}
|
||||
|
||||
details, _ := json.Marshal(map[string]string{
|
||||
"method": r.Method,
|
||||
"path": r.URL.Path,
|
||||
})
|
||||
|
||||
ip := extractIP(r)
|
||||
ua := r.UserAgent()
|
||||
method := r.Method
|
||||
path := r.URL.Path
|
||||
|
||||
go func() {
|
||||
_, _ = pool.Exec(context.Background(),
|
||||
`INSERT INTO audit_logs (user_id, action, resource_type, resource_id, details, ip_address, user_agent)
|
||||
VALUES ($1, $2, $3, NULL, $4, $5, $6)`,
|
||||
userID,
|
||||
method+"."+path,
|
||||
"api",
|
||||
details,
|
||||
ip,
|
||||
ua,
|
||||
)
|
||||
}()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/enterprise-ai-platform/server/internal/response"
|
||||
"github.com/enterprise-ai-platform/server/pkg/auth"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
UserIDKey contextKey = "user_id"
|
||||
EmailKey contextKey = "email"
|
||||
RoleKey contextKey = "role"
|
||||
)
|
||||
|
||||
func GetUserID(ctx context.Context) uuid.UUID {
|
||||
v, _ := ctx.Value(UserIDKey).(uuid.UUID)
|
||||
return v
|
||||
}
|
||||
|
||||
func GetRole(ctx context.Context) string {
|
||||
v, _ := ctx.Value(RoleKey).(string)
|
||||
return v
|
||||
}
|
||||
|
||||
func GetEmail(ctx context.Context) string {
|
||||
v, _ := ctx.Value(EmailKey).(string)
|
||||
return v
|
||||
}
|
||||
|
||||
// Auth creates a middleware that validates JWT and injects user info into context.
|
||||
func Auth(jwtMgr *auth.JWTManager) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tokenStr := extractToken(r)
|
||||
if tokenStr == "" {
|
||||
response.Unauthorized(w, "未登录")
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := jwtMgr.ValidateToken(tokenStr)
|
||||
if err != nil {
|
||||
response.Error(w, http.StatusUnauthorized, 40102, "Token 已过期或无效")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, UserIDKey, claims.UserID)
|
||||
ctx = context.WithValue(ctx, EmailKey, claims.Email)
|
||||
ctx = context.WithValue(ctx, RoleKey, claims.Role)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func extractToken(r *http.Request) string {
|
||||
// Try Authorization header first
|
||||
bearer := r.Header.Get("Authorization")
|
||||
if strings.HasPrefix(bearer, "Bearer ") {
|
||||
return strings.TrimPrefix(bearer, "Bearer ")
|
||||
}
|
||||
|
||||
// Then try cookie
|
||||
cookie, err := r.Cookie("access_token")
|
||||
if err == nil {
|
||||
return cookie.Value
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestGetUserID(t *testing.T) {
|
||||
userID := uuid.New()
|
||||
ctx := context.WithValue(context.Background(), UserIDKey, userID)
|
||||
if got := GetUserID(ctx); got != userID {
|
||||
t.Errorf("GetUserID() = %v, want %v", got, userID)
|
||||
}
|
||||
|
||||
ctx = context.Background()
|
||||
if got := GetUserID(ctx); got != uuid.Nil {
|
||||
t.Errorf("GetUserID() from empty ctx = %v, want uuid.Nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRole(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), RoleKey, "admin")
|
||||
if got := GetRole(ctx); got != "admin" {
|
||||
t.Errorf("GetRole() = %v, want admin", got)
|
||||
}
|
||||
|
||||
ctx = context.Background()
|
||||
if got := GetRole(ctx); got != "" {
|
||||
t.Errorf("GetRole() from empty ctx = %v, want empty string", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireRole_UserRole(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
wrapped := RequireRole("admin")(handler)
|
||||
mux.Handle("/admin", wrapped)
|
||||
|
||||
// user 角色 → 403
|
||||
userCtx := context.WithValue(context.Background(), RoleKey, "user")
|
||||
req := httptest.NewRequest(http.MethodGet, "/admin", nil).WithContext(userCtx)
|
||||
rr := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusForbidden {
|
||||
t.Errorf("user role: got %d, want %d", rr.Code, http.StatusForbidden)
|
||||
}
|
||||
|
||||
// admin 角色 → 200
|
||||
adminCtx := context.WithValue(context.Background(), RoleKey, "admin")
|
||||
req = httptest.NewRequest(http.MethodGet, "/admin", nil).WithContext(adminCtx)
|
||||
rr = httptest.NewRecorder()
|
||||
mux.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("admin role: got %d, want %d", rr.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
// 无角色 → 403
|
||||
req = httptest.NewRequest(http.MethodGet, "/admin", nil)
|
||||
rr = httptest.NewRecorder()
|
||||
mux.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusForbidden {
|
||||
t.Errorf("no role: got %d, want %d", rr.Code, http.StatusForbidden)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireSuperAdmin(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
wrapped := RequireSuperAdmin(handler)
|
||||
mux.Handle("/platform", wrapped)
|
||||
|
||||
// super_admin → 200
|
||||
superAdminCtx := context.WithValue(context.Background(), RoleKey, "super_admin")
|
||||
req := httptest.NewRequest(http.MethodGet, "/platform", nil).WithContext(superAdminCtx)
|
||||
rr := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("super_admin: got %d, want %d", rr.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
// admin → 403
|
||||
adminCtx := context.WithValue(context.Background(), RoleKey, "admin")
|
||||
req = httptest.NewRequest(http.MethodGet, "/platform", nil).WithContext(adminCtx)
|
||||
rr = httptest.NewRecorder()
|
||||
mux.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusForbidden {
|
||||
t.Errorf("admin: got %d, want %d", rr.Code, http.StatusForbidden)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuditLog_NilPool(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/test", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
fn := AuditLog(nil)
|
||||
var called bool
|
||||
fn(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
})).ServeHTTP(rr, req)
|
||||
|
||||
if !called {
|
||||
t.Error("AuditLog middleware did not call next handler")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimit_NilRedis(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
wrapped := RateLimit(nil, 5, 0)(handler)
|
||||
mux.Handle("/test", wrapped)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code == http.StatusTooManyRequests {
|
||||
t.Error("RateLimit should bypass when Redis is unavailable")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/enterprise-ai-platform/server/internal/response"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// RateLimit creates a per-user rate limiter using Redis sliding window.
|
||||
func RateLimit(rdb *redis.Client, maxRequests int, window time.Duration) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if rdb == nil {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
userID := GetUserID(r.Context())
|
||||
key := fmt.Sprintf("rl:%s:%s", userID.String(), r.URL.Path)
|
||||
|
||||
ctx := context.Background()
|
||||
count, err := rdb.Incr(ctx, key).Result()
|
||||
if err != nil {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if count == 1 {
|
||||
rdb.Expire(ctx, key, window)
|
||||
}
|
||||
|
||||
if count > int64(maxRequests) {
|
||||
response.TooManyRequests(w, "请求过于频繁,请稍后再试")
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/enterprise-ai-platform/server/internal/response"
|
||||
)
|
||||
|
||||
var roleLevel = map[string]int{
|
||||
"user": 0,
|
||||
"creator": 1,
|
||||
"admin": 2,
|
||||
"super_admin": 3,
|
||||
}
|
||||
|
||||
// RequireRole returns middleware that checks if user has the minimum required role.
|
||||
func RequireRole(minRole string) func(http.Handler) http.Handler {
|
||||
minLevel := roleLevel[minRole]
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
role := GetRole(r.Context())
|
||||
if roleLevel[role] < minLevel {
|
||||
response.Forbidden(w, "权限不足")
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// RequireSuperAdmin restricts access to platform-level (super_admin) operations only.
|
||||
// Unlike RequireRole("admin"),super admin 不受机构(org_id)限制,可执行跨机构操作。
|
||||
func RequireSuperAdmin(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if GetRole(r.Context()) != "super_admin" {
|
||||
response.Forbidden(w, "仅平台管理员可访问")
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user