mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
Add the user-facing account-security surface in Settings: - Active sessions (closes #64): GET /api/auth/me/sessions lists each browser login with a coarse device label, masked IP, resolved location, the sliding last-active stamp, and the current-session marker. DELETE the collection signs out everywhere else (keeps the current session, revokes other sessions and native refresh tokens, leaves API tokens alone); DELETE one revokes a single session, scoped to the owner so a foreign id 404s. - A sliding last-active stamp on the session, written throttled and fire-and-forget off the request-resolution path. - Security activity: GET /api/auth/me/security-activity, the shared read over the audit log filtered to sign-ins, second-factor changes, password changes, session revocations, exports, and deletions, with masked IPs. OpenAPI regenerated for the new endpoints.
71 lines
2.1 KiB
TypeScript
71 lines
2.1 KiB
TypeScript
/**
|
|
* GET /api/auth/me/security-activity
|
|
*
|
|
* v1.23 — the SHARED user-facing security-activity feed. Returns the caller's
|
|
* recent account-security audit events (logins, MFA, password change, token /
|
|
* session revocations, exports, deletions) with timestamp + resolved location
|
|
* + MASKED IP. The IP is never returned in full and no event detail body is
|
|
* surfaced — only the action name, time, and coarse geo. Consumed by the
|
|
* account-security surface and the privacy dashboard.
|
|
*/
|
|
import { NextRequest } from "next/server";
|
|
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
|
import { apiSuccess } from "@/lib/api-response";
|
|
import { annotate } from "@/lib/logging/context";
|
|
import { prisma } from "@/lib/db";
|
|
import { securityActivityWhere } from "@/lib/auth/security-activity";
|
|
import { maskIp } from "@/lib/auth/device-fingerprint";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
const DEFAULT_LIMIT = 50;
|
|
const MAX_LIMIT = 100;
|
|
|
|
export interface SecurityActivityDTO {
|
|
action: string;
|
|
createdAt: string;
|
|
location: string | null;
|
|
ipMasked: string | null;
|
|
carrier: string | null;
|
|
}
|
|
|
|
export const GET = apiHandler(async (request: NextRequest) => {
|
|
const { user } = await requireAuth();
|
|
|
|
const limitParam = Number(
|
|
request.nextUrl.searchParams.get("limit") ?? DEFAULT_LIMIT,
|
|
);
|
|
const limit =
|
|
Number.isFinite(limitParam) && limitParam > 0
|
|
? Math.min(Math.floor(limitParam), MAX_LIMIT)
|
|
: DEFAULT_LIMIT;
|
|
|
|
const rows = await prisma.auditLog.findMany({
|
|
where: securityActivityWhere(user.id),
|
|
orderBy: { createdAt: "desc" },
|
|
take: limit,
|
|
select: {
|
|
action: true,
|
|
createdAt: true,
|
|
location: true,
|
|
ipAddress: true,
|
|
carrier: true,
|
|
},
|
|
});
|
|
|
|
const events: SecurityActivityDTO[] = rows.map((r) => ({
|
|
action: r.action,
|
|
createdAt: r.createdAt.toISOString(),
|
|
location: r.location ?? null,
|
|
ipMasked: maskIp(r.ipAddress),
|
|
carrier: r.carrier ?? null,
|
|
}));
|
|
|
|
annotate({
|
|
action: { name: "auth.security_activity.list" },
|
|
meta: { event_count: events.length },
|
|
});
|
|
|
|
return apiSuccess({ events });
|
|
});
|