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}