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:
parent
b43a38b387
commit
d1a9e5c892
7 changed files with 47 additions and 238 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue