package web import ( "context" "errors" "fmt" "log" "log/slog" "net/http" "time" "github.com/go-chi/chi/v5/middleware" "github.com/golang-jwt/jwt/v5" ) type contextKey string const userIDKey contextKey = "userID" func (s *Server) logRequest(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() // 1. Wrap the response writer // This intercepts the Status code when it's written later in the chain ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) // 2. Call the next handler in the chain next.ServeHTTP(ww, r) // 3. Capture the actual results status := ww.Status() duration := time.Since(start) // 4. Build the log attributes // Using slog.Group keeps the "request" data organized requestGroup := slog.Group("request", slog.String("method", r.Method), slog.String("path", r.URL.Path), slog.String("ip", r.RemoteAddr), ) // 5. Log with appropriate level based on status if status >= 400 { s.logger.Error("HTTP Request Error", requestGroup, slog.Int("status", status), slog.Duration("took", duration), ) } else { s.logger.Info("HTTP Request", requestGroup, slog.Int("status", status), slog.Duration("took", duration), ) } }) } func (s *Server) hasAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var tokenString string // Authorization Header Bearer token authHeader := r.Header.Get("Authorization") if len(authHeader) > 7 && authHeader[:7] == "Bearer " { tokenString = authHeader[7:] } // Fallback to Cookie (future Web Frontend) if tokenString == "" { if cookie, err := r.Cookie("nfeeder_token"); err == nil { tokenString = cookie.Value } } if tokenString == "" { WriteJSON(w, http.StatusUnauthorized, ErrorResponse{ Error: "unauthorized", }) return } // parse and validate claims := &jwt.RegisteredClaims{} token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) { // check/confirm alg if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) } return s.jwtSecret, nil }) /// golang-jwt the library internally runs a Valid(): /// Time Check: It compares time.Now() against the ExpiresAt value. /// Not Before Check: It checks if the nbf (Not Before) time has passed. /// Issued At Check: It ensures the iat isn't in the future. if err != nil || !token.Valid { // Check if the error is specifically because the token expired if errors.Is(err, jwt.ErrTokenExpired) { WriteJSON(w, http.StatusUnauthorized, ErrorResponse{ Error: "Token expired: Please use your refresh token to get a new session", }) return } WriteJSON(w, http.StatusUnauthorized, ErrorResponse{ Error: "unauthorized", }) return } // Verify issuer if claims.Issuer != ISSUER { log.Printf("Invalid Token, issuer incorrect or tampered with") WriteJSON(w, http.StatusUnauthorized, ErrorResponse{ Error: "Invalid Token", }) return } // Success! Store userID in context for handlers to use ctx := context.WithValue(r.Context(), userIDKey, claims.Subject) next.ServeHTTP(w, r.WithContext(ctx)) }) }