mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
fix(sharing): keep life events to their owner
Life events rode the profile sharing domain, so a profile share, a legacy whole-record share and a MANAGE grant all read them, and MANAGE could write them. No share level was ever worded for them, so in this release they are the owner's only. The life-event routes take a bare requireAuth, which refuses under an acting-account switch. The day section and the timeline's life lane map to no domain, so a delegate sees the section named not_shared and never gets the lane, its readiness row or the day index marks. The profile consent copy no longer names life events, the sharing guard and the mutation inventory drop the four entries, and the OpenAPI descriptions say owner-only.
This commit is contained in:
1 parent
e2ed62c74f
commit
f950dffad4
21 files changed
+323
-196
No files matched your search
@@ -1045,14 +1045,6 @@ const DELEGABLE_ROUTES: Record<string, DelegableEntry> = {
|
||||
domain: "record",
|
||||
why: "Counts per timeline lane with an in-app link per gap. Counts only, no record content; scoped to the resolved user and module-gated like the timeline.",
|
||||
},
|
||||
"app/api/life-events/route.ts": {
|
||||
domain: "profile",
|
||||
why: "The record's life events, a section of health background beside allergies and visits; the consent copy for `profile` names them. The GET reads rows scoped `userId: user.id` and decrypts title and note for the resolved session; the create is a MANAGE verb (see the manage literal).",
|
||||
},
|
||||
"app/api/life-events/[id]/route.ts": {
|
||||
domain: "profile",
|
||||
why: "One life event of the record, fetch-then-guard against the resolved user. Reached only through its MANAGE arms; an event another record holds is a 404 like any foreign id.",
|
||||
},
|
||||
"app/api/workouts/route.ts": {
|
||||
domain: "measurements",
|
||||
why: "The record's workout list. Verified to carry no AI-written paragraph: the single-workout route beside it does, and stays refused for exactly that reason.",
|
||||
@@ -1660,16 +1652,6 @@ const DELEGABLE_MANAGE_ROUTES: Record<string, ManageEntry> = {
|
||||
conditions: ["C4"],
|
||||
why: "Correcting and removing one vaccine definition. The removal soft-deletes it and lets go of the doses that named it, each keeping a name, so no dose leaves the record; the audit row names the definition and how many doses it let go. C4 on the edit: the antigens, the series length and the booster interval are filed before and after, and a rename is named, never quoted. Neither verb re-runs the booster satisfy matcher, so a delegate cannot move a booster's due date by correcting a definition.",
|
||||
},
|
||||
"app/api/life-events/route.ts": {
|
||||
domain: "profile",
|
||||
conditions: [],
|
||||
why: "Recording a life event. The person's own anchor, so a guardian's act on a managed profile rather than a helper's; additive, rate-limited on the actor, audited with the category and precision and never the title.",
|
||||
},
|
||||
"app/api/life-events/[id]/route.ts": {
|
||||
domain: "profile",
|
||||
conditions: ["C4"],
|
||||
why: "Correcting and removing a life event. The removal soft-deletes and the audit row names the event's category and date; C4 on the edit: the dates, category and precision are filed before and after, and a changed title or note is named, never quoted.",
|
||||
},
|
||||
"app/api/illness/episodes/[id]/resolve/route.ts": {
|
||||
domain: "illness",
|
||||
conditions: [],
|
||||
@@ -2093,8 +2075,13 @@ const ACTOR_ROUTES: Record<string, string> = {
|
||||
*
|
||||
* v1.42 -- the document vault's presentation read joins the record list
|
||||
* beside the rest of the vault; its PUT stays bare. 255 -> 256.
|
||||
*
|
||||
* v1.42 -- life events leave every list: they are owner-only in this
|
||||
* release (no share level was worded for them), so both modules take a bare
|
||||
* `requireAuth()` and drop off the record list and the manage literal.
|
||||
* 256 -> 252.
|
||||
*/
|
||||
const FROZEN_ENTRY_COUNT = 256;
|
||||
const FROZEN_ENTRY_COUNT = 252;
|
||||
|
||||
/**
|
||||
* The two surfaces that authenticate a Bearer token outside `requireAuth` —
|
||||
@@ -2855,7 +2842,7 @@ describe("(g) the MANAGE route set is frozen", () => {
|
||||
|
||||
it("keeps the admitted mutation inventory complete and discoverable", () => {
|
||||
expect(ADMITTED_MUTATING_HANDLERS.length).toBeGreaterThan(0);
|
||||
expect(ADMITTED_MUTATING_HANDLERS.length).toBe(90);
|
||||
expect(ADMITTED_MUTATING_HANDLERS.length).toBe(87);
|
||||
|
||||
const expected = ADMITTED_MUTATING_HANDLERS.map(
|
||||
({ handlerModule, action, level }) =>
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("sharing and handler inventories", () => {
|
||||
});
|
||||
|
||||
it("keeps every admitted mutation uniquely addressable and fully controlled", () => {
|
||||
expect(ADMITTED_MUTATING_HANDLERS).toHaveLength(90);
|
||||
expect(ADMITTED_MUTATING_HANDLERS).toHaveLength(87);
|
||||
expect(
|
||||
new Set(
|
||||
ADMITTED_MUTATING_HANDLERS.map(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* `PATCH /api/life-events/{id}` edits one life event; `DELETE` soft-deletes
|
||||
* it (v1.42, #613). Record-scoped like the list beside it, and MANAGE like
|
||||
* the create: correcting or removing somebody's life event is a guardian's
|
||||
* act. A foreign, unknown or already deleted id is the same 404.
|
||||
* it (v1.42, #613). Owner-only like the list beside it (see there): no
|
||||
* delegate at any level, and no guardian acting for a managed profile. A
|
||||
* foreign, unknown or already deleted id is the same 404.
|
||||
*
|
||||
* The edit files the replaced dates, category and precision (C4) and names a
|
||||
* changed title or note without quoting either. The delete tombstones the
|
||||
@@ -10,7 +10,7 @@
|
||||
*/
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
@@ -42,7 +42,7 @@ function notFound() {
|
||||
|
||||
export const PATCH = apiHandler(
|
||||
async (request: NextRequest, { params }: RouteParams) => {
|
||||
const { user } = await requireRecordAuth("manage", "profile");
|
||||
const { user } = await requireAuth();
|
||||
const path = lifeEventIdPathSchema.safeParse(await params);
|
||||
if (!path.success) return notFound();
|
||||
const existing = await findOwnLifeEvent(user.id, path.data.id);
|
||||
@@ -109,7 +109,7 @@ export const PATCH = apiHandler(
|
||||
|
||||
export const DELETE = apiHandler(
|
||||
async (request: NextRequest, { params }: RouteParams) => {
|
||||
const { user } = await requireRecordAuth("manage", "profile");
|
||||
const { user } = await requireAuth();
|
||||
const path = lifeEventIdPathSchema.safeParse(await params);
|
||||
if (!path.success) return notFound();
|
||||
const existing = await findOwnLifeEvent(user.id, path.data.id);
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
/**
|
||||
* `GET /api/life-events` lists the record's life events, oldest first;
|
||||
* `GET /api/life-events` lists the account's life events, oldest first;
|
||||
* `POST` adds one (v1.42, #613). Title and note are encrypted at rest and
|
||||
* never reach a model. Shapes and request schemas in `src/lib/day/`.
|
||||
*
|
||||
* Part of the `profile` sharing domain, beside allergies, visits and the
|
||||
* immunization history. The row store answers whether or not the `timeline`
|
||||
* module is on, the data-layer posture of the other record tables, so a
|
||||
* restore keeps working and re-enabling finds the events intact.
|
||||
* Owner-only in v1.42. A life event (a loss, a separation, a new job) is
|
||||
* the most personal row the record holds, and no share level was ever
|
||||
* worded for it, so no delegate reaches it: not a `profile` share, not a
|
||||
* legacy whole-record share, not MANAGE, not a guardian acting for a
|
||||
* managed profile. Bare `requireAuth()`, which refuses under an
|
||||
* acting-account switch and refuses a narrow Bearer scope. Sharing life
|
||||
* events is a separate decision for a later release.
|
||||
*
|
||||
* The create is a MANAGE verb: a life event is the person's own anchor, and
|
||||
* recording one for somebody else is a guardian's act on a managed profile,
|
||||
* not a helper's. Additive, rate-limited on the actor, audited with the
|
||||
* category and the precision and never the title.
|
||||
* The row store answers whether or not the `timeline` module is on, the
|
||||
* data-layer posture of the other record tables, so a restore keeps working
|
||||
* and re-enabling finds the events intact. The create is additive,
|
||||
* rate-limited, audited with the category and the precision and never the
|
||||
* title.
|
||||
*/
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
@@ -35,7 +39,7 @@ import { annotate } from "@/lib/logging/context";
|
||||
import { checkRecordWriteRateLimit } from "@/lib/rate-limit";
|
||||
|
||||
export const GET = apiHandler(async () => {
|
||||
const { user } = await requireRecordAuth("read", "profile");
|
||||
const { user } = await requireAuth();
|
||||
const events = await listLifeEvents(user.id);
|
||||
annotate({
|
||||
action: { name: "life_event.list", entity_type: "life_event" },
|
||||
@@ -47,9 +51,9 @@ export const GET = apiHandler(async () => {
|
||||
export const POST = apiHandler(withIdempotency<[NextRequest]>(postLifeEvent));
|
||||
|
||||
async function postLifeEvent(request: NextRequest): Promise<Response> {
|
||||
const { user, actor } = await requireRecordAuth("manage", "profile");
|
||||
const { user } = await requireAuth();
|
||||
|
||||
const writeRl = await checkRecordWriteRateLimit(actor.id);
|
||||
const writeRl = await checkRecordWriteRateLimit(user.id);
|
||||
if (!writeRl.allowed) {
|
||||
return apiError("Too many writes, try again later", 429, {
|
||||
errorCode: "record_write.rate_limited",
|
||||
|
||||
@@ -67,14 +67,24 @@ describe("day access", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the environment rows for the owner even under a full grant", () => {
|
||||
it("keeps the environment rows and the life events for the owner even under a full grant", () => {
|
||||
const access = resolveDayAccessFrom({
|
||||
modules: ALL_ON,
|
||||
domainVisible: () => true,
|
||||
owner: false,
|
||||
});
|
||||
expect(access.readable.has("environment")).toBe(false);
|
||||
expect(access.notShared).toEqual(["environment"]);
|
||||
expect(access.readable.has("lifeEvents")).toBe(false);
|
||||
expect(access.notShared).toEqual(["environment", "lifeEvents"]);
|
||||
});
|
||||
|
||||
it("opens the life events to the owner", () => {
|
||||
const access = resolveDayAccessFrom({
|
||||
modules: ALL_ON,
|
||||
domainVisible: () => true,
|
||||
owner: true,
|
||||
});
|
||||
expect(access.readable.has("lifeEvents")).toBe(true);
|
||||
});
|
||||
|
||||
it("maps every section to a real sharing domain or to the owner", () => {
|
||||
@@ -82,7 +92,8 @@ describe("day access", () => {
|
||||
const domain = DAY_SECTION_SHARE_DOMAIN[section];
|
||||
if (domain !== null) expect(SHARE_DOMAINS).toContain(domain);
|
||||
}
|
||||
expect(DAY_SECTION_SHARE_DOMAIN.lifeEvents).toBe("profile");
|
||||
// Owner-only in v1.42: no share level reaches them.
|
||||
expect(DAY_SECTION_SHARE_DOMAIN.lifeEvents).toBeNull();
|
||||
});
|
||||
|
||||
it("narrows readings type by type through their own module", () => {
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
* Every section maps to the sharing domain the routes that own its rows
|
||||
* already declare (the sharing guard freezes those), so the day can never show
|
||||
* a delegate more of a section than its own list would. The environment rows
|
||||
* have no delegable route at all, so they map to no domain and are read for
|
||||
* the owner only.
|
||||
* and the life events have no delegable route at all, so they map to no
|
||||
* domain and are read for the owner only.
|
||||
*/
|
||||
import type { MeasurementType } from "@/generated/prisma/enums";
|
||||
|
||||
@@ -32,8 +32,11 @@ import type { ShareDomain } from "@/lib/sharing/scope";
|
||||
*
|
||||
* Mirrors the declarations of the routes that serve the same rows: readings,
|
||||
* sleep, workouts and check-up reminders are `measurements`; mood and the
|
||||
* screeners are `mind`; allergies, visits, vaccinations, the anamnesis facts
|
||||
* and life events are `profile`; symptoms ride `illness`.
|
||||
* screeners are `mind`; allergies, visits, vaccinations and the anamnesis
|
||||
* facts are `profile`; symptoms ride `illness`. Life events, like the
|
||||
* environment rows, are owner-only in v1.42: their routes take no delegate
|
||||
* at any level, so no share (a `profile` one, a legacy whole-record one, or
|
||||
* MANAGE) shows them here either.
|
||||
*/
|
||||
export const DAY_SECTION_SHARE_DOMAIN: Readonly<
|
||||
Record<DaySectionKey, ShareDomain | null>
|
||||
@@ -55,7 +58,7 @@ export const DAY_SECTION_SHARE_DOMAIN: Readonly<
|
||||
cycle: "cycle",
|
||||
environment: null,
|
||||
lifestyle: "profile",
|
||||
lifeEvents: "profile",
|
||||
lifeEvents: null,
|
||||
});
|
||||
|
||||
/** The module that owns a section, from the one surface map. */
|
||||
|
||||
@@ -32,15 +32,27 @@ import {
|
||||
idempotentWrite,
|
||||
recordRefusal,
|
||||
recordWriteRateLimitResponse,
|
||||
SHARING_NOT_PERMITTED_DESCRIPTION,
|
||||
stdResponses,
|
||||
} from "./shared";
|
||||
|
||||
const LIFE_EVENTS_OWNER_ONLY =
|
||||
"Owner-only in v1.42: no share reaches life events, not a `profile` share, a whole-record share or MANAGE, and not a guardian acting for a managed profile.";
|
||||
|
||||
/** The 403 of an owner-only life-event route. */
|
||||
const lifeEventRefusal = {
|
||||
"403": {
|
||||
description: `${LIFE_EVENTS_OWNER_ONLY}\n\n${SHARING_NOT_PERMITTED_DESCRIPTION}`,
|
||||
content: { "application/json": { schema: errorEnvelope } },
|
||||
},
|
||||
};
|
||||
|
||||
const TIMELINE_OFF =
|
||||
"`module.disabled` with `meta.module = timeline`: the record has the timeline switched off (it is opt-in), or the operator turned it off server-wide.";
|
||||
|
||||
const lifeEventNotFound = {
|
||||
"404": {
|
||||
description: "No such life event on this record.",
|
||||
description: "No such life event on this account.",
|
||||
content: { "application/json": { schema: errorEnvelope } },
|
||||
},
|
||||
};
|
||||
@@ -96,7 +108,7 @@ export const dayPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Records"],
|
||||
summary: "One day across the record",
|
||||
description:
|
||||
"Everything on one local calendar day: what ran through it (medications, an illness, a cycle phase, a trip), the readings in its window with the person's usual range, what happened on it, and deterministic notable observations as keys with parameters. Sections of a switched-off module are left out; sections the caller's grant does not cover are named in `sections`. Read live from the record, never from the UTC rollups. A date that is not a real calendar date answers 422.",
|
||||
"Everything on one local calendar day: what ran through it (medications, an illness, a cycle phase, a trip), the readings in its window with the person's usual range, what happened on it, and deterministic notable observations as keys with parameters. Sections of a switched-off module are left out; sections the caller's grant does not cover are named in `sections`. Life events and the environment are the owner's only: for a delegate at any level they are named `not_shared`. Read live from the record, never from the UTC rollups. A date that is not a real calendar date answers 422.",
|
||||
requestParams: { path: dayPathSchema },
|
||||
responses: {
|
||||
"200": {
|
||||
@@ -117,7 +129,7 @@ export const dayPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Records"],
|
||||
summary: "The record over the years",
|
||||
description:
|
||||
"Lanes of conditions, allergies, medications, vaccinations, visits, lab days, documents and life events, the standing items without a start, and monthly (or finer, by zoom) value series. Empty lanes are not sent; lanes of a switched-off module never are. Requires the opt-in `timeline` module.",
|
||||
"Lanes of conditions, allergies, medications, vaccinations, visits, lab days, documents and life events, the standing items without a start, and monthly (or finer, by zoom) value series. Empty lanes are not sent; lanes of a switched-off module never are. The `life` lane (life events and travel periods) is the owner's only and is never sent to a delegate. Requires the opt-in `timeline` module.",
|
||||
requestParams: { query: timelineQuerySchema },
|
||||
responses: {
|
||||
"200": {
|
||||
@@ -138,7 +150,7 @@ export const dayPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Records"],
|
||||
summary: "What the timeline can show",
|
||||
description:
|
||||
"One row per lane: carries, thin or empty, a count, a detail as a message key with parameters, and the gaps with one in-app link each. The verdict is one of two words, never a score. Requires the opt-in `timeline` module.",
|
||||
"One row per lane: carries, thin or empty, a count, a detail as a message key with parameters, and the gaps with one in-app link each. The verdict is one of two words, never a score. The `life` lane is reported to the owner only. Requires the opt-in `timeline` module.",
|
||||
responses: {
|
||||
"200": {
|
||||
description: "The readiness inventory.",
|
||||
@@ -161,7 +173,7 @@ export const dayPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Records"],
|
||||
summary: "List life events",
|
||||
description:
|
||||
"The record's life events, oldest first; soft-deleted ones are excluded. Title and note are decrypted for the caller. Part of the `profile` sharing domain. Life events are never sent to a model.",
|
||||
"The account's own life events, oldest first; soft-deleted ones are excluded. Title and note are decrypted for the caller. Owner-only: no delegate reads them at any share level. Life events are never sent to a model.",
|
||||
responses: {
|
||||
"200": {
|
||||
description: "The life events.",
|
||||
@@ -175,7 +187,7 @@ export const dayPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
...recordRefusal(),
|
||||
...lifeEventRefusal,
|
||||
},
|
||||
},
|
||||
post: {
|
||||
@@ -183,7 +195,7 @@ export const dayPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Records"],
|
||||
summary: "Add a life event",
|
||||
description:
|
||||
"A dated anchor in the person's life. `precision` says how much of the date is known and applies to both ends: at MONTH a date is the first of its month, at YEAR the first of its year. `endDate`, when given, is not before `startDate`. Title (1-120 characters) and note (up to 2000) are encrypted at rest.",
|
||||
"A dated anchor in the person's life. `precision` says how much of the date is known and applies to both ends: at MONTH a date is the first of its month, at YEAR the first of its year. `endDate`, when given, is not before `startDate`. Title (1-120 characters) and note (up to 2000) are encrypted at rest. Owner-only, like every life-event route.",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: lifeEventCreateSchema } },
|
||||
@@ -198,7 +210,7 @@ export const dayPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
...recordRefusal(),
|
||||
...lifeEventRefusal,
|
||||
...recordWriteRateLimitResponse,
|
||||
...idempotentWrite(),
|
||||
},
|
||||
@@ -226,7 +238,7 @@ export const dayPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
...lifeEventNotFound,
|
||||
...stdResponses,
|
||||
...recordRefusal(),
|
||||
...lifeEventRefusal,
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
@@ -245,7 +257,7 @@ export const dayPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
...lifeEventNotFound,
|
||||
...stdResponses,
|
||||
...recordRefusal(),
|
||||
...lifeEventRefusal,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* v1.42 — the `life` lane (life events, travel periods) is the owner's only.
|
||||
* No share level reaches it: not `profile`, not a whole-record share.
|
||||
*/
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/lib/db", () => ({ prisma: {} }));
|
||||
|
||||
import { laneVisible, type TimelineAccess } from "../lanes";
|
||||
|
||||
const modules = { timeline: true } as unknown as TimelineAccess["modules"];
|
||||
|
||||
describe("the life lane", () => {
|
||||
it("is hidden from a delegate whatever the grant covers", () => {
|
||||
const delegate: TimelineAccess = {
|
||||
modules,
|
||||
domainVisible: () => true,
|
||||
owner: false,
|
||||
};
|
||||
expect(laneVisible("life", delegate)).toBe(false);
|
||||
expect(laneVisible("allergies", delegate)).toBe(true);
|
||||
});
|
||||
|
||||
it("is shown to the owner", () => {
|
||||
expect(
|
||||
laneVisible("life", { modules, domainVisible: () => true, owner: true }),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -12,9 +12,11 @@
|
||||
*
|
||||
* Each lane belongs to one sharing domain and, where a module owns it, to
|
||||
* that module; a lane the caller may not see is not read. The `life` lane
|
||||
* mixes life events (the timeline module's own content, `profile`) with
|
||||
* travel periods (the environment module, owner only) and is filtered item
|
||||
* by item. A travel item never carries a place: the label is sealed.
|
||||
* holds life events (the timeline module's own content) and travel periods
|
||||
* (the environment module); both are owner-only in v1.42, so the lane maps
|
||||
* to no domain and no delegate sees it at any level. Travel periods are
|
||||
* filtered item by item on the environment module. A travel item never
|
||||
* carries a place: the label is sealed.
|
||||
*/
|
||||
import type { TimelineItem, TimelineLaneKey } from "@/lib/day/contract";
|
||||
import { openText } from "@/lib/day/records";
|
||||
@@ -26,11 +28,14 @@ import type { ShareDomain } from "@/lib/sharing/scope";
|
||||
import { dateOnlyKey } from "@/lib/tz/date-only";
|
||||
import { userDayKey } from "@/lib/tz/format";
|
||||
|
||||
/** The sharing domain each lane reads. */
|
||||
/**
|
||||
* The sharing domain each lane reads, or `null` for a lane only the record's
|
||||
* owner sees.
|
||||
*/
|
||||
export const TIMELINE_LANE_SHARE_DOMAIN: Readonly<
|
||||
Record<TimelineLaneKey, ShareDomain>
|
||||
Record<TimelineLaneKey, ShareDomain | null>
|
||||
> = Object.freeze({
|
||||
life: "profile",
|
||||
life: null,
|
||||
illness: "illness",
|
||||
allergies: "profile",
|
||||
medications: "medications",
|
||||
@@ -54,7 +59,8 @@ export function laneVisible(
|
||||
): boolean {
|
||||
const owner = surfaceModule(`timeline-lane:${lane}`);
|
||||
if (owner !== undefined && access.modules[owner] === false) return false;
|
||||
return access.domainVisible(TIMELINE_LANE_SHARE_DOMAIN[lane]);
|
||||
const domain = TIMELINE_LANE_SHARE_DOMAIN[lane];
|
||||
return domain === null ? access.owner : access.domainVisible(domain);
|
||||
}
|
||||
|
||||
export interface StandingItem {
|
||||
@@ -90,10 +96,12 @@ function item(
|
||||
}
|
||||
|
||||
async function lifeLane(frame: LaneFrame): Promise<LaneRead> {
|
||||
// `laneVisible` already keeps this lane from every delegate; the owner
|
||||
// test is repeated here so a caller that skips it cannot read either half.
|
||||
const travelVisible =
|
||||
frame.access.owner && frame.access.modules.environment !== false;
|
||||
const [events, trips] = await Promise.all([
|
||||
frame.access.modules.timeline === false
|
||||
!frame.access.owner || frame.access.modules.timeline === false
|
||||
? Promise.resolve([])
|
||||
: prisma.lifeEvent.findMany({
|
||||
where: { userId: frame.userId, deletedAt: null },
|
||||
|
||||
@@ -441,6 +441,7 @@ async function documentsLane(frame: Frame) {
|
||||
}
|
||||
|
||||
async function lifeLane(frame: Frame) {
|
||||
// Owner-only: `laneVisible` leaves this lane out for every delegate.
|
||||
const where = { userId: frame.userId, deletedAt: null };
|
||||
const [count, first] = await Promise.all([
|
||||
prisma.lifeEvent.count({ where }),
|
||||
|
||||
Reference in new issue
Block a user