Skip to content

Commit

Permalink
Merge pull request #16 from dadav/feat_ui
Browse files Browse the repository at this point in the history
feat: Add basic web ui
  • Loading branch information
dadav authored Jul 22, 2024
2 parents a2b090b + 2a32c74 commit 90f34ea
Show file tree
Hide file tree
Showing 32 changed files with 1,928 additions and 53 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ Flags:
--port int the port to listen to (default 8080)
--tls-cert string path to tls cert file
--tls-key string path to tls key file
--ui enables the web ui
--user string give control to this user or uid (requires root)
Global Flags:
Expand Down Expand Up @@ -151,6 +152,8 @@ Via file (`$HOME/.config/gorge.yaml` or `./gorge.yaml`):

```yaml
---
# Enable basic web ui
ui: false
# Set uid of process to this users uid
user: ""
# Set gid of process to this groups gid
Expand Down Expand Up @@ -196,6 +199,7 @@ tls-key: ""
Via environment:

```bash
GORGE_UI=false
GORGE_USER=""
GORGE_GROUP=""
GORGE_API_VERSION=v3
Expand Down
106 changes: 53 additions & 53 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ import (
"github.com/dadav/gorge/internal/utils"
v3 "github.com/dadav/gorge/internal/v3/api"
backend "github.com/dadav/gorge/internal/v3/backend"
"github.com/dadav/gorge/internal/v3/ui"
openapi "github.com/dadav/gorge/pkg/gen/v3/openapi"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/go-chi/jwtauth/v5"
"github.com/go-chi/stampede"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
Expand Down Expand Up @@ -127,34 +127,20 @@ You can also enable the caching functionality to speed things up.`,
r := chi.NewRouter()

// Logger should come before any middleware that modifies the response
// r.Use(middleware.Logger)
r.Use(middleware.Logger)
// Recoverer should also be pretty high in the middleware stack
r.Use(middleware.Recoverer)
r.Use(middleware.RealIP)
r.Use(customMiddleware.RequireUserAgent)
x := customMiddleware.NewStatistics()
r.Use(customMiddleware.StatisticsMiddleware(x))
r.Use(cors.Handler(cors.Options{
AllowedOrigins: strings.Split(config.CORSOrigins, ","),
AllowedMethods: []string{"GET", "POST", "DELETE", "PATCH"},
AllowedHeaders: []string{"Accept", "Content-Type"},
AllowCredentials: false,
MaxAge: 300,
}))

if !config.Dev {
tokenAuth := jwtauth.New("HS256", []byte(config.JwtSecret), nil)
r.Use(customMiddleware.AuthMiddleware(tokenAuth, func(r *http.Request) bool {
// Everything but GET is protected and requires a jwt token
return r.Method != "GET"
}))

_, tokenString, _ := tokenAuth.Encode(map[string]interface{}{"user": "admin"})
err = os.WriteFile(config.JwtTokenPath, []byte(tokenString), 0400)
if err != nil {
log.Log.Fatal(err)
}
log.Log.Infof("JWT token was written to %s", config.JwtTokenPath)
}

if !config.NoCache {
customKeyFunc := func(r *http.Request) uint64 {
token := r.Header.Get("Authorization")
Expand All @@ -164,45 +150,58 @@ You can also enable the caching functionality to speed things up.`,
r.Use(cachedMiddleware)
}

