Merge pull request #1174 from trheyi/main

Enhance user login and team configuration management
This commit is contained in:
Max 2025-10-07 10:27:39 +08:00 committed by GitHub
commit a4d0c0c9ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 740 additions and 40 deletions

View file

@ -0,0 +1,59 @@
package user_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/yao/openapi/user"
)
// TestGetTeamConfigFunction tests the GetTeamConfig function
func TestGetTeamConfigFunction(t *testing.T) {
// Test with empty locale
teamConfig := user.GetTeamConfig("")
assert.Nil(t, teamConfig, "Should return nil when no config is loaded")
// Test with specific locale
teamConfig = user.GetTeamConfig("en")
assert.Nil(t, teamConfig, "Should return nil when no config is loaded")
// Test with invalid locale
teamConfig = user.GetTeamConfig("invalid")
assert.Nil(t, teamConfig, "Should return nil when no config is loaded")
}
// TestGetPublicConfigFunction tests the GetPublicConfig function
func TestGetPublicConfigFunction(t *testing.T) {
// Test with empty locale
publicConfig := user.GetPublicConfig("")
assert.Nil(t, publicConfig, "Should return nil when no config is loaded")
// Test with specific locale
publicConfig = user.GetPublicConfig("en")
assert.Nil(t, publicConfig, "Should return nil when no config is loaded")
// Test with invalid locale
publicConfig = user.GetPublicConfig("invalid")
assert.Nil(t, publicConfig, "Should return nil when no config is loaded")
}
// TestGetYaoClientConfigFunction tests the GetYaoClientConfig function
func TestGetYaoClientConfigFunction(t *testing.T) {
// Test when no client config is loaded
clientConfig := user.GetYaoClientConfig()
assert.Nil(t, clientConfig, "Should return nil when no client config is loaded")
}
// TestGetProviderFunction tests the GetProvider function
func TestGetProviderFunction(t *testing.T) {
// Test with non-existent provider
provider, err := user.GetProvider("non-existent")
assert.Error(t, err, "Should return error for non-existent provider")
assert.Nil(t, provider, "Should return nil provider for non-existent provider")
assert.Contains(t, err.Error(), "not found", "Error should contain 'not found'")
// Test with empty provider ID
provider, err = user.GetProvider("")
assert.Error(t, err, "Should return error for empty provider ID")
assert.Nil(t, provider, "Should return nil provider for empty provider ID")
}

View file

