From 78dbaf12b2465b438ee0c7520a65dddce0987832 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 5 Dec 2024 11:12:38 +0900 Subject: [PATCH 1/4] refactor: ask user id when user first log in Signed-off-by: Youngjin Jo --- cmd/other/login.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/cmd/other/login.go b/cmd/other/login.go index aa5e22a..1ee6bca 100644 --- a/cmd/other/login.go +++ b/cmd/other/login.go @@ -429,9 +429,18 @@ func executeUserLogin(currentEnv string) { } userID := mainViper.GetString(fmt.Sprintf("environments.%s.user_id", currentEnv)) + // If no user ID is found, prompt for it if userID == "" { - pterm.Error.Println("No user ID found in current environment configuration.") - exitWithError() + // Prompt for user ID + userIDInput := pterm.DefaultInteractiveTextInput + userID, _ = userIDInput.Show("Enter your User ID") + + // Save user ID to configuration + mainViper.Set(fmt.Sprintf("environments.%s.user_id", currentEnv), userID) + if err := mainViper.WriteConfig(); err != nil { + pterm.Error.Printf("Failed to save user ID to config: %v\n", err) + exitWithError() + } } // Display the current user ID @@ -835,7 +844,7 @@ func saveCredentials(currentEnv, userID, encryptedPassword, accessToken, refresh if grantToken != "" { if err := os.WriteFile(filepath.Join(envCacheDir, "grant_token"), []byte(grantToken), 0600); err != nil { pterm.Error.Printf("Failed to save grant token: %v\n", err) - exitWithError() + exitWithError() } } } From 04f99e436b36ee045a4f7c74d6d048b9e794c873 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 5 Dec 2024 11:18:16 +0900 Subject: [PATCH 2/4] refactor: show user id when user login Signed-off-by: Youngjin Jo --- cmd/other/login.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/other/login.go b/cmd/other/login.go index 1ee6bca..6027133 100644 --- a/cmd/other/login.go +++ b/cmd/other/login.go @@ -441,11 +441,11 @@ func executeUserLogin(currentEnv string) { pterm.Error.Printf("Failed to save user ID to config: %v\n", err) exitWithError() } + } else { + // Show current user ID before password prompt + pterm.Info.Printf("Logging in as: %s\n", userID) } - // Display the current user ID - pterm.Info.Printf("Logged in as: %s\n", userID) - // Prompt for password password := promptPassword() @@ -471,6 +471,8 @@ func executeUserLogin(currentEnv string) { exitWithError() } + pterm.Info.Printf("Logged in as %s\n", userID) + // Use the tokens workspaces, err := fetchWorkspaces(baseUrl, accessToken) if err != nil { From 32baacb8c9a9fc310ef0aa52d0fd1bc5e1850507 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 5 Dec 2024 13:31:32 +0900 Subject: [PATCH 3/4] refactor: create cache directory only when a user successfully logged in Signed-off-by: Youngjin Jo --- cmd/other/login.go | 178 ++------------------------------------------- cmd/root.go | 7 +- 2 files changed, 8 insertions(+), 177 deletions(-) diff --git a/cmd/other/login.go b/cmd/other/login.go index 6027133..64902ef 100644 --- a/cmd/other/login.go +++ b/cmd/other/login.go @@ -471,6 +471,12 @@ func executeUserLogin(currentEnv string) { exitWithError() } + envCacheDir := filepath.Join(homeDir, ".cfctl", "cache", currentEnv) + if err := os.MkdirAll(envCacheDir, 0700); err != nil { + pterm.Error.Printf("Failed to create cache directory: %v\n", err) + exitWithError() + } + pterm.Info.Printf("Logged in as %s\n", userID) // Use the tokens @@ -508,13 +514,6 @@ func executeUserLogin(currentEnv string) { exitWithError() } - // Save tokens to cache - envCacheDir := filepath.Join(homeDir, ".cfctl", "cache", currentEnv) - if err := os.MkdirAll(envCacheDir, 0700); err != nil { - pterm.Error.Printf("Failed to create cache directory: %v\n", err) - exitWithError() - } - if err := os.WriteFile(filepath.Join(envCacheDir, "access_token"), []byte(accessToken), 0600); err != nil { pterm.Error.Printf("Failed to save access token: %v\n", err) exitWithError() @@ -540,171 +539,6 @@ func promptPassword() string { return password } -// Prompt for user selection, now receiving 'users' slice as an argument -func promptUserSelection(max int, users []interface{}) int { - if err := keyboard.Open(); err != nil { - pterm.Error.Println("Failed to initialize keyboard:", err) - exitWithError() - } - defer keyboard.Close() - - selectedIndex := 0 - currentPage := 0 - pageSize := 10 - searchMode := false - searchTerm := "" - filteredUsers := users - - for { - fmt.Print("\033[H\033[2J") // Clear the screen - - // Apply search filter - if searchTerm != "" { - filteredUsers = filterUsers(users, searchTerm) - if len(filteredUsers) == 0 { - filteredUsers = users // Show all users if no search results - } - } else { - filteredUsers = users - } - - // Calculate pagination - totalUsers := len(filteredUsers) - totalPages := (totalUsers + pageSize - 1) / pageSize - startIndex := currentPage * pageSize - endIndex := startIndex + pageSize - if endIndex > totalUsers { - endIndex = totalUsers - } - - // Display header with page information - pterm.DefaultHeader.WithFullWidth(). - WithBackgroundStyle(pterm.NewStyle(pterm.BgDarkGray)). - WithTextStyle(pterm.NewStyle(pterm.FgLightWhite)). - Printf("Select a user account (Page %d of %d)", currentPage+1, totalPages) - - // Display option to add new user first - if selectedIndex == 0 { - pterm.Printf("→ %d: Add new user\n", 1) - } else { - pterm.Printf(" %d: Add new user\n", 1) - } - - // Display existing users - for i := startIndex; i < endIndex; i++ { - userMap := filteredUsers[i].(map[string]interface{}) - if i+1 == selectedIndex { - pterm.Printf("→ %d: %s\n", i+2, userMap["userid"].(string)) - } else { - pterm.Printf(" %d: %s\n", i+2, userMap["userid"].(string)) - } - } - - // Show navigation help - pterm.DefaultBasicText.WithStyle(pterm.NewStyle(pterm.FgGray)). - Println("\nNavigation: [h]prev-page [j]down [k]up [l]next-page [/]search [Enter]select [q]quit") - - // Show search prompt if in search mode - if searchMode { - fmt.Println() - pterm.Info.Printf("Search (ESC to cancel, Enter to confirm): %s", searchTerm) - } - - // Get keyboard input - char, key, err := keyboard.GetKey() - if err != nil { - pterm.Error.Println("Error reading keyboard input:", err) - exitWithError() - } - - // Handle search mode input - if searchMode { - switch key { - case keyboard.KeyEsc: - searchMode = false - searchTerm = "" - filteredUsers = users // Return to full user list when search term is cleared - case keyboard.KeyBackspace, keyboard.KeyBackspace2: - if len(searchTerm) > 0 { - searchTerm = searchTerm[:len(searchTerm)-1] - } - case keyboard.KeyEnter: - searchMode = false - default: - if char != 0 { - searchTerm += string(char) - } - } - currentPage = 0 - selectedIndex = 0 - continue - } - - // Handle normal mode input - switch key { - case keyboard.KeyEnter: - if selectedIndex == 0 { - return len(users) + 1 // Add new user - } else if selectedIndex <= len(filteredUsers) { - // Find the original index of the selected user - selectedUserMap := filteredUsers[selectedIndex-1].(map[string]interface{}) - selectedUserID := selectedUserMap["userid"].(string) - - for i, user := range users { - userMap := user.(map[string]interface{}) - if userMap["userid"].(string) == selectedUserID { - return i + 1 - } - } - } - } - - switch char { - case 'j': // Down - if selectedIndex < min(endIndex-startIndex, totalUsers) { - selectedIndex++ - } - case 'k': // Up - if selectedIndex > 0 { - selectedIndex-- - } - case 'l': // Next page - if currentPage < totalPages-1 { - currentPage++ - selectedIndex = 0 - } - case 'h': // Previous page - if currentPage > 0 { - currentPage-- - selectedIndex = 0 - } - case '/': // Enter search mode - searchMode = true - searchTerm = "" - selectedIndex = 0 - case 'q', 'Q': - fmt.Println() - pterm.Error.Println("User selection cancelled.") - os.Exit(1) - } - } -} - -// filterUsers filters the users list based on the search term -func filterUsers(users []interface{}, searchTerm string) []interface{} { - var filtered []interface{} - searchTerm = strings.ToLower(searchTerm) - - for _, user := range users { - userMap := user.(map[string]interface{}) - userid := strings.ToLower(userMap["userid"].(string)) - if strings.Contains(userid, searchTerm) { - filtered = append(filtered, user) - } - } - return filtered -} - // min returns the minimum of two integers func min(a, b int) int { if a < b { diff --git a/cmd/root.go b/cmd/root.go index b41eea8..2fe4111 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -279,9 +279,6 @@ func loadCachedEndpoints() (map[string]string, error) { // Create environment-specific cache directory envCacheDir := filepath.Join(home, ".cfctl", "cache", currentEnv) - if err := os.MkdirAll(envCacheDir, 0755); err != nil { - return nil, err - } // Read from environment-specific cache file cacheFile := filepath.Join(envCacheDir, "endpoints.toml") @@ -348,7 +345,7 @@ func loadConfig() (*Config, error) { // Try to read main setting first mainV := viper.New() mainV.SetConfigFile(settingFile) - mainV.SetConfigType("toml") // Explicitly set config type to TOML + mainV.SetConfigType("toml") // Explicitly set config type to TOML mainConfigErr := mainV.ReadInConfig() if mainConfigErr != nil { @@ -373,7 +370,7 @@ func loadConfig() (*Config, error) { if endpoint == "" || token == "" { cacheV := viper.New() cacheV.SetConfigFile(cacheConfigFile) - cacheV.SetConfigType("toml") // Explicitly set config type to TOML + cacheV.SetConfigType("toml") // Explicitly set config type to TOML if err := cacheV.ReadInConfig(); err == nil { // If no current environment set, try to get it from cache setting From cffd5c97a007c4a10c929bc95bf721ae0b5ab552 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 5 Dec 2024 14:03:25 +0900 Subject: [PATCH 4/4] refactor: do not call issue_token api when grant_token is not expired yet Signed-off-by: Youngjin Jo --- cmd/other/login.go | 77 +++++++++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/cmd/other/login.go b/cmd/other/login.go index 64902ef..d692226 100644 --- a/cmd/other/login.go +++ b/cmd/other/login.go @@ -411,12 +411,7 @@ func executeUserLogin(currentEnv string) { exitWithError() } - homeDir, err := os.UserHomeDir() - if err != nil { - pterm.Error.Println("Failed to get user home directory:", err) - exitWithError() - } - + homeDir, _ := os.UserHomeDir() // Get user_id from current environment mainViper := viper.New() settingPath := filepath.Join(homeDir, ".cfctl", "setting.toml") @@ -429,27 +424,24 @@ func executeUserLogin(currentEnv string) { } userID := mainViper.GetString(fmt.Sprintf("environments.%s.user_id", currentEnv)) - // If no user ID is found, prompt for it if userID == "" { - // Prompt for user ID userIDInput := pterm.DefaultInteractiveTextInput userID, _ = userIDInput.Show("Enter your User ID") - // Save user ID to configuration mainViper.Set(fmt.Sprintf("environments.%s.user_id", currentEnv), userID) if err := mainViper.WriteConfig(); err != nil { pterm.Error.Printf("Failed to save user ID to config: %v\n", err) exitWithError() } } else { - // Show current user ID before password prompt pterm.Info.Printf("Logging in as: %s\n", userID) } - // Prompt for password - password := promptPassword() + // Check for valid tokens first + accessToken, refreshToken, newAccessToken, err := getValidTokens(currentEnv) + var password string - // Extract the middle part of the environment name for `name` + // Extract domain name from environment nameParts := strings.Split(currentEnv, "-") if len(nameParts) < 3 { pterm.Error.Println("Environment name format is invalid.") @@ -457,20 +449,24 @@ func executeUserLogin(currentEnv string) { } name := nameParts[1] - // Fetch Domain ID using the base URL and domain name + // Fetch Domain ID domainID, err := fetchDomainID(baseUrl, name) if err != nil { pterm.Error.Println("Failed to fetch Domain ID:", err) exitWithError() } - // Issue new tokens - accessToken, refreshToken, err := issueToken(baseUrl, userID, password, domainID) - if err != nil { - pterm.Error.Printf("Failed to issue token: %v\n", err) - exitWithError() + // If refresh token is not valid, get new tokens with password + if refreshToken == "" || isTokenExpired(refreshToken) { + password = promptPassword() + accessToken, refreshToken, err = issueToken(baseUrl, userID, password, domainID) + if err != nil { + pterm.Error.Printf("Failed to issue token: %v\n", err) + exitWithError() + } } + // Create cache directory and save tokens envCacheDir := filepath.Join(homeDir, ".cfctl", "cache", currentEnv) if err := os.MkdirAll(envCacheDir, 0700); err != nil { pterm.Error.Printf("Failed to create cache directory: %v\n", err) @@ -479,7 +475,7 @@ func executeUserLogin(currentEnv string) { pterm.Info.Printf("Logged in as %s\n", userID) - // Use the tokens + // Use the tokens to fetch workspaces and role workspaces, err := fetchWorkspaces(baseUrl, accessToken) if err != nil { pterm.Error.Println("Failed to fetch workspaces:", err) @@ -492,6 +488,7 @@ func executeUserLogin(currentEnv string) { exitWithError() } + // Determine scope and select workspace scope := determineScope(roleType, len(workspaces)) var workspaceID string if roleType == "DOMAIN_ADMIN" { @@ -508,12 +505,13 @@ func executeUserLogin(currentEnv string) { } // Grant new token using the refresh token - grantToken, err := grantToken(baseUrl, refreshToken, scope, domainID, workspaceID) + newAccessToken, err = grantToken(baseUrl, refreshToken, scope, domainID, workspaceID) if err != nil { pterm.Error.Println("Failed to retrieve new access token:", err) exitWithError() } + // Save all tokens if err := os.WriteFile(filepath.Join(envCacheDir, "access_token"), []byte(accessToken), 0600); err != nil { pterm.Error.Printf("Failed to save access token: %v\n", err) exitWithError() @@ -524,7 +522,7 @@ func executeUserLogin(currentEnv string) { exitWithError() } - if err := os.WriteFile(filepath.Join(envCacheDir, "grant_token"), []byte(grantToken), 0600); err != nil { + if err := os.WriteFile(filepath.Join(envCacheDir, "grant_token"), []byte(newAccessToken), 0600); err != nil { pterm.Error.Printf("Failed to save grant token: %v\n", err) exitWithError() } @@ -1655,25 +1653,42 @@ func readTokenFromFile(envDir, tokenType string) (string, error) { } // getValidTokens checks for existing valid tokens in the environment cache directory -func getValidTokens(currentEnv string) (accessToken, refreshToken string, err error) { +func getValidTokens(currentEnv string) (accessToken, refreshToken, newAccessToken string, err error) { homeDir, err := os.UserHomeDir() if err != nil { - return "", "", err + return "", "", "", err } envCacheDir := filepath.Join(homeDir, ".cfctl", "cache", currentEnv) - // Try to read and validate access token + // Try to read and validate grant token first + if newAccessToken, err = readTokenFromFile(envCacheDir, "grant_token"); err == nil { + claims, err := validateAndDecodeToken(newAccessToken) + if err == nil { + // Check if token has expired + if exp, ok := claims["exp"].(float64); ok { + if time.Now().Unix() < int64(exp) { + accessToken, _ = readTokenFromFile(envCacheDir, "access_token") + refreshToken, _ = readTokenFromFile(envCacheDir, "refresh_token") + return accessToken, refreshToken, newAccessToken, nil + } + } + } + } + + // If grant token is invalid or expired, check refresh token if accessToken, err = readTokenFromFile(envCacheDir, "access_token"); err == nil { - if !isTokenExpired(accessToken) { - // Try to read refresh token only if access token is valid - if refreshToken, err = readTokenFromFile(envCacheDir, "refresh_token"); err == nil { - if !isTokenExpired(refreshToken) { - return accessToken, refreshToken, nil + if refreshToken, err = readTokenFromFile(envCacheDir, "refresh_token"); err == nil { + claims, err := validateAndDecodeToken(refreshToken) + if err == nil { + if exp, ok := claims["exp"].(float64); ok { + if time.Now().Unix() < int64(exp) { + return accessToken, refreshToken, "", nil + } } } } } - return "", "", fmt.Errorf("no valid tokens found") + return "", "", "", fmt.Errorf("no valid tokens found") }