Refactor user authentication to unify entry configuration handling

- Renamed and updated functions and tests to replace 'login' terminology with 'entry', reflecting the unified handling of login and registration processes.
- Removed deprecated login configuration functions and structures, streamlining the codebase.
- Enhanced test coverage for entry configuration retrieval and validation, ensuring comprehensive testing of the new unified approach.
- Improved error handling and logging for entry configuration scenarios, contributing to a better user experience during authentication.
This commit is contained in:
Max 2025-10-15 11:16:54 +08:00
parent b43a38b387
commit d1a9e5c892
7 changed files with 47 additions and 238 deletions

View file

@ -22,19 +22,19 @@ func TestGetTeamConfigFunction(t *testing.T) {
assert.Nil(t, teamConfig, "Should return nil when no config is loaded") assert.Nil(t, teamConfig, "Should return nil when no config is loaded")
} }
// TestGetPublicConfigFunction tests the GetPublicConfig function // TestGetEntryConfigFunction tests the GetEntryConfig function
func TestGetPublicConfigFunction(t *testing.T) { func TestGetEntryConfigFunction(t *testing.T) {
// Test with empty locale // Test with empty locale
publicConfig := user.GetPublicConfig("") entryConfig := user.GetEntryConfig("")
assert.Nil(t, publicConfig, "Should return nil when no config is loaded") assert.Nil(t, entryConfig, "Should return nil when no config is loaded")
// Test with specific locale // Test with specific locale
publicConfig = user.GetPublicConfig("en") entryConfig = user.GetEntryConfig("en")
assert.Nil(t, publicConfig, "Should return nil when no config is loaded") assert.Nil(t, entryConfig, "Should return nil when no config is loaded")
// Test with invalid locale // Test with invalid locale
publicConfig = user.GetPublicConfig("invalid") entryConfig = user.GetEntryConfig("invalid")
assert.Nil(t, publicConfig, "Should return nil when no config is loaded") assert.Nil(t, entryConfig, "Should return nil when no config is loaded")
} }
// TestGetYaoClientConfigFunction tests the GetYaoClientConfig function // TestGetYaoClientConfigFunction tests the GetYaoClientConfig function

View file