@ -0,0 +1,98 @@
package user_test
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/yao/openapi/user"
)
// TestConfigTypes tests that our new configuration types are properly defined
func TestConfigTypes(t *testing.T) {
// Test TeamConfig type
teamConfig := &user.TeamConfig{
Roles: []*user.TeamRole{
{
RoleID: "team_owner",
Label: "Owner",
Description: "Full access to team settings",
},
},
Invite: &user.InviteConfig{
Channel: "default",
Expiry: "1d",
Templates: map[string]string{
"mail": "en.invite_message",
"sms": "en.invite_message",
},
},
}
assert.NotNil(t, teamConfig, "TeamConfig should not be nil")
assert.Len(t, teamConfig.Roles, 1, "Should have 1 role")
assert.Equal(t, "team_owner", teamConfig.Roles[0].RoleID, "Role ID should match")
assert.Equal(t, "Owner", teamConfig.Roles[0].Label, "Role label should match")
assert.NotNil(t, teamConfig.Invite, "Invite config should not be nil")
assert.Equal(t, "default", teamConfig.Invite.Channel, "Channel should match")
assert.Equal(t, "1d", teamConfig.Invite.Expiry, "Expiry should match")
assert.Len(t, teamConfig.Invite.Templates, 2, "Should have 2 templates")
// Test TeamRole type
role := &user.TeamRole{
RoleID: "team_admin",
Label: "Admin",
Description: "Manage team members",
}
assert.Equal(t, "team_admin", role.RoleID, "Role ID should match")
assert.Equal(t, "Admin", role.Label, "Role label should match")
assert.Equal(t, "Manage team members", role.Description, "Description should match")
// Test InviteConfig type
inviteConfig := &user.InviteConfig{
Channel: "email",
Expiry: "24h",
Templates: map[string]string{
"mail": "zh-cn.invite_message",
},
}
assert.Equal(t, "email", inviteConfig.Channel, "Channel should match")
assert.Equal(t, "24h", inviteConfig.Expiry, "Expiry should match")
assert.Len(t, inviteConfig.Templates, 1, "Should have 1 template")
assert.Equal(t, "zh-cn.invite_message", inviteConfig.Templates["mail"], "Template should match")
}
// TestConfigTypeCompatibility tests that our types are compatible with JSON marshaling
func TestConfigTypeCompatibility(t *testing.T) {
// Test TeamConfig JSON marshaling
teamConfig := &user.TeamConfig{
Roles: []*user.TeamRole{
{
RoleID: "test_role",
Label: "Test Role",
Description: "A test role",
},
},
Invite: &user.InviteConfig{
Channel: "test",
Expiry: "1h",
Templates: map[string]string{
"test": "test_template",
},
},
}
// Test that the struct can be marshaled to JSON using standard library
jsonData, err := json.Marshal(teamConfig)
assert.NoError(t, err, "Should marshal to JSON without error")
assert.NotEmpty(t, jsonData, "JSON data should not be empty")
// Test that the struct can be unmarshaled from JSON
var unmarshaledConfig user.TeamConfig
err = json.Unmarshal(jsonData, &unmarshaledConfig)
assert.NoError(t, err, "Should unmarshal from JSON without error")
assert.Equal(t, teamConfig.Roles[0].RoleID, unmarshaledConfig.Roles[0].RoleID, "Role ID should match after unmarshaling")
assert.Equal(t, teamConfig.Invite.Channel, unmarshaledConfig.Invite.Channel, "Channel should match after unmarshaling")
}

View file

@ -0,0 +1,99 @@
package user_test
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestConfigValidationLogic tests the configuration validation logic
func TestConfigValidationLogic(t *testing.T) {
// Test cases for different configuration scenarios
testCases := []struct {
name string
clientID string
clientSecret string
shouldPass bool
expectedError string
}{
{
name: "valid_direct_values",
clientID: "12345678901234567890123456789012",
clientSecret: "direct-secret-value",
shouldPass: true,
expectedError: "",
},
{
name: "empty_client_id",
clientID: "",
clientSecret: "some-secret",
shouldPass: false,
expectedError: "client_id is required but not set",
},
{
name: "empty_client_secret",
clientID: "12345678901234567890123456789012",
clientSecret: "",
shouldPass: false,
expectedError: "client_secret is required but not set",
},
{
name: "unresolved_env_var_client_id",
clientID: "$ENV.MISSING_VAR",
clientSecret: "some-secret",
shouldPass: false,
expectedError: "environment variable 'MISSING_VAR' is required but not set",
},
{
name: "unresolved_env_var_client_secret",
clientID: "12345678901234567890123456789012",
clientSecret: "$ENV.MISSING_SECRET",
shouldPass: false,
expectedError: "environment variable 'MISSING_SECRET' is required but not set",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// This is a conceptual test - in practice, we'd test the actual validation logic
t.Logf("Testing scenario: %s", tc.name)
t.Logf("ClientID: %s, ClientSecret: %s", tc.clientID, tc.clientSecret)
if tc.shouldPass {
t.Logf("Expected: Should pass validation")
} else {
t.Logf("Expected: Should fail with error: %s", tc.expectedError)
}
// This test documents the expected behavior
assert.True(t, true, "Validation logic should be tested through integration tests")
})
}
}
// TestEnvVarNameExtraction tests the environment variable name extraction
func TestEnvVarNameExtraction(t *testing.T) {
// Test different environment variable formats
testCases := []struct {
input string
expected string
}{
{"$ENV.SIGNIN_CLIENT_ID", "SIGNIN_CLIENT_ID"},
{"$ENV.CUSTOM_VAR", "CUSTOM_VAR"},
{"${MY_VAR}", "MY_VAR"},
{"$SIMPLE_VAR", "SIMPLE_VAR"},
{"", "unknown"},
{"not_a_var", "unknown"},
{"direct_value", "unknown"},
}
for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
t.Logf("Input: %s, Expected: %s", tc.input, tc.expected)
// This test documents the expected behavior of extractEnvVarName
// In practice, we'd need to make the function public or test it through integration
assert.True(t, true, "Function behavior should be tested through integration tests")
})
}
}

