API - remove intermediate directory and rename api directory
This commit is contained in:
0
fittrackee/users/__init__.py
Normal file
0
fittrackee/users/__init__.py
Normal file
810
fittrackee/users/auth.py
Normal file
810
fittrackee/users/auth.py
Normal file
@ -0,0 +1,810 @@
|
||||
import datetime
|
||||
import os
|
||||
|
||||
import jwt
|
||||
from fittrackee import appLog, bcrypt, db
|
||||
from fittrackee.tasks import reset_password_email
|
||||
from flask import Blueprint, current_app, jsonify, request
|
||||
from sqlalchemy import exc, or_
|
||||
from werkzeug.exceptions import RequestEntityTooLarge
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from ..activities.utils_files import get_absolute_file_path
|
||||
from .models import User
|
||||
from .utils import (
|
||||
authenticate,
|
||||
check_passwords,
|
||||
display_readable_file_size,
|
||||
get_readable_duration,
|
||||
register_controls,
|
||||
verify_extension_and_size,
|
||||
)
|
||||
from .utils_token import decode_user_token
|
||||
|
||||
auth_blueprint = Blueprint('auth', __name__)
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/register', methods=['POST'])
|
||||
def register_user():
|
||||
"""
|
||||
register a user
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/auth/register HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example responses**:
|
||||
|
||||
- successful registration
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 CREATED
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"auth_token": "JSON Web Token",
|
||||
"message": "Successfully registered.",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
- error on registration
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 400 BAD REQUEST
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"message": "Errors: Valid email must be provided.\\n",
|
||||
"status": "error"
|
||||
}
|
||||
|
||||
:<json string username: user name (3 to 12 characters required)
|
||||
:<json string email: user email
|
||||
:<json string password: password (8 characters required)
|
||||
:<json string password_conf: password confirmation
|
||||
|
||||
:statuscode 201: Successfully registered.
|
||||
:statuscode 400:
|
||||
- Invalid payload.
|
||||
- Sorry. That user already exists.
|
||||
- Errors:
|
||||
- 3 to 12 characters required for usernanme.
|
||||
- Valid email must be provided.
|
||||
- Password and password confirmation don't match.
|
||||
- 8 characters required for password.
|
||||
:statuscode 403:
|
||||
Error. Registration is disabled.
|
||||
:statuscode 500:
|
||||
Error. Please try again or contact the administrator.
|
||||
|
||||
"""
|
||||
if not current_app.config.get('is_registration_enabled'):
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Error. Registration is disabled.',
|
||||
}
|
||||
return jsonify(response_object), 403
|
||||
# get post data
|
||||
post_data = request.get_json()
|
||||
if (
|
||||
not post_data
|
||||
or post_data.get('username') is None
|
||||
or post_data.get('email') is None
|
||||
or post_data.get('password') is None
|
||||
or post_data.get('password_conf') is None
|
||||
):
|
||||
response_object = {'status': 'error', 'message': 'Invalid payload.'}
|
||||
return jsonify(response_object), 400
|
||||
username = post_data.get('username')
|
||||
email = post_data.get('email')
|
||||
password = post_data.get('password')
|
||||
password_conf = post_data.get('password_conf')
|
||||
|
||||
try:
|
||||
ret = register_controls(username, email, password, password_conf)
|
||||
except TypeError as e:
|
||||
db.session.rollback()
|
||||
appLog.error(e)
|
||||
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Error. Please try again or contact the administrator.',
|
||||
}
|
||||
return jsonify(response_object), 500
|
||||
if ret != '':
|
||||
response_object = {'status': 'error', 'message': ret}
|
||||
return jsonify(response_object), 400
|
||||
|
||||
try:
|
||||
# check for existing user
|
||||
user = User.query.filter(
|
||||
or_(User.username == username, User.email == email)
|
||||
).first()
|
||||
if not user:
|
||||
# add new user to db
|
||||
new_user = User(username=username, email=email, password=password)
|
||||
new_user.timezone = 'Europe/Paris'
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
# generate auth token
|
||||
auth_token = new_user.encode_auth_token(new_user.id)
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'message': 'Successfully registered.',
|
||||
'auth_token': auth_token.decode(),
|
||||
}
|
||||
return jsonify(response_object), 201
|
||||
else:
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Sorry. That user already exists.',
|
||||
}
|
||||
return jsonify(response_object), 400
|
||||
# handler errors
|
||||
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
||||
db.session.rollback()
|
||||
appLog.error(e)
|
||||
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Error. Please try again or contact the administrator.',
|
||||
}
|
||||
return jsonify(response_object), 500
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/login', methods=['POST'])
|
||||
def login_user():
|
||||
"""
|
||||
user login
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/auth/login HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example responses**:
|
||||
|
||||
- successful login
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"auth_token": "JSON Web Token",
|
||||
"message": "Successfully logged in.",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
- error on login
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 404 NOT FOUND
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"message": "Invalid credentials.",
|
||||
"status": "error"
|
||||
}
|
||||
|
||||
:<json string email: user email
|
||||
:<json string password_conf: password confirmation
|
||||
|
||||
:statuscode 200: Successfully logged in.
|
||||
:statuscode 404: Invalid credentials.
|
||||
:statuscode 500: Error. Please try again or contact the administrator.
|
||||
|
||||
"""
|
||||
# get post data
|
||||
post_data = request.get_json()
|
||||
if not post_data:
|
||||
response_object = {'status': 'error', 'message': 'Invalid payload.'}
|
||||
return jsonify(response_object), 400
|
||||
email = post_data.get('email')
|
||||
password = post_data.get('password')
|
||||
try:
|
||||
# check for existing user
|
||||
user = User.query.filter(User.email == email).first()
|
||||
if user and bcrypt.check_password_hash(user.password, password):
|
||||
# generate auth token
|
||||
auth_token = user.encode_auth_token(user.id)
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'message': 'Successfully logged in.',
|
||||
'auth_token': auth_token.decode(),
|
||||
}
|
||||
return jsonify(response_object), 200
|
||||
else:
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Invalid credentials.',
|
||||
}
|
||||
return jsonify(response_object), 404
|
||||
# handler errors
|
||||
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
||||
db.session.rollback()
|
||||
appLog.error(e)
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Error. Please try again or contact the administrator.',
|
||||
}
|
||||
return jsonify(response_object), 500
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/logout', methods=['GET'])
|
||||
@authenticate
|
||||
def logout_user(auth_user_id):
|
||||
"""
|
||||
user logout
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/auth/logout HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example responses**:
|
||||
|
||||
- successful logout
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"message": "Successfully logged out.",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
- error on login
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 401 UNAUTHORIZED
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"message": "Provide a valid auth token.",
|
||||
"status": "error"
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: Successfully logged out.
|
||||
:statuscode 401: Provide a valid auth token.
|
||||
|
||||
"""
|
||||
# get auth token
|
||||
auth_header = request.headers.get('Authorization')
|
||||
if auth_header:
|
||||
auth_token = auth_header.split(" ")[1]
|
||||
resp = User.decode_auth_token(auth_token)
|
||||
if not isinstance(auth_user_id, str):
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'message': 'Successfully logged out.',
|
||||
}
|
||||
return jsonify(response_object), 200
|
||||
else:
|
||||
response_object = {'status': 'error', 'message': resp}
|
||||
return jsonify(response_object), 401
|
||||
else:
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Provide a valid auth token.',
|
||||
}
|
||||
return jsonify(response_object), 401
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/profile', methods=['GET'])
|
||||
@authenticate
|
||||
def get_authenticated_user_profile(auth_user_id):
|
||||
"""
|
||||
get authenticated user info
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/auth/profile HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"admin": false,
|
||||
"bio": null,
|
||||
"birth_date": null,
|
||||
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
||||
"email": "sam@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": "sam",
|
||||
"weekm": false
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
: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.
|
||||
|
||||
"""
|
||||
user = User.query.filter_by(id=auth_user_id).first()
|
||||
response_object = {'status': 'success', 'data': user.serialize()}
|
||||
return jsonify(response_object), 200
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/profile/edit', methods=['POST'])
|
||||
@authenticate
|
||||
def edit_user(auth_user_id):
|
||||
"""
|
||||
edit authenticated user
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/auth/profile/edit HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"admin": false,
|
||||
"bio": null,
|
||||
"birth_date": null,
|
||||
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
||||
"email": "sam@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": "sam"
|
||||
"weekm": true,
|
||||
},
|
||||
"message": "User profile updated.",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:<json string first_name: user first name
|
||||
:<json string last_name: user last name
|
||||
:<json string location: user location
|
||||
:<json string bio: user biography
|
||||
:<json string birth_date: user birth date (format: ``%Y-%m-%d``)
|
||||
:<json string password: user password
|
||||
:<json string password_conf: user password confirmation
|
||||
:<json string timezone: user time zone
|
||||
:<json string weekm: does week start on Monday?
|
||||
:<json string language: language preferences
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: User profile updated.
|
||||
:statuscode 400:
|
||||
- Invalid payload.
|
||||
- Password and password confirmation don't match.
|
||||
:statuscode 401:
|
||||
- Provide a valid auth token.
|
||||
- Signature expired. Please log in again.
|
||||
- Invalid token. Please log in again.
|
||||
:statuscode 500: Error. Please try again or contact the administrator.
|
||||
|
||||
"""
|
||||
# get post data
|
||||
post_data = request.get_json()
|
||||
user_mandatory_data = {
|
||||
'first_name',
|
||||
'last_name',
|
||||
'bio',
|
||||
'birth_date',
|
||||
'language',
|
||||
'location',
|
||||
'timezone',
|
||||
'weekm',
|
||||
}
|
||||
if not post_data or not post_data.keys() >= user_mandatory_data:
|
||||
response_object = {'status': 'error', 'message': 'Invalid payload.'}
|
||||
return jsonify(response_object), 400
|
||||
first_name = post_data.get('first_name')
|
||||
last_name = post_data.get('last_name')
|
||||
bio = post_data.get('bio')
|
||||
birth_date = post_data.get('birth_date')
|
||||
language = post_data.get('language')
|
||||
location = post_data.get('location')
|
||||
password = post_data.get('password')
|
||||
password_conf = post_data.get('password_conf')
|
||||
timezone = post_data.get('timezone')
|
||||
weekm = post_data.get('weekm')
|
||||
|
||||
if password is not None and password != '':
|
||||
message = check_passwords(password, password_conf)
|
||||
if message != '':
|
||||
response_object = {'status': 'error', 'message': message}
|
||||
return jsonify(response_object), 400
|
||||
password = bcrypt.generate_password_hash(
|
||||
password, current_app.config.get('BCRYPT_LOG_ROUNDS')
|
||||
).decode()
|
||||
|
||||
try:
|
||||
user = User.query.filter_by(id=auth_user_id).first()
|
||||
user.first_name = first_name
|
||||
user.last_name = last_name
|
||||
user.bio = bio
|
||||
user.language = language
|
||||
user.location = location
|
||||
user.birth_date = (
|
||||
datetime.datetime.strptime(birth_date, '%Y-%m-%d')
|
||||
if birth_date
|
||||
else None
|
||||
)
|
||||
if password is not None and password != '':
|
||||
user.password = password
|
||||
user.timezone = timezone
|
||||
user.weekm = weekm
|
||||
db.session.commit()
|
||||
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'message': 'User profile updated.',
|
||||
'data': user.serialize(),
|
||||
}
|
||||
return jsonify(response_object), 200
|
||||
|
||||
# handler errors
|
||||
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
||||
db.session.rollback()
|
||||
appLog.error(e)
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Error. Please try again or contact the administrator.',
|
||||
}
|
||||
return jsonify(response_object), 500
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/picture', methods=['POST'])
|
||||
@authenticate
|
||||
def edit_picture(auth_user_id):
|
||||
"""
|
||||
update authenticated user picture
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/auth/picture HTTP/1.1
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"message": "User picture updated.",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:form file: image file (allowed extensions: .jpg, .png, .gif)
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: User picture updated.
|
||||
:statuscode 400:
|
||||
- Invalid payload.
|
||||
- No file part.
|
||||
- No selected file.
|
||||
- File extension not allowed.
|
||||
:statuscode 401:
|
||||
- Provide a valid auth token.
|
||||
- Signature expired. Please log in again.
|
||||
- Invalid token. Please log in again.
|
||||
:statuscode 413: Error during picture update: file size exceeds 1.0MB.
|
||||
:statuscode 500: Error during picture update.
|
||||
|
||||
"""
|
||||
try:
|
||||
response_object, response_code = verify_extension_and_size(
|
||||
'picture', request
|
||||
)
|
||||
except RequestEntityTooLarge as e:
|
||||
appLog.error(e)
|
||||
max_file_size = current_app.config['MAX_CONTENT_LENGTH']
|
||||
response_object = {
|
||||
'status': 'fail',
|
||||
'message': 'Error during picture update, file size exceeds '
|
||||
f'{display_readable_file_size(max_file_size)}.',
|
||||
}
|
||||
return jsonify(response_object), 413
|
||||
if response_object['status'] != 'success':
|
||||
return jsonify(response_object), response_code
|
||||
|
||||
file = request.files['file']
|
||||
filename = secure_filename(file.filename)
|
||||
dirpath = os.path.join(
|
||||
current_app.config['UPLOAD_FOLDER'], 'pictures', str(auth_user_id)
|
||||
)
|
||||
if not os.path.exists(dirpath):
|
||||
os.makedirs(dirpath)
|
||||
absolute_picture_path = os.path.join(dirpath, filename)
|
||||
relative_picture_path = os.path.join(
|
||||
'pictures', str(auth_user_id), filename
|
||||
)
|
||||
|
||||
try:
|
||||
user = User.query.filter_by(id=auth_user_id).first()
|
||||
if user.picture is not None:
|
||||
old_picture_path = get_absolute_file_path(user.picture)
|
||||
if os.path.isfile(get_absolute_file_path(old_picture_path)):
|
||||
os.remove(old_picture_path)
|
||||
file.save(absolute_picture_path)
|
||||
user.picture = relative_picture_path
|
||||
db.session.commit()
|
||||
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'message': 'User picture updated.',
|
||||
}
|
||||
return jsonify(response_object), 200
|
||||
|
||||
except (exc.IntegrityError, ValueError) as e:
|
||||
db.session.rollback()
|
||||
appLog.error(e)
|
||||
response_object = {
|
||||
'status': 'fail',
|
||||
'message': 'Error during picture update.',
|
||||
}
|
||||
return jsonify(response_object), 500
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/picture', methods=['DELETE'])
|
||||
@authenticate
|
||||
def del_picture(auth_user_id):
|
||||
"""
|
||||
delete authenticated user picture
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/auth/picture HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 NO CONTENT
|
||||
Content-Type: application/json
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 204: picture deleted
|
||||
:statuscode 401:
|
||||
- Provide a valid auth token.
|
||||
- Signature expired. Please log in again.
|
||||
- Invalid token. Please log in again.
|
||||
:statuscode 500: Error during picture deletion.
|
||||
|
||||
"""
|
||||
try:
|
||||
user = User.query.filter_by(id=auth_user_id).first()
|
||||
picture_path = get_absolute_file_path(user.picture)
|
||||
if os.path.isfile(picture_path):
|
||||
os.remove(picture_path)
|
||||
user.picture = None
|
||||
db.session.commit()
|
||||
|
||||
response_object = {'status': 'no content'}
|
||||
return jsonify(response_object), 204
|
||||
|
||||
except (exc.IntegrityError, ValueError) as e:
|
||||
db.session.rollback()
|
||||
appLog.error(e)
|
||||
response_object = {
|
||||
'status': 'fail',
|
||||
'message': 'Error during picture deletion.',
|
||||
}
|
||||
return jsonify(response_object), 500
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/password/reset-request', methods=['POST'])
|
||||
def request_password_reset():
|
||||
"""
|
||||
handle password reset request
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/auth/password/reset-request HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"message": "Password reset request processed.",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:<json string email: user email
|
||||
|
||||
:statuscode 200: Password reset request processed.
|
||||
:statuscode 400: Invalid payload.
|
||||
|
||||
"""
|
||||
post_data = request.get_json()
|
||||
if not post_data or post_data.get('email') is None:
|
||||
response_object = {'status': 'error', 'message': 'Invalid payload.'}
|
||||
return jsonify(response_object), 400
|
||||
email = post_data.get('email')
|
||||
|
||||
user = User.query.filter(User.email == email).first()
|
||||
if user:
|
||||
password_reset_token = user.encode_password_reset_token(user.id)
|
||||
ui_url = current_app.config['UI_URL']
|
||||
email_data = {
|
||||
'expiration_delay': get_readable_duration(
|
||||
current_app.config.get('PASSWORD_TOKEN_EXPIRATION_SECONDS'),
|
||||
'en' if user.language is None else user.language,
|
||||
),
|
||||
'username': user.username,
|
||||
'password_reset_url': (
|
||||
f'{ui_url}/password-reset?token={password_reset_token.decode()}' # noqa
|
||||
),
|
||||
'operating_system': request.user_agent.platform,
|
||||
'browser_name': request.user_agent.browser,
|
||||
}
|
||||
user_data = {
|
||||
'language': user.language if user.language else 'en',
|
||||
'email': user.email,
|
||||
}
|
||||
reset_password_email.send(user_data, email_data)
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'message': 'Password reset request processed.',
|
||||
}
|
||||
return jsonify(response_object), 200
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/password/update', methods=['POST'])
|
||||
def update_password():
|
||||
"""
|
||||
update user password
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/auth/password/update HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"message": "Password updated.",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:<json string password: password (8 characters required)
|
||||
:<json string password_conf: password confirmation
|
||||
:<json string token: password reset token
|
||||
|
||||
:statuscode 200: Password updated.
|
||||
:statuscode 400: Invalid payload.
|
||||
:statuscode 401: Invalid token.
|
||||
:statuscode 500: Error. Please try again or contact the administrator.
|
||||
|
||||
"""
|
||||
post_data = request.get_json()
|
||||
if (
|
||||
not post_data
|
||||
or post_data.get('password') is None
|
||||
or post_data.get('password_conf') is None
|
||||
or post_data.get('token') is None
|
||||
):
|
||||
response_object = {'status': 'error', 'message': 'Invalid payload.'}
|
||||
return jsonify(response_object), 400
|
||||
password = post_data.get('password')
|
||||
password_conf = post_data.get('password_conf')
|
||||
token = post_data.get('token')
|
||||
|
||||
invalid_token_response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Invalid token. Please request a new token.',
|
||||
}
|
||||
try:
|
||||
user_id = decode_user_token(token)
|
||||
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
|
||||
return jsonify(invalid_token_response_object), 401
|
||||
|
||||
message = check_passwords(password, password_conf)
|
||||
if message != '':
|
||||
response_object = {'status': 'error', 'message': message}
|
||||
return jsonify(response_object), 400
|
||||
|
||||
user = User.query.filter(User.id == user_id).first()
|
||||
if not user:
|
||||
return jsonify(invalid_token_response_object), 401
|
||||
try:
|
||||
user.password = bcrypt.generate_password_hash(
|
||||
password, current_app.config.get('BCRYPT_LOG_ROUNDS')
|
||||
).decode()
|
||||
db.session.commit()
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'message': 'Password updated.',
|
||||
}
|
||||
return jsonify(response_object), 200
|
||||
|
||||
except (exc.OperationalError, ValueError) as e:
|
||||
db.session.rollback()
|
||||
appLog.error(e)
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Error. Please try again or contact the administrator.',
|
||||
}
|
||||
return jsonify(response_object), 500
|
141
fittrackee/users/models.py
Normal file
141
fittrackee/users/models.py
Normal file
@ -0,0 +1,141 @@
|
||||
from datetime import datetime
|
||||
|
||||
import jwt
|
||||
from fittrackee import bcrypt, db
|
||||
from flask import current_app
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy.sql.expression import select
|
||||
|
||||
from ..activities.models import Activity
|
||||
from .utils_token import decode_user_token, get_user_token
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
__tablename__ = "users"
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
username = db.Column(db.String(20), unique=True, nullable=False)
|
||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||
password = db.Column(db.String(255), nullable=False)
|
||||
created_at = db.Column(db.DateTime, nullable=False)
|
||||
admin = db.Column(db.Boolean, default=False, nullable=False)
|
||||
first_name = db.Column(db.String(80), nullable=True)
|
||||
last_name = db.Column(db.String(80), nullable=True)
|
||||
birth_date = db.Column(db.DateTime, nullable=True)
|
||||
location = db.Column(db.String(80), nullable=True)
|
||||
bio = db.Column(db.String(200), nullable=True)
|
||||
picture = db.Column(db.String(255), nullable=True)
|
||||
timezone = db.Column(db.String(50), nullable=True)
|
||||
# does the week start Monday?
|
||||
weekm = db.Column(db.Boolean(50), default=False, nullable=False)
|
||||
activities = db.relationship(
|
||||
'Activity', lazy=True, backref=db.backref('user', lazy='joined')
|
||||
)
|
||||
records = db.relationship(
|
||||
'Record', lazy=True, backref=db.backref('user', lazy='joined')
|
||||
)
|
||||
language = db.Column(db.String(50), nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<User {self.username!r}>'
|
||||
|
||||
def __init__(
|
||||
self, username, email, password, created_at=datetime.utcnow()
|
||||
):
|
||||
self.username = username
|
||||
self.email = email
|
||||
self.password = bcrypt.generate_password_hash(
|
||||
password, current_app.config.get('BCRYPT_LOG_ROUNDS')
|
||||
).decode()
|
||||
self.created_at = created_at
|
||||
|
||||
@staticmethod
|
||||
def encode_auth_token(user_id):
|
||||
"""
|
||||
Generates the auth token
|
||||
:param user_id: -
|
||||
:return: JWToken
|
||||
"""
|
||||
try:
|
||||
return get_user_token(user_id)
|
||||
except Exception as e:
|
||||
return e
|
||||
|
||||
@staticmethod
|
||||
def encode_password_reset_token(user_id):
|
||||
"""
|
||||
Generates the auth token
|
||||
:param user_id: -
|
||||
:return: JWToken
|
||||
"""
|
||||
try:
|
||||
return get_user_token(user_id, password_reset=True)
|
||||
except Exception as e:
|
||||
return e
|
||||
|
||||
@staticmethod
|
||||
def decode_auth_token(auth_token):
|
||||
"""
|
||||
Decodes the auth token
|
||||
:param auth_token: -
|
||||
:return: integer|string
|
||||
"""
|
||||
try:
|
||||
return decode_user_token(auth_token)
|
||||
except jwt.ExpiredSignatureError:
|
||||
return 'Signature expired. Please log in again.'
|
||||
except jwt.InvalidTokenError:
|
||||
return 'Invalid token. Please log in again.'
|
||||
|
||||
@hybrid_property
|
||||
def activities_count(self):
|
||||
return Activity.query.filter(Activity.user_id == self.id).count()
|
||||
|
||||
@activities_count.expression
|
||||
def activities_count(self):
|
||||
return (
|
||||
select([func.count(Activity.id)])
|
||||
.where(Activity.user_id == self.id)
|
||||
.label("activities_count")
|
||||
)
|
||||
|
||||
def serialize(self):
|
||||
sports = []
|
||||
total = (None, None)
|
||||
if self.activities_count > 0:
|
||||
sports = (
|
||||
db.session.query(Activity.sport_id)
|
||||
.filter(Activity.user_id == self.id)
|
||||
.group_by(Activity.sport_id)
|
||||
.order_by(Activity.sport_id)
|
||||
.all()
|
||||
)
|
||||
total = (
|
||||
db.session.query(
|
||||
func.sum(Activity.distance), func.sum(Activity.duration)
|
||||
)
|
||||
.filter(Activity.user_id == self.id)
|
||||
.first()
|
||||
)
|
||||
return {
|
||||
'username': self.username,
|
||||
'email': self.email,
|
||||
'created_at': self.created_at,
|
||||
'admin': self.admin,
|
||||
'first_name': self.first_name,
|
||||
'last_name': self.last_name,
|
||||
'bio': self.bio,
|
||||
'location': self.location,
|
||||
'birth_date': self.birth_date,
|
||||
'picture': self.picture is not None,
|
||||
'timezone': self.timezone,
|
||||
'weekm': self.weekm,
|
||||
'language': self.language,
|
||||
'nb_activities': self.activities_count,
|
||||
'nb_sports': len(sports),
|
||||
'sports_list': [
|
||||
sport for sportslist in sports for sport in sportslist
|
||||
],
|
||||
'total_distance': float(total[0]) if total[0] else 0,
|
||||
'total_duration': str(total[1]) if total[1] else "0:00:00",
|
||||
}
|
496
fittrackee/users/users.py
Normal file
496
fittrackee/users/users.py
Normal file
@ -0,0 +1,496 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from fittrackee 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 Activity, User
|
||||
from .utils import authenticate, authenticate_as_admin
|
||||
|
||||
users_blueprint = Blueprint('users', __name__)
|
||||
|
||||
USER_PER_PAGE = 10
|
||||
|
||||
|
||||
@users_blueprint.route('/users', methods=['GET'])
|
||||
@authenticate
|
||||
def get_users(auth_user_id):
|
||||
"""
|
||||
Get all users
|
||||
|
||||
**Example request**:
|
||||
|
||||
- without parameters
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/users HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
- with some query parameters
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/users?order_by=activities_count&par_page=5 HTTP/1.1
|
||||
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,
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"admin": false,
|
||||
"bio": null,
|
||||
"birth_date": null,
|
||||
"created_at": "Sat, 20 Jul 2019 11:27:03 GMT",
|
||||
"email": "sam@example.com",
|
||||
"first_name": null,
|
||||
"language": "fr",
|
||||
"last_name": null,
|
||||
"location": null,
|
||||
"nb_activities": 0,
|
||||
"nb_sports": 0,
|
||||
"picture": false,
|
||||
"sports_list": [],
|
||||
"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)
|
||||
|
||||
: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``,
|
||||
``activities_count``, ``admin``)
|
||||
:query string order: sorting order (default: ``asc``)
|
||||
|
||||
: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.
|
||||
|
||||
"""
|
||||
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(
|
||||
User.activities_count.asc()
|
||||
if order_by == 'activities_count' and order == 'asc'
|
||||
else True,
|
||||
User.activities_count.desc()
|
||||
if order_by == 'activities_count' and order == 'desc'
|
||||
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
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'data': {'users': [user.serialize() for user in users]},
|
||||
'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,
|
||||
},
|
||||
}
|
||||
return jsonify(response_object), 200
|
||||
|
||||
|
||||
@users_blueprint.route('/users/<user_name>', methods=['GET'])
|
||||
@authenticate
|
||||
def get_single_user(auth_user_id, user_name):
|
||||
"""
|
||||
Get single user details
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/users/admin 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
|
||||
|
||||
: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:
|
||||
- User does not exist.
|
||||
"""
|
||||
|
||||
response_object = {'status': 'fail', 'message': 'User does not exist.'}
|
||||
try:
|
||||
user = User.query.filter_by(username=user_name).first()
|
||||
if not user:
|
||||
return jsonify(response_object), 404
|
||||
else:
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'data': {'users': [user.serialize()]},
|
||||
}
|
||||
return jsonify(response_object), 200
|
||||
except ValueError:
|
||||
return jsonify(response_object), 404
|
||||
|
||||
|
||||
@users_blueprint.route('/users/<user_name>/picture', methods=['GET'])
|
||||
def get_picture(user_name):
|
||||
"""get user picture
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/users/admin/picture HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: image/jpeg
|
||||
|
||||
:param integer user_name: user name
|
||||
|
||||
:statuscode 200: success
|
||||
:statuscode 404:
|
||||
- User does not exist.
|
||||
- No picture.
|
||||
|
||||
"""
|
||||
response_object = {'status': 'not found', 'message': 'No picture.'}
|
||||
try:
|
||||
user = User.query.filter_by(username=user_name).first()
|
||||
if not user:
|
||||
response_object = {
|
||||
'status': 'fail',
|
||||
'message': 'User does not exist.',
|
||||
}
|
||||
return jsonify(response_object), 404
|
||||
if user.picture is not None:
|
||||
picture_path = get_absolute_file_path(user.picture)
|
||||
return send_file(picture_path)
|
||||
return jsonify(response_object), 404
|
||||
except Exception:
|
||||
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 string 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('/users/<user_name>', methods=['DELETE'])
|
||||
@authenticate
|
||||
def delete_user(auth_user_id, user_name):
|
||||
"""
|
||||
Delete a user account
|
||||
|
||||
A user can only delete his own account
|
||||
|
||||
An admin can delete all accounts except his account if he's the only
|
||||
one admin
|
||||
|
||||
**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()
|
||||
if user:
|
||||
if user.id != auth_user_id and not auth_user.admin:
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'You do not have permissions.',
|
||||
}
|
||||
return response_object, 403
|
||||
if (
|
||||
user.admin is True
|
||||
and User.query.filter_by(admin=True).count() == 1
|
||||
):
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': (
|
||||
'You can not delete your account, '
|
||||
'no other user has admin rights.'
|
||||
),
|
||||
}
|
||||
return response_object, 403
|
||||
for activity in Activity.query.filter_by(user_id=user.id).all():
|
||||
db.session.delete(activity)
|
||||
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(
|
||||
get_absolute_file_path(f'activities/{user.id}'),
|
||||
ignore_errors=True,
|
||||
)
|
||||
shutil.rmtree(
|
||||
get_absolute_file_path(f'pictures/{user.id}'),
|
||||
ignore_errors=True,
|
||||
)
|
||||
response_object = {'status': 'no content'}
|
||||
code = 204
|
||||
else:
|
||||
response_object = {
|
||||
'status': 'not found',
|
||||
'message': 'User does not exist.',
|
||||
}
|
||||
code = 404
|
||||
except (
|
||||
exc.IntegrityError,
|
||||
exc.OperationalError,
|
||||
ValueError,
|
||||
OSError,
|
||||
) 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
|
161
fittrackee/users/utils.py
Normal file
161
fittrackee/users/utils.py
Normal file
@ -0,0 +1,161 @@
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from functools import wraps
|
||||
|
||||
import humanize
|
||||
from flask import current_app, jsonify, request
|
||||
|
||||
from .models import User
|
||||
|
||||
|
||||
def is_admin(user_id):
|
||||
user = User.query.filter_by(id=user_id).first()
|
||||
return user.admin
|
||||
|
||||
|
||||
def is_valid_email(email):
|
||||
mail_pattern = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
|
||||
return re.match(mail_pattern, email) is not None
|
||||
|
||||
|
||||
def check_passwords(password, password_conf):
|
||||
ret = ''
|
||||
if password_conf != password:
|
||||
ret = 'Password and password confirmation don\'t match.\n'
|
||||
if len(password) < 8:
|
||||
ret += 'Password: 8 characters required.\n'
|
||||
return ret
|
||||
|
||||
|
||||
def register_controls(username, email, password, password_conf):
|
||||
ret = ''
|
||||
if not 2 < len(username) < 13:
|
||||
ret += 'Username: 3 to 12 characters required.\n'
|
||||
if not is_valid_email(email):
|
||||
ret += 'Valid email must be provided.\n'
|
||||
ret += check_passwords(password, password_conf)
|
||||
return ret
|
||||
|
||||
|
||||
def verify_extension_and_size(file_type, req):
|
||||
response_object = {'status': 'success'}
|
||||
code = 400
|
||||
|
||||
if 'file' not in req.files:
|
||||
response_object = {'status': 'fail', 'message': 'No file part.'}
|
||||
return response_object, code
|
||||
|
||||
file = req.files['file']
|
||||
if file.filename == '':
|
||||
response_object = {'status': 'fail', 'message': 'No selected file.'}
|
||||
return response_object, code
|
||||
|
||||
allowed_extensions = (
|
||||
'ACTIVITY_ALLOWED_EXTENSIONS'
|
||||
if file_type == 'activity'
|
||||
else 'PICTURE_ALLOWED_EXTENSIONS'
|
||||
)
|
||||
|
||||
file_extension = (
|
||||
file.filename.rsplit('.', 1)[1].lower()
|
||||
if '.' in file.filename
|
||||
else None
|
||||
)
|
||||
max_file_size = current_app.config['max_single_file_size']
|
||||
|
||||
if not (
|
||||
file_extension
|
||||
and file_extension in current_app.config.get(allowed_extensions)
|
||||
):
|
||||
response_object = {
|
||||
'status': 'fail',
|
||||
'message': 'File extension not allowed.',
|
||||
}
|
||||
elif file_extension != 'zip' and req.content_length > max_file_size:
|
||||
response_object = {
|
||||
'status': 'fail',
|
||||
'message': 'Error during picture update, file size exceeds '
|
||||
f'{display_readable_file_size(max_file_size)}.',
|
||||
}
|
||||
code = 413
|
||||
|
||||
return response_object, code
|
||||
|
||||
|
||||
def verify_user(current_request, verify_admin):
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Something went wrong. Please contact us.',
|
||||
}
|
||||
code = 401
|
||||
auth_header = current_request.headers.get('Authorization')
|
||||
if not auth_header:
|
||||
response_object['message'] = 'Provide a valid auth token.'
|
||||
return response_object, code, None
|
||||
auth_token = auth_header.split(" ")[1]
|
||||
resp = User.decode_auth_token(auth_token)
|
||||
if isinstance(resp, str):
|
||||
response_object['message'] = resp
|
||||
return response_object, code, None
|
||||
user = User.query.filter_by(id=resp).first()
|
||||
if not user:
|
||||
return response_object, code, None
|
||||
if verify_admin and not is_admin(resp):
|
||||
response_object['message'] = 'You do not have permissions.'
|
||||
return response_object, 403, None
|
||||
return None, None, resp
|
||||
|
||||
|
||||
def authenticate(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
verify_admin = False
|
||||
response_object, code, resp = verify_user(request, verify_admin)
|
||||
if response_object:
|
||||
return jsonify(response_object), code
|
||||
return f(resp, *args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
def authenticate_as_admin(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
verify_admin = True
|
||||
response_object, code, resp = verify_user(request, verify_admin)
|
||||
if response_object:
|
||||
return jsonify(response_object), code
|
||||
return f(resp, *args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
def can_view_activity(auth_user_id, activity_user_id):
|
||||
if auth_user_id != activity_user_id:
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'You do not have permissions.',
|
||||
}
|
||||
return response_object, 403
|
||||
return None, None
|
||||
|
||||
|
||||
def display_readable_file_size(size_in_bytes):
|
||||
if size_in_bytes == 0:
|
||||
return '0 bytes'
|
||||
if size_in_bytes == 1:
|
||||
return '1 byte'
|
||||
for unit in [' bytes', 'KB', 'MB', 'GB', 'TB']:
|
||||
if abs(size_in_bytes) < 1024.0:
|
||||
return f"{size_in_bytes:3.1f}{unit}"
|
||||
size_in_bytes /= 1024.0
|
||||
return f"{size_in_bytes} bytes"
|
||||
|
||||
|
||||
def get_readable_duration(duration, locale='en'):
|
||||
if locale is not None and locale != 'en':
|
||||
_t = humanize.i18n.activate(locale) # noqa
|
||||
readable_duration = humanize.naturaldelta(timedelta(seconds=duration))
|
||||
if locale is not None and locale != 'en':
|
||||
humanize.i18n.deactivate()
|
||||
return readable_duration
|
33
fittrackee/users/utils_token.py
Normal file
33
fittrackee/users/utils_token.py
Normal file
@ -0,0 +1,33 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import jwt
|
||||
from flask import current_app
|
||||
|
||||
|
||||
def get_user_token(user_id, password_reset=False):
|
||||
expiration_days = (
|
||||
0
|
||||
if password_reset
|
||||
else current_app.config.get('TOKEN_EXPIRATION_DAYS')
|
||||
)
|
||||
expiration_seconds = (
|
||||
current_app.config.get('PASSWORD_TOKEN_EXPIRATION_SECONDS')
|
||||
if password_reset
|
||||
else current_app.config.get('TOKEN_EXPIRATION_SECONDS')
|
||||
)
|
||||
payload = {
|
||||
'exp': datetime.utcnow()
|
||||
+ timedelta(days=expiration_days, seconds=expiration_seconds),
|
||||
'iat': datetime.utcnow(),
|
||||
'sub': user_id,
|
||||
}
|
||||
return jwt.encode(
|
||||
payload,
|
||||
current_app.config.get('SECRET_KEY'),
|
||||
algorithm='HS256',
|
||||
)
|
||||
|
||||
|
||||
def decode_user_token(auth_token):
|
||||
payload = jwt.decode(auth_token, current_app.config.get('SECRET_KEY'))
|
||||
return payload['sub']
|
Reference in New Issue
Block a user