@ -30,16 +30,16 @@ func TestUserLoginConfig(t *testing.T) {
// Note: user.Load is automatically called by openapi.Load in testutils.Prepare // 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 { testCases := []struct {
name string name string
endpoint string endpoint string
expectCode int expectCode int
}{ }{
{"get login config without locale", "/user/login", 200}, {"get entry config without locale", "/user/entry", 200},
{"get login config with en locale", "/user/login?locale=en", 200}, {"get entry config with en locale", "/user/entry?locale=en", 200},
{"get login config with zh-cn locale", "/user/login?locale=zh-cn", 200}, {"get entry config with zh-cn locale", "/user/entry?locale=zh-cn", 200},
{"get login config with invalid locale", "/user/login?locale=invalid", 200}, // should fallback to default {"get entry config with invalid locale", "/user/entry?locale=invalid", 200}, // should fallback to default
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -57,7 +57,7 @@ func TestUserLoginConfig(t *testing.T) {
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
assert.NoError(t, err, "Should read response body") assert.NoError(t, err, "Should read response body")
var config user.Config var config user.EntryConfig
err = json.Unmarshal(body, &config) err = json.Unmarshal(body, &config)
assert.NoError(t, err, "Should parse JSON response") assert.NoError(t, err, "Should parse JSON response")
@ -102,13 +102,13 @@ func TestUserLoginConfigLoad(t *testing.T) {
err := user.Load(config.Conf) err := user.Load(config.Conf)
assert.NoError(t, err, "user.Load should succeed") assert.NoError(t, err, "user.Load should succeed")
// Test that we can get public config // Test that we can get entry config
publicConfig := user.GetPublicConfig("") entryConfig := user.GetEntryConfig("")
if publicConfig != nil { if entryConfig != nil {
t.Logf("Public config loaded with title: %s", publicConfig.Title) t.Logf("Entry config loaded with title: %s", entryConfig.Title)
assert.IsType(t, &user.Config{}, publicConfig, "Should return correct config type") assert.IsType(t, &user.EntryConfig{}, entryConfig, "Should return correct config type")
} else { } 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 // Note: user.Load is automatically called by openapi.Load in testutils.Prepare
// Get a config to test structure // Get a config to test structure
config := user.GetPublicConfig("") config := user.GetEntryConfig("")
if config != nil { if config != nil {
t.Logf("Config loaded successfully with title: %s", config.Title) t.Logf("Config loaded successfully with title: %s", config.Title)
// Verify config structure is valid // 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 // Test new configuration fields
assert.IsType(t, "", config.ClientID, "ClientID should be string") assert.IsType(t, "", config.ClientID, "ClientID should be string")
assert.IsType(t, "", config.ClientSecret, "ClientSecret should be string") assert.IsType(t, "", config.ClientSecret, "ClientSecret should be string")
assert.IsType(t, false, config.Default, "Default should be boolean") assert.IsType(t, false, config.Default, "Default should be boolean")
t.Logf("Config has ClientID: %t, ClientSecret: %t, Default: %t", assert.IsType(t, false, config.AutoLogin, "AutoLogin should be boolean")
config.ClientID != "", config.ClientSecret != "", config.Default) 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 // Test form configuration
if config.Form != nil { 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 { } else {
t.Log("No user configuration found") t.Log("No user configuration found")
} }

View file

@ -28,7 +28,7 @@ func TestUserLogin(t *testing.T) {
// Note: user.Load is automatically called by openapi.Load in testutils.Prepare // 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 { testCases := []struct {
name string name string
method string method string
@ -37,16 +37,16 @@ func TestUserLogin(t *testing.T) {
expectCode int expectCode int
}{ }{
{ {
"post login without credentials", "post entry without credentials",
"POST", "POST",
"/user/login", "/user/entry",
map[string]interface{}{}, map[string]interface{}{},
200, // Currently empty implementation, may change when implemented 200, // Currently empty implementation, may change when implemented
}, },
{ {
"post login with credentials", "post entry with credentials",
"POST", "POST",
"/user/login", "/user/entry",
map[string]interface{}{ map[string]interface{}{
"username": "testuser", "username": "testuser",
"password": "testpass", "password": "testpass",
@ -54,9 +54,9 @@ func TestUserLogin(t *testing.T) {
200, // Currently empty implementation, may change when implemented 200, // Currently empty implementation, may change when implemented
}, },
{ {
"post login with email", "post entry with email",
"POST", "POST",
"/user/login", "/user/entry",
map[string]interface{}{ map[string]interface{}{
"email": "test@example.com", "email": "test@example.com",
"password": "testpass", "password": "testpass",
@ -149,7 +149,7 @@ func TestUserLoginValidation(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
requestURL := serverURL + baseURL + "/user/login" requestURL := serverURL + baseURL + "/user/entry"
var req *http.Request var req *http.Request
var err error var err error

View file

@ -23,14 +23,8 @@ var (
// Client config // Client config
yaoClientConfig *YaoClientConfig 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) // Global providers map (decoupled from locale-specific configs)
providers = make(map[string]*Provider) providers = make(map[string]*Provider)
// Default configuration (marked with default: true)
defaultConfig *Config
// Team configurations by locale // Team configurations by locale
teamConfigs = make(map[string]*TeamConfig) teamConfigs = make(map[string]*TeamConfig)
// Entry configurations by locale (unified login + register) // Entry configurations by locale (unified login + register)
@ -45,21 +39,12 @@ func Load(appConfig config.Config) error {
defer configMutex.Unlock() defer configMutex.Unlock()
// Clear existing configurations // Clear existing configurations
fullConfigs = make(map[string]*Config)
publicConfigs = make(map[string]*Config)
providers = make(map[string]*Provider) providers = make(map[string]*Provider)
defaultConfig = nil
teamConfigs = make(map[string]*TeamConfig) teamConfigs = make(map[string]*TeamConfig)
entryConfigs = make(map[string]*EntryConfig) 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 // Load entry configurations from openapi/user/entry directory
err = loadEntryConfigs(appConfig.Root) err := loadEntryConfigs(appConfig.Root)
if err != nil { if err != nil {
return fmt.Errorf("failed to load entry configs: %v", err) return fmt.Errorf("failed to load entry configs: %v", err)
} }
@ -254,75 +239,6 @@ func loadProviders(_ string) error {
return nil 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 // loadTeamConfigs loads all team configurations from the openapi/user/team directory
func loadTeamConfigs(_ string) error { func loadTeamConfigs(_ string) error {
// Use Walk to find all configuration files in the team directory // Use Walk to find all configuration files in the team directory
@ -366,41 +282,6 @@ func loadTeamConfigs(_ string) error {
return nil 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 // GetProvider returns a provider by ID
func GetProvider(providerID string) (*Provider, error) { func GetProvider(providerID string) (*Provider, error) {
configMutex.RLock() configMutex.RLock()
@ -570,37 +451,6 @@ func processFormConfigENVVariables(form *FormConfig) []string {
return missingEnvVars 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 // loadEntryConfigs loads all entry configurations from the openapi/user/entry directory
// Entry config merges signin and register configurations // Entry config merges signin and register configurations
func loadEntryConfigs(_ string) error { func loadEntryConfigs(_ string) error {

View file

@ -15,45 +15,9 @@ import (
"github.com/yaoapp/yao/openapi/oauth/providers/user" "github.com/yaoapp/yao/openapi/oauth/providers/user"
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types" oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
"github.com/yaoapp/yao/openapi/response" "github.com/yaoapp/yao/openapi/response"
"github.com/yaoapp/yao/openapi/utils"
) )
// getLoginConfig is the handler for get login configuration (mapped from /signin) // getCaptcha is the handler for get captcha image for entry (login/register)
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
func getCaptcha(c *gin.Context) { func getCaptcha(c *gin.Context) {
var option helper.CaptchaOption = helper.NewCaptchaOption() var option helper.CaptchaOption = helper.NewCaptchaOption()

View file

@ -23,20 +23,6 @@ const (
ScopeTeamSelection = "team_selection" 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 // FormConfig represents the form configuration
type FormConfig struct { type FormConfig struct {
Username *UsernameConfig `json:"username,omitempty"` Username *UsernameConfig `json:"username,omitempty"`

View file

@ -27,12 +27,10 @@ func init() {
// Attach attaches the signin handlers to the router // Attach attaches the signin handlers to the router
func Attach(group *gin.RouterGroup, oauth types.OAuth) { func Attach(group *gin.RouterGroup, oauth types.OAuth) {
// User Authentication (migrated from /signin) // User Authentication
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)
group.GET("/entry", getEntryConfig) // Get unified auth entry config (public) group.GET("/entry", getEntryConfig) // Get unified auth entry config (public)
group.POST("/entry", entry) // Unified auth entry (login/register) (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 group.POST("/logout", oauth.Guard, placeholder) // User logout
// Logined User Settings // Logined User Settings