From 0e8e72b7e71b14fb8130dff075459714c12f8116 Mon Sep 17 00:00:00 2001 From: veypi Date: Thu, 26 Feb 2026 01:43:56 +0800 Subject: [PATCH] refactor(api): Improve API parameter handling and add public info endpoint - Change BindMode from bool to *bool in thirdparty auth for proper optional handling - Change Error field from string to *string in OAuth callback request - Change Email and Phone to *string pointers in bind with register request - Add public /api/info endpoint for frontend configuration - Update OAuth token request to use pointers for optional code and refresh_token - Add desc tags to various request struct fields for API documentation - Fix path parameter binding with explicit @code suffix for OAuth providers - Change Description field to *string pointer in role creation - Change Category field to *string pointer in settings list --- api/auth/thirdparty.go | 41 ++++++++------------ api/init.go | 70 +++++++++++++++++++++++++++++++++++ api/oauth/client.go | 4 +- api/oauth/providers/del.go | 2 +- api/oauth/providers/get.go | 2 +- api/oauth/providers/update.go | 2 +- api/oauth/token.go | 23 ++++++++---- api/role/create.go | 16 ++++---- api/role/list.go | 4 +- api/settings/list.go | 4 +- api/settings/update.go | 6 +-- api/user/create.go | 2 +- api/user/list.go | 4 +- 13 files changed, 125 insertions(+), 55 deletions(-) diff --git a/api/auth/thirdparty.go b/api/auth/thirdparty.go index eaf69b0..a410502 100644 --- a/api/auth/thirdparty.go +++ b/api/auth/thirdparty.go @@ -57,7 +57,7 @@ func listProviders(x *vigo.X) ([]ProviderInfo, error) { type AuthorizeRequest struct { Provider string `json:"provider" src:"query" desc:"提供商: google/github/wechat"` Redirect string `json:"redirect" src:"query" desc:"登录成功后重定向地址"` - BindMode bool `json:"bind_mode" src:"query" desc:"是否为绑定模式"` + BindMode *bool `json:"bind_mode" src:"query" desc:"是否为绑定模式"` } // AuthorizeResponse 授权响应 @@ -82,7 +82,7 @@ func authorizeThirdParty(x *vigo.X, req *AuthorizeRequest) (*AuthorizeResponse, } // 如果是绑定模式,需要当前用户登录 - if req.BindMode { + if req.BindMode != nil && *req.BindMode { userID := baseauth.VBaseAuth.UserID(x) if userID == "" { return nil, vigo.ErrUnauthorized.WithString("login required for bind mode") @@ -109,10 +109,10 @@ func authorizeThirdParty(x *vigo.X, req *AuthorizeRequest) (*AuthorizeResponse, // CallbackRequest 第三方登录回调请求 type CallbackRequest struct { - Provider string `json:"provider" src:"path" desc:"提供商"` - Code string `json:"code" src:"query" desc:"授权码"` - State string `json:"state" src:"query" desc:"状态值"` - Error string `json:"error" src:"query" desc:"错误信息"` + Provider string `json:"provider" src:"path" desc:"提供商"` + Code string `json:"code" src:"query" desc:"授权码"` + State string `json:"state" src:"query" desc:"状态值"` + Error *string `json:"error" src:"query" desc:"错误信息"` } // CallbackResponse 回调响应 @@ -133,8 +133,8 @@ type CallbackResponse struct { // callbackThirdParty 处理第三方登录回调 func callbackThirdParty(x *vigo.X, req *CallbackRequest) (*CallbackResponse, error) { - if req.Error != "" { - return nil, vigo.ErrInvalidArg.WithString("oauth error: " + req.Error) + if req.Error != nil && *req.Error != "" { + return nil, vigo.ErrInvalidArg.WithString("oauth error: " + *req.Error) } if req.Code == "" || req.State == "" { @@ -237,10 +237,10 @@ func bindThirdParty(x *vigo.X, req *BindRequest) (*AuthResponse, error) { // BindWithRegisterRequest 绑定并注册新账号(可选功能) type BindWithRegisterRequest struct { - TempToken string `json:"temp_token" src:"json" desc:"临时绑定令牌"` - Username string `json:"username" src:"json" desc:"用户名"` - Email string `json:"email" src:"json" desc:"邮箱"` - Phone string `json:"phone" src:"json" desc:"手机号"` + TempToken string `json:"temp_token" src:"json" desc:"临时绑定令牌"` + Username string `json:"username" src:"json" desc:"用户名"` + Email *string `json:"email" src:"json" desc:"邮箱"` + Phone *string `json:"phone" src:"json" desc:"手机号"` } // bindWithRegister 绑定并创建新账号 @@ -259,8 +259,8 @@ func bindWithRegister(x *vigo.X, req *BindWithRegisterRequest) (*AuthResponse, e } // 检查邮箱是否已存在 - if req.Email != "" { - cfg.DB().Model(&models.User{}).Where("email = ?", req.Email).Count(&count) + if req.Email != nil && *req.Email != "" { + cfg.DB().Model(&models.User{}).Where("email = ?", *req.Email).Count(&count) if count > 0 { return nil, vigo.ErrInvalidArg.WithString("email already exists") } @@ -270,20 +270,11 @@ func bindWithRegister(x *vigo.X, req *BindWithRegisterRequest) (*AuthResponse, e randomPassword := generateRandomPassword(16) hashedPassword, _ := crypto.HashPassword(randomPassword, 12) - var email *string - if req.Email != "" { - email = &req.Email - } - var phone *string - if req.Phone != "" { - phone = &req.Phone - } - user := &models.User{ Username: req.Username, Password: hashedPassword, - Email: email, - Phone: phone, + Email: req.Email, + Phone: req.Phone, Nickname: userInfo.Name, Avatar: userInfo.Avatar, Status: models.UserStatusActive, diff --git a/api/init.go b/api/init.go index d04067e..fd529ef 100644 --- a/api/init.go +++ b/api/init.go @@ -14,12 +14,37 @@ import ( "github.com/veypi/vbase/api/user" "github.com/veypi/vbase/api/verification" "github.com/veypi/vbase/auth" + "github.com/veypi/vbase/cfg" + "github.com/veypi/vbase/models" "github.com/veypi/vigo" "github.com/veypi/vigo/contrib/common" ) var Router = vigo.NewRouter() +// PublicInfoResponse 公开信息响应 +// 不需要登录即可访问,用于前端初始化 + type PublicInfoResponse struct { + AppName string `json:"app_name"` + AppID string `json:"app_id"` + OAuthProviders []OAuthProviderInfo `json:"oauth_providers"` + LoginMethods []string `json:"login_methods"` + PasswordFields []string `json:"password_fields"` + RegRequireEmail bool `json:"reg_require_email"` + RegRequirePhone bool `json:"reg_require_phone"` + CaptchaEnabled bool `json:"captcha_enabled"` + EmailEnabled bool `json:"email_enabled"` + SMSEnabled bool `json:"sms_enabled"` +} + +// OAuthProviderInfo OAuth提供商公开信息 + type OAuthProviderInfo struct { + Code string `json:"code"` + Name string `json:"name"` + Icon string `json:"icon"` + Enabled bool `json:"enabled"` +} + func init() { // 注册全局中间件 Router.Use(auth.VBaseAuth.Login()) @@ -37,8 +62,53 @@ func init() { Router.Extend("/settings", settings.Router) Router.Extend("/verification", verification.Router) + // 公开信息接口(不需要登录) + Router.Get("/info", vigo.SkipBefore, "获取公开配置信息", getPublicInfo) + // 404 处理 Router.Any("/**", vigo.SkipBefore, "拦截未注册的api请求,返回404", func(x *vigo.X) error { return vigo.ErrNotFound }) } + +// getPublicInfo 获取公开配置信息 + func getPublicInfo(x *vigo.X) (*PublicInfoResponse, error) { + resp := &PublicInfoResponse{} + + // 应用配置 + if name, err := models.GetSetting(models.SettingAppName); err == nil { + resp.AppName = name + } + if id, err := models.GetSetting(models.SettingAppID); err == nil { + resp.AppID = id + } + + // 登录注册配置 + if err := models.GetSettingJSON(models.SettingAuthLoginMethods, &resp.LoginMethods); err != nil || len(resp.LoginMethods) == 0 { + resp.LoginMethods = []string{"password"} + } + if err := models.GetSettingJSON(models.SettingAuthPasswordFields, &resp.PasswordFields); err != nil || len(resp.PasswordFields) == 0 { + resp.PasswordFields = []string{"username"} + } + + resp.RegRequireEmail, _ = models.GetSettingBool(models.SettingAuthRegRequireEmail) + resp.RegRequirePhone, _ = models.GetSettingBool(models.SettingAuthRegRequirePhone) + resp.CaptchaEnabled, _ = models.GetSettingBool(models.SettingSecurityCaptchaEnabled) + resp.EmailEnabled, _ = models.GetSettingBool(models.SettingEmailEnabled) + resp.SMSEnabled, _ = models.GetSettingBool(models.SettingSMSEnabled) + + // 获取启用的OAuth提供商 + var providers []models.OAuthProvider + if err := cfg.DB().Where("enabled = ?", true).Order("sort_order").Find(&providers).Error; err == nil { + for _, p := range providers { + resp.OAuthProviders = append(resp.OAuthProviders, OAuthProviderInfo{ + Code: p.Code, + Name: p.Name, + Icon: p.Icon, + Enabled: p.Enabled, + }) + } + } + + return resp, nil +} diff --git a/api/oauth/client.go b/api/oauth/client.go index 0765561..1d0fe15 100644 --- a/api/oauth/client.go +++ b/api/oauth/client.go @@ -13,8 +13,8 @@ import ( ) type ListClientsRequest struct { - Page int `json:"page" src:"query" default:"1"` - PageSize int `json:"page_size" src:"query" default:"20"` + Page int `json:"page" src:"query" default:"1" desc:"页码"` + PageSize int `json:"page_size" src:"query" default:"20" desc:"每页数量"` } type ListClientsResponse struct { diff --git a/api/oauth/providers/del.go b/api/oauth/providers/del.go index e47fb81..c40353e 100644 --- a/api/oauth/providers/del.go +++ b/api/oauth/providers/del.go @@ -14,7 +14,7 @@ import ( // DeleteRequest 删除请求 type DeleteRequest struct { - Code string `src:"path" desc:"提供商代码"` + Code string `src:"path@code" desc:"提供商代码"` } // del 删除 OAuth 提供商 diff --git a/api/oauth/providers/get.go b/api/oauth/providers/get.go index c0add27..ee44bb7 100644 --- a/api/oauth/providers/get.go +++ b/api/oauth/providers/get.go @@ -14,7 +14,7 @@ import ( // GetRequest 获取详情请求 type GetRequest struct { - Code string `src:"path" desc:"提供商代码"` + Code string `src:"path@code" desc:"提供商代码"` } // get 获取 OAuth 提供商详情 diff --git a/api/oauth/providers/update.go b/api/oauth/providers/update.go index f204e3d..2d192d9 100644 --- a/api/oauth/providers/update.go +++ b/api/oauth/providers/update.go @@ -14,7 +14,7 @@ import ( // UpdateRequest 更新请求 type UpdateRequest struct { - Code string `src:"path" desc:"提供商代码"` + Code string `src:"path@code" desc:"提供商代码"` models.OAuthProvider } diff --git a/api/oauth/token.go b/api/oauth/token.go index dba0585..d5a87fb 100644 --- a/api/oauth/token.go +++ b/api/oauth/token.go @@ -15,11 +15,11 @@ import ( ) type TokenRequest struct { - GrantType string `json:"grant_type" src:"form" desc:"授权类型"` - Code string `json:"code" src:"form" desc:"授权码"` - RefreshToken string `json:"refresh_token" src:"form" desc:"刷新令牌"` - ClientID string `json:"client_id" src:"form" desc:"客户端ID"` - ClientSecret string `json:"client_secret" src:"form" desc:"客户端密钥"` + GrantType string `json:"grant_type" src:"form" desc:"授权类型"` + Code *string `json:"code,omitempty" src:"form" desc:"授权码"` + RefreshToken *string `json:"refresh_token,omitempty" src:"form" desc:"刷新令牌"` + ClientID string `json:"client_id" src:"form" desc:"客户端ID"` + ClientSecret string `json:"client_secret" src:"form" desc:"客户端密钥"` } type TokenResponse struct { @@ -49,13 +49,17 @@ func handleAuthorizationCode(req *TokenRequest) (*TokenResponse, error) { } // 验证授权码 + if req.Code == nil { + return nil, vigo.ErrInvalidArg.WithString("code is required for authorization_code grant") + } + code := *req.Code var authData map[string]any - if err := cache.GetObject(cache.OAuthCodeKey(req.Code), &authData); err != nil { + if err := cache.GetObject(cache.OAuthCodeKey(code), &authData); err != nil { return nil, vigo.ErrUnauthorized.WithString("invalid or expired code") } // 删除已使用的授权码 - cache.Delete(cache.OAuthCodeKey(req.Code)) + cache.Delete(cache.OAuthCodeKey(code)) userID := authData["user_id"].(string) scope := authData["scope"].(string) @@ -89,8 +93,11 @@ func handleAuthorizationCode(req *TokenRequest) (*TokenResponse, error) { func handleRefreshToken(req *TokenRequest) (*TokenResponse, error) { // 查找刷新令牌 + if req.RefreshToken == nil { + return nil, vigo.ErrInvalidArg.WithString("refresh_token is required for refresh_token grant") + } var token models.OAuthToken - if err := cfg.DB().First(&token, "refresh_token = ?", req.RefreshToken).Error; err != nil { + if err := cfg.DB().First(&token, "refresh_token = ?", *req.RefreshToken).Error; err != nil { return nil, vigo.ErrTokenInvalid } diff --git a/api/role/create.go b/api/role/create.go index 185d6f5..a366b9a 100644 --- a/api/role/create.go +++ b/api/role/create.go @@ -10,7 +10,7 @@ type CreateReq struct { Scope string `json:"scope" src:"json" default:"vb" desc:"Scope"` Code string `json:"code" src:"json" desc:"Role Code"` Name string `json:"name" src:"json" desc:"Role Name"` - Description string `json:"description" src:"json" desc:"Role Description"` + Description *string `json:"description,omitempty" src:"json" desc:"Role Description"` } func create(x *vigo.X, req *CreateReq) (*models.Role, error) { @@ -24,12 +24,14 @@ func create(x *vigo.X, req *CreateReq) (*models.Role, error) { } role := &models.Role{ - Scope: req.Scope, - Code: req.Code, - Name: req.Name, - Description: req.Description, - IsSystem: false, // Default to false for user created roles - Status: 1, + Scope: req.Scope, + Code: req.Code, + Name: req.Name, + IsSystem: false, // Default to false for user created roles + Status: 1, + } + if req.Description != nil { + role.Description = *req.Description } if err := cfg.DB().Create(role).Error; err != nil { diff --git a/api/role/list.go b/api/role/list.go index bd8cfe7..e24a4fd 100644 --- a/api/role/list.go +++ b/api/role/list.go @@ -7,8 +7,8 @@ import ( ) type ListReq struct { - Page int `json:"page" src:"query" default:"1"` - PageSize int `json:"page_size" src:"query" default:"20"` + Page int `json:"page" src:"query" default:"1" desc:"页码"` + PageSize int `json:"page_size" src:"query" default:"20" desc:"每页数量"` Scope *string `json:"scope" src:"query" desc:"Scope"` Keyword *string `json:"keyword" src:"query" desc:"Search Keyword"` } diff --git a/api/settings/list.go b/api/settings/list.go index b8ba388..c811abf 100644 --- a/api/settings/list.go +++ b/api/settings/list.go @@ -14,7 +14,7 @@ import ( // ListRequest 列表请求 type ListRequest struct { - Category string `src:"query" desc:"分类过滤"` + Category *string `src:"query" desc:"分类过滤"` } // ListResponse 列表响应 @@ -29,7 +29,7 @@ func list(x *vigo.X, req *ListRequest) (*ListResponse, error) { var settings []models.Setting query := db.Order("category, `key`") - if req.Category != "" { + if req.Category != nil && *req.Category != "" { query = query.Where("category = ?", req.Category) } diff --git a/api/settings/update.go b/api/settings/update.go index 06113d2..865c1c2 100644 --- a/api/settings/update.go +++ b/api/settings/update.go @@ -15,13 +15,13 @@ import ( // UpdateItem 更新项 type UpdateItem struct { - Key string `json:"key"` - Value string `json:"value"` + Key string `json:"key" src:"json" desc:"设置键"` + Value string `json:"value" src:"json" desc:"设置值"` } // UpdateRequest 更新请求 type UpdateRequest struct { - Settings []UpdateItem `json:"settings"` + Settings []UpdateItem `json:"settings" src:"json" desc:"设置列表"` } // UpdateResponse 更新响应 diff --git a/api/user/create.go b/api/user/create.go index 70fcab6..719ebb0 100644 --- a/api/user/create.go +++ b/api/user/create.go @@ -21,7 +21,7 @@ type CreateRequest struct { Email string `json:"email,omitempty" src:"json" desc:"邮箱"` Phone string `json:"phone,omitempty" src:"json" desc:"手机号"` Nickname string `json:"nickname,omitempty" src:"json" desc:"昵称"` - Status int `json:"status" src:"json" default:"1"` + Status int `json:"status" src:"json" default:"1" desc:"用户状态"` } // create 创建用户 diff --git a/api/user/list.go b/api/user/list.go index 3b7caaa..6e53fd8 100644 --- a/api/user/list.go +++ b/api/user/list.go @@ -14,8 +14,8 @@ import ( // ListRequest 用户列表请求 type ListRequest struct { - Page int `json:"page" src:"query" default:"1"` - PageSize int `json:"page_size" src:"query" default:"20"` + Page int `json:"page" src:"query" default:"1" desc:"页码"` + PageSize int `json:"page_size" src:"query" default:"20" desc:"每页数量"` Keyword *string `json:"keyword" src:"query" desc:"搜索关键词"` Status *int `json:"status" src:"query" desc:"状态筛选"` }