2020-05-01 16:18:59 +02:00
|
|
|
import os
|
|
|
|
import shutil
|
2021-01-02 19:28:03 +01:00
|
|
|
from typing import Any, Dict, Tuple, Union
|
2020-05-01 16:18:59 +02:00
|
|
|
|
2022-03-13 09:04:46 +01:00
|
|
|
from flask import Blueprint, current_app, request, send_file
|
2022-10-26 18:05:22 +02:00
|
|
|
from sqlalchemy import asc, desc, exc
|
2021-01-20 16:47:00 +01:00
|
|
|
|
2022-09-17 19:36:03 +02:00
|
|
|
from fittrackee import db, limiter
|
2022-03-13 09:06:57 +01:00
|
|
|
from fittrackee.emails.tasks import (
|
|
|
|
email_updated_to_new_address,
|
|
|
|
password_change_email,
|
|
|
|
reset_password_email,
|
|
|
|
)
|
2022-02-16 12:55:55 +01:00
|
|
|
from fittrackee.files import get_absolute_file_path
|
2022-05-27 15:51:40 +02:00
|
|
|
from fittrackee.oauth2.server import require_auth
|
2021-01-01 16:39:25 +01:00
|
|
|
from fittrackee.responses import (
|
|
|
|
ForbiddenErrorResponse,
|
2021-01-02 19:28:03 +01:00
|
|
|
HttpResponse,
|
2021-01-01 16:39:25 +01:00
|
|
|
InvalidPayloadErrorResponse,
|
|
|
|
NotFoundErrorResponse,
|
|
|
|
UserNotFoundErrorResponse,
|
|
|
|
handle_error_and_return_response,
|
|
|
|
)
|
2022-03-13 09:04:46 +01:00
|
|
|
from fittrackee.utils import get_readable_duration
|
2021-11-13 14:30:18 +01:00
|
|
|
from fittrackee.workouts.models import Record, Workout, WorkoutSegment
|
2017-12-16 21:00:46 +01:00
|
|
|
|
2022-04-23 13:48:29 +02:00
|
|
|
from .exceptions import InvalidEmailException, UserNotFoundException
|
2023-03-01 21:00:53 +01:00
|
|
|
from .models import User, UserDataExport, UserSportPreference
|
2022-04-23 13:48:29 +02:00
|
|
|
from .utils.admin import UserManagerService
|
2023-03-04 10:49:25 +01:00
|
|
|
from .utils.language import get_language
|
2017-12-16 21:00:46 +01:00
|
|
|
|
|
|
|
users_blueprint = Blueprint('users', __name__)
|
|
|
|
|
2020-05-02 18:00:17 +02:00
|
|
|
USER_PER_PAGE = 10
|
|
|
|
|
2017-12-16 21:00:46 +01:00
|
|
|
|
|
|
|
@users_blueprint.route('/users', methods=['GET'])
|
2022-06-15 19:16:14 +02:00
|
|
|
@require_auth(scopes=['users:read'], as_admin=True)
|
2021-12-01 19:22:47 +01:00
|
|
|
def get_users(auth_user: User) -> Dict:
|
2019-07-20 21:57:35 +02:00
|
|
|
"""
|
2022-03-26 20:30:37 +01:00
|
|
|
Get all users (regardless their account status), if authenticated user
|
2022-07-14 18:36:19 +02:00
|
|
|
has admin rights.
|
2022-03-26 20:30:37 +01:00
|
|
|
|
|
|
|
It returns user preferences only for authenticated user.
|
2019-07-20 21:57:35 +02:00
|
|
|
|
2022-07-14 18:36:19 +02:00
|
|
|
**Scope**: ``users:read``
|
|
|
|
|
2019-07-20 21:57:35 +02:00
|
|
|
**Example request**:
|
|
|
|
|
2023-06-18 20:45:39 +02:00
|
|
|
- without parameters:
|
2020-05-02 18:00:17 +02:00
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
2020-05-03 11:30:40 +02:00
|
|
|
GET /api/users HTTP/1.1
|
2020-05-02 18:00:17 +02:00
|
|
|
Content-Type: application/json
|
|
|
|
|
2023-06-18 20:45:39 +02:00
|
|
|
- with some query parameters:
|
2020-05-02 18:00:17 +02:00
|
|
|
|
2019-07-20 21:57:35 +02:00
|
|
|
.. sourcecode:: http
|
|
|
|
|
2021-01-10 11:16:43 +01:00
|
|
|
GET /api/users?order_by=workouts_count&par_page=5 HTTP/1.1
|
2019-07-20 21:57:35 +02:00
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
**Example response**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
HTTP/1.1 200 OK
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
{
|
|
|
|
"data": {
|
|
|
|
"users": [
|
|
|
|
{
|
|
|
|
"admin": true,
|
|
|
|
"bio": null,
|
|
|
|
"birth_date": null,
|
|
|
|
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
|
|
|
"email": "admin@example.com",
|
|
|
|
"first_name": null,
|
2022-03-19 22:02:06 +01:00
|
|
|
"is_admin": true,
|
2021-11-13 19:46:12 +01:00
|
|
|
"imperial_units": false,
|
2019-12-29 12:50:32 +01:00
|
|
|
"language": "en",
|
2019-07-20 21:57:35 +02:00
|
|
|
"last_name": null,
|
|
|
|
"location": null,
|
|
|
|
"nb_sports": 3,
|
2021-01-10 11:16:43 +01:00
|
|
|
"nb_workouts": 6,
|
2019-07-20 21:57:35 +02:00
|
|
|
"picture": false,
|
2021-09-21 18:10:27 +02:00
|
|
|
"records": [
|
|
|
|
{
|
|
|
|
"id": 9,
|
|
|
|
"record_type": "AS",
|
|
|
|
"sport_id": 1,
|
|
|
|
"user": "admin",
|
|
|
|
"value": 18,
|
|
|
|
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
|
|
|
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"id": 10,
|
|
|
|
"record_type": "FD",
|
|
|
|
"sport_id": 1,
|
|
|
|
"user": "admin",
|
|
|
|
"value": 18,
|
|
|
|
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
|
|
|
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
|
|
|
},
|
2022-07-27 11:10:29 +02:00
|
|
|
{
|
|
|
|
"id": 13,
|
|
|
|
"record_type": "HA",
|
|
|
|
"sport_id": 1,
|
|
|
|
"user": "Sam",
|
|
|
|
"value": 43.97,
|
|
|
|
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
|
|
|
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
|
|
|
},
|
2021-09-21 18:10:27 +02:00
|
|
|
{
|
|
|
|
"id": 11,
|
|
|
|
"record_type": "LD",
|
|
|
|
"sport_id": 1,
|
|
|
|
"user": "admin",
|
|
|
|
"value": "1:01:00",
|
|
|
|
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
|
|
|
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"id": 12,
|
|
|
|
"record_type": "MS",
|
|
|
|
"sport_id": 1,
|
|
|
|
"user": "admin",
|
|
|
|
"value": 18,
|
|
|
|
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
|
|
|
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
|
|
|
}
|
|
|
|
],
|
2019-09-23 20:01:11 +02:00
|
|
|
"sports_list": [
|
|
|
|
1,
|
|
|
|
4,
|
|
|
|
6
|
|
|
|
],
|
2019-07-20 21:57:35 +02:00
|
|
|
"timezone": "Europe/Paris",
|
|
|
|
"total_distance": 67.895,
|
|
|
|
"total_duration": "6:50:27",
|
2022-03-26 20:30:37 +01:00
|
|
|
"username": "admin",
|
|
|
|
"weekm": false
|
2019-07-20 21:57:35 +02:00
|
|
|
},
|
|
|
|
{
|
|
|
|
"admin": false,
|
|
|
|
"bio": null,
|
|
|
|
"birth_date": null,
|
|
|
|
"created_at": "Sat, 20 Jul 2019 11:27:03 GMT",
|
|
|
|
"email": "sam@example.com",
|
|
|
|
"first_name": null,
|
2022-03-19 22:02:06 +01:00
|
|
|
"is_admin": false,
|
2019-12-29 12:50:32 +01:00
|
|
|
"language": "fr",
|
2019-07-20 21:57:35 +02:00
|
|
|
"last_name": null,
|
|
|
|
"location": null,
|
|
|
|
"nb_sports": 0,
|
2021-01-10 11:16:43 +01:00
|
|
|
"nb_workouts": 0,
|
2019-07-20 21:57:35 +02:00
|
|
|
"picture": false,
|
2021-09-21 18:10:27 +02:00
|
|
|
"records": [],
|
2019-09-23 20:01:11 +02:00
|
|
|
"sports_list": [],
|
2019-07-20 21:57:35 +02:00
|
|
|
"timezone": "Europe/Paris",
|
|
|
|
"total_distance": 0,
|
|
|
|
"total_duration": "0:00:00",
|
|
|
|
"username": "sam"
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
"status": "success"
|
|
|
|
}
|
|
|
|
|
2020-05-02 18:00:17 +02:00
|
|
|
:query integer page: page if using pagination (default: 1)
|
|
|
|
:query integer per_page: number of users per page (default: 10, max: 50)
|
|
|
|
:query string q: query on user name
|
2022-10-26 20:27:12 +02:00
|
|
|
:query string order: sorting order: ``asc``, ``desc`` (default: ``asc``)
|
|
|
|
:query string order_by: sorting criteria: ``username``, ``created_at``,
|
|
|
|
``workouts_count``, ``admin``, ``is_active``
|
|
|
|
(default: ``username``)
|
2020-05-02 18:00:17 +02:00
|
|
|
|
2019-07-20 21:57:35 +02:00
|
|
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
|
|
|
|
2023-06-18 20:45:39 +02:00
|
|
|
:statuscode 200: ``success``
|
2019-07-20 21:57:35 +02:00
|
|
|
:statuscode 401:
|
2023-06-18 20:45:39 +02:00
|
|
|
- ``provide a valid auth token``
|
|
|
|
- ``signature expired, please log in again``
|
|
|
|
- ``invalid token, please log in again``
|
2019-07-20 21:57:35 +02:00
|
|
|
|
|
|
|
"""
|
2020-05-02 18:00:17 +02:00
|
|
|
params = request.args.copy()
|
2021-05-22 17:14:24 +02:00
|
|
|
page = int(params.get('page', 1))
|
|
|
|
per_page = int(params.get('per_page', USER_PER_PAGE))
|
2020-05-02 18:00:17 +02:00
|
|
|
if per_page > 50:
|
|
|
|
per_page = 50
|
2022-10-26 18:05:22 +02:00
|
|
|
user_column = getattr(User, params.get('order_by', 'username'))
|
2020-05-02 18:00:17 +02:00
|
|
|
order = params.get('order', 'asc')
|
|
|
|
query = params.get('q')
|
|
|
|
users_pagination = (
|
|
|
|
User.query.filter(
|
2022-03-13 10:03:24 +01:00
|
|
|
User.username.ilike('%' + query + '%') if query else True,
|
2020-05-02 18:00:17 +02:00
|
|
|
)
|
2022-10-26 18:05:22 +02:00
|
|
|
.order_by(asc(user_column) if order == 'asc' else desc(user_column))
|
2022-11-26 12:54:56 +01:00
|
|
|
.paginate(page=page, per_page=per_page, error_out=False)
|
2020-05-02 18:00:17 +02:00
|
|
|
)
|
|
|
|
users = users_pagination.items
|
2021-01-01 16:39:25 +01:00
|
|
|
return {
|
2017-12-16 21:00:46 +01:00
|
|
|
'status': 'success',
|
2022-03-13 09:30:50 +01:00
|
|
|
'data': {'users': [user.serialize(auth_user) for user in users]},
|
2020-05-02 18:00:17 +02:00
|
|
|
'pagination': {
|
|
|
|
'has_next': users_pagination.has_next,
|
|
|
|
'has_prev': users_pagination.has_prev,
|
|
|
|
'page': users_pagination.page,
|
|
|
|
'pages': users_pagination.pages,
|
|
|
|
'total': users_pagination.total,
|
|
|
|
},
|
2017-12-16 21:00:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-02-08 13:09:01 +01:00
|
|
|
@users_blueprint.route('/users/<user_name>', methods=['GET'])
|
2022-06-15 19:16:14 +02:00
|
|
|
@require_auth(scopes=['users:read'])
|
2021-01-02 19:28:03 +01:00
|
|
|
def get_single_user(
|
2021-12-01 19:22:47 +01:00
|
|
|
auth_user: User, user_name: str
|
2021-01-02 19:28:03 +01:00
|
|
|
) -> Union[Dict, HttpResponse]:
|
2019-07-20 21:57:35 +02:00
|
|
|
"""
|
2022-03-30 11:30:26 +02:00
|
|
|
Get single user details. Only user with admin rights can get other users
|
|
|
|
details.
|
2022-03-26 20:30:37 +01:00
|
|
|
|
|
|
|
It returns user preferences only for authenticated user.
|
2019-07-20 21:57:35 +02:00
|
|
|
|
2022-07-14 18:36:19 +02:00
|
|
|
**Scope**: ``users:read``
|
|
|
|
|
2019-07-20 21:57:35 +02:00
|
|
|
**Example request**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
2020-02-08 14:49:37 +01:00
|
|
|
GET /api/users/admin HTTP/1.1
|
2019-07-20 21:57:35 +02:00
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
**Example response**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
HTTP/1.1 200 OK
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
{
|
2020-02-08 12:36:03 +01:00
|
|
|
"data": [
|
|
|
|
{
|
|
|
|
"admin": true,
|
|
|
|
"bio": null,
|
|
|
|
"birth_date": null,
|
|
|
|
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
|
|
|
"email": "admin@example.com",
|
|
|
|
"first_name": null,
|
2021-11-13 19:46:12 +01:00
|
|
|
"imperial_units": false,
|
2022-03-19 22:02:06 +01:00
|
|
|
"is_admin": true,
|
2020-02-08 12:36:03 +01:00
|
|
|
"language": "en",
|
|
|
|
"last_name": null,
|
|
|
|
"location": null,
|
|
|
|
"nb_sports": 3,
|
2021-01-10 11:16:43 +01:00
|
|
|
"nb_workouts": 6,
|
2020-02-08 12:36:03 +01:00
|
|
|
"picture": false,
|
2021-09-21 18:10:27 +02:00
|
|
|
"records": [
|
|
|
|
{
|
|
|
|
"id": 9,
|
|
|
|
"record_type": "AS",
|
|
|
|
"sport_id": 1,
|
|
|
|
"user": "admin",
|
|
|
|
"value": 18,
|
|
|
|
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
|
|
|
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"id": 10,
|
|
|
|
"record_type": "FD",
|
|
|
|
"sport_id": 1,
|
|
|
|
"user": "admin",
|
|
|
|
"value": 18,
|
|
|
|
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
|
|
|
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
|
|
|
},
|
2022-07-27 11:10:29 +02:00
|
|
|
{
|
|
|
|
"id": 13,
|
|
|
|
"record_type": "HA",
|
|
|
|
"sport_id": 1,
|
|
|
|
"user": "Sam",
|
|
|
|
"value": 43.97,
|
|
|
|
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
|
|
|
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
|
|
|
},
|
2021-09-21 18:10:27 +02:00
|
|
|
{
|
|
|
|
"id": 11,
|
|
|
|
"record_type": "LD",
|
|
|
|
"sport_id": 1,
|
|
|
|
"user": "admin",
|
|
|
|
"value": "1:01:00",
|
|
|
|
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
|
|
|
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"id": 12,
|
|
|
|
"record_type": "MS",
|
|
|
|
"sport_id": 1,
|
|
|
|
"user": "admin",
|
|
|
|
"value": 18,
|
|
|
|
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
|
|
|
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
|
|
|
}
|
|
|
|
],
|
2020-02-08 12:36:03 +01:00
|
|
|
"sports_list": [
|
|
|
|
1,
|
|
|
|
4,
|
|
|
|
6
|
|
|
|
],
|
|
|
|
"timezone": "Europe/Paris",
|
|
|
|
"total_distance": 67.895,
|
|
|
|
"total_duration": "6:50:27",
|
|
|
|
"username": "admin"
|
|
|
|
}
|
|
|
|
],
|
2019-07-20 21:57:35 +02:00
|
|
|
"status": "success"
|
|
|
|
}
|
|
|
|
|
2020-02-08 13:09:01 +01:00
|
|
|
:param integer user_name: user name
|
2019-07-20 21:57:35 +02:00
|
|
|
|
|
|
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
|
|
|
|
2023-06-18 20:45:39 +02:00
|
|
|
:statuscode 200: ``success``
|
2019-07-20 21:57:35 +02:00
|
|
|
:statuscode 401:
|
2023-06-18 20:45:39 +02:00
|
|
|
- ``provide a valid auth token``
|
|
|
|
- ``signature expired, please log in again``
|
|
|
|
- ``invalid token, please log in again``
|
2019-07-20 21:57:35 +02:00
|
|
|
:statuscode 404:
|
2023-06-18 20:45:39 +02:00
|
|
|
- ``user does not exist``
|
2019-07-20 21:57:35 +02:00
|
|
|
"""
|
2022-03-30 11:30:26 +02:00
|
|
|
if user_name != auth_user.username and not auth_user.admin:
|
|
|
|
return ForbiddenErrorResponse()
|
|
|
|
|
2017-12-16 21:00:46 +01:00
|
|
|
try:
|
2020-02-08 13:09:01 +01:00
|
|
|
user = User.query.filter_by(username=user_name).first()
|
2021-01-01 16:39:25 +01:00
|
|
|
if user:
|
|
|
|
return {
|
2020-02-08 12:36:03 +01:00
|
|
|
'status': 'success',
|
2022-03-13 09:30:50 +01:00
|
|
|
'data': {'users': [user.serialize(auth_user)]},
|
2020-02-08 12:36:03 +01:00
|
|
|
}
|
2017-12-16 21:00:46 +01:00
|
|
|
except ValueError:
|
2021-01-01 16:39:25 +01:00
|
|
|
pass
|
|
|
|
return UserNotFoundErrorResponse()
|
2017-12-16 21:00:46 +01:00
|
|
|
|
|
|
|
|
2020-02-08 14:49:37 +01:00
|
|
|
@users_blueprint.route('/users/<user_name>/picture', methods=['GET'])
|
2022-09-17 19:36:03 +02:00
|
|
|
@limiter.exempt
|
2021-01-02 19:28:03 +01:00
|
|
|
def get_picture(user_name: str) -> Any:
|
2020-09-16 11:09:32 +02:00
|
|
|
"""get user picture
|
2019-07-20 21:57:35 +02:00
|
|
|
|
|
|
|
**Example request**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
2020-02-08 14:49:37 +01:00
|
|
|
GET /api/users/admin/picture HTTP/1.1
|
2019-07-20 21:57:35 +02:00
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
**Example response**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
HTTP/1.1 200 OK
|
|
|
|
Content-Type: image/jpeg
|
|
|
|
|
2020-02-08 14:49:37 +01:00
|
|
|
:param integer user_name: user name
|
2019-07-20 21:57:35 +02:00
|
|
|
|
2023-06-18 20:45:39 +02:00
|
|
|
:statuscode 200: ``success``
|
2019-07-20 21:57:35 +02:00
|
|
|
:statuscode 404:
|
2023-06-18 20:45:39 +02:00
|
|
|
- ``user does not exist``
|
|
|
|
- ``No picture.``
|
2019-07-20 21:57:35 +02:00
|
|
|
|
|
|
|
"""
|
2018-01-01 21:54:03 +01:00
|
|
|
try:
|
2020-02-08 14:49:37 +01:00
|
|
|
user = User.query.filter_by(username=user_name).first()
|
2018-01-01 21:54:03 +01:00
|
|
|
if not user:
|
2021-01-01 16:39:25 +01:00
|
|
|
return UserNotFoundErrorResponse()
|
2019-07-20 21:57:35 +02:00
|
|
|
if user.picture is not None:
|
2018-07-04 14:13:19 +02:00
|
|
|
picture_path = get_absolute_file_path(user.picture)
|
|
|
|
return send_file(picture_path)
|
2022-05-28 20:01:14 +02:00
|
|
|
except Exception: # nosec
|
2021-01-01 16:39:25 +01:00
|
|
|
pass
|
|
|
|
return NotFoundErrorResponse('No picture.')
|
2018-01-01 21:54:03 +01:00
|
|
|
|
|
|
|
|
2020-05-01 12:12:48 +02:00
|
|
|
@users_blueprint.route('/users/<user_name>', methods=['PATCH'])
|
2022-06-15 19:16:14 +02:00
|
|
|
@require_auth(scopes=['users:write'], as_admin=True)
|
2021-12-01 19:22:47 +01:00
|
|
|
def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
|
2020-05-01 12:12:48 +02:00
|
|
|
"""
|
2022-07-14 18:36:19 +02:00
|
|
|
Update user account.
|
2022-03-26 20:30:37 +01:00
|
|
|
|
|
|
|
- add/remove admin rights (regardless user account status)
|
2022-04-23 18:04:20 +02:00
|
|
|
- reset password (and send email to update user password,
|
|
|
|
if sending enabled)
|
|
|
|
- update user email (and send email to new user email, if sending enabled)
|
2022-03-20 18:29:18 +01:00
|
|
|
- activate account for an inactive user
|
2020-05-02 18:00:17 +02:00
|
|
|
|
2022-07-14 18:36:19 +02:00
|
|
|
Only user with admin rights can modify another user.
|
|
|
|
|
|
|
|
**Scope**: ``users:write``
|
2020-05-01 12:12:48 +02:00
|
|
|
|
|
|
|
**Example request**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
2022-03-26 20:30:37 +01:00
|
|
|
PATCH /api/users/<user_name> HTTP/1.1
|
2020-05-01 12:12:48 +02:00
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
**Example response**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
HTTP/1.1 200 OK
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
{
|
|
|
|
"data": [
|
|
|
|
{
|
|
|
|
"admin": true,
|
|
|
|
"bio": null,
|
|
|
|
"birth_date": null,
|
|
|
|
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
|
|
|
"email": "admin@example.com",
|
|
|
|
"first_name": null,
|
2021-11-13 19:46:12 +01:00
|
|
|
"imperial_units": false,
|
2022-03-19 22:02:06 +01:00
|
|
|
"is_active": true,
|
2020-05-01 12:12:48 +02:00
|
|
|
"language": "en",
|
|
|
|
"last_name": null,
|
|
|
|
"location": null,
|
2021-01-10 11:16:43 +01:00
|
|
|
"nb_workouts": 6,
|
2020-05-01 12:12:48 +02:00
|
|
|
"nb_sports": 3,
|
|
|
|
"picture": false,
|
2021-09-21 18:10:27 +02:00
|
|
|
"records": [
|
|
|
|
{
|
|
|
|
"id": 9,
|
|
|
|
"record_type": "AS",
|
|
|
|
"sport_id": 1,
|
|
|
|
"user": "admin",
|
|
|
|
"value": 18,
|
|
|
|
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
|
|
|
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"id": 10,
|
|
|
|
"record_type": "FD",
|
|
|
|
"sport_id": 1,
|
|
|
|
"user": "admin",
|
|
|
|
"value": 18,
|
|
|
|
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
|
|
|
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
|
|
|
},
|
2022-07-27 11:10:29 +02:00
|
|
|
{
|
|
|
|
"id": 13,
|
|
|
|
"record_type": "HA",
|
|
|
|
"sport_id": 1,
|
|
|
|
"user": "Sam",
|
|
|
|
"value": 43.97,
|
|
|
|
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
|
|
|
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
|
|
|
},
|
2021-09-21 18:10:27 +02:00
|
|
|
{
|
|
|
|
"id": 11,
|
|
|
|
"record_type": "LD",
|
|
|
|
"sport_id": 1,
|
|
|
|
"user": "admin",
|
|
|
|
"value": "1:01:00",
|
|
|
|
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
|
|
|
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"id": 12,
|
|
|
|
"record_type": "MS",
|
|
|
|
"sport_id": 1,
|
|
|
|
"user": "admin",
|
|
|
|
"value": 18,
|
|
|
|
"workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
|
|
|
"workout_id": "hvYBqYBRa7wwXpaStWR4V2"
|
|
|
|
}
|
|
|
|
],
|
2020-05-01 12:12:48 +02:00
|
|
|
"sports_list": [
|
|
|
|
1,
|
|
|
|
4,
|
|
|
|
6
|
|
|
|
],
|
|
|
|
"timezone": "Europe/Paris",
|
|
|
|
"total_distance": 67.895,
|
|
|
|
"total_duration": "6:50:27",
|
|
|
|
"username": "admin"
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"status": "success"
|
|
|
|
}
|
|
|
|
|
2020-05-01 16:18:59 +02:00
|
|
|
:param string user_name: user name
|
2020-05-01 12:12:48 +02:00
|
|
|
|
2022-03-20 18:29:18 +01:00
|
|
|
:<json boolean activate: activate user account
|
2020-05-01 12:12:48 +02:00
|
|
|
:<json boolean admin: does the user have administrator rights
|
2022-03-20 18:29:18 +01:00
|
|
|
:<json boolean new_email: new user email
|
|
|
|
:<json boolean reset_password: reset user password
|
2020-05-01 12:12:48 +02:00
|
|
|
|
|
|
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
|
|
|
|
2023-06-18 20:45:39 +02:00
|
|
|
:statuscode 200: ``success``
|
2022-03-20 18:29:18 +01:00
|
|
|
:statuscode 400:
|
2023-06-18 20:45:39 +02:00
|
|
|
- ``invalid payload``
|
|
|
|
- ``valid email must be provided``
|
2023-08-06 23:31:42 +02:00
|
|
|
- ``new email must be different than current email``
|
2020-05-01 12:12:48 +02:00
|
|
|
:statuscode 401:
|
2023-06-18 20:45:39 +02:00
|
|
|
- ``provide a valid auth token``
|
|
|
|
- ``signature expired, please log in again``
|
|
|
|
- ``invalid token, please log in again``
|
|
|
|
:statuscode 403: ``you do not have permissions``
|
|
|
|
:statuscode 404: ``user does not exist``
|
|
|
|
:statuscode 500: ``error, please try again or contact the administrator``
|
2020-05-01 12:12:48 +02:00
|
|
|
"""
|
|
|
|
user_data = request.get_json()
|
2022-03-13 09:04:46 +01:00
|
|
|
if not user_data:
|
2021-01-01 16:39:25 +01:00
|
|
|
return InvalidPayloadErrorResponse()
|
2020-05-01 12:12:48 +02:00
|
|
|
|
|
|
|
try:
|
2022-04-23 13:48:29 +02:00
|
|
|
reset_password = user_data.get('reset_password', False)
|
|
|
|
new_email = user_data.get('new_email')
|
|
|
|
user_manager_service = UserManagerService(username=user_name)
|
|
|
|
user, _, _ = user_manager_service.update(
|
|
|
|
is_admin=user_data.get('admin'),
|
|
|
|
activate=user_data.get('activate', False),
|
|
|
|
reset_password=reset_password,
|
|
|
|
new_email=new_email,
|
2022-04-24 10:08:15 +02:00
|
|
|
with_confirmation=current_app.config['CAN_SEND_EMAILS'],
|
2022-04-23 13:48:29 +02:00
|
|
|
)
|
2022-03-13 09:04:46 +01:00
|
|
|
|
2022-04-23 18:04:20 +02:00
|
|
|
if current_app.config['CAN_SEND_EMAILS']:
|
2022-07-03 13:29:50 +02:00
|
|
|
user_language = get_language(user.language)
|
2022-04-23 18:04:20 +02:00
|
|
|
ui_url = current_app.config['UI_URL']
|
|
|
|
if reset_password:
|
|
|
|
user_data = {
|
|
|
|
'language': user_language,
|
|
|
|
'email': user.email,
|
|
|
|
}
|
|
|
|
password_change_email.send(
|
|
|
|
user_data,
|
|
|
|
{
|
|
|
|
'username': user.username,
|
|
|
|
'fittrackee_url': ui_url,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
password_reset_token = user.encode_password_reset_token(
|
|
|
|
user.id
|
|
|
|
)
|
|
|
|
reset_password_email.send(
|
|
|
|
user_data,
|
|
|
|
{
|
|
|
|
'expiration_delay': get_readable_duration(
|
|
|
|
current_app.config[
|
|
|
|
'PASSWORD_TOKEN_EXPIRATION_SECONDS'
|
|
|
|
],
|
|
|
|
user_language,
|
|
|
|
),
|
|
|
|
'username': user.username,
|
|
|
|
'password_reset_url': (
|
|
|
|
f'{ui_url}/password-reset?'
|
|
|
|
f'token={password_reset_token}'
|
|
|
|
),
|
|
|
|
'fittrackee_url': ui_url,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
if new_email:
|
|
|
|
user_data = {
|
|
|
|
'language': user_language,
|
|
|
|
'email': user.email_to_confirm,
|
|
|
|
}
|
|
|
|
email_data = {
|
2022-03-13 09:04:46 +01:00
|
|
|
'username': user.username,
|
|
|
|
'fittrackee_url': ui_url,
|
2022-04-23 18:04:20 +02:00
|
|
|
'email_confirmation_url': (
|
|
|
|
f'{ui_url}/email-update'
|
|
|
|
f'?token={user.confirmation_token}'
|
2022-03-13 09:04:46 +01:00
|
|
|
),
|
2022-04-23 18:04:20 +02:00
|
|
|
}
|
|
|
|
email_updated_to_new_address.send(user_data, email_data)
|
2022-03-13 09:06:57 +01:00
|
|
|
|
2021-01-01 16:39:25 +01:00
|
|
|
return {
|
|
|
|
'status': 'success',
|
2022-03-13 09:30:50 +01:00
|
|
|
'data': {'users': [user.serialize(auth_user)]},
|
2020-05-01 12:12:48 +02:00
|
|
|
}
|
2022-04-23 13:48:29 +02:00
|
|
|
except UserNotFoundException:
|
|
|
|
return UserNotFoundErrorResponse()
|
|
|
|
except InvalidEmailException as e:
|
|
|
|
return InvalidPayloadErrorResponse(str(e))
|
2021-01-01 16:39:25 +01:00
|
|
|
except exc.StatementError as e:
|
|
|
|
return handle_error_and_return_response(e, db=db)
|
2020-05-01 12:12:48 +02:00
|
|
|
|
|
|
|
|
2020-05-01 16:18:59 +02:00
|
|
|
@users_blueprint.route('/users/<user_name>', methods=['DELETE'])
|
2022-06-15 19:16:14 +02:00
|
|
|
@require_auth(scopes=['users:write'])
|
2021-01-02 19:28:03 +01:00
|
|
|
def delete_user(
|
2021-12-01 19:22:47 +01:00
|
|
|
auth_user: User, user_name: str
|
2021-01-02 19:28:03 +01:00
|
|
|
) -> Union[Tuple[Dict, int], HttpResponse]:
|
2020-05-01 16:18:59 +02:00
|
|
|
"""
|
2022-07-14 18:36:19 +02:00
|
|
|
Delete a user account.
|
2020-05-02 18:00:17 +02:00
|
|
|
|
2022-07-14 18:36:19 +02:00
|
|
|
A user can only delete his own account.
|
2020-05-02 18:00:17 +02:00
|
|
|
|
|
|
|
An admin can delete all accounts except his account if he's the only
|
2022-07-14 18:36:19 +02:00
|
|
|
one admin.
|
|
|
|
|
|
|
|
**Scope**: ``users:write``
|
2020-05-01 16:18:59 +02:00
|
|
|
|
|
|
|
**Example request**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
DELETE /api/users/john_doe HTTP/1.1
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
**Example response**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
HTTP/1.1 204 NO CONTENT
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
:param string user_name: user name
|
|
|
|
|
|
|
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
|
|
|
|
|
|
|
:statuscode 204: user account deleted
|
|
|
|
:statuscode 401:
|
2023-06-18 20:45:39 +02:00
|
|
|
- ``provide a valid auth token``
|
|
|
|
- ``signature expired, please log in again``
|
|
|
|
- ``invalid token, please log in again``
|
2020-05-01 16:18:59 +02:00
|
|
|
:statuscode 403:
|
2023-06-18 20:45:39 +02:00
|
|
|
- ``you do not have permissions``
|
|
|
|
- ``you can not delete your account, no other user has admin rights``
|
|
|
|
:statuscode 404: ``user does not exist``
|
|
|
|
:statuscode 500: ``error, please try again or contact the administrator``
|
2020-05-01 16:18:59 +02:00
|
|
|
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
user = User.query.filter_by(username=user_name).first()
|
2021-01-01 16:39:25 +01:00
|
|
|
if not user:
|
|
|
|
return UserNotFoundErrorResponse()
|
|
|
|
|
2021-12-01 19:22:47 +01:00
|
|
|
if user.id != auth_user.id and not auth_user.admin:
|
2021-01-01 16:39:25 +01:00
|
|
|
return ForbiddenErrorResponse()
|
|
|
|
if (
|
|
|
|
user.admin is True
|
|
|
|
and User.query.filter_by(admin=True).count() == 1
|
|
|
|
):
|
|
|
|
return ForbiddenErrorResponse(
|
2021-11-03 09:48:24 +01:00
|
|
|
'you can not delete your account, '
|
|
|
|
'no other user has admin rights'
|
2020-05-01 16:18:59 +02:00
|
|
|
)
|
2021-01-01 16:39:25 +01:00
|
|
|
|
2021-11-13 14:30:18 +01:00
|
|
|
db.session.query(UserSportPreference).filter(
|
|
|
|
UserSportPreference.user_id == user.id
|
|
|
|
).delete()
|
|
|
|
db.session.query(Record).filter(Record.user_id == user.id).delete()
|
|
|
|
db.session.query(WorkoutSegment).filter(
|
|
|
|
WorkoutSegment.workout_id == Workout.id, Workout.user_id == user.id
|
|
|
|
).delete(synchronize_session=False)
|
|
|
|
db.session.query(Workout).filter(Workout.user_id == user.id).delete()
|
2023-03-01 21:00:53 +01:00
|
|
|
db.session.query(UserDataExport).filter(
|
|
|
|
UserDataExport.user_id == user.id
|
|
|
|
).delete()
|
2021-11-13 14:30:18 +01:00
|
|
|
db.session.flush()
|
2021-01-01 16:39:25 +01:00
|
|
|
user_picture = user.picture
|
|
|
|
db.session.delete(user)
|
|
|
|
db.session.commit()
|
|
|
|
if user_picture:
|
|
|
|
picture_path = get_absolute_file_path(user.picture)
|
|
|
|
if os.path.isfile(picture_path):
|
|
|
|
os.remove(picture_path)
|
2023-03-01 21:00:53 +01:00
|
|
|
shutil.rmtree(
|
|
|
|
get_absolute_file_path(f'exports/{user.id}'),
|
|
|
|
ignore_errors=True,
|
|
|
|
)
|
2021-01-01 16:39:25 +01:00
|
|
|
shutil.rmtree(
|
2021-01-10 11:16:43 +01:00
|
|
|
get_absolute_file_path(f'workouts/{user.id}'),
|
2021-01-01 16:39:25 +01:00
|
|
|
ignore_errors=True,
|
|
|
|
)
|
|
|
|
shutil.rmtree(
|
|
|
|
get_absolute_file_path(f'pictures/{user.id}'),
|
|
|
|
ignore_errors=True,
|
|
|
|
)
|
|
|
|
return {'status': 'no content'}, 204
|
2020-05-01 16:18:59 +02:00
|
|
|
except (
|
|
|
|
exc.IntegrityError,
|
|
|
|
exc.OperationalError,
|
|
|
|
ValueError,
|
|
|
|
OSError,
|
|
|
|
) as e:
|
2021-01-01 16:39:25 +01:00
|
|
|
return handle_error_and_return_response(e, db=db)
|