Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/app/api/chat/conversations/[id]/participants/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ export async function GET(request, { params } = {}) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const { id: conversationId } = await resolveRouteParams(params);
const { id: rawConversationId } = await resolveRouteParams(params);
const conversationId = typeof rawConversationId === 'string' ? rawConversationId.trim() : '';
if (!conversationId) {
return NextResponse.json({ error: 'Missing conversation ID' }, { status: 400 });
}
Expand Down
21 changes: 21 additions & 0 deletions src/app/api/chat/conversations/[id]/participants/route.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,25 @@ describe('GET /api/chat/conversations/[id]/participants', () => {
expect(body).toEqual({ error: 'Missing conversation ID' });
expect(mocks.authGetUser).toHaveBeenCalled();
});

it('rejects whitespace-only conversation IDs before participant lookup', async () => {
mocks.serviceFrom.mockImplementation((table) => {
if (table === 'users') return createUsersQuery();
if (table === 'conversation_participants') throw new Error('Participant lookup should not run');
throw new Error(`Unexpected table: ${table}`);
});

const { GET } = await import('./route.js');
const response = await GET(
new Request('https://qrypt.chat/api/chat/conversations/%20%20/participants', {
headers: { cookie: 'session=header.payload.signature' }
}),
{ params: Promise.resolve({ id: ' ' }) }
);
const body = await response.json();

expect(response.status).toBe(400);
expect(body).toEqual({ error: 'Missing conversation ID' });
expect(mocks.authGetUser).toHaveBeenCalled();
});
});
18 changes: 17 additions & 1 deletion src/app/api/crypto/public-keys/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ export async function GET(request) {
*/
export async function POST(request) {
try {
const MAX_BATCH_SIZE = 100;

// Authenticate user
const { user, error: authError } = await authenticateUser(request);
if (authError || !user) {
Expand All @@ -163,11 +165,25 @@ export async function POST(request) {
return NextResponse.json({ error: 'Missing or invalid user_ids array' }, { status: 400 });
}

if (user_ids.length > MAX_BATCH_SIZE) {
return NextResponse.json({ error: `user_ids must contain at most ${MAX_BATCH_SIZE} items` }, { status: 400 });
}

const normalizedUserIds = [...new Set(user_ids.map((userId) => {
if (typeof userId !== 'string') return null;
const normalized = userId.trim();
return normalized || null;
}))];

if (normalizedUserIds.includes(null)) {
return NextResponse.json({ error: 'user_ids must contain non-empty strings' }, { status: 400 });
}

/** @type {Record<string, string|null>} */
const publicKeys = {};

// Process each user ID
for (const userId of user_ids) {
for (const userId of normalizedUserIds) {
try {
// Get the user's auth_user_id from the internal user ID
const { data: userData, error: userError } = await getServiceRoleClient()
Expand Down
35 changes: 35 additions & 0 deletions src/app/api/crypto/public-keys/route.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,39 @@ describe('public key cookie authentication', () => {
expect(body).toEqual({ error: 'Missing public_key' });
expect(mocks.rpc).not.toHaveBeenCalled();
});

it('rejects oversized public-key batches before service-role queries', async () => {
const { POST } = await import('./route.js');
const response = await POST(
new Request('https://qrypt.chat/api/crypto/public-keys', {
method: 'POST',
headers: {
cookie: `sb-xydzwxwsbgmznthiiscl-auth-token=${cookieValue('access-token')}`,
'content-type': 'application/json'
},
body: JSON.stringify({ user_ids: Array.from({ length: 101 }, (_, index) => `user-${index}`) })
})
);

expect(response.status).toBe(400);
expect(mocks.serviceFrom).not.toHaveBeenCalled();
});

it('trims and deduplicates user IDs before lookup', async () => {
const { POST } = await import('./route.js');
const response = await POST(
new Request('https://qrypt.chat/api/crypto/public-keys', {
method: 'POST',
headers: {
cookie: `sb-xydzwxwsbgmznthiiscl-auth-token=${cookieValue('access-token')}`,
'content-type': 'application/json'
},
body: JSON.stringify({ user_ids: [' target-user-id ', 'target-user-id'] })
})
);

expect(response.status).toBe(200);
expect(await response.json()).toEqual({ public_keys: { 'target-user-id': 'public-key' } });
expect(mocks.serviceFrom).toHaveBeenCalledTimes(1);
});
});
Loading