Browse Source

Feat(Assinatura): Página de autenticação com QR Code e verificação pública

- Substitui carimbo sobreposto na última página por página dedicada de
  autenticação no final do PDF com blocos de assinatura, código de
  autenticação (SHA-256 truncado) e QR Code
- Remove exposição de CPF do PDF e da interface web
- Adiciona campo codigo_autenticacao em MateriaLegislativa e
  DocumentoAcessorio com migration
- Adiciona views públicas de verificação (/materia/<pk>/verificar/ e
  /materia/documentoacessorio/<pk>/verificar/) sem exigir login
- Cria template verificar_assinatura.html com status verde/vermelho
- Adiciona botão "Verificar Autenticidade" nos templates de detail
- Corrige lookup de cargo do assinante via Autor.operadores
- Corrige URL de download para OnlyOffice usando build_onlyoffice_url
- Melhora layout dos botões de ação na página de detail da matéria
- Adiciona dependência qrcode[pil]==7.4.2
pull/3858/head
rangelbruno 7 months ago
parent
commit
f870eef254
  1. 1
      requirements/requirements.txt
  2. 23
      sapl/materia/migrations/0091_add_codigo_autenticacao.py
  3. 10
      sapl/materia/models.py
  4. 11
      sapl/materia/urls.py
  5. 1127
      sapl/materia/views_assinatura.py
  6. 6
      sapl/templates/materia/documentoacessorio_detail.html
  7. 27
      sapl/templates/materia/materialegislativa_detail.html
  8. 138
      sapl/templates/materia/verificar_assinatura.html

1
requirements/requirements.txt

@ -50,6 +50,7 @@ requests==2.31.0
pyhanko==0.21.0
pyhanko-certvalidator==0.26.3
cryptography>=41.0.0
qrcode[pil]==7.4.2
git+https://github.com/interlegis/django-admin-bootstrapped

23
sapl/materia/migrations/0091_add_codigo_autenticacao.py

@ -0,0 +1,23 @@
# Generated by Django 2.2.28 on 2026-02-21 19:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('materia', '0090_add_assinatura_digital_documentoacessorio'),
]
operations = [
migrations.AddField(
model_name='documentoacessorio',
name='codigo_autenticacao',
field=models.CharField(blank=True, db_index=True, max_length=16, null=True, verbose_name='Código de Autenticação'),
),
migrations.AddField(
model_name='materialegislativa',
name='codigo_autenticacao',
field=models.CharField(blank=True, db_index=True, max_length=16, null=True, verbose_name='Código de Autenticação'),
),
]

10
sapl/materia/models.py

@ -333,6 +333,11 @@ class MateriaLegislativa(models.Model):
blank=True,
related_name='materias_assinadas'
)
codigo_autenticacao = models.CharField(
max_length=16, blank=True, null=True,
verbose_name=_('Código de Autenticação'),
db_index=True
)
class Meta:
verbose_name = _('Matéria Legislativa')
@ -618,6 +623,11 @@ class DocumentoAcessorio(models.Model):
blank=True,
related_name='docacessorios_assinados'
)
codigo_autenticacao = models.CharField(
max_length=16, blank=True, null=True,
verbose_name=_('Código de Autenticação'),
db_index=True
)
class Meta:
verbose_name = _('Documento Acessório')

11
sapl/materia/urls.py

