webui.go

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