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 });
  }
}