teqo-backup/src/collections/Petition.ts
Francisco Ignácio Fontoura Solla 473c82d09d Lock down CMS/PII collection access and leader routes
- users/signature/subscription/consent: all operations Payload-admin-only
  (Payload's default allowed ANY authenticated user, incl. campaign JWTs
  via /api — privilege escalation + PII exposure)
- post/tag/media/petition: public read kept, writes admin-only,
  readVersions admin-only on drafted collections
- leader lockdown: staff gates on /campanha/demandas* and /campanha/planos*;
  removed the dead leader branch of the demand detail page
- fix createLeaderSupporter: scope now asserted via the leader's engaged
  leadership municipalities (canReadMunicipality always denied leaders)
- candidate alignment: supporter area, invite creation, and escalated
  demand decisions now treat candidate as unrestricted staff consistently
- tests: collectionAccessLockdown.int.spec pins the lockdown; leased-consent
  helpers generalized (supporter keys); fixtures purge deletes residue
  invites before leaderships; stale leader-phone tests updated to the
  lockdown model; 'sem Praça' copy renamed to 'sem município'
2026-07-23 22:25:18 -03:00

133 lines
3.1 KiB
TypeScript

import { normalizeFacebookPixelId } from '@/lib/facebookPixel'
import { payloadAdminOnly } from '@/utilities/campaignAccess'
import { revalidateDocumentById } from '@/utilities/documents'
import type { CollectionConfig } from 'payload'
const slug = 'petition'
export const Petition: CollectionConfig<typeof slug> = {
slug,
labels: {
singular: 'Abaixo-assinado',
plural: 'Abaixo-assinados',
},
admin: {
group: 'Abaixo-assinados',
useAsTitle: 'title',
livePreview: {
url: ({ data }) => `/abaixo-assinado/${data.id}`,
},
},
versions: {
drafts: {
schedulePublish: true,
},
maxPerDoc: 5,
},
access: {
read: () => true,
readVersions: payloadAdminOnly,
create: payloadAdminOnly,
update: payloadAdminOnly,
delete: payloadAdminOnly,
},
hooks: {
beforeChange: [
({ data }) => {
if (data?.tracking?.facebookPixelId != null) {
data.tracking.facebookPixelId = normalizeFacebookPixelId(data.tracking.facebookPixelId)
}
return data
},
],
afterChange: [
({ doc, previousDoc, operation }) => {
// Admin create view creates the initial draft during render.
// Skip cache invalidation only for that first draft creation.
if (operation === 'create' && doc._status !== 'published') return
revalidateDocumentById(slug, previousDoc?.id ?? doc.id)
},
],
},
fields: [
{
name: 'id',
type: 'text',
label: 'Trecho Identificador da URL',
required: true,
},
{
name: 'title',
type: 'text',
label: 'Título',
required: true,
},
{
name: 'subtitle',
type: 'text',
label: 'Subtítulo',
required: true,
},
{
name: 'enabled',
type: 'checkbox',
label: 'Ativo',
required: true,
},
{
name: 'body',
type: 'richText',
label: 'Corpo',
required: true,
},
{
name: 'form',
type: 'group',
label: 'Formulário',
required: true,
fields: [
{
name: 'title',
type: 'text',
label: 'Título',
},
{
name: 'subtitle',
type: 'text',
label: 'Subtítulo',
},
{
name: 'consent',
type: 'relationship',
label: 'Consentimento',
relationTo: 'consent',
required: true,
},
],
},
{
name: 'tracking',
type: 'group',
label: 'Rastreamento / Ads',
fields: [
{
name: 'facebookPixelId',
type: 'text',
label: 'ID do Pixel do Meta (Facebook)',
admin: {
description:
'Cole somente o ID numérico do Events Manager (ex.: 123456789012345), não o snippet HTML completo.',
},
validate: (value: string | null | undefined) => {
if (!value) return true
if (!normalizeFacebookPixelId(String(value))) {
return 'Informe somente o ID numérico do Pixel (5 a 20 dígitos), sem HTML ou script.'
}
return true
},
},
],
},
],
}