if config.FallbackProxyUrl != "" {
proxies := strings.Split(config.FallbackProxyUrl, ",")
slices.Reverse(proxies)

for _, proxy := range proxies {
r.Use(customMiddleware.ProxyFallback(proxy, func(status int) bool {
return status == http.StatusNotFound
},
func(r *http.Response) {
if config.ImportProxiedReleases && strings.HasPrefix(r.Request.URL.Path, "/v3/files/") && r.StatusCode == http.StatusOK {
body, err := io.ReadAll(r.Body)
if err != nil {
log.Log.Error(err)
return
}
if config.UI {
r.Group(func(r chi.Router) {
r.HandleFunc("/", ui.IndexHandler)
r.HandleFunc("/search", ui.SearchHandler)
r.HandleFunc("/modules/{module}", ui.ModuleHandler)
r.HandleFunc("/modules/{module}/{version}", ui.ReleaseHandler)
r.HandleFunc("/authors/{author}", ui.AuthorHandler)
r.HandleFunc("/statistics", ui.StatisticsHandler(x))
r.Handle("/assets/*", ui.HandleAssets())
})
}

// restore the body
r.Body = io.NopCloser(bytes.NewBuffer(body))
r.Group(func(r chi.Router) {
if config.FallbackProxyUrl != "" {
proxies := strings.Split(config.FallbackProxyUrl, ",")
slices.Reverse(proxies)

release, err := backend.ConfiguredBackend.AddRelease(body)
if err != nil {
log.Log.Error(err)
return
}
log.Log.Infof("Imported release %s\n", release.Slug)
}
for _, proxy := range proxies {
r.Use(customMiddleware.ProxyFallback(proxy, func(status int) bool {
return status == http.StatusNotFound
},
))
func(r *http.Response) {
if config.ImportProxiedReleases && strings.HasPrefix(r.Request.URL.Path, "/v3/files/") && r.StatusCode == http.StatusOK {
body, err := io.ReadAll(r.Body)
if err != nil {
log.Log.Error(err)
return
}

// restore the body
r.Body = io.NopCloser(bytes.NewBuffer(body))

release, err := backend.ConfiguredBackend.AddRelease(body)
if err != nil {
log.Log.Error(err)
return
}
log.Log.Infof("Imported release %s\n", release.Slug)
}
},
))
}
}
}

apiRouter := openapi.NewRouter(
openapi.NewModuleOperationsAPIController(moduleService),
openapi.NewReleaseOperationsAPIController(releaseService),
openapi.NewSearchFilterOperationsAPIController(searchFilterService),
openapi.NewUserOperationsAPIController(userService),
)

r.Mount("/", apiRouter)
apiRouter := openapi.NewRouter(
openapi.NewModuleOperationsAPIController(moduleService),
openapi.NewReleaseOperationsAPIController(releaseService),
openapi.NewSearchFilterOperationsAPIController(searchFilterService),
openapi.NewUserOperationsAPIController(userService),
)

r.Mount("/v3", apiRouter)
})

r.Get("/readyz", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
Expand Down Expand Up @@ -325,6 +324,7 @@ func init() {
serveCmd.Flags().StringVar(&config.FallbackProxyUrl, "fallback-proxy", "", "optional comma separated list of fallback upstream proxy urls")
serveCmd.Flags().BoolVar(&config.Dev, "dev", false, "enables dev mode")
serveCmd.Flags().BoolVar(&config.DropPrivileges, "drop-privileges", false, "drops privileges to the given user/group")
serveCmd.Flags().BoolVar(&config.UI, "ui", false, "enables the web ui")
serveCmd.Flags().StringVar(&config.CachePrefixes, "cache-prefixes", "/v3/files", "url prefixes to cache")
serveCmd.Flags().StringVar(&config.JwtSecret, "jwt-secret", "changeme", "jwt secret")
serveCmd.Flags().StringVar(&config.JwtTokenPath, "jwt-token-path", "~/.gorge/token", "jwt token path")
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/dadav/gorge
go 1.22.0

require (
github.com/a-h/templ v0.2.747
github.com/go-chi/chi/v5 v5.0.12
github.com/go-chi/cors v1.2.1
github.com/go-chi/jwtauth/v5 v5.3.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg=
github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ var (
Bind string
Dev bool
DropPrivileges bool
UI bool
ModulesDir string
ModulesScanSec int
Backend string
Expand Down
50 changes: 50 additions & 0 deletions internal/middleware/stats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package middleware

import (
"net/http"
"sync"
"time"
)

type Statistics struct {
ActiveConnections int
TotalConnections int
TotalResponseTime time.Duration
ConnectionsPerEndpoint map[string]int
ResponseTimePerEndpoint map[string]time.Duration
Mutex sync.Mutex
}

func NewStatistics() *Statistics {
return &Statistics{
ActiveConnections: 0,
TotalConnections: 0,
TotalResponseTime: 0,
ConnectionsPerEndpoint: make(map[string]int),
ResponseTimePerEndpoint: make(map[string]time.Duration),
}
}

func StatisticsMiddleware(stats *Statistics) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
stats.Mutex.Lock()
stats.ActiveConnections++
stats.TotalConnections++
stats.ConnectionsPerEndpoint[r.URL.Path]++
stats.Mutex.Unlock()

defer func() {
duration := time.Since(start)
stats.Mutex.Lock()
stats.ActiveConnections--
stats.TotalResponseTime += duration
stats.ResponseTimePerEndpoint[r.URL.Path] += duration
stats.Mutex.Unlock()
}()

next.ServeHTTP(w, r)
})
}
}
13 changes: 13 additions & 0 deletions internal/v3/ui/assets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ui

import (
"embed"
"net/http"
)

//go:embed all:assets
var assets embed.FS

func HandleAssets() http.Handler {
return http.FileServer(http.FS(assets))
}
Binary file added internal/v3/ui/assets/favicon.ico
Binary file not shown.
1 change: 1 addition & 0 deletions internal/v3/ui/assets/htmx.min.js

Large diffs are not rendered by default.

Binary file added internal/v3/ui/assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions internal/v3/ui/assets/pico.min.css

Large diffs are not rendered by default.

Loading

0 comments on commit 90f34ea

Please sign in to comment.