wedding-management-system

Anexos & Contratos

A partir da v0.4.0, o módulo de anexos passou por endurecimento e ganhou um fluxo dedicado para contratos com versionamento.

Conceitos

Storage

Arquivos vivem fora de public/ em <repo>/uploads/. Estrutura:

uploads/{ownerType}/{ownerId}/{hash16}_{filename}          # padrão
uploads/contract/{contractId}/v{n}/{hash16}_{filename}     # versionado

hash16 são os primeiros 16 caracteres de SHA-256 do conteúdo. O hash completo é armazenado em Attachment.sha256Full para detecção de integridade.

Validação em camadas

Toda Server Action de upload (uploadAttachment, replaceContractFile) passa por:

  1. Sessão via auth(). Falha → 401.
  2. Rate limit por usuário (10/min) e IP (30/min).
  3. Permissão por kind via canUploadAttachmentKind(role, kind).
  4. Validação Zod de ownerType, ownerId, kind.
  5. Magic bytes via detectMagic(buffer) em src/lib/file-validation.ts — PDF, PNG, JPEG, WEBP, HEIC.
  6. assertMagicMatchesMime(detected, file.type) — bloqueia conteúdo que não bate com o MIME declarado pelo cliente.
  7. assertAllowedForKind(kind, file.type) — restringe formatos por kind. CONTRACT só aceita application/pdf.
  8. Tamanho por kind via assertSizeForKind(kind, bytes). CONTRACT tem teto de 8 MB; demais kinds até 10 MB.
  9. Storage path via path.resolve + startsWith(UPLOADS_ROOT) — path traversal robusto.
  10. uploadedById preenchido com session.user.id.
  11. Audit log entry com action UPLOAD.

Matriz de permissões

Ação ADMIN GROOM BRIDE PLANNER FAMILY VIEWER
Upload CONTRACT
Ver / baixar CONTRACT
Substituir CONTRACT (nova versão)
Excluir CONTRACT (soft)
Marcar como SIGNED_*
Upload PHOTO/PROPOSAL/OTHER
Ver PHOTO/PROPOSAL/OTHER

Implementado em src/lib/permissions.ts: canUploadContract, canViewContract, canManageContract, canSignContract, canViewAttachmentKind, canUploadAttachmentKind.

Rota de download — /api/files/[id]

Versionamento de contratos

Regra: o primeiro PDF enviado a um contrato é gravado na versão atual do contrato (ex.: contrato criado em v1 + primeiro upload = attachment v1). Substituições posteriores incrementam (v2, v3…).

Fluxo de replaceContractFile:

  1. Carrega contrato + conta CONTRACT attachments ativos.
  2. Define nextVersion:
    • Sem attachment ativo (primeiro upload) → nextVersion = contract.version.
    • Com attachment ativo (substituição) → nextVersion = contract.version + 1.
  3. Atomicamente (em prisma.$transaction):
    • Se substituição: Attachment.updateMany setando deletedAt = now() no CONTRACT ativo do contrato.
    • Attachment.create com version: nextVersion, kind: CONTRACT, subdir: v{nextVersion}.
    • Se substituição: Contract.update setando version = nextVersion.
  4. Registra audit("Contract", id, "UPLOAD" | "REPLACE", { fromVersion, toVersion }).
    • UPLOAD no primeiro envio (fromVersion === toVersion).
    • REPLACE quando houve troca de versão.

Arquivos da versão antiga não são removidos do disco imediatamente — ficam soft-deletados por 30 dias.

Cleanup

Endpoint cron GET /api/cron/cleanup-files (Bearer CRON_SECRET):

Configure no orquestrador externo (cron de sistema, GitHub Actions, Cloudflare Cron Workers etc.) para rodar 1× ao dia.

UI

/dashboard/vendors/[id] ganhou bloco “Arquivo do contrato” embutido em cada contrato, com:

Limitações conhecidas