refactor(server): trip-access primitives async at every caller

This commit is contained in:
jubnl
2026-09-23 02:46:39 +02:00
parent 8efb3c8623
commit 769b9577ec
59 changed files with 472 additions and 140 deletions
@@ -91,7 +91,7 @@ export class DayScheduleController {
if (!pluginsEnabled()) return { items: [] };
const tripId = Number(tripIdRaw);
const userId = req.user?.id;
if (!Number.isFinite(tripId) || userId == null || !this.dbs.canAccessTrip(tripId, userId)) return { items: [] };
if (!Number.isFinite(tripId) || userId == null || !(await this.dbs.canAccessTrip(tripId, userId))) return { items: [] };
const ids = this.hooks.providersOf('dayScheduleProvider');
if (ids.length === 0) return { items: [] };
@@ -168,7 +168,7 @@ export class DayTintsController {
if (!pluginsEnabled()) return { tints: [] };
const tripId = Number(tripIdRaw);
const userId = req.user?.id;
if (!Number.isFinite(tripId) || userId == null || !this.dbs.canAccessTrip(tripId, userId)) return { tints: [] };
if (!Number.isFinite(tripId) || userId == null || !(await this.dbs.canAccessTrip(tripId, userId))) return { tints: [] };
const ids = this.hooks.providersOf('dayTintProvider');
if (ids.length === 0) return { tints: [] };
@@ -158,7 +158,7 @@ export class MapLayersController {
if (!pluginsEnabled()) return { layers: [] };
const tripId = Number(tripIdRaw);
const userId = req.user?.id;
if (!Number.isFinite(tripId) || userId == null || !this.dbs.canAccessTrip(tripId, userId)) return { layers: [] };
if (!Number.isFinite(tripId) || userId == null || !(await this.dbs.canAccessTrip(tripId, userId))) return { layers: [] };
const ids = this.hooks.providersOf('mapLayerProvider');
const perProvider = await Promise.all(
@@ -91,7 +91,7 @@ export class MapMarkersController {
if (!pluginsEnabled()) return { markers: [] };
const tripId = Number(tripIdRaw);
const userId = req.user?.id;
if (!Number.isFinite(tripId) || userId == null || !this.dbs.canAccessTrip(tripId, userId)) return { markers: [] };
if (!Number.isFinite(tripId) || userId == null || !(await this.dbs.canAccessTrip(tripId, userId))) return { markers: [] };
const ids = this.hooks.providersOf('mapMarkerProvider');
const perProvider = await Promise.all(
@@ -87,7 +87,7 @@ export class PdfSectionsController {
if (!pluginsEnabled()) return { sections: [] };
const tripId = Number(tripIdRaw);
const userId = req.user?.id;
if (!Number.isFinite(tripId) || userId == null || !this.dbs.canAccessTrip(tripId, userId)) return { sections: [] };
if (!Number.isFinite(tripId) || userId == null || !(await this.dbs.canAccessTrip(tripId, userId))) return { sections: [] };
const ids = this.hooks.providersOf('pdfSectionProvider');
const perProvider = await Promise.all(
@@ -78,7 +78,7 @@ export class PlaceDetailsController {
// The place must belong to a trip the caller can access — same gate as a read.
const row = this.dbs.connection.prepare('SELECT trip_id FROM places WHERE id = ?').get(placeId) as { trip_id: number } | undefined;
if (!row || !this.dbs.canAccessTrip(row.trip_id, userId)) return { providers: [] };
if (!row || !(await this.dbs.canAccessTrip(row.trip_id, userId))) return { providers: [] };
const ids = this.hooks.providersOf('placeDetailProvider');
const results = await Promise.all(
@@ -45,7 +45,7 @@ export class PluginRoutesController {
if (!pluginsEnabled()) return { route: null };
const userId = req.user?.id;
const tripId = Number(body?.tripId);
if (userId == null || !Number.isFinite(tripId) || !this.dbs.canAccessTrip(tripId, userId)) return { route: null };
if (userId == null || !Number.isFinite(tripId) || !(await this.dbs.canAccessTrip(tripId, userId))) return { route: null };
if (!PROFILE_RE.test(profileId)) return { route: null };
const waypoints = readWaypoints(body?.waypoints);
if (!waypoints) return { route: null };
@@ -103,7 +103,9 @@ export class TripCardContributionsController {
.map((s) => Number(s.trim()))
.filter((n) => Number.isInteger(n) && n > 0)
.slice(0, MAX_TRIP_IDS);
const accessible = [...new Set(requested)].filter((id) => this.dbs.canAccessTrip(id, userId));
const uniqueRequested = [...new Set(requested)];
const accessFlags = await Promise.all(uniqueRequested.map((id) => this.dbs.canAccessTrip(id, userId)));
const accessible = uniqueRequested.filter((_, i) => accessFlags[i]);
if (accessible.length === 0) return { contributions: [] };
const allowed = new Set(accessible);
@@ -40,7 +40,7 @@ export class TripWarningsController {
if (!pluginsEnabled()) return { warnings: [] };
const tripId = Number(tripIdRaw);
const userId = req.user?.id;
if (!Number.isFinite(tripId) || userId == null || !this.dbs.canAccessTrip(tripId, userId)) return { warnings: [] };
if (!Number.isFinite(tripId) || userId == null || !(await this.dbs.canAccessTrip(tripId, userId))) return { warnings: [] };
const ids = this.hooks.providersOf('warningProvider');
const perProvider = await Promise.all(
@@ -55,7 +55,7 @@ export class TripWarningsMcp {
async getTripWarnings({ tripId }: { tripId: number }, ctx: McpContext): Promise<McpTextResult> {
// Access first, so the answer to "may I look at this trip" does not depend on
// whether an admin has the plugin system switched on.
if (!this.dbs.canAccessTrip(tripId, ctx.userId)) return noAccess();
if (!(await this.dbs.canAccessTrip(tripId, ctx.userId))) return noAccess();
if (!pluginsEnabled()) return ok({ warnings: [] });
const ids = this.hooks.providersOf('warningProvider');
@@ -116,7 +116,7 @@ export class ViewContributionsController {
if (!pluginsEnabled() || !VIEWS.has(view)) return { contributions: [] };
const tripId = Number(tripIdRaw);
const userId = req.user?.id;
if (!Number.isFinite(tripId) || userId == null || !this.dbs.canAccessTrip(tripId, userId)) return { contributions: [] };
if (!Number.isFinite(tripId) || userId == null || !(await this.dbs.canAccessTrip(tripId, userId))) return { contributions: [] };
const ids = this.hooks.providersOf('tableContributor');
const perProvider = await Promise.all(
@@ -36,7 +36,7 @@ export class PluginGuards {
if (ctx.actingUserId === undefined) {
throw new ForbiddenResource('trip reads require an authenticated user context');
}
if (!this.db.canAccessTrip(tripId, ctx.actingUserId)) {
if (!(await this.db.canAccessTrip(tripId, ctx.actingUserId))) {
throw new ForbiddenResource(`no access to trip ${tripId}`);
}
// The read runs only for a bound, membership-checked user, so hand the id through
@@ -57,7 +57,7 @@ export class PluginGuards {
/** A write is allowed only if the acting user can access AND edit the trip. */
async requireTripEdit(tripId: number, userId: number, action: string): Promise<void> {
if (!this.db.canAccessTrip(tripId, userId)) throw new ForbiddenResource(`no access to trip ${tripId}`);
if (!(await this.db.canAccessTrip(tripId, userId))) throw new ForbiddenResource(`no access to trip ${tripId}`);
if (!(await this.canEditAs(action, tripId, userId))) throw new ForbiddenResource(`no permission to edit trip ${tripId}`);
}
@@ -67,7 +67,7 @@ export class PluginGuards {
* message the refusal carries.
*/
async canEditAs(action: string, tripId: number, userId: number): Promise<boolean> {
const trip = this.db.canAccessTrip(tripId, userId);
const trip = await this.db.canAccessTrip(tripId, userId);
if (!trip) return false;
const user = this.db.prepare('SELECT role FROM users WHERE id = ?').get(userId) as { role?: string } | undefined;
if (!user) return false;
@@ -62,7 +62,7 @@ export class HostSurfaceRpc {
// Namespacing the event type alone does not cross the membership boundary.
const tripId = num(params.tripId, 'tripId');
if (ctx.actingUserId === undefined) throw new ForbiddenResource('broadcasts require an authenticated user context');
if (!this.db.canAccessTrip(tripId, ctx.actingUserId)) throw new ForbiddenResource(`no access to trip ${tripId}`);
if (!(await this.db.canAccessTrip(tripId, ctx.actingUserId))) throw new ForbiddenResource(`no access to trip ${tripId}`);
// The host forces the plugin:{id}:{event} namespace, so a plugin cannot forge a
// core event.
this.realtime.broadcast(tripId, `plugin:${ctx.pluginId}:${str(params.event, 'event')}`, asPayload(params.data));
@@ -101,7 +101,7 @@ export class HostSurfaceRpc {
if (scope !== 'user' && scope !== 'trip') throw new BadParams("scope must be 'user' or 'trip'");
const targetId = num(input.targetId, 'targetId');
if (scope === 'user' && targetId !== actor) throw new ForbiddenResource('a plugin may only notify the acting user');
if (scope === 'trip' && !this.db.canAccessTrip(targetId, actor)) {
if (scope === 'trip' && !(await this.db.canAccessTrip(targetId, actor))) {
throw new ForbiddenResource('the acting user is not a member of that trip');
}
const link = this.safeLink(input.link);
+1 -1
View File
@@ -130,7 +130,7 @@ export class MetaRpc {
const entityId = num(params.entityId, 'entityId');
if (ctx.actingUserId === undefined) throw new ForbiddenResource('metadata requires an authenticated user context');
const tripId = await this.entityTrip(entityType, entityId);
if (tripId === undefined || !this.db.canAccessTrip(tripId, ctx.actingUserId)) {
if (tripId === undefined || !(await this.db.canAccessTrip(tripId, ctx.actingUserId))) {
throw new ForbiddenResource(`no access to ${entityType} ${entityId}`);
}
if (write && !(await this.guards.canEditAs(EDIT_ACTION[entityType], tripId, ctx.actingUserId))) {