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.LocalConfig().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}