mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
Merge Retry-After on every refused request, stable list pagination and the corrected error contract
This commit is contained in:
commit
6a2282fef8
80 files changed
+2261
-220
No files matched your search
@@ -32,6 +32,7 @@ vi.mock("@/lib/db-compat", () => ({
|
||||
vi.mock("@/lib/rate-limit", () => ({
|
||||
checkRateLimit: vi.fn().mockResolvedValue({
|
||||
allowed: true,
|
||||
limit: 60,
|
||||
remaining: 59,
|
||||
resetAt: Date.now() + 60_000,
|
||||
}),
|
||||
@@ -192,6 +193,7 @@ describe("PATCH /api/auth/me/diabetes", () => {
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
vi.mocked(checkRateLimit).mockResolvedValueOnce({
|
||||
allowed: false,
|
||||
limit: 60,
|
||||
remaining: 0,
|
||||
resetAt: Date.now() + 30_000,
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ vi.mock("@/lib/db-compat", () => ({
|
||||
vi.mock("@/lib/rate-limit", () => ({
|
||||
checkRateLimit: vi.fn().mockResolvedValue({
|
||||
allowed: true,
|
||||
limit: 60,
|
||||
remaining: 59,
|
||||
resetAt: Date.now() + 60_000,
|
||||
}),
|
||||
@@ -247,6 +248,7 @@ describe("PATCH /api/auth/me/disable-coach", () => {
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
vi.mocked(checkRateLimit).mockResolvedValueOnce({
|
||||
allowed: false,
|
||||
limit: 60,
|
||||
remaining: 0,
|
||||
resetAt: Date.now() + 30_000,
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ vi.mock("@/lib/db-compat", () => ({
|
||||
vi.mock("@/lib/rate-limit", () => ({
|
||||
checkRateLimit: vi.fn().mockResolvedValue({
|
||||
allowed: true,
|
||||
limit: 60,
|
||||
remaining: 59,
|
||||
resetAt: Date.now() + 60_000,
|
||||
}),
|
||||
@@ -197,6 +198,7 @@ describe("PATCH /api/auth/me/glucose-unit", () => {
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
vi.mocked(checkRateLimit).mockResolvedValueOnce({
|
||||
allowed: false,
|
||||
limit: 60,
|
||||
remaining: 0,
|
||||
resetAt: Date.now() + 30_000,
|
||||
});
|
||||
|
||||
@@ -35,6 +35,7 @@ vi.mock("@/lib/db-compat", () => ({
|
||||
vi.mock("@/lib/rate-limit", () => ({
|
||||
checkRateLimit: vi.fn().mockResolvedValue({
|
||||
allowed: true,
|
||||
limit: 60,
|
||||
remaining: 59,
|
||||
resetAt: Date.now() + 60_000,
|
||||
}),
|
||||
@@ -397,6 +398,7 @@ describe("PATCH — envelope and transport", () => {
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
vi.mocked(checkRateLimit).mockResolvedValueOnce({
|
||||
allowed: false,
|
||||
limit: 60,
|
||||
remaining: 0,
|
||||
resetAt: Date.now() + 30_000,
|
||||
});
|
||||
|
||||
@@ -147,6 +147,10 @@ describe("POST /api/auth/me/mfa/disable", () => {
|
||||
} as never);
|
||||
const res = await POST(req());
|
||||
expect(res.status).toBe(401);
|
||||
// The code is what separates "re-prompt for a digit" from "this session is
|
||||
// gone"; a client that branches on prose here signs the user out on a typo.
|
||||
const body = (await res.json()) as { meta?: { errorCode?: string } };
|
||||
expect(body.meta?.errorCode).toBe("auth.mfa.code_invalid");
|
||||
expect(destroyOtherSessions).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@
|
||||
*/
|
||||
import {
|
||||
apiHandler,
|
||||
AUTH_ERROR_CODES,
|
||||
requireMfaManagementAuth,
|
||||
HttpError,
|
||||
} from "@/lib/api-handler";
|
||||
@@ -81,7 +82,7 @@ export const POST = apiHandler(async (req: Request) => {
|
||||
},
|
||||
});
|
||||
annotate({ action: { name: "auth.mfa.disable.invalid_factor" } });
|
||||
throw new HttpError(401, "Invalid code");
|
||||
throw new HttpError(401, "Invalid code", AUTH_ERROR_CODES.mfaCode);
|
||||
}
|
||||
|
||||
// Every cheap check has passed and the teardown is next, so spend the
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
import {
|
||||
apiHandler,
|
||||
AUTH_ERROR_CODES,
|
||||
requireMfaManagementAuth,
|
||||
HttpError,
|
||||
} from "@/lib/api-handler";
|
||||
@@ -99,7 +100,7 @@ export const POST = apiHandler(async (req: Request) => {
|
||||
action: { name: "auth.mfa.totp.confirm.invalid" },
|
||||
meta: { replay: result.replay },
|
||||
});
|
||||
throw new HttpError(401, "Invalid code");
|
||||
throw new HttpError(401, "Invalid code", AUTH_ERROR_CODES.mfaCode);
|
||||
}
|
||||
|
||||
const recoveryCodes = generateRecoveryCodes();
|
||||
|
||||
@@ -35,6 +35,7 @@ vi.mock("@/lib/db-compat", () => ({
|
||||
vi.mock("@/lib/rate-limit", () => ({
|
||||
checkRateLimit: vi.fn().mockResolvedValue({
|
||||
allowed: true,
|
||||
limit: 60,
|
||||
remaining: 59,
|
||||
resetAt: Date.now() + 60_000,
|
||||
}),
|
||||
@@ -300,6 +301,7 @@ describe("PATCH /api/auth/me/modules", () => {
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
vi.mocked(checkRateLimit).mockResolvedValueOnce({
|
||||
allowed: false,
|
||||
limit: 60,
|
||||
remaining: 0,
|
||||
resetAt: Date.now() + 30_000,
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ vi.mock("@/lib/db-compat", () => ({
|
||||
vi.mock("@/lib/rate-limit", () => ({
|
||||
checkRateLimit: vi.fn().mockResolvedValue({
|
||||
allowed: true,
|
||||
limit: 60,
|
||||
remaining: 59,
|
||||
resetAt: Date.now() + 60_000,
|
||||
}),
|
||||
@@ -587,6 +588,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
vi.mocked(checkRateLimit).mockResolvedValueOnce({
|
||||
allowed: false,
|
||||
limit: 60,
|
||||
remaining: 0,
|
||||
resetAt: Date.now() + 30_000,
|
||||
});
|
||||
|
||||
@@ -13,7 +13,12 @@
|
||||
*/
|
||||
import { NextRequest } from "next/server";
|
||||
import { headers } from "next/headers";
|
||||
import { apiHandler, HttpError, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiHandler,
|
||||
AUTH_ERROR_CODES,
|
||||
HttpError,
|
||||
requireAuth,
|
||||
} from "@/lib/api-handler";
|
||||
import { apiSuccess } from "@/lib/api-response";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
@@ -139,7 +144,7 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
|
||||
async function presentedBearerToken(): Promise<string> {
|
||||
const authHeader = (await headers()).get("authorization");
|
||||
if (!authHeader?.startsWith("Bearer ")) {
|
||||
throw new HttpError(401, "Not authenticated");
|
||||
throw new HttpError(401, "Not authenticated", AUTH_ERROR_CODES.missing);
|
||||
}
|
||||
return authHeader.slice(7);
|
||||
}
|
||||
Reference in new issue
Block a user