169 lines
5.6 KiB
Go
169 lines
5.6 KiB
Go
package admin
|
|
|
|
import (
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.asio.asia/nixevol/NixMsg/internal/app/group"
|
|
"git.asio.asia/nixevol/NixMsg/internal/app/identity"
|
|
"git.asio.asia/nixevol/NixMsg/internal/auth"
|
|
"git.asio.asia/nixevol/NixMsg/internal/config"
|
|
"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
|
|
// Identity 端停用/启用/删除级联(I5);nil 时回退为仅改 enabled/删行。
|
|
Identity identity.Service
|
|
|
|
// Groups 群服务;nil 时群写操作返回 busy。列表/详情可只读库。
|
|
Groups group.Service
|
|
// Config 只读运行参数来源;零值用 config.Default()。
|
|
Config config.Config
|
|
// Version 概览里的版本号;空则 "dev"。
|
|
Version string
|
|
}
|
|
|
|
// 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
|
|
identity identity.Service
|
|
groups group.Service
|
|
cfg config.Config
|
|
version string
|
|
startedAt time.Time
|
|
|
|
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
|
|
}
|
|
cfg := d.Config
|
|
if cfg.Listen == "" {
|
|
cfg = config.Default()
|
|
}
|
|
ver := d.Version
|
|
if ver == "" {
|
|
ver = "dev"
|
|
}
|
|
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,
|
|
identity: d.Identity,
|
|
groups: d.Groups,
|
|
cfg: cfg,
|
|
version: ver,
|
|
startedAt: time.Now(),
|
|
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))
|
|
|
|
// A3
|
|
h.mux.Handle("GET /api/admin/overview", h.auth(h.handleOverview))
|
|
h.mux.Handle("GET /api/admin/registration", h.auth(h.handleRegistrationGet))
|
|
h.mux.Handle("PUT /api/admin/registration", h.auth(h.handleRegistrationPut))
|
|
h.mux.Handle("GET /api/admin/groups", h.auth(h.handleGroupList))
|
|
h.mux.Handle("POST /api/admin/groups", h.auth(h.handleGroupCreate))
|
|
h.mux.Handle("GET /api/admin/groups/{id}", h.auth(h.handleGroupGet))
|
|
h.mux.Handle("PATCH /api/admin/groups/{id}", h.auth(h.handleGroupRename))
|
|
h.mux.Handle("DELETE /api/admin/groups/{id}", h.auth(h.handleGroupDissolve))
|
|
h.mux.Handle("POST /api/admin/groups/{id}/members", h.auth(h.handleGroupAddMembers))
|
|
h.mux.Handle("DELETE /api/admin/groups/{id}/members/{endpointId}", h.auth(h.handleGroupRemoveMember))
|
|
h.mux.Handle("POST /api/admin/groups/{id}/transfer", h.auth(h.handleGroupTransfer))
|
|
h.mux.Handle("GET /api/admin/messages", h.auth(h.handleMessageList))
|
|
h.mux.Handle("GET /api/admin/messages/{seq}", h.auth(h.handleMessageGet))
|
|
h.mux.Handle("GET /api/admin/settings", h.auth(h.handleSettingsGet))
|
|
}
|