From e4e699fa1b43e15b2cbfb09b3b62375aa626b737 Mon Sep 17 00:00:00 2001
From: Tim Van Baak
Date: Wed, 16 Jun 2021 00:37:49 -0700
Subject: [PATCH] Reintegrate auth routes
---
amanuensis/backend/user.py | 19 +++++
amanuensis/db/models.py | 19 +++++
amanuensis/server/__init__.py | 8 ++-
amanuensis/server/auth/__init__.py | 111 +++++++++++++++--------------
amanuensis/server/auth/forms.py | 15 ++--
amanuensis/server/macros.jinja | 6 +-
amanuensis/server/page.jinja | 11 ++-
7 files changed, 114 insertions(+), 75 deletions(-)
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 @@