mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
feat: gate a switched browser on the record's modules, not the actor's
Turning a module off for a profile you look after wrote a column nothing on screen was reading. GET /api/auth/me published the ACTOR's module map while switched, and the navigation, the dashboard tiles and the insights sections all gate on it — so a guardian who turned Cycle off for the child kept seeing a Cycle destination inside the child's record, and a guardian who tracks it saw one there whether or not the child did (#939). The map and the resolved cycle flag now answer for the record the session is inside. They are not display preferences: every surface they gate shows the record's data, so the record is what decides. Locale, theme and layout stay the actor's. The record is taken from the grant-validated active entry rather than the session's own selector, so a revoked switch cannot publish somebody's configuration, and with no switch the payload is what it was. Cycle also gets a row on the guardian's Modules surface, which it never had. It is a delegated module, so its state is the record's own cycle profile and modulePreferences.cycle is refused — which left a guardian no way to turn it off for a record whose sex says it should be on. The family names the real column instead of a blob entry the gate ignores. Refs #939
This commit is contained in:
1 parent
f521b03c35
commit
fb1cb4e3ed
9 files changed
+556
-46
No files matched your search
@@ -20,9 +20,27 @@ vi.mock("next/navigation", () => ({
|
||||
const mockModulesRef: { value: Record<string, boolean> | undefined } = {
|
||||
value: undefined,
|
||||
};
|
||||
// v1.37.0 — the shell also filters by which record the browser is inside.
|
||||
// Unlike the module gate above, that filter is NOT hydration-deferred: the
|
||||
// server has already resolved which record is active, so it applies on the SSR
|
||||
// pass and is assertable from a static render.
|
||||
const mockActiveRecordRef: {
|
||||
value: { recordKind: "managed" | "shared"; level: string } | null;
|
||||
} = { value: null };
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
useAuth: () => ({
|
||||
user: { id: "u1", role: "USER", modules: mockModulesRef.value },
|
||||
user: {
|
||||
id: "u1",
|
||||
role: "USER",
|
||||
modules: mockModulesRef.value,
|
||||
accountAccess: mockActiveRecordRef.value
|
||||
? {
|
||||
accounts: [mockActiveRecordRef.value],
|
||||
active: mockActiveRecordRef.value,
|
||||
canSwitch: true,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
refetch: vi.fn(),
|
||||
@@ -42,9 +60,11 @@ function renderShell(props: {
|
||||
pathname?: string;
|
||||
locale?: "en" | "de";
|
||||
modules?: Record<string, boolean>;
|
||||
activeRecord?: { recordKind: "managed" | "shared"; level: string } | null;
|
||||
}) {
|
||||
mockPathnameRef.value = props.pathname ?? "/settings/account";
|
||||
mockModulesRef.value = props.modules;
|
||||
mockActiveRecordRef.value = props.activeRecord ?? null;
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale={props.locale ?? "en"}>
|
||||
<SettingsShell active={props.active}>
|
||||
@@ -327,6 +347,39 @@ describe("<SettingsShell>", () => {
|
||||
expect(optedOut).toContain('href="/settings/gesundheitsakte"');
|
||||
});
|
||||
|
||||
describe("inside somebody else's record", () => {
|
||||
/**
|
||||
* #939 — a guardian could turn a module off for the profile they look
|
||||
* after and had to find the page by URL to do it. The section is
|
||||
* classified `managed-guardian`, which is what the shell offers a managed
|
||||
* record; asserting it here is what stops a future narrowing of the filter
|
||||
* from silently taking the destination away again.
|
||||
*/
|
||||
it("offers the Modules destination to a guardian on a managed record", () => {
|
||||
const html = renderShell({
|
||||
active: "account",
|
||||
activeRecord: { recordKind: "managed", level: "manage" },
|
||||
});
|
||||
expect(html).toContain('href="/settings/modules"');
|
||||
// Its own configuration is not the record's, and stays away.
|
||||
expect(html).not.toContain('href="/settings/security"');
|
||||
});
|
||||
|
||||
it("does not offer it to an adult delegate holding MANAGE", () => {
|
||||
// A delegate manages somebody's health record, not their account:
|
||||
// modules, thresholds and notification routing stay with the owner.
|
||||
// Integrations remains status-only on a managed record, and unreachable
|
||||
// here — both are `managed-guardian`, which an adult record never opens.
|
||||
const html = renderShell({
|
||||
active: "account",
|
||||
activeRecord: { recordKind: "shared", level: "manage" },
|
||||
});
|
||||
expect(html).not.toContain('href="/settings/modules"');
|
||||
expect(html).not.toContain('href="/settings/integrations"');
|
||||
expect(html).toContain('href="/settings/anamnesis"');
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to `account` when the pathname doesn't match a known slug", () => {
|
||||
const html = renderShell({ pathname: "/settings/totally-bogus" });
|
||||
const activeLinkRegex =
|
||||
|
||||
@@ -505,6 +505,7 @@ function ModulesSettingsForm({
|
||||
modulePreferences: Object.fromEntries(
|
||||
entries.map(([key]) => [key, form.get(key) === "on"]),
|
||||
),
|
||||
cycleTrackingEnabled: form.get("cycleTrackingEnabled") === "on",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -523,6 +524,17 @@ function ModulesSettingsForm({
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
{/* v1.38.14 (#939) — Cycle sits below the rest because it is not one of
|
||||
them: it is a DELEGATED module, so its state lives in the record's own
|
||||
cycle profile rather than in the module blob, and it had no row here
|
||||
at all. A guardian could not turn it off for a record whose sex says
|
||||
it should be on, which is the case the issue was opened about. */}
|
||||
<ManagedSwitchRow
|
||||
name="cycleTrackingEnabled"
|
||||
label={t(MODULE_REGISTRY.cycle.labelKey)}
|
||||
defaultChecked={settings.cycleTrackingEnabled === true}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<FormFooter disabled={disabled} label={saveLabel} error={error} />
|
||||
</form>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user