modified internal/server/middleware.go +31 -0
| @@ -5,6 +5,9 @@ import ( |
| 5 | 5 | "encoding/hex" |
| 6 | 6 | "net/http" |
| 7 | 7 | "strings" |
| 8 | + "time" |
| 9 | + |
| 10 | + "github.com/go-chi/chi/v5/middleware" |
| 8 | 11 | ) |
| 9 | 12 | |
| 10 | 13 | func (s *Server) sessionMiddleware(next http.Handler) http.Handler { |
| @@ -81,3 +84,31 @@ func (s *Server) csrfMiddleware(next http.Handler) http.Handler { |
| 81 | 84 | func sameOrigin(origin, host string) bool { |
| 82 | 85 | return strings.HasPrefix(origin, "http://"+host) || strings.HasPrefix(origin, "https://"+host) |
| 83 | 86 | } |
| 87 | + |
| 88 | +func (s *Server) realIPLogger(next http.Handler) http.Handler { |
| 89 | + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 90 | + start := time.Now() |
| 91 | + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) |
| 92 | + |
| 93 | + next.ServeHTTP(ww, r) |
| 94 | + |
| 95 | + ip := r.RemoteAddr |
| 96 | + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { |
| 97 | + ip = strings.TrimSpace(strings.SplitN(xff, ",", 2)[0]) |
| 98 | + } |
| 99 | + |
| 100 | + scheme := "http" |
| 101 | + if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" { |
| 102 | + scheme = "https" |
| 103 | + } |
| 104 | + |
| 105 | + s.logger.Info("request", |
| 106 | + "method", r.Method, |
| 107 | + "url", scheme+"://"+r.Host+r.RequestURI, |
| 108 | + "from", ip, |
| 109 | + "status", ww.Status(), |
| 110 | + "bytes", ww.BytesWritten(), |
| 111 | + "duration", time.Since(start).Round(time.Microsecond), |
| 112 | + ) |
| 113 | + }) |
| 114 | +} |
| @@ -5,6 +5,9 @@ import ( |
| 5 | "encoding/hex" | 5 | "encoding/hex" |
| 6 | "net/http" | 6 | "net/http" |
| 7 | "strings" | 7 | "strings" |
| | 8 | + "time" |
| | 9 | + |
| | 10 | + "github.com/go-chi/chi/v5/middleware" |
| 8 | ) | 11 | ) |
| 9 | | 12 | |
| 10 | func (s *Server) sessionMiddleware(next http.Handler) http.Handler { | 13 | func (s *Server) sessionMiddleware(next http.Handler) http.Handler { |
| @@ -81,3 +84,31 @@ func (s *Server) csrfMiddleware(next http.Handler) http.Handler { |
| 81 | func sameOrigin(origin, host string) bool { | 84 | func sameOrigin(origin, host string) bool { |
| 82 | return strings.HasPrefix(origin, "http://"+host) || strings.HasPrefix(origin, "https://"+host) | 85 | return strings.HasPrefix(origin, "http://"+host) || strings.HasPrefix(origin, "https://"+host) |
| 83 | } | 86 | } |
| | 87 | + |
| | 88 | +func (s *Server) realIPLogger(next http.Handler) http.Handler { |
| | 89 | + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| | 90 | + start := time.Now() |
| | 91 | + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) |
| | 92 | + |
| | 93 | + next.ServeHTTP(ww, r) |
| | 94 | + |
| | 95 | + ip := r.RemoteAddr |
| | 96 | + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { |
| | 97 | + ip = strings.TrimSpace(strings.SplitN(xff, ",", 2)[0]) |
| | 98 | + } |
| | 99 | + |
| | 100 | + scheme := "http" |
| | 101 | + if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" { |
| | 102 | + scheme = "https" |
| | 103 | + } |
| | 104 | + |
| | 105 | + s.logger.Info("request", |
| | 106 | + "method", r.Method, |
| | 107 | + "url", scheme+"://"+r.Host+r.RequestURI, |
| | 108 | + "from", ip, |
| | 109 | + "status", ww.Status(), |
| | 110 | + "bytes", ww.BytesWritten(), |
| | 111 | + "duration", time.Since(start).Round(time.Microsecond), |
| | 112 | + ) |
| | 113 | + }) |
| | 114 | +} |
modified internal/server/server.go +1 -1
| @@ -85,7 +85,7 @@ func New(database *db.DB, clientID, callbackURL, addr string, scheduler *feed.Sc |
| 85 | 85 | } |
| 86 | 86 | |
| 87 | 87 | func (s *Server) setupMiddleware() { |
| 88 | | - s.router.Use(middleware.Logger) |
| 88 | + s.router.Use(s.realIPLogger) |
| 89 | 89 | s.router.Use(middleware.Recoverer) |
| 90 | 90 | s.router.Use(middleware.Compress(5)) |
| 91 | 91 | s.router.Use(s.metricsMiddleware) |
| @@ -85,7 +85,7 @@ func New(database *db.DB, clientID, callbackURL, addr string, scheduler *feed.Sc |
| 85 | } | 85 | } |
| 86 | | 86 | |
| 87 | func (s *Server) setupMiddleware() { | 87 | func (s *Server) setupMiddleware() { |
| 88 | - s.router.Use(middleware.Logger) | 88 | + s.router.Use(s.realIPLogger) |
| 89 | s.router.Use(middleware.Recoverer) | 89 | s.router.Use(middleware.Recoverer) |
| 90 | s.router.Use(middleware.Compress(5)) | 90 | s.router.Use(middleware.Compress(5)) |
| 91 | s.router.Use(s.metricsMiddleware) | 91 | s.router.Use(s.metricsMiddleware) |
modified internal/tmpl/dashboard.html +14 -0
| @@ -6,6 +6,19 @@ |
| 6 | 6 | <span class="text-spot-secondary"><strong class="text-spot-text">{{.SubscriptionCount}}</strong> feeds</span> |
| 7 | 7 | </div> |
| 8 | 8 | </div> |
| 9 | +{{if eq .SubscriptionCount 0}} |
| 10 | +<div class="bg-spot-surface rounded-xl py-16 px-6 text-center mb-6"> |
| 11 | + <div class="w-16 h-16 rounded-full bg-spot-hover flex items-center justify-center mb-4 mx-auto"> |
| 12 | + <svg class="w-8 h-8 text-spot-muted" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12.75 19.5v-15m0 0l-6.75 6.75M12.75 4.5l6.75 6.75"/></svg> |
| 13 | + </div> |
| 14 | + <h2 class="text-lg font-semibold text-spot-text mb-2">Add your first feed</h2> |
| 15 | + <p class="text-sm text-spot-secondary mb-6">Subscribe to RSS feeds to start building your personalized reading experience.</p> |
| 16 | + <a href="/feeds" class="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-spot-green text-white text-sm font-medium hover:opacity-90 transition"> |
| 17 | + <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg> |
| 18 | + Add feeds |
| 19 | + </a> |
| 20 | +</div> |
| 21 | +{{else}} |
| 9 | 22 | <p class="text-sm text-spot-secondary mb-6">Your personalized feed, based on your social graph.</p> |
| 10 | 23 | |
| 11 | 24 | <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> |
| @@ -119,3 +132,4 @@ |
| 119 | 132 | </div> |
| 120 | 133 | </div> |
| 121 | 134 | {{end}} |
| 135 | +{{end}} |
| @@ -6,6 +6,19 @@ |
| 6 | <span class="text-spot-secondary"><strong class="text-spot-text">{{.SubscriptionCount}}</strong> feeds</span> | 6 | <span class="text-spot-secondary"><strong class="text-spot-text">{{.SubscriptionCount}}</strong> feeds</span> |
| 7 | </div> | 7 | </div> |
| 8 | </div> | 8 | </div> |
| | 9 | +{{if eq .SubscriptionCount 0}} |
| | 10 | +<div class="bg-spot-surface rounded-xl py-16 px-6 text-center mb-6"> |
| | 11 | + <div class="w-16 h-16 rounded-full bg-spot-hover flex items-center justify-center mb-4 mx-auto"> |
| | 12 | + <svg class="w-8 h-8 text-spot-muted" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12.75 19.5v-15m0 0l-6.75 6.75M12.75 4.5l6.75 6.75"/></svg> |
| | 13 | + </div> |
| | 14 | + <h2 class="text-lg font-semibold text-spot-text mb-2">Add your first feed</h2> |
| | 15 | + <p class="text-sm text-spot-secondary mb-6">Subscribe to RSS feeds to start building your personalized reading experience.</p> |
| | 16 | + <a href="/feeds" class="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-spot-green text-white text-sm font-medium hover:opacity-90 transition"> |
| | 17 | + <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg> |
| | 18 | + Add feeds |
| | 19 | + </a> |
| | 20 | +</div> |
| | 21 | +{{else}} |
| 9 | <p class="text-sm text-spot-secondary mb-6">Your personalized feed, based on your social graph.</p> | 22 | <p class="text-sm text-spot-secondary mb-6">Your personalized feed, based on your social graph.</p> |
| 10 | | 23 | |
| 11 | <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> | 24 | <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> |
| @@ -119,3 +132,4 @@ |
| 119 | </div> | 132 | </div> |
| 120 | </div> | 133 | </div> |
| 121 | {{end}} | 134 | {{end}} |
| | 135 | +{{end}} |