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")
}
// 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

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 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")
}

View file

@ -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

View file

@ -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 {

View file

@ -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()

View file

@ -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"`

View file

@ -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