Files
MonkeyCode/backend/internal/user/handler/v1/user.go
yokowu b7f3db2194 feat: 进一步开放开源版功能
- 开源版支持查询7天内的统计数据
- 开源版支持安全扫描功能
- 优化版本信息页面
2025-09-04 16:53:56 +08:00

706 lines
20 KiB
Go

package v1
import (
"bytes"
"context"
"crypto/md5"
"fmt"
"log/slog"
"net/http"
"strings"
"sync"
"time"
"github.com/GoYoko/web"
"golang.org/x/time/rate"
"github.com/chaitin/MonkeyCode/backend/config"
"github.com/chaitin/MonkeyCode/backend/consts"
"github.com/chaitin/MonkeyCode/backend/domain"
"github.com/chaitin/MonkeyCode/backend/errcode"
"github.com/chaitin/MonkeyCode/backend/internal/middleware"
"github.com/chaitin/MonkeyCode/backend/pkg/session"
"github.com/chaitin/MonkeyCode/backend/pkg/version"
"github.com/chaitin/MonkeyCode/backend/pkg/vsix"
)
// CacheEntry 缓存条目
type CacheEntry struct {
data []byte
createdAt time.Time
}
type UserHandler struct {
usecase domain.UserUsecase
euse domain.ExtensionUsecase
duse domain.DashboardUsecase
buse domain.BillingUsecase
scuse domain.SecurityScanningUsecase
session *session.Session
logger *slog.Logger
cfg *config.Config
vsixCache map[string]*CacheEntry
cacheMu sync.RWMutex
limiter *rate.Limiter
}
func NewUserHandler(
w *web.Web,
usecase domain.UserUsecase,
euse domain.ExtensionUsecase,
scuse domain.SecurityScanningUsecase,
duse domain.DashboardUsecase,
buse domain.BillingUsecase,
auth *middleware.AuthMiddleware,
active *middleware.ActiveMiddleware,
readonly *middleware.ReadOnlyMiddleware,
session *session.Session,
logger *slog.Logger,
cfg *config.Config,
) *UserHandler {
u := &UserHandler{
usecase: usecase,
euse: euse,
duse: duse,
buse: buse,
scuse: scuse,
session: session,
logger: logger,
cfg: cfg,
vsixCache: make(map[string]*CacheEntry),
limiter: rate.NewLimiter(rate.Every(time.Duration(cfg.Extension.LimitSecond)*time.Second), cfg.Extension.Limit),
}
w.GET("/api/v1/static/vsix/:version", web.BaseHandler(u.VSIXDownload))
w.GET("/api/v1/static/vsix", web.BaseHandler(u.VSIXDownload))
w.POST("/api/v1/vscode/init-auth", web.BindHandler(u.VSCodeAuthInit))
// admin
admin := w.Group("/api/v1/admin")
admin.POST("/login", web.BindHandler(u.AdminLogin))
admin.GET("/setting", web.BaseHandler(u.GetSetting))
admin.GET("/role", web.BaseHandler(u.ListRole))
admin.Use(auth.Auth(), active.Active("admin"), readonly.Guard())
admin.GET("/profile", web.BaseHandler(u.AdminProfile))
admin.GET("/list", web.BaseHandler(u.AdminList, web.WithPage()))
admin.GET("/login-history", web.BaseHandler(u.AdminLoginHistory, web.WithPage()))
admin.PUT("/setting", web.BindHandler(u.UpdateSetting))
admin.POST("/create", web.BindHandler(u.CreateAdmin))
admin.POST("/logout", web.BaseHandler(u.AdminLogout))
admin.DELETE("/delete", web.BaseHandler(u.DeleteAdmin))
admin.GET("/export-completion-data", web.BaseHandler(u.ExportCompletionData))
admin.POST("/role", web.BindHandler(u.GrantRole))
// user
g := w.Group("/api/v1/user")
g.GET("/oauth/signup-or-in", web.BindHandler(u.OAuthSignUpOrIn))
g.GET("/oauth/callback", web.BindHandler(u.OAuthCallback))
g.POST("/register", web.BindHandler(u.Register))
g.POST("/login", web.BindHandler(u.Login))
g.Use(readonly.Guard())
g.GET("/profile", web.BaseHandler(u.Profile), auth.UserAuth())
g.PUT("/profile", web.BindHandler(u.UpdateProfile), auth.UserAuth())
g.POST("/logout", web.BaseHandler(u.Logout), auth.UserAuth())
g.Use(auth.Auth(), active.Active("admin"))
g.PUT("/update", web.BindHandler(u.Update))
g.DELETE("/delete", web.BaseHandler(u.Delete))
g.GET("/invite", web.BaseHandler(u.Invite))
g.GET("/list", web.BindHandler(u.List, web.WithPage()))
g.GET("/login-history", web.BaseHandler(u.LoginHistory, web.WithPage()))
// user dashboard
d := w.Group("/api/v1/user/dashboard")
d.Use(auth.UserAuth(), active.Active("user"))
d.GET("/stat", web.BindHandler(u.UserStat))
d.GET("/events", web.BaseHandler(u.UserEvents))
d.GET("/heatmap", web.BaseHandler(u.UserHeatmap))
// user record
uc := w.Group("/api/v1/user/chat")
uc.Use(auth.UserAuth(), active.Active("user"))
uc.GET("/record", web.BindHandler(u.ListChatRecord, web.WithPage()))
uc.GET("/info", web.BaseHandler(u.ChatInfo))
cplt := w.Group("/api/v1/user/completion")
cplt.Use(auth.UserAuth(), active.Active("user"))
cplt.GET("/record", web.BindHandler(u.ListCompletionRecord, web.WithPage()))
cplt.GET("/info", web.BaseHandler(u.CompletionInfo))
// user security
sc := w.Group("/api/v1/user/security")
sc.Use(auth.UserAuth(), active.Active("user"))
sc.GET("/scanning", web.BindHandler(u.SecurityList, web.WithPage()))
sc.GET("/scanning/detail", web.BaseHandler(u.SecurityDetail))
return u
}
func (h *UserHandler) VSCodeAuthInit(c *web.Context, req domain.VSCodeAuthInitReq) error {
s, err := h.usecase.GetSetting(c.Request().Context())
if err != nil {
return err
}
req.BaseURL = h.cfg.GetBaseURL(c.Request(), s)
resp, err := h.usecase.VSCodeAuthInit(c.Request().Context(), &req)
if err != nil {
return err
}
return c.JSON(http.StatusOK, resp)
}
// generateCacheKey 生成缓存键
func (h *UserHandler) generateCacheKey(version, baseUrl string) string {
hash := md5.Sum([]byte(version + ":" + baseUrl))
return fmt.Sprintf("%x", hash)
}
// cleanExpiredCache 清理过期缓存
func (h *UserHandler) cleanExpiredCache() {
h.cacheMu.Lock()
defer h.cacheMu.Unlock()
now := time.Now()
for key, entry := range h.vsixCache {
// 缓存1小时后过期
if now.Sub(entry.createdAt) > time.Hour {
delete(h.vsixCache, key)
}
}
}
// VSIXDownload 下载VSCode插件
//
// @Tags User
// @Summary 下载VSCode插件
// @Description 下载VSCode插件
// @ID vsix-download
// @Accept json
// @Produce octet-stream
// @Router /api/v1/static/vsix [get]
func (h *UserHandler) VSIXDownload(c *web.Context) error {
if !h.limiter.Allow() {
return c.String(http.StatusTooManyRequests, "Too Many Requests")
}
s, err := h.usecase.GetSetting(c.Request().Context())
if err != nil {
return err
}
host := c.Request().Host
h.logger.With("url", c.Request().URL).With("header", c.Request().Header).With("host", host).DebugContext(c.Request().Context(), "vsix download")
cacheKey := h.generateCacheKey(version.Version, h.cfg.GetBaseURL(c.Request(), s))
version := strings.Trim(version.Version, "v")
h.cacheMu.RLock()
if entry, exists := h.vsixCache[cacheKey]; exists {
if time.Since(entry.createdAt) < time.Hour {
h.cacheMu.RUnlock()
disposition := fmt.Sprintf("attachment; filename=monkeycode-%s.vsix", version)
c.Response().Header().Set("Content-Type", "application/octet-stream")
c.Response().Header().Set("Content-Disposition", disposition)
c.Response().Header().Set("Content-Length", fmt.Sprintf("%d", len(entry.data)))
_, err := c.Response().Writer.Write(entry.data)
return err
}
}
h.cacheMu.RUnlock()
var buf bytes.Buffer
if err := vsix.ChangeVsixEndpoint(fmt.Sprintf("/app/assets/vsix/monkeycode-%s.vsix", version), "extension/package.json", h.cfg.GetBaseURL(c.Request(), s), &buf); err != nil {
return err
}
data := buf.Bytes()
h.cacheMu.Lock()
h.vsixCache[cacheKey] = &CacheEntry{
data: data,
createdAt: time.Now(),
}
h.cacheMu.Unlock()
go h.cleanExpiredCache()
disposition := fmt.Sprintf("attachment; filename=monkeycode-%s.vsix", version)
c.Response().Header().Set("Content-Type", "application/octet-stream")
c.Response().Header().Set("Content-Disposition", disposition)
c.Response().Header().Set("Content-Length", fmt.Sprintf("%d", len(data)))
_, err = c.Response().Writer.Write(data)
return err
}
// Login 用户登录
//
// @Tags User
// @Summary 用户登录
// @Description 用户登录
// @ID login
// @Accept json
// @Produce json
// @Param param body domain.LoginReq true "登录参数"
// @Success 200 {object} web.Resp{data=domain.LoginResp}
// @Router /api/v1/user/login [post]
func (h *UserHandler) Login(c *web.Context, req domain.LoginReq) error {
req.IP = c.RealIP()
resp, err := h.usecase.Login(c.Request().Context(), &req)
if err != nil {
return err
}
if req.Source == consts.LoginSourceBrowser {
if _, err := h.session.Save(c, consts.UserSessionName, c.Request().Host, resp.User); err != nil {
return err
}
}
return c.Success(resp)
}
// Logout 用户登出
//
// @Tags User
// @Summary 用户登出
// @Description 用户登出
// @ID logout
// @Accept json
// @Produce json
// @Success 200 {object} web.Resp{}
// @Router /api/v1/user/logout [post]
func (h *UserHandler) Logout(c *web.Context) error {
if err := h.session.Del(c, consts.UserSessionName); err != nil {
return err
}
return c.Success(nil)
}
// Update 更新用户
//
// @Tags User
// @Summary 更新用户
// @Description 更新用户
// @ID update-user
// @Accept json
// @Produce json
// @Param param body domain.UpdateUserReq true "更新用户参数"
// @Success 200 {object} web.Resp{data=domain.User}
// @Router /api/v1/user/update [put]
func (h *UserHandler) Update(c *web.Context, req domain.UpdateUserReq) error {
resp, err := h.usecase.Update(c.Request().Context(), &req)
if err != nil {
return err
}
return c.Success(resp)
}
// Delete 删除用户
//
// @Tags User
// @Summary 删除用户
// @Description 删除用户
// @ID delete-user
// @Accept json
// @Produce json
// @Param id query string true "用户ID"
// @Success 200 {object} web.Resp{data=nil}
// @Router /api/v1/user/delete [delete]
func (h *UserHandler) Delete(c *web.Context) error {
err := h.usecase.Delete(c.Request().Context(), c.QueryParam("id"))
if err != nil {
return err
}
return c.Success(nil)
}
// DeleteAdmin 删除管理员
//
// @Tags Admin
// @Summary 删除管理员
// @Description 删除管理员
// @ID delete-admin
// @Accept json
// @Produce json
// @Param id query string true "管理员ID"
// @Success 200 {object} web.Resp{data=nil}
// @Router /api/v1/admin/delete [delete]
func (h *UserHandler) DeleteAdmin(c *web.Context) error {
err := h.usecase.DeleteAdmin(c.Request().Context(), c.QueryParam("id"))
if err != nil {
return err
}
return c.Success(nil)
}
// AdminLogin 管理员登录
//
// @Tags Admin
// @Summary 管理员登录
// @Description 管理员登录
// @ID admin-login
// @Accept json
// @Produce json
// @Param param body domain.LoginReq true "登录参数"
// @Success 200 {object} web.Resp{data=domain.AdminUser}
// @Router /api/v1/admin/login [post]
func (h *UserHandler) AdminLogin(c *web.Context, req domain.LoginReq) error {
req.IP = c.RealIP()
resp, err := h.usecase.AdminLogin(c.Request().Context(), &req)
if err != nil {
return err
}
h.logger.With("header", c.Request().Header).With("host", c.Request().Host).Info("admin login", "username", resp.Username)
if _, err := h.session.Save(c, consts.SessionName, c.Request().Host, resp); err != nil {
return err
}
return c.Success(resp)
}
// AdminLogout 管理员登出
//
// @Tags Admin
// @Summary 管理员登出
// @Description 管理员登出
// @ID admin-logout
// @Accept json
// @Produce json
// @Success 200 {object} web.Resp{}
// @Router /api/v1/admin/logout [post]
func (h *UserHandler) AdminLogout(c *web.Context) error {
if err := h.session.Del(c, consts.SessionName); err != nil {
return err
}
return c.Success(nil)
}
// AdminProfile 管理员信息
//
// @Tags Admin
// @Summary 管理员信息
// @Description 管理员信息
// @ID admin-profile
// @Accept json
// @Produce json
// @Success 200 {object} web.Resp{data=domain.AdminUser}
// @Router /api/v1/admin/profile [get]
func (h *UserHandler) AdminProfile(c *web.Context) error {
user := middleware.GetAdmin(c)
return c.Success(user)
}
// List 获取用户列表
//
// @Tags User
// @Summary 获取用户列表
// @Description 获取用户列表
// @ID list-user
// @Accept json
// @Produce json
// @Param page query web.Pagination true "分页"
// @Success 200 {object} web.Resp{data=domain.ListUserResp}
// @Router /api/v1/user/list [get]
func (h *UserHandler) List(c *web.Context, req domain.ListReq) error {
resp, err := h.usecase.List(c.Request().Context(), req)
if err != nil {
return err
}
return c.Success(resp)
}
// LoginHistory 获取用户登录历史
//
// @Tags User
// @Summary 获取用户登录历史
// @Description 获取用户登录历史
// @ID login-history
// @Accept json
// @Produce json
// @Param page query web.Pagination true "分页"
// @Success 200 {object} web.Resp{data=domain.ListLoginHistoryResp}
// @Router /api/v1/user/login-history [get]
func (h *UserHandler) LoginHistory(c *web.Context) error {
resp, err := h.usecase.LoginHistory(c.Request().Context(), c.Page())
if err != nil {
return err
}
return c.Success(resp)
}
// Invite 获取用户邀请码
//
// @Tags User
// @Summary 获取用户邀请码
// @Description 获取用户邀请码
// @ID invite
// @Accept json
// @Produce json
// @Success 200 {object} web.Resp{data=domain.InviteResp}
// @Router /api/v1/user/invite [get]
func (h *UserHandler) Invite(c *web.Context) error {
admin := middleware.GetAdmin(c)
resp, err := h.usecase.Invite(c.Request().Context(), admin.ID.String())
if err != nil {
return err
}
return c.Success(resp)
}
// Register 注册用户
//
// @Tags User
// @Summary 注册用户
// @Description 注册用户
// @ID register
// @Accept json
// @Produce json
// @Param param body domain.RegisterReq true "注册参数"
// @Success 200 {object} web.Resp{data=domain.User}
// @Router /api/v1/user/register [post]
func (h *UserHandler) Register(c *web.Context, req domain.RegisterReq) error {
resp, err := h.usecase.Register(c.Request().Context(), &req)
if err != nil {
return err
}
return c.Success(resp)
}
// CreateAdmin 创建管理员
//
// @Tags Admin
// @Summary 创建管理员
// @Description 创建管理员
// @ID create-admin
// @Accept json
// @Produce json
// @Param param body domain.CreateAdminReq true "创建管理员参数"
// @Success 200 {object} web.Resp{data=domain.AdminUser}
// @Router /api/v1/admin/create [post]
func (h *UserHandler) CreateAdmin(c *web.Context, req domain.CreateAdminReq) error {
user := middleware.GetAdmin(c)
if user.Username != "admin" {
return errcode.ErrPermission
}
resp, err := h.usecase.CreateAdmin(c.Request().Context(), &req)
if err != nil {
return err
}
return c.Success(resp)
}
// AdminList 获取管理员用户列表
//
// @Tags Admin
// @Summary 获取管理员用户列表
// @Description 获取管理员用户列表
// @ID list-admin-user
// @Accept json
// @Produce json
// @Param page query web.Pagination true "分页"
// @Success 200 {object} web.Resp{data=domain.ListAdminUserResp}
// @Router /api/v1/admin/list [get]
func (h *UserHandler) AdminList(c *web.Context) error {
resp, err := h.usecase.AdminList(c.Request().Context(), c.Page())
if err != nil {
return err
}
return c.Success(resp)
}
// AdminLoginHistory 获取管理员登录历史
//
// @Tags Admin
// @Summary 获取管理员登录历史
// @Description 获取管理员登录历史
// @ID admin-login-history
// @Accept json
// @Produce json
// @Param page query web.Pagination true "分页"
// @Success 200 {object} web.Resp{data=domain.ListAdminLoginHistoryResp}
// @Router /api/v1/admin/login-history [get]
func (h *UserHandler) AdminLoginHistory(c *web.Context) error {
resp, err := h.usecase.AdminLoginHistory(c.Request().Context(), c.Page())
if err != nil {
return err
}
return c.Success(resp)
}
// ListRole 获取系统角色列表
//
// @Tags Admin
// @Summary 获取角色列表
// @Description 获取角色列表
// @ID list-role
// @Accept json
// @Produce json
// @Success 200 {object} web.Resp{data=[]domain.Role}
// @Router /api/v1/admin/role [get]
func (h *UserHandler) ListRole(c *web.Context) error {
roles, err := h.usecase.ListRole(c.Request().Context())
if err != nil {
return err
}
return c.Success(roles)
}
// GrantRole 授权角色
//
// @Tags Admin
// @Summary 授权角色
// @Description 授权角色
// @ID grant-role
// @Accept json
// @Produce json
// @Param param body domain.GrantRoleReq true "授权角色参数"
// @Success 200 {object} web.Resp
// @Router /api/v1/admin/role [post]
func (h *UserHandler) GrantRole(c *web.Context, req domain.GrantRoleReq) error {
if err := h.usecase.GrantRole(c.Request().Context(), &req); err != nil {
return err
}
return c.Success(nil)
}
// GetSetting 获取系统设置
//
// @Tags Admin
// @Summary 获取系统设置
// @Description 获取系统设置
// @ID get-setting
// @Accept json
// @Produce json
// @Success 200 {object} web.Resp{data=domain.Setting}
// @Router /api/v1/admin/setting [get]
func (h *UserHandler) GetSetting(c *web.Context) error {
resp, err := h.usecase.GetSetting(c.Request().Context())
if err != nil {
return err
}
return c.Success(resp)
}
// UpdateSetting 更新系统设置
//
// @Tags Admin
// @Summary 更新系统设置
// @Description 更新为增量更新,只传需要更新的字段
// @ID update-setting
// @Accept json
// @Produce json
// @Param param body domain.UpdateSettingReq true "更新系统设置参数"
// @Success 200 {object} web.Resp{data=domain.Setting}
// @Router /api/v1/admin/setting [put]
func (h *UserHandler) UpdateSetting(c *web.Context, req domain.UpdateSettingReq) error {
resp, err := h.usecase.UpdateSetting(c.Request().Context(), &req)
if err != nil {
return err
}
return c.Success(resp)
}
// OAuthSignUpOrIn 用户 OAuth 登录或注册
//
// @Tags User
// @Summary 用户 OAuth 登录或注册
// @Description 用户 OAuth 登录或注册
// @ID user-oauth-signup-or-in
// @Accept json
// @Produce json
// @Param req query domain.OAuthSignUpOrInReq true "param"
// @Success 200 {object} web.Resp{data=domain.OAuthURLResp}
// @Router /api/v1/user/oauth/signup-or-in [get]
func (h *UserHandler) OAuthSignUpOrIn(ctx *web.Context, req domain.OAuthSignUpOrInReq) error {
h.logger.With("req", req).DebugContext(ctx.Request().Context(), "OAuthSignUpOrIn")
s, err := h.usecase.GetSetting(ctx.Request().Context())
if err != nil {
return err
}
req.BaseURL = h.cfg.GetBaseURL(ctx.Request(), s)
resp, err := h.usecase.OAuthSignUpOrIn(ctx.Request().Context(), &req)
if err != nil {
return err
}
return ctx.Success(resp)
}
// OAuthCallback 用户 OAuth 回调
//
// @Tags User
// @Summary 用户 OAuth 回调
// @Description 用户 OAuth 回调
// @ID user-oauth-callback
// @Accept json
// @Produce json
// @Param req query domain.OAuthCallbackReq true "param"
// @Success 200 {object} web.Resp{data=string}
// @Router /api/v1/user/oauth/callback [get]
func (h *UserHandler) OAuthCallback(ctx *web.Context, req domain.OAuthCallbackReq) error {
return h.usecase.OAuthCallback(ctx, &req)
}
// Profile 获取用户信息
//
// @Tags User Manage
// @Summary 获取用户信息
// @Description 获取用户信息
// @ID user-profile
// @Accept json
// @Produce json
// @Success 200 {object} web.Resp{data=domain.User}
// @Failure 401 {object} web.Resp{}
// @Router /api/v1/user/profile [get]
func (h *UserHandler) Profile(ctx *web.Context) error {
return ctx.Success(middleware.GetUser(ctx))
}
// UpdateProfile 更新用户信息
//
// @Tags User Manage
// @Summary 更新用户信息
// @Description 更新用户信息
// @ID user-update-profile
// @Accept json
// @Produce json
// @Param req body domain.ProfileUpdateReq true "param"
// @Success 200 {object} web.Resp{data=domain.User}
// @Failure 401 {object} web.Resp{}
// @Router /api/v1/user/profile [put]
func (h *UserHandler) UpdateProfile(ctx *web.Context, req domain.ProfileUpdateReq) error {
req.UID = middleware.GetUser(ctx).ID
user, err := h.usecase.ProfileUpdate(ctx.Request().Context(), &req)
if err != nil {
return err
}
return ctx.Success(user)
}
func (h *UserHandler) InitAdmin() error {
return h.usecase.InitAdmin(context.Background())
}
// ExportCompletionData godoc
//
// @Summary 导出补全数据
// @Description 管理员导出所有补全相关数据
// @Tags admin
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {object} domain.ExportCompletionDataResp
// @Failure 401 {object} web.Resp{}
// @Failure 500 {object} web.Resp{}
// @Router /api/v1/admin/export-completion-data [get]
func (h *UserHandler) ExportCompletionData(c *web.Context) error {
data, err := h.usecase.ExportCompletionData(c.Request().Context())
if err != nil {
return err
}
return c.JSON(http.StatusOK, data)
}