1
0
mirror of https://github.com/janeczku/calibre-web synced 2024-11-08 19:09:59 +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:
Ozzie Isaacs 2024-07-14 16:24:07 +02:00
parent 2d470e0ce1
commit ebe7cd7ba4
32 changed files with 1524 additions and 233 deletions

View File

@ -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

View File

@ -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
) )

View File

@ -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()

View File

@ -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)

View File

@ -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
View 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
View 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

View 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
View 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
View 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
View 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

View File

@ -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()

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)\

View File

@ -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,

View File

@ -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'])

View File

@ -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

View File

@ -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()

View File

@ -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):

View File

@ -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()

View File

@ -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()

View File

@ -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):

View File

@ -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")

View File

@ -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

View File

@ -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":
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
login_user(user)
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 _authenticate():
return Response(
'Could not verify your access level for that URL.\n'
'You have to login with proper credentials', 401,
{'WWW-Authenticate': 'Basic realm="Login Required"'})'''
def _fetch_user_by_name(username):
return ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).first()
@lm.user_loader
def load_user(user_id):
user = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
return user
@lm.request_loader
def load_user_from_reverse_proxy_header(req):
if config.config_allow_reverse_proxy_header_login: 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)
g.flask_httpauth_user = None
return login_required(func)(*args, **kwargs)
return decorated_view
def load_user_from_reverse_proxy_header(req):
rp_header_name = config.config_reverse_proxy_login_header_name rp_header_name = config.config_reverse_proxy_login_header_name
if rp_header_name: if rp_header_name:
rp_header_username = req.headers.get(rp_header_name) rp_header_username = req.headers.get(rp_header_name)
if rp_header_username: if rp_header_username:
user = _fetch_user_by_name(rp_header_username) user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == rp_header_username.lower()).first()
if user: if user:
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits] [limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
login_user(user)
return user return user
return None return None
@lm.user_loader
def load_user(user_id, random, session_key):
user = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
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:
return user
else:
return None

View File

@ -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()

View File

@ -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'

View File

@ -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