mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-09 07:42:39 +02:00
feat: add last_login column to user table and update user login tracking
This commit is contained in:
parent
2ac0e4f931
commit
a11c95e30f
8 changed files with 70 additions and 3 deletions
|
|
@ -0,0 +1,39 @@
|
|||
"""103_add_last_login_to_user
|
||||
|
||||
Revision ID: 103
|
||||
Revises: 102
|
||||
Create Date: 2026-03-08
|
||||
|
||||
Adds last_login timestamp column to the user table so we can track
|
||||
when each user last authenticated. The column is nullable — existing
|
||||
rows will have NULL until the user's next login.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "103"
|
||||
down_revision: str | None = "102"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
existing_columns = [col["name"] for col in sa.inspect(conn).get_columns("user")]
|
||||
|
||||
if "last_login" not in existing_columns:
|
||||
op.add_column(
|
||||
"user",
|
||||
sa.Column("last_login", sa.TIMESTAMP(timezone=True), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("user", "last_login")
|
||||
|
|
@ -1720,6 +1720,8 @@ if config.AUTH_TYPE == "GOOGLE":
|
|||
display_name = Column(String, nullable=True)
|
||||
avatar_url = Column(String, nullable=True)
|
||||
|
||||
last_login = Column(TIMESTAMP(timezone=True), nullable=True)
|
||||
|
||||
# Refresh tokens for this user
|
||||
refresh_tokens = relationship(
|
||||
"RefreshToken",
|
||||
|
|
@ -1820,6 +1822,8 @@ else:
|
|||
display_name = Column(String, nullable=True)
|
||||
avatar_url = Column(String, nullable=True)
|
||||
|
||||
last_login = Column(TIMESTAMP(timezone=True), nullable=True)
|
||||
|
||||
# Refresh tokens for this user
|
||||
refresh_tokens = relationship(
|
||||
"RefreshToken",
|
||||
|
|
|
|||
|
|
@ -510,6 +510,7 @@ async def list_members(
|
|||
"user_email": member_user.email if member_user else None,
|
||||
"user_display_name": member_user.display_name if member_user else None,
|
||||
"user_avatar_url": member_user.avatar_url if member_user else None,
|
||||
"user_last_login": member_user.last_login if member_user else None,
|
||||
}
|
||||
response.append(membership_dict)
|
||||
|
||||
|
|
@ -602,6 +603,7 @@ async def update_member_role(
|
|||
"created_at": db_membership.created_at,
|
||||
"role": db_membership.role,
|
||||
"user_email": member_user.email if member_user else None,
|
||||
"user_last_login": member_user.last_login if member_user else None,
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ class MembershipRead(BaseModel):
|
|||
user_email: str | None = None
|
||||
user_display_name: str | None = None
|
||||
user_avatar_url: str | None = None
|
||||
user_last_login: datetime | None = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import logging
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import httpx
|
||||
from fastapi import Depends, Request, Response
|
||||
|
|
@ -12,6 +13,7 @@ from fastapi_users.authentication import (
|
|||
)
|
||||
from fastapi_users.db import SQLAlchemyUserDatabase
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import update
|
||||
|
||||
from app.config import config
|
||||
from app.db import (
|
||||
|
|
@ -123,6 +125,23 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
|||
|
||||
return user
|
||||
|
||||
async def on_after_login(
|
||||
self,
|
||||
user: User,
|
||||
request: Request | None = None,
|
||||
response: Response | None = None,
|
||||
) -> None:
|
||||
try:
|
||||
async with async_session_maker() as session:
|
||||
await session.execute(
|
||||
update(User)
|
||||
.where(User.id == user.id)
|
||||
.values(last_login=datetime.now(UTC))
|
||||
)
|
||||
await session.commit()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to update last_login for user {user.id}: {e}")
|
||||
|
||||
async def on_after_register(self, user: User, request: Request | None = None):
|
||||
"""
|
||||
Called after a user registers. Creates a default search space for the user
|
||||
|
|
|
|||
|
|
@ -546,9 +546,9 @@ function MemberRow({
|
|||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="hidden md:table-cell w-[25%] py-2.5 text-sm text-foreground border-r border-border/40">
|
||||
{formatRelativeDate(member.joined_at)}
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell w-[25%] py-2.5 text-sm text-foreground border-r border-border/40">
|
||||
{member.user_last_login ? formatRelativeDate(member.user_last_login) : "Never"}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="w-[30%] text-right py-2.5 px-4 md:px-6">
|
||||
{showActions ? (
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const membersAtom = atomWithQuery((get) => {
|
|||
queryKey: cacheKeys.members.all(searchSpaceId?.toString() ?? ""),
|
||||
enabled: !!searchSpaceId,
|
||||
staleTime: 3 * 1000, // 3 seconds - short staleness for live collaboration
|
||||
refetchInterval: 2 * 60 * 1000, // 2 minutes
|
||||
queryFn: async () => {
|
||||
if (!searchSpaceId) {
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export const membership = z.object({
|
|||
user_email: z.string().nullable().optional(),
|
||||
user_display_name: z.string().nullable().optional(),
|
||||
user_avatar_url: z.string().nullable().optional(),
|
||||
user_last_login: z.string().nullable().optional(),
|
||||
user_is_active: z.boolean().nullable().optional(),
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue