Sistema de Apoio ao Processo Legislativo
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

804 lines
29 KiB

"""
Views para assinatura digital de PDFs de Matérias Legislativas.
Suporta certificados A1 (arquivo .pfx/.p12) e A3 (token USB/smartcard).
"""
import io
import json
import logging
import os
import tempfile
from datetime import datetime
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.files.base import ContentFile
from django.http import HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from sapl.base.models import AppConfig
from sapl.materia.models import MateriaLegislativa
logger = logging.getLogger(__name__)
def _gerar_pdf_da_materia(materia, request):
"""
Gera o PDF da matéria para assinatura.
Primeiro tenta usar o PDF existente, depois converte DOCX via OnlyOffice.
Retorna bytes do PDF ou None em caso de erro.
"""
import requests as http_requests
import xml.etree.ElementTree as ET
from django.urls import reverse
# Se não tem documento, retorna None
if not materia.texto_original:
return None, "Matéria não possui documento de texto original."
file_name = materia.texto_original.name.lower()
# Se já é PDF, retorna diretamente
if file_name.endswith('.pdf'):
try:
with open(materia.texto_original.path, 'rb') as f:
return f.read(), None
except Exception as e:
logger.error(f"Erro ao ler arquivo PDF: {e}")
return None, f"Erro ao ler o arquivo PDF: {e}"
# Converter DOCX para PDF via OnlyOffice
from sapl.materia.onlyoffice_materia_views import generate_file_key
download_url = request.build_absolute_uri(
reverse('sapl.materia:materia_onlyoffice_download', kwargs={'pk': materia.pk})
)
# URL da API de conversão do OnlyOffice
conversion_url = f'{settings.ONLYOFFICE_URL}/ConvertService.ashx'
conversion_data = {
"async": False,
"filetype": "docx",
"key": generate_file_key("materia_sign", materia.pk, request.user.pk),
"outputtype": "pdf",
"title": f"Materia_{materia.tipo}_{materia.numero}_{materia.ano}.pdf",
"url": download_url,
}
# Adiciona JWT se estiver habilitado
if getattr(settings, 'ONLYOFFICE_JWT_ENABLED', False) and getattr(settings, 'ONLYOFFICE_JWT_SECRET', None):
import jwt
token = jwt.encode(conversion_data, settings.ONLYOFFICE_JWT_SECRET, algorithm='HS256')
conversion_data['token'] = token
try:
headers = {'Content-Type': 'application/json'}
if getattr(settings, 'ONLYOFFICE_JWT_ENABLED', False) and getattr(settings, 'ONLYOFFICE_JWT_SECRET', None):
import jwt
header_token = jwt.encode({"payload": conversion_data}, settings.ONLYOFFICE_JWT_SECRET, algorithm='HS256')
headers['Authorization'] = f'Bearer {header_token}'
conversion_response = http_requests.post(
conversion_url,
json=conversion_data,
headers=headers,
timeout=60
)
if conversion_response.status_code != 200:
return None, f"Erro na conversão OnlyOffice: status={conversion_response.status_code}"
root = ET.fromstring(conversion_response.text)
error_elem = root.find('Error')
if error_elem is not None:
return None, f"Erro na conversão do documento: código {error_elem.text}"
file_url_elem = root.find('FileUrl')
if file_url_elem is None or not file_url_elem.text:
return None, "URL do PDF não retornada pelo OnlyOffice"
pdf_url = file_url_elem.text
pdf_response = http_requests.get(pdf_url, timeout=60)
if pdf_response.status_code != 200:
return None, f"Erro ao baixar PDF convertido: status={pdf_response.status_code}"
return pdf_response.content, None
except Exception as e:
logger.error(f"Erro na geração de PDF: {e}")
return None, f"Erro inesperado: {e}"
@login_required
@csrf_exempt
@require_http_methods(["POST"])
def materia_assinar_a1(request, pk):
"""
Assina o PDF da matéria com certificado A1 (arquivo .pfx/.p12).
Parâmetros POST:
- certificado: arquivo .pfx ou .p12
- senha: senha do certificado
"""
materia = get_object_or_404(MateriaLegislativa, pk=pk)
# Permissão: qualquer usuário autenticado pode assinar
# (a autenticação é garantida pelo decorator @login_required)
# Verifica se já está assinada
if materia.pdf_assinado:
return JsonResponse({
'success': False,
'error': 'Esta matéria já possui um PDF assinado.'
}, status=400)
# Obtém dados do formulário
certificado_file = request.FILES.get('certificado')
senha = request.POST.get('senha', '')
if not certificado_file:
return JsonResponse({
'success': False,
'error': 'Certificado não informado.'
}, status=400)
if not senha:
return JsonResponse({
'success': False,
'error': 'Senha do certificado não informada.'
}, status=400)
# Gera o PDF da matéria
pdf_bytes, error = _gerar_pdf_da_materia(materia, request)
if error:
return JsonResponse({
'success': False,
'error': error
}, status=400)
try:
from pyhanko.sign import signers, fields
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
from pyhanko.keys import load_cert_from_pemder
from pyhanko_certvalidator import ValidationContext
# Lê o certificado
cert_data = certificado_file.read()
# Cria o assinador - salva temporariamente o certificado
import tempfile as tmp_module
with tmp_module.NamedTemporaryFile(delete=False, suffix='.pfx') as tmp_cert:
tmp_cert.write(cert_data)
tmp_cert_path = tmp_cert.name
try:
signer = signers.SimpleSigner.load_pkcs12(
pfx_file=tmp_cert_path,
passphrase=senha.encode('utf-8')
)
# Limpa arquivo temporário do certificado após carregar
if os.path.exists(tmp_cert_path):
os.unlink(tmp_cert_path)
except Exception as cert_error:
logger.error(f"Erro ao carregar certificado: {cert_error}")
# Limpa arquivo temporário do certificado
if os.path.exists(tmp_cert_path):
os.unlink(tmp_cert_path)
error_msg = str(cert_error)
if 'password' in error_msg.lower() or 'mac' in error_msg.lower():
error_detail = 'Senha incorreta.'
elif 'decode' in error_msg.lower() or 'parse' in error_msg.lower():
error_detail = 'Arquivo não é um certificado válido (.pfx/.p12).'
else:
error_detail = f'Detalhes: {error_msg}'
return JsonResponse({
'success': False,
'error': f'Erro ao carregar certificado: {error_detail}'
}, status=400)
# Verifica validade do certificado
cert_info = signer.signing_cert
now = timezone.now()
# Converte datas do certificado para timezone-aware se necessário
valid_before = cert_info.not_valid_before
valid_after = cert_info.not_valid_after
if valid_before.tzinfo is None:
import pytz
valid_before = pytz.UTC.localize(valid_before)
if valid_after.tzinfo is None:
import pytz
valid_after = pytz.UTC.localize(valid_after)
if now < valid_before or now > valid_after:
return JsonResponse({
'success': False,
'error': 'Certificado expirado ou ainda não válido.'
}, status=400)
# Cria arquivo temporário para o PDF com carimbo
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_stamped:
temp_stamped_path = temp_stamped.name
try:
from pyhanko.sign.fields import SigSeedSubFilter
from pyhanko.pdf_utils import text
from pyhanko.sign.general import SigningError
from PyPDF4 import PdfFileReader, PdfFileWriter
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib.utils import ImageReader
import re
# Informações do assinante
nome_assinante = request.user.get_full_name() or request.user.username
data_assinatura = timezone.localtime(timezone.now())
data_formatada = data_assinatura.strftime('%d/%m/%Y %H:%M:%S')
# Tenta obter cargo do usuário (se for parlamentar/autor)
cargo = "Usuário do Sistema"
parlamentar = None
try:
from sapl.parlamentares.models import Parlamentar
parlamentar = Parlamentar.objects.filter(
usuario=request.user
).first()
if parlamentar:
cargo = "Vereador(a)"
tipo_nome = AppConfig.attr('assinatura_nome')
if tipo_nome == 'C':
nome_assinante = parlamentar.nome_completo
else:
nome_assinante = parlamentar.nome_parlamentar
except:
pass
# Determina tipo de certificado baseado no issuer
issuer_str = str(cert_info.issuer).upper()
if 'ICP-BRASIL' in issuer_str or 'ICP BRASIL' in issuer_str:
tipo_cert = "ICP-Brasil"
else:
tipo_cert = "Certificado Digital"
# Tenta obter CPF do parlamentar ou do certificado
cpf = ""
try:
if parlamentar and parlamentar.cpf:
cpf = parlamentar.cpf
except:
pass
# Se não encontrou CPF no parlamentar, tenta extrair do certificado
if not cpf:
subject_str = str(cert_info.subject)
cpf_match = re.search(r'\d{3}\.?\d{3}\.?\d{3}-?\d{2}', subject_str)
if cpf_match:
cpf = cpf_match.group()
# Formata data
data_simples = data_assinatura.strftime('%d/%m/%Y %H:%M')
# ===== PASSO 1: Criar carimbo visual e adicionar ao PDF ANTES de assinar =====
# Cria PDF com o carimbo de assinatura
stamp_buffer = io.BytesIO()
c = canvas.Canvas(stamp_buffer, pagesize=A4)
# Posição do carimbo (canto inferior esquerdo)
y_pos = 15 * mm
x_pos = 10 * mm
largura_carimbo = 70 * mm
altura_carimbo = 22 * mm
logo_width = 18 * mm
# Desenha borda fina do carimbo
c.setStrokeColorRGB(0.5, 0.5, 0.5)
c.setLineWidth(0.5)
c.rect(x_pos, y_pos, largura_carimbo, altura_carimbo)
# Texto do carimbo (lado esquerdo)
c.setFont("Helvetica", 6)
c.setFillColorRGB(0.3, 0.3, 0.3)
c.drawString(x_pos + 3*mm, y_pos + 17*mm, "Assinado digitalmente por")
c.setFont("Helvetica-Bold", 7)
c.setFillColorRGB(0, 0, 0)
nome_upper = nome_assinante.upper()
if len(nome_upper) > 28:
nome_upper = nome_upper[:28] + "..."
c.drawString(x_pos + 3*mm, y_pos + 12*mm, nome_upper)
c.setFont("Helvetica", 6)
c.setFillColorRGB(0.3, 0.3, 0.3)
if cpf:
c.drawString(x_pos + 3*mm, y_pos + 7*mm, f"CPF: {cpf}")
c.drawString(x_pos + 3*mm, y_pos + 3*mm, f"Data: {data_simples}")
else:
c.drawString(x_pos + 3*mm, y_pos + 5*mm, f"Data: {data_simples}")
# Logo da câmara (lado direito)
try:
logo_path = None
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
possible_paths = [
os.path.join(base_dir, 'sapl/static/sapl/frontend/img/logo-camara-padrao.png'),
os.path.join(base_dir, 'sapl/static/sapl/frontend/img/logo.png'),
os.path.join(base_dir, 'sapl/static/sapl/frontend/img/pdflogo.png'),
os.path.join(settings.MEDIA_ROOT, 'sapl/public/casa/logotipo/logo.png'),
os.path.join(settings.MEDIA_ROOT, 'sapl/public/casa/logotipo/logotipo.png'),
]
logo_dir = os.path.join(settings.MEDIA_ROOT, 'sapl/public/casa/logotipo')
if os.path.exists(logo_dir):
for f in os.listdir(logo_dir):
if f.lower().endswith(('.png', '.jpg', '.jpeg')):
possible_paths.insert(0, os.path.join(logo_dir, f))
for path in possible_paths:
if os.path.exists(path):
logo_path = path
break
if logo_path:
logo = ImageReader(logo_path)
logo_x = x_pos + largura_carimbo - logo_width - 2*mm
logo_y = y_pos + 2*mm
c.drawImage(logo, logo_x, logo_y,
width=logo_width, height=logo_width,
preserveAspectRatio=True, mask='auto')
except Exception as logo_error:
logger.warning(f"Não foi possível adicionar logo: {logo_error}")
c.save()
stamp_buffer.seek(0)
# Mescla o carimbo com o PDF original (ANTES de assinar)
stamp_pdf = PdfFileReader(stamp_buffer)
original_pdf = PdfFileReader(io.BytesIO(pdf_bytes))
output_pdf = PdfFileWriter()
for page_num in range(original_pdf.getNumPages()):
page = original_pdf.getPage(page_num)
if page_num == original_pdf.getNumPages() - 1: # Última página
page.mergePage(stamp_pdf.getPage(0))
output_pdf.addPage(page)
# Salva o PDF com carimbo em arquivo temporário
with open(temp_stamped_path, 'wb') as f:
output_pdf.write(f)
# ===== PASSO 2: Assinar o PDF que já contém o carimbo =====
with open(temp_stamped_path, 'rb') as stamped_file:
stamped_bytes = stamped_file.read()
signed_buffer = io.BytesIO()
with io.BytesIO(stamped_bytes) as inf:
w = IncrementalPdfFileWriter(inf)
# Metadados da assinatura
meta = signers.PdfSignatureMetadata(
field_name='AssinaturaDigital',
location='Câmara Municipal',
reason='Documento assinado digitalmente nos termos da MP 2.200-2/2001',
name=nome_assinante
)
# Executa assinatura
signers.sign_pdf(
w,
meta,
signer=signer,
output=signed_buffer
)
signed_buffer.seek(0)
signed_pdf_content = signed_buffer.read()
# Salva o PDF assinado no modelo
filename = f"materia_{materia.pk}_assinado_{int(timezone.now().timestamp())}.pdf"
materia.pdf_assinado.save(filename, ContentFile(signed_pdf_content), save=False)
# Salva informações da assinatura
materia.assinatura_info = {
'tipo_certificado': 'A1',
'tipo_certificado_display': f'{tipo_cert} – A1',
'subject': str(cert_info.subject),
'issuer': str(cert_info.issuer),
'serial': str(cert_info.serial_number),
'valid_from': cert_info.not_valid_before.isoformat(),
'valid_to': cert_info.not_valid_after.isoformat(),
'signed_by': request.user.username,
'nome_assinante': nome_assinante,
'cargo': cargo,
'data_assinatura': data_formatada,
'validade_juridica': 'Assinatura Eletrônica Qualificada'
}
materia.assinado_em = timezone.now()
materia.assinado_por = request.user
materia.save()
logger.info(f"Matéria {materia.pk} assinada por {request.user.username}")
return JsonResponse({
'success': True,
'message': 'PDF assinado com sucesso!',
'certificado': {
'nome': str(cert_info.subject),
'validade': cert_info.not_valid_after.strftime('%d/%m/%Y')
}
})
finally:
# Remove arquivo temporário
if os.path.exists(temp_stamped_path):
os.unlink(temp_stamped_path)
except ImportError:
logger.error("pyhanko não está instalado")
return JsonResponse({
'success': False,
'error': 'Biblioteca de assinatura não instalada. Contate o administrador.'
}, status=500)
except Exception as e:
logger.error(f"Erro ao assinar PDF: {e}")
return JsonResponse({
'success': False,
'error': f'Erro ao assinar o PDF: {str(e)}'
}, status=500)
@login_required
@require_http_methods(["POST"])
def materia_assinar_a3_preparar(request, pk):
"""
Prepara a assinatura A3 gerando o hash do PDF para ser assinado pelo token.
Retorna:
- hash: hash SHA-256 do PDF em hexadecimal
- pdf_base64: PDF codificado em base64 (para assinatura no cliente)
"""
import base64
import hashlib
materia = get_object_or_404(MateriaLegislativa, pk=pk)
# Verifica permissão
if not request.user.has_perm('materia.change_materialegislativa'):
return JsonResponse({
'success': False,
'error': 'Você não tem permissão para assinar esta matéria.'
}, status=403)
# Verifica se já está assinada
if materia.pdf_assinado:
return JsonResponse({
'success': False,
'error': 'Esta matéria já possui um PDF assinado.'
}, status=400)
# Gera o PDF da matéria
pdf_bytes, error = _gerar_pdf_da_materia(materia, request)
if error:
return JsonResponse({
'success': False,
'error': error
}, status=400)
# Calcula o hash SHA-256
pdf_hash = hashlib.sha256(pdf_bytes).hexdigest()
# Codifica o PDF em base64
pdf_base64 = base64.b64encode(pdf_bytes).decode('utf-8')
return JsonResponse({
'success': True,
'hash': pdf_hash,
'pdf_base64': pdf_base64,
'materia_id': materia.pk
})
@login_required
@require_http_methods(["POST"])
@csrf_exempt
def materia_assinar_a3_finalizar(request, pk):
"""
Finaliza a assinatura A3 incorporando a assinatura recebida do token.
Parâmetros POST (JSON):
- signature: assinatura em base64
- certificate: certificado em base64
- certificate_chain: cadeia de certificados em base64 (opcional)
"""
import base64
materia = get_object_or_404(MateriaLegislativa, pk=pk)
# Verifica permissão
if not request.user.has_perm('materia.change_materialegislativa'):
return JsonResponse({
'success': False,
'error': 'Você não tem permissão para assinar esta matéria.'
}, status=403)
# Verifica se já está assinada
if materia.pdf_assinado:
return JsonResponse({
'success': False,
'error': 'Esta matéria já possui um PDF assinado.'
}, status=400)
try:
data = json.loads(request.body.decode('utf-8'))
except json.JSONDecodeError:
return JsonResponse({
'success': False,
'error': 'Dados inválidos.'
}, status=400)
signature_b64 = data.get('signature')
certificate_b64 = data.get('certificate')
if not signature_b64 or not certificate_b64:
return JsonResponse({
'success': False,
'error': 'Assinatura ou certificado não informados.'
}, status=400)
# Gera o PDF da matéria
pdf_bytes, error = _gerar_pdf_da_materia(materia, request)
if error:
return JsonResponse({
'success': False,
'error': error
}, status=400)
try:
from pyhanko.sign import signers, fields
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
from pyhanko.sign.signers.pdf_cms import ExternalSigner
from cryptography import x509
from cryptography.hazmat.backends import default_backend
# Decodifica o certificado
cert_der = base64.b64decode(certificate_b64)
cert = x509.load_der_x509_certificate(cert_der, default_backend())
# Verifica validade do certificado
now = datetime.utcnow()
if now < cert.not_valid_before or now > cert.not_valid_after:
return JsonResponse({
'success': False,
'error': 'Certificado expirado ou ainda não válido.'
}, status=400)
# Decodifica a assinatura
signature = base64.b64decode(signature_b64)
# Cria arquivo temporário para o PDF assinado
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_signed:
temp_signed_path = temp_signed.name
try:
# Cria o signer externo com a assinatura pré-computada
# Nota: Esta é uma implementação simplificada
# Em produção, seria necessário usar o ExternalSigner corretamente
with io.BytesIO(pdf_bytes) as inf:
w = IncrementalPdfFileWriter(inf)
# Adiciona campo de assinatura
sig_field = fields.SigFieldSpec(
sig_field_name='AssinaturaDigital',
box=(50, 50, 250, 100)
)
# Metadados da assinatura
meta = signers.PdfSignatureMetadata(
field_name='AssinaturaDigital',
location='Câmara Municipal',
reason='Assinatura Digital de Matéria Legislativa (A3)',
name=request.user.get_full_name() or request.user.username
)
# Para assinatura A3, precisamos de integração mais complexa
# Por enquanto, retornamos erro informando que A3 requer aplicação local
return JsonResponse({
'success': False,
'error': 'Assinatura A3 requer aplicação local. Use o Assinador SERPRO ou similar.'
}, status=501)
finally:
if os.path.exists(temp_signed_path):
os.unlink(temp_signed_path)
except ImportError as e:
logger.error(f"Biblioteca não instalada: {e}")
return JsonResponse({
'success': False,
'error': 'Biblioteca de assinatura não instalada.'
}, status=500)
except Exception as e:
logger.error(f"Erro ao finalizar assinatura A3: {e}")
return JsonResponse({
'success': False,
'error': f'Erro ao processar assinatura: {str(e)}'
}, status=500)
@login_required
@require_http_methods(["GET"])
def materia_pdf_assinado(request, pk):
"""
Retorna o PDF assinado da matéria para download/visualização.
"""
materia = get_object_or_404(MateriaLegislativa, pk=pk)
if not materia.pdf_assinado:
messages.error(request, 'Esta matéria não possui PDF assinado.')
return redirect('sapl.materia:materialegislativa_detail', pk=pk)
try:
with open(materia.pdf_assinado.path, 'rb') as f:
content = f.read()
filename = f"Materia_{materia.tipo}_{materia.numero}_{materia.ano}_ASSINADO.pdf"
filename = filename.replace(' ', '_').replace('/', '-')
response = HttpResponse(content, content_type='application/pdf')
response['Content-Disposition'] = f'inline; filename="{filename}"'
return response
except Exception as e:
logger.error(f"Erro ao ler PDF assinado: {e}")
messages.error(request, 'Erro ao ler o arquivo PDF assinado.')
return redirect('sapl.materia:materialegislativa_detail', pk=pk)
@login_required
@require_http_methods(["GET"])
def materia_verificar_assinatura(request, pk):
"""
Verifica a assinatura digital do PDF da matéria.
Retorna informações sobre as assinaturas encontradas.
"""
materia = get_object_or_404(MateriaLegislativa, pk=pk)
if not materia.pdf_assinado:
return JsonResponse({
'success': False,
'error': 'Esta matéria não possui PDF assinado.',
'assinado': False
})
try:
from pyhanko.sign.validation import validate_pdf_signature
from pyhanko.pdf_utils.reader import PdfFileReader
with open(materia.pdf_assinado.path, 'rb') as f:
reader = PdfFileReader(f)
# Lista de assinaturas encontradas
assinaturas = []
# Verifica cada assinatura no PDF
for sig_field_name in reader.embedded_signatures:
try:
sig = reader.embedded_signatures[sig_field_name]
# Informações básicas da assinatura
sig_info = {
'campo': sig_field_name,
'assinante': str(sig.signer_cert.subject) if sig.signer_cert else 'Desconhecido',
'data': sig.self_reported_timestamp.isoformat() if sig.self_reported_timestamp else None,
}
assinaturas.append(sig_info)
except Exception as sig_error:
logger.warning(f"Erro ao verificar assinatura {sig_field_name}: {sig_error}")
assinaturas.append({
'campo': sig_field_name,
'erro': str(sig_error)
})
return JsonResponse({
'success': True,
'assinado': True,
'total_assinaturas': len(assinaturas),
'assinaturas': assinaturas,
'info_salva': materia.assinatura_info
})
except ImportError:
# Se pyhanko não estiver instalado, retorna info salva no modelo
return JsonResponse({
'success': True,
'assinado': True,
'info_salva': materia.assinatura_info,
'verificacao_disponivel': False
})
except Exception as e:
logger.error(f"Erro ao verificar assinatura: {e}")
return JsonResponse({
'success': False,
'error': f'Erro ao verificar assinatura: {str(e)}',
'assinado': True,
'info_salva': materia.assinatura_info
})
@login_required
@require_http_methods(["POST"])
def materia_remover_assinatura(request, pk):
"""
Remove a assinatura digital da matéria.
Apenas superusuários podem executar esta ação.
"""
if not request.user.is_superuser:
return JsonResponse({
'success': False,
'error': 'Apenas administradores podem remover assinaturas.'
}, status=403)
materia = get_object_or_404(MateriaLegislativa, pk=pk)
if not materia.pdf_assinado:
return JsonResponse({
'success': False,
'error': 'Esta matéria não possui PDF assinado.'
}, status=400)
try:
# Remove o arquivo
materia.pdf_assinado.delete(save=False)
# Limpa os campos
materia.pdf_assinado = None
materia.assinatura_info = None
materia.assinado_em = None
materia.assinado_por = None
materia.save()
logger.info(f"Assinatura da matéria {materia.pk} removida por {request.user.username}")
return JsonResponse({
'success': True,
'message': 'Assinatura removida com sucesso.'
})
except Exception as e:
logger.error(f"Erro ao remover assinatura: {e}")
return JsonResponse({
'success': False,
'error': f'Erro ao remover assinatura: {str(e)}'
}, status=500)
@login_required
@require_http_methods(["GET"])
def detectar_aplicacao_a3(request):
"""
Endpoint para verificar se há uma aplicação de assinatura A3 rodando localmente.
O frontend usa isso para decidir se mostra a opção A3.
"""
# Lista de portas comuns usadas por aplicações de assinatura
portas_conhecidas = [
{'nome': 'Assinador SERPRO', 'porta': 10443},
{'nome': 'Web PKI Local', 'porta': 5000},
{'nome': 'Signer Local', 'porta': 8080},
]
return JsonResponse({
'success': True,
'portas_conhecidas': portas_conhecidas,
'instrucoes': 'O frontend deve tentar conectar a cada porta para detectar a aplicação.'
})