mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
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:
1 parent
f6b9c8e882
commit
7104578d27
9 files changed
+28
-14
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" }),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
Reference in new issue
Block a user