feat(admin): 用户管理增强 - CSV 批量导入用户、批量状态/组操作、登录审计日志
Build and Deploy / build-and-deploy (push) Canceled after 0s

This commit is contained in:
2026-09-13 12:09:55 +08:00
parent 32959c7330
commit 62e2250202
10 changed files with 884 additions and 0 deletions
+47
View File
@@ -1,6 +1,8 @@
package controllers
import (
"time"
"github.com/cloudreve/Cloudreve/v4/application/dependency"
"github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/inventory"
@@ -13,6 +15,37 @@ import (
"github.com/samber/lo"
)
// recordLoginAuditAsync 异步写入登录审计,失败不影响主流程
func recordLoginAuditAsync(c *gin.Context, userID *int, success bool, failReason, usernameTried *string) {
defer func() { _ = recover() }()
dep := dependency.FromContext(c)
client := dep.LoginAuditClient()
ip := c.ClientIP()
ua := c.GetHeader("User-Agent")
ctx := c.Request.Context()
go func() {
defer func() { _ = recover() }()
_, _ = client.RecordLogin(ctx, userID, ip, ua, success, failReason, usernameTried)
}()
}
// markLogoutAsync 异步标记登出,失败不影响主流程
func markLogoutAsync(c *gin.Context, uid int) {
defer func() { _ = recover() }()
dep := dependency.FromContext(c)
client := dep.LoginAuditClient()
ip := c.ClientIP()
ctx := c.Request.Context()
now := time.Now()
go func() {
defer func() { _ = recover() }()
_ = client.MarkLogout(ctx, uid, ip, now)
}()
}
// ptrString 返回字符串指针
func ptrString(s string) *string { return &s }
// StartLoginAuthn 开始注册WebAuthn登录
func StartLoginAuthn(c *gin.Context) {
res, err := user.PreparePasskeyLogin(c)
@@ -29,6 +62,7 @@ func FinishLoginAuthn(c *gin.Context) {
service := ParametersFromContext[*user.FinishPasskeyLoginService](c, user.FinishPasskeyLoginParameterCtx{})
u, err := service.FinishPasskeyLogin(c)
if err != nil {
recordLoginAuditAsync(c, nil, false, ptrString(err.Error()), nil)
c.JSON(200, serializer.Err(c, err))
c.Abort()
return
@@ -77,6 +111,7 @@ func UserLoginValidation(c *gin.Context) {
service := ParametersFromContext[*user.UserLoginService](c, user.LoginParameterCtx{})
expectedUser, twoFaSession, err := service.Login(c)
if err != nil {
recordLoginAuditAsync(c, nil, false, ptrString(err.Error()), ptrString(service.UserName))
c.JSON(200, serializer.Err(c, err))
c.Abort()
return
@@ -98,6 +133,7 @@ func UserLogin2FAValidation(c *gin.Context) {
service := ParametersFromContext[*user.OtpValidationService](c, user.OtpValidationParameterCtx{})
expectedUser, err := service.Verify2FA(c)
if err != nil {
recordLoginAuditAsync(c, nil, false, ptrString(err.Error()), nil)
c.JSON(200, serializer.Err(c, err))
c.Abort()
return
@@ -116,6 +152,12 @@ func UserIssueToken(c *gin.Context) {
return
}
// 登录成功:写审计记录
if u, ok := c.Value(inventory.UserCtx{}).(*ent.User); ok {
uid := u.ID
recordLoginAuditAsync(c, &uid, true, nil, ptrString(u.Email))
}
c.JSON(200, serializer.Response{
Data: resp,
})
@@ -180,6 +222,11 @@ func UserSignOut(c *gin.Context) {
return
}
// 标记登出时间
if current := inventory.UserFromContext(c); current != nil && !inventory.IsAnonymousUser(current) {
markLogoutAsync(c, current.ID)
}
c.JSON(200, serializer.Response{
Data: res,
})