feat(19-01): create template API routes — file, fields, ai-prepare
- GET /api/templates/[id]/file: streams form PDF from seeds/forms/ with path traversal guard - GET /api/templates/[id]/fields: returns signatureFields array (or []) from documentTemplates - POST /api/templates/[id]/ai-prepare: runs AI field placement with null client context, writes to DB - All routes: auth guard + isNull(archivedAt) soft-delete filter consistent with Phase 18
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { auth } from '@/lib/auth';
|
||||
import { db } from '@/lib/db';
|
||||
import { documentTemplates } from '@/lib/db/schema';
|
||||
import { and, eq, isNull } from 'drizzle-orm';
|
||||
import path from 'node:path';
|
||||
import { extractBlanks } from '@/lib/ai/extract-text';
|
||||
import { classifyFieldsWithAI } from '@/lib/ai/field-placement';
|
||||
|
||||
const SEEDS_FORMS_DIR = path.join(process.cwd(), 'seeds', 'forms');
|
||||
|
||||
export async function POST(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 });
|
||||
|
||||
if (!process.env.OPENAI_API_KEY) {
|
||||
return Response.json(
|
||||
{ error: 'OPENAI_API_KEY not configured. Add it to .env.local.' },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const template = await db.query.documentTemplates.findFirst({
|
||||
where: and(eq(documentTemplates.id, id), isNull(documentTemplates.archivedAt)),
|
||||
with: { formTemplate: true },
|
||||
});
|
||||
if (!template?.formTemplate) return Response.json({ error: 'Not found' }, { status: 404 });
|
||||
|
||||
const filePath = path.join(SEEDS_FORMS_DIR, template.formTemplate.filename);
|
||||
if (!filePath.startsWith(SEEDS_FORMS_DIR)) return new Response('Forbidden', { status: 403 });
|
||||
|
||||
try {
|
||||
const blanks = await extractBlanks(filePath);
|
||||
const { fields } = await classifyFieldsWithAI(blanks, null); // null = no client context per D-15
|
||||
|
||||
const [updated] = await db
|
||||
.update(documentTemplates)
|
||||
.set({ signatureFields: fields, updatedAt: new Date() })
|
||||
.where(eq(documentTemplates.id, id))
|
||||
.returning();
|
||||
|
||||
return Response.json({ fields: updated.signatureFields ?? [] });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return Response.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { auth } from '@/lib/auth';
|
||||
import { db } from '@/lib/db';
|
||||
import { documentTemplates } from '@/lib/db/schema';
|
||||
import { and, eq, isNull } from 'drizzle-orm';
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session) return new Response('Unauthorized', { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const template = await db.query.documentTemplates.findFirst({
|
||||
where: and(eq(documentTemplates.id, id), isNull(documentTemplates.archivedAt)),
|
||||
});
|
||||
if (!template) return Response.json({ error: 'Not found' }, { status: 404 });
|
||||
|
||||
return Response.json(template.signatureFields ?? []);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { auth } from '@/lib/auth';
|
||||
import { db } from '@/lib/db';
|
||||
import { documentTemplates } from '@/lib/db/schema';
|
||||
import { and, eq, isNull } from 'drizzle-orm';
|
||||
import path from 'node:path';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
const SEEDS_FORMS_DIR = path.join(process.cwd(), 'seeds', 'forms');
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session) return new Response('Unauthorized', { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const template = await db.query.documentTemplates.findFirst({
|
||||
where: and(eq(documentTemplates.id, id), isNull(documentTemplates.archivedAt)),
|
||||
with: { formTemplate: true },
|
||||
});
|
||||
if (!template?.formTemplate) return Response.json({ error: 'Not found' }, { status: 404 });
|
||||
|
||||
const filePath = path.join(SEEDS_FORMS_DIR, template.formTemplate.filename);
|
||||
// Path traversal guard
|
||||
if (!filePath.startsWith(SEEDS_FORMS_DIR)) return new Response('Forbidden', { status: 403 });
|
||||
|
||||
try {
|
||||
const file = await readFile(filePath);
|
||||
return new Response(file, {
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `inline; filename="${template.formTemplate.filename}"`,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return Response.json({ error: 'Form PDF not found on disk' }, { status: 404 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user