Use cases

Certificados

Gere certificados de conclusão, participação e reconhecimento automaticamente

Este guia mostra como implementar geração automática de certificados profissionais usando RenderHub.

Visão Geral

Certificados são perfeitos para:

  • ✅ Plataformas de educação online (cursos, treinamentos)
  • ✅ Eventos (conferências, workshops, webinars)
  • ✅ Programas de reconhecimento (funcionário do mês, etc)
  • ✅ Certificações profissionais

Estrutura de Dados

interface Certificate {
  // Identificação
  id: string;
  certificateNumber: string; // CERT-2024-001
  type: 'completion' | 'participation' | 'achievement' | 'recognition';

  // Participante
  recipient: {
    name: string;
    email: string;
    documentId?: string; // CPF para validação
  };

  // Curso/Evento
  course: {
    name: string;
    description?: string;
    duration?: number; // horas
    startDate?: Date;
    endDate?: Date;
  };

  // Instituição
  institution: {
    name: string;
    logo?: string;
    website?: string;
  };

  // Assinaturas
  signatures: Array<{
    name: string;
    title: string;
    signature?: string; // imagem da assinatura
  }>;

  // Metadados
  issueDate: Date;
  validUntil?: Date;
  score?: number; // 0-100
  grade?: string; // A+, A, B, etc
  validationUrl?: string; // URL para validar autenticidade
}

Template HTML

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    @page {
      size: A4 landscape;
      margin: 0;
    }

    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      font-family: 'Georgia', serif;
      width: 297mm;
      height: 210mm;
      position: relative;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      display: flex;
      align-items: center;
      justify-content: center;
    }

    .certificate {
      width: 275mm;
      height: 190mm;
      background: white;
      border: 15px solid #f8f9fa;
      box-shadow: 0 0 50px rgba(0,0,0,0.3);
      position: relative;
      padding: 40px 60px;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: space-between;
    }

    .border-decoration {
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      border: 3px solid #667eea;
      margin: 25px;
      pointer-events: none;
    }

    .logo {
      max-width: 150px;
      margin-bottom: 20px;
    }

    .certificate-title {
      font-size: 48px;
      font-weight: bold;
      color: #667eea;
      text-transform: uppercase;
      letter-spacing: 4px;
      margin-bottom: 15px;
      text-align: center;
    }

    .certificate-subtitle {
      font-size: 18px;
      color: #6b7280;
      margin-bottom: 40px;
      text-align: center;
    }

    .recipient-section {
      text-align: center;
      margin-bottom: 30px;
    }

    .awarded-text {
      font-size: 16px;
      color: #4b5563;
      margin-bottom: 10px;
    }

    .recipient-name {
      font-size: 42px;
      font-weight: bold;
      color: #1f2937;
      border-bottom: 2px solid #667eea;
      padding-bottom: 5px;
      margin-bottom: 25px;
      display: inline-block;
      min-width: 400px;
      text-align: center;
    }

    .course-info {
      text-align: center;
      max-width: 700px;
      margin-bottom: 30px;
    }

    .completion-text {
      font-size: 16px;
      line-height: 1.8;
      color: #4b5563;
      margin-bottom: 10px;
    }

    .course-name {
      font-size: 24px;
      font-weight: bold;
      color: #1f2937;
      margin: 15px 0;
    }

    .course-details {
      font-size: 14px;
      color: #6b7280;
      margin-top: 10px;
    }

    .performance {
      background: #f0f9ff;
      border: 2px solid #667eea;
      border-radius: 8px;
      padding: 15px 30px;
      margin: 20px 0;
      display: inline-block;
    }

    .performance-label {
      font-size: 14px;
      color: #4b5563;
    }

    .performance-value {
      font-size: 28px;
      font-weight: bold;
      color: #667eea;
    }

    .footer {
      width: 100%;
      display: flex;
      justify-content: space-between;
      align-items: flex-end;
      margin-top: auto;
    }

    .date-section {
      text-align: left;
    }

    .date-label {
      font-size: 12px;
      color: #9ca3af;
      text-transform: uppercase;
    }

    .date-value {
      font-size: 14px;
      color: #4b5563;
      margin-top: 5px;
    }

    .signatures {
      display: flex;
      gap: 60px;
      justify-content: center;
    }

    .signature {
      text-align: center;
      min-width: 180px;
    }

    .signature-image {
      max-width: 150px;
      max-height: 60px;
      margin-bottom: 10px;
    }

    .signature-line {
      border-top: 2px solid #1f2937;
      padding-top: 8px;
    }

    .signature-name {
      font-size: 14px;
      font-weight: bold;
      color: #1f2937;
    }

    .signature-title {
      font-size: 12px;
      color: #6b7280;
      margin-top: 2px;
    }

    .validation {
      text-align: right;
      font-size: 10px;
      color: #9ca3af;
    }

    .certificate-number {
      font-weight: bold;
      color: #4b5563;
    }

    .qr-code {
      width: 80px;
      height: 80px;
      margin-top: 5px;
    }
  </style>
