package v1 import ( "context" "fmt" "github.com/google/uuid" "github.com/labstack/echo/v4" v1 "github.com/chaitin/panda-wiki/api/user/v1" "github.com/chaitin/panda-wiki/config" "github.com/chaitin/panda-wiki/consts" "github.com/chaitin/panda-wiki/domain" "github.com/chaitin/panda-wiki/handler" "github.com/chaitin/panda-wiki/log" "github.com/chaitin/panda-wiki/middleware" "github.com/chaitin/panda-wiki/pkg/ratelimit" "github.com/chaitin/panda-wiki/store/cache" "github.com/chaitin/panda-wiki/usecase" ) type UserHandler struct { *handler.BaseHandler usecase *usecase.UserUsecase logger *log.Logger config *config.Config auth middleware.AuthMiddleware rateLimiter *ratelimit.RateLimiter } func NewUserHandler(e *echo.Echo, baseHandler *handler.BaseHandler, logger *log.Logger, usecase *usecase.UserUsecase, auth middleware.AuthMiddleware, config *config.Config, cache *cache.Cache) *UserHandler { handlerLogger := logger.WithModule("handler.v1.user") h := &UserHandler{ BaseHandler: baseHandler, logger: handlerLogger, usecase: usecase, auth: auth, config: config, rateLimiter: ratelimit.NewRateLimiter(handlerLogger, cache), } group := e.Group("/api/v1/user") group.POST("/login", h.Login) group.GET("", h.GetUserInfo, h.auth.Authorize) group.GET("/list", h.ListUsers, h.auth.Authorize) group.POST("/create", h.CreateUser, h.auth.Authorize, h.auth.ValidateUserRole(consts.UserRoleAdmin)) group.PUT("/reset_password", h.ResetPassword, h.auth.Authorize, h.auth.ValidateUserRole(consts.UserRoleAdmin)) group.DELETE("/delete", h.DeleteUser, h.auth.Authorize, h.auth.ValidateUserRole(consts.UserRoleAdmin)) return h } // CreateUser // // @Summary CreateUser // @Description CreateUser // @Tags user // @Accept json // @Produce json // @Param body body v1.CreateUserReq true "CreateUser Request" // @Success 200 {object} domain.Response{data=v1.CreateUserResp} // @Router /api/v1/user/create [post] func (h *UserHandler) CreateUser(c echo.Context) error { var req v1.CreateUserReq if err := c.Bind(&req); err != nil { return h.NewResponseWithError(c, "invalid request", err) } if err := c.Validate(&req); err != nil { return h.NewResponseWithError(c, "invalid request", err) } uid := uuid.New().String() err := h.usecase.CreateUser(c.Request().Context(), &domain.User{ ID: uid, Account: req.Account, Password: req.Password, Role: req.Role, }, consts.GetLicenseEdition(c)) if err != nil { return h.NewResponseWithError(c, "failed to create user", err) } return h.NewResponseWithData(c, v1.CreateUserResp{ID: uid}) } // Login // // @Summary Login // @Description Login // @Tags user // @Accept json // @Produce json // @Param body body v1.LoginReq true "Login Request" // @Success 200 {object} v1.LoginResp // @Router /api/v1/user/login [post] func (h *UserHandler) Login(c echo.Context) error { var req v1.LoginReq if err := c.Bind(&req); err != nil { return h.NewResponseWithError(c, "invalid request", err) } if err := c.Validate(&req); err != nil { return h.NewResponseWithError(c, "invalid request", err) } ctx := c.Request().Context() ip := c.RealIP() locked, remaining := h.rateLimiter.CheckIPLocked(ctx, ip) if locked { h.logger.Warn("IP is locked", "ip", ip, "remaining", remaining) return h.NewResponseWithError(c, fmt.Sprintf("账号已被锁定,请 %s 后重试", remaining.String()), nil) } token, err := h.usecase.VerifyUserAndGenerateToken(ctx, req) if err != nil { h.rateLimiter.LockAttempt(ctx, ip) return h.NewResponseWithError(c, "用户名或密码错误", err) } go func() { if err := h.rateLimiter.ResetLoginAttempts(context.Background(), ip); err != nil { h.logger.Error("failed to reset login attempts", "error", err, "ip", ip) } }() return h.NewResponseWithData(c, v1.LoginResp{Token: token}) } // GetUserInfo // // @Summary GetUser // @Description GetUser // @Tags user // @Accept json // @Success 200 {object} v1.UserInfoResp // @Router /api/v1/user [get] func (h *UserHandler) GetUserInfo(c echo.Context) error { ctx := c.Request().Context() authInfo := domain.GetAuthInfoFromCtx(ctx) if authInfo == nil { return h.NewResponseWithError(c, "authInfo not found in context", nil) } user, err := h.usecase.GetUser(c.Request().Context(), authInfo.UserId) if err != nil { return h.NewResponseWithError(c, "failed to get user", err) } userInfo := &v1.UserInfoResp{ ID: user.ID, Account: user.Account, Role: user.Role, IsToken: authInfo.IsToken, LastAccess: &user.LastAccess, CreatedAt: user.CreatedAt, } return h.NewResponseWithData(c, userInfo) } // ListUsers // // @Summary ListUsers // @Description ListUsers // @Tags user // @Accept json // @Produce json // @Success 200 {object} domain.PWResponse{data=v1.UserListResp} // @Router /api/v1/user/list [get] func (h *UserHandler) ListUsers(c echo.Context) error { var req v1.UserListReq if err := c.Bind(&req); err != nil { return h.NewResponseWithError(c, "invalid request", err) } users, err := h.usecase.ListUsers(c.Request().Context()) if err != nil { return h.NewResponseWithError(c, "failed to list users", err) } return h.NewResponseWithData(c, users) } // ResetPassword // // @Summary ResetPassword // @Description ResetPassword // @Tags user // @Accept json // @Produce json // @Param body body v1.ResetPasswordReq true "ResetPassword Request" // @Success 200 {object} domain.Response // @Router /api/v1/user/reset_password [put] func (h *UserHandler) ResetPassword(c echo.Context) error { ctx := c.Request().Context() var req v1.ResetPasswordReq if err := c.Bind(&req); err != nil { return h.NewResponseWithError(c, "invalid request", err) } if err := c.Validate(&req); err != nil { return h.NewResponseWithError(c, "invalid request", err) } authInfo := domain.GetAuthInfoFromCtx(ctx) if authInfo == nil { return h.NewResponseWithError(c, "authInfo not found in context", nil) } if authInfo.IsToken { return h.NewResponseWithError(c, "this api not support token call", nil) } user, err := h.usecase.GetUser(ctx, authInfo.UserId) if err != nil { return h.NewResponseWithError(c, "failed to get user", err) } if user.Account == "admin" { // admin 改不了自己的密码 if authInfo.UserId == req.ID { return h.NewResponseWithError(c, "请修改安装目录下 .env 文件中的 ADMIN_PASSWORD,并重启 panda-wiki-api 容器使更改生效。", nil) } } else { targetUser, err := h.usecase.GetUser(ctx, req.ID) if err != nil { return h.NewResponseWithError(c, "failed to get target user", err) } // 超级管理员不能改其他超级管理员密码 if targetUser.Role == consts.UserRoleAdmin && targetUser.ID != authInfo.UserId { return h.NewResponseWithError(c, "无法修改其他超级管理员密码", nil) } } err = h.usecase.ResetPassword(c.Request().Context(), &req) if err != nil { return h.NewResponseWithError(c, "failed to reset password", err) } return h.NewResponseWithData(c, nil) } // DeleteUser // // @Summary DeleteUser // @Description DeleteUser // @Tags user // @Accept json // @Produce json // @Param params query v1.DeleteUserReq true "DeleteUser Request" // @Success 200 {object} domain.Response // @Router /api/v1/user/delete [delete] func (h *UserHandler) DeleteUser(c echo.Context) error { ctx := c.Request().Context() var req v1.DeleteUserReq if err := c.Bind(&req); err != nil { return h.NewResponseWithError(c, "invalid request", err) } authInfo := domain.GetAuthInfoFromCtx(ctx) if authInfo == nil { return h.NewResponseWithError(c, "authInfo not found in context", nil) } if authInfo.IsToken { return h.NewResponseWithError(c, "this api not support token call", nil) } if authInfo.UserId == req.UserID { return h.NewResponseWithError(c, "cannot delete yourself", nil) } user, err := h.usecase.GetUser(ctx, authInfo.UserId) if err != nil { return h.NewResponseWithError(c, "failed to get user", err) } targetUser, err := h.usecase.GetUser(ctx, req.UserID) if err != nil { return h.NewResponseWithError(c, "failed to get target user", err) } if targetUser.Account == "admin" { return h.NewResponseWithError(c, "cannot delete admin user", nil) } // 非admin账号的管理员只能删除普通用户的账户 if user.Account != "admin" { if targetUser.Role != consts.UserRoleUser { return h.NewResponseWithError(c, "cannot delete other admin users", nil) } } err = h.usecase.DeleteUser(ctx, req.UserID) if err != nil { return h.NewResponseWithError(c, "failed to delete user", err) } return h.NewResponseWithData(c, nil) }