/** * GET /api/presence/last-seen?userIds=uuid1,uuid2,... * * Batch-fetch lastSeenAt for up to 50 users. * Auth required — callers must be logged in (prevents anonymous enumeration). * * Query param: * userIds — comma-separated UUIDs (max 50) * * Response: * { success: true, data: { [userId: string]: string | null } } * Value is lastSeenAt as ISO-8601 string, or null if never seen / user not found. * * Privacy filters (both must pass): * 1. Requester follows the target (UserFollow row exists). * 2. Target has presenceVisible = true (opt-out respected). * Targets failing either filter → null in response map. * Frontend: null = no indicator shown. * * Privacy note: only lastSeenAt is exposed — no nickname, avatar, email or any * other profile field (feedback_anonymity_nickname.md). */ import { requireUser } from "../../utils/auth"; import { getLastSeenBatch } from "../../db/profile"; const MAX_USER_IDS = 50; export default defineEventHandler(async (event) => { const user = await requireUser(event); const query = getQuery(event); const raw = typeof query.userIds === "string" ? query.userIds.trim() : ""; if (!raw) { throw createError({ statusCode: 400, message: "MISSING_USER_IDS" }); } const userIds = raw .split(",") .map((id) => id.trim()) .filter(Boolean); if (userIds.length === 0) { throw createError({ statusCode: 400, message: "MISSING_USER_IDS" }); } if (userIds.length > MAX_USER_IDS) { throw createError({ statusCode: 400, message: "TOO_MANY_USER_IDS" }); } const rows = await getLastSeenBatch(user.id, userIds); // Build map — guarantee every requested ID has an entry (null for filtered/missing) const result: Record = {}; for (const id of userIds) { result[id] = null; } for (const row of rows) { result[row.id] = row.lastSeenAt ? row.lastSeenAt.toISOString() : null; } return { success: true, data: result }; });