API & Docs - init OAuth 2.0 documentation
This commit is contained in:
parent
ca53201a9e
commit
5fcc3e9a44
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
|
auth
|
||||||
configuration
|
configuration
|
||||||
|
oauth2
|
||||||
records
|
records
|
||||||
sports
|
sports
|
||||||
stats
|
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
|
@ -15,6 +15,7 @@ A command line interface (CLI) is available to manage database and users.
|
|||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
db Manage database.
|
db Manage database.
|
||||||
|
oauth2 Manage OAuth2 tokens.
|
||||||
users Manage users.
|
users Manage users.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
@ -40,6 +41,26 @@ Apply migrations.
|
|||||||
Empty database and delete uploaded files, only on development environments.
|
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
|
Users
|
||||||
~~~~~
|
~~~~~
|
||||||
|
@ -81,6 +81,8 @@ Account & preferences
|
|||||||
| A disabled sport (by admin or user) will not appear in dropdown when **adding a workout**.
|
| 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 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
|
Administration
|
||||||
^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^
|
||||||
|
@ -33,6 +33,7 @@ Table of contents
|
|||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
features
|
features
|
||||||
|
apps
|
||||||
installation
|
installation
|
||||||
cli
|
cli
|
||||||
api/index
|
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
|
- `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)
|
- `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
|
- `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:
|
- Client:
|
||||||
- Vue3/Vuex
|
- Vue3/Vuex
|
||||||
- `Leaflet <https://leafletjs.com/>`__ to display map
|
- `Leaflet <https://leafletjs.com/>`__ to display map
|
||||||
|
@ -22,7 +22,7 @@ config_blueprint = Blueprint('config', __name__)
|
|||||||
@config_blueprint.route('/config', methods=['GET'])
|
@config_blueprint.route('/config', methods=['GET'])
|
||||||
def get_application_config() -> Union[Dict, HttpResponse]:
|
def get_application_config() -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
Get Application config
|
Get Application configuration.
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@ -70,9 +70,11 @@ def get_application_config() -> Union[Dict, HttpResponse]:
|
|||||||
@require_auth(scopes=['application:write'], as_admin=True)
|
@require_auth(scopes=['application:write'], as_admin=True)
|
||||||
def update_application_config(auth_user: User) -> Union[Dict, HttpResponse]:
|
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**:
|
**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 string admin_contact: email to contact the administrator
|
||||||
:<json integer gpx_limit_import: max number of files in zip archive
|
:<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_single_file_size: max size of a single file
|
||||||
:<json integer max_users: max users allowed to register on instance
|
:<json integer max_users: max users allowed to register on instance
|
||||||
:<json integer max_zip_file_size: max size of a zip archive
|
:<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
|
:statuscode 200: success
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return {'status': 'success', 'message': 'pong!'}
|
return {'status': 'success', 'message': 'pong!'}
|
||||||
|
@ -19,7 +19,7 @@ def oauth2_cli() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@oauth2_cli.command('clean')
|
@oauth2_cli.command('clean')
|
||||||
@click.option('--days', type=int)
|
@click.option('--days', type=int, help='Number of days.')
|
||||||
def clean(
|
def clean(
|
||||||
days: int,
|
days: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -38,6 +38,73 @@ def is_errored(url: str) -> Optional[str]:
|
|||||||
@oauth2_blueprint.route('/oauth/apps', methods=['GET'])
|
@oauth2_blueprint.route('/oauth/apps', methods=['GET'])
|
||||||
@require_auth()
|
@require_auth()
|
||||||
def get_clients(auth_user: User) -> Dict:
|
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()
|
params = request.args.copy()
|
||||||
page = int(params.get('page', 1))
|
page = int(params.get('page', 1))
|
||||||
per_page = DEFAULT_PER_PAGE
|
per_page = DEFAULT_PER_PAGE
|
||||||
@ -67,6 +134,61 @@ def get_clients(auth_user: User) -> Dict:
|
|||||||
@oauth2_blueprint.route('/oauth/apps', methods=['POST'])
|
@oauth2_blueprint.route('/oauth/apps', methods=['POST'])
|
||||||
@require_auth()
|
@require_auth()
|
||||||
def create_client(auth_user: User) -> Union[HttpResponse, Tuple[Dict, int]]:
|
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()
|
client_metadata = request.get_json()
|
||||||
if not client_metadata:
|
if not client_metadata:
|
||||||
return InvalidPayloadErrorResponse(
|
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()
|
@require_auth()
|
||||||
def get_client_by_client_id(
|
def get_client_by_client_id(
|
||||||
auth_user: User, client_id: str
|
auth_user: User, client_client_id: str
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> 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'])
|
@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(
|
def get_client_by_id(
|
||||||
auth_user: User, client_id: int
|
auth_user: User, client_id: int
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> 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)
|
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(
|
def delete_client(
|
||||||
auth_user: User, client_id: int
|
auth_user: User, client_id: int
|
||||||
) -> Union[Tuple[Dict, int], HttpResponse]:
|
) -> 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(
|
client = OAuth2Client.query.filter_by(
|
||||||
id=client_id,
|
id=client_id,
|
||||||
user_id=auth_user.id,
|
user_id=auth_user.id,
|
||||||
@ -163,6 +446,41 @@ def delete_client(
|
|||||||
def revoke_client_tokens(
|
def revoke_client_tokens(
|
||||||
auth_user: User, client_id: int
|
auth_user: User, client_id: int
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> 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()
|
client = OAuth2Client.query.filter_by(id=client_id).first()
|
||||||
|
|
||||||
if not client:
|
if not client:
|
||||||
@ -175,6 +493,55 @@ def revoke_client_tokens(
|
|||||||
@oauth2_blueprint.route('/oauth/authorize', methods=['POST'])
|
@oauth2_blueprint.route('/oauth/authorize', methods=['POST'])
|
||||||
@require_auth()
|
@require_auth()
|
||||||
def authorize(auth_user: User) -> Union[HttpResponse, Dict]:
|
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
|
data = request.form
|
||||||
if not data or 'client_id' not in data or 'response_type' not in data:
|
if not data or 'client_id' not in data or 'response_type' not in data:
|
||||||
return InvalidPayloadErrorResponse()
|
return InvalidPayloadErrorResponse()
|
||||||
@ -192,9 +559,86 @@ def authorize(auth_user: User) -> Union[HttpResponse, Dict]:
|
|||||||
|
|
||||||
@oauth2_blueprint.route('/oauth/token', methods=['POST'])
|
@oauth2_blueprint.route('/oauth/token', methods=['POST'])
|
||||||
def issue_token() -> Response:
|
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()
|
return authorization_server.create_token_response()
|
||||||
|
|
||||||
|
|
||||||
@oauth2_blueprint.route('/oauth/revoke', methods=['POST'])
|
@oauth2_blueprint.route('/oauth/revoke', methods=['POST'])
|
||||||
def revoke_token() -> Response:
|
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')
|
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'])
|
@auth_blueprint.route('/auth/register', methods=['POST'])
|
||||||
def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
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
|
The newly created account is inactive. The user must confirm his email
|
||||||
to activate it.
|
to activate it.
|
||||||
@ -131,7 +131,6 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
|||||||
error, registration is disabled
|
error, registration is disabled
|
||||||
:statuscode 500:
|
:statuscode 500:
|
||||||
error, please try again or contact the administrator
|
error, please try again or contact the administrator
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not current_app.config.get('is_registration_enabled'):
|
if not current_app.config.get('is_registration_enabled'):
|
||||||
return ForbiddenErrorResponse('error, registration is disabled')
|
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'])
|
@auth_blueprint.route('/auth/login', methods=['POST'])
|
||||||
def login_user() -> Union[Dict, HttpResponse]:
|
def login_user() -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
user login
|
User login.
|
||||||
|
|
||||||
Only user with an active account can log in.
|
Only user with an active account can log in.
|
||||||
|
|
||||||
@ -268,7 +267,9 @@ def get_authenticated_user_profile(
|
|||||||
auth_user: User,
|
auth_user: User,
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
get authenticated user info (profile, account, preferences)
|
Get authenticated user info (profile, account, preferences).
|
||||||
|
|
||||||
|
**Scope**: ``profile:read``
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@ -359,7 +360,6 @@ def get_authenticated_user_profile(
|
|||||||
- provide a valid auth token
|
- provide a valid auth token
|
||||||
- signature expired, please log in again
|
- signature expired, please log in again
|
||||||
- invalid token, please log in again
|
- invalid token, please log in again
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return {'status': 'success', 'data': auth_user.serialize(auth_user)}
|
return {'status': 'success', 'data': auth_user.serialize(auth_user)}
|
||||||
|
|
||||||
@ -368,7 +368,9 @@ def get_authenticated_user_profile(
|
|||||||
@require_auth(scopes=['profile:write'])
|
@require_auth(scopes=['profile:write'])
|
||||||
def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
|
def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
edit authenticated user profile
|
Edit authenticated user profile.
|
||||||
|
|
||||||
|
**Scope**: ``profile:write``
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@ -469,7 +471,6 @@ def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
- signature expired, please log in again
|
- signature expired, please log in again
|
||||||
- invalid token, please log in again
|
- invalid token, please log in again
|
||||||
:statuscode 500: error, please try again or contact the administrator
|
:statuscode 500: error, please try again or contact the administrator
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# get post data
|
# get post data
|
||||||
post_data = request.get_json()
|
post_data = request.get_json()
|
||||||
@ -516,7 +517,7 @@ def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
@require_auth(scopes=['profile:write'])
|
@require_auth(scopes=['profile:write'])
|
||||||
def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
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:
|
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
|
- one to the current address to inform user
|
||||||
- another one to the new address to confirm it.
|
- another one to the new address to confirm it.
|
||||||
|
|
||||||
|
**Scope**: ``profile:write``
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@ -628,7 +631,6 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
- invalid token, please log in again
|
- invalid token, please log in again
|
||||||
- invalid credentials
|
- invalid credentials
|
||||||
:statuscode 500: error, please try again or contact the administrator
|
:statuscode 500: error, please try again or contact the administrator
|
||||||
|
|
||||||
"""
|
"""
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data:
|
if not data:
|
||||||
@ -724,7 +726,9 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
@require_auth(scopes=['profile:write'])
|
@require_auth(scopes=['profile:write'])
|
||||||
def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
|
def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
edit authenticated user preferences
|
Edit authenticated user preferences.
|
||||||
|
|
||||||
|
**Scope**: ``profile:write``
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@ -825,7 +829,6 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
- signature expired, please log in again
|
- signature expired, please log in again
|
||||||
- invalid token, please log in again
|
- invalid token, please log in again
|
||||||
:statuscode 500: error, please try again or contact the administrator
|
:statuscode 500: error, please try again or contact the administrator
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# get post data
|
# get post data
|
||||||
post_data = request.get_json()
|
post_data = request.get_json()
|
||||||
@ -867,7 +870,9 @@ def edit_user_sport_preferences(
|
|||||||
auth_user: User,
|
auth_user: User,
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
edit authenticated user sport preferences
|
Edit authenticated user sport preferences.
|
||||||
|
|
||||||
|
**Scope**: ``profile:write``
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@ -912,7 +917,6 @@ def edit_user_sport_preferences(
|
|||||||
:statuscode 404:
|
:statuscode 404:
|
||||||
- sport does not exist
|
- sport does not exist
|
||||||
:statuscode 500: error, please try again or contact the administrator
|
:statuscode 500: error, please try again or contact the administrator
|
||||||
|
|
||||||
"""
|
"""
|
||||||
post_data = request.get_json()
|
post_data = request.get_json()
|
||||||
if (
|
if (
|
||||||
@ -973,7 +977,9 @@ def reset_user_sport_preferences(
|
|||||||
auth_user: User, sport_id: int
|
auth_user: User, sport_id: int
|
||||||
) -> Union[Tuple[Dict, int], HttpResponse]:
|
) -> 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**:
|
**Example request**:
|
||||||
|
|
||||||
@ -1001,7 +1007,6 @@ def reset_user_sport_preferences(
|
|||||||
:statuscode 404:
|
:statuscode 404:
|
||||||
- sport does not exist
|
- sport does not exist
|
||||||
:statuscode 500: error, please try again or contact the administrator
|
:statuscode 500: error, please try again or contact the administrator
|
||||||
|
|
||||||
"""
|
"""
|
||||||
sport = Sport.query.filter_by(id=sport_id).first()
|
sport = Sport.query.filter_by(id=sport_id).first()
|
||||||
if not sport:
|
if not sport:
|
||||||
@ -1026,7 +1031,9 @@ def reset_user_sport_preferences(
|
|||||||
@require_auth(scopes=['profile:write'])
|
@require_auth(scopes=['profile:write'])
|
||||||
def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]:
|
def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
update authenticated user picture
|
Update authenticated user picture.
|
||||||
|
|
||||||
|
**Scope**: ``profile:write``
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@ -1063,7 +1070,6 @@ def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
- invalid token, please log in again
|
- invalid token, please log in again
|
||||||
:statuscode 413: error during picture update: file size exceeds 1.0MB
|
:statuscode 413: error during picture update: file size exceeds 1.0MB
|
||||||
:statuscode 500: error during picture update
|
:statuscode 500: error during picture update
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
response_object = get_error_response_if_file_is_invalid(
|
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'])
|
@require_auth(scopes=['profile:write'])
|
||||||
def del_picture(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
|
def del_picture(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
|
||||||
"""
|
"""
|
||||||
delete authenticated user picture
|
Delete authenticated user picture.
|
||||||
|
|
||||||
|
**Scope**: ``profile:write``
|
||||||
|
|
||||||
**Example request**:
|
**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'])
|
@auth_blueprint.route('/auth/password/reset-request', methods=['POST'])
|
||||||
def request_password_reset() -> Union[Dict, HttpResponse]:
|
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
|
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'])
|
@auth_blueprint.route('/auth/password/update', methods=['POST'])
|
||||||
def update_password() -> Union[Dict, HttpResponse]:
|
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**:
|
**Example request**:
|
||||||
|
|
||||||
@ -1309,7 +1317,7 @@ def update_password() -> Union[Dict, HttpResponse]:
|
|||||||
@auth_blueprint.route('/auth/email/update', methods=['POST'])
|
@auth_blueprint.route('/auth/email/update', methods=['POST'])
|
||||||
def update_email() -> Union[Dict, HttpResponse]:
|
def update_email() -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
update user email after confirmation
|
Update user email after confirmation.
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@ -1368,7 +1376,7 @@ def update_email() -> Union[Dict, HttpResponse]:
|
|||||||
@auth_blueprint.route('/auth/account/confirm', methods=['POST'])
|
@auth_blueprint.route('/auth/account/confirm', methods=['POST'])
|
||||||
def confirm_account() -> Union[Dict, HttpResponse]:
|
def confirm_account() -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
activate user account after registration
|
Activate user account after registration.
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@ -1430,9 +1438,9 @@ def confirm_account() -> Union[Dict, HttpResponse]:
|
|||||||
@auth_blueprint.route('/auth/account/resend-confirmation', methods=['POST'])
|
@auth_blueprint.route('/auth/account/resend-confirmation', methods=['POST'])
|
||||||
def resend_account_confirmation_email() -> Union[Dict, HttpResponse]:
|
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**:
|
**Example request**:
|
||||||
|
|
||||||
|
@ -39,10 +39,12 @@ USER_PER_PAGE = 10
|
|||||||
def get_users(auth_user: User) -> Dict:
|
def get_users(auth_user: User) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get all users (regardless their account status), if authenticated user
|
Get all users (regardless their account status), if authenticated user
|
||||||
has admin rights
|
has admin rights.
|
||||||
|
|
||||||
It returns user preferences only for authenticated user.
|
It returns user preferences only for authenticated user.
|
||||||
|
|
||||||
|
**Scope**: ``users:read``
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
- without parameters
|
- without parameters
|
||||||
@ -246,6 +248,8 @@ def get_single_user(
|
|||||||
|
|
||||||
It returns user preferences only for authenticated user.
|
It returns user preferences only for authenticated user.
|
||||||
|
|
||||||
|
**Scope**: ``users:read``
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@ -398,7 +402,7 @@ def get_picture(user_name: str) -> Any:
|
|||||||
@require_auth(scopes=['users:write'], as_admin=True)
|
@require_auth(scopes=['users:write'], as_admin=True)
|
||||||
def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
|
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)
|
- add/remove admin rights (regardless user account status)
|
||||||
- reset password (and send email to update user password,
|
- 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)
|
- update user email (and send email to new user email, if sending enabled)
|
||||||
- activate account for an inactive user
|
- 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**:
|
**Example request**:
|
||||||
|
|
||||||
@ -599,12 +605,14 @@ def delete_user(
|
|||||||
auth_user: User, user_name: str
|
auth_user: User, user_name: str
|
||||||
) -> Union[Tuple[Dict, int], HttpResponse]:
|
) -> 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
|
An admin can delete all accounts except his account if he's the only
|
||||||
one admin
|
one admin.
|
||||||
|
|
||||||
|
**Scope**: ``users:write``
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
|
@ -22,6 +22,8 @@ def get_records(auth_user: User) -> Dict:
|
|||||||
- longest duration (record_type: 'LD')
|
- longest duration (record_type: 'LD')
|
||||||
- maximum speed (record_type: 'MS')
|
- maximum speed (record_type: 'MS')
|
||||||
|
|
||||||
|
**Scope**: ``workouts:read``
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
|
@ -24,6 +24,8 @@ def get_sports(auth_user: User) -> Dict:
|
|||||||
"""
|
"""
|
||||||
Get all sports
|
Get all sports
|
||||||
|
|
||||||
|
**Scope**: ``workouts:read``
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@ -200,6 +202,8 @@ def get_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]:
|
|||||||
"""
|
"""
|
||||||
Get a sport
|
Get a sport
|
||||||
|
|
||||||
|
**Scope**: ``workouts:read``
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. 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)
|
@require_auth(scopes=['workouts:write'], as_admin=True)
|
||||||
def update_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]:
|
def update_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
Update a sport
|
Update a sport.
|
||||||
Authenticated user must be an admin
|
|
||||||
|
Authenticated user must be an admin.
|
||||||
|
|
||||||
|
**Scope**: ``workouts:write``
|
||||||
|
|
||||||
**Example request**:
|
**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
|
PATCH /api/sports/1 HTTP/1.1
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
**Example response**:
|
**Example responses**:
|
||||||
|
|
||||||
- success
|
- success
|
||||||
|
|
||||||
|
@ -179,7 +179,9 @@ def get_workouts_by_time(
|
|||||||
auth_user: User, user_name: str
|
auth_user: User, user_name: str
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
Get workouts statistics for a user by time
|
Get workouts statistics for a user by time.
|
||||||
|
|
||||||
|
**Scope**: ``workouts:read``
|
||||||
|
|
||||||
**Example requests**:
|
**Example requests**:
|
||||||
|
|
||||||
@ -255,7 +257,7 @@ def get_workouts_by_time(
|
|||||||
"status": "success"
|
"status": "success"
|
||||||
}
|
}
|
||||||
|
|
||||||
:param integer user_name: user name
|
:param integer user_name: username
|
||||||
|
|
||||||
:query string from: start date (format: ``%Y-%m-%d``)
|
:query string from: start date (format: ``%Y-%m-%d``)
|
||||||
:query string to: end 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
|
auth_user: User, user_name: str
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
Get workouts statistics for a user by sport
|
Get workouts statistics for a user by sport.
|
||||||
|
|
||||||
|
**Scope**: ``workouts:read``
|
||||||
|
|
||||||
**Example requests**:
|
**Example requests**:
|
||||||
|
|
||||||
@ -357,7 +361,7 @@ def get_workouts_by_sport(
|
|||||||
"status": "success"
|
"status": "success"
|
||||||
}
|
}
|
||||||
|
|
||||||
:param integer user_name: user name
|
:param integer user_name: username
|
||||||
|
|
||||||
:query integer sport_id: sport id
|
:query integer sport_id: sport id
|
||||||
|
|
||||||
@ -380,7 +384,9 @@ def get_workouts_by_sport(
|
|||||||
@require_auth(scopes=['workouts:read'], as_admin=True)
|
@require_auth(scopes=['workouts:read'], as_admin=True)
|
||||||
def get_application_stats(auth_user: User) -> Dict:
|
def get_application_stats(auth_user: User) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get all application statistics
|
Get all application statistics.
|
||||||
|
|
||||||
|
**Scope**: ``workouts:read``
|
||||||
|
|
||||||
**Example requests**:
|
**Example requests**:
|
||||||
|
|
||||||
|
@ -61,6 +61,8 @@ def get_workouts(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
"""
|
"""
|
||||||
Get workouts for the authenticated user.
|
Get workouts for the authenticated user.
|
||||||
|
|
||||||
|
**Scope**: ``workouts:read``
|
||||||
|
|
||||||
**Example requests**:
|
**Example requests**:
|
||||||
|
|
||||||
- without parameters
|
- without parameters
|
||||||
@ -303,7 +305,9 @@ def get_workout(
|
|||||||
auth_user: User, workout_short_id: str
|
auth_user: User, workout_short_id: str
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
Get a workout
|
Get a workout.
|
||||||
|
|
||||||
|
**Scope**: ``workouts:read``
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@ -357,7 +361,7 @@ def get_workout(
|
|||||||
"status": "success"
|
"status": "success"
|
||||||
}
|
}
|
||||||
|
|
||||||
- acitivity not found:
|
- workout not found:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
|
|
||||||
@ -467,7 +471,9 @@ def get_workout_gpx(
|
|||||||
auth_user: User, workout_short_id: str
|
auth_user: User, workout_short_id: str
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> 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**:
|
**Example request**:
|
||||||
|
|
||||||
@ -517,7 +523,9 @@ def get_workout_chart_data(
|
|||||||
auth_user: User, workout_short_id: str
|
auth_user: User, workout_short_id: str
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> 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**:
|
**Example request**:
|
||||||
|
|
||||||
@ -587,7 +595,9 @@ def get_segment_gpx(
|
|||||||
auth_user: User, workout_short_id: str, segment_id: int
|
auth_user: User, workout_short_id: str, segment_id: int
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> 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**:
|
**Example request**:
|
||||||
|
|
||||||
@ -641,6 +651,8 @@ def get_segment_chart_data(
|
|||||||
"""
|
"""
|
||||||
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 Recharts
|
||||||
|
|
||||||
|
**Scope**: ``workouts:read``
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@ -710,7 +722,9 @@ def download_workout_gpx(
|
|||||||
auth_user: User, workout_short_id: str
|
auth_user: User, workout_short_id: str
|
||||||
) -> Union[HttpResponse, Response]:
|
) -> Union[HttpResponse, Response]:
|
||||||
"""
|
"""
|
||||||
Download gpx file
|
Download gpx file.
|
||||||
|
|
||||||
|
**Scope**: ``workouts:read``
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@ -763,7 +777,7 @@ def download_workout_gpx(
|
|||||||
@workouts_blueprint.route('/workouts/map/<map_id>', methods=['GET'])
|
@workouts_blueprint.route('/workouts/map/<map_id>', methods=['GET'])
|
||||||
def get_map(map_id: int) -> Union[HttpResponse, Response]:
|
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**:
|
**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'])
|
@require_auth(scopes=['workouts:write'])
|
||||||
def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
|
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**:
|
**Example request**:
|
||||||
|
|
||||||
@ -1023,7 +1039,9 @@ def post_workout_no_gpx(
|
|||||||
auth_user: User,
|
auth_user: User,
|
||||||
) -> Union[Tuple[Dict, int], HttpResponse]:
|
) -> Union[Tuple[Dict, int], HttpResponse]:
|
||||||
"""
|
"""
|
||||||
Post a workout without gpx file
|
Post a workout without gpx file.
|
||||||
|
|
||||||
|
**Scope**: ``workouts:write``
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@ -1172,7 +1190,9 @@ def update_workout(
|
|||||||
auth_user: User, workout_short_id: str
|
auth_user: User, workout_short_id: str
|
||||||
) -> Union[Dict, HttpResponse]:
|
) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
Update a workout
|
Update a workout.
|
||||||
|
|
||||||
|
**Scope**: ``workouts:write``
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@ -1320,7 +1340,9 @@ def delete_workout(
|
|||||||
auth_user: User, workout_short_id: str
|
auth_user: User, workout_short_id: str
|
||||||
) -> Union[Tuple[Dict, int], HttpResponse]:
|
) -> Union[Tuple[Dict, int], HttpResponse]:
|
||||||
"""
|
"""
|
||||||
Delete a workout
|
Delete a workout.
|
||||||
|
|
||||||
|
**Scope**: ``workouts:write``
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user