Files
HealthLog/src/app/cycle/page.tsx
T
Marc-André Bombeck e974cdb5ff fix(cycle): gate the cycle ring tile and page redirect on the operator-aware module flag
The insights cycle ring tile and the /cycle page redirect gated on
user.cycleTrackingEnabled, which /api/auth/me resolves without the
operator availability layer — so with the Cycle module disabled
server-wide the ring still rendered (and fired its calendar read) and a
direct /cycle URL still loaded.

Read user.modules.cycle instead, which the /me map already AND-s with the
operator switch, so an operator-off instance suppresses both surfaces.
2026-06-16 01:19:54 +02:00

61 lines
2.0 KiB
TypeScript

"use client";
import { Suspense, useEffect } from "react";
import { useRouter } from "next/navigation";
import { Loader2 } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
import { CycleView } from "@/components/cycle/cycle-view";
/**
* v1.15.0 — the cycle vertical entry.
*
* Gated on the resolved `modules.cycle` flag from `GET /api/auth/me`
* (v1.18.0 — the per-user toggle AND the operator server-wide kill-switch;
* the operator-unaware `cycleTrackingEnabled` field is NOT used here so an
* operator-disabled instance bounces a direct URL hit too). An
* unauthenticated visitor is bounced to login; an authenticated account
* without the module enabled is bounced home (the nav entry is already
* hidden for them, so this only catches a direct URL hit). Every
* `/api/cycle/*` route also enforces the gate server-side, so this is a UX
* redirect, not the security boundary.
*/
export default function CyclePage() {
const { user, isLoading, isAuthenticated } = useAuth();
const router = useRouter();
const enabled = user?.modules?.cycle !== false;
useEffect(() => {
if (isLoading) return;
if (!isAuthenticated) {
router.push("/auth/login");
} else if (!enabled) {
router.push("/");
}
}, [isLoading, isAuthenticated, enabled, router]);
if (isLoading || !isAuthenticated || !enabled) {
return (
<div className="flex h-64 items-center justify-center">
<Loader2 className="text-primary h-8 w-8 animate-spin motion-reduce:animate-none" />
</div>
);
}
// `<CycleView>` reads `useSearchParams` for the `?tab=` deep-link; wrap it in
// a Suspense boundary so the client-search-params bailout never de-opts the
// build (Next's `missing-suspense-with-csr-bailout`).
return (
<Suspense
fallback={
<div className="flex h-64 items-center justify-center">
<Loader2 className="text-primary h-8 w-8 animate-spin motion-reduce:animate-none" />
</div>
}
>
<CycleView />
</Suspense>
);
}