mirror of
https://github.com/janeczku/calibre-web
synced 2024-11-08 11:00:00 +00:00
proxy login is now no longer saving cookies,
Cookies are saved in database for better Invalidation Cookies expiry date is saved in database for further deletion (missing) Database conversion is missing
This commit is contained in:
parent
2d470e0ce1
commit
ebe7cd7ba4
@ -21,10 +21,10 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from flask_login import LoginManager, confirm_login
|
from .cw_login import LoginManager, confirm_login
|
||||||
from flask import session, current_app
|
from flask import session, current_app
|
||||||
from flask_login.utils import decode_cookie
|
from .cw_login.utils import decode_cookie
|
||||||
from flask_login.signals import user_loaded_from_cookie
|
from .cw_login.signals import user_loaded_from_cookie
|
||||||
|
|
||||||
|
|
||||||
class MyLoginManager(LoginManager):
|
class MyLoginManager(LoginManager):
|
||||||
@ -43,7 +43,7 @@ class MyLoginManager(LoginManager):
|
|||||||
session["_fresh"] = False
|
session["_fresh"] = False
|
||||||
user = None
|
user = None
|
||||||
if self._user_callback:
|
if self._user_callback:
|
||||||
user = self._user_callback(user_id)
|
user = self._user_callback(user_id, None, None)
|
||||||
if user is not None:
|
if user is not None:
|
||||||
app = current_app._get_current_object()
|
app = current_app._get_current_object()
|
||||||
user_loaded_from_cookie.send(app, user=user)
|
user_loaded_from_cookie.send(app, user=user)
|
||||||
@ -51,3 +51,4 @@ class MyLoginManager(LoginManager):
|
|||||||
confirm_login()
|
confirm_login()
|
||||||
return user
|
return user
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -83,8 +83,8 @@ log = logger.create()
|
|||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.update(
|
app.config.update(
|
||||||
SESSION_COOKIE_HTTPONLY=True,
|
SESSION_COOKIE_HTTPONLY=True,
|
||||||
SESSION_COOKIE_SAMESITE='Lax',
|
SESSION_COOKIE_SAMESITE='Strict',
|
||||||
REMEMBER_COOKIE_SAMESITE='Lax', # will be available in flask-login 0.5.1 earliest
|
REMEMBER_COOKIE_SAMESITE='Strict', # will be available in flask-login 0.5.1 earliest
|
||||||
WTF_CSRF_SSL_STRICT=False
|
WTF_CSRF_SSL_STRICT=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -26,12 +26,12 @@ import sqlite3
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
import flask_login
|
|
||||||
import jinja2
|
import jinja2
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
from . import db, calibre_db, converter, uploader, constants, dep_check
|
from . import db, calibre_db, converter, uploader, constants, dep_check
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
|
from .usermanagement import user_login_required
|
||||||
|
|
||||||
|
|
||||||
about = flask.Blueprint('about', __name__)
|
about = flask.Blueprint('about', __name__)
|
||||||
@ -74,7 +74,7 @@ def collect_stats():
|
|||||||
|
|
||||||
|
|
||||||
@about.route("/stats")
|
@about.route("/stats")
|
||||||
@flask_login.login_required
|
@user_login_required
|
||||||
def stats():
|
def stats():
|
||||||
counter = calibre_db.session.query(db.Books).count()
|
counter = calibre_db.session.query(db.Books).count()
|
||||||
authors = calibre_db.session.query(db.Authors).count()
|
authors = calibre_db.session.query(db.Authors).count()
|
||||||
|
122
cps/admin.py
122
cps/admin.py
@ -34,10 +34,9 @@ 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, Response
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
from flask_login import login_required, current_user, logout_user
|
from .cw_login import current_user
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import get_locale, format_time, format_datetime, format_timedelta
|
from flask_babel import get_locale, format_time, format_datetime, format_timedelta
|
||||||
from flask import session as flask_session
|
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
from sqlalchemy.orm.attributes import flag_modified
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
|
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
|
||||||
@ -52,6 +51,7 @@ from .embed_helper import get_calibre_binarypath
|
|||||||
from .gdriveutils import is_gdrive_ready, gdrive_support
|
from .gdriveutils import is_gdrive_ready, gdrive_support
|
||||||
from .render_template import render_title_template, get_sidebar_config
|
from .render_template import render_title_template, get_sidebar_config
|
||||||
from .services.worker import WorkerThread
|
from .services.worker import WorkerThread
|
||||||
|
from .usermanagement import user_login_required
|
||||||
from .babel import get_available_translations, get_available_locale, get_user_locale_language
|
from .babel import get_available_translations, get_available_locale, get_user_locale_language
|
||||||
from . import debug_info
|
from . import debug_info
|
||||||
|
|
||||||
@ -103,13 +103,13 @@ def admin_required(f):
|
|||||||
|
|
||||||
@admi.before_app_request
|
@admi.before_app_request
|
||||||
def before_request():
|
def before_request():
|
||||||
try:
|
#try:
|
||||||
if not ub.check_user_session(current_user.id,
|
#if not ub.check_user_session(current_user.id,
|
||||||
flask_session.get('_id')) and 'opds' not in request.path \
|
# flask_session.get('_id')) and 'opds' not in request.path \
|
||||||
and config.config_session == 1:
|
# and config.config_session == 1:
|
||||||
logout_user()
|
# logout_user()
|
||||||
except AttributeError:
|
#except AttributeError:
|
||||||
pass # ? fails on requesting /ajax/emailstat during restart ?
|
# pass # ? fails on requesting /ajax/emailstat during restart ?
|
||||||
g.constants = constants
|
g.constants = constants
|
||||||
g.google_site_verification = os.getenv('GOOGLE_SITE_VERIFICATION', '')
|
g.google_site_verification = os.getenv('GOOGLE_SITE_VERIFICATION', '')
|
||||||
g.allow_registration = config.config_public_reg
|
g.allow_registration = config.config_public_reg
|
||||||
@ -129,14 +129,14 @@ def before_request():
|
|||||||
return redirect(url_for('admin.db_configuration'))
|
return redirect(url_for('admin.db_configuration'))
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin")
|
#@admi.route("/admin")
|
||||||
@login_required
|
#@user_login_required
|
||||||
def admin_forbidden():
|
#def admin_forbidden():
|
||||||
abort(403)
|
# abort(403)
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/shutdown", methods=["POST"])
|
@admi.route("/shutdown", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def shutdown():
|
def shutdown():
|
||||||
task = request.get_json().get('parameter', -1)
|
task = request.get_json().get('parameter', -1)
|
||||||
@ -165,7 +165,7 @@ def shutdown():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/metadata_backup", methods=["POST"])
|
@admi.route("/metadata_backup", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def queue_metadata_backup():
|
def queue_metadata_backup():
|
||||||
show_text = {}
|
show_text = {}
|
||||||
@ -189,7 +189,7 @@ def reconnect():
|
|||||||
|
|
||||||
@admi.route("/ajax/updateThumbnails", methods=['POST'])
|
@admi.route("/ajax/updateThumbnails", methods=['POST'])
|
||||||
@admin_required
|
@admin_required
|
||||||
@login_required
|
@user_login_required
|
||||||
def update_thumbnails():
|
def update_thumbnails():
|
||||||
content = config.get_scheduled_task_settings()
|
content = config.get_scheduled_task_settings()
|
||||||
if content['schedule_generate_book_covers']:
|
if content['schedule_generate_book_covers']:
|
||||||
@ -199,7 +199,7 @@ def update_thumbnails():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/view")
|
@admi.route("/admin/view")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def admin():
|
def admin():
|
||||||
version = updater_thread.get_current_version_info()
|
version = updater_thread.get_current_version_info()
|
||||||
@ -233,7 +233,7 @@ def admin():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/dbconfig", methods=["GET", "POST"])
|
@admi.route("/admin/dbconfig", methods=["GET", "POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def db_configuration():
|
def db_configuration():
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@ -242,7 +242,7 @@ def db_configuration():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/config", methods=["GET"])
|
@admi.route("/admin/config", methods=["GET"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def configuration():
|
def configuration():
|
||||||
return render_title_template("config_edit.html",
|
return render_title_template("config_edit.html",
|
||||||
@ -253,28 +253,28 @@ def configuration():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/ajaxconfig", methods=["POST"])
|
@admi.route("/admin/ajaxconfig", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def ajax_config():
|
def ajax_config():
|
||||||
return _configuration_update_helper()
|
return _configuration_update_helper()
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/ajaxdbconfig", methods=["POST"])
|
@admi.route("/admin/ajaxdbconfig", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def ajax_db_config():
|
def ajax_db_config():
|
||||||
return _db_configuration_update_helper()
|
return _db_configuration_update_helper()
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/alive", methods=["GET"])
|
@admi.route("/admin/alive", methods=["GET"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def calibreweb_alive():
|
def calibreweb_alive():
|
||||||
return "", 200
|
return "", 200
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/viewconfig")
|
@admi.route("/admin/viewconfig")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def view_configuration():
|
def view_configuration():
|
||||||
read_column = calibre_db.session.query(db.CustomColumns) \
|
read_column = calibre_db.session.query(db.CustomColumns) \
|
||||||
@ -291,7 +291,7 @@ def view_configuration():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/usertable")
|
@admi.route("/admin/usertable")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def edit_user_table():
|
def edit_user_table():
|
||||||
visibility = current_user.view_settings.get('useredit', {})
|
visibility = current_user.view_settings.get('useredit', {})
|
||||||
@ -326,7 +326,7 @@ def edit_user_table():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/listusers")
|
@admi.route("/ajax/listusers")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def list_users():
|
def list_users():
|
||||||
off = int(request.args.get("offset") or 0)
|
off = int(request.args.get("offset") or 0)
|
||||||
@ -377,7 +377,7 @@ def list_users():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/deleteuser", methods=['POST'])
|
@admi.route("/ajax/deleteuser", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def delete_user():
|
def delete_user():
|
||||||
user_ids = request.form.to_dict(flat=False)
|
user_ids = request.form.to_dict(flat=False)
|
||||||
@ -412,7 +412,7 @@ def delete_user():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/getlocale")
|
@admi.route("/ajax/getlocale")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def table_get_locale():
|
def table_get_locale():
|
||||||
locale = get_available_locale()
|
locale = get_available_locale()
|
||||||
@ -424,7 +424,7 @@ def table_get_locale():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/getdefaultlanguage")
|
@admi.route("/ajax/getdefaultlanguage")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def table_get_default_lang():
|
def table_get_default_lang():
|
||||||
languages = calibre_db.speaking_language()
|
languages = calibre_db.speaking_language()
|
||||||
@ -436,7 +436,7 @@ def table_get_default_lang():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/editlistusers/<param>", methods=['POST'])
|
@admi.route("/ajax/editlistusers/<param>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def edit_list_user(param):
|
def edit_list_user(param):
|
||||||
vals = request.form.to_dict(flat=False)
|
vals = request.form.to_dict(flat=False)
|
||||||
@ -541,7 +541,7 @@ def edit_list_user(param):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/user_table_settings", methods=['POST'])
|
@admi.route("/ajax/user_table_settings", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def update_table_settings():
|
def update_table_settings():
|
||||||
current_user.view_settings['useredit'] = json.loads(request.data)
|
current_user.view_settings['useredit'] = json.loads(request.data)
|
||||||
@ -558,7 +558,7 @@ def update_table_settings():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/viewconfig", methods=["POST"])
|
@admi.route("/admin/viewconfig", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def update_view_configuration():
|
def update_view_configuration():
|
||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
@ -603,7 +603,7 @@ def update_view_configuration():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/loaddialogtexts/<element_id>", methods=['POST'])
|
@admi.route("/ajax/loaddialogtexts/<element_id>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
def load_dialogtexts(element_id):
|
def load_dialogtexts(element_id):
|
||||||
texts = {"header": "", "main": "", "valid": 1}
|
texts = {"header": "", "main": "", "valid": 1}
|
||||||
if element_id == "config_delete_kobo_token":
|
if element_id == "config_delete_kobo_token":
|
||||||
@ -639,7 +639,7 @@ def load_dialogtexts(element_id):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/editdomain/<int:allow>", methods=['POST'])
|
@admi.route("/ajax/editdomain/<int:allow>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def edit_domain(allow):
|
def edit_domain(allow):
|
||||||
# POST /post
|
# POST /post
|
||||||
@ -653,7 +653,7 @@ def edit_domain(allow):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/adddomain/<int:allow>", methods=['POST'])
|
@admi.route("/ajax/adddomain/<int:allow>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def add_domain(allow):
|
def add_domain(allow):
|
||||||
domain_name = request.form.to_dict()['domainname'].replace('*', '%').replace('?', '_').lower()
|
domain_name = request.form.to_dict()['domainname'].replace('*', '%').replace('?', '_').lower()
|
||||||
@ -667,7 +667,7 @@ def add_domain(allow):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/deletedomain", methods=['POST'])
|
@admi.route("/ajax/deletedomain", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def delete_domain():
|
def delete_domain():
|
||||||
try:
|
try:
|
||||||
@ -685,7 +685,7 @@ def delete_domain():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/domainlist/<int:allow>")
|
@admi.route("/ajax/domainlist/<int:allow>")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def list_domain(allow):
|
def list_domain(allow):
|
||||||
answer = ub.session.query(ub.Registration).filter(ub.Registration.allow == allow).all()
|
answer = ub.session.query(ub.Registration).filter(ub.Registration.allow == allow).all()
|
||||||
@ -698,7 +698,7 @@ def list_domain(allow):
|
|||||||
|
|
||||||
@admi.route("/ajax/editrestriction/<int:res_type>", defaults={"user_id": 0}, methods=['POST'])
|
@admi.route("/ajax/editrestriction/<int:res_type>", defaults={"user_id": 0}, methods=['POST'])
|
||||||
@admi.route("/ajax/editrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
@admi.route("/ajax/editrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def edit_restriction(res_type, user_id):
|
def edit_restriction(res_type, user_id):
|
||||||
element = request.form.to_dict()
|
element = request.form.to_dict()
|
||||||
@ -764,14 +764,14 @@ def edit_restriction(res_type, user_id):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/addrestriction/<int:res_type>", methods=['POST'])
|
@admi.route("/ajax/addrestriction/<int:res_type>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def add_user_0_restriction(res_type):
|
def add_user_0_restriction(res_type):
|
||||||
return add_restriction(res_type, 0)
|
return add_restriction(res_type, 0)
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/addrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
@admi.route("/ajax/addrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def add_restriction(res_type, user_id):
|
def add_restriction(res_type, user_id):
|
||||||
element = request.form.to_dict()
|
element = request.form.to_dict()
|
||||||
@ -817,14 +817,14 @@ def add_restriction(res_type, user_id):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/deleterestriction/<int:res_type>", methods=['POST'])
|
@admi.route("/ajax/deleterestriction/<int:res_type>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def delete_user_0_restriction(res_type):
|
def delete_user_0_restriction(res_type):
|
||||||
return delete_restriction(res_type, 0)
|
return delete_restriction(res_type, 0)
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/deleterestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
@admi.route("/ajax/deleterestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def delete_restriction(res_type, user_id):
|
def delete_restriction(res_type, user_id):
|
||||||
element = request.form.to_dict()
|
element = request.form.to_dict()
|
||||||
@ -872,7 +872,7 @@ def delete_restriction(res_type, user_id):
|
|||||||
|
|
||||||
@admi.route("/ajax/listrestriction/<int:res_type>", defaults={"user_id": 0})
|
@admi.route("/ajax/listrestriction/<int:res_type>", defaults={"user_id": 0})
|
||||||
@admi.route("/ajax/listrestriction/<int:res_type>/<int:user_id>")
|
@admi.route("/ajax/listrestriction/<int:res_type>/<int:user_id>")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def list_restriction(res_type, user_id):
|
def list_restriction(res_type, user_id):
|
||||||
if res_type == 0: # Tags as template
|
if res_type == 0: # Tags as template
|
||||||
@ -916,20 +916,20 @@ def list_restriction(res_type, user_id):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/fullsync", methods=["POST"])
|
@admi.route("/ajax/fullsync", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def ajax_self_fullsync():
|
def ajax_self_fullsync():
|
||||||
return do_full_kobo_sync(current_user.id)
|
return do_full_kobo_sync(current_user.id)
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/fullsync/<int:userid>", methods=["POST"])
|
@admi.route("/ajax/fullsync/<int:userid>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def ajax_fullsync(userid):
|
def ajax_fullsync(userid):
|
||||||
return do_full_kobo_sync(userid)
|
return do_full_kobo_sync(userid)
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/pathchooser/")
|
@admi.route("/ajax/pathchooser/")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def ajax_pathchooser():
|
def ajax_pathchooser():
|
||||||
return pathchooser()
|
return pathchooser()
|
||||||
@ -1246,7 +1246,7 @@ def _configuration_ldap_helper(to_save):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/simulatedbchange", methods=['POST'])
|
@admi.route("/ajax/simulatedbchange", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def simulatedbchange():
|
def simulatedbchange():
|
||||||
db_change, db_valid = _db_simulate_change()
|
db_change, db_valid = _db_simulate_change()
|
||||||
@ -1254,7 +1254,7 @@ def simulatedbchange():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/user/new", methods=["GET", "POST"])
|
@admi.route("/admin/user/new", methods=["GET", "POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def new_user():
|
def new_user():
|
||||||
content = ub.User()
|
content = ub.User()
|
||||||
@ -1276,7 +1276,7 @@ def new_user():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/mailsettings", methods=["GET"])
|
@admi.route("/admin/mailsettings", methods=["GET"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def edit_mailsettings():
|
def edit_mailsettings():
|
||||||
content = config.get_mail_settings()
|
content = config.get_mail_settings()
|
||||||
@ -1285,7 +1285,7 @@ def edit_mailsettings():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/mailsettings", methods=["POST"])
|
@admi.route("/admin/mailsettings", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def update_mailsettings():
|
def update_mailsettings():
|
||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
@ -1342,7 +1342,7 @@ def update_mailsettings():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/scheduledtasks")
|
@admi.route("/admin/scheduledtasks")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def edit_scheduledtasks():
|
def edit_scheduledtasks():
|
||||||
content = config.get_scheduled_task_settings()
|
content = config.get_scheduled_task_settings()
|
||||||
@ -1363,7 +1363,7 @@ def edit_scheduledtasks():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/scheduledtasks", methods=["POST"])
|
@admi.route("/admin/scheduledtasks", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def update_scheduledtasks():
|
def update_scheduledtasks():
|
||||||
error = False
|
error = False
|
||||||
@ -1406,7 +1406,7 @@ def update_scheduledtasks():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/user/<int:user_id>", methods=["GET", "POST"])
|
@admi.route("/admin/user/<int:user_id>", methods=["GET", "POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def edit_user(user_id):
|
def edit_user(user_id):
|
||||||
content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User
|
content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User
|
||||||
@ -1435,7 +1435,7 @@ def edit_user(user_id):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/resetpassword/<int:user_id>", methods=["POST"])
|
@admi.route("/admin/resetpassword/<int:user_id>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def reset_user_password(user_id):
|
def reset_user_password(user_id):
|
||||||
if current_user is not None and current_user.is_authenticated:
|
if current_user is not None and current_user.is_authenticated:
|
||||||
@ -1453,7 +1453,7 @@ def reset_user_password(user_id):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/logfile")
|
@admi.route("/admin/logfile")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def view_logfile():
|
def view_logfile():
|
||||||
logfiles = {0: logger.get_logfile(config.config_logfile),
|
logfiles = {0: logger.get_logfile(config.config_logfile),
|
||||||
@ -1467,7 +1467,7 @@ def view_logfile():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/log/<int:logtype>")
|
@admi.route("/ajax/log/<int:logtype>")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def send_logfile(logtype):
|
def send_logfile(logtype):
|
||||||
if logtype == 1:
|
if logtype == 1:
|
||||||
@ -1483,7 +1483,7 @@ def send_logfile(logtype):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/logdownload/<int:logtype>")
|
@admi.route("/admin/logdownload/<int:logtype>")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def download_log(logtype):
|
def download_log(logtype):
|
||||||
if logtype == 0:
|
if logtype == 0:
|
||||||
@ -1498,14 +1498,14 @@ def download_log(logtype):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/debug")
|
@admi.route("/admin/debug")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def download_debug():
|
def download_debug():
|
||||||
return debug_info.send_debug()
|
return debug_info.send_debug()
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/get_update_status", methods=['GET'])
|
@admi.route("/get_update_status", methods=['GET'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def get_update_status():
|
def get_update_status():
|
||||||
if feature_support['updater']:
|
if feature_support['updater']:
|
||||||
@ -1516,7 +1516,7 @@ def get_update_status():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/get_updater_status", methods=['GET', 'POST'])
|
@admi.route("/get_updater_status", methods=['GET', 'POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def get_updater_status():
|
def get_updater_status():
|
||||||
status = {}
|
status = {}
|
||||||
@ -1611,7 +1611,7 @@ def ldap_import_create_user(user, user_data):
|
|||||||
|
|
||||||
|
|
||||||
@admi.route('/import_ldap_users', methods=["POST"])
|
@admi.route('/import_ldap_users', methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def import_ldap_users():
|
def import_ldap_users():
|
||||||
showtext = {}
|
showtext = {}
|
||||||
@ -1666,7 +1666,7 @@ def import_ldap_users():
|
|||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/canceltask", methods=['POST'])
|
@admi.route("/ajax/canceltask", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def cancel_task():
|
def cancel_task():
|
||||||
task_id = request.get_json().get('task_id', None)
|
task_id = request.get_json().get('task_id', None)
|
||||||
|
@ -2,7 +2,7 @@ from babel import negotiate_locale
|
|||||||
from flask_babel import Babel, Locale
|
from flask_babel import Babel, Locale
|
||||||
from babel.core import UnknownLocaleError
|
from babel.core import UnknownLocaleError
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import current_user
|
from .cw_login import current_user
|
||||||
|
|
||||||
from . import logger
|
from . import logger
|
||||||
|
|
||||||
|
98
cps/cw_login/__init__.py
Normal file
98
cps/cw_login/__init__.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# from .__about__ import __version__
|
||||||
|
from .config import AUTH_HEADER_NAME
|
||||||
|
from .config import COOKIE_DURATION
|
||||||
|
from .config import COOKIE_HTTPONLY
|
||||||
|
from .config import COOKIE_NAME
|
||||||
|
from .config import COOKIE_SECURE
|
||||||
|
from .config import ID_ATTRIBUTE
|
||||||
|
from .config import LOGIN_MESSAGE
|
||||||
|
from .config import LOGIN_MESSAGE_CATEGORY
|
||||||
|
from .config import REFRESH_MESSAGE
|
||||||
|
from .config import REFRESH_MESSAGE_CATEGORY
|
||||||
|
from .login_manager import LoginManager
|
||||||
|
from .mixins import AnonymousUserMixin
|
||||||
|
from .mixins import UserMixin
|
||||||
|
from .signals import session_protected
|
||||||
|
from .signals import user_accessed
|
||||||
|
from .signals import user_loaded_from_cookie
|
||||||
|
from .signals import user_loaded_from_request
|
||||||
|
from .signals import user_logged_in
|
||||||
|
from .signals import user_logged_out
|
||||||
|
from .signals import user_login_confirmed
|
||||||
|
from .signals import user_needs_refresh
|
||||||
|
from .signals import user_unauthorized
|
||||||
|
# from .test_client import FlaskLoginClient
|
||||||
|
from .utils import confirm_login
|
||||||
|
from .utils import current_user
|
||||||
|
from .utils import decode_cookie
|
||||||
|
from .utils import encode_cookie
|
||||||
|
from .utils import fresh_login_required
|
||||||
|
from .utils import login_fresh
|
||||||
|
from .utils import login_remembered
|
||||||
|
from .utils import login_required
|
||||||
|
from .utils import login_url
|
||||||
|
from .utils import login_user
|
||||||
|
from .utils import logout_user
|
||||||
|
from .utils import make_next_param
|
||||||
|
from .utils import set_login_view
|
||||||
|
|
||||||
|
__version_info__ = ("0", "6", "3")
|
||||||
|
__version__ = ".".join(__version_info__)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"__version__",
|
||||||
|
"AUTH_HEADER_NAME",
|
||||||
|
"COOKIE_DURATION",
|
||||||
|
"COOKIE_HTTPONLY",
|
||||||
|
"COOKIE_NAME",
|
||||||
|
"COOKIE_SECURE",
|
||||||
|
"ID_ATTRIBUTE",
|
||||||
|
"LOGIN_MESSAGE",
|
||||||
|
"LOGIN_MESSAGE_CATEGORY",
|
||||||
|
"REFRESH_MESSAGE",
|
||||||
|
"REFRESH_MESSAGE_CATEGORY",
|
||||||
|
"LoginManager",
|
||||||
|
"AnonymousUserMixin",
|
||||||
|
"UserMixin",
|
||||||
|
"session_protected",
|
||||||
|
"user_accessed",
|
||||||
|
"user_loaded_from_cookie",
|
||||||
|
"user_loaded_from_request",
|
||||||
|
"user_logged_in",
|
||||||
|
"user_logged_out",
|
||||||
|
"user_login_confirmed",
|
||||||
|
"user_needs_refresh",
|
||||||
|
"user_unauthorized",
|
||||||
|
# "FlaskLoginClient",
|
||||||
|
"confirm_login",
|
||||||
|
"current_user",
|
||||||
|
"decode_cookie",
|
||||||
|
"encode_cookie",
|
||||||
|
"fresh_login_required",
|
||||||
|
"login_fresh",
|
||||||
|
"login_remembered",
|
||||||
|
"login_required",
|
||||||
|
"login_url",
|
||||||
|
"login_user",
|
||||||
|
"logout_user",
|
||||||
|
"make_next_param",
|
||||||
|
"set_login_view",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name):
|
||||||
|
if name == "user_loaded_from_header":
|
||||||
|
import warnings
|
||||||
|
from .signals import _user_loaded_from_header
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'user_loaded_from_header' is deprecated and will be"
|
||||||
|
" removed in Flask-Login 0.7. Use"
|
||||||
|
" 'user_loaded_from_request' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return _user_loaded_from_header
|
||||||
|
|
||||||
|
raise AttributeError(name)
|
55
cps/cw_login/config.py
Normal file
55
cps/cw_login/config.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
#: The default name of the "remember me" cookie (``remember_token``)
|
||||||
|
COOKIE_NAME = "remember_token"
|
||||||
|
|
||||||
|
#: The default time before the "remember me" cookie expires (365 days).
|
||||||
|
COOKIE_DURATION = timedelta(days=365)
|
||||||
|
|
||||||
|
#: Whether the "remember me" cookie requires Secure; defaults to ``False``
|
||||||
|
COOKIE_SECURE = False
|
||||||
|
|
||||||
|
#: Whether the "remember me" cookie uses HttpOnly or not; defaults to ``True``
|
||||||
|
COOKIE_HTTPONLY = True
|
||||||
|
|
||||||
|
#: Whether the "remember me" cookie requires same origin; defaults to ``None``
|
||||||
|
COOKIE_SAMESITE = None
|
||||||
|
|
||||||
|
#: The default flash message to display when users need to log in.
|
||||||
|
LOGIN_MESSAGE = "Please log in to access this page."
|
||||||
|
|
||||||
|
#: The default flash message category to display when users need to log in.
|
||||||
|
LOGIN_MESSAGE_CATEGORY = "message"
|
||||||
|
|
||||||
|
#: The default flash message to display when users need to reauthenticate.
|
||||||
|
REFRESH_MESSAGE = "Please reauthenticate to access this page."
|
||||||
|
|
||||||
|
#: The default flash message category to display when users need to
|
||||||
|
#: reauthenticate.
|
||||||
|
REFRESH_MESSAGE_CATEGORY = "message"
|
||||||
|
|
||||||
|
#: The default attribute to retreive the str id of the user
|
||||||
|
ID_ATTRIBUTE = "get_id"
|
||||||
|
|
||||||
|
#: Default name of the auth header (``Authorization``)
|
||||||
|
AUTH_HEADER_NAME = "Authorization"
|
||||||
|
|
||||||
|
#: A set of session keys that are populated by Flask-Login. Use this set to
|
||||||
|
#: purge keys safely and accurately.
|
||||||
|
SESSION_KEYS = {
|
||||||
|
"_user_id",
|
||||||
|
"_remember",
|
||||||
|
"_remember_seconds",
|
||||||
|
"_id",
|
||||||
|
"_fresh",
|
||||||
|
"next",
|
||||||
|
}
|
||||||
|
|
||||||
|
#: A set of HTTP methods which are exempt from `login_required` and
|
||||||
|
#: `fresh_login_required`. By default, this is just ``OPTIONS``.
|
||||||
|
EXEMPT_METHODS = {"OPTIONS"}
|
||||||
|
|
||||||
|
#: If true, the page the user is attempting to access is stored in the session
|
||||||
|
#: rather than a url parameter when redirecting to the login view; defaults to
|
||||||
|
#: ``False``.
|
||||||
|
USE_SESSION_FOR_NEXT = False
|
525
cps/cw_login/login_manager.py
Normal file
525
cps/cw_login/login_manager.py
Normal file
@ -0,0 +1,525 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from flask import abort
|
||||||
|
from flask import current_app
|
||||||
|
from flask import flash
|
||||||
|
from flask import g
|
||||||
|
from flask import has_app_context
|
||||||
|
from flask import redirect
|
||||||
|
from flask import request
|
||||||
|
from flask import session
|
||||||
|
|
||||||
|
from .config import AUTH_HEADER_NAME
|
||||||
|
from .config import COOKIE_DURATION
|
||||||
|
from .config import COOKIE_HTTPONLY
|
||||||
|
from .config import COOKIE_NAME
|
||||||
|
from .config import COOKIE_SAMESITE
|
||||||
|
from .config import COOKIE_SECURE
|
||||||
|
from .config import ID_ATTRIBUTE
|
||||||
|
from .config import LOGIN_MESSAGE
|
||||||
|
from .config import LOGIN_MESSAGE_CATEGORY
|
||||||
|
from .config import REFRESH_MESSAGE
|
||||||
|
from .config import REFRESH_MESSAGE_CATEGORY
|
||||||
|
from .config import SESSION_KEYS
|
||||||
|
from .config import USE_SESSION_FOR_NEXT
|
||||||
|
from .mixins import AnonymousUserMixin
|
||||||
|
from .signals import session_protected
|
||||||
|
from .signals import user_accessed
|
||||||
|
from .signals import user_loaded_from_cookie
|
||||||
|
from .signals import user_loaded_from_request
|
||||||
|
from .signals import user_needs_refresh
|
||||||
|
from .signals import user_unauthorized
|
||||||
|
from .utils import _create_identifier
|
||||||
|
from .utils import _user_context_processor
|
||||||
|
from .utils import decode_cookie
|
||||||
|
from .utils import encode_cookie
|
||||||
|
from .utils import expand_login_view
|
||||||
|
from .utils import login_url as make_login_url
|
||||||
|
from .utils import make_next_param
|
||||||
|
|
||||||
|
|
||||||
|
class LoginManager:
|
||||||
|
"""This object is used to hold the settings used for logging in. Instances
|
||||||
|
of :class:`LoginManager` are *not* bound to specific apps, so you can
|
||||||
|
create one in the main body of your code and then bind it to your
|
||||||
|
app in a factory function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app=None, add_context_processor=True):
|
||||||
|
#: A class or factory function that produces an anonymous user, which
|
||||||
|
#: is used when no one is logged in.
|
||||||
|
self.anonymous_user = AnonymousUserMixin
|
||||||
|
|
||||||
|
#: The name of the view to redirect to when the user needs to log in.
|
||||||
|
#: (This can be an absolute URL as well, if your authentication
|
||||||
|
#: machinery is external to your application.)
|
||||||
|
self.login_view = None
|
||||||
|
|
||||||
|
#: Names of views to redirect to when the user needs to log in,
|
||||||
|
#: per blueprint. If the key value is set to None the value of
|
||||||
|
#: :attr:`login_view` will be used instead.
|
||||||
|
self.blueprint_login_views = {}
|
||||||
|
|
||||||
|
#: The message to flash when a user is redirected to the login page.
|
||||||
|
self.login_message = LOGIN_MESSAGE
|
||||||
|
|
||||||
|
#: The message category to flash when a user is redirected to the login
|
||||||
|
#: page.
|
||||||
|
self.login_message_category = LOGIN_MESSAGE_CATEGORY
|
||||||
|
|
||||||
|
#: The name of the view to redirect to when the user needs to
|
||||||
|
#: reauthenticate.
|
||||||
|
self.refresh_view = None
|
||||||
|
|
||||||
|
#: The message to flash when a user is redirected to the 'needs
|
||||||
|
#: refresh' page.
|
||||||
|
self.needs_refresh_message = REFRESH_MESSAGE
|
||||||
|
|
||||||
|
#: The message category to flash when a user is redirected to the
|
||||||
|
#: 'needs refresh' page.
|
||||||
|
self.needs_refresh_message_category = REFRESH_MESSAGE_CATEGORY
|
||||||
|
|
||||||
|
#: The mode to use session protection in. This can be either
|
||||||
|
#: ``'basic'`` (the default) or ``'strong'``, or ``None`` to disable
|
||||||
|
#: it.
|
||||||
|
self.session_protection = "basic"
|
||||||
|
|
||||||
|
#: If present, used to translate flash messages ``self.login_message``
|
||||||
|
#: and ``self.needs_refresh_message``
|
||||||
|
self.localize_callback = None
|
||||||
|
|
||||||
|
self.unauthorized_callback = None
|
||||||
|
|
||||||
|
self.needs_refresh_callback = None
|
||||||
|
|
||||||
|
self.id_attribute = ID_ATTRIBUTE
|
||||||
|
|
||||||
|
self._user_callback = None
|
||||||
|
|
||||||
|
self._header_callback = None
|
||||||
|
|
||||||
|
self._request_callback = None
|
||||||
|
|
||||||
|
self._session_identifier_generator = _create_identifier
|
||||||
|
|
||||||
|
if app is not None:
|
||||||
|
self.init_app(app, add_context_processor)
|
||||||
|
|
||||||
|
def setup_app(self, app, add_context_processor=True): # pragma: no cover
|
||||||
|
"""
|
||||||
|
This method has been deprecated. Please use
|
||||||
|
:meth:`LoginManager.init_app` instead.
|
||||||
|
"""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'setup_app' is deprecated and will be removed in"
|
||||||
|
" Flask-Login 0.7. Use 'init_app' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
self.init_app(app, add_context_processor)
|
||||||
|
|
||||||
|
def init_app(self, app, add_context_processor=True):
|
||||||
|
"""
|
||||||
|
Configures an application. This registers an `after_request` call, and
|
||||||
|
attaches this `LoginManager` to it as `app.login_manager`.
|
||||||
|
|
||||||
|
:param app: The :class:`flask.Flask` object to configure.
|
||||||
|
:type app: :class:`flask.Flask`
|
||||||
|
:param add_context_processor: Whether to add a context processor to
|
||||||
|
the app that adds a `current_user` variable to the template.
|
||||||
|
Defaults to ``True``.
|
||||||
|
:type add_context_processor: bool
|
||||||
|
"""
|
||||||
|
app.login_manager = self
|
||||||
|
app.after_request(self._update_remember_cookie)
|
||||||
|
|
||||||
|
if add_context_processor:
|
||||||
|
app.context_processor(_user_context_processor)
|
||||||
|
|
||||||
|
def unauthorized(self):
|
||||||
|
"""
|
||||||
|
This is called when the user is required to log in. If you register a
|
||||||
|
callback with :meth:`LoginManager.unauthorized_handler`, then it will
|
||||||
|
be called. Otherwise, it will take the following actions:
|
||||||
|
|
||||||
|
- Flash :attr:`LoginManager.login_message` to the user.
|
||||||
|
|
||||||
|
- If the app is using blueprints find the login view for
|
||||||
|
the current blueprint using `blueprint_login_views`. If the app
|
||||||
|
is not using blueprints or the login view for the current
|
||||||
|
blueprint is not specified use the value of `login_view`.
|
||||||
|
|
||||||
|
- Redirect the user to the login view. (The page they were
|
||||||
|
attempting to access will be passed in the ``next`` query
|
||||||
|
string variable, so you can redirect there if present instead
|
||||||
|
of the homepage. Alternatively, it will be added to the session
|
||||||
|
as ``next`` if USE_SESSION_FOR_NEXT is set.)
|
||||||
|
|
||||||
|
If :attr:`LoginManager.login_view` is not defined, then it will simply
|
||||||
|
raise a HTTP 401 (Unauthorized) error instead.
|
||||||
|
|
||||||
|
This should be returned from a view or before/after_request function,
|
||||||
|
otherwise the redirect will have no effect.
|
||||||
|
"""
|
||||||
|
user_unauthorized.send(current_app._get_current_object())
|
||||||
|
|
||||||
|
if self.unauthorized_callback:
|
||||||
|
return self.unauthorized_callback()
|
||||||
|
|
||||||
|
if request.blueprint in self.blueprint_login_views:
|
||||||
|
login_view = self.blueprint_login_views[request.blueprint]
|
||||||
|
else:
|
||||||
|
login_view = self.login_view
|
||||||
|
|
||||||
|
if not login_view:
|
||||||
|
abort(401)
|
||||||
|
|
||||||
|
if self.login_message:
|
||||||
|
if self.localize_callback is not None:
|
||||||
|
flash(
|
||||||
|
self.localize_callback(self.login_message),
|
||||||
|
category=self.login_message_category,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
flash(self.login_message, category=self.login_message_category)
|
||||||
|
|
||||||
|
config = current_app.config
|
||||||
|
if config.get("USE_SESSION_FOR_NEXT", USE_SESSION_FOR_NEXT):
|
||||||
|
login_url = expand_login_view(login_view)
|
||||||
|
session["_id"] = self._session_identifier_generator()
|
||||||
|
session["next"] = make_next_param(login_url, request.url)
|
||||||
|
redirect_url = make_login_url(login_view)
|
||||||
|
else:
|
||||||
|
redirect_url = make_login_url(login_view, next_url=request.url)
|
||||||
|
|
||||||
|
return redirect(redirect_url)
|
||||||
|
|
||||||
|
def user_loader(self, callback):
|
||||||
|
"""
|
||||||
|
This sets the callback for reloading a user from the session. The
|
||||||
|
function you set should take a user ID (a ``str``) and return a
|
||||||
|
user object, or ``None`` if the user does not exist.
|
||||||
|
|
||||||
|
:param callback: The callback for retrieving a user object.
|
||||||
|
:type callback: callable
|
||||||
|
"""
|
||||||
|
self._user_callback = callback
|
||||||
|
return self.user_callback
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_callback(self):
|
||||||
|
"""Gets the user_loader callback set by user_loader decorator."""
|
||||||
|
return self._user_callback
|
||||||
|
|
||||||
|
def request_loader(self, callback):
|
||||||
|
"""
|
||||||
|
This sets the callback for loading a user from a Flask request.
|
||||||
|
The function you set should take Flask request object and
|
||||||
|
return a user object, or `None` if the user does not exist.
|
||||||
|
|
||||||
|
:param callback: The callback for retrieving a user object.
|
||||||
|
:type callback: callable
|
||||||
|
"""
|
||||||
|
self._request_callback = callback
|
||||||
|
return self.request_callback
|
||||||
|
|
||||||
|
@property
|
||||||
|
def request_callback(self):
|
||||||
|
"""Gets the request_loader callback set by request_loader decorator."""
|
||||||
|
return self._request_callback
|
||||||
|
|
||||||
|
def unauthorized_handler(self, callback):
|
||||||
|
"""
|
||||||
|
This will set the callback for the `unauthorized` method, which among
|
||||||
|
other things is used by `login_required`. It takes no arguments, and
|
||||||
|
should return a response to be sent to the user instead of their
|
||||||
|
normal view.
|
||||||
|
|
||||||
|
:param callback: The callback for unauthorized users.
|
||||||
|
:type callback: callable
|
||||||
|
"""
|
||||||
|
self.unauthorized_callback = callback
|
||||||
|
return callback
|
||||||
|
|
||||||
|
def needs_refresh_handler(self, callback):
|
||||||
|
"""
|
||||||
|
This will set the callback for the `needs_refresh` method, which among
|
||||||
|
other things is used by `fresh_login_required`. It takes no arguments,
|
||||||
|
and should return a response to be sent to the user instead of their
|
||||||
|
normal view.
|
||||||
|
|
||||||
|
:param callback: The callback for unauthorized users.
|
||||||
|
:type callback: callable
|
||||||
|
"""
|
||||||
|
self.needs_refresh_callback = callback
|
||||||
|
return callback
|
||||||
|
|
||||||
|
def needs_refresh(self):
|
||||||
|
"""
|
||||||
|
This is called when the user is logged in, but they need to be
|
||||||
|
reauthenticated because their session is stale. If you register a
|
||||||
|
callback with `needs_refresh_handler`, then it will be called.
|
||||||
|
Otherwise, it will take the following actions:
|
||||||
|
|
||||||
|
- Flash :attr:`LoginManager.needs_refresh_message` to the user.
|
||||||
|
|
||||||
|
- Redirect the user to :attr:`LoginManager.refresh_view`. (The page
|
||||||
|
they were attempting to access will be passed in the ``next``
|
||||||
|
query string variable, so you can redirect there if present
|
||||||
|
instead of the homepage.)
|
||||||
|
|
||||||
|
If :attr:`LoginManager.refresh_view` is not defined, then it will
|
||||||
|
simply raise a HTTP 401 (Unauthorized) error instead.
|
||||||
|
|
||||||
|
This should be returned from a view or before/after_request function,
|
||||||
|
otherwise the redirect will have no effect.
|
||||||
|
"""
|
||||||
|
user_needs_refresh.send(current_app._get_current_object())
|
||||||
|
|
||||||
|
if self.needs_refresh_callback:
|
||||||
|
return self.needs_refresh_callback()
|
||||||
|
|
||||||
|
if not self.refresh_view:
|
||||||
|
abort(401)
|
||||||
|
|
||||||
|
if self.needs_refresh_message:
|
||||||
|
if self.localize_callback is not None:
|
||||||
|
flash(
|
||||||
|
self.localize_callback(self.needs_refresh_message),
|
||||||
|
category=self.needs_refresh_message_category,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
flash(
|
||||||
|
self.needs_refresh_message,
|
||||||
|
category=self.needs_refresh_message_category,
|
||||||
|
)
|
||||||
|
|
||||||
|
config = current_app.config
|
||||||
|
if config.get("USE_SESSION_FOR_NEXT", USE_SESSION_FOR_NEXT):
|
||||||
|
login_url = expand_login_view(self.refresh_view)
|
||||||
|
session["_id"] = self._session_identifier_generator()
|
||||||
|
session["next"] = make_next_param(login_url, request.url)
|
||||||
|
redirect_url = make_login_url(self.refresh_view)
|
||||||
|
else:
|
||||||
|
login_url = self.refresh_view
|
||||||
|
redirect_url = make_login_url(login_url, next_url=request.url)
|
||||||
|
|
||||||
|
return redirect(redirect_url)
|
||||||
|
|
||||||
|
def _update_request_context_with_user(self, user=None):
|
||||||
|
"""Store the given user as ctx.user."""
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
user = self.anonymous_user()
|
||||||
|
|
||||||
|
g._login_user = user
|
||||||
|
|
||||||
|
def _load_user(self):
|
||||||
|
"""Loads user from session or remember_me cookie as applicable"""
|
||||||
|
|
||||||
|
if self._user_callback is None and self._request_callback is None:
|
||||||
|
raise Exception(
|
||||||
|
"Missing user_loader or request_loader. Refer to "
|
||||||
|
"http://flask-login.readthedocs.io/#how-it-works "
|
||||||
|
"for more info."
|
||||||
|
)
|
||||||
|
|
||||||
|
user_accessed.send(current_app._get_current_object())
|
||||||
|
|
||||||
|
# Check SESSION_PROTECTION
|
||||||
|
if self._session_protection_failed():
|
||||||
|
return self._update_request_context_with_user()
|
||||||
|
|
||||||
|
user = None
|
||||||
|
|
||||||
|
# Load user from Flask Session
|
||||||
|
user_id = session.get("_user_id")
|
||||||
|
user_random = session.get("_random")
|
||||||
|
user_session_key = session.get("_id")
|
||||||
|
if (user_id is not None
|
||||||
|
and user_random is not None
|
||||||
|
and user_session_key is not None
|
||||||
|
and self._user_callback is not None):
|
||||||
|
user = self._user_callback(user_id, user_random, user_session_key)
|
||||||
|
|
||||||
|
# Load user from Remember Me Cookie or Request Loader
|
||||||
|
if user is None:
|
||||||
|
config = current_app.config
|
||||||
|
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
|
||||||
|
header_name = config.get("AUTH_HEADER_NAME", AUTH_HEADER_NAME)
|
||||||
|
has_cookie = (
|
||||||
|
cookie_name in request.cookies and session.get("_remember") != "clear"
|
||||||
|
)
|
||||||
|
if has_cookie:
|
||||||
|
cookie = request.cookies[cookie_name]
|
||||||
|
user = self._load_user_from_remember_cookie(cookie)
|
||||||
|
elif self._request_callback:
|
||||||
|
user = self._load_user_from_request(request)
|
||||||
|
elif header_name in request.headers:
|
||||||
|
header = request.headers[header_name]
|
||||||
|
user = self._load_user_from_header(header)
|
||||||
|
|
||||||
|
return self._update_request_context_with_user(user)
|
||||||
|
|
||||||
|
def _session_protection_failed(self):
|
||||||
|
sess = session._get_current_object()
|
||||||
|
ident = self._session_identifier_generator()
|
||||||
|
|
||||||
|
app = current_app._get_current_object()
|
||||||
|
mode = app.config.get("SESSION_PROTECTION", self.session_protection)
|
||||||
|
|
||||||
|
if not mode or mode not in ["basic", "strong"]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# if the sess is empty, it's an anonymous user or just logged out
|
||||||
|
# so we can skip this
|
||||||
|
if sess and ident != sess.get("_id", None):
|
||||||
|
if mode == "basic" or sess.permanent:
|
||||||
|
if sess.get("_fresh") is not False:
|
||||||
|
sess["_fresh"] = False
|
||||||
|
session_protected.send(app)
|
||||||
|
return False
|
||||||
|
elif mode == "strong":
|
||||||
|
for k in SESSION_KEYS:
|
||||||
|
sess.pop(k, None)
|
||||||
|
|
||||||
|
sess["_remember"] = "clear"
|
||||||
|
session_protected.send(app)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _load_user_from_remember_cookie(self, cookie):
|
||||||
|
user_id = decode_cookie(cookie)
|
||||||
|
if user_id is not None:
|
||||||
|
session["_user_id"] = user_id
|
||||||
|
session["_fresh"] = False
|
||||||
|
user = None
|
||||||
|
if self._user_callback:
|
||||||
|
user = self._user_callback(user_id)
|
||||||
|
if user is not None:
|
||||||
|
app = current_app._get_current_object()
|
||||||
|
user_loaded_from_cookie.send(app, user=user)
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _load_user_from_header(self, header):
|
||||||
|
if self._header_callback:
|
||||||
|
user = self._header_callback(header)
|
||||||
|
if user is not None:
|
||||||
|
app = current_app._get_current_object()
|
||||||
|
|
||||||
|
from .signals import _user_loaded_from_header
|
||||||
|
|
||||||
|
_user_loaded_from_header.send(app, user=user)
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _load_user_from_request(self, request):
|
||||||
|
if self._request_callback:
|
||||||
|
user = self._request_callback(request)
|
||||||
|
if user is not None:
|
||||||
|
app = current_app._get_current_object()
|
||||||
|
user_loaded_from_request.send(app, user=user)
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _update_remember_cookie(self, response):
|
||||||
|
# Don't modify the session unless there's something to do.
|
||||||
|
if "_remember" not in session and current_app.config.get(
|
||||||
|
"REMEMBER_COOKIE_REFRESH_EACH_REQUEST"
|
||||||
|
):
|
||||||
|
session["_remember"] = "set"
|
||||||
|
|
||||||
|
if "_remember" in session:
|
||||||
|
operation = session.pop("_remember", None)
|
||||||
|
|
||||||
|
if operation == "set" and "_user_id" in session:
|
||||||
|
self._set_cookie(response)
|
||||||
|
elif operation == "clear":
|
||||||
|
self._clear_cookie(response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _set_cookie(self, response):
|
||||||
|
# cookie settings
|
||||||
|
config = current_app.config
|
||||||
|
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
|
||||||
|
domain = config.get("REMEMBER_COOKIE_DOMAIN")
|
||||||
|
path = config.get("REMEMBER_COOKIE_PATH", "/")
|
||||||
|
|
||||||
|
secure = config.get("REMEMBER_COOKIE_SECURE", COOKIE_SECURE)
|
||||||
|
httponly = config.get("REMEMBER_COOKIE_HTTPONLY", COOKIE_HTTPONLY)
|
||||||
|
samesite = config.get("REMEMBER_COOKIE_SAMESITE", COOKIE_SAMESITE)
|
||||||
|
|
||||||
|
if "_remember_seconds" in session:
|
||||||
|
duration = timedelta(seconds=session["_remember_seconds"])
|
||||||
|
else:
|
||||||
|
duration = config.get("REMEMBER_COOKIE_DURATION", COOKIE_DURATION)
|
||||||
|
|
||||||
|
# prepare data
|
||||||
|
data = encode_cookie(str(session["_user_id"]))
|
||||||
|
|
||||||
|
if isinstance(duration, int):
|
||||||
|
duration = timedelta(seconds=duration)
|
||||||
|
|
||||||
|
try:
|
||||||
|
expires = datetime.utcnow() + duration
|
||||||
|
except TypeError as e:
|
||||||
|
raise Exception(
|
||||||
|
"REMEMBER_COOKIE_DURATION must be a datetime.timedelta,"
|
||||||
|
f" instead got: {duration}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
# actually set it
|
||||||
|
response.set_cookie(
|
||||||
|
cookie_name,
|
||||||
|
value=data,
|
||||||
|
expires=expires,
|
||||||
|
domain=domain,
|
||||||
|
path=path,
|
||||||
|
secure=secure,
|
||||||
|
httponly=httponly,
|
||||||
|
samesite=samesite,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _clear_cookie(self, response):
|
||||||
|
config = current_app.config
|
||||||
|
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
|
||||||
|
domain = config.get("REMEMBER_COOKIE_DOMAIN")
|
||||||
|
path = config.get("REMEMBER_COOKIE_PATH", "/")
|
||||||
|
response.delete_cookie(cookie_name, domain=domain, path=path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _login_disabled(self):
|
||||||
|
"""Legacy property, use app.config['LOGIN_DISABLED'] instead."""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'_login_disabled' is deprecated and will be removed in"
|
||||||
|
" Flask-Login 0.7. Use 'LOGIN_DISABLED' in 'app.config'"
|
||||||
|
" instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_app_context():
|
||||||
|
return current_app.config.get("LOGIN_DISABLED", False)
|
||||||
|
return False
|
||||||
|
|
||||||
|
@_login_disabled.setter
|
||||||
|
def _login_disabled(self, newvalue):
|
||||||
|
"""Legacy property setter, use app.config['LOGIN_DISABLED'] instead."""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'_login_disabled' is deprecated and will be removed in"
|
||||||
|
" Flask-Login 0.7. Use 'LOGIN_DISABLED' in 'app.config'"
|
||||||
|
" instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
current_app.config["LOGIN_DISABLED"] = newvalue
|
65
cps/cw_login/mixins.py
Normal file
65
cps/cw_login/mixins.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
class UserMixin:
|
||||||
|
"""
|
||||||
|
This provides default implementations for the methods that Flask-Login
|
||||||
|
expects user objects to have.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Python 3 implicitly set __hash__ to None if we override __eq__
|
||||||
|
# We set it back to its default implementation
|
||||||
|
__hash__ = object.__hash__
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_active(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self):
|
||||||
|
return self.is_active
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_anonymous(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_id(self):
|
||||||
|
try:
|
||||||
|
return str(self.id)
|
||||||
|
except AttributeError:
|
||||||
|
raise NotImplementedError("No `id` attribute - override `get_id`") from None
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
"""
|
||||||
|
Checks the equality of two `UserMixin` objects using `get_id`.
|
||||||
|
"""
|
||||||
|
if isinstance(other, UserMixin):
|
||||||
|
return self.get_id() == other.get_id()
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
"""
|
||||||
|
Checks the inequality of two `UserMixin` objects using `get_id`.
|
||||||
|
"""
|
||||||
|
equal = self.__eq__(other)
|
||||||
|
if equal is NotImplemented:
|
||||||
|
return NotImplemented
|
||||||
|
return not equal
|
||||||
|
|
||||||
|
|
||||||
|
class AnonymousUserMixin:
|
||||||
|
"""
|
||||||
|
This is the default object for representing an anonymous user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_active(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_anonymous(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_id(self):
|
||||||
|
return
|
61
cps/cw_login/signals.py
Normal file
61
cps/cw_login/signals.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
from flask.signals import Namespace
|
||||||
|
|
||||||
|
_signals = Namespace()
|
||||||
|
|
||||||
|
#: Sent when a user is logged in. In addition to the app (which is the
|
||||||
|
#: sender), it is passed `user`, which is the user being logged in.
|
||||||
|
user_logged_in = _signals.signal("logged-in")
|
||||||
|
|
||||||
|
#: Sent when a user is logged out. In addition to the app (which is the
|
||||||
|
#: sender), it is passed `user`, which is the user being logged out.
|
||||||
|
user_logged_out = _signals.signal("logged-out")
|
||||||
|
|
||||||
|
#: Sent when the user is loaded from the cookie. In addition to the app (which
|
||||||
|
#: is the sender), it is passed `user`, which is the user being reloaded.
|
||||||
|
user_loaded_from_cookie = _signals.signal("loaded-from-cookie")
|
||||||
|
|
||||||
|
#: Sent when the user is loaded from the header. In addition to the app (which
|
||||||
|
#: is the #: sender), it is passed `user`, which is the user being reloaded.
|
||||||
|
_user_loaded_from_header = _signals.signal("loaded-from-header")
|
||||||
|
|
||||||
|
#: Sent when the user is loaded from the request. In addition to the app (which
|
||||||
|
#: is the #: sender), it is passed `user`, which is the user being reloaded.
|
||||||
|
user_loaded_from_request = _signals.signal("loaded-from-request")
|
||||||
|
|
||||||
|
#: Sent when a user's login is confirmed, marking it as fresh. (It is not
|
||||||
|
#: called for a normal login.)
|
||||||
|
#: It receives no additional arguments besides the app.
|
||||||
|
user_login_confirmed = _signals.signal("login-confirmed")
|
||||||
|
|
||||||
|
#: Sent when the `unauthorized` method is called on a `LoginManager`. It
|
||||||
|
#: receives no additional arguments besides the app.
|
||||||
|
user_unauthorized = _signals.signal("unauthorized")
|
||||||
|
|
||||||
|
#: Sent when the `needs_refresh` method is called on a `LoginManager`. It
|
||||||
|
#: receives no additional arguments besides the app.
|
||||||
|
user_needs_refresh = _signals.signal("needs-refresh")
|
||||||
|
|
||||||
|
#: Sent whenever the user is accessed/loaded
|
||||||
|
#: receives no additional arguments besides the app.
|
||||||
|
user_accessed = _signals.signal("accessed")
|
||||||
|
|
||||||
|
#: Sent whenever session protection takes effect, and a session is either
|
||||||
|
#: marked non-fresh or deleted. It receives no additional arguments besides
|
||||||
|
#: the app.
|
||||||
|
session_protected = _signals.signal("session-protected")
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name):
|
||||||
|
if name == "user_loaded_from_header":
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'user_loaded_from_header' is deprecated and will be"
|
||||||
|
" removed in Flask-Login 0.7. Use"
|
||||||
|
" 'user_loaded_from_request' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return _user_loaded_from_header
|
||||||
|
|
||||||
|
raise AttributeError(name)
|
424
cps/cw_login/utils.py
Normal file
424
cps/cw_login/utils.py
Normal file
@ -0,0 +1,424 @@
|
|||||||
|
import hmac
|
||||||
|
import os
|
||||||
|
from functools import wraps
|
||||||
|
from hashlib import sha512
|
||||||
|
from urllib.parse import parse_qs
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
from urllib.parse import urlsplit
|
||||||
|
from urllib.parse import urlunsplit
|
||||||
|
|
||||||
|
from flask import current_app
|
||||||
|
from flask import g
|
||||||
|
from flask import has_request_context
|
||||||
|
from flask import request
|
||||||
|
from flask import session
|
||||||
|
from flask import url_for
|
||||||
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
|
from .config import COOKIE_NAME
|
||||||
|
from .config import EXEMPT_METHODS
|
||||||
|
from .signals import user_logged_in
|
||||||
|
from .signals import user_logged_out
|
||||||
|
from .signals import user_login_confirmed
|
||||||
|
|
||||||
|
#: A proxy for the current user. If no user is logged in, this will be an
|
||||||
|
#: anonymous user
|
||||||
|
current_user = LocalProxy(lambda: _get_user())
|
||||||
|
|
||||||
|
|
||||||
|
def encode_cookie(payload, key=None):
|
||||||
|
"""
|
||||||
|
This will encode a ``str`` value into a cookie, and sign that cookie
|
||||||
|
with the app's secret key.
|
||||||
|
|
||||||
|
:param payload: The value to encode, as `str`.
|
||||||
|
:type payload: str
|
||||||
|
|
||||||
|
:param key: The key to use when creating the cookie digest. If not
|
||||||
|
specified, the SECRET_KEY value from app config will be used.
|
||||||
|
:type key: str
|
||||||
|
"""
|
||||||
|
return f"{payload}|{_cookie_digest(payload, key=key)}"
|
||||||
|
|
||||||
|
|
||||||
|
def decode_cookie(cookie, key=None):
|
||||||
|
"""
|
||||||
|
This decodes a cookie given by `encode_cookie`. If verification of the
|
||||||
|
cookie fails, ``None`` will be implicitly returned.
|
||||||
|
|
||||||
|
:param cookie: An encoded cookie.
|
||||||
|
:type cookie: str
|
||||||
|
|
||||||
|
:param key: The key to use when creating the cookie digest. If not
|
||||||
|
specified, the SECRET_KEY value from app config will be used.
|
||||||
|
:type key: str
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
payload, digest = cookie.rsplit("|", 1)
|
||||||
|
if hasattr(digest, "decode"):
|
||||||
|
digest = digest.decode("ascii") # pragma: no cover
|
||||||
|
except ValueError:
|
||||||
|
return
|
||||||
|
|
||||||
|
if hmac.compare_digest(_cookie_digest(payload, key=key), digest):
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def make_next_param(login_url, current_url):
|
||||||
|
"""
|
||||||
|
Reduces the scheme and host from a given URL so it can be passed to
|
||||||
|
the given `login` URL more efficiently.
|
||||||
|
|
||||||
|
:param login_url: The login URL being redirected to.
|
||||||
|
:type login_url: str
|
||||||
|
:param current_url: The URL to reduce.
|
||||||
|
:type current_url: str
|
||||||
|
"""
|
||||||
|
l_url = urlsplit(login_url)
|
||||||
|
c_url = urlsplit(current_url)
|
||||||
|
|
||||||
|
if (not l_url.scheme or l_url.scheme == c_url.scheme) and (
|
||||||
|
not l_url.netloc or l_url.netloc == c_url.netloc
|
||||||
|
):
|
||||||
|
return urlunsplit(("", "", c_url.path, c_url.query, ""))
|
||||||
|
return current_url
|
||||||
|
|
||||||
|
|
||||||
|
def expand_login_view(login_view):
|
||||||
|
"""
|
||||||
|
Returns the url for the login view, expanding the view name to a url if
|
||||||
|
needed.
|
||||||
|
|
||||||
|
:param login_view: The name of the login view or a URL for the login view.
|
||||||
|
:type login_view: str
|
||||||
|
"""
|
||||||
|
if login_view.startswith(("https://", "http://", "/")):
|
||||||
|
return login_view
|
||||||
|
|
||||||
|
return url_for(login_view)
|
||||||
|
|
||||||
|
|
||||||
|
def login_url(login_view, next_url=None, next_field="next"):
|
||||||
|
"""
|
||||||
|
Creates a URL for redirecting to a login page. If only `login_view` is
|
||||||
|
provided, this will just return the URL for it. If `next_url` is provided,
|
||||||
|
however, this will append a ``next=URL`` parameter to the query string
|
||||||
|
so that the login view can redirect back to that URL. Flask-Login's default
|
||||||
|
unauthorized handler uses this function when redirecting to your login url.
|
||||||
|
To force the host name used, set `FORCE_HOST_FOR_REDIRECTS` to a host. This
|
||||||
|
prevents from redirecting to external sites if request headers Host or
|
||||||
|
X-Forwarded-For are present.
|
||||||
|
|
||||||
|
:param login_view: The name of the login view. (Alternately, the actual
|
||||||
|
URL to the login view.)
|
||||||
|
:type login_view: str
|
||||||
|
:param next_url: The URL to give the login view for redirection.
|
||||||
|
:type next_url: str
|
||||||
|
:param next_field: What field to store the next URL in. (It defaults to
|
||||||
|
``next``.)
|
||||||
|
:type next_field: str
|
||||||
|
"""
|
||||||
|
base = expand_login_view(login_view)
|
||||||
|
|
||||||
|
if next_url is None:
|
||||||
|
return base
|
||||||
|
|
||||||
|
parsed_result = urlsplit(base)
|
||||||
|
md = parse_qs(parsed_result.query, keep_blank_values=True)
|
||||||
|
md[next_field] = make_next_param(base, next_url)
|
||||||
|
netloc = current_app.config.get("FORCE_HOST_FOR_REDIRECTS") or parsed_result.netloc
|
||||||
|
parsed_result = parsed_result._replace(
|
||||||
|
netloc=netloc, query=urlencode(md, doseq=True)
|
||||||
|
)
|
||||||
|
return urlunsplit(parsed_result)
|
||||||
|
|
||||||
|
|
||||||
|
def login_fresh():
|
||||||
|
"""
|
||||||
|
This returns ``True`` if the current login is fresh.
|
||||||
|
"""
|
||||||
|
return session.get("_fresh", False)
|
||||||
|
|
||||||
|
|
||||||
|
def login_remembered():
|
||||||
|
"""
|
||||||
|
This returns ``True`` if the current login is remembered across sessions.
|
||||||
|
"""
|
||||||
|
config = current_app.config
|
||||||
|
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
|
||||||
|
has_cookie = cookie_name in request.cookies and session.get("_remember") != "clear"
|
||||||
|
if has_cookie:
|
||||||
|
cookie = request.cookies[cookie_name]
|
||||||
|
user_id = decode_cookie(cookie)
|
||||||
|
return user_id is not None
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def login_user(user, remember=False, duration=None, force=False, fresh=True):
|
||||||
|
"""
|
||||||
|
Logs a user in. You should pass the actual user object to this. If the
|
||||||
|
user's `is_active` property is ``False``, they will not be logged in
|
||||||
|
unless `force` is ``True``.
|
||||||
|
|
||||||
|
This will return ``True`` if the log in attempt succeeds, and ``False`` if
|
||||||
|
it fails (i.e. because the user is inactive).
|
||||||
|
|
||||||
|
:param user: The user object to log in.
|
||||||
|
:type user: object
|
||||||
|
:param remember: Whether to remember the user after their session expires.
|
||||||
|
Defaults to ``False``.
|
||||||
|
:type remember: bool
|
||||||
|
:param duration: The amount of time before the remember cookie expires. If
|
||||||
|
``None`` the value set in the settings is used. Defaults to ``None``.
|
||||||
|
:type duration: :class:`datetime.timedelta`
|
||||||
|
:param force: If the user is inactive, setting this to ``True`` will log
|
||||||
|
them in regardless. Defaults to ``False``.
|
||||||
|
:type force: bool
|
||||||
|
:param fresh: setting this to ``False`` will log in the user with a session
|
||||||
|
marked as not "fresh". Defaults to ``True``.
|
||||||
|
:type fresh: bool
|
||||||
|
"""
|
||||||
|
if not force and not user.is_active:
|
||||||
|
return False
|
||||||
|
|
||||||
|
user_id = getattr(user, current_app.login_manager.id_attribute)()
|
||||||
|
session["_user_id"] = user_id
|
||||||
|
session["_fresh"] = fresh
|
||||||
|
session["_id"] = current_app.login_manager._session_identifier_generator()
|
||||||
|
session["_random"] = os.urandom(10)
|
||||||
|
|
||||||
|
if remember:
|
||||||
|
session["_remember"] = "set"
|
||||||
|
if duration is not None:
|
||||||
|
try:
|
||||||
|
# equal to timedelta.total_seconds() but works with Python 2.6
|
||||||
|
session["_remember_seconds"] = (
|
||||||
|
duration.microseconds
|
||||||
|
+ (duration.seconds + duration.days * 24 * 3600) * 10**6
|
||||||
|
) / 10.0**6
|
||||||
|
except AttributeError as e:
|
||||||
|
raise Exception(
|
||||||
|
f"duration must be a datetime.timedelta, instead got: {duration}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
current_app.login_manager._update_request_context_with_user(user)
|
||||||
|
user_logged_in.send(current_app._get_current_object(), user=_get_user())
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def logout_user():
|
||||||
|
"""
|
||||||
|
Logs a user out. (You do not need to pass the actual user.) This will
|
||||||
|
also clean up the remember me cookie if it exists.
|
||||||
|
"""
|
||||||
|
|
||||||
|
user = _get_user()
|
||||||
|
|
||||||
|
if "_user_id" in session:
|
||||||
|
session.pop("_user_id")
|
||||||
|
|
||||||
|
if "_fresh" in session:
|
||||||
|
session.pop("_fresh")
|
||||||
|
|
||||||
|
if "_id" in session:
|
||||||
|
session.pop("_id")
|
||||||
|
|
||||||
|
if "_random" in session:
|
||||||
|
session.pop("_random")
|
||||||
|
|
||||||
|
|
||||||
|
cookie_name = current_app.config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
|
||||||
|
if cookie_name in request.cookies:
|
||||||
|
session["_remember"] = "clear"
|
||||||
|
if "_remember_seconds" in session:
|
||||||
|
session.pop("_remember_seconds")
|
||||||
|
|
||||||
|
user_logged_out.send(current_app._get_current_object(), user=user)
|
||||||
|
|
||||||
|
current_app.login_manager._update_request_context_with_user()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_login():
|
||||||
|
"""
|
||||||
|
This sets the current session as fresh. Sessions become stale when they
|
||||||
|
are reloaded from a cookie.
|
||||||
|
"""
|
||||||
|
session["_fresh"] = True
|
||||||
|
session["_id"] = current_app.login_manager._session_identifier_generator()
|
||||||
|
user_login_confirmed.send(current_app._get_current_object())
|
||||||
|
|
||||||
|
|
||||||
|
def login_required(func):
|
||||||
|
"""
|
||||||
|
If you decorate a view with this, it will ensure that the current user is
|
||||||
|
logged in and authenticated before calling the actual view. (If they are
|
||||||
|
not, it calls the :attr:`LoginManager.unauthorized` callback.) For
|
||||||
|
example::
|
||||||
|
|
||||||
|
@app.route('/post')
|
||||||
|
@user_login_required
|
||||||
|
def post():
|
||||||
|
pass
|
||||||
|
|
||||||
|
If there are only certain times you need to require that your user is
|
||||||
|
logged in, you can do so with::
|
||||||
|
|
||||||
|
if not current_user.is_authenticated:
|
||||||
|
return current_app.login_manager.unauthorized()
|
||||||
|
|
||||||
|
...which is essentially the code that this function adds to your views.
|
||||||
|
|
||||||
|
It can be convenient to globally turn off authentication when unit testing.
|
||||||
|
To enable this, if the application configuration variable `LOGIN_DISABLED`
|
||||||
|
is set to `True`, this decorator will be ignored.
|
||||||
|
|
||||||
|
.. Note ::
|
||||||
|
|
||||||
|
Per `W3 guidelines for CORS preflight requests
|
||||||
|
<http://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0>`_,
|
||||||
|
HTTP ``OPTIONS`` requests are exempt from login checks.
|
||||||
|
|
||||||
|
:param func: The view function to decorate.
|
||||||
|
:type func: function
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def decorated_view(*args, **kwargs):
|
||||||
|
if request.method in EXEMPT_METHODS or current_app.config.get("LOGIN_DISABLED"):
|
||||||
|
pass
|
||||||
|
elif not current_user.is_authenticated:
|
||||||
|
return current_app.login_manager.unauthorized()
|
||||||
|
|
||||||
|
# flask 1.x compatibility
|
||||||
|
# current_app.ensure_sync is only available in Flask >= 2.0
|
||||||
|
if callable(getattr(current_app, "ensure_sync", None)):
|
||||||
|
return current_app.ensure_sync(func)(*args, **kwargs)
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_view
|
||||||
|
|
||||||
|
|
||||||
|
def fresh_login_required(func):
|
||||||
|
"""
|
||||||
|
If you decorate a view with this, it will ensure that the current user's
|
||||||
|
login is fresh - i.e. their session was not restored from a 'remember me'
|
||||||
|
cookie. Sensitive operations, like changing a password or e-mail, should
|
||||||
|
be protected with this, to impede the efforts of cookie thieves.
|
||||||
|
|
||||||
|
If the user is not authenticated, :meth:`LoginManager.unauthorized` is
|
||||||
|
called as normal. If they are authenticated, but their session is not
|
||||||
|
fresh, it will call :meth:`LoginManager.needs_refresh` instead. (In that
|
||||||
|
case, you will need to provide a :attr:`LoginManager.refresh_view`.)
|
||||||
|
|
||||||
|
Behaves identically to the :func:`login_required` decorator with respect
|
||||||
|
to configuration variables.
|
||||||
|
|
||||||
|
.. Note ::
|
||||||
|
|
||||||
|
Per `W3 guidelines for CORS preflight requests
|
||||||
|
<http://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0>`_,
|
||||||
|
HTTP ``OPTIONS`` requests are exempt from login checks.
|
||||||
|
|
||||||
|
:param func: The view function to decorate.
|
||||||
|
:type func: function
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def decorated_view(*args, **kwargs):
|
||||||
|
if request.method in EXEMPT_METHODS or current_app.config.get("LOGIN_DISABLED"):
|
||||||
|
pass
|
||||||
|
elif not current_user.is_authenticated:
|
||||||
|
return current_app.login_manager.unauthorized()
|
||||||
|
elif not login_fresh():
|
||||||
|
return current_app.login_manager.needs_refresh()
|
||||||
|
try:
|
||||||
|
# current_app.ensure_sync available in Flask >= 2.0
|
||||||
|
return current_app.ensure_sync(func)(*args, **kwargs)
|
||||||
|
except AttributeError: # pragma: no cover
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_view
|
||||||
|
|
||||||
|
|
||||||
|
def set_login_view(login_view, blueprint=None):
|
||||||
|
"""
|
||||||
|
Sets the login view for the app or blueprint. If a blueprint is passed,
|
||||||
|
the login view is set for this blueprint on ``blueprint_login_views``.
|
||||||
|
|
||||||
|
:param login_view: The user object to log in.
|
||||||
|
:type login_view: str
|
||||||
|
:param blueprint: The blueprint which this login view should be set on.
|
||||||
|
Defaults to ``None``.
|
||||||
|
:type blueprint: object
|
||||||
|
"""
|
||||||
|
|
||||||
|
num_login_views = len(current_app.login_manager.blueprint_login_views)
|
||||||
|
if blueprint is not None or num_login_views != 0:
|
||||||
|
(current_app.login_manager.blueprint_login_views[blueprint.name]) = login_view
|
||||||
|
|
||||||
|
if (
|
||||||
|
current_app.login_manager.login_view is not None
|
||||||
|
and None not in current_app.login_manager.blueprint_login_views
|
||||||
|
):
|
||||||
|
(
|
||||||
|
current_app.login_manager.blueprint_login_views[None]
|
||||||
|
) = current_app.login_manager.login_view
|
||||||
|
|
||||||
|
current_app.login_manager.login_view = None
|
||||||
|
else:
|
||||||
|
current_app.login_manager.login_view = login_view
|
||||||
|
|
||||||
|
|
||||||
|
def _get_user():
|
||||||
|
if has_request_context():
|
||||||
|
if "flask_httpauth_user" in g:
|
||||||
|
if g.flask_httpauth_user is not None:
|
||||||
|
return g.flask_httpauth_user
|
||||||
|
if "_login_user" not in g:
|
||||||
|
current_app.login_manager._load_user()
|
||||||
|
|
||||||
|
return g._login_user
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _cookie_digest(payload, key=None):
|
||||||
|
key = _secret_key(key)
|
||||||
|
|
||||||
|
return hmac.new(key, payload.encode("utf-8"), sha512).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_remote_addr():
|
||||||
|
address = request.headers.get("X-Forwarded-For", request.remote_addr)
|
||||||
|
if address is not None:
|
||||||
|
# An 'X-Forwarded-For' header includes a comma separated list of the
|
||||||
|
# addresses, the first address being the actual remote address.
|
||||||
|
address = address.encode("utf-8").split(b",")[0].strip()
|
||||||
|
return address
|
||||||
|
|
||||||
|
|
||||||
|
def _create_identifier():
|
||||||
|
user_agent = request.headers.get("User-Agent")
|
||||||
|
if user_agent is not None:
|
||||||
|
user_agent = user_agent.encode("utf-8")
|
||||||
|
base = f"{_get_remote_addr()}|{user_agent}"
|
||||||
|
if str is bytes:
|
||||||
|
base = str(base, "utf-8", errors="replace") # pragma: no cover
|
||||||
|
h = sha512()
|
||||||
|
h.update(base.encode("utf8"))
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _user_context_processor():
|
||||||
|
return dict(current_user=_get_user())
|
||||||
|
|
||||||
|
|
||||||
|
def _secret_key(key=None):
|
||||||
|
if key is None:
|
||||||
|
key = current_app.config["SECRET_KEY"]
|
||||||
|
|
||||||
|
if isinstance(key, str): # pragma: no cover
|
||||||
|
key = key.encode("latin1") # ensure bytes
|
||||||
|
|
||||||
|
return key
|
@ -23,6 +23,7 @@ import json
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
import unidecode
|
import unidecode
|
||||||
|
from weakref import WeakSet
|
||||||
|
|
||||||
from sqlite3 import OperationalError as sqliteOperationalError
|
from sqlite3 import OperationalError as sqliteOperationalError
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
@ -40,7 +41,7 @@ except ImportError:
|
|||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
from sqlalchemy.sql.expression import and_, true, false, text, func, or_
|
from sqlalchemy.sql.expression import and_, true, false, text, func, or_
|
||||||
from sqlalchemy.ext.associationproxy import association_proxy
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
from flask_login import current_user
|
from .cw_login import current_user
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
from flask import flash
|
from flask import flash
|
||||||
@ -48,8 +49,6 @@ from flask import flash
|
|||||||
from . import logger, ub, isoLanguages
|
from . import logger, ub, isoLanguages
|
||||||
from .pagination import Pagination
|
from .pagination import Pagination
|
||||||
|
|
||||||
from weakref import WeakSet
|
|
||||||
|
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ from flask import Blueprint, request, flash, redirect, url_for, abort, Response
|
|||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import lazy_gettext as N_
|
from flask_babel import lazy_gettext as N_
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
from flask_login import current_user, login_required
|
from .cw_login import current_user, login_required
|
||||||
from sqlalchemy.exc import OperationalError, IntegrityError, InterfaceError
|
from sqlalchemy.exc import OperationalError, IntegrityError, InterfaceError
|
||||||
from sqlalchemy.orm.exc import StaleDataError
|
from sqlalchemy.orm.exc import StaleDataError
|
||||||
from sqlalchemy.sql.expression import func
|
from sqlalchemy.sql.expression import func
|
||||||
@ -43,10 +43,11 @@ from . import config, ub, db, calibre_db
|
|||||||
from .services.worker import WorkerThread
|
from .services.worker import WorkerThread
|
||||||
from .tasks.upload import TaskUpload
|
from .tasks.upload import TaskUpload
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
from .usermanagement import login_required_if_no_ano
|
|
||||||
from .kobo_sync_status import change_archived_books
|
from .kobo_sync_status import change_archived_books
|
||||||
from .redirect import get_redirect_location
|
from .redirect import get_redirect_location
|
||||||
from .file_helper import validate_mime_type
|
from .file_helper import validate_mime_type
|
||||||
|
from .usermanagement import user_login_required, login_required_if_no_ano
|
||||||
|
|
||||||
|
|
||||||
editbook = Blueprint('edit-book', __name__)
|
editbook = Blueprint('edit-book', __name__)
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
@ -73,14 +74,14 @@ def edit_required(f):
|
|||||||
|
|
||||||
|
|
||||||
@editbook.route("/ajax/delete/<int:book_id>", methods=["POST"])
|
@editbook.route("/ajax/delete/<int:book_id>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def delete_book_from_details(book_id):
|
def delete_book_from_details(book_id):
|
||||||
return Response(delete_book_from_table(book_id, "", True), mimetype='application/json')
|
return Response(delete_book_from_table(book_id, "", True), mimetype='application/json')
|
||||||
|
|
||||||
|
|
||||||
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""}, methods=["POST"])
|
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""}, methods=["POST"])
|
||||||
@editbook.route("/delete/<int:book_id>/<string:book_format>", methods=["POST"])
|
@editbook.route("/delete/<int:book_id>/<string:book_format>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def delete_book_ajax(book_id, book_format):
|
def delete_book_ajax(book_id, book_format):
|
||||||
return delete_book_from_table(book_id, book_format, False, request.form.to_dict().get('location', ""))
|
return delete_book_from_table(book_id, book_format, False, request.form.to_dict().get('location', ""))
|
||||||
|
|
||||||
@ -331,7 +332,7 @@ def convert_bookformat(book_id):
|
|||||||
|
|
||||||
|
|
||||||
@editbook.route("/ajax/getcustomenum/<int:c_id>")
|
@editbook.route("/ajax/getcustomenum/<int:c_id>")
|
||||||
@login_required
|
@user_login_required
|
||||||
def table_get_custom_enum(c_id):
|
def table_get_custom_enum(c_id):
|
||||||
ret = list()
|
ret = list()
|
||||||
cc = (calibre_db.session.query(db.CustomColumns)
|
cc = (calibre_db.session.query(db.CustomColumns)
|
||||||
@ -455,7 +456,7 @@ def edit_list_book(param):
|
|||||||
|
|
||||||
|
|
||||||
@editbook.route("/ajax/sort_value/<field>/<int:bookid>")
|
@editbook.route("/ajax/sort_value/<field>/<int:bookid>")
|
||||||
@login_required
|
@user_login_required
|
||||||
def get_sorted_entry(field, bookid):
|
def get_sorted_entry(field, bookid):
|
||||||
if field in ['title', 'authors', 'sort', 'author_sort']:
|
if field in ['title', 'authors', 'sort', 'author_sort']:
|
||||||
book = calibre_db.get_filtered_book(bookid)
|
book = calibre_db.get_filtered_book(bookid)
|
||||||
@ -472,7 +473,7 @@ def get_sorted_entry(field, bookid):
|
|||||||
|
|
||||||
|
|
||||||
@editbook.route("/ajax/simulatemerge", methods=['POST'])
|
@editbook.route("/ajax/simulatemerge", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@edit_required
|
@edit_required
|
||||||
def simulate_merge_list_book():
|
def simulate_merge_list_book():
|
||||||
vals = request.get_json().get('Merge_books')
|
vals = request.get_json().get('Merge_books')
|
||||||
@ -488,7 +489,7 @@ def simulate_merge_list_book():
|
|||||||
|
|
||||||
|
|
||||||
@editbook.route("/ajax/mergebooks", methods=['POST'])
|
@editbook.route("/ajax/mergebooks", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@edit_required
|
@edit_required
|
||||||
def merge_list_book():
|
def merge_list_book():
|
||||||
vals = request.get_json().get('Merge_books')
|
vals = request.get_json().get('Merge_books')
|
||||||
@ -526,7 +527,7 @@ def merge_list_book():
|
|||||||
|
|
||||||
|
|
||||||
@editbook.route("/ajax/xchange", methods=['POST'])
|
@editbook.route("/ajax/xchange", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
@edit_required
|
@edit_required
|
||||||
def table_xchange_author_title():
|
def table_xchange_author_title():
|
||||||
vals = request.get_json().get('xchange')
|
vals = request.get_json().get('xchange')
|
||||||
|
@ -29,11 +29,11 @@ from shutil import move, copyfile
|
|||||||
|
|
||||||
from flask import Blueprint, flash, request, redirect, url_for, abort
|
from flask import Blueprint, flash, request, redirect, url_for, abort
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_login import login_required
|
|
||||||
|
|
||||||
from . import logger, gdriveutils, config, ub, calibre_db, csrf
|
from . import logger, gdriveutils, config, ub, calibre_db, csrf
|
||||||
from .admin import admin_required
|
from .admin import admin_required
|
||||||
from .file_helper import get_temp_dir
|
from .file_helper import get_temp_dir
|
||||||
|
from .usermanagement import user_login_required
|
||||||
|
|
||||||
gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive')
|
gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive')
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
@ -49,7 +49,7 @@ gdrive_watch_callback_token = 'target=calibreweb-watch_files' # nosec
|
|||||||
|
|
||||||
|
|
||||||
@gdrive.route("/authenticate")
|
@gdrive.route("/authenticate")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def authenticate_google_drive():
|
def authenticate_google_drive():
|
||||||
try:
|
try:
|
||||||
@ -76,7 +76,7 @@ def google_drive_callback():
|
|||||||
|
|
||||||
|
|
||||||
@gdrive.route("/watch/subscribe")
|
@gdrive.route("/watch/subscribe")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def watch_gdrive():
|
def watch_gdrive():
|
||||||
if not config.config_google_drive_watch_changes_response:
|
if not config.config_google_drive_watch_changes_response:
|
||||||
@ -102,7 +102,7 @@ def watch_gdrive():
|
|||||||
|
|
||||||
|
|
||||||
@gdrive.route("/watch/revoke")
|
@gdrive.route("/watch/revoke")
|
||||||
@login_required
|
@user_login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def revoke_watch_gdrive():
|
def revoke_watch_gdrive():
|
||||||
last_watch_response = config.config_google_drive_watch_changes_response
|
last_watch_response = config.config_google_drive_watch_changes_response
|
||||||
|
@ -34,7 +34,7 @@ from flask import send_from_directory, make_response, abort, url_for, Response
|
|||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import lazy_gettext as N_
|
from flask_babel import lazy_gettext as N_
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
from flask_login import current_user
|
from .cw_login import current_user
|
||||||
from sqlalchemy.sql.expression import true, false, and_, or_, text, func
|
from sqlalchemy.sql.expression import true, false, and_, or_, text, func
|
||||||
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
||||||
from werkzeug.datastructures import Headers
|
from werkzeug.datastructures import Headers
|
||||||
|
@ -27,10 +27,9 @@ import datetime
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
# from babel.dates import format_date
|
|
||||||
from flask import Blueprint, request, url_for
|
from flask import Blueprint, request, url_for
|
||||||
from flask_babel import format_date
|
from flask_babel import format_date
|
||||||
from flask_login import current_user
|
from .cw_login import current_user
|
||||||
|
|
||||||
from . import constants, logger
|
from . import constants, logger
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ from flask import (
|
|||||||
redirect,
|
redirect,
|
||||||
abort
|
abort
|
||||||
)
|
)
|
||||||
from flask_login import current_user
|
from .cw_login import current_user
|
||||||
from werkzeug.datastructures import Headers
|
from werkzeug.datastructures import Headers
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from sqlalchemy.sql.expression import and_, or_
|
from sqlalchemy.sql.expression import and_, or_
|
||||||
@ -44,7 +44,6 @@ from sqlalchemy.exc import StatementError
|
|||||||
from sqlalchemy.sql import select
|
from sqlalchemy.sql import select
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
|
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
|
||||||
from . import isoLanguages
|
from . import isoLanguages
|
||||||
from .epub import get_epub_layout
|
from .epub import get_epub_layout
|
||||||
|
@ -65,12 +65,14 @@ from os import urandom
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from flask import g, Blueprint, abort, request
|
from flask import g, Blueprint, abort, request
|
||||||
from flask_login import login_user, current_user, login_required
|
from .cw_login import login_user, current_user
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_limiter import RateLimitExceeded
|
from flask_limiter import RateLimitExceeded
|
||||||
|
|
||||||
from . import logger, config, calibre_db, db, helper, ub, lm, limiter
|
from . import logger, config, calibre_db, db, helper, ub, lm, limiter
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
|
from .usermanagement import user_login_required
|
||||||
|
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
@ -78,7 +80,7 @@ kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth")
|
|||||||
|
|
||||||
|
|
||||||
@kobo_auth.route("/generate_auth_token/<int:user_id>")
|
@kobo_auth.route("/generate_auth_token/<int:user_id>")
|
||||||
@login_required
|
@user_login_required
|
||||||
def generate_auth_token(user_id):
|
def generate_auth_token(user_id):
|
||||||
warning = False
|
warning = False
|
||||||
host_list = request.host.rsplit(':')
|
host_list = request.host.rsplit(':')
|
||||||
@ -120,7 +122,7 @@ def generate_auth_token(user_id):
|
|||||||
|
|
||||||
|
|
||||||
@kobo_auth.route("/deleteauthtoken/<int:user_id>", methods=["POST"])
|
@kobo_auth.route("/deleteauthtoken/<int:user_id>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def delete_auth_token(user_id):
|
def delete_auth_token(user_id):
|
||||||
# Invalidate any previously generated Kobo Auth token for this user
|
# Invalidate any previously generated Kobo Auth token for this user
|
||||||
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
|
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
|
||||||
|
@ -17,11 +17,11 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from flask_login import current_user
|
from .cw_login import current_user
|
||||||
from . import ub
|
from . import ub
|
||||||
import datetime
|
import datetime
|
||||||
from sqlalchemy.sql.expression import or_, and_, true
|
from sqlalchemy.sql.expression import or_, and_, true
|
||||||
from sqlalchemy import exc
|
# from sqlalchemy import exc
|
||||||
|
|
||||||
|
|
||||||
# Add the current book id to kobo_synced_books table for current user, if entry is already present,
|
# Add the current book id to kobo_synced_books table for current user, if entry is already present,
|
||||||
|
@ -30,7 +30,7 @@ from flask_dance.consumer import oauth_authorized, oauth_error
|
|||||||
from flask_dance.contrib.github import make_github_blueprint, github
|
from flask_dance.contrib.github import make_github_blueprint, github
|
||||||
from flask_dance.contrib.google import make_google_blueprint, google
|
from flask_dance.contrib.google import make_google_blueprint, google
|
||||||
from oauthlib.oauth2 import TokenExpiredError, InvalidGrantError
|
from oauthlib.oauth2 import TokenExpiredError, InvalidGrantError
|
||||||
from flask_login import login_user, current_user, login_required
|
from .cw_login import login_user, current_user, login_required
|
||||||
from sqlalchemy.orm.exc import NoResultFound
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
|
|
||||||
from . import constants, logger, config, app, ub
|
from . import constants, logger, config, app, ub
|
||||||
@ -340,7 +340,7 @@ def github_login():
|
|||||||
|
|
||||||
|
|
||||||
@oauth.route('/unlink/github', methods=["GET"])
|
@oauth.route('/unlink/github', methods=["GET"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def github_login_unlink():
|
def github_login_unlink():
|
||||||
return unlink_oauth(oauthblueprints[0]['id'])
|
return unlink_oauth(oauthblueprints[0]['id'])
|
||||||
|
|
||||||
@ -364,6 +364,6 @@ def google_login():
|
|||||||
|
|
||||||
|
|
||||||
@oauth.route('/unlink/google', methods=["GET"])
|
@oauth.route('/unlink/google', methods=["GET"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def google_login_unlink():
|
def google_login_unlink():
|
||||||
return unlink_oauth(oauthblueprints[1]['id'])
|
return unlink_oauth(oauthblueprints[1]['id'])
|
||||||
|
54
cps/opds.py
54
cps/opds.py
@ -25,73 +25,25 @@ import json
|
|||||||
from urllib.parse import unquote_plus
|
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, Response, g
|
||||||
from functools import wraps
|
|
||||||
# from flask_login import current_user
|
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_httpauth import HTTPBasicAuth
|
|
||||||
from werkzeug.datastructures import Authorization
|
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
|
||||||
|
|
||||||
from sqlalchemy.sql.expression import func, text, or_, and_, true
|
from sqlalchemy.sql.expression import func, text, or_, and_, true
|
||||||
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
||||||
|
|
||||||
from . import logger, config, db, calibre_db, ub, isoLanguages, constants
|
from . import logger, config, db, calibre_db, ub, isoLanguages, constants
|
||||||
# from .usermanagement import requires_basic_auth_if_no_ano
|
from .usermanagement import requires_basic_auth_if_no_ano, auth
|
||||||
from .helper import get_download_link, get_book_cover
|
from .helper import get_download_link, get_book_cover
|
||||||
from .pagination import Pagination
|
from .pagination import Pagination
|
||||||
from .web import render_read_books
|
from .web import render_read_books
|
||||||
from . import limiter, services
|
|
||||||
|
|
||||||
opds = Blueprint('opds', __name__)
|
opds = Blueprint('opds', __name__)
|
||||||
auth = HTTPBasicAuth()
|
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
@auth.verify_password
|
|
||||||
def verify_password(username, password):
|
|
||||||
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).first()
|
|
||||||
if config.config_anonbrowse == 1 and user.name.lower() == "guest":
|
|
||||||
return user
|
|
||||||
if bool(user and check_password_hash(str(user.password), password)) and user.name != "Guest":
|
|
||||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
|
||||||
return user
|
|
||||||
else:
|
|
||||||
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
|
|
||||||
log.warning('OPDS Login failed for user "%s" IP-address: %s', username, ip_address)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def requires_basic_auth_if_no_ano(f):
|
|
||||||
@wraps(f)
|
|
||||||
def decorated(*args, **kwargs):
|
|
||||||
authorisation = auth.get_auth()
|
|
||||||
if config.config_anonbrowse == 1 and not authorisation:
|
|
||||||
authorisation = Authorization(
|
|
||||||
b"Basic", {'username': "Guest", 'password': ""})
|
|
||||||
status = None
|
|
||||||
user = auth.authenticate(authorisation, "")
|
|
||||||
if config.config_login_type == constants.LOGIN_LDAP and services.ldap:
|
|
||||||
login_result, error = services.ldap.bind_user(authorisation.username, authorisation.password)
|
|
||||||
if login_result:
|
|
||||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
|
||||||
elif login_result is not None:
|
|
||||||
log.error(error)
|
|
||||||
user = None
|
|
||||||
if user in (False, None):
|
|
||||||
status = 401
|
|
||||||
if status:
|
|
||||||
try:
|
|
||||||
return auth.auth_error_callback(status)
|
|
||||||
except TypeError:
|
|
||||||
return auth.auth_error_callback()
|
|
||||||
g.flask_httpauth_user = user if user is not True \
|
|
||||||
else auth.username if auth else None
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
return decorated
|
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/")
|
@opds.route("/opds/")
|
||||||
@opds.route("/opds")
|
@opds.route("/opds")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
|
@ -25,12 +25,13 @@ from datetime import datetime
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from flask import Blueprint, request, make_response, abort, url_for, flash, redirect
|
from flask import Blueprint, request, make_response, abort, url_for, flash, redirect
|
||||||
from flask_login import login_required, current_user, login_user
|
from .cw_login import login_user, current_user
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from sqlalchemy.sql.expression import true
|
from sqlalchemy.sql.expression import true
|
||||||
|
|
||||||
from . import config, logger, ub
|
from . import config, logger, ub
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
|
from .usermanagement import user_login_required
|
||||||
|
|
||||||
|
|
||||||
remotelogin = Blueprint('remotelogin', __name__)
|
remotelogin = Blueprint('remotelogin', __name__)
|
||||||
@ -65,7 +66,7 @@ def remote_login():
|
|||||||
|
|
||||||
@remotelogin.route('/verify/<token>')
|
@remotelogin.route('/verify/<token>')
|
||||||
@remote_login_required
|
@remote_login_required
|
||||||
@login_required
|
@user_login_required
|
||||||
def verify_token(token):
|
def verify_token(token):
|
||||||
auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first()
|
auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first()
|
||||||
|
|
||||||
|
@ -19,14 +19,13 @@
|
|||||||
from flask import render_template, g, abort, request
|
from flask import render_template, g, abort, request
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from werkzeug.local import LocalProxy
|
from werkzeug.local import LocalProxy
|
||||||
from flask_login import current_user
|
from .cw_login import current_user
|
||||||
from sqlalchemy.sql.expression import or_
|
from sqlalchemy.sql.expression import or_
|
||||||
|
|
||||||
from . import config, constants, logger, ub
|
from . import config, constants, logger, ub
|
||||||
from .ub import User
|
from .ub import User
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
def get_sidebar_config(kwargs=None):
|
def get_sidebar_config(kwargs=None):
|
||||||
|
@ -19,7 +19,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
from flask import Blueprint, request, redirect, url_for, flash
|
from flask import Blueprint, request, redirect, url_for, flash
|
||||||
from flask import session as flask_session
|
from flask import session as flask_session
|
||||||
from flask_login import current_user
|
from .cw_login import current_user
|
||||||
from flask_babel import format_date
|
from flask_babel import format_date
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from sqlalchemy.sql.expression import func, not_, and_, or_, text, true
|
from sqlalchemy.sql.expression import func, not_, and_, or_, text, true
|
||||||
@ -30,6 +30,7 @@ from .usermanagement import login_required_if_no_ano
|
|||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
from .pagination import Pagination
|
from .pagination import Pagination
|
||||||
|
|
||||||
|
|
||||||
search = Blueprint('search', __name__)
|
search = Blueprint('search', __name__)
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
@ -24,14 +24,14 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from flask import Blueprint, Response, request, url_for
|
from flask import Blueprint, Response, request, url_for
|
||||||
from flask_login import current_user
|
from .cw_login import current_user
|
||||||
from flask_login import login_required
|
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
||||||
from sqlalchemy.orm.attributes import flag_modified
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
|
|
||||||
from cps.services.Metadata import Metadata
|
from cps.services.Metadata import Metadata
|
||||||
from . import constants, logger, ub, web_server
|
from . import constants, logger, ub, web_server
|
||||||
|
from .usermanagement import user_login_required
|
||||||
|
|
||||||
# current_milli_time = lambda: int(round(time() * 1000))
|
# current_milli_time = lambda: int(round(time() * 1000))
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ cl = list_classes(new_list)
|
|||||||
|
|
||||||
|
|
||||||
@meta.route("/metadata/provider")
|
@meta.route("/metadata/provider")
|
||||||
@login_required
|
@user_login_required
|
||||||
def metadata_provider():
|
def metadata_provider():
|
||||||
active = current_user.view_settings.get("metadata", {})
|
active = current_user.view_settings.get("metadata", {})
|
||||||
provider = list()
|
provider = list()
|
||||||
@ -95,7 +95,7 @@ def metadata_provider():
|
|||||||
|
|
||||||
@meta.route("/metadata/provider", methods=["POST"])
|
@meta.route("/metadata/provider", methods=["POST"])
|
||||||
@meta.route("/metadata/provider/<prov_name>", methods=["POST"])
|
@meta.route("/metadata/provider/<prov_name>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def metadata_change_active_provider(prov_name):
|
def metadata_change_active_provider(prov_name):
|
||||||
new_state = request.get_json()
|
new_state = request.get_json()
|
||||||
active = current_user.view_settings.get("metadata", {})
|
active = current_user.view_settings.get("metadata", {})
|
||||||
@ -122,7 +122,7 @@ def metadata_change_active_provider(prov_name):
|
|||||||
|
|
||||||
|
|
||||||
@meta.route("/metadata/search", methods=["POST"])
|
@meta.route("/metadata/search", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def metadata_search():
|
def metadata_search():
|
||||||
query = request.form.to_dict().get("query")
|
query = request.form.to_dict().get("query")
|
||||||
data = list()
|
data = list()
|
||||||
|
18
cps/shelf.py
18
cps/shelf.py
@ -25,13 +25,13 @@ from datetime import datetime
|
|||||||
|
|
||||||
from flask import Blueprint, flash, redirect, request, url_for, abort
|
from flask import Blueprint, flash, redirect, request, url_for, abort
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_login import current_user, login_required
|
from .cw_login import current_user
|
||||||
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
||||||
from sqlalchemy.sql.expression import func, true
|
from sqlalchemy.sql.expression import func, true
|
||||||
|
|
||||||
from . import calibre_db, config, db, logger, ub
|
from . import calibre_db, config, db, logger, ub
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
from .usermanagement import login_required_if_no_ano
|
from .usermanagement import login_required_if_no_ano, user_login_required
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ shelf = Blueprint('shelf', __name__)
|
|||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>", methods=["POST"])
|
@shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def add_to_shelf(shelf_id, book_id):
|
def add_to_shelf(shelf_id, book_id):
|
||||||
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
@ -103,7 +103,7 @@ def add_to_shelf(shelf_id, book_id):
|
|||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/massadd/<int:shelf_id>", methods=["POST"])
|
@shelf.route("/shelf/massadd/<int:shelf_id>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def search_to_shelf(shelf_id):
|
def search_to_shelf(shelf_id):
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
if shelf is None:
|
if shelf is None:
|
||||||
@ -155,7 +155,7 @@ def search_to_shelf(shelf_id):
|
|||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/remove/<int:shelf_id>/<int:book_id>", methods=["POST"])
|
@shelf.route("/shelf/remove/<int:shelf_id>/<int:book_id>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def remove_from_shelf(shelf_id, book_id):
|
def remove_from_shelf(shelf_id, book_id):
|
||||||
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
@ -212,14 +212,14 @@ def remove_from_shelf(shelf_id, book_id):
|
|||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/create", methods=["GET", "POST"])
|
@shelf.route("/shelf/create", methods=["GET", "POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def create_shelf():
|
def create_shelf():
|
||||||
shelf = ub.Shelf()
|
shelf = ub.Shelf()
|
||||||
return create_edit_shelf(shelf, page_title=_("Create a Shelf"), page="shelfcreate")
|
return create_edit_shelf(shelf, page_title=_("Create a Shelf"), page="shelfcreate")
|
||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"])
|
@shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def edit_shelf(shelf_id):
|
def edit_shelf(shelf_id):
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
if not check_shelf_edit_permissions(shelf):
|
if not check_shelf_edit_permissions(shelf):
|
||||||
@ -229,7 +229,7 @@ def edit_shelf(shelf_id):
|
|||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/delete/<int:shelf_id>", methods=["POST"])
|
@shelf.route("/shelf/delete/<int:shelf_id>", methods=["POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def delete_shelf(shelf_id):
|
def delete_shelf(shelf_id):
|
||||||
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
try:
|
try:
|
||||||
@ -259,7 +259,7 @@ def show_shelf(shelf_id, sort_param, page):
|
|||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/order/<int:shelf_id>", methods=["GET", "POST"])
|
@shelf.route("/shelf/order/<int:shelf_id>", methods=["GET", "POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def order_shelf(shelf_id):
|
def order_shelf(shelf_id):
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
if shelf and check_shelf_view_permissions(shelf):
|
if shelf and check_shelf_view_permissions(shelf):
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
from markupsafe import escape
|
from markupsafe import escape
|
||||||
|
|
||||||
from flask import Blueprint, jsonify
|
from flask import Blueprint, jsonify
|
||||||
from flask_login import login_required, current_user
|
from .cw_login import current_user
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import format_datetime
|
from flask_babel import format_datetime
|
||||||
from babel.units import format_unit
|
from babel.units import format_unit
|
||||||
@ -26,6 +26,7 @@ from . import logger
|
|||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS, STAT_ENDED, \
|
from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS, STAT_ENDED, \
|
||||||
STAT_CANCELLED
|
STAT_CANCELLED
|
||||||
|
from .usermanagement import user_login_required
|
||||||
|
|
||||||
tasks = Blueprint('tasks', __name__)
|
tasks = Blueprint('tasks', __name__)
|
||||||
|
|
||||||
@ -33,14 +34,14 @@ log = logger.create()
|
|||||||
|
|
||||||
|
|
||||||
@tasks.route("/ajax/emailstat")
|
@tasks.route("/ajax/emailstat")
|
||||||
@login_required
|
@user_login_required
|
||||||
def get_email_status_json():
|
def get_email_status_json():
|
||||||
tasks = WorkerThread.get_instance().tasks
|
tasks = WorkerThread.get_instance().tasks
|
||||||
return jsonify(render_task_status(tasks))
|
return jsonify(render_task_status(tasks))
|
||||||
|
|
||||||
|
|
||||||
@tasks.route("/tasks")
|
@tasks.route("/tasks")
|
||||||
@login_required
|
@user_login_required
|
||||||
def get_tasks_status():
|
def get_tasks_status():
|
||||||
# if current user admin, show all email, otherwise only own emails
|
# if current user admin, show all email, otherwise only own emails
|
||||||
return render_title_template('tasks.html', title=_("Tasks"), page="tasks")
|
return render_title_template('tasks.html', title=_("Tasks"), page="tasks")
|
||||||
|
28
cps/ub.py
28
cps/ub.py
@ -26,8 +26,8 @@ import uuid
|
|||||||
from flask import session as flask_session
|
from flask import session as flask_session
|
||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
|
|
||||||
from flask_login import AnonymousUserMixin, current_user
|
from .cw_login import AnonymousUserMixin, current_user
|
||||||
from flask_login import user_logged_in
|
from .cw_login import user_logged_in
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin
|
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin
|
||||||
@ -71,17 +71,22 @@ def signal_store_user_session(object, user):
|
|||||||
|
|
||||||
|
|
||||||
def store_user_session():
|
def store_user_session():
|
||||||
if flask_session.get('user_id', ""):
|
#if flask_session.get('user_id', ""):
|
||||||
flask_session['_user_id'] = flask_session.get('user_id', "")
|
# flask_session['_user_id'] = flask_session.get('user_id', "")
|
||||||
|
_user = flask_session.get('_user_id', "")
|
||||||
|
_id = flask_session.get('_id', "")
|
||||||
|
_random = flask_session.get('_random', "")
|
||||||
|
|
||||||
if flask_session.get('_user_id', ""):
|
if flask_session.get('_user_id', ""):
|
||||||
try:
|
try:
|
||||||
if not check_user_session(flask_session.get('_user_id', ""), flask_session.get('_id', "")):
|
if not check_user_session(_user, _id):
|
||||||
user_session = User_Sessions(flask_session.get('_user_id', ""), flask_session.get('_id', ""))
|
expiry = int((datetime.datetime.now() + datetime.timedelta(days=31)).timestamp())
|
||||||
|
user_session = User_Sessions(_user, _id, _random, expiry)
|
||||||
session.add(user_session)
|
session.add(user_session)
|
||||||
session.commit()
|
session.commit()
|
||||||
log.debug("Login and store session : " + flask_session.get('_id', ""))
|
log.debug("Login and store session : " + _id)
|
||||||
else:
|
else:
|
||||||
log.debug("Found stored session: " + flask_session.get('_id', ""))
|
log.debug("Found stored session: " + _id)
|
||||||
except (exc.OperationalError, exc.InvalidRequestError) as e:
|
except (exc.OperationalError, exc.InvalidRequestError) as e:
|
||||||
session.rollback()
|
session.rollback()
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
@ -335,11 +340,16 @@ class User_Sessions(Base):
|
|||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
user_id = Column(Integer, ForeignKey('user.id'))
|
user_id = Column(Integer, ForeignKey('user.id'))
|
||||||
session_key = Column(String, default="")
|
session_key = Column(String, default="")
|
||||||
|
random = Column(String, default="")
|
||||||
|
expiry = Column(String, default="")
|
||||||
|
|
||||||
def __init__(self, user_id, session_key):
|
|
||||||
|
def __init__(self, user_id, session_key, random, expiry):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
self.session_key = session_key
|
self.session_key = session_key
|
||||||
|
self.random = random
|
||||||
|
self.expiry = expiry
|
||||||
|
|
||||||
|
|
||||||
# Baseclass representing Shelfs in calibre-web in app.db
|
# Baseclass representing Shelfs in calibre-web in app.db
|
||||||
|
@ -19,16 +19,121 @@
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from sqlalchemy.sql.expression import func
|
from sqlalchemy.sql.expression import func
|
||||||
from flask_login import login_required, login_user
|
from .cw_login import login_required
|
||||||
|
|
||||||
|
from flask import request, g
|
||||||
|
from flask_httpauth import HTTPBasicAuth
|
||||||
|
from werkzeug.datastructures import Authorization
|
||||||
|
from werkzeug.security import check_password_hash
|
||||||
|
|
||||||
|
from . import lm, ub, config, logger, limiter, constants, services
|
||||||
|
|
||||||
from . import lm, ub, config, logger, limiter
|
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
|
'''class HTTPProxyAuth(HTTPAuth):
|
||||||
|
def __init__(self, scheme='Proxy', realm=None, header=None):
|
||||||
|
super(HTTPProxyAuth, self).__init__(scheme, realm, header)
|
||||||
|
self.user = None
|
||||||
|
self.verify_user_callback = None
|
||||||
|
|
||||||
|
def set_user(self, username):
|
||||||
|
self.user = username if username else None
|
||||||
|
|
||||||
|
def verify_login(self, f):
|
||||||
|
self.verify_user_callback = f
|
||||||
|
return f
|
||||||
|
|
||||||
|
def login_required(self, f=None, role=None, optional=None):
|
||||||
|
if f is not None and \
|
||||||
|
(role is not None or optional is not None): # pragma: no cover
|
||||||
|
raise ValueError(
|
||||||
|
'role and optional are the only supported arguments')
|
||||||
|
|
||||||
|
def login_required_internal(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
if self.user:
|
||||||
|
g.flask_httpauth_user = self.user
|
||||||
|
return self.ensure_sync(f)(*args, **kwargs)
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
if f:
|
||||||
|
return login_required_internal(f)
|
||||||
|
return login_required_internal
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate(self, _auth, stored_password=None):
|
||||||
|
req = getattr(_auth, 'req', '')
|
||||||
|
if self.verify_user_callback:
|
||||||
|
return self.ensure_sync(self.verify_user_callback)(req)'''
|
||||||
|
|
||||||
|
|
||||||
|
auth = HTTPBasicAuth()
|
||||||
|
# proxy_auth = HTTPProxyAuth()
|
||||||
|
|
||||||
|
|
||||||
|
@auth.verify_password
|
||||||
|
def verify_password(username, password):
|
||||||
|
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).first()
|
||||||
|
if user:
|
||||||
|
if user.name.lower() == "guest":
|
||||||
|
if config.config_anonbrowse == 1:
|
||||||
|
return user
|
||||||
|
if config.config_login_type == constants.LOGIN_LDAP and services.ldap:
|
||||||
|
login_result, error = services.ldap.bind_user(user.name, password)
|
||||||
|
if login_result:
|
||||||
|
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||||
|
return user
|
||||||
|
if error is not None:
|
||||||
|
log.error(error)
|
||||||
|
elif check_password_hash(str(user.password), password):
|
||||||
|
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||||
|
return user
|
||||||
|
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
|
||||||
|
log.warning('OPDS Login failed for user "%s" IP-address: %s', username, ip_address)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def requires_basic_auth_if_no_ano(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
authorisation = auth.get_auth()
|
||||||
|
status = None
|
||||||
|
user = None
|
||||||
|
if config.config_allow_reverse_proxy_header_login and not authorisation:
|
||||||
|
user = load_user_from_reverse_proxy_header(request)
|
||||||
|
if config.config_anonbrowse == 1 and not authorisation:
|
||||||
|
authorisation = Authorization(
|
||||||
|
b"Basic", {'username': "Guest", 'password': ""})
|
||||||
|
if not user:
|
||||||
|
user = auth.authenticate(authorisation, "")
|
||||||
|
if user in (False, None):
|
||||||
|
status = 401
|
||||||
|
if status:
|
||||||
|
try:
|
||||||
|
return auth.auth_error_callback(status)
|
||||||
|
except TypeError:
|
||||||
|
return auth.auth_error_callback()
|
||||||
|
g.flask_httpauth_user = user if user is not True \
|
||||||
|
else auth.username if auth else None
|
||||||
|
return auth.ensure_sync(f)(*args, **kwargs)
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
def login_required_if_no_ano(func):
|
def login_required_if_no_ano(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def decorated_view(*args, **kwargs):
|
def decorated_view(*args, **kwargs):
|
||||||
|
if config.config_allow_reverse_proxy_header_login:
|
||||||
|
user = load_user_from_reverse_proxy_header(request)
|
||||||
|
if user:
|
||||||
|
g.flask_httpauth_user = user
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
# proxy_auth.set_user(user)
|
||||||
|
# return proxy_auth.login_required(func)(*args, **kwargs)
|
||||||
|
g.flask_httpauth_user = None
|
||||||
if config.config_anonbrowse == 1:
|
if config.config_anonbrowse == 1:
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
return login_required(func)(*args, **kwargs)
|
return login_required(func)(*args, **kwargs)
|
||||||
@ -36,47 +141,39 @@ def login_required_if_no_ano(func):
|
|||||||
return decorated_view
|
return decorated_view
|
||||||
|
|
||||||
|
|
||||||
'''def _load_user_from_auth_header(username, password):
|
def user_login_required(func):
|
||||||
limiter.check()
|
@wraps(func)
|
||||||
user = _fetch_user_by_name(username)
|
def decorated_view(*args, **kwargs):
|
||||||
if bool(user and check_password_hash(str(user.password), password)) and user.name != "Guest":
|
if config.config_allow_reverse_proxy_header_login:
|
||||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
user = load_user_from_reverse_proxy_header(request)
|
||||||
login_user(user)
|
if user:
|
||||||
return user
|
g.flask_httpauth_user = user
|
||||||
else:
|
return func(*args, **kwargs)
|
||||||
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
|
g.flask_httpauth_user = None
|
||||||
log.warning('OPDS Login failed for user "%s" IP-address: %s', username, ip_address)
|
return login_required(func)(*args, **kwargs)
|
||||||
return None
|
|
||||||
|
return decorated_view
|
||||||
|
|
||||||
|
|
||||||
def _authenticate():
|
def load_user_from_reverse_proxy_header(req):
|
||||||
return Response(
|
rp_header_name = config.config_reverse_proxy_login_header_name
|
||||||
'Could not verify your access level for that URL.\n'
|
if rp_header_name:
|
||||||
'You have to login with proper credentials', 401,
|
rp_header_username = req.headers.get(rp_header_name)
|
||||||
{'WWW-Authenticate': 'Basic realm="Login Required"'})'''
|
if rp_header_username:
|
||||||
|
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == rp_header_username.lower()).first()
|
||||||
|
if user:
|
||||||
def _fetch_user_by_name(username):
|
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||||
return ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).first()
|
return user
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@lm.user_loader
|
@lm.user_loader
|
||||||
def load_user(user_id):
|
def load_user(user_id, random, session_key):
|
||||||
user = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
user = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||||
return user
|
entry = ub.session.query(ub.User_Sessions).filter(ub.User_Sessions.random == random,
|
||||||
|
ub.User_Sessions.session_key == session_key).first()
|
||||||
|
if entry and entry.id == user.id:
|
||||||
@lm.request_loader
|
return user
|
||||||
def load_user_from_reverse_proxy_header(req):
|
else:
|
||||||
if config.config_allow_reverse_proxy_header_login:
|
return None
|
||||||
rp_header_name = config.config_reverse_proxy_login_header_name
|
|
||||||
if rp_header_name:
|
|
||||||
rp_header_username = req.headers.get(rp_header_name)
|
|
||||||
if rp_header_username:
|
|
||||||
user = _fetch_user_by_name(rp_header_username)
|
|
||||||
if user:
|
|
||||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
|
||||||
login_user(user)
|
|
||||||
return user
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
23
cps/web.py
23
cps/web.py
@ -29,7 +29,7 @@ from flask import request, redirect, send_from_directory, make_response, flash,
|
|||||||
from flask import session as flask_session
|
from flask import session as flask_session
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
from flask_login import login_user, logout_user, login_required, current_user
|
from .cw_login import login_user, logout_user, current_user
|
||||||
from flask_limiter import RateLimitExceeded
|
from flask_limiter import RateLimitExceeded
|
||||||
from flask_limiter.util import get_remote_address
|
from flask_limiter.util import get_remote_address
|
||||||
from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
|
from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
|
||||||
@ -59,6 +59,7 @@ from .kobo_sync_status import change_archived_books
|
|||||||
from . import limiter
|
from . import limiter
|
||||||
from .services.worker import WorkerThread
|
from .services.worker import WorkerThread
|
||||||
from .tasks_status import render_task_status
|
from .tasks_status import render_task_status
|
||||||
|
from .usermanagement import user_login_required
|
||||||
|
|
||||||
|
|
||||||
feature_support = {
|
feature_support = {
|
||||||
@ -143,14 +144,14 @@ def viewer_required(f):
|
|||||||
|
|
||||||
|
|
||||||
@web.route("/ajax/emailstat")
|
@web.route("/ajax/emailstat")
|
||||||
@login_required
|
@user_login_required
|
||||||
def get_email_status_json():
|
def get_email_status_json():
|
||||||
tasks = WorkerThread.get_instance().tasks
|
tasks = WorkerThread.get_instance().tasks
|
||||||
return jsonify(render_task_status(tasks))
|
return jsonify(render_task_status(tasks))
|
||||||
|
|
||||||
|
|
||||||
@web.route("/ajax/bookmark/<int:book_id>/<book_format>", methods=['POST'])
|
@web.route("/ajax/bookmark/<int:book_id>/<book_format>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
def set_bookmark(book_id, book_format):
|
def set_bookmark(book_id, book_format):
|
||||||
bookmark_key = request.form["bookmark"]
|
bookmark_key = request.form["bookmark"]
|
||||||
ub.session.query(ub.Bookmark).filter(and_(ub.Bookmark.user_id == int(current_user.id),
|
ub.session.query(ub.Bookmark).filter(and_(ub.Bookmark.user_id == int(current_user.id),
|
||||||
@ -170,7 +171,7 @@ def set_bookmark(book_id, book_format):
|
|||||||
|
|
||||||
|
|
||||||
@web.route("/ajax/toggleread/<int:book_id>", methods=['POST'])
|
@web.route("/ajax/toggleread/<int:book_id>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
def toggle_read(book_id):
|
def toggle_read(book_id):
|
||||||
message = edit_book_read_status(book_id)
|
message = edit_book_read_status(book_id)
|
||||||
if message:
|
if message:
|
||||||
@ -180,7 +181,7 @@ def toggle_read(book_id):
|
|||||||
|
|
||||||
|
|
||||||
@web.route("/ajax/togglearchived/<int:book_id>", methods=['POST'])
|
@web.route("/ajax/togglearchived/<int:book_id>", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
def toggle_archived(book_id):
|
def toggle_archived(book_id):
|
||||||
is_archived = change_archived_books(book_id, message="Book {} archive bit toggled".format(book_id))
|
is_archived = change_archived_books(book_id, message="Book {} archive bit toggled".format(book_id))
|
||||||
if is_archived:
|
if is_archived:
|
||||||
@ -204,7 +205,7 @@ def update_view():
|
|||||||
|
|
||||||
'''
|
'''
|
||||||
@web.route("/ajax/getcomic/<int:book_id>/<book_format>/<int:page>")
|
@web.route("/ajax/getcomic/<int:book_id>/<book_format>/<int:page>")
|
||||||
@login_required
|
@user_login_required
|
||||||
def get_comic_book(book_id, book_format, page):
|
def get_comic_book(book_id, book_format, page):
|
||||||
book = calibre_db.get_book(book_id)
|
book = calibre_db.get_book(book_id)
|
||||||
if not book:
|
if not book:
|
||||||
@ -816,7 +817,7 @@ def books_list(data, sort_param, book_id, page):
|
|||||||
|
|
||||||
|
|
||||||
@web.route("/table")
|
@web.route("/table")
|
||||||
@login_required
|
@user_login_required
|
||||||
def books_table():
|
def books_table():
|
||||||
visibility = current_user.view_settings.get('table', {})
|
visibility = current_user.view_settings.get('table', {})
|
||||||
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
@ -825,7 +826,7 @@ def books_table():
|
|||||||
|
|
||||||
|
|
||||||
@web.route("/ajax/listbooks")
|
@web.route("/ajax/listbooks")
|
||||||
@login_required
|
@user_login_required
|
||||||
def list_books():
|
def list_books():
|
||||||
off = int(request.args.get("offset") or 0)
|
off = int(request.args.get("offset") or 0)
|
||||||
limit = int(request.args.get("limit") or config.config_books_per_page)
|
limit = int(request.args.get("limit") or config.config_books_per_page)
|
||||||
@ -906,7 +907,7 @@ def list_books():
|
|||||||
|
|
||||||
|
|
||||||
@web.route("/ajax/table_settings", methods=['POST'])
|
@web.route("/ajax/table_settings", methods=['POST'])
|
||||||
@login_required
|
@user_login_required
|
||||||
def update_table_settings():
|
def update_table_settings():
|
||||||
current_user.view_settings['table'] = json.loads(request.data)
|
current_user.view_settings['table'] = json.loads(request.data)
|
||||||
try:
|
try:
|
||||||
@ -1443,7 +1444,7 @@ def login_post():
|
|||||||
|
|
||||||
|
|
||||||
@web.route('/logout')
|
@web.route('/logout')
|
||||||
@login_required
|
@user_login_required
|
||||||
def logout():
|
def logout():
|
||||||
if current_user is not None and current_user.is_authenticated:
|
if current_user is not None and current_user.is_authenticated:
|
||||||
ub.delete_user_session(current_user.id, flask_session.get('_id', ""))
|
ub.delete_user_session(current_user.id, flask_session.get('_id', ""))
|
||||||
@ -1528,7 +1529,7 @@ def change_profile(kobo_support, local_oauth_check, oauth_status, translations,
|
|||||||
|
|
||||||
|
|
||||||
@web.route("/me", methods=["GET", "POST"])
|
@web.route("/me", methods=["GET", "POST"])
|
||||||
@login_required
|
@user_login_required
|
||||||
def profile():
|
def profile():
|
||||||
languages = calibre_db.speaking_language()
|
languages = calibre_db.speaking_language()
|
||||||
translations = get_available_locale()
|
translations = get_available_locale()
|
||||||
|
@ -42,7 +42,7 @@ natsort>=2.2.0,<8.5.0
|
|||||||
comicapi>=2.2.0,<3.3.0
|
comicapi>=2.2.0,<3.3.0
|
||||||
|
|
||||||
# Kobo integration
|
# Kobo integration
|
||||||
jsonschema>=3.2.0,<4.23.0
|
jsonschema>=3.2.0,<4.24.0
|
||||||
|
|
||||||
# Hide console Window on Windows
|
# Hide console Window on Windows
|
||||||
pywin32>=220,<310 ; sys_platform == 'win32'
|
pywin32>=220,<310 ; sys_platform == 'win32'
|
||||||
|
@ -2,7 +2,7 @@ Werkzeug<3.0.0
|
|||||||
APScheduler>=3.6.3,<3.11.0
|
APScheduler>=3.6.3,<3.11.0
|
||||||
Babel>=1.3,<3.0
|
Babel>=1.3,<3.0
|
||||||
Flask-Babel>=0.11.1,<4.1.0
|
Flask-Babel>=0.11.1,<4.1.0
|
||||||
Flask-Login>=0.3.2,<0.6.4
|
# Flask-Login>=0.3.2,<0.6.4
|
||||||
Flask-Principal>=0.3.2,<0.5.1
|
Flask-Principal>=0.3.2,<0.5.1
|
||||||
Flask>=1.0.2,<3.1.0
|
Flask>=1.0.2,<3.1.0
|
||||||
iso-639>=0.4.5,<0.5.0
|
iso-639>=0.4.5,<0.5.0
|
||||||
|
Loading…
Reference in New Issue
Block a user