mirror of
https://github.com/siv-org/siv.git
synced 2026-01-09 10:27:57 -05:00
Query supabase for checking invite deliveries
This commit is contained in:
34
pages/api/election/[election_id]/admin/find-voter-invites.ts
Normal file
34
pages/api/election/[election_id]/admin/find-voter-invites.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { supabase } from 'api/_supabase'
|
||||
import { buildSubject } from 'api/invite-voters'
|
||||
import { checkJwtOwnsElection } from 'api/validate-admin-jwt'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export type VoterInvites = Record<string, string[]>
|
||||
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { election_id } = req.query as { election_id?: string }
|
||||
if (!election_id) return res.status(401).json({ error: 'Missing election_id' })
|
||||
|
||||
// Confirm they're a valid admin that created this election
|
||||
const jwt = await checkJwtOwnsElection(req, res, election_id)
|
||||
if (!jwt.valid) return
|
||||
|
||||
const subject = buildSubject(jwt.election_title)
|
||||
|
||||
const headers = 'json->event-data->message->headers'
|
||||
const toPath = headers + '->to'
|
||||
const subjectPath = headers + '->subject'
|
||||
const { data, error } = await supabase
|
||||
.from('mailgun-deliveries')
|
||||
.select(`${toPath}, created_at`)
|
||||
.eq(subjectPath, JSON.stringify(subject))
|
||||
|
||||
if (error) return res.status(400).json({ error })
|
||||
|
||||
const deliveries = data?.reduce((memo, { created_at, to }) => {
|
||||
memo[to] = [...(memo[to] || []), created_at]
|
||||
return memo
|
||||
}, {})
|
||||
|
||||
res.status(200).json(deliveries as VoterInvites)
|
||||
}
|
||||
@@ -3,10 +3,12 @@ import { Tooltip } from './Tooltip'
|
||||
export const DeliveriesAndFailures = ({
|
||||
checkmarkOnly,
|
||||
delivered,
|
||||
deliveries,
|
||||
failed,
|
||||
}: {
|
||||
checkmarkOnly?: boolean
|
||||
delivered?: unknown[]
|
||||
deliveries?: string[]
|
||||
failed?: unknown[]
|
||||
}) => {
|
||||
return (
|
||||
@@ -15,23 +17,22 @@ export const DeliveriesAndFailures = ({
|
||||
interactive={!!failed}
|
||||
placement="top"
|
||||
title={
|
||||
failed || delivered ? (
|
||||
failed || deliveries ? (
|
||||
<>
|
||||
{(failed as {
|
||||
'delivery-status': { message: string }
|
||||
id: string
|
||||
severity: string
|
||||
}[])?.map((event) => (
|
||||
{(
|
||||
failed as {
|
||||
'delivery-status': { message: string }
|
||||
id: string
|
||||
severity: string
|
||||
}[]
|
||||
)?.map((event) => (
|
||||
<div key={event.id} style={{ fontSize: 14 }}>
|
||||
<b>{event.severity} failure</b>: {event['delivery-status'].message.replace(/5.1.1 /g, '')}
|
||||
</div>
|
||||
))}
|
||||
{(delivered as {
|
||||
id: string
|
||||
timestamp: number
|
||||
}[])?.map((event) => (
|
||||
<div key={event.id} style={{ fontSize: 14 }}>
|
||||
{new Date(event.timestamp * 1000).toLocaleString()}
|
||||
{deliveries?.map((time, index) => (
|
||||
<div key={index} style={{ fontSize: 14 }}>
|
||||
{new Date(time).toLocaleString()}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
@@ -44,7 +45,7 @@ export const DeliveriesAndFailures = ({
|
||||
<span className="failed-events">
|
||||
{(failed as { severity?: string }[])?.filter((e) => e.severity === 'temporary').length ? '⚠️ ' : ''}
|
||||
</span>
|
||||
{checkmarkOnly ? (delivered ? '✓' : '') : delivered?.length}
|
||||
{checkmarkOnly ? (delivered ? '✓' : '') : deliveries?.length}
|
||||
<span className="failed-events">
|
||||
{(failed as { severity?: string }[])?.filter(({ severity }) => severity === 'permanent').length ? ' X' : ''}
|
||||
</span>
|
||||
|
||||
@@ -6,11 +6,10 @@ import { NumVotedRow } from './NumVotedRow'
|
||||
import { getStatus } from './Signature'
|
||||
import { TopBarButtons } from './TopBarButtons'
|
||||
import { UnlockedStatus } from './UnlockedStatus'
|
||||
import { use_latest_mailgun_events } from './use-latest-mailgun-events'
|
||||
import { ValidVotersTable } from './ValidVotersTables'
|
||||
|
||||
export const ExistingVoters = () => {
|
||||
const { election_id, esignature_requested, valid_voters, voters } = useStored()
|
||||
const { esignature_requested, valid_voters } = useStored()
|
||||
const [checked, set_checked] = useState<boolean[]>(new Array(valid_voters?.length).fill(false))
|
||||
const num_voted = valid_voters?.filter((v) => v.has_voted).length || 0
|
||||
const num_approved = !esignature_requested
|
||||
@@ -20,8 +19,6 @@ export const ExistingVoters = () => {
|
||||
const [hide_voted, toggle_hide_voted] = useReducer((state) => !state, false)
|
||||
const [hide_approved, toggle_hide_approved] = useReducer((state) => !state, false)
|
||||
|
||||
use_latest_mailgun_events(election_id, voters)
|
||||
|
||||
// Grow checked array to match voters list
|
||||
useEffect(() => {
|
||||
if (valid_voters && checked.length !== valid_voters.length) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { mask } from './mask-token'
|
||||
import { QueuedCell } from './QueuedCell'
|
||||
import { Signature, getStatus } from './Signature'
|
||||
import { use_multi_select } from './use-multi-select'
|
||||
import { useVoterInvites } from './useVoterInvites'
|
||||
|
||||
export const ValidVotersTable = ({
|
||||
checked,
|
||||
@@ -27,6 +28,8 @@ export const ValidVotersTable = ({
|
||||
const [mask_tokens, toggle_tokens] = useReducer((state) => !state, true)
|
||||
const { last_selected, pressing_shift, set_last_selected } = use_multi_select()
|
||||
|
||||
const voterInvites = useVoterInvites()
|
||||
|
||||
if (!voters) return null
|
||||
|
||||
const shown_voters = voters.filter(
|
||||
@@ -140,7 +143,7 @@ export const ValidVotersTable = ({
|
||||
</td>
|
||||
|
||||
<QueuedCell {...{ invite_queued }} />
|
||||
<DeliveriesAndFailures {...mailgun_events} />
|
||||
<DeliveriesAndFailures {...mailgun_events} deliveries={voterInvites[email]} />
|
||||
|
||||
<td style={{ fontWeight: 700, textAlign: 'center' }}>{has_voted ? '✓' : ''}</td>
|
||||
|
||||
|
||||
22
src/admin/Voters/useVoterInvites.ts
Normal file
22
src/admin/Voters/useVoterInvites.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { VoterInvites } from 'api/election/[election_id]/admin/find-voter-invites'
|
||||
import { useRouter } from 'next/router'
|
||||
import useSWR, { mutate } from 'swr'
|
||||
|
||||
export function useVoterInvites(): VoterInvites {
|
||||
const election_id = useRouter().query.election_id as string | undefined
|
||||
|
||||
const { data }: { data?: VoterInvites } = useSWR(election_id ? url(election_id) : null, (url: string) =>
|
||||
fetch(url).then(async (r) => {
|
||||
if (!r.ok) throw await r.json()
|
||||
return await r.json()
|
||||
}),
|
||||
)
|
||||
|
||||
return { ...data }
|
||||
}
|
||||
|
||||
export function recheckDeliveries(election_id?: string) {
|
||||
mutate(url(election_id))
|
||||
}
|
||||
|
||||
const url = (election_id?: string) => `${window.location.origin}/api/election/${election_id}/admin/find-voter-invites`
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { AdminData, Voter } from 'pages/api/election/[election_id]/admin/load-admin'
|
||||
import useSWR, { mutate } from 'swr'
|
||||
|
||||
import { AdminData, Voter } from '../../pages/api/election/[election_id]/admin/load-admin'
|
||||
|
||||
export function useStored(): AdminData & { valid_voters?: Voter[] } {
|
||||
const election_id = useRouter().query.election_id as string | undefined
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"api/*": ["./pages/api/*"],
|
||||
"public/*": ["./public/*"],
|
||||
"src/*": ["./src/*"]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user