hazo_chat 5.2.4 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/messages.d.ts.map +1 -1
- package/dist/api/messages.js +438 -437
- package/dist/api/messages.js.map +1 -1
- package/dist/api/types.d.ts +3 -1
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/unread_count.d.ts.map +1 -1
- package/dist/api/unread_count.js +87 -84
- package/dist/api/unread_count.js.map +1 -1
- package/dist/components/hazo_chat/hazo_chat.js +1 -1
- package/dist/components/hazo_chat/hazo_chat.js.map +1 -1
- package/dist/components/hazo_chat/hazo_chat_attachment_preview.js +1 -1
- package/dist/components/hazo_chat/hazo_chat_attachment_preview.js.map +1 -1
- package/dist/components/hazo_chat/hazo_chat_document_viewer.js +1 -1
- package/dist/components/hazo_chat/hazo_chat_document_viewer.js.map +1 -1
- package/dist/components/hazo_chat/hazo_chat_header.js +1 -1
- package/dist/components/hazo_chat/hazo_chat_header.js.map +1 -1
- package/dist/components/hazo_chat/hazo_chat_input.js +1 -1
- package/dist/components/hazo_chat/hazo_chat_input.js.map +1 -1
- package/dist/components/hazo_chat/hazo_chat_messages.js +1 -1
- package/dist/components/hazo_chat/hazo_chat_messages.js.map +1 -1
- package/dist/components/hazo_chat/hazo_chat_reference_list.js +1 -1
- package/dist/components/hazo_chat/hazo_chat_reference_list.js.map +1 -1
- package/dist/components/hazo_chat/hazo_chat_sidebar.js +1 -1
- package/dist/components/hazo_chat/hazo_chat_sidebar.js.map +1 -1
- package/dist/components/ui/avatar.js +1 -1
- package/dist/components/ui/avatar.js.map +1 -1
- package/dist/components/ui/badge.js +1 -1
- package/dist/components/ui/badge.js.map +1 -1
- package/dist/components/ui/button.js +1 -1
- package/dist/components/ui/button.js.map +1 -1
- package/dist/components/ui/chat_bubble.js +1 -1
- package/dist/components/ui/chat_bubble.js.map +1 -1
- package/dist/components/ui/hover-card.js +1 -1
- package/dist/components/ui/hover-card.js.map +1 -1
- package/dist/components/ui/input.js +1 -1
- package/dist/components/ui/input.js.map +1 -1
- package/dist/components/ui/loading_skeleton.js +1 -1
- package/dist/components/ui/loading_skeleton.js.map +1 -1
- package/dist/components/ui/scroll-area.js +1 -1
- package/dist/components/ui/scroll-area.js.map +1 -1
- package/dist/components/ui/separator.js +1 -1
- package/dist/components/ui/separator.js.map +1 -1
- package/dist/components/ui/skeleton.js +1 -1
- package/dist/components/ui/skeleton.js.map +1 -1
- package/dist/components/ui/textarea.js +1 -1
- package/dist/components/ui/textarea.js.map +1 -1
- package/dist/components/ui/tooltip.js +1 -1
- package/dist/components/ui/tooltip.js.map +1 -1
- package/dist/lib/config.d.ts +24 -40
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +50 -89
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/utils.d.ts +0 -11
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +0 -14
- package/dist/lib/utils.js.map +1 -1
- package/package.json +15 -6
package/dist/api/messages.js
CHANGED
|
@@ -24,6 +24,7 @@ import { cookies } from 'next/headers';
|
|
|
24
24
|
import { createCrudService } from 'hazo_connect/server';
|
|
25
25
|
import { is_valid_uuid } from './validation.js';
|
|
26
26
|
import { serialize_error } from '../lib/utils.js';
|
|
27
|
+
import { generateRequestId, withContext } from 'hazo_core';
|
|
27
28
|
// ============================================================================
|
|
28
29
|
// Constants for validation
|
|
29
30
|
// ============================================================================
|
|
@@ -40,14 +41,6 @@ const MAX_LIMIT = 100;
|
|
|
40
41
|
// ============================================================================
|
|
41
42
|
// Helper Functions
|
|
42
43
|
// ============================================================================
|
|
43
|
-
/** Generate a UUID v4 */
|
|
44
|
-
function generateUUID() {
|
|
45
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
46
|
-
const r = (Math.random() * 16) | 0;
|
|
47
|
-
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
48
|
-
return v.toString(16);
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
44
|
/** Create a standardized error response */
|
|
52
45
|
function createErrorResponse(error, status, error_code) {
|
|
53
46
|
return NextResponse.json({
|
|
@@ -108,145 +101,147 @@ export function createMessagesHandler(options) {
|
|
|
108
101
|
* - direction (optional): 'older' or 'newer' relative to cursor (default: 'older')
|
|
109
102
|
*/
|
|
110
103
|
async function GET(request) {
|
|
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
|
-
|
|
104
|
+
return withContext({}, async () => {
|
|
105
|
+
try {
|
|
106
|
+
// Get current user ID
|
|
107
|
+
const current_user_id = getUserIdFromRequest
|
|
108
|
+
? await getUserIdFromRequest(request)
|
|
109
|
+
: await defaultGetUserIdFromRequest();
|
|
110
|
+
if (!current_user_id) {
|
|
111
|
+
logger.error('[hazo_chat/messages GET] No user ID - not authenticated');
|
|
112
|
+
return createErrorResponse('User not authenticated', 401, 'UNAUTHENTICATED');
|
|
113
|
+
}
|
|
114
|
+
// Get query params
|
|
115
|
+
const { searchParams } = new URL(request.url);
|
|
116
|
+
const chat_group_id = searchParams.get('chat_group_id');
|
|
117
|
+
const reference_id = searchParams.get('reference_id') || '';
|
|
118
|
+
const reference_type = searchParams.get('reference_type') || '';
|
|
119
|
+
const cursor = searchParams.get('cursor') || '';
|
|
120
|
+
const direction = searchParams.get('direction') || 'older';
|
|
121
|
+
const limit_param = searchParams.get('limit');
|
|
122
|
+
// Validate required params
|
|
123
|
+
if (!chat_group_id) {
|
|
124
|
+
logger.error('[hazo_chat/messages GET] Missing chat_group_id');
|
|
125
|
+
return createErrorResponse('chat_group_id is required', 400, 'MISSING_CHAT_GROUP');
|
|
126
|
+
}
|
|
127
|
+
// Validate UUID formats before database queries
|
|
128
|
+
if (!is_valid_uuid(chat_group_id)) {
|
|
129
|
+
logger.debug('[hazo_chat/messages GET] Invalid chat_group_id format:', { chat_group_id });
|
|
130
|
+
return createErrorResponse('chat_group_id must be a valid UUID', 400, 'INVALID_UUID_FORMAT');
|
|
131
|
+
}
|
|
132
|
+
if (!is_valid_uuid(current_user_id)) {
|
|
133
|
+
logger.debug('[hazo_chat/messages GET] Invalid user_id format:', { current_user_id });
|
|
134
|
+
return createErrorResponse('Invalid user ID format', 400, 'INVALID_UUID_FORMAT');
|
|
135
|
+
}
|
|
136
|
+
// Get hazo_connect instance early for membership check
|
|
137
|
+
const hazoConnect = getHazoConnect();
|
|
138
|
+
// Verify user is a member of the chat group
|
|
139
|
+
const membership = await verifyGroupMembership(hazoConnect, current_user_id, chat_group_id, logger);
|
|
140
|
+
if (!membership) {
|
|
141
|
+
logger.warn('[hazo_chat/messages GET] User is not a member of chat group:', {
|
|
142
|
+
current_user_id,
|
|
143
|
+
chat_group_id,
|
|
144
|
+
});
|
|
145
|
+
return createErrorResponse('Access denied - not a member of this chat group', 403, 'FORBIDDEN');
|
|
146
|
+
}
|
|
147
|
+
// Validate input lengths
|
|
148
|
+
if (reference_id && reference_id.length > MAX_REFERENCE_ID_LENGTH) {
|
|
149
|
+
return createErrorResponse(`reference_id exceeds maximum length of ${MAX_REFERENCE_ID_LENGTH}`, 400, 'INVALID_REFERENCE_ID');
|
|
150
|
+
}
|
|
151
|
+
if (reference_type && reference_type.length > MAX_REFERENCE_TYPE_LENGTH) {
|
|
152
|
+
return createErrorResponse(`reference_type exceeds maximum length of ${MAX_REFERENCE_TYPE_LENGTH}`, 400, 'INVALID_REFERENCE_TYPE');
|
|
153
|
+
}
|
|
154
|
+
// Parse and validate limit
|
|
155
|
+
let limit = DEFAULT_LIMIT;
|
|
156
|
+
if (limit_param) {
|
|
157
|
+
const parsed_limit = parseInt(limit_param, 10);
|
|
158
|
+
if (isNaN(parsed_limit) || parsed_limit < 1) {
|
|
159
|
+
return createErrorResponse('limit must be a positive integer', 400, 'INVALID_LIMIT');
|
|
160
|
+
}
|
|
161
|
+
limit = Math.min(parsed_limit, MAX_LIMIT);
|
|
162
|
+
}
|
|
163
|
+
logger.debug('[hazo_chat/messages GET] Fetching messages:', {
|
|
148
164
|
current_user_id,
|
|
149
165
|
chat_group_id,
|
|
166
|
+
reference_id,
|
|
167
|
+
reference_type,
|
|
168
|
+
cursor,
|
|
169
|
+
direction,
|
|
170
|
+
limit,
|
|
150
171
|
});
|
|
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
|
-
const all_messages = await chatService.list((qb) => {
|
|
184
|
-
let builder = qb.select('*');
|
|
185
|
-
// Filter by chat group - this is the primary filter
|
|
186
|
-
builder = builder.where('chat_group_id', 'eq', chat_group_id);
|
|
187
|
-
// Exclude soft-deleted messages at the database level
|
|
188
|
-
builder = builder.where('deleted_at', 'is', null);
|
|
189
|
-
// Filter by reference if provided
|
|
190
|
-
if (reference_id) {
|
|
191
|
-
builder = builder.where('reference_id', 'eq', reference_id);
|
|
192
|
-
}
|
|
193
|
-
if (reference_type) {
|
|
194
|
-
builder = builder.where('reference_type', 'eq', reference_type);
|
|
195
|
-
}
|
|
196
|
-
// Apply cursor-based pagination
|
|
197
|
-
if (cursor) {
|
|
198
|
-
if (direction === 'newer') {
|
|
199
|
-
builder = builder.where('created_at', 'gt', cursor);
|
|
172
|
+
// Create CRUD service (hazoConnect already obtained above)
|
|
173
|
+
const chatService = createCrudService(hazoConnect, 'hazo_chat');
|
|
174
|
+
let messages = [];
|
|
175
|
+
try {
|
|
176
|
+
// Build query with proper filtering
|
|
177
|
+
const all_messages = await chatService.list((qb) => {
|
|
178
|
+
let builder = qb.select('*');
|
|
179
|
+
// Filter by chat group - this is the primary filter
|
|
180
|
+
builder = builder.where('chat_group_id', 'eq', chat_group_id);
|
|
181
|
+
// Exclude soft-deleted messages at the database level
|
|
182
|
+
builder = builder.where('deleted_at', 'is', null);
|
|
183
|
+
// Filter by reference if provided
|
|
184
|
+
if (reference_id) {
|
|
185
|
+
builder = builder.where('reference_id', 'eq', reference_id);
|
|
186
|
+
}
|
|
187
|
+
if (reference_type) {
|
|
188
|
+
builder = builder.where('reference_type', 'eq', reference_type);
|
|
189
|
+
}
|
|
190
|
+
// Apply cursor-based pagination
|
|
191
|
+
if (cursor) {
|
|
192
|
+
if (direction === 'newer') {
|
|
193
|
+
builder = builder.where('created_at', 'gt', cursor);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
builder = builder.where('created_at', 'lt', cursor);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Order by created_at
|
|
200
|
+
// For 'older' direction, we want desc to get the most recent first before cursor
|
|
201
|
+
// For 'newer' or initial load, we want asc
|
|
202
|
+
if (direction === 'older' && cursor) {
|
|
203
|
+
builder = builder.order('created_at', 'desc');
|
|
200
204
|
}
|
|
201
205
|
else {
|
|
202
|
-
builder = builder.
|
|
206
|
+
builder = builder.order('created_at', 'asc');
|
|
203
207
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
+
return builder;
|
|
209
|
+
});
|
|
210
|
+
// Apply limit (deleted messages already filtered at DB level)
|
|
211
|
+
messages = all_messages.slice(0, limit);
|
|
212
|
+
// If we fetched in desc order, reverse to return in asc order
|
|
208
213
|
if (direction === 'older' && cursor) {
|
|
209
|
-
|
|
214
|
+
messages.reverse();
|
|
210
215
|
}
|
|
211
|
-
else {
|
|
212
|
-
builder = builder.order('created_at', 'asc');
|
|
213
|
-
}
|
|
214
|
-
return builder;
|
|
215
|
-
});
|
|
216
|
-
// Apply limit (deleted messages already filtered at DB level)
|
|
217
|
-
messages = all_messages.slice(0, limit);
|
|
218
|
-
// If we fetched in desc order, reverse to return in asc order
|
|
219
|
-
if (direction === 'older' && cursor) {
|
|
220
|
-
messages.reverse();
|
|
221
216
|
}
|
|
217
|
+
catch (dbError) {
|
|
218
|
+
logger.error('[hazo_chat/messages GET] Database error:', { dbError: serialize_error(dbError) });
|
|
219
|
+
throw dbError;
|
|
220
|
+
}
|
|
221
|
+
logger.debug('[hazo_chat/messages GET] Found messages:', { count: messages.length });
|
|
222
|
+
// Determine if there are more messages
|
|
223
|
+
const has_more = messages.length === limit;
|
|
224
|
+
// Get cursors for next/prev page
|
|
225
|
+
const next_cursor = messages.length > 0 ? messages[messages.length - 1].created_at : null;
|
|
226
|
+
const prev_cursor = messages.length > 0 ? messages[0].created_at : null;
|
|
227
|
+
return NextResponse.json({
|
|
228
|
+
success: true,
|
|
229
|
+
messages,
|
|
230
|
+
current_user_id,
|
|
231
|
+
pagination: {
|
|
232
|
+
limit,
|
|
233
|
+
has_more,
|
|
234
|
+
next_cursor,
|
|
235
|
+
prev_cursor,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
222
238
|
}
|
|
223
|
-
catch (
|
|
224
|
-
|
|
225
|
-
|
|
239
|
+
catch (error) {
|
|
240
|
+
const error_message = error instanceof Error ? error.message : 'Unknown error';
|
|
241
|
+
logger.error('[hazo_chat/messages GET] Error:', { error_message, error: serialize_error(error) });
|
|
242
|
+
return createErrorResponse('Failed to fetch messages', 500, 'INTERNAL_ERROR');
|
|
226
243
|
}
|
|
227
|
-
|
|
228
|
-
// Determine if there are more messages
|
|
229
|
-
const has_more = messages.length === limit;
|
|
230
|
-
// Get cursors for next/prev page
|
|
231
|
-
const next_cursor = messages.length > 0 ? messages[messages.length - 1].created_at : null;
|
|
232
|
-
const prev_cursor = messages.length > 0 ? messages[0].created_at : null;
|
|
233
|
-
return NextResponse.json({
|
|
234
|
-
success: true,
|
|
235
|
-
messages,
|
|
236
|
-
current_user_id,
|
|
237
|
-
pagination: {
|
|
238
|
-
limit,
|
|
239
|
-
has_more,
|
|
240
|
-
next_cursor,
|
|
241
|
-
prev_cursor,
|
|
242
|
-
},
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
catch (error) {
|
|
246
|
-
const error_message = error instanceof Error ? error.message : 'Unknown error';
|
|
247
|
-
logger.error('[hazo_chat/messages GET] Error:', { error_message, error: serialize_error(error) });
|
|
248
|
-
return createErrorResponse('Failed to fetch messages', 500, 'INTERNAL_ERROR');
|
|
249
|
-
}
|
|
244
|
+
}); // end withContext
|
|
250
245
|
}
|
|
251
246
|
/**
|
|
252
247
|
* POST handler - Create a new chat message
|
|
@@ -258,123 +253,125 @@ export function createMessagesHandler(options) {
|
|
|
258
253
|
* - reference_type (optional): Reference type (default: 'chat')
|
|
259
254
|
*/
|
|
260
255
|
async function POST(request) {
|
|
261
|
-
|
|
262
|
-
// Get current user ID (sender)
|
|
263
|
-
const sender_user_id = getUserIdFromRequest
|
|
264
|
-
? await getUserIdFromRequest(request)
|
|
265
|
-
: await defaultGetUserIdFromRequest();
|
|
266
|
-
if (!sender_user_id) {
|
|
267
|
-
logger.error('[hazo_chat/messages POST] No user ID - not authenticated');
|
|
268
|
-
return createErrorResponse('User not authenticated', 401, 'UNAUTHENTICATED');
|
|
269
|
-
}
|
|
270
|
-
// Parse request body
|
|
271
|
-
const body = await request.json();
|
|
272
|
-
const { chat_group_id, message_text, reference_id, reference_type } = body;
|
|
273
|
-
// Validate required fields
|
|
274
|
-
if (!chat_group_id) {
|
|
275
|
-
logger.error('[hazo_chat/messages POST] Missing chat_group_id');
|
|
276
|
-
return createErrorResponse('chat_group_id is required', 400, 'MISSING_CHAT_GROUP');
|
|
277
|
-
}
|
|
278
|
-
// Validate UUID formats before database queries
|
|
279
|
-
if (!is_valid_uuid(chat_group_id)) {
|
|
280
|
-
logger.debug('[hazo_chat/messages POST] Invalid chat_group_id format:', { chat_group_id });
|
|
281
|
-
return createErrorResponse('chat_group_id must be a valid UUID', 400, 'INVALID_UUID_FORMAT');
|
|
282
|
-
}
|
|
283
|
-
if (!is_valid_uuid(sender_user_id)) {
|
|
284
|
-
logger.debug('[hazo_chat/messages POST] Invalid user_id format:', { sender_user_id });
|
|
285
|
-
return createErrorResponse('Invalid user ID format', 400, 'INVALID_UUID_FORMAT');
|
|
286
|
-
}
|
|
287
|
-
// Get hazo_connect instance early for membership check
|
|
288
|
-
const hazoConnect = getHazoConnect();
|
|
289
|
-
// Verify user is a member of the chat group
|
|
290
|
-
const membership = await verifyGroupMembership(hazoConnect, sender_user_id, chat_group_id, logger);
|
|
291
|
-
if (!membership) {
|
|
292
|
-
logger.warn('[hazo_chat/messages POST] User is not a member of chat group:', {
|
|
293
|
-
sender_user_id,
|
|
294
|
-
chat_group_id,
|
|
295
|
-
});
|
|
296
|
-
return createErrorResponse('Access denied - not a member of this chat group', 403, 'FORBIDDEN');
|
|
297
|
-
}
|
|
298
|
-
// Validate message_text
|
|
299
|
-
if (!message_text || typeof message_text !== 'string') {
|
|
300
|
-
logger.error('[hazo_chat/messages POST] Missing message_text');
|
|
301
|
-
return createErrorResponse('message_text is required', 400, 'MISSING_MESSAGE');
|
|
302
|
-
}
|
|
303
|
-
const trimmed_message = message_text.trim();
|
|
304
|
-
if (trimmed_message === '') {
|
|
305
|
-
logger.error('[hazo_chat/messages POST] Empty message_text');
|
|
306
|
-
return createErrorResponse('message_text cannot be empty or whitespace-only', 400, 'EMPTY_MESSAGE');
|
|
307
|
-
}
|
|
308
|
-
if (trimmed_message.length > MAX_MESSAGE_LENGTH) {
|
|
309
|
-
logger.error('[hazo_chat/messages POST] Message too long:', { length: trimmed_message.length });
|
|
310
|
-
return createErrorResponse(`Message exceeds maximum length of ${MAX_MESSAGE_LENGTH} characters`, 400, 'MESSAGE_TOO_LONG');
|
|
311
|
-
}
|
|
312
|
-
// Validate reference_id length
|
|
313
|
-
if (reference_id && reference_id.length > MAX_REFERENCE_ID_LENGTH) {
|
|
314
|
-
return createErrorResponse(`reference_id exceeds maximum length of ${MAX_REFERENCE_ID_LENGTH}`, 400, 'INVALID_REFERENCE_ID');
|
|
315
|
-
}
|
|
316
|
-
// Validate reference_type length
|
|
317
|
-
if (reference_type && reference_type.length > MAX_REFERENCE_TYPE_LENGTH) {
|
|
318
|
-
return createErrorResponse(`reference_type exceeds maximum length of ${MAX_REFERENCE_TYPE_LENGTH}`, 400, 'INVALID_REFERENCE_TYPE');
|
|
319
|
-
}
|
|
320
|
-
// Create CRUD service (hazoConnect already obtained above)
|
|
321
|
-
const chatService = createCrudService(hazoConnect, 'hazo_chat');
|
|
322
|
-
// Generate message ID and timestamps
|
|
323
|
-
const message_id = generateUUID();
|
|
324
|
-
const now = new Date().toISOString();
|
|
325
|
-
// Create message record
|
|
326
|
-
const message_record = {
|
|
327
|
-
id: message_id,
|
|
328
|
-
reference_id: reference_id || '',
|
|
329
|
-
reference_type: reference_type || 'chat',
|
|
330
|
-
sender_user_id,
|
|
331
|
-
chat_group_id,
|
|
332
|
-
message_text: trimmed_message,
|
|
333
|
-
reference_list: null,
|
|
334
|
-
read_at: null,
|
|
335
|
-
deleted_at: null,
|
|
336
|
-
created_at: now,
|
|
337
|
-
changed_at: now,
|
|
338
|
-
};
|
|
339
|
-
logger.info('[hazo_chat/messages POST] Saving message:', {
|
|
340
|
-
id: message_id,
|
|
341
|
-
sender_user_id,
|
|
342
|
-
chat_group_id,
|
|
343
|
-
reference_id: reference_id || '',
|
|
344
|
-
reference_type: reference_type || 'chat',
|
|
345
|
-
message_length: trimmed_message.length,
|
|
346
|
-
});
|
|
347
|
-
// Save to database
|
|
256
|
+
return withContext({}, async () => {
|
|
348
257
|
try {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
258
|
+
// Get current user ID (sender)
|
|
259
|
+
const sender_user_id = getUserIdFromRequest
|
|
260
|
+
? await getUserIdFromRequest(request)
|
|
261
|
+
: await defaultGetUserIdFromRequest();
|
|
262
|
+
if (!sender_user_id) {
|
|
263
|
+
logger.error('[hazo_chat/messages POST] No user ID - not authenticated');
|
|
264
|
+
return createErrorResponse('User not authenticated', 401, 'UNAUTHENTICATED');
|
|
265
|
+
}
|
|
266
|
+
// Parse request body
|
|
267
|
+
const body = await request.json();
|
|
268
|
+
const { chat_group_id, message_text, reference_id, reference_type } = body;
|
|
269
|
+
// Validate required fields
|
|
270
|
+
if (!chat_group_id) {
|
|
271
|
+
logger.error('[hazo_chat/messages POST] Missing chat_group_id');
|
|
272
|
+
return createErrorResponse('chat_group_id is required', 400, 'MISSING_CHAT_GROUP');
|
|
273
|
+
}
|
|
274
|
+
// Validate UUID formats before database queries
|
|
275
|
+
if (!is_valid_uuid(chat_group_id)) {
|
|
276
|
+
logger.debug('[hazo_chat/messages POST] Invalid chat_group_id format:', { chat_group_id });
|
|
277
|
+
return createErrorResponse('chat_group_id must be a valid UUID', 400, 'INVALID_UUID_FORMAT');
|
|
278
|
+
}
|
|
279
|
+
if (!is_valid_uuid(sender_user_id)) {
|
|
280
|
+
logger.debug('[hazo_chat/messages POST] Invalid user_id format:', { sender_user_id });
|
|
281
|
+
return createErrorResponse('Invalid user ID format', 400, 'INVALID_UUID_FORMAT');
|
|
282
|
+
}
|
|
283
|
+
// Get hazo_connect instance early for membership check
|
|
284
|
+
const hazoConnect = getHazoConnect();
|
|
285
|
+
// Verify user is a member of the chat group
|
|
286
|
+
const membership = await verifyGroupMembership(hazoConnect, sender_user_id, chat_group_id, logger);
|
|
287
|
+
if (!membership) {
|
|
288
|
+
logger.warn('[hazo_chat/messages POST] User is not a member of chat group:', {
|
|
289
|
+
sender_user_id,
|
|
290
|
+
chat_group_id,
|
|
291
|
+
});
|
|
292
|
+
return createErrorResponse('Access denied - not a member of this chat group', 403, 'FORBIDDEN');
|
|
293
|
+
}
|
|
294
|
+
// Validate message_text
|
|
295
|
+
if (!message_text || typeof message_text !== 'string') {
|
|
296
|
+
logger.error('[hazo_chat/messages POST] Missing message_text');
|
|
297
|
+
return createErrorResponse('message_text is required', 400, 'MISSING_MESSAGE');
|
|
298
|
+
}
|
|
299
|
+
const trimmed_message = message_text.trim();
|
|
300
|
+
if (trimmed_message === '') {
|
|
301
|
+
logger.error('[hazo_chat/messages POST] Empty message_text');
|
|
302
|
+
return createErrorResponse('message_text cannot be empty or whitespace-only', 400, 'EMPTY_MESSAGE');
|
|
303
|
+
}
|
|
304
|
+
if (trimmed_message.length > MAX_MESSAGE_LENGTH) {
|
|
305
|
+
logger.error('[hazo_chat/messages POST] Message too long:', { length: trimmed_message.length });
|
|
306
|
+
return createErrorResponse(`Message exceeds maximum length of ${MAX_MESSAGE_LENGTH} characters`, 400, 'MESSAGE_TOO_LONG');
|
|
307
|
+
}
|
|
308
|
+
// Validate reference_id length
|
|
309
|
+
if (reference_id && reference_id.length > MAX_REFERENCE_ID_LENGTH) {
|
|
310
|
+
return createErrorResponse(`reference_id exceeds maximum length of ${MAX_REFERENCE_ID_LENGTH}`, 400, 'INVALID_REFERENCE_ID');
|
|
311
|
+
}
|
|
312
|
+
// Validate reference_type length
|
|
313
|
+
if (reference_type && reference_type.length > MAX_REFERENCE_TYPE_LENGTH) {
|
|
314
|
+
return createErrorResponse(`reference_type exceeds maximum length of ${MAX_REFERENCE_TYPE_LENGTH}`, 400, 'INVALID_REFERENCE_TYPE');
|
|
315
|
+
}
|
|
316
|
+
// Create CRUD service (hazoConnect already obtained above)
|
|
317
|
+
const chatService = createCrudService(hazoConnect, 'hazo_chat');
|
|
318
|
+
// Generate message ID and timestamps
|
|
319
|
+
const message_id = generateRequestId().slice(4); // strips 'req_' prefix for UUID-like storage
|
|
320
|
+
const now = new Date().toISOString();
|
|
321
|
+
// Create message record
|
|
322
|
+
const message_record = {
|
|
359
323
|
id: message_id,
|
|
360
|
-
sender_user_id,
|
|
361
|
-
chat_group_id,
|
|
362
324
|
reference_id: reference_id || '',
|
|
363
325
|
reference_type: reference_type || 'chat',
|
|
326
|
+
sender_user_id,
|
|
327
|
+
chat_group_id,
|
|
364
328
|
message_text: trimmed_message,
|
|
365
329
|
reference_list: null,
|
|
366
330
|
read_at: null,
|
|
367
331
|
deleted_at: null,
|
|
368
332
|
created_at: now,
|
|
369
333
|
changed_at: now,
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
334
|
+
};
|
|
335
|
+
logger.info('[hazo_chat/messages POST] Saving message:', {
|
|
336
|
+
id: message_id,
|
|
337
|
+
sender_user_id,
|
|
338
|
+
chat_group_id,
|
|
339
|
+
reference_id: reference_id || '',
|
|
340
|
+
reference_type: reference_type || 'chat',
|
|
341
|
+
message_length: trimmed_message.length,
|
|
342
|
+
});
|
|
343
|
+
// Save to database
|
|
344
|
+
try {
|
|
345
|
+
await chatService.insert(message_record);
|
|
346
|
+
}
|
|
347
|
+
catch (dbError) {
|
|
348
|
+
logger.error('[hazo_chat/messages POST] Database error:', { dbError: serialize_error(dbError) });
|
|
349
|
+
throw dbError;
|
|
350
|
+
}
|
|
351
|
+
logger.info('[hazo_chat/messages POST] Message saved successfully:', { message_id });
|
|
352
|
+
return NextResponse.json({
|
|
353
|
+
success: true,
|
|
354
|
+
message: {
|
|
355
|
+
id: message_id,
|
|
356
|
+
sender_user_id,
|
|
357
|
+
chat_group_id,
|
|
358
|
+
reference_id: reference_id || '',
|
|
359
|
+
reference_type: reference_type || 'chat',
|
|
360
|
+
message_text: trimmed_message,
|
|
361
|
+
reference_list: null,
|
|
362
|
+
read_at: null,
|
|
363
|
+
deleted_at: null,
|
|
364
|
+
created_at: now,
|
|
365
|
+
changed_at: now,
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
const error_message = error instanceof Error ? error.message : 'Unknown error';
|
|
371
|
+
logger.error('[hazo_chat/messages POST] Error:', { error_message, error: serialize_error(error) });
|
|
372
|
+
return createErrorResponse('Failed to save message', 500, 'INTERNAL_ERROR');
|
|
373
|
+
}
|
|
374
|
+
}); // end withContext
|
|
378
375
|
}
|
|
379
376
|
return { GET, POST };
|
|
380
377
|
}
|
|
@@ -399,114 +396,116 @@ export function createMarkAsReadHandler(options) {
|
|
|
399
396
|
* Note: In Next.js 13+ App Router, params may be a Promise
|
|
400
397
|
*/
|
|
401
398
|
async function PATCH(request, context) {
|
|
402
|
-
|
|
403
|
-
// Get current user ID
|
|
404
|
-
const current_user_id = getUserIdFromRequest
|
|
405
|
-
? await getUserIdFromRequest(request)
|
|
406
|
-
: await defaultGetUserIdFromRequest();
|
|
407
|
-
if (!current_user_id) {
|
|
408
|
-
logger.error('[hazo_chat/messages/[id]/read PATCH] No user ID - not authenticated');
|
|
409
|
-
return NextResponse.json({ success: false, error: 'User not authenticated' }, { status: 401 });
|
|
410
|
-
}
|
|
411
|
-
// Handle params as Promise (Next.js 15+) or direct object (Next.js 13-14)
|
|
412
|
-
const params = context.params instanceof Promise ? await context.params : context.params;
|
|
413
|
-
const message_id = params.id;
|
|
414
|
-
if (!message_id) {
|
|
415
|
-
logger.error('[hazo_chat/messages/[id]/read PATCH] Missing message ID');
|
|
416
|
-
return NextResponse.json({ success: false, error: 'Message ID is required' }, { status: 400 });
|
|
417
|
-
}
|
|
418
|
-
// Validate UUID formats before database queries
|
|
419
|
-
if (!is_valid_uuid(message_id)) {
|
|
420
|
-
logger.debug('[hazo_chat/messages/[id]/read PATCH] Invalid message_id format:', { message_id });
|
|
421
|
-
return NextResponse.json({ success: false, error: 'message_id must be a valid UUID', error_code: 'INVALID_UUID_FORMAT' }, { status: 400 });
|
|
422
|
-
}
|
|
423
|
-
if (!is_valid_uuid(current_user_id)) {
|
|
424
|
-
logger.debug('[hazo_chat/messages/[id]/read PATCH] Invalid user_id format:', { current_user_id });
|
|
425
|
-
return NextResponse.json({ success: false, error: 'Invalid user ID format', error_code: 'INVALID_UUID_FORMAT' }, { status: 400 });
|
|
426
|
-
}
|
|
427
|
-
logger.info('[hazo_chat/messages/[id]/read PATCH] Marking message as read:', {
|
|
428
|
-
message_id,
|
|
429
|
-
current_user_id,
|
|
430
|
-
});
|
|
431
|
-
// Get hazo_connect instance and create CRUD service
|
|
432
|
-
const hazoConnect = getHazoConnect();
|
|
433
|
-
const chatService = createCrudService(hazoConnect, 'hazo_chat');
|
|
434
|
-
// First, fetch the message to verify ownership
|
|
435
|
-
let message = null;
|
|
399
|
+
return withContext({}, async () => {
|
|
436
400
|
try {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
401
|
+
// Get current user ID
|
|
402
|
+
const current_user_id = getUserIdFromRequest
|
|
403
|
+
? await getUserIdFromRequest(request)
|
|
404
|
+
: await defaultGetUserIdFromRequest();
|
|
405
|
+
if (!current_user_id) {
|
|
406
|
+
logger.error('[hazo_chat/messages/[id]/read PATCH] No user ID - not authenticated');
|
|
407
|
+
return NextResponse.json({ success: false, error: 'User not authenticated' }, { status: 401 });
|
|
408
|
+
}
|
|
409
|
+
// Handle params as Promise (Next.js 15+) or direct object (Next.js 13-14)
|
|
410
|
+
const params = context.params instanceof Promise ? await context.params : context.params;
|
|
411
|
+
const message_id = params.id;
|
|
412
|
+
if (!message_id) {
|
|
413
|
+
logger.error('[hazo_chat/messages/[id]/read PATCH] Missing message ID');
|
|
414
|
+
return NextResponse.json({ success: false, error: 'Message ID is required' }, { status: 400 });
|
|
415
|
+
}
|
|
416
|
+
// Validate UUID formats before database queries
|
|
417
|
+
if (!is_valid_uuid(message_id)) {
|
|
418
|
+
logger.debug('[hazo_chat/messages/[id]/read PATCH] Invalid message_id format:', { message_id });
|
|
419
|
+
return NextResponse.json({ success: false, error: 'message_id must be a valid UUID', error_code: 'INVALID_UUID_FORMAT' }, { status: 400 });
|
|
420
|
+
}
|
|
421
|
+
if (!is_valid_uuid(current_user_id)) {
|
|
422
|
+
logger.debug('[hazo_chat/messages/[id]/read PATCH] Invalid user_id format:', { current_user_id });
|
|
423
|
+
return NextResponse.json({ success: false, error: 'Invalid user ID format', error_code: 'INVALID_UUID_FORMAT' }, { status: 400 });
|
|
424
|
+
}
|
|
425
|
+
logger.info('[hazo_chat/messages/[id]/read PATCH] Marking message as read:', {
|
|
461
426
|
message_id,
|
|
462
427
|
current_user_id,
|
|
463
428
|
});
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
429
|
+
// Get hazo_connect instance and create CRUD service
|
|
430
|
+
const hazoConnect = getHazoConnect();
|
|
431
|
+
const chatService = createCrudService(hazoConnect, 'hazo_chat');
|
|
432
|
+
// First, fetch the message to verify ownership
|
|
433
|
+
let message = null;
|
|
434
|
+
try {
|
|
435
|
+
const messages = await chatService.list((qb) => qb.select('*').where('id', 'eq', message_id));
|
|
436
|
+
message = messages[0] || null;
|
|
437
|
+
}
|
|
438
|
+
catch (dbError) {
|
|
439
|
+
logger.error('[hazo_chat/messages/[id]/read PATCH] Database error fetching message:', { dbError: serialize_error(dbError) });
|
|
440
|
+
throw dbError;
|
|
441
|
+
}
|
|
442
|
+
if (!message) {
|
|
443
|
+
logger.error('[hazo_chat/messages/[id]/read PATCH] Message not found:', { message_id });
|
|
444
|
+
return NextResponse.json({ success: false, error: 'Message not found' }, { status: 404 });
|
|
445
|
+
}
|
|
446
|
+
// Verify that the current user is a member of the chat group
|
|
447
|
+
const membership = await verifyGroupMembership(hazoConnect, current_user_id, message.chat_group_id, logger);
|
|
448
|
+
if (!membership) {
|
|
449
|
+
logger.error('[hazo_chat/messages/[id]/read PATCH] User is not a member of chat group:', {
|
|
450
|
+
message_id,
|
|
451
|
+
current_user_id,
|
|
452
|
+
chat_group_id: message.chat_group_id,
|
|
453
|
+
});
|
|
454
|
+
return NextResponse.json({ success: false, error: 'Unauthorized - not a member of this chat group' }, { status: 403 });
|
|
455
|
+
}
|
|
456
|
+
// Cannot mark your own messages as read
|
|
457
|
+
if (message.sender_user_id === current_user_id) {
|
|
458
|
+
logger.error('[hazo_chat/messages/[id]/read PATCH] User cannot mark own message as read:', {
|
|
459
|
+
message_id,
|
|
460
|
+
current_user_id,
|
|
461
|
+
});
|
|
462
|
+
return NextResponse.json({ success: false, error: 'Cannot mark your own messages as read' }, { status: 400 });
|
|
463
|
+
}
|
|
464
|
+
// Don't update if already read
|
|
465
|
+
if (message.read_at) {
|
|
466
|
+
logger.debug('[hazo_chat/messages/[id]/read PATCH] Message already read:', { message_id });
|
|
467
|
+
return NextResponse.json({
|
|
468
|
+
success: true,
|
|
469
|
+
message: {
|
|
470
|
+
...message,
|
|
471
|
+
read_at: message.read_at,
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
// Update the read_at timestamp using the adapter-agnostic CRUD service
|
|
476
|
+
const now = new Date().toISOString();
|
|
477
|
+
try {
|
|
478
|
+
const updated_rows = await chatService.updateById(message_id, {
|
|
479
|
+
read_at: now,
|
|
480
|
+
changed_at: now,
|
|
481
|
+
});
|
|
482
|
+
if (updated_rows.length === 0) {
|
|
483
|
+
logger.warn('[hazo_chat/messages/[id]/read PATCH] No rows updated - message may not exist:', { message_id });
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
logger.debug('[hazo_chat/messages/[id]/read PATCH] Successfully updated', { rows: updated_rows.length });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
catch (dbError) {
|
|
490
|
+
logger.error('[hazo_chat/messages/[id]/read PATCH] Database error updating message:', { dbError: serialize_error(dbError) });
|
|
491
|
+
throw dbError;
|
|
492
|
+
}
|
|
493
|
+
logger.debug('[hazo_chat/messages/[id]/read PATCH] Message marked as read successfully:', { message_id });
|
|
469
494
|
return NextResponse.json({
|
|
470
495
|
success: true,
|
|
471
496
|
message: {
|
|
472
497
|
...message,
|
|
473
|
-
read_at:
|
|
498
|
+
read_at: now,
|
|
499
|
+
changed_at: now,
|
|
474
500
|
},
|
|
475
501
|
});
|
|
476
502
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
read_at: now,
|
|
482
|
-
changed_at: now,
|
|
483
|
-
});
|
|
484
|
-
if (updated_rows.length === 0) {
|
|
485
|
-
logger.warn('[hazo_chat/messages/[id]/read PATCH] No rows updated - message may not exist:', { message_id });
|
|
486
|
-
}
|
|
487
|
-
else {
|
|
488
|
-
logger.info('[hazo_chat/messages/[id]/read PATCH] Successfully updated', { rows: updated_rows.length });
|
|
489
|
-
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
const error_message = error instanceof Error ? error.message : 'Unknown error';
|
|
505
|
+
logger.error('[hazo_chat/messages/[id]/read PATCH] Error:', { error_message, error: serialize_error(error) });
|
|
506
|
+
return NextResponse.json({ success: false, error: 'Failed to mark message as read' }, { status: 500 });
|
|
490
507
|
}
|
|
491
|
-
|
|
492
|
-
logger.error('[hazo_chat/messages/[id]/read PATCH] Database error updating message:', { dbError: serialize_error(dbError) });
|
|
493
|
-
throw dbError;
|
|
494
|
-
}
|
|
495
|
-
logger.info('[hazo_chat/messages/[id]/read PATCH] Message marked as read successfully:', { message_id });
|
|
496
|
-
return NextResponse.json({
|
|
497
|
-
success: true,
|
|
498
|
-
message: {
|
|
499
|
-
...message,
|
|
500
|
-
read_at: now,
|
|
501
|
-
changed_at: now,
|
|
502
|
-
},
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
catch (error) {
|
|
506
|
-
const error_message = error instanceof Error ? error.message : 'Unknown error';
|
|
507
|
-
logger.error('[hazo_chat/messages/[id]/read PATCH] Error:', { error_message, error: serialize_error(error) });
|
|
508
|
-
return NextResponse.json({ success: false, error: 'Failed to mark message as read' }, { status: 500 });
|
|
509
|
-
}
|
|
508
|
+
}); // end withContext
|
|
510
509
|
}
|
|
511
510
|
return { PATCH };
|
|
512
511
|
}
|
|
@@ -547,117 +546,119 @@ export function createDeleteHandler(options) {
|
|
|
547
546
|
* Only the sender can delete their own messages.
|
|
548
547
|
*/
|
|
549
548
|
async function DELETE(request, context) {
|
|
550
|
-
|
|
551
|
-
// Get current user ID
|
|
552
|
-
const current_user_id = getUserIdFromRequest
|
|
553
|
-
? await getUserIdFromRequest(request)
|
|
554
|
-
: await defaultGetUserIdFromRequest();
|
|
555
|
-
if (!current_user_id) {
|
|
556
|
-
logger.error('[hazo_chat/messages/[id] DELETE] No user ID - not authenticated');
|
|
557
|
-
return createErrorResponse('User not authenticated', 401, 'UNAUTHENTICATED');
|
|
558
|
-
}
|
|
559
|
-
// Handle params as Promise (Next.js 15+) or direct object (Next.js 13-14)
|
|
560
|
-
const params = context.params instanceof Promise ? await context.params : context.params;
|
|
561
|
-
const message_id = params.id;
|
|
562
|
-
if (!message_id) {
|
|
563
|
-
logger.error('[hazo_chat/messages/[id] DELETE] Missing message ID');
|
|
564
|
-
return createErrorResponse('Message ID is required', 400, 'MISSING_MESSAGE_ID');
|
|
565
|
-
}
|
|
566
|
-
// Validate UUID formats before database queries
|
|
567
|
-
if (!is_valid_uuid(message_id)) {
|
|
568
|
-
logger.debug('[hazo_chat/messages/[id] DELETE] Invalid message_id format:', { message_id });
|
|
569
|
-
return createErrorResponse('message_id must be a valid UUID', 400, 'INVALID_UUID_FORMAT');
|
|
570
|
-
}
|
|
571
|
-
if (!is_valid_uuid(current_user_id)) {
|
|
572
|
-
logger.debug('[hazo_chat/messages/[id] DELETE] Invalid user_id format:', { current_user_id });
|
|
573
|
-
return createErrorResponse('Invalid user ID format', 400, 'INVALID_UUID_FORMAT');
|
|
574
|
-
}
|
|
575
|
-
logger.info('[hazo_chat/messages/[id] DELETE] Deleting message:', {
|
|
576
|
-
message_id,
|
|
577
|
-
current_user_id,
|
|
578
|
-
});
|
|
579
|
-
// Get hazo_connect instance and create CRUD service
|
|
580
|
-
const hazoConnect = getHazoConnect();
|
|
581
|
-
const chatService = createCrudService(hazoConnect, 'hazo_chat');
|
|
582
|
-
// Fetch the message to verify ownership
|
|
583
|
-
let message = null;
|
|
549
|
+
return withContext({}, async () => {
|
|
584
550
|
try {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
551
|
+
// Get current user ID
|
|
552
|
+
const current_user_id = getUserIdFromRequest
|
|
553
|
+
? await getUserIdFromRequest(request)
|
|
554
|
+
: await defaultGetUserIdFromRequest();
|
|
555
|
+
if (!current_user_id) {
|
|
556
|
+
logger.error('[hazo_chat/messages/[id] DELETE] No user ID - not authenticated');
|
|
557
|
+
return createErrorResponse('User not authenticated', 401, 'UNAUTHENTICATED');
|
|
558
|
+
}
|
|
559
|
+
// Handle params as Promise (Next.js 15+) or direct object (Next.js 13-14)
|
|
560
|
+
const params = context.params instanceof Promise ? await context.params : context.params;
|
|
561
|
+
const message_id = params.id;
|
|
562
|
+
if (!message_id) {
|
|
563
|
+
logger.error('[hazo_chat/messages/[id] DELETE] Missing message ID');
|
|
564
|
+
return createErrorResponse('Message ID is required', 400, 'MISSING_MESSAGE_ID');
|
|
565
|
+
}
|
|
566
|
+
// Validate UUID formats before database queries
|
|
567
|
+
if (!is_valid_uuid(message_id)) {
|
|
568
|
+
logger.debug('[hazo_chat/messages/[id] DELETE] Invalid message_id format:', { message_id });
|
|
569
|
+
return createErrorResponse('message_id must be a valid UUID', 400, 'INVALID_UUID_FORMAT');
|
|
570
|
+
}
|
|
571
|
+
if (!is_valid_uuid(current_user_id)) {
|
|
572
|
+
logger.debug('[hazo_chat/messages/[id] DELETE] Invalid user_id format:', { current_user_id });
|
|
573
|
+
return createErrorResponse('Invalid user ID format', 400, 'INVALID_UUID_FORMAT');
|
|
574
|
+
}
|
|
575
|
+
logger.info('[hazo_chat/messages/[id] DELETE] Deleting message:', {
|
|
609
576
|
message_id,
|
|
610
577
|
current_user_id,
|
|
611
|
-
sender_user_id: message.sender_user_id,
|
|
612
578
|
});
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
579
|
+
// Get hazo_connect instance and create CRUD service
|
|
580
|
+
const hazoConnect = getHazoConnect();
|
|
581
|
+
const chatService = createCrudService(hazoConnect, 'hazo_chat');
|
|
582
|
+
// Fetch the message to verify ownership
|
|
583
|
+
let message = null;
|
|
584
|
+
try {
|
|
585
|
+
const messages = await chatService.list((qb) => qb.select('*').where('id', 'eq', message_id));
|
|
586
|
+
message = messages[0] || null;
|
|
587
|
+
}
|
|
588
|
+
catch (dbError) {
|
|
589
|
+
logger.error('[hazo_chat/messages/[id] DELETE] Database error fetching message:', { dbError: serialize_error(dbError) });
|
|
590
|
+
throw dbError;
|
|
591
|
+
}
|
|
592
|
+
if (!message) {
|
|
593
|
+
logger.error('[hazo_chat/messages/[id] DELETE] Message not found:', { message_id });
|
|
594
|
+
return createErrorResponse('Message not found', 404, 'MESSAGE_NOT_FOUND');
|
|
595
|
+
}
|
|
596
|
+
// Verify that the current user is a member of the chat group
|
|
597
|
+
const membership = await verifyGroupMembership(hazoConnect, current_user_id, message.chat_group_id, logger);
|
|
598
|
+
if (!membership) {
|
|
599
|
+
logger.error('[hazo_chat/messages/[id] DELETE] User is not a member of chat group:', {
|
|
600
|
+
message_id,
|
|
601
|
+
current_user_id,
|
|
602
|
+
chat_group_id: message.chat_group_id,
|
|
603
|
+
});
|
|
604
|
+
return createErrorResponse('Unauthorized - not a member of this chat group', 403, 'FORBIDDEN');
|
|
605
|
+
}
|
|
606
|
+
// Verify that the current user is the sender (only senders can delete their messages)
|
|
607
|
+
if (message.sender_user_id !== current_user_id) {
|
|
608
|
+
logger.error('[hazo_chat/messages/[id] DELETE] User is not the sender:', {
|
|
609
|
+
message_id,
|
|
610
|
+
current_user_id,
|
|
611
|
+
sender_user_id: message.sender_user_id,
|
|
612
|
+
});
|
|
613
|
+
return createErrorResponse('Unauthorized - only the sender can delete their messages', 403, 'UNAUTHORIZED');
|
|
614
|
+
}
|
|
615
|
+
// Check if already deleted
|
|
616
|
+
if (message.deleted_at) {
|
|
617
|
+
logger.debug('[hazo_chat/messages/[id] DELETE] Message already deleted:', { message_id });
|
|
618
|
+
return NextResponse.json({
|
|
619
|
+
success: true,
|
|
620
|
+
message: {
|
|
621
|
+
...message,
|
|
622
|
+
message_text: null,
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
// Soft delete: set deleted_at timestamp and clear message_text using adapter-agnostic CRUD service
|
|
627
|
+
const now = new Date().toISOString();
|
|
628
|
+
try {
|
|
629
|
+
const updated_rows = await chatService.updateById(message_id, {
|
|
630
|
+
deleted_at: now,
|
|
631
|
+
message_text: null,
|
|
632
|
+
changed_at: now,
|
|
633
|
+
});
|
|
634
|
+
if (updated_rows.length === 0) {
|
|
635
|
+
logger.warn('[hazo_chat/messages/[id] DELETE] No rows updated - message may not exist:', { message_id });
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
logger.debug('[hazo_chat/messages/[id] DELETE] Successfully deleted', { rows: updated_rows.length });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
catch (dbError) {
|
|
642
|
+
logger.error('[hazo_chat/messages/[id] DELETE] Database error deleting message:', { dbError: serialize_error(dbError) });
|
|
643
|
+
throw dbError;
|
|
644
|
+
}
|
|
645
|
+
logger.debug('[hazo_chat/messages/[id] DELETE] Message deleted successfully:', { message_id });
|
|
618
646
|
return NextResponse.json({
|
|
619
647
|
success: true,
|
|
620
648
|
message: {
|
|
621
649
|
...message,
|
|
650
|
+
deleted_at: now,
|
|
622
651
|
message_text: null,
|
|
652
|
+
changed_at: now,
|
|
623
653
|
},
|
|
624
654
|
});
|
|
625
655
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
deleted_at: now,
|
|
631
|
-
message_text: null,
|
|
632
|
-
changed_at: now,
|
|
633
|
-
});
|
|
634
|
-
if (updated_rows.length === 0) {
|
|
635
|
-
logger.warn('[hazo_chat/messages/[id] DELETE] No rows updated - message may not exist:', { message_id });
|
|
636
|
-
}
|
|
637
|
-
else {
|
|
638
|
-
logger.info('[hazo_chat/messages/[id] DELETE] Successfully deleted', { rows: updated_rows.length });
|
|
639
|
-
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
const error_message = error instanceof Error ? error.message : 'Unknown error';
|
|
658
|
+
logger.error('[hazo_chat/messages/[id] DELETE] Error:', { error_message, error: serialize_error(error) });
|
|
659
|
+
return createErrorResponse('Failed to delete message', 500, 'INTERNAL_ERROR');
|
|
640
660
|
}
|
|
641
|
-
|
|
642
|
-
logger.error('[hazo_chat/messages/[id] DELETE] Database error deleting message:', { dbError: serialize_error(dbError) });
|
|
643
|
-
throw dbError;
|
|
644
|
-
}
|
|
645
|
-
logger.info('[hazo_chat/messages/[id] DELETE] Message deleted successfully:', { message_id });
|
|
646
|
-
return NextResponse.json({
|
|
647
|
-
success: true,
|
|
648
|
-
message: {
|
|
649
|
-
...message,
|
|
650
|
-
deleted_at: now,
|
|
651
|
-
message_text: null,
|
|
652
|
-
changed_at: now,
|
|
653
|
-
},
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
catch (error) {
|
|
657
|
-
const error_message = error instanceof Error ? error.message : 'Unknown error';
|
|
658
|
-
logger.error('[hazo_chat/messages/[id] DELETE] Error:', { error_message, error: serialize_error(error) });
|
|
659
|
-
return createErrorResponse('Failed to delete message', 500, 'INTERNAL_ERROR');
|
|
660
|
-
}
|
|
661
|
+
}); // end withContext
|
|
661
662
|
}
|
|
662
663
|
return { DELETE };
|
|
663
664
|
}
|