mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
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.
61 lines
2.0 KiB
TypeScript
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>
|
|
);
|
|
}
|