Skip to content

Commit 759c789

Browse files
feat: improve last login data (#5745)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent d06acd3 commit 759c789

13 files changed

Lines changed: 420 additions & 270 deletions

File tree

api/app/settings/common.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1239,7 +1239,8 @@
12391239

12401240
# Define the cooldown duration, in seconds, for password reset emails
12411241
PASSWORD_RESET_EMAIL_COOLDOWN = env.int("PASSWORD_RESET_EMAIL_COOLDOWN", 60 * 60 * 24)
1242-
1242+
# Define the threshold, in minutes, for updating the last login timestamp
1243+
LAST_LOGIN_UPDATE_THRESHOLD_MINUTES = env.int("LAST_LOGIN_UPDATE_THRESHOLD_MINUTES", 30)
12431244
# Limit the count of password reset emails that can be dispatched within the `PASSWORD_RESET_EMAIL_COOLDOWN` timeframe.
12441245
MAX_PASSWORD_RESET_EMAILS = env.int("MAX_PASSWORD_RESET_EMAILS", 5)
12451246

api/custom_auth/views.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import json
2+
from datetime import timedelta
23
from typing import Any
34

45
from django.conf import settings
56
from django.contrib.auth import user_logged_out
7+
from django.utils import timezone
68
from django.utils.decorators import method_decorator
79
from djoser.views import TokenCreateView, UserViewSet # type: ignore[import-untyped]
810
from drf_yasg.utils import swagger_auto_schema # type: ignore[import-untyped]
@@ -142,6 +144,17 @@ def perform_destroy(self, instance): # type: ignore[no-untyped-def]
142144
)
143145
)
144146

147+
def retrieve(self, request: Request, *args: Any, **kwargs: Any) -> Response:
148+
user = request.user
149+
assert isinstance(user, FFAdminUser)
150+
if not user.last_login or timezone.now() - user.last_login > timedelta(
151+
minutes=settings.LAST_LOGIN_UPDATE_THRESHOLD_MINUTES
152+
):
153+
user.last_login = timezone.now()
154+
user.save(update_fields=["last_login"])
155+
resp: Response = super().retrieve(request, *args, **kwargs)
156+
return resp
157+
145158
@action(
146159
detail=False,
147160
methods=["patch"],

api/tests/unit/users/test_unit_users_views.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import typing
3+
from datetime import datetime
34

45
import pytest
56
from dateutil.relativedelta import relativedelta
@@ -12,6 +13,7 @@
1213
from django.utils import timezone
1314
from djoser import utils # type: ignore[import-untyped]
1415
from djoser.email import PasswordResetEmail # type: ignore[import-untyped]
16+
from freezegun import freeze_time
1517
from pytest_django import DjangoAssertNumQueries
1618
from rest_framework import status
1719
from rest_framework.test import APIClient
@@ -921,3 +923,44 @@ def test_list_user_groups(
921923
(user1.pk, False),
922924
(user2.pk, True),
923925
}
926+
927+
928+
@freeze_time("2024-01-01T10:00:00Z")
929+
@pytest.mark.parametrize(
930+
"last_login,expected_last_login",
931+
[
932+
(None, datetime.fromisoformat("2024-01-01T10:00:00Z")),
933+
(
934+
datetime.fromisoformat("2023-01-01T10:00:00Z"),
935+
datetime.fromisoformat("2024-01-01T10:00:00Z"),
936+
),
937+
(
938+
datetime.fromisoformat("2024-01-01T09:59:00Z"),
939+
datetime.fromisoformat("2024-01-01T09:59:00Z"),
940+
),
941+
],
942+
)
943+
def test_get_me_view_updates_last_login(
944+
api_client: APIClient,
945+
test_user: FFAdminUser,
946+
last_login: datetime | None,
947+
expected_last_login: datetime,
948+
) -> None:
949+
# Given
950+
test_user.last_login = last_login
951+
test_user.save(update_fields=["last_login"])
952+
test_user.refresh_from_db()
953+
954+
api_client.force_authenticate(test_user)
955+
assert test_user.last_login is None or test_user.last_login < timezone.now()
956+
957+
url = reverse("api-v1:custom_auth:ffadminuser-me")
958+
959+
# When
960+
response = api_client.get(url)
961+
962+
# Then
963+
assert response.status_code == status.HTTP_200_OK
964+
test_user.refresh_from_db()
965+
966+
assert test_user.last_login == expected_last_login

frontend/common/types/responses.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,8 @@ export type User = {
256256
last_login: string
257257
uuid: string
258258
onboarding: Onboarding
259+
// TODO: Use enum
260+
role: string
259261
}
260262
export type GroupUser = Omit<User, 'role'> & {
261263
group_admin: boolean

frontend/web/components/PermissionRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
RolePermission,
99
UserPermissions,
1010
} from 'common/types/responses'
11-
import DerivedPermissionsList from './derived-permissions/DerivedPermissionsList'
11+
import DerivedPermissionsList from './users-permissions/derived-permissions/DerivedPermissionsList'
1212
import PermissionControl from './PermissionControl'
1313
import { PermissionRoleType } from 'common/types/requests'
1414

frontend/web/components/inspect-permissions/Permissions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import Format from 'common/utils/format'
88
import { PermissionRow } from 'components/PermissionRow'
99
import { useGetAvailablePermissionsQuery } from 'common/services/useAvailablePermissions'
1010

11-
import DerivedPermissionsList from 'components/derived-permissions/DerivedPermissionsList'
11+
import DerivedPermissionsList from 'components/users-permissions/derived-permissions/DerivedPermissionsList'
1212
import BooleanDotIndicator from 'components/BooleanDotIndicator'
1313

1414
const Permissions = ({

0 commit comments

Comments
 (0)