0.11.0: 旗舰版功能补齐(License/登录记录/会话管理/角色管理/门户定时任务/模型配额/输出脱敏/供应链扫描/记忆管理/AI助手/真实概览)
- 新增迁移 000031-000034(登录日志/角色/模型配额/记忆) - 新增包: license/memory/modelquota/assistant,扫描引擎 - 全部功能后端+前端+端到端验证通过(25 包单测)
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -42,6 +43,7 @@ type factorRequest struct {
|
||||
func NewHTTPHandler(service *Service) *HTTPHandler {
|
||||
handler := &HTTPHandler{service: service, mux: http.NewServeMux()}
|
||||
handler.mux.HandleFunc("POST /api/v1/admin/login", handler.login(KindAdmin))
|
||||
handler.mux.HandleFunc("GET /api/v1/admin/login-logs", handler.loginLogs(KindAdmin))
|
||||
handler.registerTOTP(KindAdmin, "/api/v1/admin")
|
||||
handler.mux.HandleFunc("GET /api/v1/admin/whoami", handler.whoami(KindAdmin))
|
||||
handler.mux.HandleFunc("POST /api/v1/admin/password", handler.changePassword(KindAdmin))
|
||||
@@ -82,6 +84,33 @@ func (h *HTTPHandler) ServeHTTP(writer http.ResponseWriter, request *http.Reques
|
||||
h.mux.ServeHTTP(writer, request)
|
||||
}
|
||||
|
||||
func (h *HTTPHandler) loginLogs(kind Kind) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
account, err := h.service.Authenticate(request.Context(), KindAdmin, request.Header.Get("Authorization"))
|
||||
if err != nil {
|
||||
apiresponse.Error(writer, http.StatusUnauthorized, "登录状态无效或已过期")
|
||||
return
|
||||
}
|
||||
if !HasPermission(account, PermissionAuditRead) {
|
||||
apiresponse.Error(writer, http.StatusForbidden, "缺少审计日志查看权限")
|
||||
return
|
||||
}
|
||||
login := strings.TrimSpace(request.URL.Query().Get("login"))
|
||||
limit := 50
|
||||
if value := request.URL.Query().Get("limit"); value != "" {
|
||||
if parsed, parseErr := strconv.Atoi(value); parseErr == nil {
|
||||
limit = parsed
|
||||
}
|
||||
}
|
||||
logs, err := h.service.ListLoginLogs(request.Context(), kind, login, limit)
|
||||
if err != nil {
|
||||
apiresponse.Error(writer, http.StatusServiceUnavailable, "登录记录查询失败")
|
||||
return
|
||||
}
|
||||
apiresponse.OK(writer, logs)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTTPHandler) login(kind Kind) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
// 防爆破:按 IP 的滑动窗口限流,超限返回 429(与账号锁定叠加)。
|
||||
@@ -109,9 +138,13 @@ func (h *HTTPHandler) login(kind Kind) http.HandlerFunc {
|
||||
}
|
||||
result, err := h.service.Login(request.Context(), kind, login, input.Password)
|
||||
if err != nil {
|
||||
// 登录失败审计(429 限流在 AllowLogin 阶段已拦截,此处都是真实失败)。
|
||||
_ = h.service.RecordLoginLog(request.Context(), kind, login, false, h.service.ClientIP(request), request.UserAgent(), loginFailureReason(err))
|
||||
h.writeIdentityError(writer, err)
|
||||
return
|
||||
}
|
||||
// 登录成功(含进入 TOTP 挑战阶段)。
|
||||
_ = h.service.RecordLoginLog(request.Context(), kind, login, true, h.service.ClientIP(request), request.UserAgent(), "success")
|
||||
apiresponse.OK(writer, map[string]any{
|
||||
"token": result.Token, "refreshToken": "", "require_totp": result.RequireTOTP,
|
||||
"temp_token": result.TempToken,
|
||||
@@ -119,6 +152,28 @@ func (h *HTTPHandler) login(kind Kind) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// loginFailureReason 把登录错误归一化为审计用原因码。
|
||||
func loginFailureReason(err error) string {
|
||||
switch {
|
||||
case errors.Is(err, ErrInvalidCredentials):
|
||||
return "invalid_credentials"
|
||||
case errors.Is(err, ErrAccountDisabled):
|
||||
return "account_disabled"
|
||||
case errors.Is(err, ErrInvalidTOTP):
|
||||
return "invalid_totp"
|
||||
case errors.Is(err, ErrTOTPNotEnabled), errors.Is(err, ErrTOTPSetupRequired):
|
||||
return "totp_not_configured"
|
||||
case errors.Is(err, ErrUnavailable):
|
||||
return "service_unavailable"
|
||||
default:
|
||||
var locked LockedError
|
||||
if errors.As(err, &locked) {
|
||||
return "account_locked"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTTPHandler) registerTOTP(kind Kind, prefix string) {
|
||||
h.mux.HandleFunc("POST "+prefix+"/login/totp", h.completeTOTPLogin(kind))
|
||||
h.mux.HandleFunc("GET "+prefix+"/totp/status", h.totpStatus(kind))
|
||||
@@ -142,9 +197,11 @@ func (h *HTTPHandler) completeTOTPLogin(kind Kind) http.HandlerFunc {
|
||||
}
|
||||
result, err := h.service.CompleteTOTPLogin(request.Context(), kind, input.TempToken, input.Code, input.BackupCode)
|
||||
if err != nil {
|
||||
_ = h.service.RecordLoginLog(request.Context(), kind, "", false, h.service.ClientIP(request), request.UserAgent(), loginFailureReason(err))
|
||||
h.writeIdentityError(writer, err)
|
||||
return
|
||||
}
|
||||
_ = h.service.RecordLoginLog(request.Context(), kind, result.Account.Login, true, h.service.ClientIP(request), request.UserAgent(), "success")
|
||||
apiresponse.OK(writer, map[string]any{"token": result.Token, "refreshToken": "", "require_totp": false})
|
||||
}
|
||||
}
|
||||
@@ -426,6 +483,13 @@ func adminMenus(account Account) []map[string]any {
|
||||
if HasPermission(account, PermissionScheduledTaskRead) || HasPermission(account, PermissionScheduledTaskManage) {
|
||||
systemChildren = append(systemChildren, map[string]any{"name": "ScheduledTasks", "path": "scheduled-tasks", "component": "/gateway/scheduled-tasks", "meta": map[string]any{"title": "定时任务"}})
|
||||
}
|
||||
if HasPermission(account, PermissionAuditRead) {
|
||||
systemChildren = append(systemChildren, map[string]any{"name": "LoginLogs", "path": "login-logs", "component": "/system/login-logs", "meta": map[string]any{"title": "登录记录"}})
|
||||
}
|
||||
if HasPermission(account, PermissionSystemManage) {
|
||||
systemChildren = append(systemChildren, map[string]any{"name": "Assistant", "path": "assistant", "component": "/system/assistant", "meta": map[string]any{"title": "AI 助手"}})
|
||||
systemChildren = append(systemChildren, map[string]any{"name": "License", "path": "license", "component": "/system/license", "meta": map[string]any{"title": "License 授权"}})
|
||||
}
|
||||
if len(systemChildren) > 0 {
|
||||
menus = append(menus, map[string]any{"name": "System", "path": "/system", "component": "/index/index", "meta": map[string]any{"title": "系统管理", "icon": "ri:user-3-line"}, "children": systemChildren})
|
||||
}
|
||||
@@ -442,6 +506,9 @@ func portalMenus() []map[string]any {
|
||||
{"name": "PortalAccess", "path": "access", "component": "/portal/access", "meta": map[string]any{"title": "模型权限"}},
|
||||
{"name": "PortalFiles", "path": "files", "component": "/portal/files", "meta": map[string]any{"title": "文件仓库"}},
|
||||
{"name": "PortalInbox", "path": "inbox", "component": "/portal/inbox", "meta": map[string]any{"title": "站内消息"}},
|
||||
{"name": "PortalScheduledTasks", "path": "scheduled-tasks", "component": "/portal/scheduled-tasks", "meta": map[string]any{"title": "定时任务"}},
|
||||
{"name": "PortalMemories", "path": "memories", "component": "/portal/memories", "meta": map[string]any{"title": "记忆管理"}},
|
||||
{"name": "PortalLoginLogs", "path": "login-logs", "component": "/portal/login-logs", "meta": map[string]any{"title": "登录记录"}},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user