@ -50,7 +50,8 @@ from sapl.materia.views_assinatura import (
materia_pdf_assinado, materia_verificar_assinatura, materia_remover_assinatura,
detectar_aplicacao_a3,
docacessorio_assinar_a1, docacessorio_pdf_assinado,
docacessorio_verificar_assinatura, docacessorio_remover_assinatura
docacessorio_verificar_assinatura, docacessorio_remover_assinatura,
materia_verificar_documento, docacessorio_verificar_documento
)
from sapl.norma.views import NormaPesquisaSimplesView
from sapl.protocoloadm.views import (
@ -184,6 +185,10 @@ urlpatterns_materia = [
url(r'^materia/assinatura/detectar-a3/$', detectar_aplicacao_a3,
name='detectar_aplicacao_a3'),
# Verificação pública de autenticidade (sem login)
url(r'^materia/(?P<pk>\d+)/verificar/$', materia_verificar_documento,
name='materia_verificar_documento'),
# OnlyOffice endpoints para Documento Acessório
url(r'^materia/documentoacessorio/(?P<pk>\d+)/onlyoffice/editor$', docacessorio_onlyoffice_editor,
name='docacessorio_onlyoffice_editor'),
@ -207,6 +212,10 @@ urlpatterns_materia = [
name='docacessorio_verificar_assinatura'),
url(r'^materia/documentoacessorio/(?P<pk>\d+)/remover-assinatura/$', docacessorio_remover_assinatura,
name='docacessorio_remover_assinatura'),
# Verificação pública de autenticidade de Documento Acessório (sem login)
url(r'^materia/documentoacessorio/(?P<pk>\d+)/verificar/$', docacessorio_verificar_documento,
name='docacessorio_verificar_documento'),
]

1127
sapl/materia/views_assinatura.py

File diff suppressed because it is too large

6
sapl/templates/materia/documentoacessorio_detail.html

@ -72,6 +72,12 @@
<a href="https://validar.iti.gov.br/" class="btn btn-sm btn-outline-info" target="_blank">
<i class="fa fa-external-link"></i> {% trans "Validar no ITI" %}
</a>
{% if object.codigo_autenticacao %}
<a href="{% url 'sapl.materia:docacessorio_verificar_documento' object.pk %}?codigo={{ object.codigo_autenticacao }}"
class="btn btn-sm btn-outline-primary" target="_blank">
<i class="fa fa-qrcode"></i> {% trans "Verificar Autenticidade" %}
</a>
{% endif %}
</div>
</div>
</div>

27
sapl/templates/materia/materialegislativa_detail.html

@ -2,35 +2,40 @@
{% load i18n tz common_tags %}
{% block sub_actions %}
{{ block.super }}
<div class="d-flex flex-wrap align-items-center mt-1" style="gap: 0.35rem;">
<div class="actions btn-group btn-group-sm flex-wrap" role="group">
{% if view.list_url %}
<a href="{{ view.list_url }}" class="btn btn-outline-primary">{% trans 'Listar' %} {{view.verbose_name_plural}}</a>
{% endif %}
{% if view.search_url %}
<a href="{{ view.search_url }}" class="btn btn-outline-primary">{% trans 'Fazer Nova Pesquisa' %}</a>
{% endif %}
{% if object.em_tramitacao and mail_service_configured and google_recaptcha_configured %}
<a href="{% url 'sapl.materia:acompanhar_materia' object.id %}" class="btn btn-sm btn-outline-primary">
<a href="{% url 'sapl.materia:acompanhar_materia' object.id %}" class="btn btn-outline-primary">
<i class="fa fa-bell"></i> {% trans "Acompanhar Matéria" %}
</a>
{% endif %}
{% if is_autor and not object.numero_protocolo or can_edit_materia %}
<a class="btn btn-sm btn-primary" href="{% url 'sapl.materia:materia_onlyoffice_editor' object.pk %}">
<a class="btn btn-primary" href="{% url 'sapl.materia:materia_onlyoffice_editor' object.pk %}">
<i class="fa fa-file-word-o"></i> {% trans "Editar com OnlyOffice" %}
</a>
{% endif %}
{% if object.numero_protocolo and object.texto_original %}
{% if object.pdf_assinado %}
<a class="btn btn-sm btn-success" href="{% url 'sapl.materia:materia_pdf_assinado' object.pk %}" target="_blank">
<a class="btn btn-success" href="{% url 'sapl.materia:materia_pdf_assinado' object.pk %}" target="_blank">
<i class="fa fa-check-circle"></i> {% trans "Ver PDF Assinado" %}
</a>
{% endif %}
{% if is_autor and not ja_assinou %}
<button type="button" class="btn btn-sm btn-warning" data-toggle="modal" data-target="#assinaturaModal">
<button type="button" class="btn btn-warning" data-toggle="modal" data-target="#assinaturaModal">
<i class="fa fa-certificate"></i> {% trans "Assinar PDF Digitalmente" %}
</button>
{% endif %}
{% endif %}
{% if object.documentoacessorio_set.all.exists %}
<a class="btn btn-sm btn-danger" href="{% url 'sapl.materia:pdf_completo_materia' object.pk %}" title="{% trans 'Mescla matéria e acessórios em um único PDF' %}">
<a class="btn btn-danger" href="{% url 'sapl.materia:pdf_completo_materia' object.pk %}" title="{% trans 'Mescla matéria e acessórios em um único PDF' %}">
<i class="fa fa-file-pdf-o"></i> {% trans "Todos em PDF" %}
</a>
<a class="btn btn-sm btn-secondary" href="{% url 'sapl.materia:zip_completo_materia' object.pk %}" title="{% trans 'Baixa matéria e acessórios em arquivos separados (ZIP)' %}">
<a class="btn btn-secondary" href="{% url 'sapl.materia:zip_completo_materia' object.pk %}" title="{% trans 'Baixa matéria e acessórios em arquivos separados (ZIP)' %}">
<i class="fa fa-file-archive-o"></i> {% trans "Todos em ZIP" %}
</a>
{% endif %}
@ -219,6 +224,12 @@
<a href="https://validar.iti.gov.br/" class="btn btn-sm btn-outline-info" target="_blank">
<i class="fa fa-external-link"></i> {% trans "Validar no ITI" %}
</a>
{% if object.codigo_autenticacao %}
<a href="{% url 'sapl.materia:materia_verificar_documento' object.pk %}?codigo={{ object.codigo_autenticacao }}"
class="btn btn-sm btn-outline-primary" target="_blank">
<i class="fa fa-qrcode"></i> {% trans "Verificar Autenticidade" %}
</a>
{% endif %}
</div>
</div>
</div>

138
sapl/templates/materia/verificar_assinatura.html

@ -0,0 +1,138 @@
{% extends "base.html" %}
{% load i18n %}
{% block head_title %}{% trans "Verificação de Autenticidade" %} - {{ tipo_documento }}{% endblock %}
{% block base_content %}
<div class="container mt-4 mb-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<h3 class="text-center mb-4">
<i class="fa fa-shield"></i> {% trans "Verificação de Autenticidade" %}
</h3>
{% if valido %}
{# Status: VÁLIDO #}
<div class="card border-success mb-4">
<div class="card-header bg-success text-white text-center">
<h4 class="mb-0">
<i class="fa fa-check-circle"></i> {% trans "Documento Autêntico" %}
</h4>
</div>
<div class="card-body">
<p class="text-center text-success mb-0">
{% trans "O código de autenticação informado é válido. Este documento foi assinado digitalmente e sua integridade está confirmada." %}
</p>
</div>
</div>
{% elif codigo_informado %}
{# Status: INVÁLIDO #}
<div class="card border-danger mb-4">
<div class="card-header bg-danger text-white text-center">
<h4 class="mb-0">
<i class="fa fa-times-circle"></i> {% trans "Código Inválido" %}
</h4>
</div>
<div class="card-body">
<p class="text-center text-danger mb-0">
{% trans "O código de autenticação informado NÃO corresponde a este documento. O documento pode ter sido alterado ou o código está incorreto." %}
</p>
</div>
</div>
{% else %}
{# Sem código informado #}
<div class="card border-warning mb-4">
<div class="card-header bg-warning text-center">
<h4 class="mb-0">
<i class="fa fa-exclamation-triangle"></i> {% trans "Código não informado" %}
</h4>
</div>
<div class="card-body">
<p class="text-center mb-0">
{% trans "Nenhum código de autenticação foi informado para verificação." %}
</p>
</div>
</div>
{% endif %}
{# Dados do Documento #}
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="fa fa-file-text"></i> {% trans "Dados do Documento" %}</h5>
</div>
<div class="card-body">
<table class="table table-sm table-borderless mb-0">
<tr>
<th style="width: 200px;">{% trans "Tipo" %}</th>
<td>{{ tipo_documento }}</td>
</tr>
<tr>
<th>{% trans "Documento" %}</th>
<td>{{ descricao_documento }}</td>
</tr>
{% if ementa %}
<tr>
<th>{% trans "Ementa" %}</th>
<td>{{ ementa }}</td>
</tr>
{% endif %}
{% if codigo_autenticacao %}
<tr>
<th>{% trans "Código de Autenticação" %}</th>
<td><code>{{ codigo_autenticacao }}</code></td>
</tr>
{% endif %}
</table>
</div>
</div>
{# Lista de Assinaturas #}
{% if assinaturas %}
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">
<i class="fa fa-certificate"></i> {% trans "Assinaturas Digitais" %}
<span class="badge badge-primary ml-1">{{ assinaturas|length }}</span>
</h5>
</div>
<div class="card-body p-0">
<table class="table table-striped mb-0">
<thead>
<tr>
<th>{% trans "Assinante" %}</th>
<th>{% trans "Cargo" %}</th>
<th>{% trans "Data/Hora" %}</th>
<th>{% trans "Certificado" %}</th>
</tr>
</thead>
<tbody>
{% for assinatura in assinaturas %}
<tr>
<td><strong>{{ assinatura.nome_assinante }}</strong></td>
<td>{{ assinatura.cargo }}</td>
<td>{{ assinatura.data_assinatura }}</td>
<td>{{ assinatura.tipo_certificado_display }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{# Links úteis #}
<div class="text-center mt-4">
<a href="https://validar.iti.gov.br/" class="btn btn-outline-info" target="_blank">
<i class="fa fa-external-link"></i> {% trans "Validar assinatura no ITI (validar.iti.gov.br)" %}
</a>
</div>
<p class="text-muted text-center mt-3 small">
{% trans "Documento assinado digitalmente nos termos da Medida Provisória nº 2.200-2/2001." %}
</p>
</div>
</div>
</div>
{% endblock base_content %}
Loading…
Cancel
Save