Skip to content
Merged
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
60 changes: 45 additions & 15 deletions src/app/api/crypto/public-keys/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,27 @@ function getServiceRoleClient() {
// Create regular client for JWT validation
const supabaseClient = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY);

export const MAX_PUBLIC_KEY_BATCH_SIZE = 100;

export function normalizePublicKeyUserIds(value) {
if (!Array.isArray(value) || value.length > MAX_PUBLIC_KEY_BATCH_SIZE) {
return null;
}

const userIds = [];
const seen = new Set();
for (const candidate of value) {
if (typeof candidate !== 'string') return null;
const userId = candidate.trim();
if (!userId || userId.length > 128) return null;
if (seen.has(userId)) continue;
seen.add(userId);
userIds.push(userId);
}

return userIds;
}

/**
* Authenticate user from request cookies
* @param {Request} request
Expand Down Expand Up @@ -159,17 +180,26 @@ export async function POST(request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const { user_ids } = await request.json();

if (!user_ids || !Array.isArray(user_ids)) {
return NextResponse.json({ error: 'Missing or invalid user_ids array' }, { status: 400 });
let body;
try {
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
body = await request.json();
} catch {
return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
}

const userIds = normalizePublicKeyUserIds(body?.user_ids);
if (!userIds) {
return NextResponse.json(
{ error: `user_ids must contain at most ${MAX_PUBLIC_KEY_BATCH_SIZE} non-empty strings` },
{ status: 400 }
);
}
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

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

// Process each user ID
for (const userId of user_ids) {
for (const userId of userIds) {
try {
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
// Get the user's auth_user_id from the internal user ID
const { data: userData, error: userError } = await getServiceRoleClient()
Expand All @@ -179,8 +209,8 @@ export async function POST(request) {
.single();

if (userError || !userData?.auth_user_id) {
console.log(`🔑 No auth_user_id found for internal user ${userId}`);
publicKeys[userId] = null;
console.log('No auth_user_id found for requested public key');
publicKeys.set(userId, null);
continue;
}

Expand All @@ -192,19 +222,19 @@ export async function POST(request) {
});

if (error) {
console.error(`Error fetching public key for user ${userId}:`, error);
publicKeys[userId] = null;
console.error('Error fetching requested public key:', error);
publicKeys.set(userId, null);
} else {
publicKeys[userId] = publicKey;
publicKeys.set(userId, publicKey);
}

} catch (error) {
console.error(`Error processing user ${userId}:`, error);
publicKeys[userId] = null;
console.error('Error processing public key lookup:', error);
publicKeys.set(userId, null);
}
}

return NextResponse.json({ public_keys: publicKeys });
return NextResponse.json({ public_keys: Object.fromEntries(publicKeys) });

} catch (error) {
console.error('Error in POST /api/crypto/public-keys:', error);
Expand Down
73 changes: 73 additions & 0 deletions src/app/api/crypto/public-keys/route.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,77 @@ describe('public key cookie authentication', () => {
expect(body).toEqual({ error: 'Missing public_key' });
expect(mocks.rpc).not.toHaveBeenCalled();
});

it('rejects oversized public key batches before lookup work', 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}`)
})
})
);
const body = await response.json();

expect(response.status).toBe(400);
expect(body.error).toContain('at most 100');
expect(mocks.serviceFrom).toHaveBeenCalledTimes(1);
expect(mocks.rpc).not.toHaveBeenCalled();
});

it('rejects malformed and invalid public key batches', async () => {
const { POST } = await import('./route.js');
const headers = {
cookie: `sb-xydzwxwsbgmznthiiscl-auth-token=${cookieValue('access-token')}`,
'content-type': 'application/json'
};
const malformedResponse = await POST(
new Request('https://qrypt.chat/api/crypto/public-keys', {
method: 'POST',
headers,
body: '{"user_ids":'
})
);
const invalidResponse = await POST(
new Request('https://qrypt.chat/api/crypto/public-keys', {
method: 'POST',
headers,
body: JSON.stringify({ user_ids: ['valid-id', 42] })
})
);

expect(malformedResponse.status).toBe(400);
expect(invalidResponse.status).toBe(400);
expect(mocks.rpc).not.toHaveBeenCalled();
});

it('trims and deduplicates public key batch ids', 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: [' user-one ', 'user-one', 'user-two'] })
})
);
const body = await response.json();

expect(response.status).toBe(200);
expect(body).toEqual({
public_keys: {
'user-one': 'public-key',
'user-two': 'public-key'
}
});
expect(mocks.serviceFrom).toHaveBeenCalledTimes(3);
expect(mocks.rpc).toHaveBeenCalledTimes(2);
});
});
Loading