fix(import): log a refused upload as a signal, not an audit row

An upload refused for its size, a broken stream or a stopped worker is an
operational outcome, not something done to the account. The admin import
keeps its audit rows, since an admin acting on another account is.
This commit is contained in:
Marc-André Bombeck committed 2026-10-20 10:03:30 +02:00
1 parent 689796db33
commit b12bece491
3 files changed
+46 -26

No files matched your search

@@ -14,6 +14,10 @@ vi.mock("@/lib/auth/audit", () => ({
auditLog: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/lib/logging/transports", () => ({ emitIfSampled: vi.fn() }));
vi.mock("@/lib/logging/signal", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/logging/signal")>()),
emitSignal: vi.fn(),
}));
vi.mock("@/lib/db-compat", () => ({
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
}));
@@ -78,6 +82,8 @@ vi.mock("@/lib/db", () => ({
import { POST } from "../route";
import { getSession } from "@/lib/auth/session";
import { auditLog } from "@/lib/auth/audit";
import { emitSignal } from "@/lib/logging/signal";
const SESSION_OK = {
session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) },
@@ -144,6 +150,18 @@ describe("POST /api/import/apple-health-export", () => {
);
const res = await POST(req);
expect(res.status).toBe(413);
// A refused upload is an operational outcome, not an account action: a
// signal, never an audit row.
expect(auditLog).not.toHaveBeenCalledWith(
"import.apple-health.kickoff.denied",
expect.anything(),
);
expect(emitSignal).toHaveBeenCalledWith(
expect.objectContaining({
action: "import.apple-health.kickoff.denied",
meta: expect.objectContaining({ reason: "content_length_exceeded" }),
}),
);
});
it("returns 429 when the rate-limit window is exhausted", async () => {
+14 -13
View File
@@ -26,6 +26,7 @@ import { apiHandler, requireAuth, HttpError } from "@/lib/api-handler";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { emitSignal } from "@/lib/logging/signal";
import { checkRateLimit } from "@/lib/rate-limit";
import { getGlobalBoss } from "@/lib/jobs/boss-instance";
import {
@@ -77,12 +78,12 @@ export const POST = apiHandler(async (request: NextRequest) => {
// content-length cannot bypass it.
const declaredBytes = Number(request.headers.get("content-length") ?? 0);
if (declaredBytes > MAX_UPLOAD_BYTES) {
await auditLog("import.apple-health.kickoff.denied", {
userId: user.id,
ipAddress: getClientIp(request),
details: {
emitSignal({
action: "import.apple-health.kickoff.denied",
level: "warn",
meta: {
reason: "content_length_exceeded",
contentLength: declaredBytes,
content_length: declaredBytes,
},
});
return apiError("Upload exceeds 1.5 GB limit", 413);
@@ -107,10 +108,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
);
} catch (err) {
const message = err instanceof Error ? err.message : "upload_failed";
await auditLog("import.apple-health.kickoff.denied", {
userId: user.id,
ipAddress: getClientIp(request),
details: { reason: "stream_to_disk_failed", message },
emitSignal({
action: "import.apple-health.kickoff.denied",
level: "warn",
meta: { reason: "stream_to_disk_failed", message },
});
return apiError(`Multipart upload failed: ${message}`, 422);
}
@@ -150,10 +151,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
const boss = getGlobalBoss();
if (!boss) {
await auditLog("import.apple-health.kickoff.denied", {
userId: user.id,
ipAddress: getClientIp(request),
details: { reason: "worker_not_running" },
emitSignal({
action: "import.apple-health.kickoff.denied",
level: "warn",
meta: { reason: "worker_not_running" },
});
// Nothing will ever read the staged export; it must not stay in /tmp.
await discardStagedUpload(uploaded.filePath);
@@ -22,6 +22,7 @@ import { apiHandler, requireAuth, HttpError } from "@/lib/api-handler";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { emitSignal } from "@/lib/logging/signal";
import { checkRateLimit } from "@/lib/rate-limit";
import { getGlobalBoss } from "@/lib/jobs/boss-instance";
import {
@@ -67,12 +68,12 @@ export const POST = apiHandler(async (request: NextRequest) => {
const declaredBytes = Number(request.headers.get("content-length") ?? 0);
if (declaredBytes > HEALTH_CONNECT_MAX_UPLOAD_BYTES) {
await auditLog("import.health-connect.kickoff.denied", {
userId: user.id,
ipAddress: getClientIp(request),
details: {
emitSignal({
action: "import.health-connect.kickoff.denied",
level: "warn",
meta: {
reason: "content_length_exceeded",
contentLength: declaredBytes,
content_length: declaredBytes,
},
});
return apiError("Upload exceeds the 1 GB limit", 413);
@@ -94,10 +95,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
);
} catch (err) {
const message = err instanceof Error ? err.message : "upload_failed";
await auditLog("import.health-connect.kickoff.denied", {
userId: user.id,
ipAddress: getClientIp(request),
details: { reason: "stream_to_disk_failed" },
emitSignal({
action: "import.health-connect.kickoff.denied",
level: "warn",
meta: { reason: "stream_to_disk_failed" },
});
return apiError(`Multipart upload failed: ${message}`, 422);
}
@@ -123,10 +124,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
const boss = getGlobalBoss();
if (!boss) {
await auditLog("import.health-connect.kickoff.denied", {
userId: user.id,
ipAddress: getClientIp(request),
details: { reason: "worker_not_running" },
emitSignal({
action: "import.health-connect.kickoff.denied",
level: "warn",
meta: { reason: "worker_not_running" },
});
await discardStagedUpload(uploaded.filePath);
throw new HttpError(503, "Background worker is not running");