Pulse/cmd/pulse/bootstrap.go
rcourtman dd1d222ad0 Improve bootstrap token UX for easier discovery
The bootstrap token security requirement was added proactively but
lacked discoverability, causing user friction during first-run setup.
These improvements make the token easier to find while maintaining
the security benefit.

Improvements:
- Display bootstrap token prominently in startup logs with ASCII box
  (previously: single line log message)
- Add `pulse bootstrap-token` CLI command to display token on demand
  (Docker: docker exec <container> /app/pulse bootstrap-token)
- Improve error messages in quick-setup API to show exact commands
  for retrieving token when missing or invalid
- Error messages now include both Docker and bare metal examples

User experience improvements:
- Token visible in `docker logs` output immediately
- Clear instructions printed with token
- Helpful error messages if token is wrong/missing
- CLI helper for operators who need to retrieve token later

Security unchanged:
- Bootstrap token still required for first-run setup
- Token still auto-deleted after successful setup
- No bypass mechanism added

Related to discussion about bootstrap token UX friction.
2025-11-06 17:29:49 +00:00

78 lines
4.2 KiB
Go

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("╚═══════════════════════════════════════════════════════════════════════╝")
}