diff --git a/modules/cybercore-app/src/main/java/br/com/cybercore/signer/app/log/InMemoryLogAppender.java b/modules/cybercore-app/src/main/java/br/com/cybercore/signer/app/log/InMemoryLogAppender.java new file mode 100644 index 0000000..259dafc --- /dev/null +++ b/modules/cybercore-app/src/main/java/br/com/cybercore/signer/app/log/InMemoryLogAppender.java @@ -0,0 +1,41 @@ +package br.com.cybercore.signer.app.log; + +import br.com.cybercore.signer.core.log.AppLogBuffer; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +/** + * Appender do Logback que intercepta eventos reais de log e os encaminha para o buffer em memória. + */ +public class InMemoryLogAppender extends AppenderBase { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + + @Override + protected void append(ILoggingEvent eventObject) { + if (eventObject == null) return; + + LocalDateTime time = LocalDateTime.ofInstant( + Instant.ofEpochMilli(eventObject.getTimeStamp()), + ZoneId.systemDefault() + ); + String timestamp = time.format(FORMATTER); + String level = eventObject.getLevel().toString(); + String loggerName = eventObject.getLoggerName(); + if (loggerName != null && loggerName.length() > 30) { + int lastDot = loggerName.lastIndexOf('.'); + if (lastDot > 0) { + loggerName = loggerName.substring(lastDot + 1); + } + } + String message = eventObject.getFormattedMessage(); + + String line = String.format("[%s] [%s] %s - %s", timestamp, level, loggerName, message); + AppLogBuffer.append(line); + } +} diff --git a/modules/cybercore-app/src/main/resources/logback.xml b/modules/cybercore-app/src/main/resources/logback.xml index 3626ce9..87c2bc4 100644 --- a/modules/cybercore-app/src/main/resources/logback.xml +++ b/modules/cybercore-app/src/main/resources/logback.xml @@ -6,8 +6,11 @@ + + + diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/log/AppLogBuffer.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/log/AppLogBuffer.java new file mode 100644 index 0000000..ed25081 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/log/AppLogBuffer.java @@ -0,0 +1,48 @@ +package br.com.cybercore.signer.core.log; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +/** + * Buffer circular thread-safe em memória para armazenar eventos reais de log da aplicação, + * permitindo auditoria visual em tempo real pela interface gráfica desktop. + */ +public final class AppLogBuffer { + + private static final int MAX_ENTRIES = 1000; + private static final Deque BUFFER = new ArrayDeque<>(MAX_ENTRIES); + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + + static { + appendSystemMessage("CyberCore Signer iniciado. Aguardando eventos criptográficos e conexões locais..."); + } + + private AppLogBuffer() { + } + + public static synchronized void append(String logLine) { + if (logLine == null || logLine.isBlank()) return; + if (BUFFER.size() >= MAX_ENTRIES) { + BUFFER.pollFirst(); + } + BUFFER.addLast(logLine.trim()); + } + + public static synchronized void appendSystemMessage(String msg) { + String timestamp = LocalDateTime.now().format(FORMATTER); + append("[" + timestamp + "] [INFO] " + msg); + } + + public static synchronized List getLogs() { + return new ArrayList<>(BUFFER); + } + + public static synchronized void clear() { + BUFFER.clear(); + appendSystemMessage("Histórico de logs limpo pelo usuário."); + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealRenderer.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealRenderer.java index baf143d..d38fc3b 100644 --- a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealRenderer.java +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealRenderer.java @@ -112,8 +112,10 @@ public final class PdfSealRenderer { String headerText = resolveHeaderText(theme); String footerText = resolveFooterText(theme, null); + Color footerColor = new Color(100, 116, 139); // Cinza corporativo neutro e sóbrio (#64748b) + if (hasIdentity && hasDate) { - // Layout com 5 linhas: Cabeçalho, Nome, Identificação (CPF/Matrícula), Data, Rodapé + // Layout com 5 linhas: Cabeçalho, Nome, Identificação (CPF/CNPJ/Matrícula), Data, Rodapé g2d.setColor(primaryColor); g2d.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 9)); g2d.drawString(headerText, textX, 16); @@ -127,9 +129,8 @@ public final class PdfSealRenderer { g2d.drawString("Data: " + data.signingDate(), textX, 60); - g2d.setColor(secondaryColor); - g2d.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 8)); - g2d.drawString(footerText, textX, 73); + g2d.setColor(footerColor); + drawFittedText(g2d, footerText, textX, 73, textWidthAvailable, 7.8f); } else if (hasIdentity || hasDate) { // Layout com 4 linhas: Cabeçalho, Nome, (Identificação ou Data), Rodapé @@ -145,9 +146,8 @@ public final class PdfSealRenderer { String singleLineInfo = hasIdentity ? identityLine : ("Data: " + data.signingDate()); drawFittedText(g2d, singleLineInfo, textX, 53, textWidthAvailable, 10.0f); - g2d.setColor(secondaryColor); - g2d.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 8)); - g2d.drawString(footerText, textX, 71); + g2d.setColor(footerColor); + drawFittedText(g2d, footerText, textX, 71, textWidthAvailable, 7.8f); } else { // Layout com 3 linhas: Cabeçalho, Nome em Destaque Máximo, Rodapé @@ -158,9 +158,8 @@ public final class PdfSealRenderer { g2d.setColor(textColor); drawFullSignerName(g2d, fullName, textX, 44, textWidthAvailable, 14.5f); - g2d.setColor(secondaryColor); - g2d.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 9)); - g2d.drawString(footerText, textX, 68); + g2d.setColor(footerColor); + drawFittedText(g2d, footerText, textX, 68, textWidthAvailable, 8.0f); } } finally { @@ -250,19 +249,28 @@ public final class PdfSealRenderer { * Formata os dados de documento (CPF/CNPJ) de acordo com o modo de exibição configurado. */ private static String formatSignerDoc(PdfSealData data) { - CpfDisplayMode mode = data.cpfDisplayMode() != null ? data.cpfDisplayMode() : CpfDisplayMode.FULL; - String rawDoc = data.identifier() != null ? data.identifier() : ""; + String rawDoc = data.identifier() != null ? data.identifier().trim() : ""; + if (rawDoc.isBlank()) return ""; - if (mode == CpfDisplayMode.NONE) { - // Se for CPF, oculta. Se for CNPJ, pode manter ou formatar. - if (rawDoc.toUpperCase().contains("CPF")) { - return ""; + String digits = rawDoc.replaceAll("\\D", ""); + + // Pessoa Jurídica (14 dígitos): Rótulo CNPJ, SEMPRE completo, sem mascaramento + if (digits.length() == 14 || rawDoc.toUpperCase().contains("CNPJ")) { + if (digits.length() == 14) { + return "CNPJ: " + String.format("%s.%s.%s/%s-%s", + digits.substring(0, 2), digits.substring(2, 5), digits.substring(5, 8), + digits.substring(8, 12), digits.substring(12, 14)); } - return rawDoc; + return rawDoc.toUpperCase().startsWith("CNPJ") ? rawDoc : "CNPJ: " + rawDoc; } - if (rawDoc.toUpperCase().contains("CPF")) { - String digits = rawDoc.replaceAll("\\D", ""); + // Pessoa Física (11 dígitos): Rótulo CPF, respeitando CpfDisplayMode + CpfDisplayMode mode = data.cpfDisplayMode() != null ? data.cpfDisplayMode() : CpfDisplayMode.FULL; + if (mode == CpfDisplayMode.NONE) { + return ""; + } + + if (digits.length() == 11) { String formatted = mode.formatCpf(digits); return formatted.isBlank() ? "" : "CPF: " + formatted; } @@ -378,10 +386,10 @@ public final class PdfSealRenderer { return theme.customHeader().toUpperCase(); } return switch (theme.id().toLowerCase()) { - case "cybercore-modern" -> "CYBERCORE SIGNER • AUTENTICAÇÃO DIGITAL"; + case "cybercore-modern" -> "DOCUMENTO ASSINADO DIGITALMENTE"; case "minimal-clean" -> "DOCUMENTO ASSINADO DIGITALMENTE"; - case "gov-br" -> "GOV.BR • CONFORMIDADE MP Nº 2.200-2/2001"; - default -> "ASSINADO DIGITALMENTE • ICP-BRASIL"; + case "gov-br" -> "DOCUMENTO ASSINADO DIGITALMENTE"; + default -> "DOCUMENTO ASSINADO DIGITALMENTE"; }; } @@ -393,10 +401,10 @@ public final class PdfSealRenderer { return "Hash Criptográfico: " + hash; } return switch (theme.id().toLowerCase()) { - case "cybercore-modern" -> "Autenticidade assegurada por chave criptográfica local"; - case "minimal-clean" -> "Assinatura qualificada padrão ICP-Brasil"; - case "gov-br" -> "Validade jurídica plena conforme MP nº 2.200-2/2001"; - default -> "Conformidade ICP-Brasil e validade jurídica plena"; + case "cybercore-modern" -> "Verifique a autenticidade em https://validar.iti.gov.br"; + case "minimal-clean" -> "Verifique a autenticidade em https://validar.iti.gov.br"; + case "gov-br" -> "Verifique a autenticidade em https://validar.iti.gov.br"; + default -> "Verifique a autenticidade em https://validar.iti.gov.br"; }; } diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealThemeRegistry.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealThemeRegistry.java index 2394154..8b94d92 100644 --- a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealThemeRegistry.java +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealThemeRegistry.java @@ -35,10 +35,10 @@ public final class PdfSealThemeRegistry { } private static void registerBuiltInThemes() { - // 1. Tema ICP-Brasil Institucional + // Único Selo Padrão Embutido do Sistema: ICP-Brasil Institucional Oficial registerTheme(new PdfSealTheme( - "icp-brasil", - "ICP-Brasil Institucional", + DEFAULT_THEME_ID, + "ICP-Brasil Institucional (Padrão)", "Estilo oficial corporativo com escudo de segurança vetorial e dados completos do titular", 320, 82, @@ -51,7 +51,7 @@ public final class PdfSealThemeRegistry { - + @@ -65,104 +65,7 @@ public final class PdfSealThemeRegistry { ${SIGNER_NAME} Doc: ${SIGNER_DOC} • AC: ${CERT_ISSUER} Data/Hora: ${SIGNING_DATE} - Verificação criptográfica válida e com fé pública - - """ - )); - - // 2. Tema CyberCore Moderno - registerTheme(new PdfSealTheme( - "cybercore-modern", - "CyberCore Moderno", - "Identidade tecnológica com cristal vetorial CyberCore e visual ciano/esmeralda", - 320, - 82, - "#0284c7", - "#10b981", - "#ffffff", - "#38bdf8", - "#0f172a", - """ - - - - - - - - - - - - CYBERCORE SIGNER • ASSINATURA ELETRÔNICA - ${SIGNER_NAME} - ${SIGNER_DOC} • ${CERT_ISSUER} - Autenticado em: ${SIGNING_DATE} - Hash: ${HASH_SUMMARY} - - """ - )); - - // 3. Tema Minimalista Clássico - registerTheme(new PdfSealTheme( - "minimal-clean", - "Minimalista Clássico", - "Design monocromático sóbrio em tons de grafite, ideal para contratos e certidões", - 300, - 76, - "#334155", - "#64748b", - "#fafafa", - "#cbd5e1", - "#1e293b", - """ - - - - - - - - - - Assinado Digitalmente - ${SIGNER_NAME} - ${SIGNER_DOC} • ${CERT_ISSUER} - ${SIGNING_DATE} - Conforme MP 2.200-2/2001 e ICP-Brasil - - """ - )); - - // 4. Tema Governamental (Gov.br / ITI) - registerTheme(new PdfSealTheme( - "gov-br", - "Governamental (Padrão Gov.br / ITI)", - "Faixa verde e ouro com declaração expressa de validade pela MP nº 2.200-2/2001", - 320, - 82, - "#047857", - "#eab308", - "#ffffff", - "#a7f3d0", - "#064e3b", - """ - - - - - - - - - - - - GOV.BR • DOCUMENTO ASSINADO DIGITALMENTE - ${SIGNER_NAME} - Doc: ${SIGNER_DOC} • AC: ${CERT_ISSUER} - Data/Hora: ${SIGNING_DATE} - Verifique a autenticidade da assinatura eletrônica em https://validar.iti.gov.br + Verifique a autenticidade em https://validar.iti.gov.br """ )); @@ -205,10 +108,9 @@ public final class PdfSealThemeRegistry { if (themeId == null) return false; String idLower = themeId.toLowerCase(); - // Protege temas padrão - if (idLower.equals("icp-brasil") || idLower.equals("cybercore-modern") || - idLower.equals("minimal-clean") || idLower.equals("gov-br")) { - throw new IllegalArgumentException("Não é permitido excluir os temas de selo pré-instalados do sistema."); + // Protege o tema padrão + if (idLower.equals(DEFAULT_THEME_ID)) { + throw new IllegalArgumentException("Não é permitido excluir o selo padrão pré-instalado do sistema."); } boolean deleted = STORAGE.deleteSeal(idLower); @@ -218,7 +120,11 @@ public final class PdfSealThemeRegistry { public static Optional findById(String id) { if (id == null) return Optional.empty(); - return Optional.ofNullable(THEMES.get(id.toLowerCase())); + PdfSealTheme theme = THEMES.get(id.toLowerCase()); + if (theme == null && ("cybercore-modern".equalsIgnoreCase(id) || "minimal-clean".equalsIgnoreCase(id) || "gov-br".equalsIgnoreCase(id))) { + return Optional.of(getDefaultTheme()); + } + return Optional.ofNullable(theme); } public static Optional getTheme(String id) { diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java index 91d9939..2a7b6d2 100644 --- a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java @@ -595,6 +595,9 @@ public class CcspServer { sealTheme = PdfSealThemeRegistry.getDefaultTheme(); } + logger.info("[CCSP REQ] Selo visual configurado: '{}' (custom={}, hasLogo={}, header='{}')", + sealTheme.name(), sealTheme.isCustom(), sealTheme.customLogoBase64() != null, sealTheme.customHeader()); + PdfSealPosition position = PdfSealPosition.BOTTOM_RIGHT; if (request.x() != null && request.y() != null) { position = PdfSealPosition.CUSTOM; @@ -916,34 +919,71 @@ public class CcspServer { } } + private static String getMapString(Map map, String defaultVal, String... keys) { + if (map == null) return defaultVal; + for (String k : keys) { + Object v = map.get(k); + if (v != null && !v.toString().isBlank()) { + return v.toString().trim(); + } + } + return defaultVal; + } + + private static int getMapInt(Map map, int defaultVal, String... keys) { + if (map == null) return defaultVal; + for (String k : keys) { + Object v = map.get(k); + if (v instanceof Number n) { + return n.intValue(); + } else if (v != null) { + try { + return Integer.parseInt(v.toString().trim()); + } catch (Exception ignored) {} + } + } + return defaultVal; + } + private PdfSealTheme parseCustomSealTheme(Map map) { if (map == null || map.isEmpty()) { return PdfSealThemeRegistry.getDefaultTheme(); } String id = "custom-" + UUID.randomUUID().toString().substring(0, 8); - String name = (String) map.getOrDefault("name", "Selo Personalizado"); - String description = (String) map.getOrDefault("description", "Selo customizado via aplicação web"); - int width = map.containsKey("width") ? ((Number) map.get("width")).intValue() : 320; - int height = map.containsKey("height") ? ((Number) map.get("height")).intValue() : 82; - String primaryColor = (String) map.getOrDefault("primaryColor", "#0d47a1"); - String secondaryColor = (String) map.getOrDefault("secondaryColor", "#0284c7"); - String backgroundColor = (String) map.getOrDefault("backgroundColor", "#ffffff"); - String borderColor = (String) map.getOrDefault("borderColor", "#cbd5e1"); - String textColor = (String) map.getOrDefault("textColor", "#0f172a"); - String customHeader = (String) map.get("customHeader"); - String customFooter = (String) map.get("customFooter"); - String iconStyle = (String) map.getOrDefault("iconStyle", "shield"); - String customLogoBase64 = (String) map.get("customLogoBase64"); - String customFieldLabel = (String) map.get("customFieldLabel"); - String customFieldValue = (String) map.get("customFieldValue"); + String name = getMapString(map, "Selo Personalizado", "name", "theme_name", "title"); + String description = getMapString(map, "Selo customizado via aplicação web", "description", "desc"); + int width = getMapInt(map, 320, "width", "w"); + int height = getMapInt(map, 82, "height", "h"); + String primaryColor = getMapString(map, "#0d47a1", "primaryColor", "primary_color"); + String secondaryColor = getMapString(map, "#0284c7", "secondaryColor", "secondary_color"); + String backgroundColor = getMapString(map, "#ffffff", "backgroundColor", "background_color"); + String borderColor = getMapString(map, "#cbd5e1", "borderColor", "border_color"); + String textColor = getMapString(map, "#0f172a", "textColor", "text_color"); + String customHeader = getMapString(map, null, "customHeader", "custom_header", "title"); + String customFooter = getMapString(map, null, "customFooter", "custom_footer"); + String iconStyle = getMapString(map, "shield", "iconStyle", "icon_style"); + String customLogoBase64 = getMapString(map, null, "customLogoBase64", "custom_logo_base64", "logo", "logoBase64"); + String customFieldLabel = getMapString(map, null, "customFieldLabel", "custom_field_label"); + String customFieldValue = getMapString(map, null, "customFieldValue", "custom_field_value"); CpfDisplayMode cpfMode = CpfDisplayMode.FULL; - if (map.containsKey("defaultCpfMode") && map.get("defaultCpfMode") != null) { + String rawCpfMode = getMapString(map, null, "defaultCpfMode", "default_cpf_mode", "cpfMode", "cpf_mode"); + if (rawCpfMode != null) { try { - cpfMode = CpfDisplayMode.valueOf(map.get("defaultCpfMode").toString().toUpperCase()); + cpfMode = CpfDisplayMode.valueOf(rawCpfMode.toUpperCase()); } catch (Exception ignored) {} } - boolean includeDateTime = !map.containsKey("defaultIncludeDateTime") || Boolean.TRUE.equals(map.get("defaultIncludeDateTime")); + + boolean includeDateTime = true; + Object dtObj = map.containsKey("defaultIncludeDateTime") ? map.get("defaultIncludeDateTime") + : (map.containsKey("default_include_date_time") ? map.get("default_include_date_time") + : (map.containsKey("includeDateTime") ? map.get("includeDateTime") + : map.get("include_date_time"))); + if (dtObj instanceof Boolean b) { + includeDateTime = b; + } else if (dtObj != null) { + includeDateTime = Boolean.parseBoolean(dtObj.toString()); + } return new PdfSealTheme( id, name, description, width, height, diff --git a/modules/cybercore-core/src/main/resources/assets/seal-icp-brasil.svg b/modules/cybercore-core/src/main/resources/assets/seal-icp-brasil.svg new file mode 100644 index 0000000..c6b0cc3 --- /dev/null +++ b/modules/cybercore-core/src/main/resources/assets/seal-icp-brasil.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/components/CertificateCardNode.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/components/CertificateCardNode.java index e4273e8..71cec39 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/components/CertificateCardNode.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/components/CertificateCardNode.java @@ -38,17 +38,16 @@ public final class CertificateCardNode extends VBox { HBox topRow = new HBox(10); topRow.setAlignment(Pos.CENTER_LEFT); - Label titleLabel = new Label(certificate.commonName() != null ? certificate.commonName() : certificate.alias()); + String rawName = certificate.commonName() != null ? certificate.commonName() : certificate.alias(); + String cleanTitle = rawName != null && rawName.contains(":") ? rawName.split(":")[0].trim() : (rawName != null ? rawName.trim() : "Certificado Digital"); + Label titleLabel = new Label(cleanTitle); titleLabel.getStyleClass().add("cert-title"); HBox.setHgrow(titleLabel, Priority.ALWAYS); - Label badgeLabel = new Label(); + Label badgeLabel = new Label("[" + certificate.type().getCode() + "]"); if (certificate.type() == CertificateType.A1) { - badgeLabel.setText("A1 • Certificado local"); badgeLabel.getStyleClass().add("cert-badge-a1"); } else { - String tokenDesc = certificate.hardwareDeviceName() != null ? certificate.hardwareDeviceName() : "Token USB"; - badgeLabel.setText("A3 • " + tokenDesc); badgeLabel.getStyleClass().add("cert-badge-a3"); } diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/LogsViewerDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/LogsViewerDialog.java index dcfd313..c517767 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/LogsViewerDialog.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/LogsViewerDialog.java @@ -57,24 +57,43 @@ public final class LogsViewerDialog { txtLogs.getStyleClass().add("log-console-area"); VBox.setVgrow(txtLogs, Priority.ALWAYS); - loadInitialLogs(); + loadLogs(); HBox footer = new HBox(10); footer.setAlignment(Pos.CENTER_RIGHT); + Button btnCopy = new Button("Copiar"); + btnCopy.getStyleClass().addAll("btn-secondary", "btn-dialog"); + btnCopy.setPrefWidth(90); + btnCopy.setOnAction(e -> { + javafx.scene.input.ClipboardContent content = new javafx.scene.input.ClipboardContent(); + content.putString(txtLogs.getText()); + javafx.scene.input.Clipboard.getSystemClipboard().setContent(content); + btnCopy.setText("✓ Copiado"); + javafx.animation.PauseTransition pause = new javafx.animation.PauseTransition(javafx.util.Duration.seconds(2)); + pause.setOnFinished(ev -> btnCopy.setText("Copiar")); + pause.play(); + }); + + Button btnClear = new Button("Limpar"); + btnClear.getStyleClass().addAll("btn-secondary", "btn-dialog"); + btnClear.setPrefWidth(90); + btnClear.setOnAction(e -> { + br.com.cybercore.signer.core.log.AppLogBuffer.clear(); + loadLogs(); + }); + Button btnRefresh = new Button("Atualizar"); btnRefresh.getStyleClass().addAll("btn-secondary", "btn-dialog"); - btnRefresh.setPrefWidth(110); - btnRefresh.setMinWidth(110); - btnRefresh.setOnAction(e -> loadInitialLogs()); + btnRefresh.setPrefWidth(100); + btnRefresh.setOnAction(e -> loadLogs()); Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13)); btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog"); - btnClose.setPrefWidth(110); - btnClose.setMinWidth(110); + btnClose.setPrefWidth(100); btnClose.setOnAction(e -> stage.close()); - footer.getChildren().addAll(btnRefresh, btnClose); + footer.getChildren().addAll(btnCopy, btnClear, btnRefresh, btnClose); root.getChildren().addAll(header, txtLogs, footer); @@ -84,15 +103,15 @@ public final class LogsViewerDialog { stage.setScene(scene); } - private void loadInitialLogs() { - String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); - String sb = "[" + now + "] [INFO] CyberCore Signer inicializado com sucesso.\n" + - "[" + now + "] [INFO] Servidor local CCSP/1 escutando em http://127.0.0.1:21325\n" + - "[" + now + "] [INFO] Provedores criptográficos registrados: Windows Certificate Store, PKCS#12, PKCS#11.\n" + - "[" + now + "] [INFO] SmartCardWatcher ativo (monitoramento de tokens USB e Smart Cards ICP-Brasil).\n" + - "[" + now + "] [INFO] Política anti-replay: nonces protegidos em memória.\n" + - "[" + now + "] [INFO] Registro de aplicações web ativo e pronto para autorização segura.\n"; - txtLogs.setText(sb); + private void loadLogs() { + java.util.List logs = br.com.cybercore.signer.core.log.AppLogBuffer.getLogs(); + if (logs.isEmpty()) { + txtLogs.setText("[Aguardando eventos de log...]"); + } else { + txtLogs.setText(String.join("\n", logs) + "\n"); + } + txtLogs.positionCaret(txtLogs.getText().length()); + txtLogs.setScrollTop(Double.MAX_VALUE); } private VBox createHeaderLabel(String title, String subtitle) { diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SignaturePromptDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SignaturePromptDialog.java index 6e3be24..83e8d2b 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SignaturePromptDialog.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SignaturePromptDialog.java @@ -106,9 +106,24 @@ public class SignaturePromptDialog { } addField(grid, row++, "Operação:", "Assinatura Digital (Digest)"); } - addField(grid, row++, "Certificado:", cert.commonName() != null ? cert.commonName() : cert.alias()); - addField(grid, row++, "Tipo / Dispositivo:", cert.type().getCode() + " • " + - (cert.hardwareDeviceName() != null ? cert.hardwareDeviceName() : cert.type().getLabel())); + String rawCertName = cert.commonName() != null ? cert.commonName() : cert.alias(); + String holderName = rawCertName.contains(":") ? rawCertName.split(":")[0].trim() : rawCertName.trim(); + String docInfo = ""; + if (cert.cnpj() != null && !cert.cnpj().isBlank()) { + docInfo = ", CNPJ " + cert.cnpj(); + } else if (cert.cpf() != null && !cert.cpf().isBlank()) { + docInfo = ", CPF " + cert.cpf(); + } else if (rawCertName.contains(":")) { + String candidate = rawCertName.split(":")[1].trim(); + if (candidate.length() == 14) { + docInfo = ", CNPJ " + candidate; + } else if (candidate.length() == 11) { + docInfo = ", CPF " + candidate; + } + } + String certDesc = holderName + docInfo + " [" + cert.type().getCode() + "]"; + addField(grid, row++, "Certificado:", certDesc); + addField(grid, row++, "Tipo:", cert.type().getCode() + " (" + cert.type().getLabel() + ")"); addField(grid, row++, "Algoritmo:", context.algorithm()); detailsBox.getChildren().add(grid); diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/seal-icp-brasil.svg b/modules/cybercore-ui/src/main/resources/assets/icons/seal-icp-brasil.svg new file mode 100644 index 0000000..c6b0cc3 --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/seal-icp-brasil.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/sdk/demo/index.html b/sdk/demo/index.html index 6663392..ea27170 100644 --- a/sdk/demo/index.html +++ b/sdk/demo/index.html @@ -107,6 +107,7 @@ letter-spacing: 0.5px; background: linear-gradient(to right, #ffffff, #cbd5e1); -webkit-background-clip: text; + background-clip: text; -webkit-text-fill-color: transparent; } @@ -671,11 +672,8 @@
@@ -1031,7 +1029,31 @@ certs.forEach((c) => { const opt = document.createElement('option'); opt.value = c.id; - opt.textContent = `${c.common_name || c.alias} [${c.type}] (${c.device_name || c.provider_id})`; + + let rawName = c.common_name || c.alias || 'Certificado'; + let displayName = rawName; + let docNum = c.cpf || c.cnpj || c.cpf_cnpj || ''; + + if (rawName.includes(':')) { + const parts = rawName.split(':'); + displayName = parts[0].trim(); + docNum = parts[1].trim(); + } + } + + const digits = docNum ? String(docNum).replace(/\D/g, '') : ''; + let docFormatted = ''; + if (digits.length === 14) { + docFormatted = `CNPJ ${digits.replace(/^(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})$/, '../-')}`; + } else if (digits.length === 11) { + docFormatted = `CPF ${digits.replace(/^(\d{3})(\d{3})(\d{3})(\d{2})$/, '..-')}`; + } else if (docNum) { + docFormatted = docNum; + } + + const typeStr = `[${c.type || 'A1'}]`; + const text = docFormatted ? `${displayName}, ${docFormatted} ${typeStr}` : `${displayName} ${typeStr}`; + opt.textContent = text; certSelect.appendChild(opt); }); @@ -1054,9 +1076,25 @@ certValidityText.textContent = `Validade: ${cert.valid_to ? new Date(cert.valid_to).toLocaleDateString() : 'N/D'}`; if (pdfStampPicker) { + const rawDoc = cert.cpf || cert.cnpj || cert.cpf_cnpj || ''; + const digits = String(rawDoc).replace(/\D/g, ''); + let docStr = ''; + if (digits.length === 14) { + docStr = `CNPJ: ${digits.replace(/^(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})$/, '../-')}`; + } else if (digits.length === 11) { + docStr = `CPF: ${digits.replace(/^(\d{3})(\d{3})(\d{3})(\d{2})$/, '..-')}`; + } else { + docStr = cert.type === 'A3' ? 'Token Físico A3' : 'ICP-Brasil'; + } + + let cleanSignerName = cert.common_name || cert.alias || 'Titular'; + if (cleanSignerName && cleanSignerName.includes(':')) { + cleanSignerName = cleanSignerName.split(':')[0].trim(); + } + pdfStampPicker.updateSealTheme(null, { - signerName: cert.common_name || cert.alias, - signerDoc: cert.cpf_cnpj ? `CPF: ${cert.cpf_cnpj}` : (cert.type === 'A3' ? 'Token Físico A3' : 'ICP-Brasil') + signerName: cleanSignerName, + signerDoc: docStr }); } } else { @@ -1209,10 +1247,15 @@ // Sincroniza alteração de tema com o selo sobre o PDF pdfSealThemeSelect.addEventListener('change', () => { const themeVal = pdfSealThemeSelect.value; - if (pdfStampPicker) { - if (themeVal === 'custom' && currentCustomSealConfig) { + if (themeVal === 'custom') { + if (!currentCustomSealConfig && typeof pdfSealDesigner !== 'undefined' && pdfSealDesigner) { + currentCustomSealConfig = pdfSealDesigner.getConfig(); + } + if (pdfStampPicker && currentCustomSealConfig) { pdfStampPicker.updateSealTheme(currentCustomSealConfig); - } else { + } + } else { + if (pdfStampPicker) { pdfStampPicker.updateSealTheme(themeVal); } } @@ -1230,21 +1273,29 @@ const stampConfig = pdfStampPicker ? pdfStampPicker.getStampConfig() : { page: 1, x: 50, y: 50, scale: 1.0 }; const selectedTheme = pdfSealThemeSelect.value; + let effectiveCustomSeal = null; + if (selectedTheme === 'custom') { + if (currentCustomSealConfig) { + effectiveCustomSeal = currentCustomSealConfig; + } else if (typeof pdfSealDesigner !== 'undefined' && pdfSealDesigner) { + effectiveCustomSeal = pdfSealDesigner.getConfig(); + currentCustomSealConfig = effectiveCustomSeal; + } + } + const signParams = { certificateId: selectedCertificateId, pdf: currentPdfFile || currentPdfBytes, page: stampConfig.page, x: stampConfig.x, y: stampConfig.y, - scale: stampConfig.scale + scale: stampConfig.scale, + sealCustom: effectiveCustomSeal, + seal_custom: effectiveCustomSeal, + sealThemeId: (selectedTheme === 'custom' ? null : selectedTheme), + seal_theme_id: (selectedTheme === 'custom' ? null : selectedTheme) }; - if (selectedTheme === 'custom' && currentCustomSealConfig) { - signParams.seal_custom = currentCustomSealConfig; - } else { - signParams.seal_theme_id = selectedTheme === 'custom' ? 'icp-brasil' : selectedTheme; - } - const result = await signer.signPdfDocument(signParams); pdfAlert.className = 'alert alert-success'; @@ -1322,22 +1373,24 @@ const mount = document.getElementById('sealDesignerMount'); pdfSealDesigner = new PdfSealDesigner({ container: mount, - initialConfig: { + initialTheme: { name: 'Selo Customizado', - title: 'ASSINADO DIGITALMENTE', - primary_color: '#0284c7', - secondary_color: '#10b981', - background_color: '#ffffff', - text_color: '#0f172a', - border_color: '#38bdf8', - icon_style: 'shield', - default_cpf_mode: 'MASKED', - default_include_date_time: true + customHeader: 'DOCUMENTO ASSINADO DIGITALMENTE', + primaryColor: '#0284c7', + secondaryColor: '#10b981', + backgroundColor: '#ffffff', + textColor: '#0f172a', + borderColor: '#38bdf8', + iconStyle: 'shield', + defaultCpfMode: 'MASKED', + defaultIncludeDateTime: true }, - onConfigChange: (cfg) => { + onChange: (cfg) => { currentCustomSealConfig = cfg; } }); + // Inicializa currentCustomSealConfig com o estado atual do designer + currentCustomSealConfig = pdfSealDesigner.getConfig(); // Botão para aplicar o selo na aba de PDF const applyBtnWrap = document.createElement('div'); diff --git a/sdk/src/pdf-seal-designer.js b/sdk/src/pdf-seal-designer.js index 269776a..c74f0ec 100644 --- a/sdk/src/pdf-seal-designer.js +++ b/sdk/src/pdf-seal-designer.js @@ -20,7 +20,31 @@ export class PdfSealDesigner { throw new Error('Container DOM obrigatório para instanciar PdfSealDesigner.'); } - this.onChange = options.onChange || null; + this.onChange = options.onChange || options.onConfigChange || null; + + const rawInitial = options.initialTheme || options.initialConfig || {}; + const normalizedInitial = { + name: rawInitial.name, + description: rawInitial.description, + width: rawInitial.width, + height: rawInitial.height, + primaryColor: rawInitial.primaryColor || rawInitial.primary_color, + secondaryColor: rawInitial.secondaryColor || rawInitial.secondary_color, + backgroundColor: rawInitial.backgroundColor || rawInitial.background_color, + borderColor: rawInitial.borderColor || rawInitial.border_color, + textColor: rawInitial.textColor || rawInitial.text_color, + iconStyle: rawInitial.iconStyle || rawInitial.icon_style, + customLogoBase64: rawInitial.customLogoBase64 || rawInitial.custom_logo_base64, + customHeader: rawInitial.customHeader || rawInitial.custom_header || rawInitial.title, + customFooter: rawInitial.customFooter || rawInitial.custom_footer, + customFieldLabel: rawInitial.customFieldLabel || rawInitial.custom_field_label, + customFieldValue: rawInitial.customFieldValue || rawInitial.custom_field_value, + defaultCpfMode: rawInitial.defaultCpfMode || rawInitial.default_cpf_mode, + defaultIncludeDateTime: rawInitial.defaultIncludeDateTime !== undefined ? rawInitial.defaultIncludeDateTime : rawInitial.default_include_date_time + }; + + // Remove chaves indefinidas para não sobrescrever os defaults + Object.keys(normalizedInitial).forEach(k => normalizedInitial[k] === undefined && delete normalizedInitial[k]); // Estado do tema this.theme = { @@ -37,11 +61,11 @@ export class PdfSealDesigner { customLogoBase64: null, customHeader: 'DOCUMENTO ASSINADO DIGITALMENTE', customFooter: 'Verifique a autenticidade da assinatura eletrônica em https://validar.iti.gov.br', - customFieldLabel: 'Cargo', - customFieldValue: 'Diretor Responsável', + customFieldLabel: '', + customFieldValue: '', defaultCpfMode: 'MASKED', defaultIncludeDateTime: true, - ...(options.initialTheme || {}) + ...normalizedInitial }; this._initDom(); diff --git a/sdk/src/signer-sdk.js b/sdk/src/signer-sdk.js index 53cf507..d7be5c2 100644 --- a/sdk/src/signer-sdk.js +++ b/sdk/src/signer-sdk.js @@ -367,13 +367,13 @@ export class CyberCoreSigner { y: params.y !== undefined ? Number(params.y) : null, scale: params.scale !== undefined ? Number(params.scale) : 1.0, visible_seal: params.visibleSeal !== undefined ? Boolean(params.visibleSeal) : true, - seal_theme_id: params.sealThemeId || params.sealTheme || null, - seal_custom: params.sealCustom || null, + seal_theme_id: params.sealThemeId || params.seal_theme_id || params.sealTheme || null, + seal_custom: params.sealCustom || params.seal_custom || null, reason: params.reason || 'Assinatura Digital ICP-Brasil', location: params.location || 'Brasil', - cpf_display_mode: params.cpfDisplayMode || null, - include_date_time: params.includeDateTime !== undefined ? Boolean(params.includeDateTime) : null, - custom_extra_info: params.customExtraInfo || null, + cpf_display_mode: params.cpfDisplayMode || params.cpf_display_mode || null, + include_date_time: params.includeDateTime !== undefined ? Boolean(params.includeDateTime) : (params.include_date_time !== undefined ? Boolean(params.include_date_time) : null), + custom_extra_info: params.customExtraInfo || params.custom_extra_info || null, nonce: nonce, expires_at: expiresAt };