2020-05-01 16:18:59 +02:00
|
|
|
import os
|
2022-03-13 09:04:46 +01:00
|
|
|
import random
|
2022-03-13 09:06:57 +01:00
|
|
|
import secrets
|
2020-05-01 16:18:59 +02:00
|
|
|
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-02-13 14:36:10 +01:00
|
|
|
import click
|
2022-03-13 09:04:46 +01:00
|
|
|
from flask import Blueprint, current_app, request, send_file
|
2021-01-20 16:47:00 +01:00
|
|
|
from sqlalchemy import exc
|
|
|
|
|
2022-03-13 09:04:46 +01:00
|
|
|
from fittrackee import bcrypt, db
|
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
|
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:06:57 +01:00
|
|
|
from fittrackee.users.utils.controls import is_valid_email
|
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
|
|
|
|
2021-01-20 16:24:01 +01:00
|
|
|
from .decorators import authenticate, authenticate_as_admin
|
2022-02-13 14:36:10 +01:00
|
|
|
from .exceptions import UserNotFoundException
|
2021-11-13 14:30:18 +01:00
|
|
|
from .models import User, UserSportPreference
|
2022-02-16 18:07:05 +01:00
|
|
|
from .utils.admin import set_admin_rights
|
2022-03-13 09:04:46 +01:00
|
|
|
from .utils.random import random_string
|
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
|
|
|
|
2022-02-13 14:36:10 +01:00
|
|
|
@users_blueprint.cli.command('set-admin')
|
|
|
|
@click.argument('username')
|
|
|
|
def set_admin(username: str) -> None:
|
|
|
|
"""Set admin rights for given user"""
|
|
|
|
try:
|
|
|
|
set_admin_rights(username)
|
|
|
|
print(f"User '{username}' updated.")
|
|
|
|
except UserNotFoundException:
|
|
|
|
print(f"User '{username}' not found.")
|
|
|
|
|
|
|
|
|
2017-12-16 21:00:46 +01:00
|
|
|
@users_blueprint.route('/users', methods=['GET'])
|
2022-03-13 09:30:50 +01:00
|
|
|
@authenticate_as_admin
|
2021-12-01 19:22:47 +01:00
|
|
|
def get_users(auth_user: User) -> Dict:
|
2019-07-20 21:57:35 +02:00
|
|
|
"""
|
|
|
|
Get all users
|
|
|
|
|
|
|
|
**Example request**:
|
|
|
|
|
2020-05-02 18:00:17 +02:00
|
|
|
- without parameters
|
|
|
|
|
|
|
|
.. 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
|
|
|
|
|
|
|
|
- with some query parameters
|
|
|
|
|
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,
|
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"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"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",
|
|
|
|
"username": "admin"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"admin": false,
|
|
|
|
"bio": null,
|
|
|
|
"birth_date": null,
|
|
|
|
"created_at": "Sat, 20 Jul 2019 11:27:03 GMT",
|
|
|
|
"email": "sam@example.com",
|
|
|
|
"first_name": null,
|
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
|
|
|
|
:query string order_by: sorting criteria (``username``, ``created_at``,
|
2021-01-10 11:16:43 +01:00
|
|
|
``workouts_count``, ``admin``)
|
2020-05-02 18:00:17 +02:00
|
|
|
:query string order: sorting order (default: ``asc``)
|
|
|
|
|
2019-07-20 21:57:35 +02:00
|
|
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
|
|
|
|
|
|
|
:statuscode 200: success
|
|
|
|
:statuscode 401:
|
2021-11-01 09:44:10 +01: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
|
|
|
|
order_by = params.get('order_by')
|
|
|
|
order = params.get('order', 'asc')
|
|
|
|
query = params.get('q')
|
|
|
|
users_pagination = (
|
|
|
|
User.query.filter(
|
|
|
|
User.username.like('%' + query + '%') if query else True,
|
|
|
|
)
|
|
|
|
.order_by(
|
2021-01-10 11:16:43 +01:00
|
|
|
User.workouts_count.asc() # type: ignore
|
|
|
|
if order_by == 'workouts_count' and order == 'asc'
|
2020-05-02 18:00:17 +02:00
|
|
|
else True,
|
2021-01-10 11:16:43 +01:00
|
|
|
User.workouts_count.desc() # type: ignore
|
|
|
|
if order_by == 'workouts_count' and order == 'desc'
|
2020-05-02 18:00:17 +02:00
|
|
|
else True,
|
|
|
|
User.username.asc()
|
|
|
|
if order_by == 'username' and order == 'asc'
|
|
|
|
else True,
|
|
|
|
User.username.desc()
|
|
|
|
if order_by == 'username' and order == 'desc'
|
|
|
|
else True,
|
|
|
|
User.created_at.asc()
|
|
|
|
if order_by == 'created_at' and order == 'asc'
|
|
|
|
else True,
|
|
|
|
User.created_at.desc()
|
|
|
|
if order_by == 'created_at' and order == 'desc'
|
|
|
|
else True,
|
|
|
|
User.admin.asc()
|
|
|
|
if order_by == 'admin' and order == 'asc'
|
|
|
|
else True,
|
|
|
|
User.admin.desc()
|
|
|
|
if order_by == 'admin' and order == 'desc'
|
|
|
|
else True,
|
|
|
|
)
|
|
|
|
.paginate(page, per_page, False)
|
|
|
|
)
|
|
|
|
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-03-13 09:30:50 +01:00
|
|
|
@authenticate_as_admin
|
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
|
|
|
"""
|
|
|
|
Get single user details
|
|
|
|
|
|
|
|
**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,
|
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"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"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
|
|
|
|
|
|
|
|
:statuscode 200: success
|
|
|
|
:statuscode 401:
|
2021-11-01 09:44:10 +01: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:
|
2021-11-01 09:44:10 +01:00
|
|
|
- user does not exist
|
2019-07-20 21:57:35 +02:00
|
|
|
"""
|
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'])
|
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
|
|
|
|
|
|
|
:statuscode 200: success
|
|
|
|
:statuscode 404:
|
2021-11-01 09:44:10 +01:00
|
|
|
- user does not exist
|
2019-07-20 21:57:35 +02:00
|
|
|
- No picture.
|
|
|
|
|
|
|
|
"""
|
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)
|
2019-07-20 21:57:35 +02:00
|
|
|
except Exception:
|
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'])
|
|
|
|
@authenticate_as_admin
|
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
|
|
|
"""
|
|
|
|
Update user to add admin rights
|
2020-05-02 18:00:17 +02:00
|
|
|
|
2020-05-01 12:12:48 +02:00
|
|
|
Only user with admin rights can modify another user
|
|
|
|
|
|
|
|
**Example request**:
|
|
|
|
|
|
|
|
.. sourcecode:: http
|
|
|
|
|
|
|
|
PATCH api/users/<user_name> HTTP/1.1
|
|
|
|
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,
|
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"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"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
|
|
|
|
|
|
|
:<json boolean admin: does the user have administrator rights
|
|
|
|
|
|
|
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
|
|
|
|
|
|
|
:statuscode 200: success
|
|
|
|
:statuscode 401:
|
2021-11-01 09:44:10 +01: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
|
2020-05-01 12:12:48 +02:00
|
|
|
:statuscode 404:
|
2021-11-01 09:44:10 +01:00
|
|
|
- user does not exist
|
2020-05-01 12:12:48 +02:00
|
|
|
:statuscode 500:
|
|
|
|
"""
|
|
|
|
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
|
|
|
|
2022-03-13 09:06:57 +01:00
|
|
|
send_password_emails = False
|
|
|
|
send_new_address_email = False
|
2020-05-01 12:12:48 +02:00
|
|
|
try:
|
|
|
|
user = User.query.filter_by(username=user_name).first()
|
|
|
|
if not user:
|
2021-01-01 16:39:25 +01:00
|
|
|
return UserNotFoundErrorResponse()
|
2020-05-01 12:12:48 +02:00
|
|
|
|
2022-03-13 09:04:46 +01:00
|
|
|
if 'admin' in user_data:
|
|
|
|
user.admin = user_data['admin']
|
|
|
|
|
|
|
|
if (
|
|
|
|
'reset_password' in user_data
|
|
|
|
and user_data['reset_password'] is True
|
|
|
|
):
|
|
|
|
new_password = random_string(length=random.randint(10, 20))
|
|
|
|
user.password = bcrypt.generate_password_hash(
|
|
|
|
new_password, current_app.config.get('BCRYPT_LOG_ROUNDS')
|
|
|
|
).decode()
|
2022-03-13 09:06:57 +01:00
|
|
|
send_password_emails = True
|
|
|
|
|
|
|
|
if 'new_email' in user_data:
|
|
|
|
if is_valid_email(user_data['new_email']):
|
|
|
|
user.email_to_confirm = user_data['new_email']
|
|
|
|
user.confirmation_token = secrets.token_urlsafe(16)
|
|
|
|
send_new_address_email = True
|
|
|
|
else:
|
|
|
|
return InvalidPayloadErrorResponse(
|
|
|
|
'valid email must be provided'
|
|
|
|
)
|
2022-03-13 09:04:46 +01:00
|
|
|
|
2021-01-01 16:39:25 +01:00
|
|
|
db.session.commit()
|
2022-03-13 09:04:46 +01:00
|
|
|
|
2022-03-13 09:06:57 +01:00
|
|
|
user_language = 'en' if user.language is None else user.language
|
|
|
|
ui_url = current_app.config['UI_URL']
|
|
|
|
if send_password_emails:
|
2022-03-13 09:04:46 +01:00
|
|
|
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?token={password_reset_token}'
|
|
|
|
),
|
|
|
|
'fittrackee_url': ui_url,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
2022-03-13 09:06:57 +01:00
|
|
|
if send_new_address_email:
|
|
|
|
user_data = {
|
|
|
|
'language': user_language,
|
|
|
|
'email': user.email_to_confirm,
|
|
|
|
}
|
|
|
|
email_data = {
|
|
|
|
'username': user.username,
|
|
|
|
'fittrackee_url': ui_url,
|
|
|
|
'email_confirmation_url': (
|
|
|
|
f'{ui_url}/email-update'
|
|
|
|
f'?token={user.confirmation_token}'
|
|
|
|
),
|
|
|
|
}
|
|
|
|
email_updated_to_new_address.send(user_data, email_data)
|
|
|
|
|
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
|
|
|
}
|
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'])
|
|
|
|
@authenticate
|
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
|
|
|
"""
|
|
|
|
Delete a user account
|
2020-05-02 18:00:17 +02:00
|
|
|
|
|
|
|
A user can only delete his own account
|
|
|
|
|
|
|
|
An admin can delete all accounts except his account if he's the only
|
|
|
|
one admin
|
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:
|
2021-11-01 09:44:10 +01: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:
|
2021-11-01 09:44:10 +01:00
|
|
|
- you do not have permissions
|
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
|
|
|
:statuscode 404:
|
2021-11-01 09:44:10 +01:00
|
|
|
- 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()
|
|
|
|
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)
|
|
|
|
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)
|