API & Docs - init OAuth 2.0 documentation
This commit is contained in:
		
							
								
								
									
										
											BIN
										
									
								
								docsrc/source/_images/fittrackee_screenshot-07.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docsrc/source/_images/fittrackee_screenshot-07.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 109 KiB  | 
@@ -7,6 +7,7 @@ API documentation
 | 
			
		||||
 | 
			
		||||
   auth
 | 
			
		||||
   configuration
 | 
			
		||||
   oauth2
 | 
			
		||||
   records
 | 
			
		||||
   sports
 | 
			
		||||
   stats
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								docsrc/source/api/oauth2.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								docsrc/source/api/oauth2.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
OAuth2
 | 
			
		||||
######
 | 
			
		||||
 | 
			
		||||
.. autoflask:: fittrackee:create_app()
 | 
			
		||||
   :endpoints:
 | 
			
		||||
    oauth2.get_clients,
 | 
			
		||||
    oauth2.create_client,
 | 
			
		||||
    oauth2.get_client_by_client_id,
 | 
			
		||||
    oauth2.get_client_by_id,
 | 
			
		||||
    oauth2.delete_client,
 | 
			
		||||
    oauth2.revoke_client_tokens,
 | 
			
		||||
    oauth2.authorize,
 | 
			
		||||
    oauth2.issue_token,
 | 
			
		||||
    oauth2.revoke_token
 | 
			
		||||
							
								
								
									
										42
									
								
								docsrc/source/apps.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								docsrc/source/apps.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
Third-party applications
 | 
			
		||||
########################
 | 
			
		||||
(*new in 0.7.0*)
 | 
			
		||||
 | 
			
		||||
FitTrackee provides a REST API (see `documentation <api/index.html>`__) whose
 | 
			
		||||
most endpoints require authorization/authentication.
 | 
			
		||||
 | 
			
		||||
To allow a third-party application to interact with API endpoints, an
 | 
			
		||||
`OAuth2 <https://datatracker.ietf.org/doc/html/rfc6749>`_ client can be created
 | 
			
		||||
in user settings ('apps' tab).
 | 
			
		||||
 | 
			
		||||
.. note::
 | 
			
		||||
  OAuth2 support is implemented with `Authlib <https://docs.authlib.org/en/latest/>`_ library.
 | 
			
		||||
 | 
			
		||||
.. warning::
 | 
			
		||||
  OAuth2 endpoints requiring authentication are not accessible by third-party
 | 
			
		||||
  applications (`documentation <api/oauth2.html>`__), only by FitTrackee
 | 
			
		||||
  client (first-party application).
 | 
			
		||||
 | 
			
		||||
FitTrackee supports only `Authorization Code <https://datatracker.ietf.org/doc/html/rfc6749#section-1.3.1>`_
 | 
			
		||||
flow (with PKCE support).
 | 
			
		||||
It allows to exchange an authorization code for an access token.
 | 
			
		||||
It is recommended to use `PKCE <https://datatracker.ietf.org/doc/html/rfc7636>`_
 | 
			
		||||
to provide a better security.
 | 
			
		||||
 | 
			
		||||
The following scopes are available:
 | 
			
		||||
 | 
			
		||||
- ``application:write``: grants write access to application configuration (only for users with administration rights),
 | 
			
		||||
- ``profile:read``: grants read access to auth endpoints,
 | 
			
		||||
- ``profile:write``: grants write access to auth endpoints,
 | 
			
		||||
- ``users:read``: grants read access to users endpoints,
 | 
			
		||||
- ``users:write``: grants write access to users endpoints,
 | 
			
		||||
- ``workouts:read``: grants read access to workouts-related endpoints,
 | 
			
		||||
- ``workouts:write``: grants write access to workouts-related endpoints.
 | 
			
		||||
 | 
			
		||||
.. figure:: _images/fittrackee_screenshot-07.png
 | 
			
		||||
   :alt: OAuth2 client creation on FitTrackee
 | 
			
		||||
 | 
			
		||||
Some resources about OAuth 2.0:
 | 
			
		||||
 | 
			
		||||
- `OAuth 2.0 Simplified <https://www.oauth.com>`_ by `Aaron Parecki <https://aaronparecki.com>`_
 | 
			
		||||
- `Web App Example of OAuth 2 web application flow <https://requests-oauthlib.readthedocs.io/en/latest/examples/real_world_example.html>`_ with Requests-OAuthlib
 | 
			
		||||
@@ -14,8 +14,9 @@ A command line interface (CLI) is available to manage database and users.
 | 
			
		||||
      --help  Show this message and exit.
 | 
			
		||||
 | 
			
		||||
    Commands:
 | 
			
		||||
      db     Manage database.
 | 
			
		||||
      users  Manage users.
 | 
			
		||||
      db      Manage database.
 | 
			
		||||
      oauth2  Manage OAuth2 tokens.
 | 
			
		||||
      users   Manage users.
 | 
			
		||||
 | 
			
		||||
.. warning::
 | 
			
		||||
    | The following commands are now deprecated and will be removed in a next version:
 | 
			
		||||
@@ -40,6 +41,26 @@ Apply migrations.
 | 
			
		||||
Empty database and delete uploaded files, only on development environments.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
OAuth2
 | 
			
		||||
~~~~~~
 | 
			
		||||
 | 
			
		||||
``ftcli oauth2 clean``
 | 
			
		||||
