From cbc22b09cbdb3048cb685d4e320e155830ade797 Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 28 Sep 2026 22:08:01 -0400 Subject: [PATCH] refactor(api): group API v1 handlers into one file per OpenAPI tag Handlers live in _handlers.go, so the package grows by tag rather than by endpoint, and shared convention helpers keep plain names without clashing with tag files. Signed-off-by: Deluan --- server/apiv1/auth_handlers.go | 41 ++++++++++++++++ .../{auth_test.go => auth_handlers_test.go} | 0 server/apiv1/capabilities.go | 10 ---- server/apiv1/capabilities_test.go | 40 ---------------- server/apiv1/password_handlers.go | 47 ------------------- .../{server_info.go => server_handlers.go} | 7 +++ ...r_info_test.go => server_handlers_test.go} | 31 ++++++++++++ 7 files changed, 79 insertions(+), 97 deletions(-) rename server/apiv1/{auth_test.go => auth_handlers_test.go} (100%) delete mode 100644 server/apiv1/capabilities.go delete mode 100644 server/apiv1/capabilities_test.go delete mode 100644 server/apiv1/password_handlers.go rename server/apiv1/{server_info.go => server_handlers.go} (69%) rename server/apiv1/{server_info_test.go => server_handlers_test.go} (68%) diff --git a/server/apiv1/auth_handlers.go b/server/apiv1/auth_handlers.go index cf07eb385..eec4a3dae 100644 --- a/server/apiv1/auth_handlers.go +++ b/server/apiv1/auth_handlers.go @@ -3,7 +3,9 @@ package apiv1 import ( "cmp" "context" + "errors" + "github.com/navidrome/navidrome/core/apiauth" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/utils/gg" "github.com/navidrome/navidrome/utils/slice" @@ -11,6 +13,45 @@ import ( const defaultPageSize = 100 +// Login relies on model.ErrInvalidAuth mapping to a detail-less 401, so unknown user and wrong password look the same. +func (rt *Router) Login(ctx context.Context, req LoginRequestObject) (LoginResponseObject, error) { + b := *req.Body + issued, err := rt.auth.Login(ctx, b.Username, b.Password, clientMeta(b), fromScopeRequests(b.Scopes)) + if err != nil { + return nil, err + } + return Login200JSONResponse{Body: toGrantCreated(issued)}, nil +} + +func (rt *Router) SetupFirstAdmin(ctx context.Context, req SetupFirstAdminRequestObject) (SetupFirstAdminResponseObject, error) { + b := *req.Body + issued, err := rt.auth.Setup(ctx, b.Username, b.Password, clientMeta(b), fromScopeRequests(b.Scopes)) + if err != nil { + return nil, err + } + return SetupFirstAdmin201JSONResponse{Body: toGrantCreated(issued)}, nil +} + +func (rt *Router) ChangePassword(ctx context.Context, req ChangePasswordRequestObject) (ChangePasswordResponseObject, error) { + p, err := principalFrom(ctx) + if err != nil { + return nil, err + } + b := *req.Body + revoke := true + if b.RevokeOtherGrants != nil { + revoke = *b.RevokeOtherGrants + } + err = rt.auth.ChangePassword(ctx, p, b.CurrentPassword, b.NewPassword, revoke) + if errors.Is(err, apiauth.ErrCurrentPasswordMismatch) { + return nil, validationFailed(ValidationError{Field: "currentPassword", Message: "is incorrect"}) + } + if err != nil { + return nil, err + } + return ChangePassword204Response{}, nil +} + func (rt *Router) ListGrants(ctx context.Context, req ListGrantsRequestObject) (ListGrantsResponseObject, error) { p, err := principalFrom(ctx) if err != nil { diff --git a/server/apiv1/auth_test.go b/server/apiv1/auth_handlers_test.go similarity index 100% rename from server/apiv1/auth_test.go rename to server/apiv1/auth_handlers_test.go diff --git a/server/apiv1/capabilities.go b/server/apiv1/capabilities.go deleted file mode 100644 index f3b8c78c7..000000000 --- a/server/apiv1/capabilities.go +++ /dev/null @@ -1,10 +0,0 @@ -package apiv1 - -import "context" - -func (rt *Router) GetCapabilities(context.Context, GetCapabilitiesRequestObject) (GetCapabilitiesResponseObject, error) { - return GetCapabilities200JSONResponse{ - Core: &CoreCapability{Version: 1}, - Password: &PasswordCapability{Version: 1}, - }, nil -} diff --git a/server/apiv1/capabilities_test.go b/server/apiv1/capabilities_test.go deleted file mode 100644 index e14248513..000000000 --- a/server/apiv1/capabilities_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package apiv1 - -import ( - "context" - "net/http" - - "github.com/navidrome/navidrome/conf/configtest" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("GET /capabilities", func() { - var ctx context.Context - var api testClient - - BeforeEach(func() { - ctx = GinkgoT().Context() - DeferCleanup(configtest.SetupConfig()) - resetDB() - api = testClient{ctx: ctx, router: New(realDS)} - }) - - It("needs a grant", func() { - w := api.call(http.MethodGet, "/api/v1/capabilities", "", nil) - Expect(w.Code).To(Equal(http.StatusUnauthorized)) - }) - - It("lists core and password for any valid grant, even one with no scopes", func() { - api.setup() - gc := api.login([]string{}) - Expect(gc.Grant.Scopes).To(BeEmpty()) - - w := api.call(http.MethodGet, "/api/v1/capabilities", gc.Secret, nil) - Expect(w.Code).To(Equal(http.StatusOK)) - var caps Capabilities - decodeJSON(w, &caps) - Expect(caps.Core.Version).To(Equal(1)) - Expect(caps.Password.Version).To(Equal(1)) - }) -}) diff --git a/server/apiv1/password_handlers.go b/server/apiv1/password_handlers.go deleted file mode 100644 index 32cf09e87..000000000 --- a/server/apiv1/password_handlers.go +++ /dev/null @@ -1,47 +0,0 @@ -package apiv1 - -import ( - "context" - "errors" - - "github.com/navidrome/navidrome/core/apiauth" -) - -// Login relies on model.ErrInvalidAuth mapping to a detail-less 401, so unknown user and wrong password look the same. -func (rt *Router) Login(ctx context.Context, req LoginRequestObject) (LoginResponseObject, error) { - b := *req.Body - issued, err := rt.auth.Login(ctx, b.Username, b.Password, clientMeta(b), fromScopeRequests(b.Scopes)) - if err != nil { - return nil, err - } - return Login200JSONResponse{Body: toGrantCreated(issued)}, nil -} - -func (rt *Router) SetupFirstAdmin(ctx context.Context, req SetupFirstAdminRequestObject) (SetupFirstAdminResponseObject, error) { - b := *req.Body - issued, err := rt.auth.Setup(ctx, b.Username, b.Password, clientMeta(b), fromScopeRequests(b.Scopes)) - if err != nil { - return nil, err - } - return SetupFirstAdmin201JSONResponse{Body: toGrantCreated(issued)}, nil -} - -func (rt *Router) ChangePassword(ctx context.Context, req ChangePasswordRequestObject) (ChangePasswordResponseObject, error) { - p, err := principalFrom(ctx) - if err != nil { - return nil, err - } - b := *req.Body - revoke := true - if b.RevokeOtherGrants != nil { - revoke = *b.RevokeOtherGrants - } - err = rt.auth.ChangePassword(ctx, p, b.CurrentPassword, b.NewPassword, revoke) - if errors.Is(err, apiauth.ErrCurrentPasswordMismatch) { - return nil, validationFailed(ValidationError{Field: "currentPassword", Message: "is incorrect"}) - } - if err != nil { - return nil, err - } - return ChangePassword204Response{}, nil -} diff --git a/server/apiv1/server_info.go b/server/apiv1/server_handlers.go similarity index 69% rename from server/apiv1/server_info.go rename to server/apiv1/server_handlers.go index d7d46175a..f5c159629 100644 --- a/server/apiv1/server_info.go +++ b/server/apiv1/server_handlers.go @@ -21,3 +21,10 @@ func (rt *Router) GetServerInfo(ctx context.Context, _ GetServerInfoRequestObjec LoginMethods: LoginMethods{Password: &PasswordLoginMethod{}}, }, nil } + +func (rt *Router) GetCapabilities(context.Context, GetCapabilitiesRequestObject) (GetCapabilitiesResponseObject, error) { + return GetCapabilities200JSONResponse{ + Core: &CoreCapability{Version: 1}, + Password: &PasswordCapability{Version: 1}, + }, nil +} diff --git a/server/apiv1/server_info_test.go b/server/apiv1/server_handlers_test.go similarity index 68% rename from server/apiv1/server_info_test.go rename to server/apiv1/server_handlers_test.go index 3d6c938d9..59d0dcb3d 100644 --- a/server/apiv1/server_info_test.go +++ b/server/apiv1/server_handlers_test.go @@ -8,6 +8,7 @@ import ( "net/http/httptest" "github.com/navidrome/navidrome/api" + "github.com/navidrome/navidrome/conf/configtest" "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/tests" @@ -60,3 +61,33 @@ var _ = Describe("GET /server", func() { Expect(decodeProblem(w).Code).To(Equal(ProblemCodeInternal)) }) }) + +var _ = Describe("GET /capabilities", func() { + var ctx context.Context + var api testClient + + BeforeEach(func() { + ctx = GinkgoT().Context() + DeferCleanup(configtest.SetupConfig()) + resetDB() + api = testClient{ctx: ctx, router: New(realDS)} + }) + + It("needs a grant", func() { + w := api.call(http.MethodGet, "/api/v1/capabilities", "", nil) + Expect(w.Code).To(Equal(http.StatusUnauthorized)) + }) + + It("lists core and password for any valid grant, even one with no scopes", func() { + api.setup() + gc := api.login([]string{}) + Expect(gc.Grant.Scopes).To(BeEmpty()) + + w := api.call(http.MethodGet, "/api/v1/capabilities", gc.Secret, nil) + Expect(w.Code).To(Equal(http.StatusOK)) + var caps Capabilities + decodeJSON(w, &caps) + Expect(caps.Core.Version).To(Equal(1)) + Expect(caps.Password.Version).To(Equal(1)) + }) +})