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