API & Client - add/remove user admin rights - #15

This commit is contained in:
Sam 2020-05-01 12:12:48 +02:00
parent aa29231fd9
commit 7cc1b40f26
10 changed files with 320 additions and 27 deletions

View File

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

View File

@ -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/<user_name>', 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/<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,
"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
:<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:
"""
response_object = {'status': 'fail', 'message': 'User does not exist.'}
user_data = request.get_json()
if 'admin' not in user_data:
response_object = {'status': 'error', 'message': 'Invalid payload.'}
return jsonify(response_object), 400
try:
user = User.query.filter_by(username=user_name).first()
if not user:
return jsonify(response_object), 404
else:
user.admin = user_data['admin']
db.session.commit()
response_object = {
'status': 'success',
'data': {'users': [user.serialize()]},
}
return jsonify(response_object), 200
except exc.StatementError as e:
db.session.rollback()
appLog.error(e)
response_object = {
'status': 'error',
'message': 'Error. Please try again or contact the administrator.',
}
code = 500
return jsonify(response_object), code
@users_blueprint.route('/ping', methods=['GET'])
def ping_pong():
""" health check endpoint

View File

@ -28,6 +28,12 @@ export const updateSportsData = data => ({
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}`))

View File

@ -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 (
<div>
<Helmet>
@ -26,7 +26,7 @@ class AdminUsers extends React.Component {
<div className="row">
<div className="col card">
<div className="card-body">
<table className="table">
<table className="table user-table">
<thead>
<tr>
<th>#</th>
@ -35,6 +35,7 @@ class AdminUsers extends React.Component {
<th>{t('user:Registration Date')}</th>
<th>{t('activities:Activities')}</th>
<th>{t('user:Admin')}</th>
<th>{t('administration:Actions')}</th>
</tr>
</thead>
<tbody>
@ -79,6 +80,23 @@ class AdminUsers extends React.Component {
/>
)}
</td>
<td>
<input
type="submit"
className={`btn btn-${
user.admin ? 'dark' : 'primary'
} btn-sm`}
disabled={user.username === authUser.username}
value={
user.admin
? t('administration:Remove')
: t('administration:Add')
}
onClick={() =>
updateUser(user.username, !user.admin)
}
/>
</td>
</tr>
))}
</tbody>
@ -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)

View File

@ -499,6 +499,10 @@ label {
padding: 0.1em;
}
.user-table {
font-size: 0.9em;
}
/* calendar */
:root {
--main-color: #1a8fff;

View File

@ -94,27 +94,36 @@ function ProfileDetail({
}
className="img-fluid App-profile-img-small"
/>
<br />
<button type="submit" onClick={() => onDeletePicture()}>
{t('user:Delete picture')}
</button>
<br />
<br />
{editable && (
<>
<br />
<button
type="submit"
onClick={() => onDeletePicture()}
>
{t('user:Delete picture')}
</button>
<br />
<br />
</>
)}
</div>
)}
<form
encType="multipart/form-data"
onSubmit={event => onUploadPicture(event)}
>
<input
type="file"
name="picture"
accept=".png,.jpg,.gif"
/>
<br />
<button type="submit">{t('user:Send')}</button>
{` (max. size: ${fileSizeLimit})`}
</form>
{editable && (
<form
encType="multipart/form-data"
onSubmit={event => onUploadPicture(event)}
>
<input
type="file"
name="picture"
accept=".png,.jpg,.gif"
/>
<br />
<button type="submit">{t('user:Send')}</button>
{` (max. size: ${fileSizeLimit})`}
</form>
)}{' '}
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

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