webui.go

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