# -*- coding: utf-8 -*-
"""
Authorisation model.
Everything with user will go here regarding login/logout and
authorisation to access something (if not done in get_item function.)
Several function are decorators for called views, e.g. login_required or role_required
"""
import functools
import datetime
# import typing
from typing import Callable
# from werkzeug import Response
from werkzeug.security import check_password_hash
from werkzeug.wrappers.response import Response
from flask import (
Blueprint,
request,
flash,
g,
redirect,
render_template,
session as login_session, # avoid ambiguities between db_session and login_session
url_for,
current_app,
abort,
)
import ResearchNotes.database_transactions as dbt
from ResearchNotes.database import db, User
from ResearchNotes.form import LoginForm, SearchForm
# Param = typing.ParamSpec("Param")
# RetType = typing.TypeVar("RetType")
# OriginalFunc = Callable[Param, RetType]
# DecoratedFunc = Callable
bp = Blueprint("auth", __name__, url_prefix="/auth")
[docs]@bp.route("/login", methods=("GET", "POST"))
def login() -> str | Response:
"""
Login of registered user.
Returns
-------
str|Flask.Response
Renders either the login template or redirects to the main index.
"""
form = LoginForm()
if g.user:
flash(
f"Already signed in as {g.user.UserName}. If it is not your user, sign out and sign in with correct user, please.",
"alert-info",
)
return redirect(url_for("main.index"))
if form.validate_on_submit():
username = form.username.data
password = form.password.data
#
# error = None
fuser = db.session.execute(db.select(User).filter_by(name=username)).scalar()
current_app.logger.debug(f"login : {fuser}")
if fuser is not None and check_password_hash(fuser.password, password) and fuser.is_active:
dbt.update_session_metadata(db, fuser, IP_address=request.remote_addr, login=True)
login_session.clear()
login_session["user_id"] = fuser.id
# login_session["active"] = datetime.datetime.now()
login_session.permanent = form.remember.data
login_session["session_salt"] = str(fuser.id) + str(datetime.datetime.now())
login_session["urls"] = []
current_app.logger.info(f" login : Signed in {fuser} (IP: {str(request.remote_addr)})")
if fuser.login_count < 2:
flash("Please change your password on first login", "alert-info")
return redirect(url_for("conf.changepasswd"))
return redirect(url_for("main.index"))
flash("Invalid username or password", "alert-warning")
current_app.logger.warning(
f" login : Failed sign in for {username} IP: {str(request.remote_addr)}"
)
return render_template("auth/login.html", form=form)
[docs]@bp.before_app_request
def load_logged_in_user():
"""
Before request function.
Loads the user data from the session cookie before a request. Will be needed
for the user logic.
Returns
-------
None.
"""
user_id = login_session.get("user_id")
if user_id is not None:
# pylint: disable=assigning-non-slot
g.user = db.session.get(User, int(user_id)) # This is correct. Pylint gives false error.
g.search_form = SearchForm()
g.salt = login_session.get("session_salt")
g.ess_for_all = current_app.config["ESS_PER_GROUP"]
else:
g.user = None # pylint: disable=assigning-non-slot
[docs]@bp.route("/logout")
def logout() -> Response:
"""
Logout current user.
Logs user out and closes the session (remove session cookie).
Returns
-------
Flask.Response
Redirect to log-in view .
"""
if g.user is None:
flash("Session expired or session cookie deleted", "alert-warning")
login_session.clear()
return redirect(url_for("auth.login"))
#
dbt.update_session_metadata(db, g.user, login=False)
current_app.logger.info(f" logout : Signed out {g.user}")
login_session.clear()
flash(f"Signed out user {g.user.name}", "alert-success")
return redirect(url_for("auth.login"))
[docs]def login_required(view: Callable) -> Callable:
"""
Check login decorator.
Decorator function that checks, if user is logged in. We can also use this later, if we
create an admin group to give rights to groups or persons.
Parameters
----------
view : function
View or function to be decorated.
Returns
-------
Callable|Flask.Response
Returns decorated function or redirects to log-in screen.
"""
@functools.wraps(view)
def wrapped_view(**kwargs) -> Callable | Response:
if g.user is None:
flash("You need to login", "alert-secondary")
current_app.logger.warning(
f" login_required: Someone tried to access {view.__name__}"
+ f" without log in (IP: {str(request.remote_addr)})"
)
return redirect(url_for("auth.login"))
# Stop checking session expiring. Reset max session time in config.
# if g.inactive > current_app.config["SESSION_LIFETIME"]:
# current_app.logger.info(
# f" : auth.load_logged_user : Signed out {g.user} as session expired)"
# )
# # login_session["active"] = now
# return redirect(url_for("auth.logout"))
return view(**kwargs)
return wrapped_view
[docs]def role_required(roles: list[str]) -> Callable: # That guy is quasi not to type in its current form.
"""
Check rights decorator.
Decorator function that checks, if user is in the group to
carry out a certain task.
Parameters
----------
roles : List
List of roles that can access the view.
Returns
-------
Callable|Flask.Response
The function the role_required decorator is applied to or redirect to
log-in view.
"""
def inner_decorator(view: Callable) -> Response | Callable:
@functools.wraps(view)
def wrapped_view(**kwargs) -> Response | Callable:
if g.user is None:
flash("You need to login", "alert-secondary")
current_app.logger.warning(
f" role_required: Someone tried to access {view.__name__}"
+ f" without log in (IP: {str(request.remote_addr)})"
)
return redirect(url_for("auth.login"))
# if g.inactive > current_app.config["SESSION_LIFETIME"]:
# current_app.logger.info(
# f" : auth.load_logged_user : Signed out {g.user} as session expired)"
# )
# return redirect(url_for("auth.logout"))
if g.user.role_member.name not in roles:
flash("Your do not having the right role for this", "alert-warning")
return abort(
403, description=f" : auth.role_required : {g.user} tried to access {view.__name__}"
)
return view(**kwargs)
return wrapped_view
return inner_decorator