1package bridgecmd
2
3import (
4 "bufio"
5 "fmt"
6 "os"
7 "strings"
8
9 "github.com/mattn/go-isatty"
10 "github.com/pkg/errors"
11 "github.com/spf13/cobra"
12
13 "github.com/MichaelMure/git-bug/bridge"
14 "github.com/MichaelMure/git-bug/bridge/core"
15 "github.com/MichaelMure/git-bug/bridge/core/auth"
16 "github.com/MichaelMure/git-bug/cache"
17 "github.com/MichaelMure/git-bug/commands/completion"
18 "github.com/MichaelMure/git-bug/commands/execenv"
19)
20
21type bridgeAuthAddTokenOptions struct {
22 target string
23 login string
24 user string
25}
26
27func newBridgeAuthAddTokenCommand() *cobra.Command {
28 env := execenv.NewEnv()
29 options := bridgeAuthAddTokenOptions{}
30
31 cmd := &cobra.Command{
32 Use: "add-token [TOKEN]",
33 Short: "Store a new token",
34 PreRunE: execenv.LoadBackendEnsureUser(env),
35 RunE: execenv.CloseBackend(env, func(cmd *cobra.Command, args []string) error {
36 return runBridgeAuthAddToken(env, options, args)
37 }),
38 Args: cobra.MaximumNArgs(1),
39 }
40
41 flags := cmd.Flags()
42 flags.SortFlags = false
43
44 flags.StringVarP(&options.target, "target", "t", "",
45 fmt.Sprintf("The target of the bridge. Valid values are [%s]", strings.Join(bridge.Targets(), ",")))
46 cmd.RegisterFlagCompletionFunc("target", completion.From(bridge.Targets()))
47 flags.StringVarP(&options.login,
48 "login", "l", "", "The login in the remote bug-tracker")
49 flags.StringVarP(&options.user,
50 "user", "u", "", "The user to add the token to. Default is the current user")
51 cmd.RegisterFlagCompletionFunc("user", completion.User(env))
52
53 return cmd
54}
55
56func runBridgeAuthAddToken(env *execenv.Env, opts bridgeAuthAddTokenOptions, args []string) error {
57 // Note: as bridgeAuthAddTokenLogin is not checked against the remote bug-tracker,
58 // it's possible to register a credential with an incorrect login (including bad case).
59 // The consequence is that it will not get picked later by the bridge. I find that
60 // checking it would require a cumbersome UX (need to provide a base URL for some bridges, ...)
61 // so it's probably not worth it, unless we refactor that entirely.
62
63 if opts.target == "" {
64 return fmt.Errorf("flag --target is required")
65 }
66 if opts.login == "" {
67 return fmt.Errorf("flag --login is required")
68 }
69
70 if !core.TargetExist(opts.target) {
71 return fmt.Errorf("unknown target")
72 }
73
74 var value string
75
76 if len(args) == 1 {
77 value = args[0]
78 } else {
79 // Read from Stdin
80 if isatty.IsTerminal(os.Stdin.Fd()) {
81 env.Err.Println("Enter the token:")
82 }
83 reader := bufio.NewReader(os.Stdin)
84 raw, err := reader.ReadString('\n')
85 if err != nil {
86 return fmt.Errorf("reading from stdin: %v", err)
87 }
88 value = strings.TrimSuffix(raw, "\n")
89 }
90
91 var user *cache.IdentityCache
92 var err error
93
94 if opts.user == "" {
95 user, err = env.Backend.GetUserIdentity()
96 } else {
97 user, err = env.Backend.Identities().ResolvePrefix(opts.user)
98 }
99 if err != nil {
100 return err
101 }
102
103 metaKey, _ := bridge.LoginMetaKey(opts.target)
104 login, ok := user.ImmutableMetadata()[metaKey]
105
106 switch {
107 case ok && login == opts.login:
108 // nothing to do
109 case ok && login != opts.login:
110 return fmt.Errorf("this user is already tagged with a different %s login", opts.target)
111 default:
112 user.SetMetadata(metaKey, opts.login)
113 err = user.Commit()
114 if err != nil {
115 return err
116 }
117 }
118
119 token := auth.NewToken(opts.target, value)
120 token.SetMetadata(auth.MetaKeyLogin, opts.login)
121
122 if err := token.Validate(); err != nil {
123 return errors.Wrap(err, "invalid token")
124 }
125
126 err = auth.Store(env.Repo, token)
127 if err != nil {
128 return err
129 }
130
131 env.Out.Printf("token %s added\n", token.ID())
132 return nil
133}