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
configuration
oauth2
records
sports
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

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

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

View File

@ -33,6 +33,7 @@ Table of contents
:maxdepth: 1
features
apps
installation
cli
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
- `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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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