import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth/next"; import { authOptions } from "@/app/api/auth/options"; import { getCachedTasksData, cacheTasksData } from "@/lib/redis"; import { logger } from "@/lib/logger"; interface Task { id: string; headline: string; projectName: string; projectId: number; status: number; dateToFinish: string | null; milestone: string | null; details: string | null; createdOn: string; editedOn: string | null; editorId: string; editorFirstname: string; editorLastname: string; } async function getLeantimeUserId(email: string): Promise { try { if (!process.env.LEANTIME_TOKEN) { logger.error('[LEANTIME_TASKS] LEANTIME_TOKEN is not set in environment variables'); return null; } logger.debug('[LEANTIME_TASKS] Fetching Leantime users', { emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12), apiUrlPresent: !!process.env.LEANTIME_API_URL, tokenLength: process.env.LEANTIME_TOKEN.length, }); const headers: Record = { 'Content-Type': 'application/json', 'X-API-Key': process.env.LEANTIME_TOKEN }; const response = await fetch(`${process.env.LEANTIME_API_URL}/api/jsonrpc`, { method: 'POST', headers, body: JSON.stringify({ jsonrpc: '2.0', method: 'leantime.rpc.users.getAll', id: 1 }), }); const responseText = await response.text(); if (!response.ok) { logger.error('[LEANTIME_TASKS] Failed to fetch Leantime users', { status: response.status, statusText: response.statusText, headers: Object.fromEntries(response.headers.entries()), }); return null; } let data; try { data = JSON.parse(responseText); } catch (e) { logger.error('[LEANTIME_TASKS] Failed to parse Leantime response', { error: e instanceof Error ? e.message : String(e), }); return null; } if (!data.result || !Array.isArray(data.result)) { logger.error('[LEANTIME_TASKS] Invalid response format from Leantime users API'); return null; } const users = data.result; const user = users.find((u: any) => u.username === email); if (user) { logger.debug('[LEANTIME_TASKS] Found Leantime user', { id: user.id, emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12), }); } else { logger.warn('[LEANTIME_TASKS] No Leantime user found for username', { emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12), }); } return user ? user.id : null; } catch (error) { logger.error('[LEANTIME_TASKS] Error fetching Leantime user ID', { error: error instanceof Error ? error.message : String(error), }); return null; } } export async function GET(request: NextRequest) { try { const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } // Check for force refresh parameter const url = new URL(request.url); const forceRefresh = url.searchParams.get('refresh') === 'true'; // Try to get data from cache if not forcing refresh if (!forceRefresh) { const cachedTasks = await getCachedTasksData(session.user.id); if (cachedTasks) { logger.debug('[LEANTIME_TASKS] Using cached tasks data', { userId: session.user.id, taskCount: Array.isArray(cachedTasks) ? cachedTasks.length : undefined, }); return NextResponse.json(cachedTasks); } } logger.debug('[LEANTIME_TASKS] Fetching tasks for user', { emailHash: Buffer.from(session.user.email.toLowerCase()).toString('base64').slice(0, 12), }); const userId = await getLeantimeUserId(session.user.email); if (!userId) { logger.error('[LEANTIME_TASKS] User not found in Leantime', { emailHash: Buffer.from(session.user.email.toLowerCase()).toString('base64').slice(0, 12), }); return NextResponse.json({ error: "User not found in Leantime" }, { status: 404 }); } logger.debug('[LEANTIME_TASKS] Fetching tasks for Leantime user ID', { userId, }); const headers: Record = { 'Content-Type': 'application/json', 'X-API-Key': process.env.LEANTIME_TOKEN! }; const response = await fetch(`${process.env.LEANTIME_API_URL}/api/jsonrpc`, { method: 'POST', headers, body: JSON.stringify({ jsonrpc: '2.0', method: 'leantime.rpc.tickets.getAll', params: { userId: userId, status: "all" }, id: 1 }), }); const responseText = await response.text(); logger.debug('[LEANTIME_TASKS] Tasks API response status', { status: response.status, }); if (!response.ok) { logger.error('[LEANTIME_TASKS] Failed to fetch tasks from Leantime', { status: response.status, statusText: response.statusText, }); throw new Error('Failed to fetch tasks from Leantime'); } let data; try { data = JSON.parse(responseText); } catch (e) { logger.error('[LEANTIME_TASKS] Failed to parse tasks response', { error: e instanceof Error ? e.message : String(e), }); throw new Error('Invalid response format from Leantime'); } if (!data.result || !Array.isArray(data.result)) { logger.error('[LEANTIME_TASKS] Invalid response format from Leantime tasks API'); throw new Error('Invalid response format from Leantime'); } // Log only the number of tasks and their IDs logger.debug('[LEANTIME_TASKS] Received tasks summary', { count: data.result.length, idsSample: data.result.slice(0, 20).map((task: any) => task.id), }); const tasks = data.result .filter((task: any) => { // Filter out any task (main or subtask) that has status Done (5) if (task.status === 5) { return false; } // Convert both to strings for comparison to handle any type mismatches const taskEditorId = String(task.editorId).trim(); const currentUserId = String(userId).trim(); // Only show tasks where the user is the editor const isUserEditor = taskEditorId === currentUserId; return isUserEditor; }) .map((task: any) => ({ id: task.id.toString(), headline: task.headline, projectName: task.projectName, projectId: task.projectId, status: task.status, dateToFinish: task.dateToFinish || null, milestone: task.type || null, details: task.description || null, createdOn: task.dateCreated, editedOn: task.editedOn || null, editorId: task.editorId, editorFirstname: task.editorFirstname, editorLastname: task.editorLastname, type: task.type || null, // Added type field to identify subtasks dependingTicketId: task.dependingTicketId || null // Added parent task reference })); logger.debug('[LEANTIME_TASKS] Filtered tasks for user', { userId, count: tasks.length, }); // Cache the results await cacheTasksData(session.user.id, tasks); return NextResponse.json(tasks); } catch (error) { logger.error('[LEANTIME_TASKS] Error in tasks route', { error: error instanceof Error ? error.message : String(error), }); return NextResponse.json( { error: "Failed to fetch tasks" }, { status: 500 } ); } }