mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
- register /vaccinations in the clinical nav spine, module-gated on vaccinations (default-on) and part of the shared record - join /vaccinations to the profile shared-record route family so a scoped profile grant can present the page it is allowed to read - add the page shell: authenticated, module-gated, with the standard module-off redirect the medications surface uses - add the nav label and the surface title/subtitle/empty copy in six locales
129 lines
4.7 KiB
TypeScript
129 lines
4.7 KiB
TypeScript
import {
|
|
ENTIRE_RECORD,
|
|
SHARE_DOMAINS,
|
|
type ShareDomain,
|
|
type ShareScope,
|
|
} from "@/lib/sharing/scope";
|
|
|
|
/**
|
|
* The browser route families a scoped grant can present.
|
|
*
|
|
* This is deliberately a presentation inventory, not an authorization table.
|
|
* `sections` arrives here as the server-resolved value from `accountAccess`;
|
|
* the request handler still evaluates the active grant for every read and
|
|
* mutation. Keeping the browser's route families closed lets a new domain
|
|
* fail closed in chrome until it has been intentionally classified.
|
|
*/
|
|
export const SHARED_RECORD_DOMAIN_ROUTE_FAMILIES = {
|
|
// `/checkups` carries content from two domains and is listed under both.
|
|
// The preventive-care list has always been a `measurements` read
|
|
// (`/api/measurement-reminders` declares it), and the page was in neither
|
|
// list — under a whole-record grant it painted anyway, because a null
|
|
// section set admits everything, so the omission stayed invisible for as
|
|
// long as a scoped grant had no reason to open it. The visits section makes
|
|
// it visible: a delegate scoped to `profile` is granted the visit data by
|
|
// the API and would be denied the only page that shows it. The table is
|
|
// built by a flatMap over (domain, href) pairs, so one href under two
|
|
// domains means a grant naming either presents the page — and the handler
|
|
// still evaluates the grant on every read, because this is a presentation
|
|
// inventory and not an authorization table.
|
|
measurements: ["/measurements", "/checkups"],
|
|
medications: ["/medications"],
|
|
labs: ["/labs"],
|
|
// `/vaccinations` joins `profile`: an immunization history is health
|
|
// background in the same sense as the visits already listed here, so a
|
|
// delegate scoped to `profile` must be able to present the page the API
|
|
// already lets them read — without this line the grant reads the data and
|
|
// the chrome hides the only page that shows it.
|
|
profile: ["/profile", "/checkups", "/vaccinations"],
|
|
illness: ["/illness"],
|
|
mind: ["/mood", "/mental-wellbeing"],
|
|
cycle: ["/cycle"],
|
|
documents: ["/documents"],
|
|
} as const satisfies Record<ShareDomain, readonly `/${string}`[]>;
|
|
|
|
/**
|
|
* Routes whose reads span multiple domains. A selected-domain grant never
|
|
* opens these, even when it happens to name every currently known domain.
|
|
*/
|
|
const WHOLE_RECORD_ROUTE_FAMILIES = ["/", "/achievements"] as const;
|
|
|
|
interface RouteFamily {
|
|
href: string;
|
|
scope: ShareScope;
|
|
}
|
|
|
|
const SHARED_RECORD_ROUTE_FAMILIES: readonly RouteFamily[] = [
|
|
...WHOLE_RECORD_ROUTE_FAMILIES.map((href): RouteFamily => ({
|
|
href,
|
|
scope: ENTIRE_RECORD,
|
|
})),
|
|
...SHARE_DOMAINS.flatMap((domain): RouteFamily[] =>
|
|
SHARED_RECORD_DOMAIN_ROUTE_FAMILIES[domain].map((href): RouteFamily => ({
|
|
href,
|
|
scope: domain,
|
|
})),
|
|
),
|
|
];
|
|
|
|
function matchesRouteFamily(pathname: string, href: string): boolean {
|
|
return href === "/"
|
|
? pathname === href
|
|
: pathname === href || pathname.startsWith(`${href}/`);
|
|
}
|
|
|
|
function includesScope(
|
|
sections: readonly ShareDomain[] | null,
|
|
scope: ShareScope,
|
|
): boolean {
|
|
if (sections === null) return true;
|
|
return scope !== ENTIRE_RECORD && sections.includes(scope);
|
|
}
|
|
|
|
/**
|
|
* Server-resolved navigation paint for the active shared record.
|
|
*
|
|
* The return value never makes an authorization decision. It only prevents a
|
|
* known-unavailable page from mounting its reads after the server has already
|
|
* said which record sections the browser is inside.
|
|
*/
|
|
export function resolveSharedRecordNavigation(
|
|
sections: readonly ShareDomain[] | null,
|
|
) {
|
|
const routeFamilies = SHARED_RECORD_ROUTE_FAMILIES.filter((family) =>
|
|
includesScope(sections, family.scope),
|
|
);
|
|
|
|
return {
|
|
// Deduplicated: an href listed under two domains is one destination, and a
|
|
// grant holding both would otherwise offer it twice — and make
|
|
// `sharedRecordLandingHref` count a page as two doorways.
|
|
destinationHrefs: [...new Set(routeFamilies.map((family) => family.href))],
|
|
allowsPath(pathname: string): boolean {
|
|
return routeFamilies.some((family) =>
|
|
matchesRouteFamily(pathname, family.href),
|
|
);
|
|
},
|
|
};
|
|
}
|
|
|
|
/** The first scope-presentable location to load after a record switch. */
|
|
export function sharedRecordLandingHref(
|
|
sections: readonly ShareDomain[] | null,
|
|
): string {
|
|
return resolveSharedRecordNavigation(sections).destinationHrefs[0] ?? "/";
|
|
}
|
|
|
|
/**
|
|
* Does the server-resolved scope make this browser route presentable?
|
|
*
|
|
* This helper is intentionally named for presentation. It must never be used
|
|
* by a route handler or to choose an API request's target record.
|
|
*/
|
|
export function isSharedRecordPathPresentable(
|
|
pathname: string,
|
|
sections: readonly ShareDomain[] | null,
|
|
): boolean {
|
|
return resolveSharedRecordNavigation(sections).allowsPath(pathname);
|
|
}
|