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
master
veypi 3 weeks ago
parent be6e07404c
commit 0e8e72b7e7

@ -57,7 +57,7 @@ func listProviders(x *vigo.X) ([]ProviderInfo, error) {
type AuthorizeRequest struct { type AuthorizeRequest struct {
Provider string `json:"provider" src:"query" desc:"提供商: google/github/wechat"` Provider string `json:"provider" src:"query" desc:"提供商: google/github/wechat"`
Redirect string `json:"redirect" src:"query" desc:"登录成功后重定向地址"` Redirect string `json:"redirect" src:"query" desc:"登录成功后重定向地址"`
BindMode bool `json:"bind_mode" src:"query" desc:"是否为绑定模式"` BindMode *bool `json:"bind_mode" src:"query" desc:"是否为绑定模式"`
} }
// AuthorizeResponse 授权响应 // 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) userID := baseauth.VBaseAuth.UserID(x)
if userID == "" { if userID == "" {
return nil, vigo.ErrUnauthorized.WithString("login required for bind mode") return nil, vigo.ErrUnauthorized.WithString("login required for bind mode")
@ -109,10 +109,10 @@ func authorizeThirdParty(x *vigo.X, req *AuthorizeRequest) (*AuthorizeResponse,
// CallbackRequest 第三方登录回调请求 // CallbackRequest 第三方登录回调请求
type CallbackRequest struct { type CallbackRequest struct {
Provider string `json:"provider" src:"path" desc:"提供商"` Provider string `json:"provider" src:"path" desc:"提供商"`
Code string `json:"code" src:"query" desc:"授权码"` Code string `json:"code" src:"query" desc:"授权码"`
State string `json:"state" src:"query" desc:"状态值"` State string `json:"state" src:"query" desc:"状态值"`
Error string `json:"error" src:"query" desc:"错误信息"` Error *string `json:"error" src:"query" desc:"错误信息"`
} }
// CallbackResponse 回调响应 // CallbackResponse 回调响应
@ -133,8 +133,8 @@ type CallbackResponse struct {
// callbackThirdParty 处理第三方登录回调 // callbackThirdParty 处理第三方登录回调
func callbackThirdParty(x *vigo.X, req *CallbackRequest) (*CallbackResponse, error) { func callbackThirdParty(x *vigo.X, req *CallbackRequest) (*CallbackResponse, error) {
if req.Error != "" { if req.Error != nil && *req.Error != "" {
return nil, vigo.ErrInvalidArg.WithString("oauth error: " + req.Error) return nil, vigo.ErrInvalidArg.WithString("oauth error: " + *req.Error)
} }
if req.Code == "" || req.State == "" { if req.Code == "" || req.State == "" {
@ -237,10 +237,10 @@ func bindThirdParty(x *vigo.X, req *BindRequest) (*AuthResponse, error) {
// BindWithRegisterRequest 绑定并注册新账号(可选功能) // BindWithRegisterRequest 绑定并注册新账号(可选功能)
type BindWithRegisterRequest struct { type BindWithRegisterRequest struct {
TempToken string `json:"temp_token" src:"json" desc:"临时绑定令牌"` TempToken string `json:"temp_token" src:"json" desc:"临时绑定令牌"`
Username string `json:"username" src:"json" desc:"用户名"` Username string `json:"username" src:"json" desc:"用户名"`
Email string `json:"email" src:"json" desc:"邮箱"` Email *string `json:"email" src:"json" desc:"邮箱"`
Phone string `json:"phone" src:"json" desc:"手机号"` Phone *string `json:"phone" src:"json" desc:"手机号"`
} }
// bindWithRegister 绑定并创建新账号 // bindWithRegister 绑定并创建新账号
@ -259,8 +259,8 @@ func bindWithRegister(x *vigo.X, req *BindWithRegisterRequest) (*AuthResponse, e
} }
// 检查邮箱是否已存在 // 检查邮箱是否已存在
if req.Email != "" { if req.Email != nil && *req.Email != "" {
cfg.DB().Model(&models.User{}).Where("email = ?", req.Email).Count(&count) cfg.DB().Model(&models.User{}).Where("email = ?", *req.Email).Count(&count)
if count > 0 { if count > 0 {
return nil, vigo.ErrInvalidArg.WithString("email already exists") return nil, vigo.ErrInvalidArg.WithString("email already exists")
} }
@ -270,20 +270,11 @@ func bindWithRegister(x *vigo.X, req *BindWithRegisterRequest) (*AuthResponse, e
randomPassword := generateRandomPassword(16) randomPassword := generateRandomPassword(16)
hashedPassword, _ := crypto.HashPassword(randomPassword, 12) 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{ user := &models.User{
Username: req.Username, Username: req.Username,
Password: hashedPassword, Password: hashedPassword,
Email: email, Email: req.Email,
Phone: phone, Phone: req.Phone,
Nickname: userInfo.Name, Nickname: userInfo.Name,
Avatar: userInfo.Avatar, Avatar: userInfo.Avatar,
Status: models.UserStatusActive, Status: models.UserStatusActive,

@ -14,12 +14,37 @@ import (
"github.com/veypi/vbase/api/user" "github.com/veypi/vbase/api/user"
"github.com/veypi/vbase/api/verification" "github.com/veypi/vbase/api/verification"
"github.com/veypi/vbase/auth" "github.com/veypi/vbase/auth"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo" "github.com/veypi/vigo"
"github.com/veypi/vigo/contrib/common" "github.com/veypi/vigo/contrib/common"
) )
var Router = vigo.NewRouter() 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() { func init() {
// 注册全局中间件 // 注册全局中间件
Router.Use(auth.VBaseAuth.Login()) Router.Use(auth.VBaseAuth.Login())
@ -37,8 +62,53 @@ func init() {
Router.Extend("/settings", settings.Router) Router.Extend("/settings", settings.Router)
Router.Extend("/verification", verification.Router) Router.Extend("/verification", verification.Router)
// 公开信息接口(不需要登录)
Router.Get("/info", vigo.SkipBefore, "获取公开配置信息", getPublicInfo)
// 404 处理 // 404 处理
Router.Any("/**", vigo.SkipBefore, "拦截未注册的api请求返回404", func(x *vigo.X) error { Router.Any("/**", vigo.SkipBefore, "拦截未注册的api请求返回404", func(x *vigo.X) error {
return vigo.ErrNotFound 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
}

@ -13,8 +13,8 @@ import (
) )
type ListClientsRequest struct { type ListClientsRequest struct {
Page int `json:"page" src:"query" default:"1"` Page int `json:"page" src:"query" default:"1" desc:"页码"`
PageSize int `json:"page_size" src:"query" default:"20"` PageSize int `json:"page_size" src:"query" default:"20" desc:"每页数量"`
} }
type ListClientsResponse struct { type ListClientsResponse struct {

@ -14,7 +14,7 @@ import (
// DeleteRequest 删除请求 // DeleteRequest 删除请求
type DeleteRequest struct { type DeleteRequest struct {
Code string `src:"path" desc:"提供商代码"` Code string `src:"path@code" desc:"提供商代码"`
} }
// del 删除 OAuth 提供商 // del 删除 OAuth 提供商

@ -14,7 +14,7 @@ import (
// GetRequest 获取详情请求 // GetRequest 获取详情请求
type GetRequest struct { type GetRequest struct {
Code string `src:"path" desc:"提供商代码"` Code string `src:"path@code" desc:"提供商代码"`
} }
// get 获取 OAuth 提供商详情 // get 获取 OAuth 提供商详情

@ -14,7 +14,7 @@ import (
// UpdateRequest 更新请求 // UpdateRequest 更新请求
type UpdateRequest struct { type UpdateRequest struct {
Code string `src:"path" desc:"提供商代码"` Code string `src:"path@code" desc:"提供商代码"`
models.OAuthProvider models.OAuthProvider
} }

@ -15,11 +15,11 @@ import (
) )
type TokenRequest struct { type TokenRequest struct {
GrantType string `json:"grant_type" src:"form" desc:"授权类型"` GrantType string `json:"grant_type" src:"form" desc:"授权类型"`
Code string `json:"code" src:"form" desc:"授权码"` Code *string `json:"code,omitempty" src:"form" desc:"授权码"`
RefreshToken string `json:"refresh_token" src:"form" desc:"刷新令牌"` RefreshToken *string `json:"refresh_token,omitempty" src:"form" desc:"刷新令牌"`
ClientID string `json:"client_id" src:"form" desc:"客户端ID"` ClientID string `json:"client_id" src:"form" desc:"客户端ID"`
ClientSecret string `json:"client_secret" src:"form" desc:"客户端密钥"` ClientSecret string `json:"client_secret" src:"form" desc:"客户端密钥"`
} }
type TokenResponse struct { 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 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") 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) userID := authData["user_id"].(string)
scope := authData["scope"].(string) scope := authData["scope"].(string)
@ -89,8 +93,11 @@ func handleAuthorizationCode(req *TokenRequest) (*TokenResponse, error) {
func handleRefreshToken(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 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 return nil, vigo.ErrTokenInvalid
} }

@ -10,7 +10,7 @@ type CreateReq struct {
Scope string `json:"scope" src:"json" default:"vb" desc:"Scope"` Scope string `json:"scope" src:"json" default:"vb" desc:"Scope"`
Code string `json:"code" src:"json" desc:"Role Code"` Code string `json:"code" src:"json" desc:"Role Code"`
Name string `json:"name" src:"json" desc:"Role Name"` 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) { 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{ role := &models.Role{
Scope: req.Scope, Scope: req.Scope,
Code: req.Code, Code: req.Code,
Name: req.Name, Name: req.Name,
Description: req.Description, IsSystem: false, // Default to false for user created roles
IsSystem: false, // Default to false for user created roles Status: 1,
Status: 1, }
if req.Description != nil {
role.Description = *req.Description
} }
if err := cfg.DB().Create(role).Error; err != nil { if err := cfg.DB().Create(role).Error; err != nil {

@ -7,8 +7,8 @@ import (
) )
type ListReq struct { type ListReq struct {
Page int `json:"page" src:"query" default:"1"` Page int `json:"page" src:"query" default:"1" desc:"页码"`
PageSize int `json:"page_size" src:"query" default:"20"` PageSize int `json:"page_size" src:"query" default:"20" desc:"每页数量"`
Scope *string `json:"scope" src:"query" desc:"Scope"` Scope *string `json:"scope" src:"query" desc:"Scope"`
Keyword *string `json:"keyword" src:"query" desc:"Search Keyword"` Keyword *string `json:"keyword" src:"query" desc:"Search Keyword"`
} }

@ -14,7 +14,7 @@ import (
// ListRequest 列表请求 // ListRequest 列表请求
type ListRequest struct { type ListRequest struct {
Category string `src:"query" desc:"分类过滤"` Category *string `src:"query" desc:"分类过滤"`
} }
// ListResponse 列表响应 // ListResponse 列表响应
@ -29,7 +29,7 @@ func list(x *vigo.X, req *ListRequest) (*ListResponse, error) {
var settings []models.Setting var settings []models.Setting
query := db.Order("category, `key`") query := db.Order("category, `key`")
if req.Category != "" { if req.Category != nil && *req.Category != "" {
query = query.Where("category = ?", req.Category) query = query.Where("category = ?", req.Category)
} }

@ -15,13 +15,13 @@ import (
// UpdateItem 更新项 // UpdateItem 更新项
type UpdateItem struct { type UpdateItem struct {
Key string `json:"key"` Key string `json:"key" src:"json" desc:"设置键"`
Value string `json:"value"` Value string `json:"value" src:"json" desc:"设置值"`
} }
// UpdateRequest 更新请求 // UpdateRequest 更新请求
type UpdateRequest struct { type UpdateRequest struct {
Settings []UpdateItem `json:"settings"` Settings []UpdateItem `json:"settings" src:"json" desc:"设置列表"`
} }
// UpdateResponse 更新响应 // UpdateResponse 更新响应

@ -21,7 +21,7 @@ type CreateRequest struct {
Email string `json:"email,omitempty" src:"json" desc:"邮箱"` Email string `json:"email,omitempty" src:"json" desc:"邮箱"`
Phone string `json:"phone,omitempty" src:"json" desc:"手机号"` Phone string `json:"phone,omitempty" src:"json" desc:"手机号"`
Nickname string `json:"nickname,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 创建用户 // create 创建用户

@ -14,8 +14,8 @@ import (
// ListRequest 用户列表请求 // ListRequest 用户列表请求
type ListRequest struct { type ListRequest struct {
Page int `json:"page" src:"query" default:"1"` Page int `json:"page" src:"query" default:"1" desc:"页码"`
PageSize int `json:"page_size" src:"query" default:"20"` PageSize int `json:"page_size" src:"query" default:"20" desc:"每页数量"`
Keyword *string `json:"keyword" src:"query" desc:"搜索关键词"` Keyword *string `json:"keyword" src:"query" desc:"搜索关键词"`
Status *int `json:"status" src:"query" desc:"状态筛选"` Status *int `json:"status" src:"query" desc:"状态筛选"`
} }

Loading…
Cancel
Save