fix(02-12): honor shared scope in module pages

- keep scoped record pages out of actor module redirects\n- suppress unrelated illness reads from document-only records
This commit is contained in:
Marc-André Bombeck committed 2026-08-06 05:12:23 +02:00
1 parent f6b9c8e882
commit 7104578d27
9 files changed
+28 -14

No files matched your search

+3 -1
View File
@@ -4,6 +4,7 @@ import { Suspense, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/hooks/use-auth";
import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { PageAuthGate } from "@/components/ui/page-auth-gate";
import { CycleView } from "@/components/cycle/cycle-view";
@@ -22,9 +23,10 @@ import { CycleView } from "@/components/cycle/cycle-view";
*/
export default function CyclePage() {
const { user, isLoading, isAuthenticated } = useAuth();
const { inSharedRecord } = useRecordCapabilities();
const router = useRouter();
const enabled = user?.modules?.cycle !== false;
const enabled = inSharedRecord || user?.modules?.cycle !== false;
useEffect(() => {
if (isLoading) return;
+3 -1
View File
@@ -14,6 +14,7 @@ import { PageAuthGate } from "@/components/ui/page-auth-gate";
import { IllnessEpisodeDetail } from "@/components/illness/illness-episode-detail";
import { BackLink } from "@/components/ui/back-link";
import { useAuth } from "@/hooks/use-auth";
import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { useMounted } from "@/hooks/use-mounted";
import { useTranslations } from "@/lib/i18n/context";
@@ -24,11 +25,12 @@ export default function IllnessEpisodePage({
}) {
const { id } = use(params);
const { user, isAuthenticated, isLoading } = useAuth();
const { inSharedRecord } = useRecordCapabilities();
const mounted = useMounted();
const router = useRouter();
const { t } = useTranslations();
const enabled = user?.modules?.illness === true;
const enabled = inSharedRecord || user?.modules?.illness === true;
useEffect(() => {
if (isLoading) return;
+3 -1
View File
@@ -4,6 +4,7 @@ import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/hooks/use-auth";
import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { PageAuthGate } from "@/components/ui/page-auth-gate";
import { IllnessView } from "@/components/illness/illness-view";
@@ -20,10 +21,11 @@ import { IllnessView } from "@/components/illness/illness-view";
*/
export default function IllnessPage() {
const { user, isLoading, isAuthenticated } = useAuth();
const { inSharedRecord } = useRecordCapabilities();
const router = useRouter();
// Born-gated: enabled ONLY when the resolved map says `true`.
const enabled = user?.modules?.illness === true;
const enabled = inSharedRecord || user?.modules?.illness === true;
useEffect(() => {
if (isLoading) return;
+2 -2
View File
@@ -41,7 +41,7 @@ export default function LabsPage() {
// Scanning a report is not: it files a document and commits through the OCR
// route, and neither is delegated. So a WRITE delegate keeps the manual add
// and loses the choice menu around it.
const { canAdd, canManage } = useRecordCapabilities();
const { canAdd, canManage, inSharedRecord } = useRecordCapabilities();
const mounted = useMounted();
const router = useRouter();
const queryClient = useQueryClient();
@@ -60,7 +60,7 @@ export default function LabsPage() {
// absent key reads as enabled, so a direct URL hit only bounces on an
// explicit `false`. Every `/api/labs/*` route also enforces the gate
// server-side, so this is a UX redirect, not the security boundary.
const enabled = user?.modules?.labs !== false;
const enabled = inSharedRecord || user?.modules?.labs !== false;
const refreshVisible = useCallback(
() => queryClient.invalidateQueries({ type: "active" }),
+3 -2
View File
@@ -168,13 +168,14 @@ export default function MedicationsPageClient() {
// to tapping each card in turn — so withholding it made a caregiver with
// five morning tablets tap five times for nothing. The customize page stays
// withheld; sharing does not cover it at all.
const { canAdd, canManage } = useRecordCapabilities();
const { canAdd, canManage, inSharedRecord } = useRecordCapabilities();
// v1.18.1 (D3) — medications is an opt-out module. When the account has it
// turned off the whole page disappears (the nav entry is hidden by the same
// gate) and the list query never fires (the API would 403 anyway).
// Default-on: an absent key reads as enabled, so the page only hides on an
// explicit `false`.
const medicationsEnabled = user?.modules?.medications !== false;
const medicationsEnabled =
inSharedRecord || user?.modules?.medications !== false;
const router = useRouter();
const queryClient = useQueryClient();
const searchParams = useSearchParams();
+3 -1
View File
@@ -4,6 +4,7 @@ import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/hooks/use-auth";
import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { PageAuthGate } from "@/components/ui/page-auth-gate";
import { MentalWellbeing } from "@/components/mental-health/mental-wellbeing";
@@ -26,9 +27,10 @@ import { MentalWellbeing } from "@/components/mental-health/mental-wellbeing";
*/
export default function MentalWellbeingPage() {
const { user, isLoading, isAuthenticated } = useAuth();
const { inSharedRecord } = useRecordCapabilities();
const router = useRouter();
const enabled = user?.modules?.mentalHealth === true;
const enabled = inSharedRecord || user?.modules?.mentalHealth === true;
useEffect(() => {
if (isLoading) return;
+2 -2
View File
@@ -21,7 +21,7 @@ import { useTranslations } from "@/lib/i18n/context";
export default function MoodPage() {
const { user, isAuthenticated, isLoading } = useAuth();
const { canManage } = useRecordCapabilities();
const { canManage, inSharedRecord } = useRecordCapabilities();
const mounted = useMounted();
const router = useRouter();
const queryClient = useQueryClient();
@@ -37,7 +37,7 @@ export default function MoodPage() {
// absent key reads as enabled, so a direct URL hit only bounces on an
// explicit `false`. Every `/api/mood-entries/*` route also enforces the gate
// server-side, so this is a UX redirect, not the security boundary.
const enabled = user?.modules?.mood !== false;
const enabled = inSharedRecord || user?.modules?.mood !== false;
// v1.16.4 — PWA pull-to-refresh: a top-anchored touch pull refetches
// whatever this page currently has mounted (`type: "active"` scopes the
+7 -3
View File
@@ -184,14 +184,15 @@ export function DocumentsView() {
// about it are none of them delegated verbs. Inside somebody else's record
// the vault reads and nothing more: no upload path, no selection, no bulk
// bar, no corpus backfill.
const { canManage } = useRecordCapabilities();
const { canManage, inSharedRecord, sections } = useRecordCapabilities();
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const queryClient = useQueryClient();
const coachLaunch = useCoachLaunch();
const moduleEnabled = user?.modules?.inboundDocuments === true;
const moduleEnabled =
inSharedRecord || user?.modules?.inboundDocuments === true;
// UX redirect only — the API routes enforce the gate server-side.
useEffect(() => {
@@ -377,7 +378,10 @@ export function DocumentsView() {
);
const { dropActive } = usePageFileDrop(canManage ? enqueueFiles : undefined);
const episodes = useIllnessEpisodes(true);
const episodes = useIllnessEpisodes(
true,
!inSharedRecord || sections === null || sections.includes("illness"),
);
// Content-search coverage: fire the corpus backfill when indexing is
// available and some documents are not yet searchable. The list search
+2 -1
View File
@@ -25,9 +25,10 @@ import type {
} from "./types";
/** Newest-first episode list. */
export function useIllnessEpisodes(includeResolved = true) {
export function useIllnessEpisodes(includeResolved = true, enabled = true) {
return useQuery({
queryKey: queryKeys.illnessEpisodes(includeResolved),
enabled,
queryFn: () =>
apiGet<IllnessEpisodeDTO[]>(
`/api/illness/episodes?includeResolved=${includeResolved}`,