222 lines
7.1 KiB
TypeScript
222 lines
7.1 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { getDatabase, Comment, db_helpers } from '@/lib/db';
|
|
import { requireRole } from '@/lib/auth';
|
|
import { validateBody, createCommentSchema } from '@/lib/validation';
|
|
import { mutationLimiter } from '@/lib/rate-limit';
|
|
import { logger } from '@/lib/logger';
|
|
|
|
/**
|
|
* GET /api/tasks/[id]/comments - Get all comments for a task
|
|
*/
|
|
export async function GET(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
const auth = requireRole(request, 'viewer');
|
|
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
|
|
|
try {
|
|
const db = getDatabase();
|
|
const resolvedParams = await params;
|
|
const taskId = parseInt(resolvedParams.id);
|
|
const workspaceId = auth.user.workspace_id ?? 1;
|
|
|
|
if (isNaN(taskId)) {
|
|
return NextResponse.json({ error: 'Invalid task ID' }, { status: 400 });
|
|
}
|
|
|
|
// Verify task exists
|
|
const task = db
|
|
.prepare('SELECT id FROM tasks WHERE id = ? AND workspace_id = ?')
|
|
.get(taskId, workspaceId);
|
|
if (!task) {
|
|
return NextResponse.json({ error: 'Task not found' }, { status: 404 });
|
|
}
|
|
|
|
// Get comments ordered by creation time
|
|
const stmt = db.prepare(`
|
|
SELECT * FROM comments
|
|
WHERE task_id = ? AND workspace_id = ?
|
|
ORDER BY created_at ASC
|
|
`);
|
|
|
|
const comments = stmt.all(taskId, workspaceId) as Comment[];
|
|
|
|
// Parse JSON fields and build thread structure
|
|
const commentsWithParsedData = comments.map(comment => ({
|
|
...comment,
|
|
mentions: comment.mentions ? JSON.parse(comment.mentions) : []
|
|
}));
|
|
|
|
// Organize into thread structure (parent comments with replies)
|
|
const commentMap = new Map();
|
|
const topLevelComments: any[] = [];
|
|
|
|
// First pass: create all comment objects
|
|
commentsWithParsedData.forEach(comment => {
|
|
commentMap.set(comment.id, { ...comment, replies: [] });
|
|
});
|
|
|
|
// Second pass: organize into threads
|
|
commentsWithParsedData.forEach(comment => {
|
|
const commentWithReplies = commentMap.get(comment.id);
|
|
|
|
if (comment.parent_id) {
|
|
// This is a reply, add to parent's replies
|
|
const parent = commentMap.get(comment.parent_id);
|
|
if (parent) {
|
|
parent.replies.push(commentWithReplies);
|
|
}
|
|
} else {
|
|
// This is a top-level comment
|
|
topLevelComments.push(commentWithReplies);
|
|
}
|
|
});
|
|
|
|
return NextResponse.json({
|
|
comments: topLevelComments,
|
|
total: comments.length
|
|
});
|
|
} catch (error) {
|
|
logger.error({ err: error }, 'GET /api/tasks/[id]/comments error');
|
|
return NextResponse.json({ error: 'Failed to fetch comments' }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/tasks/[id]/comments - Add a new comment to a task
|
|
*/
|
|
export async function POST(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
const auth = requireRole(request, 'operator');
|
|
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
|
|
|
const rateCheck = mutationLimiter(request);
|
|
if (rateCheck) return rateCheck;
|
|
|
|
try {
|
|
const db = getDatabase();
|
|
const resolvedParams = await params;
|
|
const taskId = parseInt(resolvedParams.id);
|
|
const workspaceId = auth.user.workspace_id ?? 1;
|
|
|
|
if (isNaN(taskId)) {
|
|
return NextResponse.json({ error: 'Invalid task ID' }, { status: 400 });
|
|
}
|
|
|
|
const result = await validateBody(request, createCommentSchema);
|
|
if ('error' in result) return result.error;
|
|
const { content, author = 'system', parent_id } = result.data;
|
|
|
|
// Verify task exists
|
|
const task = db
|
|
.prepare('SELECT * FROM tasks WHERE id = ? AND workspace_id = ?')
|
|
.get(taskId, workspaceId) as any;
|
|
if (!task) {
|
|
return NextResponse.json({ error: 'Task not found' }, { status: 404 });
|
|
}
|
|
|
|
// Verify parent comment exists if specified
|
|
if (parent_id) {
|
|
const parentComment = db
|
|
.prepare('SELECT id FROM comments WHERE id = ? AND task_id = ? AND workspace_id = ?')
|
|
.get(parent_id, taskId, workspaceId);
|
|
if (!parentComment) {
|
|
return NextResponse.json({ error: 'Parent comment not found' }, { status: 404 });
|
|
}
|
|
}
|
|
|
|
// Parse @mentions from content
|
|
const mentions = db_helpers.parseMentions(content);
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
|
|
// Insert comment
|
|
const stmt = db.prepare(`
|
|
INSERT INTO comments (task_id, author, content, created_at, parent_id, mentions, workspace_id)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
`);
|
|
|
|
const insertResult = stmt.run(
|
|
taskId,
|
|
author,
|
|
content,
|
|
now,
|
|
parent_id || null,
|
|
mentions.length > 0 ? JSON.stringify(mentions) : null,
|
|
workspaceId
|
|
);
|
|
|
|
const commentId = insertResult.lastInsertRowid as number;
|
|
|
|
// Log activity
|
|
const activityDescription = parent_id
|
|
? `Replied to comment on task: ${task.title}`
|
|
: `Added comment to task: ${task.title}`;
|
|
|
|
db_helpers.logActivity(
|
|
'comment_added',
|
|
'comment',
|
|
commentId,
|
|
author,
|
|
activityDescription,
|
|
{
|
|
task_id: taskId,
|
|
task_title: task.title,
|
|
parent_id,
|
|
mentions,
|
|
content_preview: content.substring(0, 100)
|
|
},
|
|
workspaceId
|
|
);
|
|
|
|
// Ensure subscriptions for author, mentions, and assignee
|
|
db_helpers.ensureTaskSubscription(taskId, author, workspaceId);
|
|
const uniqueMentions = Array.from(new Set(mentions));
|
|
uniqueMentions.forEach((mentionedAgent) => {
|
|
db_helpers.ensureTaskSubscription(taskId, mentionedAgent, workspaceId);
|
|
});
|
|
if (task.assigned_to) {
|
|
db_helpers.ensureTaskSubscription(taskId, task.assigned_to, workspaceId);
|
|
}
|
|
|
|
// Notify subscribers
|
|
const subscribers = new Set(db_helpers.getTaskSubscribers(taskId, workspaceId));
|
|
subscribers.delete(author);
|
|
const mentionSet = new Set(uniqueMentions);
|
|
|
|
for (const subscriber of subscribers) {
|
|
const isMention = mentionSet.has(subscriber);
|
|
db_helpers.createNotification(
|
|
subscriber,
|
|
isMention ? 'mention' : 'comment',
|
|
isMention ? 'You were mentioned' : 'New comment on a subscribed task',
|
|
isMention
|
|
? `${author} mentioned you in a comment on "${task.title}": ${content.substring(0, 100)}${content.length > 100 ? '...' : ''}`
|
|
: `${author} commented on "${task.title}": ${content.substring(0, 100)}${content.length > 100 ? '...' : ''}`,
|
|
'comment',
|
|
commentId,
|
|
workspaceId
|
|
);
|
|
}
|
|
|
|
// Fetch the created comment
|
|
const createdComment = db
|
|
.prepare('SELECT * FROM comments WHERE id = ? AND workspace_id = ?')
|
|
.get(commentId, workspaceId) as Comment;
|
|
|
|
return NextResponse.json({
|
|
comment: {
|
|
...createdComment,
|
|
mentions: createdComment.mentions ? JSON.parse(createdComment.mentions) : [],
|
|
replies: [] // New comments have no replies initially
|
|
}
|
|
}, { status: 201 });
|
|
} catch (error) {
|
|
logger.error({ err: error }, 'POST /api/tasks/[id]/comments error');
|
|
return NextResponse.json({ error: 'Failed to add comment' }, { status: 500 });
|
|
}
|
|
}
|