""""""""""""""""""""""
 | 
			
		||||
.. versionadded:: 0.7.0
 | 
			
		||||
 | 
			
		||||
Remove tokens expired for more than provided number of days
 | 
			
		||||
 | 
			
		||||
.. cssclass:: table-bordered
 | 
			
		||||
.. list-table::
 | 
			
		||||
   :widths: 25 50
 | 
			
		||||
   :header-rows: 1
 | 
			
		||||
 | 
			
		||||
   * - Options
 | 
			
		||||
     - Description
 | 
			
		||||
   * - ``--days``
 | 
			
		||||
     - Number of days.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Users
 | 
			
		||||
~~~~~
 | 
			
		||||
 
 | 
			
		||||
@@ -81,6 +81,8 @@ Account & preferences
 | 
			
		||||
  | A disabled sport (by admin or user) will not appear in dropdown when **adding a workout**.
 | 
			
		||||
  | A workout with a disabled sport will still be displayed in the application.
 | 
			
		||||
 | 
			
		||||
- A user can create `clients <apps.html>`__ for third-party applications (*new in 0.7.0*).
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Administration
 | 
			
		||||
^^^^^^^^^^^^^^
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,7 @@ Table of contents
 | 
			
		||||
   :maxdepth: 1
 | 
			
		||||
 | 
			
		||||
   features
 | 
			
		||||
   apps
 | 
			
		||||
   installation
 | 
			
		||||
   cli
 | 
			
		||||
   api/index
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ This application is written in Python (API) and Typescript (client):
 | 
			
		||||
    - `staticmap <https://github.com/komoot/staticmap>`_ to generate a static map image from gpx coordinates
 | 
			
		||||
    - `python-forecast.io <https://github.com/ZeevG/python-forecast.io>`_ to fetch weather data from `Dark Sky <https://darksky.net>`__ (former forecast.io)
 | 
			
		||||
    - `dramatiq <https://flask-dramatiq.readthedocs.io/en/latest/>`_ for task queue
 | 
			
		||||
    - `Authlib <https://docs.authlib.org/en/latest/>`_ for OAuth 2.0 Authorization support
 | 
			
		||||
- Client:
 | 
			
		||||
    - Vue3/Vuex
 | 
			
		||||
    - `Leaflet <https://leafletjs.com/>`__ to display map
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ config_blueprint = Blueprint('config', __name__)
 | 
			
		||||
@config_blueprint.route('/config', methods=['GET'])
 | 
			
		||||
def get_application_config() -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get Application config
 | 
			
		||||
    Get Application configuration.
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -70,9 +70,11 @@ def get_application_config() -> Union[Dict, HttpResponse]:
 | 
			
		||||
@require_auth(scopes=['application:write'], as_admin=True)
 | 
			
		||||
def update_application_config(auth_user: User) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Update Application config
 | 
			
		||||
    Update Application configuration.
 | 
			
		||||
 | 
			
		||||
    Authenticated user must be an admin
 | 
			
		||||
    Authenticated user must be an admin.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``application:write``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -105,7 +107,7 @@ def update_application_config(auth_user: User) -> Union[Dict, HttpResponse]:
 | 
			
		||||
 | 
			
		||||
    :<json string admin_contact: email to contact the administrator
 | 
			
		||||
    :<json integer gpx_limit_import: max number of files in zip archive
 | 
			
		||||
    :<json boolean is_registration_enabled: is registration enabled ?
 | 
			
		||||
    :<json boolean is_registration_enabled: is registration enabled?
 | 
			
		||||
    :<json integer max_single_file_size: max size of a single file
 | 
			
		||||
    :<json integer max_users: max users allowed to register on instance
 | 
			
		||||
    :<json integer max_zip_file_size: max size of a zip archive
 | 
			
		||||
@@ -187,6 +189,5 @@ def health_check() -> Union[Dict, HttpResponse]:
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    :statuscode 200: success
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    return {'status': 'success', 'message': 'pong!'}
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ def oauth2_cli() -> None:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@oauth2_cli.command('clean')
 | 
			
		||||
@click.option('--days', type=int)
 | 
			
		||||
@click.option('--days', type=int, help='Number of days.')
 | 
			
		||||
def clean(
 | 
			
		||||
    days: int,
 | 
			
		||||
) -> None:
 | 
			
		||||
 
 | 
			
		||||
@@ -38,6 +38,73 @@ def is_errored(url: str) -> Optional[str]:
 | 
			
		||||
@oauth2_blueprint.route('/oauth/apps', methods=['GET'])
 | 
			
		||||
@require_auth()
 | 
			
		||||
def get_clients(auth_user: User) -> Dict:
 | 
			
		||||
    """
 | 
			
		||||
    Get OAuth2 clients (apps) for authenticated user with pagination
 | 
			
		||||
    (5 clients/page).
 | 
			
		||||
 | 
			
		||||
    This endpoint is only accessible by FitTrackee client (first-party
 | 
			
		||||
    application).
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
    - without parameters
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      GET /api/oauth/apps HTTP/1.1
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
    - with 'page' parameter
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      GET /api/oauth/apps?page=2 HTTP/1.1
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
    **Example response**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      HTTP/1.1 200 SUCCESS
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        "data": {
 | 
			
		||||
          "clients": [
 | 
			
		||||
            {
 | 
			
		||||
              "client_description": "",
 | 
			
		||||
              "client_id": "o22a27s2aBPUoxJbxV3UjDOx",
 | 
			
		||||
              "id": 1,
 | 
			
		||||
              "issued_at": "Thu, 14 July 2022 06:27:53 GMT",
 | 
			
		||||
              "name": "GPX Importer",
 | 
			
		||||
              "redirect_uris": [
 | 
			
		||||
                " https://example.com/callback"
 | 
			
		||||
              ],
 | 
			
		||||
              "scope": "profile:read workouts:write",
 | 
			
		||||
              "website": "https://example.com"
 | 
			
		||||
            }
 | 
			
		||||
          ]
 | 
			
		||||
        },
 | 
			
		||||
        "pagination": {
 | 
			
		||||
          "has_next": false,
 | 
			
		||||
          "has_prev": false,
 | 
			
		||||
          "page": 1,
 | 
			
		||||
          "pages": 1,
 | 
			
		||||
          "total": 1
 | 
			
		||||
        },
 | 
			
		||||
        "status": "success"
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    :query integer page: page for pagination (default: 1)
 | 
			
		||||
 | 
			
		||||
    :reqheader Authorization: OAuth 2.0 Bearer Token
 | 
			
		||||
 | 
			
		||||
    :statuscode 200: success
 | 
			
		||||
    :statuscode 401:
 | 
			
		||||
        - provide a valid auth token
 | 
			
		||||
        - signature expired, please log in again
 | 
			
		||||
        - invalid token, please log in again
 | 
			
		||||
    """
 | 
			
		||||
    params = request.args.copy()
 | 
			
		||||
    page = int(params.get('page', 1))
 | 
			
		||||
    per_page = DEFAULT_PER_PAGE
 | 
			
		||||
@@ -67,6 +134,61 @@ def get_clients(auth_user: User) -> Dict:
 | 
			
		||||
@oauth2_blueprint.route('/oauth/apps', methods=['POST'])
 | 
			
		||||
@require_auth()
 | 
			
		||||
def create_client(auth_user: User) -> Union[HttpResponse, Tuple[Dict, int]]:
 | 
			
		||||
    """
 | 
			
		||||
    Create an OAuth2 client (app) for the authenticated user.
 | 
			
		||||
 | 
			
		||||
    This endpoint is only accessible by FitTrackee client (first-party
 | 
			
		||||
    application).
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      POST /api/oauth/apps HTTP/1.1
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
    **Example response**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      HTTP/1.1 200 SUCCESS
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        "data": {
 | 
			
		||||
          "client": {
 | 
			
		||||
            "client_description": "",
 | 
			
		||||
            "client_id": "o22a27s2aBPUoxJbxV3UjDOx",
 | 
			
		||||
            "client_secret": "<CLIENT SECRET>",
 | 
			
		||||
            "id": 1,
 | 
			
		||||
            "issued_at": "Thu, 14 July 2022 06:27:53 GMT",
 | 
			
		||||
            "name": "GPX Importer",
 | 
			
		||||
            "redirect_uris": [
 | 
			
		||||
              "https://example.com/callback"
 | 
			
		||||
            ],
 | 
			
		||||
            "scope": "profile:read workouts:write",
 | 
			
		||||
            "website": "https://example.com"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "status": "created"
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    :json string client_name: client name
 | 
			
		||||
    :json string client_uri: client URL
 | 
			
		||||
    :json array  redirect_uri: list of client redirect URLs (string)
 | 
			
		||||
    :json string scope: client scopes
 | 
			
		||||
    :json string client_description: client description (`OPTIONAL`)
 | 
			
		||||
 | 
			
		||||
    :reqheader Authorization: OAuth 2.0 Bearer Token
 | 
			
		||||
 | 
			
		||||
    :statuscode 200: success
 | 
			
		||||
    :statuscode 400:
 | 
			
		||||
        - invalid payload
 | 
			
		||||
    :statuscode 401:
 | 
			
		||||
        - provide a valid auth token
 | 
			
		||||
        - signature expired, please log in again
 | 
			
		||||
        - invalid token, please log in again
 | 
			
		||||
    """
 | 
			
		||||
    client_metadata = request.get_json()
 | 
			
		||||
    if not client_metadata:
 | 
			
		||||
        return InvalidPayloadErrorResponse(
 | 
			
		||||
@@ -124,12 +246,79 @@ def get_client(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@oauth2_blueprint.route('/oauth/apps/<string:client_id>', methods=['GET'])
 | 
			
		||||
@oauth2_blueprint.route(
 | 
			
		||||
    '/oauth/apps/<string:client_client_id>', methods=['GET']
 | 
			
		||||
)
 | 
			
		||||
@require_auth()
 | 
			
		||||
def get_client_by_client_id(
 | 
			
		||||
    auth_user: User, client_id: str
 | 
			
		||||
    auth_user: User, client_client_id: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    return get_client(auth_user, client_id=None, client_client_id=client_id)
 | 
			
		||||
    """
 | 
			
		||||
    Get an OAuth2 client (app) by 'client_id'.
 | 
			
		||||
 | 
			
		||||
    This endpoint is only accessible by FitTrackee client (first-party
 | 
			
		||||
    application).
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      GET /api/oauth/apps/o22a27s2aBPUoxJbxV3UjDOx HTTP/1.1
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
    **Example responses**:
 | 
			
		||||
 | 
			
		||||
    - success
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      HTTP/1.1 200 SUCCESS
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        "data": {
 | 
			
		||||
          "client": {
 | 
			
		||||
            "client_description": "",
 | 
			
		||||
            "client_id": "o22a27s2aBPUoxJbxV3UjDOx",
 | 
			
		||||
            "id": 1,
 | 
			
		||||
            "issued_at": "Thu, 14 July 2022 06:27:53 GMT",
 | 
			
		||||
            "name": "GPX Importer",
 | 
			
		||||
            "redirect_uris": [
 | 
			
		||||
              "https://example.com/callback"
 | 
			
		||||
            ],
 | 
			
		||||
            "scope": "profile:read workouts:write",
 | 
			
		||||
            "website": "https://example.com"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "status": "success"
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    - not found
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      HTTP/1.1 404 NOT FOUND
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        "status": "not found",
 | 
			
		||||
        "message": "OAuth2 client not found"
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    :param string client_client_id: OAuth2 client client_id
 | 
			
		||||
 | 
			
		||||
    :reqheader Authorization: OAuth 2.0 Bearer Token
 | 
			
		||||
 | 
			
		||||
    :statuscode 200: success
 | 
			
		||||
    :statuscode 401:
 | 
			
		||||
        - provide a valid auth token
 | 
			
		||||
        - signature expired, please log in again
 | 
			
		||||
        - invalid token, please log in again
 | 
			
		||||
    :statuscode 404: OAuth2 client not found
 | 
			
		||||
    """
 | 
			
		||||
    return get_client(
 | 
			
		||||
        auth_user, client_id=None, client_client_id=client_client_id
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@oauth2_blueprint.route('/oauth/apps/<int:client_id>/by_id', methods=['GET'])
 | 
			
		||||
@@ -137,6 +326,69 @@ def get_client_by_client_id(
 | 
			
		||||
def get_client_by_id(
 | 
			
		||||
    auth_user: User, client_id: int
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get an OAuth2 client (app) by id (integer value).
 | 
			
		||||
 | 
			
		||||
    This endpoint is only accessible by FitTrackee client (first-party
 | 
			
		||||
    application).
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      GET /api/oauth/apps/1/by_id HTTP/1.1
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
    **Example responses**:
 | 
			
		||||
 | 
			
		||||
    - success
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      HTTP/1.1 200 SUCCESS
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        "data": {
 | 
			
		||||
          "client": {
 | 
			
		||||
            "client_description": "",
 | 
			
		||||
            "client_id": "o22a27s2aBPUoxJbxV3UjDOx",
 | 
			
		||||
            "id": 1,
 | 
			
		||||
            "issued_at": "Thu, 14 July 2022 06:27:53 GMT",
 | 
			
		||||
            "name": "GPX Importer",
 | 
			
		||||
            "redirect_uris": [
 | 
			
		||||
              "https://example.com/callback"
 | 
			
		||||
            ],
 | 
			
		||||
            "scope": "profile:read workouts:write",
 | 
			
		||||
            "website": "https://example.com"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "status": "success"
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    - not found
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      HTTP/1.1 404 NOT FOUND
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        "status": "not found",
 | 
			
		||||
        "message": "OAuth2 client not found"
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    :param integer client_id: OAuth2 client id
 | 
			
		||||
 | 
			
		||||
    :reqheader Authorization: OAuth 2.0 Bearer Token
 | 
			
		||||
 | 
			
		||||
    :statuscode 200: success
 | 
			
		||||
    :statuscode 401:
 | 
			
		||||
        - provide a valid auth token
 | 
			
		||||
        - signature expired, please log in again
 | 
			
		||||
        - invalid token, please log in again
 | 
			
		||||
    :statuscode 404: OAuth2 client not found
 | 
			
		||||
    """
 | 
			
		||||
    return get_client(auth_user, client_id=client_id, client_client_id=None)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -145,6 +397,37 @@ def get_client_by_id(
 | 
			
		||||
def delete_client(
 | 
			
		||||
    auth_user: User, client_id: int
 | 
			
		||||
) -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Delete an OAuth2 client (app).
 | 
			
		||||
 | 
			
		||||
    This endpoint is only accessible by FitTrackee client (first-party
 | 
			
		||||
    application).
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      DELETE /api/oauth/apps/1 HTTP/1.1
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
    **Example response**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      HTTP/1.1 204 NO CONTENT
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
    :param integer client_id: OAuth2 client id
 | 
			
		||||
 | 
			
		||||
    :reqheader Authorization: OAuth 2.0 Bearer Token
 | 
			
		||||
 | 
			
		||||
    :statuscode 204: OAuth2 client deleted
 | 
			
		||||
    :statuscode 401:
 | 
			
		||||
        - provide a valid auth token
 | 
			
		||||
        - signature expired, please log in again
 | 
			
		||||
        - invalid token, please log in again
 | 
			
		||||
    :statuscode 404: OAuth2 client not found
 | 
			
		||||
    """
 | 
			
		||||
    client = OAuth2Client.query.filter_by(
 | 
			
		||||
        id=client_id,
 | 
			
		||||
        user_id=auth_user.id,
 | 
			
		||||
@@ -163,6 +446,41 @@ def delete_client(
 | 
			
		||||
def revoke_client_tokens(
 | 
			
		||||
    auth_user: User, client_id: int
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Revoke all tokens associated to an OAuth2 client (app).
 | 
			
		||||
 | 
			
		||||
    This endpoint is only accessible by FitTrackee client (first-party
 | 
			
		||||
    application).
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      POST /api/oauth/apps/1/revoke HTTP/1.1
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
    **Example response**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      HTTP/1.1 200 SUCCESS
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        "status": "success"
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    :param integer client_id: OAuth2 client id
 | 
			
		||||
 | 
			
		||||
    :reqheader Authorization: OAuth 2.0 Bearer Token
 | 
			
		||||
 | 
			
		||||
    :statuscode 200: success
 | 
			
		||||
    :statuscode 401:
 | 
			
		||||
        - provide a valid auth token
 | 
			
		||||
        - signature expired, please log in again
 | 
			
		||||
        - invalid token, please log in again
 | 
			
		||||
    :statuscode 404: OAuth2 client not found
 | 
			
		||||
    """
 | 
			
		||||
    client = OAuth2Client.query.filter_by(id=client_id).first()
 | 
			
		||||
 | 
			
		||||
    if not client:
 | 
			
		||||
@@ -175,6 +493,55 @@ def revoke_client_tokens(
 | 
			
		||||
@oauth2_blueprint.route('/oauth/authorize', methods=['POST'])
 | 
			
		||||
@require_auth()
 | 
			
		||||
def authorize(auth_user: User) -> Union[HttpResponse, Dict]:
 | 
			
		||||
    """
 | 
			
		||||
    Authorize an OAuth2 client (app).
 | 
			
		||||
    If successful, it redirects to the client callback URL with the code to
 | 
			
		||||
    issue a token.
 | 
			
		||||
 | 
			
		||||
    This endpoint is only accessible by FitTrackee client (first-party
 | 
			
		||||
    application).
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      POST /api/oauth/authorize HTTP/1.1
 | 
			
		||||
      Content-Type: multipart/form-data
 | 
			
		||||
 | 
			
		||||
    **Example response**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      HTTP/1.1 200 SUCCESS
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        "status": "success"
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    :form string  client_id: OAuth2 client 'client_id'
 | 
			
		||||
    :form string  response_type: client response type (only 'code' is supported
 | 
			
		||||
                                by FitTrackee)
 | 
			
		||||
    :form string  scopes: OAuth2 client scopes
 | 
			
		||||
    :form boolean confirm: confirmation
 | 
			
		||||
    :form string  state: unique value to prevent cross-site request forgery
 | 
			
		||||
                         (not mandatory)
 | 
			
		||||
    :form string  code_challenge: string generated from a code verifier
 | 
			
		||||
                         (for PKCE, not mandatory)
 | 
			
		||||
    :form string  code_challenge_method: method used to create challenge,
 | 
			
		||||
                         for instance "S256" (for PKCE, not mandatory)
 | 
			
		||||
 | 
			
		||||
    :reqheader Authorization: OAuth 2.0 Bearer Token
 | 
			
		||||
 | 
			
		||||
    :statuscode 200: success
 | 
			
		||||
    :statuscode 400:
 | 
			
		||||
        - invalid payload
 | 
			
		||||
        - errors returned by Authlib library
 | 
			
		||||
    :statuscode 401:
 | 
			
		||||
        - provide a valid auth token
 | 
			
		||||
        - signature expired, please log in again
 | 
			
		||||
        - invalid token, please log in again
 | 
			
		||||
    """
 | 
			
		||||
    data = request.form
 | 
			
		||||
    if not data or 'client_id' not in data or 'response_type' not in data:
 | 
			
		||||
        return InvalidPayloadErrorResponse()
 | 
			
		||||
@@ -192,9 +559,86 @@ def authorize(auth_user: User) -> Union[HttpResponse, Dict]:
 | 
			
		||||
 | 
			
		||||
@oauth2_blueprint.route('/oauth/token', methods=['POST'])
 | 
			
		||||
def issue_token() -> Response:
 | 
			
		||||
    """
 | 
			
		||||
    Issue or refresh token for a given OAuth2 client (app).
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      POST /api/oauth/token HTTP/1.1
 | 
			
		||||
      Content-Type: multipart/form-data
 | 
			
		||||
 | 
			
		||||
    **Example response**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      HTTP/1.1 200 SUCCESS
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        "access_token": "rOEHv64THCG28WcewZHRnVLUsOdUvw8NVnHKCmL57e",
 | 
			
		||||
        "expires_in": 864000,
 | 
			
		||||
        "refresh_token": "NuV9cY8VQOnrQKHTZ5pQAq2Zw7mSH0MorNPJr14AmSwD6f6I",
 | 
			
		||||
        "scope": ["profile:read", "workouts:write"],
 | 
			
		||||
        "token_type": "Bearer",
 | 
			
		||||
        "expires_at": 1658660147.0667062
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    :form string  client_id: OAuth2 client 'client_id'
 | 
			
		||||
    :form string  client_secret: OAuth2 client secret
 | 
			
		||||
    :form string  grant_type: OAuth2 client grant type
 | 
			
		||||
                         (only 'authorization_code' (for token issue)
 | 
			
		||||
                         and 'refresh_token' (for token refresh)
 | 
			
		||||
                         are supported by FitTrackee)
 | 
			
		||||
    :form string  code: code generated after authorizing the client
 | 
			
		||||
                         (for token issue)
 | 
			
		||||
    :form string  code_verifier: code verifier
 | 
			
		||||
                         (for PKCE and token issue, not mandatory)
 | 
			
		||||
    :form string  refresh_token: refresh token (for token refresh)
 | 
			
		||||
 | 
			
		||||
    :statuscode 200: success
 | 
			
		||||
    :statuscode 400:
 | 
			
		||||
        - errors returned by Authlib library
 | 
			
		||||
    :statuscode 401:
 | 
			
		||||
        - provide a valid auth token
 | 
			
		||||
        - signature expired, please log in again
 | 
			
		||||
        - invalid token, please log in again
 | 
			
		||||
    """
 | 
			
		||||
    return authorization_server.create_token_response()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@oauth2_blueprint.route('/oauth/revoke', methods=['POST'])
 | 
			
		||||
def revoke_token() -> Response:
 | 
			
		||||
    """
 | 
			
		||||
    Revoke a token for a given OAuth2 client (app).
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      POST /api/oauth/revoke HTTP/1.1
 | 
			
		||||
      Content-Type: multipart/form-data
 | 
			
		||||
 | 
			
		||||
    **Example response**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      HTTP/1.1 200 SUCCESS
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
      {}
 | 
			
		||||
 | 
			
		||||
    :form string client_id: OAuth2 client 'client_id'
 | 
			
		||||
    :form string client_secret: OAuth2 client secret
 | 
			
		||||
    :form string token: access token to revoke
 | 
			
		||||
 | 
			
		||||
    :statuscode 200: success
 | 
			
		||||
    :statuscode 400:
 | 
			
		||||
        - errors returned by Authlib library
 | 
			
		||||
    :statuscode 401:
 | 
			
		||||
        - provide a valid auth token
 | 
			
		||||
        - signature expired, please log in again
 | 
			
		||||
        - invalid token, please log in again
 | 
			
		||||
    """
 | 
			
		||||
    return authorization_server.create_endpoint_response('revocation')
 | 
			
		||||
 
 | 
			
		||||
@@ -73,7 +73,7 @@ def send_account_confirmation_email(user: User) -> None:
 | 
			
		||||
@auth_blueprint.route('/auth/register', methods=['POST'])
 | 
			
		||||
def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    register a user and send confirmation email.
 | 
			
		||||
    Register a user and send confirmation email.
 | 
			
		||||
 | 
			
		||||
    The newly created account is inactive. The user must confirm his email
 | 
			
		||||
    to activate it.
 | 
			
		||||
@@ -131,7 +131,6 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
        error, registration is disabled
 | 
			
		||||
    :statuscode 500:
 | 
			
		||||
        error, please try again or contact the administrator
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    if not current_app.config.get('is_registration_enabled'):
 | 
			
		||||
        return ForbiddenErrorResponse('error, registration is disabled')
 | 
			
		||||
@@ -190,7 +189,7 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
@auth_blueprint.route('/auth/login', methods=['POST'])
 | 
			
		||||
def login_user() -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    user login
 | 
			
		||||
    User login.
 | 
			
		||||
 | 
			
		||||
    Only user with an active account can log in.
 | 
			
		||||
 | 
			
		||||
@@ -268,7 +267,9 @@ def get_authenticated_user_profile(
 | 
			
		||||
    auth_user: User,
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    get authenticated user info (profile, account, preferences)
 | 
			
		||||
    Get authenticated user info (profile, account, preferences).
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``profile:read``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -359,7 +360,6 @@ def get_authenticated_user_profile(
 | 
			
		||||
        - provide a valid auth token
 | 
			
		||||
        - signature expired, please log in again
 | 
			
		||||
        - invalid token, please log in again
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    return {'status': 'success', 'data': auth_user.serialize(auth_user)}
 | 
			
		||||
 | 
			
		||||
@@ -368,7 +368,9 @@ def get_authenticated_user_profile(
 | 
			
		||||
@require_auth(scopes=['profile:write'])
 | 
			
		||||
def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    edit authenticated user profile
 | 
			
		||||
    Edit authenticated user profile.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``profile:write``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -469,7 +471,6 @@ def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
 | 
			
		||||
        - signature expired, please log in again
 | 
			
		||||
        - invalid token, please log in again
 | 
			
		||||
    :statuscode 500: error, please try again or contact the administrator
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    # get post data
 | 
			
		||||
    post_data = request.get_json()
 | 
			
		||||
@@ -516,7 +517,7 @@ def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
 | 
			
		||||
@require_auth(scopes=['profile:write'])
 | 
			
		||||
def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    update authenticated user email and password
 | 
			
		||||
    Update authenticated user email and password.
 | 
			
		||||
 | 
			
		||||
    It sends emails if sending is enabled:
 | 
			
		||||
 | 
			
		||||
@@ -526,6 +527,8 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
 | 
			
		||||
      - one to the current address to inform user
 | 
			
		||||
      - another one to the new address to confirm it.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``profile:write``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
@@ -628,7 +631,6 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
 | 
			
		||||
        - invalid token, please log in again
 | 
			
		||||
        - invalid credentials
 | 
			
		||||
    :statuscode 500: error, please try again or contact the administrator
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    data = request.get_json()
 | 
			
		||||
    if not data:
 | 
			
		||||
@@ -724,7 +726,9 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
 | 
			
		||||
@require_auth(scopes=['profile:write'])
 | 
			
		||||
def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    edit authenticated user preferences
 | 
			
		||||
    Edit authenticated user preferences.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``profile:write``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -825,7 +829,6 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
 | 
			
		||||
        - signature expired, please log in again
 | 
			
		||||
        - invalid token, please log in again
 | 
			
		||||
    :statuscode 500: error, please try again or contact the administrator
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    # get post data
 | 
			
		||||
    post_data = request.get_json()
 | 
			
		||||
@@ -867,7 +870,9 @@ def edit_user_sport_preferences(
 | 
			
		||||
    auth_user: User,
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    edit authenticated user sport preferences
 | 
			
		||||
    Edit authenticated user sport preferences.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``profile:write``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -912,7 +917,6 @@ def edit_user_sport_preferences(
 | 
			
		||||
    :statuscode 404:
 | 
			
		||||
        - sport does not exist
 | 
			
		||||
    :statuscode 500: error, please try again or contact the administrator
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    post_data = request.get_json()
 | 
			
		||||
    if (
 | 
			
		||||
@@ -973,7 +977,9 @@ def reset_user_sport_preferences(
 | 
			
		||||
    auth_user: User, sport_id: int
 | 
			
		||||
) -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    reset authenticated user preferences for a given sport
 | 
			
		||||
    Reset authenticated user preferences for a given sport.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``profile:write``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -1001,7 +1007,6 @@ def reset_user_sport_preferences(
 | 
			
		||||
    :statuscode 404:
 | 
			
		||||
        - sport does not exist
 | 
			
		||||
    :statuscode 500: error, please try again or contact the administrator
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    sport = Sport.query.filter_by(id=sport_id).first()
 | 
			
		||||
    if not sport:
 | 
			
		||||
@@ -1026,7 +1031,9 @@ def reset_user_sport_preferences(
 | 
			
		||||
@require_auth(scopes=['profile:write'])
 | 
			
		||||
def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    update authenticated user picture
 | 
			
		||||
    Update authenticated user picture.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``profile:write``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -1063,7 +1070,6 @@ def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]:
 | 
			
		||||
        - invalid token, please log in again
 | 
			
		||||
    :statuscode 413: error during picture update: file size exceeds 1.0MB
 | 
			
		||||
    :statuscode 500: error during picture update
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        response_object = get_error_response_if_file_is_invalid(
 | 
			
		||||
@@ -1114,7 +1120,9 @@ def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]:
 | 
			
		||||
@require_auth(scopes=['profile:write'])
 | 
			
		||||
def del_picture(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    delete authenticated user picture
 | 
			
		||||
    Delete authenticated user picture.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``profile:write``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -1156,7 +1164,7 @@ def del_picture(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
@auth_blueprint.route('/auth/password/reset-request', methods=['POST'])
 | 
			
		||||
def request_password_reset() -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    handle password reset request
 | 
			
		||||
    Handle password reset request.
 | 
			
		||||
 | 
			
		||||
    If email sending is disabled, this endpoint is not available
 | 
			
		||||
 | 
			
		||||
@@ -1226,9 +1234,9 @@ def request_password_reset() -> Union[Dict, HttpResponse]:
 | 
			
		||||
@auth_blueprint.route('/auth/password/update', methods=['POST'])
 | 
			
		||||
def update_password() -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    update user password after password reset request
 | 
			
		||||
    Update user password after password reset request.
 | 
			
		||||
 | 
			
		||||
    It sends emails if sending is enabled
 | 
			
		||||
    It sends emails if sending is enabled.
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -1309,7 +1317,7 @@ def update_password() -> Union[Dict, HttpResponse]:
 | 
			
		||||
@auth_blueprint.route('/auth/email/update', methods=['POST'])
 | 
			
		||||
def update_email() -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    update user email after confirmation
 | 
			
		||||
    Update user email after confirmation.
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -1368,7 +1376,7 @@ def update_email() -> Union[Dict, HttpResponse]:
 | 
			
		||||
@auth_blueprint.route('/auth/account/confirm', methods=['POST'])
 | 
			
		||||
def confirm_account() -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    activate user account after registration
 | 
			
		||||
    Activate user account after registration.
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -1430,9 +1438,9 @@ def confirm_account() -> Union[Dict, HttpResponse]:
 | 
			
		||||
@auth_blueprint.route('/auth/account/resend-confirmation', methods=['POST'])
 | 
			
		||||
def resend_account_confirmation_email() -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    resend email with instructions to confirm account
 | 
			
		||||
    Resend email with instructions to confirm account.
 | 
			
		||||
 | 
			
		||||
    If email sending is disabled, this endpoint is not available
 | 
			
		||||
    If email sending is disabled, this endpoint is not available.
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -39,10 +39,12 @@ USER_PER_PAGE = 10
 | 
			
		||||
def get_users(auth_user: User) -> Dict:
 | 
			
		||||
    """
 | 
			
		||||
    Get all users (regardless their account status), if authenticated user
 | 
			
		||||
    has admin rights
 | 
			
		||||
    has admin rights.
 | 
			
		||||
 | 
			
		||||
    It returns user preferences only for authenticated user.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``users:read``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
    - without parameters
 | 
			
		||||
@@ -246,6 +248,8 @@ def get_single_user(
 | 
			
		||||
 | 
			
		||||
    It returns user preferences only for authenticated user.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``users:read``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
@@ -398,7 +402,7 @@ def get_picture(user_name: str) -> Any:
 | 
			
		||||
@require_auth(scopes=['users:write'], as_admin=True)
 | 
			
		||||
def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Update user account
 | 
			
		||||
    Update user account.
 | 
			
		||||
 | 
			
		||||
    - add/remove admin rights (regardless user account status)
 | 
			
		||||
    - reset password (and send email to update user password,
 | 
			
		||||
@@ -406,7 +410,9 @@ def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    - update user email (and send email to new user email, if sending enabled)
 | 
			
		||||
    - activate account for an inactive user
 | 
			
		||||
 | 
			
		||||
    Only user with admin rights can modify another user
 | 
			
		||||
    Only user with admin rights can modify another user.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``users:write``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -599,12 +605,14 @@ def delete_user(
 | 
			
		||||
    auth_user: User, user_name: str
 | 
			
		||||
) -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Delete a user account
 | 
			
		||||
    Delete a user account.
 | 
			
		||||
 | 
			
		||||
    A user can only delete his own account
 | 
			
		||||
    A user can only delete his own account.
 | 
			
		||||
 | 
			
		||||
    An admin can delete all accounts except his account if he's the only
 | 
			
		||||
    one admin
 | 
			
		||||
    one admin.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``users:write``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,8 @@ def get_records(auth_user: User) -> Dict:
 | 
			
		||||
        - longest duration (record_type: 'LD')
 | 
			
		||||
        - maximum speed (record_type: 'MS')
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``workouts:read``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,8 @@ def get_sports(auth_user: User) -> Dict:
 | 
			
		||||
    """
 | 
			
		||||
    Get all sports
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``workouts:read``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
@@ -200,6 +202,8 @@ def get_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get a sport
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``workouts:read``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
@@ -307,8 +311,11 @@ def get_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]:
 | 
			
		||||
@require_auth(scopes=['workouts:write'], as_admin=True)
 | 
			
		||||
def update_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Update a sport
 | 
			
		||||
    Authenticated user must be an admin
 | 
			
		||||
    Update a sport.
 | 
			
		||||
 | 
			
		||||
    Authenticated user must be an admin.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``workouts:write``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -317,7 +324,7 @@ def update_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]:
 | 
			
		||||
      PATCH /api/sports/1 HTTP/1.1
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
    **Example response**:
 | 
			
		||||
    **Example responses**:
 | 
			
		||||
 | 
			
		||||
    - success
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -179,7 +179,9 @@ def get_workouts_by_time(
 | 
			
		||||
    auth_user: User, user_name: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get workouts statistics for a user by time
 | 
			
		||||
    Get workouts statistics for a user by time.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``workouts:read``
 | 
			
		||||
 | 
			
		||||
    **Example requests**:
 | 
			
		||||
 | 
			
		||||
@@ -255,7 +257,7 @@ def get_workouts_by_time(
 | 
			
		||||
        "status": "success"
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    :param integer user_name: user name
 | 
			
		||||
    :param integer user_name: username
 | 
			
		||||
 | 
			
		||||
    :query string from: start date (format: ``%Y-%m-%d``)
 | 
			
		||||
    :query string to: end date (format: ``%Y-%m-%d``)
 | 
			
		||||
@@ -286,7 +288,9 @@ def get_workouts_by_sport(
 | 
			
		||||
    auth_user: User, user_name: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get workouts statistics for a user by sport
 | 
			
		||||
    Get workouts statistics for a user by sport.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``workouts:read``
 | 
			
		||||
 | 
			
		||||
    **Example requests**:
 | 
			
		||||
 | 
			
		||||
@@ -357,7 +361,7 @@ def get_workouts_by_sport(
 | 
			
		||||
        "status": "success"
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    :param integer user_name: user name
 | 
			
		||||
    :param integer user_name: username
 | 
			
		||||
 | 
			
		||||
    :query integer sport_id: sport id
 | 
			
		||||
 | 
			
		||||
@@ -380,7 +384,9 @@ def get_workouts_by_sport(
 | 
			
		||||
@require_auth(scopes=['workouts:read'], as_admin=True)
 | 
			
		||||
def get_application_stats(auth_user: User) -> Dict:
 | 
			
		||||
    """
 | 
			
		||||
    Get all application statistics
 | 
			
		||||
    Get all application statistics.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``workouts:read``
 | 
			
		||||
 | 
			
		||||
    **Example requests**:
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -61,6 +61,8 @@ def get_workouts(auth_user: User) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get workouts for the authenticated user.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``workouts:read``
 | 
			
		||||
 | 
			
		||||
    **Example requests**:
 | 
			
		||||
 | 
			
		||||
    - without parameters
 | 
			
		||||
@@ -303,7 +305,9 @@ def get_workout(
 | 
			
		||||
    auth_user: User, workout_short_id: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get a workout
 | 
			
		||||
    Get a workout.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``workouts:read``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -357,7 +361,7 @@ def get_workout(
 | 
			
		||||
          "status": "success"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    - acitivity not found:
 | 
			
		||||
    - workout not found:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
@@ -467,7 +471,9 @@ def get_workout_gpx(
 | 
			
		||||
    auth_user: User, workout_short_id: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get gpx file for a workout displayed on map with Leaflet
 | 
			
		||||
    Get gpx file for a workout displayed on map with Leaflet.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``workouts:read``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -517,7 +523,9 @@ def get_workout_chart_data(
 | 
			
		||||
    auth_user: User, workout_short_id: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get chart data from a workout gpx file, to display it with Recharts
 | 
			
		||||
    Get chart data from a workout gpx file, to display it with Chart.js.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``workouts:read``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -587,7 +595,9 @@ def get_segment_gpx(
 | 
			
		||||
    auth_user: User, workout_short_id: str, segment_id: int
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get gpx file for a workout segment displayed on map with Leaflet
 | 
			
		||||
    Get gpx file for a workout segment displayed on map with Leaflet.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``workouts:read``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -641,6 +651,8 @@ def get_segment_chart_data(
 | 
			
		||||
    """
 | 
			
		||||
    Get chart data from a workout gpx file, to display it with Recharts
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``workouts:read``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
@@ -710,7 +722,9 @@ def download_workout_gpx(
 | 
			
		||||
    auth_user: User, workout_short_id: str
 | 
			
		||||
) -> Union[HttpResponse, Response]:
 | 
			
		||||
    """
 | 
			
		||||
    Download gpx file
 | 
			
		||||
    Download gpx file.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``workouts:read``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -763,7 +777,7 @@ def download_workout_gpx(
 | 
			
		||||
@workouts_blueprint.route('/workouts/map/<map_id>', methods=['GET'])
 | 
			
		||||
def get_map(map_id: int) -> Union[HttpResponse, Response]:
 | 
			
		||||
    """
 | 
			
		||||
    Get map image for workouts with gpx
 | 
			
		||||
    Get map image for workouts with gpx.
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -853,7 +867,9 @@ def get_map_tile(s: str, z: str, x: str, y: str) -> Tuple[Response, int]:
 | 
			
		||||
@require_auth(scopes=['workouts:write'])
 | 
			
		||||
def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Post a workout with a gpx file
 | 
			
		||||
    Post a workout with a gpx file.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``workouts:write``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -1023,7 +1039,9 @@ def post_workout_no_gpx(
 | 
			
		||||
    auth_user: User,
 | 
			
		||||
) -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Post a workout without gpx file
 | 
			
		||||
    Post a workout without gpx file.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``workouts:write``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -1172,7 +1190,9 @@ def update_workout(
 | 
			
		||||
    auth_user: User, workout_short_id: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Update a workout
 | 
			
		||||
    Update a workout.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``workouts:write``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -1320,7 +1340,9 @@ def delete_workout(
 | 
			
		||||
    auth_user: User, workout_short_id: str
 | 
			
		||||
) -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Delete a workout
 | 
			
		||||
    Delete a workout.
 | 
			
		||||
 | 
			
		||||
    **Scope**: ``workouts:write``
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user