mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
4bfb277423
Files: two :id handlers took the file id from the URL and never resolved it against :tripId. Membership of any trip was enough to read another trip's file links (with the joined reservation titles) or to delete one. Both now resolve the file the way every sibling handler already does. Journey: a contributor with role 'viewer' - blocked from every other write in the domain - could overwrite or delete the Studio book, and could read the share token, which is an anonymous credential that outlives their contributor status. The book writes take canEdit now and the token read takes the owner check its own create and delete already use. Also: the share-link MCP tool reset every flag to permissive defaults when it was called to refresh a link, so a journey deliberately shared without its gallery or map quietly started publishing both; setShareLink dropped newest_first entirely; the public routes ignored the Journey addon switch; and photo uploads committed to storage before the access check, leaking the objects on the 403 path. Trips: DELETE answered 403 to someone with no access at all, which turns sequential ids into an existence oracle. A member who simply lacks trip_delete still gets 403.
87 lines
3.4 KiB
TypeScript
87 lines
3.4 KiB
TypeScript
/**
|
|
* canAccessTrip is the shared trip-access check every domain reads its trip row
|
|
* from. It must return the trip's `currency`: the budget settlement takes its base
|
|
* currency straight off this row, and when the column was missing from the SELECT
|
|
* it silently fell back to 'EUR', inflating balances on every non-EUR trip that had
|
|
* a foreign-currency expense (#1543).
|
|
*/
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { db, canAccessTrip } from '../../../src/db/database';
|
|
import { CAN_ACCESS_TRIP_SQL, buildDbMock, createTestDb } from '../../helpers/test-db';
|
|
|
|
function seedUser(username: string): number {
|
|
return Number(
|
|
db.prepare("INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, 'x', 'user')")
|
|
.run(username, `${username}@example.test`).lastInsertRowid,
|
|
);
|
|
}
|
|
|
|
describe('canAccessTrip', () => {
|
|
beforeEach(() => {
|
|
db.exec('DELETE FROM trip_members; DELETE FROM trips; DELETE FROM users;');
|
|
});
|
|
|
|
it('returns the trip currency for the owner (#1543)', () => {
|
|
const owner = seedUser('owner');
|
|
const tripId = Number(
|
|
db.prepare("INSERT INTO trips (user_id, title, currency) VALUES (?, 'Trip', 'RUB')")
|
|
.run(owner).lastInsertRowid,
|
|
);
|
|
|
|
expect(canAccessTrip(tripId, owner)).toMatchObject({ id: tripId, user_id: owner, currency: 'RUB' });
|
|
});
|
|
|
|
it('returns the trip currency for a member too', () => {
|
|
const owner = seedUser('owner2');
|
|
const member = seedUser('member2');
|
|
const tripId = Number(
|
|
db.prepare("INSERT INTO trips (user_id, title, currency) VALUES (?, 'Trip', 'JPY')")
|
|
.run(owner).lastInsertRowid,
|
|
);
|
|
db.prepare('INSERT INTO trip_members (trip_id, user_id) VALUES (?, ?)').run(tripId, member);
|
|
|
|
expect(canAccessTrip(tripId, member)).toMatchObject({ currency: 'JPY' });
|
|
});
|
|
|
|
it('returns undefined for a user with no access', () => {
|
|
const owner = seedUser('owner3');
|
|
const stranger = seedUser('stranger3');
|
|
const tripId = Number(
|
|
db.prepare("INSERT INTO trips (user_id, title) VALUES (?, 'Trip')").run(owner).lastInsertRowid,
|
|
);
|
|
|
|
expect(canAccessTrip(tripId, stranger)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
/**
|
|
* The mock every domain test runs against had the pre-#1543 SELECT, so the very
|
|
* regression the block above pins was invisible to the suites that mock the db.
|
|
*/
|
|
describe('the buildDbMock stand-in for canAccessTrip', () => {
|
|
it('hands back the trip currency, like the real one', () => {
|
|
const testDb = createTestDb();
|
|
try {
|
|
const owner = Number(
|
|
testDb.prepare("INSERT INTO users (username, email, password_hash, role) VALUES ('m', 'm@example.test', 'x', 'user')")
|
|
.run().lastInsertRowid,
|
|
);
|
|
const tripId = Number(
|
|
testDb.prepare("INSERT INTO trips (user_id, title, currency) VALUES (?, 'Trip', 'ISK')")
|
|
.run(owner).lastInsertRowid,
|
|
);
|
|
|
|
expect(buildDbMock(testDb).canAccessTrip(tripId, owner)).toMatchObject({ id: tripId, currency: 'ISK' });
|
|
} finally {
|
|
testDb.close();
|
|
}
|
|
});
|
|
|
|
it('selects the same columns the production statement does', () => {
|
|
const columns = (sql: string) => sql.replace(/\s+/g, ' ').trim();
|
|
expect(columns(CAN_ACCESS_TRIP_SQL)).toBe(
|
|
'SELECT t.id, t.user_id, t.currency FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)',
|
|
);
|
|
});
|
|
});
|