Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/biz/file/handler/v1/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type FileHandler struct {
func NewFileHandler(i *do.Injector) (*FileHandler, error) {
w := do.MustInvoke[*web.Web](i)
auth := do.MustInvoke[*middleware.AuthMiddleware](i)
targetActive := do.MustInvoke[*middleware.TargetActiveMiddleware](i)

f := &FileHandler{
logger: do.MustInvoke[*slog.Logger](i).With("module", "handler.file"),
Expand All @@ -38,7 +39,7 @@ func NewFileHandler(i *do.Injector) (*FileHandler, error) {
}

g := w.Group("/api/v1/users")
g.Use(auth.Auth())
g.Use(auth.Auth(), targetActive.TargetActive())

g.GET("/folders", web.BindHandler(f.ListFolder))
g.POST("/folders", web.BindHandler(f.Mkdir))
Expand Down
3 changes: 2 additions & 1 deletion backend/biz/git/handler/v1/gitbot.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ type GitBotHandler struct {
func NewGitBotHandler(i *do.Injector) (*GitBotHandler, error) {
w := do.MustInvoke[*web.Web](i)
auth := do.MustInvoke[*middleware.AuthMiddleware](i)
targetActive := do.MustInvoke[*middleware.TargetActiveMiddleware](i)

h := &GitBotHandler{
usecase: do.MustInvoke[domain.GitBotUsecase](i),
logger: do.MustInvoke[*slog.Logger](i).With("module", "handler.GitBotHandler"),
}

g := w.Group("/api/v1/users/git-bots")
g.Use(auth.Auth())
g.Use(auth.Auth(), targetActive.TargetActive())
g.GET("", web.BaseHandler(h.List))
g.POST("", web.BindHandler(h.Create))
g.PUT("", web.BindHandler(h.Update))
Expand Down
3 changes: 2 additions & 1 deletion backend/biz/git/handler/v1/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ type GitIdentityHandler struct {
func NewGitIdentityHandler(i *do.Injector) (*GitIdentityHandler, error) {
w := do.MustInvoke[*web.Web](i)
auth := do.MustInvoke[*middleware.AuthMiddleware](i)
targetActive := do.MustInvoke[*middleware.TargetActiveMiddleware](i)

h := &GitIdentityHandler{
usecase: do.MustInvoke[domain.GitIdentityUsecase](i),
logger: do.MustInvoke[*slog.Logger](i).With("module", "handler.git_identity"),
}

g := w.Group("/api/v1/users/git-identities")
g.Use(auth.Auth())
g.Use(auth.Auth(), targetActive.TargetActive())
g.GET("", web.BaseHandler(h.List))
g.GET("/:id", web.BindHandler(h.Get))
g.POST("", web.BindHandler(h.Add))
Expand Down
3 changes: 2 additions & 1 deletion backend/biz/host/handler/v1/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type HostHandler struct {
func NewHostHandler(i *do.Injector) (*HostHandler, error) {
w := do.MustInvoke[*web.Web](i)
auth := do.MustInvoke[*middleware.AuthMiddleware](i)
targetActive := do.MustInvoke[*middleware.TargetActiveMiddleware](i)

h := &HostHandler{
usecase: do.MustInvoke[domain.HostUsecase](i),
Expand All @@ -47,7 +48,7 @@ func NewHostHandler(i *do.Injector) (*HostHandler, error) {
g.GET("/install", web.BindHandler(h.Install))
g.GET("/vms/terminals/join", web.BindHandler(h.JoinTerminal))

g.Use(auth.Auth())
g.Use(auth.Auth(), targetActive.TargetActive())
g.GET("/install-command", web.BaseHandler(h.GetInstallCommand))
g.DELETE("/:id", web.BindHandler(h.DeleteHost))
g.PUT("/:id", web.BindHandler(h.UpdateHost))
Expand Down
3 changes: 2 additions & 1 deletion backend/biz/notify/handler/v1/notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type NotifyHandler struct {
func NewNotifyHandler(i *do.Injector) (*NotifyHandler, error) {
w := do.MustInvoke[*web.Web](i)
auth := do.MustInvoke[*middleware.AuthMiddleware](i)
targetActive := do.MustInvoke[*middleware.TargetActiveMiddleware](i)

h := &NotifyHandler{
channelUsecase: do.MustInvoke[domain.NotifyChannelUsecase](i),
Expand All @@ -30,7 +31,7 @@ func NewNotifyHandler(i *do.Injector) (*NotifyHandler, error) {

// 用户接口
usr := w.Group("/api/v1/users/notify")
usr.Use(auth.Auth())
usr.Use(auth.Auth(), targetActive.TargetActive())
usr.POST("/channels", web.BindHandler(h.CreateUserChannel))
usr.GET("/channels", web.BaseHandler(h.ListUserChannels))
usr.PUT("/channels/:id", web.BindHandler(h.UpdateUserChannel))
Expand Down
3 changes: 2 additions & 1 deletion backend/biz/project/handler/v1/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ type ProjectHandler struct {
func NewProjectHandler(i *do.Injector) (*ProjectHandler, error) {
w := do.MustInvoke[*web.Web](i)
auth := do.MustInvoke[*middleware.AuthMiddleware](i)
targetActive := do.MustInvoke[*middleware.TargetActiveMiddleware](i)

h := &ProjectHandler{
usecase: do.MustInvoke[domain.ProjectUsecase](i),
logger: do.MustInvoke[*slog.Logger](i).With("module", "handler.project"),
}

g := w.Group("/api/v1/users/projects")
g.Use(auth.Auth())
g.Use(auth.Auth(), targetActive.TargetActive())
g.GET("", web.BindHandler(h.List))
g.GET("/:id", web.BindHandler(h.Get))
g.POST("", web.BindHandler(h.Create))
Expand Down
3 changes: 2 additions & 1 deletion backend/biz/setting/handler/v1/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func NewImageHandler(i *do.Injector) (*ImageHandler, error) {
logger := do.MustInvoke[*slog.Logger](i)
usecase := do.MustInvoke[domain.ImageUsecase](i)
auth := do.MustInvoke[*middleware.AuthMiddleware](i)
targetActive := do.MustInvoke[*middleware.TargetActiveMiddleware](i)

h := &ImageHandler{
logger: logger.With("component", "handler.images"),
Expand All @@ -31,7 +32,7 @@ func NewImageHandler(i *do.Injector) (*ImageHandler, error) {

v1 := w.Group("/api/v1/users/images")

v1.Use(auth.Auth())
v1.Use(auth.Auth(), targetActive.TargetActive())
v1.GET("", web.BindHandler(h.List))
v1.POST("", web.BindHandler(h.Create))
v1.DELETE("/:id", web.BindHandler(h.Delete))
Expand Down
3 changes: 2 additions & 1 deletion backend/biz/setting/handler/v1/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func NewModelHandler(i *do.Injector) (*ModelHandler, error) {
logger := do.MustInvoke[*slog.Logger](i)
usecase := do.MustInvoke[domain.ModelUsecase](i)
auth := do.MustInvoke[*middleware.AuthMiddleware](i)
targetActive := do.MustInvoke[*middleware.TargetActiveMiddleware](i)

h := &ModelHandler{
logger: logger.With("component", "handler.models"),
Expand All @@ -34,7 +35,7 @@ func NewModelHandler(i *do.Injector) (*ModelHandler, error) {

v1.GET("/providers", web.BindHandler(h.GetProviderModelList))

v1.Use(auth.Auth())
v1.Use(auth.Auth(), targetActive.TargetActive())
v1.GET("", web.BindHandler(h.List))
v1.POST("", web.BindHandler(h.Create))
v1.PUT("/:id", web.BindHandler(h.Update))
Expand Down
3 changes: 2 additions & 1 deletion backend/biz/task/handler/v1/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func NewTaskHandler(i *do.Injector) (*TaskHandler, error) {
tf := do.MustInvoke[taskflow.Clienter](i)
lok := do.MustInvoke[*loki.Client](i)
auth := do.MustInvoke[*middleware.AuthMiddleware](i)
targetActive := do.MustInvoke[*middleware.TargetActiveMiddleware](i)
tc := do.MustInvoke[*ws.TaskConn](i)
cc := do.MustInvoke[*ws.ControlConn](i)
ts := do.MustInvoke[*service.TaskSummaryService](i)
Expand Down Expand Up @@ -92,7 +93,7 @@ func NewTaskHandler(i *do.Injector) (*TaskHandler, error) {

v1.GET("/public-stream", web.BindHandler(h.PublicStream), auth.Check())

v1.Use(auth.Auth())
v1.Use(auth.Auth(), targetActive.TargetActive())

// 任务管理接口
v1.GET("", web.BindHandler(h.List, web.WithPage()))
Expand Down
15 changes: 14 additions & 1 deletion backend/biz/team/usecase/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/samber/do"

"github.com/chaitin/MonkeyCode/backend/config"
"github.com/chaitin/MonkeyCode/backend/consts"
"github.com/chaitin/MonkeyCode/backend/db"
"github.com/chaitin/MonkeyCode/backend/domain"
"github.com/chaitin/MonkeyCode/backend/errcode"
Expand All @@ -21,6 +22,7 @@ import (
// TeamGroupUserUsecase 团队分组成员业务逻辑层
type TeamGroupUserUsecase struct {
repo domain.TeamGroupUserRepo
activeRepo domain.UserActiveRepo
logger *slog.Logger
config *config.Config
smtpClient domain.EmailSender
Expand All @@ -33,6 +35,7 @@ func NewTeamGroupUserUsecase(i *do.Injector) (domain.TeamGroupUserUsecase, error

t := &TeamGroupUserUsecase{
repo: do.MustInvoke[domain.TeamGroupUserRepo](i),
activeRepo: do.MustInvoke[domain.UserActiveRepo](i),
logger: do.MustInvoke[*slog.Logger](i).With("module", "usecase.team_group_user"),
config: cfg,
smtpClient: do.MustInvoke[domain.EmailSender](i),
Expand Down Expand Up @@ -207,11 +210,21 @@ func (u *TeamGroupUserUsecase) MemberList(ctx context.Context, teamUser *domain.
return &domain.MemberListResp{
MemberLimit: team.MemberLimit,
Members: cvt.Iter(members, func(_ int, member *db.TeamMember) *domain.TeamMemberInfo {
var lastActiveAtTs int64
if member.Edges.User != nil && u.activeRepo != nil {
lastActiveAt, err := u.activeRepo.GetActiveRecord(ctx, consts.UserActiveKey, member.Edges.User.ID.String())
if err != nil {
u.logger.ErrorContext(ctx, "get last active time failed", "error", err)
}
if !lastActiveAt.IsZero() {
lastActiveAtTs = lastActiveAt.Unix()
}
}
return &domain.TeamMemberInfo{
User: cvt.From(member.Edges.User, &domain.User{}),
Role: member.Role,
CreatedAt: member.CreatedAt.Unix(),
LastActiveAt: 0,
LastActiveAt: lastActiveAtTs,
}
}),
}, nil
Expand Down
5 changes: 3 additions & 2 deletions backend/biz/user/handler/v1/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func NewAuthHandler(i *do.Injector) (*AuthHandler, error) {
usecase := do.MustInvoke[domain.UserUsecase](i)
redisClient := do.MustInvoke[*redis.Client](i)
auth := do.MustInvoke[*middleware.AuthMiddleware](i)
targetActive := do.MustInvoke[*middleware.TargetActiveMiddleware](i)
captchaSvc := do.MustInvoke[*captcha.Captcha](i)

h := &AuthHandler{
Expand All @@ -58,10 +59,10 @@ func NewAuthHandler(i *do.Injector) (*AuthHandler, error) {
v1.POST("/password-login", web.BindHandler(h.PasswordLogin))
v1.PUT("/passwords/change", web.BindHandler(h.ChangePassword), auth.Check())
v1.GET("/status", web.BaseHandler(h.Status), auth.Check())
v1.POST("/logout", web.BaseHandler(h.Logout), auth.Auth())
v1.POST("/logout", web.BaseHandler(h.Logout), auth.Auth(), targetActive.TargetActive())

// 邮箱绑定接口
v1.PUT("/email/bind-request", web.BindHandler(h.SendBindEmailVerification), auth.Auth())
v1.PUT("/email/bind-request", web.BindHandler(h.SendBindEmailVerification), auth.Auth(), targetActive.TargetActive())
v1.GET("/email/verify", web.BindHandler(h.VerifyBindEmail))

return h, nil
Expand Down
1 change: 1 addition & 0 deletions backend/biz/user/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
// ProvideUser 注册 user 模块的服务工厂
func ProvideUser(i *do.Injector) {
do.Provide(i, repo.NewUserRepo)
do.Provide(i, repo.NewUserActiveRepo)
do.Provide(i, usecase.NewUserUsecase)
do.Provide(i, v1.NewAuthHandler)
}
Expand Down
52 changes: 52 additions & 0 deletions backend/biz/user/repo/active.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package repo

import (
"context"
"fmt"
"log/slog"
"time"

"github.com/redis/go-redis/v9"
"github.com/samber/do"

"github.com/chaitin/MonkeyCode/backend/consts"
"github.com/chaitin/MonkeyCode/backend/domain"
)

// UserActiveRepo 用户活跃记录数据访问层
type UserActiveRepo struct {
redis *redis.Client
logger *slog.Logger
}

// NewUserActiveRepo 创建用户活跃记录数据访问层实例
func NewUserActiveRepo(i *do.Injector) (domain.UserActiveRepo, error) {
return &UserActiveRepo{
redis: do.MustInvoke[*redis.Client](i),
logger: do.MustInvoke[*slog.Logger](i).With("module", "repo.user_active"),
}, nil
}

// RecordActiveRecord 记录用户最后活跃时间
func (r *UserActiveRepo) RecordActiveRecord(ctx context.Context, key consts.RedisKey, field string, score time.Time) error {
if err := r.redis.ZAdd(ctx, string(key), redis.Z{
Score: float64(score.Unix()),
Member: field,
}).Err(); err != nil {
r.logger.ErrorContext(ctx, "failed to record user active time", "error", err, "field", field)
return fmt.Errorf("failed to record user active time: %w", err)
}
return nil
}

// GetActiveRecord 获取用户最后活跃时间
func (r *UserActiveRepo) GetActiveRecord(ctx context.Context, key consts.RedisKey, userID string) (time.Time, error) {
score, err := r.redis.ZScore(ctx, string(key), userID).Result()
if err != nil {
if err == redis.Nil {
return time.Time{}, nil
}
return time.Time{}, err
}
return time.Unix(int64(score), 0), nil
}
6 changes: 6 additions & 0 deletions backend/consts/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ const (
UserRoleAdmin UserRole = "admin" // MonkeyCode-AI 管理员账号,用来配置公共资源. 如公共宿主机等
)

type RedisKey string

const (
UserActiveKey RedisKey = "monkeycode_ai:user:active"
)

type DefaultConfigType string

const (
Expand Down
7 changes: 7 additions & 0 deletions backend/domain/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package domain

import (
"context"
"time"

"github.com/google/uuid"

Expand Down Expand Up @@ -33,6 +34,12 @@ type UserRepo interface {
SetEmail(ctx context.Context, userID uuid.UUID, email string) error
}

// UserActiveRepo 用户活跃记录仓储接口
type UserActiveRepo interface {
RecordActiveRecord(ctx context.Context, key consts.RedisKey, field string, score time.Time) error
GetActiveRecord(ctx context.Context, key consts.RedisKey, userID string) (time.Time, error)
}

type User struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Expand Down
47 changes: 47 additions & 0 deletions backend/middleware/target_active.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package middleware

import (
"log/slog"
"time"

"github.com/labstack/echo/v4"

"github.com/chaitin/MonkeyCode/backend/consts"
"github.com/chaitin/MonkeyCode/backend/domain"
)

// TargetActiveMiddleware 用户活动时间追踪中间件
type TargetActiveMiddleware struct {
logger *slog.Logger
activeRepo domain.UserActiveRepo
}

// NewTargetActiveMiddleware 创建用户活动时间追踪中间件
func NewTargetActiveMiddleware(logger *slog.Logger, activeRepo domain.UserActiveRepo) *TargetActiveMiddleware {
return &TargetActiveMiddleware{
logger: logger.With("module", "middleware.target_active"),
activeRepo: activeRepo,
}
}

// TargetActive 记录用户活动时间
func (t *TargetActiveMiddleware) TargetActive() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ctx := c.Request().Context()
user := GetUser(c)

if user != nil && t.activeRepo != nil {
if err := t.activeRepo.RecordActiveRecord(
ctx,
consts.UserActiveKey,
user.ID.String(),
time.Now(),
); err != nil {
t.logger.WarnContext(ctx, "failed to record user active time", "error", err, "user_id", user.ID)
}
}
return next(c)
}
}
}
7 changes: 7 additions & 0 deletions backend/pkg/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ func RegisterInfra(i *do.Injector, w ...*web.Web) error {
return middleware.NewAuthMiddleware(sess, nil, l), nil
})

// TargetActive Middleware
do.Provide(i, func(i *do.Injector) (*middleware.TargetActiveMiddleware, error) {
l := do.MustInvoke[*slog.Logger](i)
activeRepo := do.MustInvoke[domain.UserActiveRepo](i)
return middleware.NewTargetActiveMiddleware(l, activeRepo), nil
})

// Audit Middleware
do.Provide(i, func(i *do.Injector) (*middleware.AuditMiddleware, error) {
l := do.MustInvoke[*slog.Logger](i)
Expand Down