Repository: saleor/saleor

Issue:
When user login from GraphQL, last_login field is not updated
### What I'm trying to achieve
I would like to be able to know when an user last logged-in from the GraphQL API (JWT).

### Steps to reproduce the problem
1. Look at `last_login` in the user model, remember the value
1. Login using JWT
1.. Look at `last_login` in the user model, it shouldn't have been updated

### What I expected to happen
- `last_login` to be updated when `createToken`
- `last_login` to be updated when `refreshToken`



Discussion:

Resolving patch:
diff --git a/saleor/graphql/core/mutations.py b/saleor/graphql/core/mutations.py
--- a/saleor/graphql/core/mutations.py
+++ b/saleor/graphql/core/mutations.py
@@ -9,11 +9,12 @@
     ValidationError,
 )
 from django.db.models.fields.files import FileField
+from django.utils import timezone
 from graphene import ObjectType
 from graphene.types.mutation import MutationOptions
 from graphene_django.registry import get_global_registry
 from graphql.error import GraphQLError
-from graphql_jwt import ObtainJSONWebToken, Verify
+from graphql_jwt import ObtainJSONWebToken, Refresh, Verify
 from graphql_jwt.exceptions import JSONWebTokenError, PermissionDenied
 
 from ...account import models
@@ -626,6 +627,9 @@ def mutate(cls, root, info, **kwargs):
             errors = validation_error_to_error_type(e)
             return cls.handle_typed_errors(errors)
         else:
+            user = result.user
+            user.last_login = timezone.now()
+            user.save(update_fields=["last_login"])
             return result
 
     @classmethod
@@ -641,6 +645,21 @@ def resolve(cls, root, info, **kwargs):
         return cls(user=info.context.user, errors=[], account_errors=[])
 
 
+class RefreshToken(Refresh):
+    """Mutation that refresh user token.
+
+    It overrides the default graphql_jwt.Refresh to update user's last_login field.
+    """
+
+    @classmethod
+    def mutate(cls, root, info, **kwargs):
+        result = super().mutate(root, info, **kwargs)
+        user = graphene.Node.get_node_from_global_id(info, result.payload["user_id"])
+        user.last_login = timezone.now()
+        user.save(update_fields=["last_login"])
+        return result
+
+
 class VerifyToken(Verify):
     """Mutation that confirms if token is valid and also returns user data."""
 
diff --git a/saleor/graphql/core/schema.py b/saleor/graphql/core/schema.py
--- a/saleor/graphql/core/schema.py
+++ b/saleor/graphql/core/schema.py
@@ -1,13 +1,12 @@
 import graphene
-import graphql_jwt
 
-from .mutations import CreateToken, VerifyToken
+from .mutations import CreateToken, RefreshToken, VerifyToken
 from .types.common import TaxType
 
 
 class CoreMutations(graphene.ObjectType):
     token_create = CreateToken.Field()
-    token_refresh = graphql_jwt.Refresh.Field()
+    token_refresh = RefreshToken.Field()
     token_verify = VerifyToken.Field()
 
 


Test patch:
diff --git a/tests/api/test_account.py b/tests/api/test_account.py
--- a/tests/api/test_account.py
+++ b/tests/api/test_account.py
@@ -10,6 +10,7 @@
 from django.core.files import File
 from django.core.validators import URLValidator
 from django.test import override_settings
+from django.utils import timezone
 from freezegun import freeze_time
 from prices import Money
 
@@ -89,10 +90,14 @@ def test_create_token_mutation(api_client, staff_user, settings):
     }
     """
     variables = {"email": staff_user.email, "password": "password"}
-    response = api_client.post_graphql(query, variables)
+    time = timezone.now()
+    with freeze_time(time):
+        response = api_client.post_graphql(query, variables)
     content = get_graphql_content(response)
     token_data = content["data"]["tokenCreate"]
     token = jwt.decode(token_data["token"], settings.SECRET_KEY)
+    staff_user.refresh_from_db()
+    assert staff_user.last_login == time
     assert token["email"] == staff_user.email
     assert token["user_id"] == graphene.Node.to_global_id("User", staff_user.id)