Merge pull request #1157 from trheyi/main
Add authorized info handling and session ID retrieval in OAuth guard
This commit is contained in:
commit
1852e2cff0
8 changed files with 2061 additions and 38 deletions
|
|
@ -33,6 +33,53 @@ func (s *Service) Guard(c *gin.Context) {
|
|||
if claims.ExpiresAt.Before(time.Now()) {
|
||||
s.tryAutoRefreshToken(c, claims)
|
||||
}
|
||||
|
||||
// Set Authorized Info
|
||||
s.setAuthorizedInfo(c, claims)
|
||||
}
|
||||
|
||||
// GetAuthorizedInfo Get Authorized Info from context
|
||||
func GetAuthorizedInfo(c *gin.Context) *types.AuthorizedInfo {
|
||||
info := &types.AuthorizedInfo{}
|
||||
|
||||
if subject, ok := c.Get("__subject"); ok {
|
||||
info.Subject = subject.(string)
|
||||
}
|
||||
|
||||
if clientID, ok := c.Get("__client_id"); ok {
|
||||
info.ClientID = clientID.(string)
|
||||
}
|
||||
|
||||
if userID, ok := c.Get("__user_id"); ok {
|
||||
info.UserID = userID.(string)
|
||||
}
|
||||
|
||||
if scope, ok := c.Get("__scope"); ok {
|
||||
info.Scope = scope.(string)
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
// Set Authorized Info in context
|
||||
func (s *Service) setAuthorizedInfo(c *gin.Context, claims *types.TokenClaims) {
|
||||
sid := s.getSessionID(c)
|
||||
|
||||
// Set __sid in context
|
||||
if sid != "" {
|
||||
c.Set("__sid", sid)
|
||||
}
|
||||
|
||||
// Set __userID in context
|
||||
userID, err := s.UserID(claims.ClientID, claims.Subject)
|
||||
if err == nil && userID != "" {
|
||||
c.Set("__user_id", userID)
|
||||
}
|
||||
|
||||
// Set subject scope, client_id, user_id in context
|
||||
c.Set("__subject", claims.Subject)
|
||||
c.Set("__scope", claims.Scope)
|
||||
c.Set("__client_id", claims.ClientID)
|
||||
}
|
||||
|
||||
func (s *Service) tryAutoRefreshToken(c *gin.Context, _ *types.TokenClaims) {
|
||||
|
|
@ -77,3 +124,35 @@ func (s *Service) getRefreshToken(c *gin.Context) string {
|
|||
}
|
||||
return strings.TrimPrefix(token, "Bearer ")
|
||||
}
|
||||
|
||||
// Get Session ID from cookies, headers, or query string
|
||||
func (s *Service) getSessionID(c *gin.Context) string {
|
||||
|
||||
// 0. If has __sid in context, return it
|
||||
sid, ok := c.Get("__sid")
|
||||
if ok {
|
||||
return sid.(string)
|
||||
}
|
||||
|
||||
// 1. Try to get Session ID from cookies first
|
||||
if sid, err := c.Cookie("__Host-session_id"); err == nil && sid != "" {
|
||||
return sid
|
||||
}
|
||||
|
||||
// 2. Try to get Session ID from X-Session-ID header
|
||||
if sessionHeader := c.GetHeader("X-Session-ID"); sessionHeader != "" {
|
||||
return sessionHeader
|
||||
}
|
||||
|
||||
// 3. Try to get Session ID from query string
|
||||
if sessionQuery := c.Query("session_id"); sessionQuery != "" {
|
||||
return sessionQuery
|
||||
}
|
||||
|
||||
// 4. Try alternative query parameter names
|
||||
if sessionQuery := c.Query("sid"); sessionQuery != "" {
|
||||
return sessionQuery
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -574,6 +574,15 @@ type TokenClaims struct {
|
|||
JTI string `json:"jti,omitempty"` // JWT ID (for JWT tokens)
|
||||
}
|
||||
|
||||
// AuthorizedInfo represents authorized information
|
||||
type AuthorizedInfo struct {
|
||||
Subject string `json:"sub,omitempty"` // Subject identifier
|
||||
ClientID string `json:"client_id"` // OAuth client ID
|
||||
Scope string `json:"scope,omitempty"` // Access scope
|
||||
SessionID string `json:"session_id,omitempty"` // Session ID
|
||||
UserID string `json:"user_id,omitempty"` // User ID
|
||||
}
|
||||
|
||||
// JWTClaims represents JWT-specific claims structure
|
||||
type JWTClaims struct {
|
||||
jwt.StandardClaims
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/kb"
|
||||
"github.com/yaoapp/yao/openapi"
|
||||
"github.com/yaoapp/yao/openapi/oauth"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
|
@ -618,6 +619,7 @@ type TokenInfo struct {
|
|||
ExpiresIn int
|
||||
Scope string
|
||||
ClientID string
|
||||
UserID string
|
||||
}
|
||||
|
||||
// ObtainAccessToken obtains an access token for testing OAuth endpoints that require authentication.
|
||||
|
|
@ -630,19 +632,33 @@ func ObtainAccessToken(t *testing.T, serverURL, clientID, clientSecret, redirect
|
|||
t.Fatal("OpenAPI server not initialized. Call Prepare(t) first.")
|
||||
}
|
||||
|
||||
// Step 1: Get authorization code with PKCE parameters
|
||||
authInfo := ObtainAuthorizationCode(t, serverURL, clientID, redirectURI, scope)
|
||||
// Step 1: Create a test user and set up fingerprint mapping
|
||||
testUserID, subject := createTestUser(t, server, clientID)
|
||||
|
||||
// Step 2: Exchange authorization code for access token with PKCE code verifier
|
||||
ctx := context.Background()
|
||||
token, err := server.OAuth.Token(ctx, "authorization_code", authInfo.Code, clientID, authInfo.CodeVerifier)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to exchange authorization code for token: %v", err)
|
||||
// Step 2: Create access token directly using the OAuth service
|
||||
// This bypasses the authorization flow and creates a token for our test user
|
||||
oauthService := oauth.OAuth
|
||||
if oauthService == nil {
|
||||
t.Fatal("Global OAuth service not initialized")
|
||||
}
|
||||
|
||||
// Verify we got a valid token
|
||||
if token.AccessToken == "" {
|
||||
t.Fatal("Token response missing access token")
|
||||
accessToken, err := oauthService.MakeAccessToken(clientID, scope, subject, 3600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create access token: %v", err)
|
||||
}
|
||||
|
||||
refreshToken, err := oauthService.MakeRefreshToken(clientID, scope, subject, 7200)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create refresh token: %v", err)
|
||||
}
|
||||
|
||||
// Create a synthetic token response
|
||||
token := &types.Token{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 3600,
|
||||
Scope: scope,
|
||||
}
|
||||
|
||||
tokenInfo := &TokenInfo{
|
||||
|
|
@ -652,10 +668,11 @@ func ObtainAccessToken(t *testing.T, serverURL, clientID, clientSecret, redirect
|
|||
ExpiresIn: token.ExpiresIn,
|
||||
Scope: token.Scope,
|
||||
ClientID: clientID,
|
||||
UserID: testUserID, // Include the test user ID
|
||||
}
|
||||
|
||||
t.Logf("Obtained access token: %s (type: %s, expires_in: %d)",
|
||||
tokenInfo.AccessToken, tokenInfo.TokenType, tokenInfo.ExpiresIn)
|
||||
t.Logf("Obtained access token: %s (type: %s, expires_in: %d, user_id: %s)",
|
||||
tokenInfo.AccessToken, tokenInfo.TokenType, tokenInfo.ExpiresIn, tokenInfo.UserID)
|
||||
return tokenInfo
|
||||
}
|
||||
|
||||
|
|
@ -681,3 +698,31 @@ func generateCodeChallenge(codeVerifier string) string {
|
|||
// Base64 URL encode the hash without padding
|
||||
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// createTestUser creates a test user and sets up proper fingerprint mapping for OAuth authentication
|
||||
func createTestUser(t *testing.T, server *openapi.OpenAPI, clientID string) (string, string) {
|
||||
if server.OAuth == nil {
|
||||
t.Fatal("OAuth service not initialized")
|
||||
}
|
||||
|
||||
// Generate a unique test user ID
|
||||
testUserID := fmt.Sprintf("test_user_%d", time.Now().UnixNano())
|
||||
|
||||
// Access the global OAuth service to set up fingerprint mapping
|
||||
// The OAuth interface doesn't expose Subject method, so we need to access the concrete service
|
||||
oauthService := oauth.OAuth
|
||||
if oauthService == nil {
|
||||
t.Fatal("Global OAuth service not initialized")
|
||||
}
|
||||
|
||||
// Create subject (fingerprint) for this user using the concrete OAuth service
|
||||
// This will set up the proper fingerprint mapping: clientID:subject -> userID
|
||||
subject, err := oauthService.Subject(clientID, testUserID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create user subject: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Created test user: %s with subject: %s", testUserID, subject)
|
||||
return testUserID, subject
|
||||
}
|
||||
|
||||
|
|
|
|||
871
openapi/tests/user/team_test.go
Normal file
871
openapi/tests/user/team_test.go
Normal file
|
|
@ -0,0 +1,871 @@
|
|||
package user_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yaoapp/yao/openapi"
|
||||
"github.com/yaoapp/yao/openapi/tests/testutils"
|
||||
)
|
||||
|
||||
// TestTeamList tests the GET /user/teams endpoint
|
||||
func TestTeamList(t *testing.T) {
|
||||
// Initialize test environment
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
// Get base URL from server config
|
||||
baseURL := ""
|
||||
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||
baseURL = openapi.Server.Config.BaseURL
|
||||
}
|
||||
|
||||
// Register a test client for OAuth authentication
|
||||
testClient := testutils.RegisterTestClient(t, "Team Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
||||
|
||||
// Obtain access token for authenticated requests
|
||||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
endpoint string
|
||||
headers map[string]string
|
||||
expectCode int
|
||||
expectMsg string
|
||||
}{
|
||||
{
|
||||
"list teams without authentication",
|
||||
"/user/teams",
|
||||
map[string]string{},
|
||||
401,
|
||||
"should require authentication",
|
||||
},
|
||||
{
|
||||
"list teams with valid token",
|
||||
"/user/teams",
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
200,
|
||||
"should return user teams",
|
||||
},
|
||||
{
|
||||
"list teams with pagination",
|
||||
"/user/teams?page=1&pagesize=10",
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
200,
|
||||
"should handle pagination parameters",
|
||||
},
|
||||
{
|
||||
"list teams with status filter",
|
||||
"/user/teams?status=active",
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
200,
|
||||
"should filter by status",
|
||||
},
|
||||
{
|
||||
"list teams with name search",
|
||||
"/user/teams?name=test",
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
200,
|
||||
"should search by name",
|
||||
},
|
||||
{
|
||||
"list teams with invalid pagesize",
|
||||
"/user/teams?pagesize=1000",
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
200,
|
||||
"should limit pagesize to maximum allowed",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
requestURL := serverURL + baseURL + tc.endpoint
|
||||
req, err := http.NewRequest("GET", requestURL, nil)
|
||||
assert.NoError(t, err, "Should create HTTP request")
|
||||
|
||||
// Add headers
|
||||
for key, value := range tc.headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err, "HTTP request should succeed")
|
||||
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err, "Should read response body")
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
// Parse response as pagination result
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(body, &response)
|
||||
assert.NoError(t, err, "Should parse JSON response")
|
||||
|
||||
// Check pagination structure (consistent with other modules)
|
||||
if data, ok := response["data"]; ok {
|
||||
assert.IsType(t, []interface{}{}, data, "Should have data array")
|
||||
}
|
||||
if total, ok := response["total"]; ok {
|
||||
assert.IsType(t, float64(0), total, "Should have total count")
|
||||
}
|
||||
if page, ok := response["page"]; ok {
|
||||
assert.IsType(t, float64(0), page, "Should have page number")
|
||||
}
|
||||
if pagesize, ok := response["pagesize"]; ok {
|
||||
assert.IsType(t, float64(0), pagesize, "Should have pagesize")
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Team list test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamCreate tests the POST /user/teams endpoint
|
||||
func TestTeamCreate(t *testing.T) {
|
||||
// Initialize test environment
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
// Get base URL from server config
|
||||
baseURL := ""
|
||||
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||
baseURL = openapi.Server.Config.BaseURL
|
||||
}
|
||||
|
||||
// Register a test client for OAuth authentication
|
||||
testClient := testutils.RegisterTestClient(t, "Team Create Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
||||
|
||||
// Obtain access token for authenticated requests
|
||||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
body map[string]interface{}
|
||||
headers map[string]string
|
||||
expectCode int
|
||||
expectMsg string
|
||||
}{
|
||||
{
|
||||
"create team without authentication",
|
||||
map[string]interface{}{
|
||||
"name": "Test Team",
|
||||
},
|
||||
map[string]string{},
|
||||
401,
|
||||
"should require authentication",
|
||||
},
|
||||
{
|
||||
"create team with valid data",
|
||||
map[string]interface{}{
|
||||
"name": "Test Team",
|
||||
"description": "A test team for unit testing",
|
||||
},
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
201,
|
||||
"should create team successfully",
|
||||
},
|
||||
{
|
||||
"create team with settings",
|
||||
map[string]interface{}{
|
||||
"name": "Team with Settings",
|
||||
"description": "Team with custom settings",
|
||||
"settings": map[string]interface{}{
|
||||
"theme": "dark",
|
||||
"visibility": "private",
|
||||
},
|
||||
},
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
201,
|
||||
"should create team with settings",
|
||||
},
|
||||
{
|
||||
"create team without name",
|
||||
map[string]interface{}{
|
||||
"description": "Team without name",
|
||||
},
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
400,
|
||||
"should require team name",
|
||||
},
|
||||
{
|
||||
"create team with empty name",
|
||||
map[string]interface{}{
|
||||
"name": "",
|
||||
},
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
400,
|
||||
"should require non-empty team name",
|
||||
},
|
||||
{
|
||||
"create team with invalid JSON",
|
||||
nil, // Will send invalid JSON
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
400,
|
||||
"should handle invalid JSON",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
requestURL := serverURL + baseURL + "/user/teams"
|
||||
|
||||
var req *http.Request
|
||||
var err error
|
||||
|
||||
if tc.body == nil {
|
||||
// Send invalid JSON for invalid JSON test case
|
||||
req, err = http.NewRequest("POST", requestURL, bytes.NewBufferString("invalid json"))
|
||||
} else {
|
||||
bodyBytes, _ := json.Marshal(tc.body)
|
||||
req, err = http.NewRequest("POST", requestURL, bytes.NewBuffer(bodyBytes))
|
||||
}
|
||||
assert.NoError(t, err, "Should create HTTP request")
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Add headers
|
||||
for key, value := range tc.headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err, "HTTP request should succeed")
|
||||
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err, "Should read response body")
|
||||
|
||||
if resp.StatusCode == 201 {
|
||||
// Parse response as team object
|
||||
var team map[string]interface{}
|
||||
err = json.Unmarshal(body, &team)
|
||||
assert.NoError(t, err, "Should parse JSON response")
|
||||
|
||||
// Verify team structure
|
||||
assert.Contains(t, team, "id", "Should have team ID")
|
||||
assert.Contains(t, team, "team_id", "Should have team_id")
|
||||
assert.Contains(t, team, "name", "Should have team name")
|
||||
assert.Contains(t, team, "owner_id", "Should have owner_id")
|
||||
assert.Contains(t, team, "status", "Should have status")
|
||||
assert.Contains(t, team, "created_at", "Should have created_at")
|
||||
assert.Contains(t, team, "updated_at", "Should have updated_at")
|
||||
|
||||
// Verify values
|
||||
if tc.body != nil {
|
||||
if name, ok := tc.body["name"]; ok {
|
||||
assert.Equal(t, name, team["name"], "Should have correct team name")
|
||||
}
|
||||
if description, ok := tc.body["description"]; ok {
|
||||
assert.Equal(t, description, team["description"], "Should have correct description")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Team create test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamGet tests the GET /user/teams/:team_id endpoint
|
||||
func TestTeamGet(t *testing.T) {
|
||||
// Initialize test environment
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
// Get base URL from server config
|
||||
baseURL := ""
|
||||
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||
baseURL = openapi.Server.Config.BaseURL
|
||||
}
|
||||
|
||||
// Register a test client for OAuth authentication
|
||||
testClient := testutils.RegisterTestClient(t, "Team Get Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
||||
|
||||
// Obtain access token for authenticated requests
|
||||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
// First create a team to test with
|
||||
createTeamBody := map[string]interface{}{
|
||||
"name": "Get Test Team",
|
||||
"description": "Team for testing get functionality",
|
||||
"settings": map[string]interface{}{
|
||||
"theme": "light",
|
||||
},
|
||||
}
|
||||
|
||||
createReq := createTeamRequest(t, serverURL+baseURL+"/user/teams", createTeamBody, tokenInfo.AccessToken)
|
||||
createResp, err := (&http.Client{}).Do(createReq)
|
||||
assert.NoError(t, err, "Should create test team")
|
||||
defer createResp.Body.Close()
|
||||
|
||||
var createdTeam map[string]interface{}
|
||||
if createResp.StatusCode == 201 {
|
||||
createBody, _ := io.ReadAll(createResp.Body)
|
||||
json.Unmarshal(createBody, &createdTeam)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
teamID string
|
||||
headers map[string]string
|
||||
expectCode int
|
||||
expectMsg string
|
||||
}{
|
||||
{
|
||||
"get team without authentication",
|
||||
getTeamID(createdTeam),
|
||||
map[string]string{},
|
||||
401,
|
||||
"should require authentication",
|
||||
},
|
||||
{
|
||||
"get existing team",
|
||||
getTeamID(createdTeam),
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
200,
|
||||
"should return team details",
|
||||
},
|
||||
{
|
||||
"get non-existent team",
|
||||
"non-existent-team-id",
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
404,
|
||||
"should return not found for non-existent team",
|
||||
},
|
||||
{
|
||||
"get team with empty team_id returns team list",
|
||||
"",
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
200,
|
||||
"should return team list when team_id is empty",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
endpoint := "/user/teams"
|
||||
if tc.teamID != "" {
|
||||
endpoint += "/" + tc.teamID
|
||||
}
|
||||
requestURL := serverURL + baseURL + endpoint
|
||||
|
||||
req, err := http.NewRequest("GET", requestURL, nil)
|
||||
assert.NoError(t, err, "Should create HTTP request")
|
||||
|
||||
// Add headers
|
||||
for key, value := range tc.headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err, "HTTP request should succeed")
|
||||
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err, "Should read response body")
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
if tc.teamID == "" {
|
||||
// Parse response as team list (pagination result)
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(body, &response)
|
||||
assert.NoError(t, err, "Should parse JSON response")
|
||||
|
||||
// Check pagination structure
|
||||
assert.Contains(t, response, "data", "Should have data array")
|
||||
assert.Contains(t, response, "total", "Should have total count")
|
||||
assert.Contains(t, response, "page", "Should have page number")
|
||||
assert.Contains(t, response, "pagesize", "Should have pagesize")
|
||||
} else {
|
||||
// Parse response as team detail object
|
||||
var team map[string]interface{}
|
||||
err = json.Unmarshal(body, &team)
|
||||
assert.NoError(t, err, "Should parse JSON response")
|
||||
|
||||
// Verify team detail structure
|
||||
assert.Contains(t, team, "id", "Should have team ID")
|
||||
assert.Contains(t, team, "team_id", "Should have team_id")
|
||||
assert.Contains(t, team, "name", "Should have team name")
|
||||
assert.Contains(t, team, "description", "Should have description")
|
||||
assert.Contains(t, team, "owner_id", "Should have owner_id")
|
||||
assert.Contains(t, team, "status", "Should have status")
|
||||
assert.Contains(t, team, "settings", "Should have settings")
|
||||
assert.Contains(t, team, "created_at", "Should have created_at")
|
||||
assert.Contains(t, team, "updated_at", "Should have updated_at")
|
||||
|
||||
// Verify values match created team
|
||||
assert.Equal(t, "Get Test Team", team["name"], "Should have correct team name")
|
||||
assert.Equal(t, "Team for testing get functionality", team["description"], "Should have correct description")
|
||||
if settings, ok := team["settings"].(map[string]interface{}); ok {
|
||||
assert.Equal(t, "light", settings["theme"], "Should have correct theme setting")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Team get test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamUpdate tests the PUT /user/teams/:team_id endpoint
|
||||
func TestTeamUpdate(t *testing.T) {
|
||||
// Initialize test environment
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
// Get base URL from server config
|
||||
baseURL := ""
|
||||
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||
baseURL = openapi.Server.Config.BaseURL
|
||||
}
|
||||
|
||||
// Register a test client for OAuth authentication
|
||||
testClient := testutils.RegisterTestClient(t, "Team Update Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
||||
|
||||
// Obtain access token for authenticated requests
|
||||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
// Create a team to test updates
|
||||
createTeamBody := map[string]interface{}{
|
||||
"name": "Update Test Team",
|
||||
"description": "Team for testing update functionality",
|
||||
}
|
||||
|
||||
createReq := createTeamRequest(t, serverURL+baseURL+"/user/teams", createTeamBody, tokenInfo.AccessToken)
|
||||
createResp, err := (&http.Client{}).Do(createReq)
|
||||
assert.NoError(t, err, "Should create test team")
|
||||
defer createResp.Body.Close()
|
||||
|
||||
var createdTeam map[string]interface{}
|
||||
if createResp.StatusCode == 201 {
|
||||
createBody, _ := io.ReadAll(createResp.Body)
|
||||
json.Unmarshal(createBody, &createdTeam)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
teamID string
|
||||
body map[string]interface{}
|
||||
headers map[string]string
|
||||
expectCode int
|
||||
expectMsg string
|
||||
}{
|
||||
{
|
||||
"update team without authentication",
|
||||
getTeamID(createdTeam),
|
||||
map[string]interface{}{
|
||||
"name": "Updated Name",
|
||||
},
|
||||
map[string]string{},
|
||||
401,
|
||||
"should require authentication",
|
||||
},
|
||||
{
|
||||
"update team name",
|
||||
getTeamID(createdTeam),
|
||||
map[string]interface{}{
|
||||
"name": "Updated Team Name",
|
||||
},
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
200,
|
||||
"should update team name",
|
||||
},
|
||||
{
|
||||
"update team description",
|
||||
getTeamID(createdTeam),
|
||||
map[string]interface{}{
|
||||
"description": "Updated description",
|
||||
},
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
200,
|
||||
"should update team description",
|
||||
},
|
||||
{
|
||||
"update team settings",
|
||||
getTeamID(createdTeam),
|
||||
map[string]interface{}{
|
||||
"settings": map[string]interface{}{
|
||||
"theme": "dark",
|
||||
"visibility": "public",
|
||||
},
|
||||
},
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
200,
|
||||
"should update team settings",
|
||||
},
|
||||
{
|
||||
"update non-existent team",
|
||||
"non-existent-team-id",
|
||||
map[string]interface{}{
|
||||
"name": "Updated Name",
|
||||
},
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
404,
|
||||
"should return not found for non-existent team",
|
||||
},
|
||||
{
|
||||
"update team with invalid JSON",
|
||||
getTeamID(createdTeam),
|
||||
nil, // Will send invalid JSON
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
400,
|
||||
"should handle invalid JSON",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
requestURL := serverURL + baseURL + "/user/teams/" + tc.teamID
|
||||
|
||||
var req *http.Request
|
||||
var err error
|
||||
|
||||
if tc.body == nil {
|
||||
// Send invalid JSON for invalid JSON test case
|
||||
req, err = http.NewRequest("PUT", requestURL, bytes.NewBufferString("invalid json"))
|
||||
} else {
|
||||
bodyBytes, _ := json.Marshal(tc.body)
|
||||
req, err = http.NewRequest("PUT", requestURL, bytes.NewBuffer(bodyBytes))
|
||||
}
|
||||
assert.NoError(t, err, "Should create HTTP request")
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Add headers
|
||||
for key, value := range tc.headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err, "HTTP request should succeed")
|
||||
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err, "Should read response body")
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
// Parse response as updated team object
|
||||
var team map[string]interface{}
|
||||
err = json.Unmarshal(body, &team)
|
||||
assert.NoError(t, err, "Should parse JSON response")
|
||||
|
||||
// Verify updated values
|
||||
if tc.body != nil {
|
||||
if name, ok := tc.body["name"]; ok {
|
||||
assert.Equal(t, name, team["name"], "Should have updated team name")
|
||||
}
|
||||
if description, ok := tc.body["description"]; ok {
|
||||
assert.Equal(t, description, team["description"], "Should have updated description")
|
||||
}
|
||||
if settings, ok := tc.body["settings"]; ok {
|
||||
assert.Equal(t, settings, team["settings"], "Should have updated settings")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Team update test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamDelete tests the DELETE /user/teams/:team_id endpoint
|
||||
func TestTeamDelete(t *testing.T) {
|
||||
// Initialize test environment
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
// Get base URL from server config
|
||||
baseURL := ""
|
||||
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||
baseURL = openapi.Server.Config.BaseURL
|
||||
}
|
||||
|
||||
// Register a test client for OAuth authentication
|
||||
testClient := testutils.RegisterTestClient(t, "Team Delete Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
||||
|
||||
// Obtain access token for authenticated requests
|
||||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
// Create teams to test deletion
|
||||
createTeam := func(name string) map[string]interface{} {
|
||||
createTeamBody := map[string]interface{}{
|
||||
"name": name,
|
||||
"description": "Team for testing delete functionality",
|
||||
}
|
||||
|
||||
createReq := createTeamRequest(t, serverURL+baseURL+"/user/teams", createTeamBody, tokenInfo.AccessToken)
|
||||
createResp, err := (&http.Client{}).Do(createReq)
|
||||
assert.NoError(t, err, "Should create test team")
|
||||
defer createResp.Body.Close()
|
||||
|
||||
var createdTeam map[string]interface{}
|
||||
if createResp.StatusCode == 201 {
|
||||
createBody, _ := io.ReadAll(createResp.Body)
|
||||
json.Unmarshal(createBody, &createdTeam)
|
||||
}
|
||||
return createdTeam
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
teamID string
|
||||
headers map[string]string
|
||||
expectCode int
|
||||
expectMsg string
|
||||
}{
|
||||
{
|
||||
"delete team without authentication",
|
||||
getTeamID(createTeam("Delete Test Team 1")),
|
||||
map[string]string{},
|
||||
401,
|
||||
"should require authentication",
|
||||
},
|
||||
{
|
||||
"delete existing team",
|
||||
getTeamID(createTeam("Delete Test Team 2")),
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
200,
|
||||
"should delete team successfully",
|
||||
},
|
||||
{
|
||||
"delete non-existent team",
|
||||
"non-existent-team-id",
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
404,
|
||||
"should return not found for non-existent team",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
requestURL := serverURL + baseURL + "/user/teams/" + tc.teamID
|
||||
|
||||
req, err := http.NewRequest("DELETE", requestURL, nil)
|
||||
assert.NoError(t, err, "Should create HTTP request")
|
||||
|
||||
// Add headers
|
||||
for key, value := range tc.headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err, "HTTP request should succeed")
|
||||
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err, "Should read response body")
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
// Parse response as success message
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(body, &response)
|
||||
assert.NoError(t, err, "Should parse JSON response")
|
||||
|
||||
assert.Contains(t, response, "message", "Should have success message")
|
||||
assert.Equal(t, "Team deleted successfully", response["message"], "Should have correct success message")
|
||||
}
|
||||
|
||||
t.Logf("Team delete test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamAuthenticationEdgeCases tests authentication and authorization edge cases
|
||||
func TestTeamAuthenticationEdgeCases(t *testing.T) {
|
||||
// Initialize test environment
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
// Get base URL from server config
|
||||
baseURL := ""
|
||||
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||
baseURL = openapi.Server.Config.BaseURL
|
||||
}
|
||||
|
||||
// Register a test client for OAuth authentication
|
||||
testClient := testutils.RegisterTestClient(t, "Team Auth Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
||||
|
||||
// Obtain access token for authenticated requests
|
||||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
endpoint string
|
||||
method string
|
||||
headers map[string]string
|
||||
expectCode int
|
||||
expectMsg string
|
||||
}{
|
||||
{
|
||||
"invalid bearer token format",
|
||||
"/user/teams",
|
||||
"GET",
|
||||
map[string]string{
|
||||
"Authorization": "Bearer invalid-token",
|
||||
},
|
||||
401,
|
||||
"should reject invalid token",
|
||||
},
|
||||
{
|
||||
"missing bearer prefix",
|
||||
"/user/teams",
|
||||
"GET",
|
||||
map[string]string{
|
||||
"Authorization": tokenInfo.AccessToken,
|
||||
},
|
||||
200,
|
||||
"may accept token without Bearer prefix (implementation dependent)",
|
||||
},
|
||||
{
|
||||
"expired token simulation",
|
||||
"/user/teams",
|
||||
"GET",
|
||||
map[string]string{
|
||||
"Authorization": "Bearer expired.token.here",
|
||||
},
|
||||
401,
|
||||
"should reject expired token",
|
||||
},
|
||||
{
|
||||
"malformed authorization header",
|
||||
"/user/teams",
|
||||
"GET",
|
||||
map[string]string{
|
||||
"Authorization": "Malformed",
|
||||
},
|
||||
401,
|
||||
"should reject malformed header",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
requestURL := serverURL + baseURL + tc.endpoint
|
||||
|
||||
req, err := http.NewRequest(tc.method, requestURL, nil)
|
||||
assert.NoError(t, err, "Should create HTTP request")
|
||||
|
||||
// Add headers
|
||||
for key, value := range tc.headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err, "HTTP request should succeed")
|
||||
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err, "Should read response body")
|
||||
|
||||
t.Logf("Auth edge case test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// createTeamRequest creates a POST request for team creation
|
||||
func createTeamRequest(t *testing.T, url string, body map[string]interface{}, accessToken string) *http.Request {
|
||||
bodyBytes, err := json.Marshal(body)
|
||||
assert.NoError(t, err, "Should marshal team creation body")
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(bodyBytes))
|
||||
assert.NoError(t, err, "Should create team creation request")
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
// getTeamID extracts team_id from created team response
|
||||
func getTeamID(team map[string]interface{}) string {
|
||||
if team == nil {
|
||||
return ""
|
||||
}
|
||||
if teamID, ok := team["team_id"].(string); ok {
|
||||
return teamID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
844
openapi/user/team.go
Normal file
844
openapi/user/team.go
Normal file
|
|
@ -0,0 +1,844 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/gou/model"
|
||||
"github.com/yaoapp/gou/process"
|
||||
"github.com/yaoapp/kun/exception"
|
||||
"github.com/yaoapp/kun/log"
|
||||
"github.com/yaoapp/kun/maps"
|
||||
"github.com/yaoapp/yao/openapi/oauth"
|
||||
"github.com/yaoapp/yao/openapi/oauth/providers/user"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
)
|
||||
|
||||
// Team Management Handlers
|
||||
|
||||
// GinTeamList handles GET /teams - Get user teams
|
||||
func GinTeamList(c *gin.Context) {
|
||||
// Get authorized user info
|
||||
authInfo := oauth.GetAuthorizedInfo(c)
|
||||
if authInfo == nil || authInfo.UserID == "" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidClient.Code,
|
||||
ErrorDescription: "User not authenticated",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse pagination parameters
|
||||
page := 1
|
||||
pagesize := 20
|
||||
|
||||
if p := c.Query("page"); p != "" {
|
||||
if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 {
|
||||
page = parsed
|
||||
}
|
||||
}
|
||||
|
||||
if ps := c.Query("pagesize"); ps != "" {
|
||||
if parsed, err := strconv.Atoi(ps); err == nil && parsed > 0 && parsed <= 100 {
|
||||
pagesize = parsed
|
||||
}
|
||||
}
|
||||
|
||||
// Get user provider instance
|
||||
provider, err := getUserProvider()
|
||||
if err != nil {
|
||||
log.Error("Failed to get user provider: %v", err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to initialize user provider",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Build query parameters
|
||||
param := model.QueryParam{
|
||||
Wheres: []model.QueryWhere{
|
||||
{Column: "owner_id", Value: authInfo.UserID},
|
||||
},
|
||||
Orders: []model.QueryOrder{
|
||||
{Column: "created_at", Option: "desc"},
|
||||
},
|
||||
}
|
||||
|
||||
// Add status filter if provided
|
||||
if status := c.Query("status"); status != "" {
|
||||
param.Wheres = append(param.Wheres, model.QueryWhere{
|
||||
Column: "status",
|
||||
Value: status,
|
||||
})
|
||||
}
|
||||
|
||||
// Add name search if provided
|
||||
if name := c.Query("name"); name != "" {
|
||||
param.Wheres = append(param.Wheres, model.QueryWhere{
|
||||
Column: "name",
|
||||
Value: "%" + name + "%",
|
||||
OP: "like",
|
||||
})
|
||||
}
|
||||
|
||||
// Get paginated teams
|
||||
result, err := provider.PaginateTeams(c.Request.Context(), param, page, pagesize)
|
||||
if err != nil {
|
||||
log.Error("Failed to get user teams: %v", err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to retrieve teams",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Return the paginated result directly (consistent with other modules)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// GinTeamGet handles GET /teams/:team_id - Get user team details
|
||||
func GinTeamGet(c *gin.Context) {
|
||||
// Get authorized user info
|
||||
authInfo := oauth.GetAuthorizedInfo(c)
|
||||
if authInfo == nil || authInfo.UserID == "" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidClient.Code,
|
||||
ErrorDescription: "User not authenticated",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
teamID := c.Param("team_id")
|
||||
if teamID == "" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Team ID is required",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Get user provider instance
|
||||
provider, err := getUserProvider()
|
||||
if err != nil {
|
||||
log.Error("Failed to get user provider: %v", err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to initialize user provider",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Get team details
|
||||
teamData, err := provider.GetTeamDetail(c.Request.Context(), teamID)
|
||||
if err != nil {
|
||||
log.Error("Failed to get team details: %v", err)
|
||||
// Check if it's a "team not found" error
|
||||
if err.Error() == "team not found" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Team not found",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||
} else {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to retrieve team details",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user owns this team
|
||||
ownerID := toString(teamData["owner_id"])
|
||||
if ownerID != authInfo.UserID {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrAccessDenied.Code,
|
||||
ErrorDescription: "Access denied: you don't own this team",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to response format
|
||||
team := mapToTeamDetailResponse(teamData)
|
||||
c.JSON(http.StatusOK, team)
|
||||
}
|
||||
|
||||
// GinTeamCreate handles POST /teams - Create user team
|
||||
func GinTeamCreate(c *gin.Context) {
|
||||
// Get authorized user info
|
||||
authInfo := oauth.GetAuthorizedInfo(c)
|
||||
if authInfo == nil || authInfo.UserID == "" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidClient.Code,
|
||||
ErrorDescription: "User not authenticated",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var req CreateTeamRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Invalid request body: " + err.Error(),
|
||||
}
|
||||
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Get user provider instance
|
||||
provider, err := getUserProvider()
|
||||
if err != nil {
|
||||
log.Error("Failed to get user provider: %v", err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to initialize user provider",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Prepare team data
|
||||
teamData := maps.MapStrAny{
|
||||
"name": req.Name,
|
||||
"description": req.Description,
|
||||
"owner_id": authInfo.UserID,
|
||||
"status": "active",
|
||||
"is_verified": false,
|
||||
"created_at": time.Now(),
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
|
||||
// Add settings if provided
|
||||
if req.Settings != nil {
|
||||
teamData["settings"] = req.Settings
|
||||
}
|
||||
|
||||
// Create team
|
||||
teamID, err := provider.CreateTeam(c.Request.Context(), teamData)
|
||||
if err != nil {
|
||||
log.Error("Failed to create team: %v", err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to create team",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the created team details
|
||||
createdTeam, err := provider.GetTeamDetail(c.Request.Context(), teamID)
|
||||
if err != nil {
|
||||
log.Error("Failed to get created team details: %v", err)
|
||||
// Return basic response if we can't get details
|
||||
c.JSON(http.StatusCreated, gin.H{"team_id": teamID})
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to response format
|
||||
team := mapToTeamDetailResponse(createdTeam)
|
||||
c.JSON(http.StatusCreated, team)
|
||||
}
|
||||
|
||||
// GinTeamUpdate handles PUT /teams/:team_id - Update user team
|
||||
func GinTeamUpdate(c *gin.Context) {
|
||||
// Get authorized user info
|
||||
authInfo := oauth.GetAuthorizedInfo(c)
|
||||
if authInfo == nil || authInfo.UserID == "" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidClient.Code,
|
||||
ErrorDescription: "User not authenticated",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
teamID := c.Param("team_id")
|
||||
if teamID == "" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Team ID is required",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var req UpdateTeamRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Invalid request body: " + err.Error(),
|
||||
}
|
||||
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Get user provider instance
|
||||
provider, err := getUserProvider()
|
||||
if err != nil {
|
||||
log.Error("Failed to get user provider: %v", err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to initialize user provider",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if team exists and user owns it
|
||||
teamData, err := provider.GetTeam(c.Request.Context(), teamID)
|
||||
if err != nil {
|
||||
log.Error("Failed to get team: %v", err)
|
||||
// Check if it's a "team not found" error
|
||||
if err.Error() == "team not found" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Team not found",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||
} else {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to get team",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check ownership
|
||||
ownerID := toString(teamData["owner_id"])
|
||||
if ownerID != authInfo.UserID {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrAccessDenied.Code,
|
||||
ErrorDescription: "Access denied: you don't own this team",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Prepare update data
|
||||
updateData := maps.MapStrAny{
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
updateData["name"] = req.Name
|
||||
}
|
||||
if req.Description != "" {
|
||||
updateData["description"] = req.Description
|
||||
}
|
||||
if req.Settings != nil {
|
||||
updateData["settings"] = req.Settings
|
||||
}
|
||||
|
||||
// Update team
|
||||
err = provider.UpdateTeam(c.Request.Context(), teamID, updateData)
|
||||
if err != nil {
|
||||
log.Error("Failed to update team: %v", err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to update team",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Get updated team details
|
||||
updatedTeam, err := provider.GetTeamDetail(c.Request.Context(), teamID)
|
||||
if err != nil {
|
||||
log.Error("Failed to get updated team details: %v", err)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Team updated successfully"})
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to response format
|
||||
team := mapToTeamDetailResponse(updatedTeam)
|
||||
c.JSON(http.StatusOK, team)
|
||||
}
|
||||
|
||||
// GinTeamDelete handles DELETE /teams/:team_id - Delete user team
|
||||
func GinTeamDelete(c *gin.Context) {
|
||||
// Get authorized user info
|
||||
authInfo := oauth.GetAuthorizedInfo(c)
|
||||
if authInfo == nil || authInfo.UserID == "" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidClient.Code,
|
||||
ErrorDescription: "User not authenticated",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
teamID := c.Param("team_id")
|
||||
if teamID == "" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Team ID is required",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Get user provider instance
|
||||
provider, err := getUserProvider()
|
||||
if err != nil {
|
||||
log.Error("Failed to get user provider: %v", err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to initialize user provider",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if team exists and user owns it
|
||||
teamData, err := provider.GetTeam(c.Request.Context(), teamID)
|
||||
if err != nil {
|
||||
log.Error("Failed to get team: %v", err)
|
||||
// Check if it's a "team not found" error
|
||||
if err.Error() == "team not found" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Team not found",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||
} else {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to get team",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check ownership
|
||||
ownerID := toString(teamData["owner_id"])
|
||||
if ownerID != authInfo.UserID {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrAccessDenied.Code,
|
||||
ErrorDescription: "Access denied: you don't own this team",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete team
|
||||
err = provider.DeleteTeam(c.Request.Context(), teamID)
|
||||
if err != nil {
|
||||
log.Error("Failed to delete team: %v", err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to delete team",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Team deleted successfully"})
|
||||
}
|
||||
|
||||
// Yao Process Handlers (for Yao application calls)
|
||||
|
||||
// ProcessTeamList user.team.list Team list processor
|
||||
// Args[0] map: Query parameters {"status": "active", "name": "search", "page": 1, "pagesize": 20}
|
||||
// Return: map: Paginated team list
|
||||
func ProcessTeamList(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(1)
|
||||
|
||||
// Get user_id from session
|
||||
userIDStr := GetUserIDFromSession(process)
|
||||
|
||||
// Parse query parameters
|
||||
queryMap := process.ArgsMap(0)
|
||||
|
||||
// Build query parameters
|
||||
param := model.QueryParam{}
|
||||
|
||||
// Add filters
|
||||
if status, ok := queryMap["status"].(string); ok && status != "" {
|
||||
param.Wheres = append(param.Wheres, model.QueryWhere{
|
||||
Column: "status",
|
||||
Value: status,
|
||||
})
|
||||
}
|
||||
|
||||
if name, ok := queryMap["name"].(string); ok && name != "" {
|
||||
param.Wheres = append(param.Wheres, model.QueryWhere{
|
||||
Column: "name",
|
||||
Value: "%" + name + "%",
|
||||
OP: "like",
|
||||
})
|
||||
}
|
||||
|
||||
// Parse pagination
|
||||
page := 1
|
||||
pagesize := 20
|
||||
|
||||
if p, ok := queryMap["page"]; ok {
|
||||
if pageInt, ok := p.(int); ok && pageInt > 0 {
|
||||
page = pageInt
|
||||
}
|
||||
}
|
||||
|
||||
if ps, ok := queryMap["pagesize"]; ok {
|
||||
if pagesizeInt, ok := ps.(int); ok && pagesizeInt > 0 && pagesizeInt <= 100 {
|
||||
pagesize = pagesizeInt
|
||||
}
|
||||
}
|
||||
|
||||
// Get context
|
||||
ctx := process.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
// Call business logic
|
||||
result, err := teamList(ctx, userIDStr, param, page, pagesize)
|
||||
if err != nil {
|
||||
exception.New("failed to list teams: %s", 500, err.Error()).Throw()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ProcessTeamGet user.team.get Team get processor
|
||||
// Args[0] string: team_id
|
||||
// Return: map: Team details
|
||||
func ProcessTeamGet(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(1)
|
||||
|
||||
// Get user_id from session
|
||||
userIDStr := GetUserIDFromSession(process)
|
||||
|
||||
teamID := process.ArgsString(0)
|
||||
if teamID == "" {
|
||||
exception.New("team_id is required", 400).Throw()
|
||||
}
|
||||
|
||||
// Get context
|
||||
ctx := process.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
// Call business logic
|
||||
result, err := teamGet(ctx, userIDStr, teamID)
|
||||
if err != nil {
|
||||
exception.New("failed to get team: %s", 500, err.Error()).Throw()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ProcessTeamCreate user.team.create Team create processor
|
||||
// Args[0] map: Team data {"name": "Team Name", "description": "Description", "settings": {...}}
|
||||
// Return: map: {"team_id": "created_team_id"}
|
||||
func ProcessTeamCreate(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(1)
|
||||
|
||||
// Get user_id from session
|
||||
userIDStr := GetUserIDFromSession(process)
|
||||
|
||||
teamData := maps.MapStrAny(process.ArgsMap(0))
|
||||
|
||||
// Validate required fields
|
||||
if _, ok := teamData["name"]; !ok {
|
||||
exception.New("name is required", 400).Throw()
|
||||
}
|
||||
|
||||
// Get context
|
||||
ctx := process.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
// Call business logic
|
||||
teamID, err := teamCreate(ctx, userIDStr, teamData)
|
||||
if err != nil {
|
||||
exception.New("failed to create team: %s", 500, err.Error()).Throw()
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"team_id": teamID,
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessTeamUpdate user.team.update Team update processor
|
||||
// Args[0] string: team_id
|
||||
// Args[1] map: Update data {"name": "New Name", "description": "New Description", "settings": {...}}
|
||||
// Return: map: {"message": "success"}
|
||||
func ProcessTeamUpdate(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(2)
|
||||
|
||||
// Get user_id from session
|
||||
userIDStr := GetUserIDFromSession(process)
|
||||
|
||||
teamID := process.ArgsString(0)
|
||||
updateData := maps.MapStrAny(process.ArgsMap(1))
|
||||
|
||||
if teamID == "" {
|
||||
exception.New("team_id is required", 400).Throw()
|
||||
}
|
||||
|
||||
// Get context
|
||||
ctx := process.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
// Call business logic
|
||||
err := teamUpdate(ctx, userIDStr, teamID, updateData)
|
||||
if err != nil {
|
||||
exception.New("failed to update team: %s", 500, err.Error()).Throw()
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"message": "success",
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessTeamDelete user.team.delete Team delete processor
|
||||
// Args[0] string: team_id
|
||||
// Return: map: {"message": "success"}
|
||||
func ProcessTeamDelete(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(1)
|
||||
|
||||
// Get user_id from session
|
||||
userIDStr := GetUserIDFromSession(process)
|
||||
|
||||
teamID := process.ArgsString(0)
|
||||
if teamID == "" {
|
||||
exception.New("team_id is required", 400).Throw()
|
||||
}
|
||||
|
||||
// Get context
|
||||
ctx := process.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
// Call business logic
|
||||
err := teamDelete(ctx, userIDStr, teamID)
|
||||
if err != nil {
|
||||
exception.New("failed to delete team: %s", 500, err.Error()).Throw()
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"message": "success",
|
||||
}
|
||||
}
|
||||
|
||||
// Private Business Logic Functions (internal use only)
|
||||
|
||||
// teamList handles the business logic for listing user teams
|
||||
func teamList(ctx context.Context, userID string, param model.QueryParam, page, pagesize int) (maps.MapStr, error) {
|
||||
// Get user provider instance
|
||||
provider, err := getUserProvider()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user provider: %w", err)
|
||||
}
|
||||
|
||||
// Add owner filter to query parameters
|
||||
param.Wheres = append(param.Wheres, model.QueryWhere{
|
||||
Column: "owner_id",
|
||||
Value: userID,
|
||||
})
|
||||
|
||||
// Set default ordering if not provided
|
||||
if len(param.Orders) == 0 {
|
||||
param.Orders = []model.QueryOrder{
|
||||
{Column: "created_at", Option: "desc"},
|
||||
}
|
||||
}
|
||||
|
||||
// Get paginated teams
|
||||
result, err := provider.PaginateTeams(ctx, param, page, pagesize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve teams: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// teamGet handles the business logic for getting a specific user team
|
||||
func teamGet(ctx context.Context, userID, teamID string) (maps.MapStrAny, error) {
|
||||
// Get user provider instance
|
||||
provider, err := getUserProvider()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user provider: %w", err)
|
||||
}
|
||||
|
||||
// Get team details
|
||||
teamData, err := provider.GetTeamDetail(ctx, teamID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve team details: %w", err)
|
||||
}
|
||||
|
||||
// Check if user owns this team
|
||||
ownerID := toString(teamData["owner_id"])
|
||||
if ownerID != userID {
|
||||
return nil, fmt.Errorf("access denied: user does not own this team")
|
||||
}
|
||||
|
||||
return teamData, nil
|
||||
}
|
||||
|
||||
// teamCreate handles the business logic for creating a user team
|
||||
func teamCreate(ctx context.Context, userID string, teamData maps.MapStrAny) (string, error) {
|
||||
// Get user provider instance
|
||||
provider, err := getUserProvider()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get user provider: %w", err)
|
||||
}
|
||||
|
||||
// Set owner and default values
|
||||
teamData["owner_id"] = userID
|
||||
teamData["status"] = "active"
|
||||
teamData["is_verified"] = false
|
||||
teamData["created_at"] = time.Now()
|
||||
teamData["updated_at"] = time.Now()
|
||||
|
||||
// Create team
|
||||
teamID, err := provider.CreateTeam(ctx, teamData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create team: %w", err)
|
||||
}
|
||||
|
||||
return teamID, nil
|
||||
}
|
||||
|
||||
// teamUpdate handles the business logic for updating a user team
|
||||
func teamUpdate(ctx context.Context, userID, teamID string, updateData maps.MapStrAny) error {
|
||||
// Get user provider instance
|
||||
provider, err := getUserProvider()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user provider: %w", err)
|
||||
}
|
||||
|
||||
// Check if team exists and user owns it
|
||||
teamData, err := provider.GetTeam(ctx, teamID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("team not found or access denied: %w", err)
|
||||
}
|
||||
|
||||
// Check ownership
|
||||
ownerID := toString(teamData["owner_id"])
|
||||
if ownerID != userID {
|
||||
return fmt.Errorf("access denied: user does not own this team")
|
||||
}
|
||||
|
||||
// Add updated_at timestamp
|
||||
updateData["updated_at"] = time.Now()
|
||||
|
||||
// Update team
|
||||
err = provider.UpdateTeam(ctx, teamID, updateData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update team: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// teamDelete handles the business logic for deleting a user team
|
||||
func teamDelete(ctx context.Context, userID, teamID string) error {
|
||||
// Get user provider instance
|
||||
provider, err := getUserProvider()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user provider: %w", err)
|
||||
}
|
||||
|
||||
// Check if team exists and user owns it
|
||||
teamData, err := provider.GetTeam(ctx, teamID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("team not found or access denied: %w", err)
|
||||
}
|
||||
|
||||
// Check ownership
|
||||
ownerID := toString(teamData["owner_id"])
|
||||
if ownerID != userID {
|
||||
return fmt.Errorf("access denied: user does not own this team")
|
||||
}
|
||||
|
||||
// Delete team
|
||||
err = provider.DeleteTeam(ctx, teamID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete team: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Private Helper Functions (internal use only)
|
||||
|
||||
// getUserProvider gets the user provider from the global OAuth service
|
||||
func getUserProvider() (*user.DefaultUser, error) {
|
||||
// Check if global OAuth service is initialized
|
||||
if oauth.OAuth == nil {
|
||||
return nil, fmt.Errorf("OAuth service not initialized")
|
||||
}
|
||||
|
||||
// Get user provider from OAuth service
|
||||
userProvider, err := oauth.OAuth.GetUserProvider()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user provider: %w", err)
|
||||
}
|
||||
|
||||
// Type assert to DefaultUser (this should be safe based on the OAuth service implementation)
|
||||
if defaultUser, ok := userProvider.(*user.DefaultUser); ok {
|
||||
return defaultUser, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("user provider is not of type DefaultUser")
|
||||
}
|
||||
|
||||
// mapToTeamResponse converts a map to TeamResponse
|
||||
func mapToTeamResponse(data maps.MapStr) TeamResponse {
|
||||
team := TeamResponse{
|
||||
ID: toInt64(data["id"]),
|
||||
TeamID: toString(data["team_id"]),
|
||||
Name: toString(data["name"]),
|
||||
Description: toString(data["description"]),
|
||||
OwnerID: toString(data["owner_id"]),
|
||||
Status: toString(data["status"]),
|
||||
IsVerified: toBool(data["is_verified"]),
|
||||
VerifiedBy: toString(data["verified_by"]),
|
||||
VerifiedAt: toTimeString(data["verified_at"]),
|
||||
CreatedAt: toTimeString(data["created_at"]),
|
||||
UpdatedAt: toTimeString(data["updated_at"]),
|
||||
}
|
||||
|
||||
return team
|
||||
}
|
||||
|
||||
// mapToTeamDetailResponse converts a map to TeamDetailResponse
|
||||
func mapToTeamDetailResponse(data maps.MapStr) TeamDetailResponse {
|
||||
team := TeamDetailResponse{
|
||||
TeamResponse: mapToTeamResponse(data),
|
||||
}
|
||||
|
||||
// Add settings if available
|
||||
if settings, ok := data["settings"]; ok {
|
||||
if settingsMap, ok := settings.(map[string]interface{}); ok {
|
||||
team.Settings = settingsMap
|
||||
}
|
||||
}
|
||||
|
||||
return team
|
||||
}
|
||||
|
|
@ -201,26 +201,40 @@ const (
|
|||
UserInfoSourceAccessToken = "access_token" // Extract user info from access token response
|
||||
)
|
||||
|
||||
// toBool converts various types to boolean
|
||||
// Supports: bool, int, int64, float64, string
|
||||
// Returns false for nil or unsupported types
|
||||
func toBool(v interface{}) bool {
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
// ==== Team API Types ====
|
||||
|
||||
switch val := v.(type) {
|
||||
case bool:
|
||||
return val
|
||||
case int:
|
||||
return val != 0
|
||||
case int64:
|
||||
return val != 0
|
||||
case float64:
|
||||
return val != 0
|
||||
case string:
|
||||
return val == "true" || val == "1"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
// TeamResponse represents a team in API responses
|
||||
type TeamResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
TeamID string `json:"team_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
Status string `json:"status"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
VerifiedBy string `json:"verified_by,omitempty"`
|
||||
VerifiedAt string `json:"verified_at,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TeamDetailResponse represents detailed team information
|
||||
type TeamDetailResponse struct {
|
||||
TeamResponse
|
||||
// Add additional fields that are only included in detailed responses
|
||||
Settings map[string]interface{} `json:"settings,omitempty"`
|
||||
}
|
||||
|
||||
// CreateTeamRequest represents the request to create a team
|
||||
type CreateTeamRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Settings map[string]interface{} `json:"settings,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateTeamRequest represents the request to update a team
|
||||
type UpdateTeamRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Settings map[string]interface{} `json:"settings,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,21 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/gou/process"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register user process handlers
|
||||
process.RegisterGroup("user", map[string]process.Handler{
|
||||
"team.list": ProcessTeamList,
|
||||
"team.get": ProcessTeamGet,
|
||||
"team.create": ProcessTeamCreate,
|
||||
"team.update": ProcessTeamUpdate,
|
||||
"team.delete": ProcessTeamDelete,
|
||||
})
|
||||
}
|
||||
|
||||
// Attach attaches the signin handlers to the router
|
||||
func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
||||
|
||||
|
|
@ -43,11 +55,11 @@ func attachTeam(group *gin.RouterGroup, oauth types.OAuth) {
|
|||
team.Use(oauth.Guard)
|
||||
|
||||
// Team CRUD
|
||||
team.GET("/", placeholder) // Get user teams
|
||||
team.GET("/:team_id", placeholder) // Get user team details
|
||||
team.POST("/", placeholder) // Create user team
|
||||
team.PUT("/:team_id", placeholder) // Update user team
|
||||
team.DELETE("/:team_id", placeholder) // Delete user team
|
||||
team.GET("/", GinTeamList) // Get user teams
|
||||
team.GET("/:team_id", GinTeamGet) // Get user team details
|
||||
team.POST("/", GinTeamCreate) // Create user team
|
||||
team.PUT("/:team_id", GinTeamUpdate) // Update user team
|
||||
team.DELETE("/:team_id", GinTeamDelete) // Delete user team
|
||||
|
||||
// Member Management
|
||||
team.GET("/:team_id/members", placeholder) // Get user team members
|
||||
|
|
|
|||
149
openapi/user/utils.go
Normal file
149
openapi/user/utils.go
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/yaoapp/gou/process"
|
||||
"github.com/yaoapp/gou/session"
|
||||
"github.com/yaoapp/kun/exception"
|
||||
)
|
||||
|
||||
// Session Utilities
|
||||
|
||||
// GetUserIDFromSession gets the current user ID from session
|
||||
// Returns the user ID string or throws an exception if not authenticated
|
||||
func GetUserIDFromSession(process *process.Process) string {
|
||||
sessionData, err := session.Global().ID(process.Sid).Get("__user_id")
|
||||
if err != nil || sessionData == nil {
|
||||
exception.New("user not authenticated", 401).Throw()
|
||||
}
|
||||
|
||||
userIDStr, ok := sessionData.(string)
|
||||
if !ok {
|
||||
exception.New("invalid user_id in session", 401).Throw()
|
||||
}
|
||||
|
||||
return userIDStr
|
||||
}
|
||||
|
||||
// Type Conversion Utilities
|
||||
|
||||
// toBool converts various types to boolean
|
||||
// Supports: bool, int, int64, float64, string
|
||||
// Returns false for nil or unsupported types
|
||||
func toBool(v interface{}) bool {
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch val := v.(type) {
|
||||
case bool:
|
||||
return val
|
||||
case int:
|
||||
return val != 0
|
||||
case int64:
|
||||
return val != 0
|
||||
case float64:
|
||||
return val != 0
|
||||
case string:
|
||||
return val == "true" || val == "1"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// toString converts various types to string
|
||||
// Supports: string, int, int64, float64, bool
|
||||
// Returns empty string for nil or unsupported types
|
||||
func toString(v interface{}) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return val
|
||||
case int:
|
||||
return fmt.Sprintf("%d", val)
|
||||
case int64:
|
||||
return fmt.Sprintf("%d", val)
|
||||
case float64:
|
||||
return fmt.Sprintf("%.0f", val)
|
||||
case bool:
|
||||
if val {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// toInt64 converts various types to int64
|
||||
// Supports: int, int64, float64, string
|
||||
// Returns 0 for nil or unsupported types
|
||||
func toInt64(v interface{}) int64 {
|
||||
if v == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
switch val := v.(type) {
|
||||
case int64:
|
||||
return val
|
||||
case int:
|
||||
return int64(val)
|
||||
case float64:
|
||||
return int64(val)
|
||||
case string:
|
||||
if parsed, err := strconv.ParseInt(val, 10, 64); err == nil {
|
||||
return parsed
|
||||
}
|
||||
return 0
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// toTimeString converts various time types to RFC3339 string
|
||||
// Supports: time.Time, string, int64 (unix timestamp)
|
||||
// Returns empty string for nil or unsupported types
|
||||
func toTimeString(v interface{}) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch val := v.(type) {
|
||||
case time.Time:
|
||||
if val.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return val.Format(time.RFC3339)
|
||||
case string:
|
||||
// Try to parse as RFC3339 first
|
||||
if t, err := time.Parse(time.RFC3339, val); err == nil {
|
||||
return t.Format(time.RFC3339)
|
||||
}
|
||||
// Try to parse as other common formats
|
||||
formats := []string{
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02T15:04:05Z",
|
||||
"2006-01-02T15:04:05.000Z",
|
||||
}
|
||||
for _, format := range formats {
|
||||
if t, err := time.Parse(format, val); err == nil {
|
||||
return t.Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
return val // Return as-is if can't parse
|
||||
case int64:
|
||||
// Assume unix timestamp
|
||||
if val > 0 {
|
||||
return time.Unix(val, 0).Format(time.RFC3339)
|
||||
}
|
||||
return ""
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue