Added logging of ip address (#3237)

Refactored Response(json.dumps -> make_response(jsonify..)
Update mimetypes - Allow different mimetypes for download and file upload check (#3245, #3243)
Bugfixes from tests
Updated optional-requirements
This commit is contained in:
Ozzie Isaacs
2024-12-07 07:27:57 +01:00
11 changed files with 166 additions and 249 deletions
+3 -3
View File
@@ -55,13 +55,13 @@ mimetypes.init()
mimetypes.add_type('application/xhtml+xml', '.xhtml')
mimetypes.add_type('application/epub+zip', '.epub')
mimetypes.add_type('application/epub+zip', '.kepub')
mimetypes.add_type('text/xml', '.fb2')
mimetypes.add_type('application/fb2+zip', '.fb2')
mimetypes.add_type('application/octet-stream', '.mobi')
mimetypes.add_type('application/octet-stream', '.prc')
mimetypes.add_type('application/vnd.amazon.ebook', '.azw')
mimetypes.add_type('application/x-mobi8-ebook', '.azw3')
mimetypes.add_type('application/x-rar', '.cbr')
mimetypes.add_type('application/zip', '.cbz')
mimetypes.add_type('application/x-cbr', '.cbr')
mimetypes.add_type('application/x-cbz', '.cbz')
mimetypes.add_type('application/x-tar', '.cbt')
mimetypes.add_type('application/x-7z-compressed', '.cb7')
mimetypes.add_type('image/vnd.djv', '.djv')
+11 -13
View File
@@ -32,7 +32,8 @@ from datetime import time as datetime_time
from functools import wraps
from urllib.parse import urlparse
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, \
send_from_directory, g, jsonify
from markupsafe import Markup
from .cw_login import current_user
from flask_babel import gettext as _
@@ -378,10 +379,7 @@ def list_users():
user.default = get_user_locale_language(user.default_language)
table_entries = {'totalNotFiltered': total_count, 'total': filtered_count, "rows": users}
js_list = json.dumps(table_entries, cls=db.AlchemyEncoder)
response = make_response(js_list)
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
return make_response(json.dumps(table_entries, cls=db.AlchemyEncoder))
@admi.route("/ajax/deleteuser", methods=['POST'])
@@ -400,7 +398,7 @@ def delete_user():
success = list()
if not users:
log.error("User not found")
return Response(json.dumps({'type': "danger", 'message': _("User not found")}), mimetype='application/json')
return make_response(jsonify(type="danger", message=_("User not found")))
for user in users:
try:
message = _delete_user(user)
@@ -416,7 +414,7 @@ def delete_user():
log.info("Users {} deleted".format(user_ids))
success = [{'type': "success", 'message': _("{} users deleted successfully").format(count)}]
success.extend(errors)
return Response(json.dumps(success), mimetype='application/json')
return make_response(jsonify(success))
@admi.route("/ajax/getlocale")
@@ -498,10 +496,10 @@ def edit_list_user(param):
if not ub.session.query(ub.User). \
filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
ub.User.id != user.id).count():
return Response(
json.dumps([{'type': "danger",
return make_response(
jsonify([{'type': "danger",
'message': _("No admin user remaining, can't remove admin role",
nick=user.name)}]), mimetype='application/json')
nick=user.name)}]))
user.role &= ~value
else:
raise Exception(_("Value has to be true or false"))
@@ -947,7 +945,7 @@ def do_full_kobo_sync(userid):
count = ub.session.query(ub.KoboSyncedBooks).filter(userid == ub.KoboSyncedBooks.user_id).delete()
message = _("{} sync entries deleted").format(count)
ub.session_commit(message)
return Response(json.dumps([{"type": "success", "message": message}]), mimetype='application/json')
return make_response(jsonify(type="success", message=message))
def check_valid_read_column(column):
@@ -1264,7 +1262,7 @@ def _configuration_ldap_helper(to_save):
@admin_required
def simulatedbchange():
db_change, db_valid = _db_simulate_change()
return Response(json.dumps({"change": db_change, "valid": db_valid}), mimetype='application/json')
return make_response(jsonify(change=db_change, valid=db_valid))
@admi.route("/admin/user/new", methods=["GET", "POST"])
@@ -1896,7 +1894,7 @@ def _configuration_result(error_flash=None, reboot=False):
resp['result'] = [{'type': "success", 'message': _("Calibre-Web configuration updated")}]
resp['reboot'] = reboot
resp['config_upload'] = config.config_upload_formats
return Response(json.dumps(resp), mimetype='application/json')
return make_response(jsonify(resp))
def _db_configuration_result(error_flash=None, gdrive_error=None):
+45 -61
View File
@@ -28,11 +28,11 @@ from shutil import copyfile
from markupsafe import escape, Markup # dependency of flask
from functools import wraps
from flask import Blueprint, request, flash, redirect, url_for, abort, Response
from flask import Blueprint, request, flash, redirect, url_for, abort, jsonify, make_response, Response
from flask_babel import gettext as _
from flask_babel import lazy_gettext as N_
from flask_babel import get_locale
from .cw_login import current_user, login_required
from .cw_login import current_user
from sqlalchemy.exc import OperationalError, IntegrityError, InterfaceError
from sqlalchemy.orm.exc import StaleDataError
from sqlalchemy.sql.expression import func
@@ -76,7 +76,7 @@ def edit_required(f):
@editbook.route("/ajax/delete/<int:book_id>", methods=["POST"])
@user_login_required
def delete_book_from_details(book_id):
return Response(delete_book_from_table(book_id, "", True), mimetype='application/json')
return delete_book_from_table(book_id, "", True) # , mimetype='application/json')
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""}, methods=["POST"])
@@ -158,16 +158,16 @@ def upload():
if len(request.files.getlist("btn-upload")) < 2:
if current_user.role_edit() or current_user.role_admin():
resp = {"location": url_for('edit-book.show_edit_book', book_id=book_id)}
return Response(json.dumps(resp), mimetype='application/json')
return make_response(jsonify(resp))
else:
resp = {"location": url_for('web.show_book', book_id=book_id)}
return Response(json.dumps(resp), mimetype='application/json')
return make_response(jsonify(resp))
except (OperationalError, IntegrityError, StaleDataError) as e:
calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e))
flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e),
category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
return make_response(jsonify(location=url_for("web.index")))
abort(404)
@@ -206,7 +206,7 @@ def table_get_custom_enum(c_id):
ret.append({'value': "", 'text': ""})
for idx, en in enumerate(cc.get_display_dict()['enum_values']):
ret.append({'value': en, 'text': en})
return json.dumps(ret)
return make_response(jsonify(ret))
@editbook.route("/ajax/editbooks/<param>", methods=['POST'])
@@ -221,68 +221,54 @@ def edit_list_book(param):
try:
if param == 'series_index':
edit_book_series_index(vals['value'], book)
ret = Response(json.dumps({'success': True, 'newValue': book.series_index}), mimetype='application/json')
ret = make_response(jsonify(success=True, newValue=book.series_index))
elif param == 'tags':
edit_book_tags(vals['value'], book)
ret = Response(json.dumps({'success': True, 'newValue': ', '.join([tag.name for tag in book.tags])}),
mimetype='application/json')
ret = make_response(jsonify(success=True, newValue=', '.join([tag.name for tag in book.tags])))
elif param == 'series':
edit_book_series(vals['value'], book)
ret = Response(json.dumps({'success': True, 'newValue': ', '.join([serie.name for serie in book.series])}),
mimetype='application/json')
ret = make_response(jsonify(success=True, newValue=', '.join([serie.name for serie in book.series])))
elif param == 'publishers':
edit_book_publisher(vals['value'], book)
ret = Response(json.dumps({'success': True,
'newValue': ', '.join([publisher.name for publisher in book.publishers])}),
mimetype='application/json')
ret = make_response(jsonify(success=True,
newValue=', '.join([publisher.name for publisher in book.publishers])))
elif param == 'languages':
invalid = list()
edit_book_languages(vals['value'], book, invalid=invalid)
if invalid:
ret = Response(json.dumps({'success': False,
'msg': 'Invalid languages in request: {}'.format(','.join(invalid))}),
mimetype='application/json')
ret = make_response(jsonify(success=False,
msg='Invalid languages in request: {}'.format(','.join(invalid))))
else:
lang_names = list()
for lang in book.languages:
lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code))
ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}),
mimetype='application/json')
ret = make_response(jsonify(success=True, newValue=', '.join(lang_names)))
elif param == 'author_sort':
book.author_sort = vals['value']
ret = Response(json.dumps({'success': True, 'newValue': book.author_sort}),
mimetype='application/json')
ret = make_response(jsonify(success=True, newValue=book.author_sort))
elif param == 'title':
sort_param = book.sort
if handle_title_on_edit(book, vals.get('value', "")):
rename_error = helper.update_dir_structure(book.id, config.get_book_path())
if not rename_error:
ret = Response(json.dumps({'success': True, 'newValue': book.title}),
mimetype='application/json')
ret = make_response(jsonify(success=True, newValue=book.title))
else:
ret = Response(json.dumps({'success': False,
'msg': rename_error}),
mimetype='application/json')
ret = make_response(jsonify(success=False, msg=rename_error))
elif param == 'sort':
book.sort = vals['value']
ret = Response(json.dumps({'success': True, 'newValue': book.sort}),
mimetype='application/json')
ret = make_response(jsonify(success=True,newValue=book.sort))
elif param == 'comments':
edit_book_comments(vals['value'], book)
ret = Response(json.dumps({'success': True, 'newValue': book.comments[0].text}),
mimetype='application/json')
ret = make_response(jsonify(success=True, newValue=book.comments[0].text))
elif param == 'authors':
input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true")
rename_error = helper.update_dir_structure(book.id, config.get_book_path(), input_authors[0])
if not rename_error:
ret = Response(json.dumps({
'success': True,
'newValue': ' & '.join([author.replace('|', ',') for author in input_authors])}),
mimetype='application/json')
ret = make_response(jsonify(
success=True,
newValue=' & '.join([author.replace('|', ',') for author in input_authors])))
else:
ret = Response(json.dumps({'success': False,
'msg': rename_error}),
mimetype='application/json')
ret = make_response(jsonify(success=False, msg=rename_error))
elif param == 'is_archived':
is_archived = change_archived_books(book.id, vals['value'] == "True",
message="Book {} archive bit set to: {}".format(book.id, vals['value']))
@@ -301,8 +287,7 @@ def edit_list_book(param):
if vals['value'] in ["True", "False"]:
ret = ""
else:
ret = Response(json.dumps({'success': True, 'newValue': vals['value']}),
mimetype='application/json')
ret = make_response(jsonify(success=True, newValue=vals['value']))
else:
return _("Parameter not found"), 400
book.last_modified = datetime.now(timezone.utc)
@@ -315,9 +300,8 @@ def edit_list_book(param):
except (OperationalError, IntegrityError, StaleDataError) as e:
calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e))
ret = Response(json.dumps({'success': False,
'msg': 'Database error: {}'.format(e.orig if hasattr(e, "orig") else e)}),
mimetype='application/json')
ret = make_response(jsonify(success=False,
msg='Database error: {}'.format(e.orig if hasattr(e, "orig") else e)))
return ret
@@ -328,13 +312,13 @@ def get_sorted_entry(field, bookid):
book = calibre_db.get_filtered_book(bookid)
if book:
if field == 'title':
return json.dumps({'sort': book.sort})
return make_response(jsonify(sort=book.sort))
elif field == 'authors':
return json.dumps({'author_sort': book.author_sort})
return make_response(jsonify(author_sort=book.author_sort))
if field == 'sort':
return json.dumps({'sort': book.title})
return make_response(jsonify(sort=book.title))
if field == 'author_sort':
return json.dumps({'authors': " & ".join([a.name for a in calibre_db.order_authors([book])])})
return make_response(jsonify(authors=" & ".join([a.name for a in calibre_db.order_authors([book])])))
return ""
@@ -350,7 +334,7 @@ def simulate_merge_list_book():
from_book = []
for book_id in vals:
from_book.append(calibre_db.get_book(book_id).title)
return json.dumps({'to': to_book, 'from': from_book})
return make_response(jsonify({'to': to_book, 'from': from_book}))
return ""
@@ -388,7 +372,7 @@ def merge_list_book():
element.uncompressed_size,
to_name))
delete_book_from_table(from_book.id, "", True)
return json.dumps({'success': True})
return make_response(jsonify(success=True))
return ""
@@ -428,11 +412,11 @@ def table_xchange_author_title():
except (OperationalError, IntegrityError, StaleDataError) as e:
calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e))
return json.dumps({'success': False})
return make_response(jsonify(success=False))
if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal()
return json.dumps({'success': True})
return make_response(jsonify(success=True))
return ""
@@ -560,7 +544,7 @@ def do_edit_book(book_id, upload_formats=None):
if upload_formats:
resp = {"location": url_for('edit-book.show_edit_book', book_id=book_id)}
return Response(json.dumps(resp), mimetype='application/json')
return make_response(jsonify(resp))
if "detail_view" in to_save:
return redirect(url_for('web.show_book', book_id=book.id))
@@ -769,17 +753,17 @@ def file_handling_on_upload(requested_file):
if config.config_check_extensions and allowed_extensions != ['']:
if not validate_mime_type(requested_file, allowed_extensions):
flash(_("File type isn't allowed to be uploaded to this server"), category="error")
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
return None, make_response(jsonify(location=url_for("web.index")))
if '.' in requested_file.filename:
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
if file_ext not in allowed_extensions and '' not in allowed_extensions:
flash(
_("File extension '%(ext)s' is not allowed to be uploaded to this server",
ext=file_ext), category="error")
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
return None, make_response(jsonify(location=url_for("web.index")))
else:
flash(_('File to be uploaded must have an extension'), category="error")
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
return None, make_response(jsonify(location=url_for("web.index")))
# extract metadata from file
try:
@@ -788,7 +772,7 @@ def file_handling_on_upload(requested_file):
log.error("File %s could not saved to temp dir", requested_file.filename)
flash(_("File %(filename)s could not saved to temp dir",
filename=requested_file.filename), category="error")
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
return None, make_response(jsonify(location=url_for("web.index")))
return meta, None
@@ -860,7 +844,7 @@ def delete_whole_book(book_id, book):
def render_delete_book_result(book_format, json_response, warning, book_id, location=""):
if book_format:
if json_response:
return json.dumps([warning, {"location": url_for("edit-book.show_edit_book", book_id=book_id),
return jsonify([warning, {"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "success",
"format": book_format,
"message": _('Book Format Successfully Deleted')}])
@@ -869,7 +853,7 @@ def render_delete_book_result(book_format, json_response, warning, book_id, loca
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
else:
if json_response:
return json.dumps([warning, {"location": get_redirect_location(location, "web.index"),
return jsonify([warning, {"location": get_redirect_location(location, "web.index"),
"type": "success",
"format": book_format,
"message": _('Book Successfully Deleted')}])
@@ -887,7 +871,7 @@ def delete_book_from_table(book_id, book_format, json_response, location=""):
result, error = helper.delete_book(book, config.get_book_path(), book_format=book_format.upper())
if not result:
if json_response:
return json.dumps([{"location": url_for("edit-book.show_edit_book", book_id=book_id),
return jsonify([{"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "danger",
"format": "",
"message": error}])
@@ -914,7 +898,7 @@ def delete_book_from_table(book_id, book_format, json_response, location=""):
log.error_or_exception(ex)
calibre_db.session.rollback()
if json_response:
return json.dumps([{"location": url_for("edit-book.show_edit_book", book_id=book_id),
return jsonify([{"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "danger",
"format": "",
"message": ex}])
@@ -928,7 +912,7 @@ def delete_book_from_table(book_id, book_format, json_response, location=""):
return render_delete_book_result(book_format, json_response, warning, book_id, location)
message = _("You are missing permissions to delete books")
if json_response:
return json.dumps({"location": url_for("edit-book.show_edit_book", book_id=book_id),
return jsonify({"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "danger",
"format": "",
"message": message})
+10 -1
View File
@@ -34,6 +34,15 @@ except ImportError as e:
error = "Cannot import python-magic, checking uploaded file metadata will not work: {}".format(e)
def get_mimetype(ext):
# overwrite some mimetypes for proper file detection
mimes = {".fb2": "text/xml",
".cbz": "application/zip",
".cbr": "application/x-rar"
}
return mimes.get(ext, mimetypes.types_map[ext])
def get_temp_dir():
tmp_dir = os.path.join(gettempdir(), 'calibre_web')
if not os.path.isdir(tmp_dir):
@@ -54,7 +63,7 @@ def validate_mime_type(file_buffer, allowed_extensions):
allowed_mimetypes = list()
for x in allowed_extensions:
try:
allowed_mimetypes.append(mimetypes.types_map["." + x])
allowed_mimetypes.append(get_mimetype("." + x))
except KeyError:
log.error("Unkown mimetype for Extension: {}".format(x))
tmp_mime_type = mime.from_buffer(file_buffer.read())
+3 -2
View File
@@ -30,7 +30,7 @@ import requests
import unidecode
from uuid import uuid4
from flask import send_from_directory, make_response, abort, url_for, Response
from flask import send_from_directory, make_response, abort, url_for, Response, request
from flask_babel import gettext as _
from flask_babel import lazy_gettext as N_
from flask_babel import get_locale
@@ -974,7 +974,8 @@ def do_download_file(book, book_format, client, data, headers):
# ToDo Check headers parameter
for element in headers:
response.headers[element[0]] = element[1]
log.info('Downloading file: {}'.format(os.path.join(filename, book_name + "." + book_format)))
log.info('Downloading file: \'%s\' by %s - %s', format(os.path.join(filename, book_name + "." + book_format)),
current_user.name, request.headers.get('X-Forwarded-For', request.remote_addr))
return response
+3 -3
View File
@@ -21,10 +21,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
import json
# import json
from urllib.parse import unquote_plus
from flask import Blueprint, request, render_template, make_response, abort, Response, g
from flask import Blueprint, request, render_template, make_response, abort, g, jsonify
from flask_babel import get_locale
from flask_babel import gettext as _
@@ -451,7 +451,7 @@ def get_database_stats():
stat['authors'] = calibre_db.session.query(db.Authors).count()
stat['categories'] = calibre_db.session.query(db.Tags).count()
stat['series'] = calibre_db.session.query(db.Series).count()
return Response(json.dumps(stat), mimetype="application/json")
return make_response(jsonify(stat))
@opds.route("/opds/thumb_240_240/<book_id>")
+4 -6
View File
@@ -23,7 +23,7 @@ import json
import os
import sys
from flask import Blueprint, Response, request, url_for
from flask import Blueprint, request, url_for, make_response, jsonify
from .cw_login import current_user
from flask_babel import get_locale
from sqlalchemy.exc import InvalidRequestError, OperationalError
@@ -89,7 +89,7 @@ def metadata_provider():
provider.append(
{"name": c.__name__, "active": ac, "initial": ac, "id": c.__id__}
)
return Response(json.dumps(provider), mimetype="application/json")
return make_response(jsonify(provider))
@meta.route("/metadata/provider", methods=["POST"])
@@ -114,9 +114,7 @@ def metadata_change_active_provider(prov_name):
provider = next((c for c in cl if c.__id__ == prov_name), None)
if provider is not None:
data = provider.search(new_state.get("query", ""))
return Response(
json.dumps([asdict(x) for x in data]), mimetype="application/json"
)
return make_response(jsonify([asdict(x) for x in data]))
return ""
@@ -138,4 +136,4 @@ def metadata_search():
}
for future in concurrent.futures.as_completed(meta):
data.extend([asdict(x) for x in future.result() if x])
return Response(json.dumps(data), mimetype="application/json")
return make_response(jsonify(data))
+7 -7
View File
@@ -210,13 +210,13 @@ class TaskConvert(CalibreTask):
else:
error_message = N_('%(format)s format not found on disk', format=format_new_ext.upper())
local_db.session.close()
log.info("ebook converter failed with error while converting book")
if not error_message:
error_message = N_('Ebook converter failed with unknown error')
else:
log.error(error_message)
self._handleError(error_message)
return
log.info("ebook converter failed with error while converting book")
if not error_message:
error_message = N_('Ebook converter failed with unknown error')
else:
log.error(error_message)
self._handleError(error_message)
return
def _convert_kepubify(self, file_path, format_old_ext, format_new_ext):
if config.config_embed_metadata and config.config_binariesdir:
+7 -10
View File
@@ -25,8 +25,7 @@ import chardet # dependency of requests
import copy
from importlib.metadata import metadata
from flask import Blueprint, jsonify
from flask import request, redirect, send_from_directory, make_response, flash, abort, url_for, Response
from flask import Blueprint, jsonify, request, redirect, send_from_directory, make_response, flash, abort, url_for
from flask import session as flask_session
from flask_babel import gettext as _
from flask_babel import get_locale
@@ -1198,13 +1197,14 @@ def serve_book(book_id, book_format, anyname):
if not data:
return "File not in Database"
range_header = request.headers.get('Range', None)
if not range_header:
log.info('Serving book: \'%s\' to %s - %s', data.name, current_user.name,
request.headers.get('X-Forwarded-For', request.remote_addr))
if config.config_use_google_drive:
try:
headers = Headers()
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
if not range_header:
log.info('Serving book: %s', data.name)
if not range_header:
headers['Accept-Ranges'] = 'bytes'
df = getFileFromEbooksFolder(book.path, data.name + "." + book_format)
return do_gdrive_download(df, headers, (book_format.upper() == 'TXT'))
@@ -1213,7 +1213,6 @@ def serve_book(book_id, book_format, anyname):
return "File Not Found"
else:
if book_format.upper() == 'TXT':
log.info('Serving book: %s', data.name)
try:
rawdata = open(os.path.join(config.get_book_path(), book.path, data.name + "." + book_format),
"rb").read()
@@ -1234,7 +1233,6 @@ def serve_book(book_id, book_format, anyname):
response = make_response(
send_from_directory(os.path.join(config.get_book_path(), book.path), data.name + "." + book_format))
if not range_header:
log.info('Serving book: %s', data.name)
response.headers['Accept-Ranges'] = 'bytes'
return response
@@ -1253,8 +1251,7 @@ def download_link(book_id, book_format, anyname):
@download_required
def send_to_ereader(book_id, book_format, convert):
if not config.get_mail_server_configured():
response = [{'type': "danger", 'message': _("Please configure the SMTP mail settings first...")}]
return Response(json.dumps(response), mimetype='application/json')
return make_response(jsonify(type="danger", message=_("Please configure the SMTP mail settings first...")))
elif current_user.kindle_mail:
result = send_mail(book_id, book_format, convert, current_user.kindle_mail, config.get_book_path(),
current_user.name)
@@ -1266,7 +1263,7 @@ def send_to_ereader(book_id, book_format, convert):
response = [{'type': "danger", 'message': _("Oops! There was an error sending book: %(res)s", res=result)}]
else:
response = [{'type': "danger", 'message': _("Oops! Please update your profile with a valid eReader Email.")}]
return Response(json.dumps(response), mimetype='application/json')
return make_response(jsonify(response))
# ################################### Login Logout ##################################################################