feat: show new updates in dashboard (#690)

* feat: show new update banner

* feat(http): add request logger

* refactor: updates checker

* feat: make update check optional

* fix: empty releases

* add toggle switch for update checks

* feat: toggle updates check from settings

* feat: toggle updates check from settings

* feat: check on toggle enabled

---------

Co-authored-by: soup <soup@r4tio.dev>
This commit is contained in:
ze0s 2023-02-05 18:44:11 +01:00 committed by GitHub
parent 3fdd7cf5e4
commit 2917a7d42d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 687 additions and 121 deletions

View file

@ -1,52 +1,84 @@
package http
import (
"encoding/json"
"net/http"
"github.com/autobrr/autobrr/internal/config"
"github.com/autobrr/autobrr/internal/domain"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
type configJson struct {
Host string `json:"host"`
Port int `json:"port"`
LogLevel string `json:"log_level"`
LogPath string `json:"log_path"`
BaseURL string `json:"base_url"`
Version string `json:"version"`
Commit string `json:"commit"`
Date string `json:"date"`
Host string `json:"host"`
Port int `json:"port"`
LogLevel string `json:"log_level"`
LogPath string `json:"log_path"`
BaseURL string `json:"base_url"`
CheckForUpdates bool `json:"check_for_updates"`
Version string `json:"version"`
Commit string `json:"commit"`
Date string `json:"date"`
}
type configHandler struct {
encoder encoder
cfg *config.AppConfig
server Server
}
func newConfigHandler(encoder encoder, server Server) *configHandler {
func newConfigHandler(encoder encoder, server Server, cfg *config.AppConfig) *configHandler {
return &configHandler{
encoder: encoder,
cfg: cfg,
server: server,
}
}
func (h configHandler) Routes(r chi.Router) {
r.Get("/", h.getConfig)
r.Patch("/", h.updateConfig)
}
func (h configHandler) getConfig(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
conf := configJson{
Host: h.server.config.Host,
Port: h.server.config.Port,
LogLevel: h.server.config.LogLevel,
LogPath: h.server.config.LogPath,
BaseURL: h.server.config.BaseURL,
Version: h.server.version,
Commit: h.server.commit,
Date: h.server.date,
Host: h.cfg.Config.Host,
Port: h.cfg.Config.Port,
LogLevel: h.cfg.Config.LogLevel,
LogPath: h.cfg.Config.LogPath,
BaseURL: h.cfg.Config.BaseURL,
CheckForUpdates: h.cfg.Config.CheckForUpdates,
Version: h.server.version,
Commit: h.server.commit,
Date: h.server.date,
}
h.encoder.StatusResponse(ctx, w, conf, http.StatusOK)
render.JSON(w, r, conf)
}
func (h configHandler) updateConfig(w http.ResponseWriter, r *http.Request) {
var data domain.ConfigUpdate
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
h.encoder.Error(w, err)
return
}
if data.CheckForUpdates != nil {
h.cfg.Config.CheckForUpdates = *data.CheckForUpdates
}
if err := h.cfg.UpdateConfig(); err != nil {
render.Status(r, http.StatusInternalServerError)
render.JSON(w, r, errorResponse{
Message: err.Error(),
Status: http.StatusInternalServerError,
})
return
}
render.NoContent(w, r)
}

View file

@ -1,6 +1,13 @@
package http
import "net/http"
import (
"net/http"
"runtime/debug"
"time"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog"
)
func (s Server) IsAuthenticated(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -31,3 +38,49 @@ func (s Server) IsAuthenticated(next http.Handler) http.Handler {
next.ServeHTTP(w, r)
})
}
func LoggerMiddleware(logger *zerolog.Logger) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
log := logger.With().Logger()
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
t1 := time.Now()
defer func() {
t2 := time.Now()
// Recover and record stack traces in case of a panic
if rec := recover(); rec != nil {
log.Error().
Str("type", "error").
Timestamp().
Interface("recover_info", rec).
Bytes("debug_stack", debug.Stack()).
Msg("log system error")
http.Error(ww, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
// log end request
log.Info().
Str("type", "access").
Timestamp().
Fields(map[string]interface{}{
"remote_ip": r.RemoteAddr,
"url": r.URL.Path,
"proto": r.Proto,
"method": r.Method,
"user_agent": r.Header.Get("User-Agent"),
"status": ww.Status(),
"latency_ms": float64(t2.Sub(t1).Nanoseconds()) / 1000000.0,
"bytes_in": r.Header.Get("Content-Length"),
"bytes_out": ww.BytesWritten(),
}).
Msg("incoming_request")
}()
next.ServeHTTP(ww, r)
}
return http.HandlerFunc(fn)
}
}

View file

