Browse Source

feat: user last active

Timothy J. Baek 1 year ago
parent
commit
9094536d37

+ 79 - 0
backend/apps/web/internal/migrations/007_add_user_last_active_at.py

@@ -0,0 +1,79 @@
+"""Peewee migrations -- 002_add_local_sharing.py.
+
+Some examples (model - class or model name)::
+
+    > Model = migrator.orm['table_name']            # Return model in current state by name
+    > Model = migrator.ModelClass                   # Return model in current state by name
+
+    > migrator.sql(sql)                             # Run custom SQL
+    > migrator.run(func, *args, **kwargs)           # Run python function with the given args
+    > migrator.create_model(Model)                  # Create a model (could be used as decorator)
+    > migrator.remove_model(model, cascade=True)    # Remove a model
+    > migrator.add_fields(model, **fields)          # Add fields to a model
+    > migrator.change_fields(model, **fields)       # Change fields
+    > migrator.remove_fields(model, *field_names, cascade=True)
+    > migrator.rename_field(model, old_field_name, new_field_name)
+    > migrator.rename_table(model, new_table_name)
+    > migrator.add_index(model, *col_names, unique=False)
+    > migrator.add_not_null(model, *field_names)
+    > migrator.add_default(model, field_name, default)
+    > migrator.add_constraint(model, name, sql)
+    > migrator.drop_index(model, *col_names)
+    > migrator.drop_not_null(model, *field_names)
+    > migrator.drop_constraints(model, *constraints)
+
+"""
+
+from contextlib import suppress
+
+import peewee as pw
+from peewee_migrate import Migrator
+
+
+with suppress(ImportError):
+    import playhouse.postgres_ext as pw_pext
+
+
+def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
+    """Write your migrations here."""
+
+    # Adding fields created_at and updated_at to the 'user' table
+    migrator.add_fields(
+        "user",
+        created_at=pw.BigIntegerField(null=True),  # Allow null for transition
+        updated_at=pw.BigIntegerField(null=True),  # Allow null for transition
+        last_active_at=pw.BigIntegerField(null=True),  # Allow null for transition
+    )
+
+    # Populate the new fields from an existing 'timestamp' field
+    migrator.sql(
+        "UPDATE user SET created_at = timestamp, updated_at = timestamp, last_active_at = timestamp WHERE timestamp IS NOT NULL"
+    )
+
+    # Now that the data has been copied, remove the original 'timestamp' field
+    migrator.remove_fields("user", "timestamp")
+
+    # Update the fields to be not null now that they are populated
+    migrator.change_fields(
+        "user",
+        created_at=pw.BigIntegerField(null=False),
+        updated_at=pw.BigIntegerField(null=False),
+        last_active_at=pw.BigIntegerField(null=False),
+    )
+
+
+def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
+    """Write your rollback migrations here."""
+
+    # Recreate the timestamp field initially allowing null values for safe transition
+    migrator.add_fields("user", timestamp=pw.BigIntegerField(null=True))
+
+    # Copy the earliest created_at date back into the new timestamp field
+    # This assumes created_at was originally a copy of timestamp
+    migrator.sql("UPDATE user SET timestamp = created_at")
+
+    # Remove the created_at and updated_at fields
+    migrator.remove_fields("user", "created_at", "updated_at", "last_active_at")
+
+    # Finally, alter the timestamp field to not allow nulls if that was the original setting
+    migrator.change_fields("user", timestamp=pw.BigIntegerField(null=False))

+ 23 - 3
backend/apps/web/models/users.py

@@ -19,7 +19,11 @@ class User(Model):
     email = CharField()
     role = CharField()
     profile_image_url = TextField()
-    timestamp = BigIntegerField()
+
+    last_active_at = BigIntegerField()
+    updated_at = BigIntegerField()
+    created_at = BigIntegerField()
+
     api_key = CharField(null=True, unique=True)
 
     class Meta:
@@ -32,7 +36,11 @@ class UserModel(BaseModel):
     email: str
     role: str = "pending"
     profile_image_url: str
-    timestamp: int  # timestamp in epoch
+
+    last_active_at: int  # timestamp in epoch
+    updated_at: int  # timestamp in epoch
+    created_at: int  # timestamp in epoch
+
     api_key: Optional[str] = None
 
 
@@ -73,7 +81,9 @@ class UsersTable:
                 "email": email,
                 "role": role,
                 "profile_image_url": profile_image_url,
-                "timestamp": int(time.time()),
+                "last_active_at": int(time.time()),
+                "created_at": int(time.time()),
+                "updated_at": int(time.time()),
             }
         )
         result = User.create(**user.model_dump())
@@ -137,6 +147,16 @@ class UsersTable:
         except:
             return None
 
+    def update_user_last_active_by_id(self, id: str) -> Optional[UserModel]:
+        try:
+            query = User.update(last_active_at=int(time.time())).where(User.id == id)
+            query.execute()
+
+            user = User.get(User.id == id)
+            return UserModel(**model_to_dict(user))
+        except:
+            return None
+
     def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
         try:
             query = User.update(**updated).where(User.id == id)

+ 6 - 0
backend/utils/utils.py

@@ -89,6 +89,8 @@ def get_current_user(
                 status_code=status.HTTP_401_UNAUTHORIZED,
                 detail=ERROR_MESSAGES.INVALID_TOKEN,
             )
+        else:
+            Users.update_user_last_active_by_id(user.id)
         return user
     else:
         raise HTTPException(
@@ -99,11 +101,15 @@ def get_current_user(
 
 def get_current_user_by_api_key(api_key: str):
     user = Users.get_user_by_api_key(api_key)
+
     if user is None:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
             detail=ERROR_MESSAGES.INVALID_TOKEN,
         )
+    else:
+        Users.update_user_last_active_by_id(user.id)
+
     return user
 
 

+ 1 - 1
src/lib/components/admin/EditUserModal.svelte

@@ -86,7 +86,7 @@
 
 							<div class="text-xs text-gray-500">
 								{$i18n.t('Created at')}
-								{dayjs(selectedUser.timestamp * 1000).format($i18n.t('MMMM DD, YYYY'))}
+								{dayjs(selectedUser.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))}
 							</div>
 						</div>
 					</div>

+ 9 - 1
src/routes/(app)/admin/+page.svelte

@@ -5,6 +5,8 @@
 	import { onMount, getContext } from 'svelte';
 
 	import dayjs from 'dayjs';
+	import relativeTime from 'dayjs/plugin/relativeTime';
+	dayjs.extend(relativeTime);
 
 	import { toast } from 'svelte-sonner';
 
@@ -164,6 +166,8 @@
 											<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
 											<th scope="col" class="px-3 py-2"> {$i18n.t('Email')} </th>
 											<th scope="col" class="px-3 py-2"> {$i18n.t('Created at')} </th>
+											<th scope="col" class="px-3 py-2"> {$i18n.t('Last Active')} </th>
+
 											<th scope="col" class="px-3 py-2 text-right" />
 										</tr>
 									</thead>
@@ -221,7 +225,11 @@
 												<td class=" px-3 py-2"> {user.email} </td>
 
 												<td class=" px-3 py-2">
-													{dayjs(user.timestamp * 1000).format($i18n.t('MMMM DD, YYYY'))}
+													{dayjs(user.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))}
+												</td>
+
+												<td class=" px-3 py-2">
+													{dayjs(user.last_active_at * 1000).fromNow()}
 												</td>
 
 												<td class="px-3 py-2 text-right">