Spaces:
Sleeping
Sleeping
File size: 8,254 Bytes
bea55e2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | 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 });
}
}
|