diff --git a/cmd/agent/main.go b/cmd/agent/main.go index c30e5b1..910a41d 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -493,7 +493,7 @@ func registerAgent(cmd *cobra.Command, args []string) { }, AutoUpdate: config.AutoUpdateConfig{ Enabled: true, - CheckInterval: "10m", + CheckInterval: "1m", }, } diff --git a/config.example.yaml b/config.example.yaml index 62f66be..c3406bc 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -19,4 +19,29 @@ Server: App: JWT: Secret: "you_must_change_me" # 替换为任意 UUID 字符串 - ExpiresHours: 168 # 7天 \ No newline at end of file + ExpiresHours: 168 # 7天 + + # Basic Auth 用户配置(使用 bcrypt 加密) + # 生成密码命令: htpasswd -nBC 12 '' | tr -d ':\n' + # 或使用 Go: bcrypt.GenerateFromPassword([]byte("your_password"), bcrypt.DefaultCost) + Users: + admin: "$2y$12$7DXcOiX1D59xNTIn5riUKusAPLP88LxxoczWmUT83MBj5EFznbp8a" # 默认密码: admin123 + + # OIDC 认证配置(可选) + OIDC: + Enabled: false + Issuer: "https://your-oidc-provider.com" + ClientID: "your-client-id" + ClientSecret: "your-client-secret" + RedirectURL: "http://localhost:8080/login/oidc/callback" # 前端回调页面 + + # GitHub OAuth 认证配置(可选) + # 创建 GitHub OAuth App: https://github.com/settings/developers + GitHub: + Enabled: false + ClientID: "your-github-client-id" + ClientSecret: "your-github-client-secret" + RedirectURL: "http://localhost:8080/login/github/callback" # 前端回调页面 + AllowedUsers: # 允许登录的 GitHub 用户名白名单,不配置或留空则允许所有用户 + - "your-github-username" + - "another-username" \ No newline at end of file diff --git a/go.mod b/go.mod index a64e33a..3523469 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/dushixiang/pika go 1.25.0 require ( + github.com/coreos/go-oidc/v3 v3.16.0 github.com/docker/docker v28.5.2+incompatible github.com/go-errors/errors v1.5.1 github.com/go-orz/orz v0.2.9 @@ -21,6 +22,7 @@ require ( github.com/spf13/cobra v1.10.1 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.43.0 + golang.org/x/oauth2 v0.33.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/datatypes v1.2.7 gorm.io/gorm v1.31.1 @@ -42,6 +44,7 @@ require ( github.com/gabriel-vasile/mimetype v1.4.11 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/glebarez/sqlite v1.11.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect diff --git a/go.sum b/go.sum index e418fb6..89bb99d 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow= +github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -42,6 +44,8 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -221,6 +225,8 @@ golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/app.go b/internal/app.go index f49e1e0..363ec04 100644 --- a/internal/app.go +++ b/internal/app.go @@ -11,7 +11,6 @@ import ( "github.com/dushixiang/pika/internal/handler" "github.com/dushixiang/pika/internal/models" "github.com/dushixiang/pika/internal/scheduler" - "github.com/dushixiang/pika/internal/service" "github.com/dushixiang/pika/web" "github.com/google/uuid" @@ -59,14 +58,8 @@ func setup(app *orz.App) error { return err } - // 创建默认管理员用户 - ctx := context.Background() - if err := createDefaultUser(ctx, components, app.Logger()); err != nil { - app.Logger().Error("创建默认用户失败", zap.Error(err)) - // 不返回错误,因为可能用户已存在 - } - // 初始化默认属性配置 + ctx := context.Background() if err := initDefaultProperties(ctx, components, app.Logger()); err != nil { app.Logger().Error("初始化默认属性配置失败", zap.Error(err)) // 不返回错误,继续启动 @@ -132,6 +125,9 @@ func setupApi(app *orz.App, components *AppComponents) { { // 认证相关 publicApi.POST("/login", components.AccountHandler.Login) + publicApi.GET("/auth/config", components.AccountHandler.GetAuthConfig) + publicApi.GET("/auth/oidc/url", components.AccountHandler.GetOIDCAuthURL) + publicApi.GET("/auth/github/url", components.AccountHandler.GetGitHubAuthURL) // 探针信息(公开访问)- 用于公共展示页面 publicApi.GET("/agents", components.AgentHandler.GetAgents) @@ -141,7 +137,7 @@ func setupApi(app *orz.App, components *AppComponents) { // Agent 版本和下载 publicApi.GET("/agent/version", components.AgentHandler.GetAgentVersion) - publicApi.GET("/agent/downloads/:fi1lename", components.AgentHandler.DownloadAgent) + publicApi.GET("/agent/downloads/:filename", components.AgentHandler.DownloadAgent) // 监控统计数据(公开访问)- 用于公共展示页面 publicApi.GET("/monitors/stats", components.MonitorHandler.GetAllStats) @@ -205,23 +201,18 @@ func setupApi(app *orz.App, components *AppComponents) { adminApi.GET("/monitors/:id", components.MonitorHandler.Get) adminApi.PUT("/monitors/:id", components.MonitorHandler.Update) adminApi.DELETE("/monitors/:id", components.MonitorHandler.Delete) - - // 用户管理 - adminApi.GET("/users", components.UserHandler.Paging) - adminApi.POST("/users", components.UserHandler.Create) - adminApi.GET("/users/:id", components.UserHandler.Get) - adminApi.PUT("/users/:id", components.UserHandler.Update) - adminApi.DELETE("/users/:id", components.UserHandler.Delete) - adminApi.POST("/users/:id/password", components.UserHandler.ChangePassword) - adminApi.POST("/users/:id/reset-password", components.UserHandler.ResetPassword) - adminApi.POST("/users/:id/status", components.UserHandler.UpdateStatus) } + + // OIDC 认证路由(如果启用) + publicApi.POST("/auth/oidc/callback", components.AccountHandler.OIDCLogin) + + // GitHub 认证路由(如果启用) + publicApi.POST("/auth/github/callback", components.AccountHandler.GitHubLogin) } func autoMigrate(database *gorm.DB) error { // 自动迁移数据库表 return database.AutoMigrate( - &models.User{}, &models.Agent{}, &models.ApiKey{}, &models.CPUMetric{}, @@ -244,40 +235,6 @@ func autoMigrate(database *gorm.DB) error { ) } -// createDefaultUser 创建默认管理员用户 -func createDefaultUser(ctx context.Context, components *AppComponents, logger *zap.Logger) error { - // 检查是否已存在管理员用户 - _, total, err := components.UserService.ListUsers(ctx, 1, 1) - if err != nil { - return err - } - - // 如果已有用户,则不创建 - if total > 0 { - logger.Info("已存在用户,跳过创建默认用户") - return nil - } - - // 创建默认管理员用户 - defaultUser := &service.CreateUserRequest{ - Username: "admin", - Password: "admin123", - Nickname: "管理员", - } - - user, err := components.UserService.CreateUser(ctx, defaultUser) - if err != nil { - return err - } - - logger.Info("默认管理员用户创建成功", - zap.String("username", user.Username), - zap.String("password", "admin123"), - zap.String("提示", "请及时修改默认密码")) - - return nil -} - // initDefaultProperties 初始化默认属性配置 func initDefaultProperties(ctx context.Context, components *AppComponents, logger *zap.Logger) error { const propertyIDNotificationChannels = "notification_channels" diff --git a/internal/config/config.go b/internal/config/config.go index 8031301..36e1b9d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,8 +2,11 @@ package config // AppConfig 应用配置 type AppConfig struct { - JWT JWTConfig `mapstructure:"JWT"` - MonitorInterval int `mapstructure:"MonitorInterval"` + JWT JWTConfig `mapstructure:"JWT"` + MonitorInterval int `mapstructure:"MonitorInterval"` + Users map[string]string `mapstructure:"Users"` // 用户名 -> bcrypt加密的密码 + OIDC *OIDCConfig `mapstructure:"OIDC"` // OIDC配置(可选) + GitHub *GitHubOAuthConfig `mapstructure:"GitHub"` // GitHub OAuth配置(可选) } // JWTConfig JWT配置 @@ -11,3 +14,21 @@ type JWTConfig struct { Secret string `mapstructure:"Secret"` ExpiresHours int `mapstructure:"ExpiresHours"` } + +// OIDCConfig OIDC认证配置 +type OIDCConfig struct { + Enabled bool `mapstructure:"Enabled"` // 是否启用OIDC + Issuer string `mapstructure:"Issuer"` // OIDC Provider的Issuer URL + ClientID string `mapstructure:"ClientID"` // Client ID + ClientSecret string `mapstructure:"ClientSecret"` // Client Secret + RedirectURL string `mapstructure:"RedirectURL"` // 回调URL +} + +// GitHubOAuthConfig GitHub OAuth认证配置 +type GitHubOAuthConfig struct { + Enabled bool `mapstructure:"Enabled"` // 是否启用GitHub登录 + ClientID string `mapstructure:"ClientID"` // GitHub OAuth App Client ID + ClientSecret string `mapstructure:"ClientSecret"` // GitHub OAuth App Client Secret + RedirectURL string `mapstructure:"RedirectURL"` // 回调URL + AllowedUsers []string `mapstructure:"AllowedUsers"` // 允许登录的GitHub用户名白名单(为空则允许所有用户) +} diff --git a/internal/handler/account_handler.go b/internal/handler/account_handler.go index af3de38..3507c6e 100644 --- a/internal/handler/account_handler.go +++ b/internal/handler/account_handler.go @@ -24,7 +24,7 @@ type LoginRequest struct { Password string `json:"password" validate:"required"` } -// Login 用户登录 +// Login 用户登录(Basic Auth) func (r AccountHandler) Login(c echo.Context) error { var req LoginRequest if err := c.Bind(&req); err != nil { @@ -37,7 +37,81 @@ func (r AccountHandler) Login(c echo.Context) error { ctx := c.Request().Context() loginResp, err := r.accountService.Login(ctx, req.Username, req.Password) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "用户或密码错误") + return echo.NewHTTPError(http.StatusBadRequest, "用户名或密码错误") + } + + return orz.Ok(c, loginResp) +} + +// OIDCLoginRequest OIDC 登录请求 +type OIDCLoginRequest struct { + Code string `json:"code" validate:"required"` + State string `json:"state" validate:"required"` +} + +// OIDCLogin OIDC 登录回调 +func (r AccountHandler) OIDCLogin(c echo.Context) error { + var req OIDCLoginRequest + if err := c.Bind(&req); err != nil { + return err + } + if err := c.Validate(&req); err != nil { + return err + } + + ctx := c.Request().Context() + loginResp, err := r.accountService.LoginWithOIDC(ctx, req.Code, req.State) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "OIDC 认证失败: "+err.Error()) + } + + return orz.Ok(c, loginResp) +} + +// GetAuthConfig 获取认证配置 +func (r AccountHandler) GetAuthConfig(c echo.Context) error { + config := r.accountService.GetAuthConfig() + return orz.Ok(c, config) +} + +// GetOIDCAuthURL 获取 OIDC 认证 URL +func (r AccountHandler) GetOIDCAuthURL(c echo.Context) error { + authURL, err := r.accountService.GetOIDCAuthURL() + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + return orz.Ok(c, authURL) +} + +// GetGitHubAuthURL 获取 GitHub 认证 URL +func (r AccountHandler) GetGitHubAuthURL(c echo.Context) error { + authURL, err := r.accountService.GetGitHubAuthURL() + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + return orz.Ok(c, authURL) +} + +// GitHubLoginRequest GitHub 登录请求 +type GitHubLoginRequest struct { + Code string `json:"code" validate:"required"` + State string `json:"state" validate:"required"` +} + +// GitHubLogin GitHub 登录回调 +func (r AccountHandler) GitHubLogin(c echo.Context) error { + var req GitHubLoginRequest + if err := c.Bind(&req); err != nil { + return err + } + if err := c.Validate(&req); err != nil { + return err + } + + ctx := c.Request().Context() + loginResp, err := r.accountService.LoginWithGitHub(ctx, req.Code, req.State) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "GitHub 认证失败: "+err.Error()) } return orz.Ok(c, loginResp) diff --git a/internal/handler/agent_handler.go b/internal/handler/agent_handler.go index 1685180..88c3cc8 100644 --- a/internal/handler/agent_handler.go +++ b/internal/handler/agent_handler.go @@ -210,7 +210,7 @@ func (h *AgentHandler) Paging(c echo.Context) error { ip := c.QueryParam("ip") status := c.QueryParam("status") - pr := orz.GetPageRequest(c, "last_seen_at", "hostname") + pr := orz.GetPageRequest(c, "name") builder := orz.NewPageBuilder(h.agentService.AgentRepo). PageRequest(pr). diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go deleted file mode 100644 index dd48f91..0000000 --- a/internal/handler/user_handler.go +++ /dev/null @@ -1,182 +0,0 @@ -package handler - -import ( - "github.com/dushixiang/pika/internal/service" - "github.com/go-orz/orz" - "github.com/labstack/echo/v4" -) - -type UserHandler struct { - userService *service.UserService -} - -func NewUserHandler(userService *service.UserService) *UserHandler { - return &UserHandler{ - userService: userService, - } -} - -// ResetPasswordRequest 重置密码请求 -type ResetPasswordRequest struct { - NewPassword string `json:"newPassword" validate:"required,min=6"` -} - -// UpdateUserStatusRequest 更新用户状态请求 -type UpdateUserStatusRequest struct { - Status int `json:"status" validate:"required,oneof=0 1"` -} - -// Paging 用户分页查询 -func (r UserHandler) Paging(c echo.Context) error { - username := c.QueryParam("username") - - pr := orz.GetPageRequest(c, "created_at", "username") - - builder := orz.NewPageBuilder(r.userService.UserRepo). - PageRequest(pr). - Contains("username", username) - - ctx := c.Request().Context() - page, err := builder.Execute(ctx) - if err != nil { - return err - } - - return orz.Ok(c, orz.Map{ - "items": page.Items, - "total": page.Total, - }) -} - -// Create 创建用户 -func (r UserHandler) Create(c echo.Context) error { - var req service.CreateUserRequest - if err := c.Bind(&req); err != nil { - return err - } - if err := c.Validate(&req); err != nil { - return err - } - - ctx := c.Request().Context() - user, err := r.userService.CreateUser(ctx, &req) - if err != nil { - return err - } - - return orz.Ok(c, user) -} - -// Get 获取用户详情 -func (r UserHandler) Get(c echo.Context) error { - id := c.Param("id") - ctx := c.Request().Context() - - user, err := r.userService.GetUser(ctx, id) - if err != nil { - return err - } - - return orz.Ok(c, user) -} - -// Update 更新用户 -func (r UserHandler) Update(c echo.Context) error { - id := c.Param("id") - - var req service.UpdateUserRequest - if err := c.Bind(&req); err != nil { - return err - } - if err := c.Validate(&req); err != nil { - return err - } - - ctx := c.Request().Context() - user, err := r.userService.UpdateUser(ctx, id, &req) - if err != nil { - return err - } - - return orz.Ok(c, user) -} - -// Delete 删除用户 -func (r UserHandler) Delete(c echo.Context) error { - id := c.Param("id") - ctx := c.Request().Context() - - if err := r.userService.DeleteUser(ctx, id); err != nil { - return err - } - - return orz.Ok(c, orz.Map{ - "message": "用户删除成功", - }) -} - -// ChangePassword 修改密码 -func (r UserHandler) ChangePassword(c echo.Context) error { - id := c.Param("id") - - var req service.ChangePasswordRequest - if err := c.Bind(&req); err != nil { - return err - } - if err := c.Validate(&req); err != nil { - return err - } - - ctx := c.Request().Context() - if err := r.userService.ChangePassword(ctx, id, req.OldPassword, req.NewPassword); err != nil { - return err - } - - return orz.Ok(c, orz.Map{ - "message": "密码修改成功", - }) -} - -// ResetPassword 重置密码(管理员操作) -func (r UserHandler) ResetPassword(c echo.Context) error { - id := c.Param("id") - - var req ResetPasswordRequest - if err := c.Bind(&req); err != nil { - return err - } - if err := c.Validate(&req); err != nil { - return err - } - - ctx := c.Request().Context() - if err := r.userService.ResetPassword(ctx, id, req.NewPassword); err != nil { - return err - } - - return orz.Ok(c, orz.Map{ - "message": "密码重置成功", - }) -} - -// UpdateStatus 更新用户状态 -func (r UserHandler) UpdateStatus(c echo.Context) error { - id := c.Param("id") - - var req UpdateUserStatusRequest - if err := c.Bind(&req); err != nil { - return err - } - if err := c.Validate(&req); err != nil { - return err - } - - ctx := c.Request().Context() - if err := r.userService.UpdateUserStatus(ctx, id, req.Status); err != nil { - return err - } - - return orz.Ok(c, orz.Map{ - "message": "用户状态更新成功", - }) -} diff --git a/internal/models/user.go b/internal/models/user.go deleted file mode 100644 index 900bdfb..0000000 --- a/internal/models/user.go +++ /dev/null @@ -1,15 +0,0 @@ -package models - -// User 用户信息 -type User struct { - ID string `gorm:"primaryKey" json:"id"` // 用户ID (UUID) - Username string `gorm:"uniqueIndex" json:"username"` // 用户名 - Password string `json:"-"` // 密码(加密存储,不返回给前端) - Nickname string `json:"nickname"` // 昵称 - CreatedAt int64 `json:"createdAt"` // 创建时间(时间戳毫秒) - UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"` // 更新时间(时间戳毫秒) -} - -func (User) TableName() string { - return "users" -} diff --git a/internal/repo/user_repo.go b/internal/repo/user_repo.go deleted file mode 100644 index 29ee49c..0000000 --- a/internal/repo/user_repo.go +++ /dev/null @@ -1,81 +0,0 @@ -package repo - -import ( - "context" - - "github.com/dushixiang/pika/internal/models" - "github.com/go-orz/orz" - "gorm.io/gorm" -) - -type UserRepo struct { - orz.Repository[models.User, string] - db *gorm.DB -} - -func NewUserRepo(db *gorm.DB) *UserRepo { - return &UserRepo{ - Repository: orz.NewRepository[models.User, string](db), - db: db, - } -} - -// FindByUsername 根据用户名查找用户 -func (r *UserRepo) FindByUsername(ctx context.Context, username string) (*models.User, error) { - var user models.User - err := r.db.WithContext(ctx). - Where("username = ?", username). - First(&user).Error - if err != nil { - return nil, err - } - return &user, nil -} - -// FindByEmail 根据邮箱查找用户 -func (r *UserRepo) FindByEmail(ctx context.Context, email string) (*models.User, error) { - var user models.User - err := r.db.WithContext(ctx). - Where("email = ?", email). - First(&user).Error - if err != nil { - return nil, err - } - return &user, nil -} - -// UpdatePassword 更新密码 -func (r *UserRepo) UpdatePassword(ctx context.Context, userID string, password string) error { - return r.db.WithContext(ctx). - Model(&models.User{}). - Where("id = ?", userID). - Update("password", password).Error -} - -// UpdateStatus 更新状态 -func (r *UserRepo) UpdateStatus(ctx context.Context, userID string, status int) error { - return r.db.WithContext(ctx). - Model(&models.User{}). - Where("id = ?", userID). - Update("status", status).Error -} - -// ListUsers 列出用户(分页) -func (r *UserRepo) ListUsers(ctx context.Context, offset, limit int) ([]models.User, int64, error) { - var users []models.User - var total int64 - - // 获取总数 - if err := r.db.WithContext(ctx).Model(&models.User{}).Count(&total).Error; err != nil { - return nil, 0, err - } - - // 获取分页数据 - err := r.db.WithContext(ctx). - Offset(offset). - Limit(limit). - Order("created_at DESC"). - Find(&users).Error - - return users, total, err -} diff --git a/internal/service/account_service.go b/internal/service/account_service.go index 75b9b98..ca1e4c4 100644 --- a/internal/service/account_service.go +++ b/internal/service/account_service.go @@ -5,13 +5,12 @@ import ( "time" "github.com/dushixiang/pika/internal/config" - "github.com/dushixiang/pika/internal/models" "github.com/go-errors/errors" "github.com/golang-jwt/jwt/v5" "go.uber.org/zap" ) -func NewAccountService(logger *zap.Logger, userService *UserService, appConfig *config.AppConfig) *AccountService { +func NewAccountService(logger *zap.Logger, userService *UserService, oidcService *OIDCService, githubService *GitHubOAuthService, appConfig *config.AppConfig) *AccountService { jwtSecret := appConfig.JWT.Secret tokenExpireHours := appConfig.JWT.ExpiresHours @@ -28,6 +27,8 @@ func NewAccountService(logger *zap.Logger, userService *UserService, appConfig * service := &AccountService{ logger: logger, userService: userService, + oidcService: oidcService, + githubService: githubService, jwtSecret: jwtSecret, tokenExpireHours: tokenExpireHours, } @@ -37,6 +38,8 @@ func NewAccountService(logger *zap.Logger, userService *UserService, appConfig * type AccountService struct { logger *zap.Logger userService *UserService + oidcService *OIDCService + githubService *GitHubOAuthService jwtSecret string tokenExpireHours int } @@ -48,32 +51,82 @@ type JWTClaims struct { jwt.RegisteredClaims } -// LoginResponse 登录响应 -type LoginResponse struct { - Token string `json:"token"` - ExpiresAt int64 `json:"expiresAt"` - User *models.User `json:"user"` +// UserInfo 用户信息(简化版) +type UserInfo struct { + Username string `json:"username"` + Nickname string `json:"nickname"` } -// Login 用户登录 +// LoginResponse 登录响应 +type LoginResponse struct { + Token string `json:"token"` + ExpiresAt int64 `json:"expiresAt"` + User *UserInfo `json:"user"` +} + +// Login 用户登录(Basic Auth) func (s *AccountService) Login(ctx context.Context, username, password string) (*LoginResponse, error) { - // 验证用户名密码 - user, err := s.userService.ValidatePassword(ctx, username, password) + // 使用 Basic Auth 验证 + if err := s.userService.ValidateCredentials(ctx, username, password); err != nil { + return nil, err + } + + // 生成 JWT token + token, expiresAt, err := s.generateToken(username, username) + if err != nil { + return nil, err + } + + s.logger.Info("用户登录成功", zap.String("username", username)) + + return &LoginResponse{ + Token: token, + ExpiresAt: expiresAt, + User: &UserInfo{ + Username: username, + Nickname: username, + }, + }, nil +} + +// LoginWithOIDC OIDC 登录 +func (s *AccountService) LoginWithOIDC(ctx context.Context, code, state string) (*LoginResponse, error) { + // 使用 OIDC 验证 + username, nickname, err := s.oidcService.ExchangeCode(ctx, code, state) if err != nil { return nil, err } // 生成 JWT token + token, expiresAt, err := s.generateToken(username, nickname) + if err != nil { + return nil, err + } + + s.logger.Info("OIDC 登录成功", zap.String("username", username)) + + return &LoginResponse{ + Token: token, + ExpiresAt: expiresAt, + User: &UserInfo{ + Username: username, + Nickname: nickname, + }, + }, nil +} + +// generateToken 生成 JWT token +func (s *AccountService) generateToken(username, nickname string) (string, int64, error) { expiresAt := time.Now().Add(time.Duration(s.tokenExpireHours) * time.Hour) claims := &JWTClaims{ - UserID: user.ID, - Username: user.Username, + UserID: username, // 使用 username 作为 userID + Username: username, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expiresAt), IssuedAt: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now()), Issuer: "pika", - Subject: user.ID, + Subject: username, }, } @@ -81,16 +134,10 @@ func (s *AccountService) Login(ctx context.Context, username, password string) ( tokenString, err := token.SignedString([]byte(s.jwtSecret)) if err != nil { s.logger.Error("生成token失败", zap.Error(err)) - return nil, errors.New("生成token失败") + return "", 0, errors.New("生成token失败") } - s.logger.Info("用户登录成功", zap.String("userID", user.ID), zap.String("username", user.Username)) - - return &LoginResponse{ - Token: tokenString, - ExpiresAt: expiresAt.UnixMilli(), - User: user, - }, nil + return tokenString, expiresAt.UnixMilli(), nil } // Logout 用户登出 @@ -120,3 +167,89 @@ func (s *AccountService) ValidateToken(tokenString string) (*JWTClaims, error) { return nil, errors.New("无效的token") } + +// AuthConfig 认证配置 +type AuthConfig struct { + OIDCEnabled bool `json:"oidcEnabled"` + GitHubEnabled bool `json:"githubEnabled"` +} + +// GetAuthConfig 获取认证配置 +func (s *AccountService) GetAuthConfig() *AuthConfig { + return &AuthConfig{ + OIDCEnabled: s.oidcService.IsEnabled(), + GitHubEnabled: s.githubService.IsEnabled(), + } +} + +// OIDCAuthURL OIDC 认证 URL 响应 +type OIDCAuthURL struct { + AuthURL string `json:"authUrl"` + State string `json:"state"` +} + +// GetOIDCAuthURL 获取 OIDC 认证 URL +func (s *AccountService) GetOIDCAuthURL() (*OIDCAuthURL, error) { + if !s.oidcService.IsEnabled() { + return nil, errors.New("OIDC 未启用") + } + + authURL, state, err := s.oidcService.GenerateAuthURL() + if err != nil { + return nil, err + } + + return &OIDCAuthURL{ + AuthURL: authURL, + State: state, + }, nil +} + +// GitHubAuthURL GitHub 认证 URL 响应 +type GitHubAuthURL struct { + AuthURL string `json:"authUrl"` + State string `json:"state"` +} + +// GetGitHubAuthURL 获取 GitHub 认证 URL +func (s *AccountService) GetGitHubAuthURL() (*GitHubAuthURL, error) { + if !s.githubService.IsEnabled() { + return nil, errors.New("GitHub OAuth 未启用") + } + + authURL, state, err := s.githubService.GenerateAuthURL() + if err != nil { + return nil, err + } + + return &GitHubAuthURL{ + AuthURL: authURL, + State: state, + }, nil +} + +// LoginWithGitHub GitHub 登录 +func (s *AccountService) LoginWithGitHub(ctx context.Context, code, state string) (*LoginResponse, error) { + // 使用 GitHub OAuth 验证 + username, nickname, err := s.githubService.ExchangeCode(ctx, code, state) + if err != nil { + return nil, err + } + + // 生成 JWT token + token, expiresAt, err := s.generateToken(username, nickname) + if err != nil { + return nil, err + } + + s.logger.Info("GitHub 登录成功", zap.String("username", username)) + + return &LoginResponse{ + Token: token, + ExpiresAt: expiresAt, + User: &UserInfo{ + Username: username, + Nickname: nickname, + }, + }, nil +} diff --git a/internal/service/github_oauth_service.go b/internal/service/github_oauth_service.go new file mode 100644 index 0000000..522d8c8 --- /dev/null +++ b/internal/service/github_oauth_service.go @@ -0,0 +1,274 @@ +package service + +import ( + "context" + "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/dushixiang/pika/internal/config" + "go.uber.org/zap" +) + +// GitHubOAuthService GitHub OAuth 认证服务 +type GitHubOAuthService struct { + logger *zap.Logger + config *config.GitHubOAuthConfig + stateStore map[string]time.Time // 简单的 state 存储(生产环境应使用 Redis 等) + httpClient *http.Client +} + +// GitHubUserInfo GitHub 用户信息 +type GitHubUserInfo struct { + Login string `json:"login"` // GitHub 用户名 + Name string `json:"name"` // 显示名称 + Email string `json:"email"` // 邮箱 + AvatarURL string `json:"avatar_url"` // 头像 +} + +// GitHubAccessTokenResponse GitHub Access Token 响应 +type GitHubAccessTokenResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + Scope string `json:"scope"` +} + +// NewGitHubOAuthService 创建 GitHub OAuth 服务 +func NewGitHubOAuthService(logger *zap.Logger, appConfig *config.AppConfig) *GitHubOAuthService { + if appConfig.GitHub == nil || !appConfig.GitHub.Enabled { + logger.Info("GitHub OAuth 认证未启用") + return &GitHubOAuthService{ + logger: logger, + config: nil, + } + } + + githubConfig := appConfig.GitHub + + // 验证配置 + if githubConfig.ClientID == "" || githubConfig.ClientSecret == "" { + logger.Error("GitHub OAuth 配置不完整,GitHub 认证将被禁用") + return &GitHubOAuthService{ + logger: logger, + config: nil, + } + } + + logger.Info("GitHub OAuth 服务初始化成功") + + return &GitHubOAuthService{ + logger: logger, + config: githubConfig, + stateStore: make(map[string]time.Time), + httpClient: &http.Client{ + Timeout: 10 * time.Second, + }, + } +} + +// IsEnabled 检查 GitHub OAuth 是否启用 +func (s *GitHubOAuthService) IsEnabled() bool { + return s.config != nil && s.config.Enabled +} + +// GenerateAuthURL 生成 GitHub 认证 URL +func (s *GitHubOAuthService) GenerateAuthURL() (string, string, error) { + if !s.IsEnabled() { + return "", "", errors.New("GitHub OAuth 未启用") + } + + // 生成随机 state + state, err := s.generateState() + if err != nil { + return "", "", fmt.Errorf("生成 state 失败: %w", err) + } + + // 存储 state(有效期 10 分钟) + s.stateStore[state] = time.Now().Add(10 * time.Minute) + + // 清理过期的 state + s.cleanExpiredStates() + + // 构建 GitHub 授权 URL + authURL := fmt.Sprintf("https://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s&state=%s&scope=user:email", + url.QueryEscape(s.config.ClientID), + url.QueryEscape(s.config.RedirectURL), + url.QueryEscape(state), + ) + + return authURL, state, nil +} + +// ExchangeCode 交换授权码获取 access token 和用户信息 +func (s *GitHubOAuthService) ExchangeCode(ctx context.Context, code, state string) (string, string, error) { + if !s.IsEnabled() { + return "", "", errors.New("GitHub OAuth 未启用") + } + + // 验证 state + if !s.validateState(state) { + return "", "", errors.New("无效的 state") + } + + // 删除已使用的 state + delete(s.stateStore, state) + + // 交换 code 获取 access token + accessToken, err := s.getAccessToken(ctx, code) + if err != nil { + return "", "", fmt.Errorf("获取 access token 失败: %w", err) + } + + // 使用 access token 获取用户信息 + userInfo, err := s.getUserInfo(ctx, accessToken) + if err != nil { + return "", "", fmt.Errorf("获取用户信息失败: %w", err) + } + + // 确定用户标识 + username := userInfo.Login + if username == "" { + return "", "", errors.New("无法获取 GitHub 用户名") + } + + // 检查用户是否在白名单中 + if !s.isUserAllowed(username) { + s.logger.Warn("GitHub 用户不在白名单中,拒绝登录", + zap.String("username", username)) + return "", "", fmt.Errorf("用户 %s 不在允许登录的白名单中", username) + } + + nickname := userInfo.Name + if nickname == "" { + nickname = username + } + + s.logger.Info("GitHub OAuth 认证成功", + zap.String("username", username), + zap.String("nickname", nickname), + zap.String("email", userInfo.Email)) + + return username, nickname, nil +} + +// getAccessToken 获取 access token +func (s *GitHubOAuthService) getAccessToken(ctx context.Context, code string) (string, error) { + // 构建请求 + data := url.Values{} + data.Set("client_id", s.config.ClientID) + data.Set("client_secret", s.config.ClientSecret) + data.Set("code", code) + data.Set("redirect_uri", s.config.RedirectURL) + + req, err := http.NewRequestWithContext(ctx, "POST", "https://github.com/login/oauth/access_token", nil) + if err != nil { + return "", err + } + + req.URL.RawQuery = data.Encode() + req.Header.Set("Accept", "application/json") + + // 发送请求 + resp, err := s.httpClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return "", fmt.Errorf("GitHub API 返回错误: %d, %s", resp.StatusCode, string(body)) + } + + // 解析响应 + var tokenResp GitHubAccessTokenResponse + if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { + return "", err + } + + if tokenResp.AccessToken == "" { + return "", errors.New("未获取到 access token") + } + + return tokenResp.AccessToken, nil +} + +// getUserInfo 获取用户信息 +func (s *GitHubOAuthService) getUserInfo(ctx context.Context, accessToken string) (*GitHubUserInfo, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/user", nil) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", "Bearer "+accessToken) + req.Header.Set("Accept", "application/vnd.github+json") + + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("GitHub API 返回错误: %d, %s", resp.StatusCode, string(body)) + } + + var userInfo GitHubUserInfo + if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil { + return nil, err + } + + return &userInfo, nil +} + +// generateState 生成随机 state +func (s *GitHubOAuthService) generateState() (string, error) { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(b), nil +} + +// validateState 验证 state +func (s *GitHubOAuthService) validateState(state string) bool { + expiresAt, exists := s.stateStore[state] + if !exists { + return false + } + return time.Now().Before(expiresAt) +} + +// cleanExpiredStates 清理过期的 state +func (s *GitHubOAuthService) cleanExpiredStates() { + now := time.Now() + for state, expiresAt := range s.stateStore { + if now.After(expiresAt) { + delete(s.stateStore, state) + } + } +} + +// isUserAllowed 检查用户是否在白名单中 +func (s *GitHubOAuthService) isUserAllowed(username string) bool { + // 如果未配置白名单,则允许所有用户 + if len(s.config.AllowedUsers) == 0 { + return true + } + + // 检查用户是否在白名单中 + for _, allowedUser := range s.config.AllowedUsers { + if allowedUser == username { + return true + } + } + + return false +} diff --git a/internal/service/oidc_service.go b/internal/service/oidc_service.go new file mode 100644 index 0000000..dee4ffa --- /dev/null +++ b/internal/service/oidc_service.go @@ -0,0 +1,203 @@ +package service + +import ( + "context" + "crypto/rand" + "encoding/base64" + "errors" + "fmt" + "time" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/dushixiang/pika/internal/config" + "go.uber.org/zap" + "golang.org/x/oauth2" +) + +// OIDCService OIDC 认证服务 +type OIDCService struct { + logger *zap.Logger + config *config.OIDCConfig + provider *oidc.Provider + oauth2Config oauth2.Config + verifier *oidc.IDTokenVerifier + stateStore map[string]time.Time // 简单的 state 存储(生产环境应使用 Redis 等) +} + +// NewOIDCService 创建 OIDC 服务 +func NewOIDCService(logger *zap.Logger, appConfig *config.AppConfig) *OIDCService { + if appConfig.OIDC == nil || !appConfig.OIDC.Enabled { + logger.Info("OIDC 认证未启用") + return &OIDCService{ + logger: logger, + config: nil, + } + } + + oidcConfig := appConfig.OIDC + + // 验证配置 + if oidcConfig.Issuer == "" || oidcConfig.ClientID == "" || oidcConfig.ClientSecret == "" { + logger.Error("OIDC 配置不完整,OIDC 认证将被禁用") + return &OIDCService{ + logger: logger, + config: nil, + } + } + + ctx := context.Background() + + // 初始化 OIDC Provider + provider, err := oidc.NewProvider(ctx, oidcConfig.Issuer) + if err != nil { + logger.Error("初始化 OIDC Provider 失败,OIDC 认证将被禁用", zap.Error(err)) + return &OIDCService{ + logger: logger, + config: nil, + } + } + + // 配置 OAuth2 + oauth2Config := oauth2.Config{ + ClientID: oidcConfig.ClientID, + ClientSecret: oidcConfig.ClientSecret, + RedirectURL: oidcConfig.RedirectURL, + Endpoint: provider.Endpoint(), + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + } + + // 创建 ID Token 验证器 + verifier := provider.Verifier(&oidc.Config{ClientID: oidcConfig.ClientID}) + + logger.Info("OIDC 服务初始化成功", zap.String("issuer", oidcConfig.Issuer)) + + return &OIDCService{ + logger: logger, + config: oidcConfig, + provider: provider, + oauth2Config: oauth2Config, + verifier: verifier, + stateStore: make(map[string]time.Time), + } +} + +// IsEnabled 检查 OIDC 是否启用 +func (s *OIDCService) IsEnabled() bool { + return s.config != nil && s.config.Enabled +} + +// GenerateAuthURL 生成认证 URL +func (s *OIDCService) GenerateAuthURL() (string, string, error) { + if !s.IsEnabled() { + return "", "", errors.New("OIDC 未启用") + } + + // 生成随机 state + state, err := s.generateState() + if err != nil { + return "", "", fmt.Errorf("生成 state 失败: %w", err) + } + + // 存储 state(有效期 10 分钟) + s.stateStore[state] = time.Now().Add(10 * time.Minute) + + // 清理过期的 state + s.cleanExpiredStates() + + authURL := s.oauth2Config.AuthCodeURL(state) + return authURL, state, nil +} + +// ExchangeCode 交换授权码获取 token 和用户信息 +func (s *OIDCService) ExchangeCode(ctx context.Context, code, state string) (string, string, error) { + if !s.IsEnabled() { + return "", "", errors.New("OIDC 未启用") + } + + // 验证 state + if !s.validateState(state) { + return "", "", errors.New("无效的 state") + } + + // 删除已使用的 state + delete(s.stateStore, state) + + // 交换授权码 + oauth2Token, err := s.oauth2Config.Exchange(ctx, code) + if err != nil { + return "", "", fmt.Errorf("交换授权码失败: %w", err) + } + + // 提取 ID Token + rawIDToken, ok := oauth2Token.Extra("id_token").(string) + if !ok { + return "", "", errors.New("未获取到 ID Token") + } + + // 验证 ID Token + idToken, err := s.verifier.Verify(ctx, rawIDToken) + if err != nil { + return "", "", fmt.Errorf("验证 ID Token 失败: %w", err) + } + + // 提取用户信息 + var claims struct { + Email string `json:"email"` + EmailVerified bool `json:"email_verified"` + Name string `json:"name"` + PreferredUsername string `json:"preferred_username"` + } + + if err := idToken.Claims(&claims); err != nil { + return "", "", fmt.Errorf("解析 claims 失败: %w", err) + } + + // 确定用户标识(优先使用 email,其次 preferred_username,最后使用 subject) + username := claims.Email + if username == "" { + username = claims.PreferredUsername + } + if username == "" { + username = idToken.Subject + } + + nickname := claims.Name + if nickname == "" { + nickname = username + } + + s.logger.Info("OIDC 认证成功", + zap.String("username", username), + zap.String("nickname", nickname), + zap.String("subject", idToken.Subject)) + + return username, nickname, nil +} + +// generateState 生成随机 state +func (s *OIDCService) generateState() (string, error) { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(b), nil +} + +// validateState 验证 state +func (s *OIDCService) validateState(state string) bool { + expiresAt, exists := s.stateStore[state] + if !exists { + return false + } + return time.Now().Before(expiresAt) +} + +// cleanExpiredStates 清理过期的 state +func (s *OIDCService) cleanExpiredStates() { + now := time.Now() + for state, expiresAt := range s.stateStore { + if now.After(expiresAt) { + delete(s.stateStore, state) + } + } +} diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 781992e..93c603f 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -3,194 +3,54 @@ package service import ( "context" "errors" - "time" - "github.com/dushixiang/pika/internal/models" - "github.com/dushixiang/pika/internal/repo" - "github.com/google/uuid" + "github.com/dushixiang/pika/internal/config" "go.uber.org/zap" "golang.org/x/crypto/bcrypt" - "gorm.io/gorm" ) +// UserService User 认证服务 type UserService struct { - logger *zap.Logger - UserRepo *repo.UserRepo + logger *zap.Logger + users map[string]string // 用户名 -> bcrypt加密的密码 } -func NewUserService(logger *zap.Logger, db *gorm.DB) *UserService { +// NewUserService 创建 User 服务 +func NewUserService(logger *zap.Logger, appConfig *config.AppConfig) *UserService { return &UserService{ - logger: logger, - UserRepo: repo.NewUserRepo(db), + logger: logger, + users: appConfig.Users, } } -// CreateUser 创建用户 -func (s *UserService) CreateUser(ctx context.Context, req *CreateUserRequest) (*models.User, error) { - // 检查用户名是否已存在 - existingUser, err := s.UserRepo.FindByUsername(ctx, req.Username) - if err == nil && existingUser != nil { - return nil, errors.New("用户名已存在") - } - // 如果不是"记录不存在"的错误,说明是数据库错误 - if err != nil && err.Error() != "record not found" { - s.logger.Error("查询用户名失败", zap.Error(err), zap.String("username", req.Username)) - return nil, errors.New("系统错误,请稍后重试") - } - - // 加密密码 - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) - if err != nil { - return nil, err - } - - now := time.Now().UnixMilli() - user := &models.User{ - ID: uuid.NewString(), - Username: req.Username, - Password: string(hashedPassword), - Nickname: req.Nickname, - CreatedAt: now, - UpdatedAt: now, - } - - if err := s.UserRepo.Create(ctx, user); err != nil { - return nil, err - } - - s.logger.Info("user created successfully", zap.String("userID", user.ID), zap.String("username", user.Username)) - return user, nil -} - -// UpdateUser 更新用户 -func (s *UserService) UpdateUser(ctx context.Context, userID string, req *UpdateUserRequest) (*models.User, error) { - user, err := s.UserRepo.FindById(ctx, userID) - if err != nil { - return nil, errors.New("用户不存在") - } - - // 更新字段 - if req.Nickname != "" { - user.Nickname = req.Nickname - } - - if err := s.UserRepo.UpdateById(ctx, &user); err != nil { - return nil, err - } - - s.logger.Info("user updated successfully", zap.String("userID", userID)) - return &user, nil -} - -// DeleteUser 删除用户 -func (s *UserService) DeleteUser(ctx context.Context, userID string) error { - if err := s.UserRepo.DeleteById(ctx, userID); err != nil { - return err - } - - s.logger.Info("user deleted successfully", zap.String("userID", userID)) - return nil -} - -// GetUser 获取用户信息 -func (s *UserService) GetUser(ctx context.Context, userID string) (*models.User, error) { - user, err := s.UserRepo.FindById(ctx, userID) - if err != nil { - return nil, errors.New("用户不存在") - } - return &user, nil -} - -// GetUserByUsername 根据用户名获取用户 -func (s *UserService) GetUserByUsername(ctx context.Context, username string) (*models.User, error) { - return s.UserRepo.FindByUsername(ctx, username) -} - -// ListUsers 列出用户 -func (s *UserService) ListUsers(ctx context.Context, page, pageSize int) ([]models.User, int64, error) { - offset := (page - 1) * pageSize - return s.UserRepo.ListUsers(ctx, offset, pageSize) -} - -// ChangePassword 修改密码 -func (s *UserService) ChangePassword(ctx context.Context, userID string, oldPassword, newPassword string) error { - user, err := s.UserRepo.FindById(ctx, userID) - if err != nil { - return errors.New("用户不存在") - } - - // 验证旧密码 - if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(oldPassword)); err != nil { - return errors.New("旧密码错误") - } - - // 加密新密码 - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) - if err != nil { - return err - } - - if err := s.UserRepo.UpdatePassword(ctx, userID, string(hashedPassword)); err != nil { - return err - } - - s.logger.Info("password changed successfully", zap.String("userID", userID)) - return nil -} - -// ResetPassword 重置密码(管理员操作) -func (s *UserService) ResetPassword(ctx context.Context, userID, newPassword string) error { - // 加密新密码 - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) - if err != nil { - return err - } - - if err := s.UserRepo.UpdatePassword(ctx, userID, string(hashedPassword)); err != nil { - return err - } - - s.logger.Info("password reset successfully", zap.String("userID", userID)) - return nil -} - -// UpdateUserStatus 更新用户状态 -func (s *UserService) UpdateUserStatus(ctx context.Context, userID string, status int) error { - if err := s.UserRepo.UpdateStatus(ctx, userID, status); err != nil { - return err - } - - s.logger.Info("user status updated successfully", zap.String("userID", userID), zap.Int("status", status)) - return nil -} - -// ValidatePassword 验证密码 -func (s *UserService) ValidatePassword(ctx context.Context, username, password string) (*models.User, error) { - user, err := s.UserRepo.FindByUsername(ctx, username) - if err != nil { - return nil, errors.New("用户名或密码错误") +// ValidateCredentials 验证用户名和密码 +func (s *UserService) ValidateCredentials(ctx context.Context, username, password string) error { + // 从配置中获取用户的bcrypt密码哈希 + hashedPassword, exists := s.users[username] + if !exists { + s.logger.Debug("用户不存在", zap.String("username", username)) + return errors.New("用户名或密码错误") } // 验证密码 - if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { - return nil, errors.New("用户名或密码错误") + if err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)); err != nil { + s.logger.Debug("密码验证失败", zap.String("username", username), zap.Error(err)) + return errors.New("用户名或密码错误") } - return user, nil + s.logger.Info("User 认证成功", zap.String("username", username)) + return nil } -// 数据结构定义 -type CreateUserRequest struct { - Username string `json:"username" validate:"required"` - Password string `json:"password" validate:"required,min=6"` - Nickname string `json:"nickname"` +// GetUsername 获取用户名(如果认证成功) +func (s *UserService) GetUsername(ctx context.Context, username string) (string, error) { + if _, exists := s.users[username]; !exists { + return "", errors.New("用户不存在") + } + return username, nil } -type UpdateUserRequest struct { - Nickname string `json:"nickname"` -} - -type ChangePasswordRequest struct { - OldPassword string `json:"oldPassword" validate:"required"` - NewPassword string `json:"newPassword" validate:"required,min=6"` +// IsEnabled 检查 User 是否配置 +func (s *UserService) IsEnabled() bool { + return len(s.users) > 0 } diff --git a/internal/wire.go b/internal/wire.go index d836bb4..d5fe5c3 100644 --- a/internal/wire.go +++ b/internal/wire.go @@ -19,6 +19,8 @@ func InitializeApp(logger *zap.Logger, db *gorm.DB, cfg *config.AppConfig) (*App service.NewAccountService, service.NewAgentService, service.NewUserService, + service.NewOIDCService, + service.NewGitHubOAuthService, service.NewApiKeyService, service.NewAlertService, service.NewPropertyService, @@ -30,7 +32,6 @@ func InitializeApp(logger *zap.Logger, db *gorm.DB, cfg *config.AppConfig) (*App // Handlers handler.NewAgentHandler, - handler.NewUserHandler, handler.NewAlertHandler, handler.NewPropertyHandler, handler.NewMonitorHandler, @@ -47,14 +48,12 @@ func InitializeApp(logger *zap.Logger, db *gorm.DB, cfg *config.AppConfig) (*App type AppComponents struct { AccountHandler *handler.AccountHandler AgentHandler *handler.AgentHandler - UserHandler *handler.UserHandler ApiKeyHandler *handler.ApiKeyHandler AlertHandler *handler.AlertHandler PropertyHandler *handler.PropertyHandler MonitorHandler *handler.MonitorHandler AgentService *service.AgentService - UserService *service.UserService AlertService *service.AlertService PropertyService *service.PropertyService MonitorService *service.MonitorService diff --git a/internal/wire_gen.go b/internal/wire_gen.go index 76446f0..710926c 100644 --- a/internal/wire_gen.go +++ b/internal/wire_gen.go @@ -19,15 +19,16 @@ import ( // InitializeApp 初始化应用 func InitializeApp(logger *zap.Logger, db *gorm.DB, cfg *config.AppConfig) (*AppComponents, error) { - userService := service.NewUserService(logger, db) - accountService := service.NewAccountService(logger, userService, cfg) + userService := service.NewUserService(logger, cfg) + oidcService := service.NewOIDCService(logger, cfg) + gitHubOAuthService := service.NewGitHubOAuthService(logger, cfg) + accountService := service.NewAccountService(logger, userService, oidcService, gitHubOAuthService, cfg) accountHandler := handler.NewAccountHandler(accountService) apiKeyService := service.NewApiKeyService(logger, db) agentService := service.NewAgentService(logger, db, apiKeyService) manager := websocket.NewManager(logger) monitorService := service.NewMonitorService(logger, db, manager) agentHandler := handler.NewAgentHandler(logger, agentService, monitorService, manager) - userHandler := handler.NewUserHandler(userService) apiKeyHandler := handler.NewApiKeyHandler(logger, apiKeyService) propertyService := service.NewPropertyService(logger, db) notifier := service.NewNotifier(logger) @@ -38,13 +39,11 @@ func InitializeApp(logger *zap.Logger, db *gorm.DB, cfg *config.AppConfig) (*App appComponents := &AppComponents{ AccountHandler: accountHandler, AgentHandler: agentHandler, - UserHandler: userHandler, ApiKeyHandler: apiKeyHandler, AlertHandler: alertHandler, PropertyHandler: propertyHandler, MonitorHandler: monitorHandler, AgentService: agentService, - UserService: userService, AlertService: alertService, PropertyService: propertyService, MonitorService: monitorService, @@ -60,14 +59,12 @@ func InitializeApp(logger *zap.Logger, db *gorm.DB, cfg *config.AppConfig) (*App type AppComponents struct { AccountHandler *handler.AccountHandler AgentHandler *handler.AgentHandler - UserHandler *handler.UserHandler ApiKeyHandler *handler.ApiKeyHandler AlertHandler *handler.AlertHandler PropertyHandler *handler.PropertyHandler MonitorHandler *handler.MonitorHandler AgentService *service.AgentService - UserService *service.UserService AlertService *service.AlertService PropertyService *service.PropertyService MonitorService *service.MonitorService diff --git a/web/eslint.config.js b/web/eslint.config.js deleted file mode 100644 index b19330b..0000000 --- a/web/eslint.config.js +++ /dev/null @@ -1,23 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs['recommended-latest'], - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - }, -]) diff --git a/web/src/api/agent.ts b/web/src/api/agent.ts index c7dabd1..b7eb2d3 100644 --- a/web/src/api/agent.ts +++ b/web/src/api/agent.ts @@ -1,5 +1,5 @@ -import { get, post, put } from './request'; -import type { Agent, LatestMetrics } from '../types'; +import {get, post, put} from './request'; +import type {Agent, LatestMetrics} from '../types'; export interface ListAgentsResponse { items: Agent[]; @@ -36,6 +36,8 @@ export const getAgentPaging = (pageIndex: number = 1, pageSize: number = 10, hos if (status) { params.append('status', status); } + params.set('sortOrder', 'asc'); + params.set('sortField', 'name'); return get(`/admin/agents?${params.toString()}`); }; @@ -59,7 +61,7 @@ export const getAgentForAdmin = (id: string) => { }; export const getAgentMetrics = (params: GetAgentMetricsRequest) => { - const { agentId, type, range = '1h' } = params; + const {agentId, type, range = '1h'} = params; const query = new URLSearchParams(); query.append('type', type); query.append('range', range); @@ -149,7 +151,7 @@ export const listAuditResults = (agentId: string) => { // 更新探针名称 export const updateAgentName = (agentId: string, name: string) => { - return put(`/admin/agents/${agentId}/name`, { name }); + return put(`/admin/agents/${agentId}/name`, {name}); }; // 更新探针信息(名称、平台、位置、到期时间) diff --git a/web/src/api/auth.ts b/web/src/api/auth.ts index 206d449..aae4229 100644 --- a/web/src/api/auth.ts +++ b/web/src/api/auth.ts @@ -1,10 +1,55 @@ -import { post } from './request'; +import { get, post } from './request'; import type { LoginRequest, LoginResponse } from '../types'; +// 认证配置 +export interface AuthConfig { + oidcEnabled: boolean; + githubEnabled: boolean; +} + +// OIDC 认证 URL +export interface OIDCAuthURL { + authUrl: string; + state: string; +} + +// GitHub 认证 URL +export interface GitHubAuthURL { + authUrl: string; + state: string; +} + +// 获取认证配置 +export const getAuthConfig = () => { + return get('/auth/config'); +}; + +// 获取 OIDC 认证 URL +export const getOIDCAuthURL = () => { + return get('/auth/oidc/url'); +}; + +// OIDC 登录回调 +export const oidcLogin = (code: string, state: string) => { + return post('/auth/oidc/callback', { code, state }); +}; + +// 获取 GitHub 认证 URL +export const getGitHubAuthURL = () => { + return get('/auth/github/url'); +}; + +// GitHub 登录回调 +export const githubLogin = (code: string, state: string) => { + return post('/auth/github/callback', { code, state }); +}; + +// Basic Auth 登录 export const login = (data: LoginRequest) => { return post('/login', data); }; +// 登出 export const logout = () => { return post('/admin/logout'); }; diff --git a/web/src/api/user.ts b/web/src/api/user.ts deleted file mode 100644 index b5197e0..0000000 --- a/web/src/api/user.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { get, post, put, del } from './request'; -import type { User, CreateUserRequest, UpdateUserRequest, ChangePasswordRequest } from '../types'; - -export interface ListUsersResponse { - items: User[]; - total: number; -} - -export const listUsers = (page: number = 1, pageSize: number = 10, username?: string) => { - const params = new URLSearchParams(); - params.append('page', page.toString()); - params.append('pageSize', pageSize.toString()); - if (username) { - params.append('username', username); - } - return get(`/admin/users?${params.toString()}`); -}; - -export const getUser = (id: string) => { - return get(`/admin/users/${id}`); -}; - -export const createUser = (data: CreateUserRequest) => { - return post('/admin/users', data); -}; - -export const updateUser = (id: string, data: UpdateUserRequest) => { - return put(`/admin/users/${id}`, data); -}; - -export const deleteUser = (id: string) => { - return del(`/admin/users/${id}`); -}; - -export const changePassword = (id: string, data: ChangePasswordRequest) => { - return post(`/admin/users/${id}/password`, data); -}; - -export const resetPassword = (id: string, newPassword: string) => { - return post(`/admin/users/${id}/reset-password`, { newPassword }); -}; - -export const updateUserStatus = (id: string, status: number) => { - return post(`/admin/users/${id}/status`, { status }); -}; - diff --git a/web/src/pages/AdminLayout/index.tsx b/web/src/pages/AdminLayout/index.tsx index d9e395e..719ff6e 100644 --- a/web/src/pages/AdminLayout/index.tsx +++ b/web/src/pages/AdminLayout/index.tsx @@ -2,7 +2,7 @@ import { type JSX, useEffect, useMemo, useState } from 'react'; import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import type { MenuProps } from 'antd'; import { App, Avatar, Button, Dropdown, Space } from 'antd'; -import { Eye, Key, LogOut, Server, User as UserIcon, Users, BookOpen, Settings, Activity } from 'lucide-react'; +import { Eye, Key, LogOut, Server, User as UserIcon, BookOpen, Settings, Activity } from 'lucide-react'; import { logout } from '../../api/auth'; import { getServerVersion } from '../../api/agent'; import type { User } from '../../types'; @@ -46,12 +46,6 @@ const AdminLayout = () => { path: '/admin/monitors', icon: , }, - { - key: 'users', - label: '用户管理', - path: '/admin/users', - icon: , - }, { key: 'settings', label: '系统设置', @@ -74,9 +68,7 @@ const AdminLayout = () => { setUserInfo(JSON.parse(userInfoStr)); const path = location.pathname; - if (path.startsWith('/admin/users')) { - setSelectedKey('users'); - } else if (path.startsWith('/admin/api-keys')) { + if (path.startsWith('/admin/api-keys')) { setSelectedKey('api-keys'); } else if (path.startsWith('/admin/monitors')) { setSelectedKey('monitors'); diff --git a/web/src/pages/Login/GitHubCallback.tsx b/web/src/pages/Login/GitHubCallback.tsx new file mode 100644 index 0000000..0eab475 --- /dev/null +++ b/web/src/pages/Login/GitHubCallback.tsx @@ -0,0 +1,51 @@ +import {useEffect} from 'react'; +import {useNavigate, useSearchParams} from 'react-router-dom'; +import {App} from 'antd'; +import {githubLogin} from '../../api/auth'; + +const GitHubCallback = () => { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const {message: messageApi} = App.useApp(); + + useEffect(() => { + const handleCallback = async () => { + const code = searchParams.get('code'); + const state = searchParams.get('state'); + + if (!code || !state) { + messageApi.error('缺少认证参数'); + navigate('/login'); + return; + } + + try { + const response = await githubLogin(code, state); + const {token, user} = response.data; + + // 保存 token 和用户信息 + localStorage.setItem('token', token); + localStorage.setItem('userInfo', JSON.stringify(user)); + + messageApi.success('登录成功'); + navigate('/admin/agents'); + } catch (error: any) { + messageApi.error(error.response?.data?.message || 'GitHub 认证失败'); + navigate('/login'); + } + }; + + handleCallback(); + }, []); + + return ( +
+
+
正在处理 GitHub 认证...
+
请稍候
+
+
+ ); +}; + +export default GitHubCallback; diff --git a/web/src/pages/Login/OIDCCallback.tsx b/web/src/pages/Login/OIDCCallback.tsx new file mode 100644 index 0000000..b48225a --- /dev/null +++ b/web/src/pages/Login/OIDCCallback.tsx @@ -0,0 +1,51 @@ +import {useEffect} from 'react'; +import {useNavigate, useSearchParams} from 'react-router-dom'; +import {App} from 'antd'; +import {oidcLogin} from '../../api/auth'; + +const OIDCCallback = () => { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const {message: messageApi} = App.useApp(); + + useEffect(() => { + const handleCallback = async () => { + const code = searchParams.get('code'); + const state = searchParams.get('state'); + + if (!code || !state) { + messageApi.error('缺少认证参数'); + navigate('/login'); + return; + } + + try { + const response = await oidcLogin(code, state); + const {token, user} = response.data; + + // 保存 token 和用户信息 + localStorage.setItem('token', token); + localStorage.setItem('userInfo', JSON.stringify(user)); + + messageApi.success('登录成功'); + navigate('/admin/agents'); + } catch (error: any) { + messageApi.error(error.response?.data?.message || 'OIDC 认证失败'); + navigate('/login'); + } + }; + + handleCallback(); + }, []); + + return ( +
+
+
正在处理 OIDC 认证...
+
请稍候
+
+
+ ); +}; + +export default OIDCCallback; diff --git a/web/src/pages/Login/index.tsx b/web/src/pages/Login/index.tsx index 9b172a5..b7643ae 100644 --- a/web/src/pages/Login/index.tsx +++ b/web/src/pages/Login/index.tsx @@ -1,15 +1,34 @@ -import {useState} from 'react'; +import {useEffect, useState} from 'react'; import {useNavigate} from 'react-router-dom'; -import {App, Button, Form, Input} from 'antd'; -import {LockOutlined, UserOutlined} from '@ant-design/icons'; -import {login} from '../../api/auth'; +import {App, Button, Divider, Form, Input} from 'antd'; +import {LockOutlined, UserOutlined, GithubOutlined} from '@ant-design/icons'; +import {getAuthConfig, getOIDCAuthURL, getGitHubAuthURL, login} from '../../api/auth'; import type {LoginRequest} from '../../types'; const Login = () => { const [loading, setLoading] = useState(false); + const [oidcEnabled, setOidcEnabled] = useState(false); + const [githubEnabled, setGithubEnabled] = useState(false); + const [oidcLoading, setOidcLoading] = useState(false); + const [githubLoading, setGithubLoading] = useState(false); const navigate = useNavigate(); const {message: messageApi} = App.useApp(); + // 获取认证配置 + useEffect(() => { + fetchAuthConfig(); + }, []); + + const fetchAuthConfig = async () => { + try { + const response = await getAuthConfig(); + setOidcEnabled(response.data.oidcEnabled); + setGithubEnabled(response.data.githubEnabled); + } catch (error) { + console.error('获取认证配置失败:', error); + } + }; + const onFinish = async (values: LoginRequest) => { setLoading(true); try { @@ -29,9 +48,34 @@ const Login = () => { } }; + const handleOIDCLogin = async () => { + setOidcLoading(true); + try { + const response = await getOIDCAuthURL(); + const {authUrl} = response.data; + // 跳转到 OIDC 认证页面 + window.location.href = authUrl; + } catch (error: any) { + messageApi.error(error.response?.data?.message || '获取 OIDC 认证地址失败'); + setOidcLoading(false); + } + }; + + const handleGitHubLogin = async () => { + setGithubLoading(true); + try { + const response = await getGitHubAuthURL(); + const {authUrl} = response.data; + // 跳转到 GitHub 认证页面 + window.location.href = authUrl; + } catch (error: any) { + messageApi.error(error.response?.data?.message || '获取 GitHub 认证地址失败'); + setGithubLoading(false); + } + }; + return ( -
+

Pika 探针

@@ -74,6 +118,32 @@ const Login = () => { + + {(oidcEnabled || githubEnabled) && ( + <> + 或 + {githubEnabled && ( + + )} + {oidcEnabled && ( + + )} + + )}
); diff --git a/web/src/pages/Users/UserList.tsx b/web/src/pages/Users/UserList.tsx deleted file mode 100644 index ebecbd1..0000000 --- a/web/src/pages/Users/UserList.tsx +++ /dev/null @@ -1,330 +0,0 @@ -import {useRef, useState} from 'react'; -import type {ActionType, ProColumns} from '@ant-design/pro-components'; -import {ProTable} from '@ant-design/pro-components'; -import {App, Button, Divider, Form, Input, Modal, Popconfirm} from 'antd'; -import {Edit, Key, Plus, RefreshCw, Trash2} from 'lucide-react'; -import {createUser, deleteUser, listUsers, resetPassword, updateUser} from '../../api/user'; -import type {CreateUserRequest, UpdateUserRequest, User} from '../../types'; -import dayjs from 'dayjs'; -import {getErrorMessage} from '../../lib/utils'; -import {PageHeader} from '../../components'; - -const UserList = () => { - const {message: messageApi, modal} = App.useApp(); - const actionRef = useRef(null); - const [submitting, setSubmitting] = useState(false); - const [isModalVisible, setIsModalVisible] = useState(false); - const [editingUser, setEditingUser] = useState(null); - const [form] = Form.useForm(); - - const handleCreate = () => { - setEditingUser(null); - setIsModalVisible(true); - form.resetFields(); - }; - - const handleEdit = (user: User) => { - setEditingUser(user); - form.setFieldsValue({ - username: user.username, - nickname: user.nickname, - }); - setIsModalVisible(true); - }; - - const handleDelete = async (userId: string) => { - try { - await deleteUser(userId); - messageApi.success('删除成功'); - actionRef.current?.reload(); - } catch (error: unknown) { - messageApi.error(getErrorMessage(error, '删除失败')); - } - }; - - const handleResetPassword = (user: User) => { - modal.confirm({ - title: `重置 ${user.username} 的密码`, - content: ( -
{ - try { - await resetPassword(user.id, values.newPassword); - messageApi.success('密码重置成功'); - Modal.destroyAll(); - } catch (error: unknown) { - messageApi.error(getErrorMessage(error, '密码重置失败')); - } - }} - > - - - -
- ), - okText: '重置', - cancelText: '取消', - onOk: () => - new Promise((resolve) => { - const formElement = document.getElementById('resetPasswordForm') as HTMLFormElement | null; - if (formElement) { - formElement.dispatchEvent(new Event('submit', {cancelable: true, bubbles: true})); - } - resolve(undefined); - }), - }); - }; - - const handleModalOk = async () => { - try { - const values = await form.validateFields(); - const nickname = values.nickname?.trim(); - - if (!nickname) { - messageApi.warning('昵称不能为空'); - return; - } - - const isEdit = Boolean(editingUser); - let request: Promise | null = null; - - if (editingUser) { - if (nickname === editingUser.nickname) { - messageApi.info('昵称未发生变化'); - return; - } - - const updateData: UpdateUserRequest = {nickname}; - request = updateUser(editingUser.id, updateData); - } else { - const username = values.username.trim(); - if (!username) { - messageApi.warning('用户名不能为空'); - return; - } - - const createData: CreateUserRequest = { - username, - nickname, - password: values.password, - }; - request = createUser(createData); - } - - if (!request) { - return; - } - - setSubmitting(true); - await request; - messageApi.success(isEdit ? '更新成功' : '创建成功'); - - setIsModalVisible(false); - form.resetFields(); - actionRef.current?.reload(); - } catch (error: unknown) { - if (typeof error === 'object' && error !== null && 'errorFields' in error) { - return; - } - messageApi.error(getErrorMessage(error, '操作失败')); - } finally { - setSubmitting(false); - } - }; - - const columns: ProColumns[] = [ - { - title: '用户名', - dataIndex: 'username', - key: 'username', - render: (text) => {text}, - }, - { - title: '昵称', - dataIndex: 'nickname', - key: 'nickname', - hideInSearch: true, - render: (text) => {text || '未设置'}, - }, - { - title: '创建时间', - dataIndex: 'createdAt', - key: 'createdAt', - hideInSearch: true, - render: (value: number) => ( - {dayjs(value).format('YYYY-MM-DD HH:mm')} - ), - width: 180, - }, - { - title: '最近更新', - dataIndex: 'updatedAt', - key: 'updatedAt', - hideInSearch: true, - render: (value: number) => ( - {dayjs(value).format('YYYY-MM-DD HH:mm')} - ), - width: 180, - }, - { - title: '操作', - key: 'action', - valueType: 'option', - width: 200, - render: (_, record) => [ - , - , - handleDelete(record.id)} - okText="确定" - cancelText="取消" - > - - , - ], - }, - ]; - - return ( -
- {/* 页面头部 */} - , - onClick: () => actionRef.current?.reload(), - }, - { - key: 'create', - label: '新建用户', - icon: , - type: 'primary', - onClick: handleCreate, - }, - ]} - /> - - - - {/* 用户列表 */} - - actionRef={actionRef} - rowKey="id" - search={{labelWidth: 80}} - columns={columns} - pagination={{ - defaultPageSize: 10, - showSizeChanger: true, - }} - options={false} - request={async (params) => { - const {current = 1, pageSize = 10, username} = params; - try { - const response = await listUsers(current, pageSize, username); - const items = response.data.items || []; - return { - data: items, - success: true, - total: response.data.total, - }; - } catch (error: unknown) { - messageApi.error(getErrorMessage(error, '获取用户列表失败')); - return { - data: [], - success: false, - }; - } - }} - /> - - {/* 新建/编辑用户弹窗 */} - { - setIsModalVisible(false); - form.resetFields(); - }} - okText={editingUser ? '保存' : '创建'} - cancelText="取消" - confirmLoading={submitting} - destroyOnClose - > -
- - - - - - - - - {!editingUser && ( - - - - )} -
-
-
- ); -}; - -export default UserList; diff --git a/web/src/router/index.tsx b/web/src/router/index.tsx index 478935b..24370a6 100644 --- a/web/src/router/index.tsx +++ b/web/src/router/index.tsx @@ -1,14 +1,15 @@ -import { createBrowserRouter, Navigate } from 'react-router-dom'; -import { lazy, Suspense, type ComponentType, type LazyExoticComponent } from 'react'; +import {createBrowserRouter, Navigate} from 'react-router-dom'; +import {type ComponentType, lazy, type LazyExoticComponent, Suspense} from 'react'; import PrivateRoute from '../components/PrivateRoute'; const LoginPage = lazy(() => import('../pages/Login')); +const GitHubCallbackPage = lazy(() => import('../pages/Login/GitHubCallback')); +const OIDCCallbackPage = lazy(() => import('../pages/Login/OIDCCallback')); const AdminLayout = lazy(() => import('../pages/AdminLayout')); const AgentListPage = lazy(() => import('../pages/Agents/AgentList')); const AgentDetailPage = lazy(() => import('../pages/Agents/AgentDetail')); const AgentInstallPage = lazy(() => import('../pages/Agents/AgentInstall')); const AuditResultPage = lazy(() => import('../pages/Agents/AuditResult')); -const UserListPage = lazy(() => import('../pages/Users/UserList')); const ApiKeyListPage = lazy(() => import('../pages/ApiKeys/ApiKeyList')); const SettingsPage = lazy(() => import('../pages/Settings')); const ServerListPage = lazy(() => import('../pages/Public/ServerList')); @@ -24,8 +25,8 @@ const LoadingFallback = () => ( ); const lazyLoad = (Component: LazyExoticComponent>) => ( - }> - + }> + ); @@ -35,6 +36,14 @@ const router = createBrowserRouter([ path: '/login', element: lazyLoad(LoginPage), }, + { + path: '/github/callback', + element: lazyLoad(GitHubCallbackPage), + }, + { + path: '/oidc/callback', + element: lazyLoad(OIDCCallbackPage), + }, // 公开页面 - 不需要登录 { path: '/', @@ -63,7 +72,7 @@ const router = createBrowserRouter([ children: [ { index: true, - element: , + element: , }, { path: 'agents', @@ -89,10 +98,6 @@ const router = createBrowserRouter([ path: 'monitors', element: lazyLoad(MonitorListPage), }, - { - path: 'users', - element: lazyLoad(UserListPage), - }, { path: 'settings', element: lazyLoad(SettingsPage), diff --git a/web/src/types/index.ts b/web/src/types/index.ts index 2f1245d..8c718c1 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -1,10 +1,7 @@ -// 用户相关 +// 用户相关(简化版,仅用于登录) export interface User { - id: string; username: string; nickname: string; - createdAt: number; - updatedAt: number; } export interface LoginRequest { @@ -386,22 +383,6 @@ export interface LatestMetrics { temperature?: TemperatureMetric[]; // 温度传感器列表 } -// 用户管理相关 -export interface CreateUserRequest { - username: string; - nickname: string; - password: string; -} - -export interface UpdateUserRequest { - nickname?: string; -} - -export interface ChangePasswordRequest { - oldPassword: string; - newPassword: string; -} - // API Key 相关 export interface ApiKey { id: string;