</head>
<body>
  <div class="certificate">
    <div class="border-decoration"></div>

    {{#if institution.logo}}
    <img src="{{institution.logo}}" alt="{{institution.name}}" class="logo">
    {{/if}}

    <div class="certificate-title">Certificado</div>
    <div class="certificate-subtitle">{{typeLabel type}}</div>

    <div class="recipient-section">
      <div class="awarded-text">Certificamos que</div>
      <div class="recipient-name">{{recipient.name}}</div>
    </div>

    <div class="course-info">
      <div class="completion-text">
        {{#if type "completion"}}
        concluiu com êxito o curso
        {{else if type "participation"}}
        participou do evento
        {{else}}
        recebeu reconhecimento por
        {{/if}}
      </div>

      <div class="course-name">{{course.name}}</div>

      {{#if course.description}}
      <div class="course-details">{{course.description}}</div>
      {{/if}}

      {{#if course.duration}}
      <div class="course-details">
        Carga horária: {{course.duration}} horas
        {{#if course.startDate}}
        | Período: {{formatDate course.startDate}} a {{formatDate course.endDate}}
        {{/if}}
      </div>
      {{/if}}
    </div>

    {{#if score}}
    <div class="performance">
      <div class="performance-label">Nota Final</div>
      <div class="performance-value">
        {{score}}{{#if grade}} ({{grade}}){{/if}}
      </div>
    </div>
    {{/if}}

    <div class="footer">
      <div class="date-section">
        <div class="date-label">Data de Emissão</div>
        <div class="date-value">{{formatDate issueDate}}</div>
        {{#if validUntil}}
        <div class="date-label" style="margin-top: 10px;">Válido até</div>
        <div class="date-value">{{formatDate validUntil}}</div>
        {{/if}}
      </div>

      <div class="signatures">
        {{#each signatures}}
        <div class="signature">
          {{#if signature}}
          <img src="{{signature}}" alt="Assinatura" class="signature-image">
          {{/if}}
          <div class="signature-line">
            <div class="signature-name">{{name}}</div>
            <div class="signature-title">{{title}}</div>
          </div>
        </div>
        {{/each}}
      </div>

      <div class="validation">
        <div class="certificate-number">Nº {{certificateNumber}}</div>
        {{#if validationUrl}}
        <div style="margin-top: 8px;">Valide em:</div>
        <div>{{validationUrl}}</div>
        {{#if qrCode}}
        <img src="{{qrCode}}" alt="QR Code" class="qr-code">
        {{/if}}
        {{/if}}
      </div>
    </div>
  </div>
</body>
</html>

Implementação

1. Serviço de Certificados

// src/services/certificate.service.ts
import { RenderHubClient } from '@renderhub/sdk';
import QRCode from 'qrcode';
import { Certificate } from '../models/Certificate';

export class CertificateService {
  private renderHub: RenderHubClient;

  constructor() {
    this.renderHub = new RenderHubClient({
      apiKey: process.env.RENDERHUB_API_KEY!,
    });
  }

  async generateCertificate(certificate: Certificate): Promise<Buffer> {
    // 1. Gerar QR Code para validação
    const qrCodeDataUrl = await this.generateQRCode(certificate.validationUrl);

    // 2. Preparar dados
    const templateData = {
      ...certificate,
      issueDate: certificate.issueDate.toLocaleDateString('pt-BR'),
      validUntil: certificate.validUntil?.toLocaleDateString('pt-BR'),
      qrCode: qrCodeDataUrl,
    };

    // 3. Gerar PDF
    const pdf = await this.renderHub.generatePDF(
      'certificate-v1',
      templateData
    );

    return pdf;
  }

  private async generateQRCode(url?: string): Promise<string | undefined> {
    if (!url) return undefined;

    return await QRCode.toDataURL(url, {
      width: 200,
      margin: 1,
    });
  }

  async issueOnCompletion(userId: string, courseId: string): Promise<Certificate> {
    // 1. Verificar se completou o curso
    const progress = await this.getCourseProgress(userId, courseId);

    if (progress.completionPercentage < 100) {
      throw new Error('Curso não completado');
    }

    // 2. Criar certificado
    const user = await this.getUser(userId);
    const course = await this.getCourse(courseId);

    const certificateNumber = await this.generateCertificateNumber();
    const validationUrl = `${process.env.APP_URL}/validate/${certificateNumber}`;

    const certificate: Certificate = {
      id: crypto.randomUUID(),
      certificateNumber,
      type: 'completion',
      recipient: {
        name: user.name,
        email: user.email,
        documentId: user.cpf,
      },
      course: {
        name: course.name,
        description: course.description,
        duration: course.durationHours,
        startDate: progress.startedAt,
        endDate: progress.completedAt!,
      },
      institution: {
        name: 'Minha Plataforma',
        logo: 'https://cdn.example.com/logo.png',
        website: 'https://example.com',
      },
      signatures: [
        {
          name: 'João Silva',
          title: 'Diretor de Educação',
          signature: 'https://cdn.example.com/signature-joao.png',
        },
      ],
      issueDate: new Date(),
      score: progress.finalScore,
      grade: this.calculateGrade(progress.finalScore),
      validationUrl,
    };

    // 3. Salvar no banco
    await this.saveCertificate(certificate);

    // 4. Gerar PDF
    const pdf = await this.generateCertificate(certificate);

    // 5. Upload para S3
    await this.uploadCertificate(certificate.id, pdf);

    // 6. Enviar email
    await this.sendCertificateEmail(certificate, pdf);

    return certificate;
  }

  private calculateGrade(score: number): string {
    if (score >= 95) return 'A+';
    if (score >= 90) return 'A';
    if (score >= 85) return 'B+';
    if (score >= 80) return 'B';
    if (score >= 75) return 'C+';
    if (score >= 70) return 'C';
    return 'D';
  }

  private async generateCertificateNumber(): Promise<string> {
    const year = new Date().getFullYear();
    const count = await this.getCertificateCount(year);
    return `CERT-${year}-${String(count + 1).padStart(6, '0')}`;
  }

  // Métodos auxiliares (implementar conforme seu banco de dados)
  private async getCourseProgress(userId: string, courseId: string): Promise<any> {
    throw new Error('Not implemented');
  }

  private async getUser(userId: string): Promise<any> {
    throw new Error('Not implemented');
  }

  private async getCourse(courseId: string): Promise<any> {
    throw new Error('Not implemented');
  }

  private async saveCertificate(certificate: Certificate): Promise<void> {
    throw new Error('Not implemented');
  }

  private async uploadCertificate(id: string, pdf: Buffer): Promise<void> {
    throw new Error('Not implemented');
  }

  private async sendCertificateEmail(certificate: Certificate, pdf: Buffer): Promise<void> {
    throw new Error('Not implemented');
  }

  private async getCertificateCount(year: number): Promise<number> {
    throw new Error('Not implemented');
  }
}

2. Endpoint de Validação

// src/routes/certificates.routes.ts
import { Router } from 'express';
import { CertificateService } from '../services/certificate.service';

const router = Router();
const certificateService = new CertificateService();

// Validar autenticidade de certificado
router.get('/validate/:number', async (req, res) => {
  try {
    const certificate = await db.certificates.findOne({
      where: { certificateNumber: req.params.number },
    });

    if (!certificate) {
      return res.status(404).render('validation-not-found');
    }

    res.render('validation-success', { certificate });
  } catch (error) {
    console.error('Error validating certificate:', error);
    res.status(500).render('validation-error');
  }
});

// Download de certificado
router.get('/certificates/:id/download', async (req, res) => {
  try {
    const certificate = await db.certificates.findByPk(req.params.id);

    if (!certificate) {
      return res.status(404).json({ error: 'Certificado não encontrado' });
    }

    const pdf = await certificateService.generateCertificate(certificate);

    res.setHeader('Content-Type', 'application/pdf');
    res.setHeader(
      'Content-Disposition',
      `attachment; filename="Certificado-${certificate.certificateNumber}.pdf"`
    );
    res.send(pdf);
  } catch (error) {
    console.error('Error downloading certificate:', error);
    res.status(500).json({ error: 'Erro ao baixar certificado' });
  }
});

export default router;

3. Webhook de Conclusão

// src/webhooks/course.webhook.ts
import { CertificateService } from '../services/certificate.service';

const certificateService = new CertificateService();

export async function handleCourseCompleted(data: {
  userId: string;
  courseId: string;
}) {
  try {
    // Emitir certificado automaticamente
    const certificate = await certificateService.issueOnCompletion(
      data.userId,
      data.courseId
    );

    console.log(`Certificate issued: ${certificate.certificateNumber}`);
  } catch (error) {
    console.error('Error issuing certificate:', error);
  }
}

Variações de Certificados

Certificado de Participação em Evento

const eventCertificate: Certificate = {
  type: 'participation',
  course: {
    name: 'DevConf Brasil 2024',
    description: 'Conferência de Desenvolvimento de Software',
    startDate: new Date('2024-11-15'),
    endDate: new Date('2024-11-17'),
  },
  // ... outros campos
};

Certificado de Reconhecimento

const recognitionCertificate: Certificate = {
  type: 'recognition',
  course: {
    name: 'Funcionário do Mês - Novembro 2024',
    description: 'Reconhecimento por excelência em atendimento ao cliente',
  },
  // ... outros campos
};

Proteção Contra Fraude

Blockchain/Hash

import crypto from 'crypto';

class CertificateBlockchain {
  async registerCertificate(certificate: Certificate): Promise<string> {
    // Criar hash único do certificado
    const hash = crypto
      .createHash('sha256')
      .update(JSON.stringify({
        number: certificate.certificateNumber,
        recipient: certificate.recipient.name,
        course: certificate.course.name,
        issueDate: certificate.issueDate,
      }))
      .digest('hex');

    // Armazenar hash em blockchain ou banco imutável
    await this.storeHash(hash, certificate.id);

    return hash;
  }

  async verifyCertificate(certificateNumber: string): Promise<boolean> {
    const certificate = await db.certificates.findOne({
      where: { certificateNumber },
    });

    if (!certificate) return false;

    const storedHash = await this.getStoredHash(certificate.id);
    const calculatedHash = await this.calculateHash(certificate);

    return storedHash === calculatedHash;
  }

  private async storeHash(hash: string, certificateId: string): Promise<void> {
    // Implementar storage (banco, blockchain, etc)
  }

  private async getStoredHash(certificateId: string): Promise<string> {
    // Implementar retrieval
    throw new Error('Not implemented');
  }

  private async calculateHash(certificate: any): Promise<string> {
    // Implementar cálculo de hash
    throw new Error('Not implemented');
  }
}

Próximos Passos

On this page