mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(vacay): keep guest accounts out of the invite pickers
Guests are trip-scoped accounts. Trip Members excludes them from its picker and refuses them on the write path, and Collections does the same, but Vacay's two directories did not: a guest stayed selectable in a plan's Invite User dialog and in the shared-calendar picker, including after being removed from the trip it was created for. The queries are server-side, so the filter belongs there rather than in the client, which never received the flag to filter on. Both write paths get it too: the picker only hands out a list, the id comes back from the client, and the Vacay MCP tools reach the same two service methods. Fixes #2112
This commit is contained in:
@@ -639,7 +639,9 @@ export class VacayService {
|
||||
sendInvite(planId: number, inviterId: number, inviterUsername: string, inviterEmail: string, targetUserId: number): { error?: string; status?: number } {
|
||||
if (targetUserId === inviterId) return { error: 'Cannot invite yourself', status: 400 };
|
||||
|
||||
const targetUser = this.db.get('SELECT id, username FROM users WHERE id = ?', targetUserId);
|
||||
// The picker no longer offers guests, but the id arrives from the client, so the
|
||||
// write path has to refuse them too rather than trust the list it handed out.
|
||||
const targetUser = this.db.get('SELECT id, username FROM users WHERE id = ? AND COALESCE(is_guest, 0) = 0', targetUserId);
|
||||
if (!targetUser) return { error: 'User not found', status: 404 };
|
||||
|
||||
const existing = this.db.get<{ id: number; status: string }>('SELECT id, status FROM vacay_plan_members WHERE plan_id = ? AND user_id = ?', planId, targetUserId);
|
||||
@@ -785,6 +787,7 @@ export class VacayService {
|
||||
return this.db.all(`
|
||||
SELECT u.id, u.username, u.email FROM users u
|
||||
WHERE u.id != ?
|
||||
AND COALESCE(u.is_guest, 0) = 0
|
||||
AND u.id NOT IN (SELECT user_id FROM vacay_plan_members WHERE plan_id = ?)
|
||||
AND u.id NOT IN (SELECT user_id FROM vacay_plan_members WHERE status = 'accepted')
|
||||
AND u.id NOT IN (SELECT owner_id FROM vacay_plans WHERE id IN (
|
||||
@@ -877,7 +880,7 @@ export class VacayService {
|
||||
shareCalendar(ownerId: number, ownerEmail: string, targetUserId: number, socketId?: string): { error?: string; status?: number } {
|
||||
if (targetUserId === ownerId) return { error: 'Cannot share with yourself', status: 400 };
|
||||
|
||||
const targetUser = this.db.get('SELECT id FROM users WHERE id = ?', targetUserId);
|
||||
const targetUser = this.db.get('SELECT id FROM users WHERE id = ? AND COALESCE(is_guest, 0) = 0', targetUserId);
|
||||
if (!targetUser) return { error: 'User not found', status: 404 };
|
||||
|
||||
const existing = this.db.get('SELECT id FROM vacay_shares WHERE owner_id = ? AND user_id = ?', ownerId, targetUserId);
|
||||
@@ -931,6 +934,7 @@ export class VacayService {
|
||||
return this.db.all(`
|
||||
SELECT u.id, u.username FROM users u
|
||||
WHERE u.id != ?
|
||||
AND COALESCE(u.is_guest, 0) = 0
|
||||
AND u.id NOT IN (SELECT user_id FROM vacay_shares WHERE owner_id = ?)
|
||||
AND u.id NOT IN (
|
||||
SELECT owner_id FROM vacay_plans WHERE id = ?
|
||||
|
||||
@@ -779,6 +779,42 @@ describe('getAvailableUsers', () => {
|
||||
|
||||
expect(available.map(u => u.id)).not.toContain(alreadyFused.id);
|
||||
});
|
||||
|
||||
// #2112 — guests are trip-scoped accounts, and every other directory in the app
|
||||
// already leaves them out. Vacay's two pickers did not, so a guest stayed
|
||||
// selectable here even after being removed from the trip it was created for.
|
||||
it('VACAY-SVC-073: guest accounts are not offered in the plan invite picker', () => {
|
||||
const { user: owner, plan } = setupUserWithPlan();
|
||||
const { user: guest } = createUser(testDb);
|
||||
testDb.prepare('UPDATE users SET is_guest = 1 WHERE id = ?').run(guest.id);
|
||||
|
||||
const available = svc.getAvailableUsers(owner.id, plan.id) as { id: number }[];
|
||||
|
||||
expect(available.map(u => u.id)).not.toContain(guest.id);
|
||||
});
|
||||
|
||||
it('VACAY-SVC-074: guest accounts are not offered in the shared-calendar picker', () => {
|
||||
const { user: owner } = setupUserWithPlan();
|
||||
const { user: guest } = createUser(testDb);
|
||||
testDb.prepare('UPDATE users SET is_guest = 1 WHERE id = ?').run(guest.id);
|
||||
|
||||
const available = svc.getShareAvailableUsers(owner.id) as { id: number }[];
|
||||
|
||||
expect(available.map(u => u.id)).not.toContain(guest.id);
|
||||
});
|
||||
|
||||
it('VACAY-SVC-075: a guest id sent straight to the write paths is refused', () => {
|
||||
const { user: owner, plan } = setupUserWithPlan();
|
||||
const { user: guest } = createUser(testDb);
|
||||
testDb.prepare('UPDATE users SET is_guest = 1 WHERE id = ?').run(guest.id);
|
||||
|
||||
// The picker is only a list. The id comes back from the client, and the MCP
|
||||
// tools reach the same two methods, so refusing has to happen here.
|
||||
const invited = svc.sendInvite(plan.id, owner.id, 'owner', 'owner@example.test', guest.id);
|
||||
expect(invited.error).toBe('User not found');
|
||||
const shared = svc.shareCalendar(owner.id, 'owner@example.test', guest.id);
|
||||
expect(shared.error).toBe('User not found');
|
||||
});
|
||||
});
|
||||
|
||||
// ── getStats ──────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user