mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
Merge branch 'fix/v1438-api' into release/v1.43.8
This commit is contained in:
commit
4a0db28713
99 files changed
+5219
-1018
No files matched your search
@@ -0,0 +1,68 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
vi.mock("@/lib/db", () => ({
|
||||
prisma: {
|
||||
auditLog: { findMany: vi.fn(), create: vi.fn() },
|
||||
},
|
||||
}));
|
||||
vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn() }));
|
||||
vi.mock("@/lib/logging/transports", () => ({ emitIfSampled: vi.fn() }));
|
||||
vi.mock("@/lib/db-compat", () => ({
|
||||
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
vi.mock("next/headers", () => ({
|
||||
headers: vi.fn(async () => ({ get: () => null })),
|
||||
cookies: vi.fn(async () => ({
|
||||
get: () => undefined,
|
||||
set: () => {},
|
||||
delete: () => {},
|
||||
})),
|
||||
}));
|
||||
|
||||
import { GET } from "../route";
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
const SESSION_OK = {
|
||||
session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) },
|
||||
user: { id: "user-1", username: "u", role: "USER" as const, locale: "en" },
|
||||
};
|
||||
|
||||
const req = (qs = "") =>
|
||||
new NextRequest(`http://localhost/api/auth/me/security-activity${qs}`);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
vi.mocked(prisma.auditLog.findMany).mockResolvedValue([] as never);
|
||||
});
|
||||
|
||||
describe("GET /api/auth/me/security-activity", () => {
|
||||
it("reads one row past the requested page, in a total order", async () => {
|
||||
const res = await GET(req("?limit=10"));
|
||||
expect(res.status).toBe(200);
|
||||
const call = vi.mocked(prisma.auditLog.findMany).mock.calls[0][0] as {
|
||||
take: number;
|
||||
orderBy: unknown;
|
||||
};
|
||||
expect(call.take).toBe(11);
|
||||
expect(call.orderBy).toEqual([{ createdAt: "desc" }, { id: "desc" }]);
|
||||
const body = (await res.json()) as {
|
||||
meta: { truncated: boolean; limit: number };
|
||||
};
|
||||
expect(body.meta).toMatchObject({ truncated: false, limit: 10 });
|
||||
});
|
||||
|
||||
it("answers a limit past the published ceiling with a 422 naming it", async () => {
|
||||
const res = await GET(req("?limit=500"));
|
||||
expect(res.status).toBe(422);
|
||||
const body = (await res.json()) as {
|
||||
meta: { errorCode: string };
|
||||
details: { issues: Array<{ path: string }> };
|
||||
};
|
||||
expect(body.meta.errorCode).toBe("security_activity.invalid");
|
||||
expect(body.details.issues.map((i) => i.path)).toEqual(["limit"]);
|
||||
expect(prisma.auditLog.findMany).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,9 @@ 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";
|
||||
import { apiListSuccess, capRows, overfetch } from "@/lib/api/list-cap";
|
||||
import { apiListSuccess } from "@/lib/api/list-cap";
|
||||
import { listPage, parseListQuery } from "@/lib/http/list-query";
|
||||
import { returnAllZodIssues } from "@/lib/api-response";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -32,28 +34,34 @@ export interface SecurityActivityDTO {
|
||||
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 found = await prisma.auditLog.findMany({
|
||||
where: securityActivityWhere(user.id),
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: overfetch(limit),
|
||||
select: {
|
||||
action: true,
|
||||
createdAt: true,
|
||||
location: true,
|
||||
ipAddress: true,
|
||||
carrier: true,
|
||||
},
|
||||
// The shared list query: an out-of-range limit is a 422 naming it, as on
|
||||
// every other list, rather than a silent clamp to something not asked for.
|
||||
const query = parseListQuery(request.nextUrl.searchParams, {
|
||||
defaultLimit: DEFAULT_LIMIT,
|
||||
maxLimit: MAX_LIMIT,
|
||||
});
|
||||
if (!query.success) {
|
||||
return returnAllZodIssues(query.error, 422, {
|
||||
errorCode: "security_activity.invalid",
|
||||
});
|
||||
}
|
||||
const { limit } = query.data;
|
||||
|
||||
const { rows, cap } = await listPage(limit, (take) =>
|
||||
prisma.auditLog.findMany({
|
||||
where: securityActivityWhere(user.id),
|
||||
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
|
||||
take,
|
||||
select: {
|
||||
action: true,
|
||||
createdAt: true,
|
||||
location: true,
|
||||
ipAddress: true,
|
||||
carrier: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const { rows, cap } = capRows(found, limit);
|
||||
const events: SecurityActivityDTO[] = rows.map((r) => ({
|
||||
action: r.action,
|
||||
createdAt: r.createdAt.toISOString(),
|
||||
|
||||
Reference in new issue
Block a user