mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 02:17:25 +02:00
feat(api): report login methods and add GET /capabilities
This commit is contained in:
parent
294c1a9a6a
commit
28d023449d
17 changed files with 458 additions and 79 deletions
|
|
@ -3,7 +3,7 @@
|
|||
"info": {
|
||||
"title": "Navidrome API",
|
||||
"version": "1.0.0",
|
||||
"description": "Navidrome API v1. Spec-first, additive within v1. Clients discover implemented\ncapability modules through `GET /server` and never sniff versions.\n\nEnums are open: new values may be added to any enum within v1. Clients must\naccept values they do not recognise instead of failing.\n\nEvery operation declares `x-stability-level`: `alpha` operations may change or\ndisappear without notice, `beta` and `stable` operations only change additively.\nA level is only ever raised, never lowered.\n\n`HEAD` is accepted wherever `GET` is. A `405` response lists the allowed methods\nin its `Allow` header.\n\nOperations that need an access token declare `security: [{bearerAuth: []}]` and the scope they need in\n`x-scope` (OpenAPI 3.0 does not allow scopes on bearer schemes). A revoked grant, and every token minted\nfrom it, stops working within one access-token lifetime at most.\n",
|
||||
"description": "Navidrome API v1. Spec-first, additive within v1. Clients discover implemented\ncapability modules through `GET /capabilities` and never sniff versions.\n\nEnums are open: new values may be added to any enum within v1. Clients must\naccept values they do not recognise instead of failing.\n\nEvery operation declares `x-stability-level`: `alpha` operations may change or\ndisappear without notice, `beta` and `stable` operations only change additively.\nA level is only ever raised, never lowered.\n\n`HEAD` is accepted wherever `GET` is. A `405` response lists the allowed methods\nin its `Allow` header.\n\nOperations that need an access token declare `security: [{bearerAuth: []}]` and the scope they need in\n`x-scope` (OpenAPI 3.0 does not allow scopes on bearer schemes). A revoked grant, and every token minted\nfrom it, stops working within one access-token lifetime at most.\n",
|
||||
"license": {
|
||||
"name": "GPL-3.0",
|
||||
"url": "https://www.gnu.org/licenses/gpl-3.0.html"
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
],
|
||||
"security": [],
|
||||
"summary": "Describe the server",
|
||||
"description": "Returns the public server description. No authentication required.\nAuthenticated requests will additionally receive the implemented capability modules\nonce authentication is available.\n",
|
||||
"description": "Returns the public server description. No authentication required.\nCapability modules are listed by `GET /capabilities`.\n",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Server description.",
|
||||
|
|
@ -53,6 +53,41 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/capabilities": {
|
||||
"get": {
|
||||
"operationId": "getCapabilities",
|
||||
"x-module": "core",
|
||||
"x-stability-level": "alpha",
|
||||
"tags": [
|
||||
"server"
|
||||
],
|
||||
"summary": "List implemented capability modules",
|
||||
"description": "The capability modules this server implements. Any valid access token may read it, whatever its scopes.",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Implemented modules.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Capabilities"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"500": {
|
||||
"$ref": "#/components/responses/InternalError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/openapi.json": {
|
||||
"get": {
|
||||
"operationId": "getOpenAPISpecJSON",
|
||||
|
|
@ -512,17 +547,23 @@
|
|||
"description": "True until the first admin user has been created."
|
||||
},
|
||||
"loginMethods": {
|
||||
"type": "array",
|
||||
"description": "Login methods this server accepts. New methods may be added; clients ignore values they do not recognise.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"password"
|
||||
]
|
||||
}
|
||||
"$ref": "#/components/schemas/LoginMethods"
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoginMethods": {
|
||||
"type": "object",
|
||||
"description": "Login methods this server accepts, keyed by method. A missing key means the method is not offered.\nKeys are optional on purpose: discovery is read by clients of any version against servers of any\nversion, so new methods are added as new optional keys. Clients ignore keys they do not know.\n",
|
||||
"properties": {
|
||||
"password": {
|
||||
"$ref": "#/components/schemas/PasswordLoginMethod"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PasswordLoginMethod": {
|
||||
"type": "object",
|
||||
"description": "Username and password login (`POST /auth/login`). No settings yet."
|
||||
},
|
||||
"Problem": {
|
||||
"type": "object",
|
||||
"description": "RFC 9457 problem details, returned for every 4xx and 5xx response.",
|
||||
|
|
@ -598,6 +639,44 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Capabilities": {
|
||||
"type": "object",
|
||||
"description": "Capability modules this server implements, keyed by module. Keys are optional; a missing key means the\nmodule is not implemented. New modules are added as new optional keys. These are server facts, not what\nthe calling token may use.\n",
|
||||
"properties": {
|
||||
"core": {
|
||||
"$ref": "#/components/schemas/CoreCapability"
|
||||
},
|
||||
"password": {
|
||||
"$ref": "#/components/schemas/PasswordCapability"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CoreCapability": {
|
||||
"type": "object",
|
||||
"description": "The mandatory core module.",
|
||||
"required": [
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "integer",
|
||||
"description": "Module version. Bumped only on semantic change."
|
||||
}
|
||||
}
|
||||
},
|
||||
"PasswordCapability": {
|
||||
"type": "object",
|
||||
"description": "The password login module (login, first-admin setup, password change).",
|
||||
"required": [
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "integer",
|
||||
"description": "Module version. Bumped only on semantic change."
|
||||
}
|
||||
}
|
||||
},
|
||||
"TokenRequest": {
|
||||
"type": "object",
|
||||
"description": "Optional narrowing of a new access token.",
|
||||
|
|
@ -924,6 +1003,21 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Unauthorized": {
|
||||
"description": "Missing, invalid, or expired credentials.",
|
||||
"headers": {
|
||||
"WWW-Authenticate": {
|
||||
"$ref": "#/components/headers/WWWAuthenticate"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"NotModified": {
|
||||
"description": "Not modified.",
|
||||
"headers": {
|
||||
|
|
@ -942,21 +1036,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Unauthorized": {
|
||||
"description": "Missing, invalid, or expired credentials.",
|
||||
"headers": {
|
||||
"WWW-Authenticate": {
|
||||
"$ref": "#/components/headers/WWWAuthenticate"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PayloadTooLarge": {
|
||||
"description": "The request body is too large (`payload_too_large`).",
|
||||
"content": {
|
||||
|
|
@ -1047,14 +1126,14 @@
|
|||
}
|
||||
},
|
||||
"headers": {
|
||||
"ETag": {
|
||||
"description": "Entity tag for `If-None-Match` revalidation.",
|
||||
"WWWAuthenticate": {
|
||||
"description": "RFC 6750 Bearer challenge, for example `Bearer error=\"insufficient_scope\", scope=\"read\"`.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"WWWAuthenticate": {
|
||||
"description": "RFC 6750 Bearer challenge, for example `Bearer error=\"insufficient_scope\", scope=\"read\"`.",
|
||||
"ETag": {
|
||||
"description": "Entity tag for `If-None-Match` revalidation.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ info:
|
|||
version: 1.0.0
|
||||
description: |
|
||||
Navidrome API v1. Spec-first, additive within v1. Clients discover implemented
|
||||
capability modules through `GET /server` and never sniff versions.
|
||||
capability modules through `GET /capabilities` and never sniff versions.
|
||||
|
||||
Enums are open: new values may be added to any enum within v1. Clients must
|
||||
accept values they do not recognise instead of failing.
|
||||
|
|
@ -40,8 +40,7 @@ paths:
|
|||
summary: Describe the server
|
||||
description: |
|
||||
Returns the public server description. No authentication required.
|
||||
Authenticated requests will additionally receive the implemented capability modules
|
||||
once authentication is available.
|
||||
Capability modules are listed by `GET /capabilities`.
|
||||
responses:
|
||||
'200':
|
||||
description: Server description.
|
||||
|
|
@ -51,6 +50,23 @@ paths:
|
|||
$ref: '#/components/schemas/ServerInfo'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
/capabilities:
|
||||
get:
|
||||
operationId: getCapabilities
|
||||
x-module: core
|
||||
x-stability-level: alpha
|
||||
tags: [server]
|
||||
summary: List implemented capability modules
|
||||
description: The capability modules this server implements. Any valid access token may read it, whatever its scopes.
|
||||
security: [{bearerAuth: []}]
|
||||
responses:
|
||||
'200':
|
||||
description: Implemented modules.
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/Capabilities'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'500': {$ref: '#/components/responses/InternalError'}
|
||||
/openapi.json:
|
||||
get:
|
||||
operationId: getOpenAPISpecJSON
|
||||
|
|
@ -342,12 +358,19 @@ components:
|
|||
type: boolean
|
||||
description: True until the first admin user has been created.
|
||||
loginMethods:
|
||||
type: array
|
||||
description: Login methods this server accepts. New methods may be added; clients ignore values they do not recognise.
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- password
|
||||
$ref: '#/components/schemas/LoginMethods'
|
||||
LoginMethods:
|
||||
type: object
|
||||
description: |
|
||||
Login methods this server accepts, keyed by method. A missing key means the method is not offered.
|
||||
Keys are optional on purpose: discovery is read by clients of any version against servers of any
|
||||
version, so new methods are added as new optional keys. Clients ignore keys they do not know.
|
||||
properties:
|
||||
password:
|
||||
$ref: '#/components/schemas/PasswordLoginMethod'
|
||||
PasswordLoginMethod:
|
||||
type: object
|
||||
description: Username and password login (`POST /auth/login`). No settings yet.
|
||||
Problem:
|
||||
type: object
|
||||
description: RFC 9457 problem details, returned for every 4xx and 5xx response.
|
||||
|
|
@ -409,6 +432,35 @@ components:
|
|||
message:
|
||||
type: string
|
||||
description: Why the value was rejected.
|
||||
Capabilities:
|
||||
type: object
|
||||
description: |
|
||||
Capability modules this server implements, keyed by module. Keys are optional; a missing key means the
|
||||
module is not implemented. New modules are added as new optional keys. These are server facts, not what
|
||||
the calling token may use.
|
||||
properties:
|
||||
core:
|
||||
$ref: '#/components/schemas/CoreCapability'
|
||||
password:
|
||||
$ref: '#/components/schemas/PasswordCapability'
|
||||
CoreCapability:
|
||||
type: object
|
||||
description: The mandatory core module.
|
||||
required:
|
||||
- version
|
||||
properties:
|
||||
version:
|
||||
type: integer
|
||||
description: Module version. Bumped only on semantic change.
|
||||
PasswordCapability:
|
||||
type: object
|
||||
description: The password login module (login, first-admin setup, password change).
|
||||
required:
|
||||
- version
|
||||
properties:
|
||||
version:
|
||||
type: integer
|
||||
description: Module version. Bumped only on semantic change.
|
||||
TokenRequest:
|
||||
type: object
|
||||
description: Optional narrowing of a new access token.
|
||||
|
|
@ -654,6 +706,15 @@ components:
|
|||
application/problem+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Problem'
|
||||
Unauthorized:
|
||||
description: Missing, invalid, or expired credentials.
|
||||
headers:
|
||||
WWW-Authenticate:
|
||||
$ref: '#/components/headers/WWWAuthenticate'
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Problem'
|
||||
NotModified:
|
||||
description: Not modified.
|
||||
headers:
|
||||
|
|
@ -665,15 +726,6 @@ components:
|
|||
application/problem+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Problem'
|
||||
Unauthorized:
|
||||
description: Missing, invalid, or expired credentials.
|
||||
headers:
|
||||
WWW-Authenticate:
|
||||
$ref: '#/components/headers/WWWAuthenticate'
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Problem'
|
||||
PayloadTooLarge:
|
||||
description: "The request body is too large (`payload_too_large`)."
|
||||
content:
|
||||
|
|
@ -733,11 +785,11 @@ components:
|
|||
maximum: 2000
|
||||
default: 100
|
||||
headers:
|
||||
ETag:
|
||||
description: Entity tag for `If-None-Match` revalidation.
|
||||
schema:
|
||||
type: string
|
||||
WWWAuthenticate:
|
||||
description: 'RFC 6750 Bearer challenge, for example `Bearer error="insufficient_scope", scope="read"`.'
|
||||
schema:
|
||||
type: string
|
||||
ETag:
|
||||
description: Entity tag for `If-None-Match` revalidation.
|
||||
schema:
|
||||
type: string
|
||||
|
|
|
|||
10
api/openapi/components/schemas/Capabilities.yaml
Normal file
10
api/openapi/components/schemas/Capabilities.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
type: object
|
||||
description: |
|
||||
Capability modules this server implements, keyed by module. Keys are optional; a missing key means the
|
||||
module is not implemented. New modules are added as new optional keys. These are server facts, not what
|
||||
the calling token may use.
|
||||
properties:
|
||||
core:
|
||||
$ref: ./CoreCapability.yaml
|
||||
password:
|
||||
$ref: ./PasswordCapability.yaml
|
||||
5
api/openapi/components/schemas/CoreCapability.yaml
Normal file
5
api/openapi/components/schemas/CoreCapability.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
type: object
|
||||
description: The mandatory core module.
|
||||
required: [version]
|
||||
properties:
|
||||
version: {type: integer, description: Module version. Bumped only on semantic change.}
|
||||
8
api/openapi/components/schemas/LoginMethods.yaml
Normal file
8
api/openapi/components/schemas/LoginMethods.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
type: object
|
||||
description: |
|
||||
Login methods this server accepts, keyed by method. A missing key means the method is not offered.
|
||||
Keys are optional on purpose: discovery is read by clients of any version against servers of any
|
||||
version, so new methods are added as new optional keys. Clients ignore keys they do not know.
|
||||
properties:
|
||||
password:
|
||||
$ref: ./PasswordLoginMethod.yaml
|
||||
5
api/openapi/components/schemas/PasswordCapability.yaml
Normal file
5
api/openapi/components/schemas/PasswordCapability.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
type: object
|
||||
description: The password login module (login, first-admin setup, password change).
|
||||
required: [version]
|
||||
properties:
|
||||
version: {type: integer, description: Module version. Bumped only on semantic change.}
|
||||
2
api/openapi/components/schemas/PasswordLoginMethod.yaml
Normal file
2
api/openapi/components/schemas/PasswordLoginMethod.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
type: object
|
||||
description: Username and password login (`POST /auth/login`). No settings yet.
|
||||
|
|
@ -15,8 +15,4 @@ properties:
|
|||
type: boolean
|
||||
description: True until the first admin user has been created.
|
||||
loginMethods:
|
||||
type: array
|
||||
description: Login methods this server accepts. New methods may be added; clients ignore values they do not recognise.
|
||||
items:
|
||||
type: string
|
||||
enum: [password]
|
||||
$ref: ./LoginMethods.yaml
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ info:
|
|||
version: 1.0.0
|
||||
description: |
|
||||
Navidrome API v1. Spec-first, additive within v1. Clients discover implemented
|
||||
capability modules through `GET /server` and never sniff versions.
|
||||
capability modules through `GET /capabilities` and never sniff versions.
|
||||
|
||||
Enums are open: new values may be added to any enum within v1. Clients must
|
||||
accept values they do not recognise instead of failing.
|
||||
|
|
@ -32,6 +32,8 @@ tags:
|
|||
paths:
|
||||
/server:
|
||||
$ref: ./paths/server.yaml
|
||||
/capabilities:
|
||||
$ref: ./paths/capabilities.yaml
|
||||
/openapi.json:
|
||||
$ref: ./paths/openapi.yaml#/json
|
||||
/openapi.yaml:
|
||||
|
|
|
|||
16
api/openapi/paths/capabilities.yaml
Normal file
16
api/openapi/paths/capabilities.yaml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
get:
|
||||
operationId: getCapabilities
|
||||
x-module: core
|
||||
x-stability-level: alpha
|
||||
tags: [server]
|
||||
summary: List implemented capability modules
|
||||
description: The capability modules this server implements. Any valid access token may read it, whatever its scopes.
|
||||
security: [{bearerAuth: []}]
|
||||
responses:
|
||||
'200':
|
||||
description: Implemented modules.
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: ../components/schemas/Capabilities.yaml}
|
||||
'401': {$ref: ../components/responses/Unauthorized.yaml}
|
||||
'500': {$ref: ../components/responses/InternalError.yaml}
|
||||
|
|
@ -7,8 +7,7 @@ get:
|
|||
summary: Describe the server
|
||||
description: |
|
||||
Returns the public server description. No authentication required.
|
||||
Authenticated requests will additionally receive the implemented capability modules
|
||||
once authentication is available.
|
||||
Capability modules are listed by `GET /capabilities`.
|
||||
responses:
|
||||
'200':
|
||||
description: Server description.
|
||||
|
|
|
|||
|
|
@ -104,21 +104,6 @@ func (e Scope) Valid() bool {
|
|||
}
|
||||
}
|
||||
|
||||
// Defines values for ServerInfoLoginMethods.
|
||||
const (
|
||||
ServerInfoLoginMethodsPassword ServerInfoLoginMethods = "password"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the ServerInfoLoginMethods enum.
|
||||
func (e ServerInfoLoginMethods) Valid() bool {
|
||||
switch e {
|
||||
case ServerInfoLoginMethodsPassword:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// AccessToken A short-lived access token. Opaque; clients must not decode it.
|
||||
type AccessToken struct {
|
||||
// AccessToken The token. Send it as `Authorization: Bearer <token>`.
|
||||
|
|
@ -155,6 +140,23 @@ type AuthUser struct {
|
|||
UserName string `json:"userName"`
|
||||
}
|
||||
|
||||
// Capabilities Capability modules this server implements, keyed by module. Keys are optional; a missing key means the
|
||||
// module is not implemented. New modules are added as new optional keys. These are server facts, not what
|
||||
// the calling token may use.
|
||||
type Capabilities struct {
|
||||
// Core The mandatory core module.
|
||||
Core *CoreCapability `json:"core,omitempty"`
|
||||
|
||||
// Password The password login module (login, first-admin setup, password change).
|
||||
Password *PasswordCapability `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
// CoreCapability The mandatory core module.
|
||||
type CoreCapability struct {
|
||||
// Version Module version. Bumped only on semantic change.
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
// CredentialsRequest Username, password and client description for a login or first-admin setup.
|
||||
type CredentialsRequest struct {
|
||||
// Client Name of the client app.
|
||||
|
|
@ -236,12 +238,26 @@ type GrantList struct {
|
|||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// LoginMethods Login methods this server accepts, keyed by method. A missing key means the method is not offered.
|
||||
// Keys are optional on purpose: discovery is read by clients of any version against servers of any
|
||||
// version, so new methods are added as new optional keys. Clients ignore keys they do not know.
|
||||
type LoginMethods struct {
|
||||
// Password Username and password login (`POST /auth/login`). No settings yet.
|
||||
Password *PasswordLoginMethod `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
// LogoutResponse Result of a logout.
|
||||
type LogoutResponse struct {
|
||||
// LogoutUrl Where to send the browser to finish logging out of an external provider. Null when there is nothing more to do.
|
||||
LogoutUrl *string `json:"logoutUrl"`
|
||||
}
|
||||
|
||||
// PasswordCapability The password login module (login, first-admin setup, password change).
|
||||
type PasswordCapability struct {
|
||||
// Version Module version. Bumped only on semantic change.
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
// PasswordChangeRequest Change the caller's own password.
|
||||
type PasswordChangeRequest struct {
|
||||
// CurrentPassword The current password.
|
||||
|
|
@ -254,6 +270,9 @@ type PasswordChangeRequest struct {
|
|||
RevokeOtherGrants *bool `json:"revokeOtherGrants,omitempty"`
|
||||
}
|
||||
|
||||
// PasswordLoginMethod Username and password login (`POST /auth/login`). No settings yet.
|
||||
type PasswordLoginMethod = map[string]interface{}
|
||||
|
||||
// Problem RFC 9457 problem details, returned for every 4xx and 5xx response.
|
||||
type Problem struct {
|
||||
// Code Machine-readable error code, and the value clients switch on. New codes may be added.
|
||||
|
|
@ -292,8 +311,10 @@ type ScopeRequest = string
|
|||
|
||||
// ServerInfo Public server description. Everything an add-server screen needs before login.
|
||||
type ServerInfo struct {
|
||||
// LoginMethods Login methods this server accepts. New methods may be added; clients ignore values they do not recognise.
|
||||
LoginMethods []ServerInfoLoginMethods `json:"loginMethods"`
|
||||
// LoginMethods Login methods this server accepts, keyed by method. A missing key means the method is not offered.
|
||||
// Keys are optional on purpose: discovery is read by clients of any version against servers of any
|
||||
// version, so new methods are added as new optional keys. Clients ignore keys they do not know.
|
||||
LoginMethods LoginMethods `json:"loginMethods"`
|
||||
|
||||
// Name Human-readable server product name.
|
||||
Name string `json:"name"`
|
||||
|
|
@ -308,9 +329,6 @@ type ServerInfo struct {
|
|||
SpecVersion string `json:"specVersion"`
|
||||
}
|
||||
|
||||
// ServerInfoLoginMethods defines model for ServerInfo.LoginMethods.
|
||||
type ServerInfoLoginMethods string
|
||||
|
||||
// TokenRequest Optional narrowing of a new access token.
|
||||
type TokenRequest struct {
|
||||
// Scopes Subset of the grant's scopes. Omit for all of them; an empty list asks for none.
|
||||
|
|
@ -400,6 +418,9 @@ type ServerInterface interface {
|
|||
// CreateAccessToken Mint an access token
|
||||
// (POST /auth/token)
|
||||
CreateAccessToken(w http.ResponseWriter, r *http.Request)
|
||||
// GetCapabilities List implemented capability modules
|
||||
// (GET /capabilities)
|
||||
GetCapabilities(w http.ResponseWriter, r *http.Request)
|
||||
// GetServerInfo Describe the server
|
||||
// (GET /server)
|
||||
GetServerInfo(w http.ResponseWriter, r *http.Request)
|
||||
|
|
@ -451,6 +472,12 @@ func (_ Unimplemented) CreateAccessToken(w http.ResponseWriter, r *http.Request)
|
|||
w.WriteHeader(http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// GetCapabilities List implemented capability modules
|
||||
// (GET /capabilities)
|
||||
func (_ Unimplemented) GetCapabilities(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// GetServerInfo Describe the server
|
||||
// (GET /server)
|
||||
func (_ Unimplemented) GetServerInfo(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -608,6 +635,20 @@ func (siw *ServerInterfaceWrapper) CreateAccessToken(w http.ResponseWriter, r *h
|
|||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// GetCapabilities operation middleware
|
||||
func (siw *ServerInterfaceWrapper) GetCapabilities(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
siw.Handler.GetCapabilities(w, r)
|
||||
}))
|
||||
|
||||
for _, middleware := range siw.HandlerMiddlewares {
|
||||
handler = middleware(handler)
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// GetServerInfo operation middleware
|
||||
func (siw *ServerInterfaceWrapper) GetServerInfo(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
|
|
@ -756,6 +797,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl
|
|||
r.Group(func(r chi.Router) {
|
||||
r.Post(options.BaseURL+"/auth/token", wrapper.CreateAccessToken)
|
||||
})
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Get(options.BaseURL+"/capabilities", wrapper.GetCapabilities)
|
||||
})
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Get(options.BaseURL+"/server", wrapper.GetServerInfo)
|
||||
})
|
||||
|
|
@ -1508,6 +1552,62 @@ func (response CreateAccessToken500ApplicationProblemPlusJSONResponse) VisitCrea
|
|||
return err
|
||||
}
|
||||
|
||||
type GetCapabilitiesRequestObject struct {
|
||||
}
|
||||
|
||||
type GetCapabilitiesResponseObject interface {
|
||||
VisitGetCapabilitiesResponse(w http.ResponseWriter) error
|
||||
}
|
||||
|
||||
type GetCapabilities200JSONResponse Capabilities
|
||||
|
||||
func (response GetCapabilities200JSONResponse) VisitGetCapabilitiesResponse(w http.ResponseWriter) error {
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := json.NewEncoder(&buf).Encode(response); err != nil {
|
||||
return err
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(200)
|
||||
_, err := buf.WriteTo(w)
|
||||
return err
|
||||
}
|
||||
|
||||
type GetCapabilities401ApplicationProblemPlusJSONResponse struct {
|
||||
UnauthorizedApplicationProblemPlusJSONResponse
|
||||
}
|
||||
|
||||
func (response GetCapabilities401ApplicationProblemPlusJSONResponse) VisitGetCapabilitiesResponse(w http.ResponseWriter) error {
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := json.NewEncoder(&buf).Encode(response.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/problem+json")
|
||||
if response.Headers.WWWAuthenticate != nil {
|
||||
w.Header().Set("WWW-Authenticate", fmt.Sprint(*response.Headers.WWWAuthenticate))
|
||||
}
|
||||
w.WriteHeader(401)
|
||||
_, err := buf.WriteTo(w)
|
||||
return err
|
||||
}
|
||||
|
||||
type GetCapabilities500ApplicationProblemPlusJSONResponse struct {
|
||||
InternalErrorApplicationProblemPlusJSONResponse
|
||||
}
|
||||
|
||||
func (response GetCapabilities500ApplicationProblemPlusJSONResponse) VisitGetCapabilitiesResponse(w http.ResponseWriter) error {
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := json.NewEncoder(&buf).Encode(response); err != nil {
|
||||
return err
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/problem+json")
|
||||
w.WriteHeader(500)
|
||||
_, err := buf.WriteTo(w)
|
||||
return err
|
||||
}
|
||||
|
||||
type GetServerInfoRequestObject struct {
|
||||
}
|
||||
|
||||
|
|
@ -1568,6 +1668,9 @@ type StrictServerInterface interface {
|
|||
// CreateAccessToken Mint an access token
|
||||
// (POST /auth/token)
|
||||
CreateAccessToken(ctx context.Context, request CreateAccessTokenRequestObject) (CreateAccessTokenResponseObject, error)
|
||||
// GetCapabilities List implemented capability modules
|
||||
// (GET /capabilities)
|
||||
GetCapabilities(ctx context.Context, request GetCapabilitiesRequestObject) (GetCapabilitiesResponseObject, error)
|
||||
// GetServerInfo Describe the server
|
||||
// (GET /server)
|
||||
GetServerInfo(ctx context.Context, request GetServerInfoRequestObject) (GetServerInfoResponseObject, error)
|
||||
|
|
@ -1815,6 +1918,30 @@ func (sh *strictHandler) CreateAccessToken(w http.ResponseWriter, r *http.Reques
|
|||
}
|
||||
}
|
||||
|
||||
// GetCapabilities operation middleware
|
||||
func (sh *strictHandler) GetCapabilities(w http.ResponseWriter, r *http.Request) {
|
||||
var request GetCapabilitiesRequestObject
|
||||
|
||||
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
|
||||
return sh.ssi.GetCapabilities(ctx, request.(GetCapabilitiesRequestObject))
|
||||
}
|
||||
for _, middleware := range sh.middlewares {
|
||||
handler = middleware(handler, "GetCapabilities")
|
||||
}
|
||||
|
||||
response, err := handler(r.Context(), w, r, request)
|
||||
|
||||
if err != nil {
|
||||
sh.options.ResponseErrorHandlerFunc(w, r, err)
|
||||
} else if validResponse, ok := response.(GetCapabilitiesResponseObject); ok {
|
||||
if err := validResponse.VisitGetCapabilitiesResponse(w); err != nil {
|
||||
sh.options.ResponseErrorHandlerFunc(w, r, err)
|
||||
}
|
||||
} else if response != nil {
|
||||
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response))
|
||||
}
|
||||
}
|
||||
|
||||
// GetServerInfo operation middleware
|
||||
func (sh *strictHandler) GetServerInfo(w http.ResponseWriter, r *http.Request) {
|
||||
var request GetServerInfoRequestObject
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import (
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/navidrome/navidrome/api"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
|
@ -16,6 +19,17 @@ var _ = Describe("Router", func() {
|
|||
router = New(&tests.MockDataStore{})
|
||||
})
|
||||
|
||||
It("routes every operation in the embedded spec", func() {
|
||||
doc, err := openapi3.NewLoader().LoadFromData(api.SpecJSON())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
mux := New(&tests.MockDataStore{}).Handler.(chi.Routes)
|
||||
for path, item := range doc.Paths.Map() {
|
||||
for method := range item.Operations() {
|
||||
Expect(mux.Find(chi.NewRouteContext(), method, path)).To(Equal(path), method+" "+path)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
It("returns a 404 problem for unknown paths", func() {
|
||||
w := serve(router, httptest.NewRequest(http.MethodGet, "/api/v1/nope", nil))
|
||||
Expect(w.Code).To(Equal(http.StatusNotFound))
|
||||
|
|
|
|||
10
server/apiv1/capabilities.go
Normal file
10
server/apiv1/capabilities.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package apiv1
|
||||
|
||||
import "context"
|
||||
|
||||
func (rt *Router) GetCapabilities(context.Context, GetCapabilitiesRequestObject) (GetCapabilitiesResponseObject, error) {
|
||||
return GetCapabilities200JSONResponse{
|
||||
Core: &CoreCapability{Version: 1},
|
||||
Password: &PasswordCapability{Version: 1},
|
||||
}, nil
|
||||
}
|
||||
53
server/apiv1/capabilities_test.go
Normal file
53
server/apiv1/capabilities_test.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package apiv1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
"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 router *Router
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = GinkgoT().Context()
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
resetDB()
|
||||
router = New(realDS)
|
||||
})
|
||||
|
||||
It("needs a token", func() {
|
||||
w := serve(router, httptest.NewRequestWithContext(ctx, http.MethodGet, "/api/v1/capabilities", nil))
|
||||
Expect(w.Code).To(Equal(http.StatusUnauthorized))
|
||||
})
|
||||
|
||||
It("lists core and password for any valid token, even one with no scopes", func() {
|
||||
body, _ := json.Marshal(map[string]any{"username": "admin", "password": "pw", "client": "c"})
|
||||
setupReq := httptest.NewRequestWithContext(ctx, http.MethodPost, "/api/v1/auth/setup", bytes.NewReader(body))
|
||||
setupReq.Header.Set("Content-Type", "application/json")
|
||||
var gc GrantCreated
|
||||
Expect(json.Unmarshal(serve(router, setupReq).Body.Bytes(), &gc)).To(Succeed())
|
||||
|
||||
tokReq := httptest.NewRequestWithContext(ctx, http.MethodPost, "/api/v1/auth/token", bytes.NewReader([]byte(`{"scopes":[]}`)))
|
||||
tokReq.Header.Set("Content-Type", "application/json")
|
||||
tokReq.Header.Set("Authorization", "Bearer "+gc.Secret)
|
||||
var at AccessToken
|
||||
Expect(json.Unmarshal(serve(router, tokReq).Body.Bytes(), &at)).To(Succeed())
|
||||
|
||||
req := httptest.NewRequestWithContext(ctx, http.MethodGet, "/api/v1/capabilities", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+at.AccessToken)
|
||||
w := serve(router, req)
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
var caps Capabilities
|
||||
Expect(json.Unmarshal(w.Body.Bytes(), &caps)).To(Succeed())
|
||||
Expect(caps.Core.Version).To(Equal(1))
|
||||
Expect(caps.Password.Version).To(Equal(1))
|
||||
})
|
||||
})
|
||||
|
|
@ -18,6 +18,6 @@ func (rt *Router) GetServerInfo(ctx context.Context, _ GetServerInfoRequestObjec
|
|||
ServerVersion: consts.Version,
|
||||
SpecVersion: api.SpecVersion(),
|
||||
SetupRequired: count == 0,
|
||||
LoginMethods: []ServerInfoLoginMethods{ServerInfoLoginMethodsPassword},
|
||||
LoginMethods: LoginMethods{Password: &PasswordLoginMethod{}},
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@ var _ = Describe("GET /server", func() {
|
|||
Expect(info.ServerVersion).To(Equal(consts.Version))
|
||||
Expect(info.SpecVersion).To(Equal(api.SpecVersion()))
|
||||
Expect(info.SetupRequired).To(BeTrue())
|
||||
Expect(info.LoginMethods).To(ConsistOf(ServerInfoLoginMethodsPassword))
|
||||
Expect(info.LoginMethods.Password).ToNot(BeNil())
|
||||
Expect(w.Body.String()).To(ContainSubstring(`"loginMethods":{"password":{}}`))
|
||||
})
|
||||
|
||||
It("reports setupRequired=false once a user exists", func() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue