mirror of https://github.com/veypi/OneAuth.git
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.
400 lines
12 KiB
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
|
|
}
|