Skip to content

Commit

Permalink
feat: add support for casting ballots (#100)
Browse files Browse the repository at this point in the history
* fix(types-rs): remove VX-specific CVR types

These were removed on the TS side a while back after it was ruled that we couldn't do custom extensions.

* fix(jx): only navigate to elections page from "locked" page

* feat: add support for casting ballots

Also shows the ballot metadata in the JX terminal's "Ballots" page.
  • Loading branch information
eventualbuddha authored Apr 3, 2024
1 parent 44b526d commit 144ab49
Show file tree
Hide file tree
Showing 13 changed files with 792 additions and 173 deletions.
2 changes: 2 additions & 0 deletions apps/cacvote-jx-terminal/backend/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,13 @@ pub(crate) fn setup(pool: PgPool, config: Config, smartcard: smartcard::DynSmart
.await
.unwrap();
let registrations = db::get_registrations(&mut connection).await.unwrap();
let cast_ballots = db::get_cast_ballots(&mut connection).await.unwrap();
SessionData::Authenticated {
jurisdiction_code: jurisdiction_code.clone(),
elections,
pending_registration_requests,
registrations,
cast_ballots,
}
}
Some(_) => SessionData::Unauthenticated {
Expand Down
95 changes: 94 additions & 1 deletion apps/cacvote-jx-terminal/backend/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,15 +426,108 @@ pub(crate) async fn get_journal_entries_for_objects_to_pull(
created_at
FROM journal_entries
WHERE object_id IS NOT NULL
AND object_type IN ($1)
AND object_type IN ($1, $2)
AND object_id NOT IN (SELECT id FROM objects)
"#,
cacvote::Payload::registration_request_object_type(),
cacvote::Payload::cast_ballot_object_type(),
)
.fetch_all(&mut *executor)
.await?)
}

pub(crate) async fn get_cast_ballots(
executor: &mut sqlx::PgConnection,
) -> color_eyre::Result<Vec<cacvote::CastBallotPresenter>> {
let records = sqlx::query!(
r#"
SELECT
cb.id AS cast_ballot_id,
cb.payload AS cast_ballot_payload,
cb.certificates AS cast_ballot_certificates,
cb.signature AS cast_ballot_signature,
rr.id AS registration_request_id,
rr.payload AS registration_request_payload,
rr.certificates AS registration_request_certificates,
rr.signature AS registration_request_signature,
r.id AS registration_id,
r.payload AS registration_payload,
r.certificates AS registration_certificates,
r.signature AS registration_signature,
cb.created_at AS created_at
FROM objects AS cb
-- join on registration request
INNER JOIN objects AS rr
ON (convert_from(cb.payload, 'UTF8')::jsonb ->> $1)::uuid = rr.id
-- join on registration
INNER JOIN objects AS r
ON (convert_from(cb.payload, 'UTF8')::jsonb ->> $2)::uuid = r.id
WHERE rr.object_type = $3
AND cb.object_type = $4
AND r.object_type = $5
ORDER BY cb.created_at DESC
"#,
cacvote::CastBallot::registration_request_object_id_field_name(),
cacvote::CastBallot::registration_object_id_field_name(),
cacvote::Payload::registration_request_object_type(),
cacvote::Payload::cast_ballot_object_type(),
cacvote::Payload::registration_object_type(),
)
.fetch_all(&mut *executor)
.await?;

let mut cast_ballots = Vec::new();

for record in records {
let cast_ballot_object = cacvote::SignedObject {
id: record.cast_ballot_id,
payload: record.cast_ballot_payload,
certificates: record.cast_ballot_certificates,
signature: record.cast_ballot_signature,
};
let registration_object = cacvote::SignedObject {
id: record.registration_id,
payload: record.registration_payload,
certificates: record.registration_certificates,
signature: record.registration_signature,
};
let registration_request_object = cacvote::SignedObject {
id: record.registration_request_id,
payload: record.registration_request_payload,
certificates: record.registration_request_certificates,
signature: record.registration_request_signature,
};

if let cacvote::Payload::CastBallot(cast_ballot) = cast_ballot_object.try_to_inner()? {
if let cacvote::Payload::RegistrationRequest(registration_request) =
registration_request_object.try_to_inner()?
{
if let cacvote::Payload::Registration(registration) =
registration_object.try_to_inner()?
{
// TODO: remove this or replace with actual verification status
// we already verify the signature as part of adding the object to the DB,
// so we can assume that the verification status is success
let verification_status = cacvote::VerificationStatus::Success {
common_access_card_id: cast_ballot.common_access_card_id.clone(),
display_name: "test".to_owned(),
};
let created_at = record.created_at;
cast_ballots.push(cacvote::CastBallotPresenter::new(
cast_ballot,
registration_request,
registration,
verification_status,
created_at,
));
}
}
}
}

Ok(cast_ballots)
}

