Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 16 additions & 48 deletions src/app/api/auth/login/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ export async function POST(request: NextRequest) {
const startedAtMs = Date.now();
const context = getRequestLogContext(request, "/api/auth/login");
const clientIp = readClientIp(request.headers);
let requestBody: unknown;
const respond: typeof jsonWithRequestContext = (incoming, input) =>
jsonWithRequestContext(incoming, {
...input,
logExchange: true,
requestBody,
});

const rate = await consumeRateLimit(request, {
key: "auth-login",
Expand All @@ -55,7 +62,7 @@ export async function POST(request: NextRequest) {

if (!rate.ok) {
logWarn("Auth login rate limited", context);
return jsonWithRequestContext(request, {
return respond(request, {
route: "/api/auth/login",
startedAtMs,
status: 429,
Expand All @@ -65,23 +72,13 @@ export async function POST(request: NextRequest) {
}

try {
const bodyResult = await readJsonBody(request);
if (!bodyResult.ok) {
logWarn("Auth login payload too large", context);
return jsonWithRequestContext(request, {
route: "/api/auth/login",
startedAtMs,
status: bodyResult.status,
body: { error: bodyResult.error },
headers: rateLimitHeaders(rate),
});
}
const rawBody = bodyResult.data;
const rawBody = (await request.json().catch(() => ({}))) as unknown;
requestBody = rawBody;
const parsed = loginSchema.safeParse(rawBody);

if (!parsed.success) {
logValidationFailure("Auth login validation failed", context, parsed.error, rawBody);
return jsonWithRequestContext(request, {
return respond(request, {
route: "/api/auth/login",
startedAtMs,
status: 400,
Expand All @@ -100,7 +97,7 @@ export async function POST(request: NextRequest) {
const lockState = await isLoginLocked(userId, clientIp);
if (lockState.locked) {
logWarn("Auth login blocked by lockout", { ...context, wallet: normalizedWallet, ip: clientIp });
return jsonWithRequestContext(request, {
return respond(request, {
route: "/api/auth/login",
startedAtMs,
status: 423,
Expand Down Expand Up @@ -128,21 +125,7 @@ export async function POST(request: NextRequest) {
code: challengeResult.code,
});

if (failure?.justLocked) {
const retryAfterSeconds = Math.max(1, Math.ceil((failure.lockedUntilMs - Date.now()) / 1000));
return jsonWithRequestContext(request, {
route: "/api/auth/login",
startedAtMs,
status: 423,
body: lockedResponse(retryAfterSeconds),
headers: {
...rateLimitHeaders(rate),
"Retry-After": String(retryAfterSeconds),
},
});
}

return jsonWithRequestContext(request, {
return respond(request, {
route: "/api/auth/login",
startedAtMs,
status: challengeResult.code === "invalid_signature" ? 401 : 400,
Expand All @@ -156,22 +139,7 @@ export async function POST(request: NextRequest) {
if (!role) {
const failure = await registerLoginFailure(userId, clientIp);
logWarn("Auth login unknown wallet", { ...context, wallet: normalizedWallet });

if (failure.justLocked) {
const retryAfterSeconds = Math.max(1, Math.ceil((failure.lockedUntilMs - Date.now()) / 1000));
return jsonWithRequestContext(request, {
route: "/api/auth/login",
startedAtMs,
status: 423,
body: lockedResponse(retryAfterSeconds),
headers: {
...rateLimitHeaders(rate),
"Retry-After": String(retryAfterSeconds),
},
});
}

return jsonWithRequestContext(request, {
return respond(request, {
route: "/api/auth/login",
startedAtMs,
status: 401,
Expand All @@ -194,7 +162,7 @@ export async function POST(request: NextRequest) {
publicKey: normalizedWallet,
});

const response = jsonWithRequestContext(request, {
const response = respond(request, {
route: "/api/auth/login",
startedAtMs,
status: 200,
Expand Down Expand Up @@ -231,7 +199,7 @@ export async function POST(request: NextRequest) {
...context,
detail: error instanceof Error ? error.message : "unknown",
});
return jsonWithRequestContext(request, {
return respond(request, {
route: "/api/auth/login",
startedAtMs,
status: 500,
Expand Down
39 changes: 17 additions & 22 deletions src/app/api/decision/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { readJsonBody } from "@/lib/http/read-json-body";

Check warning on line 1 in src/app/api/decision/route.ts

View workflow job for this annotation

GitHub Actions / quality

'readJsonBody' is defined but never used
import { randomUUID } from "node:crypto";
import { NextRequest } from "next/server";

Expand All @@ -11,7 +11,7 @@
logInfo,
logWarn,
} from "@/lib/observability/logger";
import { readinessBlockResponse } from "@/lib/readiness/guard";

Check warning on line 14 in src/app/api/decision/route.ts

View workflow job for this annotation

GitHub Actions / quality

'readinessBlockResponse' is defined but never used
import { recordDecisionOutcome } from "@/lib/observability/metrics";
import { redactSensitiveFields } from "@/lib/observability/redact";
import { demoScenarios } from "@/lib/scenarios/seed";
Expand All @@ -21,7 +21,7 @@
consumeUsage,
getDailyUsage,
} from "@/lib/storage/audit-store";
import { getUserWallet } from "@/lib/storage/user-wallet-store";

Check warning on line 24 in src/app/api/decision/route.ts

View workflow job for this annotation

GitHub Actions / quality

'getUserWallet' is defined but never used
import { getPolicyConfig } from "@/lib/storage/policy-store";
import { buildPaymentQuoteFromDecision } from "@/lib/stellar/verify-payment-quote";
import type { AuditEntry } from "@/lib/types/domain";
Expand All @@ -30,6 +30,13 @@
export async function POST(request: NextRequest) {
const startedAtMs = Date.now();
const context = getRequestLogContext(request, "/api/decision");
let requestBody: unknown;
const respond: typeof jsonWithRequestContext = (incoming, input) =>
jsonWithRequestContext(incoming, {
...input,
logExchange: true,
requestBody,
});

// Read the body before consuming the rate budget so the gate can check the
// requested destination against the blocklist in the same atomic step.
Expand Down Expand Up @@ -58,12 +65,9 @@
actionDomain: typeof rawActionDomain === "string" ? rawActionDomain : undefined,
});

if (!gate.ok) {
logWarn("Decision route blocked by enforcement gate", {
...context,
gateCode: gate.code,
});
return jsonWithRequestContext(request, {
if (!rate.ok) {
logWarn("Decision route rate limited", context);
return respond(request, {
route: "/api/decision",
startedAtMs,
status: gate.status,
Expand All @@ -87,22 +91,13 @@

const userId = auth.session.userId;

const bodyResult = await readJsonBody(request);
if (!bodyResult.ok) {
logWarn("Decision payload too large", { ...context, userId });
return jsonWithRequestContext(request, {
route: "/api/decision",
startedAtMs,
status: 413,
body: { error: bodyResult.error },
});
}
const rawBody = bodyResult.data;
const rawBody = (await request.json().catch(() => ({}))) as unknown;
requestBody = rawBody;
const parsedBody = decisionRequestSchema.safeParse(rawBody);

if (!parsedBody.success) {
logWarn("Decision route validation failed", { ...context, userId });
return jsonWithRequestContext(request, {
logValidationFailure("Decision route validation failed", { ...context, userId }, parsedBody.error, rawBody);
return respond(request, {
route: "/api/decision",
startedAtMs,
status: 400,
Expand All @@ -125,7 +120,7 @@

if (!action) {
logWarn("Decision route action missing", { ...context, userId });
return jsonWithRequestContext(request, {
return respond(request, {
route: "/api/decision",
startedAtMs,
status: 400,
Expand Down Expand Up @@ -210,7 +205,7 @@

recordDecisionOutcome(finalDecision);

return jsonWithRequestContext(request, {
return respond(request, {
route: "/api/decision",
startedAtMs,
status: 200,
Expand Down Expand Up @@ -239,7 +234,7 @@
...context,
detail: redactedDetail,
});
return jsonWithRequestContext(request, {
return respond(request, {
route: "/api/decision",
startedAtMs,
status: 500,
Expand Down
Loading
Loading