espace-paie-odentas/app/api/odentas-sign/requests/[id]/positions/route.ts
odentas 59749d481b feat: Migration Cloudinary vers Poppler pour conversion PDF→JPEG
- 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)
2025-10-28 10:22:45 +01:00

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 });
}
}