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