merge: admin-web H-02 audit

# Conflicts:
#	docs/DEVIATIONS.md
#	internal/admin/endpoints.go
This commit is contained in:
Nixevol
2026-09-30 15:39:20 +08:00
13 changed files with 650 additions and 130 deletions
+56 -45
View File
@@ -246,7 +246,7 @@ func (h *Handler) handleEndpointCreate(w http.ResponseWriter, r *http.Request) {
DefaultDelaySeconds *int64 `json:"default_delay_seconds"`
}
if err := httpx.DecodeJSON(r, &req); err != nil {
h.audit(actorString(p), "endpoint_create", "", "bad_request", ip)
h.auditP(p, "endpoint_create", "", "bad_request", ip)
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "请求体无效")
return
}
@@ -255,7 +255,7 @@ func (h *Handler) handleEndpointCreate(w http.ResponseWriter, r *http.Request) {
delaySec = *req.DefaultDelaySeconds
}
if errMsg := validateEndpointFields(req.ID, req.Name, req.Remark, req.LoginPassword, req.TalkPassword, delaySec, h.maxScheduleSeconds()); errMsg != "" {
h.audit(actorString(p), "endpoint_create", req.ID, "bad_request", ip)
h.auditP(p, "endpoint_create", req.ID, "bad_request", ip)
httpx.WriteError(w, http.StatusBadRequest, "bad_request", errMsg)
return
}
@@ -266,7 +266,7 @@ func (h *Handler) handleEndpointCreate(w http.ResponseWriter, r *http.Request) {
if loginPW == "" {
pw, err := generateLoginPassword()
if err != nil {
h.audit(actorString(p), "endpoint_create", id, "error", ip)
h.auditP(p, "endpoint_create", id, "error", ip)
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
@@ -275,7 +275,7 @@ func (h *Handler) handleEndpointCreate(w http.ResponseWriter, r *http.Request) {
}
loginHash, err := h.hash.Hash(r.Context(), auth.PasswordLogin, loginPW)
if err != nil {
h.audit(actorString(p), "endpoint_create", id, "error", ip)
h.auditP(p, "endpoint_create", id, "error", ip)
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
@@ -283,7 +283,7 @@ func (h *Handler) handleEndpointCreate(w http.ResponseWriter, r *http.Request) {
if req.TalkPassword != "" {
th, hashErr := h.hash.Hash(r.Context(), auth.PasswordTalk, req.TalkPassword)
if hashErr != nil {
h.audit(actorString(p), "endpoint_create", id, "error", ip)
h.auditP(p, "endpoint_create", id, "error", ip)
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
@@ -301,15 +301,15 @@ func (h *Handler) handleEndpointCreate(w http.ResponseWriter, r *http.Request) {
})
if err != nil {
if isUniqueConstraint(err) {
h.audit(actorString(p), "endpoint_create", id, "id_taken", ip)
h.auditP(p, "endpoint_create", id, "id_taken", ip)
httpx.WriteError(w, http.StatusConflict, "id_taken", "编号已占用")
return
}
h.audit(actorString(p), "endpoint_create", id, "error", ip)
h.auditP(p, "endpoint_create", id, "error", ip)
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
h.audit(actorString(p), "endpoint_create", createdID, "ok", ip)
h.auditP(p, "endpoint_create", createdID, "ok", ip)
data := map[string]any{"id": createdID}
if pwGenerated {
data[loginPasswordOnceKey] = loginPW
@@ -343,28 +343,28 @@ func (h *Handler) handleEndpointPatch(w http.ResponseWriter, r *http.Request) {
Enabled *bool `json:"enabled"`
}
if err := httpx.DecodeJSON(r, &req); err != nil {
h.audit(actorString(p), "endpoint_patch", id, "bad_request", ip)
h.auditP(p, "endpoint_patch", id, "bad_request", ip)
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "请求体无效")
return
}
if req.Name == nil && req.Remark == nil && req.DefaultDelaySeconds == nil && req.Enabled == nil {
h.audit(actorString(p), "endpoint_patch", id, "bad_request", ip)
h.auditP(p, "endpoint_patch", id, "bad_request", ip)
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "无更新字段")
return
}
if req.Name != nil && !protocol.ValidName(*req.Name) {
h.audit(actorString(p), "endpoint_patch", id, "bad_request", ip)
h.auditP(p, "endpoint_patch", id, "bad_request", ip)
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "名称不合法")
return
}
if req.Remark != nil && utf8.RuneCountInString(*req.Remark) > maxRemarkChars {
h.audit(actorString(p), "endpoint_patch", id, "bad_request", ip)
h.auditP(p, "endpoint_patch", id, "bad_request", ip)
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "备注过长")
return
}
if req.DefaultDelaySeconds != nil {
if msg := validateDelaySeconds(*req.DefaultDelaySeconds, h.maxScheduleSeconds()); msg != "" {
h.audit(actorString(p), "endpoint_patch", id, "bad_request", ip)
h.auditP(p, "endpoint_patch", id, "bad_request", ip)
httpx.WriteError(w, http.StatusBadRequest, "bad_request", msg)
return
}
@@ -382,11 +382,11 @@ func (h *Handler) handleEndpointPatch(w http.ResponseWriter, r *http.Request) {
wasEnabled, err = h.patchEndpoint(r.Context(), id, req.Name, req.Remark, req.DefaultDelaySeconds, patchEnabled)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
h.audit(actorString(p), "endpoint_patch", id, "not_found", ip)
h.auditP(p, "endpoint_patch", id, "not_found", ip)
httpx.WriteError(w, http.StatusNotFound, "not_found", "端不存在")
return
}
h.audit(actorString(p), "endpoint_patch", id, "error", ip)
h.auditP(p, "endpoint_patch", id, "error", ip)
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
@@ -395,12 +395,12 @@ func (h *Handler) handleEndpointPatch(w http.ResponseWriter, r *http.Request) {
var found bool
found, err = h.setEndpointEnabled(r.Context(), id, *req.Enabled)
if err != nil {
h.audit(actorString(p), "endpoint_patch", id, "error", ip)
h.auditP(p, "endpoint_patch", id, "error", ip)
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
if !found {
h.audit(actorString(p), "endpoint_patch", id, "not_found", ip)
h.auditP(p, "endpoint_patch", id, "not_found", ip)
httpx.WriteError(w, http.StatusNotFound, "not_found", "端不存在")
return
}
@@ -412,11 +412,19 @@ func (h *Handler) handleEndpointPatch(w http.ResponseWriter, r *http.Request) {
}
row, err := h.getEndpoint(r.Context(), id)
if err != nil {
h.audit(actorString(p), "endpoint_patch", id, "error", ip)
h.auditP(p, "endpoint_patch", id, "error", ip)
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
h.audit(actorString(p), "endpoint_patch", id, "ok", ip)
detail := map[string]any{}
if req.Enabled != nil {
detail["enabled"] = *req.Enabled
}
if len(detail) == 0 {
h.auditP(p, "endpoint_patch", id, "ok", ip)
} else {
h.auditPD(p, "endpoint_patch", id, "ok", ip, detail)
}
httpx.WriteOK(w, row.toAPI(h.endpointLoginLocked(row.ID), true))
}
@@ -427,17 +435,17 @@ func (h *Handler) handleEndpointDelete(w http.ResponseWriter, r *http.Request) {
ok, err := h.deleteEndpointBasic(r.Context(), id)
if err != nil {
h.audit(actorString(p), "endpoint_delete", id, "error", ip)
h.auditP(p, "endpoint_delete", id, "error", ip)
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
if !ok {
h.audit(actorString(p), "endpoint_delete", id, "not_found", ip)
h.auditP(p, "endpoint_delete", id, "not_found", ip)
httpx.WriteError(w, http.StatusNotFound, "not_found", "端不存在")
return
}
h.afterDeleteKick(r.Context(), id)
h.audit(actorString(p), "endpoint_delete", id, "ok", ip)
h.auditP(p, "endpoint_delete", id, "ok", ip)
httpx.WriteOK(w, map[string]any{})
}
@@ -450,14 +458,14 @@ func (h *Handler) handleEndpointBatch(w http.ResponseWriter, r *http.Request) {
Action string `json:"action"`
}
if err := httpx.DecodeJSON(r, &req); err != nil || len(req.IDs) == 0 {
h.audit(actorString(p), "endpoint_batch", "", "bad_request", ip)
h.auditP(p, "endpoint_batch", "", "bad_request", ip)
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "请求体无效")
return
}
switch req.Action {
case "disable", "enable", "delete":
default:
h.audit(actorString(p), "endpoint_batch", "", "bad_request", ip)
h.auditP(p, "endpoint_batch", "", "bad_request", ip)
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "action 无效")
return
}
@@ -491,7 +499,10 @@ func (h *Handler) handleEndpointBatch(w http.ResponseWriter, r *http.Request) {
}
okIDs = append(okIDs, id)
}
h.audit(actorString(p), "endpoint_batch_"+req.Action, strings.Join(okIDs, ","), "ok", ip)
h.auditPD(p, "endpoint_batch_"+req.Action, strings.Join(okIDs, ","), batchAuditResult(len(okIDs), len(failed)), ip, map[string]any{
"ok_ids": okIDs,
"failed": failed,
})
httpx.WriteOK(w, map[string]any{"ok_ids": okIDs, "failed": failed})
}
@@ -502,22 +513,22 @@ func (h *Handler) handleEndpointKick(w http.ResponseWriter, r *http.Request) {
exists, err := h.endpointExists(r.Context(), id)
if err != nil {
h.audit(actorString(p), "endpoint_kick", id, "error", ip)
h.auditP(p, "endpoint_kick", id, "error", ip)
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
if !exists {
h.audit(actorString(p), "endpoint_kick", id, "not_found", ip)
h.auditP(p, "endpoint_kick", id, "not_found", ip)
httpx.WriteError(w, http.StatusNotFound, "not_found", "端不存在")
return
}
kicked, err := h.kickEndpoint(r.Context(), id)
if err != nil {
h.audit(actorString(p), "endpoint_kick", id, "error", ip)
h.auditP(p, "endpoint_kick", id, "error", ip)
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
h.audit(actorString(p), "endpoint_kick", id, "ok", ip)
h.auditP(p, "endpoint_kick", id, "ok", ip)
httpx.WriteOK(w, map[string]any{"kicked": kicked})
}
@@ -530,12 +541,12 @@ func (h *Handler) handleEndpointResetLoginPassword(w http.ResponseWriter, r *htt
LoginPassword string `json:"login_password"`
}
if err := httpx.DecodeJSON(r, &req); err != nil {
h.audit(actorString(p), "endpoint_reset_login_password", id, "bad_request", ip)
h.auditP(p, "endpoint_reset_login_password", id, "bad_request", ip)
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "请求体无效")
return
}
if !protocol.ValidLoginPassword(req.LoginPassword) {
h.audit(actorString(p), "endpoint_reset_login_password", id, "bad_request", ip)
h.auditP(p, "endpoint_reset_login_password", id, "bad_request", ip)
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "登录密码不合法")
return
}
@@ -544,7 +555,7 @@ func (h *Handler) handleEndpointResetLoginPassword(w http.ResponseWriter, r *htt
if pw == "" {
gen, err := generateLoginPassword()
if err != nil {
h.audit(actorString(p), "endpoint_reset_login_password", id, "error", ip)
h.auditP(p, "endpoint_reset_login_password", id, "error", ip)
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
@@ -552,18 +563,18 @@ func (h *Handler) handleEndpointResetLoginPassword(w http.ResponseWriter, r *htt
}
hash, err := h.hash.Hash(r.Context(), auth.PasswordLogin, pw)
if err != nil {
h.audit(actorString(p), "endpoint_reset_login_password", id, "error", ip)
h.auditP(p, "endpoint_reset_login_password", id, "error", ip)
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
ok, err := h.resetLoginPassword(r.Context(), id, hash)
if err != nil {
h.audit(actorString(p), "endpoint_reset_login_password", id, "error", ip)
h.auditP(p, "endpoint_reset_login_password", id, "error", ip)
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
if !ok {
h.audit(actorString(p), "endpoint_reset_login_password", id, "not_found", ip)
h.auditP(p, "endpoint_reset_login_password", id, "not_found", ip)
httpx.WriteError(w, http.StatusNotFound, "not_found", "端不存在")
return
}
@@ -572,7 +583,7 @@ func (h *Handler) handleEndpointResetLoginPassword(w http.ResponseWriter, r *htt
if kickErr != nil {
result = "ok_kick_failed"
}
h.audit(actorString(p), "endpoint_reset_login_password", id, result, ip)
h.auditP(p, "endpoint_reset_login_password", id, result, ip)
httpx.WriteOK(w, map[string]any{loginPasswordOnceKey: pw})
}
@@ -585,12 +596,12 @@ func (h *Handler) handleEndpointTalkPassword(w http.ResponseWriter, r *http.Requ
TalkPassword string `json:"talk_password"`
}
if err := httpx.DecodeJSON(r, &req); err != nil {
h.audit(actorString(p), "endpoint_talk_password", id, "bad_request", ip)
h.auditP(p, "endpoint_talk_password", id, "bad_request", ip)
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "请求体无效")
return
}
if !protocol.ValidTalkPassword(req.TalkPassword) {
h.audit(actorString(p), "endpoint_talk_password", id, "bad_request", ip)
h.auditP(p, "endpoint_talk_password", id, "bad_request", ip)
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "对话密码不合法")
return
}
@@ -599,7 +610,7 @@ func (h *Handler) handleEndpointTalkPassword(w http.ResponseWriter, r *http.Requ
if req.TalkPassword != "" {
th, err := h.hash.Hash(r.Context(), auth.PasswordTalk, req.TalkPassword)
if err != nil {
h.audit(actorString(p), "endpoint_talk_password", id, "error", ip)
h.auditP(p, "endpoint_talk_password", id, "error", ip)
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
@@ -607,16 +618,16 @@ func (h *Handler) handleEndpointTalkPassword(w http.ResponseWriter, r *http.Requ
}
ok, err := h.setTalkPassword(r.Context(), id, talkHash)
if err != nil {
h.audit(actorString(p), "endpoint_talk_password", id, "error", ip)
h.auditP(p, "endpoint_talk_password", id, "error", ip)
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
if !ok {
h.audit(actorString(p), "endpoint_talk_password", id, "not_found", ip)
h.auditP(p, "endpoint_talk_password", id, "not_found", ip)
httpx.WriteError(w, http.StatusNotFound, "not_found", "端不存在")
return
}
h.audit(actorString(p), "endpoint_talk_password", id, "ok", ip)
h.auditP(p, "endpoint_talk_password", id, "ok", ip)
httpx.WriteOK(w, map[string]any{"talk_password_set": talkHash.Valid})
}
@@ -627,17 +638,17 @@ func (h *Handler) handleEndpointUnlock(w http.ResponseWriter, r *http.Request) {
exists, err := h.endpointExists(r.Context(), id)
if err != nil {
h.audit(actorString(p), "endpoint_unlock", id, "error", ip)
h.auditP(p, "endpoint_unlock", id, "error", ip)
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
return
}
if !exists {
h.audit(actorString(p), "endpoint_unlock", id, "not_found", ip)
h.auditP(p, "endpoint_unlock", id, "not_found", ip)
httpx.WriteError(w, http.StatusNotFound, "not_found", "端不存在")
return
}
h.locks.ClearEndpoint(id)
h.audit(actorString(p), "endpoint_unlock", id, "ok", ip)
h.auditP(p, "endpoint_unlock", id, "ok", ip)
httpx.WriteOK(w, map[string]any{})
}