refactor(trip-membership): domains ask TripAccessService who may see a trip

Thirty services, controllers, MCP tools and RPC handlers outside the trips
domain injected TripsRepository for one call, findAccessible. They now
inject TripAccessService from the trip-membership leaf, in the same
constructor slot under the same field name; it answers with the same
access row (or undefined) and binds the id exactly as given, so each
caller keeps its own coercion and its own 404, 403 or tool error.
This commit is contained in:
Maurice
2026-10-10 13:11:10 +02:00
parent 734cbc2a94
commit fe35e26313
70 changed files with 219 additions and 195 deletions
@@ -26,6 +26,7 @@ import type { AddonsService } from '../../../src/nest/addons/addons.service';
import type { UsersRepository } from '../../../src/db/repositories/Users.repository';
import type { RpcRequest, RpcError } from '../../../src/nest/plugins/protocol/envelope';
import { makeDeps } from '../../helpers/rpc-host-deps';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
const req = (method: string, params: Record<string, unknown> = {}): RpcRequest => ({ k: 'req', id: 'x', method, params });
@@ -81,7 +82,7 @@ function build(addonOn: boolean) {
// SV8 — Plan 3i: DemoService.isDemoUserId, the shared demo-gate primitive.
{ isDemoUserId: async () => false } as never),
new CollectionsRpc(spyService(calls, 'collections'), guards),
new CostsRpc(spyService(calls, 'budget'), db, { broadcast: vi.fn() } as never, guards, spyService(calls, 'membership')),
new CostsRpc(spyService(calls, 'budget'), new TripAccessService(db), { broadcast: vi.fn() } as never, guards, spyService(calls, 'membership')),
]);
const granted = new Set([
'db:read:collab', 'db:write:collab', 'db:read:journal', 'db:write:journal',
@@ -22,6 +22,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { DayScheduleController } from '../../../src/nest/plugins/contributions/day-schedule.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { DaysRepository } from '../../../src/db/repositories/Days.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -32,7 +33,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) {
} as unknown as PluginHooks;
// CT1 (Plan 3j Task 5) — the day-id-set read is now DaysRepository.listIdsByTrip.
const days = { listIdsByTrip: vi.fn(async () => tripDays.value.map((d) => d.id)) } as unknown as DaysRepository;
return { c: new DayScheduleController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository, days), runtime };
return { c: new DayScheduleController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository), days), runtime };
}
const item = (over: Record<string, unknown> = {}) => ({ id: 's1', dayId: 10, label: 'Charging', ...over });
@@ -22,6 +22,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { DayTintsController } from '../../../src/nest/plugins/contributions/day-tints.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { DaysRepository } from '../../../src/db/repositories/Days.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -32,7 +33,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) {
} as unknown as PluginHooks;
// CT2 (Plan 3j Task 5) — the day-id-set read is now DaysRepository.listIdsByTrip.
const days = { listIdsByTrip: vi.fn(async () => tripDays.value.map((d) => d.id)) } as unknown as DaysRepository;
return { c: new DayTintsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository, days), runtime };
return { c: new DayTintsController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository), days), runtime };
}
const tint = (over: Record<string, unknown> = {}) => ({ dayId: 10, tone: 'success', ...over });
@@ -17,6 +17,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { MapLayersController } from '../../../src/nest/plugins/contributions/map-layers.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { TripsRepository } from '../../../src/db/repositories/Trips.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -25,7 +26,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) {
providersOf: vi.fn(() => providers),
mapLayers: vi.fn(async (id: string) => invoke(id)),
} as unknown as PluginHooks;
return { c: new MapLayersController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime };
return { c: new MapLayersController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository)), runtime };
}
const line = (n = 3, over: Record<string, unknown> = {}) => ({
type: 'polyline',
@@ -16,6 +16,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { MapMarkersController } from '../../../src/nest/plugins/contributions/map-markers.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { TripsRepository } from '../../../src/db/repositories/Trips.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -24,7 +25,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) {
providersOf: vi.fn(() => providers),
mapMarkers: vi.fn(async (id: string) => invoke(id)),
} as unknown as PluginHooks;
return { c: new MapMarkersController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime };
return { c: new MapMarkersController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository)), runtime };
}
const mk = (over: Record<string, unknown> = {}) => ({ id: 'm1', lat: 48.85, lng: 2.35, ...over });
@@ -17,6 +17,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { PdfSectionsController } from '../../../src/nest/plugins/contributions/pdf-sections.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { TripsRepository } from '../../../src/db/repositories/Trips.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -25,7 +26,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) {
providersOf: vi.fn(() => providers),
pdfSections: vi.fn(async (id: string) => invoke(id)),
} as unknown as PluginHooks;
return { c: new PdfSectionsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime };
return { c: new PdfSectionsController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository)), runtime };
}
const sec = (over: Record<string, unknown> = {}) => ({ title: 'Weather', ...over });
@@ -15,6 +15,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { PlaceDetailsController } from '../../../src/nest/plugins/contributions/place-details.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { PlacesRepository } from '../../../src/db/repositories/Places.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -26,7 +27,7 @@ function controller(over: Partial<PluginHooks> = {}) {
} as unknown as PluginHooks;
// CT7 (Plan 3j Task 5) — the place's owning trip id is now Places.repository.ts#findTripId.
const places = { findTripId: vi.fn(async (placeId: number) => placeTrip(placeId)?.trip_id) } as unknown as PlacesRepository;
return { c: new PlaceDetailsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository, places), runtime };
return { c: new PlaceDetailsController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository), places), runtime };
}
describe('PlaceDetailsController', () => {
@@ -22,6 +22,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { PluginRoutesController } from '../../../src/nest/plugins/contributions/plugin-routes.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { PluginsRepository } from '../../../src/db/repositories/Plugins.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -36,7 +37,7 @@ function controller(invoke: () => unknown, providers = ['ev-plug']) {
const plugins = {
findCapabilities: vi.fn(async () => (capabilitiesRow.value === undefined ? null : capabilitiesRow.value)),
} as unknown as PluginsRepository;
return { c: new PluginRoutesController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository, plugins), runtime };
return { c: new PluginRoutesController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository), plugins), runtime };
}
const wp = (n = 3) => Array.from({ length: n }, (_, i) => ({ lat: 48 + i * 0.1, lng: 2 + i * 0.1 }));
const goodRoute = (n = 3, over: Record<string, unknown> = {}) => ({
@@ -18,6 +18,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { TripCardContributionsController } from '../../../src/nest/plugins/contributions/trip-card-contributions.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { TripsRepository } from '../../../src/db/repositories/Trips.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -26,7 +27,7 @@ function controller(invoke: (id: string, args: unknown[]) => unknown, providers
providersOf: vi.fn(() => providers),
tripCards: vi.fn(async (id: string, _h: string, _fn: string, args: unknown[]) => invoke(id, args)),
} as unknown as PluginHooks;
return { c: new TripCardContributionsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime };
return { c: new TripCardContributionsController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository)), runtime };
}
const badge = (over: Record<string, unknown> = {}) => ({ tripId: 1, id: 'b1', label: 'Visa', ...over });
@@ -10,6 +10,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { TripWarningsController } from '../../../src/nest/plugins/contributions/trip-warnings.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { TripsRepository } from '../../../src/db/repositories/Trips.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -20,7 +21,7 @@ function controller(over: Partial<PluginHooks> = {}) {
id === 'p2' ? [{ level: 'error', message: 'Day 3 is overpacked', dayId: 3 }] : [{ level: 'warning', message: 'Museum closed Mon', placeId: 7 }]),
...over,
} as unknown as PluginHooks;
return { c: new TripWarningsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime };
return { c: new TripWarningsController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository)), runtime };
}
describe('TripWarningsController', () => {
@@ -17,6 +17,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { ViewContributionsController } from '../../../src/nest/plugins/contributions/view-contributions.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { TripsRepository } from '../../../src/db/repositories/Trips.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -25,7 +26,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) {
providersOf: vi.fn(() => providers),
tableContributions: vi.fn(async (id: string) => invoke(id)),
} as unknown as PluginHooks;
return { c: new ViewContributionsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime };
return { c: new ViewContributionsController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository)), runtime };
}
const col = (over: Record<string, unknown> = {}) => ({ kind: 'column', entityId: 1, id: 'c1', label: 'X', ...over });
const act = (over: Record<string, unknown> = {}) => ({ kind: 'action', entityId: 1, id: 'a1', label: 'Go', target: { kind: 'frame', sub: '/ui' }, ...over });