← back to Norma
app/api/clickup/route.ts
114 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
const CLICKUP_BASE = 'https://api.clickup.com/api/v2';
function getApiKey(): string | null {
return process.env.CLICKUP_API_KEY || null;
}
async function clickupFetch(path: string, options?: RequestInit) {
const apiKey = getApiKey();
if (!apiKey) throw new Error('CLICKUP_API_KEY not configured');
const res = await fetch(`${CLICKUP_BASE}${path}`, {
...options,
headers: {
Authorization: apiKey,
'Content-Type': 'application/json',
...options?.headers,
},
});
if (!res.ok) throw new Error(`ClickUp API ${res.status}: ${await res.text()}`);
return res.json();
}
/**
* GET /api/clickup?action=spaces|tasks|lists
* Fetch ClickUp data.
*/
export async function GET(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
const { searchParams } = new URL(request.url);
const action = searchParams.get('action') || 'spaces';
try {
if (action === 'teams') {
const data = await clickupFetch('/team');
return NextResponse.json({ teams: data.teams || [] });
}
if (action === 'spaces') {
const teamData = await clickupFetch('/team');
const teamId = teamData.teams?.[0]?.id;
if (!teamId) return NextResponse.json({ spaces: [] });
const data = await clickupFetch(`/team/${teamId}/space`);
return NextResponse.json({ spaces: data.spaces || [] });
}
if (action === 'lists') {
const spaceId = searchParams.get('space_id');
if (!spaceId) return NextResponse.json({ error: 'space_id required' }, { status: 400 });
const data = await clickupFetch(`/space/${spaceId}/list`);
return NextResponse.json({ lists: data.lists || [] });
}
if (action === 'tasks') {
const listId = searchParams.get('list_id');
if (!listId) return NextResponse.json({ error: 'list_id required' }, { status: 400 });
const data = await clickupFetch(`/list/${listId}/task?limit=50`);
return NextResponse.json({ tasks: data.tasks || [] });
}
return NextResponse.json({ error: `Unknown action: ${action}` }, { status: 400 });
} catch (err) {
return NextResponse.json({ error: (err as Error).message }, { status: 500 });
}
}
/**
* POST /api/clickup
* Create a ClickUp task from Norma (petition, grant, campaign).
*/
export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
const body = await request.json();
const { list_id, name, description, priority, due_date, tags, assignees, source_type, source_id } = body;
if (!list_id || !name) {
return NextResponse.json({ error: 'list_id and name required' }, { status: 400 });
}
try {
const taskPayload: Record<string, unknown> = {
name,
description: description || '',
priority: priority || 3,
tags: tags || [],
};
if (due_date) taskPayload.due_date = new Date(due_date).getTime();
if (assignees) taskPayload.assignees = assignees;
const task = await clickupFetch(`/list/${list_id}/task`, {
method: 'POST',
body: JSON.stringify(taskPayload),
});
// Log the integration in DB
await query(
`INSERT INTO connected_services_log (service_id, action, source_type, source_id, external_id, metadata)
VALUES ('clickup', 'create_task', $1, $2, $3, $4)
ON CONFLICT DO NOTHING`,
[source_type || 'manual', source_id || null, task.id, JSON.stringify({ name, list_id })]
).catch(() => {}); // table may not exist yet, that's ok
return NextResponse.json({ task });
} catch (err) {
return NextResponse.json({ error: (err as Error).message }, { status: 500 });
}
}