You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
OneAuth/internal/api/oauth/client.go

400 lines
12 KiB
Go

package oauth
import (
"crypto/rand"
"encoding/hex"
"strings"
"github.com/veypi/vbase/internal/api/middleware"
"github.com/veypi/vbase/internal/model"
"github.com/veypi/vigo"
)
// ClientRequest 客户端请求
type ClientRequest struct {
Name string `json:"name" src:"json" desc:"应用名称"`
Description string `json:"description" src:"json" desc:"应用描述"`
RedirectURIs []string `json:"redirect_uris" src:"json" desc:"回调地址列表"`
GrantTypes []string `json:"grant_types" src:"json" desc:"授权类型"`
ResponseTypes []string `json:"response_types" src:"json" desc:"响应类型"`
AllowedScopes []string `json:"allowed_scopes" src:"json" desc:"允许的权限范围"`
TokenExpiry int `json:"token_expiry" src:"json" desc:"Token有效期(秒)"`
RefreshExpiry int `json:"refresh_expiry" src:"json" desc:"RefreshToken有效期(秒)"`
OrgID string `json:"org_id" src:"json" desc:"所属组织ID(可选)"`
}
// ClientResponse 客户端响应
type ClientResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
RedirectURIs []string `json:"redirect_uris"`
GrantTypes []string `json:"grant_types"`
ResponseTypes []string `json:"response_types"`
AllowedScopes []string `json:"allowed_scopes"`
TokenExpiry int `json:"token_expiry"`
RefreshExpiry int `json:"refresh_expiry"`
OwnerID string `json:"owner_id"`
OrgID string `json:"org_id,omitempty"`
Status int `json:"status"`
CreatedAt string `json:"created_at"`
}
// generateClientID 生成客户端ID
func generateClientID() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
// generateClientSecret 生成客户端密钥
func generateClientSecret() string {
b := make([]byte, 32)
rand.Read(b)
return hex.EncodeToString(b)
}
// CreateClient 创建OAuth客户端
func CreateClient(x *vigo.X, req *ClientRequest) (*ClientResponse, error) {
userID := middleware.CurrentUser(x)
if userID == "" {
return nil, vigo.ErrNotAuthorized
}
// 默认值
grantTypes := req.GrantTypes
if len(grantTypes) == 0 {
grantTypes = []string{model.GrantTypeAuthorizationCode, model.GrantTypeRefreshToken}
}
responseTypes := req.ResponseTypes
if len(responseTypes) == 0 {
responseTypes = []string{model.ResponseTypeCode}
}
allowedScopes := req.AllowedScopes
if len(allowedScopes) == 0 {
allowedScopes = []string{model.ScopeOpenID, model.ScopeProfile, model.ScopeEmail}
}
tokenExpiry := req.TokenExpiry
if tokenExpiry == 0 {
tokenExpiry = 3600
}
refreshExpiry := req.RefreshExpiry
if refreshExpiry == 0 {
refreshExpiry = 2592000 // 30天
}
client := &model.OAuthClient{
Name: req.Name,
Description: req.Description,
ClientID: generateClientID(),
ClientSecret: generateClientSecret(),
RedirectURIs: strings.Join(req.RedirectURIs, ","),
GrantTypes: strings.Join(grantTypes, ","),
ResponseTypes: strings.Join(responseTypes, ","),
AllowedScopes: strings.Join(allowedScopes, ","),
TokenExpiry: tokenExpiry,
RefreshExpiry: refreshExpiry,
OwnerID: userID,
Status: 1,
}
if req.OrgID != "" {
// 检查用户是否是组织成员
var member model.OrgMember
if err := model.DB.Where("org_id = ? AND user_id = ? AND status = ?", req.OrgID, userID, model.MemberStatusActive).First(&member).Error; err != nil {
return nil, vigo.ErrForbidden.WithString("you are not a member of this organization")
}
client.OrgID = req.OrgID
}
if err := model.DB.Create(client).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
return &ClientResponse{
ID: client.ID,
Name: client.Name,
Description: client.Description,
ClientID: client.ClientID,
ClientSecret: client.ClientSecret,
RedirectURIs: req.RedirectURIs,
GrantTypes: grantTypes,
ResponseTypes: responseTypes,
AllowedScopes: allowedScopes,
TokenExpiry: client.TokenExpiry,
RefreshExpiry: client.RefreshExpiry,
OwnerID: client.OwnerID,
OrgID: client.OrgID,
Status: client.Status,
CreatedAt: client.CreatedAt.Format("2006-01-02 15:04:05"),
}, nil
}
// ListClientsRequest 客户端列表请求
type ListClientsRequest struct {
Page int `json:"page" src:"query" default:"1" desc:"页码"`
PageSize int `json:"page_size" src:"query" default:"10" desc:"每页数量"`
OrgID string `json:"org_id" src:"query" desc:"组织ID筛选"`
}
// ListClientsResponse 客户端列表响应
type ListClientsResponse struct {
Items []ClientResponse `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
// ListClients 获取OAuth客户端列表
func ListClients(x *vigo.X, req *ListClientsRequest) (*ListClientsResponse, error) {
userID := middleware.CurrentUser(x)
if userID == "" {
return nil, vigo.ErrNotAuthorized
}
if req.Page < 1 {
req.Page = 1
}
if req.PageSize < 1 || req.PageSize > 100 {
req.PageSize = 10
}
query := model.DB.Model(&model.OAuthClient{}).Where("owner_id = ? OR org_id IN (SELECT org_id FROM org_members WHERE user_id = ? AND status = ?)", userID, userID, model.MemberStatusActive)
if req.OrgID != "" {
query = query.Where("org_id = ?", req.OrgID)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
var clients []model.OAuthClient
offset := (req.Page - 1) * req.PageSize
if err := query.Offset(offset).Limit(req.PageSize).Order("created_at DESC").Find(&clients).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
items := make([]ClientResponse, 0, len(clients))
for _, c := range clients {
items = append(items, toClientResponse(&c, false))
}
totalPages := int((total + int64(req.PageSize) - 1) / int64(req.PageSize))
return &ListClientsResponse{
Items: items,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
TotalPages: totalPages,
}, nil
}
// GetClientRequest 获取客户端请求
type GetClientRequest struct {
ID string `json:"id" src:"path@client_id" desc:"客户端ID"`
}
// GetClient 获取客户端详情
func GetClient(x *vigo.X, req *GetClientRequest) (*ClientResponse, error) {
userID := middleware.CurrentUser(x)
if userID == "" {
return nil, vigo.ErrNotAuthorized
}
var client model.OAuthClient
if err := model.DB.Where("client_id = ?", req.ID).First(&client).Error; err != nil {
return nil, vigo.ErrNotFound
}
// 检查权限
if client.OwnerID != userID {
// 检查是否是组织成员
if client.OrgID != "" {
var member model.OrgMember
if err := model.DB.Where("org_id = ? AND user_id = ? AND status = ?", client.OrgID, userID, model.MemberStatusActive).First(&member).Error; err != nil {
return nil, vigo.ErrForbidden
}
} else {
return nil, vigo.ErrForbidden
}
}
resp := toClientResponse(&client, false)
return &resp, nil
}
// UpdateClientRequest 更新客户端请求
type UpdateClientRequest struct {
ID string `json:"id" src:"path@client_id" desc:"客户端ID"`
Name *string `json:"name" src:"json" desc:"应用名称"`
Description *string `json:"description" src:"json" desc:"应用描述"`
RedirectURIs []string `json:"redirect_uris" src:"json" desc:"回调地址列表"`
AllowedScopes []string `json:"allowed_scopes" src:"json" desc:"允许的权限范围"`
TokenExpiry *int `json:"token_expiry" src:"json" desc:"Token有效期"`
RefreshExpiry *int `json:"refresh_expiry" src:"json" desc:"RefreshToken有效期"`
Status *int `json:"status" src:"json" desc:"状态"`
}
// UpdateClient 更新OAuth客户端
func UpdateClient(x *vigo.X, req *UpdateClientRequest) (*ClientResponse, error) {
userID := middleware.CurrentUser(x)
if userID == "" {
return nil, vigo.ErrNotAuthorized
}
var client model.OAuthClient
if err := model.DB.Where("client_id = ?", req.ID).First(&client).Error; err != nil {
return nil, vigo.ErrNotFound
}
// 检查权限
if client.OwnerID != userID {
if client.OrgID != "" {
var member model.OrgMember
if err := model.DB.Where("org_id = ? AND user_id = ? AND status = ?", client.OrgID, userID, model.MemberStatusActive).First(&member).Error; err != nil {
return nil, vigo.ErrForbidden
}
} else {
return nil, vigo.ErrForbidden
}
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.RedirectURIs != nil {
updates["redirect_uris"] = strings.Join(req.RedirectURIs, ",")
}
if req.AllowedScopes != nil {
updates["allowed_scopes"] = strings.Join(req.AllowedScopes, ",")
}
if req.TokenExpiry != nil {
updates["token_expiry"] = *req.TokenExpiry
}
if req.RefreshExpiry != nil {
updates["refresh_expiry"] = *req.RefreshExpiry
}
if req.Status != nil {
updates["status"] = *req.Status
}
if len(updates) > 0 {
if err := model.DB.Model(&client).Updates(updates).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
}
// 重新获取
model.DB.First(&client, "client_id = ?", req.ID)
resp := toClientResponse(&client, false)
return &resp, nil
}
// DeleteClientRequest 删除客户端请求
type DeleteClientRequest struct {
ID string `json:"id" src:"path@client_id" desc:"客户端ID"`
}
// DeleteClient 删除OAuth客户端
func DeleteClient(x *vigo.X, req *DeleteClientRequest) error {
userID := middleware.CurrentUser(x)
if userID == "" {
return vigo.ErrNotAuthorized
}
var client model.OAuthClient
if err := model.DB.Where("client_id = ?", req.ID).First(&client).Error; err != nil {
return vigo.ErrNotFound
}
// 检查权限
if client.OwnerID != userID {
if client.OrgID != "" {
var member model.OrgMember
if err := model.DB.Where("org_id = ? AND user_id = ? AND status = ?", client.OrgID, userID, model.MemberStatusActive).First(&member).Error; err != nil {
return vigo.ErrForbidden
}
} else {
return vigo.ErrForbidden
}
}
// 软删除
if err := model.DB.Delete(&client).Error; err != nil {
return vigo.ErrInternalServer.WithError(err)
}
return nil
}
// RegenerateSecretRequest 重新生成密钥请求
type RegenerateSecretRequest struct {
ID string `json:"id" src:"path@client_id" desc:"客户端ID"`
}
// RegenerateSecret 重新生成客户端密钥
func RegenerateSecret(x *vigo.X, req *RegenerateSecretRequest) (*ClientResponse, error) {
userID := middleware.CurrentUser(x)
if userID == "" {
return nil, vigo.ErrNotAuthorized
}
var client model.OAuthClient
if err := model.DB.Where("client_id = ?", req.ID).First(&client).Error; err != nil {
return nil, vigo.ErrNotFound
}
// 检查权限
if client.OwnerID != userID {
if client.OrgID != "" {
var member model.OrgMember
if err := model.DB.Where("org_id = ? AND user_id = ? AND status = ?", client.OrgID, userID, model.MemberStatusActive).First(&member).Error; err != nil {
return nil, vigo.ErrForbidden
}
} else {
return nil, vigo.ErrForbidden
}
}
// 生成新密钥
newSecret := generateClientSecret()
if err := model.DB.Model(&client).Update("client_secret", newSecret).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
resp := toClientResponse(&client, true)
return &resp, nil
}
// helper functions
func toClientResponse(c *model.OAuthClient, includeSecret bool) ClientResponse {
resp := ClientResponse{
ID: c.ID,
Name: c.Name,
Description: c.Description,
ClientID: c.ClientID,
RedirectURIs: strings.Split(c.RedirectURIs, ","),
GrantTypes: strings.Split(c.GrantTypes, ","),
ResponseTypes: strings.Split(c.ResponseTypes, ","),
AllowedScopes: strings.Split(c.AllowedScopes, ","),
TokenExpiry: c.TokenExpiry,
RefreshExpiry: c.RefreshExpiry,
OwnerID: c.OwnerID,
OrgID: c.OrgID,
Status: c.Status,
CreatedAt: c.CreatedAt.Format("2006-01-02 15:04:05"),
}
if includeSecret {
resp.ClientSecret = c.ClientSecret
}
return resp
}