@ -6,8 +6,8 @@ import (
"net"
"net/http"
"github.com/autobrr/autobrr/internal/config"
"github.com/autobrr/autobrr/internal/database"
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/internal/logger"
"github.com/autobrr/autobrr/web"
@ -24,7 +24,7 @@ type Server struct {
sse *sse.Server
db *database.DB
config *domain.Config
config *config.AppConfig
cookieStore *sessions.CookieStore
version string
@ -41,9 +41,10 @@ type Server struct {
ircService ircService
notificationService notificationService
releaseService releaseService
updateService updateService
}
func NewServer(log logger.Logger, config *domain.Config, sse *sse.Server, db *database.DB, version string, commit string, date string, actionService actionService, apiService apikeyService, authService authService, downloadClientSvc downloadClientService, filterSvc filterService, feedSvc feedService, indexerSvc indexerService, ircSvc ircService, notificationSvc notificationService, releaseSvc releaseService) Server {
func NewServer(log logger.Logger, config *config.AppConfig, sse *sse.Server, db *database.DB, version string, commit string, date string, actionService actionService, apiService apikeyService, authService authService, downloadClientSvc downloadClientService, filterSvc filterService, feedSvc feedService, indexerSvc indexerService, ircSvc ircService, notificationSvc notificationService, releaseSvc releaseService, updateSvc updateService) Server {
return Server{
log: log.With().Str("module", "http").Logger(),
config: config,
@ -53,7 +54,7 @@ func NewServer(log logger.Logger, config *domain.Config, sse *sse.Server, db *da
commit: commit,
date: date,
cookieStore: sessions.NewCookieStore([]byte(config.SessionSecret)),
cookieStore: sessions.NewCookieStore([]byte(config.Config.SessionSecret)),
actionService: actionService,
apiService: apiService,
@ -65,11 +66,12 @@ func NewServer(log logger.Logger, config *domain.Config, sse *sse.Server, db *da
ircService: ircSvc,
notificationService: notificationSvc,
releaseService: releaseSvc,
updateService: updateSvc,
}
}
func (s Server) Open() error {
addr := fmt.Sprintf("%v:%v", s.config.Host, s.config.Port)
addr := fmt.Sprintf("%v:%v", s.config.Config.Host, s.config.Config.Port)
listener, err := net.Listen("tcp", addr)
if err != nil {
return err
@ -79,6 +81,8 @@ func (s Server) Open() error {
Handler: s.Handler(),
}
s.log.Info().Msgf("Starting server. Listening on %s", listener.Addr().String())
return server.Serve(listener)
}
@ -88,6 +92,7 @@ func (s Server) Handler() http.Handler {
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Recoverer)
r.Use(LoggerMiddleware(&s.log))
c := cors.New(cors.Options{
AllowCredentials: true,
@ -113,15 +118,15 @@ func (s Server) Handler() http.Handler {
fileSystem.ServeHTTP(w, r)
})
r.Route("/api/auth", newAuthHandler(encoder, s.log, s.config, s.cookieStore, s.authService).Routes)
r.Route("/api/healthz", newHealthHandler(encoder, s.db).Routes)
r.Route("/api", func(r chi.Router) {
r.Route("/auth", newAuthHandler(encoder, s.log, s.config.Config, s.cookieStore, s.authService).Routes)
r.Route("/healthz", newHealthHandler(encoder, s.db).Routes)
r.Group(func(r chi.Router) {
r.Use(s.IsAuthenticated)
r.Group(func(r chi.Router) {
r.Use(s.IsAuthenticated)
r.Route("/api", func(r chi.Router) {
r.Route("/actions", newActionHandler(encoder, s.actionService).Routes)
r.Route("/config", newConfigHandler(encoder, s).Routes)
r.Route("/config", newConfigHandler(encoder, s, s.config).Routes)
r.Route("/download_clients", newDownloadClientHandler(encoder, s.downloadClientService).Routes)
r.Route("/filters", newFilterHandler(encoder, s.filterService).Routes)
r.Route("/feeds", newFeedHandler(encoder, s.feedService).Routes)
@ -130,6 +135,7 @@ func (s Server) Handler() http.Handler {
r.Route("/keys", newAPIKeyHandler(encoder, s.apiService).Routes)
r.Route("/notification", newNotificationHandler(encoder, s.notificationService).Routes)
r.Route("/release", newReleaseHandler(encoder, s.releaseService).Routes)
r.Route("/updates", newUpdateHandler(encoder, s.updateService).Routes)
r.HandleFunc("/events", func(w http.ResponseWriter, r *http.Request) {
@ -157,7 +163,7 @@ func (s Server) index(w http.ResponseWriter, r *http.Request) {
p := web.IndexParams{
Title: "Dashboard",
Version: s.version,
BaseUrl: s.config.BaseURL,
BaseUrl: s.config.Config.BaseURL,
}
web.Index(w, p)
}

50
internal/http/update.go Normal file
View file

@ -0,0 +1,50 @@
package http
import (
"context"
"net/http"
"github.com/autobrr/autobrr/pkg/version"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
type updateService interface {
CheckUpdates(ctx context.Context)
GetLatestRelease(ctx context.Context) *version.Release
}
type updateHandler struct {
encoder encoder
service updateService
}
func newUpdateHandler(encoder encoder, service updateService) *updateHandler {
return &updateHandler{
encoder: encoder,
service: service,
}
}
func (h updateHandler) Routes(r chi.Router) {
r.Get("/latest", h.getLatest)
r.Get("/check", h.checkUpdates)
}
func (h updateHandler) getLatest(w http.ResponseWriter, r *http.Request) {
latest := h.service.GetLatestRelease(r.Context())
if latest != nil {
render.Status(r, http.StatusOK)
render.JSON(w, r, latest)
return
}
render.NoContent(w, r)
}
func (h updateHandler) checkUpdates(w http.ResponseWriter, r *http.Request) {
h.service.CheckUpdates(r.Context())
render.NoContent(w, r)
}