Files
YouduWiki/backend/handler/v1/user.go
2026-05-21 19:52:45 +08:00

303 lines
8.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}