chore(deps): migrate fiber from v2 to v3

The automated update changed only go.mod, so the build failed on the old v2 imports. This commit moves the code to the v3 API.

The commit replaces BodyParser with Bind().Body, Static with the static middleware, and ListenTLS with ListenConfig. It also updates the tests for TestConfig.

Fixes #179
This commit is contained in:
2026-10-04 00:50:37 +01:00
parent 7d75dac29c
commit f72b481720
14 changed files with 127 additions and 116 deletions
+7 -5
View File
@@ -15,8 +15,9 @@ import (
"syscall"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/adaptor"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/adaptor"
"github.com/gofiber/fiber/v3/middleware/static"
"github.com/lukaszraczylo/gohoarder/pkg/analytics"
"github.com/lukaszraczylo/gohoarder/pkg/auth"
"github.com/lukaszraczylo/gohoarder/pkg/cache"
@@ -554,10 +555,10 @@ func (a *App) setupServer() error {
frontendDir := "frontend/dist"
if _, err := os.Stat(frontendDir); err == nil {
log.Info().Str("dir", frontendDir).Msg("Serving frontend static files")
a.app.Static("/", frontendDir)
a.app.Use("/", static.New(frontendDir))
} else {
log.Warn().Msg("Frontend dist directory not found, frontend won't be served")
a.app.Get("/", func(c *fiber.Ctx) error {
a.app.Get("/", func(c fiber.Ctx) error {
return c.Type("html").SendString(`
<html>
<head><title>GoHoarder</title></head>
@@ -607,7 +608,7 @@ func (a *App) Run() error {
Str("addr", addr).
Str("cert_file", a.config.Server.TLS.CertFile).
Msg("Starting Fiber server with TLS")
if err := a.app.ListenTLS(addr, a.config.Server.TLS.CertFile, a.config.Server.TLS.KeyFile); err != nil {
if err := a.app.Listen(addr, fiber.ListenConfig{CertFile: a.config.Server.TLS.CertFile, CertKeyFile: a.config.Server.TLS.KeyFile}); err != nil {
errChan <- err
}
return
@@ -643,6 +644,7 @@ func (a *App) Run() error {
// Graceful shutdown
return a.Shutdown()
}
// Shutdown gracefully shuts down the application
func (a *App) Shutdown() error {
log.Info().Msg("Starting graceful shutdown")
+8 -8
View File
@@ -4,7 +4,7 @@ import (
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v3"
"github.com/lukaszraczylo/gohoarder/internal/version"
"github.com/lukaszraczylo/gohoarder/pkg/metadata"
"github.com/lukaszraczylo/gohoarder/pkg/websocket"
@@ -12,7 +12,7 @@ import (
)
// handlePackages handles /api/packages endpoint
func (a *App) handlePackages(c *fiber.Ctx) error {
func (a *App) handlePackages(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
c.Set("Access-Control-Allow-Origin", "*")
c.Set("Access-Control-Allow-Methods", "GET, DELETE, OPTIONS")
@@ -38,7 +38,7 @@ func (a *App) handlePackages(c *fiber.Ctx) error {
}
// handleListPackages returns list of cached packages
func (a *App) handleListPackages(c *fiber.Ctx) error {
func (a *App) handleListPackages(c fiber.Ctx) error {
ctx := c.Context()
// Get packages from metadata store
@@ -201,7 +201,7 @@ func (a *App) handleListPackages(c *fiber.Ctx) error {
}
// handleDeletePackage deletes a cached package
func (a *App) handleDeletePackage(c *fiber.Ctx) error {
func (a *App) handleDeletePackage(c fiber.Ctx) error {
ctx := c.Context()
// Parse path: /api/packages/{registry}/{name}/{version}
@@ -331,7 +331,7 @@ func (a *App) handleDeletePackage(c *fiber.Ctx) error {
}
// handleStats handles /api/stats endpoint
func (a *App) handleStats(c *fiber.Ctx) error {
func (a *App) handleStats(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
c.Set("Access-Control-Allow-Origin", "*")
c.Set("Access-Control-Allow-Methods", "GET, OPTIONS")
@@ -421,7 +421,7 @@ func (a *App) handleStats(c *fiber.Ctx) error {
// handleTimeSeriesStats handles /api/stats/timeseries endpoint
// Returns time-series download statistics for charts
func (a *App) handleTimeSeriesStats(c *fiber.Ctx) error {
func (a *App) handleTimeSeriesStats(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
c.Set("Access-Control-Allow-Origin", "*")
c.Set("Access-Control-Allow-Methods", "GET, OPTIONS")
@@ -463,7 +463,7 @@ func (a *App) handleTimeSeriesStats(c *fiber.Ctx) error {
// handleConfig handles /api/config endpoint
// Returns runtime configuration for the frontend
func (a *App) handleConfig(c *fiber.Ctx) error {
func (a *App) handleConfig(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
c.Set("Access-Control-Allow-Origin", "*")
c.Set("Access-Control-Allow-Methods", "GET, OPTIONS")
@@ -497,7 +497,7 @@ func (a *App) handleConfig(c *fiber.Ctx) error {
}
// handleInfo handles /api/info endpoint
func (a *App) handleInfo(c *fiber.Ctx) error {
func (a *App) handleInfo(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
c.Set("Access-Control-Allow-Origin", "*")
c.Set("Access-Control-Allow-Methods", "GET, OPTIONS")
+10 -10
View File
@@ -3,7 +3,7 @@ package app
import (
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v3"
"github.com/lukaszraczylo/gohoarder/pkg/auth"
"github.com/lukaszraczylo/gohoarder/pkg/errors"
"github.com/lukaszraczylo/gohoarder/pkg/metadata"
@@ -14,7 +14,7 @@ import (
// requireAdmin middleware checks for admin authentication. It accepts the API
// key via either the Authorization Bearer header or the X-API-Key header
// (shared extractAPIKey), so admin clients are not limited to the bearer form.
func (a *App) requireAdmin(c *fiber.Ctx) error {
func (a *App) requireAdmin(c fiber.Ctx) error {
// Get API key from Authorization or X-API-Key header
apiKey, ok := extractAPIKey(c)
if !ok {
@@ -43,7 +43,7 @@ func (a *App) requireAdmin(c *fiber.Ctx) error {
}
// handleAdminBypasses handles /api/admin/bypasses endpoint
func (a *App) handleAdminBypasses(c *fiber.Ctx) error {
func (a *App) handleAdminBypasses(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
c.Set("Access-Control-Allow-Origin", "*")
c.Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS")
@@ -74,7 +74,7 @@ func (a *App) handleAdminBypasses(c *fiber.Ctx) error {
}
// handleListBypasses lists all CVE bypasses
func (a *App) handleListBypasses(c *fiber.Ctx) error {
func (a *App) handleListBypasses(c fiber.Ctx) error {
ctx := c.Context()
// Parse query parameters
@@ -112,11 +112,11 @@ type CreateBypassRequest struct {
}
// handleCreateBypass creates a new CVE bypass
func (a *App) handleCreateBypass(c *fiber.Ctx) error {
func (a *App) handleCreateBypass(c fiber.Ctx) error {
ctx := c.Context()
var req CreateBypassRequest
if err := c.BodyParser(&req); err != nil {
if err := c.Bind().Body(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON in request body"})
}
@@ -179,7 +179,7 @@ func (a *App) handleCreateBypass(c *fiber.Ctx) error {
}
// handleGetBypass gets a specific bypass by ID
func (a *App) handleGetBypass(c *fiber.Ctx) error {
func (a *App) handleGetBypass(c fiber.Ctx) error {
ctx := c.Context()
// Extract ID from parameter
@@ -217,7 +217,7 @@ type UpdateBypassRequest struct {
}
// handleUpdateBypass updates a bypass (activate/deactivate or extend expiration)
func (a *App) handleUpdateBypass(c *fiber.Ctx) error {
func (a *App) handleUpdateBypass(c fiber.Ctx) error {
ctx := c.Context()
// Extract ID from parameter
@@ -228,7 +228,7 @@ func (a *App) handleUpdateBypass(c *fiber.Ctx) error {
}
var req UpdateBypassRequest
if err := c.BodyParser(&req); err != nil {
if err := c.Bind().Body(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON in request body"})
}
@@ -284,7 +284,7 @@ func (a *App) handleUpdateBypass(c *fiber.Ctx) error {
}
// handleDeleteBypass deletes a bypass
func (a *App) handleDeleteBypass(c *fiber.Ctx) error {
func (a *App) handleDeleteBypass(c fiber.Ctx) error {
ctx := c.Context()
// Extract ID from parameter
+8 -8
View File
@@ -3,12 +3,12 @@ package app
import (
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v3"
"github.com/rs/zerolog/log"
)
// handleAnalyticsTopPackages returns the most downloaded packages
func (a *App) handleAnalyticsTopPackages(c *fiber.Ctx) error {
func (a *App) handleAnalyticsTopPackages(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
c.Set("Access-Control-Allow-Origin", "*")
@@ -29,7 +29,7 @@ func (a *App) handleAnalyticsTopPackages(c *fiber.Ctx) error {
}
// handleAnalyticsTrendingPackages returns trending packages
func (a *App) handleAnalyticsTrendingPackages(c *fiber.Ctx) error {
func (a *App) handleAnalyticsTrendingPackages(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
c.Set("Access-Control-Allow-Origin", "*")
@@ -50,7 +50,7 @@ func (a *App) handleAnalyticsTrendingPackages(c *fiber.Ctx) error {
}
// handleAnalyticsTrends returns download trends over time
func (a *App) handleAnalyticsTrends(c *fiber.Ctx) error {
func (a *App) handleAnalyticsTrends(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
c.Set("Access-Control-Allow-Origin", "*")
@@ -62,7 +62,7 @@ func (a *App) handleAnalyticsTrends(c *fiber.Ctx) error {
}
// handleAnalyticsTotalStats returns overall statistics
func (a *App) handleAnalyticsTotalStats(c *fiber.Ctx) error {
func (a *App) handleAnalyticsTotalStats(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
c.Set("Access-Control-Allow-Origin", "*")
@@ -72,7 +72,7 @@ func (a *App) handleAnalyticsTotalStats(c *fiber.Ctx) error {
}
// handleAnalyticsRegistryStats returns per-registry statistics
func (a *App) handleAnalyticsRegistryStats(c *fiber.Ctx) error {
func (a *App) handleAnalyticsRegistryStats(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
c.Set("Access-Control-Allow-Origin", "*")
@@ -89,7 +89,7 @@ func (a *App) handleAnalyticsRegistryStats(c *fiber.Ctx) error {
}
// handleAnalyticsPackageStats returns statistics for a specific package
func (a *App) handleAnalyticsPackageStats(c *fiber.Ctx) error {
func (a *App) handleAnalyticsPackageStats(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
c.Set("Access-Control-Allow-Origin", "*")
@@ -115,7 +115,7 @@ func (a *App) handleAnalyticsPackageStats(c *fiber.Ctx) error {
}
// handleAnalyticsSearch searches for packages matching a query
func (a *App) handleAnalyticsSearch(c *fiber.Ctx) error {
func (a *App) handleAnalyticsSearch(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
c.Set("Access-Control-Allow-Origin", "*")
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"testing"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v3"
"github.com/lukaszraczylo/gohoarder/pkg/analytics"
"github.com/stretchr/testify/suite"
)
+5 -5
View File
@@ -3,7 +3,7 @@ package app
import (
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v3"
"github.com/lukaszraczylo/gohoarder/pkg/auth"
"github.com/rs/zerolog/log"
)
@@ -16,11 +16,11 @@ type GenerateAPIKeyRequest struct {
}
// handleGenerateAPIKey generates a new API key
func (a *App) handleGenerateAPIKey(c *fiber.Ctx) error {
func (a *App) handleGenerateAPIKey(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
var req GenerateAPIKeyRequest
if err := c.BodyParser(&req); err != nil {
if err := c.Bind().Body(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "invalid JSON in request body",
})
@@ -82,7 +82,7 @@ func (a *App) handleGenerateAPIKey(c *fiber.Ctx) error {
}
// handleListAPIKeys lists all API keys
func (a *App) handleListAPIKeys(c *fiber.Ctx) error {
func (a *App) handleListAPIKeys(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
keys := a.authManager.ListAPIKeys()
@@ -108,7 +108,7 @@ func (a *App) handleListAPIKeys(c *fiber.Ctx) error {
}
// handleRevokeAPIKey revokes an API key
func (a *App) handleRevokeAPIKey(c *fiber.Ctx) error {
func (a *App) handleRevokeAPIKey(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
keyID := c.Params("key_id")
+10 -9
View File
@@ -5,8 +5,9 @@ import (
"encoding/json"
"net/http/httptest"
"testing"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v3"
"github.com/lukaszraczylo/gohoarder/pkg/auth"
"github.com/stretchr/testify/suite"
)
@@ -114,7 +115,7 @@ func (s *AuthHandlersTestSuite) TestHandleGenerateAPIKey() {
req := httptest.NewRequest("POST", "/api/admin/keys", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
resp, err := s.app.Test(req, 5000) // 5 second timeout for CI environments
resp, err := s.app.Test(req, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true}) // 5 second timeout for CI environments
s.Require().NoError(err)
s.Equal(tt.expectedStatus, resp.StatusCode)
@@ -144,7 +145,7 @@ func (s *AuthHandlersTestSuite) TestHandleListAPIKeys() {
_, _, _ = s.authManager.GenerateAPIKey("test-key-3", auth.RoleAdmin, nil)
req := httptest.NewRequest("GET", "/api/admin/keys", nil)
resp, err := s.app.Test(req, 5000) // 5 second timeout for CI environments
resp, err := s.app.Test(req, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true}) // 5 second timeout for CI environments
s.Require().NoError(err)
s.Equal(200, resp.StatusCode)
@@ -189,7 +190,7 @@ func (s *AuthHandlersTestSuite) TestHandleRevokeAPIKey() {
for _, tt := range tests {
s.Run(tt.name, func() {
req := httptest.NewRequest("DELETE", "/api/admin/keys/"+tt.keyID, nil)
resp, err := s.app.Test(req, 5000) // 5 second timeout for CI environments
resp, err := s.app.Test(req, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true}) // 5 second timeout for CI environments
s.Require().NoError(err)
s.Equal(tt.expectedStatus, resp.StatusCode)
@@ -207,7 +208,7 @@ func (s *AuthHandlersTestSuite) TestHandleGenerateAPIKeyInvalidJSON() {
req := httptest.NewRequest("POST", "/api/admin/keys", bytes.NewReader([]byte("invalid json")))
req.Header.Set("Content-Type", "application/json")
resp, err := s.app.Test(req, 5000) // 5 second timeout for CI environments
resp, err := s.app.Test(req, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true}) // 5 second timeout for CI environments
s.Require().NoError(err)
s.Equal(400, resp.StatusCode)
}
@@ -221,7 +222,7 @@ func (s *AuthHandlersTestSuite) TestGenerateAndRevokeKeyFlow() {
req1 := httptest.NewRequest("POST", "/api/admin/keys", bytes.NewReader(bodyBytes))
req1.Header.Set("Content-Type", "application/json")
resp1, err := s.app.Test(req1, 5000) // 5 second timeout for CI environments
resp1, err := s.app.Test(req1, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true}) // 5 second timeout for CI environments
s.Require().NoError(err)
s.Equal(201, resp1.StatusCode)
@@ -235,7 +236,7 @@ func (s *AuthHandlersTestSuite) TestGenerateAndRevokeKeyFlow() {
// List keys - should include our new key
req2 := httptest.NewRequest("GET", "/api/admin/keys", nil)
resp2, err := s.app.Test(req2, 5000) // 5 second timeout for CI environments
resp2, err := s.app.Test(req2, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true}) // 5 second timeout for CI environments
s.Require().NoError(err)
s.Equal(200, resp2.StatusCode)
@@ -257,13 +258,13 @@ func (s *AuthHandlersTestSuite) TestGenerateAndRevokeKeyFlow() {
// Revoke the key
req3 := httptest.NewRequest("DELETE", "/api/admin/keys/"+keyID, nil)
resp3, err := s.app.Test(req3, 5000) // 5 second timeout for CI environments
resp3, err := s.app.Test(req3, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true}) // 5 second timeout for CI environments
s.Require().NoError(err)
s.Equal(200, resp3.StatusCode)
// List keys again - should not include the revoked key
req4 := httptest.NewRequest("GET", "/api/admin/keys", nil)
resp4, err := s.app.Test(req4, 5000) // 5 second timeout for CI environments
resp4, err := s.app.Test(req4, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true}) // 5 second timeout for CI environments
s.Require().NoError(err)
s.Equal(200, resp4.StatusCode)
+5 -5
View File
@@ -1,12 +1,12 @@
package app
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v3"
"github.com/rs/zerolog/log"
)
// handlePrewarmingStatus returns the status of the pre-warming worker
func (a *App) handlePrewarmingStatus(c *fiber.Ctx) error {
func (a *App) handlePrewarmingStatus(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
status := a.prewarmWorker.GetStatus()
@@ -15,7 +15,7 @@ func (a *App) handlePrewarmingStatus(c *fiber.Ctx) error {
}
// handlePrewarmingTrigger manually triggers a pre-warming cycle
func (a *App) handlePrewarmingTrigger(c *fiber.Ctx) error {
func (a *App) handlePrewarmingTrigger(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
ctx := c.Context()
@@ -36,11 +36,11 @@ type PrewarmPackageRequest struct {
}
// handlePrewarmingPackage pre-warms a specific package
func (a *App) handlePrewarmingPackage(c *fiber.Ctx) error {
func (a *App) handlePrewarmingPackage(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
var req PrewarmPackageRequest
if err := c.BodyParser(&req); err != nil {
if err := c.Bind().Body(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "invalid JSON in request body",
})
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v3"
"github.com/lukaszraczylo/gohoarder/pkg/prewarming"
"github.com/stretchr/testify/suite"
)
+2 -2
View File
@@ -3,13 +3,13 @@ package app
import (
"strings"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v3"
"github.com/lukaszraczylo/gohoarder/pkg/metadata"
"github.com/rs/zerolog/log"
)
// handleVulnerabilities handles /api/packages/{registry}/{name}/{version}/vulnerabilities endpoint
func (a *App) handleVulnerabilities(c *fiber.Ctx) error {
func (a *App) handleVulnerabilities(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
c.Set("Access-Control-Allow-Origin", "*")
c.Set("Access-Control-Allow-Methods", "GET, OPTIONS")
+9 -9
View File
@@ -3,7 +3,7 @@ package app
import (
"strings"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v3"
"github.com/lukaszraczylo/gohoarder/pkg/auth"
"github.com/lukaszraczylo/gohoarder/pkg/errors"
"github.com/rs/zerolog/log"
@@ -20,7 +20,7 @@ const (
// extractAPIKey pulls the raw API key from either the Authorization bearer
// header or the X-API-Key header. Returns ("", false) when neither is set or
// the Authorization header has the wrong shape.
func extractAPIKey(c *fiber.Ctx) (string, bool) {
func extractAPIKey(c fiber.Ctx) (string, bool) {
if h := strings.TrimSpace(c.Get("Authorization")); h != "" {
// Expect: "Bearer <token>". Be tolerant of casing on the scheme.
parts := strings.SplitN(h, " ", 2)
@@ -40,7 +40,7 @@ func extractAPIKey(c *fiber.Ctx) (string, bool) {
// writeAuthError sends a structured error response matching the project's
// errors envelope (errors.Error JSON shape). Uses the HTTPStatusCode map so
// callers don't need to remember which status maps to which code.
func writeAuthError(c *fiber.Ctx, code, message string) error {
func writeAuthError(c fiber.Ctx, code, message string) error {
status, ok := errors.HTTPStatusCode[code]
if !ok {
status = fiber.StatusInternalServerError
@@ -51,7 +51,7 @@ func writeAuthError(c *fiber.Ctx, code, message string) error {
// requestIDFromCtx returns the request ID set by an upstream middleware or
// the X-Request-ID header. Empty string when neither is available — callers
// should treat empty as "no correlation id".
func requestIDFromCtx(c *fiber.Ctx) string {
func requestIDFromCtx(c fiber.Ctx) string {
if v := c.Locals("request_id"); v != nil {
if s, ok := v.(string); ok && s != "" {
return s
@@ -65,7 +65,7 @@ func requestIDFromCtx(c *fiber.Ctx) string {
// resolved key plus a boolean indicating whether a key was present at all
// (false when no header). The error is non-nil only when validation failed
// (i.e. a key was present but invalid/expired).
func validateAndAttach(c *fiber.Ctx, authMgr *auth.Manager) (key *auth.APIKey, present bool, err error) {
func validateAndAttach(c fiber.Ctx, authMgr *auth.Manager) (key *auth.APIKey, present bool, err error) {
rawKey, ok := extractAPIKey(c)
if !ok {
return nil, false, nil
@@ -83,7 +83,7 @@ func validateAndAttach(c *fiber.Ctx, authMgr *auth.Manager) (key *auth.APIKey, p
// API key. On success the resolved *auth.APIKey is stored in c.Locals under
// LocalAuthKey.
func RequireAuth(authMgr *auth.Manager) fiber.Handler {
return func(c *fiber.Ctx) error {
return func(c fiber.Ctx) error {
key, present, err := validateAndAttach(c, authMgr)
if !present {
log.Debug().
@@ -115,7 +115,7 @@ func RequireAuth(authMgr *auth.Manager) fiber.Handler {
// requires the resolved key's role to be present in the supplied list (any-of
// semantics). An empty roles list behaves like RequireAuth.
func RequireRole(authMgr *auth.Manager, roles ...string) fiber.Handler {
return func(c *fiber.Ctx) error {
return func(c fiber.Ctx) error {
key, present, err := validateAndAttach(c, authMgr)
if !present {
return writeAuthError(c, errors.ErrCodeUnauthorized, "missing API key; provide Authorization: Bearer <key> or X-API-Key header")
@@ -146,7 +146,7 @@ func RequireRole(authMgr *auth.Manager, roles ...string) fiber.Handler {
// then requires the resolved key to hold at least one of the supplied
// permissions. An empty perms list behaves like RequireAuth.
func RequirePermission(authMgr *auth.Manager, perms ...string) fiber.Handler {
return func(c *fiber.Ctx) error {
return func(c fiber.Ctx) error {
key, present, err := validateAndAttach(c, authMgr)
if !present {
return writeAuthError(c, errors.ErrCodeUnauthorized, "missing API key; provide Authorization: Bearer <key> or X-API-Key header")
@@ -177,7 +177,7 @@ func RequirePermission(authMgr *auth.Manager, perms ...string) fiber.Handler {
// changes when the caller is authenticated (e.g. per-key rate limits) but
// which still serve anonymous traffic. Locals are populated on success.
func OptionalAuth(authMgr *auth.Manager) fiber.Handler {
return func(c *fiber.Ctx) error {
return func(c fiber.Ctx) error {
_, present, err := validateAndAttach(c, authMgr)
if present && err != nil {
// Key was provided but invalid — log for observability, continue
+41 -41
View File
@@ -7,7 +7,7 @@ import (
"testing"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v3"
"github.com/lukaszraczylo/gohoarder/pkg/auth"
"github.com/lukaszraczylo/gohoarder/pkg/errors"
"github.com/stretchr/testify/require"
@@ -23,9 +23,9 @@ func mwTestSetup(t *testing.T, role auth.Role, mw fiber.Handler) (*fiber.App, *a
apiKey, raw, err := mgr.GenerateAPIKey("test-"+string(role), role, nil)
require.NoError(t, err)
app := fiber.New(fiber.Config{DisableStartupMessage: true})
app := fiber.New(fiber.Config{})
app.Use("/protected", mw)
app.Get("/protected", func(c *fiber.Ctx) error {
app.Get("/protected", func(c fiber.Ctx) error {
got, _ := c.Locals(LocalAuthKey).(*auth.APIKey)
gotRole, _ := c.Locals(LocalAuthRole).(string)
body := fiber.Map{"ok": true, "have_key": got != nil}
@@ -48,7 +48,7 @@ func decodeErrorBody(t *testing.T, body io.Reader) errors.Error {
func TestRequireAuth_NoHeader_Returns401(t *testing.T) {
app, _, _, _ := mwTestSetup(t, auth.RoleReadOnly, RequireAuth(auth.New()))
req := httptest.NewRequest("GET", "/protected", nil)
resp, err := app.Test(req, 5000)
resp, err := app.Test(req, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true})
require.NoError(t, err)
require.Equal(t, fiber.StatusUnauthorized, resp.StatusCode)
body := decodeErrorBody(t, resp.Body)
@@ -63,7 +63,7 @@ func TestRequireAuth_BadKey_Returns401(t *testing.T) {
app, _, _, _ := mwTestSetup(t, auth.RoleReadOnly, RequireAuth(mgr))
req := httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "Bearer not-a-real-key")
resp, err := app.Test(req, 5000)
resp, err := app.Test(req, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true})
require.NoError(t, err)
require.Equal(t, fiber.StatusUnauthorized, resp.StatusCode)
body := decodeErrorBody(t, resp.Body)
@@ -84,14 +84,14 @@ func TestRequireAuth_BadHeaderShape_Returns401(t *testing.T) {
tc := tc
t.Run(tc.name, func(t *testing.T) {
mgr := auth.New()
app := fiber.New(fiber.Config{DisableStartupMessage: true})
app := fiber.New(fiber.Config{})
app.Use("/protected", RequireAuth(mgr))
app.Get("/protected", func(c *fiber.Ctx) error { return c.SendStatus(200) })
app.Get("/protected", func(c fiber.Ctx) error { return c.SendStatus(200) })
req := httptest.NewRequest("GET", "/protected", nil)
if tc.value != "" {
req.Header.Set(tc.header, tc.value)
}
resp, err := app.Test(req, 5000)
resp, err := app.Test(req, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true})
require.NoError(t, err)
require.Equal(t, fiber.StatusUnauthorized, resp.StatusCode)
})
@@ -103,9 +103,9 @@ func TestRequireAuth_ValidBearer_Returns200_AndPopulatesLocals(t *testing.T) {
apiKey, raw, err := mgr.GenerateAPIKey("good", auth.RoleReadWrite, nil)
require.NoError(t, err)
app := fiber.New(fiber.Config{DisableStartupMessage: true})
app := fiber.New(fiber.Config{})
app.Use("/protected", RequireAuth(mgr))
app.Get("/protected", func(c *fiber.Ctx) error {
app.Get("/protected", func(c fiber.Ctx) error {
k, _ := c.Locals(LocalAuthKey).(*auth.APIKey)
require.NotNil(t, k)
require.Equal(t, apiKey.ID, k.ID)
@@ -115,7 +115,7 @@ func TestRequireAuth_ValidBearer_Returns200_AndPopulatesLocals(t *testing.T) {
req := httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "Bearer "+raw)
resp, err := app.Test(req, 5000)
resp, err := app.Test(req, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true})
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
@@ -129,13 +129,13 @@ func TestRequireAuth_ValidXAPIKey_Returns200(t *testing.T) {
_, raw, err := mgr.GenerateAPIKey("good", auth.RoleReadOnly, nil)
require.NoError(t, err)
app := fiber.New(fiber.Config{DisableStartupMessage: true})
app := fiber.New(fiber.Config{})
app.Use("/protected", RequireAuth(mgr))
app.Get("/protected", func(c *fiber.Ctx) error { return c.SendStatus(200) })
app.Get("/protected", func(c fiber.Ctx) error { return c.SendStatus(200) })
req := httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("X-API-Key", raw)
resp, err := app.Test(req, 5000)
resp, err := app.Test(req, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true})
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
}
@@ -146,13 +146,13 @@ func TestRequireAuth_ExpiredKey_Returns401(t *testing.T) {
_, raw, err := mgr.GenerateAPIKey("expired", auth.RoleReadOnly, &d)
require.NoError(t, err)
app := fiber.New(fiber.Config{DisableStartupMessage: true})
app := fiber.New(fiber.Config{})
app.Use("/protected", RequireAuth(mgr))
app.Get("/protected", func(c *fiber.Ctx) error { return c.SendStatus(200) })
app.Get("/protected", func(c fiber.Ctx) error { return c.SendStatus(200) })
req := httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "Bearer "+raw)
resp, err := app.Test(req, 5000)
resp, err := app.Test(req, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true})
require.NoError(t, err)
require.Equal(t, fiber.StatusUnauthorized, resp.StatusCode)
}
@@ -162,13 +162,13 @@ func TestRequireRole_Match_Returns200(t *testing.T) {
_, raw, err := mgr.GenerateAPIKey("admin", auth.RoleAdmin, nil)
require.NoError(t, err)
app := fiber.New(fiber.Config{DisableStartupMessage: true})
app := fiber.New(fiber.Config{})
app.Use("/protected", RequireRole(mgr, string(auth.RoleAdmin), string(auth.RoleReadWrite)))
app.Get("/protected", func(c *fiber.Ctx) error { return c.SendStatus(200) })
app.Get("/protected", func(c fiber.Ctx) error { return c.SendStatus(200) })
req := httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "Bearer "+raw)
resp, err := app.Test(req, 5000)
resp, err := app.Test(req, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true})
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
}
@@ -178,13 +178,13 @@ func TestRequireRole_Mismatch_Returns403(t *testing.T) {
_, raw, err := mgr.GenerateAPIKey("ro", auth.RoleReadOnly, nil)
require.NoError(t, err)
app := fiber.New(fiber.Config{DisableStartupMessage: true})
app := fiber.New(fiber.Config{})
app.Use("/protected", RequireRole(mgr, string(auth.RoleAdmin)))
app.Get("/protected", func(c *fiber.Ctx) error { return c.SendStatus(200) })
app.Get("/protected", func(c fiber.Ctx) error { return c.SendStatus(200) })
req := httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "Bearer "+raw)
resp, err := app.Test(req, 5000)
resp, err := app.Test(req, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true})
require.NoError(t, err)
require.Equal(t, fiber.StatusForbidden, resp.StatusCode)
body := decodeErrorBody(t, resp.Body)
@@ -193,12 +193,12 @@ func TestRequireRole_Mismatch_Returns403(t *testing.T) {
func TestRequireRole_NoHeader_Returns401(t *testing.T) {
mgr := auth.New()
app := fiber.New(fiber.Config{DisableStartupMessage: true})
app := fiber.New(fiber.Config{})
app.Use("/protected", RequireRole(mgr, string(auth.RoleAdmin)))
app.Get("/protected", func(c *fiber.Ctx) error { return c.SendStatus(200) })
app.Get("/protected", func(c fiber.Ctx) error { return c.SendStatus(200) })
req := httptest.NewRequest("GET", "/protected", nil)
resp, err := app.Test(req, 5000)
resp, err := app.Test(req, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true})
require.NoError(t, err)
require.Equal(t, fiber.StatusUnauthorized, resp.StatusCode)
}
@@ -208,13 +208,13 @@ func TestRequirePermission_Match_Returns200(t *testing.T) {
_, raw, err := mgr.GenerateAPIKey("rw", auth.RoleReadWrite, nil)
require.NoError(t, err)
app := fiber.New(fiber.Config{DisableStartupMessage: true})
app := fiber.New(fiber.Config{})
app.Use("/protected", RequirePermission(mgr, string(auth.PermissionWritePackage)))
app.Get("/protected", func(c *fiber.Ctx) error { return c.SendStatus(200) })
app.Get("/protected", func(c fiber.Ctx) error { return c.SendStatus(200) })
req := httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "Bearer "+raw)
resp, err := app.Test(req, 5000)
resp, err := app.Test(req, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true})
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
}
@@ -224,43 +224,43 @@ func TestRequirePermission_Mismatch_Returns403(t *testing.T) {
_, raw, err := mgr.GenerateAPIKey("ro", auth.RoleReadOnly, nil)
require.NoError(t, err)
app := fiber.New(fiber.Config{DisableStartupMessage: true})
app := fiber.New(fiber.Config{})
app.Use("/protected", RequirePermission(mgr, string(auth.PermissionDeletePackage)))
app.Get("/protected", func(c *fiber.Ctx) error { return c.SendStatus(200) })
app.Get("/protected", func(c fiber.Ctx) error { return c.SendStatus(200) })
req := httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "Bearer "+raw)
resp, err := app.Test(req, 5000)
resp, err := app.Test(req, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true})
require.NoError(t, err)
require.Equal(t, fiber.StatusForbidden, resp.StatusCode)
}
func TestOptionalAuth_NoHeader_Continues(t *testing.T) {
mgr := auth.New()
app := fiber.New(fiber.Config{DisableStartupMessage: true})
app := fiber.New(fiber.Config{})
app.Use("/protected", OptionalAuth(mgr))
app.Get("/protected", func(c *fiber.Ctx) error {
app.Get("/protected", func(c fiber.Ctx) error {
require.Nil(t, c.Locals(LocalAuthKey))
return c.SendStatus(200)
})
req := httptest.NewRequest("GET", "/protected", nil)
resp, err := app.Test(req, 5000)
resp, err := app.Test(req, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true})
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
}
func TestOptionalAuth_BadKey_ContinuesAnonymously(t *testing.T) {
mgr := auth.New()
app := fiber.New(fiber.Config{DisableStartupMessage: true})
app := fiber.New(fiber.Config{})
app.Use("/protected", OptionalAuth(mgr))
app.Get("/protected", func(c *fiber.Ctx) error {
app.Get("/protected", func(c fiber.Ctx) error {
// Bad key should NOT populate locals.
require.Nil(t, c.Locals(LocalAuthKey))
return c.SendStatus(200)
})
req := httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "Bearer not-real")
resp, err := app.Test(req, 5000)
resp, err := app.Test(req, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true})
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
}
@@ -270,9 +270,9 @@ func TestOptionalAuth_GoodKey_PopulatesLocals(t *testing.T) {
apiKey, raw, err := mgr.GenerateAPIKey("good", auth.RoleAdmin, nil)
require.NoError(t, err)
app := fiber.New(fiber.Config{DisableStartupMessage: true})
app := fiber.New(fiber.Config{})
app.Use("/protected", OptionalAuth(mgr))
app.Get("/protected", func(c *fiber.Ctx) error {
app.Get("/protected", func(c fiber.Ctx) error {
k, _ := c.Locals(LocalAuthKey).(*auth.APIKey)
require.NotNil(t, k)
require.Equal(t, apiKey.ID, k.ID)
@@ -281,7 +281,7 @@ func TestOptionalAuth_GoodKey_PopulatesLocals(t *testing.T) {
req := httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "Bearer "+raw)
resp, err := app.Test(req, 5000)
resp, err := app.Test(req, fiber.TestConfig{Timeout: 5 * time.Second, FailOnTimeout: true})
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
}