fix(consent): close the receipt mint race, rate-limit, and share the back link

Mint the web AI-consent receipt inside a serializable transaction with an
in-transaction recheck, backed by a partial unique index over one active
receipt per user and kind, so two concurrent mounts can no longer leave two
active grants; a duplicate maps to a no-op mint. Supersede an active row
against the server clock rather than the client-supplied timestamp so a
backdated grant cannot invert the audit chain, and revoke through a single
atomic update. Rate-limit the consent endpoints per user. Extract the
hand-rolled sub-page back link into one component and adopt it across the
four pages that duplicated it.
This commit is contained in:
Marc-André Bombeck committed 2026-06-14 17:26:36 +02:00
commit 555240cd28
19 files changed
+780 -176

No files matched your search

@@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { BackLink } from "../back-link";
describe("<BackLink>", () => {
it("renders an anchor to the href carrying the label", () => {
const html = renderToStaticMarkup(
<BackLink href="/insights/workouts" label="Back to workouts" />,
);
expect(html).toContain('href="/insights/workouts"');
expect(html).toContain("Back to workouts");
// The decorative chevron is hidden from screen readers; the label
// carries the meaning.
expect(html).toContain('aria-hidden="true"');
});
it("keeps the shared left-bleed alignment on the control", () => {
const html = renderToStaticMarkup(
<BackLink href="/medications" label="Back" />,
);
// The `-ml-2 w-fit` left-bleed is the standard back-nav placement; it
// must survive on every adopting page.
expect(html).toContain("-ml-2");
expect(html).toContain("w-fit");
});
it("threads the optional data-slot for e2e/visual targeting", () => {
const html = renderToStaticMarkup(
<BackLink
href="/insights"
label="Back"
dataSlot="composite-score-back"
/>,
);
expect(html).toContain('data-slot="composite-score-back"');
});
});
+47
View File
@@ -0,0 +1,47 @@
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
/**
* v1.16.16 — shared back-navigation control.
*
* The detail / sub-page surfaces (`/insights/workouts/[id]`,
* `/insights/values/[type]`, `/insights/scores/[metric]`,
* `/medications/[id]`) each hand-rolled the identical
* `Button(ghost, sm, -ml-2 w-fit) > Link > ArrowLeft + label` block. This
* folds that into one component so the back-nav reads the same — same hit
* area, same icon, same left-bleed alignment — on every page, and a future
* change to the affordance lives in one place.
*
* The label travels in from the caller (each page already owns its own
* translation key, e.g. `insights.workouts.detail.backToList`), so this
* component adds no new i18n surface.
*/
export interface BackLinkProps {
/** Internal href the back-nav points at. */
href: string;
/** Already-translated label text. */
label: string;
/** Optional per-surface `data-slot` for e2e/visual targeting. */
dataSlot?: string;
className?: string;
}
export function BackLink({ href, label, dataSlot, className }: BackLinkProps) {
return (
<Button
asChild
variant="ghost"
size="sm"
data-slot={dataSlot}
className={cn("-ml-2 w-fit", className)}
>
<Link href={href}>
<ArrowLeft className="size-4" aria-hidden="true" />
{label}
</Link>
</Button>
);
}