|
|
@ -85,72 +85,178 @@ def _extrair_posicao_custom(post_data): |
|
|
'sig_height': float(sig_height), |
|
|
'sig_height': float(sig_height), |
|
|
'sig_page': int(sig_page), |
|
|
'sig_page': int(sig_page), |
|
|
} |
|
|
} |
|
|
except (ValueError, TypeError): |
|
|
except (ValueError, TypeError) as exc: |
|
|
|
|
|
# Descartar a posição em silêncio faz a assinatura cair no bloco automático |
|
|
|
|
|
# da grade em vez de onde foi pedido — diferença de layout que ninguém |
|
|
|
|
|
# consegue explicar depois olhando só o PDF. |
|
|
|
|
|
logger.warning( |
|
|
|
|
|
f'Posicao customizada da assinatura ignorada (valores invalidos): {exc}' |
|
|
|
|
|
) |
|
|
return None |
|
|
return None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _assinar_pdf_com_pagina_auth(pdf_bytes, *, request, tipo_doc, pk_doc, |
|
|
class ErroAssinaturaUsuario(Exception): |
|
|
assinaturas_existentes, |
|
|
|
|
|
certificado_bytes=None, senha=None, |
|
|
|
|
|
assinatura_a3_bytes=None, cert_chain_bytes=None, |
|
|
|
|
|
tipo_cert_input='a1', |
|
|
|
|
|
posicao_custom=None): |
|
|
|
|
|
""" |
|
|
""" |
|
|
Orquestra a assinatura digital completa de um PDF: |
|
|
Erro que o operador entende e consegue corrigir sozinho. |
|
|
- Monta a página de autenticação (1ª assinatura) ou assina incrementalmente |
|
|
|
|
|
(assinaturas subsequentes). |
|
|
|
|
|
- Usa a API externa quando ASSINATURA_API_URL estiver configurado; |
|
|
|
|
|
caso contrário usa pyhanko localmente. |
|
|
|
|
|
|
|
|
|
|
|
posicao_custom : dict opcional com chaves sig_left, sig_bottom, sig_width, |
|
|
Senha errada, arquivo que não é um .pfx, certificado vencido: nada disso é |
|
|
sig_height (em pontos PDF) e sig_page (1-based). |
|
|
falha do sistema, e responder 500 com o texto cru do erro só assusta quem |
|
|
Quando fornecido, substitui o cálculo automático de grid. |
|
|
está assinando. Separado do Exception genérico para as views devolverem 400 |
|
|
|
|
|
com a mensagem — que é o que o caminho local já fazia em `_validar_certificado`. |
|
|
|
|
|
""" |
|
|
|
|
|
|
|
|
Retorna (signed_pdf_bytes, nova_assinatura_dict, codigo_autenticacao_ou_None). |
|
|
|
|
|
- codigo_autenticacao_ou_None é não-None apenas na 1ª assinatura. |
|
|
|
|
|
|
|
|
|
|
|
Lança Exception em caso de erro. |
|
|
def _erro_de_assinatura(exc): |
|
|
""" |
|
|
""" |
|
|
from PyPDF4 import PdfFileReader, PdfFileWriter |
|
|
Converte um AssinaturaAPIError na exceção certa para o chamador levantar. |
|
|
import base64 as _base64 |
|
|
|
|
|
|
|
|
|
|
|
ja_tem_pdf_assinado = bool(assinaturas_existentes) |
|
|
HTTP 400 do microserviço é recusa de validação (certificado fora da validade, |
|
|
|
|
|
senha incorreta, parâmetro faltando) — problema do usuário. Qualquer outra |
|
|
|
|
|
coisa (conexão, timeout, 5xx) é falha de infraestrutura e continua sendo erro |
|
|
|
|
|
genérico. |
|
|
|
|
|
""" |
|
|
|
|
|
if getattr(exc, 'status_code', None) == 400: |
|
|
|
|
|
return ErroAssinaturaUsuario(str(exc)) |
|
|
|
|
|
return Exception(str(exc)) |
|
|
|
|
|
|
|
|
# ── Informações do assinante ────────────────────────────────────────────── |
|
|
|
|
|
# Para API externa: não há signer local; tentamos extrair info do certificado |
|
|
|
|
|
# de forma leve (apenas para metadados visuais). |
|
|
|
|
|
if _usar_api_externa(): |
|
|
|
|
|
from sapl.materia.assinatura_api_client import assinar_pdf_via_api, AssinaturaAPIError |
|
|
|
|
|
|
|
|
|
|
|
# Obter nome/cargo via relação User → Autor (sem pyhanko) |
|
|
def _metadados_certificado_via_api(certificado_bytes, senha): |
|
|
|
|
|
""" |
|
|
|
|
|
Lê os metadados do certificado no microserviço (POST /validate-pfx). |
|
|
|
|
|
|
|
|
|
|
|
Existe por dois motivos: |
|
|
|
|
|
|
|
|
|
|
|
1. Preencher `subject`, `issuer`, `serial`, `valid_from` e `valid_to` no |
|
|
|
|
|
`assinatura_info`. Quando o SAPL delega a assinatura, ele não abre o PFX — |
|
|
|
|
|
e sem esta chamada esses campos ficavam string vazia, deixando emissor, |
|
|
|
|
|
série e validade em branco em tudo que lê o `assinatura_info`. |
|
|
|
|
|
2. Recusar certificado fora da validade antes de assinar, com a mesma |
|
|
|
|
|
mensagem do caminho local (`_validar_certificado`). |
|
|
|
|
|
|
|
|
|
|
|
A senha é usada só nesta requisição e não é persistida em lugar nenhum. |
|
|
|
|
|
|
|
|
|
|
|
Retorna dict com as chaves já no formato do `assinatura_info` (vazio se o |
|
|
|
|
|
microserviço não respondeu). Lança ErroAssinaturaUsuario para PFX recusado. |
|
|
|
|
|
""" |
|
|
|
|
|
from sapl.materia.assinatura_api_client import ( |
|
|
|
|
|
AssinaturaAPIError, validar_pfx_via_api, |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
info = validar_pfx_via_api(certificado_bytes, senha) or {} |
|
|
|
|
|
except AssinaturaAPIError as exc: |
|
|
|
|
|
if getattr(exc, 'status_code', None) == 400: |
|
|
|
|
|
# PFX ilegível ou senha incorreta: o /sign recusaria do mesmo jeito, |
|
|
|
|
|
# e aqui a mensagem chega antes de enviar o documento. |
|
|
|
|
|
raise ErroAssinaturaUsuario(str(exc)) |
|
|
|
|
|
# Indisponibilidade do /validate-pfx não pode impedir a assinatura: o |
|
|
|
|
|
# /sign também recusa certificado vencido. Só perdemos os metadados. |
|
|
|
|
|
logger.warning( |
|
|
|
|
|
f'[assinatura-api] Nao foi possivel ler os metadados do certificado ' |
|
|
|
|
|
f'(/validate-pfx): {exc}. A assinatura segue sem eles.' |
|
|
|
|
|
) |
|
|
|
|
|
return {} |
|
|
|
|
|
|
|
|
|
|
|
if info.get('is_valid_now') is False: |
|
|
|
|
|
validade = ' a '.join( |
|
|
|
|
|
v for v in (info.get('not_valid_before'), info.get('not_valid_after')) if v |
|
|
|
|
|
) |
|
|
|
|
|
raise ErroAssinaturaUsuario( |
|
|
|
|
|
'Certificado expirado ou ainda não válido.' |
|
|
|
|
|
+ (f' Validade: {validade}.' if validade else '') |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
serial = info.get('serial_number') |
|
|
|
|
|
return { |
|
|
|
|
|
'subject': info.get('subject') or '', |
|
|
|
|
|
'issuer': info.get('issuer') or '', |
|
|
|
|
|
'serial': '' if serial is None else str(serial), |
|
|
|
|
|
'valid_from': info.get('not_valid_before') or '', |
|
|
|
|
|
'valid_to': info.get('not_valid_after') or '', |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _nome_e_cargo_do_assinante(request): |
|
|
|
|
|
""" |
|
|
|
|
|
Nome e cargo que vão IMPRESSOS no documento, via Autor.operadores → Parlamentar. |
|
|
|
|
|
|
|
|
|
|
|
Fonte única para os dois backends. Existia em duas cópias — uma aqui e outra |
|
|
|
|
|
inline no ramo da API — que hoje coincidiam por acaso: mudar o critério em uma |
|
|
|
|
|
faria o mesmo vereador sair com nomes diferentes conforme o backend da casa. |
|
|
|
|
|
|
|
|
|
|
|
Quem assina é o vereador, então o nome impresso é o dele. O operador (assessor) |
|
|
|
|
|
fica registrado à parte, em `signed_by`. |
|
|
|
|
|
""" |
|
|
nome_assinante = request.user.get_full_name() or request.user.username |
|
|
nome_assinante = request.user.get_full_name() or request.user.username |
|
|
cargo = 'Usuário do Sistema' |
|
|
cargo = 'Usuário do Sistema' |
|
|
tipo_cert_display = 'Certificado Digital' |
|
|
|
|
|
|
|
|
|
|
|
try: |
|
|
try: |
|
|
from sapl.base.models import Autor |
|
|
from sapl.base.models import Autor |
|
|
from sapl.parlamentares.models import Parlamentar |
|
|
from sapl.parlamentares.models import Parlamentar |
|
|
|
|
|
|
|
|
autor = Autor.objects.filter(operadores=request.user).first() |
|
|
autor = Autor.objects.filter(operadores=request.user).first() |
|
|
if autor: |
|
|
if autor: |
|
|
tipo_desc = autor.tipo.descricao if autor.tipo else '' |
|
|
# Tipo do autor vira cargo (ex.: "Parlamentar" → "Vereador(a)") |
|
|
if tipo_desc == 'Parlamentar': |
|
|
tipo_descricao = autor.tipo.descricao if autor.tipo else '' |
|
|
|
|
|
if tipo_descricao == 'Parlamentar': |
|
|
cargo = 'Vereador(a)' |
|
|
cargo = 'Vereador(a)' |
|
|
elif tipo_desc: |
|
|
elif tipo_descricao: |
|
|
cargo = tipo_desc |
|
|
cargo = tipo_descricao |
|
|
|
|
|
|
|
|
if isinstance(autor.autor_related, Parlamentar): |
|
|
if isinstance(autor.autor_related, Parlamentar): |
|
|
parl = autor.autor_related |
|
|
parlamentar = autor.autor_related |
|
|
tipo_nome = AppConfig.attr('assinatura_nome') |
|
|
tipo_nome = AppConfig.attr('assinatura_nome') |
|
|
nome_assinante = parl.nome_completo if tipo_nome == 'C' else parl.nome_parlamentar |
|
|
nome_assinante = ( |
|
|
except Exception: |
|
|
parlamentar.nome_completo if tipo_nome == 'C' |
|
|
pass |
|
|
else parlamentar.nome_parlamentar |
|
|
|
|
|
) |
|
|
|
|
|
except Exception as exc: |
|
|
|
|
|
# Cair no username do operador significa imprimir no PDF um nome que não é |
|
|
|
|
|
# o do vereador. Não derruba a assinatura, mas não pode passar em silêncio. |
|
|
|
|
|
logger.warning( |
|
|
|
|
|
f'Nao foi possivel resolver o parlamentar de {request.user.username}: ' |
|
|
|
|
|
f'{exc}. A assinatura sai com "{nome_assinante}".' |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
data_assinatura = timezone.localtime(timezone.now()) |
|
|
return nome_assinante, cargo |
|
|
data_formatada = data_assinatura.strftime('%d/%m/%Y %H:%M:%S') |
|
|
|
|
|
data_simples = data_assinatura.strftime('%d/%m/%Y %H:%M') |
|
|
|
|
|
codigo_retorno = None |
|
|
def _tipo_certificado_pelo_emissor(issuer): |
|
|
|
|
|
"""Rotula o certificado a partir do emissor (mesma regra nos dois backends).""" |
|
|
|
|
|
issuer_str = str(issuer or '').upper() |
|
|
|
|
|
if 'ICP-BRASIL' in issuer_str or 'ICP BRASIL' in issuer_str: |
|
|
|
|
|
return 'ICP-Brasil' |
|
|
|
|
|
return 'Certificado Digital' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ler_brasao(): |
|
|
|
|
|
""" |
|
|
|
|
|
Bytes do logotipo da casa, ou None. |
|
|
|
|
|
|
|
|
|
|
|
Precisa ir em TODAS as assinaturas da mesma matéria: o brasão desloca a grade |
|
|
|
|
|
de blocos em 3 mm, então mandá-lo só na primeira desalinharia os blocos |
|
|
|
|
|
seguintes com o que já está desenhado na página. |
|
|
|
|
|
""" |
|
|
|
|
|
caminho = _encontrar_logo() |
|
|
|
|
|
if not caminho: |
|
|
|
|
|
return None |
|
|
|
|
|
try: |
|
|
|
|
|
with open(caminho, 'rb') as f: |
|
|
|
|
|
return f.read() |
|
|
|
|
|
except OSError as exc: |
|
|
|
|
|
logger.warning(f'Nao foi possivel ler o brasao em {caminho}: {exc}') |
|
|
|
|
|
return None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _compor_pagina_auth_localmente(pdf_bytes, request, tipo_doc, pk_doc, blocos): |
|
|
|
|
|
""" |
|
|
|
|
|
Anexa a página de autenticação ao PDF aqui mesmo (caminho pré-C3). |
|
|
|
|
|
|
|
|
|
|
|
Continua em uso pelo backend local (pyhanko no Django) e pelo caso de posição |
|
|
|
|
|
explícita, em que o microserviço não pode compor. Retorna (pdf, codigo). |
|
|
|
|
|
""" |
|
|
|
|
|
from PyPDF4 import PdfFileReader, PdfFileWriter |
|
|
|
|
|
|
|
|
if not ja_tem_pdf_assinado: |
|
|
|
|
|
# 1ª assinatura: montar PDF com página de autenticação ANTES de enviar |
|
|
|
|
|
original_pdf = PdfFileReader(io.BytesIO(pdf_bytes)) |
|
|
original_pdf = PdfFileReader(io.BytesIO(pdf_bytes)) |
|
|
last_page = original_pdf.getPage(original_pdf.getNumPages() - 1) |
|
|
last_page = original_pdf.getPage(original_pdf.getNumPages() - 1) |
|
|
page_width = float(last_page.mediaBox.getWidth()) |
|
|
page_width = float(last_page.mediaBox.getWidth()) |
|
|
@ -158,15 +264,8 @@ def _assinar_pdf_com_pagina_auth(pdf_bytes, *, request, tipo_doc, pk_doc, |
|
|
|
|
|
|
|
|
codigo = _gerar_codigo_autenticacao(pdf_bytes) |
|
|
codigo = _gerar_codigo_autenticacao(pdf_bytes) |
|
|
url_verificacao = _construir_url_verificacao(request, tipo_doc, pk_doc, codigo) |
|
|
url_verificacao = _construir_url_verificacao(request, tipo_doc, pk_doc, codigo) |
|
|
|
|
|
|
|
|
nova_assinatura_info_visual = { |
|
|
|
|
|
'nome_assinante': nome_assinante, |
|
|
|
|
|
'cargo': cargo, |
|
|
|
|
|
'data_assinatura': data_simples, |
|
|
|
|
|
} |
|
|
|
|
|
auth_page_bytes = _gerar_pagina_autenticacao( |
|
|
auth_page_bytes = _gerar_pagina_autenticacao( |
|
|
[nova_assinatura_info_visual], codigo, url_verificacao, |
|
|
blocos, codigo, url_verificacao, page_width, page_height |
|
|
page_width, page_height |
|
|
|
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
auth_page_pdf = PdfFileReader(io.BytesIO(auth_page_bytes)) |
|
|
auth_page_pdf = PdfFileReader(io.BytesIO(auth_page_bytes)) |
|
|
@ -175,39 +274,104 @@ def _assinar_pdf_com_pagina_auth(pdf_bytes, *, request, tipo_doc, pk_doc, |
|
|
output_pdf.addPage(original_pdf.getPage(page_num)) |
|
|
output_pdf.addPage(original_pdf.getPage(page_num)) |
|
|
output_pdf.addPage(auth_page_pdf.getPage(0)) |
|
|
output_pdf.addPage(auth_page_pdf.getPage(0)) |
|
|
|
|
|
|
|
|
buf_combinado = io.BytesIO() |
|
|
buf = io.BytesIO() |
|
|
output_pdf.write(buf_combinado) |
|
|
output_pdf.write(buf) |
|
|
pdf_para_assinar = buf_combinado.getvalue() |
|
|
return buf.getvalue(), codigo |
|
|
codigo_retorno = codigo |
|
|
|
|
|
else: |
|
|
|
|
|
pdf_para_assinar = pdf_bytes # já inclui página de autenticação |
|
|
def _assinar_pdf_com_pagina_auth(pdf_bytes, *, request, tipo_doc, pk_doc, |
|
|
|
|
|
assinaturas_existentes, |
|
|
|
|
|
certificado_bytes=None, senha=None, |
|
|
|
|
|
assinatura_a3_bytes=None, cert_chain_bytes=None, |
|
|
|
|
|
tipo_cert_input='a1', |
|
|
|
|
|
posicao_custom=None, |
|
|
|
|
|
hash_doc=''): |
|
|
|
|
|
""" |
|
|
|
|
|
Orquestra a assinatura digital completa de um PDF. |
|
|
|
|
|
|
|
|
|
|
|
Dois backends, escolhidos por ASSINATURA_API_URL: |
|
|
|
|
|
|
|
|
|
|
|
- **API externa (padrão quando configurada)**: desde a fase C3 (AB#1473) quem |
|
|
|
|
|
compõe a página de autenticação é o microserviço. O SAPL manda os dados da |
|
|
|
|
|
casa legislativa (URL de verificação, nome, brasão, bloco do assinante) e |
|
|
|
|
|
recebe de volta o PDF já composto e assinado, mais o código impresso. Não |
|
|
|
|
|
manda posição: a grade é do microserviço, e é isso que faz SAPL e app do AMU |
|
|
|
|
|
produzirem o mesmo artefato sem reimplementar o desenho. |
|
|
|
|
|
- **pyhanko local (ASSINATURA_API_URL vazio)**: compõe a página aqui e assina |
|
|
|
|
|
no próprio Django, como sempre fez. |
|
|
|
|
|
|
|
|
|
|
|
posicao_custom : dict opcional com chaves sig_left, sig_bottom, sig_width, |
|
|
|
|
|
sig_height (em pontos PDF) e sig_page (1-based). |
|
|
|
|
|
Quando fornecido, substitui o cálculo automático de grid — |
|
|
|
|
|
e, na API externa, mantém a composição local, porque |
|
|
|
|
|
coordenada explícita e composição no microserviço disputam |
|
|
|
|
|
o mesmo campo. |
|
|
|
|
|
hash_doc : código de autenticação já emitido para o documento. Vai para |
|
|
|
|
|
o carimbo visual no backend local e, na API externa, é o |
|
|
|
|
|
`codigo_autenticacao` devolvido ao microserviço da 2ª |
|
|
|
|
|
assinatura em diante. |
|
|
|
|
|
|
|
|
|
|
|
Retorna (signed_pdf_bytes, nova_assinatura_dict, codigo_autenticacao_ou_None). |
|
|
|
|
|
- codigo_autenticacao_ou_None é não-None apenas na 1ª assinatura. |
|
|
|
|
|
|
|
|
|
|
|
Lança ErroAssinaturaUsuario quando o problema é do operador (certificado |
|
|
|
|
|
vencido, senha incorreta) e Exception nos demais casos. |
|
|
|
|
|
""" |
|
|
|
|
|
from PyPDF4 import PdfFileReader, PdfFileWriter |
|
|
|
|
|
import base64 as _base64 |
|
|
|
|
|
|
|
|
|
|
|
ja_tem_pdf_assinado = bool(assinaturas_existentes) |
|
|
|
|
|
|
|
|
|
|
|
# ── Informações do assinante ────────────────────────────────────────────── |
|
|
|
|
|
# Na API externa o SAPL não abre o PFX: quem assina é o microserviço. O nome |
|
|
|
|
|
# que vai no PDF é o do parlamentar (relação User → Autor), e os dados do |
|
|
|
|
|
# certificado vêm do /validate-pfx. |
|
|
|
|
|
if _usar_api_externa(): |
|
|
|
|
|
from sapl.materia.assinatura_api_client import ( |
|
|
|
|
|
AssinaturaAPIError, |
|
|
|
|
|
assinar_pdf_com_pagina_autenticacao, |
|
|
|
|
|
assinar_pdf_via_api, |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
# O tipo do certificado só é conhecido de verdade pelo /validate-pfx (C3); |
|
|
|
|
|
# este é o rótulo de partida, sobrescrito quando os metadados chegam. |
|
|
|
|
|
tipo_cert_display = 'Certificado Digital' |
|
|
|
|
|
|
|
|
|
|
|
# Nome/cargo pela MESMA função do backend local: o vereador não pode sair |
|
|
|
|
|
# com nome diferente conforme a casa usa microserviço ou pyhanko. |
|
|
|
|
|
nome_assinante, cargo = _nome_e_cargo_do_assinante(request) |
|
|
|
|
|
|
|
|
|
|
|
data_assinatura = timezone.localtime(timezone.now()) |
|
|
|
|
|
data_formatada = data_assinatura.strftime('%d/%m/%Y %H:%M:%S') |
|
|
|
|
|
data_simples = data_assinatura.strftime('%d/%m/%Y %H:%M') |
|
|
|
|
|
codigo_retorno = None |
|
|
|
|
|
|
|
|
|
|
|
# Metadados do certificado: o /validate-pfx responde ANTES de assinar quem |
|
|
|
|
|
# emitiu, qual a série e até quando vale. Sem esta chamada esses campos iam |
|
|
|
|
|
# vazios para o assinatura_info — e o SAPL exibia "Válido até:" em branco. |
|
|
|
|
|
# Ela também é a checagem de validade que o caminho local faz em |
|
|
|
|
|
# `_validar_certificado`: certificado vencido não chega a ser enviado. |
|
|
|
|
|
cert_api = _metadados_certificado_via_api(certificado_bytes, senha) |
|
|
|
|
|
|
|
|
|
|
|
# O bloco desta assinatura — o único que a página precisa desenhar agora. |
|
|
|
|
|
bloco_atual = { |
|
|
|
|
|
'nome_assinante': nome_assinante, |
|
|
|
|
|
'cargo': cargo, |
|
|
|
|
|
'data_assinatura': data_simples, |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
# Calcula a posição do campo de assinatura na última página (página de autenticação) |
|
|
|
|
|
# para que a assinatura visual fique no bloco correto do grid. |
|
|
|
|
|
# Se posicao_custom foi fornecido pelo usuário, usa ele diretamente. |
|
|
|
|
|
if posicao_custom: |
|
|
if posicao_custom: |
|
|
sig_page = posicao_custom.get('sig_page') |
|
|
# Posição explícita pedida pelo chamador. Quando o microserviço compõe a |
|
|
sig_left = posicao_custom.get('sig_left') |
|
|
# página, é ELE quem decide a posição do bloco (a grade é dele), então |
|
|
sig_bottom = posicao_custom.get('sig_bottom') |
|
|
# `auth_page` e coordenada explícita não convivem: pedir os dois faria a |
|
|
sig_width = posicao_custom.get('sig_width') |
|
|
# coordenada do usuário ser silenciosamente descartada. Neste caso — e só |
|
|
sig_height = posicao_custom.get('sig_height') |
|
|
# nele — a composição continua sendo feita aqui, como antes da C3. |
|
|
else: |
|
|
if not ja_tem_pdf_assinado: |
|
|
n_assinatura = len(assinaturas_existentes) |
|
|
pdf_para_assinar, codigo_retorno = _compor_pagina_auth_localmente( |
|
|
try: |
|
|
pdf_bytes, request, tipo_doc, pk_doc, [bloco_atual] |
|
|
temp_pypdf = PdfFileReader(io.BytesIO(pdf_para_assinar)) |
|
|
|
|
|
auth_pg = temp_pypdf.getPage(temp_pypdf.getNumPages() - 1) |
|
|
|
|
|
auth_w = float(auth_pg.mediaBox.getWidth()) |
|
|
|
|
|
auth_h = float(auth_pg.mediaBox.getHeight()) |
|
|
|
|
|
sig_x1, sig_y1, sig_x2, sig_y2 = _posicao_bloco_assinatura( |
|
|
|
|
|
n_assinatura, auth_w, auth_h |
|
|
|
|
|
) |
|
|
) |
|
|
sig_page = temp_pypdf.getNumPages() # 1-based, última página |
|
|
else: |
|
|
sig_left = sig_x1 |
|
|
pdf_para_assinar = pdf_bytes # já inclui página de autenticação |
|
|
sig_bottom = sig_y1 |
|
|
|
|
|
sig_width = sig_x2 - sig_x1 |
|
|
|
|
|
sig_height = sig_y2 - sig_y1 |
|
|
|
|
|
except Exception: |
|
|
|
|
|
sig_page = sig_left = sig_bottom = sig_width = sig_height = None |
|
|
|
|
|
|
|
|
|
|
|
try: |
|
|
try: |
|
|
pdf_assinado_bytes = assinar_pdf_via_api( |
|
|
pdf_assinado_bytes = assinar_pdf_via_api( |
|
|
@ -215,24 +379,68 @@ def _assinar_pdf_com_pagina_auth(pdf_bytes, *, request, tipo_doc, pk_doc, |
|
|
certificado_bytes=certificado_bytes, |
|
|
certificado_bytes=certificado_bytes, |
|
|
senha=senha, |
|
|
senha=senha, |
|
|
reason='Documento assinado digitalmente nos termos da MP 2.200-2/2001', |
|
|
reason='Documento assinado digitalmente nos termos da MP 2.200-2/2001', |
|
|
location='Câmara Municipal', |
|
|
location=_obter_nome_casa_legislativa(), |
|
|
signature_page=sig_page, |
|
|
signature_page=posicao_custom.get('sig_page'), |
|
|
signature_left=sig_left, |
|
|
signature_left=posicao_custom.get('sig_left'), |
|
|
signature_bottom=sig_bottom, |
|
|
signature_bottom=posicao_custom.get('sig_bottom'), |
|
|
signature_width=sig_width, |
|
|
signature_width=posicao_custom.get('sig_width'), |
|
|
signature_height=sig_height, |
|
|
signature_height=posicao_custom.get('sig_height'), |
|
|
) |
|
|
) |
|
|
except AssinaturaAPIError as exc: |
|
|
except AssinaturaAPIError as exc: |
|
|
raise Exception(str(exc)) |
|
|
raise _erro_de_assinatura(exc) |
|
|
|
|
|
else: |
|
|
|
|
|
# Caminho normal (C3): o microserviço compõe a página de autenticação — |
|
|
|
|
|
# código, URL de verificação, QR e os blocos — e escolhe o bloco desta |
|
|
|
|
|
# assinatura contando as que já existem no PDF. O SAPL só manda o que é |
|
|
|
|
|
# da casa legislativa. Nenhum `signature_*` vai junto, de propósito. |
|
|
|
|
|
try: |
|
|
|
|
|
resultado = assinar_pdf_com_pagina_autenticacao( |
|
|
|
|
|
pdf_bytes, |
|
|
|
|
|
certificado_bytes=certificado_bytes, |
|
|
|
|
|
senha=senha, |
|
|
|
|
|
verification_url_base=_construir_url_verificacao_base( |
|
|
|
|
|
request, tipo_doc, pk_doc |
|
|
|
|
|
), |
|
|
|
|
|
assinaturas=[bloco_atual], |
|
|
|
|
|
casa_legislativa=_obter_nome_casa_legislativa(), |
|
|
|
|
|
signer_name=nome_assinante, |
|
|
|
|
|
signer_role=cargo, |
|
|
|
|
|
# Da 2ª assinatura em diante o código já está impresso na página: |
|
|
|
|
|
# o PDF mudou ao ser assinado e o hash de agora não o reproduz. |
|
|
|
|
|
codigo_autenticacao=(hash_doc or '') if ja_tem_pdf_assinado else None, |
|
|
|
|
|
brasao_bytes=_ler_brasao(), |
|
|
|
|
|
brasao_filename=os.path.basename(_encontrar_logo() or 'brasao.png'), |
|
|
|
|
|
reason='Documento assinado digitalmente nos termos da MP 2.200-2/2001', |
|
|
|
|
|
location=_obter_nome_casa_legislativa(), |
|
|
|
|
|
) |
|
|
|
|
|
except AssinaturaAPIError as exc: |
|
|
|
|
|
raise _erro_de_assinatura(exc) |
|
|
|
|
|
|
|
|
|
|
|
if not resultado.auth_page_suportado: |
|
|
|
|
|
# Sem os cabeçalhos do auth_page o microserviço ignorou a composição: |
|
|
|
|
|
# o PDF sairia sem página de autenticação e sem código verificável. |
|
|
|
|
|
# Falhar aqui é melhor do que gravar um documento inverificável. |
|
|
|
|
|
raise Exception( |
|
|
|
|
|
'O microserviço de assinatura não compôs a página de ' |
|
|
|
|
|
'autenticação (resposta sem X-Auth-Page-Applied). Atualize o ' |
|
|
|
|
|
'microserviço para a versão com suporte a auth_page.' |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
pdf_assinado_bytes = resultado.pdf |
|
|
|
|
|
if resultado.auth_page_aplicada: |
|
|
|
|
|
codigo_retorno = resultado.codigo_autenticacao |
|
|
|
|
|
|
|
|
nova_assinatura = { |
|
|
nova_assinatura = { |
|
|
'tipo_certificado': tipo_cert_input.upper(), |
|
|
'tipo_certificado': tipo_cert_input.upper(), |
|
|
'tipo_certificado_display': f'Certificado Digital – {tipo_cert_input.upper()}', |
|
|
'tipo_certificado_display': ( |
|
|
'subject': '', |
|
|
f'{_tipo_certificado_pelo_emissor(cert_api.get("issuer", ""))} – ' |
|
|
'issuer': '', |
|
|
f'{tipo_cert_input.upper()}' |
|
|
'serial': '', |
|
|
), |
|
|
'valid_from': '', |
|
|
'subject': cert_api.get('subject', ''), |
|
|
'valid_to': '', |
|
|
'issuer': cert_api.get('issuer', ''), |
|
|
|
|
|
'serial': cert_api.get('serial', ''), |
|
|
|
|
|
'valid_from': cert_api.get('valid_from', ''), |
|
|
|
|
|
'valid_to': cert_api.get('valid_to', ''), |
|
|
'signed_by': request.user.username, |
|
|
'signed_by': request.user.username, |
|
|
'nome_assinante': nome_assinante, |
|
|
'nome_assinante': nome_assinante, |
|
|
'cargo': cargo, |
|
|
'cargo': cargo, |
|
|
@ -261,17 +469,26 @@ def _assinar_pdf_com_pagina_auth(pdf_bytes, *, request, tipo_doc, pk_doc, |
|
|
passphrase=senha.encode('utf-8') |
|
|
passphrase=senha.encode('utf-8') |
|
|
) |
|
|
) |
|
|
except Exception as cert_error: |
|
|
except Exception as cert_error: |
|
|
raise Exception(f'Erro ao carregar certificado: {cert_error}') |
|
|
logger.warning(f'Falha ao abrir o PFX: {cert_error}') |
|
|
|
|
|
raise ErroAssinaturaUsuario(_mensagem_de_erro_do_pfx(cert_error)) |
|
|
finally: |
|
|
finally: |
|
|
if os.path.exists(tmp_cert_path): |
|
|
if os.path.exists(tmp_cert_path): |
|
|
os.unlink(tmp_cert_path) |
|
|
os.unlink(tmp_cert_path) |
|
|
|
|
|
|
|
|
|
|
|
if signer is None: |
|
|
|
|
|
raise ErroAssinaturaUsuario( |
|
|
|
|
|
'Não foi possível carregar o certificado. Verifique se o arquivo ' |
|
|
|
|
|
'.pfx/.p12 é válido e contém uma chave de assinatura.' |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
cert_info = signer.signing_cert |
|
|
cert_info = signer.signing_cert |
|
|
error_response = _validar_certificado(cert_info) |
|
|
error_response = _validar_certificado(cert_info) |
|
|
if error_response: |
|
|
if error_response: |
|
|
import json as _json |
|
|
import json as _json |
|
|
data = _json.loads(error_response.content) |
|
|
data = _json.loads(error_response.content) |
|
|
raise Exception(data.get('error', 'Certificado inválido.')) |
|
|
# Certificado vencido é problema do operador, não falha do sistema: 400 |
|
|
|
|
|
# com a mensagem, igual ao que o microserviço devolve no outro backend. |
|
|
|
|
|
raise ErroAssinaturaUsuario(data.get('error', 'Certificado inválido.')) |
|
|
|
|
|
|
|
|
# Criar objeto fake para _obter_info_assinante |
|
|
# Criar objeto fake para _obter_info_assinante |
|
|
class _CertWrapper: |
|
|
class _CertWrapper: |
|
|
@ -324,7 +541,7 @@ def _assinar_pdf_com_pagina_auth(pdf_bytes, *, request, tipo_doc, pk_doc, |
|
|
|
|
|
|
|
|
meta = signers.PdfSignatureMetadata( |
|
|
meta = signers.PdfSignatureMetadata( |
|
|
field_name=sig_field_name, |
|
|
field_name=sig_field_name, |
|
|
location='Câmara Municipal', |
|
|
location=_obter_nome_casa_legislativa(), |
|
|
reason='Documento assinado digitalmente nos termos da MP 2.200-2/2001', |
|
|
reason='Documento assinado digitalmente nos termos da MP 2.200-2/2001', |
|
|
name=nome_assinante |
|
|
name=nome_assinante |
|
|
) |
|
|
) |
|
|
@ -343,46 +560,21 @@ def _assinar_pdf_com_pagina_auth(pdf_bytes, *, request, tipo_doc, pk_doc, |
|
|
|
|
|
|
|
|
else: |
|
|
else: |
|
|
# Primeira assinatura: página de autenticação + pyhanko |
|
|
# Primeira assinatura: página de autenticação + pyhanko |
|
|
original_pdf = PdfFileReader(io.BytesIO(pdf_bytes)) |
|
|
stamped_bytes, codigo = _compor_pagina_auth_localmente( |
|
|
last_page = original_pdf.getPage(original_pdf.getNumPages() - 1) |
|
|
pdf_bytes, request, tipo_doc, pk_doc, |
|
|
page_width = float(last_page.mediaBox.getWidth()) |
|
|
[{ |
|
|
page_height = float(last_page.mediaBox.getHeight()) |
|
|
|
|
|
|
|
|
|
|
|
codigo = _gerar_codigo_autenticacao(pdf_bytes) |
|
|
|
|
|
url_verificacao = _construir_url_verificacao(request, tipo_doc, pk_doc, codigo) |
|
|
|
|
|
nova_assinatura_info_visual = { |
|
|
|
|
|
'nome_assinante': nome_assinante, |
|
|
'nome_assinante': nome_assinante, |
|
|
'cargo': cargo, |
|
|
'cargo': cargo, |
|
|
'data_assinatura': data_simples, |
|
|
'data_assinatura': data_simples, |
|
|
} |
|
|
}], |
|
|
auth_page_bytes = _gerar_pagina_autenticacao( |
|
|
|
|
|
[nova_assinatura_info_visual], codigo, url_verificacao, |
|
|
|
|
|
page_width, page_height |
|
|
|
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
auth_page_pdf = PdfFileReader(io.BytesIO(auth_page_bytes)) |
|
|
|
|
|
output_pdf = PdfFileWriter() |
|
|
|
|
|
for page_num in range(original_pdf.getNumPages()): |
|
|
|
|
|
output_pdf.addPage(original_pdf.getPage(page_num)) |
|
|
|
|
|
output_pdf.addPage(auth_page_pdf.getPage(0)) |
|
|
|
|
|
|
|
|
|
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_f: |
|
|
|
|
|
tmp_path = tmp_f.name |
|
|
|
|
|
output_pdf.write(tmp_f) |
|
|
|
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
with open(tmp_path, 'rb') as f: |
|
|
|
|
|
stamped_bytes = f.read() |
|
|
|
|
|
finally: |
|
|
|
|
|
if os.path.exists(tmp_path): |
|
|
|
|
|
os.unlink(tmp_path) |
|
|
|
|
|
|
|
|
|
|
|
signed_buffer = io.BytesIO() |
|
|
signed_buffer = io.BytesIO() |
|
|
with io.BytesIO(stamped_bytes) as inf: |
|
|
with io.BytesIO(stamped_bytes) as inf: |
|
|
w = IncrementalPdfFileWriter(inf) |
|
|
w = IncrementalPdfFileWriter(inf) |
|
|
meta = signers.PdfSignatureMetadata( |
|
|
meta = signers.PdfSignatureMetadata( |
|
|
field_name='AssinaturaDigital', |
|
|
field_name='AssinaturaDigital', |
|
|
location='Câmara Municipal', |
|
|
location=_obter_nome_casa_legislativa(), |
|
|
reason='Documento assinado digitalmente nos termos da MP 2.200-2/2001', |
|
|
reason='Documento assinado digitalmente nos termos da MP 2.200-2/2001', |
|
|
name=nome_assinante |
|
|
name=nome_assinante |
|
|
) |
|
|
) |
|
|
@ -437,17 +629,24 @@ def _gerar_qrcode_image(url): |
|
|
return buf |
|
|
return buf |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _construir_url_verificacao(request, tipo, pk, codigo): |
|
|
def _construir_url_verificacao_base(request, tipo, pk): |
|
|
"""Monta URL pública de verificação.""" |
|
|
""" |
|
|
|
|
|
URL pública de verificação SEM o `?codigo=`. |
|
|
|
|
|
|
|
|
|
|
|
É o que vai para o microserviço: só o SAPL sabe a URL pública da casa, mas o |
|
|
|
|
|
código quem gera é quem compõe a página — então a montagem final é lá. |
|
|
|
|
|
""" |
|
|
if tipo == 'materia': |
|
|
if tipo == 'materia': |
|
|
url_name = 'sapl.materia:materia_verificar_documento' |
|
|
url_name = 'sapl.materia:materia_verificar_documento' |
|
|
else: |
|
|
else: |
|
|
url_name = 'sapl.materia:docacessorio_verificar_documento' |
|
|
url_name = 'sapl.materia:docacessorio_verificar_documento' |
|
|
|
|
|
|
|
|
base_url = request.build_absolute_uri( |
|
|
return request.build_absolute_uri(reverse(url_name, kwargs={'pk': pk})) |
|
|
reverse(url_name, kwargs={'pk': pk}) |
|
|
|
|
|
) |
|
|
|
|
|
return f'{base_url}?codigo={codigo}' |
|
|
def _construir_url_verificacao(request, tipo, pk, codigo): |
|
|
|
|
|
"""Monta URL pública de verificação.""" |
|
|
|
|
|
return f'{_construir_url_verificacao_base(request, tipo, pk)}?codigo={codigo}' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _obter_nome_casa_legislativa(): |
|
|
def _obter_nome_casa_legislativa(): |
|
|
@ -736,89 +935,30 @@ def _criar_stamp_style(nome_assinante, cargo, hash_doc=''): |
|
|
def _obter_info_assinante(request, cert_info): |
|
|
def _obter_info_assinante(request, cert_info): |
|
|
""" |
|
|
""" |
|
|
Obtém informações do assinante (nome, cargo, tipo_cert). |
|
|
Obtém informações do assinante (nome, cargo, tipo_cert). |
|
|
Busca via Autor.operadores → Parlamentar (GenericFK). |
|
|
|
|
|
Retorna (nome_assinante, cargo, tipo_cert). |
|
|
Retorna (nome_assinante, cargo, tipo_cert). |
|
|
""" |
|
|
""" |
|
|
nome_assinante = request.user.get_full_name() or request.user.username |
|
|
nome_assinante, cargo = _nome_e_cargo_do_assinante(request) |
|
|
cargo = "Usuário do Sistema" |
|
|
return nome_assinante, cargo, _tipo_certificado_pelo_emissor(cert_info.issuer) |
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
from sapl.base.models import Autor |
|
|
|
|
|
from sapl.parlamentares.models import Parlamentar |
|
|
|
|
|
|
|
|
|
|
|
autor = Autor.objects.filter(operadores=request.user).first() |
|
|
|
|
|
if autor: |
|
|
|
|
|
# Usa tipo do autor como cargo (ex: "Parlamentar" → "Vereador(a)") |
|
|
|
|
|
tipo_descricao = autor.tipo.descricao if autor.tipo else '' |
|
|
|
|
|
if tipo_descricao == 'Parlamentar': |
|
|
|
|
|
cargo = "Vereador(a)" |
|
|
|
|
|
elif tipo_descricao: |
|
|
|
|
|
cargo = tipo_descricao |
|
|
|
|
|
|
|
|
|
|
|
# Se o autor está vinculado a um Parlamentar, usa o nome dele |
|
|
|
|
|
if isinstance(autor.autor_related, Parlamentar): |
|
|
|
|
|
parlamentar = autor.autor_related |
|
|
|
|
|
tipo_nome = AppConfig.attr('assinatura_nome') |
|
|
|
|
|
if tipo_nome == 'C': |
|
|
|
|
|
nome_assinante = parlamentar.nome_completo |
|
|
|
|
|
else: |
|
|
|
|
|
nome_assinante = parlamentar.nome_parlamentar |
|
|
|
|
|
except Exception: |
|
|
|
|
|
pass |
|
|
|
|
|
|
|
|
|
|
|
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" |
|
|
|
|
|
|
|
|
|
|
|
return nome_assinante, cargo, tipo_cert |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _mensagem_de_erro_do_pfx(exc): |
|
|
def _carregar_certificado(certificado_file, senha): |
|
|
|
|
|
""" |
|
|
|
|
|
Carrega certificado PKCS12 de um arquivo. |
|
|
|
|
|
Retorna (signer, error_response) — se error_response não é None, retornar direto. |
|
|
|
|
|
""" |
|
|
""" |
|
|
from pyhanko.sign import signers |
|
|
Traduz a falha do pyhanko ao abrir o PKCS12 para algo que o operador resolve. |
|
|
|
|
|
|
|
|
cert_data = certificado_file.read() |
|
|
Sem isto, senha errada vira `Exception` genérica e a view responde 500 com o |
|
|
|
|
|
texto cru da biblioteca — quem está assinando não tem como saber que só errou |
|
|
import tempfile as tmp_module |
|
|
a senha. É a mesma tradução que o caminho via microserviço faz ao converter o |
|
|
with tmp_module.NamedTemporaryFile(delete=False, suffix='.pfx') as tmp_cert: |
|
|
HTTP 400 em `ErroAssinaturaUsuario`; os dois backends precisam falar igual. |
|
|
tmp_cert.write(cert_data) |
|
|
""" |
|
|
tmp_cert_path = tmp_cert.name |
|
|
texto = str(exc) |
|
|
|
|
|
minusculo = texto.lower() |
|
|
try: |
|
|
if 'password' in minusculo or 'mac' in minusculo: |
|
|
signer = signers.SimpleSigner.load_pkcs12( |
|
|
detalhe = 'Senha incorreta.' |
|
|
pfx_file=tmp_cert_path, |
|
|
elif 'decode' in minusculo or 'parse' in minusculo: |
|
|
passphrase=senha.encode('utf-8') |
|
|
detalhe = 'Arquivo não é um certificado válido (.pfx/.p12).' |
|
|
) |
|
|
|
|
|
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}") |
|
|
|
|
|
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: |
|
|
else: |
|
|
error_detail = f'Detalhes: {error_msg}' |
|
|
detalhe = f'Detalhes: {texto}' |
|
|
return None, JsonResponse({ |
|
|
return f'Erro ao carregar certificado: {detalhe}' |
|
|
'success': False, |
|
|
|
|
|
'error': f'Erro ao carregar certificado: {error_detail}' |
|
|
|
|
|
}, status=400) |
|
|
|
|
|
|
|
|
|
|
|
if signer is None: |
|
|
|
|
|
return None, JsonResponse({ |
|
|
|
|
|
'success': False, |
|
|
|
|
|
'error': 'Não foi possível carregar o certificado. Verifique se o arquivo .pfx/.p12 é válido e contém uma chave de assinatura.' |
|
|
|
|
|
}, status=400) |
|
|
|
|
|
|
|
|
|
|
|
return signer, None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _validar_certificado(cert_info): |
|
|
def _validar_certificado(cert_info): |
|
|
@ -1093,6 +1233,14 @@ def materia_assinar_a1(request, pk): |
|
|
'success': False, |
|
|
'success': False, |
|
|
'error': 'Biblioteca de assinatura não instalada. Contate o administrador.' |
|
|
'error': 'Biblioteca de assinatura não instalada. Contate o administrador.' |
|
|
}, status=500) |
|
|
}, status=500) |
|
|
|
|
|
except ErroAssinaturaUsuario as e: |
|
|
|
|
|
# Certificado vencido, senha incorreta: o operador resolve sozinho, então |
|
|
|
|
|
# a resposta é 400 com a mensagem — não 500 com o erro cru. |
|
|
|
|
|
logger.warning(f"Assinatura da matéria {pk} recusada: {e}") |
|
|
|
|
|
return JsonResponse({ |
|
|
|
|
|
'success': False, |
|
|
|
|
|
'error': str(e) |
|
|
|
|
|
}, status=400) |
|
|
except Exception as e: |
|
|
except Exception as e: |
|
|
logger.error(f"Erro ao assinar PDF da matéria {pk}: {e}") |
|
|
logger.error(f"Erro ao assinar PDF da matéria {pk}: {e}") |
|
|
return JsonResponse({'success': False, 'error': str(e)}, status=500) |
|
|
return JsonResponse({'success': False, 'error': str(e)}, status=500) |
|
|
@ -1272,7 +1420,7 @@ def materia_assinar_a3_finalizar(request, pk): |
|
|
# Metadados da assinatura |
|
|
# Metadados da assinatura |
|
|
meta = signers.PdfSignatureMetadata( |
|
|
meta = signers.PdfSignatureMetadata( |
|
|
field_name='AssinaturaDigital', |
|
|
field_name='AssinaturaDigital', |
|
|
location='Câmara Municipal', |
|
|
location=_obter_nome_casa_legislativa(), |
|
|
reason='Assinatura Digital de Matéria Legislativa (A3)', |
|
|
reason='Assinatura Digital de Matéria Legislativa (A3)', |
|
|
name=request.user.get_full_name() or request.user.username |
|
|
name=request.user.get_full_name() or request.user.username |
|
|
) |
|
|
) |
|
|
@ -1694,6 +1842,14 @@ def docacessorio_assinar_a1(request, pk): |
|
|
'success': False, |
|
|
'success': False, |
|
|
'error': 'Biblioteca de assinatura não instalada. Contate o administrador.' |
|
|
'error': 'Biblioteca de assinatura não instalada. Contate o administrador.' |
|
|
}, status=500) |
|
|
}, status=500) |
|
|
|
|
|
except ErroAssinaturaUsuario as e: |
|
|
|
|
|
# Certificado vencido, senha incorreta: o operador resolve sozinho, então |
|
|
|
|
|
# a resposta é 400 com a mensagem — não 500 com o erro cru. |
|
|
|
|
|
logger.warning(f"Assinatura do doc acessório {pk} recusada: {e}") |
|
|
|
|
|
return JsonResponse({ |
|
|
|
|
|
'success': False, |
|
|
|
|
|
'error': str(e) |
|
|
|
|
|
}, status=400) |
|
|
except Exception as e: |
|
|
except Exception as e: |
|
|
logger.error(f"Erro ao assinar PDF do doc acessório {pk}: {e}") |
|
|
logger.error(f"Erro ao assinar PDF do doc acessório {pk}: {e}") |
|
|
return JsonResponse({'success': False, 'error': str(e)}, status=500) |
|
|
return JsonResponse({'success': False, 'error': str(e)}, status=500) |
|
|
@ -2153,33 +2309,18 @@ def materia_assinar_lote(request): |
|
|
ja_tem = bool(assinaturas_existentes) |
|
|
ja_tem = bool(assinaturas_existentes) |
|
|
|
|
|
|
|
|
if not ja_tem: |
|
|
if not ja_tem: |
|
|
original_pdf = PdfFileReader(io.BytesIO(pdf_bytes)) |
|
|
# Composição local, documento por documento — é o que torna o |
|
|
last_page = original_pdf.getPage(original_pdf.getNumPages() - 1) |
|
|
# /sign/batch utilizável: ele só carimba. Mesmo helper do caminho |
|
|
page_width = float(last_page.mediaBox.getWidth()) |
|
|
# individual local, para não existir um terceiro compositor. |
|
|
page_height = float(last_page.mediaBox.getHeight()) |
|
|
pdf_para_assinar, codigo = _compor_pagina_auth_localmente( |
|
|
|
|
|
pdf_bytes, request, 'materia', pk, |
|
|
codigo = _gerar_codigo_autenticacao(pdf_bytes) |
|
|
[{ |
|
|
url_verificacao = _construir_url_verificacao(request, 'materia', pk, codigo) |
|
|
|
|
|
codigos_map[pk] = codigo |
|
|
|
|
|
|
|
|
|
|
|
nova_assinatura_info_visual = { |
|
|
|
|
|
'nome_assinante': nome_assinante, |
|
|
'nome_assinante': nome_assinante, |
|
|
'cargo': cargo, |
|
|
'cargo': cargo, |
|
|
'data_assinatura': data_simples, |
|
|
'data_assinatura': data_simples, |
|
|
} |
|
|
}], |
|
|
auth_page_bytes = _gerar_pagina_autenticacao( |
|
|
|
|
|
[nova_assinatura_info_visual], codigo, url_verificacao, |
|
|
|
|
|
page_width, page_height |
|
|
|
|
|
) |
|
|
) |
|
|
|
|
|
codigos_map[pk] = codigo |
|
|
auth_pdf = PdfFileReader(io.BytesIO(auth_page_bytes)) |
|
|
|
|
|
output_pdf = PdfFileWriter() |
|
|
|
|
|
for i in range(original_pdf.getNumPages()): |
|
|
|
|
|
output_pdf.addPage(original_pdf.getPage(i)) |
|
|
|
|
|
output_pdf.addPage(auth_pdf.getPage(0)) |
|
|
|
|
|
buf = io.BytesIO() |
|
|
|
|
|
output_pdf.write(buf) |
|
|
|
|
|
pdf_para_assinar = buf.getvalue() |
|
|
|
|
|
else: |
|
|
else: |
|
|
pdf_para_assinar = pdf_bytes |
|
|
pdf_para_assinar = pdf_bytes |
|
|
|
|
|
|
|
|
@ -2496,23 +2637,17 @@ def docacessorio_assinar_lote(request): |
|
|
ja_tem = bool(assinaturas_existentes) |
|
|
ja_tem = bool(assinaturas_existentes) |
|
|
|
|
|
|
|
|
if not ja_tem: |
|
|
if not ja_tem: |
|
|
original_pdf = PdfFileReader(io.BytesIO(pdf_bytes)) |
|
|
# Mesma composição local do lote de matéria e do caminho |
|
|
last_page = original_pdf.getPage(original_pdf.getNumPages() - 1) |
|
|
# individual local — um compositor só (ver _compor_pagina_auth_localmente). |
|
|
page_width = float(last_page.mediaBox.getWidth()) |
|
|
pdf_para_assinar, codigo = _compor_pagina_auth_localmente( |
|
|
page_height = float(last_page.mediaBox.getHeight()) |
|
|
pdf_bytes, request, 'docacessorio', pk, |
|
|
codigo = _gerar_codigo_autenticacao(pdf_bytes) |
|
|
[{ |
|
|
url_verificacao = _construir_url_verificacao(request, 'docacessorio', pk, codigo) |
|
|
'nome_assinante': nome_assinante, |
|
|
|
|
|
'cargo': cargo, |
|
|
|
|
|
'data_assinatura': data_simples, |
|
|
|
|
|
}], |
|
|
|
|
|
) |
|
|
codigos_map[pk] = codigo |
|
|
codigos_map[pk] = codigo |
|
|
nova_assinatura_info_visual = {'nome_assinante': nome_assinante, 'cargo': cargo, 'data_assinatura': data_simples} |
|
|
|
|
|
auth_page_bytes = _gerar_pagina_autenticacao([nova_assinatura_info_visual], codigo, url_verificacao, page_width, page_height) |
|
|
|
|
|
auth_pdf = PdfFileReader(io.BytesIO(auth_page_bytes)) |
|
|
|
|
|
output_pdf = PdfFileWriter() |
|
|
|
|
|
for i in range(original_pdf.getNumPages()): |
|
|
|
|
|
output_pdf.addPage(original_pdf.getPage(i)) |
|
|
|
|
|
output_pdf.addPage(auth_pdf.getPage(0)) |
|
|
|
|
|
buf = io.BytesIO() |
|
|
|
|
|
output_pdf.write(buf) |
|
|
|
|
|
pdf_para_assinar = buf.getvalue() |
|
|
|
|
|
else: |
|
|
else: |
|
|
pdf_para_assinar = pdf_bytes |
|
|
pdf_para_assinar = pdf_bytes |
|
|
|
|
|
|
|
|
|