cellex-web / src /app /api /messenger /route.ts
eeshaAI
feat: Migrate frontend from static HTML to Next.js 16 App Router
a09d2a4
Raw
History Blame Contribute Delete
8.25 kB
import { NextRequest, NextResponse } from 'next/server';
const SUPABASE_URL = process.env.SUPABASE_URL || 'https://tcwdbokruvlizkxcpkzj.supabase.co';
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || '';
const SUPABASE_TOKEN = process.env.SUPABASE_TOKEN || '';
const EDGE_FUNCTIONS_URL = `${SUPABASE_URL}/functions/v1`;
const COOKIE_NAME = 'cellex_session_id';
/**
* Messenger API — E2E encrypted messaging
*
* Messages are encrypted client-side with AES-GCM (Web Crypto API).
* The server only stores encrypted_content + IV — it can NEVER read messages.
*
* Operations:
* - list: Get all conversations for the authenticated user
* - messages: Get all messages in a conversation
* - send: Send an encrypted message
* - create: Create or get an existing conversation with another user
*/
export async function POST(request: NextRequest) {
if (!SUPABASE_ANON_KEY) {
return NextResponse.json({ success: false, error: 'SUPABASE_ANON_KEY not set' }, { status: 500 });
}
const sessionId = request.cookies.get(COOKIE_NAME)?.value || '';
if (!sessionId) {
return NextResponse.json({ success: false, error: 'Not authenticated' }, { status: 401 });
}
let body: any;
try {
body = await request.json();
} catch {
return NextResponse.json({ success: false, error: 'Invalid JSON' }, { status: 400 });
}
// Step 1: Get the user ID via the auth edge function
try {
const authResp = await fetch(`${EDGE_FUNCTIONS_URL}/auth`, {
method: 'POST',
headers: {
'apikey': SUPABASE_ANON_KEY,
'Authorization': `Bearer ${sessionId}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ op: 'session' }),
});
const authData = await authResp.json();
if (!authData.success || !authData.user) {
return NextResponse.json({ success: false, error: 'Not authenticated' }, { status: 401 });
}
const userId = authData.user.id;
// Step 2: Use SQL API for all messenger operations
if (!SUPABASE_TOKEN) {
return NextResponse.json({ success: false, error: 'SUPABASE_TOKEN not set' }, { status: 500 });
}
const sqlHeaders: Record<string, string> = {
'Authorization': `Bearer ${SUPABASE_TOKEN}`,
'Content-Type': 'application/json',
};
const sqlApiUrl = `https://api.supabase.com/v1/projects/tcwdbokruvlizkxcpkzj/database/query`;
switch (body.op) {
case 'list': {
// Get all conversations where the user is a participant
const query = `
SELECT c.*,
u1.email as p1_email, u2.email as p2_email,
s.business_name as p1_name, s2.business_name as p2_name
FROM conversations c
LEFT JOIN auth.users u1 ON c.participant1 = u1.id
LEFT JOIN auth.users u2 ON c.participant2 = u2.id
LEFT JOIN sellers s ON c.participant1 = s.id
LEFT JOIN sellers s2 ON c.participant2 = s2.id
WHERE c.participant1 = '${userId}'::uuid OR c.participant2 = '${userId}'::uuid
ORDER BY c.last_message_at DESC;
`;
const resp = await fetch(sqlApiUrl, {
method: 'POST', headers: sqlHeaders,
body: JSON.stringify({ query }),
});
const data = await resp.json();
if (Array.isArray(data)) {
// Transform to include the "other participant" info
const conversations = data.map((c: any) => {
const isP1 = c.participant1 === userId;
return {
id: c.id,
type: c.type,
lastMessage: c.last_message || '',
lastMessageAt: c.last_message_at,
otherUserId: isP1 ? c.participant2 : c.participant1,
otherUserEmail: isP1 ? c.p2_email : c.p1_email,
otherUserName: isP1 ? c.p2_name || c.p2_email : c.p1_name || c.p1_email,
};
});
return NextResponse.json({ success: true, conversations });
}
return NextResponse.json({ success: true, conversations: [] });
}
case 'messages': {
const convId = body.conversationId;
if (!convId) {
return NextResponse.json({ success: false, error: 'conversationId required' }, { status: 400 });
}
const query = `
SELECT m.*, u.email as sender_email
FROM messages m
JOIN auth.users u ON m.sender_id = u.id
WHERE m.conversation_id = '${convId}'::uuid
ORDER BY m.created_at ASC
LIMIT 100;
`;
const resp = await fetch(sqlApiUrl, {
method: 'POST', headers: sqlHeaders,
body: JSON.stringify({ query }),
});
const data = await resp.json();
if (Array.isArray(data)) {
return NextResponse.json({ success: true, messages: data });
}
return NextResponse.json({ success: true, messages: [] });
}
case 'send': {
const { conversationId, encryptedContent, iv } = body;
if (!conversationId || !encryptedContent || !iv) {
return NextResponse.json({ success: false, error: 'Missing fields' }, { status: 400 });
}
const query = `
INSERT INTO messages (conversation_id, sender_id, encrypted_content, iv)
VALUES ('${conversationId}'::uuid, '${userId}'::uuid, '${encryptedContent.replace(/'/g, "''")}', '${iv}')
RETURNING *;
`;
const resp = await fetch(sqlApiUrl, {
method: 'POST', headers: sqlHeaders,
body: JSON.stringify({ query }),
});
const data = await resp.json();
// Update conversation's last_message
const updateQuery = `
UPDATE conversations SET last_message = '[Encrypted message]', last_message_at = NOW()
WHERE id = '${conversationId}'::uuid;
`;
await fetch(sqlApiUrl, {
method: 'POST', headers: sqlHeaders,
body: JSON.stringify({ query: updateQuery }),
});
if (Array.isArray(data) && data.length > 0) {
return NextResponse.json({ success: true, message: data[0] });
}
return NextResponse.json({ success: false, error: 'Send failed' }, { status: 500 });
}
case 'create': {
const { otherUserId, type, groupBuyId } = body;
if (!otherUserId) {
return NextResponse.json({ success: false, error: 'otherUserId required' }, { status: 400 });
}
// Try to find existing conversation
const findQuery = `
SELECT * FROM conversations
WHERE (participant1 = '${userId}'::uuid AND participant2 = '${otherUserId}'::uuid)
OR (participant1 = '${otherUserId}'::uuid AND participant2 = '${userId}'::uuid)
LIMIT 1;
`;
const findResp = await fetch(sqlApiUrl, {
method: 'POST', headers: sqlHeaders,
body: JSON.stringify({ query: findQuery }),
});
const findData = await findResp.json();
if (Array.isArray(findData) && findData.length > 0) {
return NextResponse.json({ success: true, conversation: findData[0] });
}
// Create new conversation
const convType = type || 'direct';
const gbId = groupBuyId ? `'${groupBuyId}'::uuid` : 'NULL';
const createQuery = `
INSERT INTO conversations (type, participant1, participant2, group_buy_id)
VALUES ('${convType}', '${userId}'::uuid, '${otherUserId}'::uuid, ${gbId})
RETURNING *;
`;
const createResp = await fetch(sqlApiUrl, {
method: 'POST', headers: sqlHeaders,
body: JSON.stringify({ query: createQuery }),
});
const createData = await createResp.json();
if (Array.isArray(createData) && createData.length > 0) {
return NextResponse.json({ success: true, conversation: createData[0] });
}
return NextResponse.json({ success: false, error: 'Create failed' }, { status: 500 });
}
default:
return NextResponse.json({ success: false, error: `Unknown op: ${body.op}` }, { status: 400 });
}
} catch (error) {
return NextResponse.json({ success: false, error: String(error) }, { status: 500 });
}
}