Skip to content

Commit ada7567

Browse files
improve logging
1 parent dcb545f commit ada7567

8 files changed

Lines changed: 90 additions & 7 deletions

File tree

src/app/api/auth/account/route.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { deviceLinkStatus } from "@/lib/account-link";
3+
import { logApiReject } from "@/lib/api-log";
34

45
export const runtime = "nodejs";
56

@@ -9,11 +10,13 @@ const COOKIE = "wh_lid";
910
export async function GET(req: NextRequest) {
1011
const localId = req.cookies.get(COOKIE)?.value;
1112
if (!localId) {
13+
logApiReject("auth/account", "no_device", { hasCookie: false });
1214
return NextResponse.json({ error: "no_device" }, { status: 401 });
1315
}
1416

1517
const status = await deviceLinkStatus(localId);
1618
if (!status) {
19+
logApiReject("auth/account", "device_missing");
1720
return NextResponse.json({ error: "device_missing" }, { status: 404 });
1821
}
1922

src/app/api/auth/link/accept/route.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { z } from "zod";
33
import { sql } from "drizzle-orm";
44
import { db } from "@/db";
55
import { linkDevices } from "@/lib/account-link";
6+
import { logApiReject, zodIssueSummary } from "@/lib/api-log";
67
import { admitLinkAccept } from "@/lib/ratelimit";
78

89
export const runtime = "nodejs";
@@ -22,11 +23,17 @@ export async function POST(req: NextRequest) {
2223
const targetLocalId = req.cookies.get(COOKIE)?.value;
2324

2425
if (!parsed.success || !targetLocalId) {
26+
logApiReject("auth/link/accept", "bad_request", {
27+
bodyValid: parsed.success,
28+
hasCookie: !!targetLocalId,
29+
...(parsed.success ? {} : zodIssueSummary(parsed.error.issues)),
30+
});
2531
return NextResponse.json({ error: "bad_request" }, { status: 400 });
2632
}
2733

2834
const verdict = await admitLinkAccept(targetLocalId);
2935
if (!verdict.ok) {
36+
logApiReject("auth/link/accept", "rate_limited", { reason: verdict.reason });
3037
return NextResponse.json({ error: "rate_limited" }, { status: 429 });
3138
}
3239

@@ -39,13 +46,18 @@ export async function POST(req: NextRequest) {
3946

4047
const row = consumed.rows?.[0];
4148
if (!row) {
49+
logApiReject("auth/link/accept", "invalid_code", { codeLen: code.length });
4250
return NextResponse.json({ error: "invalid_code" }, { status: 400 });
4351
}
4452

4553
const result = await linkDevices(row.sourceLocalId, targetLocalId);
4654
if (!result.ok) {
4755
const status =
4856
result.reason === "same_device" ? 409 : result.reason === "target_missing" ? 404 : 400;
57+
logApiReject("auth/link/accept", result.reason, {
58+
sourcePresent: result.reason !== "source_missing",
59+
targetPresent: result.reason !== "target_missing",
60+
});
4961
return NextResponse.json({ error: result.reason }, { status });
5062
}
5163

src/app/api/auth/link/route.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
22
import { db } from "@/db";
33
import { linkTokens } from "@/db/schema";
44
import { newLinkCode } from "@/lib/codes";
5+
import { logApiError, logApiReject } from "@/lib/api-log";
56
import { LINK_TTL_MS } from "@/lib/token";
67
import { admitLinkCreate } from "@/lib/ratelimit";
78

@@ -13,11 +14,13 @@ const COOKIE = "wh_lid";
1314
export async function POST(req: NextRequest) {
1415
const localId = req.cookies.get(COOKIE)?.value;
1516
if (!localId) {
17+
logApiReject("auth/link", "no_device", { hasCookie: false });
1618
return NextResponse.json({ error: "no_device" }, { status: 401 });
1719
}
1820

1921
const verdict = await admitLinkCreate(localId);
2022
if (!verdict.ok) {
23+
logApiReject("auth/link", "rate_limited", { reason: verdict.reason });
2124
return NextResponse.json({ error: "rate_limited" }, { status: 429 });
2225
}
2326

@@ -33,9 +36,10 @@ export async function POST(req: NextRequest) {
3336
expiresAt,
3437
});
3538
break;
36-
} catch {
39+
} catch (err) {
3740
code = newLinkCode();
3841
if (i === 3) {
42+
logApiError("auth/link", "code insert failed after retries", err);
3943
return NextResponse.json({ error: "unavailable" }, { status: 503 });
4044
}
4145
}

src/app/api/auth/magic/route.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { magicTokens } from "@/db/schema";
55
import { newNonce, signNonce, MAGIC_TTL_MS } from "@/lib/token";
66
import { emailHash, hashKeyed } from "@/lib/crypto";
77
import { sendMail } from "@/lib/mailer";
8+
import { logApiReject } from "@/lib/api-log";
89
import { admitMagic } from "@/lib/ratelimit";
910
import { clientIp } from "@/lib/geo";
1011

@@ -26,7 +27,9 @@ export async function POST(req: NextRequest) {
2627
const ipHash = hashKeyed(clientIp(req.headers));
2728

2829
const verdict = await admitMagic(ipHash, hashed);
29-
if (verdict.ok) {
30+
if (!verdict.ok) {
31+
logApiReject("auth/magic", "skipped", { reason: verdict.reason });
32+
} else {
3033
const nonce = newNonce();
3134
const token = signNonce(nonce);
3235
const expiresAt = new Date(Date.now() + MAGIC_TTL_MS);
@@ -49,6 +52,11 @@ export async function POST(req: NextRequest) {
4952
console.error("[auth/magic] send failed:", err);
5053
});
5154
}
55+
} else {
56+
logApiReject("auth/magic", "skipped", {
57+
bodyValid: parsed.success,
58+
hasCookie: !!localId,
59+
});
5260
}
5361

5462
const elapsed = Date.now() - started;

src/app/api/auth/unlink/route.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { unlinkDevice } from "@/lib/account-link";
3+
import { logApiReject } from "@/lib/api-log";
34

45
export const runtime = "nodejs";
56

@@ -9,12 +10,14 @@ const COOKIE = "wh_lid";
910
export async function POST(req: NextRequest) {
1011
const localId = req.cookies.get(COOKIE)?.value;
1112
if (!localId) {
13+
logApiReject("auth/unlink", "no_device", { hasCookie: false });
1214
return NextResponse.json({ error: "no_device" }, { status: 401 });
1315
}
1416

1517
const result = await unlinkDevice(localId);
1618
if (!result.ok) {
1719
const status = result.reason === "device_missing" ? 404 : 400;
20+
logApiReject("auth/unlink", result.reason);
1821
return NextResponse.json({ error: result.reason }, { status });
1922
}
2023

src/app/api/auth/verify/route.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { verifyNonce } from "@/lib/token";
3+
import { logApiReject } from "@/lib/api-log";
34
import { executeMagicVerify, magicTokenStatus } from "@/lib/magic-verify";
45
import type { VerifyFailureReason } from "@/lib/verify-feedback";
56

@@ -15,9 +16,13 @@ const COOKIE = "wh_lid";
1516
export async function GET(req: NextRequest) {
1617
const token = req.nextUrl.searchParams.get("token");
1718
const nonce = token ? verifyNonce(token) : null;
18-
if (!nonce) return failRedirect(req, "invalid");
19+
if (!nonce) {
20+
logApiReject("auth/verify", "invalid", { phase: "get", hasToken: !!token });
21+
return failRedirect(req, "invalid");
22+
}
1923
const status = await magicTokenStatus(nonce);
2024
if (status !== "valid") {
25+
logApiReject("auth/verify", status === "expired" ? "expired" : "used", { phase: "get" });
2126
return failRedirect(req, status === "expired" ? "expired" : "used");
2227
}
2328

@@ -64,15 +69,22 @@ export async function POST(req: NextRequest) {
6469

6570
const nonce = token ? verifyNonce(token) : null;
6671
const deviceLocalId = req.cookies.get(COOKIE)?.value;
67-
if (!nonce) return failRedirect(req, "invalid");
68-
if (!deviceLocalId) return failRedirect(req, "no_session");
72+
if (!nonce) {
73+
logApiReject("auth/verify", "invalid", { phase: "post", hasToken: !!token });
74+
return failRedirect(req, "invalid");
75+
}
76+
if (!deviceLocalId) {
77+
logApiReject("auth/verify", "no_session", { phase: "post" });
78+
return failRedirect(req, "no_session");
79+
}
6980

7081
try {
7182
const result = await executeMagicVerify(nonce, deviceLocalId);
7283
if (result === "ok") {
7384
return NextResponse.redirect(new URL("/?verified=1", req.nextUrl.origin));
7485
}
7586
if (result === "device_mismatch") {
87+
logApiReject("auth/verify", "wrong_device", { phase: "post" });
7688
return failRedirect(req, "wrong_device");
7789
}
7890
if (result === "token_invalid") {
@@ -83,6 +95,7 @@ export async function POST(req: NextRequest) {
8395
return failRedirect(req, "error");
8496
}
8597

98+
logApiReject("auth/verify", "error", { phase: "post", detail: "unexpected_result" });
8699
return failRedirect(req, "error");
87100
}
88101

@@ -92,6 +105,8 @@ function failRedirect(req: NextRequest, reason: VerifyFailureReason) {
92105

93106
async function failRedirectForToken(req: NextRequest, nonce: string) {
94107
const status = await magicTokenStatus(nonce);
108+
const reason = status === "expired" ? "expired" : "used";
109+
logApiReject("auth/verify", reason, { phase: "post", tokenStatus: status });
95110
if (status === "expired") return failRedirect(req, "expired");
96111
return failRedirect(req, "used");
97112
}

src/app/api/node/route.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { newCode } from "@/lib/codes";
1414
import { classify, isHuman } from "@/lib/classify";
1515
import { geoFromHeaders, clientIp } from "@/lib/geo";
1616
import { hashKeyed } from "@/lib/crypto";
17+
import { logApiError, logApiReject, zodIssueSummary } from "@/lib/api-log";
1718
import { admit } from "@/lib/ratelimit";
1819
import { riskScore } from "@/lib/risk";
1920
import { checkBotId } from "botid/server";
@@ -35,11 +36,13 @@ const COOKIE = "wh_lid";
3536
export async function POST(req: NextRequest) {
3637
const parsed = Body.safeParse(await req.json().catch(() => null));
3738
if (!parsed.success) {
39+
logApiReject("node", "bad_request", zodIssueSummary(parsed.error.issues));
3840
return NextResponse.json({ error: "bad_request" }, { status: 400 });
3941
}
4042

4143
const cookieLocalId = req.cookies.get(COOKIE)?.value;
4244
if (cookieLocalId && cookieLocalId !== parsed.data.localId) {
45+
logApiReject("node", "identity_mismatch", { hasCookie: true });
4346
return NextResponse.json({ error: "identity_mismatch" }, { status: 403 });
4447
}
4548

@@ -91,6 +94,7 @@ export async function POST(req: NextRequest) {
9194
// ── Admission control (Redis counters; allow-all if no Redis in dev). ──
9295
const verdict = await admit({ localId, fingerprint: fpHash, referrerId, ipHash });
9396
if (!verdict.ok) {
97+
logApiReject("node", "rate_limited", { reason: verdict.reason });
9498
return NextResponse.json({ error: "rate_limited", reason: verdict.reason }, { status: 429 });
9599
}
96100

@@ -114,6 +118,7 @@ export async function POST(req: NextRequest) {
114118
} catch (e) {
115119
const msg = (e as Error).message;
116120
if (msg === "NODE_CREATE_REJECTED") {
121+
logApiReject("node", "rejected");
117122
return NextResponse.json({ error: "rejected" }, { status: 409 });
118123
}
119124
// unique(local_id) race → treat as existing
@@ -124,10 +129,9 @@ export async function POST(req: NextRequest) {
124129
setCookie(res, localId);
125130
return res;
126131
}
132+
logApiError("node", "create failed", e);
127133
throw e;
128134
}
129-
130-
// ── Metric fan-out: only human nodes count toward ancestors' reach. ──
131135
if (isHuman(nodeClass)) {
132136
await bumpAncestors(created, geo.country);
133137
}

src/lib/api-log.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/** Structured server-side logging for API rejections and failures. */
2+
3+
export type ApiLogContext = Record<string, unknown>;
4+
5+
function formatContext(ctx?: ApiLogContext): string {
6+
if (!ctx || Object.keys(ctx).length === 0) return "";
7+
try {
8+
return ` ${JSON.stringify(ctx)}`;
9+
} catch {
10+
return " [context unserializable]";
11+
}
12+
}
13+
14+
/** Log a deliberate 4xx response (client error, rate limit, conflict, etc.). */
15+
export function logApiReject(tag: string, error: string, ctx?: ApiLogContext): void {
16+
console.warn(`[${tag}] ${error}${formatContext(ctx)}`);
17+
}
18+
19+
/** Log an unexpected server error (5xx, thrown exception). */
20+
export function logApiError(tag: string, message: string, err?: unknown, ctx?: ApiLogContext): void {
21+
const suffix = formatContext(ctx);
22+
if (err !== undefined) {
23+
console.error(`[${tag}] ${message}${suffix}`, err);
24+
} else {
25+
console.error(`[${tag}] ${message}${suffix}`);
26+
}
27+
}
28+
29+
/** Zod issue summary safe for logs (no raw user input). */
30+
export function zodIssueSummary(issues: { path: PropertyKey[]; code: string }[]): ApiLogContext {
31+
return {
32+
issues: issues.map((i) => ({ path: i.path.join("."), code: i.code })),
33+
};
34+
}

0 commit comments

Comments
 (0)