Merge pull request #733 from dod-ccpo/add-last-login-timestamp

Add last login timestamp
This commit is contained in:
leigh-mil 2019-04-03 13:10:52 -04:00 committed by GitHub
commit f1c6717a1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 85 additions and 7 deletions

View File

@ -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 ###

View File

@ -22,6 +22,8 @@ def apply_authentication(app):
user = get_current_user() user = get_current_user()
if user: if user:
g.current_user = user g.current_user = user
g.last_login = get_last_login()
if should_redirect_to_user_profile(request, user): if should_redirect_to_user_profile(request, user):
return redirect(url_for("users.user", next=request.path)) return redirect(url_for("users.user", next=request.path))
elif not _unprotected_route(request): elif not _unprotected_route(request):
@ -50,9 +52,14 @@ def get_current_user():
return False return False
def get_last_login():
return session.get("user_id") and session.get("last_login")
def logout(): def logout():
if session.get("user_id"): # pragma: no branch if session.get("user_id"): # pragma: no branch
del session["user_id"] del session["user_id"]
del session["last_login"]
def _unprotected_route(request): def _unprotected_route(request):

View File

@ -1,5 +1,6 @@
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from datetime import datetime
from atst.database import db from atst.database import db
from atst.models import User from atst.models import User
@ -82,6 +83,12 @@ class Users(object):
return user return user
@classmethod
def update_last_login(cls, user):
user.last_login = datetime.now()
db.session.add(user)
db.session.commit()
@classmethod @classmethod
def finalize(cls, user): def finalize(cls, user):
user.provisional = False user.provisional = False

View File

@ -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.orm import relationship
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
@ -36,6 +36,7 @@ class User(
citizenship = Column(String) citizenship = Column(String)
designation = Column(String) designation = Column(String)
date_latest_training = Column(Date) date_latest_training = Column(Date)
last_login = Column(TIMESTAMP(timezone=True), nullable=True)
provisional = Column(Boolean) provisional = Column(Boolean)

View File

@ -122,6 +122,12 @@ def redirect_after_login_url():
return url_for("atst.home") 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") @bp.route("/login-redirect")
def login_redirect(): def login_redirect():
auth_context = _make_authentication_context() auth_context = _make_authentication_context()
@ -131,8 +137,7 @@ def login_redirect():
if user.provisional: if user.provisional:
Users.finalize(user) Users.finalize(user)
session["user_id"] = user.id current_user_setup(user)
return redirect(redirect_after_login_url()) return redirect(redirect_after_login_url())

View File

@ -1,7 +1,6 @@
from flask import ( from flask import (
Blueprint, Blueprint,
request, request,
session,
redirect, redirect,
render_template, render_template,
url_for, url_for,
@ -9,7 +8,7 @@ from flask import (
) )
import pendulum 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.users import Users
from atst.domain.permission_sets import PermissionSets from atst.domain.permission_sets import PermissionSets
from atst.queue import queue from atst.queue import queue
@ -124,8 +123,7 @@ def login_dev():
user_data, user_data,
), ),
) )
session["user_id"] = user.id current_user_setup(user)
return redirect(redirect_after_login_url()) return redirect(redirect_after_login_url())

View File

@ -11,6 +11,8 @@
bottom: 0; bottom: 0;
width: 100%; width: 100%;
height: $footer-height; height: $footer-height;
color: $color-gray-dark;
font-size: 1.5rem;
.app-footer__info { .app-footer__info {
flex-grow: 1; flex-grow: 1;

View File

@ -7,4 +7,9 @@
<span>{{ "footer.jedi_help_link_text" | translate }}</span> <span>{{ "footer.jedi_help_link_text" | translate }}</span>
</a> </a>
</div> </div>
{% if g.last_login %}
<div class="">
Last Login: <local-datetime timestamp='{{ g.last_login }}'></local-datetime>
</div>
{% endif %}
</footer> </footer>

View File

@ -1,4 +1,5 @@
import pytest import pytest
from datetime import datetime
from uuid import uuid4 from uuid import uuid4
from atst.domain.users import Users from atst.domain.users import Users
@ -65,3 +66,11 @@ def test_update_user_with_dod_id():
Users.update(new_user, {"dod_id": "1234567890"}) Users.update(new_user, {"dod_id": "1234567890"})
assert "dod_id" in str(excinfo.value) 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

View File

@ -1,6 +1,7 @@
from urllib.parse import urlparse from urllib.parse import urlparse
import pytest import pytest
from datetime import datetime
from flask import session, url_for from flask import session, url_for
from .mocks import DOD_SDN_INFO, DOD_SDN, FIXTURE_EMAIL_ADDRESS from .mocks import DOD_SDN_INFO, DOD_SDN, FIXTURE_EMAIL_ADDRESS
from atst.domain.users import Users from atst.domain.users import Users
@ -224,3 +225,18 @@ def test_error_on_invalid_crl(client, monkeypatch):
response = _login(client) response = _login(client)
assert response.status_code == 401 assert response.status_code == 401
assert "Error Code 008" in response.data.decode() 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)