Files
NixMsg/internal/admin/handler.go
T

145 lines
4.3 KiB
Go

package admin
import (
"log/slog"
"net"
"net/http"
"sync"
"time"
"git.asio.asia/nixevol/NixMsg/internal/auth"
"git.asio.asia/nixevol/NixMsg/internal/store"
)
const (
cookieName = "nixmsg_admin"
csrfHeader = "X-Nixmsg-Request"
csrfValue = "1"
adminUsername = "admin"
settingAdminHash = "admin_password_hash"
defaultSessionTTL = 12 * time.Hour
minPasswordLen = 12
lastUsedMinGap = time.Minute
)
// Deps 是管理 Handler 的依赖。
type Deps struct {
DB *store.DB
Hash auth.HashPool
Tokens auth.APITokens
Locks auth.LoginLocks
Logger *slog.Logger
// TrustedProxies 受信任代理网段。
TrustedProxies []*net.IPNet
// SessionTTL 会话有效期;零值用 12 小时。
SessionTTL time.Duration
// SecureCookies 为 true 时 Cookie 始终带 Secure;否则按请求是否 HTTPS 决定。
SecureCookies bool
// KickEndpoint 踢下线钩子(只断开连接);nil 时踢线为 no-op。
KickEndpoint EndpointKickFunc
}
// Handler 是可挂载的管理接口(路由前缀 /api/admin/)。
type Handler struct {
db *store.DB
hash auth.HashPool
tokens auth.APITokens
locks auth.LoginLocks
log *slog.Logger
trusted []*net.IPNet
ttl time.Duration
forceSec bool
kick EndpointKickFunc
mux *http.ServeMux
lastUsedMu sync.Mutex
lastUsed map[string]time.Time // api token id -> last DB write
}
// New 构造可挂载的管理 Handler。返回值实现 http.Handler。
func New(d Deps) *Handler {
if d.Logger == nil {
d.Logger = slog.Default()
}
if d.Locks == nil {
d.Locks = NewMemoryLoginLocks()
}
ttl := d.SessionTTL
if ttl <= 0 {
ttl = defaultSessionTTL
}
h := &Handler{
db: d.DB,
hash: d.Hash,
tokens: d.Tokens,
locks: d.Locks,
log: d.Logger,
trusted: d.TrustedProxies,
ttl: ttl,
forceSec: d.SecureCookies,
kick: d.KickEndpoint,
mux: http.NewServeMux(),
lastUsed: make(map[string]time.Time),
}
h.routes()
return h
}
// ServeHTTP 实现 http.Handler。
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.mux.ServeHTTP(w, r)
}
func (h *Handler) routes() {
// 公开
h.mux.HandleFunc("POST /api/admin/login", h.handleLogin)
// 需鉴权
h.mux.Handle("POST /api/admin/logout", h.auth(h.handleLogout))
h.mux.Handle("GET /api/admin/me", h.auth(h.handleMe))
h.mux.Handle("POST /api/admin/password", h.auth(h.handlePassword))
// 令牌管理:仅 Cookie
h.mux.Handle("GET /api/admin/tokens", h.auth(h.handleTokenList))
h.mux.Handle("POST /api/admin/tokens", h.auth(h.handleTokenCreate))
h.mux.Handle("PATCH /api/admin/tokens/{id}", h.auth(h.handleTokenPatch))
h.mux.Handle("DELETE /api/admin/tokens/{id}", h.auth(h.handleTokenDelete))
// A2 端管理
h.mux.Handle("GET /api/admin/endpoints", h.auth(h.handleEndpointList))
h.mux.Handle("POST /api/admin/endpoints", h.auth(h.handleEndpointCreate))
h.mux.Handle("POST /api/admin/endpoints/import", h.auth(h.handleEndpointImport))
h.mux.Handle("POST /api/admin/endpoints/batch", h.auth(h.handleEndpointBatch))
h.mux.Handle("GET /api/admin/endpoints/{id}", h.auth(h.handleEndpointGet))
h.mux.Handle("PATCH /api/admin/endpoints/{id}", h.auth(h.handleEndpointPatch))
h.mux.Handle("DELETE /api/admin/endpoints/{id}", h.auth(h.handleEndpointDelete))
h.mux.Handle("POST /api/admin/endpoints/{id}/kick", h.auth(h.handleEndpointKick))
h.mux.Handle("POST /api/admin/endpoints/{id}/reset-login-password", h.auth(h.handleEndpointResetLoginPassword))
h.mux.Handle("PUT /api/admin/endpoints/{id}/talk-password", h.auth(h.handleEndpointTalkPassword))
h.mux.Handle("POST /api/admin/endpoints/{id}/unlock", h.auth(h.handleEndpointUnlock))
// 其余管理路由:鉴权生效,业务暂 501(A3)
for _, p := range stubRoutes {
h.mux.Handle(p, h.auth(h.handleNotImplemented))
}
}
var stubRoutes = []string{
"GET /api/admin/overview",
"GET /api/admin/registration",
"PUT /api/admin/registration",
"GET /api/admin/groups",
"POST /api/admin/groups",
"GET /api/admin/groups/{id}",
"PATCH /api/admin/groups/{id}",
"DELETE /api/admin/groups/{id}",
"POST /api/admin/groups/{id}/members",
"DELETE /api/admin/groups/{id}/members/{endpointId}",
"POST /api/admin/groups/{id}/transfer",
"GET /api/admin/messages",
"GET /api/admin/messages/{seq}",
"GET /api/admin/settings",
}