feat(admin): 用户管理增强 - CSV 批量导入用户、批量状态/组操作、登录审计日志
Build and Deploy / build-and-deploy (push) Canceled after 0s
Build and Deploy / build-and-deploy (push) Canceled after 0s
This commit is contained in:
@@ -664,3 +664,62 @@ func AdminBatchDeleteOAuthClient(c *gin.Context) {
|
||||
}
|
||||
c.JSON(200, serializer.Response{})
|
||||
}
|
||||
|
||||
// AdminImportUsersCSV 批量导入 CSV 用户(字段:用户名,密码)
|
||||
func AdminImportUsersCSV(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.Err(c, serializer.NewError(serializer.CodeParamErr, "未找到上传文件 field=file", err)))
|
||||
return
|
||||
}
|
||||
res, err := admin.ImportUsers(c, file)
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.Err(c, err))
|
||||
return
|
||||
}
|
||||
c.JSON(200, serializer.Response{Data: res})
|
||||
}
|
||||
|
||||
// AdminBatchSetUserStatus 批量设置用户状态
|
||||
func AdminBatchSetUserStatus(c *gin.Context) {
|
||||
service := ParametersFromContext[*admin.BatchUserStatusService](c, admin.BatchUserStatusParamCtx{})
|
||||
err := service.SetStatus(c)
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.Err(c, err))
|
||||
return
|
||||
}
|
||||
c.JSON(200, serializer.Response{})
|
||||
}
|
||||
|
||||
// AdminBatchMoveUserGroup 批量移动用户到指定用户组
|
||||
func AdminBatchMoveUserGroup(c *gin.Context) {
|
||||
service := ParametersFromContext[*admin.BatchUserMoveGroupService](c, admin.BatchUserMoveGroupParamCtx{})
|
||||
err := service.MoveGroup(c)
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.Err(c, err))
|
||||
return
|
||||
}
|
||||
c.JSON(200, serializer.Response{})
|
||||
}
|
||||
|
||||
// AdminListLoginAudits 登录审计列表
|
||||
func AdminListLoginAudits(c *gin.Context) {
|
||||
service := ParametersFromContext[*admin.LoginAuditListService](c, admin.LoginAuditListParamCtx{})
|
||||
res, err := service.List(c)
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.Err(c, err))
|
||||
return
|
||||
}
|
||||
c.JSON(200, serializer.Response{Data: res})
|
||||
}
|
||||
|
||||
// AdminGetLoginAuditDailyStats 登录审计每日统计
|
||||
func AdminGetLoginAuditDailyStats(c *gin.Context) {
|
||||
service := ParametersFromContext[*admin.LoginAuditDailyStatsService](c, admin.LoginAuditDailyStatsParamCtx{})
|
||||
res, err := service.DailyStats(c)
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.Err(c, err))
|
||||
return
|
||||
}
|
||||
c.JSON(200, serializer.Response{Data: res})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user