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
|
|
|
|
2021-01-01 16:39:25 +01:00
|
|
|
from fittrackee import db
|
|
|
|
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,
|
|
|
|
)
|
|
|
|
from flask import Blueprint, request, send_file
|
2020-05-01 12:12:48 +02:00
|
|
|
from sqlalchemy import exc
|
2017-12-16 21:00:46 +01:00
|
|
|
|
2021-01-10 11:16:43 +01:00
|
|
|
from ..workouts.utils_files import get_absolute_file_path
|
|
|
|
from .models import User, Workout
|
2020-05-01 12:12:48 +02:00
|
|
|
from .utils import authenticate, authenticate_as_admin
|
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'])
|
2019-07-20 21:57:35 +02:00
|
|
|
@authenticate
|
2021-01-02 19:28:03 +01:00
|
|
|
def get_users(auth_user_id: int) -> 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,
|
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,
|
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,
|
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"
|
|
|
|
}
|
|
|
|
|
|
|
|
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
|
|
|
|
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:
|
|
|
|
- Provide a valid auth token.
|
|
|
|
- Signature expired. Please log in again.
|
|
|
|
- Invalid token. Please log in again.
|
|
|
|
|
|
|
|
"""
|
2020-05-02 18:00:17 +02:00
|
|
|
params = request.args.copy()
|
|
|
|
page = 1 if 'page' not in params.keys() else int(params.get('page'))
|
|
|
|
per_page = (
|
|
|
|
int(params.get('per_page'))
|
|
|
|
if params.get('per_page')
|
|
|
|
else USER_PER_PAGE
|
|
|
|
)
|
|
|
|
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',
|
2019-08-28 13:25:39 +02:00
|
|
|
'data': {'users': [user.serialize() 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'])
|
2019-07-20 21:57:35 +02:00
|
|
|
@authenticate
|
2021-01-02 19:28:03 +01:00
|
|
|
def get_single_user(
|
|
|
|
auth_user_id: int, user_name: str
|
|
|
|
) -> 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,
|
|
|
|
"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,
|
|
|
|
"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"
|
|
|
|
}
|
|
|
|
|
|
|
|
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
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:
|
|
|
|
- Provide a valid auth token.
|
|
|
|
- Signature expired. Please log in again.
|
|
|
|
- Invalid token. Please log in again.
|
|
|
|
:statuscode 404:
|
2019-09-16 17:54:21 +02: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',
|
|
|
|
'data': {'users': [user.serialize()]},
|
|
|
|
}
|
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:
|
2019-09-16 17:54:21 +02: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-01-02 19:28:03 +01:00
|
|
|
def update_user(
|
|
|
|
auth_user_id: int, 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,
|
|
|
|
"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,
|
|
|
|
"sports_list": [
|
|
|
|
1,
|
|
|
|
4,
|
|
|
|
6
|
|
|
|
],
|
|
|
|
"timezone": "Europe/Paris",
|
|
|
|
"total_distance": 67.895,
|
|
|
|
"total_duration": "6:50:27",
|
|
|
|
"username": "admin"
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"status": "success"
|
|
|
|
}
|
|
|
|
|
|
|
|
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
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:
|
|
|
|
- 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:
|
|
|
|
"""
|
|
|
|
user_data = request.get_json()
|
|
|
|
if 'admin' not in user_data:
|
2021-01-01 16:39:25 +01:00
|
|
|
return InvalidPayloadErrorResponse()
|
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
|
|
|
|
2021-01-01 16:39:25 +01:00
|
|
|
user.admin = user_data['admin']
|
|
|
|
db.session.commit()
|
|
|
|
return {
|
|
|
|
'status': 'success',
|
|
|
|
'data': {'users': [user.serialize()]},
|
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(
|
|
|
|
auth_user_id: int, user_name: str
|
|
|
|
) -> 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 integer auth_user_id: authenticate user id (from JSON Web Token)
|
|
|
|
:param string user_name: user name
|
|
|
|
|
|
|
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
|
|
|
|
|
|
|
:statuscode 204: user account deleted
|
|
|
|
:statuscode 401:
|
|
|
|
- Provide a valid auth token.
|
|
|
|
- Signature expired. Please log in again.
|
|
|
|
- Invalid token. Please log in again.
|
|
|
|
:statuscode 403:
|
|
|
|
- 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.
|
|
|
|
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
auth_user = User.query.filter_by(id=auth_user_id).first()
|
|
|
|
user = User.query.filter_by(username=user_name).first()
|
2021-01-01 16:39:25 +01:00
|
|
|
if not user:
|
|
|
|
return UserNotFoundErrorResponse()
|
|
|
|
|
|
|
|
if user.id != auth_user_id and not auth_user.admin:
|
|
|
|
return ForbiddenErrorResponse()
|
|
|
|
if (
|
|
|
|
user.admin is True
|
|
|
|
and User.query.filter_by(admin=True).count() == 1
|
|
|
|
):
|
|
|
|
return ForbiddenErrorResponse(
|
|
|
|
'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-01-10 11:16:43 +01:00
|
|
|
for workout in Workout.query.filter_by(user_id=user.id).all():
|
|
|
|
db.session.delete(workout)
|
2021-01-01 16:39:25 +01:00
|
|
|
db.session.flush()
|
|
|
|
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)
|