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/domain/auth.py b/atst/domain/auth.py index 8be8429f..1737763f 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,14 @@ def get_current_user(): return False +def get_last_login(): + return session.get("user_id") and session.get("last_login") + + 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..baf5acb0 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): + 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 22806c7d..1a156fec 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,7 @@ class User( citizenship = Column(String) designation = Column(String) date_latest_training = Column(Date) + 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/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..39e1bf96 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 %} diff --git a/tests/domain/test_users.py b/tests/domain/test_users.py index c83cbd1e..116e5294 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,11 @@ 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() + Users.update_last_login(new_user) + last_login = new_user.last_login + 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)