diff --git a/.gitignore b/.gitignore index 548cfd7b3753f07e48f7004df68f9138fa1e5976..25b088739964de23eb6ab5916132062e65d931fb 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,4 @@ data/ .env po/*.po +docs/swagger diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6a0f4b9d89ef23ee59872c3f0ea7d19225e16182..5f65e60daa665d061cc7589f91ee3ab2755f056c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -92,12 +92,14 @@ build_front: pages: stage: test - image: python:3.6-alpine + image: python:3.6 + variables: + BUILD_PATH: "../public" before_script: - cd docs script: - pip install sphinx - - python -m sphinx . ../public + - ./build_docs.sh artifacts: paths: - public diff --git a/api/config/settings/common.py b/api/config/settings/common.py index de1d653cb91fa3518273b8b103ed74fdee4b9259..f1a383c587b123c39a6cbf3dcd5ad0de445a30cf 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -377,6 +377,7 @@ REST_FRAMEWORK = { ), 'DEFAULT_AUTHENTICATION_CLASSES': ( 'funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS', + 'funkwhale_api.common.authentication.BearerTokenHeaderAuth', 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication', diff --git a/api/funkwhale_api/common/auth.py b/api/funkwhale_api/common/auth.py index 75839b93662bfb0eb19a78d3b108c5c3690e5e5d..faf13571d6cd73208530a479ed92364989e304ad 100644 --- a/api/funkwhale_api/common/auth.py +++ b/api/funkwhale_api/common/auth.py @@ -29,9 +29,6 @@ class TokenHeaderAuth(BaseJSONWebTokenAuthentication): class TokenAuthMiddleware: - """ - Custom middleware (insecure) that takes user IDs from the query string. - """ def __init__(self, inner): # Store the ASGI application we were passed diff --git a/api/funkwhale_api/common/authentication.py b/api/funkwhale_api/common/authentication.py index b75f3b516d5d2505f40900f7314f1f70e557ecb2..c7566eac8bd0f3d5c115a3a5d6b16dd3aec5dd9e 100644 --- a/api/funkwhale_api/common/authentication.py +++ b/api/funkwhale_api/common/authentication.py @@ -1,3 +1,6 @@ +from django.utils.encoding import smart_text +from django.utils.translation import ugettext as _ + from rest_framework import exceptions from rest_framework_jwt import authentication from rest_framework_jwt.settings import api_settings @@ -18,3 +21,37 @@ class JSONWebTokenAuthenticationQS( def authenticate_header(self, request): return '{0} realm="{1}"'.format( api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm) + + +class BearerTokenHeaderAuth( + authentication.BaseJSONWebTokenAuthentication): + """ + For backward compatibility purpose, we used Authorization: JWT <token> + but Authorization: Bearer <token> is probably better. + """ + www_authenticate_realm = 'api' + + def get_jwt_value(self, request): + auth = authentication.get_authorization_header(request).split() + auth_header_prefix = 'bearer' + + if not auth: + if api_settings.JWT_AUTH_COOKIE: + return request.COOKIES.get(api_settings.JWT_AUTH_COOKIE) + return None + + if smart_text(auth[0].lower()) != auth_header_prefix: + return None + + if len(auth) == 1: + msg = _('Invalid Authorization header. No credentials provided.') + raise exceptions.AuthenticationFailed(msg) + elif len(auth) > 2: + msg = _('Invalid Authorization header. Credentials string ' + 'should not contain spaces.') + raise exceptions.AuthenticationFailed(msg) + + return auth[1] + + def authenticate_header(self, request): + return '{0} realm="{1}"'.format('Bearer', self.www_authenticate_realm) diff --git a/api/funkwhale_api/music/filters.py b/api/funkwhale_api/music/filters.py index 752422e75e64aae20f19bc46ab00cd4678c220d6..6da9cca63636fd51c562f066c790c473093cffe3 100644 --- a/api/funkwhale_api/music/filters.py +++ b/api/funkwhale_api/music/filters.py @@ -20,6 +20,9 @@ class ListenableMixin(filters.FilterSet): class ArtistFilter(ListenableMixin): + q = fields.SearchFilter(search_fields=[ + 'name', + ]) class Meta: model = models.Artist diff --git a/changes/changelog.d/178.doc b/changes/changelog.d/178.doc new file mode 100644 index 0000000000000000000000000000000000000000..419e6984b80460904ec4859d8072554c133ca264 --- /dev/null +++ b/changes/changelog.d/178.doc @@ -0,0 +1 @@ +Foundations for API documentation with Swagger (#178) diff --git a/dev.yml b/dev.yml index 264fc953483d1dec5b19b30d3ade754efb47691d..534d8f5b5d8bbde692acde76ba1a2f1b7cb9dfbc 100644 --- a/dev.yml +++ b/dev.yml @@ -123,6 +123,15 @@ services: - '35730:35730' - '8001:8001' + api-docs: + image: swaggerapi/swagger-ui + environment: + - "API_URL=/swagger.yml" + ports: + - '8002:8080' + volumes: + - "./api/docs/swagger.yml:/usr/share/nginx/html/swagger.yml" + networks: internal: federation: diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000000000000000000000000000000000000..650b3885ea73c7192cc17edb5cb9169b5b06ca2f --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,6 @@ +Funkwhale API +============= + +Funkwhale API is still a work in progress and should not be considered as +stable. We offer an `interactive documentation using swagger </swagger/>`_ +were you can browse available endpoints and try the API. diff --git a/docs/build_docs.sh b/docs/build_docs.sh new file mode 100755 index 0000000000000000000000000000000000000000..fbf2036af5599f845fb769c32555eeba9e77d948 --- /dev/null +++ b/docs/build_docs.sh @@ -0,0 +1,5 @@ +#!/bin/bash -eux +# Building sphinx and swagger docs + +python -m sphinx . $BUILD_PATH +TARGET_PATH="$BUILD_PATH/swagger" ./build_swagger.sh diff --git a/docs/build_swagger.sh b/docs/build_swagger.sh new file mode 100755 index 0000000000000000000000000000000000000000..13ae21b065856e864f3798f9498afd897e3ef265 --- /dev/null +++ b/docs/build_swagger.sh @@ -0,0 +1,9 @@ +#!/bin/bash -eux + +SWAGGER_VERSION="3.13.6" +TARGET_PATH=${TARGET_PATH-"swagger"} +rm -rf $TARGET_PATH /tmp/swagger-ui +git clone --branch="v$SWAGGER_VERSION" --depth=1 "https://github.com/swagger-api/swagger-ui.git" /tmp/swagger-ui +mv /tmp/swagger-ui/dist $TARGET_PATH +cp swagger.yml $TARGET_PATH +sed -i "s,http://petstore.swagger.io/v2/swagger.json,swagger.yml,g" $TARGET_PATH/index.html diff --git a/docs/index.rst b/docs/index.rst index a48b8353c77d88c414ed87e8a7f4f777cc109c24..e4f0e4422e90ce27adfb13044ec6ba936b413788 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,6 +16,7 @@ Funkwhale is a self-hosted, modern free and open-source music server, heavily in configuration importing-music federation + api upgrading third-party changelog diff --git a/docs/swagger.yml b/docs/swagger.yml new file mode 100644 index 0000000000000000000000000000000000000000..7735a8f20ab18053e771951846e9f16911b2d408 --- /dev/null +++ b/docs/swagger.yml @@ -0,0 +1,186 @@ +openapi: "3.0" +info: + description: "Documentation for [Funkwhale](https://funkwhale.audio) API. The API is **not** stable yet." + version: "1.0.0" + title: "Funkwhale API" + +servers: + - url: https://demo.funkwhale.audio/api/v1 + description: Demo server + - url: https://node1.funkwhale.test/api/v1 + description: Node 1 (local) + +components: + securitySchemes: + jwt: + type: http + scheme: bearer + bearerFormat: JWT + description: "You can get a token by using the /token endpoint" + +security: + - jwt: [] + +paths: + /token/: + post: + tags: + - "auth" + description: + Obtain a JWT token you can use for authenticating your next requests. + security: [] + responses: + '200': + description: Successfull auth + '400': + description: Invalid credentials + requestBody: + required: true + content: + application/json: + schema: + type: "object" + properties: + username: + type: "string" + example: "demo" + password: + type: "string" + example: "demo" + + /artists/: + get: + tags: + - "artists" + parameters: + - name: "q" + in: "query" + description: "Search query used to filter artists" + schema: + required: false + type: "string" + example: "carpenter" + - name: "listenable" + in: "query" + description: "Filter/exclude artists with listenable tracks" + schema: + required: false + type: "boolean" + responses: + 200: + content: + application/json: + schema: + type: "object" + properties: + count: + $ref: "#/properties/resultsCount" + results: + type: "array" + items: + $ref: "#/definitions/ArtistNested" + +properties: + resultsCount: + type: "integer" + format: "int64" + description: "The total number of resources matching the request" + mbid: + type: "string" + formats: "uuid" + description: "A musicbrainz ID" +definitions: + Artist: + type: "object" + properties: + mbid: + required: false + $ref: "#/properties/mbid" + id: + type: "integer" + format: "int64" + example: 42 + name: + type: "string" + example: "System of a Down" + creation_date: + type: "string" + format: "date-time" + ArtistNested: + type: "object" + allOf: + - $ref: "#/definitions/Artist" + - type: "object" + properties: + albums: + type: "array" + items: + $ref: "#/definitions/AlbumNested" + + Album: + type: "object" + properties: + mbid: + required: false + $ref: "#/properties/mbid" + id: + type: "integer" + format: "int64" + example: 16 + artist: + type: "integer" + format: "int64" + example: 42 + title: + type: "string" + example: "Toxicity" + creation_date: + type: "string" + format: "date-time" + release_date: + type: "string" + required: false + format: "date" + example: "2001-01-01" + + AlbumNested: + type: "object" + allOf: + - $ref: "#/definitions/Album" + - type: "object" + properties: + tracks: + type: "array" + items: + $ref: "#/definitions/Track" + + Track: + type: "object" + properties: + mbid: + required: false + $ref: "#/properties/mbid" + id: + type: "integer" + format: "int64" + example: 66 + artist: + type: "integer" + format: "int64" + example: 42 + album: + type: "integer" + format: "int64" + example: 16 + title: + type: "string" + example: "Chop Suey!" + position: + required: false + description: "Position of the track in the album" + type: "number" + minimum: 1 + example: 1 + creation_date: + type: "string" + format: "date-time"