重构:拆出 auth / blocking / runtimesettings / help / mqttforward 包
第二批:把根目录中纯逻辑领域文件(cache、service、admin route)按业务边界
迁到 internal/ 下的子包。各子包暴露 RegisterRoutes 给 web 包调用,根目录
只留下一行 bridge 文件保留旧名字别名。
新增包
- internal/auth/ SessionClaims / Manager / RequireAdmin / HashPassword /
VerifyPassword / AdminUserResponse 等。原 auth.go 中
被两个 admin route 依赖的 sessionClaims 现在以 auth.
SessionClaims 形式被它们 import;不再被锁在 main 包。
- internal/blocking/ Cache + RegisterRoutes,以前散在 blocking_cache.go
和 admin_blocking_routes.go 里。
- internal/runtimesettings/ Cache + RegisterRoutes。
- internal/help/ RenderMarkdown / RegisterPublicRoutes /
RegisterAdminRoutes(拆分原来的 registerHelpRoutes
和 registerAdminHelpRoutes 两条入口)。
- internal/mqttforward/ Manager / Reloader / Stats / RegisterRoutes。
forwarder runner、循环抑制 cache 等运行时逻辑随之迁入。
- internal/webutil/ ParseListOptions / WriteListResponse[WithTotal] /
ParseMapReportListOptions / ParseMapReportViewportOptions
以及 PtrString/PtrInt64/... 等指针解引用 helper。
以前散在 web.go 中,现在被各 admin route 子包共享,
避免 internal/blocking → internal/web → internal/blocking
的循环依赖。
- internal/store/testutil/ OpenStore(t) helper,让其它包测试零样板拿到 store。
根目录新增 bridge 文件
- blocking_bridge.go / runtime_settings_bridge.go / help_bridge.go /
mqttforward_bridge.go:用 type alias + thin wrapper 把上述子包的导出
名映射到旧的小写名(blockingCache、registerAdminBlockingRoutes 等),
让 main.go / web.go 等仍未迁出的文件无须改动。
修改
- auth.go 改为对 internal/auth 的 bridge;web.go 中 sessions.newCookie /
clearCookie 改为 NewCookie / ClearCookie。
- main_test.go 中 BlockingViolationForRecord* 测试不再直接构造未导出字段,
改成走 store.CreateNodeBlocking → newBlockingCache 的真实路径。
- internal/mqttforward 把以前 *_store.go 中没有方法依赖的运行时类型
(forwarder runner、loop cache)和 admin route 一并归位;mqtt_status.go
暂时仍留在根目录(依赖 main 中的 mqttClientInfoFromClient)。
go build ./... / go test ./... 全部通过;测试数量未变。
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,147 +1,29 @@
|
||||
package main
|
||||
|
||||
// 桥接到 internal/auth — 让根目录其余文件继续用旧的小写名(sessionManager、
|
||||
// sessionClaims、newSessionManager、requireAdmin、verifyPassword 等)。
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
authpkg "meshtastic_mqtt_server/internal/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
adminRole = "admin"
|
||||
adminSessionCookie = "mesh_admin_session"
|
||||
// 类型别名
|
||||
type (
|
||||
sessionManager = authpkg.Manager
|
||||
sessionClaims = authpkg.SessionClaims
|
||||
adminUserDTO = authpkg.AdminUserDTO
|
||||
)
|
||||
|
||||
type adminUserDTO struct {
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
type sessionClaims struct {
|
||||
UserID uint64 `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
Expires int64 `json:"expires"`
|
||||
}
|
||||
|
||||
type sessionManager struct {
|
||||
secret []byte
|
||||
secure bool
|
||||
ttl time.Duration
|
||||
}
|
||||
const adminRole = authpkg.AdminRole
|
||||
|
||||
func newSessionManager(cfg webAdminConfig) (*sessionManager, error) {
|
||||
secret := strings.TrimSpace(cfg.SessionSecret)
|
||||
if secret == "" {
|
||||
generated := make([]byte, 32)
|
||||
if _, err := rand.Read(generated); err != nil {
|
||||
return nil, fmt.Errorf("generate admin session secret: %w", err)
|
||||
}
|
||||
return &sessionManager{secret: generated, secure: cfg.SessionSecure, ttl: 24 * time.Hour}, nil
|
||||
}
|
||||
return &sessionManager{secret: []byte(secret), secure: cfg.SessionSecure, ttl: 24 * time.Hour}, nil
|
||||
return authpkg.NewManager(cfg)
|
||||
}
|
||||
|
||||
func hashPassword(password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
}
|
||||
func verifyPassword(hash, password string) bool { return authpkg.VerifyPassword(hash, password) }
|
||||
|
||||
func verifyPassword(hash, password string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
|
||||
}
|
||||
func adminUserResponse(user userRecord) adminUserDTO { return authpkg.AdminUserResponse(user) }
|
||||
|
||||
func adminUserResponse(user userRecord) adminUserDTO {
|
||||
return adminUserDTO{Username: user.Username, Role: user.Role}
|
||||
}
|
||||
|
||||
func (sm *sessionManager) newCookie(user userRecord) (*http.Cookie, error) {
|
||||
claims := sessionClaims{UserID: user.ID, Username: user.Username, Role: user.Role, Expires: time.Now().Add(sm.ttl).Unix()}
|
||||
data, err := json.Marshal(claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload := base64.RawURLEncoding.EncodeToString(data)
|
||||
signature := sm.sign(payload)
|
||||
return &http.Cookie{
|
||||
Name: adminSessionCookie,
|
||||
Value: payload + "." + signature,
|
||||
Path: "/",
|
||||
MaxAge: int(sm.ttl.Seconds()),
|
||||
HttpOnly: true,
|
||||
Secure: sm.secure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (sm *sessionManager) clearCookie() *http.Cookie {
|
||||
return &http.Cookie{
|
||||
Name: adminSessionCookie,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
Secure: sm.secure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *sessionManager) claimsFromRequest(c *gin.Context) (*sessionClaims, error) {
|
||||
cookie, err := c.Cookie(adminSessionCookie)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parts := strings.Split(cookie, ".")
|
||||
if len(parts) != 2 {
|
||||
return nil, errors.New("invalid session")
|
||||
}
|
||||
if !hmac.Equal([]byte(parts[1]), []byte(sm.sign(parts[0]))) {
|
||||
return nil, errors.New("invalid session signature")
|
||||
}
|
||||
data, err := base64.RawURLEncoding.DecodeString(parts[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var claims sessionClaims
|
||||
if err := json.Unmarshal(data, &claims); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if claims.Expires <= time.Now().Unix() {
|
||||
return nil, errors.New("session expired")
|
||||
}
|
||||
if claims.Role != adminRole {
|
||||
return nil, errors.New("admin required")
|
||||
}
|
||||
return &claims, nil
|
||||
}
|
||||
|
||||
func (sm *sessionManager) sign(payload string) string {
|
||||
mac := hmac.New(sha256.New, sm.secret)
|
||||
mac.Write([]byte(payload))
|
||||
return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
func requireAdmin(sm *sessionManager) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
claims, err := sm.claimsFromRequest(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "admin login required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set("admin_claims", claims)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
func requireAdmin(sm *sessionManager) gin.HandlerFunc { return authpkg.RequireAdmin(sm) }
|
||||
|
||||
Reference in New Issue
Block a user