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