diff --git a/cmd/pulse/bootstrap.go b/cmd/pulse/bootstrap.go new file mode 100644 index 0000000..4437c5b --- /dev/null +++ b/cmd/pulse/bootstrap.go @@ -0,0 +1,78 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" +) + +var bootstrapTokenCmd = &cobra.Command{ + Use: "bootstrap-token", + Short: "Display the bootstrap setup token", + Long: `Display the bootstrap setup token required for first-time setup. + +This token is generated on first boot and must be entered in the web UI +to unlock the initial setup wizard. The token is automatically deleted +after successful setup completion.`, + Run: func(cmd *cobra.Command, args []string) { + showBootstrapToken() + }, +} + +func showBootstrapToken() { + // Determine data path (same logic as main server) + dataPath := os.Getenv("PULSE_DATA_PATH") + if dataPath == "" { + if os.Getenv("PULSE_DOCKER") == "true" { + dataPath = "/data" + } else { + dataPath = "/var/lib/pulse" + } + } + + tokenPath := filepath.Join(dataPath, ".bootstrap_token") + + // Read token file + data, err := os.ReadFile(tokenPath) + if err != nil { + if os.IsNotExist(err) { + fmt.Println("╔═══════════════════════════════════════════════════════════════════════╗") + fmt.Println("║ NO BOOTSTRAP TOKEN FOUND ║") + fmt.Println("╠═══════════════════════════════════════════════════════════════════════╣") + fmt.Println("║ Possible reasons: ║") + fmt.Println("║ • Initial setup has already been completed ║") + fmt.Println("║ • Authentication is configured (token auto-deleted) ║") + fmt.Println("║ • Server hasn't started yet (token not generated) ║") + fmt.Printf("║ • Token file not found: %-44s║\n", tokenPath) + fmt.Println("╚═══════════════════════════════════════════════════════════════════════╝") + os.Exit(1) + } + fmt.Printf("Error reading bootstrap token: %v\n", err) + os.Exit(1) + } + + token := strings.TrimSpace(string(data)) + if token == "" { + fmt.Println("Error: Bootstrap token file is empty") + os.Exit(1) + } + + // Display token prominently + fmt.Println("╔═══════════════════════════════════════════════════════════════════════╗") + fmt.Println("║ BOOTSTRAP TOKEN FOR FIRST-TIME SETUP ║") + fmt.Println("╠═══════════════════════════════════════════════════════════════════════╣") + fmt.Printf("║ Token: %-61s ║\n", token) + fmt.Printf("║ File: %-61s ║\n", tokenPath) + fmt.Println("╠═══════════════════════════════════════════════════════════════════════╣") + fmt.Println("║ Instructions: ║") + fmt.Println("║ 1. Copy the token above ║") + fmt.Println("║ 2. Open Pulse in your web browser ║") + fmt.Println("║ 3. Paste the token into the unlock screen ║") + fmt.Println("║ 4. Complete the admin account setup ║") + fmt.Println("║ ║") + fmt.Println("║ This token will be automatically deleted after successful setup. ║") + fmt.Println("╚═══════════════════════════════════════════════════════════════════════╝") +} diff --git a/cmd/pulse/main.go b/cmd/pulse/main.go index ba318b3..59aef35 100644 --- a/cmd/pulse/main.go +++ b/cmd/pulse/main.go @@ -48,6 +48,8 @@ func init() { rootCmd.AddCommand(configCmd) // Add version command rootCmd.AddCommand(versionCmd) + // Add bootstrap-token command + rootCmd.AddCommand(bootstrapTokenCmd) } var versionCmd = &cobra.Command{ diff --git a/internal/api/bootstrap_token.go b/internal/api/bootstrap_token.go index 6bfa599..28f447a 100644 --- a/internal/api/bootstrap_token.go +++ b/internal/api/bootstrap_token.go @@ -87,9 +87,16 @@ func (r *Router) initializeBootstrapToken() { r.bootstrapTokenPath = path if created { - log.Warn(). - Str("token_path", path). - Msg("Bootstrap setup token generated; retrieve it from the host to complete initial authentication") + // Display token prominently for easy discovery + log.Warn().Msg("╔═══════════════════════════════════════════════════════════════════════╗") + log.Warn().Msg("║ BOOTSTRAP TOKEN REQUIRED FOR FIRST-TIME SETUP ║") + log.Warn().Msg("╠═══════════════════════════════════════════════════════════════════════╣") + log.Warn().Msgf("║ Token: %-61s ║", token) + log.Warn().Msgf("║ File: %-61s ║", path) + log.Warn().Msg("╠═══════════════════════════════════════════════════════════════════════╣") + log.Warn().Msg("║ Copy this token and paste it into the unlock screen in your browser. ║") + log.Warn().Msg("║ This token will be automatically deleted after successful setup. ║") + log.Warn().Msg("╚═══════════════════════════════════════════════════════════════════════╝") } else { log.Info(). Str("token_path", path). diff --git a/internal/api/security_setup_fix.go b/internal/api/security_setup_fix.go index 7e17074..5109197 100644 --- a/internal/api/security_setup_fix.go +++ b/internal/api/security_setup_fix.go @@ -173,7 +173,12 @@ func handleQuickSecuritySetupFixed(r *Router) http.HandlerFunc { } if providedToken == "" { - http.Error(w, "Bootstrap setup token required", http.StatusUnauthorized) + errorMsg := fmt.Sprintf("Bootstrap setup token required. Retrieve it from the host:\n\n"+ + "Docker: docker exec cat /data/.bootstrap_token\n"+ + "Docker: docker exec /app/pulse bootstrap-token\n"+ + "Bare metal: cat %s\n"+ + "Bare metal: pulse bootstrap-token", r.bootstrapTokenPath) + http.Error(w, errorMsg, http.StatusUnauthorized) return } @@ -181,7 +186,12 @@ func handleQuickSecuritySetupFixed(r *Router) http.HandlerFunc { log.Warn(). Str("ip", clientIP). Msg("Rejected quick setup with invalid bootstrap token") - http.Error(w, "Invalid bootstrap setup token", http.StatusUnauthorized) + errorMsg := fmt.Sprintf("Invalid bootstrap setup token. Retrieve the correct token from the host:\n\n"+ + "Docker: docker exec cat /data/.bootstrap_token\n"+ + "Docker: docker exec /app/pulse bootstrap-token\n"+ + "Bare metal: cat %s\n"+ + "Bare metal: pulse bootstrap-token", r.bootstrapTokenPath) + http.Error(w, errorMsg, http.StatusUnauthorized) return }