forked from open-webui/open-webui
		
	feat: user last active
This commit is contained in:
		
							parent
							
								
									01c4647dfc
								
							
						
					
					
						commit
						9094536d37
					
				
					 5 changed files with 118 additions and 5 deletions
				
			
		|  | @ -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)) | ||||||
|  | @ -19,7 +19,11 @@ class User(Model): | ||||||
|     email = CharField() |     email = CharField() | ||||||
|     role = CharField() |     role = CharField() | ||||||
|     profile_image_url = TextField() |     profile_image_url = TextField() | ||||||
|     timestamp = BigIntegerField() | 
 | ||||||
|  |     last_active_at = BigIntegerField() | ||||||
|  |     updated_at = BigIntegerField() | ||||||
|  |     created_at = BigIntegerField() | ||||||
|  | 
 | ||||||
|     api_key = CharField(null=True, unique=True) |     api_key = CharField(null=True, unique=True) | ||||||
| 
 | 
 | ||||||
|     class Meta: |     class Meta: | ||||||
|  | @ -32,7 +36,11 @@ class UserModel(BaseModel): | ||||||
|     email: str |     email: str | ||||||
|     role: str = "pending" |     role: str = "pending" | ||||||
|     profile_image_url: str |     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 |     api_key: Optional[str] = None | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -73,7 +81,9 @@ class UsersTable: | ||||||
|                 "email": email, |                 "email": email, | ||||||
|                 "role": role, |                 "role": role, | ||||||
|                 "profile_image_url": profile_image_url, |                 "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()) |         result = User.create(**user.model_dump()) | ||||||
|  | @ -137,6 +147,16 @@ class UsersTable: | ||||||
|         except: |         except: | ||||||
|             return None |             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]: |     def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]: | ||||||
|         try: |         try: | ||||||
|             query = User.update(**updated).where(User.id == id) |             query = User.update(**updated).where(User.id == id) | ||||||
|  |  | ||||||
|  | @ -89,6 +89,8 @@ def get_current_user( | ||||||
|                 status_code=status.HTTP_401_UNAUTHORIZED, |                 status_code=status.HTTP_401_UNAUTHORIZED, | ||||||
|                 detail=ERROR_MESSAGES.INVALID_TOKEN, |                 detail=ERROR_MESSAGES.INVALID_TOKEN, | ||||||
|             ) |             ) | ||||||
|  |         else: | ||||||
|  |             Users.update_user_last_active_by_id(user.id) | ||||||
|         return user |         return user | ||||||
|     else: |     else: | ||||||
|         raise HTTPException( |         raise HTTPException( | ||||||
|  | @ -99,11 +101,15 @@ def get_current_user( | ||||||
| 
 | 
 | ||||||
| def get_current_user_by_api_key(api_key: str): | def get_current_user_by_api_key(api_key: str): | ||||||
|     user = Users.get_user_by_api_key(api_key) |     user = Users.get_user_by_api_key(api_key) | ||||||
|  | 
 | ||||||
|     if user is None: |     if user is None: | ||||||
|         raise HTTPException( |         raise HTTPException( | ||||||
|             status_code=status.HTTP_401_UNAUTHORIZED, |             status_code=status.HTTP_401_UNAUTHORIZED, | ||||||
|             detail=ERROR_MESSAGES.INVALID_TOKEN, |             detail=ERROR_MESSAGES.INVALID_TOKEN, | ||||||
|         ) |         ) | ||||||
|  |     else: | ||||||
|  |         Users.update_user_last_active_by_id(user.id) | ||||||
|  | 
 | ||||||
|     return user |     return user | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -86,7 +86,7 @@ | ||||||
| 
 | 
 | ||||||
| 							<div class="text-xs text-gray-500"> | 							<div class="text-xs text-gray-500"> | ||||||
| 								{$i18n.t('Created at')} | 								{$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> | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
|  |  | ||||||
|  | @ -5,6 +5,8 @@ | ||||||
| 	import { onMount, getContext } from 'svelte'; | 	import { onMount, getContext } from 'svelte'; | ||||||
| 
 | 
 | ||||||
| 	import dayjs from 'dayjs'; | 	import dayjs from 'dayjs'; | ||||||
|  | 	import relativeTime from 'dayjs/plugin/relativeTime'; | ||||||
|  | 	dayjs.extend(relativeTime); | ||||||
| 
 | 
 | ||||||
| 	import { toast } from 'svelte-sonner'; | 	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('Name')} </th> | ||||||
| 											<th scope="col" class="px-3 py-2"> {$i18n.t('Email')} </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('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" /> | 											<th scope="col" class="px-3 py-2 text-right" /> | ||||||
| 										</tr> | 										</tr> | ||||||
| 									</thead> | 									</thead> | ||||||
|  | @ -221,7 +225,11 @@ | ||||||
| 												<td class=" px-3 py-2"> {user.email} </td> | 												<td class=" px-3 py-2"> {user.email} </td> | ||||||
| 
 | 
 | ||||||
| 												<td class=" px-3 py-2"> | 												<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> | ||||||
| 
 | 
 | ||||||
| 												<td class="px-3 py-2 text-right"> | 												<td class="px-3 py-2 text-right"> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy J. Baek
						Timothy J. Baek