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 response.status_code == 404
|
||||||
assert 'fail' in data['status']
|
assert 'fail' in data['status']
|
||||||
assert 'User does not exist.' in data['message']
|
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 ..activities.utils_files import get_absolute_file_path
|
||||||
from .models import User
|
from .models import User
|
||||||
from .utils import authenticate
|
from .utils import authenticate, authenticate_as_admin
|
||||||
|
|
||||||
users_blueprint = Blueprint('users', __name__)
|
users_blueprint = Blueprint('users', __name__)
|
||||||
|
|
||||||
@ -217,6 +219,103 @@ def get_picture(user_name):
|
|||||||
return jsonify(response_object), 404
|
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'])
|
@users_blueprint.route('/ping', methods=['GET'])
|
||||||
def ping_pong():
|
def ping_pong():
|
||||||
""" health check endpoint
|
""" health check endpoint
|
||||||
|
@ -28,6 +28,12 @@ export const updateSportsData = data => ({
|
|||||||
type: 'UPDATE_SPORT_DATA',
|
type: 'UPDATE_SPORT_DATA',
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const updateUsersData = data => ({
|
||||||
|
type: 'UPDATE_USER_DATA',
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
|
||||||
export const getOrUpdateData = (
|
export const getOrUpdateData = (
|
||||||
action,
|
action,
|
||||||
target,
|
target,
|
||||||
@ -47,6 +53,8 @@ export const getOrUpdateData = (
|
|||||||
dispatch(setData(target, ret.data))
|
dispatch(setData(target, ret.data))
|
||||||
} else if (action === 'updateData' && target === 'sports') {
|
} else if (action === 'updateData' && target === 'sports') {
|
||||||
dispatch(updateSportsData(ret.data.sports[0]))
|
dispatch(updateSportsData(ret.data.sports[0]))
|
||||||
|
} else if (action === 'updateData' && target === 'users') {
|
||||||
|
dispatch(updateUsersData(ret.data.users[0]))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dispatch(setError(`${target}|${ret.message || ret.status}`))
|
dispatch(setError(`${target}|${ret.message || ret.status}`))
|
||||||
|
@ -15,7 +15,7 @@ class AdminUsers extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { message, t, users } = this.props
|
const { message, t, updateUser, authUser, users } = this.props
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@ -26,7 +26,7 @@ class AdminUsers extends React.Component {
|
|||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col card">
|
<div className="col card">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<table className="table">
|
<table className="table user-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
@ -35,6 +35,7 @@ class AdminUsers extends React.Component {
|
|||||||
<th>{t('user:Registration Date')}</th>
|
<th>{t('user:Registration Date')}</th>
|
||||||
<th>{t('activities:Activities')}</th>
|
<th>{t('activities:Activities')}</th>
|
||||||
<th>{t('user:Admin')}</th>
|
<th>{t('user:Admin')}</th>
|
||||||
|
<th>{t('administration:Actions')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -79,6 +80,23 @@ class AdminUsers extends React.Component {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -101,11 +119,16 @@ class AdminUsers extends React.Component {
|
|||||||
export default connect(
|
export default connect(
|
||||||
state => ({
|
state => ({
|
||||||
message: state.message,
|
message: state.message,
|
||||||
|
authUser: state.user,
|
||||||
users: state.users.data,
|
users: state.users.data,
|
||||||
}),
|
}),
|
||||||
dispatch => ({
|
dispatch => ({
|
||||||
loadUsers: () => {
|
loadUsers: () => {
|
||||||
dispatch(getOrUpdateData('getData', 'users'))
|
dispatch(getOrUpdateData('getData', 'users'))
|
||||||
},
|
},
|
||||||
|
updateUser: (userName, isAdmin) => {
|
||||||
|
const data = { username: userName, admin: isAdmin }
|
||||||
|
dispatch(getOrUpdateData('updateData', 'users', data, false))
|
||||||
|
},
|
||||||
})
|
})
|
||||||
)(AdminUsers)
|
)(AdminUsers)
|
||||||
|
@ -499,6 +499,10 @@ label {
|
|||||||
padding: 0.1em;
|
padding: 0.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-table {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
/* calendar */
|
/* calendar */
|
||||||
:root {
|
:root {
|
||||||
--main-color: #1a8fff;
|
--main-color: #1a8fff;
|
||||||
|
@ -94,27 +94,36 @@ function ProfileDetail({
|
|||||||
}
|
}
|
||||||
className="img-fluid App-profile-img-small"
|
className="img-fluid App-profile-img-small"
|
||||||
/>
|
/>
|
||||||
<br />
|
{editable && (
|
||||||
<button type="submit" onClick={() => onDeletePicture()}>
|
<>
|
||||||
{t('user:Delete picture')}
|
<br />
|
||||||
</button>
|
<button
|
||||||
<br />
|
type="submit"
|
||||||
<br />
|
onClick={() => onDeletePicture()}
|
||||||
|
>
|
||||||
|
{t('user:Delete picture')}
|
||||||
|
</button>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<form
|
{editable && (
|
||||||
encType="multipart/form-data"
|
<form
|
||||||
onSubmit={event => onUploadPicture(event)}
|
encType="multipart/form-data"
|
||||||
>
|
onSubmit={event => onUploadPicture(event)}
|
||||||
<input
|
>
|
||||||
type="file"
|
<input
|
||||||
name="picture"
|
type="file"
|
||||||
accept=".png,.jpg,.gif"
|
name="picture"
|
||||||
/>
|
accept=".png,.jpg,.gif"
|
||||||
<br />
|
/>
|
||||||
<button type="submit">{t('user:Send')}</button>
|
<br />
|
||||||
{` (max. size: ${fileSizeLimit})`}
|
<button type="submit">{t('user:Send')}</button>
|
||||||
</form>
|
{` (max. size: ${fileSizeLimit})`}
|
||||||
|
</form>
|
||||||
|
)}{' '}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,7 +48,9 @@ export default class FitTrackeeApi {
|
|||||||
|
|
||||||
static updateData(target, data) {
|
static updateData(target, data) {
|
||||||
const params = {
|
const params = {
|
||||||
url: `${target}${data.id ? `/${data.id}` : ''}`,
|
url: `${target}${
|
||||||
|
data.id ? `/${data.id}` : data.username ? `/${data.username}` : ''
|
||||||
|
}`,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: data,
|
body: data,
|
||||||
type: 'application/json',
|
type: 'application/json',
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
"Actions": "Actions",
|
"Actions": "Actions",
|
||||||
"Active": "Active",
|
"Active": "Active",
|
||||||
"activities exist": "activities exist",
|
"activities exist": "activities exist",
|
||||||
|
"Add": "Add",
|
||||||
"Administration": "Administration",
|
"Administration": "Administration",
|
||||||
"Application": "Application",
|
"Application": "Application",
|
||||||
"Application configuration": "Application configuration",
|
"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 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": "Max. size of zip archive",
|
||||||
"Max. size of zip archive (in Mb)": "Max. size of zip archive (in Mb)",
|
"Max. size of zip archive (in Mb)": "Max. size of zip archive (in Mb)",
|
||||||
|
"Remove": "Remove",
|
||||||
"Sports": "Sports",
|
"Sports": "Sports",
|
||||||
"user": "user",
|
"user": "user",
|
||||||
"Users": "Users",
|
"Users": "Users",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"Actions": "Actions",
|
"Actions": "Actions",
|
||||||
"Active": "Active",
|
"Active": "Active",
|
||||||
|
"Add": "Ajouter",
|
||||||
"Administration": "Administration",
|
"Administration": "Administration",
|
||||||
"activities exist": "des activités existent",
|
"activities exist": "des activités existent",
|
||||||
"Application": "Application",
|
"Application": "Application",
|
||||||
@ -18,6 +19,7 @@
|
|||||||
"Max. size of uploaded files (in Mb)": "Taille max. des fichiers (en Mo)",
|
"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": "Taille max. des archives zip",
|
||||||
"Max. size of zip archive (in Mb)": "Taille max. des archives zip (en Mo)",
|
"Max. size of zip archive (in Mb)": "Taille max. des archives zip (en Mo)",
|
||||||
|
"Remove": "Retirer",
|
||||||
"Sports": "Sports",
|
"Sports": "Sports",
|
||||||
"user": "user",
|
"user": "user",
|
||||||
"Users": "Utilisateurs",
|
"Users": "Utilisateurs",
|
||||||
|
@ -146,6 +146,21 @@ const sports = (state = initial.sports, action) => {
|
|||||||
return handleDataAndError(state, '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) => {
|
const user = (state = initial.user, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'AUTH_ERROR':
|
case 'AUTH_ERROR':
|
||||||
@ -167,9 +182,6 @@ const statistics = (state = initial.statistics, action) => {
|
|||||||
return handleDataAndError(state, 'statistics', action)
|
return handleDataAndError(state, 'statistics', action)
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = (state = initial.users, action) =>
|
|
||||||
handleDataAndError(state, 'users', action)
|
|
||||||
|
|
||||||
export default history =>
|
export default history =>
|
||||||
combineReducers({
|
combineReducers({
|
||||||
activities,
|
activities,
|
||||||
|
Loading…
Reference in New Issue
Block a user