View file

@ -0,0 +1,24 @@
package user_test
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestEnvironmentVariables(t *testing.T) {
// Test that environment variables are available
signinClientID := os.Getenv("SIGNIN_CLIENT_ID")
signinClientSecret := os.Getenv("SIGNIN_CLIENT_SECRET")
t.Logf("SIGNIN_CLIENT_ID: %s (length: %d)", signinClientID, len(signinClientID))
t.Logf("SIGNIN_CLIENT_SECRET: %s (length: %d)", signinClientSecret, len(signinClientSecret))
// Check if environment variables are set
assert.NotEmpty(t, signinClientID, "SIGNIN_CLIENT_ID should be set")
assert.NotEmpty(t, signinClientSecret, "SIGNIN_CLIENT_SECRET should be set")
// Check if client ID is exactly 32 characters
assert.Equal(t, 32, len(signinClientID), "SIGNIN_CLIENT_ID should be exactly 32 characters")
}

View file

@ -0,0 +1,70 @@
package user_test
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestExtractEnvVarName tests the extractEnvVarName function
func TestExtractEnvVarName(t *testing.T) {
// Import the user package to access the function
// Note: This test assumes the function is exported or we can test it indirectly
testCases := []struct {
input string
expected string
}{
{"$ENV.SIGNIN_CLIENT_ID", "SIGNIN_CLIENT_ID"},
{"$ENV.CUSTOM_VAR", "CUSTOM_VAR"},
{"${MY_VAR}", "MY_VAR"},
{"$SIMPLE_VAR", "SIMPLE_VAR"},
{"", "unknown"},
{"not_a_var", "unknown"},
}
for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
// Since extractEnvVarName is not exported, we'll test the behavior indirectly
// by checking if the error message contains the correct variable name
t.Logf("Testing input: %s, expected: %s", tc.input, tc.expected)
// This is a conceptual test - in practice, we'd need to make the function public
// or test it through the public API
assert.True(t, true, "Function behavior should be tested through integration tests")
})
}
}
// TestEnvVarNameExtractionIntegration tests the environment variable name extraction through integration
func TestEnvVarNameExtractionIntegration(t *testing.T) {
// This test verifies that the error message correctly identifies the missing environment variable
// by checking the actual error message format
// Test with a custom environment variable name
testCases := []struct {
name string
envVar string
expected string
}{
{
name: "SIGNIN_CLIENT_ID",
envVar: "SIGNIN_CLIENT_ID",
expected: "SIGNIN_CLIENT_ID",
},
{
name: "CUSTOM_CLIENT_ID",
envVar: "CUSTOM_CLIENT_ID",
expected: "CUSTOM_CLIENT_ID",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// This test would require modifying the client.yao file to use different env var names
// For now, we'll just verify the expected behavior conceptually
t.Logf("Expected error message should contain: environment variable '%s' is required but not set", tc.expected)
assert.Equal(t, tc.expected, tc.expected, "Error message should contain the correct environment variable name")
})
}
}

View file

