Skip to content

Commit

Permalink
refactor: implement login options
Browse files Browse the repository at this point in the history
Signed-off-by: Youngjin Jo <[email protected]>
  • Loading branch information
yjinjo committed Nov 27, 2024
1 parent fdb08bf commit 3436669
Show file tree
Hide file tree
Showing 2 changed files with 204 additions and 92 deletions.
124 changes: 71 additions & 53 deletions cmd/common/fetchService.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,62 +69,80 @@ func FetchService(serviceName string, verb string, resourceName string, options
// Get current endpoint
endpoint := mainViper.GetString(fmt.Sprintf("environments.%s.endpoint", currentEnv))

// Create a styled box for the authentication guidance
headerBox := pterm.DefaultBox.WithTitle("Authentication Guide").
WithTitleTopCenter().
WithRightPadding(4).
WithLeftPadding(4).
WithBoxStyle(pterm.NewStyle(pterm.FgLightCyan))

authExplain := "Please login to SpaceONE Console first.\n" +
"This requires your SpaceONE credentials.\n\n" +
"Or use an App token if you're using automation."

headerBox.Println(authExplain)
fmt.Println()

// Create the steps content
steps := []string{
"1. Go to SpaceONE Console",
"2. Run 'cfctl login'",
"3. Enter your credentials when prompted",
"4. Select your workspace",
}
if strings.HasSuffix(currentEnv, "-app") {
// App environment message
headerBox := pterm.DefaultBox.WithTitle("App Guide").
WithTitleTopCenter().
WithRightPadding(4).
WithLeftPadding(4).
WithBoxStyle(pterm.NewStyle(pterm.FgLightCyan))

appTokenExplain := "Please create a Domain Admin App in SpaceONE Console.\n" +
"This requires Domain Admin privilege.\n\n" +
"Or Please create a Workspace App in SpaceONE Console.\n" +
"This requires Workspace Owner privilege."

pterm.Info.Printf("Using endpoint: %s\n", endpoint)
headerBox.Println(appTokenExplain)
fmt.Println()

steps := []string{
"1. Go to SpaceONE Console",
"2. Navigate to either 'Admin > App Page' or specific 'Workspace > App page'",
"3. Click 'Create' to create your App",
"4. Copy value of either 'client_secret' from Client ID or 'token' from Spacectl (CLI)",
}

yamlExample := pterm.DefaultBox.WithTitle("Config Example").
WithTitleTopCenter().
WithRightPadding(4).
WithLeftPadding(4).
Sprint(fmt.Sprintf("environment: %s\nenvironments:\n %s:\n endpoint: %s\n proxy: true\n token: %s",
currentEnv,
currentEnv,
endpoint,
pterm.FgLightCyan.Sprint("YOUR_COPIED_TOKEN")))

instructionBox := pterm.DefaultBox.WithTitle("Required Steps").
WithTitleTopCenter().
WithRightPadding(4).
WithLeftPadding(4)

allSteps := append(steps,
fmt.Sprintf("5. Add the token under the proxy in your config file:\n%s", yamlExample),
"6. Run 'cfctl login' again")

instructionBox.Println(strings.Join(allSteps, "\n\n"))
} else {
// User environment message
headerBox := pterm.DefaultBox.WithTitle("Authentication Required").
WithTitleTopCenter().
WithRightPadding(4).
WithLeftPadding(4).
WithBoxStyle(pterm.NewStyle(pterm.FgLightCyan))

authExplain := "Please login to SpaceONE Console first.\n" +
"This requires your SpaceONE credentials."

headerBox.Println(authExplain)
fmt.Println()

steps := []string{
"1. Run 'cfctl login'",
"2. Enter your credentials when prompted",
"3. Select your workspace",
"4. Try your command again",
}

instructionBox := pterm.DefaultBox.WithTitle("Required Steps").
WithTitleTopCenter().
WithRightPadding(4).
WithLeftPadding(4)

// Determine proxy value based on endpoint
isIdentityEndpoint := strings.Contains(strings.ToLower(endpoint), "identity")
proxyValue := "true"
if !isIdentityEndpoint {
proxyValue = "false"
instructionBox.Println(strings.Join(steps, "\n\n"))
}

// Create yaml config example with highlighting
yamlExample := pterm.DefaultBox.WithTitle("Config Example").
WithTitleTopCenter().
WithRightPadding(4).
WithLeftPadding(4).
Sprint(fmt.Sprintf("environment: %s\nenvironments:\n %s:\n endpoint: %s\n proxy: %s\n token: %s",
currentEnv,
currentEnv,
endpoint,
proxyValue,
pterm.FgLightCyan.Sprint("YOUR_TOKEN")))

// Create instruction box
instructionBox := pterm.DefaultBox.WithTitle("Required Steps").
WithTitleTopCenter().
WithRightPadding(4).
WithLeftPadding(4)

// Combine all steps
allSteps := append(steps,
fmt.Sprintf("5. Verify your config file has the token:\n%s", yamlExample),
"6. Try your command again")

// Print all steps in the instruction box
instructionBox.Println(strings.Join(allSteps, "\n\n"))

return nil, fmt.Errorf("authentication required")
return nil, nil
}

config, err := loadConfig()
Expand Down
172 changes: 133 additions & 39 deletions cmd/other/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"io"
"os"
"path/filepath"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -114,9 +115,9 @@ func executeAppLogin(currentEnv string, mainViper *viper.Viper) {
WithBoxStyle(pterm.NewStyle(pterm.FgLightCyan))

appTokenExplain := "Please create a Domain Admin App in SpaceONE Console.\n" +
"This requires Domain Admin privileges.\n\n" +
"This requires Domain Admin privilege.\n\n" +
"Or Please create a Workspace App in SpaceONE Console.\n" +
"This requires Workspace Owner privileges."
"This requires Workspace Owner privilege."

headerBox.Println(appTokenExplain)
fmt.Println()
Expand Down Expand Up @@ -1166,6 +1167,7 @@ func saveToken(newToken string) {
exitWithError()
}

fmt.Println()
pterm.Success.Printf("Token successfully saved to %s\n", configPath)
}

Expand Down Expand Up @@ -1253,20 +1255,28 @@ func selectWorkspace(workspaces []map[string]interface{}) string {
func selectScopeOrWorkspace(workspaces []map[string]interface{}) string {
const pageSize = 9
currentPage := 0
searchMode := false
searchTerm := ""
selectedIndex := 0
inputBuffer := ""
filteredWorkspaces := workspaces

// Initialize keyboard
if err := keyboard.Open(); err != nil {
pterm.Error.Println("Failed to initialize keyboard:", err)
exitWithError()
}
defer keyboard.Close()

for {
// Clear screen
fmt.Print("\033[H\033[2J")

// Apply search filter
if searchTerm != "" {
filteredWorkspaces = filterWorkspaces(workspaces, searchTerm)
if len(filteredWorkspaces) == 0 {
filteredWorkspaces = workspaces
}
} else {
filteredWorkspaces = workspaces
}
Expand All @@ -1280,82 +1290,166 @@ func selectScopeOrWorkspace(workspaces []map[string]interface{}) string {
endIndex = totalWorkspaces
}

// Clear screen
fmt.Print("\033[H\033[2J")

// Show search term if active
if searchTerm != "" {
pterm.Info.Printf("Search term: %s\n", searchTerm)
}

pterm.Info.Printf("Available Options (Page %d of %d):\n", currentPage+1, totalPages)

// Always show DOMAIN ADMIN option on first page
if currentPage == 0 {
pterm.DefaultBasicText.WithStyle(pterm.NewStyle(pterm.FgLightCyan)).
Printf(" 0: DOMAIN ADMIN\n")
if selectedIndex == 0 {
fmt.Printf("→ 0: DOMAIN ADMIN\n")
} else {
fmt.Printf(" 0: DOMAIN ADMIN\n")
}
}

// Display current page items
// Display workspace options with selection indicator
for i := startIndex; i < endIndex; i++ {
name := filteredWorkspaces[i]["name"].(string)
fmt.Printf(" %d: %s\n", i+1, name)
displayIndex := i - startIndex + 1
if displayIndex == selectedIndex {
fmt.Printf("→ %d: %s\n", i+1, name)
} else {
fmt.Printf(" %d: %s\n", i+1, name)
}
}

// Show navigation help
fmt.Print("\nNavigation: [p]revious page, [n]ext page")
if searchTerm != "" {
fmt.Print("\nNavigation: [j]down [k]up [h]prev-page [l]next-page")
if searchTerm != "" && !searchMode {
fmt.Print(", [c]lear search")
}
fmt.Print(", [/]search, [q]uit\n")
fmt.Print("> ")
fmt.Print(", [/]search, [q]uit")

// Show search or input prompt at the bottom
if searchMode {
fmt.Print("\n\nSearch (ESC to cancel, Enter to confirm): ")
fmt.Print(searchTerm)
} else if !searchMode {
fmt.Print("\n\nPlease select an option or input a number: ")
if inputBuffer != "" {
fmt.Print(inputBuffer)
}
}

// Get keyboard input
char, _, err := keyboard.GetKey()
char, key, err := keyboard.GetKey()
if err != nil {
pterm.Error.Println("Error reading keyboard input:", err)
exitWithError()
}

if searchMode {
switch key {
case keyboard.KeyEsc:
searchMode = false
searchTerm = ""
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
}

switch key {
case keyboard.KeyEnter:
if inputBuffer != "" {
index, err := strconv.Atoi(inputBuffer)
if err == nil && index >= 0 {
if index == 0 {
return "0"
}
index-- // 1-based to 0-based
if index < len(filteredWorkspaces) {
return filteredWorkspaces[index]["workspace_id"].(string)
}
}
inputBuffer = ""
} else {
if selectedIndex == 0 && currentPage == 0 {
return "0"
}
adjustedIndex := startIndex + (selectedIndex - 1)
if adjustedIndex >= 0 && adjustedIndex < len(filteredWorkspaces) {
return filteredWorkspaces[adjustedIndex]["workspace_id"].(string)
}
}
case keyboard.KeyBackspace, keyboard.KeyBackspace2:
if len(inputBuffer) > 0 {
inputBuffer = inputBuffer[:len(inputBuffer)-1]
}
}

switch char {
case 'n', 'N':
case 'j': // Down
if selectedIndex < min(pageSize-1, endIndex-startIndex) {
selectedIndex++
} else if currentPage < totalPages-1 {
currentPage++
selectedIndex = 0
}
inputBuffer = ""
case 'k': // Up
if selectedIndex > 0 {
selectedIndex--
} else if currentPage > 0 {
currentPage--
selectedIndex = pageSize - 1
}
inputBuffer = ""
case 'l': // Next page
if currentPage < totalPages-1 {
currentPage++
} else {
currentPage = 0
}
case 'p', 'P':
selectedIndex = 0
inputBuffer = ""
case 'h': // Previous page
if currentPage > 0 {
currentPage--
} else {
currentPage = totalPages - 1
}
selectedIndex = 0
inputBuffer = ""
case 'q', 'Q':
fmt.Println()
pterm.Error.Println("Workspace selection cancelled.")
os.Exit(1)
case 'c', 'C':
searchTerm = ""
currentPage = 0
if searchTerm != "" {
searchTerm = ""
currentPage = 0
selectedIndex = 0
}
inputBuffer = ""
case '/':
keyboard.Close()
fmt.Print("\nEnter search term: ")
var input string
fmt.Scanln(&input)
searchTerm = input
searchMode = true
searchTerm = ""
currentPage = 0
keyboard.Open()
case '0':
return "0"
case '1', '2', '3', '4', '5', '6', '7', '8', '9':
index := int(char - '0')
adjustedIndex := startIndex + (index - 1)
if adjustedIndex >= 0 && adjustedIndex < len(filteredWorkspaces) {
return filteredWorkspaces[adjustedIndex]["workspace_id"].(string)
selectedIndex = 0
inputBuffer = ""
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
if !searchMode {
inputBuffer += string(char)
}
}
}
}

func min(a, b int) int {
if a < b {
return a
}
return b
}

func filterWorkspaces(workspaces []map[string]interface{}, searchTerm string) []map[string]interface{} {
var filtered []map[string]interface{}
searchTerm = strings.ToLower(searchTerm)
Expand Down

0 comments on commit 3436669

Please sign in to comment.