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/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}