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}