feat: add last_seen tracking for users

Adds last_seen field to track when users last accessed the tool:
- Add lastSeen column to users table schema (nullable timestamp)
- Create migration to add last_seen column to existing databases
- Add updateLastSeen() function to auth.service.js
- Update auth derive middleware to update last_seen on each authenticated request (async, non-blocking)
- Add lastSeen to admin getUserStats() query for display in admin users table
- Add "Last Seen" column to admin users table in frontend

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-23 09:57:45 +01:00
parent 24e0e3bfdb
commit d1e4c39ad6
6 changed files with 119 additions and 1 deletions

View File

@@ -127,6 +127,7 @@ export async function getUserStats() {
email: users.email,
callsign: users.callsign,
isAdmin: users.isAdmin,
lastSeen: users.lastSeen,
qsoCount: sql`CAST(COUNT(${qsos.id}) AS INTEGER)`,
lotwConfirmed: sql`CAST(SUM(CASE WHEN ${qsos.lotwQslRstatus} = 'Y' THEN 1 ELSE 0 END) AS INTEGER)`,
dclConfirmed: sql`CAST(SUM(CASE WHEN ${qsos.dclQslRstatus} = 'Y' THEN 1 ELSE 0 END) AS INTEGER)`,
@@ -144,10 +145,13 @@ export async function getUserStats() {
.groupBy(users.id)
.orderBy(sql`COUNT(${qsos.id}) DESC`);
// Convert lastSync timestamps (seconds) to Date objects for JSON serialization
// Convert timestamps (seconds) to Date objects for JSON serialization
// Note: lastSeen from Drizzle is already a Date object (timestamp mode)
// lastSync is raw SQL returning seconds, needs conversion
return stats.map(stat => ({
...stat,
lastSync: stat.lastSync ? new Date(stat.lastSync * 1000) : null,
// lastSeen is already a Date object from Drizzle, don't convert
}));
}

View File

@@ -204,6 +204,7 @@ export async function getAllUsers() {
email: users.email,
callsign: users.callsign,
isAdmin: users.isAdmin,
lastSeen: users.lastSeen,
createdAt: users.createdAt,
updatedAt: users.updatedAt,
})
@@ -236,3 +237,17 @@ export async function getUserByIdFull(userId) {
return user || null;
}
/**
* Update user's last seen timestamp
* @param {number} userId - User ID
* @returns {Promise<void>}
*/
export async function updateLastSeen(userId) {
await db
.update(users)
.set({
lastSeen: new Date(),
})
.where(eq(users.id, userId));
}