Skip to content

Commit 90f34ea

Browse files
authored
Merge pull request #16 from dadav/feat_ui
feat: Add basic web ui
2 parents a2b090b + 2a32c74 commit 90f34ea

32 files changed

+1928
-53
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ Flags:
105105
--port int the port to listen to (default 8080)
106106
--tls-cert string path to tls cert file
107107
--tls-key string path to tls key file
108+
--ui enables the web ui
108109
--user string give control to this user or uid (requires root)
109110
110111
Global Flags:
@@ -151,6 +152,8 @@ Via file (`$HOME/.config/gorge.yaml` or `./gorge.yaml`):
151152

152153
```yaml
153154
---
155+
# Enable basic web ui
156+
ui: false
154157
# Set uid of process to this users uid
155158
user: ""
156159
# Set gid of process to this groups gid
@@ -196,6 +199,7 @@ tls-key: ""
196199
Via environment:
197200

198201
```bash
202+
GORGE_UI=false
199203
GORGE_USER=""
200204
GORGE_GROUP=""
201205
GORGE_API_VERSION=v3

cmd/serve.go

Lines changed: 53 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ import (
3838
"github.com/dadav/gorge/internal/utils"
3939
v3 "github.com/dadav/gorge/internal/v3/api"
4040
backend "github.com/dadav/gorge/internal/v3/backend"
41+
"github.com/dadav/gorge/internal/v3/ui"
4142
openapi "github.com/dadav/gorge/pkg/gen/v3/openapi"
4243
"github.com/go-chi/chi/v5"
4344
"github.com/go-chi/chi/v5/middleware"
4445
"github.com/go-chi/cors"
45-
"github.com/go-chi/jwtauth/v5"
4646
"github.com/go-chi/stampede"
4747
"github.com/spf13/cobra"
4848
"golang.org/x/sync/errgroup"
@@ -127,34 +127,20 @@ You can also enable the caching functionality to speed things up.`,
127127
r := chi.NewRouter()
128128

129129
// Logger should come before any middleware that modifies the response
130-
// r.Use(middleware.Logger)
130+
r.Use(middleware.Logger)
131131
// Recoverer should also be pretty high in the middleware stack
132132
r.Use(middleware.Recoverer)
133133
r.Use(middleware.RealIP)
134134
r.Use(customMiddleware.RequireUserAgent)
135+
x := customMiddleware.NewStatistics()
136+
r.Use(customMiddleware.StatisticsMiddleware(x))
135137
r.Use(cors.Handler(cors.Options{
136138
AllowedOrigins: strings.Split(config.CORSOrigins, ","),
137139
AllowedMethods: []string{"GET", "POST", "DELETE", "PATCH"},
138140
AllowedHeaders: []string{"Accept", "Content-Type"},
139141
AllowCredentials: false,
140142
MaxAge: 300,
141143
}))
142-
143-
if !config.Dev {
144-
tokenAuth := jwtauth.New("HS256", []byte(config.JwtSecret), nil)
145-
r.Use(customMiddleware.AuthMiddleware(tokenAuth, func(r *http.Request) bool {
146-
// Everything but GET is protected and requires a jwt token
147-
return r.Method != "GET"
148-
}))
149-
150-
_, tokenString, _ := tokenAuth.Encode(map[string]interface{}{"user": "admin"})
151-
err = os.WriteFile(config.JwtTokenPath, []byte(tokenString), 0400)
152-
if err != nil {
153-
log.Log.Fatal(err)
154-
}
155-
log.Log.Infof("JWT token was written to %s", config.JwtTokenPath)
156-
}
157-
158144
if !config.NoCache {
159145
customKeyFunc := func(r *http.Request) uint64 {
160146
token := r.Header.Get("Authorization")
@@ -164,45 +150,58 @@ You can also enable the caching functionality to speed things up.`,
164150
r.Use(cachedMiddleware)
165151
}
166152

