diff --git a/amanuensis/backend/user.py b/amanuensis/backend/user.py index 1fc5e17..73dfe57 100644 --- a/amanuensis/backend/user.py +++ b/amanuensis/backend/user.py @@ -2,6 +2,7 @@ User query interface """ +import datetime import re from typing import Optional, Sequence @@ -75,6 +76,15 @@ def get_all_users(db: DbContext) -> Sequence[User]: return db(select(User)).scalars() +def get_user_by_id(db: DbContext, user_id: int) -> Optional[User]: + """ + Get a user by the user's id. + Returns None if no user was found. + """ + user: User = db(select(User).where(User.id == user_id)).scalar_one_or_none() + return user + + def get_user_by_username(db: DbContext, username: str) -> Optional[User]: """ Get a user by the user's username. @@ -98,3 +108,12 @@ def password_check(db: DbContext, username: str, password: str) -> bool: ).scalar_one() return check_password_hash(user_password_hash, password) + +def update_logged_in(db: DbContext, username: str) -> None: + """Bump the value of the last_login column for a user.""" + db( + update(User) + .where(User.username == username) + .values(last_login=datetime.datetime.utcnow()) + ) + db.session.commit() diff --git a/amanuensis/db/models.py b/amanuensis/db/models.py index 0b1642f..c304e13 100644 --- a/amanuensis/db/models.py +++ b/amanuensis/db/models.py @@ -100,6 +100,25 @@ class User(ModelBase): articles = relationship("Article", back_populates="user") posts = relationship("Post", back_populates="user") + ######################### + # Flask-Login interface # + ######################### + + @property + def is_authenticated(self: "User") -> bool: + return True + + @property + def is_active(self: "User") -> bool: + return True + + @property + def is_anonymous(self: "User") -> bool: + return False + + def get_id(self: "User") -> str: + return str(self.id) + class Lexicon(ModelBase): """ diff --git a/amanuensis/server/__init__.py b/amanuensis/server/__init__.py index eeb6a29..de649ec 100644 --- a/amanuensis/server/__init__.py +++ b/amanuensis/server/__init__.py @@ -5,7 +5,8 @@ from flask import Flask, g from amanuensis.config import AmanuensisConfig, CommandLineConfig from amanuensis.db import DbContext -import amanuensis.server.home +import amanuensis.server.auth as auth +import amanuensis.server.home as home def get_app( @@ -48,10 +49,11 @@ def get_app( app.jinja_options.update(trim_blocks=True, lstrip_blocks=True) # Set up Flask-Login - # TODO + auth.get_login_manager().init_app(app) # Register blueprints - app.register_blueprint(amanuensis.server.home.bp) + app.register_blueprint(auth.bp) + app.register_blueprint(home.bp) def test(): return "Hello, world!" diff --git a/amanuensis/server/auth/__init__.py b/amanuensis/server/auth/__init__.py index b8f6fc6..ef33b59 100644 --- a/amanuensis/server/auth/__init__.py +++ b/amanuensis/server/auth/__init__.py @@ -1,76 +1,79 @@ import logging -import time +from typing import Optional from flask import ( - Blueprint, - render_template, - redirect, - url_for, - flash, - current_app) + Blueprint, + flash, + g, + redirect, + render_template, + url_for, +) from flask_login import ( - login_user, - logout_user, - login_required, - LoginManager) + AnonymousUserMixin, + login_user, + logout_user, + login_required, + LoginManager, +) -from amanuensis.config import RootConfigDirectoryContext -from amanuensis.models import ModelFactory, AnonymousUserModel +import amanuensis.backend.user as userq +from amanuensis.db import User from .forms import LoginForm -logger = logging.getLogger(__name__) + +LOG = logging.getLogger(__name__) + +bp = Blueprint("auth", __name__, url_prefix="/auth", template_folder=".") -def get_login_manager(root: RootConfigDirectoryContext) -> LoginManager: - """ - Creates a login manager - """ - login_manager = LoginManager() - login_manager.login_view = 'auth.login' - login_manager.anonymous_user = AnonymousUserModel +def get_login_manager() -> LoginManager: + """Login manager factory""" + login_manager = LoginManager() + login_manager.login_view = "auth.login" + login_manager.anonymous_user = AnonymousUserMixin - @login_manager.user_loader - def load_user(uid): - return current_app.config['model_factory'].user(str(uid)) + def load_user(user_id_str: str) -> Optional[User]: + try: + user_id = int(user_id_str) + except: + return None + return userq.get_user_by_id(g.db, user_id) - return login_manager + login_manager.user_loader(load_user) + + return login_manager -bp_auth = Blueprint('auth', __name__, - url_prefix='/auth', - template_folder='.') - - -@bp_auth.route('/login/', methods=['GET', 'POST']) +@bp.route("/login/", methods=["GET", "POST"]) def login(): - model_factory: ModelFactory = current_app.config['model_factory'] - form = LoginForm() + form = LoginForm() - if not form.validate_on_submit(): - # Either the request was GET and we should render the form, - # or the request was POST and validation failed. - return render_template('auth.login.jinja', form=form) + if not form.validate_on_submit(): + # Either the request was GET and we should render the form, + # or the request was POST and validation failed. + return render_template("auth.login.jinja", form=form) - # POST with valid data - username = form.username.data - user = model_factory.try_user(username) - if not user or not user.check_password(form.password.data): - # Bad creds - flash("Login not recognized") - return redirect(url_for('auth.login')) + # POST with valid data + username: str = form.username.data + password: str = form.password.data + user: User = userq.get_user_by_username(g.db, username) + if not user or not userq.password_check(g.db, username, password): + # Bad creds + flash("Login not recognized") + return redirect(url_for("auth.login")) - # Login credentials were correct - remember_me = form.remember.data - login_user(user, remember=remember_me) - with user.ctx.edit_config() as cfg: - cfg.last_login = int(time.time()) - logger.info('Logged in user "{0.username}" ({0.uid})'.format(user.cfg)) - return redirect(url_for('home.home')) + # Login credentials were correct + remember_me: bool = form.remember.data + login_user(user, remember=remember_me) + userq.update_logged_in(g.db, username) + LOG.info("Logged in user {0.username} ({0.id})".format(user)) + return redirect(url_for("home.admin")) -@bp_auth.route("/logout/", methods=['GET']) +@bp.get("/logout/") @login_required def logout(): - logout_user() - return redirect(url_for('home.home')) + logout_user() + return redirect(url_for("home.admin")) diff --git a/amanuensis/server/auth/forms.py b/amanuensis/server/auth/forms.py index cf466a3..06dd1b3 100644 --- a/amanuensis/server/auth/forms.py +++ b/amanuensis/server/auth/forms.py @@ -4,12 +4,9 @@ from wtforms.validators import DataRequired class LoginForm(FlaskForm): - """/auth/login/""" - username = StringField( - 'Username', - validators=[DataRequired()]) - password = PasswordField( - 'Password', - validators=[DataRequired()]) - remember = BooleanField('Stay logged in') - submit = SubmitField('Log in') + """/auth/login/""" + + username = StringField("Username", validators=[DataRequired()]) + password = PasswordField("Password", validators=[DataRequired()]) + remember = BooleanField("Stay logged in") + submit = SubmitField("Log in") diff --git a/amanuensis/server/macros.jinja b/amanuensis/server/macros.jinja index 5ac6843..fcf3d45 100644 --- a/amanuensis/server/macros.jinja +++ b/amanuensis/server/macros.jinja @@ -9,12 +9,12 @@ [{{ lexicon.status.capitalize() }}]

{{ lexicon.prompt }}

- {# {% if current_user.is_authenticated %} #} + {% if current_user.is_authenticated %}

{# TODO #} {# {% if current_user.uid in lexicon.cfg.join.joined - or current_user.cfg.is_admin + or current_user.is_site_admin %} #} Editor: {#{ lexicon.cfg.editor|user_attr('username') }#} / Players: @@ -31,7 +31,7 @@ {# {% endif %} #} {# {% endif %} #}

- {# {% endif %} #} + {% endif %} {% endmacro %} diff --git a/amanuensis/server/page.jinja b/amanuensis/server/page.jinja index 9e1e362..4cf7e36 100644 --- a/amanuensis/server/page.jinja +++ b/amanuensis/server/page.jinja @@ -11,13 +11,12 @@