chore(release): v1.28.42 — snappier lists, correct mood refresh, safer first sign-up

Performance + correctness fixes from the campaign's verified perf-correctness
audit (the two server-query findings — comprehensive-generate + boot-discovery
distinct — are deferred to a careful separate perf release):

- format-locale built a fresh Intl.DateTimeFormat/NumberFormat per call
  (main-thread stall across every history list). Routed through the existing
  process-wide formatter-cache keyed on locale + options (timeZone + hour-cycle
  in the key); output byte-identical.
- measurement-list rendered every row twice (desktop table + mobile cards, one
  CSS-hidden), unvirtualized. Now renders one layout via the SSR-safe
  useIsMobile(md) hook; row markup + every data-slot unchanged.
- moodDependentKeys omitted dashboardSnapshot(), so a mood write left the
  dashboard tile/score stale ~120s (server cache was already correct). Added it.
- register first-user admin was a count-then-create race (two concurrent
  first sign-ups could both become ADMIN). Now an advisory lock + in-transaction
  re-count makes first-admin atomic; P2002 maps to 409 instead of 500.

Regression test per fix. No migration (0243 still free).
This commit is contained in:
Marc-André Bombeck committed 2026-07-16 03:18:55 +02:00
1 parent 9569d1c528
commit d4be568625
13 files changed
+708 -426

No files matched your search

