mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 08:53:11 +00:00
Papel communicator com nav própria e gate speechCatalog; acervo em /campanha/comunicacao/acervo com busca normalizada (destaque no trecho), filtros de ano/tema/alcance/fase/município/duração em URL canônica e paginação por discurso via speech.searchText (GIN trgm + backfill). Detalhe com player nativo no trecho, transcrição clicável, baixar/abrir fonte e fallback sem VOD. Resolve o débito do C153 (Sollinha fora da vertical) e corrige o parser de keywords oficiais no dono. Closes #956
86 lines
4.3 KiB
TypeScript
86 lines
4.3 KiB
TypeScript
import 'server-only'
|
|
|
|
import { redirect } from 'next/navigation'
|
|
|
|
import { CAMPAIGN_COMMUNICATION_HOME, LEADER_CONTACTS_HOME } from '@/lib/campaignPaths'
|
|
import {
|
|
canReadSpeechCatalog,
|
|
isStaffCampaignRole,
|
|
isUnrestrictedCampaignRole,
|
|
} from '@/lib/campaignRoles'
|
|
import { advisorEditingAccess } from '@/utilities/access/shared'
|
|
import { getCampaignUser, getCampaignUserWithAvatar } from '@/utilities/campaignAuth'
|
|
|
|
/** Exactly the non-null actor `getCampaignUser` proves (keeps its `email` refinement). */
|
|
export type CampaignPageActor = NonNullable<Awaited<ReturnType<typeof getCampaignUser>>>
|
|
|
|
export type CampaignPageGate = 'staff' | 'unrestricted' | 'noLeader' | 'writable' | 'speechCatalog'
|
|
|
|
export { LEADER_CONTACTS_HOME }
|
|
|
|
/** Staff dashboard (B43 — moved off blank `/campanha`). */
|
|
export const CAMPAIGN_STAFF_QUADRO_PATH = '/campanha/quadro'
|
|
|
|
/**
|
|
* THE staff page prologue (Pass 3 P3-I — was ~110 hand-spelled lines across 30
|
|
* pages, with a real divergence: 5 pages answered a missing session with
|
|
* `return null` — a blank screen — instead of redirecting to login). Gates:
|
|
*
|
|
* - (none) → any authenticated campaign user;
|
|
* - 'staff' → non-staff (leader) goes to `/campanha/meus-contatos` (B43, C139);
|
|
* - 'unrestricted' → non-coordinator/candidate goes to `/campanha`;
|
|
* - 'noLeader' → leader goes to `/campanha/meus-contatos` (B43, C139);
|
|
* - 'writable' → advisor with Edição `somente_leitura` goes to `/campanha`
|
|
* (C142 — write destinations must not be offered to a
|
|
* read-only advisor; the server already rejects the write);
|
|
* - 'speechCatalog'→ advisor/leader lose the communication vertical (C154);
|
|
* communicator/coordinator/candidate pass, the same
|
|
* predicate the collection access uses.
|
|
*
|
|
* A custom `redirectTo` overrides the gate's default target. `withAvatar`
|
|
* loads the actor through `getCampaignUserWithAvatar` (avatar populated at
|
|
* depth 1) instead of `getCampaignUser` — both return
|
|
* `AuthenticatedCampaignUser | null`; the avatar depth is runtime-only and
|
|
* does not change the gates. The convention guard in
|
|
* `codebaseConventions.unit.spec.ts` fails the build on a `getCampaignUser*()`
|
|
* call in a `(app)` route page outside this helper.
|
|
*/
|
|
export const requireCampaignPageActor = async (
|
|
/**
|
|
* `withAvatar` loads the actor through `getCampaignUserWithAvatar` (avatar
|
|
* populated at depth 1) instead of `getCampaignUser` — both return
|
|
* `AuthenticatedCampaignUser | null`; the avatar depth is runtime-only and
|
|
* does not change the gates. The perfil shell renders `avatarUrl`, which
|
|
* requires the populated media object — depth 0 resolves to a number id.
|
|
*/
|
|
options: { gate?: CampaignPageGate; redirectTo?: string; withAvatar?: boolean } = {},
|
|
): Promise<CampaignPageActor> => {
|
|
const getter = options.withAvatar ? getCampaignUserWithAvatar : getCampaignUser
|
|
const user = await getter()
|
|
if (!user) redirect('/campanha/login')
|
|
|
|
const { gate, redirectTo } = options
|
|
// C154 — a denied communicator belongs in the vertical, not in the staff
|
|
// home (which itself redirects there, but a direct redirect avoids a hop).
|
|
const roleHome: Partial<Record<typeof user.role, string>> = {
|
|
leader: LEADER_CONTACTS_HOME,
|
|
communicator: CAMPAIGN_COMMUNICATION_HOME,
|
|
}
|
|
const denyRedirect = redirectTo ?? roleHome[user.role] ?? '/campanha'
|
|
|
|
if (gate === 'staff' && !isStaffCampaignRole(user.role)) redirect(denyRedirect)
|
|
// C154 — the speech catalog vertical: communicator/coordinator/candidate,
|
|
// the same predicate the collection access uses.
|
|
if (gate === 'speechCatalog' && !canReadSpeechCatalog(user.role)) redirect(denyRedirect)
|
|
if (gate === 'unrestricted' && !isUnrestrictedCampaignRole(user.role)) redirect(denyRedirect)
|
|
if (gate === 'noLeader' && user.role === 'leader') redirect(denyRedirect)
|
|
if (gate === 'writable' && !isStaffCampaignRole(user.role)) redirect(denyRedirect)
|
|
// C142 — `somente_leitura` applies to advisors only; `advisorEditingAccess`
|
|
// returns 'none' for every non-advisor role, so gate on the role first or
|
|
// coordinators/candidates are wrongly redirected away from write pages.
|
|
if (gate === 'writable' && user.role === 'advisor' && advisorEditingAccess(user) === 'none') {
|
|
redirect(denyRedirect)
|
|
}
|
|
|
|
return user
|
|
}
|