API & Client - add/remove user admin rights - #15
This commit is contained in:
parent
aa29231fd9
commit
7cc1b40f26
@ -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']
|
||||
|
@ -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
|
||||
|
@ -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}`))
|
||||
|
@ -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)
|
||||
|
@ -499,6 +499,10 @@ label {
|
||||
padding: 0.1em;
|
||||
}
|
||||
|
||||
.user-table {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* calendar */
|
||||
:root {
|
||||
--main-color: #1a8fff;
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user