diff --git a/openapi/tests/user/config_functions_test.go b/openapi/tests/user/config_functions_test.go index 06763202..76c24371 100644 --- a/openapi/tests/user/config_functions_test.go +++ b/openapi/tests/user/config_functions_test.go @@ -22,19 +22,19 @@ func TestGetTeamConfigFunction(t *testing.T) { assert.Nil(t, teamConfig, "Should return nil when no config is loaded") } -// TestGetPublicConfigFunction tests the GetPublicConfig function -func TestGetPublicConfigFunction(t *testing.T) { +// TestGetEntryConfigFunction tests the GetEntryConfig function +func TestGetEntryConfigFunction(t *testing.T) { // Test with empty locale - publicConfig := user.GetPublicConfig("") - assert.Nil(t, publicConfig, "Should return nil when no config is loaded") + entryConfig := user.GetEntryConfig("") + assert.Nil(t, entryConfig, "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") + entryConfig = user.GetEntryConfig("en") + assert.Nil(t, entryConfig, "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") + entryConfig = user.GetEntryConfig("invalid") + assert.Nil(t, entryConfig, "Should return nil when no config is loaded") } // TestGetYaoClientConfigFunction tests the GetYaoClientConfig function diff --git a/openapi/tests/user/login_config_test.go b/openapi/tests/user/login_config_test.go index 1051f586..deb733bf 100644 --- a/openapi/tests/user/login_config_test.go +++ b/openapi/tests/user/login_config_test.go @@ -30,16 +30,16 @@ func TestUserLoginConfig(t *testing.T) { // Note: user.Load is automatically called by openapi.Load in testutils.Prepare - // Test API endpoints for signin configuration + // Test API endpoints for entry configuration testCases := []struct { name string endpoint string expectCode int }{ - {"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 + {"get entry config without locale", "/user/entry", 200}, + {"get entry config with en locale", "/user/entry?locale=en", 200}, + {"get entry config with zh-cn locale", "/user/entry?locale=zh-cn", 200}, + {"get entry config with invalid locale", "/user/entry?locale=invalid", 200}, // should fallback to default } for _, tc := range testCases { @@ -57,7 +57,7 @@ func TestUserLoginConfig(t *testing.T) { body, err := io.ReadAll(resp.Body) assert.NoError(t, err, "Should read response body") - var config user.Config + var config user.EntryConfig err = json.Unmarshal(body, &config) assert.NoError(t, err, "Should parse JSON response") @@ -102,13 +102,13 @@ func TestUserLoginConfigLoad(t *testing.T) { err := user.Load(config.Conf) assert.NoError(t, err, "user.Load should succeed") - // Test that we can get public config - publicConfig := user.GetPublicConfig("") - if publicConfig != nil { - t.Logf("Public config loaded with title: %s", publicConfig.Title) - assert.IsType(t, &user.Config{}, publicConfig, "Should return correct config type") + // Test that we can get entry config + entryConfig := user.GetEntryConfig("") + if entryConfig != nil { + t.Logf("Entry config loaded with title: %s", entryConfig.Title) + assert.IsType(t, &user.EntryConfig{}, entryConfig, "Should return correct config type") } else { - t.Log("No public config found") + t.Log("No entry config found") } } @@ -122,19 +122,23 @@ func TestUserLoginConfigStructure(t *testing.T) { // Note: user.Load is automatically called by openapi.Load in testutils.Prepare // Get a config to test structure - config := user.GetPublicConfig("") + config := user.GetEntryConfig("") if config != nil { t.Logf("Config loaded successfully with title: %s", config.Title) // Verify config structure is valid - assert.IsType(t, &user.Config{}, config, "Should return correct config type") + assert.IsType(t, &user.EntryConfig{}, config, "Should return correct config type") // Test new configuration fields assert.IsType(t, "", config.ClientID, "ClientID should be string") assert.IsType(t, "", config.ClientSecret, "ClientSecret should be string") assert.IsType(t, false, config.Default, "Default should be boolean") - t.Logf("Config has ClientID: %t, ClientSecret: %t, Default: %t", - config.ClientID != "", config.ClientSecret != "", config.Default) + assert.IsType(t, false, config.AutoLogin, "AutoLogin should be boolean") + assert.IsType(t, "", config.Role, "Role should be string") + assert.IsType(t, "", config.Type, "Type should be string") + assert.IsType(t, false, config.InviteRequired, "InviteRequired should be boolean") + t.Logf("Config has ClientID: %t, ClientSecret: %t, Default: %t, AutoLogin: %t", + config.ClientID != "", config.ClientSecret != "", config.Default, config.AutoLogin) // Test form configuration if config.Form != nil { @@ -162,6 +166,13 @@ func TestUserLoginConfigStructure(t *testing.T) { } } } + + // Test messenger configuration (for registration) + if config.Messenger != nil { + t.Logf("Messenger configuration found") + assert.IsType(t, "", config.Messenger.Channel, "Messenger channel should be string") + assert.IsType(t, map[string]string{}, config.Messenger.Templates, "Messenger templates should be map") + } } else { t.Log("No user configuration found") } diff --git a/openapi/tests/user/login_test.go b/openapi/tests/user/login_test.go index d4591361..4e1d415f 100644 --- a/openapi/tests/user/login_test.go +++ b/openapi/tests/user/login_test.go @@ -28,7 +28,7 @@ func TestUserLogin(t *testing.T) { // Note: user.Load is automatically called by openapi.Load in testutils.Prepare - // Test login endpoint (currently empty implementation) + // Test entry endpoint (unified login/register, currently empty implementation) testCases := []struct { name string method string @@ -37,16 +37,16 @@ func TestUserLogin(t *testing.T) { expectCode int }{ { - "post login without credentials", + "post entry without credentials", "POST", - "/user/login", + "/user/entry", map[string]interface{}{}, 200, // Currently empty implementation, may change when implemented }, { - "post login with credentials", + "post entry with credentials", "POST", - "/user/login", + "/user/entry", map[string]interface{}{ "username": "testuser", "password": "testpass", @@ -54,9 +54,9 @@ func TestUserLogin(t *testing.T) { 200, // Currently empty implementation, may change when implemented }, { - "post login with email", + "post entry with email", "POST", - "/user/login", + "/user/entry", map[string]interface{}{ "email": "test@example.com", "password": "testpass", @@ -149,7 +149,7 @@ func TestUserLoginValidation(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - requestURL := serverURL + baseURL + "/user/login" + requestURL := serverURL + baseURL + "/user/entry" var req *http.Request var err error diff --git a/openapi/user/config.go b/openapi/user/config.go index 61082624..e9503cbc 100644 --- a/openapi/user/config.go +++ b/openapi/user/config.go @@ -23,14 +23,8 @@ var ( // Client config yaoClientConfig *YaoClientConfig - // Full configurations with sensitive data (for backend use) - fullConfigs = make(map[string]*Config) - // Public configurations without sensitive data (for frontend use) - publicConfigs = make(map[string]*Config) // Global providers map (decoupled from locale-specific configs) providers = make(map[string]*Provider) - // Default configuration (marked with default: true) - defaultConfig *Config // Team configurations by locale teamConfigs = make(map[string]*TeamConfig) // Entry configurations by locale (unified login + register) @@ -45,21 +39,12 @@ func Load(appConfig config.Config) error { defer configMutex.Unlock() // Clear existing configurations - fullConfigs = make(map[string]*Config) - publicConfigs = make(map[string]*Config) providers = make(map[string]*Provider) - defaultConfig = nil teamConfigs = make(map[string]*TeamConfig) entryConfigs = make(map[string]*EntryConfig) - // 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 entry configurations from openapi/user/entry directory - err = loadEntryConfigs(appConfig.Root) + err := loadEntryConfigs(appConfig.Root) if err != nil { return fmt.Errorf("failed to load entry configs: %v", err) } @@ -254,75 +239,6 @@ func loadProviders(_ string) error { return nil } -// loadSigninConfigs loads all signin configurations from the openapi/user/signin directory -func loadSigninConfigs(_ string) 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 - } - - // 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 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 signin config %s: %v", filename, err) - } - - // Process ENV variables in the configuration - processConfigENVVariables(&config) - - // Store full configuration - fullConfigs[locale] = &config - - // Create public configuration (without sensitive data) - publicConfig := config - publicConfig.ClientSecret = "" // Remove sensitive data - - // Remove captcha secret from public config - if publicConfig.Form != nil && publicConfig.Form.Captcha != nil && publicConfig.Form.Captcha.Options != nil { - // Create a copy of captcha options without the secret - captchaOptions := make(map[string]interface{}) - for k, v := range publicConfig.Form.Captcha.Options { - if k != "secret" { - captchaOptions[k] = v - } - } - publicConfig.Form.Captcha.Options = captchaOptions - } - - publicConfigs[locale] = &publicConfig - - // Set as default if marked - if config.Default { - defaultConfig = &config - } - - return nil - }) - - if err != nil { - return fmt.Errorf("failed to walk signin directory: %v", err) - } - - 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 @@ -366,41 +282,6 @@ func loadTeamConfigs(_ string) error { return nil } -// GetPublicConfig returns the public configuration for a given locale -func GetPublicConfig(locale string) *Config { - 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 := publicConfigs[locale]; exists { - return config - } - - // Fallback to default config's public version - if defaultConfig != nil { - // Find the public version of the default config - for lang, fullConfig := range fullConfigs { - if fullConfig == defaultConfig { - if publicConfig, exists := publicConfigs[lang]; exists { - return publicConfig - } - } - } - } - - // If no default, try to get any available configuration - for _, config := range publicConfigs { - return config - } - - return nil -} - // GetProvider returns a provider by ID func GetProvider(providerID string) (*Provider, error) { configMutex.RLock() @@ -570,37 +451,6 @@ func processFormConfigENVVariables(form *FormConfig) []string { return missingEnvVars } -// processConfigENVVariables processes environment variables in the signin configuration -func processConfigENVVariables(config *Config) { - var missingEnvVars []string - - // Process client_id and client_secret - if strings.HasPrefix(config.ClientID, "$ENV.") { - envVar := strings.TrimPrefix(config.ClientID, "$ENV.") - if _, exists := os.LookupEnv(envVar); !exists { - missingEnvVars = append(missingEnvVars, envVar) - } - } - config.ClientID = replaceENVVar(config.ClientID) - - if strings.HasPrefix(config.ClientSecret, "$ENV.") { - envVar := strings.TrimPrefix(config.ClientSecret, "$ENV.") - if _, exists := os.LookupEnv(envVar); !exists { - missingEnvVars = append(missingEnvVars, envVar) - } - } - config.ClientSecret = replaceENVVar(config.ClientSecret) - - // Process form configuration - formMissingVars := processFormConfigENVVariables(config.Form) - missingEnvVars = append(missingEnvVars, formMissingVars...) - - // Log warning for missing environment variables - if len(missingEnvVars) > 0 { - fmt.Printf("Warning: The following environment variables are not set in signin configuration: %v\n", missingEnvVars) - } -} - // loadEntryConfigs loads all entry configurations from the openapi/user/entry directory // Entry config merges signin and register configurations func loadEntryConfigs(_ string) error { diff --git a/openapi/user/login.go b/openapi/user/login.go index 05f8d4e2..ae1c6646 100644 --- a/openapi/user/login.go +++ b/openapi/user/login.go @@ -15,45 +15,9 @@ import ( "github.com/yaoapp/yao/openapi/oauth/providers/user" oauthtypes "github.com/yaoapp/yao/openapi/oauth/types" "github.com/yaoapp/yao/openapi/response" - "github.com/yaoapp/yao/openapi/utils" ) -// getLoginConfig is the handler for get login configuration (mapped from /signin) -func getLoginConfig(c *gin.Context) { - // Get locale from query parameter (optional) - locale := c.Query("locale") - - // Get public configuration for the specified locale - config := GetPublicConfig(locale) - - // Set session id if not exists - sid := utils.GetSessionID(c) - if sid == "" { - sid = generateSessionID() - response.SendSessionCookie(c, sid) - } - - // If no configuration found, return error - if config == nil { - errorResp := &response.ErrorResponse{ - Code: response.ErrInvalidRequest.Code, - ErrorDescription: "No signin configuration found for the requested locale", - } - response.RespondWithError(c, response.StatusNotFound, errorResp) - return - } - - // Return the public configuration - response.RespondWithSuccess(c, response.StatusOK, config) -} - -// login is the handler for login (password login, mapped from /signin) -func login(c *gin.Context) { - // This is a placeholder - the original signin function was empty - // You may need to implement the actual login logic here -} - -// getCaptcha is the handler for get captcha image for login +// getCaptcha is the handler for get captcha image for entry (login/register) func getCaptcha(c *gin.Context) { var option helper.CaptchaOption = helper.NewCaptchaOption() diff --git a/openapi/user/types.go b/openapi/user/types.go index dd679abb..909e21bf 100644 --- a/openapi/user/types.go +++ b/openapi/user/types.go @@ -23,20 +23,6 @@ const ( ScopeTeamSelection = "team_selection" ) -// Config represents the signin page configuration -type Config struct { - Title string `json:"title,omitempty"` - Description string `json:"description,omitempty"` - Default bool `json:"default,omitempty"` - SuccessURL string `json:"success_url,omitempty"` - FailureURL string `json:"failure_url,omitempty"` - ClientID string `json:"client_id,omitempty"` - ClientSecret string `json:"client_secret,omitempty"` - Form *FormConfig `json:"form,omitempty"` - Token *TokenConfig `json:"token,omitempty"` - ThirdParty *ThirdParty `json:"third_party,omitempty"` -} - // FormConfig represents the form configuration type FormConfig struct { Username *UsernameConfig `json:"username,omitempty"` diff --git a/openapi/user/user.go b/openapi/user/user.go index 093d1bc6..bb703585 100644 --- a/openapi/user/user.go +++ b/openapi/user/user.go @@ -27,12 +27,10 @@ func init() { // Attach attaches the signin handlers to the router func Attach(group *gin.RouterGroup, oauth types.OAuth) { - // User Authentication (migrated from /signin) - group.GET("/login", getLoginConfig) // Get login page config (public) - migrated from /signin - group.POST("/login", login) // User login (public) - migrated from /signin - group.GET("/login/captcha", getCaptcha) // Get captcha for login (public) + // User Authentication group.GET("/entry", getEntryConfig) // Get unified auth entry config (public) group.POST("/entry", entry) // Unified auth entry (login/register) (public) + group.GET("/entry/captcha", getCaptcha) // Get captcha for login/register (public) group.POST("/logout", oauth.Guard, placeholder) // User logout // Logined User Settings