API & Docs - init OAuth 2.0 documentation

This commit is contained in:
Sam 2022-07-14 18:36:19 +02:00
parent ca53201a9e
commit 5fcc3e9a44
17 changed files with 641 additions and 61 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View File

@ -7,6 +7,7 @@ API documentation
auth auth
configuration configuration
oauth2
records records
sports sports
stats stats

View 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
View 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

View File

@ -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
~~~~~ ~~~~~

View File

@ -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
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^

View File

@ -33,6 +33,7 @@ Table of contents
:maxdepth: 1 :maxdepth: 1
features features
apps
installation installation
cli cli
api/index api/index

View File

@ -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

View File

@ -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!'}

View File

@ -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:

View File

@ -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')

View File

@ -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**:

View File

@ -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**:

View File

@ -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

View File

@ -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

View File

@ -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**:

View File

@ -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**: