teqo-backup/src/payload.config.ts
Francisco Solla 8e32c7cee7
Some checks are pending
Deploy / preflight (push) Waiting to run
Deploy / verify (push) Blocked by required conditions
Deploy / deploy staging (teqo-staging) (push) Blocked by required conditions
Deploy / requeue when main advanced (push) Blocked by required conditions
Deploy / deploy production (teqo-1313) (push) Blocked by required conditions
fix(storage): desliga checksum automático de upload no client S3 (Garage multipart) (C246-hotfix)
O adapter storage-s3 usa multipart acima de 50 MB e o Garage grava um x-amz-checksum-crc32 errado; o SDK valida no GET e toda leitura do objeto falhava (500 nas fotos 80/83, panorâmicas de 54 MB — conteúdo correto). requestChecksumCalculation: WHEN_REQUIRED no client do plugin; a leitura segue validando o checksum que o objeto tiver e a varredura de integridade segue como checagem honesta. Os dois objetos foram reescritos com PUT único (checksum correto) no ops: canário 2/2 e URLs públicas 200 com 53.762.718 bytes.
2026-10-02 20:01:24 -03:00

249 lines
9.2 KiB
TypeScript

import { postgresAdapter } from '@payloadcms/db-postgres'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import path from 'path'
import { buildConfig, type Access, type CollectionConfig } from 'payload'
import sharp from 'sharp'
import { fileURLToPath } from 'url'
import { resendAdapter } from '@payloadcms/email-resend'
import { importExportPlugin } from '@payloadcms/plugin-import-export'
import { s3Storage } from '@payloadcms/storage-s3'
import { pt } from 'payload/i18n/pt'
import { Activity } from './collections/Activity'
import { AllocationDecision } from './collections/AllocationDecision'
import { ArchivePhoto } from './collections/ArchivePhoto'
import { ArchivePhotoFace } from './collections/ArchivePhotoFace'
import { CalendarFeed } from './collections/CalendarFeed'
import { CampaignDemand } from './collections/CampaignDemand'
import { CampaignInvite } from './collections/CampaignInvite'
import { CampaignUser } from './collections/CampaignUser'
import { CampaignVoteSummarySnapshot } from './collections/CampaignVoteSummarySnapshot'
import { CampaignWebAuthnCredential } from './collections/CampaignWebAuthnCredential'
import { Consent } from './collections/Consent'
import { Contact } from './collections/Contact'
import { ContentEvent } from './collections/ContentEvent'
import { ContentMedia } from './collections/ContentMedia'
import { ContentPiece } from './collections/ContentPiece'
import { ElectionCandidate } from './collections/ElectionCandidate'
import { ElectionCandidateVote } from './collections/ElectionCandidateVote'
import { ElectionTally } from './collections/ElectionTally'
import { FaceFigure } from './collections/FaceFigure'
import { GoogleCalendarSync } from './collections/GoogleCalendarSync'
import { InternetSpeechMedia } from './collections/InternetSpeechMedia'
import { Jingle } from './collections/Jingle'
import { Leadership } from './collections/Leadership'
import { Media } from './collections/Media'
import { Municipality } from './collections/Municipality'
import { MunicipalityUpdate } from './collections/MunicipalityUpdate'
import { Notification } from './collections/Notification'
import { Organization } from './collections/Organization'
import { Petition } from './collections/Petition'
import { Post } from './collections/Post'
import { PushSubscription } from './collections/PushSubscription'
import { Recording } from './collections/Recording'
import { RecordingMedia } from './collections/RecordingMedia'
import { RecordingSegment } from './collections/RecordingSegment'
import { Reel } from './collections/Reel'
import { ReelMedia } from './collections/ReelMedia'
import { ShareLink } from './collections/ShareLink'
import { Signature } from './collections/Signature'
import { Speech } from './collections/Speech'
import { SpeechCut } from './collections/SpeechCut'
import { SpeechEmbedding } from './collections/SpeechEmbedding'
import { SpeechSegment } from './collections/SpeechSegment'
import { StateDeputy } from './collections/StateDeputy'
import { Subscription } from './collections/Subscription'
import { Supporter } from './collections/Supporter'
import { SupporterImportBatch } from './collections/SupporterImportBatch'
import { Tag } from './collections/Tag'
import { Users } from './collections/Users'
import { VotePledge } from './collections/VotePledge'
import { CampaignGoals } from './globals/CampaignGoals'
import { HomePage } from './globals/HomePage'
import { Metadata } from './globals/Metadata'
import { PhotoAlbum } from './globals/PhotoAlbum'
import { PrivacyPolicy } from './globals/PrivacyPolicy'
import { SiteSettings } from './globals/SiteSettings'
import { SocialFeedSettings } from './globals/SocialFeedSettings'
import { ARCHIVE_PHOTO_SLUG } from './lib/archivePhoto'
import { isPayloadAdmin } from './utilities/campaignAccess'
import { resolveS3StorageEnv } from './utilities/mediaStorage'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const mediaStorage = resolveS3StorageEnv(process.env)
const campaignEmailFromAddress =
process.env.CAMPAIGN_EMAIL_FROM?.trim() || 'campanha@jorgesolla.com.br'
const campaignEmailFromName = process.env.CAMPAIGN_EMAIL_FROM_NAME?.trim() || 'Campanha Jorge Solla'
const importExportAdminOnly: Access = ({ req }) => isPayloadAdmin(req.user)
const withImportExportAdminAccess = ({ collection }: { collection: CollectionConfig }) => ({
...collection,
access: {
...collection.access,
read: importExportAdminOnly,
create: importExportAdminOnly,
update: importExportAdminOnly,
delete: importExportAdminOnly,
},
})
const adminCsvExportCollection = (slug: 'signature' | 'contact') => ({
slug,
import: false as const,
export: {
format: 'csv' as const,
disableJobsQueue: true,
disableSave: true,
},
})
export default buildConfig({
admin: {
user: Users.slug,
importMap: {
baseDir: path.resolve(dirname),
},
livePreview: {
url: 'http://localhost:3000',
globals: [SiteSettings.slug, HomePage.slug],
},
},
collections: [
Users,
CampaignUser,
CampaignWebAuthnCredential,
Notification,
PushSubscription,
CampaignInvite,
CampaignVoteSummarySnapshot,
Municipality,
Leadership,
Organization,
StateDeputy,
VotePledge,
AllocationDecision,
CampaignDemand,
Supporter,
SupporterImportBatch,
MunicipalityUpdate,
Activity,
Speech,
SpeechSegment,
SpeechCut,
SpeechEmbedding,
InternetSpeechMedia,
Reel,
ReelMedia,
ContentPiece,
ContentMedia,
ArchivePhoto,
ArchivePhotoFace,
FaceFigure,
ContentEvent,
Recording,
RecordingMedia,
RecordingSegment,
CalendarFeed,
GoogleCalendarSync,
ElectionTally,
ElectionCandidateVote,
ElectionCandidate,
Media,
Petition,
Contact,
Consent,
Signature,
Subscription,
Post,
Tag,
ShareLink,
Jingle,
],
globals: [
SiteSettings,
HomePage,
Metadata,
PrivacyPolicy,
CampaignGoals,
SocialFeedSettings,
PhotoAlbum,
],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || '',
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
db: postgresAdapter({
pool: {
connectionString: process.env.DATABASE_URL || '',
},
// Schema changes are applied via committed migrations (pnpm migrate:create /
// pnpm migrate), never auto-pushed. `pnpm build` runs `payload migrate` before
// building, so migrations apply to prod on every Vercel deploy.
migrationDir: path.resolve(dirname, 'migrations'),
push: false,
}),
sharp,
email: resendAdapter({
defaultFromAddress: campaignEmailFromAddress,
defaultFromName: campaignEmailFromName,
apiKey: process.env.RESEND_API_KEY || '',
}),
plugins: [
// Media storage (OPS52): Garage S3 when the S3_* envs are set, local disk
// otherwise; a partial S3 config throws at boot (see mediaStorage.ts). The
// bucket stays private — Payload proxies files through /media/file.
...(mediaStorage.enabled
? [
s3Storage({
collections: {
media: true,
// C193 — private reel artifacts; without this entry they would
// fall back to the container's ephemeral disk in production.
reelMedia: true,
// C199 — private recording files; same failure mode if omitted.
recordingMedia: true,
// C211 — private content-piece files; same failure mode if omitted.
contentMedia: true,
// C215 — private mirrored web-speech files; same failure mode if omitted.
internetSpeechMedia: true,
// C231 — private Flickr photo-archive originals; same failure mode if omitted.
[ARCHIVE_PHOTO_SLUG]: true,
},
bucket: mediaStorage.bucket,
config: {
credentials: {
accessKeyId: mediaStorage.accessKeyId,
secretAccessKey: mediaStorage.secretAccessKey,
},
region: mediaStorage.region,
endpoint: mediaStorage.endpoint,
forcePathStyle: true,
// C246-ops (2026-10-02): Garage stores a WRONG
// `x-amz-checksum-crc32` for multipart uploads (the storage-s3
// adapter switches to multipart above 50 MB), so every GET of a
// >50 MB object failed the SDK's response validation — the two
// archive originals affected were 54 MB panoramas. WHEN_REQUIRED
// stops the SDK from attaching upload checksums; reads still
// validate whatever checksum an object has and the integrity
// sweep is the honest corruption check (decode included).
requestChecksumCalculation: 'WHEN_REQUIRED',
},
}),
]
: []),
importExportPlugin({
collections: [adminCsvExportCollection('signature'), adminCsvExportCollection('contact')],
overrideExportCollection: withImportExportAdminAccess,
overrideImportCollection: withImportExportAdminAccess,
}),
],
i18n: {
fallbackLanguage: 'pt', // use 'pt' (not 'pt-BR') for Payload admin language code
supportedLanguages: { pt },
},
})