#[cfg(test)]
mod tests {
use openssl::{
Expand Down
116 changes: 107 additions & 9 deletions apps/cacvote-jx-terminal/frontend/public/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -586,14 +586,14 @@ video {
width: 80%;
}

.w-screen {
width: 100vw;
}

.w-full {
width: 100%;
}

.w-screen {
width: 100vw;
}

.table-auto {
table-layout: auto;
}
Expand Down Expand Up @@ -622,10 +622,19 @@ video {
border-radius: 0.375rem;
}

.rounded-l-md {
border-top-left-radius: 0.375rem;
border-bottom-left-radius: 0.375rem;
}

.border {
border-width: 1px;
}

.border-2 {
border-width: 2px;
}

.bg-gray-200 {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
Expand All @@ -636,11 +645,36 @@ video {
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
}

.bg-gray-400 {
--tw-bg-opacity: 1;
background-color: rgb(156 163 175 / var(--tw-bg-opacity));
}

.bg-green-300 {
--tw-bg-opacity: 1;
background-color: rgb(134 239 172 / var(--tw-bg-opacity));
}

.bg-orange-300 {
--tw-bg-opacity: 1;
background-color: rgb(253 186 116 / var(--tw-bg-opacity));
}

.bg-purple-500 {
--tw-bg-opacity: 1;
background-color: rgb(168 85 247 / var(--tw-bg-opacity));
}

.bg-red-300 {
--tw-bg-opacity: 1;
background-color: rgb(252 165 165 / var(--tw-bg-opacity));
}

.bg-yellow-300 {
--tw-bg-opacity: 1;
background-color: rgb(253 224 71 / var(--tw-bg-opacity));
}

.p-1 {
padding: 0.25rem;
}
Expand Down Expand Up @@ -672,6 +706,21 @@ video {
padding-bottom: 0.5rem;
}

.pe-2 {
-webkit-padding-end: 0.5rem;
padding-inline-end: 0.5rem;
}

.ps-0 {
-webkit-padding-start: 0px;
padding-inline-start: 0px;
}

.ps-2 {
-webkit-padding-start: 0.5rem;
padding-inline-start: 0.5rem;
}

.text-left {
text-align: left;
}
Expand All @@ -695,34 +744,63 @@ video {
line-height: 1.75rem;
}

.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}

.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}

.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}

.font-bold {
font-weight: 700;
}

.font-semibold {
font-weight: 600;
}

.italic {
font-style: italic;
}

.text-gray-200 {
--tw-text-opacity: 1;
color: rgb(229 231 235 / var(--tw-text-opacity));
}

.text-gray-400 {
--tw-text-opacity: 1;
color: rgb(156 163 175 / var(--tw-text-opacity));
}

.text-green-800 {
--tw-text-opacity: 1;
color: rgb(22 101 52 / var(--tw-text-opacity));
}

.text-orange-800 {
--tw-text-opacity: 1;
color: rgb(154 52 18 / var(--tw-text-opacity));
}

.text-red-800 {
--tw-text-opacity: 1;
color: rgb(153 27 27 / var(--tw-text-opacity));
}

.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}

.text-yellow-800 {
--tw-text-opacity: 1;
color: rgb(133 77 14 / var(--tw-text-opacity));
}

.hover\:cursor-pointer:hover {
cursor: pointer;
}
Expand All @@ -732,6 +810,16 @@ video {
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
}

.focus\:border-blue-500:focus {
--tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity));
}

.focus\:outline-none:focus {
outline: 2px solid transparent;
outline-offset: 2px;
}

.active\:bg-purple-700:active {
--tw-bg-opacity: 1;
background-color: rgb(126 34 206 / var(--tw-bg-opacity));
Expand All @@ -743,6 +831,11 @@ video {
}

@media (prefers-color-scheme: dark) {
.dark\:border-gray-600 {
--tw-border-opacity: 1;
border-color: rgb(75 85 99 / var(--tw-border-opacity));
}

.dark\:bg-gray-700 {
--tw-bg-opacity: 1;
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
Expand All @@ -758,6 +851,11 @@ video {
color: rgb(209 213 219 / var(--tw-text-opacity));
}

.dark\:text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}

.hover\:dark\:text-gray-700:hover {
--tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity));
Expand Down
11 changes: 7 additions & 4 deletions apps/cacvote-jx-terminal/frontend/src/layouts/app_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub fn AppLayout(cx: Scope) -> Element {
use_shared_state_provider(cx, SessionData::default);
let session_data = use_shared_state::<SessionData>(cx).unwrap();
let nav = use_navigator(cx);
let route: Route = use_route(cx).unwrap();

use_coroutine(cx, {
to_owned![nav, session_data];
Expand All @@ -21,15 +22,17 @@ pub fn AppLayout(cx: Scope) -> Element {

let callback = Closure::wrap(Box::new(move |event: MessageEvent| {
if let Some(data) = event.data().as_string() {
log::info!("received status event: {:?}", data);
log::info!("received status event: {data:?}");
match serde_json::from_str::<SessionData>(&data) {
Ok(new_session_data) => {
log::info!("updating session data: {:?}", new_session_data);
log::info!("updating session data: {new_session_data:?}");

match new_session_data {
SessionData::Authenticated { .. } => {
log::info!("redirecting to elections page");
nav.push(Route::ElectionsPage);
if matches!(route, Route::MachineLockedPage) {
log::info!("redirecting to elections page");
nav.push(Route::ElectionsPage);
}
}
SessionData::Unauthenticated { .. } => {
log::info!("redirecting to machine locked page");
Expand Down
Loading

0 comments on commit 144ab49

Please sign in to comment.