← back to Norma
app/api/social/competitors/route.ts
168 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
/**
* GET /api/social/competitors?category=&platform=&active=true
* List competitor accounts with optional filters.
*/
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 category = searchParams.get('category');
const platform = searchParams.get('platform');
const active = searchParams.get('active');
const conditions: string[] = [];
const values: unknown[] = [];
let idx = 1;
if (category) {
conditions.push(`category = $${idx++}`);
values.push(category);
}
if (platform) {
conditions.push(`platform = $${idx++}`);
values.push(platform);
}
if (active !== null && active !== '') {
conditions.push(`is_active = $${idx++}`);
values.push(active === 'true');
}
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const result = await query(
`SELECT *
FROM social_competitors
${where}
ORDER BY follower_count DESC, created_at DESC`,
values
);
return NextResponse.json({ competitors: result.rows, count: result.rows.length });
}
/**
* POST /api/social/competitors
* Add a new competitor account to track.
* Body: { platform, handle, display_name?, category?, expertise_area?, avatar_url?, bio? }
*/
export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
const body = await request.json();
const {
platform,
handle,
display_name,
category = 'competitor',
expertise_area,
avatar_url,
bio,
follower_count = 0,
following_count = 0,
org_id,
} = body;
if (!platform || !handle) {
return NextResponse.json({ error: 'platform and handle are required' }, { status: 400 });
}
const result = await query(
`INSERT INTO social_competitors
(platform, handle, display_name, category, expertise_area, avatar_url, bio,
follower_count, following_count, org_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
ON CONFLICT (platform, handle) DO UPDATE SET
display_name = EXCLUDED.display_name,
category = EXCLUDED.category,
expertise_area = EXCLUDED.expertise_area,
avatar_url = COALESCE(EXCLUDED.avatar_url, social_competitors.avatar_url),
bio = COALESCE(EXCLUDED.bio, social_competitors.bio),
follower_count = EXCLUDED.follower_count,
following_count = EXCLUDED.following_count,
org_id = COALESCE(EXCLUDED.org_id, social_competitors.org_id),
is_active = true
RETURNING *`,
[platform, handle, display_name || null, category, expertise_area || null,
avatar_url || null, bio || null, follower_count, following_count, org_id || null]
);
return NextResponse.json({ competitor: result.rows[0] }, { status: 201 });
}
/**
* PATCH /api/social/competitors
* Update a competitor account.
* Body: { id, ...fields }
*/
export async function PATCH(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
const body = await request.json();
const { id, ...fields } = body;
if (!id) return NextResponse.json({ error: 'id is required' }, { status: 400 });
const allowed = [
'display_name', 'avatar_url', 'bio', 'follower_count', 'following_count',
'category', 'expertise_area', 'org_id', 'last_crawled_at', 'is_active',
];
const sets: string[] = [];
const values: unknown[] = [];
let idx = 1;
for (const key of allowed) {
if (key in fields) {
sets.push(`${key} = $${idx++}`);
values.push(fields[key]);
}
}
if (sets.length === 0) {
return NextResponse.json({ error: 'No updatable fields provided' }, { status: 400 });
}
values.push(id);
const result = await query(
`UPDATE social_competitors SET ${sets.join(', ')} WHERE id = $${idx} RETURNING *`,
values
);
if (result.rows.length === 0) {
return NextResponse.json({ error: 'Competitor not found' }, { status: 404 });
}
return NextResponse.json({ competitor: result.rows[0] });
}
/**
* DELETE /api/social/competitors
* Remove a competitor and all their crawled posts (CASCADE).
* Body: { id }
*/
export async function DELETE(request: NextRequest) {
const auth = requireRole(request, 'admin');
if (auth instanceof NextResponse) return auth;
const { id } = await request.json();
if (!id) return NextResponse.json({ error: 'id is required' }, { status: 400 });
const result = await query(
`DELETE FROM social_competitors WHERE id = $1 RETURNING id`,
[id]
);
if (result.rows.length === 0) {
return NextResponse.json({ error: 'Competitor not found' }, { status: 404 });
}
return NextResponse.json({ deleted: true, id });
}