feat: classify managed record settings

- Inventory every Settings destination as an explicit access class\n- Fail closed for unknown deep-link destinations
This commit is contained in:
Marc-André Bombeck committed 2026-08-06 02:28:29 +02:00
1 parent d965076609
commit d95c48f48d
3 files changed
+116

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
import type { SettingsSectionSlug } from "@/components/settings/section-slugs";
export type SettingsDestinationKind =
| "personal"
| "managed-guardian"
| "adult-shared-unavailable"
| "unavailable";
export interface SettingsDestinationClassification {
kind: SettingsDestinationKind;
guardianWritable: boolean;
}
const PERSONAL: SettingsDestinationClassification = {
kind: "personal",
guardianWritable: false,
};
const MANAGED_GUARDIAN: SettingsDestinationClassification = {
kind: "managed-guardian",
guardianWritable: true,
};
const MANAGED_GUARDIAN_STATUS: SettingsDestinationClassification = {
kind: "managed-guardian",
guardianWritable: false,
};
const ADULT_SHARED_UNAVAILABLE: SettingsDestinationClassification = {
kind: "adult-shared-unavailable",
guardianWritable: false,
};
const UNAVAILABLE: SettingsDestinationClassification = {
kind: "unavailable",
guardianWritable: false,
};
/**
* The complete Settings route inventory. `satisfies Record<SettingsSectionSlug,
* ...>` makes adding a new destination a compile error until its record
* classification is reviewed. Unknown destinations resolve to unavailable so
* direct links fail closed rather than inheriting a neighbouring category.
*/
export const SETTINGS_DESTINATION_INVENTORY = {
account: MANAGED_GUARDIAN,
security: PERSONAL,
access: PERSONAL,
modules: MANAGED_GUARDIAN,
integrations: MANAGED_GUARDIAN_STATUS,
sources: UNAVAILABLE,
notifications: MANAGED_GUARDIAN,
layout: ADULT_SHARED_UNAVAILABLE,
dashboard: ADULT_SHARED_UNAVAILABLE,
insights: MANAGED_GUARDIAN,
medications: ADULT_SHARED_UNAVAILABLE,
mood: ADULT_SHARED_UNAVAILABLE,
labs: ADULT_SHARED_UNAVAILABLE,
illness: ADULT_SHARED_UNAVAILABLE,
environment: UNAVAILABLE,
anamnesis: ADULT_SHARED_UNAVAILABLE,
score: ADULT_SHARED_UNAVAILABLE,
vorsorge: ADULT_SHARED_UNAVAILABLE,
thresholds: MANAGED_GUARDIAN,
ai: UNAVAILABLE,
coach: MANAGED_GUARDIAN,
api: PERSONAL,
mcp: PERSONAL,
gesundheitsakte: PERSONAL,
sharing: UNAVAILABLE,
export: PERSONAL,
advanced: PERSONAL,
privacy: PERSONAL,
about: PERSONAL,
} as const satisfies Record<SettingsSectionSlug, SettingsDestinationClassification>;
export function classifySettingsDestination(
destination: string,
): SettingsDestinationClassification {
return (
SETTINGS_DESTINATION_INVENTORY[
destination as SettingsSectionSlug
] ?? UNAVAILABLE
);
}
export function isGuardianSettingsWriteAllowed(destination: string): boolean {
const classification = classifySettingsDestination(destination);
return (
classification.kind === "managed-guardian" &&
classification.guardianWritable
);
}
+7
View File
@@ -4,3 +4,10 @@ export {
type RecordSettingsAccess,
} from "./access";
export { toRecordSettingsDto, type RecordSettingsDto } from "./dto";
export {
classifySettingsDestination,
isGuardianSettingsWriteAllowed,
SETTINGS_DESTINATION_INVENTORY,
type SettingsDestinationClassification,
type SettingsDestinationKind,
} from "./classification";
@@ -4,6 +4,10 @@ import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { resolveGuardianRecordSettingsAccess } from "@/lib/record-settings/access";
import {
classifySettingsDestination,
SETTINGS_DESTINATION_INVENTORY,
} from "@/lib/record-settings/classification";
const fromRoot = (...segments: string[]) => resolve(process.cwd(), ...segments);
@@ -32,4 +36,16 @@ describe("record settings authorization", () => {
expect(recordSettingsRoute).toContain("requireGuardianAuth");
expect(recordSettingsRoute).toContain("toRecordSettingsDto");
});
it("keeps settings classification exhaustive and fail closed", () => {
expect(Object.keys(SETTINGS_DESTINATION_INVENTORY)).not.toHaveLength(0);
expect(classifySettingsDestination("integrations")).toMatchObject({
kind: "managed-guardian",
guardianWritable: false,
});
expect(classifySettingsDestination("ai").kind).toBe("unavailable");
expect(classifySettingsDestination("unknown-settings-path").kind).toBe(
"unavailable",
);
});
});