1package commands
  2
  3import (
  4	"context"
  5	"fmt"
  6	"log"
  7	"net"
  8	"net/http"
  9	"net/url"
 10	"os"
 11	"os/signal"
 12	"strconv"
 13	"time"
 14
 15	"github.com/99designs/gqlgen/graphql/playground"
 16	"github.com/gorilla/mux"
 17	"github.com/phayes/freeport"
 18	"github.com/skratchdot/open-golang/open"
 19	"github.com/spf13/cobra"
 20
 21	"github.com/MichaelMure/git-bug/api/auth"
 22	"github.com/MichaelMure/git-bug/api/graphql"
 23	httpapi "github.com/MichaelMure/git-bug/api/http"
 24	"github.com/MichaelMure/git-bug/cache"
 25	"github.com/MichaelMure/git-bug/entities/identity"
 26	"github.com/MichaelMure/git-bug/repository"
 27	"github.com/MichaelMure/git-bug/webui"
 28)
 29
 30const webUIOpenConfigKey = "git-bug.webui.open"
 31
 32type webUIOptions struct {
 33	host     string
 34	port     int
 35	open     bool
 36	noOpen   bool
 37	readOnly bool
 38	query    string
 39}
 40
 41func newWebUICommand() *cobra.Command {
 42	env := newEnv()
 43	options := webUIOptions{}
 44
 45	cmd := &cobra.Command{
 46		Use:   "webui",
 47		Short: "Launch the web UI.",
 48		Long: `Launch the web UI.
 49
 50Available git config:
 51  git-bug.webui.open [bool]: control the automatic opening of the web UI in the default browser
 52`,
 53		PreRunE: loadRepo(env),
 54		RunE: func(cmd *cobra.Command, args []string) error {
 55			return runWebUI(env, options, args)
 56		},
 57	}
 58
 59	flags := cmd.Flags()
 60	flags.SortFlags = false
 61
 62	flags.StringVar(&options.host, "host", "127.0.0.1", "Network address or hostname to listen to (default to 127.0.0.1)")
 63	flags.BoolVar(&options.open, "open", false, "Automatically open the web UI in the default browser")
 64	flags.BoolVar(&options.noOpen, "no-open", false, "Prevent the automatic opening of the web UI in the default browser")
 65	flags.IntVarP(&options.port, "port", "p", 0, "Port to listen to (default to random available port)")
 66	flags.BoolVar(&options.readOnly, "read-only", false, "Whether to run the web UI in read-only mode")
 67	flags.StringVarP(&options.query, "query", "q", "", "The query to open in the web UI bug list")
 68
 69	return cmd
 70}
 71
 72func runWebUI(env *Env, opts webUIOptions, args []string) error {
 73	if opts.port == 0 {
 74		var err error
 75		opts.port, err = freeport.GetFreePort()
 76		if err != nil {
 77			return err
 78		}
 79	}
 80
 81	addr := net.JoinHostPort(opts.host, strconv.Itoa(opts.port))
 82	webUiAddr := fmt.Sprintf("http://%s", addr)
 83	toOpen := webUiAddr
 84
 85	if len(opts.query) > 0 {
 86		// Explicitly set the query parameter instead of going with a default one.
 87		toOpen = fmt.Sprintf("%s/?q=%s", webUiAddr, url.QueryEscape(opts.query))
 88	}
 89
 90	router := mux.NewRouter()
 91
 92	// If the webUI is not read-only, use an authentication middleware with a
 93	// fixed identity: the default user of the repo
 94	// TODO: support dynamic authentication with OAuth
 95	if !opts.readOnly {
 96		author, err := identity.GetUserIdentity(env.repo)
 97		if err != nil {
 98			return err
 99		}
100		router.Use(auth.Middleware(author.Id()))
101	}
102
103	mrc := cache.NewMultiRepoCache()
104	_, err := mrc.RegisterDefaultRepository(env.repo)
105	if err != nil {
106		return err
107	}
108
109	graphqlHandler := graphql.NewHandler(mrc)
110
111	// Routes
112	router.Path("/playground").Handler(playground.Handler("git-bug", "/graphql"))
113	router.Path("/graphql").Handler(graphqlHandler)
114	router.Path("/gitfile/{repo}/{hash}").Handler(httpapi.NewGitFileHandler(mrc))
115	router.Path("/upload/{repo}").Methods("POST").Handler(httpapi.NewGitUploadFileHandler(mrc))
116	router.PathPrefix("/").Handler(webui.NewHandler())
117
118	srv := &http.Server{
119		Addr:    addr,
120		Handler: router,
121	}
122
123	done := make(chan bool)
124	quit := make(chan os.Signal, 1)
125
126	// register as handler of the interrupt signal to trigger the teardown
127	signal.Notify(quit, os.Interrupt)
128
129	go func() {
130		<-quit
131		env.out.Println("WebUI is shutting down...")
132
133		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
134		defer cancel()
135
136		srv.SetKeepAlivesEnabled(false)
137		if err := srv.Shutdown(ctx); err != nil {
138			log.Fatalf("Could not gracefully shutdown the WebUI: %v\n", err)
139		}
140
141		// Teardown
142		err := graphqlHandler.Close()
143		if err != nil {
144			env.out.Println(err)
145		}
146
147		close(done)
148	}()
149
150	env.out.Printf("Web UI: %s\n", webUiAddr)
151	env.out.Printf("Graphql API: http://%s/graphql\n", addr)
152	env.out.Printf("Graphql Playground: http://%s/playground\n", addr)
153	env.out.Println("Press Ctrl+c to quit")
154
155	configOpen, err := env.repo.AnyConfig().ReadBool(webUIOpenConfigKey)
156	if err == repository.ErrNoConfigEntry {
157		// default to true
158		configOpen = true
159	} else if err != nil {
160		return err
161	}
162
163	shouldOpen := (configOpen && !opts.noOpen) || opts.open
164
165	if shouldOpen {
166		err = open.Run(toOpen)
167		if err != nil {
168			env.out.Println(err)
169		}
170	}
171
172	err = srv.ListenAndServe()
173	if err != nil && err != http.ErrServerClosed {
174		return err
175	}
176
177	<-done
178
179	env.out.Println("WebUI stopped")
180	return nil
181}