Skip to content

Commit d2cc128

Browse files
authored
Merge pull request #509 from MerginMaps/cache_last_login
Cache last sign in date in User table
2 parents cee21df + 9828bb1 commit d2cc128

3 files changed

Lines changed: 73 additions & 0 deletions

File tree

‎server/mergin/auth/models.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ class User(db.Model):
3636
default=datetime.datetime.utcnow,
3737
)
3838

39+
last_signed_in = db.Column(db.DateTime(), nullable=True)
40+
3941
__table_args__ = (
4042
db.Index("ix_user_username", func.lower(username), unique=True),
4143
db.Index("ix_user_email", func.lower(email), unique=True),
@@ -289,6 +291,7 @@ def __init__(self, user_id: int, ua: str, ip: str, device_id: Optional[str] = No
289291
self.user_agent = ua
290292
self.ip_address = ip
291293
self.device_id = device_id
294+
self.timestamp = datetime.datetime.now(tz=datetime.timezone.utc)
292295

293296
@staticmethod
294297
def add_record(user_id: int, req: request) -> None:
@@ -300,4 +303,37 @@ def add_record(user_id: int, req: request) -> None:
300303
return
301304
lh = LoginHistory(user_id, ua, ip, device_id)
302305
db.session.add(lh)
306+
307+
# cache user last login
308+
User.query.filter_by(id=user_id).update({"last_signed_in": lh.timestamp})
309+
db.session.commit()
310+
311+
@staticmethod
312+
def get_users_last_signed_in(user_ids: list) -> dict:
313+
"""Get users last signed in dates.
314+
Result is also cached in User table for future use.
315+
"""
316+
result = (
317+
db.session.query(
318+
LoginHistory.user_id,
319+
func.max(LoginHistory.timestamp).label("last_signed_in"),
320+
)
321+
.filter(LoginHistory.user_id.in_(user_ids))
322+
.group_by(LoginHistory.user_id)
323+
.all()
324+
)
325+
326+
user_mapping = [
327+
{
328+
"id": row.user_id, # user_id as PK in User table
329+
"last_signed_in": row.last_signed_in,
330+
}
331+
for row in result
332+
]
333+
if not user_mapping:
334+
return {}
335+
336+
# cache users last signed in
337+
db.session.bulk_update_mappings(User, user_mapping)
303338
db.session.commit()
339+
return {item["id"]: item["last_signed_in"] for item in user_mapping}

‎server/mergin/tests/test_auth.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,15 @@ def test_login(client, data, headers, expected):
7777
)
7878
assert login_history
7979
assert login_history.device_id == str(headers.get("X-Device-Id"))
80+
assert user.last_signed_in == login_history.timestamp
81+
users_last_signed_in = LoginHistory.get_users_last_signed_in([user.id])
82+
assert users_last_signed_in[user.id] == login_history.timestamp
83+
84+
# verify missing value is cached on first LoginHistory access
85+
user.last_signed_in = None
86+
db.session.commit()
87+
users_last_signed_in = LoginHistory.get_users_last_signed_in([user.id])
88+
assert user.last_signed_in == users_last_signed_in[user.id]
8089

8190

8291
def test_logout(client):
@@ -376,6 +385,7 @@ def test_api_login(client, data, headers, expected):
376385
.first()
377386
)
378387
assert login_history
388+
assert user.last_signed_in == login_history.timestamp
379389

380390

381391
def test_api_login_from_urllib(client):
@@ -394,6 +404,8 @@ def test_api_login_from_urllib(client):
394404
.first()
395405
)
396406
assert not login_history
407+
# we do not have recored last login yet
408+
assert user.last_signed_in is None
397409

398410

399411
def test_api_user_profile(client):
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Add user last signed in
2+
3+
Revision ID: b9ec9ab6694f
4+
Revises: 6cb54659c1de
5+
Create Date: 2025-09-09 15:43:19.554498
6+
7+
"""
8+
9+
from alembic import op
10+
import sqlalchemy as sa
11+
from sqlalchemy.dialects import postgresql
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "b9ec9ab6694f"
15+
down_revision = "6cb54659c1de"
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
op.add_column("user", sa.Column("last_signed_in", sa.DateTime(), nullable=True))
22+
23+
24+
def downgrade():
25+
op.drop_column("user", "last_signed_in")

0 commit comments

Comments
 (0)