From dec3a0eb48646e668218a61bddb5c7fbcdd08f9f Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Thu, 28 Mar 2019 15:49:35 -0400 Subject: [PATCH 1/5] Migration for last_login column --- .../49e12ae7c9ca_add_last_login_to_user.py | 28 +++++++++++++++++++ atst/models/user.py | 5 +++- 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 alembic/versions/49e12ae7c9ca_add_last_login_to_user.py diff --git a/alembic/versions/49e12ae7c9ca_add_last_login_to_user.py b/alembic/versions/49e12ae7c9ca_add_last_login_to_user.py new file mode 100644 index 00000000..5c49b24b --- /dev/null +++ b/alembic/versions/49e12ae7c9ca_add_last_login_to_user.py @@ -0,0 +1,28 @@ +"""add last login to user + +Revision ID: 49e12ae7c9ca +Revises: fc08d99bb7f7 +Create Date: 2019-03-28 15:46:58.226281 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '49e12ae7c9ca' +down_revision = 'fc08d99bb7f7' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('last_login', sa.TIMESTAMP(timezone=True), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('users', 'last_login') + # ### end Alembic commands ### diff --git a/atst/models/user.py b/atst/models/user.py index 22806c7d..eea21268 100644 --- a/atst/models/user.py +++ b/atst/models/user.py @@ -1,4 +1,4 @@ -from sqlalchemy import String, ForeignKey, Column, Date, Boolean, Table +from sqlalchemy import String, ForeignKey, Column, Date, Boolean, Table, TIMESTAMP from sqlalchemy.orm import relationship from sqlalchemy.dialects.postgresql import UUID @@ -36,6 +36,9 @@ class User( citizenship = Column(String) designation = Column(String) date_latest_training = Column(Date) + last_login = Column( + TIMESTAMP(timezone=True), nullable=True + ) provisional = Column(Boolean) From 610aef428dba5bb3368d9690f39f6e2371b34629 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Mon, 1 Apr 2019 14:06:44 -0400 Subject: [PATCH 2/5] Add user's last login to the session data --- atst/domain/auth.py | 11 +++++++++++ atst/domain/users.py | 7 +++++++ atst/models/user.py | 4 +--- atst/routes/__init__.py | 9 +++++++-- atst/routes/dev.py | 6 ++---- tests/domain/test_users.py | 13 +++++++++++++ tests/test_auth.py | 16 ++++++++++++++++ 7 files changed, 57 insertions(+), 9 deletions(-) diff --git a/atst/domain/auth.py b/atst/domain/auth.py index 8be8429f..256511f9 100644 --- a/atst/domain/auth.py +++ b/atst/domain/auth.py @@ -22,6 +22,8 @@ def apply_authentication(app): user = get_current_user() if user: g.current_user = user + g.last_login = get_last_login() + if should_redirect_to_user_profile(request, user): return redirect(url_for("users.user", next=request.path)) elif not _unprotected_route(request): @@ -50,9 +52,18 @@ def get_current_user(): return False +def get_last_login(): + last_login = session.get("last_login") + if last_login and session.get("user_id"): + return last_login + else: + return False + + def logout(): if session.get("user_id"): # pragma: no branch del session["user_id"] + del session["last_login"] def _unprotected_route(request): diff --git a/atst/domain/users.py b/atst/domain/users.py index 21a896a3..b276a5bd 100644 --- a/atst/domain/users.py +++ b/atst/domain/users.py @@ -1,5 +1,6 @@ from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.exc import IntegrityError +from datetime import datetime from atst.database import db from atst.models import User @@ -82,6 +83,12 @@ class Users(object): return user + @classmethod + def update_last_login(cls, user): + setattr(user, "last_login", datetime.now()) + db.session.add(user) + db.session.commit() + @classmethod def finalize(cls, user): user.provisional = False diff --git a/atst/models/user.py b/atst/models/user.py index eea21268..1a156fec 100644 --- a/atst/models/user.py +++ b/atst/models/user.py @@ -36,9 +36,7 @@ class User( citizenship = Column(String) designation = Column(String) date_latest_training = Column(Date) - last_login = Column( - TIMESTAMP(timezone=True), nullable=True - ) + last_login = Column(TIMESTAMP(timezone=True), nullable=True) provisional = Column(Boolean) diff --git a/atst/routes/__init__.py b/atst/routes/__init__.py index 2f1ad3c2..b4543d07 100644 --- a/atst/routes/__init__.py +++ b/atst/routes/__init__.py @@ -122,6 +122,12 @@ def redirect_after_login_url(): return url_for("atst.home") +def current_user_setup(user): + session["user_id"] = user.id + session["last_login"] = user.last_login + Users.update_last_login(user) + + @bp.route("/login-redirect") def login_redirect(): auth_context = _make_authentication_context() @@ -131,8 +137,7 @@ def login_redirect(): if user.provisional: Users.finalize(user) - session["user_id"] = user.id - + current_user_setup(user) return redirect(redirect_after_login_url()) diff --git a/atst/routes/dev.py b/atst/routes/dev.py index 6a9631d5..72a9b75f 100644 --- a/atst/routes/dev.py +++ b/atst/routes/dev.py @@ -1,7 +1,6 @@ from flask import ( Blueprint, request, - session, redirect, render_template, url_for, @@ -9,7 +8,7 @@ from flask import ( ) import pendulum -from . import redirect_after_login_url +from . import redirect_after_login_url, current_user_setup from atst.domain.users import Users from atst.domain.permission_sets import PermissionSets from atst.queue import queue @@ -124,8 +123,7 @@ def login_dev(): user_data, ), ) - session["user_id"] = user.id - + current_user_setup(user) return redirect(redirect_after_login_url()) diff --git a/tests/domain/test_users.py b/tests/domain/test_users.py index c83cbd1e..1c0cdcbc 100644 --- a/tests/domain/test_users.py +++ b/tests/domain/test_users.py @@ -1,4 +1,5 @@ import pytest +from datetime import datetime from uuid import uuid4 from atst.domain.users import Users @@ -65,3 +66,15 @@ def test_update_user_with_dod_id(): Users.update(new_user, {"dod_id": "1234567890"}) assert "dod_id" in str(excinfo.value) + + +def test_update_user_with_last_login(): + new_user = UserFactory.create(last_login=datetime.now()) + Users.update_last_login(new_user) + last_login = new_user.last_login + + with pytest.raises(UnauthorizedError): + Users.update(new_user, {"last_login": datetime.now()}) + + Users.update_last_login(new_user) + assert new_user.last_login > last_login diff --git a/tests/test_auth.py b/tests/test_auth.py index 5b95a9d7..9d89697a 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,6 +1,7 @@ from urllib.parse import urlparse import pytest +from datetime import datetime from flask import session, url_for from .mocks import DOD_SDN_INFO, DOD_SDN, FIXTURE_EMAIL_ADDRESS from atst.domain.users import Users @@ -224,3 +225,18 @@ def test_error_on_invalid_crl(client, monkeypatch): response = _login(client) assert response.status_code == 401 assert "Error Code 008" in response.data.decode() + + +def test_last_login_set_when_user_logs_in(client, monkeypatch): + last_login = datetime.now() + user = UserFactory.create(last_login=last_login) + monkeypatch.setattr( + "atst.domain.authnid.AuthenticationContext.authenticate", lambda *args: True + ) + monkeypatch.setattr( + "atst.domain.authnid.AuthenticationContext.get_user", lambda *args: user + ) + response = _login(client) + assert session["last_login"] + assert user.last_login > session["last_login"] + assert isinstance(session["last_login"], datetime) From 2feb10ea9892f598940374fb754cfcdb937631ac Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Mon, 1 Apr 2019 14:30:43 -0400 Subject: [PATCH 3/5] Add timestamp to footer --- styles/components/_footer.scss | 2 ++ templates/footer.html | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/styles/components/_footer.scss b/styles/components/_footer.scss index 0915d67b..7ab3ba50 100644 --- a/styles/components/_footer.scss +++ b/styles/components/_footer.scss @@ -11,6 +11,8 @@ bottom: 0; width: 100%; height: $footer-height; + color: $color-gray-dark; + font-size: 1.5rem; .app-footer__info { flex-grow: 1; diff --git a/templates/footer.html b/templates/footer.html index ff1f807a..340b10cf 100644 --- a/templates/footer.html +++ b/templates/footer.html @@ -7,4 +7,9 @@ {{ "footer.jedi_help_link_text" | translate }} + {% if g.last_login %} +
+ Last Login: +
+ {% endif %} From 215c2b4cbcb3499b93eb241cebe6606c09e020d0 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Wed, 3 Apr 2019 12:23:42 -0400 Subject: [PATCH 4/5] Updates from PR feedback --- atst/domain/users.py | 2 +- templates/footer.html | 2 +- tests/domain/test_users.py | 6 +----- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/atst/domain/users.py b/atst/domain/users.py index b276a5bd..baf5acb0 100644 --- a/atst/domain/users.py +++ b/atst/domain/users.py @@ -85,7 +85,7 @@ class Users(object): @classmethod def update_last_login(cls, user): - setattr(user, "last_login", datetime.now()) + user.last_login = datetime.now() db.session.add(user) db.session.commit() diff --git a/templates/footer.html b/templates/footer.html index 340b10cf..39e1bf96 100644 --- a/templates/footer.html +++ b/templates/footer.html @@ -9,7 +9,7 @@ {% if g.last_login %}
- Last Login: + Last Login:
{% endif %} diff --git a/tests/domain/test_users.py b/tests/domain/test_users.py index 1c0cdcbc..116e5294 100644 --- a/tests/domain/test_users.py +++ b/tests/domain/test_users.py @@ -69,12 +69,8 @@ def test_update_user_with_dod_id(): def test_update_user_with_last_login(): - new_user = UserFactory.create(last_login=datetime.now()) + new_user = UserFactory.create() Users.update_last_login(new_user) last_login = new_user.last_login - - with pytest.raises(UnauthorizedError): - Users.update(new_user, {"last_login": datetime.now()}) - Users.update_last_login(new_user) assert new_user.last_login > last_login From cc11123eba3065d55ac57ddb64c818530b15c19d Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Wed, 3 Apr 2019 13:03:17 -0400 Subject: [PATCH 5/5] Simplify get_last_login() --- atst/domain/auth.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/atst/domain/auth.py b/atst/domain/auth.py index 256511f9..1737763f 100644 --- a/atst/domain/auth.py +++ b/atst/domain/auth.py @@ -53,11 +53,7 @@ def get_current_user(): def get_last_login(): - last_login = session.get("last_login") - if last_login and session.get("user_id"): - return last_login - else: - return False + return session.get("user_id") and session.get("last_login") def logout():