167-
if config.FallbackProxyUrl != "" {
168-
proxies := strings.Split(config.FallbackProxyUrl, ",")
169-
slices.Reverse(proxies)
170-
171-
for _, proxy := range proxies {
172-
r.Use(customMiddleware.ProxyFallback(proxy, func(status int) bool {
173-
return status == http.StatusNotFound
174-
},
175-
func(r *http.Response) {
176-
if config.ImportProxiedReleases && strings.HasPrefix(r.Request.URL.Path, "/v3/files/") && r.StatusCode == http.StatusOK {
177-
body, err := io.ReadAll(r.Body)
178-
if err != nil {
179-
log.Log.Error(err)
180-
return
181-
}
153+
if config.UI {
154+
r.Group(func(r chi.Router) {
155+
r.HandleFunc("/", ui.IndexHandler)
156+
r.HandleFunc("/search", ui.SearchHandler)
157+
r.HandleFunc("/modules/{module}", ui.ModuleHandler)
158+
r.HandleFunc("/modules/{module}/{version}", ui.ReleaseHandler)
159+
r.HandleFunc("/authors/{author}", ui.AuthorHandler)
160+
r.HandleFunc("/statistics", ui.StatisticsHandler(x))
161+
r.Handle("/assets/*", ui.HandleAssets())
162+
})
163+
}
182164

183-
// restore the body
184-
r.Body = io.NopCloser(bytes.NewBuffer(body))
165+
r.Group(func(r chi.Router) {
166+
if config.FallbackProxyUrl != "" {
167+
proxies := strings.Split(config.FallbackProxyUrl, ",")
168+
slices.Reverse(proxies)
185169

186-
release, err := backend.ConfiguredBackend.AddRelease(body)
187-
if err != nil {
188-
log.Log.Error(err)
189-
return
190-
}
191-
log.Log.Infof("Imported release %s\n", release.Slug)
192-
}
170+
for _, proxy := range proxies {
171+
r.Use(customMiddleware.ProxyFallback(proxy, func(status int) bool {
172+
return status == http.StatusNotFound
193173
},
194-
))
174+
func(r *http.Response) {
175+
if config.ImportProxiedReleases && strings.HasPrefix(r.Request.URL.Path, "/v3/files/") && r.StatusCode == http.StatusOK {
176+
body, err := io.ReadAll(r.Body)
177+
if err != nil {
178+
log.Log.Error(err)
179+
return
180+
}
181+
182+
// restore the body
183+
r.Body = io.NopCloser(bytes.NewBuffer(body))
184+
185+
release, err := backend.ConfiguredBackend.AddRelease(body)
186+
if err != nil {
187+
log.Log.Error(err)
188+
return
189+
}
190+
log.Log.Infof("Imported release %s\n", release.Slug)
191+
}
192+
},
193+
))
194+
}
195195
}
196-
}
197-
198-
apiRouter := openapi.NewRouter(
199-
openapi.NewModuleOperationsAPIController(moduleService),
200-
openapi.NewReleaseOperationsAPIController(releaseService),
201-
openapi.NewSearchFilterOperationsAPIController(searchFilterService),
202-
openapi.NewUserOperationsAPIController(userService),
203-
)
204-
205-
r.Mount("/", apiRouter)
196+
apiRouter := openapi.NewRouter(
197+
openapi.NewModuleOperationsAPIController(moduleService),
198+
openapi.NewReleaseOperationsAPIController(releaseService),
199+
openapi.NewSearchFilterOperationsAPIController(searchFilterService),
200+
openapi.NewUserOperationsAPIController(userService),
201+
)
202+
203+
r.Mount("/v3", apiRouter)
204+
})
206205

207206
r.Get("/readyz", func(w http.ResponseWriter, r *http.Request) {
208207
w.Header().Set("Content-Type", "application/json")
@@ -325,6 +324,7 @@ func init() {
325324
serveCmd.Flags().StringVar(&config.FallbackProxyUrl, "fallback-proxy", "", "optional comma separated list of fallback upstream proxy urls")
326325
serveCmd.Flags().BoolVar(&config.Dev, "dev", false, "enables dev mode")
327326
serveCmd.Flags().BoolVar(&config.DropPrivileges, "drop-privileges", false, "drops privileges to the given user/group")
327+
serveCmd.Flags().BoolVar(&config.UI, "ui", false, "enables the web ui")
328328
serveCmd.Flags().StringVar(&config.CachePrefixes, "cache-prefixes", "/v3/files", "url prefixes to cache")
329329
serveCmd.Flags().StringVar(&config.JwtSecret, "jwt-secret", "changeme", "jwt secret")
330330
serveCmd.Flags().StringVar(&config.JwtTokenPath, "jwt-token-path", "~/.gorge/token", "jwt token path")

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/dadav/gorge
33
go 1.22.0
44

55
require (
6+
github.com/a-h/templ v0.2.747
67
github.com/go-chi/chi/v5 v5.0.12
78
github.com/go-chi/cors v1.2.1
89
github.com/go-chi/jwtauth/v5 v5.3.1

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg=
2+
github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4=
13
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
24
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
35
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=

internal/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ var (
88
Bind string
99
Dev bool
1010
DropPrivileges bool
11+
UI bool
1112
ModulesDir string
1213
ModulesScanSec int
1314
Backend string

internal/middleware/stats.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
"sync"
6+
"time"
7+
)
8+
9+
type Statistics struct {
10+
ActiveConnections int
11+
TotalConnections int
12+
TotalResponseTime time.Duration
13+
ConnectionsPerEndpoint map[string]int
14+
ResponseTimePerEndpoint map[string]time.Duration
15+
Mutex sync.Mutex
16+
}
17+
18+
func NewStatistics() *Statistics {
19+
return &Statistics{
20+
ActiveConnections: 0,
21+
TotalConnections: 0,
22+
TotalResponseTime: 0,
23+
ConnectionsPerEndpoint: make(map[string]int),
24+
ResponseTimePerEndpoint: make(map[string]time.Duration),
25+
}
26+
}
27+
28+
func StatisticsMiddleware(stats *Statistics) func(next http.Handler) http.Handler {
29+
return func(next http.Handler) http.Handler {
30+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
31+
start := time.Now()
32+
stats.Mutex.Lock()
33+
stats.ActiveConnections++
34+
stats.TotalConnections++
35+
stats.ConnectionsPerEndpoint[r.URL.Path]++
36+
stats.Mutex.Unlock()
37+
38+
defer func() {
39+
duration := time.Since(start)
40+
stats.Mutex.Lock()
41+
stats.ActiveConnections--
42+
stats.TotalResponseTime += duration
43+
stats.ResponseTimePerEndpoint[r.URL.Path] += duration
44+
stats.Mutex.Unlock()
45+
}()
46+
47+
next.ServeHTTP(w, r)
48+
})
49+
}
50+
}

internal/v3/ui/assets.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package ui
2+
3+
import (
4+
"embed"
5+
"net/http"
6+
)
7+
8+
//go:embed all:assets
9+
var assets embed.FS
10+
11+
func HandleAssets() http.Handler {
12+
return http.FileServer(http.FS(assets))
13+
}

internal/v3/ui/assets/favicon.ico

79.3 KB
Binary file not shown.

internal/v3/ui/assets/htmx.min.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/v3/ui/assets/logo.png

370 KB
Loading

internal/v3/ui/assets/pico.min.css

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)