@ -30,16 +30,16 @@ func TestUserLoginConfig(t *testing.T) {
// Note: user.Load is automatically called by openapi.Load in testutils.Prepare
// Test API endpoints
// Test API endpoints for signin configuration
testCases := []struct {
name string
endpoint string
expectCode int
}{
{"get config without locale", "/user/login", 200},
{"get config with en locale", "/user/login?locale=en", 200},
{"get config with zh-cn locale", "/user/login?locale=zh-cn", 200},
{"get config with invalid locale", "/user/login?locale=invalid", 200}, // should fallback to default
{"get login config without locale", "/user/login", 200},
{"get login config with en locale", "/user/login?locale=en", 200},
{"get login config with zh-cn locale", "/user/login?locale=zh-cn", 200},
{"get login config with invalid locale", "/user/login?locale=invalid", 200}, // should fallback to default
}
for _, tc := range testCases {

View file

@ -0,0 +1,176 @@
package user_test
import (
"encoding/json"
"io"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/openapi"
"github.com/yaoapp/yao/openapi/tests/testutils"
"github.com/yaoapp/yao/openapi/user"
)
func TestTeamConfigLoad(t *testing.T) {
// Initialize test environment
serverURL := testutils.Prepare(t)
defer testutils.Clean()
_ = serverURL // Server URL not needed for this test
// Test loading team configurations
err := user.Load(config.Conf)
assert.NoError(t, err, "user.Load should succeed")
// Test that we can get team config
teamConfig := user.GetTeamConfig("")
if teamConfig != nil {
t.Logf("Team config loaded with %d roles", len(teamConfig.Roles))
assert.IsType(t, &user.TeamConfig{}, teamConfig, "Should return correct team config type")
} else {
t.Log("No team config found")
}
}
func TestTeamConfigStructure(t *testing.T) {
// Initialize test environment
serverURL := testutils.Prepare(t)
defer testutils.Clean()
_ = serverURL // Server URL not needed for this test
// Note: user.Load is automatically called by openapi.Load in testutils.Prepare
// Get a team config to test structure
teamConfig := user.GetTeamConfig("")
if teamConfig != nil {
t.Logf("Team config loaded successfully with %d roles", len(teamConfig.Roles))
// Verify team config structure is valid
assert.IsType(t, &user.TeamConfig{}, teamConfig, "Should return correct team config type")
// Test roles configuration
if teamConfig.Roles != nil {
assert.IsType(t, []*user.TeamRole{}, teamConfig.Roles, "Roles should be slice of TeamRole pointers")
for i, role := range teamConfig.Roles {
t.Logf("Role %d: %s (%s)", i, role.RoleID, role.Label)
assert.NotEmpty(t, role.RoleID, "Role ID should not be empty")
assert.NotEmpty(t, role.Label, "Role label should not be empty")
assert.NotEmpty(t, role.Description, "Role description should not be empty")
}
}
// Test invite configuration
if teamConfig.Invite != nil {
t.Logf("Invite config found: channel=%s, expiry=%s", teamConfig.Invite.Channel, teamConfig.Invite.Expiry)
assert.IsType(t, &user.InviteConfig{}, teamConfig.Invite, "Invite should be InviteConfig type")
if teamConfig.Invite.Templates != nil {
assert.IsType(t, map[string]string{}, teamConfig.Invite.Templates, "Templates should be map[string]string")
for templateType, templateName := range teamConfig.Invite.Templates {
t.Logf("Template %s: %s", templateType, templateName)
assert.NotEmpty(t, templateName, "Template name should not be empty")
}
}
}
} else {
t.Log("No team configuration found")
}
}
func TestTeamConfigByLocale(t *testing.T) {
// Initialize test environment
serverURL := testutils.Prepare(t)
defer testutils.Clean()
_ = serverURL // Server URL not needed for this test
// Test different locales
locales := []string{"en", "zh-cn", "invalid", ""}
for _, locale := range locales {
t.Run("locale_"+locale, func(t *testing.T) {
teamConfig := user.GetTeamConfig(locale)
if teamConfig != nil {
t.Logf("Team config for locale '%s' loaded with %d roles", locale, len(teamConfig.Roles))
assert.IsType(t, &user.TeamConfig{}, teamConfig, "Should return correct team config type")
} else {
t.Logf("No team config found for locale '%s'", locale)
}
})
}
}
func TestTeamConfigAPI(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 first (needed for user.Load validation)
testClient := testutils.RegisterTestClient(t, "Team Config Test Client", []string{"https://localhost/callback"})
defer testutils.CleanupTestClient(t, testClient.ClientID)
// Test API endpoints for team configuration
testCases := []struct {
name string
endpoint string
expectCode int
}{
{"get team config without locale", "/user/teams/config", 200},
{"get team config with en locale", "/user/teams/config?locale=en", 200},
{"get team config with zh-cn locale", "/user/teams/config?locale=zh-cn", 200},
{"get team config with invalid locale", "/user/teams/config?locale=invalid", 200}, // should fallback to default
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
requestURL := serverURL + baseURL + tc.endpoint
resp, err := http.Get(requestURL)
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", tc.expectCode)
if resp.StatusCode == 200 {
// Parse response body
body, err := io.ReadAll(resp.Body)
assert.NoError(t, err, "Should read response body")
var teamConfig user.TeamConfig
err = json.Unmarshal(body, &teamConfig)
assert.NoError(t, err, "Should parse JSON response")
t.Logf("API response for %s: %d roles", tc.endpoint, len(teamConfig.Roles))
// Verify team config structure
assert.IsType(t, &user.TeamConfig{}, &teamConfig, "Should return correct team config type")
// Test roles if present
if teamConfig.Roles != nil {
assert.IsType(t, []*user.TeamRole{}, teamConfig.Roles, "Roles should be slice of TeamRole pointers")
for i, role := range teamConfig.Roles {
t.Logf("Role %d: %s (%s)", i, role.RoleID, role.Label)
assert.NotEmpty(t, role.RoleID, "Role ID should not be empty")
assert.NotEmpty(t, role.Label, "Role label should not be empty")
}
}
// Test invite config if present
if teamConfig.Invite != nil {
t.Logf("Invite config: channel=%s, expiry=%s", teamConfig.Invite.Channel, teamConfig.Invite.Expiry)
assert.IsType(t, &user.InviteConfig{}, teamConfig.Invite, "Invite should be InviteConfig type")
}
}
}
})
}
}

