mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 02:17:25 +02:00
refactor(api): group API v1 handlers into one file per OpenAPI tag
Handlers live in <tag>_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 <deluan@navidrome.org>
This commit is contained in:
parent
0628721006
commit
cbc22b09cb
7 changed files with 79 additions and 97 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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))
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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))
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue