From 7cc1b40f26c77dfa51745c7d466e2bd863c7e616 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 1 May 2020 12:12:48 +0200 Subject: [PATCH] API & Client - add/remove user admin rights - #15 --- .../fittrackee_api/tests/test_users_api.py | 132 ++++++++++++++++++ fittrackee_api/fittrackee_api/users/users.py | 103 +++++++++++++- fittrackee_client/src/actions/index.js | 8 ++ .../src/components/Admin/Users/index.jsx | 27 +++- fittrackee_client/src/components/App.css | 4 + .../src/components/User/ProfileDetail.jsx | 47 ++++--- fittrackee_client/src/fitTrackeeApi/index.js | 4 +- .../src/locales/en/administration.json | 2 + .../src/locales/fr/administration.json | 2 + fittrackee_client/src/reducers/index.js | 18 ++- 10 files changed, 320 insertions(+), 27 deletions(-) diff --git a/fittrackee_api/fittrackee_api/tests/test_users_api.py b/fittrackee_api/fittrackee_api/tests/test_users_api.py index a294cacc..af22e1f2 100644 --- a/fittrackee_api/fittrackee_api/tests/test_users_api.py +++ b/fittrackee_api/fittrackee_api/tests/test_users_api.py @@ -277,3 +277,135 @@ def test_user_picture_no_user(app, user_1): assert response.status_code == 404 assert 'fail' in data['status'] assert 'User does not exist.' in data['message'] + + +def test_it_adds_admin_rights_to_a_user(app, user_1_admin, user_2): + client = app.test_client() + resp_login = client.post( + '/api/auth/login', + data=json.dumps(dict(email='admin@example.com', password='12345678')), + content_type='application/json', + ) + response = client.patch( + '/api/users/toto', + content_type='application/json', + data=json.dumps(dict(admin=True)), + headers=dict( + Authorization='Bearer ' + + json.loads(resp_login.data.decode())['auth_token'] + ), + ) + data = json.loads(response.data.decode()) + + assert response.status_code == 200 + assert 'success' in data['status'] + assert len(data['data']['users']) == 1 + + user = data['data']['users'][0] + assert user['email'] == 'toto@toto.com' + assert user['admin'] is True + + +def test_it_removes_admin_rights_to_a_user(app, user_1_admin, user_2): + client = app.test_client() + resp_login = client.post( + '/api/auth/login', + data=json.dumps(dict(email='admin@example.com', password='12345678')), + content_type='application/json', + ) + response = client.patch( + '/api/users/toto', + content_type='application/json', + data=json.dumps(dict(admin=False)), + headers=dict( + Authorization='Bearer ' + + json.loads(resp_login.data.decode())['auth_token'] + ), + ) + data = json.loads(response.data.decode()) + + assert response.status_code == 200 + assert 'success' in data['status'] + assert len(data['data']['users']) == 1 + + user = data['data']['users'][0] + assert user['email'] == 'toto@toto.com' + assert user['admin'] is False + + +def test_it_returns_error_if_payload_for_admin_rights_is_empty( + app, user_1_admin, user_2 +): + client = app.test_client() + resp_login = client.post( + '/api/auth/login', + data=json.dumps(dict(email='admin@example.com', password='12345678')), + content_type='application/json', + ) + response = client.patch( + '/api/users/toto', + content_type='application/json', + data=json.dumps(dict()), + headers=dict( + Authorization='Bearer ' + + json.loads(resp_login.data.decode())['auth_token'] + ), + ) + data = json.loads(response.data.decode()) + + assert response.status_code == 400 + assert 'error' in data['status'] + assert 'Invalid payload.' in data['message'] + + +def test_it_returns_error_if_payload_for_admin_rights_is_invalid( + app, user_1_admin, user_2 +): + client = app.test_client() + resp_login = client.post( + '/api/auth/login', + data=json.dumps(dict(email='admin@example.com', password='12345678')), + content_type='application/json', + ) + response = client.patch( + '/api/users/toto', + content_type='application/json', + data=json.dumps(dict(admin="")), + headers=dict( + Authorization='Bearer ' + + json.loads(resp_login.data.decode())['auth_token'] + ), + ) + data = json.loads(response.data.decode()) + + assert response.status_code == 500 + assert 'error' in data['status'] + assert ( + 'Error. Please try again or contact the administrator.' + in data['message'] + ) + + +def test_it_returns_error_if_user_can_not_change_admin_rights( + app, user_1, user_2 +): + client = app.test_client() + resp_login = client.post( + '/api/auth/login', + data=json.dumps(dict(email='test@test.com', password='12345678')), + content_type='application/json', + ) + response = client.patch( + '/api/users/toto', + content_type='application/json', + data=json.dumps(dict(admin=True)), + headers=dict( + Authorization='Bearer ' + + json.loads(resp_login.data.decode())['auth_token'] + ), + ) + data = json.loads(response.data.decode()) + + assert response.status_code == 403 + assert 'error' in data['status'] + assert 'You do not have permissions.' in data['message'] diff --git a/fittrackee_api/fittrackee_api/users/users.py b/fittrackee_api/fittrackee_api/users/users.py index 89552ba8..147d0489 100644 --- a/fittrackee_api/fittrackee_api/users/users.py +++ b/fittrackee_api/fittrackee_api/users/users.py @@ -1,8 +1,10 @@ -from flask import Blueprint, jsonify, send_file +from fittrackee_api import appLog, db +from flask import Blueprint, jsonify, request, send_file +from sqlalchemy import exc from ..activities.utils_files import get_absolute_file_path from .models import User -from .utils import authenticate +from .utils import authenticate, authenticate_as_admin users_blueprint = Blueprint('users', __name__) @@ -217,6 +219,103 @@ def get_picture(user_name): return jsonify(response_object), 404 +@users_blueprint.route('/users/', methods=['PATCH']) +@authenticate_as_admin +def update_user(auth_user_id, user_name): + """ + Update user to add admin rights + Only user with admin rights can modify another user + + **Example request**: + + .. sourcecode:: http + + PATCH api/users/ 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, + "nb_activities": 6, + "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) + :param integer user_name: user name + + : ({ type: 'UPDATE_SPORT_DATA', data, }) + +export const updateUsersData = data => ({ + type: 'UPDATE_USER_DATA', + data, +}) + export const getOrUpdateData = ( action, target, @@ -47,6 +53,8 @@ export const getOrUpdateData = ( dispatch(setData(target, ret.data)) } else if (action === 'updateData' && target === 'sports') { dispatch(updateSportsData(ret.data.sports[0])) + } else if (action === 'updateData' && target === 'users') { + dispatch(updateUsersData(ret.data.users[0])) } } else { dispatch(setError(`${target}|${ret.message || ret.status}`)) diff --git a/fittrackee_client/src/components/Admin/Users/index.jsx b/fittrackee_client/src/components/Admin/Users/index.jsx index 517f1751..f4cc7507 100644 --- a/fittrackee_client/src/components/Admin/Users/index.jsx +++ b/fittrackee_client/src/components/Admin/Users/index.jsx @@ -15,7 +15,7 @@ class AdminUsers extends React.Component { } render() { - const { message, t, users } = this.props + const { message, t, updateUser, authUser, users } = this.props return (
@@ -26,7 +26,7 @@ class AdminUsers extends React.Component {
- +
@@ -35,6 +35,7 @@ class AdminUsers extends React.Component { + @@ -79,6 +80,23 @@ class AdminUsers extends React.Component { /> )} + ))} @@ -101,11 +119,16 @@ class AdminUsers extends React.Component { export default connect( state => ({ message: state.message, + authUser: state.user, users: state.users.data, }), dispatch => ({ loadUsers: () => { dispatch(getOrUpdateData('getData', 'users')) }, + updateUser: (userName, isAdmin) => { + const data = { username: userName, admin: isAdmin } + dispatch(getOrUpdateData('updateData', 'users', data, false)) + }, }) )(AdminUsers) diff --git a/fittrackee_client/src/components/App.css b/fittrackee_client/src/components/App.css index 7e6c4403..69883f0e 100644 --- a/fittrackee_client/src/components/App.css +++ b/fittrackee_client/src/components/App.css @@ -499,6 +499,10 @@ label { padding: 0.1em; } +.user-table { + font-size: 0.9em; +} + /* calendar */ :root { --main-color: #1a8fff; diff --git a/fittrackee_client/src/components/User/ProfileDetail.jsx b/fittrackee_client/src/components/User/ProfileDetail.jsx index b809b821..4f9e703e 100644 --- a/fittrackee_client/src/components/User/ProfileDetail.jsx +++ b/fittrackee_client/src/components/User/ProfileDetail.jsx @@ -94,27 +94,36 @@ function ProfileDetail({ } className="img-fluid App-profile-img-small" /> -
- -
-
+ {editable && ( + <> +
+ +
+
+ + )} )} - onUploadPicture(event)} - > - -
- - {` (max. size: ${fileSizeLimit})`} - + {editable && ( + onUploadPicture(event)} + > + +
+ + {` (max. size: ${fileSizeLimit})`} + + )}{' '} diff --git a/fittrackee_client/src/fitTrackeeApi/index.js b/fittrackee_client/src/fitTrackeeApi/index.js index a8c9c668..623ec100 100644 --- a/fittrackee_client/src/fitTrackeeApi/index.js +++ b/fittrackee_client/src/fitTrackeeApi/index.js @@ -48,7 +48,9 @@ export default class FitTrackeeApi { static updateData(target, data) { const params = { - url: `${target}${data.id ? `/${data.id}` : ''}`, + url: `${target}${ + data.id ? `/${data.id}` : data.username ? `/${data.username}` : '' + }`, method: 'PATCH', body: data, type: 'application/json', diff --git a/fittrackee_client/src/locales/en/administration.json b/fittrackee_client/src/locales/en/administration.json index 0254c2f7..ee16638a 100644 --- a/fittrackee_client/src/locales/en/administration.json +++ b/fittrackee_client/src/locales/en/administration.json @@ -2,6 +2,7 @@ "Actions": "Actions", "Active": "Active", "activities exist": "activities exist", + "Add": "Add", "Administration": "Administration", "Application": "Application", "Application configuration": "Application configuration", @@ -18,6 +19,7 @@ "Max. size of uploaded files (in Mb)": "Max. size of uploaded files (in Mb)", "Max. size of zip archive": "Max. size of zip archive", "Max. size of zip archive (in Mb)": "Max. size of zip archive (in Mb)", + "Remove": "Remove", "Sports": "Sports", "user": "user", "Users": "Users", diff --git a/fittrackee_client/src/locales/fr/administration.json b/fittrackee_client/src/locales/fr/administration.json index ef98743f..08fb7363 100644 --- a/fittrackee_client/src/locales/fr/administration.json +++ b/fittrackee_client/src/locales/fr/administration.json @@ -1,6 +1,7 @@ { "Actions": "Actions", "Active": "Active", + "Add": "Ajouter", "Administration": "Administration", "activities exist": "des activités existent", "Application": "Application", @@ -18,6 +19,7 @@ "Max. size of uploaded files (in Mb)": "Taille max. des fichiers (en Mo)", "Max. size of zip archive": "Taille max. des archives zip", "Max. size of zip archive (in Mb)": "Taille max. des archives zip (en Mo)", + "Remove": "Retirer", "Sports": "Sports", "user": "user", "Users": "Utilisateurs", diff --git a/fittrackee_client/src/reducers/index.js b/fittrackee_client/src/reducers/index.js index b507b3fc..83306164 100644 --- a/fittrackee_client/src/reducers/index.js +++ b/fittrackee_client/src/reducers/index.js @@ -146,6 +146,21 @@ const sports = (state = initial.sports, action) => { return handleDataAndError(state, 'sports', action) } +const users = (state = initial.users, action) => { + if (action.type === 'UPDATE_USER_DATA') { + return { + ...state, + data: state.data.map(user => { + if (user.username === action.data.username) { + user.admin = action.data.admin + } + return user + }), + } + } + return handleDataAndError(state, 'users', action) +} + const user = (state = initial.user, action) => { switch (action.type) { case 'AUTH_ERROR': @@ -167,9 +182,6 @@ const statistics = (state = initial.statistics, action) => { return handleDataAndError(state, 'statistics', action) } -const users = (state = initial.users, action) => - handleDataAndError(state, 'users', action) - export default history => combineReducers({ activities,
#{t('user:Registration Date')} {t('activities:Activities')} {t('user:Admin')}{t('administration:Actions')}
+ + updateUser(user.username, !user.admin) + } + /> +