teqo-backup/src/utilities/campaignConsent.ts
Cursor Agent de67acf41b D2 — Push + sino in-app para /campanha
- Collections Notification e PushSubscription com migration
- Sino no header (mobile + desktop) com drawer de avisos lidos/não lidos
- Hooks em municipalityUpdate, supporter, activity e convite aceito
- Push Web via web-push + handlers no service worker /campanha/sw.js
- Opt-in LGPD em /campanha/perfil (chave campanha-notificacoes-push)
- Testes unit/int de contrato e access

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-07-31 01:51:41 +00:00

159 lines
4.7 KiB
TypeScript

import 'server-only'
import type { Payload } from 'payload'
import {
CAMPAIGN_INVITE_CONSENT_KEY,
CAMPAIGN_PUSH_CONSENT_KEY,
CAMPAIGN_PUSH_CONSENT_MISSING_MESSAGE,
SUPPORTER_REGISTRATION_CONSENT_KEY,
SUPPORTER_REGISTRATION_CONSENT_MISSING_MESSAGE,
SUPPORTER_VOTE_INTENTION_CONSENT_KEY,
SUPPORTER_VOTE_INTENTION_CONSENT_MISSING_MESSAGE,
} from '@/lib/campaignConsentKeys'
import type { Consent } from '@/payload-types'
import { hashConsentContent } from '@/utilities/consentContentHash'
export {
SUPPORTER_REGISTRATION_CONSENT_KEY,
SUPPORTER_VOTE_INTENTION_CONSENT_KEY,
} from '@/lib/campaignConsentKeys'
export type ConsentDescriptor = {
id: number
text: Consent['text']
contentHash: string
key: string
}
type ConsentRequest = { transactionID?: number | string }
export type InviteConsentResolution =
| { shouldUpdate: false }
| {
shouldUpdate: true
consentID: number
consentContentHash: string
consentedAt: string
}
export const resolveInviteConsent = ({
existingConsentID,
existingConsentContentHash,
configuredConsentID,
configuredConsentContentHash,
consentAccepted,
}: {
existingConsentID: number | null
existingConsentContentHash?: string | null
configuredConsentID: number
configuredConsentContentHash: string
consentAccepted?: boolean
}): InviteConsentResolution => {
if (
existingConsentID === configuredConsentID &&
existingConsentContentHash === configuredConsentContentHash
) {
return { shouldUpdate: false }
}
if (!consentAccepted) {
throw new Error('É necessário aceitar o consentimento.')
}
return {
shouldUpdate: true,
consentID: configuredConsentID,
consentContentHash: configuredConsentContentHash,
consentedAt: new Date().toISOString(),
}
}
export const getConsentByKey = async (
payload: Payload,
key: string,
req?: ConsentRequest,
): Promise<ConsentDescriptor | null> => {
const result = await payload.find({
collection: 'consent',
where: { key: { equals: key } },
depth: 0,
limit: 1,
pagination: false,
select: { text: true, key: true },
// Intentional admin bypass: consent documents are legal configuration, not
// actor data — `consent` read access is admin-only, but every campaign flow
// must resolve the SAME published text by stable key, failing closed when
// it is absent. The key (not the actor) is the selector.
overrideAccess: true,
req,
})
const consent = result.docs[0]
if (!consent) return null
return {
id: consent.id,
text: consent.text,
contentHash: hashConsentContent(consent.text ?? null),
key,
}
}
export const requireConsentByKey = async (
payload: Payload,
key: string,
req?: ConsentRequest,
message = 'Consentimento ainda não configurado.',
): Promise<ConsentDescriptor> => {
const consent = await getConsentByKey(payload, key, req)
if (!consent) throw new Error(message)
return consent
}
export const getLeadershipConsent = (
payload: Payload,
req?: ConsentRequest,
): Promise<ConsentDescriptor | null> => getConsentByKey(payload, CAMPAIGN_INVITE_CONSENT_KEY, req)
export const requireLeadershipConsent = (
payload: Payload,
req?: ConsentRequest,
message = 'Consentimento ainda não configurado.',
): Promise<ConsentDescriptor> =>
requireConsentByKey(payload, CAMPAIGN_INVITE_CONSENT_KEY, req, message)
export const getSupporterRegistrationConsent = (
payload: Payload,
req?: ConsentRequest,
): Promise<ConsentDescriptor | null> =>
getConsentByKey(payload, SUPPORTER_REGISTRATION_CONSENT_KEY, req)
export const requireSupporterRegistrationConsent = (
payload: Payload,
req?: ConsentRequest,
message = SUPPORTER_REGISTRATION_CONSENT_MISSING_MESSAGE,
): Promise<ConsentDescriptor> =>
requireConsentByKey(payload, SUPPORTER_REGISTRATION_CONSENT_KEY, req, message)
export const getSupporterVoteIntentionConsent = (
payload: Payload,
req?: ConsentRequest,
): Promise<ConsentDescriptor | null> =>
getConsentByKey(payload, SUPPORTER_VOTE_INTENTION_CONSENT_KEY, req)
export const requireSupporterVoteIntentionConsent = (
payload: Payload,
req?: ConsentRequest,
message = SUPPORTER_VOTE_INTENTION_CONSENT_MISSING_MESSAGE,
): Promise<ConsentDescriptor> =>
requireConsentByKey(payload, SUPPORTER_VOTE_INTENTION_CONSENT_KEY, req, message)
export const getCampaignPushConsent = (
payload: Payload,
req?: ConsentRequest,
): Promise<ConsentDescriptor | null> => getConsentByKey(payload, CAMPAIGN_PUSH_CONSENT_KEY, req)
export const requireCampaignPushConsent = (
payload: Payload,
req?: ConsentRequest,
message = CAMPAIGN_PUSH_CONSENT_MISSING_MESSAGE,
): Promise<ConsentDescriptor> =>
requireConsentByKey(payload, CAMPAIGN_PUSH_CONSENT_KEY, req, message)