feat: add read-only /api/projects endpoint for external tools
Returns project tags from tag-manager (category=project). Supports search (?q=), company filter (?company=), single by ID (?id=). Same Bearer token auth as address-book API. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/core/storage/prisma";
|
||||
|
||||
const NAMESPACE = "tags";
|
||||
|
||||
// ─── Auth: same Bearer token as address-book ────────────────────────
|
||||
|
||||
function checkBearerAuth(req: NextRequest): boolean {
|
||||
const secret = process.env.ADDRESSBOOK_API_KEY;
|
||||
if (!secret) return false;
|
||||
const authHeader = req.headers.get("Authorization");
|
||||
const token = authHeader?.replace("Bearer ", "");
|
||||
return token === secret;
|
||||
}
|
||||
|
||||
// ─── GET /api/projects ──────────────────────────────────────────────
|
||||
// Read-only. Returns all tags with category = "project".
|
||||
//
|
||||
// Query params:
|
||||
// ?q=<search> → search in label / projectCode
|
||||
// ?company=<companyId> → filter by companyId (beletage, urban-switch, studii-de-teren)
|
||||
// ?id=<uuid> → single project by tag ID
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
if (!checkBearerAuth(req)) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const params = req.nextUrl.searchParams;
|
||||
const id = params.get("id");
|
||||
const q = params.get("q")?.toLowerCase();
|
||||
const company = params.get("company");
|
||||
|
||||
try {
|
||||
// Single project by ID
|
||||
if (id) {
|
||||
const item = await prisma.keyValueStore.findUnique({
|
||||
where: { namespace_key: { namespace: NAMESPACE, key: id } },
|
||||
});
|
||||
if (!item) {
|
||||
return NextResponse.json({ error: "Project not found" }, { status: 404 });
|
||||
}
|
||||
const val = item.value as Record<string, unknown>;
|
||||
if (val.category !== "project") {
|
||||
return NextResponse.json({ error: "Not a project tag" }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json({ project: val });
|
||||
}
|
||||
|
||||
// All project tags
|
||||
const items = await prisma.keyValueStore.findMany({
|
||||
where: { namespace: NAMESPACE },
|
||||
select: { key: true, value: true },
|
||||
});
|
||||
|
||||
let projects: Record<string, unknown>[] = [];
|
||||
for (const item of items) {
|
||||
const val = item.value as Record<string, unknown>;
|
||||
if (!val || val.category !== "project") continue;
|
||||
|
||||
// Company filter
|
||||
if (company && val.companyId !== company) continue;
|
||||
|
||||
// Search filter
|
||||
if (q) {
|
||||
const label = String(val.label ?? "").toLowerCase();
|
||||
const code = String(val.projectCode ?? "").toLowerCase();
|
||||
if (!label.includes(q) && !code.includes(q)) continue;
|
||||
}
|
||||
|
||||
projects.push(val);
|
||||
}
|
||||
|
||||
// Sort by projectCode (B-001, B-002, US-001...) then label
|
||||
projects.sort((a, b) => {
|
||||
const aCode = String(a.projectCode ?? "");
|
||||
const bCode = String(b.projectCode ?? "");
|
||||
if (aCode && bCode) return aCode.localeCompare(bCode);
|
||||
if (aCode) return -1;
|
||||
if (bCode) return 1;
|
||||
return String(a.label ?? "").localeCompare(String(b.label ?? ""), "ro");
|
||||
});
|
||||
|
||||
return NextResponse.json({ projects, total: projects.length });
|
||||
} catch (error) {
|
||||
console.error("Projects GET error:", error);
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -46,6 +46,6 @@ export const config = {
|
||||
* - /favicon.ico, /robots.txt, /sitemap.xml
|
||||
* - Files with extensions (images, fonts, etc.)
|
||||
*/
|
||||
"/((?!api/auth|api/notifications/digest|api/compress-pdf|api/address-book|auth/signin|_next|favicon\\.ico|robots\\.txt|sitemap\\.xml|.*\\..*).*)",
|
||||
"/((?!api/auth|api/notifications/digest|api/compress-pdf|api/address-book|api/projects|auth/signin|_next|favicon\\.ico|robots\\.txt|sitemap\\.xml|.*\\..*).*)",
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user