@@ -14,9 +14,23 @@ vi.mock("@/lib/db", () => ({
appSettings: {
findUnique: vi.fn().mockResolvedValue({ registrationEnabled: true }),
},
// v1.28.42 (M1) — the first-admin count+insert now runs inside a
// transaction-scoped advisory lock. Default `$transaction` to run the
// callback with the same prisma mock as its `tx` handle.
$transaction: vi.fn(),
$queryRaw: vi.fn().mockResolvedValue([{ locked: 1 }]),
},
}));
// v1.28.42 — fail-open breach check + silent device record; mock so the
// happy-path register tests don't reach the network / a real DB write.
vi.mock("@/lib/auth/hibp", () => ({
checkPasswordBreach: vi.fn().mockResolvedValue(null),
}));
vi.mock("@/lib/auth/login-alert", () => ({
recordSignInDevice: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/lib/auth/session", () => ({
createSession: vi.fn().mockResolvedValue(undefined),
}));
@@ -62,6 +76,10 @@ vi.mock("next/headers", () => ({
import { POST } from "../route";
import { checkRateLimit, checkAuthSurfaceRateLimit } from "@/lib/rate-limit";
import { prisma } from "@/lib/db";
import { checkPasswordStrength, hashPassword } from "@/lib/auth/password";
import { createSession } from "@/lib/auth/session";
import { Prisma } from "@/generated/prisma/client";
function postReq(body: unknown): NextRequest {
return new NextRequest("http://localhost/api/auth/register", {
@@ -119,6 +137,90 @@ async function postPathThrough(body: unknown): Promise<Response> {
return POST(postReq(body));
}
// v1.28.42 (M1 + L4) — first-admin race + duplicate-under-race status code.
describe("POST /api/auth/register — first-admin atomicity (M1)", () => {
beforeEach(() => {
// Happy-path setup: reach the create call deterministically.
vi.mocked(checkPasswordStrength).mockReturnValue({
isAcceptable: true,
feedback: [],
} as never);
vi.mocked(hashPassword).mockResolvedValue("hashed");
vi.mocked(createSession).mockResolvedValue(undefined as never);
vi.mocked(prisma.user.findUnique).mockResolvedValue(null as never);
vi.mocked(prisma.user.create).mockImplementation((async (args: {
data: Record<string, unknown>;
}) => ({
id: "u1",
username: args.data.username,
email: args.data.email,
role: args.data.role,
})) as never);
// Run the transaction callback against the same prisma mock as `tx`.
vi.mocked(prisma.$transaction).mockImplementation(((
fn: (tx: typeof prisma) => unknown,
) => fn(prisma)) as never);
vi.mocked(prisma.$queryRaw).mockResolvedValue([{ locked: 1 }] as never);
});
const validBody = {
email: "new@example.com",
username: "newuser",
password: "a-very-strong-password-123",
};
it("mints ADMIN when the in-transaction count is 0 (genuine first user)", async () => {
// Early gate count and in-tx count both 0.
vi.mocked(prisma.user.count).mockResolvedValue(0 as never);
const res = await postPathThrough(validBody);
expect(res.status).toBe(201);
const createArgs = vi.mocked(prisma.user.create).mock.calls[0]?.[0] as {
data: { role: string };
};
expect(createArgs.data.role).toBe("ADMIN");
});
it("mints USER when a concurrent registration committed first (stale early read, fresh in-tx re-count)", async () => {
// The early gate read observed the empty-DB window (0), but by the time
// the locked transaction re-counts, the racing first registration has
// committed → 1. Without the in-tx re-count this second user would also
// be minted ADMIN (the double-admin bug); the fresh count closes it.
vi.mocked(prisma.user.count)
.mockResolvedValueOnce(0 as never) // early registrationEnabled gate
.mockResolvedValueOnce(1 as never); // inside the advisory-locked tx
const res = await postPathThrough(validBody);
expect(res.status).toBe(201);
const createArgs = vi.mocked(prisma.user.create).mock.calls[0]?.[0] as {
data: { role: string };
};
expect(createArgs.data.role).toBe("USER");
});
it("acquires the transaction-scoped advisory lock before counting", async () => {
vi.mocked(prisma.user.count).mockResolvedValue(0 as never);
await postPathThrough(validBody);
expect(prisma.$queryRaw).toHaveBeenCalled();
expect(prisma.$transaction).toHaveBeenCalled();
});
it("maps a P2002 duplicate under the race to 409, not 500 (L4)", async () => {
vi.mocked(prisma.user.count).mockResolvedValue(1 as never);
vi.mocked(prisma.user.create).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError("Unique constraint", {
code: "P2002",
clientVersion: "test",
}) as never,
);
const res = await postPathThrough(validBody);
expect(res.status).toBe(409);
const body = (await res.json()) as { error: string };
expect(body.error).toBe("Username or email already taken");
});
});
describe("POST /api/auth/register — OIDC_ONLY server-side enforcement", () => {
const OIDC_ENV_KEYS = [
"OIDC_ISSUER_URL",
+50 -12
View File
@@ -1,4 +1,5 @@
import { prisma } from "@/lib/db";
import { Prisma } from "@/generated/prisma/client";
import {
consumeInviteToken,
recordInviteConsumer,
@@ -179,19 +180,56 @@ export const POST = apiHandler(async (request: NextRequest) => {
}
}
// First user becomes admin
const role = userCount === 0 ? "ADMIN" : "USER";
// v1.28.42 (M1) — the first registered user becomes ADMIN. Deriving that
// role from the early `userCount` (read at the top, before all the
// validation above) and then creating without coordination is a
// check-then-act race: on a freshly-exposed instance two registrations
// racing the empty-DB window both observe `0` and are BOTH minted ADMIN.
// Serialise the count+insert behind a transaction-scoped advisory lock
// (released on commit/rollback) and re-count *inside* the lock, so the
// second registration always observes the first's committed row and is
// minted USER. The lock scope is only the count + insert — none of the
// argon2 hash / HIBP / invite work above runs under it. Mirrors the
// advisory-lock single-flight in `drain-per-sample-cumulative.ts`.
//
// v1.28.42 (L4) — a concurrent same-email/username signup (both passed the
// findUnique probe above before either committed) surfaces the unique
// violation as `P2002`; map it to the unified 409 rather than an unhandled
// 500. The DB unique index is the real guard against duplicate accounts;
// this only fixes the status code under the race.
const user = await prisma
.$transaction(async (tx) => {
// `pg_advisory_xact_lock` returns void, which the client cannot
// deserialize as a column — selecting FROM it yields a plain int row.
await tx.$queryRaw`
SELECT 1 AS locked
FROM pg_advisory_xact_lock(hashtextextended('register:first-admin', 0))
`;
const priorUsers = await tx.user.count();
const role = priorUsers === 0 ? "ADMIN" : "USER";
return tx.user.create({
data: {
email,
username,
passwordHash,
role,
timezone,
},
});
})
.catch((err: unknown) => {
if (
err instanceof Prisma.PrismaClientKnownRequestError &&
err.code === "P2002"
) {
return null;
}
throw err;
});
// Create user
const user = await prisma.user.create({
data: {
email,
username,
passwordHash,
role,
timezone,
},
});
if (user === null) {
return apiError("Username or email already taken", 409);
}
// Create session immediately. v1.4.22 W5 reconcile (Sr-H1) —
// `createSession` anchors the `hl_onboarding` cookie itself; fresh