- Remplacer Cloudinary (US) par solution 100% AWS eu-west-3 - Lambda odentas-sign-pdf-converter avec pdftoppm - Lambda Layer poppler-utils v5 avec dépendances complètes - Trigger S3 ObjectCreated pour conversion automatique - Support multi-pages validé (PDF 3 pages) - Stockage images dans S3 odentas-docs - PDFImageViewer pour affichage images converties - Conformité RGPD garantie (données EU uniquement)
210 lines
6.6 KiB
TypeScript
210 lines
6.6 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { verifySignatureSession } from '@/lib/odentas-sign/jwt';
|
|
import { supabaseAdmin } from '@/lib/odentas-sign/supabase';
|
|
import { getPresignedDownloadUrl } from '@/lib/odentas-sign/s3';
|
|
import {
|
|
extractPlaceholdersFromPdfBuffer,
|
|
countPdfPagesFromBytes,
|
|
estimatePositionsFromPlaceholders,
|
|
} from '@/lib/odentas-sign/placeholders';
|
|
|
|
/**
|
|
* GET /api/odentas-sign/requests/:id/positions
|
|
* Récupère les positions de signature pour toutes les parties
|
|
*/
|
|
export async function GET(
|
|
request: NextRequest,
|
|
{ params }: { params: { id: string } }
|
|
) {
|
|
try {
|
|
const requestId = params.id;
|
|
|
|
// Vérifier le token JWT
|
|
const authHeader = request.headers.get('Authorization');
|
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
return NextResponse.json(
|
|
{ error: 'Token manquant ou invalide' },
|
|
{ status: 401 }
|
|
);
|
|
}
|
|
|
|
const token = authHeader.split(' ')[1];
|
|
const payload = verifySignatureSession(token);
|
|
|
|
if (!payload || payload.requestId !== requestId) {
|
|
return NextResponse.json(
|
|
{ error: 'Token invalide ou expiré' },
|
|
{ status: 401 }
|
|
);
|
|
}
|
|
|
|
// Récupérer toutes les positions de signature existantes
|
|
const { data: positions, error } = await supabaseAdmin
|
|
.from('sign_positions')
|
|
.select('page, x, y, w, h, role')
|
|
.eq('request_id', requestId)
|
|
.order('role');
|
|
|
|
if (error) {
|
|
console.error('Erreur DB lors de la récupération des positions:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Erreur lors de la récupération des positions' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
// Si positions déjà présentes, renvoyer directement
|
|
if (positions && positions.length > 0) {
|
|
const transformedPositions = positions.map((p) => ({
|
|
page: p.page,
|
|
x: p.x,
|
|
y: p.y,
|
|
width: p.w,
|
|
height: p.h,
|
|
role: p.role,
|
|
}));
|
|
return NextResponse.json({ positions: transformedPositions });
|
|
}
|
|
|
|
// Pas de positions en DB: tenter d'extraire depuis le PDF via placeholders
|
|
// 1) Récupérer la clé S3 du PDF source
|
|
const { data: signRequest, error: requestErr } = await supabaseAdmin
|
|
.from('sign_requests')
|
|
.select('source_s3_key')
|
|
.eq('id', requestId)
|
|
.single();
|
|
|
|
if (requestErr || !signRequest?.source_s3_key) {
|
|
console.error('Impossible de récupérer sign_request pour extraction:', requestErr);
|
|
return NextResponse.json({ positions: [] });
|
|
}
|
|
|
|
// 2) Générer une URL présignée et télécharger le PDF
|
|
const pdfUrl = await getPresignedDownloadUrl(signRequest.source_s3_key, 300);
|
|
const resp = await fetch(pdfUrl);
|
|
if (!resp.ok) {
|
|
console.error('Téléchargement du PDF échoué:', resp.status, pdfUrl);
|
|
return NextResponse.json({ positions: [] });
|
|
}
|
|
const arrayBuf = await resp.arrayBuffer();
|
|
const bytes = Buffer.from(arrayBuf);
|
|
|
|
// 3) Fallback regex + estimation (pas d'extraction pdfjs côté serveur)
|
|
const placeholders = extractPlaceholdersFromPdfBuffer(bytes);
|
|
if (!placeholders || placeholders.length === 0) {
|
|
return NextResponse.json({ positions: [] });
|
|
}
|
|
const pageCount = countPdfPagesFromBytes(bytes);
|
|
const precise = estimatePositionsFromPlaceholders(placeholders, pageCount);
|
|
|
|
// 5) Persister en DB pour cette demande (meilleure UX aux prochains chargements)
|
|
try {
|
|
const rows = precise.map((pos) => ({
|
|
request_id: requestId,
|
|
role: pos.role,
|
|
page: pos.page,
|
|
x: pos.x, // mm
|
|
y: pos.y, // mm
|
|
w: pos.width, // mm
|
|
h: pos.height, // mm
|
|
kind: 'signature',
|
|
label: pos.label,
|
|
}));
|
|
const { error: insertErr } = await supabaseAdmin.from('sign_positions').insert(rows);
|
|
if (insertErr) {
|
|
console.warn('Insertion positions estimées échouée (non bloquant):', insertErr);
|
|
}
|
|
} catch (e) {
|
|
console.warn('Erreur lors de la persistance des positions estimées:', e);
|
|
}
|
|
|
|
// 6) Retourner au format attendu par le front
|
|
const transformedPositions = precise.map((p) => ({
|
|
page: p.page,
|
|
x: p.x,
|
|
y: p.y,
|
|
width: p.width,
|
|
height: p.height,
|
|
role: p.role,
|
|
}));
|
|
|
|
return NextResponse.json({ positions: transformedPositions });
|
|
|
|
} catch (error) {
|
|
console.error('Erreur lors de la récupération des positions:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Erreur serveur lors de la récupération des positions' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/odentas-sign/requests/:id/positions
|
|
* Upsert des positions détectées côté client (mm)
|
|
*/
|
|
export async function POST(
|
|
request: NextRequest,
|
|
{ params }: { params: { id: string } }
|
|
) {
|
|
try {
|
|
const requestId = params.id;
|
|
|
|
// Vérifier le token JWT
|
|
const authHeader = request.headers.get('Authorization');
|
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
return NextResponse.json(
|
|
{ error: 'Token manquant ou invalide' },
|
|
{ status: 401 }
|
|
);
|
|
}
|
|
const token = authHeader.split(' ')[1];
|
|
const payload = verifySignatureSession(token);
|
|
if (!payload || payload.requestId !== requestId) {
|
|
return NextResponse.json(
|
|
{ error: 'Token invalide ou expiré' },
|
|
{ status: 401 }
|
|
);
|
|
}
|
|
|
|
const body = await request.json();
|
|
const positions = Array.isArray(body?.positions) ? body.positions : [];
|
|
if (positions.length === 0) {
|
|
return NextResponse.json({ success: false, message: 'Aucune position fournie' }, { status: 400 });
|
|
}
|
|
|
|
// Nettoyer les positions existantes de type signature pour cette requête
|
|
await supabaseAdmin
|
|
.from('sign_positions')
|
|
.delete()
|
|
.eq('request_id', requestId)
|
|
.eq('kind', 'signature');
|
|
|
|
// Insérer les nouvelles positions (mm)
|
|
const rows = positions.map((p: any) => ({
|
|
request_id: requestId,
|
|
role: p.role,
|
|
page: p.page,
|
|
x: p.x,
|
|
y: p.y,
|
|
w: p.w,
|
|
h: p.h,
|
|
kind: p.kind || 'signature',
|
|
label: p.label || null,
|
|
}));
|
|
|
|
const { error: insertErr } = await supabaseAdmin
|
|
.from('sign_positions')
|
|
.insert(rows);
|
|
|
|
if (insertErr) {
|
|
console.error('Erreur insertion positions (POST):', insertErr);
|
|
return NextResponse.json({ error: 'Insertion des positions échouée' }, { status: 500 });
|
|
}
|
|
|
|
return NextResponse.json({ success: true, inserted: rows.length });
|
|
} catch (error) {
|
|
console.error('Erreur POST positions:', error);
|
|
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
}
|
|
}
|