teqo-backup/src/utilities/campaignActionContext.ts
Francisco Ignácio Fontoura Solla 5f11960c3a B32+: share the campaign JSON route wrapper, fetch helper and cell auto-save.
`campaignJsonMutationRoute` now builds every JSON POST route under /campanha —
`isSameOriginRequest` had been copied into five of them, so the CSRF policy was
failing open by omission; a convention guard sweeps all of `src/app` and names
its three exceptions. `postCampaignJson` is the only place that spells
`credentials: 'same-origin'`, and `useCampaignCellAutosave` is the debounce /
abort / revert / flush-on-close machine the quick-edit cells shipped three
times.

The shell keeps the `aria-live` region outside the Popover/Drawer, since closing
is what commits and it was unmounting the node that would announce the result;
`variant="sheet"` took a `footer` slot, which let the signal control drop the
overlay it only kept for one button.

Two bugs the extraction made visible: a revert issued while a different payload
was in flight was dropped silently, and `change()` never cleared a past failure,
which outranks everything the live region would otherwise say.
2026-07-28 08:07:29 -03:00

91 lines
3 KiB
TypeScript

import 'server-only'
import config from '@payload-config'
import { getPayload, type Payload } from 'payload'
import type { CampaignUser } from '@/payload-types'
import { isCampaignStaff, isCampaignUnrestricted } from '@/utilities/campaignAccess'
import { getCampaignUser } from '@/utilities/campaignAuth'
import { CAMPAIGN_AUTH_REQUIRED_MESSAGE } from '@/utilities/campaignFormActionError'
import type { PayloadTransactionRequest } from '@/utilities/payloadTransaction'
export type CampaignActionContext = {
payload: Payload
actor: CampaignUser
}
type CampaignUserGetter = () => Promise<CampaignUser | null>
type CampaignActorPayload = Pick<Payload, 'findByID'>
export const requireCampaignUser = async (
getUser: CampaignUserGetter = getCampaignUser,
): Promise<CampaignUser> => {
const actor = await getUser()
if (!actor) throw new Error(CAMPAIGN_AUTH_REQUIRED_MESSAGE)
return actor
}
export const reloadCampaignActor = (
payload: CampaignActorPayload,
actor: CampaignUser,
req?: PayloadTransactionRequest,
): Promise<CampaignUser> =>
payload.findByID({
collection: 'campaignUser',
id: actor.id,
depth: 0,
// Intentional admin bypass: this is the trusted fresh-role reload used before authorization.
overrideAccess: true,
req,
})
/**
* Reloads the actor's fresh role and asserts they are campaign staff
* (coordinator, advisor, or candidate — matching collection-level access via
* `isCampaignStaff`). Throws `errorMessage` otherwise.
*/
export const reloadStaffActor = async (
payload: CampaignActorPayload,
actor: CampaignUser,
errorMessage: string,
req?: PayloadTransactionRequest,
): Promise<CampaignUser> => {
const currentActor = await reloadCampaignActor(payload, actor, req)
if (!isCampaignStaff(currentActor)) throw new Error(errorMessage)
return currentActor
}
/**
* Reloads the actor's fresh role and asserts they are the coordinator.
* Throws `errorMessage` otherwise.
*/
export const reloadCoordinatorActor = async (
payload: CampaignActorPayload,
actor: CampaignUser,
errorMessage: string,
req?: PayloadTransactionRequest,
): Promise<CampaignUser> => {
const currentActor = await reloadCampaignActor(payload, actor, req)
if (currentActor.role !== 'coordinator') throw new Error(errorMessage)
return currentActor
}
/**
* Reloads the actor's fresh role and asserts they are unrestricted
* (coordinator or candidate). Throws `errorMessage` otherwise.
*/
export const reloadUnrestrictedActor = async (
payload: CampaignActorPayload,
actor: CampaignUser,
errorMessage: string,
req?: PayloadTransactionRequest,
): Promise<CampaignUser> => {
const currentActor = await reloadCampaignActor(payload, actor, req)
if (!isCampaignUnrestricted(currentActor)) throw new Error(errorMessage)
return currentActor
}
export const getCampaignActionContext = async (): Promise<CampaignActionContext> => {
const [payload, actor] = await Promise.all([getPayload({ config }), requireCampaignUser()])
return { payload, actor }
}