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