login.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. package cmd
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "gogs.dmsc.dev/arp/arp_cli/client"
  7. "gogs.dmsc.dev/arp/arp_cli/config"
  8. "github.com/AlecAivazis/survey/v2"
  9. "github.com/urfave/cli/v3"
  10. )
  11. // LoginCommand returns the login command
  12. func LoginCommand() *cli.Command {
  13. return &cli.Command{
  14. Name: "login",
  15. Usage: "Authenticate with the ARP server",
  16. Description: `Login to an ARP server using email and password.
  17. This command will store the authentication token in ~/.arp_cli/config.json
  18. for use by subsequent commands.
  19. Examples:
  20. # Login with URL and email specified
  21. arp_cli login --url http://localhost:8080/query --email user@example.com
  22. # Login interactively (will prompt for all fields)
  23. arp_cli login`,
  24. Flags: []cli.Flag{
  25. &cli.StringFlag{
  26. Name: "url",
  27. Aliases: []string{"u"},
  28. Usage: "ARP server URL",
  29. Sources: cli.EnvVars("ARP_URL"),
  30. },
  31. &cli.StringFlag{
  32. Name: "email",
  33. Aliases: []string{"e"},
  34. Usage: "User email address",
  35. },
  36. &cli.StringFlag{
  37. Name: "password",
  38. Aliases: []string{"p"},
  39. Usage: "User password (will prompt if not provided)",
  40. },
  41. },
  42. Action: doLogin,
  43. }
  44. }
  45. func doLogin(ctx context.Context, cmd *cli.Command) error {
  46. // Get URL
  47. serverURL := cmd.String("url")
  48. if serverURL == "" {
  49. cfg, err := config.Load()
  50. if err != nil {
  51. return fmt.Errorf("failed to load config: %w", err)
  52. }
  53. serverURL = cfg.ServerURL
  54. }
  55. // Prompt for URL if still empty
  56. if serverURL == "" {
  57. prompt := &survey.Input{
  58. Message: "Server URL",
  59. Default: "http://localhost:8080",
  60. Help: "The ARP server URL (e.g., http://localhost:8080)",
  61. }
  62. if err := survey.AskOne(prompt, &serverURL, survey.WithValidator(survey.Required)); err != nil {
  63. return err
  64. }
  65. }
  66. // Ensure URL has the /query endpoint
  67. serverURL = ensureQueryEndpoint(serverURL)
  68. // Get email
  69. email := cmd.String("email")
  70. if email == "" {
  71. prompt := &survey.Input{
  72. Message: "Email",
  73. Help: "Your user account email address",
  74. }
  75. if err := survey.AskOne(prompt, &email, survey.WithValidator(survey.Required)); err != nil {
  76. return err
  77. }
  78. }
  79. // Get password
  80. password := cmd.String("password")
  81. if password == "" {
  82. prompt := &survey.Password{
  83. Message: "Password",
  84. Help: "Your user account password",
  85. }
  86. if err := survey.AskOne(prompt, &password, survey.WithValidator(survey.Required)); err != nil {
  87. return err
  88. }
  89. }
  90. // Perform login
  91. c := client.New(serverURL)
  92. query := `
  93. mutation Login($email: String!, $password: String!) {
  94. login(email: $email, password: $password) {
  95. token
  96. user {
  97. id
  98. email
  99. roles {
  100. id
  101. name
  102. }
  103. }
  104. }
  105. }`
  106. variables := map[string]interface{}{
  107. "email": email,
  108. "password": password,
  109. }
  110. resp, err := c.Mutation(query, variables)
  111. if err != nil {
  112. return fmt.Errorf("login failed: %w", err)
  113. }
  114. // Parse response
  115. var loginResp struct {
  116. Login struct {
  117. Token string `json:"token"`
  118. User struct {
  119. ID string `json:"id"`
  120. Email string `json:"email"`
  121. Roles []struct {
  122. ID string `json:"id"`
  123. Name string `json:"name"`
  124. } `json:"roles"`
  125. } `json:"user"`
  126. } `json:"login"`
  127. }
  128. if err := json.Unmarshal(resp.Data, &loginResp); err != nil {
  129. return fmt.Errorf("failed to parse response: %w", err)
  130. }
  131. // Save config
  132. cfg, err := config.Load()
  133. if err != nil {
  134. return fmt.Errorf("failed to load config: %w", err)
  135. }
  136. cfg.ServerURL = serverURL
  137. cfg.Token = loginResp.Login.Token
  138. cfg.UserEmail = loginResp.Login.User.Email
  139. if err := config.Save(cfg); err != nil {
  140. return fmt.Errorf("failed to save config: %w", err)
  141. }
  142. fmt.Printf("Logged in as %s\n", loginResp.Login.User.Email)
  143. if len(loginResp.Login.User.Roles) > 0 {
  144. fmt.Print("Roles: ")
  145. for i, role := range loginResp.Login.User.Roles {
  146. if i > 0 {
  147. fmt.Print(", ")
  148. }
  149. fmt.Print(role.Name)
  150. }
  151. fmt.Println()
  152. }
  153. return nil
  154. }
  155. // ensureQueryEndpoint ensures the URL ends with /query
  156. func ensureQueryEndpoint(url string) string {
  157. // Remove trailing slash if present
  158. for len(url) > 0 && url[len(url)-1] == '/' {
  159. url = url[:len(url)-1]
  160. }
  161. // Add /query if not already present
  162. if len(url) < 6 || url[len(url)-6:] != "/query" {
  163. url = url + "/query"
  164. }
  165. return url
  166. }