View file

@ -12,6 +12,7 @@ import (
"time"
"github.com/yaoapp/gou/application"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/openapi/oauth"
"github.com/yaoapp/yao/openapi/oauth/types"
@ -30,6 +31,8 @@ var (
providers = make(map[string]*Provider)
// Default configuration (marked with default: true)
defaultConfig *Config
// Team configurations by locale
teamConfigs = make(map[string]*TeamConfig)
// Mutex for thread safety
configMutex sync.RWMutex
)
@ -44,13 +47,20 @@ func Load(appConfig config.Config) error {
publicConfigs = make(map[string]*Config)
providers = make(map[string]*Provider)
defaultConfig = nil
teamConfigs = make(map[string]*TeamConfig)
// Load signin configurations
// Load signin configurations from openapi/user/signin directory
err := loadSigninConfigs(appConfig.Root)
if err != nil {
return fmt.Errorf("failed to load signin configs: %v", err)
}
// Load team configurations from openapi/user/team directory
err = loadTeamConfigs(appConfig.Root)
if err != nil {
return fmt.Errorf("failed to load team configs: %v", err)
}
// Load providers first
err = loadProviders(appConfig.Root)
if err != nil {
@ -94,6 +104,25 @@ func loadClientConfig() error {
clientConfig.ClientID = replaceENVVar(clientConfig.ClientID)
clientConfig.ClientSecret = replaceENVVar(clientConfig.ClientSecret)
// Check if required values are missing or unresolved
if clientConfig.ClientID == "" {
return fmt.Errorf("client_id is required but not set")
}
// Check if ClientID still contains unresolved environment variable references
if strings.HasPrefix(clientConfig.ClientID, "$ENV.") || strings.HasPrefix(clientConfig.ClientID, "${") || strings.HasPrefix(clientConfig.ClientID, "$") {
envVarName := extractEnvVarName(clientConfig.ClientID)
return fmt.Errorf("environment variable '%s' is required but not set", envVarName)
}
// ClientSecret is optional - if it contains unresolved environment variable references, set it to empty
// This allows the system to generate a new secret during client registration
if clientConfig.ClientSecret != "" && (strings.HasPrefix(clientConfig.ClientSecret, "$ENV.") || strings.HasPrefix(clientConfig.ClientSecret, "${") || strings.HasPrefix(clientConfig.ClientSecret, "$")) {
// Log a warning but don't fail - the system will generate a new secret
log.Warn("Client secret environment variable not set, will generate new secret during registration")
clientConfig.ClientSecret = ""
}
// Validate client config
err = validateClientConfig(&clientConfig)
if err != nil {
@ -216,10 +245,10 @@ func loadProviders(_ string) error {
return nil
}
// loadSigninConfigs loads all signin configurations from the openapi/signin directory
// loadSigninConfigs loads all signin configurations from the openapi/user/signin directory
func loadSigninConfigs(_ string) error {
// Use Walk to find all configuration files in the user directory
err := application.App.Walk("openapi/user", func(root, filename string, isdir bool) error {
// Use Walk to find all configuration files in the signin directory
err := application.App.Walk("openapi/user/signin", func(root, filename string, isdir bool) error {
if isdir {
return nil
}
@ -229,11 +258,6 @@ func loadSigninConfigs(_ string) error {
return nil
}
// Skip providers directory and client.yao file
if strings.Contains(filename, "providers/") || filepath.Base(filename) == "client.yao" {
return nil
}
// Extract locale from filename (basename without extension)
baseName := filepath.Base(filename)
locale := strings.ToLower(strings.TrimSuffix(baseName, ".yao"))
@ -241,14 +265,14 @@ func loadSigninConfigs(_ string) error {
// Read configuration
configRaw, err := application.App.Read(filename)
if err != nil {
return fmt.Errorf("failed to read config %s: %v", filename, err)
return fmt.Errorf("failed to read signin config %s: %v", filename, err)
}
// Parse the configuration
var config Config
err = application.Parse(filename, configRaw, &config)
if err != nil {
return fmt.Errorf("failed to parse config %s: %v", filename, err)
return fmt.Errorf("failed to parse signin config %s: %v", filename, err)
}
// Process ENV variables in the configuration
@ -290,6 +314,49 @@ func loadSigninConfigs(_ string) error {
return nil
}
// loadTeamConfigs loads all team configurations from the openapi/user/team directory
func loadTeamConfigs(_ string) error {
// Use Walk to find all configuration files in the team directory
err := application.App.Walk("openapi/user/team", func(root, filename string, isdir bool) error {
if isdir {
return nil
}
// Only process .yao files
if !strings.HasSuffix(filename, ".yao") {
return nil
}
// Extract locale from filename (basename without extension)
baseName := filepath.Base(filename)
locale := strings.ToLower(strings.TrimSuffix(baseName, ".yao"))
// Read configuration
configRaw, err := application.App.Read(filename)
if err != nil {
return fmt.Errorf("failed to read team config %s: %v", filename, err)
}
// Parse the configuration
var teamConfig TeamConfig
err = application.Parse(filename, configRaw, &teamConfig)
if err != nil {
return fmt.Errorf("failed to parse team config %s: %v", filename, err)
}
// Store team configuration
teamConfigs[locale] = &teamConfig
return nil
})
if err != nil {
return fmt.Errorf("failed to walk team directory: %v", err)
}
return nil
}
// GetPublicConfig returns the public configuration for a given locale
func GetPublicConfig(locale string) *Config {
configMutex.RLock()
@ -345,6 +412,53 @@ func GetYaoClientConfig() *YaoClientConfig {
return yaoClientConfig
}
// GetTeamConfig returns the team configuration for a given locale
func GetTeamConfig(locale string) *TeamConfig {
configMutex.RLock()
defer configMutex.RUnlock()
// Normalize language code to lowercase
if locale != "" {
locale = strings.ToLower(locale)
}
// Try to get the specific locale configuration
if config, exists := teamConfigs[locale]; exists {
return config
}
// If no specific locale, try to get any available configuration
for _, config := range teamConfigs {
return config
}
return nil
}
// extractEnvVarName extracts the environment variable name from a string like "$ENV.VAR_NAME"
func extractEnvVarName(value string) string {
if value == "" {
return "unknown"
}
// Handle $ENV.VAR_NAME format
if strings.HasPrefix(value, "$ENV.") {
return strings.TrimPrefix(value, "$ENV.")
}
// Handle ${VAR_NAME} format
if strings.HasPrefix(value, "${") && strings.HasSuffix(value, "}") {
return value[2 : len(value)-1]
}
// Handle $VAR_NAME format
if strings.HasPrefix(value, "$") {
return value[1:]
}
return "unknown"
}
// replaceENVVar replaces environment variables in a string
func replaceENVVar(value string) string {
if value == "" {

View file

@ -21,6 +21,23 @@ import (
// Team Management Handlers
// Public Team Configuration Endpoint
// GinTeamConfig handles GET /teams/config - Get team configuration (public)
func GinTeamConfig(c *gin.Context) {
locale := c.Query("locale")
if locale == "" {
locale = "en" // default locale
}
config := GetTeamConfig(locale)
if config == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Team configuration not found"})
return
}
c.JSON(http.StatusOK, config)
}
// GinTeamList handles GET /teams - Get user teams
func GinTeamList(c *gin.Context) {
// Get authorized user info
@ -105,7 +122,7 @@ func GinTeamList(c *gin.Context) {
c.JSON(http.StatusOK, result)
}
// GinTeamGet handles GET /teams/:team_id - Get user team details
// GinTeamGet handles GET /teams/:id - Get user team details
func GinTeamGet(c *gin.Context) {
// Get authorized user info
authInfo := oauth.GetAuthorizedInfo(c)
@ -118,7 +135,7 @@ func GinTeamGet(c *gin.Context) {
return
}
teamID := c.Param("team_id")
teamID := c.Param("id")
if teamID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
@ -246,7 +263,7 @@ func GinTeamCreate(c *gin.Context) {
c.JSON(http.StatusCreated, team)
}
// GinTeamUpdate handles PUT /teams/:team_id - Update user team
// GinTeamUpdate handles PUT /teams/:id - Update user team
func GinTeamUpdate(c *gin.Context) {
// Get authorized user info
authInfo := oauth.GetAuthorizedInfo(c)
@ -259,7 +276,7 @@ func GinTeamUpdate(c *gin.Context) {
return
}
teamID := c.Param("team_id")
teamID := c.Param("id")
if teamID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
@ -340,7 +357,7 @@ func GinTeamUpdate(c *gin.Context) {
c.JSON(http.StatusOK, team)
}
// GinTeamDelete handles DELETE /teams/:team_id - Delete user team
// GinTeamDelete handles DELETE /teams/:id - Delete user team
func GinTeamDelete(c *gin.Context) {
// Get authorized user info
authInfo := oauth.GetAuthorizedInfo(c)
@ -353,7 +370,7 @@ func GinTeamDelete(c *gin.Context) {
return
}
teamID := c.Param("team_id")
teamID := c.Param("id")
if teamID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,

View file

@ -317,3 +317,25 @@ type CreateInvitationRequest struct {
Message string `json:"message,omitempty"`
Settings map[string]interface{} `json:"settings,omitempty"`
}
// ==== Team Configuration Types ====
// TeamConfig represents the team configuration loaded from DSL files
type TeamConfig struct {
Roles []*TeamRole `json:"roles,omitempty"`
Invite *InviteConfig `json:"invite,omitempty"`
}
// TeamRole represents a team role configuration
type TeamRole struct {
RoleID string `json:"role_id"`
Label string `json:"label"`
Description string `json:"description"`
}
// InviteConfig represents the invitation configuration
type InviteConfig struct {
Channel string `json:"channel,omitempty"`
Expiry string `json:"expiry,omitempty"`
Templates map[string]string `json:"templates,omitempty"`
}

View file

@ -52,28 +52,33 @@ func Attach(group *gin.RouterGroup, oauth types.OAuth) {
// User Team Management
func attachTeam(group *gin.RouterGroup, oauth types.OAuth) {
team := group.Group("/teams")
// Public endpoints (no authentication required)
team.GET("/config", GinTeamConfig) // Get team configuration (public)
// Protected endpoints (authentication required)
team.Use(oauth.Guard)
// Team CRUD
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
// Team CRUD - Standard REST endpoints
team.GET("/", GinTeamList) // GET /teams - List user teams
team.POST("/", GinTeamCreate) // POST /teams - Create new team
team.GET("/:id", GinTeamGet) // GET /teams/:id - Get team details
team.PUT("/:id", GinTeamUpdate) // PUT /teams/:id - Update team
team.DELETE("/:id", GinTeamDelete) // DELETE /teams/:id - Delete team
// Member Management
team.GET("/:team_id/members", GinMemberList) // Get user team members
team.GET("/:team_id/members/:member_id", GinMemberGet) // Get user team member details
team.POST("/:team_id/members/direct", GinMemberCreateDirect) // Add member directly (for bots/system)
team.PUT("/:team_id/members/:member_id", GinMemberUpdate) // Update user team member
team.DELETE("/:team_id/members/:member_id", GinMemberDelete) // Remove user team member
// Team Members - Nested resource endpoints
team.GET("/:id/members", GinMemberList) // GET /teams/:id/members - List team members
team.POST("/:id/members", GinMemberCreateDirect) // POST /teams/:id/members - Add team member
team.GET("/:id/members/:member_id", GinMemberGet) // GET /teams/:id/members/:member_id - Get member details
team.PUT("/:id/members/:member_id", GinMemberUpdate) // PUT /teams/:id/members/:member_id - Update member
team.DELETE("/:id/members/:member_id", GinMemberDelete) // DELETE /teams/:id/members/:member_id - Remove member
// Member Invitation Management
team.POST("/:team_id/invitations", GinInvitationCreate) // Send team invitation
team.GET("/:team_id/invitations", GinInvitationList) // Get team invitations
team.GET("/:team_id/invitations/:invitation_id", GinInvitationGet) // Get invitation details
team.PUT("/:team_id/invitations/:invitation_id/resend", GinInvitationResend) // Resend invitation
team.DELETE("/:team_id/invitations/:invitation_id", GinInvitationDelete) // Cancel invitation
// Team Invitations - Nested resource endpoints
team.GET("/:id/invitations", GinInvitationList) // GET /teams/:id/invitations - List invitations
team.POST("/:id/invitations", GinInvitationCreate) // POST /teams/:id/invitations - Send invitation
team.GET("/:id/invitations/:invitation_id", GinInvitationGet) // GET /teams/:id/invitations/:invitation_id - Get invitation
team.PUT("/:id/invitations/:invitation_id/resend", GinInvitationResend) // PUT /teams/:id/invitations/:invitation_id/resend - Resend invitation
team.DELETE("/:id/invitations/:invitation_id", GinInvitationDelete) // DELETE /teams/:id/invitations/:invitation_id - Cancel invitation
}
// Invitation Response Management (Cross-module invitation handling)
@ -250,6 +255,22 @@ func attachThirdParty(group *gin.RouterGroup, oauth types.OAuth) {
}
// getTeamConfig returns the team configuration
func getTeamConfig(c *gin.Context) {
locale := c.Query("locale")
if locale == "" {
locale = "en" // default locale
}
config := GetTeamConfig(locale)
if config == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Team configuration not found"})
return
}
c.JSON(http.StatusOK, config)
}
func placeholder(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Hello, World!"})
}